Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
James Calfee committed May 24, 2017
0 parents commit decfee2
Show file tree
Hide file tree
Showing 8 changed files with 1,522 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules
coverage
test
npm-debug.log
6 changes: 6 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
sudo: true
language: node_js
node_js:
- "6"

after_success: npm run coveralls
48 changes: 48 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
[![Build Status](https://travis-ci.org/jcalfee/bip39-seeder.svg?branch=master)](https://travis-ci.org/jcalfee/bip39-seeder)
[![Coverage Status](https://coveralls.io/repos/github/jcalfee/bip39-seeder/badge.svg?branch=master)](https://coveralls.io/github/jcalfee/bip39-seeder?branch=master)


# Seeder (bip39)

Create and validate bip39 compatible mnemonic seeds.

## Entropy

Combines secure random generator and variations in CPU performance.

## Validation

Mnemonic seeds contain a hidden checksum. Suggested spelling corrections are provided.

## Command Line Interface

```bash
bip39-seeder -?

```

## API

```javascript
const assert = require('assert')
const {randomMnemonicSeed, mnemonicToSeed, checkSeed, suggest} = require('bip39-seeder')

randomMnemonicSeed(null, seed => {

assert(checkSeed(seed))

// Strengthen the seed by 11 bits
const stretched = mnemonicToSeed(seed)
assert(stretched.toString('hex'))
assert.equal(stretched.length, 64)
// By convention:
// stretched.slice(0, 32) is the private key or HD master private key, etc..
// stretched.I.slice(32) may be used for something else like an initialization vector, chain code, etc..

assert(checkSeed(seed).valid)

assert.equal(suggest('quality1')[0], 'quality')

console.log('Random mnemonic seed:', seed)
})
```
72 changes: 72 additions & 0 deletions bin/seeder
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#!/usr/bin/env node

const read = require('read')
const argv = require('yargs').usage(`
Create or validate strong bip39 mnemonic seeds.
> ocean earn race rack swing odor yard manage illegal draw window desk
Suggested spelling corrections are provided.
`)
.example('$0 -c', 'Create a new random mnemonic seed')
.example('$0 -s', 'Validate an existing seed')
.option('create', {describe: 'Create a new secure random mnemonic seed', type: 'boolean', alias: 'c'})
.option('seed', {describe: 'Provide mnemonic seed of up to 12 words', type: 'string', alias: 's', default: null})
.option('bits', {describe: 'Bit strength', type: 'number', alias: 'b', default: 128})
.option('language', {describe: 'language: chinese_simplified, chinese_traditional, english, french, italian, japanese, spanish', type: 'string', alias: 'l', default: 'english'})
.option('no-suggest', {describe: 'Don\'t suggest correction to misspelled words in the mnemonic seed', type: 'boolean', default: false})
.help('help').alias('help', 'h').alias('help', '?')
.argv

const {randomMnemonicSeed, checkSeed} = require('..')
const {suggest} = require('bip39-checker')

const noSuggest = argv['no-suggest']
const {language, bits} = argv

function assertValidSeed (seed) {
const cs = checkSeed(seed, language)
if (cs.error) {
console.error(cs.error)
let oneSuggested = false // only show first suggestion (for security reasons)
if (!noSuggest) {
const words = seed.split(' ')
let idx = 0
for (const word of words) {
idx++
const suggestions = suggest(word)
if (suggestions === true) {
// console.error(`Word ${idx} ok`);
} else if (!oneSuggested && suggestions !== true && suggestions.length) {
oneSuggested = true
console.error(`Word ${idx} suggestions: ${suggestions.join(', ')}`)
}
}
}
process.exit(1)
}
}

if (argv.create && argv.seed) {
console.error('WARNING: --create ignored (seed specified)')
}

if (argv.seed) {
assertValidSeed(argv.seed)
console.error('Seed is valid')
process.exit(0)
}

if (argv.create) {
randomMnemonicSeed({language, bits}, seed => {
console.log(seed)
})
} else {
read({prompt: 'Mnemonic seed: ', silent: true}, (err, s) => {
if (err || s.trim() === '') {
console.error('Try -? for help or -c to create')
process.exit(1)
}
assertValidSeed(s)
console.error('Seed is valid')
})
}
144 changes: 144 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/**
@module seed
*/
const {normalize, checkWords, validWordlist, bip39} = require('bip39-checker')
const MoreEntropy = require('more-entropy')
const randomBytes = require('randombytes')
const createHash = require('create-hash')

module.exports = {
randomMnemonicSeed,
mnemonicToSeed,
checkSeed,
bip39
}

/**
@summary Create and validate mnemonic seeds. Operates similar to bip39 except this uses a version instead of a checksum and allows for more flexibility in bit strength.
@description Create a random mnemonic seed. This about 12 random private words that may be used to generate private keys. The mnemonic seed is designed to be strong enough to resist off-line brute-force attacks. This package uses the bip39 word list but differs in that it can embed a hidden version number in the mnemonic seed (instead of a checksum) and allows for smaller variations in the bit strength (need not be multiples of 32 bits or 4 words).
This mnemonic seed is versioned which will assist in checked the mnemonic seed against typing errors. Before deriving private keys see the mnemonicToSeed function.
randomMnemonicSeed is like bip39.generateMnemonic
@see mnemonicToSeed(mnemonicSeed)
If additional entropy is not provided, the 'more-entropy' package is used and combined with nodes secureRandom number generator.
@see https://www.npmjs.com/package/more-entropy
@see https://www.npmjs.com/package/secure-random
@arg {Array|Buffer|string} config.entropy - Any size Buffer or String but at least 128 bits are recommended. Additional entropy combined with secureRandom (@see https://github.com/keybase/more-entropy)
@arg {function} [seedCallback = null] - Called when seed is available. Additional entropy combined with secureRandom (@see https://github.com/keybase/more-entropy)
@arg {object} [config = {}]
@arg {number} [config.bits = 128] - Bit strength. Should be at least 128. Each hex digit in the version removes 4 bits from the strength but stretching adds 11 bits back. If stretching can't be performed then at least 140 bits would be better.
@arg {number} [config.work_min = 5] - Higher value uses more CPU cycles when gathering entropy.
@arg {string} [config.language = 'english'] - chinese_simplified, chinese_traditional, english, french, italian, japanese, spanish
@example seeder.randomMnemonicSeed(null, mnemonicSeed => {})
@return {string} undefined and seedCallback will be called. If custom entropy is provided returns a new 12 word mnemonic seed.
*/
function randomMnemonicSeed (config, seedCallback) {
config = config || {}
const {bits = 128, language = 'english', work_min = 5} = config
let {entropy} = config

const bytes = Math.ceil(bits / 8)

if (!entropy) {
if (!seedCallback) {
throw new TypeError('Provide seedCallback parameter unless you plan to provide config.entropy')
}
const moreEntropy = new MoreEntropy.Generator({work_min})
moreEntropy.generate(Math.max(bits, 128), newEntropy => {
moreEntropy.stop()
seedCallback(
randomMnemonicSeed(Object.assign(config, {entropy: newEntropy}), seedCallback)
)
})
return
}
let seedBuf = randomBytes(bytes)

try {
if (Array.isArray(entropy)) {
// preserve entropy for array elements with a value over 255
// require('assert').equal(entropy.length * 4, Buffer.from(new Int32Array(entropy).buffer).length)
entropy = Buffer.from(new Int32Array(entropy).buffer)
}
seedBuf = createHash('sha256').update(entropy).update(seedBuf).digest().slice(0, bytes)
} catch (err) {
if (/string or a buffer/.test(err.message)) {
throw new TypeError('entropy parameter must be a string, buffer, or array')
}
throw err
}
const wordlist = validWordlist(language)
const mnemonicSeed = bip39.generateMnemonic(bits, rng => seedBuf, wordlist)
return mnemonicSeed
}

/** Stretching can be done prior to deriving private keys. Adds 11 bits of
entropy to compensate for the version.
@arg {string} mnemonicSeed
@arg {string} version - Up to 3 HEX characters (lowercase)
@example seeder.mnemonicToSeed(mnemonicSeed)
@return {Buffer} 64 bytes or 512 bits
*/
function mnemonicToSeed (mnemonicSeed, passphrase = '') {
mnemonicSeed = normalize(mnemonicSeed)
return bip39.mnemonicToSeed(mnemonicSeed, passphrase)
}

/**
@typedef {object} Validity
@property {boolean} Validity.valid
@property {string} Validity.error
*/
/**
All functions in seeder check the seed's validity already. This is provided for
extra user interface checking (prior to stretching for example).
When a checksum is invalid, warn the user and ask if they would like to use it anyway. This way
you can recover phrases made by other apps in other languages.
@arg {string} mnemonicSeed
@arg {string} planguage = 'english']
@example assert(seeder.checkSeed(mnemonicSeed))
@return {Validity}
*/
function checkSeed (mnemonicSeed, language = 'english') {
try {
mnemonicSeed = normalize(mnemonicSeed)
assertValidSeed(mnemonicSeed, language)
return {
valid: true,
error: null
}
} catch (err) {
return {
valid: false,
error: err.message
}
}
}

function assertValidSeed (mnemonicSeed, language) {
if (!checkWords(mnemonicSeed, language)) {
throw new Error('Invalid mnemonic seed')
}
const wordlist = validWordlist(language)
if (!bip39.validateMnemonic(mnemonicSeed, wordlist)) {
const words = mnemonicSeed.split(' ').length
// user forgot to quote command line arg
const shortStr = words < 11 ? `. Mnemonic seeds are about 12 words but this seed is only ${words} words.` : ''
throw new Error(`Invalid mnemonic seed checksum${shortStr}`)
}
}
76 changes: 76 additions & 0 deletions index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/* eslint-env mocha */
const assert = require('assert')

const {randomMnemonicSeed, mnemonicToSeed, checkSeed} = require('.')

describe('Seed', () => {
it('Stretches', () => { // S L O W
const seed = 'possible mother domain sweet brown strategy element school february merit silver edit'
const stretched = mnemonicToSeed(seed, 'passphrase')
assert.equal(stretched.length, 64)
assert.equal(stretched.toString('hex').substring(0, 8), '0c619b5d')
})

it('Random mnemonic seed', () => {
assert(randomMnemonicSeed({entropy: [0], work_min: 1}))
assert(randomMnemonicSeed({entropy: 'entropy', work_min: 1}))
assert(randomMnemonicSeed({entropy: Buffer.from([0]), work_min: 1}))
assert(randomMnemonicSeed({entropy: Buffer.from([0]), bits: 256, work_min: 1}))
assert(
randomMnemonicSeed({entropy: 'entropy', work_min: 1}) !==
randomMnemonicSeed({entropy: 'entropy', work_min: 1})
)
})

it('Invalid entropy arg type', () => {
throws(() => randomMnemonicSeed({entropy: 2, work_min: 1}), /must be a string, buffer, or array/) // fails in browser
})

it('Catches type errors', () => {
throws(() => mnemonicToSeed(), /seed string required/)
throws(() => randomMnemonicSeed({work_min: 1}), /Provide seedCallback parameter unless you plan to provide config.entropy/)
assert(/this seed is only 2 words/.test(checkSeed('lazy dog').error))
const seed = randomMnemonicSeed({entropy: 'entropy', work_min: 1})
assert.equal(checkSeed(seed + ' nonword').error, 'Invalid mnemonic seed')
assert(/Invalid mnemonic seed checksum/.test(checkSeed(seed + ' able').error))
assert.equal(checkSeed(null).error, 'seed string required')
assert(checkSeed(seed))
})

it('Random mnemonic seed internal entropy', (done) => {
randomMnemonicSeed({work_min: 1}, mnemonicSeed => {
const words = mnemonicSeed.split(' ')
assert(words.length <= 12, `A mnemonic seed with more than 12 words: ${mnemonicSeed}`)
assert(words.length > 9, `Very odd, a seed with only ${words.length} words: ${mnemonicSeed}`)
done()
})
})

it('Localization', () => {
const check = language => {
const seed = randomMnemonicSeed({entropy: 'entropy', language, work_min: 1})
assert(checkSeed(seed, language))
}
throws(() => check('pig_latin'), /Missing wordlist/)
check('chinese_simplified')
check('chinese_traditional')
check('english')
check('french')
check('italian')
check('japanese')
check('spanish')
})
})

/* istanbul ignore next */
function throws (fn, match) {
try {
fn()
assert(false, 'Expecting error')
} catch (error) {
if (!match.test(error.message)) {
error.message = `Error did not match ${match}\n${error.message}`
throw error
}
}
}
30 changes: 30 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "bip39-seeder",
"version": "1.0.0",
"description": "Creates a random numonic using Bip39 adding: CPU entropy, seed checking using bip39-checker, and a command-line interface.",
"main": "src/index.js",
"bin": "bin/seeder",
"scripts": {
"test": "mocha index.test.js src/**/*.test.js",
"coverage": "istanbul cover _mocha -- -R spec index.test.js src/**/*.test.js",
"coveralls": "npm run coverage && cat ./coverage/lcov.info | ./node_modules/.bin/coveralls"
},
"author": "jcalfee",
"license": "ISC",
"repository": {
"type": "git",
"url": "git://github.com/jcalfee/bip39-seeder.git"
},
"dependencies": {
"bip39-checker": "^1.0.2",
"more-entropy": "^0.0.7",
"randombytes": "^2.0.3",
"read": "^1.0.7",
"yargs": "^7.1.0"
},
"devDependencies": {
"coveralls": "^2.13.1",
"istanbul": "^0.4.5",
"mocha": "^3.3.0"
}
}
Loading

0 comments on commit decfee2

Please sign in to comment.