@bsv/sdk 1.9.12 → 1.9.15

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.
Files changed (45) hide show
  1. package/dist/cjs/package.json +1 -1
  2. package/dist/cjs/src/primitives/AESGCM.js +15 -0
  3. package/dist/cjs/src/primitives/AESGCM.js.map +1 -1
  4. package/dist/cjs/src/primitives/Secp256r1.js +327 -0
  5. package/dist/cjs/src/primitives/Secp256r1.js.map +1 -0
  6. package/dist/cjs/src/primitives/SymmetricKey.js +0 -3
  7. package/dist/cjs/src/primitives/SymmetricKey.js.map +1 -1
  8. package/dist/cjs/src/primitives/index.js +3 -1
  9. package/dist/cjs/src/primitives/index.js.map +1 -1
  10. package/dist/cjs/src/primitives/utils.js +59 -12
  11. package/dist/cjs/src/primitives/utils.js.map +1 -1
  12. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  13. package/dist/esm/src/primitives/AESGCM.js +15 -0
  14. package/dist/esm/src/primitives/AESGCM.js.map +1 -1
  15. package/dist/esm/src/primitives/Secp256r1.js +319 -0
  16. package/dist/esm/src/primitives/Secp256r1.js.map +1 -0
  17. package/dist/esm/src/primitives/SymmetricKey.js +0 -3
  18. package/dist/esm/src/primitives/SymmetricKey.js.map +1 -1
  19. package/dist/esm/src/primitives/index.js +1 -0
  20. package/dist/esm/src/primitives/index.js.map +1 -1
  21. package/dist/esm/src/primitives/utils.js +58 -12
  22. package/dist/esm/src/primitives/utils.js.map +1 -1
  23. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  24. package/dist/types/src/primitives/AESGCM.d.ts.map +1 -1
  25. package/dist/types/src/primitives/Secp256r1.d.ts +91 -0
  26. package/dist/types/src/primitives/Secp256r1.d.ts.map +1 -0
  27. package/dist/types/src/primitives/SymmetricKey.d.ts.map +1 -1
  28. package/dist/types/src/primitives/index.d.ts +1 -0
  29. package/dist/types/src/primitives/index.d.ts.map +1 -1
  30. package/dist/types/src/primitives/utils.d.ts +1 -0
  31. package/dist/types/src/primitives/utils.d.ts.map +1 -1
  32. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  33. package/dist/umd/bundle.js +3 -3
  34. package/dist/umd/bundle.js.map +1 -1
  35. package/docs/reference/primitives.md +164 -5
  36. package/package.json +1 -1
  37. package/src/auth/utils/__tests/cryptononce.test.ts +3 -3
  38. package/src/primitives/AESGCM.ts +20 -0
  39. package/src/primitives/Secp256r1.ts +334 -0
  40. package/src/primitives/SymmetricKey.ts +0 -4
  41. package/src/primitives/__tests/AESGCM.test.ts +57 -1
  42. package/src/primitives/__tests/Secp256r1.test.ts +101 -0
  43. package/src/primitives/__tests/utils.test.ts +44 -0
  44. package/src/primitives/index.ts +1 -0
  45. package/src/primitives/utils.ts +57 -13
@@ -48,11 +48,11 @@ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](
48
48
  | [BigNumber](#class-bignumber) | [Polynomial](#class-polynomial) | [SHA512](#class-sha512) |
49
49
  | [Curve](#class-curve) | [PrivateKey](#class-privatekey) | [SHA512HMAC](#class-sha512hmac) |
50
50
  | [DRBG](#class-drbg) | [PublicKey](#class-publickey) | [Schnorr](#class-schnorr) |
51
- | [JacobianPoint](#class-jacobianpoint) | [RIPEMD160](#class-ripemd160) | [Signature](#class-signature) |
52
- | [K256](#class-k256) | [Reader](#class-reader) | [SymmetricKey](#class-symmetrickey) |
53
- | [KeyShares](#class-keyshares) | [ReductionContext](#class-reductioncontext) | [TransactionSignature](#class-transactionsignature) |
54
- | [Mersenne](#class-mersenne) | [SHA1](#class-sha1) | [Writer](#class-writer) |
55
- | [MontgomoryMethod](#class-montgomorymethod) | [SHA1HMAC](#class-sha1hmac) | |
51
+ | [JacobianPoint](#class-jacobianpoint) | [RIPEMD160](#class-ripemd160) | [Secp256r1](#class-secp256r1) |
52
+ | [K256](#class-k256) | [Reader](#class-reader) | [Signature](#class-signature) |
53
+ | [KeyShares](#class-keyshares) | [ReductionContext](#class-reductioncontext) | [SymmetricKey](#class-symmetrickey) |
54
+ | [Mersenne](#class-mersenne) | [SHA1](#class-sha1) | [TransactionSignature](#class-transactionsignature) |
55
+ | [MontgomoryMethod](#class-montgomorymethod) | [SHA1HMAC](#class-sha1hmac) | [Writer](#class-writer) |
56
56
  | [Point](#class-point) | [SHA256](#class-sha256) | |
57
57
 
58
58
  Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
@@ -4320,6 +4320,141 @@ Argument Details
4320
4320
 
4321
4321
  Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
4322
4322
 
4323
+ ---
4324
+ ### Class: Secp256r1
4325
+
4326
+ Pure BigInt implementation of the NIST P-256 (secp256r1) curve with ECDSA sign/verify.
4327
+
4328
+ This class is standalone (no dependency on the existing secp256k1 primitives) and exposes
4329
+ key generation, point encoding/decoding, scalar multiplication, and SHA-256 based ECDSA.
4330
+
4331
+ ```ts
4332
+ export default class Secp256r1 {
4333
+ readonly p = P;
4334
+ readonly n = N;
4335
+ readonly a = A;
4336
+ readonly b = B;
4337
+ readonly g = G;
4338
+ pointFromAffine(x: bigint, y: bigint): P256Point
4339
+ pointFromHex(hex: string): P256Point
4340
+ pointToHex(p: P256Point, compressed = false): string
4341
+ add(p1: P256Point, p2: P256Point): P256Point
4342
+ multiply(point: P256Point, scalar: bigint): P256Point
4343
+ multiplyBase(scalar: bigint): P256Point
4344
+ isOnCurve(p: P256Point): boolean
4345
+ generatePrivateKeyHex(): string
4346
+ publicKeyFromPrivate(privateKey: string | bigint): P256Point
4347
+ sign(message: ByteSource, privateKey: string | bigint, opts: {
4348
+ prehashed?: boolean;
4349
+ nonce?: bigint;
4350
+ } = {}): {
4351
+ r: string;
4352
+ s: string;
4353
+ }
4354
+ verify(message: ByteSource, signature: {
4355
+ r: string | bigint;
4356
+ s: string | bigint;
4357
+ }, publicKey: P256Point | string, opts: {
4358
+ prehashed?: boolean;
4359
+ } = {}): boolean
4360
+ }
4361
+ ```
4362
+
4363
+ See also: [P256Point](./primitives.md#type-p256point), [multiply](./primitives.md#variable-multiply), [sign](./compat.md#variable-sign), [verify](./compat.md#variable-verify)
4364
+
4365
+ #### Method add
4366
+
4367
+ Add two points (handles infinity).
4368
+
4369
+ ```ts
4370
+ add(p1: P256Point, p2: P256Point): P256Point
4371
+ ```
4372
+ See also: [P256Point](./primitives.md#type-p256point)
4373
+
4374
+ #### Method generatePrivateKeyHex
4375
+
4376
+ Generate a new random private key as 32-byte hex.
4377
+
4378
+ ```ts
4379
+ generatePrivateKeyHex(): string
4380
+ ```
4381
+
4382
+ #### Method isOnCurve
4383
+
4384
+ Check if a point lies on the curve (including infinity).
4385
+
4386
+ ```ts
4387
+ isOnCurve(p: P256Point): boolean
4388
+ ```
4389
+ See also: [P256Point](./primitives.md#type-p256point)
4390
+
4391
+ #### Method multiply
4392
+
4393
+ Scalar multiply an arbitrary point using double-and-add.
4394
+
4395
+ ```ts
4396
+ multiply(point: P256Point, scalar: bigint): P256Point
4397
+ ```
4398
+ See also: [P256Point](./primitives.md#type-p256point)
4399
+
4400
+ #### Method multiplyBase
4401
+
4402
+ Scalar multiply the base point.
4403
+
4404
+ ```ts
4405
+ multiplyBase(scalar: bigint): P256Point
4406
+ ```
4407
+ See also: [P256Point](./primitives.md#type-p256point)
4408
+
4409
+ #### Method pointFromHex
4410
+
4411
+ Decode a point from compressed or uncompressed hex.
4412
+
4413
+ ```ts
4414
+ pointFromHex(hex: string): P256Point
4415
+ ```
4416
+ See also: [P256Point](./primitives.md#type-p256point)
4417
+
4418
+ #### Method pointToHex
4419
+
4420
+ Encode a point to compressed or uncompressed hex. Infinity is encoded as `00`.
4421
+
4422
+ ```ts
4423
+ pointToHex(p: P256Point, compressed = false): string
4424
+ ```
4425
+ See also: [P256Point](./primitives.md#type-p256point)
4426
+
4427
+ #### Method sign
4428
+
4429
+ Create an ECDSA signature over a message. Uses SHA-256 unless `prehashed` is true.
4430
+ Returns low-s normalized signature hex parts.
4431
+
4432
+ ```ts
4433
+ sign(message: ByteSource, privateKey: string | bigint, opts: {
4434
+ prehashed?: boolean;
4435
+ nonce?: bigint;
4436
+ } = {}): {
4437
+ r: string;
4438
+ s: string;
4439
+ }
4440
+ ```
4441
+
4442
+ #### Method verify
4443
+
4444
+ Verify an ECDSA signature against a message and public key.
4445
+
4446
+ ```ts
4447
+ verify(message: ByteSource, signature: {
4448
+ r: string | bigint;
4449
+ s: string | bigint;
4450
+ }, publicKey: P256Point | string, opts: {
4451
+ prehashed?: boolean;
4452
+ } = {}): boolean
4453
+ ```
4454
+ See also: [P256Point](./primitives.md#type-p256point)
4455
+
4456
+ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
4457
+
4323
4458
  ---
4324
4459
  ### Class: Signature
4325
4460
 
@@ -4818,6 +4953,7 @@ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](
4818
4953
  | [AES](#function-aes) |
4819
4954
  | [AESGCM](#function-aesgcm) |
4820
4955
  | [AESGCMDecrypt](#function-aesgcmdecrypt) |
4956
+ | [base64ToArray](#function-base64toarray) |
4821
4957
  | [ghash](#function-ghash) |
4822
4958
  | [pbkdf2](#function-pbkdf2) |
4823
4959
  | [red](#function-red) |
@@ -4858,6 +4994,15 @@ export function AESGCMDecrypt(cipherText: number[], additionalAuthenticatedData:
4858
4994
 
4859
4995
  Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
4860
4996
 
4997
+ ---
4998
+ ### Function: base64ToArray
4999
+
5000
+ ```ts
5001
+ export function base64ToArray(msg: string): number[]
5002
+ ```
5003
+
5004
+ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
5005
+
4861
5006
  ---
4862
5007
  ### Function: ghash
4863
5008
 
@@ -4984,6 +5129,18 @@ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](
4984
5129
  ---
4985
5130
  ## Types
4986
5131
 
5132
+ ### Type: P256Point
5133
+
5134
+ ```ts
5135
+ export type P256Point = {
5136
+ x: bigint;
5137
+ y: bigint;
5138
+ } | null
5139
+ ```
5140
+
5141
+ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
5142
+
5143
+ ---
4987
5144
  ## Enums
4988
5145
 
4989
5146
  ## Variables
@@ -5853,6 +6010,8 @@ toArray = (msg: any, enc?: "hex" | "utf8" | "base64"): any[] => {
5853
6010
  }
5854
6011
  ```
5855
6012
 
6013
+ See also: [base64ToArray](./primitives.md#function-base64toarray)
6014
+
5856
6015
  Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
5857
6016
 
5858
6017
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bsv/sdk",
3
- "version": "1.9.12",
3
+ "version": "1.9.15",
4
4
  "type": "module",
5
5
  "description": "BSV Blockchain Software Development Kit",
6
6
  "main": "dist/cjs/mod.js",
@@ -52,9 +52,9 @@ describe('verifyNonce', () => {
52
52
  (mockWallet.verifyHmac as jest.Mock).mockResolvedValue({ valid: false })
53
53
 
54
54
  const nonce = await createNonce(mockWallet)
55
- await expect(verifyNonce(nonce + 'ABC', mockWallet)).resolves.toEqual(
56
- false
57
- )
55
+ await expect(verifyNonce(nonce + 'ABC', mockWallet)).rejects.toThrow(
56
+ /Invalid base64 padding|Invalid base64/i
57
+ )
58
58
  await expect(verifyNonce(nonce + '=', mockWallet)).resolves.toEqual(false)
59
59
  await expect(
60
60
  verifyNonce(
@@ -333,6 +333,14 @@ export function AESGCM (
333
333
  initializationVector: number[],
334
334
  key: number[]
335
335
  ): { result: number[], authenticationTag: number[] } {
336
+ if (initializationVector.length === 0) {
337
+ throw new Error('Initialization vector must not be empty')
338
+ }
339
+
340
+ if (key.length === 0) {
341
+ throw new Error('Key must not be empty')
342
+ }
343
+
336
344
  let preCounterBlock
337
345
  let plainTag
338
346
  const hashSubKey = AES(createZeroBlock(16), key)
@@ -387,6 +395,18 @@ export function AESGCMDecrypt (
387
395
  authenticationTag: number[],
388
396
  key: number[]
389
397
  ): number[] | null {
398
+ if (cipherText.length === 0) {
399
+ throw new Error('Cipher text must not be empty')
400
+ }
401
+
402
+ if (initializationVector.length === 0) {
403
+ throw new Error('Initialization vector must not be empty')
404
+ }
405
+
406
+ if (key.length === 0) {
407
+ throw new Error('Key must not be empty')
408
+ }
409
+
390
410
  let preCounterBlock
391
411
  let compareTag
392
412
 
@@ -0,0 +1,334 @@
1
+ import Random from './Random.js'
2
+ import { sha256, sha256hmac } from './Hash.js'
3
+ import { toArray, toHex } from './utils.js'
4
+
5
+ export type P256Point = { x: bigint, y: bigint } | null
6
+
7
+ type ByteSource = string | Uint8Array | ArrayBufferView
8
+
9
+ const HEX_REGEX = /^[0-9a-fA-F]+$/
10
+
11
+ const P = BigInt('0xffffffff00000001000000000000000000000000ffffffffffffffffffffffff')
12
+ const N = BigInt('0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551')
13
+ const A = P - 3n // a = -3 mod p
14
+ const B = BigInt('0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b')
15
+ const GX = BigInt('0x6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296')
16
+ const GY = BigInt('0x4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5')
17
+ const G: P256Point = { x: GX, y: GY }
18
+ const HALF_N = N >> 1n
19
+
20
+ const COMPRESSED_EVEN = '02'
21
+ const COMPRESSED_ODD = '03'
22
+ const UNCOMPRESSED = '04'
23
+
24
+ /**
25
+ * Pure BigInt implementation of the NIST P-256 (secp256r1) curve with ECDSA sign/verify.
26
+ *
27
+ * This class is standalone (no dependency on the existing secp256k1 primitives) and exposes
28
+ * key generation, point encoding/decoding, scalar multiplication, and SHA-256 based ECDSA.
29
+ */
30
+ export default class Secp256r1 {
31
+ readonly p = P
32
+ readonly n = N
33
+ readonly a = A
34
+ readonly b = B
35
+ readonly g = G
36
+
37
+ private mod (x: bigint, m: bigint = this.p): bigint {
38
+ const v = x % m
39
+ return v >= 0n ? v : v + m
40
+ }
41
+
42
+ private modInv (x: bigint, m: bigint): bigint {
43
+ if (x === 0n || m <= 0n) throw new Error('Invalid mod inverse input')
44
+ let [a, b] = [this.mod(x, m), m]
45
+ let [u, v] = [1n, 0n]
46
+ while (b !== 0n) {
47
+ const q = a / b
48
+ ;[a, b] = [b, a - q * b]
49
+ ;[u, v] = [v, u - q * v]
50
+ }
51
+ if (a !== 1n) throw new Error('Inverse does not exist')
52
+ return this.mod(u, m)
53
+ }
54
+
55
+ private modPow (base: bigint, exponent: bigint, modulus: bigint): bigint {
56
+ if (modulus === 1n) return 0n
57
+ let result = 1n
58
+ let b = this.mod(base, modulus)
59
+ let e = exponent
60
+ while (e > 0n) {
61
+ if ((e & 1n) === 1n) result = this.mod(result * b, modulus)
62
+ e >>= 1n
63
+ b = this.mod(b * b, modulus)
64
+ }
65
+ return result
66
+ }
67
+
68
+ private isInfinity (p: P256Point): p is null {
69
+ return p === null
70
+ }
71
+
72
+ private assertOnCurve (p: P256Point): void {
73
+ if (this.isInfinity(p)) return
74
+ const { x, y } = p
75
+ const left = this.mod(y * y)
76
+ const right = this.mod(this.mod(x * x * x + this.a * x) + this.b)
77
+ if (left !== right) {
78
+ throw new Error('Point is not on secp256r1')
79
+ }
80
+ }
81
+
82
+ pointFromAffine (x: bigint, y: bigint): P256Point {
83
+ const point: P256Point = { x: this.mod(x), y: this.mod(y) }
84
+ this.assertOnCurve(point)
85
+ return point
86
+ }
87
+
88
+ /**
89
+ * Decode a point from compressed or uncompressed hex.
90
+ */
91
+ pointFromHex (hex: string): P256Point {
92
+ if (hex.startsWith(UNCOMPRESSED)) {
93
+ const x = BigInt('0x' + hex.slice(2, 66))
94
+ const y = BigInt('0x' + hex.slice(66))
95
+ return this.pointFromAffine(x, y)
96
+ }
97
+ if (hex.startsWith(COMPRESSED_EVEN) || hex.startsWith(COMPRESSED_ODD)) {
98
+ const x = BigInt('0x' + hex.slice(2))
99
+ const ySq = this.mod(this.mod(x * x * x + this.a * x) + this.b)
100
+ const y = this.modPow(ySq, (this.p + 1n) >> 2n, this.p)
101
+ const isOdd = (y & 1n) === 1n
102
+ const shouldBeOdd = hex.startsWith(COMPRESSED_ODD)
103
+ const yFinal = (isOdd === shouldBeOdd) ? y : this.p - y
104
+ return this.pointFromAffine(x, yFinal)
105
+ }
106
+ throw new Error('Invalid point encoding')
107
+ }
108
+
109
+ /**
110
+ * Encode a point to compressed or uncompressed hex. Infinity is encoded as `00`.
111
+ */
112
+ pointToHex (p: P256Point, compressed = false): string {
113
+ if (this.isInfinity(p)) return '00'
114
+ const xHex = this.to32BytesHex(p.x)
115
+ const yHex = this.to32BytesHex(p.y)
116
+ if (!compressed) return UNCOMPRESSED + xHex + yHex
117
+ const prefix = (p.y & 1n) === 0n ? COMPRESSED_EVEN : COMPRESSED_ODD
118
+ return prefix + xHex
119
+ }
120
+
121
+ /**
122
+ * Add two affine points (handles infinity).
123
+ */
124
+ private addPoints (p1: P256Point, p2: P256Point): P256Point {
125
+ if (this.isInfinity(p1)) return p2
126
+ if (this.isInfinity(p2)) return p1
127
+
128
+ const { x: x1, y: y1 } = p1
129
+ const { x: x2, y: y2 } = p2
130
+
131
+ if (x1 === x2) {
132
+ if (y1 === y2) {
133
+ return this.doublePoint(p1)
134
+ }
135
+ return null
136
+ }
137
+
138
+ const m = this.mod((y2 - y1) * this.modInv(x2 - x1, this.p))
139
+ const x3 = this.mod(m * m - x1 - x2)
140
+ const y3 = this.mod(m * (x1 - x3) - y1)
141
+ return { x: x3, y: y3 }
142
+ }
143
+
144
+ private doublePoint (p: P256Point): P256Point {
145
+ if (this.isInfinity(p)) return p
146
+ if (p.y === 0n) return null
147
+ const m = this.mod((3n * p.x * p.x + this.a) * this.modInv(2n * p.y, this.p))
148
+ const x3 = this.mod(m * m - 2n * p.x)
149
+ const y3 = this.mod(m * (p.x - x3) - p.y)
150
+ return { x: x3, y: y3 }
151
+ }
152
+
153
+ /**
154
+ * Add two points (handles infinity).
155
+ */
156
+ add (p1: P256Point, p2: P256Point): P256Point {
157
+ return this.addPoints(p1, p2)
158
+ }
159
+
160
+ /**
161
+ * Scalar multiply an arbitrary point using double-and-add.
162
+ */
163
+ multiply (point: P256Point, scalar: bigint): P256Point {
164
+ if (scalar === 0n || this.isInfinity(point)) return null
165
+ let k = this.mod(scalar, this.n)
166
+ let result: P256Point = null
167
+ let addend: P256Point = point
168
+ while (k > 0n) {
169
+ if ((k & 1n) === 1n) {
170
+ result = this.addPoints(result, addend)
171
+ }
172
+ addend = this.doublePoint(addend)
173
+ k >>= 1n
174
+ }
175
+ return result
176
+ }
177
+
178
+ /**
179
+ * Scalar multiply the base point.
180
+ */
181
+ multiplyBase (scalar: bigint): P256Point {
182
+ return this.multiply(this.g, scalar)
183
+ }
184
+
185
+ /**
186
+ * Check if a point lies on the curve (including infinity).
187
+ */
188
+ isOnCurve (p: P256Point): boolean {
189
+ try {
190
+ this.assertOnCurve(p)
191
+ return true
192
+ } catch (err) {
193
+ return false
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Generate a new random private key as 32-byte hex.
199
+ */
200
+ generatePrivateKeyHex (): string {
201
+ return this.to32BytesHex(this.randomScalar())
202
+ }
203
+
204
+ private randomScalar (): bigint {
205
+ while (true) {
206
+ const bytes = Random(32)
207
+ const k = BigInt('0x' + toHex(bytes))
208
+ if (k > 0n && k < this.n) return k
209
+ }
210
+ }
211
+
212
+ private normalizePrivateKey (d: bigint): bigint {
213
+ const key = this.mod(d, this.n)
214
+ if (key === 0n) throw new Error('Invalid private key')
215
+ return key
216
+ }
217
+
218
+ private toScalar (input: string | bigint): bigint {
219
+ if (typeof input === 'bigint') return this.normalizePrivateKey(input)
220
+ const hex = input.startsWith('0x') ? input.slice(2) : input
221
+ if (!HEX_REGEX.test(hex) || hex.length === 0 || hex.length > 64) {
222
+ throw new Error('Private key must be a hex string <= 32 bytes')
223
+ }
224
+ const value = BigInt('0x' + hex.padStart(64, '0'))
225
+ return this.normalizePrivateKey(value)
226
+ }
227
+
228
+ publicKeyFromPrivate (privateKey: string | bigint): P256Point {
229
+ const d = this.toScalar(privateKey)
230
+ return this.multiplyBase(d)
231
+ }
232
+
233
+ /**
234
+ * Create an ECDSA signature over a message. Uses SHA-256 unless `prehashed` is true.
235
+ * Returns low-s normalized signature hex parts.
236
+ */
237
+ sign (message: ByteSource, privateKey: string | bigint, opts: { prehashed?: boolean, nonce?: bigint } = {}): { r: string, s: string } {
238
+ const { prehashed = false, nonce } = opts
239
+ const d = this.toScalar(privateKey)
240
+ const digest = this.normalizeMessage(message, prehashed)
241
+ const z = this.bytesToScalar(digest)
242
+ let k = nonce ?? this.deterministicNonce(d, digest)
243
+
244
+ while (true) {
245
+ const p = this.multiplyBase(k)
246
+ if (this.isInfinity(p)) {
247
+ k = nonce ?? this.deterministicNonce(d, digest)
248
+ continue
249
+ }
250
+ const r = this.mod(p.x, this.n)
251
+ if (r === 0n) {
252
+ k = nonce ?? this.deterministicNonce(d, digest)
253
+ continue
254
+ }
255
+ const kinv = this.modInv(k, this.n)
256
+ let s = this.mod(kinv * (z + r * d), this.n)
257
+ if (s === 0n) {
258
+ k = nonce ?? this.deterministicNonce(d, digest)
259
+ continue
260
+ }
261
+ if (s > HALF_N) s = this.n - s // enforce low-s
262
+ return { r: this.to32BytesHex(r), s: this.to32BytesHex(s) }
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Verify an ECDSA signature against a message and public key.
268
+ */
269
+ verify (message: ByteSource, signature: { r: string | bigint, s: string | bigint }, publicKey: P256Point | string, opts: { prehashed?: boolean } = {}): boolean {
270
+ const { prehashed = false } = opts
271
+ let q: P256Point
272
+ try {
273
+ q = typeof publicKey === 'string' ? this.pointFromHex(publicKey) : publicKey
274
+ } catch {
275
+ return false
276
+ }
277
+ if ((q == null) || !this.isOnCurve(q)) return false
278
+
279
+ const r = typeof signature.r === 'bigint' ? signature.r : BigInt('0x' + signature.r)
280
+ const s = typeof signature.s === 'bigint' ? signature.s : BigInt('0x' + signature.s)
281
+ if (r <= 0n || r >= this.n || s <= 0n || s >= this.n) return false
282
+
283
+ const z = this.bytesToScalar(this.normalizeMessage(message, prehashed))
284
+ const w = this.modInv(s, this.n)
285
+ const u1 = this.mod(z * w, this.n)
286
+ const u2 = this.mod(r * w, this.n)
287
+ const p = this.addPoints(this.multiplyBase(u1), this.multiply(q, u2))
288
+ if (this.isInfinity(p)) return false
289
+ const v = this.mod(p.x, this.n)
290
+ return v === r
291
+ }
292
+
293
+ private normalizeMessage (message: ByteSource, prehashed: boolean): Uint8Array {
294
+ const bytes = this.toBytes(message)
295
+ if (prehashed) return bytes
296
+ return new Uint8Array(sha256(bytes))
297
+ }
298
+
299
+ private bytesToScalar (bytes: Uint8Array): bigint {
300
+ const hex = toHex(Array.from(bytes))
301
+ return BigInt('0x' + hex) % this.n
302
+ }
303
+
304
+ private deterministicNonce (priv: bigint, msgDigest: Uint8Array): bigint {
305
+ const keyBytes = toArray(this.to32BytesHex(priv), 'hex')
306
+ let counter = 0
307
+ while (counter < 1024) { // safety bound
308
+ const data = counter === 0
309
+ ? Array.from(msgDigest)
310
+ : Array.from(msgDigest).concat([counter & 0xff])
311
+ const hmac = sha256hmac(keyBytes, data)
312
+ const k = BigInt('0x' + toHex(hmac)) % this.n
313
+ if (k > 0n) return k
314
+ counter++
315
+ }
316
+ throw new Error('Failed to derive deterministic nonce')
317
+ }
318
+
319
+ private toBytes (data: ByteSource): Uint8Array {
320
+ if (typeof data === 'string') {
321
+ const isHex = HEX_REGEX.test(data) && data.length % 2 === 0
322
+ return Uint8Array.from(toArray(data, isHex ? 'hex' : 'utf8'))
323
+ }
324
+ if (data instanceof Uint8Array) return data
325
+ if (ArrayBuffer.isView(data)) {
326
+ return new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
327
+ }
328
+ throw new Error('Unsupported message format')
329
+ }
330
+
331
+ private to32BytesHex (num: bigint): string {
332
+ return num.toString(16).padStart(64, '0')
333
+ }
334
+ }
@@ -87,10 +87,6 @@ export default class SymmetricKey extends BigNumber {
87
87
  const ciphertext = msg.slice(ivLength, tagStart)
88
88
  const messageTag = msg.slice(tagStart)
89
89
 
90
- if (tagStart < ivLength) {
91
- throw new Error('Malformed ciphertext')
92
- }
93
-
94
90
  const result = AESGCMDecrypt(
95
91
  ciphertext,
96
92
  [],
@@ -8,7 +8,8 @@ import {
8
8
  incrementLeastSignificantThirtyTwoBits,
9
9
  checkBit,
10
10
  getBytes,
11
- exclusiveOR
11
+ exclusiveOR,
12
+ AESGCMDecrypt
12
13
  } from '../../primitives/AESGCM'
13
14
  import { toArray } from '../../primitives/utils'
14
15
 
@@ -642,3 +643,58 @@ describe('getBytes', () => {
642
643
  expect([0x04, 0x03, 0x02, 0x01]).toEqual(getBytes(0x0504030201))
643
644
  })
644
645
  })
646
+
647
+ describe('AESGCM IV validation', () => {
648
+ const key = new Array(16).fill(0x01)
649
+ const aad: number[] = []
650
+ const plaintext = [1, 2, 3, 4]
651
+
652
+ it('AESGCM throws when IV is empty', () => {
653
+ expect(() => {
654
+ AESGCM(plaintext, aad, [], key)
655
+ }).toThrow(new Error('Initialization vector must not be empty'))
656
+ })
657
+
658
+ it('AESGCMDecrypt throws when IV is empty', () => {
659
+ const iv = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
660
+ const { result: ciphertext, authenticationTag } = AESGCM(plaintext, aad, iv, key)
661
+
662
+ // Now call decrypt but with an empty IV – this should be rejected
663
+ expect(() => {
664
+ AESGCMDecrypt(ciphertext, aad, [], authenticationTag, key)
665
+ }).toThrow(new Error('Initialization vector must not be empty'))
666
+ })
667
+
668
+ it('AESGCM throws when key is empty', () => {
669
+ const iv = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
670
+
671
+ expect(() => {
672
+ AESGCM(plaintext, aad, iv, [])
673
+ }).toThrow(new Error('Key must not be empty'))
674
+ })
675
+
676
+ it('AESGCMDecrypt throws when key is empty', () => {
677
+ const iv = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
678
+ const { result: ciphertext, authenticationTag } = AESGCM(plaintext, aad, iv, key)
679
+
680
+ expect(() => {
681
+ AESGCMDecrypt(ciphertext, aad, iv, authenticationTag, [])
682
+ }).toThrow(new Error('Key must not be empty'))
683
+ })
684
+
685
+ it('AESGCMDecrypt throws when cipher text is empty', () => {
686
+ const iv = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
687
+
688
+ expect(() => {
689
+ AESGCMDecrypt([], aad, iv, [], key)
690
+ }).toThrow(new Error('Cipher text must not be empty'))
691
+ })
692
+
693
+ it('AESGCM still work with a valid IV', () => {
694
+ const iv = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
695
+ const { result: ciphertext, authenticationTag } = AESGCM(plaintext, aad, iv, key)
696
+ const decrypted = AESGCMDecrypt(ciphertext, aad, iv, authenticationTag, key)
697
+
698
+ expect(decrypted).toEqual(plaintext)
699
+ })
700
+ })