Skip to content

Commit

Permalink
Add Ledger-compatible derivation (#860)
Browse files Browse the repository at this point in the history
* Add Ledger-compatible derivation

* Export hd from root

* Renames
  • Loading branch information
jacogr authored Jan 30, 2021
1 parent 5158951 commit bc84220
Show file tree
Hide file tree
Showing 12 changed files with 173 additions and 2 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# CHANGELOG

## 5.5.1 Jan 31, 2021

Changes:

- Added `hdLedger` to allow for Ledger-compatible bip32+ed25519 derivation


## 5.4.1 Jan 24, 2021

Contributed:
Expand Down
4 changes: 4 additions & 0 deletions packages/networks/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const all: NetworkFromSubstrate[] = [
icon: 'polkadot',
network: 'polkadot',
prefix: 0,
slip44: 0x00000162,
standardAccount: '*25519',
symbols: ['DOT'],
website: 'https://polkadot.network'
Expand All @@ -53,6 +54,7 @@ const all: NetworkFromSubstrate[] = [
icon: 'polkadot',
network: 'kusama',
prefix: 2,
slip44: 0x000001b2,
standardAccount: '*25519',
symbols: ['KSM'],
website: 'https://kusama.network'
Expand Down Expand Up @@ -139,6 +141,7 @@ const all: NetworkFromSubstrate[] = [
hasLedgerSupport: true,
network: 'polymesh',
prefix: 12,
slip44: 0x00000253,
standardAccount: '*25519',
symbols: ['POLYX'],
website: 'https://polymath.network/'
Expand Down Expand Up @@ -234,6 +237,7 @@ const all: NetworkFromSubstrate[] = [
hasLedgerSupport: true,
network: 'dock-mainnet',
prefix: 22,
slip44: 0x00000252,
standardAccount: '*25519',
symbols: ['DCK'],
website: 'https://dock.io'
Expand Down
1 change: 1 addition & 0 deletions packages/networks/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export interface NetworkFromSubstrate {
hasLedgerSupport?: boolean;
icon?: Icon | null;
isIgnored?: boolean;
slip44?: number;
standardAccount: '*25519' | null;
symbols: string[] | null;
website: string | null;
Expand Down
4 changes: 4 additions & 0 deletions packages/util-crypto/src/hd/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Copyright 2017-2021 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0

export { hdLedger } from './ledger';
38 changes: 38 additions & 0 deletions packages/util-crypto/src/hd/ledger/derivePrivate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright 2017-2021 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0

import BN from 'bn.js';
import hash from 'hash.js';

import { bnToU8a, u8aConcat } from '@polkadot/util';

// performs hard-only derivation on the xprv
export function ledgerDerivePrivate (xprv: Uint8Array, index: number): Uint8Array {
const kl = xprv.slice(0, 32);
const kr = xprv.slice(32, 64);
const cc = xprv.slice(64, 96);

const data = new Uint8Array(1 + 64 + 4);

data.set(bnToU8a(index, { bitLength: 32, isLe: true }), 1 + 64);
data.set(kl, 1);
data.set(kr, 1 + 32);
data[0] = 0x00;

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const z = hash.hmac(hash.sha512, cc).update(data).digest();

data[0] = 0x01;

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const i = hash.hmac(hash.sha512, cc).update(data).digest();
const chainCode = i.slice(32, 64);
const zl = z.slice(0, 32);
const zr = z.slice(32, 64);
const left = bnToU8a(new BN(kl, 16, 'le').add(new BN(zl.slice(0, 28), 16, 'le').mul(new BN(8))), { bitLength: 512, isLe: true }).slice(0, 32);
const right = bnToU8a(new BN(kr, 16, 'le').add(new BN(zr, 16, 'le')), { bitLength: 512, isLe: true }).slice(0, 32);

return u8aConcat(left, right, chainCode);
}
24 changes: 24 additions & 0 deletions packages/util-crypto/src/hd/ledger/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright 2017-2021 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0

import { u8aToHex } from '@polkadot/util';

import { hdLedger } from '..';

describe('ledgerDerive', (): void => {
it('derives a known private key for Kusama', (): void => {
expect(u8aToHex(
hdLedger('abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about', `m/44'/${0x01b2}'/0'/0'/0'`)
.secretKey
.slice(0, 32)
)).toEqual('0x98cb4e14e0e08ea876f88d728545ea7572dc07dbbe69f1731c418fb827e69d41');
});

it('derives a known private key for Polkadot', (): void => {
expect(u8aToHex(
hdLedger('abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about', `m/44'/${0x80000162}/0'/0'/0'`)
.secretKey
.slice(0, 32)
)).toEqual('0xe8c68348586d53e4e8d1a864b0e4e17c75e4eb06e0c63c1432bef2ba29e69d41');
});
});
29 changes: 29 additions & 0 deletions packages/util-crypto/src/hd/ledger/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright 2017-2021 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0

import type { Keypair } from '../../types';

import { assert } from '@polkadot/util';

import { mnemonicValidate } from '../../mnemonic';
import { naclKeypairFromSeed } from '../../nacl';
import { ledgerDerivePrivate } from './derivePrivate';
import { ledgerMaster } from './master';
import { ledgerValidatePath } from './validatePath';

const HARDENED = 0x80000000;

export function hdLedger (mnemonic: string, path: string): Keypair {
assert(mnemonicValidate(mnemonic), 'Invalid mnemonic passed to ledger derivation');
assert(ledgerValidatePath(path), 'Invalid derivation path');

return naclKeypairFromSeed(
path
.split('/')
.slice(1)
.map((n) => parseInt(n.replace("'", ''), 10))
.map((n) => (n < HARDENED) ? (n + HARDENED) : n)
.reduce((x, n) => ledgerDerivePrivate(x, n), ledgerMaster(mnemonic))
.slice(0, 32)
);
}
17 changes: 17 additions & 0 deletions packages/util-crypto/src/hd/ledger/master.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright 2017-2021 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0

import { u8aToHex } from '@polkadot/util';

import { ledgerMaster } from './master';

const MNEMONIC = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
const XPRV = '0x402b03cd9c8bed9ba9f9bd6cd9c315ce9fcc59c7c25d37c85a36096617e69d418e35cb4a3b737afd007f0688618f21a8831643c0e6c77fc33c06026d2a0fc93832596435e70647d7d98ef102a32ea40319ca8fb6c851d7346d3bd8f9d1492658';

describe('ledgerDerive', (): void => {
it('derives a known master xprv', (): void => {
expect(u8aToHex(
ledgerMaster(MNEMONIC)
)).toEqual(XPRV);
});
});
32 changes: 32 additions & 0 deletions packages/util-crypto/src/hd/ledger/master.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright 2017-2021 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0

import hash from 'hash.js';

import { u8aConcat } from '@polkadot/util';

import { mnemonicToSeedSync } from '../../mnemonic/bip39';

const ED25519_CRYPTO = 'ed25519 seed';

// gets an xprv from a mnemonic
export function ledgerMaster (mnemonic: string): Uint8Array {
const seed = mnemonicToSeedSync(mnemonic);

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const chainCode = hash.hmac(hash.sha256, ED25519_CRYPTO).update(new Uint8Array([1, ...seed])).digest();
let priv;

while (!priv || (priv[31] & 0b0010_0000)) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
priv = hash.hmac(hash.sha512, ED25519_CRYPTO).update(priv || seed).digest();
}

priv[0] &= 0b1111_1000;
priv[31] &= 0b0111_1111;
priv[31] |= 0b0100_0000;

return u8aConcat(priv, chainCode);
}
14 changes: 14 additions & 0 deletions packages/util-crypto/src/hd/ledger/validatePath.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright 2017-2021 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0

export function ledgerValidatePath (path: string): boolean {
if (!path.startsWith('m/')) {
return false;
}

return !path
.split('/')
.slice(1)
.map((n) => parseInt(n.replace("'", ''), 10))
.some((n) => isNaN(n));
}
1 change: 1 addition & 0 deletions packages/util-crypto/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export * from './base64';
export * from './blake2';
export * from './crypto';
export * from './ethereum';
export * from './hd';
export * from './keccak';
export * from './key';
export * from './mnemonic';
Expand Down
4 changes: 2 additions & 2 deletions packages/util-crypto/src/mnemonic/toLegacySeed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ import { mnemonicToSeedSync } from './bip39';
* }
* ```
*/
export function mnemonicToLegacySeed (mnemonic: string, password = '', onlyJs = false): Uint8Array {
export function mnemonicToLegacySeed (mnemonic: string, password = '', onlyJs = false, length = 32): Uint8Array {
return isReady() && !onlyJs
? bip39ToSeed(mnemonic, password)
: mnemonicToSeedSync(mnemonic, password).subarray(0, 32);
: mnemonicToSeedSync(mnemonic, password).subarray(0, length);
}

0 comments on commit bc84220

Please sign in to comment.