Share hdValidatePath (Ledger & Ethereum) (#895)
* Share hsValidatePath (Ledger & Ethereum) * Adjust * Cleanup hdEthereum * Cleanups * Renames
This commit is contained in:
@@ -263,6 +263,25 @@ describe('keypair', (): void => {
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it('encodes a pair toJSON (and decodes)', (): void => {
|
||||
const pair = keyring.createFromUri('moral movie very draw assault whisper awful rebuild speed purity repeat card');
|
||||
const json = pair.toJson('password');
|
||||
|
||||
expect(json.address).toEqual('0x03ddca309bd5fedd01f914d6fb76f23aa848a2a520802159215dba5085d7863619');
|
||||
expect(json.encoding).toEqual({
|
||||
content: ['pkcs8', 'ecdsa'],
|
||||
type: ['scrypt', 'xsalsa20-poly1305'],
|
||||
version: '3'
|
||||
});
|
||||
|
||||
const newPair = keyring.createFromJson(json);
|
||||
|
||||
expect(newPair.publicKey).toEqual(pair.publicKey);
|
||||
expect(
|
||||
() => newPair.unlock('password')
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it('signs and verifies', (): void => {
|
||||
const MESSAGE = stringToU8a('this is a message');
|
||||
const pair = keyring.getPair(addressKeyOne);
|
||||
@@ -320,9 +339,12 @@ describe('keypair', (): void => {
|
||||
version: '3'
|
||||
});
|
||||
|
||||
const newPair = keyring.addFromJson(json);
|
||||
const newPair = keyring.createFromJson(json);
|
||||
|
||||
expect(newPair.publicKey).toEqual(pair.publicKey);
|
||||
expect(
|
||||
() => newPair.unlock('password')
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it('encodes a pair toJSON and back', (): void => {
|
||||
|
||||
@@ -205,11 +205,9 @@ export class Keyring implements KeyringInstance {
|
||||
* @description Encodes the input into an ss58 representation
|
||||
*/
|
||||
public encodeAddress = (address: Uint8Array | string, ss58Format?: number): string => {
|
||||
if (this.type === 'ethereum') {
|
||||
return ethereumEncode(address);
|
||||
} else {
|
||||
return encodeAddress(address, isUndefined(ss58Format) ? this.#ss58 : ss58Format);
|
||||
}
|
||||
return this.type === 'ethereum'
|
||||
? ethereumEncode(address)
|
||||
: encodeAddress(address, isUndefined(ss58Format) ? this.#ss58 : ss58Format);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright 2017-2021 @polkadot/keyring authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { hexToU8a } from '@polkadot/util';
|
||||
import { hexToU8a, u8aToHex } from '@polkadot/util';
|
||||
import { cryptoWaitReady, encodeAddress as toSS58, setSS58Format } from '@polkadot/util-crypto';
|
||||
|
||||
import { PAIRSSR25519 } from '../testing';
|
||||
@@ -162,6 +162,7 @@ describe('pair', (): void => {
|
||||
expect(pair.address).toEqual('0x4119b2e6c3Cb618F4f0B93ac77f9BeeC7FF02887');
|
||||
expect(pair.addressRaw).toEqual(hexToU8a('0x4119b2e6c3Cb618F4f0B93ac77f9BeeC7FF02887'));
|
||||
});
|
||||
|
||||
it('converts to json', (): void => {
|
||||
const pair = createPair({ toSS58, type: 'ethereum' }, { publicKey: PUBLICDERIVED, secretKey: SECRETDERIVED });
|
||||
const json = pair.toJson('password');
|
||||
@@ -171,7 +172,9 @@ describe('pair', (): void => {
|
||||
type: ['scrypt', 'xsalsa20-poly1305'],
|
||||
version: '3'
|
||||
});
|
||||
expect(json.address).toEqual(u8aToHex(PUBLICDERIVED));
|
||||
});
|
||||
|
||||
it('has Gerald as test address for Ethereum type parachains', (): void => {
|
||||
const keyringEthereum = createTestPairs({ type: 'ethereum' }, false);
|
||||
|
||||
|
||||
@@ -37,22 +37,14 @@ describe('hdEthereum', (): void => {
|
||||
]);
|
||||
|
||||
it('derives the right key pair from a mnemonic', (): void => {
|
||||
// derive seed from mnemonic
|
||||
const seed = mnemonicToLegacySeed(PHRASE, '', false, 64);
|
||||
|
||||
// obtain key from seed
|
||||
const key = hdEthereum(seed);
|
||||
const key = hdEthereum(mnemonicToLegacySeed(PHRASE, '', false, 64));
|
||||
|
||||
expect(key.publicKey).toEqual(PUBLIC);
|
||||
expect(key.secretKey).toEqual(SECRET);
|
||||
});
|
||||
|
||||
it('derives the right key pair from a mnemonic and a derivation path', (): void => {
|
||||
// derive seed from mnemonic
|
||||
const seed = mnemonicToLegacySeed(PHRASE, '', false, 64);
|
||||
|
||||
// obtain key from seed
|
||||
const key = hdEthereum(seed, derivationPath);
|
||||
const key = hdEthereum(mnemonicToLegacySeed(PHRASE, '', false, 64), derivationPath);
|
||||
|
||||
expect(key.publicKey).toEqual(PUBLICDERIVED);
|
||||
expect(key.secretKey).toEqual(SECRETDERIVED);
|
||||
|
||||
@@ -5,139 +5,73 @@ import type { Keypair } from '../../types';
|
||||
|
||||
import { assert, bnToU8a, stringToU8a, u8aConcat } from '@polkadot/util';
|
||||
|
||||
import { secp256k1KeypairFromSeed, secp256k1PrivateKeyTweakAdd } from '../..';
|
||||
import { hmacSha512 } from '../../hmac';
|
||||
import { secp256k1KeypairFromSeed, secp256k1PrivateKeyTweakAdd } from '../../secp256k1';
|
||||
import { HARDENED, hdValidatePath } from '../validatePath';
|
||||
|
||||
const MASTER_SECRET = stringToU8a('Bitcoin seed');
|
||||
const HARDENED_OFFSET = 0x80000000;
|
||||
|
||||
export class HDKeyEth {
|
||||
public chainCode: Uint8Array | null;
|
||||
export class HDKey {
|
||||
readonly chainCode: Uint8Array;
|
||||
readonly publicKey: Uint8Array;
|
||||
readonly secretKey: Uint8Array;
|
||||
|
||||
#privateKey: Uint8Array | null;
|
||||
#publicKey: Uint8Array | null;
|
||||
|
||||
constructor () {
|
||||
this.#privateKey = null;
|
||||
this.#publicKey = null;
|
||||
this.chainCode = null;
|
||||
}
|
||||
|
||||
// private key
|
||||
set privateKey (value: Uint8Array | null) {
|
||||
assert(value && value.length === 32, 'Private key must be 32 bytes.');
|
||||
|
||||
this.#privateKey = value;
|
||||
|
||||
if (value) {
|
||||
this.#publicKey = secp256k1KeypairFromSeed(value).publicKey;
|
||||
}
|
||||
}
|
||||
|
||||
get privateKey (): Uint8Array | null {
|
||||
return this.#privateKey;
|
||||
}
|
||||
|
||||
// public key
|
||||
|
||||
get publicKey (): Uint8Array | null {
|
||||
return this.#publicKey;
|
||||
}
|
||||
|
||||
set publicKey (value: Uint8Array | null) {
|
||||
assert(value && (value.length === 33 || value.length === 65), 'Public key must be 33 or 65 bytes.');
|
||||
|
||||
this.#publicKey = value; // new Uint8Array(Buffer.from(secp256k1.publicKeyConvert(value, true))); // force compressed point
|
||||
this.#privateKey = null;
|
||||
constructor (secretKey: Uint8Array, chainCode: Uint8Array) {
|
||||
this.secretKey = secretKey;
|
||||
this.publicKey = secp256k1KeypairFromSeed(secretKey).publicKey;
|
||||
this.chainCode = chainCode;
|
||||
}
|
||||
|
||||
// derive
|
||||
public derive (path: string): HDKeyEth {
|
||||
public derive (path: string): HDKey {
|
||||
if (path === 'm' || path === 'M' || path === "m'" || path === "M'") {
|
||||
return this;
|
||||
}
|
||||
|
||||
const entries = path.split('/');
|
||||
let hdkey = this as HDKeyEth;
|
||||
assert(hdValidatePath(path), 'Invalid derivation path');
|
||||
|
||||
entries.forEach((c, i): void => {
|
||||
if (i === 0) {
|
||||
assert(/^[mM]{1}/.test(c), 'Path must start with "m" or "M"');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const hardened = (c.length > 1) && (c[c.length - 1] === "'");
|
||||
let childIndex = parseInt(c, 10); // & (HARDENED_OFFSET - 1)
|
||||
|
||||
assert(childIndex < HARDENED_OFFSET, 'Invalid index');
|
||||
|
||||
if (hardened) {
|
||||
childIndex += HARDENED_OFFSET;
|
||||
}
|
||||
|
||||
hdkey = hdkey.deriveChild(childIndex);
|
||||
});
|
||||
|
||||
return hdkey;
|
||||
return path
|
||||
.split('/')
|
||||
.slice(1)
|
||||
.reduce((hd: HDKey, c): HDKey => hd.deriveChild(
|
||||
parseInt(c, 10) + (
|
||||
(c.length > 1) && c.endsWith("'")
|
||||
? HARDENED
|
||||
: 0
|
||||
)
|
||||
), this);
|
||||
}
|
||||
|
||||
// deriveChild
|
||||
private deriveChild (index: number): HDKeyEth {
|
||||
const isHardened = index >= HARDENED_OFFSET;
|
||||
private deriveChild (index: number): HDKey {
|
||||
const indexBuffer = bnToU8a(index, { bitLength: 32, isLe: false });
|
||||
let data: Uint8Array;
|
||||
|
||||
if (isHardened) { // Hardened child
|
||||
assert(this.privateKey, 'Could not derive hardened child key without private key');
|
||||
|
||||
// data = 0x00 || ser256(kpar) || ser32(index)
|
||||
data = u8aConcat(new Uint8Array(1), this.privateKey, indexBuffer);
|
||||
} else { // Normal child
|
||||
// data = serP(point(kpar)) || ser32(index)
|
||||
// = serP(Kpar) || ser32(index)
|
||||
assert(this.publicKey, 'Could not derive hardened child key : no publicKey');
|
||||
|
||||
data = u8aConcat(this.publicKey, indexBuffer);
|
||||
}
|
||||
|
||||
assert(this.chainCode, 'deriveChild error: no chainCode');
|
||||
assert(this.privateKey, 'PublicKey derivation without private key is not supported');
|
||||
|
||||
const I = hmacSha512(this.chainCode, data);
|
||||
const IL = I.slice(0, 32);
|
||||
const IR = I.slice(32);
|
||||
const hd = new HDKeyEth();
|
||||
const data = index >= HARDENED
|
||||
? u8aConcat(new Uint8Array(1), this.secretKey, indexBuffer)
|
||||
: u8aConcat(this.publicKey, indexBuffer);
|
||||
|
||||
// Private parent key -> private child key
|
||||
try {
|
||||
hd.privateKey = secp256k1PrivateKeyTweakAdd(this.privateKey, IL);
|
||||
// throw if IL >= n || (privateKey + IL) === 0
|
||||
const I = hmacSha512(this.chainCode, data);
|
||||
|
||||
return new HDKey(
|
||||
secp256k1PrivateKeyTweakAdd(this.secretKey, I.slice(0, 32)),
|
||||
I.slice(32)
|
||||
);
|
||||
} catch (err) {
|
||||
console.log('error when secp256k1PrivateKeyTweakAdd in eth key derivation', err);
|
||||
|
||||
// In case parse256(IL) >= n or ki == 0, one should proceed with the next value for i
|
||||
return this.deriveChild(index + 1);
|
||||
}
|
||||
|
||||
hd.chainCode = IR;
|
||||
|
||||
return hd;
|
||||
}
|
||||
}
|
||||
|
||||
export function hdEthereum (seed: Uint8Array, path = ''): Keypair {
|
||||
const hdkey = new HDKeyEth();
|
||||
const I = hmacSha512(MASTER_SECRET, seed);
|
||||
|
||||
hdkey.privateKey = I.slice(0, 32);
|
||||
hdkey.chainCode = I.slice(32);
|
||||
|
||||
const child = path
|
||||
const hdkey = new HDKey(I.slice(0, 32), I.slice(32));
|
||||
const { publicKey, secretKey } = path
|
||||
? hdkey.derive(path)
|
||||
: hdkey;
|
||||
|
||||
assert(child.publicKey && child.privateKey, 'Unable to derive HD key from path');
|
||||
|
||||
return { publicKey: child.publicKey, secretKey: child.privateKey };
|
||||
return { publicKey, secretKey };
|
||||
}
|
||||
|
||||
@@ -3,3 +3,4 @@
|
||||
|
||||
export { hdEthereum } from './ethereum';
|
||||
export { hdLedger } from './ledger';
|
||||
export { hdValidatePath } from './validatePath';
|
||||
|
||||
@@ -7,15 +7,13 @@ import { assert } from '@polkadot/util';
|
||||
|
||||
import { mnemonicValidate } from '../../mnemonic';
|
||||
import { naclKeypairFromSeed } from '../../nacl';
|
||||
import { HARDENED, hdValidatePath } from '../validatePath';
|
||||
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');
|
||||
assert(hdValidatePath(path), 'Invalid derivation path');
|
||||
|
||||
return naclKeypairFromSeed(
|
||||
path
|
||||
|
||||
+4
-2
@@ -1,7 +1,9 @@
|
||||
// Copyright 2017-2021 @polkadot/util-crypto authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
export function ledgerValidatePath (path: string): boolean {
|
||||
export const HARDENED = 0x80000000;
|
||||
|
||||
export function hdValidatePath (path: string): boolean {
|
||||
if (!path.startsWith('m/')) {
|
||||
return false;
|
||||
}
|
||||
@@ -10,5 +12,5 @@ export function ledgerValidatePath (path: string): boolean {
|
||||
.split('/')
|
||||
.slice(1)
|
||||
.map((n) => parseInt(n.replace("'", ''), 10))
|
||||
.some((n) => isNaN(n));
|
||||
.some((n) => isNaN(n) && n < HARDENED);
|
||||
}
|
||||
Reference in New Issue
Block a user