Skip to content

Commit

Permalink
feat: derive child seed for swap clients
Browse files Browse the repository at this point in the history
This change uses separate mnemonics and seeds for each swap client that
are unique to that client, rather than using a single seed and mnemonic
across all clients. To accomplish this, a new method is added to the
seedutil tool to derive a child mnemonic from a provided mnemonic. This
is done by extracting the 16 bytes of entropy from the provided seed,
concatenating the entropy with the name of the client (e.g. "LND-BTC")
and then hashing the concatenation and taking 16 bytes from the
resulting hash.

Closes #1576.
  • Loading branch information
sangaman committed Oct 9, 2020
1 parent 0f2b841 commit 386b2ba
Show file tree
Hide file tree
Showing 6 changed files with 80 additions and 4 deletions.
9 changes: 7 additions & 2 deletions lib/lndclient/LndClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import { LightningClient, WalletUnlockerClient } from '../proto/lndrpc_grpc_pb';
import * as lndrpc from '../proto/lndrpc_pb';
import swapErrors from '../swaps/errors';
import SwapClient, { ChannelBalance, ClientStatus, PaymentState, SwapClientInfo, TradingLimits, WithdrawArguments } from '../swaps/SwapClient';
import { SwapDeal, CloseChannelParams, OpenChannelParams } from '../swaps/types';
import { CloseChannelParams, OpenChannelParams, SwapDeal } from '../swaps/types';
import { deriveChild } from '../utils/seedutil';
import { base64ToHex, hexToUint8Array } from '../utils/utils';
import errors from './errors';
import { Chain, ChannelCount, ClientMethods, LndClientConfig, LndInfo } from './types';
Expand Down Expand Up @@ -918,7 +919,11 @@ class LndClient extends SwapClient {
public initWallet = async (walletPassword: string, seedMnemonic: string[], restore = false, backup?: Uint8Array):
Promise<lndrpc.InitWalletResponse.AsObject> => {
const request = new lndrpc.InitWalletRequest();
request.setCipherSeedMnemonicList(seedMnemonic);

// from the master seed/mnemonic we derive a child mnemonic for this specific client
const childMnemonic = await deriveChild(seedMnemonic, this.label);
request.setCipherSeedMnemonicList(childMnemonic);

request.setWalletPassword(Uint8Array.from(Buffer.from(walletPassword, 'utf8')));
if (restore) {
request.setRecoveryWindow(2500);
Expand Down
3 changes: 2 additions & 1 deletion lib/nodekey/NodeKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,9 @@ class NodeKey {
}

/**
* Derives a child seed from the private key for the swap client
* Derives a child mnemonic seed from the private key for the swap client.
* @param swapClient the swap client to create the seed for
* @returns a BIP39 mnemonic
*/
public childSeed = (swapClient: SwapClientType) => {
const privKeyHex = this.privKey.toString('hex');
Expand Down
14 changes: 13 additions & 1 deletion lib/utils/seedutil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,18 @@ async function decipher(mnemonic: string[]) {
return Buffer.from(decipheredSeed, 'hex');
}

async function deriveChild(mnemonic: string[], clientType: string) {
const { stdout, stderr } = await exec(`${seedutilPath} derivechild -client ${clientType} ${mnemonic.join(' ')}`);

if (stderr) {
throw new Error(stderr);
}

const childMnenomic = stdout.trim().split(' ');
assert.equal(childMnenomic.length, 24, 'seedutil did not derive child mnemonic of exactly 24 words');
return childMnenomic;
}

async function generate() {
const { stdout, stderr } = await exec(`${seedutilPath} generate`);

Expand All @@ -65,4 +77,4 @@ async function generate() {
return mnemonic;
}

export { keystore, encipher, decipher, generate };
export { keystore, encipher, decipher, deriveChild, generate };
19 changes: 19 additions & 0 deletions seedutil/SeedUtil.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,25 @@ describe('SeedUtil generate', () => {
});
});

describe('SeedUtil derivechild', () => {
test('it errors without a client type', async () => {
const cmd = `./seedutil/seedutil derivechild ${VALID_SEED.seedWords.slice(0, 24).join(' ')}`;
await expect(executeCommand(cmd))
.rejects.toThrow('client is required');
});

test('it errors with 23 words', async () => {
const cmd = `./seedutil/seedutil derivechild -client BTC ${VALID_SEED.seedWords.slice(0, 23).join(' ')}`;
await expect(executeCommand(cmd))
.rejects.toThrow(ERRORS.INVALID_ARGS_LENGTH);
});

test('it succeeds with 24 words, no aezeed password', async () => {
const cmd = `./seedutil/seedutil derivechild -client BTC ${VALID_SEED_NO_PASS.seedWords.join(' ')}`;
await expect(executeCommand(cmd)).resolves.toMatchSnapshot();
});
});

describe('SeedUtil keystore', () => {
beforeEach(async () => {
await deleteDir(DEFAULT_KEYSTORE_PATH);
Expand Down
5 changes: 5 additions & 0 deletions seedutil/__snapshots__/SeedUtil.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ exports[`SeedUtil decipher it succeeds with 24 words, valid aezeed password 1`]
"
`;

exports[`SeedUtil derivechild it succeeds with 24 words, no aezeed password 1`] = `
"abandon improve various common first return kind mixed birth lend system section foil moral conduct expose eager glide abandon walnut agent right feel violin
"
`;

exports[`SeedUtil encipher it succeeds with 24 words, no aezeed password 1`] = `
"00738860374692022c462027a35aaaef3c3289aa0a057e2600000000002cad2e2b
"
Expand Down
34 changes: 34 additions & 0 deletions seedutil/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ func main() {
encipherCommand := flag.NewFlagSet("encipher", flag.ExitOnError)
decipherCommand := flag.NewFlagSet("decipher", flag.ExitOnError)
generateCommand := flag.NewFlagSet("generate", flag.ExitOnError)
deriveChildCommand := flag.NewFlagSet("derivechild", flag.ExitOnError)

if len(os.Args) < 2 {
fmt.Println("subcommand is required")
Expand Down Expand Up @@ -132,6 +133,39 @@ func main() {
}

fmt.Println(hex.EncodeToString(decipheredSeed[:]))
case "derivechild":
client := deriveChildCommand.String("client", "", "client type")
aezeedPassphrase := deriveChildCommand.String("aezeedpass", defaultAezeedPassphrase, "aezeed passphrase")
deriveChildCommand.Parse(os.Args[2:])
args = deriveChildCommand.Args()

if client == nil || len(*client) == 0 {
fmt.Fprintf(os.Stderr, "client is required")
os.Exit(1)
}

mnemonic := parseMnemonic(args)
cipherSeed := mnemonicToCipherSeed(mnemonic, aezeedPassphrase)

// derive 64-byte hash from client type + cipherSeed's 16 bytes of entropy
hash := sha512.Sum512(append([]byte(*client), cipherSeed.Entropy[:]...))

// use the first 16 byte of the hash as the new entropy
var newEntropy [16]byte
copy(newEntropy[:], hash[:16])

childCipherSeed, err := aezeed.New(0, &newEntropy, time.Now())
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
childMnemonic, err := childCipherSeed.ToMnemonic([]byte(*aezeedPassphrase))
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}

fmt.Println(strings.Join([]string(childMnemonic[:]), " "))
case "generate":
aezeedPassphrase := generateCommand.String("aezeedpass", defaultAezeedPassphrase, "aezeed passphrase")
generateCommand.Parse(os.Args[2:])
Expand Down

0 comments on commit 386b2ba

Please sign in to comment.