/** * Copyright JS Foundation and other contributors, http://js.foundation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. **/ var crypto = require('crypto'); var runtime; var settings; var log; var encryptedCredentials = null; var credentialCache = {}; var credentialsDef = {}; var dirty = false; var removeDefaultKey = false; var encryptionEnabled = null; var encryptionKeyType; // disabled, system, user, project var encryptionAlgorithm = "aes-256-ctr"; var encryptionKey; function decryptCredentials(key,credentials) { var creds = credentials["$"]; var initVector = Buffer.from(creds.substring(0, 32),'hex'); creds = creds.substring(32); var decipher = crypto.createDecipheriv(encryptionAlgorithm, key, initVector); var decrypted = decipher.update(creds, 'base64', 'utf8') + decipher.final('utf8'); return JSON.parse(decrypted); } function encryptCredentials(key,credentials) { var initVector = crypto.randomBytes(16); var cipher = crypto.createCipheriv(encryptionAlgorithm, key, initVector); return {"$":initVector.toString('hex') + cipher.update(JSON.stringify(credentials), 'utf8', 'base64') + cipher.final('base64')}; } var api = module.exports = { init: function(_runtime) { runtime = _runtime; log = runtime.log; settings = runtime.settings; dirty = false; credentialCache = {}; credentialsDef = {}; encryptionEnabled = null; }, /** * Sets the credentials from storage. */ load: async function (credentials) { dirty = false; var credentialsEncrypted = credentials.hasOwnProperty("$") && Object.keys(credentials).length === 1; // Case 1: Active Project in place // - use whatever its config says // Case 2: _credentialSecret unset, credentialSecret unset // - generate _credentialSecret and encrypt // Case 3: _credentialSecret set, credentialSecret set // - migrate from _credentialSecret to credentialSecret // - delete _credentialSecret // Case 4: credentialSecret set // - use it var setupEncryptionPromise = Promise.resolve(); var projectKey = false; var activeProject; encryptionKeyType = ""; if (runtime.storage && runtime.storage.projects) { // projects enabled activeProject = runtime.storage.projects.getActiveProject(); if (activeProject) { projectKey = activeProject.credentialSecret; if (!projectKey) { log.debug("red/runtime/nodes/credentials.load : using active project key - disabled"); encryptionKeyType = "disabled"; encryptionEnabled = false; } else { log.debug("red/runtime/nodes/credentials.load : using active project key"); encryptionKeyType = "project"; encryptionKey = crypto.createHash('sha256').update(projectKey).digest(); encryptionEnabled = true; } } } if (encryptionKeyType === '') { var defaultKey; try { defaultKey = settings.get('_credentialSecret'); } catch(err) { } if (defaultKey) { defaultKey = crypto.createHash('sha256').update(defaultKey).digest(); encryptionKeyType = "system"; } var userKey; try { userKey = settings.get('credentialSecret'); } catch(err) { userKey = false; } if (userKey === false) { encryptionKeyType = "disabled"; log.debug("red/runtime/nodes/credentials.load : user disabled encryption"); // User has disabled encryption encryptionEnabled = false; // Check if we have a generated _credSecret to decrypt with and remove if (defaultKey) { log.debug("red/runtime/nodes/credentials.load : default key present. Will migrate"); if (credentialsEncrypted) { try { credentials = decryptCredentials(defaultKey,credentials) } catch(err) { credentials = {}; log.warn(log._("nodes.credentials.error",{message:err.toString()})) var error = new Error("Failed to decrypt credentials"); error.code = "credentials_load_failed"; throw error; } } dirty = true; removeDefaultKey = true; } } else if (typeof userKey === 'string') { if (!projectKey) { log.debug("red/runtime/nodes/credentials.load : user provided key"); } if (encryptionKeyType !== 'project') { encryptionKeyType = 'user'; } // User has provided own encryption key, get the 32-byte hash of it encryptionKey = crypto.createHash('sha256').update(userKey).digest(); encryptionEnabled = true; if (encryptionKeyType !== 'project' && defaultKey) { log.debug("red/runtime/nodes/credentials.load : default key present. Will migrate"); // User has provided their own key, but we already have a default key // Decrypt using default key if (credentialsEncrypted) { try { credentials = decryptCredentials(defaultKey,credentials) } catch(err) { credentials = {}; log.warn(log._("nodes.credentials.error",{message:err.toString()})) var error = new Error("Failed to decrypt credentials"); error.code = "credentials_load_failed"; throw error; } } dirty = true; removeDefaultKey = true; } } else { log.debug("red/runtime/nodes/credentials.load : no user key present"); // User has not provide their own key if (encryptionKeyType !== 'project') { encryptionKeyType = 'system'; } encryptionKey = defaultKey; encryptionEnabled = true; if (encryptionKey === undefined) { log.debug("red/runtime/nodes/credentials.load : no default key present - generating one"); // No user-provided key, no generated key // Generate a new key defaultKey = crypto.randomBytes(32).toString('hex'); try { setupEncryptionPromise = settings.set('_credentialSecret',defaultKey); encryptionKey = crypto.createHash('sha256').update(defaultKey).digest(); } catch(err) { log.debug("red/runtime/nodes/credentials.load : settings unavailable - disabling encryption"); // Settings unavailable encryptionEnabled = false; encryptionKey = null; } dirty = true; } else { log.debug("red/runtime/nodes/credentials.load : using default key"); } } } if (encryptionEnabled && !dirty) { encryptedCredentials = credentials; } log.debug("red/runtime/nodes/credentials.load : keyType="+encryptionKeyType); if (encryptionKeyType === 'system') { log.warn(log._("nodes.credentials.system-key-warning")); } return setupEncryptionPromise.then(function() { var clearInvalidFlag = false; if (credentials.hasOwnProperty("$")) { if (encryptionEnabled === false) { // The credentials appear to be encrypted, but our config // thinks they are not. var error = new Error("Failed to decrypt credentials"); error.code = "credentials_load_failed"; if (activeProject) { // This is a project with a bad key. Mark it as invalid // TODO: this delves too deep into Project structure activeProject.credentialSecretInvalid = true; throw error; } throw error; } // These are encrypted credentials try { credentialCache = decryptCredentials(encryptionKey,credentials) clearInvalidFlag = true; } catch(err) { credentialCache = {}; dirty = true; log.warn(log._("nodes.credentials.error",{message:err.toString()})) var error = new Error("Failed to decrypt credentials"); error.code = "credentials_load_failed"; if (activeProject) { // This is a project with a bad key. Mark it as invalid // TODO: this delves too deep into Project structure activeProject.credentialSecretInvalid = true; throw error; } throw error; } } else { if (encryptionEnabled) { // Our config expects the credentials to be encrypted but the encrypted object is not found log.warn(log._("nodes.credentials.encryptedNotFound")) credentialCache = credentials; } else { // credentialSecret is set to False log.warn(log._("nodes.credentials.unencrypted")) credentialCache = credentials; } } if (clearInvalidFlag) { // TODO: this delves too deep into Project structure if (activeProject) { delete activeProject.credentialSecretInvalid; } } }); }, /** * Adds a set of credentials for the given node id. * @param id the node id for the credentials * @param creds an object of credential key/value pairs * @return a promise for backwards compatibility TODO: can this be removed? */ add: async function (id, creds) { if (!credentialCache.hasOwnProperty(id) || JSON.stringify(creds) !== JSON.stringify(credentialCache[id])) { credentialCache[id] = creds; dirty = true; } }, /** * Gets the credentials for the given node id. * @param id the node id for the credentials * @return the credentials */ get: function (id) { return credentialCache[id]; }, /** * Deletes the credentials for the given node id. * @param id the node id for the credentials * @return a promise for the saving of credentials to storage */ delete: function (id) { delete credentialCache[id]; dirty = true; }, clear: function() { credentialCache = {}; dirty = true; }, /** * Deletes any credentials for nodes that no longer exist * @param config a flow config * @return a promise for the saving of credentials to storage */ clean: async function (config) { var existingIds = {}; config.forEach(function(n) { existingIds[n.id] = true; if (n.credentials) { api.extract(n); } }); var deletedCredentials = false; for (var c in credentialCache) { if (credentialCache.hasOwnProperty(c)) { if (!existingIds[c]) { deletedCredentials = true; delete credentialCache[c]; } } } if (deletedCredentials) { dirty = true; } }, /** * Registers a node credential definition. * @param type the node type * @param definition the credential definition */ register: function (type, definition) { var dashedType = type.replace(/\s+/g, '-'); credentialsDef[dashedType] = definition; }, /** * Extracts and stores any credential updates in the provided node. * The provided node may have a .credentials property that contains * new credentials for the node. * This function loops through the credentials in the definition for * the node-type and applies any of the updates provided in the node. * * This function does not save the credentials to disk as it is expected * to be called multiple times when a new flow is deployed. * * @param node the node to extract credentials from */ extract: function(node) { var nodeID = node.id; var nodeType = node.type; var cred; var newCreds = node.credentials; if (newCreds) { delete node.credentials; var savedCredentials = credentialCache[nodeID] || {}; // Need to check the type of constructor for this node. // - Function : regular node // - !Function: subflow module if (/^subflow(:|$)/.test(nodeType) || typeof runtime.nodes.getType(nodeType) !== 'function') { for (cred in newCreds) { if (newCreds.hasOwnProperty(cred)) { if (newCreds[cred] === "__PWRD__") { continue; } if (0 === newCreds[cred].length || /^\s*$/.test(newCreds[cred])) { delete savedCredentials[cred]; dirty = true; continue; } if (!savedCredentials.hasOwnProperty(cred) || JSON.stringify(savedCredentials[cred]) !== JSON.stringify(newCreds[cred])) { savedCredentials[cred] = newCreds[cred]; dirty = true; } } } if (/^subflow(:|$)/.test(nodeType)) { for (cred in savedCredentials) { if (savedCredentials.hasOwnProperty(cred)) { if (!newCreds.hasOwnProperty(cred)) { delete savedCredentials[cred]; dirty = true; } } } } } else if (nodeType === "global-config") { if (JSON.stringify(savedCredentials.map) !== JSON.stringify(newCreds.map)) { savedCredentials.map = newCreds.map; dirty = true; } } else { var dashedType = nodeType.replace(/\s+/g, '-'); var definition = credentialsDef[dashedType]; if (!definition) { log.warn(log._("nodes.credentials.not-registered",{type:nodeType})); return; } for (cred in definition) { if (definition.hasOwnProperty(cred)) { if (newCreds[cred] === undefined) { continue; } if (definition[cred].type == "password" && newCreds[cred] == '__PWRD__') { continue; } if (0 === newCreds[cred].length || /^\s*$/.test(newCreds[cred])) { delete savedCredentials[cred]; dirty = true; continue; } if (!savedCredentials.hasOwnProperty(cred) || JSON.stringify(savedCredentials[cred]) !== JSON.stringify(newCreds[cred])) { savedCredentials[cred] = newCreds[cred]; dirty = true; } } } } credentialCache[nodeID] = savedCredentials; } }, /** * Gets the credential definition for the given node type * @param type the node type * @return the credential definition */ getDefinition: function (type) { return credentialsDef[type]; }, dirty: function() { return dirty; }, setKey: function(key) { if (key) { encryptionKey = crypto.createHash('sha256').update(key).digest(); encryptionEnabled = true; dirty = true; encryptionKeyType = "project"; } else { encryptionKey = null; encryptionEnabled = false; dirty = true; encryptionKeyType = "disabled"; } }, getKeyType: function() { return encryptionKeyType; }, export: async function() { var result = credentialCache; if (encryptionEnabled) { if (dirty) { try { log.debug("red/runtime/nodes/credentials.export : encrypting"); result = encryptCredentials(encryptionKey, credentialCache); } catch(err) { log.warn(log._("nodes.credentials.error-saving",{message:err.toString()})) } } else { result = encryptedCredentials; } } dirty = false; if (removeDefaultKey) { log.debug("red/runtime/nodes/credentials.export : removing unused default key"); return settings.delete('_credentialSecret').then(function() { removeDefaultKey = false; return result; }) } else { return result; } } }