Source: wallet/masterkey.js

/*!
 * masterkey.js - master bip32 key object for hsd
 * Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
 * https://github.com/handshake-org/hsd
 */

'use strict';

const assert = require('bsert');
const bio = require('bufio');
const {Lock} = require('bmutex');
const random = require('bcrypto/lib/random');
const cleanse = require('bcrypto/lib/cleanse');
const aes = require('bcrypto/lib/aes');
const sha256 = require('bcrypto/lib/sha256');
const secp256k1 = require('bcrypto/lib/secp256k1');
const pbkdf2 = require('bcrypto/lib/pbkdf2');
const scrypt = require('bcrypto/lib/scrypt');
const util = require('../utils/util');
const HDPrivateKey = require('../hd/private');
const Mnemonic = require('../hd/mnemonic');
const pkg = require('../pkg');
const {encoding} = bio;

/**
 * Master Key
 * Master BIP32 key which can exist
 * in a timed out encrypted state.
 * @alias module:wallet.MasterKey
 */

class MasterKey extends bio.Struct {
  /**
   * Create a master key.
   * @constructor
   * @param {Object} options
   */

  constructor(options) {
    super();

    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.locker = new Lock();

    if (options)
      this.fromOptions(options);
  }

  /**
   * Inject properties from options object.
   * @private
   * @param {Object} options
   */

  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(HDPrivateKey.isHDPrivateKey(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((options.rounds >>> 0) === options.rounds);
      this.rounds = options.rounds;
    }

    if (options.n != null) {
      assert((options.n >>> 0) === options.n);
      this.n = options.n;
    }

    if (options.r != null) {
      assert((options.r >>> 0) === options.r);
      this.r = options.r;
    }

    if (options.p != null) {
      assert((options.p >>> 0) === options.p);
      this.p = options.p;
    }

    assert(this.encrypted ? !this.key : this.key);

    return this;
  }

  /**
   * 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}.
   */

  async 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}.
   */

  async _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.readKey(data);

    this.start(timeout);

    this.aesKey = key;

    return this.key;
  }

  /**
   * Start the destroy timer.
   * @private
   * @param {Number} [timeout=60] timeout in seconds.
   */

  start(timeout) {
    if (!timeout)
      timeout = 60;

    this.stop();

    if (timeout === -1)
      return;

    assert((timeout >>> 0) === timeout);

    this.until = util.now() + timeout;
    this.timer = setTimeout(() => this.lock(), timeout * 1000);
  }

  /**
   * Stop the destroy timer.
   * @private
   */

  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}
   */

  async 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 pbkdf2.deriveAsync(sha256, passwd, salt, n, 32);
      case MasterKey.alg.SCRYPT:
        return 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}
   */

  encipher(data, iv) {
    if (!this.aesKey)
      return null;

    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}
   */

  decipher(data, iv) {
    if (!this.aesKey)
      return null;

    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}
   */

  async 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.
   */

  _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.
   */

  async destroy() {
    await this.lock();
    this.locker.destroy();
  }

  /**
   * Decrypt the key permanently.
   * @param {Buffer|String} passphrase - Zero this yourself.
   * @returns {Promise}
   */

  async 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}
   */

  async _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.readKey(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}
   */

  async 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}
   */

  async _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.writeKey();
    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}
   */

  keySize() {
    let size = 0;

    size += 64;
    size += 1;

    if (this.mnemonic)
      size += this.mnemonic.getSize();

    return size;
  }

  /**
   * Serialize key and menmonic to a single buffer.
   * @returns {Buffer}
   */

  writeKey() {
    const bw = bio.write(this.keySize());

    bw.writeBytes(this.key.chainCode);
    bw.writeBytes(this.key.privateKey);

    if (this.mnemonic) {
      bw.writeU8(1);
      this.mnemonic.write(bw);
    } else {
      bw.writeU8(0);
    }

    return bw.render();
  }

  /**
   * Inject properties from serialized key.
   * @param {Buffer} data
   */

  readKey(data) {
    const br = bio.read(data);

    this.key = new HDPrivateKey();
    this.key.chainCode = br.readBytes(32);
    this.key.privateKey = br.readBytes(32);
    this.key.publicKey = secp256k1.publicKeyCreate(this.key.privateKey, true);

    if (br.readU8() === 1)
      this.mnemonic = Mnemonic.read(br);

    return this;
  }

  /**
   * Calculate serialization size.
   * @returns {Number}
   */

  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 += this.keySize();

    return size;
  }

  /**
   * Serialize the key in the form of:
   * `[enc-flag][iv?][ciphertext?][extended-key?]`
   * @returns {Buffer}
   */

  write(bw) {
    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;
    }

    bw.writeU8(0);

    bw.writeBytes(this.key.chainCode);
    bw.writeBytes(this.key.privateKey);

    if (this.mnemonic) {
      bw.writeU8(1);
      this.mnemonic.write(bw);
    } else {
      bw.writeU8(0);
    }

    return bw;
  }

  /**
   * Inject properties from serialized data.
   * @private
   * @param {Buffer} raw
   */

  read(br) {
    this.encrypted = br.readU8() === 1;

    if (this.encrypted) {
      this.iv = br.readVarBytes();
      this.ciphertext = br.readVarBytes();

      this.alg = br.readU8();

      assert(this.alg < MasterKey.algByVal.length);

      this.n = br.readU32();
      this.r = br.readU32();
      this.p = br.readU32();

      return this;
    }

    this.key = new HDPrivateKey();
    this.key.chainCode = br.readBytes(32);
    this.key.privateKey = br.readBytes(32);
    this.key.publicKey = secp256k1.publicKeyCreate(this.key.privateKey, true);

    if (br.readU8() === 1)
      this.mnemonic = Mnemonic.read(br);

    return this;
  }

  /**
   * Inject properties from an HDPrivateKey.
   * @private
   * @param {HDPrivateKey} key
   * @param {Mnemonic?} mnemonic
   */

  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}
   */

  static fromKey(key, mnemonic) {
    return new this().fromKey(key, mnemonic);
  }

  /**
   * Convert master key to a jsonifiable object.
   * @param {Network?} network
   * @param {Boolean?} unsafe - Whether to include
   * the key data in the JSON.
   * @returns {Object}
   */

  getJSON(network, 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.getJSON(network) : undefined,
      mnemonic: unsafe && this.mnemonic ? this.mnemonic.toJSON() : undefined
    };
  }

  /**
   * Inspect the key.
   * @returns {Object}
   */

  format() {
    const json = this.getJSON(null, 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}
   */

  static isMasterKey(obj) {
    return obj instanceof MasterKey;
  }
}

/**
 * Key derivation salt.
 * @const {Buffer}
 * @default
 */

MasterKey.SALT = Buffer.from(pkg.name, 'ascii');

/**
 * Key derivation algorithms.
 * @enum {Number}
 * @default
 */

MasterKey.alg = {
  PBKDF2: 0,
  SCRYPT: 1
};

/**
 * Key derivation algorithms by value.
 * @enum {String}
 * @default
 */

MasterKey.algByVal = [
  'PBKDF2',
  'SCRYPT'
];

/*
 * Expose
 */

module.exports = MasterKey;