mirror of
https://github.com/node-red/node-red.git
synced 2023-10-10 13:36:53 +02:00
396 lines
15 KiB
JavaScript
396 lines
15 KiB
JavaScript
/**
|
|
* 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 when = require("when");
|
|
var crypto = require('crypto');
|
|
var settings;
|
|
var log;
|
|
|
|
var encryptedCredentials = null;
|
|
var credentialCache = {};
|
|
var credentialsDef = {};
|
|
var dirty = false;
|
|
|
|
var removeDefaultKey = false;
|
|
var encryptionEnabled = null;
|
|
var encryptionAlgorithm = "aes-256-ctr";
|
|
var encryptionKey;
|
|
|
|
function decryptCredentials(key,credentials) {
|
|
var creds = credentials["$"];
|
|
var initVector = new Buffer(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 markProjectSecretValid(project,valid) {
|
|
try {
|
|
var projects = settings.get('projects');
|
|
if (projects) {
|
|
if (valid) {
|
|
if (!projects.projects[project].credentialSecretInvalid) {
|
|
return when.resolve();
|
|
} else {
|
|
delete projects.projects[project].credentialSecretInvalid;
|
|
}
|
|
} else {
|
|
projects.projects[project].credentialSecretInvalid = true;
|
|
}
|
|
return settings.set('projects',projects);
|
|
} else {
|
|
return when.resolve();
|
|
}
|
|
} catch(err) {
|
|
return when.resolve();
|
|
}
|
|
}
|
|
|
|
var api = module.exports = {
|
|
init: function(runtime) {
|
|
log = runtime.log;
|
|
settings = runtime.settings;
|
|
dirty = false;
|
|
credentialCache = {};
|
|
credentialsDef = {};
|
|
encryptionEnabled = null;
|
|
},
|
|
|
|
/**
|
|
* Sets the credentials from storage.
|
|
*/
|
|
load: function (credentials) {
|
|
dirty = false;
|
|
/*
|
|
- if encryptionEnabled === null, check the current configuration
|
|
*/
|
|
var credentialsEncrypted = credentials.hasOwnProperty("$") && Object.keys(credentials).length === 1;
|
|
var setupEncryptionPromise = when.resolve();
|
|
// if (encryptionEnabled === null) {
|
|
var defaultKey;
|
|
try {
|
|
defaultKey = settings.get('_credentialSecret');
|
|
} catch(err) {
|
|
}
|
|
if (defaultKey) {
|
|
defaultKey = crypto.createHash('sha256').update(defaultKey).digest();
|
|
}
|
|
var userKey;
|
|
try {
|
|
userKey = settings.get('credentialSecret');
|
|
} catch(err) {
|
|
userKey = false;
|
|
}
|
|
|
|
var projectKey = false;
|
|
var activeProject;
|
|
try {
|
|
var projects = settings.get('projects');
|
|
if (projects && projects.activeProject) {
|
|
activeProject = projects.activeProject;
|
|
projectKey = projects.projects[projects.activeProject].credentialSecret;
|
|
}
|
|
} catch(err) {
|
|
}
|
|
if (projectKey) {
|
|
log.debug("red/runtime/nodes/credentials.load : using active project key - ignoring user provided key");
|
|
userKey = projectKey;
|
|
}
|
|
|
|
//TODO: Need to consider the various migration scenarios from no-project to project
|
|
// - _credentialSecret exists, projectKey exists
|
|
// - _credentialSecret does not exist, projectKey exists
|
|
// - userKey exists, projectKey exists
|
|
if (userKey === false) {
|
|
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) {
|
|
console.log("****************************************************************");
|
|
console.log("* Oh oh - default key present. We don't handle this well today *");
|
|
console.log("****************************************************************");
|
|
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";
|
|
return when.reject(error);
|
|
}
|
|
}
|
|
dirty = true;
|
|
removeDefaultKey = true;
|
|
}
|
|
} else if (typeof userKey === 'string') {
|
|
if (!projectKey) {
|
|
log.debug("red/runtime/nodes/credentials.load : user provided key");
|
|
}
|
|
// User has provided own encryption key, get the 32-byte hash of it
|
|
encryptionKey = crypto.createHash('sha256').update(userKey).digest();
|
|
encryptionEnabled = true;
|
|
|
|
if (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";
|
|
return when.reject(error);
|
|
}
|
|
}
|
|
dirty = true;
|
|
removeDefaultKey = true;
|
|
}
|
|
} else {
|
|
log.debug("red/runtime/nodes/credentials.load : no user key present");
|
|
// User has not provide their own key
|
|
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;
|
|
}
|
|
return setupEncryptionPromise.then(function() {
|
|
var clearInvalidFlag = false;
|
|
if (credentials.hasOwnProperty("$")) {
|
|
// 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 (projectKey) {
|
|
// This is a project with a bad key. Mark it as invalid
|
|
return markProjectSecretValid(activeProject,false).then(function() {
|
|
return when.reject(error);
|
|
})
|
|
}
|
|
return when.reject(error);
|
|
}
|
|
} else {
|
|
credentialCache = credentials;
|
|
}
|
|
if (clearInvalidFlag) {
|
|
return markProjectSecretValid(activeProject,true)
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* 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: function (id, creds) {
|
|
if (!credentialCache.hasOwnProperty(id) || JSON.stringify(creds) !== JSON.stringify(credentialCache[id])) {
|
|
credentialCache[id] = creds;
|
|
dirty = true;
|
|
}
|
|
return when.resolve();
|
|
},
|
|
|
|
/**
|
|
* 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: 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;
|
|
}
|
|
return when.resolve();
|
|
},
|
|
|
|
/**
|
|
* 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 newCreds = node.credentials;
|
|
if (newCreds) {
|
|
delete node.credentials;
|
|
var savedCredentials = credentialCache[nodeID] || {};
|
|
var dashedType = nodeType.replace(/\s+/g, '-');
|
|
var definition = credentialsDef[dashedType];
|
|
if (!definition) {
|
|
log.warn(log._("nodes.credentials.not-registered",{type:nodeType}));
|
|
return;
|
|
}
|
|
|
|
for (var 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) {
|
|
encryptionKey = crypto.createHash('sha256').update(key).digest();
|
|
encryptionEnabled = true;
|
|
dirty = true;
|
|
},
|
|
export: function() {
|
|
var result = credentialCache;
|
|
|
|
if (encryptionEnabled) {
|
|
if (dirty) {
|
|
try {
|
|
log.debug("red/runtime/nodes/credentials.export : encrypting");
|
|
var initVector = crypto.randomBytes(16);
|
|
var cipher = crypto.createCipheriv(encryptionAlgorithm, encryptionKey, initVector);
|
|
result = {"$":initVector.toString('hex') + cipher.update(JSON.stringify(credentialCache), 'utf8', 'base64') + cipher.final('base64')};
|
|
} 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 when.resolve(result);
|
|
}
|
|
}
|
|
}
|