Merge pull request #10 from oed/develop

Release v0.2.0
This commit is contained in:
Joel Torstensson
2018-05-14 17:22:57 +02:00
committed by GitHub
8 changed files with 149 additions and 89 deletions

View File

@@ -2,7 +2,7 @@
Seedsplit lets you split a mnemonic seed into a selected number of shards which are also encoded as mnemonics. n-of-m shards can then be combined in order to get the initial mnemonic seed. This is accomplished by using Shamirs Secret Sharing. Seedsplit supports 12 and 24 word seeds.
## Why?
If you use a hardware wallet like Trezor or Ledger you get a mnemonic seed that can be used to recover your device in case of loss or breakage. After you have written this seed down you obviously need to keep it very safe, but how? Some people put it in a safety deposit box in their bank. However, this gives you a trust issue again, which is what you where trying to avoid. With seedsplit you can split your seed into multiple mnemonics that you can had out to your friends and family. They can only recreate your seed if some of them come together to do so.
If you use a hardware wallet like Trezor or Ledger you get a mnemonic seed that can be used to recover your device in case of loss or breakage. After you have written this seed down you obviously need to keep it very safe, but how? Some people put it in a safety deposit box in their bank. However, this gives you a trust issue again, which is what you where trying to avoid. With seedsplit you can split your seed into multiple mnemonics that you can hand out to your friends and family. They can only recreate your seed if some of them come together to do so.
## Safe usage
For maximal safety you should only run this program on a computer that is not connected to the internet. Make sure to write down the mnemonic shards by hand, do **not** print them.
@@ -13,22 +13,22 @@ $ npm i -g seedsplit
```
## Example usage
To split the mnemonic seed (`subway allow sketch yard proof apart world affair awful crop jealous bar` is used in the example):
To split the mnemonic seed (`island rich ghost moral city vital ignore plastic slab drift surprise grid idea distance regret gospel page across bird obscure copy either vessel jeans` is used in the example):
```
$ seedsplit split -t 2 -s 3
Enter seed mnemonic:
divorce husband dawn found essence field slim cycle warm claim empower artist caution merit
divorce object rule lemon possible public frozen expire twin evidence slim photo ivory leader
divorce wasp dentist company immune aim solve improve train hollow phone siren run spirit
1 pony quality biology flush middle flight universe stool like ocean climb casino super buyer smooth owner hidden gravity unable hunt mass media early borrow
2 sorry earn angry best glide purpose chat grant fox wall lawsuit such liquid wrong chimney raven husband boss grass inject they special warm shuffle
3 bus farm lecture segment shiver adjust rookie beyond blade clutch monster output clog taxi expect embrace omit lazy palace lobster fix budget donate rebel
```
Note that when you enter the seed no input will be displayed
Note that when you enter the seed no input will be displayed.
To combine mnemonics to get a seed:
```
$ seedsplit combine -t 2
Enter shard mnemonic: divorce object rule lemon possible public frozen expire twin evidence slim photo ivory leader
Enter shard mnemonic: divorce wasp dentist company immune aim solve improve train hollow phone siren run spirit
subway allow sketch yard proof apart world affair awful crop jealous bar
Enter shard mnemonic: 2 sorry earn angry best glide purpose chat grant fox wall lawsuit such liquid wrong chimney raven husband boss grass inject they special warm shuffle
Enter shard mnemonic: 3 bus farm lecture segment shiver adjust rookie beyond blade clutch monster output clog taxi expect embrace omit lazy palace lobster fix budget donate rebel
island rich ghost moral city vital ignore plastic slab drift surprise grid idea distance regret gospel page across bird obscure copy either vessel jeans
```
## Tests
@@ -37,5 +37,11 @@ To run tests:
$ npm test
```
## Older versions
Releases 0.1.2 and older used a different Shamirs Secret Sharing library and are incompatible with newer versions. If you need to combine shards created with an old version, please [use version 0.1.2](https://github.com/oed/seedsplit/releases/tag/v0.1.2).
## Wordlists
Seedsplit uses wordlists from [bitcoinjs/bip39](https://github.com/bitcoinjs/bip39/tree/master/wordlists) project.
## Acknowledgments
Thanks to Christian Lundkvist for the idea of encoding the shards to mnemonics.

7
RELEASE-NOTES.md Normal file
View File

@@ -0,0 +1,7 @@
# Releases
### v0.2.0
A switch to dsprenkels/sss-node library. Which is a more robust implementation of Shamirs secret sharing.
### v0.1.2
Last release using old Shamirs secret library. Use this version if you split your seed before May 2018.

View File

@@ -25,8 +25,8 @@ function split({ threshold, shards }) {
console.log('Please specify threshold and shards')
return
}
handlePrompt(prompts.seed, result => {
let shardMnemonics = seedsplit.split(result.seed, shards, threshold)
handlePrompt(prompts.seed, async result => {
let shardMnemonics = await seedsplit.split(result.seed, shards, threshold)
console.log(shardMnemonics.join('\n'))
})
}
@@ -36,8 +36,8 @@ function combine({ threshold }) {
console.log('Please specify threshold')
return
}
getMnemonics([], threshold, mnemonics => {
let seedMnemonic = seedsplit.combine(mnemonics)
getMnemonics([], threshold, async mnemonics => {
let seedMnemonic = await seedsplit.combine(mnemonics)
console.log(seedMnemonic)
})
}
@@ -85,4 +85,3 @@ program.parse(process.argv)
if (!process.argv.slice(2).length) {
program.outputHelp()
}

View File

@@ -8,9 +8,11 @@ console.log(m1)
let sm = seedsplit.split(m1, 3, 2)
console.log(sm)
sm.then((x) => console.log(x))
let m2 = seedsplit.combine(sm.slice(1))
let m2 = sm.then((x) => seedsplit.combine(x.slice(1)))
console.log(m2)
console.log('Is correct:', m1 === m2)
m2.then((x) => {
console.log(x)
console.log('Is correct:', m1 === x)
})

View File

@@ -1,34 +1,55 @@
const bip39 = require('bip39')
const ssss = require('secrets.js')
const ssss = require("shamirsecretsharing");
const paddingWord = 'abandon '
const numPaddingZeros = 3
const SECRET_LENGTH = 64
function split(seed, numShards, threshold) {
const lengthToPrefix = { 64: '', 56: 'd', 48: 'c', 40: 'b', 32: 'a' }
const prefixToLength = { '': 64, 'd': 56, 'c': 48, 'b': 40, 'a': 32 }
const getExtraEntopy = length => bip39.mnemonicToEntropy(
bip39.generateMnemonic(SECRET_LENGTH * 2)).substring(0, SECRET_LENGTH - length)
async function split(seed, numShards, threshold) {
if (threshold > numShards) {
throw new Error('Threshold can\'t be larger than the number of shards')
}
if (!bip39.validateMnemonic(seed)) {
throw new Error('Invalid mnemonic')
}
let ent = bip39.mnemonicToEntropy(seed)
let shards = ssss.share(ent, numShards, threshold)
let prefix = lengthToPrefix[ent.length]
ent = ent + getExtraEntopy(ent.length)
let shardMnemonics = shards.map(shard => {
let padding = '0'.repeat(numPaddingZeros)
return bip39.entropyToMnemonic(padding + shard)
})
// due to padding first word is always the same
return shardMnemonics.map(sm => sm.split(' ').slice(1).join(' '))
let shards = await ssss.createKeyshares(Buffer.from(ent, 'hex'), numShards, threshold)
return shards.map(shard =>
prefix + shard[0] + ' ' + bip39.entropyToMnemonic(shard.slice(1).toString('hex'))
)
}
function combine(shardMnemonics) {
let shards = shardMnemonics.map(sm =>
// due to padding first word is always the same
bip39.mnemonicToEntropy(paddingWord + sm).slice(numPaddingZeros))
let comb = ssss.combine(shards)
try {
return bip39.entropyToMnemonic(comb)
} catch (e) {
throw new Error('Could not combine the given mnemonics')
async function combine(shardMnemonics) {
let prefix = ""
let shards = shardMnemonics.map(sm => {
if (!bip39.validateMnemonic(sm.split(' ').slice(1).join(' '))) {
throw new Error('Invalid mnemonic ' + sm.split(' ')[0])
}
let buf = new Buffer.from('00' + bip39.mnemonicToEntropy(sm.split(' ').slice(1).join(' ')), 'hex')
let number = sm.split(' ')[0]
if (!/\d/.test(number[0])) {
prefix = number[0]
number = number.slice(1)
}
buf.writeUInt8(parseInt(number), 0)
return buf
})
let combined = await ssss.combineKeyshares(shards)
try{
return bip39.entropyToMnemonic(combined.toString('hex').substring(0, prefixToLength[prefix]))
} catch (e) {
throw new Error('Could not combine the given mnemonics')
}
}
module.exports = { split, combine }

21
package-lock.json generated
View File

@@ -21,13 +21,14 @@
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
},
"bip39": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/bip39/-/bip39-2.3.0.tgz",
"integrity": "sha1-5O5sbRvZDKAP/VetRGvfjAF/9IQ=",
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/bip39/-/bip39-2.5.0.tgz",
"integrity": "sha512-xwIx/8JKoT2+IPJpFEfXoWdYwP7UVAoUxxLNfGCfVowaJE7yg1Y5B1BVPqlUNsBq5/nGwmFkwRJ8xDW4sX8OdA==",
"requires": {
"create-hash": "1.2.0",
"pbkdf2": "3.0.16",
"randombytes": "2.0.6",
"safe-buffer": "5.1.2",
"unorm": "1.4.1"
}
},
@@ -400,6 +401,11 @@
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz",
"integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s="
},
"nan": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.6.2.tgz",
"integrity": "sha1-5P805slf37WuzAjeZZb0NgWn20U="
},
"ncp": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/ncp/-/ncp-1.0.1.tgz",
@@ -491,9 +497,6 @@
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"secrets.js": {
"version": "git+https://github.com/oed/secrets.js.git#17340ac1404d4f2d569d20b16c03473e84247918"
},
"sha.js": {
"version": "2.4.11",
"resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
@@ -503,6 +506,12 @@
"safe-buffer": "5.1.2"
}
},
"shamirsecretsharing": {
"version": "github:dsprenkels/sss-node#e69eb0404f09fc56780646baf2145feb3db3b4bc",
"requires": {
"nan": "2.6.2"
}
},
"stack-trace": {
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "seedsplit",
"version": "0.1.2",
"version": "0.2.0",
"description": "Use shamirs secret sharing scheme to split a seed mnemonic for crypto wallets to multiple mnemonics.",
"main": "lib/seedsplit.js",
"scripts": {
@@ -12,10 +12,10 @@
"seedsplit": "./bin/cli.js"
},
"dependencies": {
"bip39": "^2.3.0",
"bip39": "^2.5.0",
"commander": "^2.9.0",
"prompt": "^1.0.0",
"secrets.js": "git+https://github.com/oed/secrets.js"
"shamirsecretsharing": "github:dsprenkels/sss-node#release-0.x"
},
"devDependencies": {
"chai": "^3.5.0",

View File

@@ -17,80 +17,96 @@ describe('seedsplit', () => {
twentyfourWordSeeds = arr.map(e => bip39.generateMnemonic(256))
})
it('should create the correct number of shards', (done) => {
shardsList1 = testCorrectSplit(twelveWordSeeds, 5, 3)
shardsList2 = testCorrectSplit(twentyfourWordSeeds, 5, 3)
shardsList3 = testCorrectSplit(twelveWordSeeds, 3, 2)
shardsList4 = testCorrectSplit(twentyfourWordSeeds, 3, 2)
testCorrectSplit(twelveWordSeeds, 2, 2)
testCorrectSplit(twelveWordSeeds, 4, 2)
testCorrectSplit(twelveWordSeeds, 6, 2)
testCorrectSplit(twelveWordSeeds, 7, 2)
testCorrectSplit(twelveWordSeeds, 8, 2)
testCorrectSplit(twelveWordSeeds, 9, 2)
done()
it('should create the correct number of shards', async () => {
let results = []
results.push(testCorrectSplit(twelveWordSeeds, 5, 3))
results.push(testCorrectSplit(twentyfourWordSeeds, 5, 3))
results.push(testCorrectSplit(twelveWordSeeds, 3, 2))
results.push(testCorrectSplit(twentyfourWordSeeds, 3, 2))
results.push(testCorrectSplit(twelveWordSeeds, 2, 2))
results.push(testCorrectSplit(twelveWordSeeds, 4, 2))
results.push(testCorrectSplit(twelveWordSeeds, 6, 2))
results.push(testCorrectSplit(twelveWordSeeds, 7, 2))
results.push(testCorrectSplit(twelveWordSeeds, 8, 2))
results.push(testCorrectSplit(twelveWordSeeds, 9, 2))
results.push(testCorrectSplit(twelveWordSeeds, 255, 127))
results.push(testCorrectSplit(twentyfourWordSeeds, 255, 127))
shardsList1 = await results[0]
shardsList2 = await results[1]
shardsList3 = await results[2]
shardsList4 = await results[3]
await Promise.all(results)
})
it('should throw if threshold > shards', (done) => {
let fn = () => seedsplit.split(twelveWordSeeds[0], 3, 4)
assert.throws(fn)
done()
it('should throw if threshold > shards', async () => {
let didThrow = false
try {
await seedsplit.split(twelveWordSeeds[0], 3, 4)
} catch (e) {
didThrow = true
}
assert.isTrue(didThrow)
})
it('should give the correct seed with any combination of the right number of shards', (done) => {
testSufficientNumShards(shardsList1, twelveWordSeeds, 3)
testSufficientNumShards(shardsList2, twentyfourWordSeeds, 3)
testSufficientNumShards(shardsList1, twelveWordSeeds, 4)
testSufficientNumShards(shardsList2, twentyfourWordSeeds, 4)
testSufficientNumShards(shardsList1, twelveWordSeeds, 5)
testSufficientNumShards(shardsList2, twentyfourWordSeeds, 5)
it('should give the correct seed with any combination of the right number of shards', async () => {
let results = []
results.push(testSufficientNumShards(shardsList1, twelveWordSeeds, 3))
results.push(testSufficientNumShards(shardsList2, twentyfourWordSeeds, 3))
results.push(testSufficientNumShards(shardsList1, twelveWordSeeds, 4))
results.push(testSufficientNumShards(shardsList2, twentyfourWordSeeds, 4))
results.push(testSufficientNumShards(shardsList1, twelveWordSeeds, 5))
results.push(testSufficientNumShards(shardsList2, twentyfourWordSeeds, 5))
testSufficientNumShards(shardsList3, twelveWordSeeds, 2)
testSufficientNumShards(shardsList4, twentyfourWordSeeds, 2)
testSufficientNumShards(shardsList3, twelveWordSeeds, 3)
testSufficientNumShards(shardsList4, twentyfourWordSeeds, 3)
done()
results.push(testSufficientNumShards(shardsList3, twelveWordSeeds, 2))
results.push(testSufficientNumShards(shardsList4, twentyfourWordSeeds, 2))
results.push(testSufficientNumShards(shardsList3, twelveWordSeeds, 3))
results.push(testSufficientNumShards(shardsList4, twentyfourWordSeeds, 3))
await Promise.all(results)
}).timeout(4000)
it('should not give correct seed with a combination of to few shards', (done) => {
testInsufficientNumShards(shardsList1, twelveWordSeeds, 1)
testInsufficientNumShards(shardsList2, twentyfourWordSeeds, 1)
testInsufficientNumShards(shardsList1, twelveWordSeeds, 2)
testInsufficientNumShards(shardsList2, twentyfourWordSeeds, 2)
it('should not give correct seed with a combination of too few shards', async () => {
let results = []
results.push(testInsufficientNumShards(shardsList1, twelveWordSeeds, 1))
results.push(testInsufficientNumShards(shardsList2, twentyfourWordSeeds, 1))
results.push(testInsufficientNumShards(shardsList1, twelveWordSeeds, 2))
results.push(testInsufficientNumShards(shardsList2, twentyfourWordSeeds, 2))
testInsufficientNumShards(shardsList3, twelveWordSeeds, 1)
testInsufficientNumShards(shardsList4, twentyfourWordSeeds, 1)
done()
results.push(testInsufficientNumShards(shardsList3, twelveWordSeeds, 1))
results.push(testInsufficientNumShards(shardsList4, twentyfourWordSeeds, 1))
await Promise.all(results)
})
})
function testCorrectSplit(wordSeeds, numShards, threshold) {
async function testCorrectSplit(wordSeeds, numShards, threshold) {
let shardsList = []
for (const ws of wordSeeds) {
let shards = seedsplit.split(ws, numShards, threshold)
let shards = await seedsplit.split(ws, numShards, threshold)
assert.equal(shards.length, numShards, 'should have created right number of shares')
shardsList.push(shards)
}
return shardsList
}
function testSufficientNumShards(shardsList, wordSeeds, numShards) {
async function testSufficientNumShards(shardsList, wordSeeds, numShards) {
for (let i = 0; i < shardsList.length; i++) {
let cmbs = Combinatorics.combination(shardsList[i], numShards)
while (cmb = cmbs.next()) {
let combination = seedsplit.combine(cmb)
let combination = await seedsplit.combine(cmb)
assert.equal(combination, wordSeeds[i])
}
}
}
function testInsufficientNumShards(shardsList, wordSeeds, numShards) {
async function testInsufficientNumShards(shardsList, wordSeeds, numShards) {
for (let i = 0; i < shardsList.length; i++) {
let cmbs = Combinatorics.combination(shardsList[i], numShards)
while (cmb = cmbs.next()) {
let combination
try {
combination = seedsplit.combine(cmb)
combination = await seedsplit.combine(cmb)
} catch (e) {
// only throws when decoded hex is not valid
assert.equal(e.message, 'Could not combine the given mnemonics')