Merge pull request #38 from eordano/fix/imports

Fixes import & export with new encryption scheme
This commit is contained in:
Matias Alejo Garcia 2014-10-28 15:02:00 -03:00
commit e80dda1325
22 changed files with 367 additions and 271 deletions

View file

@ -83,6 +83,10 @@ module.exports = function(grunt) {
config: {
files: ['config.js'],
tasks: ['shell:dev', 'concat:main']
},
test: {
files: ['test/**/*.js'],
tasks: ['mochaTest']
}
},
mochaTest: {

View file

@ -3,7 +3,6 @@ module.exports.PublicKeyRing = require('./js/models/PublicKeyRing');
module.exports.TxProposal = require('./js/models/TxProposal');
module.exports.TxProposals = require('./js/models/TxProposals');
module.exports.PrivateKey = require('./js/models/PrivateKey');
module.exports.Passphrase = require('./js/models/Passphrase');
module.exports.HDPath = require('./js/models/HDPath');
module.exports.HDParams = require('./js/models/HDParams');

View file

@ -1,6 +1,6 @@
'use strict';
angular.module('copayApp.controllers').controller('HeadController', function($scope, $rootScope, notification, controllerUtils) {
angular.module('copayApp.controllers').controller('HeadController', function($scope, $rootScope, $filter, notification, controllerUtils) {
$scope.username = $rootScope.iden ? $rootScope.iden.fullName || $rootScope.iden.email : 'undefined';
$scope.hoverMenu = false;

View file

@ -1,7 +1,7 @@
'use strict';
angular.module('copayApp.controllers').controller('ImportController',
function($scope, $rootScope, $location, controllerUtils, Passphrase, notification, isMobile) {
function($scope, $rootScope, $location, controllerUtils, notification, isMobile, Compatibility) {
$rootScope.title = 'Import a backup';
$scope.importStatus = 'Importing wallet - Reading backup...';
@ -25,7 +25,7 @@ angular.module('copayApp.controllers').controller('ImportController',
if ($scope.skipTxProposals)
skipFields.push('txProposals');
$rootScope.iden.importWallet(encryptedObj, password, skipFields, function(err, w) {
$rootScope.iden.importEncryptedWallet(encryptedObj, password, skipFields, function(err, w) {
if (!w) {
$scope.loading = false;
notification.error('Error', err || 'Wrong password');
@ -94,7 +94,11 @@ angular.module('copayApp.controllers').controller('ImportController',
reader.readAsBinaryString(backupFile);
}
else {
_importBackup(backupText);
try {
_importBackup(backupText);
} catch(e) {
Compatibility.preDotEightImportWalletToStorage(backupText, $scope.password, $scope.skipPublicKeyRing, $scope.skipTxProposals);
}
}
};
});

186
js/models/Compatibility.js Normal file
View file

@ -0,0 +1,186 @@
'use strict';
var Identity = require('./Identity');
var Wallet = require('./Wallet');
var cryptoUtils = require('../util/crypto');
var CryptoJS = require('node-cryptojs-aes').CryptoJS;
var Compatibility = {};
/**
* Reads from localstorage wallets saved previously to 0.8
*/
Compatibility._getWalletIds = function(cb) {
preconditions.checkArgument(cb);
var walletIds = [];
var uniq = {};
for (key in localStorage) {
var split = key.split('::');
if (split.length == 2) {
var walletId = split[0];
if (!walletId
|| walletId === 'nameFor'
|| walletId === 'lock'
|| walletId === 'wallet') {
continue;
}
if (typeof uniq[walletId] === 'undefined') {
walletIds.push(walletId);
uniq[walletId] = 1;
}
}
}
return cb(walletIds);
};
/**
* @param {string} encryptedWallet - base64-encoded encrypted wallet
* @param {string} passphrase - base64-encoded passphrase
* @returns {Object}
*/
Compatibility.importLegacy = function(encryptedWallet, passphrase) {
var ret = Compatibility._decrypt(encryptedWallet, passphrase);
if (!ret) return null;
ret = ret.toString(CryptoJS.enc.Utf8);
ret = JSON.parse(ret);
return ret;
};
/**
* Decrypts using the CryptoJS library (unknown encryption schema)
*
* Don't use CryptoJS to encrypt. This still exists for compatibility reasons only.
*/
Compatibility._decrypt = function(base64, passphrase) {
var decryptedStr = null;
try {
var decrypted = CryptoJS.AES.decrypt(base64, passphrase);
if (decrypted)
decryptedStr = decrypted.toString(CryptoJS.enc.Utf8);
} catch (e) {
// Error while decrypting
return null;
}
return decryptedStr;
};
/**
* Reads an item from localstorage, decrypts it with passphrase
*/
Compatibility._read = function(k, passphrase, cb) {
preconditions.checkArgument(cb);
var ret = localStorage.getItem(k);
if (!ret) return cb(null);
var ret = self._decrypt(ret, passphrase);
if (!ret) return cb(null);
ret = ret.toString(CryptoJS.enc.Utf8);
ret = JSON.parse(ret);
return ret;
};
Compatibility.getWallets_Old = function(cb) {
preconditions.checkArgument(cb);
var wallets = [];
var self = this;
this._getWalletIds(function(ids) {
if (!ids.length) {
return cb([]);
}
_.each(ids, function(id) {
var name = localStorage.getItem('nameFor::' + id);
if (name) {
wallets.push({
id: id,
name: name,
});
}
});
return cb(wallets);
});
};
Compatibility.getWallets2 = function(cb) {
var self = this;
var re = /wallet::([^_]+)(_?(.*))/;
var keys = [];
for (key in localStorage) {
keys.push(key);
}
var wallets = _.compact(_.map(keys, function(key) {
if (key.indexOf('wallet::') !== 0)
return null;
var match = key.match(re);
if (match.length != 4)
return null;
return {
id: match[1],
name: match[3] ? match[3] : undefined,
};
}));
return cb(wallets);
};
/**
* Lists all wallets in localstorage
*/
Compatibility.listWalletsPre8 = function (cb) {
var self = this;
self.getWallets2(function(wallets) {
self.getWallets_Old(function(wallets2) {
var ids = _.pluck(wallets, 'id');
_.each(wallets2, function(w) {
if (!_.contains(ids, w.id))
wallets.push(w);
});
return cb(wallets);
});
})
};
/**
* Retrieves a wallet that predates the 0.8 release
*/
Compatibility.readWalletPre8 = function(walletId, password, cb) {
var self = this;
var passphrase = cryptoUtils.kdf(password);
var obj = {};
for (key in localStorage) {
if (key.indexOf('wallet::' + walletId) !== -1) {
var ret = self._read(localStorage.getItem(key), passphrase);
if (err) return cb(err);
_.each(Wallet.PERSISTED_PROPERTIES, function(p) {
obj[p] = ret[p];
});
if (!_.any(_.values(obj)))
return cb(new Error('Wallet not found'));
var w, err;
obj.id = walletId;
try {
w = self.fromObj(obj);
} catch (e) {
if (e && e.message && e.message.indexOf('MISSOPTS')) {
err = new Error('Could not read: ' + walletId);
} else {
err = e;
}
w = null;
}
return cb(err, w);
}
}
};
module.exports = Compatibility;

View file

@ -207,13 +207,20 @@ Identity.prototype.toObj = function() {
_.pick(this, 'version', 'fullName', 'password', 'email'));
};
Identity.prototype.exportWithWalletInfo = function() {
Identity.prototype.exportEncryptedWithWalletInfo = function(opts) {
var crypto = opts.cryptoUtil || cryptoUtil;
var key = crypto.kdf(this.password);
return crypto.encrypt(key, this.exportWithWalletInfo);
};
Identity.prototype.exportWithWalletInfo = function(opts) {
return _.extend({
wallets: _.map(this.wallets, function(wallet) {
return wallet.toObj();
})
},
_.pick(this, 'version', 'fullName', 'password', 'email'));
_.pick(this, 'version', 'fullName', 'password', 'email')
);
};
/**
@ -248,8 +255,8 @@ Identity.prototype.close = function(cb) {
};
/**
* @desc Imports a wallet from an encrypted base64 object
* @param {string} base64 - the base64 encoded object
* @desc Imports a wallet from an encrypted string
* @param {string} cypherText - the encrypted object
* @param {string} passphrase - passphrase to decrypt it
* @param {string[]} opts.skipFields - fields to ignore when importing
* @param {string[]} opts.salt -
@ -257,12 +264,25 @@ Identity.prototype.close = function(cb) {
* @param {string[]} opts.importFunction - for stubbing
* @return {Wallet}
*/
Identity.prototype.importWallet = function(base64, password, opts, cb) {
Identity.prototype.importEncryptedWallet = function(cypherText, password, opts, cb) {
var crypto = opts.cryptoUtil || cryptoUtil;
// TODO set iter and salt using config.js
var key = crypto.kdf(password);
var obj = crypto.decrypt(key, cypherText);
if (!obj) return cb(new Error('Could not decrypt'));
try {
obj = JSON.parse(obj);
} catch (e) {
return cb(new Error('Could not decrypt'));
}
return this.importWalletFromObj(obj, opts, cb)
};
Identity.prototype.importWalletFromObj = function(obj, opts, cb) {
var self = this;
preconditions.checkArgument(password);
preconditions.checkArgument(cb);
var importFunction = opts.importWallet || Wallet.fromUntrustedObj;
var crypto = opts.cryptoUtil || cryptoUtil;
var readOpts = {
networkOpts: this.networkOpts,
@ -270,12 +290,6 @@ Identity.prototype.importWallet = function(base64, password, opts, cb) {
skipFields: opts.skipFields,
};
// TODO set iter and salt using config.js
var key = crypto.kdf(password);
var obj = crypto.decrypt(key, base64);
if (!obj) return cb(new Error('Could not decrypt'));
var w = importFunction(obj, readOpts);
if (!w) return cb(new Error('Could not decrypt'));
@ -303,6 +317,12 @@ Identity.prototype.closeWallet = function(wallet, cb) {
});
};
Identity.importFromEncryptedFullJson = function(str, password, opts, cb) {
var crypto = opts.cryptoUtil || cryptoUtil;
var key = crypto.kdf(password);
return Identity.importFromFullJson(crypto.decript(key, str));
};
Identity.importFromFullJson = function(str, password, opts, cb) {
preconditions.checkArgument(str);
var json;
@ -320,7 +340,7 @@ Identity.importFromFullJson = function(str, password, opts, cb) {
json.wallets = json.wallets || {};
async.map(json.wallets, function(walletData, callback) {
iden.importWallet(wstr, password, opts, function(err, w) {
iden.importEncryptedWallet(wstr, password, opts, function(err, w) {
if (err) return callback(err);
log.debug('Wallet ' + w.getId() + ' imported');
callback();

View file

@ -1,82 +0,0 @@
'use strict';
// 65.7% typed (by google's closure-compiler account)
// this line throws a warning on Chrome Desktop
var sjcl = require('../../lib/sjcl');
var preconditions = require('preconditions').instance();
var _ = require('lodash');
/**
* @desc
* Class for a Passphrase object, uses PBKDF2 to expand a password
*
* @constructor
* @param {object} config
* @param {string=} config.salt - 'mjuBtGybi/4=' by default
* @param {number=} config.iterations - 1000 by default
*/
function Passphrase(config) {
preconditions.checkArgument(!config || !config.salt || _.isString(config.salt));
preconditions.checkArgument(!config || !config.iterations || _.isNumber(config.iterations));
config = config || {};
this.salt = config.salt || 'mjuBtGybi/4=';
this.iterations = config.iterations;
};
/**
* @desc Generate a WordArray expanding a password
*
* @param {string} password - the password to expand
* @returns WordArray 512 bits with the expanded key generated from password
*/
Passphrase.prototype.get = function(password) {
var hash = sjcl.hash.sha256.hash(sjcl.hash.sha256.hash(password));
var salt = sjcl.codec.base64.toBits(this.salt);
var crypto2 = function(key, salt, iterations, length, alg) {
return sjcl.codec.hex.fromBits(sjcl.misc.pbkdf2(key, salt, iterations, length * 8,
alg == 'sha1' ? function(key) {
return new sjcl.misc.hmac(key, sjcl.hash.sha1)
} : null
))
};
var key512 = crypto2(hash, salt, this.iterations, 64, 'sha1');
return key512;
};
/**
* @desc Generate a base64 encoded key
*
* @param {string} password - the password to expand
* @returns {string} 512 bits of a base64 encoded passphrase based on password
*/
Passphrase.prototype.getBase64 = function(password) {
var key512 = this.get(password);
var sbase64 = sjcl.codec.base64.fromBits(sjcl.codec.hex.toBits(key512));
return sbase64;
};
/**
* @desc Generate a base64 encoded key, without blocking
*
* @param {string} password - the password to expand
* @param {passphraseCallback} cb
*/
Passphrase.prototype.getBase64Async = function(password, cb) {
var self = this;
setTimeout(function() {
var ret = self.getBase64(password);
return cb(ret);
}, 0);
};
module.exports = Passphrase;

View file

@ -6,6 +6,7 @@ var preconditions = require('preconditions').singleton();
var inherits = require('inherits');
var events = require('events');
var async = require('async');
var cryptoUtil = require('../util/crypto');
var bitcore = require('bitcore');
var bignum = bitcore.Bignum;
@ -683,7 +684,6 @@ Wallet.prototype.getNetworkName = function() {
};
/**
* @desc
* @return {bool}
*/
Wallet.prototype.isTestnet = function() {
@ -2752,66 +2752,6 @@ Wallet.request = function(options, callback) {
return ret;
};
/*
* Old fns, only for compat
*
*/
Wallet.prototype.migrateWallet = function(walletId, passphrase, cb) {
var self = this;
self.storage.setPassphrase(passphrase);
self.read_Old(walletId, null, function(err, wallet) {
if (err) return cb(err);
// TODO
TODO(fix_this);
wallet.store(function(err) {
if (err) return cb(err);
self.storage.deleteWallet_Old(walletId, function(err) {
if (err) return cb(err);
self.storage.removeGlobal('nameFor::' + walletId, function() {
return cb();
});
});
});
});
};
Wallet.prototype.read_Old = function(walletId, skipFields, cb) {
var self = this,
err;
var obj = {};
this.storage.readWallet_Old(walletId, function(err, ret) {
if (err) return cb(err);
_.each(Wallet.PERSISTED_PROPERTIES, function(p) {
obj[p] = ret[p];
});
if (!_.any(_.values(obj)))
return cb(new Error('Wallet not found'));
var w, err;
obj.id = walletId;
try {
w = self.fromObj(obj, skipFields);
} catch (e) {
if (e && e.message && e.message.indexOf('MISSOPTS')) {
err = new Error('Could not read: ' + walletId);
} else {
err = e;
}
w = null;
}
return cb(err, w);
});
};
Wallet.prototype.getTransactionHistory = function(cb) {
var self = this;
@ -2924,4 +2864,11 @@ Wallet.prototype.getTransactionHistory = function(cb) {
}
};
Wallet.prototype.exportEncrypted = function(password, opts) {
opts = opts || {};
var crypto = opts.cryptoUtil || cryptoUtil;
var key = crypto.kdf(password);
return crypto.encrypt(key, this.toObj());
};
module.exports = Wallet;

View file

@ -8,7 +8,7 @@ function EncryptedInsightStorage(config) {
inherits(EncryptedInsightStorage, InsightStorage);
EncryptedInsightStorage.prototype.getItem = function(name, callback) {
var key = cryptoUtil.kdf(this.password, this.email);
var key = cryptoUtil.kdfbinary(this.password + this.email);
InsightStorage.prototype.getItem.apply(this, [name,
function(err, body) {
var decryptedJson = cryptoUtil.decrypt(key, body);
@ -21,13 +21,13 @@ EncryptedInsightStorage.prototype.getItem = function(name, callback) {
};
EncryptedInsightStorage.prototype.setItem = function(name, value, callback) {
var key = cryptoUtil.kdf(this.password, this.email);
var key = cryptoUtil.kdfbinary(this.password + this.email);
var record = cryptoUtil.encrypt(key, value);
InsightStorage.prototype.setItem.apply(this, [name, record, callback]);
};
EncryptedInsightStorage.prototype.removeItem = function(name, callback) {
var key = cryptoUtil.kdf(this.password, this.email);
var key = cryptoUtil.kdfbinary(this.password + this.email);
InsightStorage.prototype.removeItem.apply(this, [name, callback]);
};

View file

@ -8,7 +8,7 @@ function EncryptedLocalStorage(config) {
inherits(EncryptedLocalStorage, LocalStorage);
EncryptedLocalStorage.prototype.getItem = function(name, callback) {
var key = cryptoUtil.kdf(this.password, this.email);
var key = cryptoUtil.kdf(this.password + this.email);
LocalStorage.prototype.getItem.apply(this, [name, function(err, body) {
var decryptedJson = cryptoUtil.decrypt(key, body);
if (!decryptedJson) {
@ -19,7 +19,7 @@ EncryptedLocalStorage.prototype.getItem = function(name, callback) {
};
EncryptedLocalStorage.prototype.setItem = function(name, value, callback) {
var key = cryptoUtil.kdf(this.password, this.email);
var key = cryptoUtil.kdf(this.password + this.email);
if (!_.isString(value)) {
value = JSON.stringify(value);
}

View file

@ -17,7 +17,7 @@ InsightStorage.prototype.setCredentials = function(email, password, opts) {
};
InsightStorage.prototype.getItem = function(name, callback) {
var key = cryptoUtil.kdf(this.password, this.email);
var key = cryptoUtil.kdf(this.password + this.email);
var secret = cryptoUtil.kdf(key, this.password);
var encodedEmail = encodeURIComponent(this.email);
var retrieveUrl = this.storeUrl + '/retrieve/' + encodedEmail;
@ -35,7 +35,7 @@ InsightStorage.prototype.getItem = function(name, callback) {
};
InsightStorage.prototype.setItem = function(name, value, callback) {
var key = cryptoUtil.kdf(this.password, this.email);
var key = cryptoUtil.kdf(this.password + this.email);
var secret = cryptoUtil.kdf(key, this.password);
var registerUrl = this.storeUrl + '/register';
this.request.post({

View file

@ -1,7 +1,7 @@
'use strict';
var BackupService = function(notification) {
var BackupService = function($rootScope, notification) {
this.$rootScope = $rootScope;
this.notifications = notification;
};
@ -40,11 +40,11 @@ BackupService.prototype._download = function(ew, walletName, filename) {
};
BackupService.prototype.walletEncrypted = function(wallet) {
return wallet.toEncryptedObj();
return wallet.exportEncrypted(this.$rootScope.iden.password);
}
BackupService.prototype.walletDownload = function(wallet) {
var ew = wallet.toEncryptedObj();
var ew = this.walletEncrypted(wallet);
var walletName = wallet.getName();
var copayerName = this.getCopayer(wallet);
var filename = (copayerName ? copayerName + '-' : '') + walletName + '-keybackup.json.aes';
@ -52,17 +52,14 @@ BackupService.prototype.walletDownload = function(wallet) {
};
BackupService.prototype.profileEncrypted = function(iden) {
return iden.toEncryptedObj();
return iden.exportEncryptedWithWalletInfo(iden.password);
}
BackupService.prototype.profileDownload = function(iden) {
var ew = iden.toEncryptedObj();
var name = iden.profile.getName();
var ew = this.profileEncrypted(iden);
var name = iden.fullName;
var filename = name + '-profile.json';
this._download(ew, name, filename)
};
angular.module('copayApp.services').service('backupService', BackupService);

View file

@ -0,0 +1,5 @@
'use strict';
angular.module('copayApp.services').value('Compatibility', function() {
return require('copay').Compatibility;
});

View file

@ -1,4 +0,0 @@
'use strict';
angular.module('copayApp.services')
.value('Passphrase', new copay.Passphrase(config.passphraseConfig));

View file

@ -8,32 +8,39 @@ var _ = require('lodash');
var defaultSalt = 'mjuBtGybi/4=';
var defaultIterations = 100;
sjcl.defaults = {
v: 1,
iter: 100,
ks: 128,
ts: 64,
mode: "ccm",
adata: "",
cipher: "aes"
},
module.exports = {
kdf: function(value1, value2, salt, iterations) {
iterations = iterations || defaultIterations;
salt = salt || defaultSalt;
/**
* @param {string} password
* @param {string} salt - base64 encoded, defaults to 'mjuBtGybi/4='
* @param {number} iterations - defaults to 100
* @param {number} length - bits, defaults to 512 bits
* @returns {string} base64 encoded pbkdf2 derivation using sha1 for hmac
*/
kdf: function(password, salt, iterations, length) {
return sjcl.codec.base64.fromBits(
this.kdfbinary(password, salt, iterations, length)
);
},
/**
* @param {string} password
* @param {string} salt - base64 encoded, defaults to 'mjuBtGybi/4='
* @param {number} iterations - defaults to 100
* @param {number} length - bits, defaults to 512 bits
* @returns {string} base64 encoded pbkdf2 derivation using sha1 for hmac
*/
kdfbinary: function(password, salt, iterations, length) {
iterations = iterations || defaultIterations;
length = length || 512;
salt = sjcl.codec.base64.toBits(salt || defaultSalt);
var password = value1 + (value2 || '');
var hash = sjcl.hash.sha256.hash(sjcl.hash.sha256.hash(password));
var salt = sjcl.codec.base64.toBits(salt);
var prff = function(key) {
return new sjcl.misc.hmac(hash, sjcl.hash.sha1);
};
var bits = sjcl.misc.pbkdf2(hash, salt, iterations, 64 * 8, prff);
var base64 = sjcl.codec.base64.fromBits(bits);
return base64;
return sjcl.misc.pbkdf2(hash, salt, iterations, length, prff);
},
/**

40
test/Compatibility.js Normal file
View file

@ -0,0 +1,40 @@
var compat = require('../js/models/Compatibility');
describe('Compatibility', function() {
describe('#import', function() {
it('should not be able to decrypt with wrong password', function() {
var wo = compat.importLegacy(encryptedLegacy1, 'badpassword');
should.not.exist(wo);
});
it('should be able to decrypt an old backup', function() {
var wo = compat.importLegacy(encryptedLegacy1, legacyPassword1);
should.exist(wo);
console.log(wo);
wo.opts.id.should.equal('48ba2f1ffdfe9708');
wo.opts.spendUnconfirmed.should.equal(true);
wo.opts.requiredCopayers.should.equal(1);
wo.opts.totalCopayers.should.equal(1);
wo.opts.name.should.equal('pepe wallet');
wo.opts.version.should.equal('0.4.7');
wo.publicKeyRing.walletId.should.equal('48ba2f1ffdfe9708');
wo.publicKeyRing.networkName.should.equal('testnet');
wo.publicKeyRing.requiredCopayers.should.equal(1);
wo.publicKeyRing.totalCopayers.should.equal(1);
wo.publicKeyRing.indexes.length.should.equal(2);
JSON.stringify(wo.publicKeyRing.indexes[0]).should.equal('{"copayerIndex":2147483647,"changeIndex":0,"receiveIndex":1}');
JSON.stringify(wo.publicKeyRing.indexes[1]).should.equal('{"copayerIndex":0,"changeIndex":0,"receiveIndex":1}');
wo.publicKeyRing.copayersBackup.length.should.equal(1);
wo.publicKeyRing.copayersBackup[0].should.equal('0298f65b2694c55f9048bc05f10368242727c7f9d2065cbd788c3ecde1ec57f33f');
wo.publicKeyRing.copayersExtPubKeys.length.should.equal(1);
wo.publicKeyRing.copayersExtPubKeys[0].should.equal('tpubD9SGoP7CXsqSKTiQxCZSCpicDcophqnE4yuqjfw5M9tAR3fSjT9GDGwPEUFCN7SSmRKGDLZgKQePYFaLWyK32akeSan45TNTd8sgef9Ymh6');
wo.privateKey.extendedPrivateKeyString.should.equal('tprv8ZgxMBicQKsPfQCscb7CtJKzixxcVSyrCVcfr3WCFbtT8kYTzNubhjQ5R7AuYJgPCcSH4R8T34YVxeohKGhAB9wbB4eFBbQFjUpjGCqptHm');
wo.privateKey.networkName.should.equal('testnet');
});
});
});
var legacyPassword1 = '1DUpLRbuVpgLkcEY8gY8iod/SmA7+OheGZJ9PtvmTlvNE0FkEWpCKW9STdzXYJqbn0wiAapE4ojHNYj2hjYYAQ==';
var encryptedLegacy1 = 'U2FsdGVkX19yGM1uBAIzQa8Po/dvUicmxt1YyRk/S97PcZ6I6rHMp9dMagIrehg4Qd6JHn/ustmFHS7vmBYj0EBpf6rdXiQezaWnVAJS9/xYjAO36EFUbl+NmUanuwujAxgYdSP/sNssRLeInvExmZYW993EEclxkwL6YUyX66kKsxGQo2oWng0NreBJNhFmrbOEWeFje2PiWP57oUjKsurFzwpluAAarUTYSLud+nXeabC7opzOP5yqniWBMJz0Ou8gpNCWCMhG/P9F9ccVPY7juyd0Hf41FVse8nd2++axKB57+paozLdO+HRfV6zkMqC3h8gWY7LkS75j3bvqcTw9LhXmzE0Sz21n9yDnRpA4chiAvtwQvvBGgj1pFMKhNQU6Obac9ZwKYzUTgdDn3Uzg1UlDzgyOh9S89rbRTV84WB+hXwhuVluWzbNNYV3vXe5PFrocVktIrtS3xQh+k/7my4A6/gRRrzNYpKrUASJqDS/9u9WBkG35xD63J/qXjtG2M0YPwbI57BK1IK4K510b8V72lz5U2XQrIC4ldBwni1rpSavwCJV9xF6hUdOmNV8fZsVHP0NeN1PYlLkSb2QgfuoWnkcsJerwuFR7GZC/i6efrswtpO0wMEQr/J0CLbeXlHAru6xxjCBhWoJvZpMGw72zgnDLoyMNsEVglNhx/VlV9ZMYkkdaEYAxPOEIyZdQ5MS+2jEAlXf818n/xzJSVrniCn9be8EPePvkw35pivprvy09vbW4cKsWBKvgIyoT6A3OhUOCCS8E9cg0WAjjav2EymrbKmGWRHaiD+EoJqaDg6s20zhHn1YEa/YwvGGSB5+Hg8baLHD8ZASvxz4cFFAAVZrBUedRFgHzqwaMUlFXLgueivWUj7RXlIw6GuNhLoo1QkhZMacf23hrFxxQYvGBRw1hekBuDmcsGWljA28udBxBd5f9i+3gErttMLJ6IPaud590uvrxRIclu0Sz9R2EQX64YJxqDtLpMY0PjddSMu8vaDRpK9/ZSrnz/xrXsyabaafz4rE/ItFXjwFUFkvtmuauHTz6nmuKjVfxvNLNAiKb/gI7vQyUhnTbKIApe7XyJsjedNDtZqsPoJRIzdDmrZYxGStbAZ7HThqFJlSJ9NPNhH+E2jm3TwL5mwt0fFZ5h+p497lHMtIcKffESo7KNa2juSVNMDREk0NcyxGXGiVB2FWl4sLdvyhcsVq0I7tmW6OGZKRf8W49GCJXq6Ie69DJ9LB1DO67NV1jsYbsLx9uhE2yEmpWZ3jkoCV/Eas4grxt0CGN6EavzQ==';

View file

@ -249,7 +249,7 @@ describe('Identity model', function() {
var fakeCrypto = {
kdf: sinon.stub().returns('passphrase'),
decrypt: sinon.stub().returns({walletId:123}),
decrypt: sinon.stub().returns('{"walletId":123}'),
};
var opts = {
@ -258,7 +258,7 @@ describe('Identity model', function() {
};
Identity.create(args.params, function(err, iden) {
iden.importWallet(123,'password', opts, function(err){
iden.importEncryptedWallet(123,'password', opts, function(err){
should.not.exist(err);
fakeCrypto.kdf.getCall(0).args[0].should.equal('password');
fakeCrypto.decrypt.getCall(0).args[0].should.equal('passphrase');

View file

@ -1,30 +0,0 @@
'use strict';
var Passphrase = copay.Passphrase;
describe('Passphrase model', function() {
it('should create an instance', function() {
var p = new Passphrase();
should.exist(p);
});
it('should generate key from password', function(done) {
var p = new Passphrase({
salt: 'mjuBtGybi/4=',
iterations: 10,
});
var pass = '123456';
var k = p.get(pass);
var k64 = p.getBase64(pass);
// Note: hashes were generated using CryptoJS
k.toString().should.equal('2283fe11b9a189b82f1c09200806920cbdd8ef752f53dea910f90ab526f441acdbd5128555647a7e390a1a9fea042226963ccd0f7851030b3d6e282ccebaa17e');
k64.toString().should.equal('IoP+EbmhibgvHAkgCAaSDL3Y73UvU96pEPkKtSb0Qazb1RKFVWR6fjkKGp/qBCImljzND3hRAws9bigszrqhfg==');
p.getBase64Async(pass, function(ret) {
ret.toString().should.equal('IoP+EbmhibgvHAkgCAaSDL3Y73UvU96pEPkKtSb0Qazb1RKFVWR6fjkKGp/qBCImljzND3hRAws9bigszrqhfg==');
done();
});
});
});

View file

@ -1,6 +1,5 @@
'use strict';
var _ = require('lodash');
var chai = chai || require('chai');
var sinon = sinon || require('sinon');
@ -19,7 +18,7 @@ describe('cryptoUtil', function() {
};
var pass = '123456';
var phrase = crypto.kdf(pass, null, test.salt, test.iterations);
var phrase = crypto.kdf(pass, test.salt, test.iterations);
phrase.should.equal(test.phraseBase64);
});
@ -32,43 +31,11 @@ describe('cryptoUtil', function() {
phraseBase64: legacyPassphrase,
};
var phrase = crypto.kdf(legacyPassword, null, test.salt, test.iterations);
var phrase = crypto.kdf(legacyPassword, test.salt, test.iterations);
phrase.should.equal(test.phraseBase64);
});
it('should be able to decrypt an old backup',function(){
var wo = crypto.decrypt(legacyPassword, encryptedLegacy1);
console.log('[cryptoUtil.js.43:wo:]',wo); //TODO
should.exist(wo);
wo.opts.id.should.equal('48ba2f1ffdfe9708');
wo.opts.spendUnconfirmed.should.equal(true);
wo.opts.requiredCopayers.should.equal(1);
wo.opts.totalCopayers.should.equal(1);
wo.opts.name.should.equal('pepe wallet');
wo.opts.version.should.equal('0.4.7');
wo.publicKeyRing.walletId.should.equal('48ba2f1ffdfe9708');
wo.publicKeyRing.networkName.should.equal('testnet');
wo.publicKeyRing.requiredCopayers.should.equal(1);
wo.publicKeyRing.totalCopayers.should.equal(1);
wo.publicKeyRing.indexes.length.should.equal(2);
JSON.stringify(wo.publicKeyRing.indexes[0]).should.equal('{"copayerIndex":2147483647,"changeIndex":0,"receiveIndex":1}');
JSON.stringify(wo.publicKeyRing.indexes[1]).should.equal('{"copayerIndex":0,"changeIndex":0,"receiveIndex":1}');
wo.publicKeyRing.copayersBackup.length.should.equal(1);
wo.publicKeyRing.copayersBackup[0].should.equal('0298f65b2694c55f9048bc05f10368242727c7f9d2065cbd788c3ecde1ec57f33f');
wo.publicKeyRing.copayersExtPubKeys.length.should.equal(1);
wo.publicKeyRing.copayersExtPubKeys[0].should.equal('tpubD9SGoP7CXsqSKTiQxCZSCpicDcophqnE4yuqjfw5M9tAR3fSjT9GDGwPEUFCN7SSmRKGDLZgKQePYFaLWyK32akeSan45TNTd8sgef9Ymh6');
wo.privateKey.extendedPrivateKeyString.should.equal('tprv8ZgxMBicQKsPfQCscb7CtJKzixxcVSyrCVcfr3WCFbtT8kYTzNubhjQ5R7AuYJgPCcSH4R8T34YVxeohKGhAB9wbB4eFBbQFjUpjGCqptHm');
wo.privateKey.networkName.should.equal('testnet');
});
});
var legacyPassword = '1';
var legacyPassphrase = '1DUpLRbuVpgLkcEY8gY8iod/SmA7+OheGZJ9PtvmTlvNE0FkEWpCKW9STdzXYJqbn0wiAapE4ojHNYj2hjYYAQ==';
var encryptedLegacy1 = 'U2FsdGVkX19yGM1uBAIzQa8Po/dvUicmxt1YyRk/S97PcZ6I6rHMp9dMagIrehg4Qd6JHn/ustmFHS7vmBYj0EBpf6rdXiQezaWnVAJS9/xYjAO36EFUbl+NmUanuwujAxgYdSP/sNssRLeInvExmZYW993EEclxkwL6YUyX66kKsxGQo2oWng0NreBJNhFmrbOEWeFje2PiWP57oUjKsurFzwpluAAarUTYSLud+nXeabC7opzOP5yqniWBMJz0Ou8gpNCWCMhG/P9F9ccVPY7juyd0Hf41FVse8nd2++axKB57+paozLdO+HRfV6zkMqC3h8gWY7LkS75j3bvqcTw9LhXmzE0Sz21n9yDnRpA4chiAvtwQvvBGgj1pFMKhNQU6Obac9ZwKYzUTgdDn3Uzg1UlDzgyOh9S89rbRTV84WB+hXwhuVluWzbNNYV3vXe5PFrocVktIrtS3xQh+k/7my4A6/gRRrzNYpKrUASJqDS/9u9WBkG35xD63J/qXjtG2M0YPwbI57BK1IK4K510b8V72lz5U2XQrIC4ldBwni1rpSavwCJV9xF6hUdOmNV8fZsVHP0NeN1PYlLkSb2QgfuoWnkcsJerwuFR7GZC/i6efrswtpO0wMEQr/J0CLbeXlHAru6xxjCBhWoJvZpMGw72zgnDLoyMNsEVglNhx/VlV9ZMYkkdaEYAxPOEIyZdQ5MS+2jEAlXf818n/xzJSVrniCn9be8EPePvkw35pivprvy09vbW4cKsWBKvgIyoT6A3OhUOCCS8E9cg0WAjjav2EymrbKmGWRHaiD+EoJqaDg6s20zhHn1YEa/YwvGGSB5+Hg8baLHD8ZASvxz4cFFAAVZrBUedRFgHzqwaMUlFXLgueivWUj7RXlIw6GuNhLoo1QkhZMacf23hrFxxQYvGBRw1hekBuDmcsGWljA28udBxBd5f9i+3gErttMLJ6IPaud590uvrxRIclu0Sz9R2EQX64YJxqDtLpMY0PjddSMu8vaDRpK9/ZSrnz/xrXsyabaafz4rE/ItFXjwFUFkvtmuauHTz6nmuKjVfxvNLNAiKb/gI7vQyUhnTbKIApe7XyJsjedNDtZqsPoJRIzdDmrZYxGStbAZ7HThqFJlSJ9NPNhH+E2jm3TwL5mwt0fFZ5h+p497lHMtIcKffESo7KNa2juSVNMDREk0NcyxGXGiVB2FWl4sLdvyhcsVq0I7tmW6OGZKRf8W49GCJXq6Ie69DJ9LB1DO67NV1jsYbsLx9uhE2yEmpWZ3jkoCV/Eas4grxt0CGN6EavzQ==';

36
test/util.crypto.js Normal file
View file

@ -0,0 +1,36 @@
'use strict';
var cryptoUtils = require('../js/util/crypto');
var assert = require('assert');
describe('crypto utils', function() {
it('should use pbkdf2 to generate a passphrase from password', function() {
var salt = 'mjuBtGybi/4=';
var iterations = 10;
var pass = '123456';
var passphrase = cryptoUtils.kdf(pass, salt, iterations);
passphrase.toString().should.equal('IoP+EbmhibgvHAkgCAaSDL3Y73UvU96pEPkKtSb0Qazb1RKFVWR6fjkKGp/qBCImljzND3hRAws9bigszrqhfg==');
});
it('should decrypt what it encrypts', function() {
var key = 'My secret key';
var message = 'My secret message';
var encrypted = cryptoUtils.encrypt(key, message);
var decrypted = cryptoUtils.decrypt(key, encrypted);
decrypted.should.equal(message);
});
it('should return null if the provided key cant decrypt', function() {
var key = 'My secret key';
var message = 'My secret message';
var encrypted = cryptoUtils.encrypt(key, message);
var decrypted = cryptoUtils.decrypt('Invalid key', encrypted);
assert(decrypted === null);
});
});

View file

@ -69,15 +69,15 @@ var createBundle = function(opts) {
b.require('./js/models/Insight', {
expose: '../js/models/Insight'
});
b.require('./js/models/Compatibility', {
expose: '../js/models/Compatibility'
});
b.require('./js/models/PrivateKey', {
expose: '../js/models/PrivateKey'
});
b.require('./js/models/PublicKeyRing', {
expose: '../js/models/PublicKeyRing'
});
b.require('./js/models/Passphrase', {
expose: '../js/models/Passphrase'
});
b.require('./js/models/HDPath', {
expose: '../js/models/HDPath'
});

View file

@ -1,6 +1,6 @@
#!/bin/bash
cd ./lib/sjcl && \
./configure &&\
./configure --with-sha1 &&\
make && cp -v sjcl.js .. && echo "Done!" || echo " ## Please run $0 on copay root directory"