From 57bc9bf4b07244306bab83674924e273c83b8544 Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Sun, 2 Oct 2016 18:57:58 -0700 Subject: [PATCH] wallet: better encrypted for imported keys. --- lib/wallet/wallet.js | 121 +++++++++++++++++++++++++++++++++-------- lib/wallet/walletdb.js | 50 +++++++++++++++++ test/wallet-test.js | 44 ++++++++++++++- 3 files changed, 191 insertions(+), 24 deletions(-) diff --git a/lib/wallet/wallet.js b/lib/wallet/wallet.js index f49a0fa4..42de1cff 100644 --- a/lib/wallet/wallet.js +++ b/lib/wallet/wallet.js @@ -339,35 +339,104 @@ Wallet.prototype._removeKey = co(function* removeKey(account, key) { */ Wallet.prototype.setPassphrase = co(function* setPassphrase(old, new_) { - var unlock = yield this.writeLock.lock(); - try { - return yield this._setPassphrase(old, new_); - } finally { - unlock(); - } -}); - -/** - * Change or set master key's passphrase without a lock. - * @private - * @param {(String|Buffer)?} old - * @param {String|Buffer} new_ - * @returns {Promise} - */ - -Wallet.prototype._setPassphrase = co(function* setPassphrase(old, new_) { if (new_ == null) { new_ = old; old = null; } if (old != null) - yield this.master.decrypt(old); + yield this.decrypt(old); if (new_ != null) - yield this.master.encrypt(new_); + yield this.encrypt(new_); +}); + +/** + * Encrypt the wallet permanently. + * @param {String|Buffer} passphrase + * @returns {Promise} + */ + +Wallet.prototype.encrypt = co(function* encrypt(passphrase) { + var unlock = yield this.writeLock.lock(); + try { + return yield this._encrypt(passphrase); + } finally { + unlock(); + } +}); + +/** + * Encrypt the wallet permanently, without a lock. + * @private + * @param {String|Buffer} passphrase + * @returns {Promise} + */ + +Wallet.prototype._encrypt = co(function* encrypt(passphrase) { + var key; + + if (this.master.encrypted) + throw new Error('Wallet is already encrypted.'); this.start(); + + try { + key = yield this.master.encrypt(passphrase); + yield this.db.encryptKeys(this.wid, key); + } catch (e) { + this.drop(); + throw e; + } + + key.fill(0); + + this.save(); + + yield this.commit(); +}); + + +/** + * Decrypt the wallet permanently. + * @param {String|Buffer} passphrase + * @returns {Promise} + */ + +Wallet.prototype.decrypt = co(function* decrypt(passphrase) { + var unlock = yield this.writeLock.lock(); + try { + return yield this._decrypt(passphrase); + } finally { + unlock(); + } +}); + +/** + * Decrypt the wallet permanently, without a lock. + * @private + * @param {String|Buffer} passphrase + * @returns {Promise} + */ + +Wallet.prototype._decrypt = co(function* decrypt(passphrase) { + var key; + + if (!this.master.encrypted) + throw new Error('Wallet is not encrypted.'); + + this.start(); + + try { + key = yield this.master.decrypt(passphrase); + yield this.db.decryptKeys(this.wid, key); + } catch (e) { + this.drop(); + throw e; + } + + key.fill(0); + this.save(); yield this.commit(); @@ -2328,7 +2397,7 @@ MasterKey.prototype.decrypt = co(function* decrypt(passphrase) { */ MasterKey.prototype._decrypt = co(function* decrypt(passphrase) { - var data; + var key, data; if (!this.encrypted) { assert(this.key); @@ -2340,12 +2409,15 @@ MasterKey.prototype._decrypt = co(function* decrypt(passphrase) { this.destroy(); - data = yield crypto.decrypt(this.ciphertext, passphrase, this.iv); + key = yield crypto.derive(passphrase); + data = crypto.decipher(this.ciphertext, key, this.iv); this.key = HD.fromExtended(data); this.encrypted = false; this.iv = null; this.ciphertext = null; + + return key; }); /** @@ -2371,7 +2443,7 @@ MasterKey.prototype.encrypt = co(function* encrypt(passphrase) { */ MasterKey.prototype._encrypt = co(function* encrypt(passphrase) { - var data, iv; + var key, data, iv; if (this.encrypted) return; @@ -2384,12 +2456,15 @@ MasterKey.prototype._encrypt = co(function* encrypt(passphrase) { this.stop(); - data = yield crypto.encrypt(data, passphrase, iv); + key = yield crypto.derive(passphrase); + data = crypto.encipher(data, key, iv); this.key = null; this.encrypted = true; this.iv = iv; this.ciphertext = data; + + return key; }); /** diff --git a/lib/wallet/walletdb.js b/lib/wallet/walletdb.js index cb5ba1db..c06fcbb1 100644 --- a/lib/wallet/walletdb.js +++ b/lib/wallet/walletdb.js @@ -1149,6 +1149,56 @@ WalletDB.prototype.getWallets = function getWallets() { }); }; +/** + * Encrypt all imported keys for a wallet. + * @param {WalletID} wid + * @returns {Promise} + */ + +WalletDB.prototype.encryptKeys = co(function* encryptKeys(wid, key) { + var paths = yield this.getWalletPaths(wid); + var batch = this.batch(wid); + var i, path, iv; + + for (i = 0; i < paths.length; i++) { + path = paths[i]; + + if (path.data && !path.encrypted) { + iv = new Buffer(path.hash, 'hex'); + iv = iv.slice(0, 16); + path.data = crypto.encipher(path.data, key, iv); + path.encrypted = true; + this.pathCache.set(wid + path.hash, path); + batch.put(layout.P(wid, path.hash), path.toRaw()); + } + } +}); + +/** + * Decrypt all imported keys for a wallet. + * @param {WalletID} wid + * @returns {Promise} + */ + +WalletDB.prototype.decryptKeys = co(function* decryptKeys(wid, key) { + var paths = yield this.getWalletPaths(wid); + var batch = this.batch(wid); + var i, path, iv; + + for (i = 0; i < paths.length; i++) { + path = paths[i]; + + if (path.data && path.encrypted) { + iv = new Buffer(path.hash, 'hex'); + iv = iv.slice(0, 16); + path.data = crypto.decipher(path.data, key, iv); + path.encrypted = false; + this.pathCache.set(wid + path.hash, path); + batch.put(layout.P(wid, path.hash), path.toRaw()); + } + } +}); + /** * Rescan the blockchain. * @param {ChainDB} chaindb diff --git a/test/wallet-test.js b/test/wallet-test.js index 2d7fde29..659ac0a6 100644 --- a/test/wallet-test.js +++ b/test/wallet-test.js @@ -41,7 +41,7 @@ var dummyInput = { }; describe('Wallet', function() { - var walletdb, wallet, doubleSpendWallet, doubleSpend; + var walletdb, wallet, ewallet, ekey, doubleSpendWallet, doubleSpend; walletdb = new bcoin.walletdb({ name: 'wallet-test', @@ -938,6 +938,9 @@ describe('Wallet', function() { yield w.sign(t2); assert(t2.verify()); assert(t2.inputs[0].prevout.hash === tx.hash('hex')); + + ewallet = w; + ekey = key; })); it('should import address', cob(function *() { @@ -967,6 +970,45 @@ describe('Wallet', function() { assert.equal(details[0].toJSON().id, 'test'); })); + it('should handle changed passphrase with encrypted imports', cob(function *() { + var w = ewallet; + var addr = ekey.getAddress(); + var path, d1, d2, k; + + assert(w.master.encrypted); + + path = yield w.getPath(addr); + assert(path); + assert(path.data && path.encrypted); + d1 = path.data; + + yield w.decrypt('test'); + + path = yield w.getPath(addr); + assert(path); + assert(path.data && !path.encrypted); + + k = yield w.getKey(addr); + assert(k); + + yield w.encrypt('foo'); + + path = yield w.getPath(addr); + assert(path); + assert(path.data && path.encrypted); + d2 = path.data; + + assert(!utils.equal(d1, d2)); + + k = yield w.getKey(addr); + assert(!k); + + yield w.unlock('foo'); + k = yield w.getKey(addr); + assert(k); + assert.equal(k.getHash('hex'), addr.getHash('hex')); + })); + it('should cleanup', cob(function *() { var records = yield walletdb.dump(); constants.tx.COINBASE_MATURITY = 100;