ETH derive to u8a (#890)

* ETH derive to u8a

* assert

* Extract hash

* link

* hdEthereum

* Adjust tests

* cosmetics

* Remove unused parts
This commit is contained in:
Jaco Greeff
2021-02-25 13:34:01 +01:00
committed by GitHub
parent 56a7788e28
commit 882826a7e3
6 changed files with 77 additions and 167 deletions
+4 -14
View File
@@ -5,8 +5,7 @@ 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, 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 { base64Decode, decodeAddress, encodeAddress, ethereumEncode, hdEthereum, keyExtractSuri, keyFromPath, mnemonicToLegacySeed, mnemonicToMiniSecret, naclKeypairFromSeed as naclFromSeed, schnorrkelKeypairFromSeed as schnorrkelFromSeed, secp256k1KeypairFromSeed as secp256k1FromSeed } from '@polkadot/util-crypto';
import { DEV_PHRASE } from './defaults';
import { createPair } from './pair';
@@ -193,18 +192,9 @@ export class Keyring implements KeyringInstance {
}
}
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);
}
const derived = type === 'ethereum'
? hdEthereum(seed, derivePath.substring(1))
: keyFromPath(keypairFromSeed[type](seed), path, type);
return createPair({ toSS58: this.encodeAddress, type }, derived, meta, null);
}
@@ -3,9 +3,9 @@
import { mnemonicToLegacySeed } from '@polkadot/util-crypto';
import { HDKeyEth } from '.';
import { hdEthereum } from '.';
describe('HDKeyEth', (): void => {
describe('hdEthereum', (): 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([
@@ -36,20 +36,25 @@ describe('HDKeyEth', (): void => {
24, 217, 127, 147, 175, 105, 158, 70
]);
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);
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 = HDKeyEth.fromMasterSeed(seed);
const key = hdEthereum(seed, derivationPath);
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);
expect(key.publicKey).toEqual(PUBLICDERIVED);
expect(key.secretKey).toEqual(SECRETDERIVED);
});
});
+48 -128
View File
@@ -1,69 +1,36 @@
// 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 type { Keypair } from '../../types';
import { assert } from '@polkadot/util';
import { assert, bnToU8a, stringToU8a, u8aConcat } from '@polkadot/util';
import { secp256k1KeypairFromSeed, secp256k1PrivateKeyTweakAdd } from '../..';
import { hmacSha512 } from '../../hmac';
const MASTER_SECRET = Buffer.from('Bitcoin seed', 'utf8');
const MASTER_SECRET = stringToU8a('Bitcoin seed');
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;
public chainCode: Uint8Array | null;
parentFingerprint: number | null;
#fingerprint: number | null;
#identifier: Buffer | null;
#privateKey: Uint8Array | null;
#publicKey: Uint8Array | null;
constructor (versions?: Versions) {
this.versions = versions || BITCOIN_VERSIONS;
this.depth = 0;
this.index = 0;
constructor () {
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;
}
}
@@ -79,31 +46,11 @@ export class HDKeyEth {
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'") {
@@ -124,7 +71,11 @@ export class HDKeyEth {
let childIndex = parseInt(c, 10); // & (HARDENED_OFFSET - 1)
assert(childIndex < HARDENED_OFFSET, 'Invalid index');
if (hardened) childIndex += HARDENED_OFFSET;
if (hardened) {
childIndex += HARDENED_OFFSET;
}
hdkey = hdkey.deriveChild(childIndex);
});
@@ -134,90 +85,59 @@ export class HDKeyEth {
// deriveChild
private deriveChild (index: number): HDKeyEth {
const isHardened = index >= HARDENED_OFFSET;
const indexBuffer = Buffer.allocUnsafe(4);
indexBuffer.writeUInt32BE(index, 0);
let data;
const indexBuffer = bnToU8a(index, { bitLength: 32, isLe: false });
let data: Uint8Array;
if (isHardened) { // Hardened child
assert(this.privateKey, 'Could not derive hardened child key');
assert(this.privateKey, 'Could not derive hardened child key without private 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');
}
// 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)
if (this.publicKey) {
data = Buffer.concat([bytesAsBuffer(this.publicKey), indexBuffer]);
} else {
throw new Error('Could not derive hardened child key : no publicKey');
}
assert(this.publicKey, 'Could not derive hardened child key : no publicKey');
data = u8aConcat(this.publicKey, indexBuffer);
}
let I: Buffer = Buffer.from('');
assert(this.chainCode, 'deriveChild error: no chainCode');
assert(this.privateKey, 'PublicKey derivation without private key is not supported');
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);
const I = hmacSha512(this.chainCode, data);
const IL = I.slice(0, 32);
const IR = I.slice(32);
const hd = new HDKeyEth();
// 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);
try {
hd.privateKey = secp256k1PrivateKeyTweakAdd(this.privateKey, 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');
// In case parse256(IL) >= n or ki == 0, one should proceed with the next value for i
return this.deriveChild(index + 1);
}
hd.chainCode = new Uint8Array(bufferAsArrayBuffer(IR));
hd.depth = this.depth + 1;
hd.parentFingerprint = this.fingerprint;// .readUInt32BE(0)
hd.index = index;
hd.chainCode = IR;
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;
}
}
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
? hdkey.derive(path)
: hdkey;
assert(child.publicKey && child.privateKey, 'Unable to derive HD key from path');
return { publicKey: child.publicKey, secretKey: child.privateKey };
}
+1 -1
View File
@@ -1,5 +1,5 @@
// Copyright 2017-2021 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
export { hdEthereum } from './ethereum';
export { hdLedger } from './ledger';
export { HDKeyEth } from './ethereum';
+1 -1
View File
@@ -6,5 +6,5 @@ export { secp256k1Expand } from './expand';
export { secp256k1KeypairFromSeed } from './keypair/fromSeed';
export { secp256k1Recover } from './recover';
export { secp256k1Sign } from './sign';
export { secp256k1Verify } from './verify';
export { secp256k1PrivateKeyTweakAdd } from './tweakAdd';
export { secp256k1Verify } from './verify';
@@ -10,27 +10,22 @@ describe('TweakAdd', (): void => {
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([
secp256k1PrivateKeyTweakAdd(A, B)
).toEqual(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);
expect(
() => secp256k1PrivateKeyTweakAdd(A, B)
).toThrow(/Expected tweak to be an Uint8Array/);
});
});
});