Skip to content

Commit 5937c24

Browse files
authored
Merge pull request #17 from paulmillr/master
Use assertBytes from noble-hashes
2 parents 7956729 + 756d4c6 commit 5937c24

File tree

6 files changed

+193
-136
lines changed

6 files changed

+193
-136
lines changed

README.md

Lines changed: 39 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,26 @@ Note: if you've been using ethereum-cryptography v0.1, it had different API. We'
205205

206206
## BIP32 HD Keygen
207207

208-
This module exports a single class whose type is
208+
This module exports a single class `HDKey`, which should be used like this:
209+
210+
```ts
211+
const { HDKey } = require("ethereum-cryptography/secp256k1");
212+
const hdkey1 = HDKey.fromMasterSeed(seed);
213+
const hdkey2 = HDKey.fromExtendedKey(base58key);
214+
const hdkey3 = HDKey.fromJSON({ xpriv: string });
215+
216+
// props
217+
[hdkey1.depth, hdkey1.index, hdkey1.chainCode];
218+
console.log(hdkey2.privateKey, hdkey2.publicKey);
219+
console.log(hdkey3.derive("m/0/2147483647'/1"));
220+
const sig = hdkey3.sign(hash);
221+
hdkey3.verify(hash, sig);
222+
```
223+
224+
Note: `chainCode` property is essentially a private part
225+
of a secret "master" key, it should be guarded from unauthorized access.
226+
227+
The full API is:
209228

210229
```ts
211230
class HDKey {
@@ -214,26 +233,25 @@ class HDKey {
214233
public static fromExtendedKey(base58key: string, versions: Versions): HDKey;
215234
public static fromJSON(json: { xpriv: string }): HDKey;
216235

217-
public versions: Versions;
218-
public depth: number;
219-
public index: number;
220-
public chainCode: Uint8Array | null;
221-
public privateKey: Uint8Array | null;
222-
public publicKey: Uint8Array | null;
223-
public fingerprint: number;
224-
public parentFingerprint: number;
225-
public pubKeyHash: Uint8Array | undefined;
226-
public identifier: Uint8Array | undefined;
227-
public privateExtendedKey: string;
228-
public publicExtendedKey: string;
229-
230-
private constructor(versios: Versions);
231-
public derive(path: string): HDKey;
232-
public deriveChild(index: number): HDKey;
233-
public sign(hash: Uint8Array): Uint8Array;
234-
public verify(hash: Uint8Array, signature: Uint8Array): boolean;
235-
public wipePrivateData(): this;
236-
public toJSON(): { xpriv: string; xpub: string };
236+
readonly versions: Versions;
237+
readonly depth: number = 0;
238+
readonly index: number = 0;
239+
readonly chainCode: Uint8Array | null = null;
240+
readonly parentFingerprint: number = 0;
241+
242+
get fingerprint(): number;
243+
get identifier(): Uint8Array | undefined;
244+
get pubKeyHash(): Uint8Array | undefined;
245+
get privateKey(): Uint8Array | null;
246+
get publicKey(): Uint8Array | null;
247+
get privateExtendedKey(): string;
248+
get publicExtendedKey(): string;
249+
250+
derive(path: string): HDKey;
251+
deriveChild(index: number): HDKey;
252+
sign(hash: Uint8Array): Uint8Array;
253+
verify(hash: Uint8Array, signature: Uint8Array): boolean;
254+
wipePrivateData(): this;
237255
}
238256

239257
interface Versions {

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
],
2626
"dependencies": {
2727
"micro-base": "^0.10.0",
28-
"@noble/hashes": "^0.5.1",
28+
"@noble/hashes": "^0.5.2",
2929
"@noble/secp256k1": "^1.3.3"
3030
},
3131
"browser": {

src/bip39/index.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import { pbkdf2, pbkdf2Async } from "@noble/hashes/pbkdf2";
22
import { sha256 } from "@noble/hashes/sha256";
33
import { sha512 } from "@noble/hashes/sha512";
4-
import { assertNumber } from "@noble/hashes/utils";
4+
import { assertBytes, assertNumber } from "@noble/hashes/utils";
55
import { utils as baseUtils } from "micro-base";
66
import { getRandomBytesSync } from "../random";
7-
import { assertBytes } from "../utils";
87

98
const isJapanese = (wordlist: string[]) =>
109
wordlist[0] === "\u3042\u3044\u3053\u304f\u3057\u3093"; // Japanese wordlist

src/hdkey.ts

Lines changed: 117 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ function modN(a: bigint, b: bigint = secp.CURVE.n): bigint {
3030
const MASTER_SECRET = utf8ToBytes("Bitcoin seed");
3131
// Bitcoin hardcoded by default
3232
const BITCOIN_VERSIONS: Versions = { private: 0x0488ade4, public: 0x0488b21e };
33+
export const HARDENED_OFFSET: number = 0x80000000;
3334

3435
export interface Versions {
3536
private: number;
@@ -44,55 +45,17 @@ const toU32 = (n: number) => {
4445
return buf;
4546
};
4647

47-
export class HDKey {
48-
public static HARDENED_OFFSET: number = 0x80000000;
49-
public static fromMasterSeed(seed: Uint8Array, versions?: Versions): HDKey {
50-
const I = hmac(sha512, MASTER_SECRET, seed);
51-
const hdkey = new HDKey(versions);
52-
hdkey.chainCode = I.slice(32);
53-
hdkey.privateKey = I.slice(0, 32);
54-
return hdkey;
55-
}
56-
public static fromExtendedKey(base58key: string, versions?: Versions): HDKey {
57-
// => version(4) || depth(1) || fingerprint(4) || index(4) || chain(32) || key(33)
58-
const hdkey = new HDKey(versions);
59-
const keyBuffer: Uint8Array = base58c.decode(base58key);
60-
const keyView = createView(keyBuffer);
61-
const version = keyView.getUint32(0, false);
62-
hdkey.depth = keyBuffer[4];
63-
hdkey.parentFingerprint = keyView.getUint32(5, false);
64-
hdkey.index = keyView.getUint32(9, false);
65-
hdkey.chainCode = keyBuffer.slice(13, 45);
66-
const key = keyBuffer.slice(45);
67-
const isPriv = key[0] === 0;
68-
if (version !== hdkey.versions[isPriv ? "private" : "public"]) {
69-
throw new Error("Version mismatch");
70-
}
71-
if (isPriv) {
72-
hdkey.privateKey = key.slice(1);
73-
} else {
74-
hdkey.publicKey = key;
75-
}
76-
return hdkey;
77-
}
78-
79-
public static fromJSON(json: { xpriv: string }): HDKey {
80-
return HDKey.fromExtendedKey(json.xpriv);
81-
}
82-
83-
public versions: Versions;
84-
public depth: number = 0;
85-
public index: number = 0;
86-
public chainCode: Uint8Array | null = null;
87-
public parentFingerprint: number = 0;
88-
private privKey?: bigint;
89-
private privKeyBytes?: Uint8Array;
90-
private pubKey?: Uint8Array;
91-
private pubHash: Uint8Array | undefined;
48+
interface HDKeyOpt {
49+
versions: Versions;
50+
depth?: number;
51+
index?: number;
52+
parentFingerprint?: number;
53+
chainCode: Uint8Array;
54+
publicKey?: Uint8Array;
55+
privateKey?: Uint8Array | bigint;
56+
}
9257

93-
constructor(versions?: Versions) {
94-
this.versions = versions || BITCOIN_VERSIONS;
95-
}
58+
export class HDKey {
9659
get fingerprint(): number {
9760
if (!this.pubHash) {
9861
throw new Error("No publicKey set!");
@@ -108,34 +71,9 @@ export class HDKey {
10871
get privateKey(): Uint8Array | null {
10972
return this.privKeyBytes || null;
11073
}
111-
set privateKey(value: Uint8Array | bigint | null) {
112-
if (value == null) {
113-
this.wipePrivateData();
114-
return;
115-
}
116-
if (!secp.utils.isValidPrivateKey(value)) {
117-
throw new Error("Invalid private key");
118-
}
119-
this.privKey = typeof value === "bigint" ? value : bytesToNumber(value);
120-
this.privKeyBytes = numberToBytes(this.privKey);
121-
this.pubKey = secp.getPublicKey(value, true);
122-
this.pubHash = hash160(this.pubKey);
123-
}
12474
get publicKey(): Uint8Array | null {
12575
return this.pubKey || null;
12676
}
127-
set publicKey(value: Uint8Array | null) {
128-
let hex;
129-
try {
130-
hex = secp.Point.fromHex(value!);
131-
} catch (error) {
132-
throw new Error("Invalid public key");
133-
}
134-
this.pubKey = hex.toRawBytes(true); // force compressed point
135-
this.pubHash = hash160(this.pubKey);
136-
this.privKey = undefined;
137-
}
138-
13977
get privateExtendedKey(): string {
14078
const priv = this.privateKey;
14179
if (!priv) {
@@ -155,6 +93,88 @@ export class HDKey {
15593
return base58c.encode(this.serialize(this.versions.public, this.pubKey));
15694
}
15795

96+
public static fromMasterSeed(
97+
seed: Uint8Array,
98+
versions: Versions = BITCOIN_VERSIONS
99+
): HDKey {
100+
const I = hmac(sha512, MASTER_SECRET, seed);
101+
return new HDKey({
102+
versions,
103+
chainCode: I.slice(32),
104+
privateKey: I.slice(0, 32)
105+
});
106+
}
107+
108+
public static fromExtendedKey(
109+
base58key: string,
110+
versions: Versions = BITCOIN_VERSIONS
111+
): HDKey {
112+
// => version(4) || depth(1) || fingerprint(4) || index(4) || chain(32) || key(33)
113+
const keyBuffer: Uint8Array = base58c.decode(base58key);
114+
const keyView = createView(keyBuffer);
115+
const version = keyView.getUint32(0, false);
116+
const opt = {
117+
versions,
118+
depth: keyBuffer[4],
119+
parentFingerprint: keyView.getUint32(5, false),
120+
index: keyView.getUint32(9, false),
121+
chainCode: keyBuffer.slice(13, 45)
122+
};
123+
const key = keyBuffer.slice(45);
124+
const isPriv = key[0] === 0;
125+
if (version !== versions[isPriv ? "private" : "public"]) {
126+
throw new Error("Version mismatch");
127+
}
128+
if (isPriv) {
129+
return new HDKey({ ...opt, privateKey: key.slice(1) });
130+
} else {
131+
return new HDKey({ ...opt, publicKey: key });
132+
}
133+
}
134+
135+
public static fromJSON(json: { xpriv: string }): HDKey {
136+
return HDKey.fromExtendedKey(json.xpriv);
137+
}
138+
public readonly versions: Versions;
139+
public readonly depth: number = 0;
140+
public readonly index: number = 0;
141+
public readonly chainCode: Uint8Array | null = null;
142+
public readonly parentFingerprint: number = 0;
143+
private privKey?: bigint;
144+
private privKeyBytes?: Uint8Array;
145+
private pubKey?: Uint8Array;
146+
private pubHash: Uint8Array | undefined;
147+
148+
constructor(opt: HDKeyOpt) {
149+
if (!opt || typeof opt !== "object") {
150+
throw new Error("HDKey.constructor must not be called directly");
151+
}
152+
this.versions = opt.versions || BITCOIN_VERSIONS;
153+
this.depth = opt.depth || 0;
154+
this.chainCode = opt.chainCode;
155+
this.index = opt.index || 0;
156+
this.parentFingerprint = opt.parentFingerprint || 0;
157+
if (opt.publicKey && opt.privateKey) {
158+
throw new Error("HDKey: publicKey and privateKey at same time.");
159+
}
160+
if (opt.privateKey) {
161+
if (!secp.utils.isValidPrivateKey(opt.privateKey)) {
162+
throw new Error("Invalid private key");
163+
}
164+
this.privKey =
165+
typeof opt.privateKey === "bigint"
166+
? opt.privateKey
167+
: bytesToNumber(opt.privateKey);
168+
this.privKeyBytes = numberToBytes(this.privKey);
169+
this.pubKey = secp.getPublicKey(opt.privateKey, true);
170+
} else if (opt.publicKey) {
171+
this.pubKey = secp.Point.fromHex(opt.publicKey).toRawBytes(true); // force compressed point
172+
} else {
173+
throw new Error("HDKey: no public or private key provided");
174+
}
175+
this.pubHash = hash160(this.pubKey);
176+
}
177+
158178
public derive(path: string): HDKey {
159179
if (!/^[mM]'?/.test(path)) {
160180
throw new Error('Path must start with "m" or "M"');
@@ -171,17 +191,18 @@ export class HDKey {
171191
throw new Error(`Invalid child index: ${c}`);
172192
}
173193
let idx = +m[1];
174-
if (!Number.isSafeInteger(idx) || idx >= HDKey.HARDENED_OFFSET) {
194+
if (!Number.isSafeInteger(idx) || idx >= HARDENED_OFFSET) {
175195
throw new Error("Invalid index");
176196
}
177197
// hardened key
178198
if (m[2] === "'") {
179-
idx += HDKey.HARDENED_OFFSET;
199+
idx += HARDENED_OFFSET;
180200
}
181201
child = child.deriveChild(idx);
182202
}
183203
return child;
184204
}
205+
185206
public deriveChild(index: number): HDKey {
186207
if (!Number.isSafeInteger(index) || index < 0 || index >= 2 ** 33) {
187208
throw new Error(
@@ -193,7 +214,7 @@ export class HDKey {
193214
}
194215
let data = new Uint8Array(4);
195216
createView(data).setUint32(0, index, false);
196-
if (index >= HDKey.HARDENED_OFFSET) {
217+
if (index >= HARDENED_OFFSET) {
197218
// Hardened
198219
const priv = this.privateKey;
199220
if (!priv) {
@@ -211,7 +232,13 @@ export class HDKey {
211232
if (!secp.utils.isValidPrivateKey(childTweak)) {
212233
throw new Error("Tweak bigger than curve order");
213234
}
214-
const child = new HDKey(this.versions);
235+
const opt: HDKeyOpt = {
236+
versions: this.versions,
237+
chainCode,
238+
depth: this.depth + 1,
239+
parentFingerprint: this.fingerprint,
240+
index
241+
};
215242
try {
216243
// Private parent key -> private child key
217244
if (this.privateKey) {
@@ -221,28 +248,29 @@ export class HDKey {
221248
"The tweak was out of range or the resulted private key is invalid"
222249
);
223250
}
224-
child.privateKey = added;
251+
opt.privateKey = added;
225252
} else {
226-
child.publicKey = secp.Point.fromHex(this.pubKey)
253+
opt.publicKey = secp.Point.fromHex(this.pubKey)
227254
.add(secp.Point.fromPrivateKey(childTweak))
228255
.toRawBytes(true);
229256
}
257+
return new HDKey(opt);
230258
} catch (err) {
231259
return this.deriveChild(index + 1);
232260
}
233-
child.chainCode = chainCode;
234-
child.depth = this.depth + 1;
235-
child.parentFingerprint = this.fingerprint;
236-
child.index = index;
237-
return child;
238261
}
262+
239263
public sign(hash: Uint8Array): Uint8Array {
240264
if (!this.privateKey) {
241265
throw new Error("No privateKey set!");
242266
}
243267
assertBytes(hash, 32);
244-
return secp.signSync(hash, this.privKey!, { canonical: true, der: false });
268+
return secp.signSync(hash, this.privKey!, {
269+
canonical: true,
270+
der: false
271+
});
245272
}
273+
246274
public verify(hash: Uint8Array, signature: Uint8Array): boolean {
247275
assertBytes(hash, 32);
248276
assertBytes(signature, 64);
@@ -257,13 +285,13 @@ export class HDKey {
257285
}
258286
return secp.verify(sig, hash, this.publicKey);
259287
}
288+
260289
public wipePrivateData(): this {
261-
if (this.privKey) {
262-
this.privKey = undefined;
263-
this.privKeyBytes!.fill(0);
290+
this.privKey = undefined;
291+
if (this.privKeyBytes) {
292+
this.privKeyBytes.fill(0);
264293
this.privKeyBytes = undefined;
265294
}
266-
this.privKey = undefined;
267295
return this;
268296
}
269297
public toJSON(): { xpriv: string; xpub: string } {

0 commit comments

Comments
 (0)