diff --git a/.gitignore b/.gitignore index 74022a3..84fabb7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules -bip39.js +.nyc_output/ +npm-debug.log diff --git a/README.md b/README.md index c0eb834..3ae3282 100644 --- a/README.md +++ b/README.md @@ -16,41 +16,36 @@ When a checksum is invalid, warn the user that the phrase is not something gener However, there should be other checks in place, such as checking to make sure the user is inputting 12 words or more separated by a space. ie. `phrase.trim().split(/\s+/g).length >= 12` -## Usage -`npm install bip39` - -```javascript -var bip39 = require('bip39') - -var mnemonic = bip39.entropyToMnemonic('133755ff') // hex input, defaults to BIP39 English word list -// 'basket rival lemon' - - -bip39.mnemonicToEntropy(mnemonic) // hex input, defaults to BIP39 English word list -// '133755ff' - -// Generate a random mnemonic using crypto.randomBytes -mnemonic = bip39.generateMnemonic() // strength defaults to 128 bits -// 'seed sock milk update focus rotate barely fade car face mechanic mercy' +## Examples +``` js +// Generate a random mnemonic (uses crypto.randomBytes under the hood), defaults to 128-bits of entropy +var mnemonic = bip39.generateMnemonic() +// => 'seed sock milk update focus rotate barely fade car face mechanic mercy' bip39.mnemonicToSeedHex('basket actual') -// '5cf2d4a8b0355e90295bdfc565a022a409af063d5365bb57bf74d9528f494bfa4400f53d8349b80fdae44082d7f9541e1dba2b003bcfec9d0d53781ca676651f' +// => '5cf2d4a8b0355e90295bdfc565a022a409af063d5365bb57bf74d9528f494bfa4400f53d8349b80fdae44082d7f9541e1dba2b003bcfec9d0d53781ca676651f' bip39.mnemonicToSeed('basket actual') -// +// => bip39.validateMnemonic(mnemonic) -// true +// => true bip39.validateMnemonic('basket actual') -// false +// => false ``` -### Browser -Compile `bip39.js` with the following command: +``` js +var bip39 = require('bip39') - $ npm run compile +// defaults to BIP39 English word list +// uses HEX strings for entropy +var mnemonic = bip39.entropyToMnemonic('133755ff') +// => basket rival lemon -After loading this file in your browser, you will be able to use the global `bip39` object. +// reversible +bip39.mnemonicToEntropy(mnemonic) +// => '133755ff' +``` diff --git a/index.js b/index.js index 7ee6d50..621c23f 100644 --- a/index.js +++ b/index.js @@ -1,4 +1,3 @@ -var assert = require('assert') var createHash = require('create-hash') var pbkdf2 = require('pbkdf2').pbkdf2Sync var randomBytes = require('randombytes') @@ -28,13 +27,10 @@ function mnemonicToEntropy (mnemonic, wordlist) { wordlist = wordlist || DEFAULT_WORDLIST var words = unorm.nfkd(mnemonic).split(' ') - assert(words.length % 3 === 0, 'Invalid mnemonic') - - var belongToList = words.every(function (word) { - return wordlist.indexOf(word) > -1 - }) - - assert(belongToList, 'Invalid mnemonic') + if (words.length % 3 !== 0) throw new Error('Invalid mnemonic') + if (words.some(function (word) { + return wordlist.indexOf(word) === -1 + })) throw new Error('Invalid mnemonic') // convert word indices to 11 bit binary strings var bits = words.map(function (word) { @@ -57,12 +53,14 @@ function mnemonicToEntropy (mnemonic, wordlist) { // recreate properly chunked and padded bits to get the properly padded checksum var bits2 = (entropy + newChecksum).match(/(.{1,11})/g).map(function (index) { return lpad(index, '0', 11) - }).join('') + var dividerIndex2 = Math.floor(bits2.length / 33) * 32 var newChecksum2 = bits2.slice(dividerIndex2) - assert(newChecksum2 === checksum, 'Invalid mnemonic checksum') + if (newChecksum2 !== checksum) { + throw new Error('Invalid mnemonic checksum') + } return entropyBuffer.toString('hex') } @@ -83,7 +81,7 @@ function entropyToMnemonic (entropy, wordlist) { return wordlist[index] }) - return wordlist == JAPANESE_WORDLIST ? words.join('\u3000') : words.join(' ') + return wordlist === JAPANESE_WORDLIST ? words.join('\u3000') : words.join(' ') } function generateMnemonic (strength, rng, wordlist) { diff --git a/package.json b/package.json index 340d616..c0cf9d5 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,10 @@ "description": "Bitcoin BIP39: Mnemonic code for generating deterministic keys", "main": "index.js", "scripts": { - "test": "mocha --reporter list test/*.js", - "compile": "browserify index.js -s bip39 > bip39.js" + "coverage": "nyc --branches 100 --functions 100 --check-coverage npm run unit", + "standard": "standard", + "test": "npm run standard && npm run unit", + "unit": "tape test/*.js" }, "author": "Wei Lu", "contributors": [ @@ -22,14 +24,14 @@ "license": "ISC", "dependencies": { "create-hash": "^1.1.0", - "pbkdf2": "^3.0.0", + "pbkdf2": "^3.0.9", "randombytes": "^2.0.1", "unorm": "^1.3.3" }, "devDependencies": { - "browserify": "^9.0.0", - "mocha": "^2.2.0", - "mock-require": "^1.0.5", - "standard": "*" + "nyc": "^8.3.0", + "proxyquire": "^1.7.10", + "standard": "*", + "tape": "^4.6.2" } } diff --git a/test/index.js b/test/index.js index 58fadb2..7dd5ea0 100644 --- a/test/index.js +++ b/test/index.js @@ -1,196 +1,118 @@ -/* global describe it */ - -var assert = require('assert') -var mock = require('mock-require') - -mock('randombytes', function (size) { - return new Buffer('qwertyuiopasdfghjklzxcvbnm[];,./'.slice(0, size)) -}) - -var BIP39 = require('../index.js') - -var wordlists = { +var bip39 = require('../') +var proxyquire = require('proxyquire') +var WORDLISTS = { english: require('../wordlists/en.json'), japanese: require('../wordlists/ja.json'), custom: require('./wordlist.json') } var vectors = require('./vectors.json') +var test = require('tape') -describe('BIP39', function () { - describe('mnemonicToSeedHex', function () { - this.timeout(20000) +function testVector (description, wordlist, password, v, i) { + var ventropy = v[0] + var vmnemonic = v[1] + var vseedHex = v[2] - vectors.english.forEach(function (v, i) { - it('works for tests vector ' + i, function () { - assert.equal(BIP39.mnemonicToSeedHex(v[1], 'TREZOR'), v[2]) - }) - }) - }) + test('for ' + description + ' test vector ' + i, function (t) { + t.plan(5) - describe('mnemonicToEntropy', function () { - vectors.english.forEach(function (v, i) { - it('works for tests vector ' + i, function () { - assert.equal(BIP39.mnemonicToEntropy(v[1]), v[0]) - }) - }) - - vectors.japanese.forEach(function (v, i) { - it('works for japanese tests vector ' + i, function () { - assert.equal(BIP39.mnemonicToEntropy(v[1], wordlists.japanese), v[0]) - }) - }) - - vectors.custom.forEach(function (v, i) { - it('works for custom test vector ' + i, function () { - assert.equal(BIP39.mnemonicToEntropy(v[1], wordlists.custom), v[0]) - }) - }) - }) + t.equal(bip39.mnemonicToEntropy(vmnemonic, wordlist), ventropy, 'mnemonicToEntropy returns ' + ventropy.slice(0, 40) + '...') + t.equal(bip39.mnemonicToSeedHex(vmnemonic, password), vseedHex, 'mnemonicToSeedHex returns ' + vseedHex.slice(0, 40) + '...') - describe('entropyToMnemonic', function () { - vectors.english.forEach(function (v, i) { - it('works for tests vector ' + i, function () { - assert.equal(BIP39.entropyToMnemonic(v[0]), v[1]) - }) - }) - - vectors.japanese.forEach(function (v, i) { - it('works for japanese test vector ' + i, function () { - assert.equal(BIP39.entropyToMnemonic(v[0], wordlists.japanese), v[1]) - }) - }) - - vectors.custom.forEach(function (v, i) { - it('works for custom test vector ' + i, function () { - assert.equal(BIP39.entropyToMnemonic(v[0], wordlists.custom), v[1]) - }) - }) - }) + t.equal(bip39.entropyToMnemonic(ventropy, wordlist), vmnemonic, 'entropyToMnemonic returns ' + vmnemonic.slice(0, 40) + '...') - describe('generateMnemonic', function () { - vectors.english.forEach(function (v, i) { - it('works for tests vector ' + i, function () { - function rng () { return new Buffer(v[0], 'hex') } - - assert.equal(BIP39.generateMnemonic(undefined, rng), v[1]) - }) - }) - - it('can vary generated entropy bit length', function () { - var mnemonic = BIP39.generateMnemonic(96) - var words = mnemonic.split(' ') - - assert.equal(words.length, 9) - }) - - it('defaults to randombytes for the RNG', function () { - assert.equal(BIP39.generateMnemonic(32), 'imitate robot frequent') - }) - - it('allows a custom RNG to be used', function () { - var rng = function (size) { - var buffer = new Buffer(size) - buffer.fill(4) // guaranteed random - return buffer - } - - var mnemonic = BIP39.generateMnemonic(64, rng) - assert.equal(mnemonic, 'advice cage absurd amount doctor act') - }) - - it('adheres to a custom wordlist', function () { - var rng = function (size) { - var buffer = new Buffer(size) - buffer.fill(4) // guaranteed random - return buffer - } - - var mnemonic = BIP39.generateMnemonic(64, rng, wordlists.custom) - assert.equal(mnemonic, 'adv1c3 cag3 ab5urd am0unt d0ct0r act') - }) + function rng () { return new Buffer(ventropy, 'hex') } + t.equal(bip39.generateMnemonic(undefined, rng, wordlist), vmnemonic, 'generateMnemonic returns RNG entropy unmodified') + t.equal(bip39.validateMnemonic(vmnemonic, wordlist), true, 'validateMnemonic returns true') }) +} - describe('validateMnemonic', function () { - vectors.english.forEach(function (v, i) { - it('passes check ' + i, function () { - assert(BIP39.validateMnemonic(v[1])) - }) - }) - - describe('with a custom wordlist', function () { - vectors.custom.forEach(function (v, i) { - it('passes custom check ' + i, function () { - assert(BIP39.validateMnemonic(v[1], wordlists.custom)) - }) - }) - }) - - it('fails for mnemonics of wrong length', function () { - assert(!BIP39.validateMnemonic('sleep kitten')) - assert(!BIP39.validateMnemonic('sleep kitten sleep kitten sleep kitten')) - }) - - it('fails for mnemonics that contains words not from the word list', function () { - assert(!BIP39.validateMnemonic('turtle front uncle idea crush write shrug there lottery flower risky shell')) - }) - - it('fails for mnemonics of invalid checksum', function () { - assert(!BIP39.validateMnemonic('sleep kitten sleep kitten sleep kitten sleep kitten sleep kitten sleep kitten')) - }) - }) +vectors.english.forEach(function (v, i) { testVector('English', undefined, 'TREZOR', v, i) }) +vectors.japanese.forEach(function (v, i) { testVector('Japanese', WORDLISTS.japanese, '㍍ガバヴァぱばぐゞちぢ十人十色', v, i) }) +vectors.custom.forEach(function (v, i) { testVector('Custom', WORDLISTS.custom, undefined, v, i) }) + +test('UTF8 passwords', function (t) { + t.plan(vectors.japanese.length * 2) + + vectors.japanese.forEach(function (v) { + var vmnemonic = v[1] + var vseedHex = v[2] + + var password = '㍍ガバヴァぱばぐゞちぢ十人十色' + var normalizedPassword = 'メートルガバヴァぱばぐゞちぢ十人十色' - describe('utf8 passwords', function () { - vectors.japanese.forEach(function (v) { - it('creates the correct seed', function () { - var utf8Password = '㍍ガバヴァぱばぐゞちぢ十人十色' - assert.equal(BIP39.mnemonicToSeedHex(v[1], utf8Password), v[2]) - }) - - it('works with already normalized password', function () { - var normalizedPassword = 'メートルガバヴァぱばぐゞちぢ十人十色' - assert.equal(BIP39.mnemonicToSeedHex(v[1], normalizedPassword), v[2]) - }) - }) + t.equal(bip39.mnemonicToSeedHex(vmnemonic, password), vseedHex, 'mnemonicToSeedHex normalizes passwords') + t.equal(bip39.mnemonicToSeedHex(vmnemonic, normalizedPassword), vseedHex, 'mnemonicToSeedHex leaves normalizes passwords as-is') }) +}) - describe('Examples in readme', function () { - var bip39 = BIP39 +test('README example 1', function (t) { + // defaults to BIP39 English word list + var entropy = '133755ff' + var mnemonic = bip39.entropyToMnemonic(entropy) - var mnemonic = bip39.entropyToMnemonic('133755ff') // hex input, defaults to BIP39 English word list - // 'basket rival lemon' - assert.ok((/^\w+ \w+ \w+$/).test(mnemonic)) + t.plan(2) + t.equal(mnemonic, 'basket rival lemon') - var temp = bip39.mnemonicToEntropy(mnemonic) // hex input, defaults to BIP39 English word list - // '133755ff' - assert.equal(temp, '133755ff') + // reversible + t.equal(bip39.mnemonicToEntropy(mnemonic), entropy) +}) - // Generate a random mnemonic using crypto.randomBytes - mnemonic = bip39.generateMnemonic() // strength defaults to 128 bits - // 'bench maximum balance appear cousin negative muscle inform enjoy chief vocal hello' - assert.ok(/^(\w+ ){11}\w+$/.test(mnemonic)) +test('README example 2', function (t) { + var stub = { + randombytes: function (size) { + return new Buffer('qwertyuiopasdfghjklzxcvbnm[];,./'.slice(0, size)) + } + } + var proxiedbip39 = proxyquire('../', stub) - var str = bip39.mnemonicToSeedHex('basket actual') - // '5cf2d4a8b0355e90295bdfc565a022a409af063d5365bb57bf74d9528f494bfa4400f53d8349b80fdae44082d7f9541e1dba2b003bcfec9d0d53781ca676651f' - assert.equal(str, '5cf2d4a8b0355e90295bdfc565a022a409af063d5365bb57bf74d9528f494bfa4400f53d8349b80fdae44082d7f9541e1dba2b003bcfec9d0d53781ca676651f') + // mnemonic strength defaults to 128 bits + var mnemonic = proxiedbip39.generateMnemonic() - var buff = bip39.mnemonicToSeed('basket actual') - var fiveC = 5 * 16 + 12 - assert.equal(buff[0], fiveC) - // + t.plan(2) + t.equal(mnemonic, 'imitate robot frame trophy nuclear regret saddle around inflict case oil spice') + t.equal(bip39.validateMnemonic(mnemonic), true) +}) - var bool = bip39.validateMnemonic(mnemonic) - // true - assert.ok(bool) +test('README example 3', function (t) { + var mnemonic = 'basket actual' + var seed = bip39.mnemonicToSeed(mnemonic) + var seedHex = bip39.mnemonicToSeedHex(mnemonic) - bool = bip39.validateMnemonic('basket actual') + t.plan(3) + t.equal(seed.toString('hex'), seedHex) + t.equal(seedHex, '5cf2d4a8b0355e90295bdfc565a022a409af063d5365bb57bf74d9528f494bfa4400f53d8349b80fdae44082d7f9541e1dba2b003bcfec9d0d53781ca676651f') + t.equal(bip39.validateMnemonic(mnemonic), false) +}) - // false - assert.ok(!bool) - }) +test('generateMnemonic can vary entropy length', function (t) { + var words = bip39.generateMnemonic(96).split(' ') + + t.plan(1) + t.equal(words.length, 9, 'can vary generated entropy bit length') +}) - it('exposes standard wordlists', function () { - assert(BIP39.wordlists.EN) - assert.equal(BIP39.wordlists.EN.length, 2048) +test('generateMnemonic only requests the exact amount of data from an RNG', function (t) { + t.plan(1) + + bip39.generateMnemonic(96, function (size) { + t.equal(size, 96 / 8) + return new Buffer(size) }) }) + +test('validateMnemonic', function (t) { + t.plan(4) + + t.equal(bip39.validateMnemonic('sleep kitten'), false, 'fails for a mnemonic that is too short') + t.equal(bip39.validateMnemonic('sleep kitten sleep kitten sleep kitten'), false, 'fails for a mnemonic that is too short') + t.equal(bip39.validateMnemonic('turtle front uncle idea crush write shrug there lottery flower risky shell'), false, 'fails if mnemonic words are not in the word list') + t.equal(bip39.validateMnemonic('sleep kitten sleep kitten sleep kitten sleep kitten sleep kitten sleep kitten'), false, 'fails for invalid checksum') +}) + +test('exposes standard wordlists', function (t) { + t.plan(2) + t.same(bip39.wordlists.EN, WORDLISTS.english) + t.equal(bip39.wordlists.EN.length, 2048) +}) diff --git a/test/vectors.json b/test/vectors.json index fc32ddd..12b0515 100644 --- a/test/vectors.json +++ b/test/vectors.json @@ -130,12 +130,12 @@ [ "00000000000000000000000000000000", "aband0n aband0n aband0n aband0n aband0n aband0n aband0n aband0n aband0n aband0n aband0n ab0ut", - "c55257c360c07c72029aebc1b53c05ed0362ada38ead3e3e9efa3708e53495531f09a6987599d18264c1e1c92f2cf141630c7a3c4ab7c81b2f001698e7463b04" + "a3f1b782bc3315cea2f93e8a6db3190a18b4870afe6fb40f6e3ac2fdc2216dfe33b7ef97e31845f710231d8a7a30a49fe82df5707f4a35917a92337a4da8184d" ], [ "15da872c95a13dd738fbf50e427583ad61f18fd99f628c417a61cf8343c90419", "b3y0nd 5tag3 5l33p cl1p b3cau53 tw15t t0k3n l3af at0m b3auty g3n1u5 f00d bu51n355 51d3 gr1d unabl3 m1ddl3 arm3d 0b53rv3 pa1r cr0uch t0n1ght away c0c0nut", - "b15509eaa2d09d3efd3e006ef42151b30367dc6e3aa5e44caba3fe4d3e352e65101fbdb86a96776b91946ff06f8eac594dc6ee1d3e82a42dfe1b40fef6bcc3fd" + "2e9a0929ca67cd8c1a11cf71abee2c8b51c2555758f37a133ea9f491f55c352a4a831b2bf8dda61e9a4ed0ffeeae7324704f26d1304ab35ffebf8c997f73badd" ] ], "japanese": [