diff --git a/config.js b/config.js index 8d29ace43..4e6e38d67 100644 --- a/config.js +++ b/config.js @@ -64,7 +64,14 @@ var defaultConfig = { // This can be changed on the UX > Settings > Insight livenet EncryptedInsightStorage: { url: 'https://insight.bitpay.com:443/api/email', - //url: 'http://localhost:3001/api/email' + //url: 'http://localhost:3001/api/email' + + // This KDF parameters are for the passphrase for Insight authentication + // Are not related to encryption itself. + // + // WARN: Changing this parameters would prevent accesing previously created profiles. + iterations: 1000, + salt: 'jBbYTj8zTrOt6V', }, GoogleDrive: { diff --git a/js/controllers/settings.js b/js/controllers/settings.js index bfe125063..057f30bef 100644 --- a/js/controllers/settings.js +++ b/js/controllers/settings.js @@ -93,7 +93,9 @@ angular.module('copayApp.controllers').controller('SettingsController', function defaultLanguage: $scope.selectedLanguage.isoCode, plugins: plugins, logLevel: $scope.selectedLogLevel.name, - EncryptedInsightStorage: {url: insightSettings.livenet.url + '/api/email' }, + EncryptedInsightStorage: _.extend(config.EncryptedInsightStorage, { + url: insightSettings.livenet.url + '/api/email' + }), })); // Go home reloading the application diff --git a/js/plugins/EncryptedInsightStorage.js b/js/plugins/EncryptedInsightStorage.js index 6e759f794..622c92678 100644 --- a/js/plugins/EncryptedInsightStorage.js +++ b/js/plugins/EncryptedInsightStorage.js @@ -1,21 +1,38 @@ var cryptoUtil = require('../util/crypto'); var InsightStorage = require('./InsightStorage'); var inherits = require('inherits'); +var log = require('../log'); +var SEPARATOR = '%^#@'; function EncryptedInsightStorage(config) { InsightStorage.apply(this, [config]); } inherits(EncryptedInsightStorage, InsightStorage); + +EncryptedInsightStorage.prototype._brokenDecrypt = function(body) { + var key = cryptoUtil.kdf(this.password + this.email, 'mjuBtGybi/4=', 100); + log.debug('Trying legacy decrypt') + var decryptedJson = cryptoUtil.decrypt(key, body); + return decryptedJson; +}; + EncryptedInsightStorage.prototype.getItem = function(name, callback) { - var key = cryptoUtil.kdf(this.password + this.email); + var self = this; InsightStorage.prototype.getItem.apply(this, [name, function(err, body) { if (err) { return callback(err); } - var decryptedJson = cryptoUtil.decrypt(key, body); + var decryptedJson = cryptoUtil.decrypt(self.email + SEPARATOR + self.password, body); + if (!decryptedJson) { + log.debug('Could not decrypt value using current decryption schema'); + decryptedJson = self._brokenDecrypt(body); + } + + if (!decryptedJson) { + log.debug('Could not decrypt value.'); return callback('PNOTFOUND'); } return callback(null, decryptedJson); @@ -24,13 +41,11 @@ EncryptedInsightStorage.prototype.getItem = function(name, callback) { }; EncryptedInsightStorage.prototype.setItem = function(name, value, callback) { - var key = cryptoUtil.kdf(this.password + this.email); - var record = cryptoUtil.encrypt(key, value); + var record = cryptoUtil.encrypt(this.email + SEPARATOR + this.password, value); InsightStorage.prototype.setItem.apply(this, [name, record, callback]); }; EncryptedInsightStorage.prototype.removeItem = function(name, callback) { - var key = cryptoUtil.kdf(this.password + this.email); InsightStorage.prototype.removeItem.apply(this, [name, callback]); }; diff --git a/js/plugins/EncryptedLocalStorage.js b/js/plugins/EncryptedLocalStorage.js index 698e999d6..1cf2c3af2 100644 --- a/js/plugins/EncryptedLocalStorage.js +++ b/js/plugins/EncryptedLocalStorage.js @@ -1,32 +1,57 @@ var cryptoUtil = require('../util/crypto'); +var log = require('../log'); var LocalStorage = require('./LocalStorage'); var inherits = require('inherits'); +var SEPARATOR = '@#$'; + function EncryptedLocalStorage(config) { LocalStorage.apply(this, [config]); } inherits(EncryptedLocalStorage, LocalStorage); + +EncryptedLocalStorage.prototype._brokenDecrypt = function(body) { + var key = cryptoUtil.kdf(this.password + this.email, 'mjuBtGybi/4=', 100); + log.debug('Trying legacy decrypt') + var decryptedJson = cryptoUtil.decrypt(key, body); + return decryptedJson; +}; + + EncryptedLocalStorage.prototype.getItem = function(name, callback) { - var key = cryptoUtil.kdf(this.password + this.email); + var self = this; LocalStorage.prototype.getItem.apply(this, [name, function(err, body) { - var decryptedJson = cryptoUtil.decrypt(key, body); + var decryptedJson = cryptoUtil.decrypt(self.email + SEPARATOR + self.password, body); + if (!decryptedJson) { + log.debug('Could not decrypt value using current decryption schema'); + decryptedJson = self._brokenDecrypt(body); + } + + if (!decryptedJson) { + log.debug('Could not decrypt value.'); return callback('PNOTFOUND'); } + return callback(null, decryptedJson); } ]); }; EncryptedLocalStorage.prototype.setItem = function(name, value, callback) { - var key = cryptoUtil.kdf(this.password + this.email); if (!_.isString(value)) { value = JSON.stringify(value); } - var record = cryptoUtil.encrypt(key, value); + var record = cryptoUtil.encrypt(this.email + SEPARATOR + this.password, value); LocalStorage.prototype.setItem.apply(this, [name, record, callback]); }; +EncryptedLocalStorage.prototype.removeItem = function(name, callback) { + InsightStorage.prototype.removeItem.apply(this, [name, callback]); +}; + + + module.exports = EncryptedLocalStorage; diff --git a/js/plugins/InsightStorage.js b/js/plugins/InsightStorage.js index b27cb4deb..c65ccbfe2 100644 --- a/js/plugins/InsightStorage.js +++ b/js/plugins/InsightStorage.js @@ -1,8 +1,10 @@ var request = require('request'); var cryptoUtil = require('../util/crypto'); +var bitcore = require('bitcore'); var buffers = require('buffer'); var querystring = require('querystring'); var Identity = require('../models/Identity'); +var log = require('../log'); var SEPARATOR = '|'; @@ -10,9 +12,12 @@ function InsightStorage(config) { this.type = 'DB'; this.storeUrl = config.url || 'https://insight.bitpay.com:443/api/email', this.request = config.request || request; + + this.iterations = config.iterations || 1000; + this.salt = config.salt || 'jBbYTj8zTrOt6V'; } -InsightStorage.prototype.init = function () {}; +InsightStorage.prototype.init = function() {}; InsightStorage.prototype.setCredentials = function(email, password, opts) { this.email = email; @@ -42,6 +47,7 @@ InsightStorage.prototype.getItem = function(name, callback) { var self = this; this._makeGetRequest(passphrase, name, function(err, body) { + if (err) log.info('[InsightStorage. err]', err); if (err && err.indexOf('PNOTFOUND') !== -1 && mayBeOldPassword(self.password)) { return self._brokenGetItem(name, callback); } @@ -49,17 +55,38 @@ InsightStorage.prototype.getItem = function(name, callback) { }); }; +/* This key need to have DIFFERENT + * settings(salt,iterations) than the kdf for wallet/profile encryption + * in Encrpted*Storage. The user should be able + * to change the settings on config.js to modify salt / iterations + * for encryption, but + * mantain the same key & passphrase. This is why those settings are + * not shared with encryption + */ +InsightStorage.prototype.getKey = function() { + if (!this._cachedKey) { + this._cachedKey = cryptoUtil.kdf(this.password + SEPARATOR + this.email, this.salt, this.iterations); + } + return this._cachedKey; +}; + InsightStorage.prototype.getPassphrase = function() { - return cryptoUtil.hmac(this.getKey(), this.password); + return bitcore.util.twoSha256(this.getKey()).toString('base64'); }; InsightStorage.prototype._makeGetRequest = function(passphrase, key, callback) { var authHeader = new buffers.Buffer(this.email + ':' + passphrase).toString('base64'); var retrieveUrl = this.storeUrl + '/retrieve'; - this.request.get({ - url: retrieveUrl + '?' + querystring.encode({key: key}), - headers: {'Authorization': authHeader} - }, + var getParams = { + url: retrieveUrl + '?' + querystring.encode({ + key: key + }), + headers: { + 'Authorization': authHeader + } + }; + log.debug('Insight request', getParams); + this.request.get(getParams, function(err, response, body) { if (err) { return callback('Connection error'); @@ -78,6 +105,7 @@ InsightStorage.prototype._makeGetRequest = function(passphrase, key, callback) { InsightStorage.prototype._brokenGetItem = function(name, callback) { var passphrase = this._makeBrokenSecret(); var self = this; + log.debug('using legacy get'); this._makeGetRequest(passphrase, name, function(err, body) { if (!err) { return self._changePassphrase(function(err) { @@ -91,16 +119,9 @@ InsightStorage.prototype._brokenGetItem = function(name, callback) { }); }; -InsightStorage.prototype.getKey = function() { - if (!this._cachedKey) { - this._cachedKey = cryptoUtil.kdf(this.password + SEPARATOR + this.email); - } - return this._cachedKey; -}; - InsightStorage.prototype._makeBrokenSecret = function() { - var key = cryptoUtil.kdf(this.password + this.email); - return cryptoUtil.kdf(key, this.password); + var key = cryptoUtil.kdf(this.password + this.email, 'mjuBtGybi/4=', 100); + return cryptoUtil.kdf(key, this.password, 100); }; InsightStorage.prototype._changePassphrase = function(callback) { @@ -111,7 +132,9 @@ InsightStorage.prototype._changePassphrase = function(callback) { var url = this.storeUrl + '/change_passphrase'; this.request.post({ url: url, - headers: {'Authorization': authHeader}, + headers: { + 'Authorization': authHeader + }, body: querystring.encode({ newPassphrase: newPassphrase }) @@ -135,7 +158,9 @@ InsightStorage.prototype.setItem = function(name, value, callback) { var registerUrl = this.storeUrl + '/save'; this.request.post({ url: registerUrl, - headers: {'Authorization': authHeader}, + headers: { + 'Authorization': authHeader + }, body: querystring.encode({ key: name, record: value diff --git a/test/plugin.insight.js b/test/plugin.insight.js index 5f65942ea..c086d6ea1 100644 --- a/test/plugin.insight.js +++ b/test/plugin.insight.js @@ -5,16 +5,17 @@ var querystring = require('querystring'); describe('insight storage plugin', function() { var requestMock = sinon.stub(); - var storage = new InsightStorage({request: requestMock}); + var storage = new InsightStorage({ + request: requestMock + }); var email = 'john@doe.com'; var password = '1234'; var data = '{"random": true}'; var namespace = 'profile::0000000000000000000000000000000000000000'; - var oldSecret = 'rFA+F/N+ZvKXp717zBdfCKYQ5v9Fjry0W6tautj5etIH' - + 'KLQliZBEYXA7AXjTJ9K3DglzGWJKost3QJUCMbhM/A==' - var newSecret = '+72pwnQ/ukrXVXZ/L4vFeiykwn522uVz0J6p81TGXvI='; + var oldSecret = 'rFA+F/N+ZvKXp717zBdfCKYQ5v9Fjry0W6tautj5etIH' + 'KLQliZBEYXA7AXjTJ9K3DglzGWJKost3QJUCMbhM/A==' + var newSecret = '96KnVsaQFv8vsbxAFeYyGM4nO/8B6YaVNKz9IxDmwzk='; var setupStorageCredentials = function() { storage.setCredentials(email, password); @@ -29,7 +30,9 @@ describe('insight storage plugin', function() { var setupForCreation = function() { requestMock.get.onFirstCall().callsArgWith(1, 'Not found'); - requestMock.post.onFirstCall().callsArgWith(1, null, {statusCode: 200}); + requestMock.post.onFirstCall().callsArgWith(1, null, { + statusCode: 200 + }); }; it('should be able to create a namespace for storage', function(done) { @@ -43,7 +46,9 @@ describe('insight storage plugin', function() { }); var setupForRetrieval = function() { - requestMock.get.onFirstCall().callsArgWith(1, null, {statusCode: 200}, data); + requestMock.get.onFirstCall().callsArgWith(1, null, { + statusCode: 200 + }, data); }; it('should be able to retrieve data in a namespace', function(done) { @@ -57,8 +62,10 @@ describe('insight storage plugin', function() { }); }); - var setupForSave = function () { - requestMock.post.onFirstCall().callsArgWith(1, null, {statusCode: 200}); + var setupForSave = function() { + requestMock.post.onFirstCall().callsArgWith(1, null, { + statusCode: 200 + }); }; it('should be able to overwrite data when using same password', function(done) { @@ -95,10 +102,16 @@ describe('insight storage plugin', function() { var setupForOldData = function() { requestMock.get = sinon.stub(); - requestMock.get.onFirstCall().callsArgWith(1, null, {statusCode: 403}); - requestMock.get.onSecondCall().callsArgWith(1, null, {statusCode: 200}, data); + requestMock.get.onFirstCall().callsArgWith(1, null, { + statusCode: 403 + }); + requestMock.get.onSecondCall().callsArgWith(1, null, { + statusCode: 200 + }, data); requestMock.post = sinon.stub(); - requestMock.post.onFirstCall().callsArgWith(1, null, {statusCode: 200}); + requestMock.post.onFirstCall().callsArgWith(1, null, { + statusCode: 200 + }); } it('should be able to restore 0.7.2 data', function(done) { @@ -120,11 +133,9 @@ describe('insight storage plugin', function() { var receivedArgs = requestMock.post.firstCall.args[0].body; var url = requestMock.post.firstCall.args[0].url; var args = querystring.decode(receivedArgs); - assert(url.indexOf('change_passphrase') !== -1); - assert(requestMock.post.firstCall.args[0].headers.Authorization - === - new Buffer(email + ':' + oldSecret).toString('base64')); - assert(args.newPassphrase === newSecret); + url.indexOf('change_passphrase').should.not.be.equal(-1); + requestMock.post.firstCall.args[0].headers.Authorization.should.be.equal( new Buffer(email + ':' + oldSecret).toString('base64')); + args.newPassphrase.should.be.equal(newSecret); done(); }); } diff --git a/test/util.crypto.js b/test/util.crypto.js index 2cb42211c..765e537ba 100644 --- a/test/util.crypto.js +++ b/test/util.crypto.js @@ -49,7 +49,7 @@ describe('crypto utils', function() { phrase.should.equal(t.phrase); }); it('should generate a passphrase from weird chars', function() { - var phrase = cryptoUtils.kdf('Pwd123!@#$%^&*(){}[]\|/?.>,<=+-_`~åéþ䲤þçæ¶'); + var phrase = cryptoUtils.kdf('Pwd123!@#$%^&*(){}[]\|/?.>,<=+-_`~åéþ䲤þçæ¶', tests[0].salt, 100); var expected = 'CZwb5KdikvZHVsEoZUdJckAy+yyzGnd++XhyqxJXbc30' + 'pEoO+WqHgqBbdf0gn2wiyWZv3zymB+7L75Xnz3uSlg=='; phrase.should.equal(expected);