Skip to content

Commit

Permalink
Merge pull request #5174 from swaponline/issue5165
Browse files Browse the repository at this point in the history
Issue5165 - SLIP-0039: Shamir's Secret-Sharing for Mnemonic Codes
  • Loading branch information
shendel authored Dec 30, 2022
2 parents b29fae7 + cec24ca commit c86a57a
Show file tree
Hide file tree
Showing 50 changed files with 12,395 additions and 5,855 deletions.
3 changes: 0 additions & 3 deletions .github/workflows/nodejs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,6 @@ jobs:
name: screenshots
path: ./tests/e2e/screenshots
if-no-files-found: ignore
- name: bot test
run: |
npm run bot:test
env:
CI: true

75 changes: 75 additions & 0 deletions src/common/utils/mnemonic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import * as bitcoin from 'bitcoinjs-lib'
import * as bip32 from 'bip32'
import { hdkey } from 'ethereumjs-wallet'
import * as bip39 from 'bip39'
import Slip39 from './slip39/slip39.js'
import * as slipHelper from './slip39/slip39_helper.js'


const getRandomMnemonicWords = () => {
return bip39.generateMnemonic()
Expand All @@ -11,6 +14,75 @@ const validateMnemonicWords = (mnemonic) => {
return bip39.validateMnemonic(convertMnemonicToValid(mnemonic))
}

// Shamir's Secret Sharing alternative to saving 12 words seed (Split mnemonic to three secrets)
const splitMnemonicToSecretParts = (mnemonic, passphrase = ``) => {
mnemonic = convertMnemonicToValid(mnemonic)
const mnemonicEntropy: string = bip39.mnemonicToEntropy(mnemonic)
const masterSecret: number[] = slipHelper.toByteArray(mnemonicEntropy)

const getMnemonicInt = (mnemonic) => {
return slipHelper.intFromIndices(slipHelper.mnemonicToIndices(mnemonic))
}

const slip = Slip39.fromArray(masterSecret, {
passphrase,
threshold: 2, // number of group-shares required to reconstruct the master secret.
groups: [
[1, 1], [1, 1], [1, 1] // split master key to 3 parts
]
})

const mnemonics = [
slip.fromPath('r/0/0').mnemonics[0],
slip.fromPath('r/1/0').mnemonics[0],
slip.fromPath('r/2/0').mnemonics[0]
]

const secretParts = [
getMnemonicInt(mnemonics[0]),
getMnemonicInt(mnemonics[1]),
getMnemonicInt(mnemonics[2])
]
return {
mnemonics,
secretParts
}
}

// Shamir's Secret Sharing alternative to saving 12 words seed (Restore mnemonic from two secrets)
const restoryMnemonicFromSecretParts = (secretParts, isMnemonics = false, passphrase = ``) => {
// prepare mnemonics
const mnemonics: string[] = (isMnemonics)
? secretParts
: secretParts.map((mnemonicInt) => {
return slipHelper.mnemonicFromIndices(
slipHelper.intToIndices(
(typeof(mnemonicInt) === 'string')
? BigInt(`${mnemonicInt}`)
: mnemonicInt,
20,
10
)
)
})
// do recover
const recoveredEntropy = Slip39.recoverSecret(mnemonics, passphrase)
const recoveredMnemonic = bip39.entropyToMnemonic(recoveredEntropy)
return recoveredMnemonic
}

// Shamir's Secret Sharing alternative to saving 12 words seed (Check secret is valid)
const isValidShamirsSecret = (secret) => {
try {
const secretInt = (typeof(secret) === 'string') ? BigInt(`${secret}`) : secret
const mnemonicIndices = slipHelper.intToIndices(secretInt, 20, 10)
const mnemonic = slipHelper.mnemonicFromIndices(mnemonicIndices)
return slipHelper.validateMnemonic(mnemonic)
} catch (e) {
return false
}
}

const convertMnemonicToValid = (mnemonic) => {
return mnemonic
.trim()
Expand Down Expand Up @@ -111,4 +183,7 @@ export {
getEthLikeWallet,
getGhostWallet,
getNextWallet,
splitMnemonicToSecretParts,
restoryMnemonicFromSecretParts,
isValidShamirsSecret,
}
193 changes: 193 additions & 0 deletions src/common/utils/slip39/slip39.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
/* eslint-disable radix */
const slipHelper = require('./slip39_helper.js');

const MAX_DEPTH = 2;

/**
* Slip39Node
* For root node, description refers to the whole set's title e.g. "Hardware wallet X SSSS shares"
* For children nodes, description refers to the group e.g. "Family group: mom, dad, sister, wife"
*/
class Slip39Node {
constructor(index = 0, description = '', mnemonic = '', children = [], secret = []) {
this.index = index;
this.description = description;
this.mnemonic = mnemonic;
this.children = children;
this.secret = secret;
}

get mnemonics() {
if (this.children.length === 0) {
return [this.mnemonic];
}
const result = this.children.reduce((prev, item) => {
return prev.concat(item.mnemonics);
}, []);
return result;
}
}

//
// The javascript implementation of the SLIP-0039: Shamir's Secret-Sharing for Mnemonic Codes
// see: https://github.com/satoshilabs/slips/blob/master/slip-0039.md)
//
class Slip39 {
constructor({
iterationExponent = 0,
identifier,
groupCount,
groupThreshold
} = {}) {
this.iterationExponent = iterationExponent;
this.identifier = identifier;
this.groupCount = groupCount;
this.groupThreshold = groupThreshold;
}

static fromArray(masterSecret, {
passphrase = '',
threshold = 1,
groups = [
[1, 1, 'Default 1-of-1 group share']
],
iterationExponent = 0,
title = 'My default slip39 shares'
} = {}) {
if (masterSecret.length * 8 < slipHelper.MIN_ENTROPY_BITS) {
throw Error(`The length of the master secret (${masterSecret.length} bytes) must be at least ${slipHelper.bitsToBytes(slipHelper.MIN_ENTROPY_BITS)} bytes.`);
}

if (masterSecret.length % 2 !== 0) {
throw Error('The length of the master secret in bytes must be an even number.');
}

if (!/^[\x20-\x7E]*$/.test(passphrase)) {
throw Error('The passphrase must contain only printable ASCII characters (code points 32-126).');
}

if (threshold > groups.length) {
throw Error(`The requested group threshold (${threshold}) must not exceed the number of groups (${groups.length}).`);
}

groups.forEach((item) => {
if (item[0] === 1 && item[1] > 1) {
throw Error(`Creating multiple member shares with member threshold 1 is not allowed. Use 1-of-1 member sharing instead. ${groups.join()}`);
}
});

const identifier = slipHelper.generateIdentifier();

const slip = new Slip39({
iterationExponent: iterationExponent,
identifier: identifier,
groupCount: groups.length,
groupThreshold: threshold
});

const encryptedMasterSecret = slipHelper.crypt(
masterSecret, passphrase, iterationExponent, slip.identifier);

const root = slip.buildRecursive(
new Slip39Node(0, title),
groups,
encryptedMasterSecret,
threshold
);

slip.root = root;
return slip;
}

buildRecursive(currentNode, nodes, secret, threshold, index) {
//console.log('>>> buildRecursive', secret, threshold, index)
// It means it's a leaf.
if (nodes.length === 0) {
const mnemonic = slipHelper.encodeMnemonic(this.identifier, this.iterationExponent, index,
this.groupThreshold, this.groupCount, currentNode.index, threshold, secret);

currentNode.mnemonic = mnemonic;
currentNode.secret = secret;
return currentNode;
}

const secretShares = slipHelper.splitSecret(threshold, nodes.length, secret);
let children = [];
let idx = 0;

nodes.forEach((item) => {
// n=threshold
const n = item[0];
// m=members
const m = item[1];
// d=description
const d = item[2] || '';

// Generate leaf members, means their `m` is `0`
const members = Array().slip39Generate(m, () => [n, 0, d]);

const node = new Slip39Node(idx, d);
const branch = this.buildRecursive(
node,
members,
secretShares[idx],
n,
currentNode.index);

children = children.concat(branch);
idx = idx + 1;
});
currentNode.children = children;
return currentNode;
}

static recoverSecret(mnemonics, passphrase) {
return slipHelper.combineMnemonics(mnemonics, passphrase);
}

static validateMnemonic(mnemonic) {
return slipHelper.validateMnemonic(mnemonic);
}

fromPath(path) {
this.validatePath(path);

const children = this.parseChildren(path);

if (typeof children === 'undefined' || children.length === 0) {
return this.root;
}

return children.reduce((prev, childNumber) => {
let childrenLen = prev.children.length;
if (childNumber >= childrenLen) {
throw new Error(`The path index (${childNumber}) exceeds the children index (${childrenLen - 1}).`);
}

return prev.children[childNumber];
}, this.root);
}

validatePath(path) {
if (!path.match(/(^r)(\/\d{1,2}){0,2}$/)) {
throw new Error('Expected valid path e.g. "r/0/0".');
}

const depth = path.split('/');
const pathLength = depth.length - 1;
if (pathLength > MAX_DEPTH) {
throw new Error(`Path\'s (${path}) max depth (${MAX_DEPTH}) is exceeded (${pathLength}).`);
}
}

parseChildren(path) {
const splitted = path.split('/').slice(1);

const result = splitted.map((pathFragment) => {
return parseInt(pathFragment);
});
return result;
}
}

export default Slip39
Loading

0 comments on commit c86a57a

Please sign in to comment.