From a0019d966c8beed5cd7790819ec90e2573129572 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Sun, 1 Mar 2015 14:05:06 -0300 Subject: [PATCH] import & export with compress/encrypt --- lib/client/api.js | 84 ++++++++++++++++++++++++++++------ lib/walletutils.js | 34 -------------- test/integration/client.js | 94 ++++++++++++++++++++++++++++++-------- 3 files changed, 146 insertions(+), 66 deletions(-) diff --git a/lib/client/api.js b/lib/client/api.js index ac69f44..8f07729 100644 --- a/lib/client/api.js +++ b/lib/client/api.js @@ -9,6 +9,7 @@ var request = require('request') var events = require('events'); log.debug = log.verbose; var Bitcore = require('bitcore') +var sjcl = require('sjcl'); var Credentials = require('./credentials'); var WalletUtils = require('../walletutils'); @@ -17,6 +18,9 @@ var ServerCompromisedError = require('./servercompromisederror'); var ClientError = require('../clienterror'); var BASE_URL = 'http://localhost:3001/copay/api'; +var WALLET_ENCRYPTION_OPTS = { + iter: 5000 +}; function _encryptMessage(message, encryptingKey) { if (!message) return null; @@ -95,6 +99,73 @@ API.prototype.seedFromAirGapped = function(seed) { this.credentials = Credentials.fromExtendedPublicKey(seed.xPubKey, seed.requestPrivKey); }; +/** + * export + * + * @param opts + * @param opts.compressed + * @param opts.password + */ +API.prototype.export = function(opts) { + $.checkState(this.credentials); + + opts = opts || {}; + + var output; + if (opts.compressed) { + output = this.credentials.exportCompressed(); + } else { + output = JSON.stringify(this.credentials.toObj()); + } + + if (opts.password) { + output = sjcl.encrypt(opts.password, output, WALLET_ENCRYPTION_OPTS); + } + + return output; +} + + +/** + * export + * + * @param opts + * @param opts.compressed + * @param opts.password + */ +API.prototype.import = function(str, opts) { + opts = opts || {}; + + var input = str; + if (opts.password) { + try { + input = sjcl.decrypt(opts.password, input); + } catch (ex) { + throw new Error('Incorrect password'); + } + } + + try { + if (opts.compressed) { + this.credentials = Credentials.importCompressed(input); + // TODO: complete missing fields that live on the server only such as: walletId, walletName, copayerName + } else { + this.credentials = Credentials.fromObj(JSON.parse(input)); + } + } catch (ex) { + throw new Error('Error importing from source'); + } +}; + +API.prototype.toString = function(password) { + $.checkState(this.credentials); + return this.credentials.toObject(); +}; + +API.prototype.fromString = function(str) { + this.credentials = Credentials.fromObject(str); +}; + API.prototype._doRequest = function(method, url, args, cb) { $.checkState(this.credentials); @@ -340,19 +411,6 @@ API.prototype.getBalance = function(cb) { self._doGetRequest('/v1/balance/', cb); }; -/** - * Exports the wallet as it is now. - */ -API.prototype.export = function() { - $.checkState(this.credentials); - - return this.credentials.exportCompressed(); -} - - -API.prototype.import = function(str) { - this.credentials = Credentials.importCompressed(str); -}; /** * diff --git a/lib/walletutils.js b/lib/walletutils.js index 2dc853e..4958b9d 100644 --- a/lib/walletutils.js +++ b/lib/walletutils.js @@ -133,40 +133,6 @@ WalletUtils.privateKeyToAESKey = function(privKey) { return Bitcore.crypto.Hash.sha256(pk.toBuffer()).slice(0, 16).toString('base64'); }; -WalletUtils.decryptWallet = function(data, password) { - $.checkArgument(data.enc); - var extraFields = JSON.parse(sjcl.decrypt(password, data.enc)); - delete data.enc; - return _.extend(data, extraFields); -}; - - -WalletUtils.sjclOpts = { - iter: 5000, -}; - -WalletUtils.encryptWallet = function(data, accessWithoutEncrytion, password) { - - // Fields to encrypt, given the NOPASSWD access level - var fieldsEncryptByLevel = { - none: _.keys(data), - readonly: ['xPrivKey', 'requestPrivKey', 'publicKeyRing'], - readwrite: ['xPrivKey', ], - full: [], - }; - - var fieldsEncrypt = fieldsEncryptByLevel[accessWithoutEncrytion]; - $.checkState(!_.isUndefined(fieldsEncrypt)); - - var toEncrypt = _.pick(data, fieldsEncrypt); - var enc = sjcl.encrypt(password, JSON.stringify(toEncrypt), WalletUtils.sjclOpts); - - var ret = _.omit(data, fieldsEncrypt); - ret.enc = enc; - return ret; -}; - - WalletUtils.signTxp = function(txp, xPrivKey) { var self = this; diff --git a/test/integration/client.js b/test/integration/client.js index 63bc0fc..e923a27 100644 --- a/test/integration/client.js +++ b/test/integration/client.js @@ -865,29 +865,85 @@ describe('client API ', function() { }); describe('Export & Import', function() { - it('should export & import', function(done) { + var address, importedClient; + beforeEach(function(done) { + importedClient = null; helpers.createAndJoinWallet(clients, 1, 1, function() { - clients[0].createAddress(function(err, address) { + clients[0].createAddress(function(err, addr) { should.not.exist(err); - should.exist(address.address); - - var exported = clients[0].export(); - - var importedClient = new Client({ - request: helpers.getRequest(app), - }); - importedClient.import(exported); - - importedClient.getMainAddresses({}, function(err, list) { - should.not.exist(err); - should.exist(list); - list.length.should.equal(1); - list[0].address.should.equal(address.address); - done(); - }); + should.exist(addr.address); + address = addr.address; + done(); }); - }) + }); }); + afterEach(function(done) { + importedClient.getMainAddresses({}, function(err, list) { + should.not.exist(err); + should.exist(list); + list.length.should.equal(1); + list[0].address.should.equal(address); + done(); + }); + }); + + it('should export & import', function() { + var exported = clients[0].export(); + + importedClient = new Client({ + request: helpers.getRequest(app), + }); + importedClient.import(exported); + }); + it.skip('should export & import compressed', function() { + var walletId = clients[0].credentials.walletId; + var walletName = clients[0].credentials.walletName; + var copayerName = clients[0].credentials.copayerName; + + var exported = clients[0].export({ + compressed: true + }); + + importedClient = new Client({ + request: helpers.getRequest(app), + }); + importedClient.import(exported, { + compressed: true + }); + importedClient.credentials.walletId.should.equal(walletId); + importedClient.credentials.walletName.should.equal(walletName); + importedClient.credentials.copayerName.should.equal(copayerName); + }); + it('should export & import encrypted', function() { + var exported = clients[0].export({ + password: '123' + }); + + importedClient = new Client({ + request: helpers.getRequest(app), + }); + importedClient.import(exported, { + password: '123' + }); + }); + it('should export & import compressed & encrypted', function() { + var exported = clients[0].export({ + compressed: true, + password: '123' + }); + + importedClient = new Client({ + request: helpers.getRequest(app), + }); + importedClient.import(exported, { + compressed: true, + password: '123' + }); + }); + it.skip('should fail to export compressed & import uncompressed', function() {}); + it.skip('should fail to export uncompressed & import compressed', function() {}); + it.skip('should fail to export unencrypted & import with password', function() {}); + it.skip('should fail to export encrypted & import with incorrect password', function() {}); }); describe('Air gapped related flows', function() {