Implement first eth hd derivation test (#858)
* implement first eth hd test * lint * added gerald test address * test now passes * started implementing sith local secp256k1 * Pass publicKey through on verify * Some test fixes * renamed tweakadd * added suggestions * remove logs and complete test for tweakAdd * lint * reviewed * solve linting problems * relocate hd package * Apply suggestions from code review Co-authored-by: Jaco Greeff <jacogr@gmail.com> * added asset for tolegacyseed * yarn.lock and linting * remove isUint8 util * fix tests and added tests * fixed test Co-authored-by: Jaco Greeff <jacogr@gmail.com>
This commit is contained in:
co-authored by
Jaco Greeff
parent
c7e474cdc6
commit
c46cd5030e
@@ -285,25 +285,35 @@ describe('keypair', (): void => {
|
||||
});
|
||||
|
||||
describe('ethereum', (): void => {
|
||||
const PHRASE = 'seed sock milk update focus rotate barely fade car face mechanic mercy';
|
||||
// combine mnemonic with derivation path
|
||||
const PHRASE = 'seed sock milk update focus rotate barely fade car face mechanic mercy' + '/m/44\'/60\'/0\'/0/0';
|
||||
const ETH_ADDRESS_ONE = '0x31ea8795EE32D782C8ff41a5C68Dcbf0F5B27f6d';
|
||||
|
||||
let keyring: Keyring;
|
||||
|
||||
beforeEach((): void => {
|
||||
keyring = new Keyring({ type: 'ethereum' });
|
||||
});
|
||||
|
||||
it('adds a pair with the correct address', (): void => {
|
||||
it('creates with dev phrase with derivation path specified', (): void => {
|
||||
const pair = keyring.createFromUri(PHRASE);
|
||||
|
||||
expect(pair.publicKey).toEqual(hexToU8a('0x03b9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb13077'));
|
||||
expect(pair.address).toEqual('0x4119b2e6c3Cb618F4f0B93ac77f9BeeC7FF02887');
|
||||
expect(
|
||||
pair.address
|
||||
).toEqual(ETH_ADDRESS_ONE);
|
||||
});
|
||||
|
||||
it('creates with dev phrase with derivation path specified - addFromUri', (): void => {
|
||||
expect(
|
||||
keyring.addFromUri(PHRASE).address
|
||||
).toEqual(ETH_ADDRESS_ONE);
|
||||
});
|
||||
|
||||
it('encodes a pair toJSON', (): void => {
|
||||
const pair = keyring.createFromUri(PHRASE);
|
||||
const json = pair.toJson('password');
|
||||
|
||||
expect(json.address).toEqual('0x03b9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb13077');
|
||||
expect(json.address).toEqual(ETH_ADDRESS_ONE);
|
||||
expect(json.encoding).toEqual({
|
||||
content: ['pkcs8', 'ethereum'],
|
||||
type: ['scrypt', 'xsalsa20-poly1305'],
|
||||
|
||||
@@ -5,7 +5,8 @@ import type { Keypair, KeypairType } from '@polkadot/util-crypto/types';
|
||||
import type { KeyringInstance, KeyringOptions, KeyringPair, KeyringPair$Json, KeyringPair$JsonEncodingTypes, KeyringPair$Meta } from './types';
|
||||
|
||||
import { assert, hexToU8a, isHex, isUndefined, stringToU8a } from '@polkadot/util';
|
||||
import { base64Decode, decodeAddress, encodeAddress, keyExtractSuri, keyFromPath, mnemonicToLegacySeed, mnemonicToMiniSecret, naclKeypairFromSeed as naclFromSeed, schnorrkelKeypairFromSeed as schnorrkelFromSeed, secp256k1KeypairFromSeed as secp256k1FromSeed } from '@polkadot/util-crypto';
|
||||
import { base64Decode, decodeAddress, encodeAddress, ethereumEncode, keyExtractSuri, keyFromPath, mnemonicToLegacySeed, mnemonicToMiniSecret, naclKeypairFromSeed as naclFromSeed, schnorrkelKeypairFromSeed as schnorrkelFromSeed, secp256k1KeypairFromSeed as secp256k1FromSeed } from '@polkadot/util-crypto';
|
||||
import { HDKeyEth } from '@polkadot/util-crypto/hd';
|
||||
|
||||
import { DEV_PHRASE } from './defaults';
|
||||
import { createPair } from './pair';
|
||||
@@ -46,7 +47,7 @@ export class Keyring implements KeyringInstance {
|
||||
constructor (options: KeyringOptions = {}) {
|
||||
options.type = options.type || 'ed25519';
|
||||
|
||||
assert(options && ['ecdsa', 'ethereum', 'ed25519', 'sr25519'].includes(options.type || 'undefined'), `Expected a keyring type of either 'ed25519', 'sr25519' or 'ecdsa', found '${options.type}`);
|
||||
assert(options && ['ecdsa', 'ethereum', 'ed25519', 'sr25519'].includes(options.type || 'undefined'), `Expected a keyring type of either 'ed25519', 'sr25519', 'ethereum' or 'ecdsa', found '${options.type}`);
|
||||
|
||||
this.#pairs = new Pairs();
|
||||
this.#ss58 = options.ss58Format;
|
||||
@@ -172,7 +173,7 @@ export class Keyring implements KeyringInstance {
|
||||
const suri = _suri.startsWith('//')
|
||||
? `${DEV_PHRASE}${_suri}`
|
||||
: _suri;
|
||||
const { password, path, phrase } = keyExtractSuri(suri);
|
||||
const { derivePath, password, path, phrase } = keyExtractSuri(suri);
|
||||
let seed: Uint8Array;
|
||||
|
||||
if (isHex(phrase, 256)) {
|
||||
@@ -183,7 +184,7 @@ export class Keyring implements KeyringInstance {
|
||||
|
||||
if ([12, 15, 18, 21, 24].includes(parts.length)) {
|
||||
seed = type === 'ethereum'
|
||||
? mnemonicToLegacySeed(phrase)
|
||||
? mnemonicToLegacySeed(phrase, '', false, 64)
|
||||
: mnemonicToMiniSecret(phrase, password);
|
||||
} else {
|
||||
assert(str.length <= 32, 'specified phrase is not a valid mnemonic and is invalid as a raw seed at > 32 bytes');
|
||||
@@ -192,8 +193,18 @@ export class Keyring implements KeyringInstance {
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME Need to support Ethereum-type derivation paths
|
||||
const derived = keyFromPath(keypairFromSeed[type](seed), path, type);
|
||||
let derived: Keypair;
|
||||
|
||||
if (type === 'ethereum') {
|
||||
const key = HDKeyEth.fromMasterSeed(seed);
|
||||
const child = key.derive(derivePath.substring(1));
|
||||
|
||||
assert(child.publicKey && child.privateKey, 'Unable to derive HD key from path');
|
||||
|
||||
derived = { publicKey: child.publicKey, secretKey: child.privateKey };
|
||||
} else {
|
||||
derived = keyFromPath(keypairFromSeed[type](seed), path, type);
|
||||
}
|
||||
|
||||
return createPair({ toSS58: this.encodeAddress, type }, derived, meta, null);
|
||||
}
|
||||
@@ -203,7 +214,11 @@ export class Keyring implements KeyringInstance {
|
||||
* @description Encodes the input into an ss58 representation
|
||||
*/
|
||||
public encodeAddress = (address: Uint8Array | string, ss58Format?: number): string => {
|
||||
return encodeAddress(address, isUndefined(ss58Format) ? this.#ss58 : ss58Format);
|
||||
if (this.type === 'ethereum') {
|
||||
return ethereumEncode(address);
|
||||
} else {
|
||||
return encodeAddress(address, isUndefined(ss58Format) ? this.#ss58 : ss58Format);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { hexToU8a } from '@polkadot/util';
|
||||
import { cryptoWaitReady, encodeAddress as toSS58, setSS58Format } from '@polkadot/util-crypto';
|
||||
|
||||
import { PAIRS } from '../testing';
|
||||
import { PAIRSSR25519 } from '../testing';
|
||||
import { createTestPairs } from '../testingPairs';
|
||||
import { createPair } from '.';
|
||||
|
||||
@@ -124,11 +124,11 @@ describe('pair', (): void => {
|
||||
});
|
||||
|
||||
it('allows derivation on the pair', (): void => {
|
||||
const alice = createPair({ toSS58, type: 'sr25519' }, { publicKey: PAIRS[0].publicKey, secretKey: PAIRS[0].secretKey }, {});
|
||||
const alice = createPair({ toSS58, type: 'sr25519' }, { publicKey: PAIRSSR25519[0].publicKey, secretKey: PAIRSSR25519[0].secretKey }, {});
|
||||
const stash = alice.derive('//stash');
|
||||
const soft = alice.derive('//funding/0');
|
||||
|
||||
expect(stash.publicKey).toEqual(PAIRS[1].publicKey);
|
||||
expect(stash.publicKey).toEqual(PAIRSSR25519[1].publicKey);
|
||||
expect(soft.address).toEqual('5ECQNn7UueWHPFda5qUi4fTmTtyCnPvGnuoyVVSj5CboJh9J');
|
||||
});
|
||||
|
||||
@@ -148,5 +148,10 @@ describe('pair', (): void => {
|
||||
expect(pair.address).toEqual('0x4119b2e6c3Cb618F4f0B93ac77f9BeeC7FF02887');
|
||||
expect(pair.addressRaw).toEqual(hexToU8a('0x4119b2e6c3Cb618F4f0B93ac77f9BeeC7FF02887'));
|
||||
});
|
||||
it('has Gerald as test address for Ethereum type parachains', (): void => {
|
||||
const keyringEthereum = createTestPairs({ type: 'ethereum' }, false);
|
||||
|
||||
expect(keyringEthereum.Gerald.address).toEqual('0x6Be02d1d3665660d22FF9624b7BE0551ee1Ac91b');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,8 +5,8 @@ import type { Keypair, KeypairType } from '@polkadot/util-crypto/types';
|
||||
import type { KeyringPair, KeyringPair$Json, KeyringPair$JsonEncodingTypes, KeyringPair$Meta, SignOptions } from '../types';
|
||||
import type { PairInfo } from './types';
|
||||
|
||||
import { assert, u8aConcat, u8aEq, u8aToHex, u8aToU8a } from '@polkadot/util';
|
||||
import { blake2AsU8a, ethereumEncode, keccakAsU8a, keyExtractPath, keyFromPath, naclKeypairFromSeed as naclFromSeed, naclSign, schnorrkelKeypairFromSeed as schnorrkelFromSeed, schnorrkelSign, schnorrkelVrfSign, schnorrkelVrfVerify, secp256k1Compress, secp256k1Expand, secp256k1KeypairFromSeed as secp256k1FromSeed, secp256k1Sign, signatureVerify } from '@polkadot/util-crypto';
|
||||
import { assert, u8aConcat, u8aEq, u8aToU8a } from '@polkadot/util';
|
||||
import { blake2AsU8a, ethereumEncode, keccakAsU8a, keyExtractPath, keyFromPath, naclKeypairFromSeed as naclFromSeed, naclSign, schnorrkelKeypairFromSeed as schnorrkelFromSeed, schnorrkelSign, schnorrkelVrfSign, schnorrkelVrfVerify, secp256k1Expand, secp256k1KeypairFromSeed as secp256k1FromSeed, secp256k1Sign, signatureVerify } from '@polkadot/util-crypto';
|
||||
|
||||
import { decodePair } from './decode';
|
||||
import { encodePair } from './encode';
|
||||
@@ -172,9 +172,7 @@ export function createPair ({ toSS58, type }: Setup, { publicKey, secretKey }: P
|
||||
);
|
||||
},
|
||||
toJson: (passphrase?: string): KeyringPair$Json => {
|
||||
const address = ['ecdsa', 'ethereum'].includes(type)
|
||||
? u8aToHex(secp256k1Compress(publicKey))
|
||||
: encodeAddress();
|
||||
const address = encodeAddress();
|
||||
|
||||
return pairToJson(type, { address, meta }, recode(passphrase), !!passphrase);
|
||||
},
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import type { KeyringInstance, KeyringOptions } from './types';
|
||||
|
||||
import { hexToU8a } from '@polkadot/util';
|
||||
import { KeypairType } from '@polkadot/util-crypto/types';
|
||||
|
||||
import { createPair } from './pair';
|
||||
import Keyring from '.';
|
||||
@@ -13,53 +14,75 @@ interface PairDef {
|
||||
publicKey: Uint8Array;
|
||||
seed: string;
|
||||
secretKey?: Uint8Array;
|
||||
type?: 'ed25519' | 'sr25519';
|
||||
type: KeypairType
|
||||
}
|
||||
|
||||
// NOTE This is not great since we have the secretKey here explicitly, but a testing
|
||||
// keyring is for testing - what happens is that in most cases the keyring is initialises
|
||||
// before anything else. Since the sr25519 crypto is async, this creates problems with
|
||||
// adding the keys when only the keyring is used.
|
||||
export const PAIRS: PairDef[] = [
|
||||
export const PAIRSSR25519: PairDef[] = [
|
||||
{
|
||||
publicKey: hexToU8a('0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d'),
|
||||
secretKey: hexToU8a('0x98319d4ff8a9508c4bb0cf0b5a78d760a0b2082c02775e6e82370816fedfff48925a225d97aa00682d6a59b95b18780c10d7032336e88f3442b42361f4a66011'),
|
||||
seed: 'Alice'
|
||||
seed: 'Alice',
|
||||
type: 'sr25519'
|
||||
},
|
||||
{
|
||||
publicKey: hexToU8a('0xbe5ddb1579b72e84524fc29e78609e3caf42e85aa118ebfe0b0ad404b5bdd25f'),
|
||||
secretKey: hexToU8a('0xe8da6c9d810e020f5e3c7f5af2dea314cbeaa0d72bc6421e92c0808a0c584a6046ab28e97c3ffc77fe12b5a4d37e8cd4afbfebbf2391ffc7cb07c0f38c023efd'),
|
||||
seed: 'Alice//stash'
|
||||
seed: 'Alice//stash',
|
||||
type: 'sr25519'
|
||||
},
|
||||
{
|
||||
publicKey: hexToU8a('0x8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48'),
|
||||
secretKey: hexToU8a('0x081ff694633e255136bdb456c20a5fc8fed21f8b964c11bb17ff534ce80ebd5941ae88f85d0c1bfc37be41c904e1dfc01de8c8067b0d6d5df25dd1ac0894a325'),
|
||||
seed: 'Bob'
|
||||
seed: 'Bob',
|
||||
type: 'sr25519'
|
||||
},
|
||||
{
|
||||
publicKey: hexToU8a('0xfe65717dad0447d715f660a0a58411de509b42e6efb8375f562f58a554d5860e'),
|
||||
secretKey: hexToU8a('0xc006507cdfc267a21532394c49ca9b754ca71de21e15a1cdf807c7ceab6d0b6c3ed408d9d35311540dcd54931933e67cf1ea10d46f75408f82b789d9bd212fde'),
|
||||
seed: 'Bob//stash'
|
||||
seed: 'Bob//stash',
|
||||
type: 'sr25519'
|
||||
},
|
||||
{
|
||||
publicKey: hexToU8a('0x90b5ab205c6974c9ea841be688864633dc9ca8a357843eeacf2314649965fe22'),
|
||||
secretKey: hexToU8a('0xa8f2d83016052e5d6d77b2f6fd5d59418922a09024cda701b3c34369ec43a7668faf12ff39cd4e5d92bb773972f41a7a5279ebc2ed92264bed8f47d344f8f18c'),
|
||||
seed: 'Charlie'
|
||||
seed: 'Charlie',
|
||||
type: 'sr25519'
|
||||
},
|
||||
{
|
||||
publicKey: hexToU8a('0x306721211d5404bd9da88e0204360a1a9ab8b87c66c1bc2fcdd37f3c2222cc20'),
|
||||
secretKey: hexToU8a('0x20e05482ca4677e0edbc58ae9a3a59f6ed3b1a9484ba17e64d6fe8688b2b7b5d108c4487b9323b98b11fe36cb301b084e920f7b7895536809a6d62a451b25568'),
|
||||
seed: 'Dave'
|
||||
seed: 'Dave',
|
||||
type: 'sr25519'
|
||||
},
|
||||
{
|
||||
publicKey: hexToU8a('0xe659a7a1628cdd93febc04a4e0646ea20e9f5f0ce097d9a05290d4a9e054df4e'),
|
||||
secretKey: hexToU8a('0x683576abfd5dc35273e4264c23095a1bf21c14517bece57c7f0cc5c0ed4ce06a3dbf386b7828f348abe15d76973a72009e6ef86a5c91db2990cb36bb657c6587'),
|
||||
seed: 'Eve'
|
||||
seed: 'Eve',
|
||||
type: 'sr25519'
|
||||
},
|
||||
{
|
||||
publicKey: hexToU8a('0x1cbd2d43530a44705ad088af313e18f80b53ef16b36177cd4b77b846f2a5f07c'),
|
||||
secretKey: hexToU8a('0xb835c20f450079cf4f513900ae9faf8df06ad86c681884122c752a4b2bf74d4303e4f21bc6cc62bb4eeed5a9cce642c25e2d2ac1464093b50f6196d78e3a7426'),
|
||||
seed: 'Ferdie'
|
||||
seed: 'Ferdie',
|
||||
type: 'sr25519'
|
||||
}
|
||||
];
|
||||
|
||||
export const PAIRSETHEREUM: PairDef[] = [
|
||||
{
|
||||
name: 'Gerald',
|
||||
publicKey: new Uint8Array([3, 98, 79, 114, 14, 174, 103, 106,
|
||||
4, 17, 22, 49, 201, 202, 51, 140,
|
||||
17, 208, 245, 168, 14, 228, 34, 16,
|
||||
198, 190, 114, 152, 60, 235, 98, 15,
|
||||
191]),
|
||||
secretKey: hexToU8a('0x99b3c12287537e38c90a9219d4cb074a89a16e9cdb20bf85728ebd97c343e342'),
|
||||
seed: 'Gerald',
|
||||
type: 'ethereum'
|
||||
}
|
||||
];
|
||||
|
||||
@@ -71,8 +94,9 @@ export const PAIRS: PairDef[] = [
|
||||
*/
|
||||
export function createTestKeyring (options: KeyringOptions = {}, isDerived = true): KeyringInstance {
|
||||
const keyring = new Keyring(options);
|
||||
const pairs: PairDef[] = (options.type && options.type === 'ethereum') ? PAIRSETHEREUM : PAIRSSR25519;
|
||||
|
||||
PAIRS.forEach(({ name, publicKey, secretKey, seed, type = 'sr25519' }): void => {
|
||||
pairs.forEach(({ name, publicKey, secretKey, seed, type }): void => {
|
||||
const meta = {
|
||||
isTesting: true,
|
||||
name: name || seed.replace('//', '_').toLowerCase()
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"elliptic": "^6.5.4",
|
||||
"hash.js": "^1.1.7",
|
||||
"js-sha3": "^0.8.0",
|
||||
"safe-buffer": "^5.1.1",
|
||||
"scryptsy": "^2.1.0",
|
||||
"tweetnacl": "^1.0.3",
|
||||
"xxhashjs": "^0.2.2"
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
// Copyright 2017-2021 @polkadot/util-crypto authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { mnemonicToLegacySeed } from '@polkadot/util-crypto';
|
||||
|
||||
import { HDKeyEth } from '.';
|
||||
|
||||
describe('HDKeyEth', (): void => {
|
||||
const PHRASE = 'seed sock milk update focus rotate barely fade car face mechanic mercy';
|
||||
const derivationPath = 'm/44\'/60\'/0\'/0/0';
|
||||
const PUBLIC = new Uint8Array([
|
||||
3, 118, 64, 77, 247, 27, 4, 157,
|
||||
236, 206, 251, 221, 230, 244, 154, 147,
|
||||
189, 131, 249, 169, 102, 78, 3, 185,
|
||||
153, 19, 89, 40, 24, 25, 139, 131,
|
||||
93
|
||||
]);
|
||||
const SECRET = new Uint8Array([
|
||||
166, 162, 203, 17, 2, 206, 110, 176,
|
||||
18, 102, 230, 144, 90, 158, 25, 232,
|
||||
43, 180, 176, 49, 189, 149, 3, 71,
|
||||
243, 228, 223, 104, 125, 132, 58, 228
|
||||
]
|
||||
);
|
||||
const PUBLICDERIVED = new Uint8Array([
|
||||
3, 129, 53, 27, 27, 70, 210, 96,
|
||||
43, 9, 146, 187, 93, 85, 49, 249,
|
||||
193, 105, 107, 8, 18, 254, 178, 83,
|
||||
75, 104, 132, 173, 196, 126, 46, 29,
|
||||
139
|
||||
]);
|
||||
const SECRETDERIVED = new Uint8Array([
|
||||
7, 13, 195, 17, 115, 0, 1, 25,
|
||||
24, 226, 107, 2, 23, 105, 69, 204,
|
||||
21, 195, 213, 72, 207, 73, 253, 132,
|
||||
24, 217, 127, 147, 175, 105, 158, 70
|
||||
]);
|
||||
|
||||
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 = HDKeyEth.fromMasterSeed(seed);
|
||||
|
||||
expect(key.publicKey).toEqual(PUBLIC);
|
||||
expect(key.privateKey).toEqual(SECRET);
|
||||
|
||||
// derive key from original key using derivation path
|
||||
const { privateKey, publicKey } = key.derive(derivationPath);
|
||||
|
||||
expect(publicKey).toEqual(PUBLICDERIVED);
|
||||
expect(privateKey).toEqual(SECRETDERIVED);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,222 @@
|
||||
// Copyright 2017-2021 @polkadot/util-crypto authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as crypto from 'crypto';
|
||||
import { Buffer } from 'safe-buffer';
|
||||
|
||||
import { assert } from '@polkadot/util';
|
||||
|
||||
import { secp256k1KeypairFromSeed, secp256k1PrivateKeyTweakAdd } from '../..';
|
||||
|
||||
const MASTER_SECRET = Buffer.from('Bitcoin seed', 'utf8');
|
||||
const HARDENED_OFFSET = 0x80000000;
|
||||
// Bitcoin hardcoded by default, can use package `coininfo` for others
|
||||
const BITCOIN_VERSIONS = { private: 0x0488ADE4, public: 0x0488B21E };
|
||||
|
||||
interface Versions{
|
||||
private: number;
|
||||
public: number;
|
||||
}
|
||||
|
||||
// function to retype Buffer to ArrayBuffer in a controlled manner
|
||||
function bufferAsArrayBuffer (buffer: Buffer): ArrayBuffer {
|
||||
return buffer as unknown as ArrayBuffer;
|
||||
}
|
||||
|
||||
// function to retype Uint8Array to Buffer in a controlled manner
|
||||
function bytesAsBuffer (bytes: Uint8Array): Buffer {
|
||||
return bytes as unknown as Buffer;
|
||||
}
|
||||
|
||||
export class HDKeyEth {
|
||||
versions: Versions;
|
||||
public depth: number;
|
||||
index: number;
|
||||
private _privateKey: Uint8Array|null;
|
||||
private _publicKey: Uint8Array|null;
|
||||
public chainCode: Uint8Array|null;
|
||||
private _fingerprint: number|null;
|
||||
parentFingerprint: number|null;
|
||||
private _identifier: Buffer|null;
|
||||
|
||||
constructor (versions?: Versions) {
|
||||
this.versions = versions || BITCOIN_VERSIONS;
|
||||
this.depth = 0;
|
||||
this.index = 0;
|
||||
this._privateKey = null;
|
||||
this._publicKey = null;
|
||||
this._identifier = null;
|
||||
this.chainCode = null;
|
||||
this._fingerprint = null;
|
||||
this.parentFingerprint = 0;
|
||||
}
|
||||
|
||||
// private key
|
||||
set privateKey (value: Uint8Array|null) {
|
||||
assert(value && value.length === 32, 'Private key must be 32 bytes.');
|
||||
// TODO: implement privateKeyVerify for local secp256k1
|
||||
// assert(secp256k1.privateKeyVerify(value) === true, 'Invalid private key');
|
||||
|
||||
this._privateKey = value;
|
||||
|
||||
if (value) {
|
||||
this._publicKey = secp256k1KeypairFromSeed(value).publicKey;
|
||||
this._identifier = this._publicKey ? this.hash160(this._publicKey) : null;
|
||||
this._fingerprint = this._identifier ? this._identifier.slice(0, 4).readUInt32BE(0) : null;
|
||||
}
|
||||
}
|
||||
|
||||
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.');
|
||||
// TODO: implement publicKeyVerify for local secp256k1
|
||||
// assert(secp256k1.publicKeyVerify(value) === true, 'Invalid public key');
|
||||
|
||||
// TODO: should I use the compress function here?
|
||||
this._publicKey = value; // new Uint8Array(Buffer.from(secp256k1.publicKeyConvert(value, true))); // force compressed point
|
||||
this._identifier = this._publicKey ? this.hash160(this._publicKey) : null;
|
||||
this._fingerprint = this._identifier ? this._identifier.slice(0, 4).readUInt32BE(0) : null;
|
||||
this._privateKey = null;
|
||||
}
|
||||
|
||||
// fingerprint
|
||||
get fingerprint (): number|null {
|
||||
return this._fingerprint;
|
||||
}
|
||||
|
||||
// identifier
|
||||
get identifier (): Buffer|null {
|
||||
return this._identifier;
|
||||
}
|
||||
|
||||
// fingerprint
|
||||
get pubKeyHash (): Buffer|null {
|
||||
return this.identifier;
|
||||
}
|
||||
|
||||
// derive
|
||||
public derive (path: string): HDKeyEth {
|
||||
if (path === 'm' || path === 'M' || path === "m'" || path === "M'") {
|
||||
return this;
|
||||
}
|
||||
|
||||
const entries = path.split('/');
|
||||
let hdkey = this as HDKeyEth;
|
||||
|
||||
entries.forEach(function (c, i) {
|
||||
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;
|
||||
}
|
||||
|
||||
// deriveChild
|
||||
private deriveChild (index: number): HDKeyEth {
|
||||
const isHardened = index >= HARDENED_OFFSET;
|
||||
const indexBuffer = Buffer.allocUnsafe(4);
|
||||
|
||||
indexBuffer.writeUInt32BE(index, 0);
|
||||
|
||||
let data;
|
||||
|
||||
if (isHardened) { // Hardened child
|
||||
assert(this.privateKey, 'Could not derive hardened child key');
|
||||
|
||||
if (this.privateKey) {
|
||||
const pk = this.privateKey;
|
||||
const zb = Buffer.alloc(1);
|
||||
const pkConcat = Buffer.concat([zb, bytesAsBuffer(pk)]);
|
||||
|
||||
// data = 0x00 || ser256(kpar) || ser32(index)
|
||||
data = Buffer.concat([pkConcat, indexBuffer]);
|
||||
} else {
|
||||
throw new Error('Could not derive hardened child key : no privatekey');
|
||||
}
|
||||
} else { // Normal child
|
||||
// data = serP(point(kpar)) || ser32(index)
|
||||
// = serP(Kpar) || ser32(index)
|
||||
if (this.publicKey) {
|
||||
data = Buffer.concat([bytesAsBuffer(this.publicKey), indexBuffer]);
|
||||
} else {
|
||||
throw new Error('Could not derive hardened child key : no publicKey');
|
||||
}
|
||||
}
|
||||
|
||||
let I: Buffer = Buffer.from('');
|
||||
|
||||
if (this.chainCode) {
|
||||
I = bytesAsBuffer(crypto.createHmac('sha512', this.chainCode).update(data as unknown as crypto.BinaryLike).digest());
|
||||
} else {
|
||||
throw new Error('deriveChild error: no chainCode');
|
||||
}
|
||||
|
||||
const IL: Buffer = I.slice(0, 32);
|
||||
const IR: Buffer = I.slice(32);
|
||||
|
||||
const hd = new HDKeyEth(this.versions);
|
||||
|
||||
// Private parent key -> private child key
|
||||
if (this.privateKey) {
|
||||
// ki = parse256(IL) + kpar (mod n)r
|
||||
try {
|
||||
hd.privateKey = new Uint8Array(bufferAsArrayBuffer(Buffer.from(secp256k1PrivateKeyTweakAdd(new Uint8Array(bufferAsArrayBuffer(Buffer.from(this.privateKey))), new Uint8Array(bufferAsArrayBuffer(IL))))));
|
||||
// throw if IL >= n || (privateKey + IL) === 0
|
||||
} 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);
|
||||
}
|
||||
// Public parent key -> public child key
|
||||
} else {
|
||||
throw new Error('PublicKey derivation without private key is not supported at the moment');
|
||||
}
|
||||
|
||||
hd.chainCode = new Uint8Array(bufferAsArrayBuffer(IR));
|
||||
hd.depth = this.depth + 1;
|
||||
hd.parentFingerprint = this.fingerprint;// .readUInt32BE(0)
|
||||
hd.index = index;
|
||||
|
||||
return hd;
|
||||
}
|
||||
|
||||
// fromMasterSeed
|
||||
public static fromMasterSeed = function (seedBuffer: crypto.BinaryLike, versions?: Versions): HDKeyEth {
|
||||
const I = crypto.createHmac('sha512', new Uint8Array(bufferAsArrayBuffer(MASTER_SECRET))).update(seedBuffer).digest();
|
||||
const IL = I.slice(0, 32);
|
||||
const IR = I.slice(32);
|
||||
|
||||
const hdkey = versions ? new HDKeyEth(versions) : new HDKeyEth();
|
||||
|
||||
hdkey.chainCode = new Uint8Array(IR);
|
||||
hdkey.privateKey = new Uint8Array(IL);
|
||||
|
||||
return hdkey;
|
||||
}
|
||||
|
||||
private hash160 (buf: crypto.BinaryLike): Buffer {
|
||||
const sha = crypto.createHash('sha256').update(buf).digest();
|
||||
|
||||
return (crypto.createHash('ripemd160').update(sha).digest()) as unknown as Buffer;
|
||||
}
|
||||
}
|
||||
@@ -2,3 +2,4 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
export { hdLedger } from './ledger';
|
||||
export { HDKeyEth } from './ethereum';
|
||||
|
||||
@@ -7,6 +7,7 @@ import { DeriveJunction } from './DeriveJunction';
|
||||
import { keyExtractPath } from './extractPath';
|
||||
|
||||
export interface ExtractResult {
|
||||
derivePath: string,
|
||||
password?: string;
|
||||
path: DeriveJunction[];
|
||||
phrase: string;
|
||||
@@ -27,6 +28,7 @@ export function keyExtractSuri (suri: string): ExtractResult {
|
||||
const { path } = keyExtractPath(derivePath);
|
||||
|
||||
return {
|
||||
derivePath,
|
||||
password,
|
||||
path,
|
||||
phrase
|
||||
|
||||
@@ -7,7 +7,8 @@ import { waitReady } from '@polkadot/wasm-crypto';
|
||||
import { mnemonicToLegacySeed } from './';
|
||||
|
||||
const MNEMONIC = 'seed sock milk update focus rotate barely fade car face mechanic mercy';
|
||||
const SEED = '0x3c121e20de068083b49c2315697fb59a2d9e8643c24e5ea7628132c58969a027';
|
||||
const SEED_32 = '0x3c121e20de068083b49c2315697fb59a2d9e8643c24e5ea7628132c58969a027';
|
||||
const SEED_64 = '0x3c121e20de068083b49c2315697fb59a2d9e8643c24e5ea7628132c58969a0275693dd5bd9d4cc9e648475eba9613ed4678f4d62560a9c42f75bac04022ded25';
|
||||
|
||||
describe('mnemonicToLegacySeed', (): void => {
|
||||
beforeEach(async (): Promise<void> => {
|
||||
@@ -26,10 +27,18 @@ describe('mnemonicToLegacySeed', (): void => {
|
||||
});
|
||||
|
||||
[false, true].forEach((onlyJs): void => {
|
||||
it(`generates a valid seed (onlyJs = ${onlyJs.toString()})`, (): void => {
|
||||
it(`generates a valid 64bytes seed (onlyJs = ${onlyJs.toString()})`, (): void => {
|
||||
expect(
|
||||
u8aToHex(mnemonicToLegacySeed(MNEMONIC, undefined, onlyJs, 64))
|
||||
).toEqual(SEED_64);
|
||||
});
|
||||
});
|
||||
|
||||
[false, true].forEach((onlyJs): void => {
|
||||
it(`generates a valid 32bytes seed (onlyJs = ${onlyJs.toString()})`, (): void => {
|
||||
expect(
|
||||
u8aToHex(mnemonicToLegacySeed(MNEMONIC, undefined, onlyJs))
|
||||
).toEqual(SEED);
|
||||
).toEqual(SEED_32);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -24,10 +24,17 @@ import { mnemonicValidate } from './validate';
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function mnemonicToLegacySeed (mnemonic: string, password = '', onlyJs = false): Uint8Array {
|
||||
export function mnemonicToLegacySeed (mnemonic: string, password = '', onlyJs = false, byteLength: 32 | 64 = 32): Uint8Array {
|
||||
assert(mnemonicValidate(mnemonic), 'Invalid bip39 mnemonic specified');
|
||||
assert([32, 64].includes(byteLength), `Invalid seed length ${byteLength}, expected 32 or 64`);
|
||||
|
||||
return isReady() && !onlyJs
|
||||
? bip39ToSeed(mnemonic, password)
|
||||
: mnemonicToSeedSync(mnemonic, password).subarray(0, 32);
|
||||
if (byteLength && byteLength === 32) {
|
||||
return isReady() && !onlyJs
|
||||
? bip39ToSeed(mnemonic, password)
|
||||
: mnemonicToSeedSync(mnemonic, password).subarray(0, 32);
|
||||
} else if (byteLength === 64) {
|
||||
return mnemonicToSeedSync(mnemonic, password);
|
||||
} else {
|
||||
return new Uint8Array();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,3 +7,4 @@ export { secp256k1KeypairFromSeed } from './keypair/fromSeed';
|
||||
export { secp256k1Recover } from './recover';
|
||||
export { secp256k1Sign } from './sign';
|
||||
export { secp256k1Verify } from './verify';
|
||||
export { secp256k1PrivateKeyTweakAdd } from './tweakAdd';
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright 2017-2021 @polkadot/util-crypto authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { secp256k1PrivateKeyTweakAdd } from './tweakAdd';
|
||||
|
||||
describe('TweakAdd', (): void => {
|
||||
describe('PrivateKey TweakAdd', (): void => {
|
||||
it('succeeds for a simple case', (): void => {
|
||||
const A = new Uint8Array([0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1]);
|
||||
const B = new Uint8Array([3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3]);
|
||||
|
||||
expect(
|
||||
secp256k1PrivateKeyTweakAdd(A, B).toString()
|
||||
).toBe(new Uint8Array([
|
||||
3, 4, 3, 4, 3, 4, 3, 4, 3,
|
||||
4, 3, 4, 3, 4, 3, 4, 3, 4,
|
||||
3, 4, 3, 4, 3, 4, 3, 4, 3,
|
||||
4, 3, 4, 3, 4
|
||||
]).toString());
|
||||
});
|
||||
it('fails for wrong array length', (): void => {
|
||||
const A = new Uint8Array([0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1]);
|
||||
const B = new Uint8Array([3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3]);
|
||||
let bool = false;
|
||||
|
||||
try {
|
||||
secp256k1PrivateKeyTweakAdd(A, B);
|
||||
} catch (e) {
|
||||
expect((e as Error).toString()).toEqual('Error: Expected tweak to be an Uint8Array with length 32');
|
||||
bool = true;
|
||||
}
|
||||
|
||||
expect(bool).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright 2017-2021 @polkadot/util-crypto authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import BN from 'bn.js';
|
||||
import { ec as EC } from 'elliptic';
|
||||
|
||||
import { assert, isU8a } from '@polkadot/util';
|
||||
|
||||
const ec = new EC('secp256k1');
|
||||
const ecparams = ec.curve as {n: BN};
|
||||
|
||||
const errors = {
|
||||
PUBKEY_PARSE: 'Public Key could not be parsed',
|
||||
TWEAK_ADD: 'The tweak was out of range or the resulted private key is invalid'
|
||||
};
|
||||
|
||||
// Private key
|
||||
|
||||
export function secp256k1PrivateKeyTweakAdd (seckey: Uint8Array, tweak: Uint8Array): Uint8Array {
|
||||
assert(isU8a(seckey) && seckey.length === 32, 'Expected seckey to be an Uint8Array with length 32');
|
||||
assert(isU8a(tweak) && tweak.length === 32, 'Expected tweak to be an Uint8Array with length 32');
|
||||
|
||||
switch (_secp256k1PrivateKeyTweakAdd(seckey, tweak)) {
|
||||
case 0:
|
||||
return seckey;
|
||||
case 1:
|
||||
throw new Error(errors.TWEAK_ADD);
|
||||
}
|
||||
|
||||
return seckey;
|
||||
}
|
||||
|
||||
function _secp256k1PrivateKeyTweakAdd (seckey: Uint8Array, tweak: Uint8Array): number {
|
||||
const bn = new BN(tweak);
|
||||
|
||||
if (bn.cmp(ecparams.n) >= 0) return 1;
|
||||
|
||||
bn.iadd(new BN(seckey));
|
||||
if (bn.cmp(ecparams.n) >= 0) bn.isub(ecparams.n);
|
||||
if (bn.isZero()) return 1;
|
||||
const tweaked = bn.toArrayLike(Buffer, 'be', 32);
|
||||
|
||||
seckey.set(tweaked);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -2019,6 +2019,7 @@ __metadata:
|
||||
elliptic: ^6.5.4
|
||||
hash.js: ^1.1.7
|
||||
js-sha3: ^0.8.0
|
||||
safe-buffer: ^5.1.1
|
||||
scryptsy: ^2.1.0
|
||||
tweetnacl: ^1.0.3
|
||||
xxhashjs: ^0.2.2
|
||||
@@ -9588,7 +9589,7 @@ fsevents@~2.1.2:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"safe-buffer@npm:^5.0.1, safe-buffer@npm:^5.1.2, safe-buffer@npm:^5.2.0, safe-buffer@npm:~5.2.0":
|
||||
"safe-buffer@npm:^5.0.1, safe-buffer@npm:^5.1.1, safe-buffer@npm:^5.1.2, safe-buffer@npm:^5.2.0, safe-buffer@npm:~5.2.0":
|
||||
version: 5.2.1
|
||||
resolution: "safe-buffer@npm:5.2.1"
|
||||
checksum: 0bb57f0d8f9d1fa4fe35ad8a2db1f83a027d48f2822d59ede88fd5cd4ddad83c0b497213feb7a70fbf90597a70c5217f735b0eb1850df40ce9b4ae81dd22b3f9
|
||||
|
||||
Reference in New Issue
Block a user