From d0cf927606ad6d2606b9adea41773fe15aab32f4 Mon Sep 17 00:00:00 2001 From: Denis Shulyaka Date: Tue, 8 May 2018 23:40:26 +0300 Subject: [PATCH] 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 --- README.md | 22 +++++++---- bin/cli.js | 4 +- index.js | 10 +++-- lib/seedsplit.js | 93 ++++++++++++++++++++++++++++++++++++----------- package-lock.json | 21 ++++++++--- package.json | 4 +- test/seedsplit.js | 47 +++++++++++++++--------- 7 files changed, 140 insertions(+), 61 deletions(-) diff --git a/README.md b/README.md index 28100f7..405105a 100644 --- a/README.md +++ b/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. diff --git a/bin/cli.js b/bin/cli.js index 5dfd18d..2271b23 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -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)) }) } diff --git a/index.js b/index.js index 98eec66..6fba328 100644 --- a/index.js +++ b/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) +}) diff --git a/lib/seedsplit.js b/lib/seedsplit.js index 512007f..cdc03ae 100644 --- a/lib/seedsplit.js +++ b/lib/seedsplit.js @@ -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 } diff --git a/package-lock.json b/package-lock.json index a85e03d..5d0d979 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index aa52d23..450b042 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/test/seedsplit.js b/test/seedsplit.js index a1d2f46..489d3e4 100644 --- a/test/seedsplit.js +++ b/test/seedsplit.js @@ -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])) } } }