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:
Antoine Estienne
2021-02-24 15:50:12 +01:00
committed by GitHub
co-authored by Jaco Greeff
parent c7e474cdc6
commit c46cd5030e
16 changed files with 472 additions and 39 deletions
+15 -5
View File
@@ -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'],
+22 -7
View File
@@ -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);
}
}
/**
+8 -3
View File
@@ -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');
});
});
});
+3 -5
View File
@@ -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);
},
+35 -11
View File
@@ -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()
+1
View File
@@ -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;
}
}
+1
View File
@@ -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;
}
+2 -1
View File
@@ -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