diff --git a/lib/bcoin/env.js b/lib/bcoin/env.js index e50147d4..58fae1d7 100644 --- a/lib/bcoin/env.js +++ b/lib/bcoin/env.js @@ -453,7 +453,7 @@ function normalize(path, dirname) { */ function mkdirp(path) { - var i, parts; + var i, parts, stat; if (!fs) return; @@ -477,7 +477,9 @@ function mkdirp(path) { path += parts[i]; try { - fs.statSync(path); + stat = fs.statSync(path); + if (!stat.isDirectory()) + throw new Error('Could not create directory.'); } catch (e) { if (e.code === 'ENOENT') fs.mkdirSync(path, 488 /* 0750 */); diff --git a/lib/bcoin/hd.js b/lib/bcoin/hd.js index 734310ad..006203cf 100644 --- a/lib/bcoin/hd.js +++ b/lib/bcoin/hd.js @@ -144,8 +144,7 @@ Mnemonic.prototype.toSeed = function toSeed() { this.seed = utils.pbkdf2( nfkd(this.phrase), nfkd('mnemonic' + this.passphrase), - 2048, - 64); + 2048, 64, 'sha512'); return this.seed; }; @@ -304,12 +303,13 @@ HD.fromMnemonic = function fromMnemonic(options, network) { /** * Instantiate an HD key from a jsonified key object. * @param {Object} json - The jsonified transaction object. - * @param {String?} passphrase * @returns {HDPrivateKey|HDPublicKey} */ -HD.fromJSON = function fromJSON(json, passphrase) { - return HDPrivateKey.fromJSON(json, passphrase); +HD.fromJSON = function fromJSON(json) { + if (json.xprivkey) + return HDPrivateKey.fromJSON(json); + return HDPublicKey.fromJSON(json); }; /** @@ -995,29 +995,20 @@ HDPrivateKey.fromRaw = function fromRaw(raw) { /** * Convert key to a more json-friendly object. - * @param {String?} passphrase - Address passphrase * @returns {Object} */ -HDPrivateKey.prototype.toJSON = function toJSON(passphrase) { +HDPrivateKey.prototype.toJSON = function toJSON() { var json = { - network: this.network, - encrypted: false + network: this.network }; if (this instanceof HDPrivateKey) { - json.encrypted = passphrase ? true : false; if (this.mnemonic) { - json.phrase = passphrase - ? utils.encrypt(this.mnemonic.phrase, passphrase).toString('hex') - : this.mnemonic.phrase; - json.passphrase = passphrase - ? utils.encrypt(this.mnemonic.passphrase, passphrase).toString('hex') - : this.mnemonic.passphrase; + json.phrase = this.mnemonic.phrase; + json.passphrase = this.mnemonic.passphrase; } - json.xprivkey = passphrase - ? utils.encrypt(this.xprivkey, passphrase).toString('hex') - : this.xprivkey; + json.xprivkey = this.xprivkey; return json; } @@ -1032,66 +1023,35 @@ HDPrivateKey.prototype.toJSON = function toJSON(passphrase) { * @returns {Object} A "naked" HDPrivateKey. */ -HDPrivateKey.parseJSON = function parseJSON(json, passphrase) { +HDPrivateKey.parseJSON = function parseJSON(json) { var data = {}; - if (json.encrypted && !passphrase) - throw new Error('Cannot decrypt address'); - if (json.phrase) { data.mnemonic = { - phrase: json.encrypted - ? utils.decrypt(json.phrase, passphrase).toString('utf8') - : json.phrase, - passphrase: json.encrypted - ? utils.decrypt(json.passphrase, passphrase).toString('utf8') - : json.passphrase - }; - if (!json.xprivkey) - return data; - } - - if (json.xprivkey) { - data.xprivkey = json.encrypted - ? utils.decrypt(json.xprivkey, passphrase).toString('utf8') - : json.xprivkey; - return data; - } - - if (json.xpubkey) { - return { - xpubkey: json.xpubkey + phrase: json.phrase, + passphrase: json.passphrase }; } - assert(false); + assert(json.xprivkey, 'Could not handle key JSON.'); + + data.xprivkey = json.xprivkey; + + return data; }; /** * Instantiate an HDPrivateKey from a jsonified key object. * @param {Object} json - The jsonified transaction object. - * @param {String?} passphrase * @returns {HDPrivateKey} */ -HDPrivateKey.fromJSON = function fromJSON(json, passphrase) { +HDPrivateKey.fromJSON = function fromJSON(json) { var key; - - json = HDPrivateKey.parseJSON(json, passphrase); - - if (json.xprivkey) { - key = HDPrivateKey.fromBase58(json.xprivkey); - key.mnemonic = json.mnemonic ? new Mnemonic(json.mnemonic) : null; - return key; - } - - if (json.mnemonic) - return HDPrivateKey.fromMnemonic(json.mnemonic, json.network); - - if (json.xpubkey) - return HDPublicKey.fromBase58(json.xprivkey); - - assert(false, 'Could not handle HD key JSON.'); + json = HDPrivateKey.parseJSON(json); + key = HDPrivateKey.fromBase58(json.xprivkey); + key.mnemonic = json.mnemonic ? new Mnemonic(json.mnemonic) : null; + return key; }; /** @@ -1330,7 +1290,13 @@ HDPublicKey.prototype.equal = function equal(obj) { * @returns {Object} */ -HDPublicKey.prototype.toJSON = HDPrivateKey.prototype.toJSON; +HDPublicKey.prototype.toJSON = function toJSON() { + return { + network: this.network, + xpubkey: this.xpubkey + }; +}; + /** * Handle a deserialized JSON HDPublicKey object. @@ -1338,7 +1304,12 @@ HDPublicKey.prototype.toJSON = HDPrivateKey.prototype.toJSON; * @returns {Object} A "naked" HDPublicKey. */ -HDPublicKey.parseJSON = HDPrivateKey.parseJSON; +HDPublicKey.parseJSON = function parseJSON(json) { + assert(json.xpubkey, 'Could not handle HD key JSON.'); + return { + xpubkey: json.xpubkey + }; +}; /** * Instantiate an HDPrivateKey from a jsonified key object. @@ -1347,7 +1318,10 @@ HDPublicKey.parseJSON = HDPrivateKey.parseJSON; * @returns {HDPrivateKey} */ -HDPublicKey.fromJSON = HDPrivateKey.fromJSON; +HDPublicKey.fromJSON = function fromJSON(json) { + json = HDPrivateKey.parseJSON(json); + return HDPublicKey.fromBase58(json.xpubkey); +}; /** * Test whether an object is in the form of a base58 xpubkey. diff --git a/lib/bcoin/utils.js b/lib/bcoin/utils.js index 1fe2a4f0..2113346c 100644 --- a/lib/bcoin/utils.js +++ b/lib/bcoin/utils.js @@ -352,21 +352,37 @@ utils.hmac = function hmac(alg, data, salt) { * Perform key stretching using PBKDF2. * @param {Buffer} key * @param {Buffer} salt - * @param {Number} iterations - * @param {Number} dkLen - Output size. - * @param {String?} alg + * @param {Number} iter + * @param {Number} len + * @param {String} alg + * @returns {Buffer} */ -/*! - * PBKDF2 - * Credit to: https://github.com/stayradiated/pbkdf2-sha512 - * Copyright (c) 2010-2011 Intalio Pte, All Rights Reserved - * Copyright (c) 2014, JP Richardson +utils.pbkdf2 = function pbkdf2(key, salt, iter, len, alg) { + if (typeof key === 'string') + key = new Buffer(key, 'utf8'); + + if (typeof salt === 'string') + salt = new Buffer(salt, 'utf8'); + + if (crypto && crypto.pbkdf2Sync) + return crypto.pbkdf2Sync(key, salt, iter, len, alg); + + return utils._pbkdf2(key, salt, iter, len, alg); +}; + +/** + * Execute pbkdf2 asynchronously. + * @param {Buffer} key + * @param {Buffer} salt + * @param {Number} iter + * @param {Number} len + * @param {String} alg + * @param {Function} callback */ -utils.pbkdf2 = function pbkdf2(key, salt, iterations, dkLen, alg) { - var hLen, DK, U, T, block, l, r; - var i, j, k, destPos, len; +utils.pbkdf2Async = function pbkdf2Async(key, salt, iter, len, alg, callback) { + var result; if (typeof key === 'string') key = new Buffer(key, 'utf8'); @@ -374,154 +390,166 @@ utils.pbkdf2 = function pbkdf2(key, salt, iterations, dkLen, alg) { if (typeof salt === 'string') salt = new Buffer(salt, 'utf8'); - if (!alg) - alg = 'sha512'; + if (crypto && crypto.pbkdf2) + return crypto.pbkdf2(key, salt, iter, len, alg, callback); - if (crypto && crypto.pbkdf2Sync) - return crypto.pbkdf2Sync(key, salt, iterations, dkLen, alg); - - if (alg === 'sha512') - hLen = 64; - else if (alg === 'sha256') - hLen = 32; - else if (alg === 'sha1' || alg === 'ripemd160') - hLen = 20; - else if (alg === 'md5') - hLen = 16; - - assert(dkLen <= 0xffffffff * hLen, 'Requested key length too long'); - - DK = new Buffer(dkLen); - T = new Buffer(hLen); - block = new Buffer(salt.length + 4); - - l = Math.ceil(dkLen / hLen); - r = dkLen - (l - 1) * hLen; - - salt.copy(block, 0, 0, salt.length); - - for (i = 1; i <= l; i++) { - block[salt.length + 0] = (i >>> 24) & 0xff; - block[salt.length + 1] = (i >>> 16) & 0xff; - block[salt.length + 2] = (i >>> 8) & 0xff; - block[salt.length + 3] = (i >>> 0) & 0xff; - - U = utils.hmac(alg, block, key); - - U.copy(T, 0, 0, hLen); - - for (j = 1; j < iterations; j++) { - U = utils.hmac(alg, U, key); - - for (k = 0; k < hLen; k++) - T[k] ^= U[k]; - } - - destPos = (i - 1) * hLen; - len = i === l ? r : hLen; - T.copy(DK, destPos, 0, len); + try { + result = utils._pbkdf2(key, salt, iter, len, alg); + } catch (e) { + return callback(e); } - return DK; + return utils.asyncify(callback)(null, result); +}; + +/** + * Derive a key using pbkdf2 with 25,000 iterations. + * @param {Buffer|String} passphrase + * @param {Function} callback + */ + +utils.derive = function derive(passphrase, callback) { + utils.pbkdf2Async(passphrase, 'bcoin', 25000, 32, 'sha256', callback); +}; + +/** + * Encrypt with aes-256-cbc. Derives key with {@link utils.derive}. + * @param {Buffer} data + * @param {Buffer|String} passphrase + * @param {Buffer} iv - 128 bit initialization vector. + * @param {Function} callback + */ + +utils.encrypt = function encrypt(data, passphrase, iv, callback) { + assert(Buffer.isBuffer(data)); + assert(passphrase, 'No passphrase.'); + + utils.derive(passphrase, function(err, key) { + if (err) + return callback(err); + + try { + data = utils.encipher(data, key, iv); + } catch (e) { + key.fill(0); + return callback(e); + } + + key.fill(0); + + return callback(null, data); + }); }; /** * Encrypt with aes-256-cbc. - * @param {Buffer|String} data - * @param {Buffer|String} passphrase + * @param {Buffer} data + * @param {Buffer} key - 256 bit key. + * @param {Buffer} iv - 128 bit initialization vector. * @returns {Buffer} */ -utils.encrypt = function encrypt(data, passphrase) { - var key, cipher, out; +utils.encipher = function encipher(data, key, iv) { + var cipher; - assert(passphrase, 'No passphrase.'); + if (!crypto) + return aes.cbc.encrypt(data, key, iv); - if (typeof data === 'string') - data = new Buffer(data, 'utf8'); + cipher = crypto.createCipheriv('aes-256-cbc', key, iv); - if (typeof passphrase === 'string') - passphrase = new Buffer(passphrase, 'utf8'); - - key = utils.pbkdf2key(passphrase, 2048, 32, 16); - - if (!crypto) { - out = aes.cbc.encrypt(data, key.key, key.iv); - key.key.fill(0); - key.iv.fill(0); - return out; - } - - cipher = crypto.createCipheriv('aes-256-cbc', key.key, key.iv); - - out = Buffer.concat([ + return Buffer.concat([ cipher.update(data), cipher.final() ]); - - key.key.fill(0); - key.iv.fill(0); - - return out; }; /** - * Decrypt from aes-256-cbc. - * @param {Buffer|String} data + * Decrypt with aes-256-cbc. Derives key with {@link utils.derive}. + * @param {Buffer} data * @param {Buffer|String} passphrase + * @param {Buffer} iv - 128 bit initialization vector. + * @param {Function} callback + */ + +utils.decrypt = function decrypt(data, passphrase, iv, callback) { + assert(Buffer.isBuffer(data)); + assert(passphrase, 'No passphrase.'); + + utils.derive(passphrase, function(err, key) { + if (err) + return callback(err); + + try { + data = utils.decipher(data, key, iv); + } catch (e) { + key.fill(0); + return callback(e); + } + + key.fill(0); + + return callback(null, data); + }); +}; + +/** + * Decrypt with aes-256-cbc. + * @param {Buffer} data + * @param {Buffer} key - 256 bit key. + * @param {Buffer} iv - 128 bit initialization vector. * @returns {Buffer} */ -utils.decrypt = function decrypt(data, passphrase) { - var key, decipher, out; +utils.decipher = function decipher(data, key, iv) { + var key, decipher; - assert(passphrase, 'No passphrase.'); + if (!crypto) + return aes.cbc.decrypt(data, key, iv); - if (typeof data === 'string') - data = new Buffer(data, 'hex'); + decipher = crypto.createDecipheriv('aes-256-cbc', key, iv); - if (typeof passphrase === 'string') - passphrase = new Buffer(passphrase, 'utf8'); - - key = utils.pbkdf2key(passphrase, 2048, 32, 16); - - if (!crypto) { - out = aes.cbc.decrypt(data, key.key, key.iv); - key.key.fill(0); - key.iv.fill(0); - return out; - } - - decipher = crypto.createDecipheriv('aes-256-cbc', key.key, key.iv); - - out = Buffer.concat([ + return Buffer.concat([ decipher.update(data), decipher.final() ]); - - key.key.fill(0); - key.iv.fill(0); - - return out; }; /** - * Generate a key and IV using pbkdf2. - * @param {Buffer|String} passphrase - * @param {(Buffer|String)?} salt - * @param {Number} iterations - * @param {Number} dkLen - * @param {Number} ivLen - * @param {String?} alg + * Perform key stretching using PBKDF2. + * @private + * @param {Buffer} key + * @param {Buffer} salt + * @param {Number} iter + * @param {Number} len + * @param {String} alg * @returns {Buffer} */ -utils.pbkdf2key = function pbkdf2key(passphrase, iterations, dkLen, ivLen, alg) { - var key = utils.pbkdf2(passphrase, '', iterations, dkLen + ivLen, alg); - return { - key: key.slice(0, dkLen), - iv: key.slice(dkLen, dkLen + ivLen) - }; +utils._pbkdf2 = function pbkdf2(key, salt, iter, len, alg) { + var size = utils.hash(alg, '').length; + var blocks = Math.ceil(len / size); + var out = new Buffer(blocks * size); + var buf = new Buffer(salt.length + 4); + var block = new Buffer(size); + var pos = 0; + var i, j, k, mac; + + salt.copy(buf, 0); + + for (i = 0; i < blocks; i++) { + buf.writeUInt32BE(i + 1, salt.length, true); + mac = utils.hmac(alg, buf, key); + mac.copy(block, 0); + for (j = 1; j < iter; j++) { + mac = utils.hmac(alg, mac, key); + for (k = 0; k < size; k++) + block[k] ^= mac[k]; + } + block.copy(out, pos); + pos += size; + } + + return out.slice(0, len); }; /** diff --git a/lib/bcoin/wallet.js b/lib/bcoin/wallet.js index a6006cb6..f84fc248 100644 --- a/lib/bcoin/wallet.js +++ b/lib/bcoin/wallet.js @@ -94,18 +94,20 @@ Wallet.prototype.init = function init(options, callback) { assert(!this.initialized); this.initialized = true; - if (options.passphrase) - this.master.encrypt(options.passphrase); - - this.createAccount(options, function(err, account) { + this.master.encrypt(options.passphrase, function(err) { if (err) return callback(err); - assert(account); + self.createAccount(options, function(err, account) { + if (err) + return callback(err); - self.account = account; + assert(account); - return callback(); + self.account = account; + + return callback(); + }); }); }; @@ -135,10 +137,9 @@ Wallet.prototype.open = function open(callback) { Wallet.prototype.destroy = function destroy(callback) { callback = utils.ensure(callback); - this.master.destroy(); - try { - this.db.unregister(this); + if (this.db.unregister(this)) + this.master.destroy(); } catch (e) { this.emit('error', e); return callback(e); @@ -231,32 +232,35 @@ Wallet.prototype.removeKey = function removeKey(account, key, callback) { */ Wallet.prototype.setPassphrase = function setPassphrase(old, new_, callback) { + var self = this; + var unlock; + if (typeof new_ === 'function') { callback = new_; new_ = old; old = null; } - if (old) { - try { - this.master.decrypt(old); - } catch (e) { - return callback(e); - } - } + unlock = this.locker.lock(setPassphrase, [old, new_, callback]); - if (new_) { - try { - this.master.encrypt(new_); - } catch (e) { - return callback(e); - } - } + if (!unlock) + return; - return this.save(callback); + callback = utils.wrap(callback, unlock); + + this.master.decrypt(old, function(err) { + if (err) + return callback(err); + + self.master.encrypt(new_, function(err) { + if (err) + return callback(err); + + return self.save(callback); + }); + }); }; - /** * Lock the wallet, destroy decrypted key. */ @@ -271,8 +275,8 @@ Wallet.prototype.lock = function lock() { * @param {Number?} [timeout=60000] - ms. */ -Wallet.prototype.unlock = function unlock(passphrase, timeout) { - this.master.toKey(passphrase, timeout); +Wallet.prototype.unlock = function unlock(passphrase, timeout, callback) { + this.master.toKey(passphrase, timeout, callback); }; /** @@ -308,7 +312,7 @@ Wallet.prototype.getID = function getID() { Wallet.prototype.createAccount = function createAccount(options, callback, force) { var self = this; - var master, key, unlock; + var key, unlock; unlock = this.locker.lock(createAccount, [options, callback], force); @@ -317,37 +321,36 @@ Wallet.prototype.createAccount = function createAccount(options, callback, force callback = utils.wrap(callback, unlock); - try { - master = this.master.toKey(options.passphrase, options.timeout); - } catch (e) { - return callback(e); - } - - key = master.deriveAccount44(this.accountDepth); - - options = { - network: this.network, - id: this.id, - name: this.accountDepth === 0 ? 'default' : options.name, - witness: options.witness, - accountKey: key.hdPublicKey, - accountIndex: this.accountDepth, - type: options.type, - keys: options.keys, - m: options.m, - n: options.n - }; - - this.db.createAccount(options, function(err, account) { + this.master.toKey(options.passphrase, options.timeout, function(err, master) { if (err) return callback(err); - self.accountDepth++; + key = master.deriveAccount44(self.accountDepth); - self.save(function(err) { + options = { + network: self.network, + id: self.id, + name: self.accountDepth === 0 ? 'default' : options.name, + witness: options.witness, + accountKey: key.hdPublicKey, + accountIndex: self.accountDepth, + type: options.type, + keys: options.keys, + m: options.m, + n: options.n + }; + + self.db.createAccount(options, function(err, account) { if (err) return callback(err); - return callback(null, account); + + self.accountDepth++; + + self.save(function(err) { + if (err) + return callback(err); + return callback(null, account); + }); }); }); }; @@ -976,7 +979,7 @@ Wallet.prototype.scriptInputs = function scriptInputs(tx, callback) { Wallet.prototype.sign = function sign(tx, options, callback) { var self = this; var total = 0; - var i, address, key, master; + var i, address, key; if (typeof options === 'function') { callback = options; @@ -986,25 +989,24 @@ Wallet.prototype.sign = function sign(tx, options, callback) { if (typeof options === 'string' || Buffer.isBuffer(options)) options = { passphrase: options }; - this.deriveInputs(tx, function(err, addresses) { + this.master.toKey(options.passphrase, options.timeout, function(err, master) { if (err) return callback(err); - try { - master = self.master.toKey(options.passphrase, options.timeout); - } catch (e) { - return callback(e); - } + self.deriveInputs(tx, function(err, addresses) { + if (err) + return callback(err); - for (i = 0; i < addresses.length; i++) { - address = addresses[i]; - key = master.deriveAccount44(address.account); - key = key.derive(address.change).derive(address.index); - assert(utils.equal(key.getPublicKey(), address.key)); - total += address.sign(tx, key, options.index, options.type); - } + for (i = 0; i < addresses.length; i++) { + address = addresses[i]; + key = master.deriveAccount44(address.account); + key = key.derive(address.change).derive(address.index); + assert(utils.equal(key.getPublicKey(), address.key)); + total += address.sign(tx, key, options.index, options.type); + } - return callback(null, total); + return callback(null, total); + }); }); }; @@ -1532,7 +1534,6 @@ function Account(db, options) { this.db = db; this.network = db.network; this.lookahead = Account.LOOKAHEAD; - this.cache = new bcoin.lru(20, 1); this.loaded = false; this.loading = false; @@ -2276,7 +2277,7 @@ Account.isAccount = function isAccount(obj) { /** * Master BIP32 key which can exist - * in an timed out encrypted state. + * in a timed out encrypted state. * @exports Master * @constructor * @param {Object} options @@ -2291,15 +2292,26 @@ function MasterKey(options) { this.phrase = null; this.passphrase = null; this.key = null; + this.iv = null; + this.ciphertext = null; this.timer = null; this._destroy = this.destroy.bind(this); + this.locker = new bcoin.locker(this); } +/** + * Inject properties from options object. + * @private + * @param {Object} options + */ + MasterKey.prototype.fromOptions = function fromOptions(options) { this.encrypted = !!options.encrypted; this.xprivkey = options.xprivkey; this.phrase = options.phrase; this.passphrase = options.passphrase; + this.iv = options.iv; + this.ciphertext = options.ciphertext; this.key = options.key || null; assert(this.encrypted ? !this.key : this.key); @@ -2307,6 +2319,11 @@ MasterKey.prototype.fromOptions = function fromOptions(options) { return this; }; +/** + * Instantiate master key from options. + * @returns {MasterKey} + */ + MasterKey.fromOptions = function fromOptions(options) { return new MasterKey().fromOptions(options); }; @@ -2318,18 +2335,42 @@ MasterKey.fromOptions = function fromOptions(options) { * @returns {HDPrivateKey} */ -MasterKey.prototype.toKey = function toKey(passphrase, timeout) { - var xprivkey; +MasterKey.prototype.toKey = function toKey(passphrase, timeout, callback) { + var self = this; + var unlock, data; - if (!this.key) { - assert(this.encrypted); - xprivkey = utils.decrypt(this.xprivkey, passphrase); - this.key = bcoin.hd.fromRaw(xprivkey); - xprivkey.fill(0); - this.start(timeout); - } + unlock = this.locker.lock(toKey, [passphrase, timeout, callback]); - return this.key; + if (!unlock) + return; + + callback = utils.wrap(callback, unlock); + + if (this.key) + return callback(null, this.key); + + if (!passphrase) + return callback(new Error('No passphrase.')); + + assert(this.encrypted); + + utils.decrypt(this.ciphertext, passphrase, this.iv, function(err, data) { + if (err) + return callback(err); + + try { + data = MasterKey.fromRaw(data); + } catch (e) { + return callback(e); + } + + self.key = data.key; + self.start(timeout); + + data.destroy(); + + return callback(null, self.key); + }); }; /** @@ -2390,25 +2431,39 @@ MasterKey.prototype.destroy = function destroy() { * @param {Buffer|String} passphrase - Zero this yourself. */ -MasterKey.prototype.decrypt = function decrypt(passphrase) { +MasterKey.prototype.decrypt = function decrypt(passphrase, callback) { + var self = this; + var unlock; + + unlock = this.locker.lock(decrypt, [passphrase, callback]); + + if (!unlock) + return; + + callback = utils.wrap(callback, unlock); + if (!this.encrypted) { assert(this.key); - return; + return callback(); } - assert(passphrase, 'Passphrase is required.'); + if (!passphrase) + return callback(); this.destroy(); - this.encrypted = false; - this.xprivkey = utils.decrypt(this.xprivkey, passphrase); + utils.decrypt(this.ciphertext, passphrase, this.iv, function(err, data) { + if (err) + return callback(err); - if (this.phrase) { - this.phrase = utils.decrypt(this.phrase, passphrase); - this.passphrase = utils.decrypt(this.passphrase, passphrase); - } + self.encrypted = false; + self.iv = null; + self.ciphertext = null; - this.key = bcoin.hd.fromRaw(this.xprivkey); + self.fromRaw(data); + + return callback(); + }); }; /** @@ -2416,39 +2471,70 @@ MasterKey.prototype.decrypt = function decrypt(passphrase) { * @param {Buffer|String} passphrase - Zero this yourself. */ -MasterKey.prototype.encrypt = function encrypt(passphrase) { - var xprivkey = this.xprivkey; - var phrase = this.phrase; - var pass = this.passphrase; +MasterKey.prototype.encrypt = function encrypt(passphrase, callback) { + var self = this; + var unlock, data, iv; + + unlock = this.locker.lock(encrypt, [passphrase, callback]); + + if (!unlock) + return; + + callback = utils.wrap(callback, unlock); if (this.encrypted) return; - assert(passphrase, 'Passphrase is required.'); + if (!passphrase) + return callback(); - this.key = null; - this.encrypted = true; - this.xprivkey = utils.encrypt(xprivkey, passphrase); - xprivkey.fill(0); + iv = bcoin.ec.random(16); + data = this.toRaw(); - if (this.phrase) { - this.phrase = utils.encrypt(phrase, passphrase); - this.passphrase = utils.encrypt(pass, passphrase); - phrase.fill(0); - pass.fill(0); - } + utils.encrypt(data, passphrase, iv, function(err, data) { + if (err) + return callback(err); + + self.key = null; + self.encrypted = true; + self.iv = iv; + self.ciphertext = data; + + self.xprivkey.fill(0); + self.xprivkey = null; + + if (self.phrase) { + self.phrase.fill(0); + self.phrase = null; + self.passphrase.fill(0); + self.passphrase = null; + } + + return callback(); + }); }; /** * Serialize the key in the form of: - * `[enc-flag][phrase-marker][phrase?][passphrase?][xprivkey]` + * `[enc-flag][iv?][ciphertext?][phrase-marker][phrase?][passphrase?][xprivkey]` * @returns {Buffer} */ MasterKey.prototype.toRaw = function toRaw(writer) { var p = new BufferWriter(writer); - p.writeU8(this.encrypted ? 1 : 0); + if (this.encrypted) { + p.writeU8(1); + p.writeVarBytes(this.iv); + p.writeVarBytes(this.ciphertext); + + if (!writer) + p = p.render(); + + return p; + } + + p.writeU8(0); if (this.phrase) { p.writeU8(1); @@ -2458,7 +2544,7 @@ MasterKey.prototype.toRaw = function toRaw(writer) { p.writeU8(0); } - p.writeVarBytes(this.xprivkey); + p.writeBytes(this.xprivkey); if (!writer) p = p.render(); @@ -2467,8 +2553,9 @@ MasterKey.prototype.toRaw = function toRaw(writer) { }; /** - * Instantiate master key from serialized data. - * @returns {MasterKey} + * Inject properties from serialized data. + * @private + * @param {Buffer} raw */ MasterKey.prototype.fromRaw = function fromRaw(raw) { @@ -2476,31 +2563,42 @@ MasterKey.prototype.fromRaw = function fromRaw(raw) { this.encrypted = p.readU8() === 1; + if (this.encrypted) { + this.iv = p.readVarBytes(); + this.ciphertext = p.readVarBytes(); + return this; + } + if (p.readU8() === 1) { this.phrase = p.readVarBytes(); this.passphrase = p.readVarBytes(); } - this.xprivkey = p.readVarBytes(); - - if (!this.encrypted) - this.key = bcoin.hd.fromRaw(this.xprivkey); + this.xprivkey = p.readBytes(82); + this.key = bcoin.hd.fromRaw(this.xprivkey); return this; }; +/** + * Instantiate master key from serialized data. + * @returns {MasterKey} + */ + MasterKey.fromRaw = function fromRaw(raw) { return new MasterKey().fromRaw(raw); }; /** - * Instantiate master key from an HDPrivateKey. + * Inject properties from an HDPrivateKey. + * @private * @param {HDPrivateKey} key - * @returns {MasterKey} */ MasterKey.prototype.fromKey = function fromKey(key) { this.encrypted = false; + this.iv = null; + this.ciphertext = null; if (key.mnemonic) { this.phrase = new Buffer(key.mnemonic.phrase, 'utf8'); @@ -2513,6 +2611,12 @@ MasterKey.prototype.fromKey = function fromKey(key) { return this; }; +/** + * Instantiate master key from an HDPrivateKey. + * @param {HDPrivateKey} key + * @returns {MasterKey} + */ + MasterKey.fromKey = function fromKey(key) { return new MasterKey().fromKey(key); }; @@ -2523,14 +2627,11 @@ MasterKey.fromKey = function fromKey(key) { */ MasterKey.prototype.toJSON = function toJSON() { - var phrase, passphrase, xprivkey; + var phrase, passphrase, xprivkey, iv, ciphertext; if (this.encrypted) { - if (this.phrase) { - phrase = this.phrase.toString('hex'); - passphrase = this.passphrase.toString('hex'); - } - xprivkey = this.xprivkey.toString('hex'); + iv = this.iv.toString('hex'); + ciphertext = this.ciphertext.toString('hex'); } else { if (this.phrase) { phrase = this.phrase.toString('utf8'); @@ -2541,6 +2642,8 @@ MasterKey.prototype.toJSON = function toJSON() { return { encrypted: this.encrypted, + iv: iv, + ciphertext: ciphertext, phrase: phrase, passphrase: passphrase, xprivkey: xprivkey @@ -2548,35 +2651,46 @@ MasterKey.prototype.toJSON = function toJSON() { }; /** - * Instantiate master key from jsonified object. - * @returns {MasterKey} + * Inject properties from JSON object. + * @private + * @param {Object} json */ MasterKey.prototype.fromJSON = function fromJSON(json) { this.encrypted = json.encrypted; if (json.encrypted) { - if (json.phrase) { - this.phrase = new Buffer(json.phrase, 'hex'); - this.passphrase = new Buffer(json.passphrase, 'hex'); - } - this.xprivkey = new Buffer(json.xprivkey, 'hex'); + this.iv = new Buffer(json.iv, 'hex'); + this.ciphertext = new Buffer(json.ciphertext, 'hex'); } else { if (json.phrase) { this.phrase = new Buffer(json.phrase, 'utf8'); this.passphrase = new Buffer(json.passphrase, 'utf8'); } this.xprivkey = utils.fromBase58(json.xprivkey); + this.key = bcoin.hd.fromRaw(this.xprivkey); } - if (!json.encrypted) - this.key = bcoin.hd.fromRaw(xprivkey); - return this; }; -MasterKey.fromJSON = function fromJSON(key) { - return new MasterKey().fromJSON(key); +/** + * Instantiate master key from jsonified object. + * @param {Object} json + * @returns {MasterKey} + */ + +MasterKey.fromJSON = function fromJSON(json) { + return new MasterKey().fromJSON(json); +}; + +/** + * Inspect the key. + * @returns {Object} + */ + +MasterKey.prototype.inspect = function inspect() { + return this.toJSON(); }; /** diff --git a/lib/bcoin/walletdb.js b/lib/bcoin/walletdb.js index 58796099..d27315aa 100644 --- a/lib/bcoin/walletdb.js +++ b/lib/bcoin/walletdb.js @@ -271,6 +271,7 @@ WalletDB.prototype.register = function register(object) { /** * Unregister a object with the walletdb. * @param {Object} object + * @returns {Boolean} */ WalletDB.prototype.unregister = function unregister(object) { @@ -278,13 +279,17 @@ WalletDB.prototype.unregister = function unregister(object) { var watcher = this.watchers[id]; if (!watcher) - return; + return false; assert(watcher.object === object); assert(watcher.refs !== 0, '`destroy()` called twice!'); - if (--watcher.refs === 0) + if (--watcher.refs === 0) { delete this.watchers[id]; + return true; + } + + return false; }; /** @@ -1049,7 +1054,7 @@ WalletDB.prototype.fetchWallet = function fetchWallet(id, callback, handler) { handler(wallet, function(err, result) { // Kill the reference. - self.unregister(wallet); + wallet.destroy(); if (err) return callback(err); diff --git a/test/aes-test.js b/test/aes-test.js index d05d61cc..d9f60ac0 100644 --- a/test/aes-test.js +++ b/test/aes-test.js @@ -7,6 +7,14 @@ var aes = require('../lib/bcoin/aes'); var crypto = require('crypto'); describe('AES', function() { + function pbkdf2key(passphrase, iterations, dkLen, ivLen, alg) { + var key = utils.pbkdf2(passphrase, '', iterations, dkLen + ivLen, 'sha512'); + return { + key: key.slice(0, dkLen), + iv: key.slice(dkLen, dkLen + ivLen) + }; + } + function nencrypt(data, passphrase) { var key, cipher; @@ -19,7 +27,7 @@ describe('AES', function() { if (typeof passphrase === 'string') passphrase = new Buffer(passphrase, 'utf8'); - key = utils.pbkdf2key(passphrase, 2048, 32, 16); + key = pbkdf2key(passphrase, 2048, 32, 16); cipher = crypto.createCipheriv('aes-256-cbc', key.key, key.iv); return Buffer.concat([ @@ -40,7 +48,7 @@ describe('AES', function() { if (typeof passphrase === 'string') passphrase = new Buffer(passphrase, 'utf8'); - key = utils.pbkdf2key(passphrase, 2048, 32, 16); + key = pbkdf2key(passphrase, 2048, 32, 16); decipher = crypto.createDecipheriv('aes-256-cbc', key.key, key.iv); return Buffer.concat([ @@ -61,7 +69,7 @@ describe('AES', function() { if (typeof passphrase === 'string') passphrase = new Buffer(passphrase, 'utf8'); - key = utils.pbkdf2key(passphrase, 2048, 32, 16); + key = pbkdf2key(passphrase, 2048, 32, 16); return aes.cbc.encrypt(data, key.key, key.iv); } @@ -78,7 +86,7 @@ describe('AES', function() { if (typeof passphrase === 'string') passphrase = new Buffer(passphrase, 'utf8'); - key = utils.pbkdf2key(passphrase, 2048, 32, 16); + key = pbkdf2key(passphrase, 2048, 32, 16); return aes.cbc.decrypt(data, key.key, key.iv); } diff --git a/test/hd-test.js b/test/hd-test.js index d260cba2..a496de50 100644 --- a/test/hd-test.js +++ b/test/hd-test.js @@ -90,7 +90,7 @@ describe('HD', function() { var master, child1, child2, child3, child4, child5, child6; it('should create a pbkdf2 seed', function() { - var checkSeed = bcoin.utils.pbkdf2(phrase, 'mnemonic' + 'foo', 2048, 64).toString('hex'); + var checkSeed = bcoin.utils.pbkdf2(phrase, 'mnemonic' + 'foo', 2048, 64, 'sha512').toString('hex'); assert.equal(checkSeed, seed); }); diff --git a/test/wallet-test.js b/test/wallet-test.js index e6e6901e..51dfe1e2 100644 --- a/test/wallet-test.js +++ b/test/wallet-test.js @@ -869,7 +869,8 @@ describe('Wallet', function() { it('should fill tx with inputs when encrypted', function(cb) { walletdb.create({ passphrase: 'foo' }, function(err, w1) { assert.ifError(err); - w1.master.destroy(); + w1.master.stop(); + w1.master.key = null; // Coinbase var t1 = bcoin.mtx()