-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
James Calfee
committed
May 24, 2017
0 parents
commit decfee2
Showing
8 changed files
with
1,522 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
node_modules | ||
coverage | ||
test | ||
npm-debug.log |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}) | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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') | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}`) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
Oops, something went wrong.