/** * 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 should = require("should"); var sinon = require("sinon"); var util = require("util"); var NR_TEST_UTILS = require("nr-test-utils"); var index = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/index"); var credentials = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/credentials"); var log = NR_TEST_UTILS.require("@node-red/util").log; describe('red/runtime/nodes/credentials', function() { var encryptionDisabledSettings = { get: function(key) { return false; } } afterEach(function() { index.clearRegistry(); }); it('loads provided credentials',function() { credentials.init({ log: log, settings: encryptionDisabledSettings }); return credentials.load({"a":{"b":1,"c":2}}).then(function() { credentials.get("a").should.have.property('b',1); credentials.get("a").should.have.property('c',2); }); }); it('adds a new credential',function() { credentials.init({ log: log, settings: encryptionDisabledSettings }); return credentials.load({"a":{"b":1,"c":2}}).then(function() { credentials.dirty().should.be.false(); should.not.exist(credentials.get("b")); return credentials.add("b",{"foo":"bar"}).then(function() { credentials.get("b").should.have.property("foo","bar"); credentials.dirty().should.be.true(); }); }); }); it('deletes an existing credential',function() { credentials.init({ log: log, settings: encryptionDisabledSettings }); return credentials.load({"a":{"b":1,"c":2}}).then(function() { credentials.dirty().should.be.false(); credentials.delete("a"); should.not.exist(credentials.get("a")); credentials.dirty().should.be.true(); }); }); it('exports the credentials, clearing dirty flag', function() { credentials.init({ log: log, settings: encryptionDisabledSettings }); var creds = {"a":{"b":1,"c":2}}; return credentials.load(creds).then(function() { return credentials.add("b",{"foo":"bar"}) }).then(function() { credentials.dirty().should.be.true(); return credentials.export().then(function(exported) { exported.should.eql(creds); credentials.dirty().should.be.false(); }) }); }) describe("#clean",function() { it("removes credentials of unknown nodes",function() { credentials.init({ log: log, settings: encryptionDisabledSettings, nodes: { getType: () => function(){} } }); var creds = {"a":{"b":1,"c":2},"b":{"d":3}}; return credentials.load(creds).then(function() { credentials.dirty().should.be.false(); should.exist(credentials.get("a")); should.exist(credentials.get("b")); return credentials.clean([{id:"b"}]).then(function() { credentials.dirty().should.be.true(); should.not.exist(credentials.get("a")); should.exist(credentials.get("b")); }); }); }); it("extracts credentials of known nodes",function() { credentials.init({ log: log, settings: encryptionDisabledSettings, nodes: { getType: () => function(){} } }); credentials.register("testNode",{"b":"text","c":"password"}) var creds = {"a":{"b":1,"c":2}}; var newConfig = [{id:"a",type:"testNode",credentials:{"b":"newBValue","c":"newCValue"}}]; return credentials.load(creds).then(function() { credentials.dirty().should.be.false(); return credentials.clean(newConfig).then(function() { credentials.dirty().should.be.true(); credentials.get("a").should.have.property('b',"newBValue"); credentials.get("a").should.have.property('c',"newCValue"); should.not.exist(newConfig[0].credentials); }); }); }); }); it('warns if a node has no credential definition', function() { credentials.init({ log: log, settings: encryptionDisabledSettings, nodes: { getType: () => function(){} } }); return credentials.load({}).then(function() { var node = {id:"node",type:"test",credentials:{ user1:"newUser", password1:"newPassword" }}; sinon.spy(log,"warn"); credentials.extract(node); log.warn.called.should.be.true(); should.not.exist(node.credentials); log.warn.restore(); }); }) it('extract credential updates in the provided node', function(done) { credentials.init({ log: log, settings: encryptionDisabledSettings, nodes: { getType: () => function(){} } }); var defintion = { user1:{type:"text"}, password1:{type:"password"}, user2:{type:"text"}, password2:{type:"password"}, user3:{type:"text"}, password3:{type:"password"} }; credentials.register("test",defintion); var def = credentials.getDefinition("test"); defintion.should.eql(def); credentials.load({"node":{user1:"abc",password1:"123",user2:"def",password2:"456",user3:"ghi",password3:"789"}}).then(function() { var node = {id:"node",type:"test",credentials:{ // user1 unchanged password1:"__PWRD__", user2: "", password2:" ", user3:"newUser", password3:"newPassword" }}; credentials.dirty().should.be.false(); credentials.extract(node); node.should.not.have.a.property("credentials"); credentials.dirty().should.be.true(); var newCreds = credentials.get("node"); newCreds.should.have.a.property("user1","abc"); newCreds.should.have.a.property("password1","123"); newCreds.should.not.have.a.property("user2"); newCreds.should.not.have.a.property("password2"); newCreds.should.have.a.property("user3","newUser"); newCreds.should.have.a.property("password3","newPassword"); done(); }); }); it('extract ignores node without credentials', function(done) { credentials.init({ log: log, settings: encryptionDisabledSettings, nodes: { getType: () => function(){} } }); credentials.load({"node":{user1:"abc",password1:"123"}}).then(function() { var node = {id:"node",type:"test"}; credentials.dirty().should.be.false(); credentials.extract(node); credentials.dirty().should.be.false(); done(); }); }); describe("encryption",function() { var settings = {}; var runtime = { log: log, settings: { get: function(key) { return settings[key]; }, set: function(key,value) { settings[key] = value; return Promise.resolve(); }, delete: function(key) { delete settings[key]; return Promise.resolve(); } }, nodes: { getType: () => function(){} } } it('migrates to encrypted and generates default key', function(done) { settings = {}; credentials.init(runtime); credentials.load({"node":{user1:"abc",password1:"123"}}).then(function() { settings.should.have.a.property("_credentialSecret"); settings._credentialSecret.should.have.a.length(64); credentials.dirty().should.be.true(); credentials.export().then(function(result) { result.should.have.a.property("$"); // reset everything - but with _credentialSecret still set credentials.init(runtime); // load the freshly encrypted version credentials.load(result).then(function() { should.exist(credentials.get("node")); done(); }) }); }); }); it('uses default key', function(done) { settings = { _credentialSecret: "e3a36f47f005bf2aaa51ce3fc6fcaafd79da8d03f2b1a9281f8fb0a285e6255a" }; // {"node":{user1:"abc",password1:"123"}} var cryptedFlows = {"$":"5b89d8209b5158a3c313675561b1a5b5phN1gDBe81Zv98KqS/hVDmc9EKvaKqRIvcyXYvBlFNzzzJtvN7qfw06i"}; credentials.init(runtime); credentials.load(cryptedFlows).then(function() { should.exist(credentials.get("node")); credentials.dirty().should.be.false(); credentials.add("node",{user1:"def",password1:"456"}); credentials.export().then(function(result) { result.should.have.a.property("$"); // reset everything - but with _credentialSecret still set credentials.init(runtime); // load the freshly encrypted version credentials.load(result).then(function() { should.exist(credentials.get("node")); credentials.get("node").should.have.a.property("user1","def"); credentials.get("node").should.have.a.property("password1","456"); done(); }) }); }); }); it('uses user key', function(done) { settings = { credentialSecret: "e3a36f47f005bf2aaa51ce3fc6fcaafd79da8d03f2b1a9281f8fb0a285e6255a" }; // {"node":{user1:"abc",password1:"123"}} var cryptedFlows = {"$":"5b89d8209b5158a3c313675561b1a5b5phN1gDBe81Zv98KqS/hVDmc9EKvaKqRIvcyXYvBlFNzzzJtvN7qfw06i"}; credentials.init(runtime); credentials.load(cryptedFlows).then(function() { credentials.dirty().should.be.false(); should.exist(credentials.get("node")); credentials.add("node",{user1:"def",password1:"456"}); credentials.export().then(function(result) { result.should.have.a.property("$"); // reset everything - but with _credentialSecret still set credentials.init(runtime); // load the freshly encrypted version credentials.load(result).then(function() { should.exist(credentials.get("node")); credentials.get("node").should.have.a.property("user1","def"); credentials.get("node").should.have.a.property("password1","456"); done(); }) }); }); }); it('uses user key - when settings are otherwise unavailable', function(done) { var runtime = { log: log, settings: { get: function(key) { if (key === 'credentialSecret') { return "e3a36f47f005bf2aaa51ce3fc6fcaafd79da8d03f2b1a9281f8fb0a285e6255a"; } throw new Error(); }, set: function(key,value) { throw new Error(); } } } // {"node":{user1:"abc",password1:"123"}} var cryptedFlows = {"$":"5b89d8209b5158a3c313675561b1a5b5phN1gDBe81Zv98KqS/hVDmc9EKvaKqRIvcyXYvBlFNzzzJtvN7qfw06i"}; credentials.init(runtime); credentials.load(cryptedFlows).then(function() { should.exist(credentials.get("node")); credentials.add("node",{user1:"def",password1:"456"}); credentials.export().then(function(result) { result.should.have.a.property("$"); // reset everything - but with _credentialSecret still set credentials.init(runtime); // load the freshly encrypted version credentials.load(result).then(function() { should.exist(credentials.get("node")); credentials.get("node").should.have.a.property("user1","def"); credentials.get("node").should.have.a.property("password1","456"); done(); }) }); }); }); it('migrates from default key to user key', function() { settings = { _credentialSecret: "e3a36f47f005bf2aaa51ce3fc6fcaafd79da8d03f2b1a9281f8fb0a285e6255a", credentialSecret: "aaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbcccccccccccccddddddddddddeeeee" }; // {"node":{user1:"abc",password1:"123"}} var cryptedFlows = {"$":"5b89d8209b5158a3c313675561b1a5b5phN1gDBe81Zv98KqS/hVDmc9EKvaKqRIvcyXYvBlFNzzzJtvN7qfw06i"}; credentials.init(runtime); return credentials.load(cryptedFlows).then(function() { credentials.dirty().should.be.true(); should.exist(credentials.get("node")); return credentials.export().then(function(result) { result.should.have.a.property("$"); settings.should.not.have.a.property("_credentialSecret"); // reset everything - but with _credentialSecret still set credentials.init(runtime); // load the freshly encrypted version return credentials.load(result).then(function() { should.exist(credentials.get("node")); credentials.get("node").should.have.a.property("user1","abc"); credentials.get("node").should.have.a.property("password1","123"); }) }); }); }); it('migrates from default key to user key - unencrypted original', function(done) { settings = { _credentialSecret: "e3a36f47f005bf2aaa51ce3fc6fcaafd79da8d03f2b1a9281f8fb0a285e6255a", credentialSecret: "aaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbcccccccccccccddddddddddddeeeee" }; // {"node":{user1:"abc",password1:"123"}} var unencryptedFlows = {"node":{user1:"abc",password1:"123"}}; credentials.init(runtime); credentials.load(unencryptedFlows).then(function() { credentials.dirty().should.be.true(); should.exist(credentials.get("node")); credentials.export().then(function(result) { result.should.have.a.property("$"); settings.should.not.have.a.property("_credentialSecret"); // reset everything - but with _credentialSecret still set credentials.init(runtime); // load the freshly encrypted version credentials.load(result).then(function() { should.exist(credentials.get("node")); credentials.get("node").should.have.a.property("user1","abc"); credentials.get("node").should.have.a.property("password1","123"); done(); }) }); }); }); it('migrates from default key to unencrypted', function(done) { settings = { _credentialSecret: "e3a36f47f005bf2aaa51ce3fc6fcaafd79da8d03f2b1a9281f8fb0a285e6255a", credentialSecret: false }; // {"node":{user1:"abc",password1:"123"}} var cryptedFlows = {"$":"5b89d8209b5158a3c313675561b1a5b5phN1gDBe81Zv98KqS/hVDmc9EKvaKqRIvcyXYvBlFNzzzJtvN7qfw06i"}; credentials.init(runtime); credentials.load(cryptedFlows).then(function() { credentials.dirty().should.be.true(); should.exist(credentials.get("node")); credentials.export().then(function(result) { result.should.not.have.a.property("$"); settings.should.not.have.a.property("_credentialSecret"); result.should.eql({"node":{user1:"abc",password1:"123"}}); done(); }); }); }); it('handles bad default key - resets credentials', function(done) { settings = { _credentialSecret: "badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadb" }; // {"node":{user1:"abc",password1:"123"}} var cryptedFlows = {"$":"5b89d8209b5158a3c313675561b1a5b5phN1gDBe81Zv98KqS/hVDmc9EKvaKqRIvcyXYvBlFNzzzJtvN7qfw06i"}; credentials.init(runtime); credentials.load(cryptedFlows).then(function() { // credentials.dirty().should.be.true(); // should.not.exist(credentials.get("node")); done(); }).catch(function(err) { err.should.have.property('code','credentials_load_failed'); done(); }); }); it('handles bad user key - resets credentials', function(done) { settings = { credentialSecret: "badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadb" }; // {"node":{user1:"abc",password1:"123"}} var cryptedFlows = {"$":"5b89d8209b5158a3c313675561b1a5b5phN1gDBe81Zv98KqS/hVDmc9EKvaKqRIvcyXYvBlFNzzzJtvN7qfw06i"}; credentials.init(runtime); credentials.load(cryptedFlows).then(function() { // credentials.dirty().should.be.true(); // should.not.exist(credentials.get("node")); done(); }).catch(function(err) { err.should.have.property('code','credentials_load_failed'); done(); }); }); it('handles bad credentials object - resets credentials', function(done) { settings = { credentialSecret: "e3a36f47f005bf2aaa51ce3fc6fcaafd79da8d03f2b1a9281f8fb0a285e6255a" }; // {"node":{user1:"abc",password1:"123"}} var cryptedFlows = {"BADKEY":"5b89d8209b5158a3c313675561b1a5b5phN1gDBe81Zv98KqS/hVDmc9EKvaKqRIvcyXYvBlFNzzzJtvN7qfw06i"}; credentials.init(runtime); credentials.load(cryptedFlows).then(function() { done(); }).catch(function(err) { err.should.have.property('code','credentials_load_failed'); done(); }); }); it('handles unavailable settings - leaves creds unencrypted', function(done) { var runtime = { log: log, settings: { get: function(key) { throw new Error(); }, set: function(key,value) { throw new Error(); } }, nodes: { getType: () => function(){} } } // {"node":{user1:"abc",password1:"123"}} credentials.init(runtime); credentials.load({"node":{user1:"abc",password1:"123"}}).then(function() { credentials.dirty().should.be.false(); should.exist(credentials.get("node")); credentials.export().then(function(result) { result.should.not.have.a.property("$"); result.should.have.a.property("node"); done(); }); }); }); }) })