@did-btcr2/keypair 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/public.ts ADDED
@@ -0,0 +1,424 @@
1
+ import {
2
+ BIP340_PUBLIC_KEY_MULTIBASE_PREFIX,
3
+ BIP340_PUBLIC_KEY_MULTIBASE_PREFIX_HASH,
4
+ CURVE,
5
+ Hex,
6
+ KeyBytes,
7
+ Maybe,
8
+ MultibaseObject,
9
+ PublicKeyError,
10
+ PublicKeyObject
11
+ } from '@did-btcr2/common';
12
+ import { sha256 } from '@noble/hashes/sha2';
13
+ import { base58btc } from 'multiformats/bases/base58';
14
+ import { SecretKey } from './secret.js';
15
+
16
+ export interface Point {
17
+ x: KeyBytes;
18
+ y: KeyBytes;
19
+ }
20
+
21
+ /**
22
+ * Interface for the PublicKey class.
23
+ * @interface IPublicKey
24
+ * @type {IPublicKey}
25
+ */
26
+ export interface IPublicKey {
27
+ /**
28
+ * Uncompressed public key getter.
29
+ * @readonly @type {KeyBytes} The 65 byte uncompressed public key [0x04, x-coord, y-coord].
30
+ */
31
+ uncompressed: KeyBytes;
32
+
33
+ /**
34
+ * Compressed public key getter.
35
+ * @readonly @type {KeyBytes} The 33 byte compressed public key [parity, x-coord].
36
+ */
37
+ compressed: KeyBytes;
38
+
39
+ /**
40
+ * PublicKey parity getter.
41
+ * @readonly @type {number} The 1 byte parity (0x02 if even, 0x03 if odd).
42
+ */
43
+ parity: number;
44
+
45
+ /**
46
+ * PublicKey x-coordinate getter.
47
+ * @readonly @type {KeyBytes} The 32 byte x-coordinate of the public key.
48
+ */
49
+ x: KeyBytes;
50
+
51
+ /**
52
+ * PublicKey y-coordinate getter.
53
+ * @readonly @type {KeyBytes} The 32 byte y-coordinate of the public key.
54
+ */
55
+ y: KeyBytes;
56
+
57
+ /**
58
+ * PublicKey multibase getter.
59
+ * @readonly @returns {MultibaseObject} The public key as MultibaseObject as a address string, key and prefix bytes.
60
+ */
61
+ multibase: MultibaseObject;
62
+
63
+ /**
64
+ * PublicKey hex string getter.
65
+ * @readonly @type {Hex} The public key as a hex string.
66
+ */
67
+ hex: Hex;
68
+
69
+ /**
70
+ * Decode the base58btc multibase string to the compressed public key prefixed with 0x02.
71
+ * @returns {KeyBytes} The public key as a 33-byte compressed public key with header.
72
+ */
73
+ decode(): KeyBytes;
74
+
75
+ /**
76
+ * Encode the PublicKey as an x-only base58btc multibase public key.
77
+ * @returns {string} The public key formatted a base58btc multibase string.
78
+ */
79
+ encode(): string;
80
+
81
+ /**
82
+ * PublicKey key equality check. Checks if `this` public key is equal to `other` public key.
83
+ * @param {PublicKey} other The public key to compare.
84
+ * @returns {boolean} True if the public keys are equal.
85
+ */
86
+ equals(other: PublicKey): boolean;
87
+
88
+ /**
89
+ * JSON representation of a PublicKey object.
90
+ * @returns {PublicKeyObject} The PublicKey as a JSON object.
91
+ */
92
+ json(): PublicKeyObject;
93
+ }
94
+
95
+ /**
96
+ * Encapsulates a secp256k1 public key compliant to BIP-340 BIP schnorr signature scheme.
97
+ * Provides get methods for different formats (compressed, x-only, multibase).
98
+ * Provides helpers methods for comparison and serialization.
99
+ * @class PublicKey
100
+ * @type {PublicKey}
101
+ */
102
+ export class PublicKey implements PublicKey {
103
+ /** @type {KeyBytes} The public key bytes */
104
+ private readonly _bytes: KeyBytes;
105
+
106
+ /** @type {MultibaseObject} The public key as a MultibaseObject */
107
+ private _multibase: MultibaseObject = {
108
+ prefix : BIP340_PUBLIC_KEY_MULTIBASE_PREFIX,
109
+ key : [],
110
+ address : ''
111
+ };
112
+
113
+ /**
114
+ * Creates a PublicKey instance.
115
+ * @param {KeyBytes} bytes The public key byte array.
116
+ * @throws {PublicKeyError} if the byte length is not 32 (x-only) or 33 (compressed)
117
+ */
118
+ constructor(bytes: KeyBytes) {
119
+ // If the byte length is not 33, throw an error
120
+ if(bytes.length !== 33) {
121
+ throw new PublicKeyError(
122
+ 'Invalid argument: byte length must be 33 (compressed)',
123
+ 'CONSTRUCTOR_ERROR', { bytes }
124
+ );
125
+ }
126
+ // Set the bytes
127
+ this._bytes = bytes;
128
+
129
+ // Set multibase
130
+ this._multibase.address = this.encode();
131
+ this._multibase.key = [...this._multibase.prefix, ...this.compressed];
132
+ }
133
+
134
+ /**
135
+ * Get the compressed public key.
136
+ * @returns {KeyBytes} The 33-byte compressed public key (0x02 or 0x03, x).
137
+ */
138
+ get compressed(): KeyBytes {
139
+ const bytes = new Uint8Array(this._bytes);
140
+ return bytes;
141
+ };
142
+
143
+ /**
144
+ * Get the uncompressed public key.
145
+ * @returns {Uint8Array} The 65-byte uncompressed public key (0x04, x, y).
146
+ */
147
+ get uncompressed(): KeyBytes {
148
+ const uncompressed = this.liftX();
149
+ return uncompressed;
150
+ }
151
+
152
+ /**
153
+ * Get the parity byte of the public key.
154
+ * @returns {number} The parity byte of the public key.
155
+ */
156
+ get parity(): number {
157
+ const parity = this.compressed[0];
158
+ return parity;
159
+ }
160
+
161
+ /**
162
+ * Get the x-coordinate of the public key.
163
+ * @returns {Uint8Array} The 32-byte x-coordinate of the public key.
164
+ */
165
+ get x(): KeyBytes {
166
+ const x = this.compressed.slice(1, 33);
167
+ return x;
168
+ }
169
+
170
+ /**
171
+ * Get the y-coordinate of the public key.
172
+ * @returns {Uint8Array} The 32-byte y-coordinate of the public key.
173
+ */
174
+ get y(): KeyBytes {
175
+ const y = this.uncompressed.slice(33, 65);
176
+ return y;
177
+ }
178
+
179
+ /**
180
+ * Get the multibase public key.
181
+ * @returns {MultibaseObject} An object containing the multibase bytes, address and prefix.
182
+ */
183
+ get multibase(): MultibaseObject {
184
+ const multibase = this._multibase;
185
+ return multibase;
186
+ }
187
+
188
+ /**
189
+ * Returns the raw public key as a hex string.
190
+ * @returns {Hex} The public key as a hex string.
191
+ */
192
+ get hex(): Hex {
193
+ const hex = Buffer.from(this.compressed).toString('hex');
194
+ return hex;
195
+ }
196
+
197
+ /**
198
+ * Return the public key point.
199
+ * @returns {Point} The public key point.
200
+ */
201
+ get point(): Point {
202
+ return {
203
+ x : this.x,
204
+ y : this.y
205
+ };
206
+ }
207
+
208
+ /**
209
+ * Returns the point of the public key.
210
+ * @param {Hex} pk The public key in hex (Uint8Array or string) format.
211
+ * @returns {Point} The point of the public key.
212
+ * @throws {PublicKeyError} If the public key is not a valid hex string or byte array.
213
+ */
214
+ static point(pk: Hex): Point {
215
+ // If the public key is a hex string, convert it to a PublicKey object and return the point
216
+ if(typeof pk === 'string' && /^[0-9a-fA-F]+$/.test(pk)) {
217
+ const publicKey = new PublicKey(Buffer.fromHex(pk));
218
+ return publicKey.point;
219
+ }
220
+
221
+ // If the public key is a byte array or ArrayBuffer, convert it to a PublicKey object and return the point
222
+ if(pk instanceof Uint8Array || ArrayBuffer.isView(pk)) {
223
+ const publicKey = new PublicKey(pk as KeyBytes);
224
+ return publicKey.point;
225
+ }
226
+
227
+ // If the public key is neither a hex string nor a byte array, throw an error
228
+ throw new PublicKeyError(
229
+ 'Invalid publicKey: must be a hex string or byte array',
230
+ 'POINT_ERROR', { publicKey: pk }
231
+ );
232
+ }
233
+
234
+ /**
235
+ * Decodes the multibase string to the 35-byte corresponding public key (2 byte prefix + 32 byte public key).
236
+ * @returns {KeyBytes} The decoded public key: prefix and public key bytes
237
+ */
238
+ public decode(): KeyBytes {
239
+ // Decode the public key multibase string
240
+ const decoded = base58btc.decode(this.multibase.address);
241
+
242
+ // If the public key bytes are not 35 bytes, throw an error
243
+ if(decoded.length !== 35) {
244
+ throw new PublicKeyError(
245
+ 'Invalid argument: must be 35 byte publicKeyMultibase',
246
+ 'DECODE_MULTIBASE_ERROR'
247
+ );
248
+ }
249
+
250
+ // Grab the prefix bytes
251
+ const prefix = decoded.slice(0, 2);
252
+
253
+ // Compute the prefix hash
254
+ const prefixHash = Buffer.from(sha256(prefix)).toString('hex');
255
+
256
+ // If the prefix hash does not equal the BIP340 prefix hash, throw an error
257
+ if (prefixHash !== BIP340_PUBLIC_KEY_MULTIBASE_PREFIX_HASH) {
258
+ throw new PublicKeyError(
259
+ `Invalid prefix: malformed multibase prefix ${prefix}`,
260
+ 'DECODE_MULTIBASE_ERROR'
261
+ );
262
+ }
263
+
264
+ // Return the decoded public key bytes
265
+ return decoded;
266
+ }
267
+
268
+ /**
269
+ * Encodes compressed secp256k1 public key from bytes to BIP340 multibase format.
270
+ * @returns {string} The public key encoded in base-58-btc multibase format.
271
+ */
272
+ public encode(): string {
273
+ // Convert public key bytes to an array
274
+ const pk = this.compressed.toArray();
275
+
276
+ // Ensure the public key is 33-byte secp256k1 compressed public key
277
+ if (pk.length !== 33) {
278
+ throw new PublicKeyError(
279
+ 'Invalid argument: must be 33-byte (compressed) public key',
280
+ 'ENCODE_MULTIBASE_ERROR'
281
+ );
282
+ }
283
+
284
+ // Convert prefix to an array
285
+ const publicKeyMultibase = BIP340_PUBLIC_KEY_MULTIBASE_PREFIX.toArray();
286
+
287
+ // Push the public key bytes at the end of the prefix
288
+ publicKeyMultibase.push(...pk);
289
+
290
+ // Encode the bytes in base58btc format and return
291
+ return base58btc.encode(publicKeyMultibase.toUint8Array());
292
+ }
293
+
294
+ /**
295
+ * Compares this public key to another public key.
296
+ * @param {PublicKey} other The other public key to compare
297
+ * @returns {boolean} True if the public keys are equal, false otherwise.
298
+ */
299
+ public equals(other: PublicKey): boolean {
300
+ return this.hex === other.hex;
301
+ }
302
+
303
+ /**
304
+ * JSON representation of a PublicKey object.
305
+ * @returns {PublicKeyObject} The PublicKey as a JSON object.
306
+ */
307
+ public json(): PublicKeyObject {
308
+ return {
309
+ hex : this.hex,
310
+ multibase : this.multibase,
311
+ point : {
312
+ x : this.x.toArray(),
313
+ y : this.y.toArray(),
314
+ parity : this.parity,
315
+ },
316
+ };
317
+ }
318
+
319
+ /**
320
+ * Creates a PublicKey object from a JSON representation.
321
+ * @param {PublicKeyObject} json The JSON object to initialize the PublicKey.
322
+ * @returns {PublicKey} The initialized PublicKey object.
323
+ */
324
+ public static fromJSON(json: Maybe<PublicKeyObject>): PublicKey {
325
+ json.x.unshift(json.parity);
326
+ return new PublicKey(json.x.toUint8Array());
327
+ }
328
+
329
+ /**
330
+ * Computes the deterministic public key for a given private key.
331
+ * @param {PrivateKey | KeyBytes} sk The PrivateKey object or the private key bytes
332
+ * @returns {PublicKey} A new PublicKey object
333
+ */
334
+ public static fromSecretKey(sk: SecretKey | KeyBytes): PublicKey {
335
+ // If the private key is a PrivateKey object, get the raw bytes else use the bytes
336
+ const bytes = sk instanceof SecretKey ? sk.bytes : sk;
337
+
338
+ // Throw error if the private key is not 32 bytes
339
+ if(bytes.length !== 32) {
340
+ throw new PublicKeyError('Invalid arg: must be 32 byte private key', 'FROM_PRIVATE_KEY_ERROR');
341
+ }
342
+
343
+ // Compute the public key from the private key
344
+ const privateKey = sk instanceof SecretKey ? sk : new SecretKey(sk);
345
+
346
+ // Return a new PublicKey object
347
+ return new PublicKey(privateKey.computePublicKey());
348
+ }
349
+
350
+ /**
351
+ * Computes modular exponentiation: (base^exp) % mod.
352
+ * Used for computing modular square roots.
353
+ * @param {bigint} base The base value
354
+ * @param {bigint} exp The exponent value
355
+ * @param {bigint} mod The modulus value
356
+ * @returns {bigint} The result of the modular exponentiation
357
+ */
358
+ public modPow(base: bigint, exp: bigint, mod: bigint): bigint {
359
+ let result = 1n;
360
+ while (exp > 0n) {
361
+ if (exp & 1n) result = (result * base) % mod;
362
+ base = (base * base) % mod;
363
+ exp >>= 1n;
364
+ }
365
+ return result;
366
+ };
367
+
368
+ /**
369
+ * Computes `sqrt(a) mod p` using Tonelli-Shanks algorithm.
370
+ * This finds `y` such that `y^2 ≡ a mod p`.
371
+ * @param {bigint} a The value to find the square root of
372
+ * @param {bigint} p The prime modulus
373
+ * @returns {bigint} The square root of `a` mod `p`
374
+ */
375
+ public sqrtMod(a: bigint, p: bigint): bigint {
376
+ return this.modPow(a, (p + 1n) >> 2n, p);
377
+ };
378
+
379
+ /**
380
+ * Lifts a 32-byte x-only coordinate into a full secp256k1 point (x, y).
381
+ * @param xBytes 32-byte x-coordinate
382
+ * @returns {Uint8Array} 65-byte uncompressed public key (starts with `0x04`)
383
+ */
384
+ public liftX(): Uint8Array {
385
+ // Ensure x-coordinate is 32 bytes
386
+ if (this.x.length !== 32) {
387
+ throw new PublicKeyError('Invalid argument: x-coordinate length must be 32 bytes', 'LIFT_X_ERROR');
388
+ }
389
+
390
+ // Convert x from Uint8Array → BigInt
391
+ const x = BigInt('0x' + Buffer.from(this.x).toString('hex'));
392
+ if (x <= 0n || x >= CURVE.p) {
393
+ throw new PublicKeyError('Invalid conversion: x out of range as BigInt', 'LIFT_X_ERROR');
394
+ }
395
+
396
+ // Compute y² = x³ + 7 mod p
397
+ const ySquared = BigInt((x ** 3n + CURVE.b) % CURVE.p);
398
+
399
+ // Compute y (do not enforce parity)
400
+ const y = this.sqrtMod(ySquared, CURVE.p);
401
+
402
+ // Convert x and y to Uint8Array
403
+ const yBytes = Buffer.fromHex(y.toString(16).padStart(64, '0'));
404
+
405
+ // Return 65-byte uncompressed public key: `0x04 || x || y`
406
+ return new Uint8Array(Buffer.concat([Buffer.from([0x04]), Buffer.from(this.x), yBytes]));
407
+ };
408
+
409
+ /**
410
+ * Static version of liftX method.
411
+ * @param {KeyBytes} x The 32-byte x-coordinate to lift.
412
+ * @returns {Uint8Array} The 65-byte uncompressed public key (0x04, x, y).
413
+ */
414
+ public static xOnly(x: KeyBytes): Uint8Array {
415
+ // Ensure x-coordinate is 32 bytes
416
+ if (x.length !== 32) {
417
+ throw new PublicKeyError('Invalid argument: x-coordinate length must be 32 bytes', 'LIFT_X_ERROR');
418
+ }
419
+
420
+ // Create a PublicKey instance and lift the x-coordinate
421
+ const publicKey = new PublicKey(x);
422
+ return publicKey.x;
423
+ }
424
+ }