Skip to content

Commit

Permalink
Merge pull request #49 from bitcoinjs/ent
Browse files Browse the repository at this point in the history
Limit ENT to 128-256, multiple of 4 (cont.)
  • Loading branch information
dcousens committed May 23, 2017
2 parents f6bcccc + 59cfe5d commit 9b12ca4
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 104 deletions.
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)
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)
})

0 comments on commit 9b12ca4

Please sign in to comment.