Create a Compatibility namespace

This commit is contained in:
Esteban Ordano 2014-10-27 22:15:23 -03:00
parent 202a047edc
commit 1b0f6836dc
14 changed files with 262 additions and 280 deletions

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,7 +1,7 @@
'use strict';
angular.module('copayApp.controllers').controller('ImportController',
function($scope, $rootScope, $location, controllerUtils, Passphrase, notification, isMobile, Compatibility) {
function($scope, $rootScope, $location, controllerUtils, notification, isMobile, Compatibility) {
$rootScope.title = 'Import a backup';
$scope.importStatus = 'Importing wallet - Reading backup...';

View File

@ -1,94 +1,173 @@
'use strict';
var Identity = require('Identity'),
Passphrase = require('Passphrase'),
Wallet = require('Wallet'),
// walletFactory = new WalletFactory(),
passphrase = new Passphrase();
function Compatibility(){
// - preDotEightListWallets()
// - preDotEightImportWalletToStorage(walletId, passphrase, profile) (edited)
}
var Identity = require('./Identity');
var Wallet = require('./Wallet');
var cryptoUtils = require('../util/crypto');
var CryptoJS = require('node-cryptojs-aes').CryptoJS;
Compatibility.prototype.preDotEightListWallets = function () {};
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];
Compatibility.prototype.preDotEightImportWalletToStorage = function(encryptedObj, password, skipPublicKeyRing, skipTxProposals) {
passphrase.getBase64Async(password, function(passphrase) {
// updateStatus('Importing wallet - Setting things up...');
var w, errMsg;
var skipFields = [];
if (skipPublicKeyRing)
skipFields.push('publicKeyRing');
if (skipTxProposals)
skipFields.push('txProposals');
// try to import encrypted wallet with passphrase
try {
w = walletFactory.import(encryptedObj, passphrase, skipFields);
} catch (e) {
errMsg = e.message;
}
if (!w) {
// $scope.loading = false;
// notification.error('Error', errMsg || 'Wrong password');
$rootScope.$digest();
return;
}
// if wallet was never used, we're done
if (!w.isReady()) {
$rootScope.wallet = w;
// controllerUtils.startNetwork($rootScope.wallet, $scope);
return;
}
// if it was used, we need to scan for indices
w.updateIndexes(function(err) {
// updateStatus('Importing wallet - We are almost there...');
if (err) {
// $scope.loading = false;
// notification.error('Error', 'Error updating indexes: ' + err);
if (!walletId
|| walletId === 'nameFor'
|| walletId === 'lock'
|| walletId === 'wallet') {
continue;
}
if (typeof uniq[walletId] === 'undefined') {
walletIds.push(walletId);
uniq[walletId] = 1;
}
}
}
return cb(walletIds);
};
/**
* 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,
});
}
$rootScope.wallet = w;
// controllerUtils.startNetwork($rootScope.wallet, $scope);
});
return cb(wallets);
});
};
Compatibility.prototype.fromEncryptedObj = function(base64, passphrase, skipFields) {
this.storage.setPassphrase(passphrase);
var walletObj = this.storage.import(base64);
if (!walletObj) return false;
return this.fromObj(walletObj, skipFields);
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);
};
Compatibility.prototype.fromObj = function(inObj, skipFields) {
var networkName = this.obtainNetworkName(inObj);
preconditions.checkState(networkName);
preconditions.checkArgument(inObj);
/**
* 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);
});
})
};
var obj = JSON.parse(JSON.stringify(inObj));
/**
* 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 = {};
// not stored options
obj.opts = obj.opts || {};
obj.opts.reconnectDelay = this.walletDefaults.reconnectDelay;
for (key in localStorage) {
if (key.indexOf('wallet::' + walletId) !== -1) {
var ret = self._read(localStorage.getItem(key), passphrase);
if (err) return cb(err);
skipFields = skipFields || [];
skipFields.forEach(function(k) {
if (obj[k]) {
delete obj[k];
} else
throw new Error('unknown field:' + k);
});
_.each(Wallet.PERSISTED_PROPERTIES, function(p) {
obj[p] = ret[p];
});
var w = Wallet.fromObj(obj, this.storage, this.networks[networkName], this.blockchains[networkName]);
if (!w) return false;
this._checkVersion(w.version);
return w;
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

@ -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

@ -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;

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.kdf(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.kdf(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.kdf(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

@ -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 Compatibility = require('../js/models/Compatibility');
describe('Compatibility', function() {
var compat = new Compatibility();
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);
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

@ -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();
});
});
});

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

@ -78,9 +78,6 @@ var createBundle = function(opts) {
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"