/*!
* 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;