Switch to dsprenkles/sss-node library

1. Replaced secrets.js with dsprenkles/sss-node
2. Each shard is a number followed by exactly 24 words
3. If the initial seed is not 24 words, then the shard number is prefixed with a character indicating the seed length
4. The lib/seedsplit.js functions now return a promise
5. Shards are not backwards compatible with the current seedsplit version
This commit is contained in:
Denis Shulyaka
2018-05-08 23:40:26 +03:00
parent ac84c5fb3b
commit d0cf927606
7 changed files with 140 additions and 61 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
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.

View File

@@ -27,7 +27,7 @@ function split({ threshold, shards }) {
}
handlePrompt(prompts.seed, result => {
let shardMnemonics = seedsplit.split(result.seed, shards, threshold)
console.log(shardMnemonics.join('\n'))
shardMnemonics.then((x) => console.log(x.join('\n')))
})
}
@@ -38,7 +38,7 @@ function combine({ threshold }) {
}
getMnemonics([], threshold, mnemonics => {
let seedMnemonic = seedsplit.combine(mnemonics)
console.log(seedMnemonic)
seedMnemonic.then((x) => console.log(x))
})
}

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,85 @@
const bip39 = require('bip39')
const ssss = require('secrets.js')
const paddingWord = 'abandon '
const numPaddingZeros = 3
const ssss = require("shamirsecretsharing");
function split(seed, numShards, threshold) {
if (threshold > numShards) {
throw new Error('Threshold can\'t be larger than the number of shards')
}
let ent = bip39.mnemonicToEntropy(seed)
let shards = ssss.share(ent, numShards, threshold)
if (!bip39.validateMnemonic(seed)) {
throw new Error('Invalid mnemonic')
}
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 ent = bip39.mnemonicToEntropy(seed)
let prefix = ""
switch(ent.length) {
case 32:
ent = ent + bip39.mnemonicToEntropy(bip39.generateMnemonic(128))
prefix = "x"
break;
case 40:
ent = ent + bip39.mnemonicToEntropy(bip39.generateMnemonic(128)).substring(0,24)
prefix = "s"
break;
case 48:
ent = ent + bip39.mnemonicToEntropy(bip39.generateMnemonic(128)).substring(0,16)
prefix = "t"
break;
case 56:
ent = ent + bip39.mnemonicToEntropy(bip39.generateMnemonic(128)).substring(0,8)
prefix = "m"
break;
}
let shards = ssss.createKeyshares(Buffer.from(ent, 'hex'), numShards, threshold)
return shards.then((x) => x.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')
}
let prefix = ""
let shards = shardMnemonics.map(sm => {
if (!bip39.validateMnemonic(sm.split(' ').slice(1).join(' '))) {
throw new Error('Invalid mnemonic')
}
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 comb = ssss.combineKeyshares(shards)
try{
return comb.then((x) => {
switch(prefix) {
case "x":
return bip39.entropyToMnemonic(x.toString('hex').substring(0, 32))
break;
case "s":
return bip39.entropyToMnemonic(x.toString('hex').substring(0, 40))
break;
case "t":
return bip39.entropyToMnemonic(x.toString('hex').substring(0, 48))
break;
case "m":
return bip39.entropyToMnemonic(x.toString('hex').substring(0, 56))
break;
default:
return bip39.entropyToMnemonic(x.toString('hex'))
}
})
} 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

@@ -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,18 +17,27 @@ 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]
return Promise.all(results)
})
it('should throw if threshold > shards', (done) => {
@@ -52,7 +61,7 @@ describe('seedsplit', () => {
done()
}).timeout(4000)
it('should not give correct seed with a combination of to few shards', (done) => {
it('should not give correct seed with a combination of too few shards', (done) => {
testInsufficientNumShards(shardsList1, twelveWordSeeds, 1)
testInsufficientNumShards(shardsList2, twentyfourWordSeeds, 1)
testInsufficientNumShards(shardsList1, twelveWordSeeds, 2)
@@ -68,10 +77,12 @@ function testCorrectSplit(wordSeeds, numShards, threshold) {
let shardsList = []
for (const ws of wordSeeds) {
let shards = seedsplit.split(ws, numShards, threshold)
assert.equal(shards.length, numShards, 'should have created right number of shares')
shardsList.push(shards)
shardsList.push(shards.then((x) => {
assert.equal(x.length, numShards, 'should have created right number of shares')
return x
}))
}
return shardsList
return Promise.all(shardsList)
}
function testSufficientNumShards(shardsList, wordSeeds, numShards) {
@@ -79,7 +90,7 @@ function testSufficientNumShards(shardsList, wordSeeds, numShards) {
let cmbs = Combinatorics.combination(shardsList[i], numShards)
while (cmb = cmbs.next()) {
let combination = seedsplit.combine(cmb)
assert.equal(combination, wordSeeds[i])
combination.then((x) => assert.equal(x, wordSeeds[i]))
}
}
}
@@ -95,7 +106,7 @@ function testInsufficientNumShards(shardsList, wordSeeds, numShards) {
// only throws when decoded hex is not valid
assert.equal(e.message, 'Could not combine the given mnemonics')
}
assert.notEqual(combination, wordSeeds[i])
combination.then((x) => assert.notEqual(x, wordSeeds[i]))
}
}
}