bip39 work.

This commit is contained in:
Christopher Jeffrey 2016-06-29 03:38:50 -07:00
parent 7821d65db4
commit f2edf8f4b0
No known key found for this signature in database
GPG Key ID: 8962AB9DE6666BBD
3 changed files with 288 additions and 133 deletions

View File

@ -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 '<Mnemonic: ' + this.getPhrase() + '>';
};
/**
* 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;

View File

@ -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
});

View File

@ -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());
});
});