UNPKG

hsd

Version:
522 lines (414 loc) 11 kB
/*! * mnemonic.js - hd mnemonics 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 sha256 = require('bcrypto/lib/sha256'); const cleanse = require('bcrypto/lib/cleanse'); const random = require('bcrypto/lib/random'); const pbkdf2 = require('bcrypto/lib/pbkdf2'); const sha512 = require('bcrypto/lib/sha512'); const wordlist = require('./wordlist'); const common = require('./common'); const nfkd = require('./nfkd'); /** @typedef {import('../types').BufioWriter} BufioWriter */ /* * Constants */ const wordlistCache = Object.create(null); /** * @typedef {Object} MnemonicOptions * @property {Number?} [bits] * @property {Buffer?} [entropy] * @property {String?} [phrase] * @property {String?} [language] */ /** * HD Mnemonic * @alias module:hd.Mnemonic */ class Mnemonic extends bio.Struct { /** * Create a mnemonic. * @constructor * @param {String|MnemonicOptions} [options] */ constructor(options) { super(); this.bits = 256; this.language = 'english'; this.entropy = null; this.phrase = null; if (options) this.fromOptions(options); } /** * Inject properties from options object. * @param {String|MnemonicOptions} options */ fromOptions(options) { if (typeof options === 'string') options = { phrase: options }; if (options.bits != null) { assert((options.bits & 0xffff) === options.bits); assert(options.bits >= common.MIN_ENTROPY); assert(options.bits <= common.MAX_ENTROPY); assert(options.bits % 32 === 0); this.bits = options.bits; } if (options.language) { assert(typeof options.language === 'string'); if (Mnemonic.languages.indexOf(options.language) === -1) throw new Error('Unknown language.'); this.language = options.language; } if (options.phrase) { this.fromPhrase(options.phrase); return this; } if (options.entropy) { this.fromEntropy(options.entropy); return this; } return this; } /** * Destroy the mnemonic (zeroes entropy). */ destroy() { this.bits = common.MIN_ENTROPY; this.language = 'english'; if (this.entropy) { cleanse(this.entropy); this.entropy = null; } this.phrase = null; } /** * Generate the seed. * @param {String?} [passphrase] * @returns {Buffer} pbkdf2 seed. */ toSeed(passphrase) { if (!passphrase) passphrase = ''; const phrase = nfkd(this.getPhrase()); const passwd = nfkd(`mnemonic${passphrase}`); return pbkdf2.derive(sha512, Buffer.from(phrase, 'utf8'), Buffer.from(passwd, 'utf8'), 2048, 64); } /** * Get or generate entropy. * @returns {Buffer} */ getEntropy() { if (!this.entropy) this.entropy = random.randomBytes(this.bits / 8); assert(this.bits / 8 === this.entropy.length); return this.entropy; } /** * Generate a mnemonic phrase from chosen language. * @returns {String} */ getPhrase() { if (this.phrase) return this.phrase; // Include the first `ENT / 32` bits // of the hash (the checksum). const wbits = this.bits + (this.bits / 32); // Get entropy and checksum. const entropy = this.getEntropy(); const chk = sha256.digest(entropy); // Append the hash to the entropy to // make things easy when grabbing // the checksum bits. const size = Math.ceil(wbits / 8); const data = Buffer.allocUnsafe(size); entropy.copy(data, 0); chk.copy(data, entropy.length); // Build the mnemonic by reading // 11 bit indexes from the entropy. const list = Mnemonic.getWordlist(this.language); /** @type {String[]} */ const words = []; for (let i = 0; i < wbits / 11; i++) { let index = 0; for (let j = 0; j < 11; j++) { const pos = i * 11 + j; const bit = pos % 8; const oct = (pos - bit) / 8; index <<= 1; index |= (data[oct] >>> (7 - bit)) & 1; } words.push(list.words[index]); } let phrase; // Japanese likes double-width spaces. if (this.language === 'japanese') phrase = words.join('\u3000'); else phrase = words.join(' '); this.phrase = phrase; return phrase; } /** * Inject properties from phrase. * @param {String} phrase */ fromPhrase(phrase) { assert(typeof phrase === 'string'); assert(phrase.length <= 1000); const words = phrase.trim().split(/[\s\u3000]+/); const wbits = words.length * 11; const cbits = wbits % 32; assert(cbits !== 0, 'Invalid checksum.'); const bits = wbits - cbits; assert(bits >= common.MIN_ENTROPY); assert(bits <= common.MAX_ENTROPY); assert(bits % 32 === 0); const size = Math.ceil(wbits / 8); const data = Buffer.allocUnsafe(size); data.fill(0); const lang = Mnemonic.getLanguage(words[0]); const list = Mnemonic.getWordlist(lang); // Rebuild entropy bytes. for (let i = 0; i < words.length; i++) { const word = words[i]; const index = list.map[word]; if (index == null) throw new Error('Could not find word.'); for (let j = 0; j < 11; j++) { const pos = i * 11 + j; const bit = pos % 8; const oct = (pos - bit) / 8; const val = (index >>> (10 - j)) & 1; data[oct] |= val << (7 - bit); } } const cbytes = Math.ceil(cbits / 8); const entropy = data.slice(0, data.length - cbytes); const chk1 = data.slice(data.length - cbytes); const chk2 = sha256.digest(entropy); // Verify checksum. for (let i = 0; i < cbits; i++) { const bit = i % 8; const oct = (i - bit) / 8; const b1 = (chk1[oct] >>> (7 - bit)) & 1; const b2 = (chk2[oct] >>> (7 - bit)) & 1; if (b1 !== b2) throw new Error('Invalid checksum.'); } assert(bits / 8 === entropy.length); this.bits = bits; this.language = lang; this.entropy = entropy; // Japanese likes double-width spaces. if (this.language === 'japanese') this.phrase = words.join('\u3000'); else this.phrase = words.join(' '); return this; } /** * Instantiate mnemonic from a phrase (validates checksum). * @param {String} phrase * @returns {Mnemonic} * @throws on bad checksum */ static fromPhrase(phrase) { return new this().fromPhrase(phrase); } /** * Inject properties from entropy. * @private * @param {Buffer} entropy * @param {String?} [lang] */ fromEntropy(entropy, lang) { assert(Buffer.isBuffer(entropy)); assert(entropy.length * 8 >= common.MIN_ENTROPY); assert(entropy.length * 8 <= common.MAX_ENTROPY); assert((entropy.length * 8) % 32 === 0); assert(!lang || Mnemonic.languages.indexOf(lang) !== -1); this.entropy = entropy; this.bits = entropy.length * 8; if (lang) this.language = lang; return this; } /** * Instantiate mnemonic from entropy. * @param {Buffer} entropy * @param {String?} lang * @returns {Mnemonic} */ static fromEntropy(entropy, lang) { return new this().fromEntropy(entropy, lang); } /** * Determine a single word's language. * @param {String} word * @returns {String} Language. * @throws on not found. */ static getLanguage(word) { for (const lang of Mnemonic.languages) { const list = Mnemonic.getWordlist(lang); if (list.map[word] != null) return lang; } throw new Error('Could not determine language.'); } /** * Retrieve the wordlist for a language. * @param {String} lang * @returns {WordList} */ static getWordlist(lang) { const cache = wordlistCache[lang]; if (cache) return cache; const words = wordlist.get(lang); const list = new WordList(words); wordlistCache[lang] = list; return list; } /** * Convert mnemonic to a json-friendly object. * @returns {Object} */ getJSON() { return { bits: this.bits, language: this.language, entropy: this.getEntropy().toString('hex'), phrase: this.getPhrase() }; } /** * Inject properties from json object. * @param {Object} json */ fromJSON(json) { assert(json); assert((json.bits & 0xffff) === json.bits); assert(typeof json.language === 'string'); assert(typeof json.entropy === 'string'); assert(typeof json.phrase === 'string'); assert(json.bits >= common.MIN_ENTROPY); assert(json.bits <= common.MAX_ENTROPY); assert(json.bits % 32 === 0); assert(json.bits / 8 === json.entropy.length / 2); this.bits = json.bits; this.language = json.language; this.entropy = Buffer.from(json.entropy, 'hex'); this.phrase = json.phrase; return this; } /** * Calculate serialization size. * @returns {Number} */ getSize() { let size = 0; size += 3; size += this.getEntropy().length; return size; } /** * Write the mnemonic to a buffer writer. * @param {BufioWriter} bw * @returns {BufioWriter} */ write(bw) { const lang = Mnemonic.languages.indexOf(this.language); assert(lang !== -1); bw.writeU16(this.bits); bw.writeU8(lang); bw.writeBytes(this.getEntropy()); return bw; } /** * Inject properties from buffer reader. * @param {bio.BufferReader} br */ read(br) { const bits = br.readU16(); assert(bits >= common.MIN_ENTROPY); assert(bits <= common.MAX_ENTROPY); assert(bits % 32 === 0); const language = Mnemonic.languages[br.readU8()]; assert(language); this.bits = bits; this.language = language; this.entropy = br.readBytes(bits / 8); return this; } /** * Convert the mnemonic to a string. * @returns {String} */ toString() { return this.getPhrase(); } /** * Inspect the mnemonic. * @returns {String} */ format() { return `<Mnemonic: ${this.getPhrase()}>`; } /** * Test whether an object is a Mnemonic. * @param {Object} obj * @returns {Boolean} */ static isMnemonic(obj) { return obj instanceof Mnemonic; } } /** * List of languages. * @const {String[]} * @default */ Mnemonic.languages = [ 'simplified chinese', 'traditional chinese', 'english', 'french', 'italian', 'japanese', 'portuguese', 'spanish' ]; /** * Word List * @ignore */ class WordList { /** * Create word list. * @constructor * @ignore * @param {String[]} words */ constructor(words) { this.words = words; this.map = Object.create(null); for (let i = 0; i < words.length; i++) { const word = words[i]; this.map[word] = i; } } } /* * Expose */ module.exports = Mnemonic;