@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.
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/src/primitives/AESGCM.js +15 -0
- package/dist/cjs/src/primitives/AESGCM.js.map +1 -1
- package/dist/cjs/src/primitives/Secp256r1.js +327 -0
- package/dist/cjs/src/primitives/Secp256r1.js.map +1 -0
- package/dist/cjs/src/primitives/SymmetricKey.js +0 -3
- package/dist/cjs/src/primitives/SymmetricKey.js.map +1 -1
- package/dist/cjs/src/primitives/index.js +3 -1
- package/dist/cjs/src/primitives/index.js.map +1 -1
- package/dist/cjs/src/primitives/utils.js +59 -12
- package/dist/cjs/src/primitives/utils.js.map +1 -1
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/src/primitives/AESGCM.js +15 -0
- package/dist/esm/src/primitives/AESGCM.js.map +1 -1
- package/dist/esm/src/primitives/Secp256r1.js +319 -0
- package/dist/esm/src/primitives/Secp256r1.js.map +1 -0
- package/dist/esm/src/primitives/SymmetricKey.js +0 -3
- package/dist/esm/src/primitives/SymmetricKey.js.map +1 -1
- package/dist/esm/src/primitives/index.js +1 -0
- package/dist/esm/src/primitives/index.js.map +1 -1
- package/dist/esm/src/primitives/utils.js +58 -12
- package/dist/esm/src/primitives/utils.js.map +1 -1
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/types/src/primitives/AESGCM.d.ts.map +1 -1
- package/dist/types/src/primitives/Secp256r1.d.ts +91 -0
- package/dist/types/src/primitives/Secp256r1.d.ts.map +1 -0
- package/dist/types/src/primitives/SymmetricKey.d.ts.map +1 -1
- package/dist/types/src/primitives/index.d.ts +1 -0
- package/dist/types/src/primitives/index.d.ts.map +1 -1
- package/dist/types/src/primitives/utils.d.ts +1 -0
- package/dist/types/src/primitives/utils.d.ts.map +1 -1
- package/dist/types/tsconfig.types.tsbuildinfo +1 -1
- package/dist/umd/bundle.js +3 -3
- package/dist/umd/bundle.js.map +1 -1
- package/docs/reference/primitives.md +164 -5
- package/package.json +1 -1
- package/src/auth/utils/__tests/cryptononce.test.ts +3 -3
- package/src/primitives/AESGCM.ts +20 -0
- package/src/primitives/Secp256r1.ts +334 -0
- package/src/primitives/SymmetricKey.ts +0 -4
- package/src/primitives/__tests/AESGCM.test.ts +57 -1
- package/src/primitives/__tests/Secp256r1.test.ts +101 -0
- package/src/primitives/__tests/utils.test.ts +44 -0
- package/src/primitives/index.ts +1 -0
- 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) | [
|
|
52
|
-
| [K256](#class-k256) | [Reader](#class-reader) | [
|
|
53
|
-
| [KeyShares](#class-keyshares) | [ReductionContext](#class-reductioncontext) | [
|
|
54
|
-
| [Mersenne](#class-mersenne) | [SHA1](#class-sha1) | [
|
|
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
|
@@ -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)).
|
|
56
|
-
|
|
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(
|
package/src/primitives/AESGCM.ts
CHANGED
|
@@ -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
|
+
})
|