From f2edf8f4b0c23d7a0ca59369c4b479ad280d7c93 Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Wed, 29 Jun 2016 03:38:50 -0700 Subject: [PATCH] bip39 work. --- lib/bcoin/hd.js | 402 +++++++++++++++++++++++++++++------------- lib/bcoin/mtx.js | 3 +- test/mnemonic-test.js | 16 +- 3 files changed, 288 insertions(+), 133 deletions(-) diff --git a/lib/bcoin/hd.js b/lib/bcoin/hd.js index 9ad01a13..0f57be06 100644 --- a/lib/bcoin/hd.js +++ b/lib/bcoin/hd.js @@ -123,16 +123,30 @@ function Mnemonic(options) { return new Mnemonic(options); this.bits = 128; + this.language = 'english'; this.entropy = null; this.phrase = null; this.passphrase = ''; - this.language = 'english'; - this.seed = null; if (options) this.fromOptions(options); } +/** + * List of languages. + * @const {String[]} + * @default + */ + +Mnemonic.languages = [ + 'simplified chinese', + 'traditional chinese', + 'english', + 'french', + 'italian', + 'japanese' +]; + /** * Inject properties from options object. * @private @@ -143,22 +157,35 @@ Mnemonic.prototype.fromOptions = function fromOptions(options) { if (typeof options === 'string') options = { phrase: options }; - if (options.bits != null) + if (options.bits != null) { + assert(utils.isNumber(options.bits)); + assert(options.bits >= 128); + assert(options.bits % 32 === 0); this.bits = options.bits; + } - this.entropy = options.entropy; - this.phrase = options.phrase; - - if (options.passphrase) - this.passphrase = options.passphrase; - - if (options.language) + if (options.language) { + assert(typeof options.language === 'string'); + assert(Mnemonic.languages.indexOf(options.language) !== -1); this.language = options.language; + } - this.seed = options.seed; + if (options.passphrase) { + assert(typeof options.passphrase === 'string'); + this.passphrase = options.passphrase; + } - assert(this.bits >= 128); - assert(this.bits % 32 === 0); + if (options.phrase) { + this.fromPhrase(options.phrase); + return this; + } + + if (options.entropy) { + this.fromEntropy(options.entropy); + return this; + } + + return this; }; /** @@ -171,24 +198,163 @@ Mnemonic.fromOptions = function fromOptions(options) { return new Mnemonic().fromOptions(options); }; +/** + * Inject properties from entropy. + * @private + * @param {Buffer} entropy + */ + +Mnemonic.prototype.fromEntropy = function fromEntropy(entropy, lang) { + assert(Buffer.isBuffer(entropy)); + assert(entropy.length * 8 >= 128); + 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 + * @returns {Mnemonic} + */ + +Mnemonic.fromEntropy = function fromEntropy(entropy, lang) { + return new Mnemonic().fromEntropy(entropy, lang); +}; + +/** + * Inject properties from phrase. + * @private + * @param {String} phrase + */ + +Mnemonic.prototype.fromPhrase = function fromPhrase(phrase) { + var i, j, bits, pos, oct, bit, b, ent, entropy, lang; + var chk, word, wordlist, index, cbits, cbytes, words; + + assert(typeof phrase === 'string'); + + words = phrase.split(/[ \u3000]/); + + bits = words.length * 11; + + lang = Mnemonic.getLanguage(words[0]); + + ent = new Buffer(Math.ceil(bits / 8)); + ent.fill(0); + + wordlist = Mnemonic.getWordlist(lang); + + for (i = 0; i < words.length; i++) { + word = words[i]; + index = wordlist.indexOf(word); + + if (index === -1) + throw new Error('Could not find word.'); + + for (j = 0; j < 11; j++) { + pos = i * 11 + j; + bit = pos % 8; + oct = (pos - bit) / 8; + b = (index >>> (10 - j)) & 1; + ent[oct] |= b << (7 - bit); + } + } + + // Checksum bits: + cbits = bits % 32; + + // Checksum bytes: + cbytes = Math.ceil(cbits / 8); + + bits -= bits % 32; + entropy = ent.slice(0, ent.length - cbytes); + + ent = ent.slice(ent.length - cbytes); + chk = utils.sha256(entropy); + + for (i = 0; i < cbits; i++) { + bit = i % 8; + oct = (i - bit) / 8; + b = (chk[oct] >>> (7 - bit)) & 1; + j = (ent[oct] >>> (7 - bit)) & 1; + if (b !== j) + throw new Error('Invalid checksum.'); + } + + assert(bits / 8 === entropy.length); + assert(bits >= 128); + assert(bits % 32 === 0); + + this.bits = bits; + this.language = lang; + this.entropy = entropy; + this.phrase = phrase; + + return this; +}; + +/** + * Instantiate mnemonic from a phrase. + * @param {String} phrase + * @returns {Mnemonic} + */ + +Mnemonic.fromPhrase = function fromPhrase(phrase) { + return new Mnemonic().fromPhrase(phrase); +}; + /** * Generate the seed. + * @param {String?} passphrase * @returns {Buffer} pbkdf2 seed. */ -Mnemonic.prototype.toSeed = function toSeed() { - if (this.seed) - return this.seed; +Mnemonic.prototype.toSeed = function toSeed(passphrase) { + if (!passphrase) + passphrase = this.passphrase; - if (!this.phrase) - this.phrase = this.createMnemonic(); + this.passphrase = passphrase; - this.seed = utils.pbkdf2Sync( - nfkd(this.phrase), - nfkd('mnemonic' + this.passphrase), + return utils.pbkdf2Sync( + nfkd(this.getPhrase()), + nfkd('mnemonic' + passphrase), 2048, 64, 'sha512'); +}; - return this.seed; +/** + * Generate seed and create an hd private key. + * @param {String?} passphrase + * @param {(Network|NetworkType)?} network + * @returns {HDPrivateKey} + */ + +Mnemonic.prototype.toKey = function toKey(passphrase, network) { + var seed = this.toSeed(passphrase); + var key = HDPrivateKey.fromSeed(seed, network); + key.mnemonic = this; + return key; +}; + +/** + * Get or generate entropy. + * @returns {Buffer} + */ + +Mnemonic.prototype.getEntropy = function getEntropy() { + if (!this.entropy) + this.entropy = ec.random(this.bits / 8); + + assert(this.bits / 8 === this.entropy.length); + + return this.entropy; }; /** @@ -196,23 +362,22 @@ Mnemonic.prototype.toSeed = function toSeed() { * @returns {String} */ -Mnemonic.prototype.createMnemonic = function createMnemonic() { - var mnemonic = []; - var wordlist = Mnemonic.getWordlist(this.language); - var i, j, bits, entropy, word, oct, bit; +Mnemonic.prototype.getPhrase = function getPhrase() { + var i, j, phrase, wordlist, bits, entropy, index, pos, oct, bit; - if (!this.entropy) - this.entropy = ec.random(this.bits / 8); + if (this.phrase) + return this.phrase; - bits = this.entropy.length * 8; + phrase = []; + wordlist = Mnemonic.getWordlist(this.language); + + entropy = this.getEntropy(); + bits = this.bits; // Append the hash to the entropy to // make things easy when grabbing // the checksum bits. - entropy = Buffer.concat([ - this.entropy, - utils.sha256(this.entropy) - ]); + entropy = Buffer.concat([entropy, utils.sha256(entropy)]); // Include the first `ENT / 32` bits // of the hash (the checksum). @@ -220,24 +385,47 @@ Mnemonic.prototype.createMnemonic = function createMnemonic() { // Build the mnemonic by reading // 11 bit indexes from the entropy. - for (i = 0; i < bits; i++) { - i--; - word = 0; + for (i = 0; i < bits / 11; i++) { + index = 0; for (j = 0; j < 11; j++) { - i++; - bit = i % 8; - oct = (i - bit) / 8; - word <<= 1; - word |= (entropy[oct] >>> (7 - bit)) & 1; + pos = i * 11 + j; + bit = pos % 8; + oct = (pos - bit) / 8; + index <<= 1; + index |= (entropy[oct] >>> (7 - bit)) & 1; } - mnemonic.push(wordlist[word]); + phrase.push(wordlist[index]); } // Japanese likes double-width spaces. if (this.language === 'japanese') - return mnemonic.join('\u3000'); + phrase = phrase.join('\u3000'); + else + phrase = phrase.join(' '); - return mnemonic.join(' '); + this.phrase = phrase; + + return phrase; +}; + +/** + * Determine a single word's language. + * @param {String} word + * @returns {String} Language. + * @throws on not found. + */ + +Mnemonic.getLanguage = function getLanguage(word) { + var i, lang, wordlist; + + for (i = 0; i < Mnemonic.languages.length; i++) { + lang = Mnemonic.languages[i]; + wordlist = Mnemonic.getWordlist(lang); + if (wordlist.indexOf(word) !== -1) + return lang; + } + + throw new Error('Could not determine language.'); }; /** @@ -261,7 +449,7 @@ Mnemonic.getWordlist = function getWordlist(language) { case 'japanese': return require('../../etc/japanese.js'); default: - assert(false, 'Unknown language: ' + language); + throw new Error('Unknown language: ' + language); } }; @@ -273,11 +461,10 @@ Mnemonic.getWordlist = function getWordlist(language) { Mnemonic.prototype.toJSON = function toJSON() { return { bits: this.bits, - entropy: this.entropy ? this.entropy.toString('hex') : null, - phrase: this.phrase, - passphrase: this.passphrase, language: this.language, - seed: this.seed ? this.seed.toString('hex') : null + entropy: this.getEntropy().toString('hex'), + phrase: this.getPhrase(), + passphrase: this.passphrase }; }; @@ -289,25 +476,18 @@ Mnemonic.prototype.toJSON = function toJSON() { Mnemonic.prototype.fromJSON = function fromJSON(json) { assert(utils.isNumber(json.bits)); - assert(!json.entropy || typeof json.entropy === 'string'); - assert(!json.phrase || typeof json.phrase === 'string'); - assert(typeof json.passphrase === 'string'); assert(typeof json.language === 'string'); - assert(!json.seed || typeof json.seed === 'string'); + assert(typeof json.entropy === 'string'); + assert(typeof json.phrase === 'string'); + assert(typeof json.passphrase === 'string'); this.bits = json.bits; - - if (json.entry) - this.entropy = new Buffer(json.entropy, 'hex'); - - if (json.phrase) - this.phrase = json.phrase; - - this.passphrase = json.passphrase; this.language = json.language; + this.entropy = new Buffer(json.entropy, 'hex'); + this.phrase = json.phrase; + this.passphrase = json.passphrase; - if (json.seed) - this.seed = new Buffer(json.seed, 'hex'); + assert(this.bits / 8 === this.entropy.length); return this; }; @@ -331,30 +511,10 @@ Mnemonic.prototype.toRaw = function toRaw(writer) { var p = new BufferWriter(writer); p.writeU16(this.bits); - - if (this.entropy) { - p.writeU8(1); - p.writeBytes(this.entropy); - } else { - p.writeU8(0); - } - - if (this.phrase) { - p.writeU8(1); - p.writeVarString(this.phrase, 'utf8'); - } else { - p.writeU8(0); - } - - p.writeVarString(this.passphrase, 'utf8'); p.writeVarString(this.language, 'utf8'); - - if (this.seed) { - p.writeU8(1); - p.writeBytes(this.seed); - } else { - p.writeU8(0); - } + p.writeBytes(this.getEntropy()); + p.writeVarString(this.getPhrase(), 'utf8'); + p.writeVarString(this.passphrase, 'utf8'); if (!writer) p = p.render(); @@ -372,18 +532,10 @@ Mnemonic.prototype.fromRaw = function fromRaw(data) { var p = new BufferReader(data); this.bits = p.readU16(); - - if (p.readU8() === 1) - this.entropy = p.readBytes(this.bits / 8); - - if (p.readU8() === 1) - this.phrase = p.readVarString('utf8'); - - this.passphrase = p.readVarString('utf8'); this.language = p.readVarString('utf8'); - - if (p.readU8() === 1) - this.seed = p.readBytes(64); + this.entropy = p.readBytes(this.bits / 8); + this.phrase = p.readVarString('utf8'); + this.passphrase = p.readVarString('utf8'); return this; }; @@ -398,6 +550,24 @@ Mnemonic.fromRaw = function fromRaw(data) { return new Mnemonic().fromRaw(data); }; +/** + * Convert the mnemonic to a string. + * @returns {String} + */ + +Mnemonic.prototype.toString = function toString() { + return this.getPhrase(); +}; + +/** + * Inspect the mnemonic. + * @returns {String} + */ + +Mnemonic.prototype.inspect = function inspect() { + return ''; +}; + /** * Test whether an object is a Mnemonic. * @param {Object} obj @@ -1038,31 +1208,8 @@ HDPrivateKey.fromSeed = function fromSeed(seed, network) { HDPrivateKey.prototype.fromMnemonic = function fromMnemonic(mnemonic, network) { if (!(mnemonic instanceof Mnemonic)) mnemonic = new Mnemonic(mnemonic); - - if (mnemonic.seed || mnemonic.phrase || mnemonic.entropy) { - this.fromSeed(mnemonic.toSeed(), network); - this.mnemonic = mnemonic; - return this; - } - - // Very unlikely, but not impossible - // to get an invalid private key. - for (;;) { - try { - this.fromSeed(mnemonic.toSeed(), network); - this.mnemonic = mnemonic; - } catch (e) { - if (e.message === 'Master private key is invalid.') { - mnemonic.seed = null; - mnemonic.phrase = null; - mnemonic.entropy = null; - continue; - } - throw e; - } - break; - } - + this.fromSeed(mnemonic.toSeed(), network); + this.mnemonic = mnemonic; return this; }; @@ -1301,11 +1448,11 @@ HDPrivateKey.prototype.toJSON = function toJSON() { HDPrivateKey.prototype.fromJSON = function fromJSON(json) { assert(json.xprivkey, 'Could not handle key JSON.'); + this.fromBase58(json.xprivkey); + if (json.mnemonic) this.mnemonic = Mnemonic.fromJSON(json.mnemonic); - this.fromBase58(json.xprivkey); - return this; }; @@ -1861,8 +2008,9 @@ function nfkd(str) { * Expose */ -HD.Mnemonic = Mnemonic; -HD.PrivateKey = HDPrivateKey; -HD.PublicKey = HDPublicKey; +exports = HD; +exports.Mnemonic = Mnemonic; +exports.PrivateKey = HDPrivateKey; +exports.PublicKey = HDPublicKey; module.exports = HD; diff --git a/lib/bcoin/mtx.js b/lib/bcoin/mtx.js index dacfbdd5..afdd2f5b 100644 --- a/lib/bcoin/mtx.js +++ b/lib/bcoin/mtx.js @@ -13,7 +13,6 @@ var assert = utils.assert; var constants = bcoin.protocol.constants; var Script = bcoin.script; var opcodes = constants.opcodes; -var HASH160 = constants.ZERO_HASH.slice(0, 20); var FundingError = bcoin.errors.FundingError; /** @@ -1061,7 +1060,7 @@ MTX.prototype.selectCoins = function selectCoins(coins, options) { // use a fake p2pkh output to gauge size. script: options.changeAddress ? Script.fromAddress(options.changeAddress) - : Script.fromPubkeyhash(HASH160), + : Script.fromPubkeyhash(constants.ZERO_HASH160), value: 0 }); diff --git a/test/mnemonic-test.js b/test/mnemonic-test.js index daedd667..4b0bd236 100644 --- a/test/mnemonic-test.js +++ b/test/mnemonic-test.js @@ -19,13 +19,13 @@ describe('Mnemonic', function() { entropy: entropy, passphrase: 'TREZOR' }); - mnemonic.toSeed(); - assert.equal(mnemonic.phrase, phrase); + assert.equal(mnemonic.getPhrase(), phrase); assert.equal(mnemonic.toSeed().toString('hex'), seed.toString('hex')); var key = bcoin.hd.fromMnemonic(mnemonic); assert.equal(key.xprivkey, xpriv); }); }); + mnemonic2.forEach(function(data, i) { var entropy = new Buffer(data.entropy, 'hex'); var phrase = data.mnemonic; @@ -38,11 +38,19 @@ describe('Mnemonic', function() { entropy: entropy, passphrase: passphrase }); - mnemonic.toSeed(); - assert.equal(mnemonic.phrase, phrase); + assert.equal(mnemonic.getPhrase(), phrase); assert.equal(mnemonic.toSeed().toString('hex'), seed.toString('hex')); var key = bcoin.hd.fromMnemonic(mnemonic); assert.equal(key.xprivkey, xpriv); }); }); + + it('should verify phrase', function() { + var m1 = new bcoin.hd.Mnemonic(); + var m2 = bcoin.hd.Mnemonic.fromPhrase(m1.getPhrase()); + assert.deepEqual(m2.getEntropy(), m1.getEntropy()); + assert.equal(m2.bits, m1.bits); + assert.equal(m2.language, m1.language); + assert.deepEqual(m2.toSeed(), m1.toSeed()); + }); });