Skip to content

Commit

Permalink
Merge pull request bitpay#230 from isocolsky/restore-legacy
Browse files Browse the repository at this point in the history
Restore import of legacy backups
  • Loading branch information
matiu committed Apr 5, 2016
2 parents 4c707f2 + ea7214a commit 5437f2e
Show file tree
Hide file tree
Showing 3 changed files with 322 additions and 1 deletion.
64 changes: 64 additions & 0 deletions lib/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -2148,4 +2148,68 @@ API.prototype.getSendMaxInfo = function(opts, cb) {
});
};


/*
*
* Compatibility Functions
*
*/

API.prototype._oldCopayDecrypt = function(username, password, blob) {
var SEP1 = '@#$';
var SEP2 = '%^#@';

var decrypted;
try {
var passphrase = username + SEP1 + password;
decrypted = sjcl.decrypt(passphrase, blob);
} catch (e) {
passphrase = username + SEP2 + password;
try {
decrypted = sjcl.decrypt(passphrase, blob);
} catch (e) {
log.debug(e);
};
}

if (!decrypted)
return null;

var ret;
try {
ret = JSON.parse(decrypted);
} catch (e) {};
return ret;
};


API.prototype.getWalletIdsFromOldCopay = function(username, password, blob) {
var p = this._oldCopayDecrypt(username, password, blob);
if (!p) return null;
var ids = p.walletIds.concat(_.keys(p.focusedTimestamps));
return _.uniq(ids);
};


/**
* createWalletFromOldCopay
*
* @param username
* @param password
* @param blob
* @param cb
* @return {undefined}
*/
API.prototype.createWalletFromOldCopay = function(username, password, blob, cb) {
var self = this;
var w = this._oldCopayDecrypt(username, password, blob);
if (!w) return cb(new Error('Could not decrypt'));

if (w.publicKeyRing.copayersExtPubKeys.length != w.opts.totalCopayers)
return cb(new Error('Wallet is incomplete, cannot be imported'));

this.credentials = Credentials.fromOldCopayWallet(w);
this.recreateWallet(cb);
};

module.exports = API;
53 changes: 53 additions & 0 deletions lib/credentials.js
Original file line number Diff line number Diff line change
Expand Up @@ -383,4 +383,57 @@ Credentials.prototype.clearMnemonic = function() {
delete this.mnemonicEncrypted;
};


Credentials.fromOldCopayWallet = function(w) {
function walletPrivKeyFromOldCopayWallet(w) {
// IN BWS, the master Pub Keys are not sent to the server,
// so it is safe to use them as seed for wallet's shared secret.
var seed = w.publicKeyRing.copayersExtPubKeys.sort().join('');
var seedBuf = new Buffer(seed);
var privKey = new Bitcore.PrivateKey.fromBuffer(Bitcore.crypto.Hash.sha256(seedBuf));
return privKey.toString();
};

var credentials = new Credentials();
credentials.derivationStrategy = Constants.DERIVATION_STRATEGIES.BIP45;
credentials.xPrivKey = w.privateKey.extendedPrivateKeyString;
credentials._expand();

credentials.addWalletPrivateKey(walletPrivKeyFromOldCopayWallet(w));
credentials.addWalletInfo(w.opts.id, w.opts.name, w.opts.requiredCopayers, w.opts.totalCopayers)

var pkr = _.map(w.publicKeyRing.copayersExtPubKeys, function(xPubStr) {

var isMe = xPubStr === credentials.xPubKey;
var requestDerivation;

if (isMe) {
var path = Constants.PATHS.REQUEST_KEY;
requestDerivation = (new Bitcore.HDPrivateKey(credentials.xPrivKey))
.derive(path).hdPublicKey;
} else {
// this
var path = Constants.PATHS.REQUEST_KEY_AUTH;
requestDerivation = (new Bitcore.HDPublicKey(xPubStr)).derive(path);
}

// Grab Copayer Name
var hd = new Bitcore.HDPublicKey(xPubStr).derive('m/2147483646/0/0');
var pubKey = hd.publicKey.toString('hex');
var copayerName = w.publicKeyRing.nicknameFor[pubKey];
if (isMe) {
credentials.copayerName = copayerName;
}

return {
xPubKey: xPubStr,
requestPubKey: requestDerivation.publicKey.toString(),
copayerName: copayerName,
};
});
credentials.addPublicKeyRing(pkr);
return credentials;
};


module.exports = Credentials;
206 changes: 205 additions & 1 deletion test/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -2892,7 +2892,7 @@ describe('client API', function() {
});
});

it.skip('Send, reject actions in 2-3 wallet much have correct copayerNames', function(done) {
it.skip('Send, reject actions in 2-3 wallet must have correct copayerNames', function(done) {
helpers.createAndJoinWallet(clients, 2, 3, function(w) {
clients[0].createAddress(function(err, x0) {
should.not.exist(err);
Expand Down Expand Up @@ -3863,6 +3863,210 @@ describe('client API', function() {
});
});

describe('Legacy Copay Import', function() {
it('Should get wallets from profile', function(done) {
var t = ImportData.copayers[0];
var c = helpers.newClient(app);
var ids = c.getWalletIdsFromOldCopay(t.username, t.password, t.ls['profile::4872dd8b2ceaa54f922e8e6ba6a8eaa77b488721']);
ids.should.deep.equal([
'8f197244e661f4d0',
'4d32f0737a05f072',
'e2c2d72024979ded',
'7065a73486c8cb5d'
]);
done();
});
it('Should import a 1-1 wallet', function(done) {
var t = ImportData.copayers[0];
var c = helpers.newClient(app);
c.createWalletFromOldCopay(t.username, t.password, t.ls['wallet::e2c2d72024979ded'], function(err) {
should.not.exist(err);
c.credentials.m.should.equal(1);
c.credentials.n.should.equal(1);

c.createAddress(function(err, x0) {
// This is the first 'shared' address, created automatically
// by old copay
x0.address.should.equal('2N3w8sJUyAXCQirqNsTayWr7pWADFNdncmf');
c.getStatus({}, function(err, status) {
should.not.exist(err);
status.wallet.name.should.equal('1-1');
status.wallet.status.should.equal('complete');
c.credentials.walletId.should.equal('e2c2d72024979ded');
c.credentials.walletPrivKey.should.equal('c3463113c6e1d0fc2f2bd520f7d9d62f8e1fdcdd96005254571c64902aeb1648');
c.credentials.sharedEncryptingKey.should.equal('x3D/7QHa4PkKMbSXEvXwaw==');
status.wallet.copayers.length.should.equal(1);
status.wallet.copayers[0].name.should.equal('123');
done();
});
});
});
});

it('Should to import the same wallet twice with different clients', function(done) {
var t = ImportData.copayers[0];
var c = helpers.newClient(app);
c.createWalletFromOldCopay(t.username, t.password, t.ls['wallet::4d32f0737a05f072'], function(err) {
should.not.exist(err);
c.getStatus({}, function(err, status) {
should.not.exist(err);
status.wallet.status.should.equal('complete');
c.credentials.walletId.should.equal('4d32f0737a05f072');
var c2 = helpers.newClient(app);
c2.createWalletFromOldCopay(t.username, t.password, t.ls['wallet::4d32f0737a05f072'], function(err) {
should.not.exist(err);
c2.getStatus({}, function(err, status) {
should.not.exist(err);
status.wallet.status.should.equal('complete');
c2.credentials.walletId.should.equal('4d32f0737a05f072');
done();
});
});
});
});
});

it('Should not fail when importing the same wallet twice, same copayer', function(done) {
var t = ImportData.copayers[0];
var c = helpers.newClient(app);
c.createWalletFromOldCopay(t.username, t.password, t.ls['wallet::4d32f0737a05f072'], function(err) {
should.not.exist(err);
c.getStatus({}, function(err, status) {
should.not.exist(err);
status.wallet.status.should.equal('complete');
c.credentials.walletId.should.equal('4d32f0737a05f072');
c.createWalletFromOldCopay(t.username, t.password, t.ls['wallet::4d32f0737a05f072'], function(err) {
should.not.exist(err);
done();
});
});
});
});

it('Should import and complete 2-2 wallet from 2 copayers, and create addresses', function(done) {
var t = ImportData.copayers[0];
var c = helpers.newClient(app);
c.createWalletFromOldCopay(t.username, t.password, t.ls['wallet::4d32f0737a05f072'], function(err) {
should.not.exist(err);
c.getStatus({}, function(err, status) {
should.not.exist(err);
status.wallet.status.should.equal('complete');
c.credentials.sharedEncryptingKey.should.equal('Ou2j4kq3z1w4yTr9YybVxg==');

var t2 = ImportData.copayers[1];
var c2 = helpers.newClient(app);
c2.createWalletFromOldCopay(t2.username, t2.password, t2.ls['wallet::4d32f0737a05f072'], function(err) {
should.not.exist(err);
c2.credentials.sharedEncryptingKey.should.equal('Ou2j4kq3z1w4yTr9YybVxg==');

// This should pull the non-temporary keys
c2.getStatus({}, function(err, status) {
should.not.exist(err);
status.wallet.status.should.equal('complete');
c2.createAddress(function(err, x0) {
x0.address.should.be.equal('2Mv1DHpozzZ9fup2nZ1kmdRXoNnDJ8b1JF2');
c.createAddress(function(err, x0) {
x0.address.should.be.equal('2N2dZ1HogpxHVKv3CD2R4WrhWRwqZtpDc2M');
done();
});
});
});
});
});
});
});

it('Should import and complete 2-3 wallet from 2 copayers, and create addresses', function(done) {
var w = 'wallet::7065a73486c8cb5d';
var key = 'fS4HhoRd25KJY4VpNpO1jg==';
var t = ImportData.copayers[0];
var c = helpers.newClient(app);
c.createWalletFromOldCopay(t.username, t.password, t.ls[w], function(err) {
should.not.exist(err);
c.getStatus({}, function(err, status) {
should.not.exist(err);
status.wallet.status.should.equal('complete');
c.credentials.sharedEncryptingKey.should.equal(key);

var t2 = ImportData.copayers[1];
var c2 = helpers.newClient(app);
c2.createWalletFromOldCopay(t2.username, t2.password, t2.ls[w], function(err) {
should.not.exist(err);
c2.credentials.sharedEncryptingKey.should.equal(key);

c2.getStatus({}, function(err, status) {
should.not.exist(err);
status.wallet.status.should.equal('complete');

var t3 = ImportData.copayers[2];
var c3 = helpers.newClient(app);
c3.createWalletFromOldCopay(t3.username, t3.password, t3.ls[w], function(err) {
should.not.exist(err);
c3.credentials.sharedEncryptingKey.should.equal(key);

// This should pull the non-temporary keys
c3.getStatus({}, function(err, status) {
should.not.exist(err);
status.wallet.status.should.equal('complete');
done();
});
});
});
});
});
});
});

it('Should import a 2-3 wallet from 2 copayers, and recreate it, and then on the recreated other copayers should be able to access', function(done) {
var w = 'wallet::7065a73486c8cb5d';
var key = 'fS4HhoRd25KJY4VpNpO1jg==';
var t = ImportData.copayers[0];
var c = helpers.newClient(app);
c.createWalletFromOldCopay(t.username, t.password, t.ls[w], function(err) {
should.not.exist(err);
var t2 = ImportData.copayers[1];
var c2 = helpers.newClient(app);
c2.createWalletFromOldCopay(t2.username, t2.password, t2.ls[w], function(err) {
should.not.exist(err);

// New BWS server...
var storage = new Storage({
db: helpers.newDb(),
});
var newApp;
var expressApp = new ExpressApp();
expressApp.start({
storage: storage,
blockchainExplorer: blockchainExplorerMock,
disableLogs: true,
},
function() {
newApp = expressApp.app;
});
var recoveryClient = helpers.newClient(newApp);
recoveryClient.import(c.export());
recoveryClient.recreateWallet(function(err) {
should.not.exist(err);
recoveryClient.getStatus({}, function(err, status) {
should.not.exist(err);
_.pluck(status.wallet.copayers, 'name').sort().should.deep.equal(['123', '234', '345']);
var t2 = ImportData.copayers[1];
var c2p = helpers.newClient(newApp);
c2p.createWalletFromOldCopay(t2.username, t2.password, t2.ls[w], function(err) {
should.not.exist(err);
c2p.getStatus({}, function(err, status) {
should.not.exist(err);
_.pluck(status.wallet.copayers, 'name').sort().should.deep.equal(['123', '234', '345']);
done();
});
});
});
});
});
});
});
});

describe('Private key encryption', function() {
var password = 'jesuissatoshi';
var c1, c2;
Expand Down

0 comments on commit 5437f2e

Please sign in to comment.