mirror of
https://github.com/oed/seedsplit.git
synced 2026-01-08 20:27:55 -05:00
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:
22
README.md
22
README.md
@@ -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.
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
10
index.js
10
index.js
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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
21
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user