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
104 changes: 52 additions & 52 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,33 @@ 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'
var INVALID_CHECKSUM = 'Invalid mnemonic checksum'

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

function binaryToByte (bin) {
return parseInt(bin, 2)
}

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

function deriveChecksumBits (entropyBuffer) {
var ENT = entropyBuffer.length * 8
var CS = ENT / 32
var hash = createHash('sha256').update(entropyBuffer).digest()

return bytesToBinary([].slice.call(hash)).slice(0, CS)
}

function salt (password) {
return 'mnemonic' + (password || '')
}
Expand All @@ -31,57 +58,52 @@ 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.some(function (word) {
return wordlist.indexOf(word) === -1
})) throw new Error('Invalid mnemonic')
if (words.length % 3 !== 0) throw new Error(INVALID_MNEMONIC)

// convert word indices to 11 bit binary strings
var bits = words.map(function (word) {
var index = wordlist.indexOf(word)
if (index === -1) throw new Error(INVALID_MNEMONIC)

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')
}

// split the binary string into ENT/CS
var dividerIndex = Math.floor(bits.length / 33) * 32
var entropy = bits.slice(0, dividerIndex)
var checksum = bits.slice(dividerIndex)
var entropyBits = bits.slice(0, dividerIndex)
var checksumBits = bits.slice(dividerIndex)

// calculate the checksum and compare
var entropyBytes = entropy.match(/(.{1,8})/g).map(function (bin) {
return parseInt(bin, 2)
})
var entropyBuffer = new Buffer(entropyBytes)
var newChecksum = checksumBits(entropyBuffer)
var entropyBytes = entropyBits.match(/(.{1,8})/g).map(binaryToByte)
if (entropyBytes.length < 16) throw new Error(INVALID_ENTROPY)
if (entropyBytes.length > 32) throw new Error(INVALID_ENTROPY)
if (entropyBytes.length % 4 !== 0) throw new Error(INVALID_ENTROPY)

if (newChecksum !== checksum) throw new Error('Invalid mnemonic checksum')
var entropy = new Buffer(entropyBytes)
var newChecksum = deriveChecksumBits(entropy)
if (newChecksum !== checksumBits) throw new Error(INVALID_CHECKSUM)

return entropyBuffer.toString('hex')
return entropy.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 TypeError(INVALID_ENTROPY)
if (entropyHex.length > 64) throw new TypeError(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 TypeError(INVALID_ENTROPY)

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

var bits = entropyBits + checksum
var bits = entropyBits + checksumBits
var chunks = bits.match(/(.{1,11})/g)

var words = chunks.map(function (binary) {
var index = parseInt(binary, 2)

var index = binaryToByte(binary)
return wordlist[index]
})

Expand All @@ -90,6 +112,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,29 +129,6 @@ function validateMnemonic (mnemonic, wordlist) {
return true
}

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

// Calculated constants from BIP39
var ENT = entropyBuffer.length * 8
var CS = ENT / 32

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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"node-fetch": "^1.6.3",
"nyc": "^8.3.0",
"proxyquire": "^1.7.10",
"standard": "*",
"standard": "^9.0.0",
"tape": "^4.6.2"
}
}
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')
}, /^TypeError: 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')
}, /^TypeError: 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')
}, /^TypeError: 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)
})