Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Limit ENT to 128-256, multiple of 4 (cont.) #49

Merged
merged 11 commits into from
May 23, 2017
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ var bip39 = require('bip39')

// defaults to BIP39 English word list
// uses HEX strings for entropy
var mnemonic = bip39.entropyToMnemonic('133755ff')
// => basket rival lemon
var mnemonic = bip39.entropyToMnemonic('00000000000000000000000000000000')
// => zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong

// reversible
bip39.mnemonicToEntropy(mnemonic)
Expand Down
57 changes: 30 additions & 27 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,21 @@ var randomBytes = require('randombytes')
// use unorm until String.prototype.normalize gets better browser support
var unorm = require('unorm')

function lpad (str, padString, length) {
while (str.length < length) str = padString + str
return str
}

var ENGLISH_WORDLIST = require('./wordlists/english.json')
var FRENCH_WORDLIST = require('./wordlists/french.json')
var ITALIAN_WORDLIST = require('./wordlists/italian.json')
var JAPANESE_WORDLIST = require('./wordlists/japanese.json')
var SPANISH_WORDLIST = require('./wordlists/spanish.json')
var DEFAULT_WORDLIST = ENGLISH_WORDLIST

var INVALID_MNEMONIC = 'Invalid mnemonic'
var INVALID_ENTROPY = 'Invalid entropy'

function salt (password) {
return 'mnemonic' + (password || '')
}
Expand All @@ -31,21 +39,19 @@ function mnemonicToEntropy (mnemonic, wordlist) {
wordlist = wordlist || DEFAULT_WORDLIST

var words = unorm.nfkd(mnemonic).split(' ')
if (words.length % 3 !== 0) throw new Error('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')
})) throw new Error(INVALID_MNEMONIC)

// convert word indices to 11 bit binary strings
var bits = words.map(function (word) {
var index = wordlist.indexOf(word)
return lpad(index.toString(2), '0', 11)
}).join('')

// max entropy is 1024; (1024×8)+((1024×8)÷32) = 8448
if (bits.length > 8448) {
throw new Error('Invalid mnemonic')
}
if (bits.length < 128) throw new Error(INVALID_MNEMONIC)
if (bits.length > 264) throw new Error(INVALID_MNEMONIC)
Copy link
Contributor Author

@dcousens dcousens May 12, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rubensayshi I haven't got time to check, but why does a 24-word mnemonic decode to 264 bits of ENT bits?


// split the binary string into ENT/CS
var dividerIndex = Math.floor(bits.length / 33) * 32
Expand All @@ -56,25 +62,28 @@ function mnemonicToEntropy (mnemonic, wordlist) {
var entropyBytes = entropy.match(/(.{1,8})/g).map(function (bin) {
return parseInt(bin, 2)
})
if (entropy.length % 4 !== 0) throw new TypeError(INVALID_ENTROPY)
var entropyBuffer = new Buffer(entropyBytes)
var newChecksum = checksumBits(entropyBuffer)

var newChecksum = checksumBits(entropyBuffer)
if (newChecksum !== checksum) throw new Error('Invalid mnemonic checksum')

return entropyBuffer.toString('hex')
}

function entropyToMnemonic (entropy, wordlist) {
function entropyToMnemonic (entropyHex, wordlist) {
wordlist = wordlist || DEFAULT_WORDLIST

var entropyBuffer = new Buffer(entropy, 'hex')
// 128 <= ENT <= 256
if (entropyHex.length < 32) throw new Error(INVALID_ENTROPY)
if (entropyHex.length > 64) throw new Error(INVALID_ENTROPY)

if (entropyBuffer.length === 0 || entropyBuffer.length > 1024 || entropyBuffer.length % 4 !== 0) {
throw new Error('Invalid entropy')
}
// multiple of 4
if (entropyHex.length % 8 !== 0) throw new Error(INVALID_ENTROPY)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

boo hex


var entropyBits = bytesToBinary([].slice.call(entropyBuffer))
var checksum = checksumBits(entropyBuffer)
var entropy = new Buffer(entropyHex, 'hex')
var entropyBits = bytesToBinary([].slice.call(entropy))
var checksum = checksumBits(entropy)

var bits = entropyBits + checksum
var chunks = bits.match(/(.{1,11})/g)
Expand All @@ -90,6 +99,7 @@ function entropyToMnemonic (entropy, wordlist) {

function generateMnemonic (strength, rng, wordlist) {
strength = strength || 128
if (strength % 32 !== 0) throw new TypeError(INVALID_ENTROPY)
Copy link
Contributor Author

@dcousens dcousens May 12, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not really needed here, would be caught by entropyToMnemonic, alas lets save the RNG some time?

rng = rng || randomBytes

var hex = rng(strength / 8).toString('hex')
Expand All @@ -106,6 +116,12 @@ function validateMnemonic (mnemonic, wordlist) {
return true
}

function bytesToBinary (bytes) {
return bytes.map(function (x) {
return lpad(x.toString(2), '0', 8)
}).join('')
}

function checksumBits (entropyBuffer) {
var hash = createHash('sha256').update(entropyBuffer).digest()

Expand All @@ -116,19 +132,6 @@ function checksumBits (entropyBuffer) {
return bytesToBinary([].slice.call(hash)).slice(0, CS)
}

// =========== helper methods from bitcoinjs-lib ========

function bytesToBinary (bytes) {
return bytes.map(function (x) {
return lpad(x.toString(2), '0', 8)
}).join('')
}

function lpad (str, padString, length) {
while (str.length < length) str = padString + str
return str
}

module.exports = {
mnemonicToSeed: mnemonicToSeed,
mnemonicToSeedHex: mnemonicToSeedHex,
Expand Down
58 changes: 9 additions & 49 deletions test/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
var bip39 = require('../')
var proxyquire = require('proxyquire')
var download = require('../util/wordlists').download
var WORDLISTS = {
english: require('../wordlists/english.json'),
Expand All @@ -15,7 +14,7 @@ function testVector (description, wordlist, password, v, i) {
var vmnemonic = v[1]
var vseedHex = v[2]

test('for ' + description + ' test vector ' + i, function (t) {
test('for ' + description + '(' + i + '), ' + ventropy, function (t) {
t.plan(5)

t.equal(bip39.mnemonicToEntropy(vmnemonic, wordlist), ventropy, 'mnemonicToEntropy returns ' + ventropy.slice(0, 40) + '...')
Expand All @@ -37,15 +36,15 @@ test('invalid entropy', function (t) {

t.throws(function () {
bip39.entropyToMnemonic(new Buffer('', 'hex'))
}, /^Invalid entropy$/, 'throws for empty entropy')
}, /^Error: Invalid entropy$/, 'throws for empty entropy')

t.throws(function () {
bip39.entropyToMnemonic(new Buffer('000000', 'hex'))
}, /^Invalid entropy$/, 'throws for entropy that\'s not a multitude of 4 bytes')
}, /^Error: Invalid entropy$/, 'throws for entropy that\'s not a multitude of 4 bytes')

t.throws(function () {
bip39.entropyToMnemonic(new Buffer(new Array(1028 + 1).join('00'), 'hex'))
}, /^Invalid entropy$/, 'throws for entropy that is larger than 1024')
}, /^Error: Invalid entropy$/, 'throws for entropy that is larger than 1024')
})

test('UTF8 passwords', function (t) {
Expand All @@ -63,57 +62,18 @@ test('UTF8 passwords', function (t) {
})
})

test('README example 1', function (t) {
// defaults to BIP39 English word list
var entropy = '133755ff'
var mnemonic = bip39.entropyToMnemonic(entropy)

t.plan(2)
t.equal(mnemonic, 'basket rival lemon')

// reversible
t.equal(bip39.mnemonicToEntropy(mnemonic), entropy)
})

test('README example 2', function (t) {
var stub = {
randombytes: function (size) {
return new Buffer('qwertyuiopasdfghjklzxcvbnm[];,./'.slice(0, size))
}
}
var proxiedbip39 = proxyquire('../', stub)

// mnemonic strength defaults to 128 bits
var mnemonic = proxiedbip39.generateMnemonic()

t.plan(2)
t.equal(mnemonic, 'imitate robot frame trophy nuclear regret saddle around inflict case oil spice')
t.equal(bip39.validateMnemonic(mnemonic), true)
})

test('README example 3', function (t) {
var mnemonic = 'basket actual'
var seed = bip39.mnemonicToSeed(mnemonic)
var seedHex = bip39.mnemonicToSeedHex(mnemonic)

t.plan(3)
t.equal(seed.toString('hex'), seedHex)
t.equal(seedHex, '5cf2d4a8b0355e90295bdfc565a022a409af063d5365bb57bf74d9528f494bfa4400f53d8349b80fdae44082d7f9541e1dba2b003bcfec9d0d53781ca676651f')
t.equal(bip39.validateMnemonic(mnemonic), false)
})

test('generateMnemonic can vary entropy length', function (t) {
var words = bip39.generateMnemonic(96).split(' ')
var words = bip39.generateMnemonic(160).split(' ')

t.plan(1)
t.equal(words.length, 9, 'can vary generated entropy bit length')
t.equal(words.length, 15, 'can vary generated entropy bit length')
})

test('generateMnemonic only requests the exact amount of data from an RNG', function (t) {
test('generateMnemonic requests the exact amount of data from an RNG', function (t) {
t.plan(1)

bip39.generateMnemonic(96, function (size) {
t.equal(size, 96 / 8)
bip39.generateMnemonic(160, function (size) {
t.equal(size, 160 / 8)
return new Buffer(size)
})
})
Expand Down
42 changes: 42 additions & 0 deletions test/readme.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
var bip39 = require('../')
var proxyquire = require('proxyquire')
var test = require('tape')

test('README example 1', function (t) {
// defaults to BIP39 English word list
var entropy = 'ffffffffffffffffffffffffffffffff'
var mnemonic = bip39.entropyToMnemonic(entropy)

t.plan(2)
t.equal(mnemonic, 'zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong')

// reversible
t.equal(bip39.mnemonicToEntropy(mnemonic), entropy)
})

test('README example 2', function (t) {
var stub = {
randombytes: function (size) {
return new Buffer('qwertyuiopasdfghjklzxcvbnm[];,./'.slice(0, size))
}
}
var proxiedbip39 = proxyquire('../', stub)

// mnemonic strength defaults to 128 bits
var mnemonic = proxiedbip39.generateMnemonic()

t.plan(2)
t.equal(mnemonic, 'imitate robot frame trophy nuclear regret saddle around inflict case oil spice')
t.equal(bip39.validateMnemonic(mnemonic), true)
})

test('README example 3', function (t) {
var mnemonic = 'basket actual'
var seed = bip39.mnemonicToSeed(mnemonic)
var seedHex = bip39.mnemonicToSeedHex(mnemonic)

t.plan(3)
t.equal(seed.toString('hex'), seedHex)
t.equal(seedHex, '5cf2d4a8b0355e90295bdfc565a022a409af063d5365bb57bf74d9528f494bfa4400f53d8349b80fdae44082d7f9541e1dba2b003bcfec9d0d53781ca676651f')
t.equal(bip39.validateMnemonic(mnemonic), false)
})