/*! * masterkey.js - master bip32 key object for bcoin * Copyright (c) 2014-2017, Christopher Jeffrey (MIT License). * https://github.com/bcoin-org/bcoin */ 'use strict'; const assert = require('assert'); const util = require('../utils/util'); const Lock = require('../utils/lock'); const random = require('../crypto/random'); const cleanse = require('../crypto/cleanse'); const aes = require('../crypto/aes'); const pbkdf2 = require('../crypto/pbkdf2'); const scrypt = require('../crypto/scrypt'); const BufferReader = require('../utils/reader'); const StaticWriter = require('../utils/staticwriter'); const encoding = require('../utils/encoding'); const HD = require('../hd/hd'); const Mnemonic = HD.Mnemonic; /** * Master BIP32 key which can exist * in a timed out encrypted state. * @alias module:wallet.MasterKey * @constructor * @param {Object} options */ function MasterKey(options) { if (!(this instanceof MasterKey)) return new MasterKey(options); this.encrypted = false; this.iv = null; this.ciphertext = null; this.key = null; this.mnemonic = null; this.alg = MasterKey.alg.PBKDF2; this.N = 50000; this.r = 0; this.p = 0; this.aesKey = null; this.timer = null; this.until = 0; this._onTimeout = this.lock.bind(this); this.locker = new Lock(); if (options) this.fromOptions(options); } /** * Key derivation salt. * @const {Buffer} * @default */ MasterKey.SALT = Buffer.from('bcoin', 'ascii'); /** * Key derivation algorithms. * @enum {Number} * @default */ MasterKey.alg = { PBKDF2: 0, SCRYPT: 1 }; /** * Key derivation algorithms by value. * @enum {String} * @default */ MasterKey.algByVal = { 0: 'PBKDF2', 1: 'SCRYPT' }; /** * Inject properties from options object. * @private * @param {Object} options */ MasterKey.prototype.fromOptions = function fromOptions(options) { assert(options); if (options.encrypted != null) { assert(typeof options.encrypted === 'boolean'); this.encrypted = options.encrypted; } if (options.iv) { assert(Buffer.isBuffer(options.iv)); this.iv = options.iv; } if (options.ciphertext) { assert(Buffer.isBuffer(options.ciphertext)); this.ciphertext = options.ciphertext; } if (options.key) { assert(HD.isPrivate(options.key)); this.key = options.key; } if (options.mnemonic) { assert(options.mnemonic instanceof Mnemonic); this.mnemonic = options.mnemonic; } if (options.alg != null) { if (typeof options.alg === 'string') { this.alg = MasterKey.alg[options.alg.toUpperCase()]; assert(this.alg != null, 'Unknown algorithm.'); } else { assert(typeof options.alg === 'number'); assert(MasterKey.algByVal[options.alg]); this.alg = options.alg; } } if (options.rounds != null) { assert(util.isU32(options.rounds)); this.N = options.rounds; } if (options.N != null) { assert(util.isU32(options.N)); this.N = options.N; } if (options.r != null) { assert(util.isU32(options.r)); this.r = options.r; } if (options.p != null) { assert(util.isU32(options.p)); this.p = options.p; } assert(this.encrypted ? !this.key : this.key); return this; }; /** * Instantiate master key from options. * @returns {MasterKey} */ MasterKey.fromOptions = function fromOptions(options) { return new MasterKey().fromOptions(options); }; /** * Decrypt the key and set a timeout to destroy decrypted data. * @param {Buffer|String} passphrase - Zero this yourself. * @param {Number} [timeout=60000] timeout in ms. * @returns {Promise} - Returns {@link HDPrivateKey}. */ MasterKey.prototype.unlock = async function unlock(passphrase, timeout) { const _unlock = await this.locker.lock(); try { return await this._unlock(passphrase, timeout); } finally { _unlock(); } }; /** * Decrypt the key without a lock. * @private * @param {Buffer|String} passphrase - Zero this yourself. * @param {Number} [timeout=60000] timeout in ms. * @returns {Promise} - Returns {@link HDPrivateKey}. */ MasterKey.prototype._unlock = async function _unlock(passphrase, timeout) { if (this.key) { if (this.encrypted) { assert(this.timer != null); this.start(timeout); } return this.key; } if (!passphrase) throw new Error('No passphrase.'); assert(this.encrypted); const key = await this.derive(passphrase); const data = aes.decipher(this.ciphertext, key, this.iv); this.fromKeyRaw(data); this.start(timeout); this.aesKey = key; return this.key; }; /** * Start the destroy timer. * @private * @param {Number} [timeout=60000] timeout in ms. */ MasterKey.prototype.start = function start(timeout) { if (!timeout) timeout = 60; this.stop(); if (timeout === -1) return; this.until = util.now() + timeout; this.timer = setTimeout(this._onTimeout, timeout * 1000); }; /** * Stop the destroy timer. * @private */ MasterKey.prototype.stop = function stop() { if (this.timer != null) { clearTimeout(this.timer); this.timer = null; this.until = 0; } }; /** * Derive an aes key based on params. * @param {String|Buffer} passphrase * @returns {Promise} */ MasterKey.prototype.derive = async function derive(passwd) { const salt = MasterKey.SALT; const N = this.N; const r = this.r; const p = this.p; if (typeof passwd === 'string') passwd = Buffer.from(passwd, 'utf8'); switch (this.alg) { case MasterKey.alg.PBKDF2: return await pbkdf2.deriveAsync(passwd, salt, N, 32, 'sha256'); case MasterKey.alg.SCRYPT: return await scrypt.deriveAsync(passwd, salt, N, r, p, 32); default: throw new Error(`Unknown algorithm: ${this.alg}.`); } }; /** * Encrypt data with in-memory aes key. * @param {Buffer} data * @param {Buffer} iv * @returns {Buffer} */ MasterKey.prototype.encipher = function encipher(data, iv) { if (!this.aesKey) return null; if (typeof iv === 'string') iv = Buffer.from(iv, 'hex'); return aes.encipher(data, this.aesKey, iv.slice(0, 16)); }; /** * Decrypt data with in-memory aes key. * @param {Buffer} data * @param {Buffer} iv * @returns {Buffer} */ MasterKey.prototype.decipher = function decipher(data, iv) { if (!this.aesKey) return null; if (typeof iv === 'string') iv = Buffer.from(iv, 'hex'); return aes.decipher(data, this.aesKey, iv.slice(0, 16)); }; /** * Destroy the key by zeroing the * privateKey and chainCode. Stop * the timer if there is one. * @returns {Promise} */ MasterKey.prototype.lock = async function lock() { const unlock = await this.locker.lock(); try { return await this._lock(); } finally { unlock(); } }; /** * Destroy the key by zeroing the * privateKey and chainCode. Stop * the timer if there is one. */ MasterKey.prototype._lock = function _lock() { if (!this.encrypted) { assert(this.timer == null); assert(this.key); return; } this.stop(); if (this.key) { this.key.destroy(true); this.key = null; } if (this.aesKey) { cleanse(this.aesKey); this.aesKey = null; } }; /** * Destroy the key permanently. */ MasterKey.prototype.destroy = async function destroy() { await this.lock(); this.locker.destroy(); }; /** * Decrypt the key permanently. * @param {Buffer|String} passphrase - Zero this yourself. * @returns {Promise} */ MasterKey.prototype.decrypt = async function decrypt(passphrase, clean) { const unlock = await this.locker.lock(); try { return await this._decrypt(passphrase, clean); } finally { unlock(); } }; /** * Decrypt the key permanently without a lock. * @private * @param {Buffer|String} passphrase - Zero this yourself. * @returns {Promise} */ MasterKey.prototype._decrypt = async function _decrypt(passphrase, clean) { if (!this.encrypted) throw new Error('Master key is not encrypted.'); if (!passphrase) throw new Error('No passphrase provided.'); this._lock(); const key = await this.derive(passphrase); const data = aes.decipher(this.ciphertext, key, this.iv); this.fromKeyRaw(data); this.encrypted = false; this.iv = null; this.ciphertext = null; if (!clean) { cleanse(key); return null; } return key; }; /** * Encrypt the key permanently. * @param {Buffer|String} passphrase - Zero this yourself. * @returns {Promise} */ MasterKey.prototype.encrypt = async function encrypt(passphrase, clean) { const unlock = await this.locker.lock(); try { return await this._encrypt(passphrase, clean); } finally { unlock(); } }; /** * Encrypt the key permanently without a lock. * @private * @param {Buffer|String} passphrase - Zero this yourself. * @returns {Promise} */ MasterKey.prototype._encrypt = async function _encrypt(passphrase, clean) { if (this.encrypted) throw new Error('Master key is already encrypted.'); if (!passphrase) throw new Error('No passphrase provided.'); const raw = this.toKeyRaw(); const iv = random.randomBytes(16); this.stop(); const key = await this.derive(passphrase); const data = aes.encipher(raw, key, iv); this.key = null; this.mnemonic = null; this.encrypted = true; this.iv = iv; this.ciphertext = data; if (!clean) { cleanse(key); return null; } return key; }; /** * Calculate key serialization size. * @returns {Number} */ MasterKey.prototype.getKeySize = function getKeySize() { let size = 0; size += this.key.getSize(); size += 1; if (this.mnemonic) size += this.mnemonic.getSize(); return size; }; /** * Serialize key and menmonic to a single buffer. * @returns {Buffer} */ MasterKey.prototype.toKeyRaw = function toKeyRaw() { const bw = new StaticWriter(this.getKeySize()); this.key.toWriter(bw); if (this.mnemonic) { bw.writeU8(1); this.mnemonic.toWriter(bw); } else { bw.writeU8(0); } return bw.render(); }; /** * Inject properties from serialized key. * @param {Buffer} data */ MasterKey.prototype.fromKeyRaw = function fromKeyRaw(data) { const br = new BufferReader(data); this.key = HD.PrivateKey.fromReader(br); if (br.readU8() === 1) this.mnemonic = Mnemonic.fromReader(br); return this; }; /** * Calculate serialization size. * @returns {Number} */ MasterKey.prototype.getSize = function getSize() { let size = 0; if (this.encrypted) { size += 1; size += encoding.sizeVarBytes(this.iv); size += encoding.sizeVarBytes(this.ciphertext); size += 13; return size; } size += 1; size += encoding.sizeVarlen(this.getKeySize()); return size; }; /** * Serialize the key in the form of: * `[enc-flag][iv?][ciphertext?][extended-key?]` * @returns {Buffer} */ MasterKey.prototype.toRaw = function toRaw() { const bw = new StaticWriter(this.getSize()); if (this.encrypted) { bw.writeU8(1); bw.writeVarBytes(this.iv); bw.writeVarBytes(this.ciphertext); bw.writeU8(this.alg); bw.writeU32(this.N); bw.writeU32(this.r); bw.writeU32(this.p); return bw.render(); } bw.writeU8(0); // NOTE: useless varint const size = this.getKeySize(); bw.writeVarint(size); bw.writeBytes(this.key.toRaw()); if (this.mnemonic) { bw.writeU8(1); this.mnemonic.toWriter(bw); } else { bw.writeU8(0); } return bw.render(); }; /** * Inject properties from serialized data. * @private * @param {Buffer} raw */ MasterKey.prototype.fromRaw = function fromRaw(raw) { const br = new BufferReader(raw); this.encrypted = br.readU8() === 1; if (this.encrypted) { this.iv = br.readVarBytes(); this.ciphertext = br.readVarBytes(); this.alg = br.readU8(); assert(MasterKey.algByVal[this.alg]); this.N = br.readU32(); this.r = br.readU32(); this.p = br.readU32(); return this; } // NOTE: useless varint br.skipVarint(); this.key = HD.PrivateKey.fromRaw(br.readBytes(82)); if (br.readU8() === 1) this.mnemonic = Mnemonic.fromReader(br); return this; }; /** * Instantiate master key from serialized data. * @returns {MasterKey} */ MasterKey.fromRaw = function fromRaw(raw) { return new MasterKey().fromRaw(raw); }; /** * Inject properties from an HDPrivateKey. * @private * @param {HDPrivateKey} key * @param {Mnemonic?} mnemonic */ MasterKey.prototype.fromKey = function fromKey(key, mnemonic) { this.encrypted = false; this.iv = null; this.ciphertext = null; this.key = key; this.mnemonic = mnemonic || null; return this; }; /** * Instantiate master key from an HDPrivateKey. * @param {HDPrivateKey} key * @param {Mnemonic?} mnemonic * @returns {MasterKey} */ MasterKey.fromKey = function fromKey(key, mnemonic) { return new MasterKey().fromKey(key, mnemonic); }; /** * Convert master key to a jsonifiable object. * @param {Boolean?} unsafe - Whether to include * the key data in the JSON. * @returns {Object} */ MasterKey.prototype.toJSON = function toJSON(unsafe) { if (this.encrypted) { return { encrypted: true, until: this.until, iv: this.iv.toString('hex'), ciphertext: unsafe ? this.ciphertext.toString('hex') : undefined, algorithm: MasterKey.algByVal[this.alg].toLowerCase(), N: this.N, r: this.r, p: this.p }; } return { encrypted: false, key: unsafe ? this.key.toJSON() : undefined, mnemonic: unsafe && this.mnemonic ? this.mnemonic.toJSON() : undefined }; }; /** * Inspect the key. * @returns {Object} */ MasterKey.prototype.inspect = function inspect() { const json = this.toJSON(true); if (this.key) json.key = this.key.toJSON(); if (this.mnemonic) json.mnemonic = this.mnemonic.toJSON(); return json; }; /** * Test whether an object is a MasterKey. * @param {Object} obj * @returns {Boolean} */ MasterKey.isMasterKey = function isMasterKey(obj) { return obj && typeof obj.encrypted === 'boolean' && typeof obj.decrypt === 'function'; }; /* * Expose */ module.exports = MasterKey;