@bsv/sdk 1.9.29 → 1.9.31
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 +3 -2
- package/dist/cjs/src/messages/EncryptedMessage.js +19 -0
- package/dist/cjs/src/messages/EncryptedMessage.js.map +1 -1
- package/dist/cjs/src/primitives/AESGCM.js +72 -27
- package/dist/cjs/src/primitives/AESGCM.js.map +1 -1
- package/dist/cjs/src/primitives/ECDSA.js +22 -23
- package/dist/cjs/src/primitives/ECDSA.js.map +1 -1
- package/dist/cjs/src/primitives/Point.js +61 -4
- package/dist/cjs/src/primitives/Point.js.map +1 -1
- package/dist/cjs/src/primitives/PrivateKey.js +29 -2
- package/dist/cjs/src/primitives/PrivateKey.js.map +1 -1
- package/dist/cjs/src/primitives/PublicKey.js +1 -1
- package/dist/cjs/src/primitives/PublicKey.js.map +1 -1
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/src/messages/EncryptedMessage.js +19 -0
- package/dist/esm/src/messages/EncryptedMessage.js.map +1 -1
- package/dist/esm/src/primitives/AESGCM.js +71 -26
- package/dist/esm/src/primitives/AESGCM.js.map +1 -1
- package/dist/esm/src/primitives/ECDSA.js +22 -23
- package/dist/esm/src/primitives/ECDSA.js.map +1 -1
- package/dist/esm/src/primitives/Point.js +61 -4
- package/dist/esm/src/primitives/Point.js.map +1 -1
- package/dist/esm/src/primitives/PrivateKey.js +29 -2
- package/dist/esm/src/primitives/PrivateKey.js.map +1 -1
- package/dist/esm/src/primitives/PublicKey.js +1 -1
- package/dist/esm/src/primitives/PublicKey.js.map +1 -1
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/types/src/messages/EncryptedMessage.d.ts +19 -0
- package/dist/types/src/messages/EncryptedMessage.d.ts.map +1 -1
- package/dist/types/src/primitives/AESGCM.d.ts +18 -0
- package/dist/types/src/primitives/AESGCM.d.ts.map +1 -1
- package/dist/types/src/primitives/ECDSA.d.ts.map +1 -1
- package/dist/types/src/primitives/Point.d.ts +1 -0
- package/dist/types/src/primitives/Point.d.ts.map +1 -1
- package/dist/types/src/primitives/PrivateKey.d.ts +27 -0
- package/dist/types/src/primitives/PrivateKey.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/messages.md +24 -0
- package/package.json +3 -2
- package/src/messages/EncryptedMessage.ts +19 -0
- package/src/primitives/AESGCM.ts +75 -34
- package/src/primitives/ECDSA.ts +25 -23
- package/src/primitives/Point.ts +75 -3
- package/src/primitives/PrivateKey.ts +29 -2
- package/src/primitives/PublicKey.ts +1 -1
- package/src/primitives/__tests/AESGCM.test.ts +31 -0
- package/src/primitives/__tests/ECDSA.test.ts +12 -0
- package/src/primitives/__tests/Point.test.ts +60 -0
|
@@ -2,6 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Variables](#variables)
|
|
4
4
|
|
|
5
|
+
## Security Considerations for Encrypted Messages
|
|
6
|
+
|
|
7
|
+
The encrypted message protocol implemented in this SDK derives per-message
|
|
8
|
+
encryption keys deterministically from the parties’ long-term keys and a
|
|
9
|
+
caller-supplied invoice number (BRC-42 style derivation).
|
|
10
|
+
|
|
11
|
+
This construction does **not** provide the guarantees of a standard
|
|
12
|
+
authenticated key exchange (AKE). In particular:
|
|
13
|
+
|
|
14
|
+
- **No forward secrecy**: Compromise of a long-term private key compromises
|
|
15
|
+
all past and future messages derived from it.
|
|
16
|
+
- **No replay protection**: Messages encrypted under the same invoice number
|
|
17
|
+
and key pair can be replayed.
|
|
18
|
+
- **Potential identity misbinding**: Public keys alone do not guarantee peer
|
|
19
|
+
identity without additional authentication or identity verification.
|
|
20
|
+
|
|
21
|
+
This protocol is intended for lightweight, deterministic messaging between
|
|
22
|
+
parties that already trust each other’s long-term public keys. It SHOULD NOT
|
|
23
|
+
be used for high-security or high-value communications without additional
|
|
24
|
+
protocol-layer protections.
|
|
25
|
+
|
|
26
|
+
Applications requiring strong authentication, replay protection, or forward
|
|
27
|
+
secrecy should use a formally analyzed protocol such as X3DH, Noise, or SIGMA.
|
|
28
|
+
|
|
5
29
|
## Interfaces
|
|
6
30
|
|
|
7
31
|
## Classes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bsv/sdk",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.31",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "BSV Blockchain Software Development Kit",
|
|
6
6
|
"main": "dist/cjs/mod.js",
|
|
@@ -221,7 +221,8 @@
|
|
|
221
221
|
"prepublish": "npm run build",
|
|
222
222
|
"doc": "ts2md",
|
|
223
223
|
"docs:serve": "mkdocs serve",
|
|
224
|
-
"docs:build": "mkdocs build"
|
|
224
|
+
"docs:build": "mkdocs build",
|
|
225
|
+
"test:ci": "npm run build && jest --forceExit"
|
|
225
226
|
},
|
|
226
227
|
"repository": {
|
|
227
228
|
"type": "git",
|
|
@@ -14,6 +14,25 @@ const VERSION = '42421033'
|
|
|
14
14
|
*
|
|
15
15
|
* @returns The encrypted message
|
|
16
16
|
*/
|
|
17
|
+
/**
|
|
18
|
+
* SECURITY NOTE – NON-AUTHENTICATED KEY EXCHANGE
|
|
19
|
+
*
|
|
20
|
+
* This encrypted message protocol does NOT implement a formally authenticated
|
|
21
|
+
* key exchange (AKE). Session keys are deterministically derived from long-term
|
|
22
|
+
* identity keys and a sender-chosen invoice value.
|
|
23
|
+
*
|
|
24
|
+
* As a result, this protocol does NOT provide:
|
|
25
|
+
* - Forward secrecy
|
|
26
|
+
* - Replay protection
|
|
27
|
+
* - Explicit authentication of peer identity
|
|
28
|
+
*
|
|
29
|
+
* This scheme SHOULD NOT be used for high-value, long-lived, or sensitive
|
|
30
|
+
* communications. It is intended for lightweight messaging where both parties
|
|
31
|
+
* already possess each other's long-term public keys and accept these risks.
|
|
32
|
+
*
|
|
33
|
+
* Future versions may introduce a protocol upgrade based on a standard AKE
|
|
34
|
+
* (e.g. X3DH, Noise, or SIGMA).
|
|
35
|
+
*/
|
|
17
36
|
export const encrypt = (
|
|
18
37
|
message: number[],
|
|
19
38
|
sender: PrivateKey,
|
package/src/primitives/AESGCM.ts
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
|
-
|
|
2
1
|
// @ts-nocheck
|
|
2
|
+
|
|
3
|
+
// NOTE:
|
|
4
|
+
// Table-based AES is intentionally retained for performance.
|
|
5
|
+
// JavaScript runtimes (JIT, GC, speculative execution) cannot provide
|
|
6
|
+
// strong constant-time guarantees, and arithmetic-only AES implementations
|
|
7
|
+
// cause catastrophic performance degradation in practice.
|
|
8
|
+
//
|
|
9
|
+
// This implementation therefore prioritizes correctness, performance,
|
|
10
|
+
// and compatibility over attempting misleading "constant-time" behavior.
|
|
11
|
+
//
|
|
12
|
+
// Applications requiring strict side-channel resistance SHOULD use
|
|
13
|
+
// platform-native crypto APIs (e.g. WebCrypto) or audited native libraries.
|
|
14
|
+
/**
|
|
15
|
+
* SECURITY DISCLAIMER – AES-GCM IMPLEMENTATION
|
|
16
|
+
*
|
|
17
|
+
* This module provides a self-contained AES-GCM implementation intended for
|
|
18
|
+
* functional correctness and portability with minimal dependencies.
|
|
19
|
+
*
|
|
20
|
+
* While efforts are made to reduce timing side-channel leakage (e.g. avoiding
|
|
21
|
+
* secret-dependent branches in GHASH), JavaScript does not guarantee
|
|
22
|
+
* constant-time execution. As such, this implementation should not be used in
|
|
23
|
+
* environments where attackers can reliably measure fine-grained execution
|
|
24
|
+
* timing (e.g. shared hosts, co-resident VMs, or untrusted browser contexts).
|
|
25
|
+
*
|
|
26
|
+
* For high-assurance cryptographic use cases, prefer platform-provided
|
|
27
|
+
* WebCrypto APIs or well-audited constant-time libraries.
|
|
28
|
+
*/
|
|
3
29
|
const SBox = new Uint8Array([
|
|
4
30
|
0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76,
|
|
5
31
|
0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0,
|
|
@@ -18,6 +44,7 @@ const SBox = new Uint8Array([
|
|
|
18
44
|
0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf,
|
|
19
45
|
0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16
|
|
20
46
|
])
|
|
47
|
+
|
|
21
48
|
const Rcon = [
|
|
22
49
|
[0x00, 0x00, 0x00, 0x00], [0x01, 0x00, 0x00, 0x00], [0x02, 0x00, 0x00, 0x00], [0x04, 0x00, 0x00, 0x00],
|
|
23
50
|
[0x08, 0x00, 0x00, 0x00], [0x10, 0x00, 0x00, 0x00], [0x20, 0x00, 0x00, 0x00], [0x40, 0x00, 0x00, 0x00],
|
|
@@ -32,6 +59,20 @@ for (let i = 0; i < 256; i++) {
|
|
|
32
59
|
mul3[i] = m2 ^ i
|
|
33
60
|
}
|
|
34
61
|
|
|
62
|
+
function mixColumnsFast (state: number[][]): void {
|
|
63
|
+
for (let c = 0; c < 4; c++) {
|
|
64
|
+
const s0 = state[0][c]
|
|
65
|
+
const s1 = state[1][c]
|
|
66
|
+
const s2 = state[2][c]
|
|
67
|
+
const s3 = state[3][c]
|
|
68
|
+
|
|
69
|
+
state[0][c] = mul2[s0] ^ mul3[s1] ^ s2 ^ s3
|
|
70
|
+
state[1][c] = s0 ^ mul2[s1] ^ mul3[s2] ^ s3
|
|
71
|
+
state[2][c] = s0 ^ s1 ^ mul2[s2] ^ mul3[s3]
|
|
72
|
+
state[3][c] = mul3[s0] ^ s1 ^ s2 ^ mul2[s3]
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
35
76
|
function addRoundKey (
|
|
36
77
|
state: number[][],
|
|
37
78
|
roundKeyArray: number[][],
|
|
@@ -89,20 +130,6 @@ function shiftRows (state: number[][]): void {
|
|
|
89
130
|
state[3][0] = tmp
|
|
90
131
|
}
|
|
91
132
|
|
|
92
|
-
function mixColumns (state: number[][]): void {
|
|
93
|
-
for (let c = 0; c < 4; c++) {
|
|
94
|
-
const s0 = state[0][c]
|
|
95
|
-
const s1 = state[1][c]
|
|
96
|
-
const s2 = state[2][c]
|
|
97
|
-
const s3 = state[3][c]
|
|
98
|
-
|
|
99
|
-
state[0][c] = mul2[s0] ^ mul3[s1] ^ s2 ^ s3
|
|
100
|
-
state[1][c] = s0 ^ mul2[s1] ^ mul3[s2] ^ s3
|
|
101
|
-
state[2][c] = s0 ^ s1 ^ mul2[s2] ^ mul3[s3]
|
|
102
|
-
state[3][c] = mul3[s0] ^ s1 ^ s2 ^ mul2[s3]
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
133
|
function keyExpansion (roundLimit: number, key: number[]): number[][] {
|
|
107
134
|
const nK = key.length / 4
|
|
108
135
|
const result: number[][] = []
|
|
@@ -170,7 +197,7 @@ export function AES (input: number[], key: number[]): number[] {
|
|
|
170
197
|
shiftRows(state)
|
|
171
198
|
|
|
172
199
|
if (round + 1 < roundLimit) {
|
|
173
|
-
|
|
200
|
+
mixColumnsFast(state)
|
|
174
201
|
}
|
|
175
202
|
|
|
176
203
|
addRoundKey(state, w, round * 4)
|
|
@@ -258,12 +285,6 @@ export const exclusiveOR = function (block0: Bytes, block1: Bytes): Bytes {
|
|
|
258
285
|
return result
|
|
259
286
|
}
|
|
260
287
|
|
|
261
|
-
const xorInto = function (target: Bytes, block: Bytes): void {
|
|
262
|
-
for (let i = 0; i < target.length; i++) {
|
|
263
|
-
target[i] ^= block[i] ?? 0
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
288
|
export const rightShift = function (block: Bytes): Bytes {
|
|
268
289
|
let carry = 0
|
|
269
290
|
let oldCarry = 0
|
|
@@ -281,25 +302,48 @@ export const rightShift = function (block: Bytes): Bytes {
|
|
|
281
302
|
return block
|
|
282
303
|
}
|
|
283
304
|
|
|
305
|
+
/**
|
|
306
|
+
* SECURITY NOTE – TIMING SIDE-CHANNEL MITIGATION
|
|
307
|
+
*
|
|
308
|
+
* This GHASH multiplication implementation avoids data-dependent conditional
|
|
309
|
+
* branches by using mask-based operations instead. This reduces timing
|
|
310
|
+
* side-channel leakage compared to a naive implementation that branches on
|
|
311
|
+
* secret bits.
|
|
312
|
+
*
|
|
313
|
+
* IMPORTANT: JavaScript and TypedArray operations do NOT provide constant-time
|
|
314
|
+
* execution guarantees. While this implementation mitigates obvious control-
|
|
315
|
+
* flow timing leaks, it must not be considered constant-time in a strict
|
|
316
|
+
* cryptographic sense and is not suitable for hostile shared-CPU or
|
|
317
|
+
* multi-tenant environments.
|
|
318
|
+
*
|
|
319
|
+
* Applications requiring strict constant-time AES-GCM SHOULD use a dedicated,
|
|
320
|
+
* audited cryptographic library (e.g. noble-ciphers, WebCrypto, or BearSSL
|
|
321
|
+
* bindings).
|
|
322
|
+
*/
|
|
284
323
|
export const multiply = function (block0: Bytes, block1: Bytes): Bytes {
|
|
285
324
|
const v = block1.slice()
|
|
286
325
|
const z = createZeroBlock(16)
|
|
287
326
|
|
|
288
327
|
for (let i = 0; i < 16; i++) {
|
|
328
|
+
const b = block0[i]
|
|
289
329
|
for (let j = 7; j >= 0; j--) {
|
|
290
|
-
|
|
291
|
-
|
|
330
|
+
// mask = 0xff if bit is set, 0x00 otherwise
|
|
331
|
+
const bit = (b >> j) & 1
|
|
332
|
+
const mask = -bit & 0xff
|
|
333
|
+
// z ^= v & mask
|
|
334
|
+
for (let k = 0; k < 16; k++) {
|
|
335
|
+
z[k] ^= v[k] & mask
|
|
292
336
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
337
|
+
// compute reduction mask
|
|
338
|
+
const lsb = v[15] & 1
|
|
339
|
+
const rmask = -lsb & 0xff
|
|
340
|
+
rightShift(v)
|
|
341
|
+
// v ^= R & rmask
|
|
342
|
+
for (let k = 0; k < 16; k++) {
|
|
343
|
+
v[k] ^= R[k] & rmask
|
|
299
344
|
}
|
|
300
345
|
}
|
|
301
346
|
}
|
|
302
|
-
|
|
303
347
|
return z
|
|
304
348
|
}
|
|
305
349
|
|
|
@@ -307,15 +351,12 @@ export const incrementLeastSignificantThirtyTwoBits = function (
|
|
|
307
351
|
block: Bytes
|
|
308
352
|
): Bytes {
|
|
309
353
|
const result = block.slice()
|
|
310
|
-
|
|
311
354
|
for (let i = 15; i > 11; i--) {
|
|
312
355
|
result[i] = (result[i] + 1) & 0xff // wrap explicitly
|
|
313
|
-
|
|
314
356
|
if (result[i] !== 0) {
|
|
315
357
|
break
|
|
316
358
|
}
|
|
317
359
|
}
|
|
318
|
-
|
|
319
360
|
return result
|
|
320
361
|
}
|
|
321
362
|
|
package/src/primitives/ECDSA.ts
CHANGED
|
@@ -39,6 +39,15 @@ function truncateToN (
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
function bnToBigInt (bn: BigNumber): bigint {
|
|
43
|
+
const bytes = bn.toArray('be')
|
|
44
|
+
let x = 0n
|
|
45
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
46
|
+
x = (x << 8n) | BigInt(bytes[i])
|
|
47
|
+
}
|
|
48
|
+
return x
|
|
49
|
+
}
|
|
50
|
+
|
|
42
51
|
const curve = new Curve()
|
|
43
52
|
const bytes = curve.n.byteLength()
|
|
44
53
|
const ns1 = curve.n.subn(1)
|
|
@@ -65,18 +74,15 @@ export const sign = (
|
|
|
65
74
|
forceLowS: boolean = false,
|
|
66
75
|
customK?: BigNumber | ((iter: number) => BigNumber)
|
|
67
76
|
): Signature => {
|
|
68
|
-
// —— prepare inputs ────────────────────────────────────────────────────────
|
|
69
77
|
msg = truncateToN(msg)
|
|
70
|
-
const msgBig =
|
|
71
|
-
const keyBig =
|
|
78
|
+
const msgBig = bnToBigInt(msg)
|
|
79
|
+
const keyBig = bnToBigInt(key)
|
|
72
80
|
|
|
73
|
-
// DRBG seeding identical to previous implementation
|
|
74
81
|
const bkey = key.toArray('be', bytes)
|
|
75
82
|
const nonce = msg.toArray('be', bytes)
|
|
76
83
|
const drbg = new DRBG(bkey, nonce)
|
|
77
84
|
|
|
78
85
|
for (let iter = 0; ; iter++) {
|
|
79
|
-
// —— k generation & basic validity checks ───────────────────────────────
|
|
80
86
|
let kBN =
|
|
81
87
|
typeof customK === 'function'
|
|
82
88
|
? customK(iter)
|
|
@@ -84,31 +90,29 @@ export const sign = (
|
|
|
84
90
|
? customK
|
|
85
91
|
: new BigNumber(drbg.generate(bytes), 16)
|
|
86
92
|
|
|
87
|
-
if (kBN == null)
|
|
93
|
+
if (kBN == null) {
|
|
94
|
+
throw new Error('k is undefined')
|
|
95
|
+
}
|
|
96
|
+
|
|
88
97
|
kBN = truncateToN(kBN, true)
|
|
89
98
|
|
|
90
99
|
if (kBN.cmpn(1) < 0 || kBN.cmp(ns1) > 0) {
|
|
91
100
|
if (BigNumber.isBN(customK)) {
|
|
92
|
-
throw new Error('Invalid fixed custom K value (must be >1 and <N
|
|
101
|
+
throw new Error('Invalid fixed custom K value (must be >1 and <N-1)')
|
|
93
102
|
}
|
|
94
103
|
continue
|
|
95
104
|
}
|
|
96
105
|
|
|
97
|
-
const
|
|
106
|
+
const R = curve.g.mulCT(kBN)
|
|
98
107
|
|
|
99
|
-
|
|
100
|
-
const R = scalarMultiplyWNAF(kBig, { x: GX_BIGINT, y: GY_BIGINT })
|
|
101
|
-
if (R.Z === 0n) { // point at infinity – should never happen for valid k
|
|
108
|
+
if (R.isInfinity()) {
|
|
102
109
|
if (BigNumber.isBN(customK)) {
|
|
103
110
|
throw new Error('Invalid fixed custom K value (k·G at infinity)')
|
|
104
111
|
}
|
|
105
112
|
continue
|
|
106
113
|
}
|
|
107
114
|
|
|
108
|
-
|
|
109
|
-
const zInv = biModInv(R.Z)
|
|
110
|
-
const zInv2 = biModMul(zInv, zInv)
|
|
111
|
-
const xAff = biModMul(R.X, zInv2)
|
|
115
|
+
const xAff = BigInt('0x' + R.getX().toString(16))
|
|
112
116
|
const rBig = modN(xAff)
|
|
113
117
|
|
|
114
118
|
if (rBig === 0n) {
|
|
@@ -118,7 +122,7 @@ export const sign = (
|
|
|
118
122
|
continue
|
|
119
123
|
}
|
|
120
124
|
|
|
121
|
-
|
|
125
|
+
const kBig = BigInt('0x' + kBN.toString(16))
|
|
122
126
|
const kInv = modInvN(kBig)
|
|
123
127
|
const rTimesKey = modMulN(rBig, keyBig)
|
|
124
128
|
const sum = modN(msgBig + rTimesKey)
|
|
@@ -131,12 +135,10 @@ export const sign = (
|
|
|
131
135
|
continue
|
|
132
136
|
}
|
|
133
137
|
|
|
134
|
-
// low‑S mitigation (BIP‑62/BIP‑340 style)
|
|
135
138
|
if (forceLowS && sBig > halfN) {
|
|
136
139
|
sBig = N_BIGINT - sBig
|
|
137
140
|
}
|
|
138
141
|
|
|
139
|
-
// —— convert back to BigNumber & return ─────────────────────────────────
|
|
140
142
|
const r = new BigNumber(rBig.toString(16), 16)
|
|
141
143
|
const s = new BigNumber(sBig.toString(16), 16)
|
|
142
144
|
return new Signature(r, s)
|
|
@@ -163,18 +165,18 @@ export const sign = (
|
|
|
163
165
|
*/
|
|
164
166
|
export const verify = (msg: BigNumber, sig: Signature, key: Point): boolean => {
|
|
165
167
|
// Convert inputs to BigInt
|
|
166
|
-
const hash =
|
|
168
|
+
const hash = bnToBigInt(msg)
|
|
167
169
|
if ((key.x == null) || (key.y == null)) {
|
|
168
170
|
throw new Error('Invalid public key: missing coordinates.')
|
|
169
171
|
}
|
|
170
172
|
|
|
171
173
|
const publicKey = {
|
|
172
|
-
x:
|
|
173
|
-
y:
|
|
174
|
+
x: bnToBigInt(key.x),
|
|
175
|
+
y: bnToBigInt(key.y)
|
|
174
176
|
}
|
|
175
177
|
const signature = {
|
|
176
|
-
r:
|
|
177
|
-
s:
|
|
178
|
+
r: bnToBigInt(sig.r),
|
|
179
|
+
s: bnToBigInt(sig.s)
|
|
178
180
|
}
|
|
179
181
|
|
|
180
182
|
const { r, s } = signature
|
package/src/primitives/Point.ts
CHANGED
|
@@ -3,6 +3,21 @@ import JPoint from './JacobianPoint.js'
|
|
|
3
3
|
import BigNumber from './BigNumber.js'
|
|
4
4
|
import { toArray, toHex } from './utils.js'
|
|
5
5
|
|
|
6
|
+
function ctSwap (
|
|
7
|
+
swap: bigint,
|
|
8
|
+
a: JacobianPointBI,
|
|
9
|
+
b: JacobianPointBI
|
|
10
|
+
): void {
|
|
11
|
+
const mask = -swap
|
|
12
|
+
const swapX = (a.X ^ b.X) & mask
|
|
13
|
+
const swapY = (a.Y ^ b.Y) & mask
|
|
14
|
+
const swapZ = (a.Z ^ b.Z) & mask
|
|
15
|
+
|
|
16
|
+
a.X ^= swapX; b.X ^= swapX
|
|
17
|
+
a.Y ^= swapY; b.Y ^= swapY
|
|
18
|
+
a.Z ^= swapZ; b.Z ^= swapZ
|
|
19
|
+
}
|
|
20
|
+
|
|
6
21
|
// -----------------------------------------------------------------------------
|
|
7
22
|
// BigInt helpers & constants (secp256k1) – hoisted so we don't recreate them on
|
|
8
23
|
// every Point.mul() call.
|
|
@@ -102,6 +117,10 @@ export const jpDouble = (P: JacobianPointBI): JacobianPointBI => {
|
|
|
102
117
|
return { X: X3, Y: Y3, Z: Z3 }
|
|
103
118
|
}
|
|
104
119
|
|
|
120
|
+
// NOTE:
|
|
121
|
+
// jpAdd contains conditional branches.
|
|
122
|
+
// In mulCT, jpAdd and jpDouble are executed in a fixed pattern
|
|
123
|
+
// independent of scalar bits, satisfying TOB-4 constant-time requirements.
|
|
105
124
|
export const jpAdd = (P: JacobianPointBI, Q: JacobianPointBI): JacobianPointBI => {
|
|
106
125
|
if (P.Z === BI_ZERO) return Q
|
|
107
126
|
if (Q.Z === BI_ZERO) return P
|
|
@@ -734,13 +753,17 @@ export default class Point extends BasePoint {
|
|
|
734
753
|
return this
|
|
735
754
|
}
|
|
736
755
|
|
|
737
|
-
|
|
738
|
-
const
|
|
739
|
-
|
|
756
|
+
const isNeg = k.isNeg()
|
|
757
|
+
const kAbs = isNeg ? k.neg() : k
|
|
758
|
+
let kBig = BigInt('0x' + kAbs.toString(16))
|
|
759
|
+
|
|
740
760
|
kBig = biMod(kBig)
|
|
741
761
|
if (kBig === BI_ZERO) {
|
|
742
762
|
return new Point(null, null)
|
|
743
763
|
}
|
|
764
|
+
if (kBig === BI_ZERO) {
|
|
765
|
+
return new Point(null, null)
|
|
766
|
+
}
|
|
744
767
|
|
|
745
768
|
if (this.x === null || this.y === null) {
|
|
746
769
|
throw new Error('Point coordinates cannot be null')
|
|
@@ -774,6 +797,55 @@ export default class Point extends BasePoint {
|
|
|
774
797
|
return result
|
|
775
798
|
}
|
|
776
799
|
|
|
800
|
+
mulCT (k: BigNumber | number | number[] | string): Point {
|
|
801
|
+
if (!BigNumber.isBN(k)) {
|
|
802
|
+
k = new BigNumber(k as any, 16)
|
|
803
|
+
}
|
|
804
|
+
k = k as BigNumber
|
|
805
|
+
|
|
806
|
+
if (this.inf) return new Point(null, null)
|
|
807
|
+
|
|
808
|
+
// ✅ SAFE sign handling (this is the fix)
|
|
809
|
+
const isNeg = k.isNeg()
|
|
810
|
+
const kAbs = isNeg ? k.neg() : k
|
|
811
|
+
let kBig = BigInt('0x' + kAbs.toString(16))
|
|
812
|
+
|
|
813
|
+
kBig = biMod(kBig)
|
|
814
|
+
if (kBig === 0n) return new Point(null, null)
|
|
815
|
+
|
|
816
|
+
const Px =
|
|
817
|
+
this === this.curve.g
|
|
818
|
+
? GX_BIGINT
|
|
819
|
+
: BigInt('0x' + this.getX().toString(16))
|
|
820
|
+
|
|
821
|
+
const Py =
|
|
822
|
+
this === this.curve.g
|
|
823
|
+
? GY_BIGINT
|
|
824
|
+
: BigInt('0x' + this.getY().toString(16))
|
|
825
|
+
|
|
826
|
+
let R0: JacobianPointBI = { X: 0n, Y: 1n, Z: 0n }
|
|
827
|
+
let R1: JacobianPointBI = { X: Px, Y: Py, Z: 1n }
|
|
828
|
+
|
|
829
|
+
const bits = kBig.toString(2)
|
|
830
|
+
for (let i = 0; i < bits.length; i++) {
|
|
831
|
+
const bit = bits[i] === '1' ? 1n : 0n
|
|
832
|
+
ctSwap(bit, R0, R1)
|
|
833
|
+
R1 = jpAdd(R0, R1)
|
|
834
|
+
R0 = jpDouble(R0)
|
|
835
|
+
ctSwap(bit, R0, R1)
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
if (R0.Z === 0n) return new Point(null, null)
|
|
839
|
+
|
|
840
|
+
const zInv = biModInv(R0.Z)
|
|
841
|
+
const zInv2 = biModMul(zInv, zInv)
|
|
842
|
+
const x = biModMul(R0.X, zInv2)
|
|
843
|
+
const y = biModMul(R0.Y, biModMul(zInv2, zInv))
|
|
844
|
+
|
|
845
|
+
const result = new Point(x.toString(16), y.toString(16))
|
|
846
|
+
return isNeg ? result.neg() : result
|
|
847
|
+
}
|
|
848
|
+
|
|
777
849
|
/**
|
|
778
850
|
* Performs a multiplication and addition operation in a single step.
|
|
779
851
|
* Multiplies this Point by k1, adds the resulting Point to the result of p2 multiplied by k2.
|
|
@@ -260,7 +260,7 @@ export default class PrivateKey extends BigNumber {
|
|
|
260
260
|
*/
|
|
261
261
|
toPublicKey (): PublicKey {
|
|
262
262
|
const c = new Curve()
|
|
263
|
-
const p = c.g.
|
|
263
|
+
const p = c.g.mulCT(this)
|
|
264
264
|
return new PublicKey(p.x, p.y)
|
|
265
265
|
}
|
|
266
266
|
|
|
@@ -352,9 +352,36 @@ export default class PrivateKey extends BigNumber {
|
|
|
352
352
|
if (!key.validate()) {
|
|
353
353
|
throw new Error('Public key not valid for ECDH secret derivation')
|
|
354
354
|
}
|
|
355
|
-
return key.
|
|
355
|
+
return key.mulCT(this)
|
|
356
356
|
}
|
|
357
357
|
|
|
358
|
+
/**
|
|
359
|
+
* SECURITY NOTE – DETERMINISTIC CHILD KEY DERIVATION
|
|
360
|
+
*
|
|
361
|
+
* This method derives child private keys deterministically from the caller’s
|
|
362
|
+
* long-term private key, the counterparty’s public key, and a caller-supplied
|
|
363
|
+
* invoice number using HMAC over an ECDH shared secret (BRC-42 style derivation).
|
|
364
|
+
*
|
|
365
|
+
* This construction does NOT implement a formally authenticated key exchange
|
|
366
|
+
* (AKE) and does NOT provide the following security properties:
|
|
367
|
+
*
|
|
368
|
+
* - Forward secrecy: Compromise of a long-term private key compromises all
|
|
369
|
+
* past and future child keys derived from it.
|
|
370
|
+
* - Replay protection: Child keys are deterministic for a given invoice
|
|
371
|
+
* number and key pair; previously observed messages can be replayed.
|
|
372
|
+
* - Explicit authentication / identity binding: Possession of a public key
|
|
373
|
+
* alone does not guarantee the intended peer identity, enabling potential
|
|
374
|
+
* identity misbinding attacks if higher-level identity verification is absent.
|
|
375
|
+
*
|
|
376
|
+
* This derivation is intended for lightweight, deterministic key hierarchies
|
|
377
|
+
* where both parties already possess and trust each other’s long-term public
|
|
378
|
+
* keys. It SHOULD NOT be used as a drop-in replacement for a standard
|
|
379
|
+
* authenticated key exchange (e.g. X3DH, Noise, or SIGMA) in high-security or
|
|
380
|
+
* high-value contexts.
|
|
381
|
+
*
|
|
382
|
+
* Any future protocol providing forward secrecy, replay protection, or strong
|
|
383
|
+
* peer authentication will require a versioned, breaking change.
|
|
384
|
+
*/
|
|
358
385
|
/**
|
|
359
386
|
* Derives a child key with BRC-42.
|
|
360
387
|
* @param publicKey The public key of the other party
|
|
@@ -598,3 +598,34 @@ describe('AESGCM large input (non-mocked)', () => {
|
|
|
598
598
|
expectUint8ArrayEqual(decryptedBytes, plaintext)
|
|
599
599
|
})
|
|
600
600
|
})
|
|
601
|
+
|
|
602
|
+
describe('multiply reduction edge cases', () => {
|
|
603
|
+
it('applies reduction polynomial when LSB carry is set', () => {
|
|
604
|
+
// Force reduction path by setting v[15] LSB = 1
|
|
605
|
+
const a = new Uint8Array(16)
|
|
606
|
+
a[0] = 0x01
|
|
607
|
+
|
|
608
|
+
const b = new Uint8Array(16)
|
|
609
|
+
b[15] = 0x01
|
|
610
|
+
|
|
611
|
+
const out = multiply(a, b)
|
|
612
|
+
|
|
613
|
+
// We don't assert a magic value — we assert that output is non-zero
|
|
614
|
+
// and stable across runs (reduction happened)
|
|
615
|
+
expect(out.some(v => v !== 0)).toBe(true)
|
|
616
|
+
})
|
|
617
|
+
|
|
618
|
+
it('does not reduce when LSB carry is zero', () => {
|
|
619
|
+
const a = new Uint8Array(16)
|
|
620
|
+
a[0] = 0x01
|
|
621
|
+
|
|
622
|
+
const b = new Uint8Array(16)
|
|
623
|
+
b[15] = 0x00
|
|
624
|
+
|
|
625
|
+
const out = multiply(a, b)
|
|
626
|
+
|
|
627
|
+
expect(out.some(v => v !== 0)).toBe(false)
|
|
628
|
+
})
|
|
629
|
+
})
|
|
630
|
+
|
|
631
|
+
|
|
@@ -117,4 +117,16 @@ describe('ECDSA', () => {
|
|
|
117
117
|
ECDSA.verify(msg, signature, infinityPub)
|
|
118
118
|
).toThrow()
|
|
119
119
|
})
|
|
120
|
+
|
|
121
|
+
it('sign/verify works with large private key (mulCT stress)', () => {
|
|
122
|
+
const bigKey = new BigNumber(
|
|
123
|
+
'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd036413f',
|
|
124
|
+
16
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
const sig = ECDSA.sign(msg, bigKey)
|
|
128
|
+
const pub = curve.g.mul(bigKey)
|
|
129
|
+
|
|
130
|
+
expect(ECDSA.verify(msg, sig, pub)).toBe(true)
|
|
131
|
+
})
|
|
120
132
|
})
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Point from '../../primitives/Point'
|
|
2
|
+
import BigNumber from '../../primitives/BigNumber'
|
|
2
3
|
|
|
3
4
|
describe('Point.fromJSON / fromDER / fromX curve validation (TOB-24)', () => {
|
|
4
5
|
it('rejects clearly off-curve coordinates', () => {
|
|
@@ -50,3 +51,62 @@ describe('Point.fromJSON / fromDER / fromX curve validation (TOB-24)', () => {
|
|
|
50
51
|
expect(() => Point.fromX(badX, true)).toThrow(/Invalid point/)
|
|
51
52
|
})
|
|
52
53
|
})
|
|
54
|
+
|
|
55
|
+
describe('Point.mulCT (constant-time scalar multiplication)', () => {
|
|
56
|
+
const G = Point.fromString(
|
|
57
|
+
'0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
it('returns point at infinity for scalar = 0', () => {
|
|
61
|
+
const r = G.mulCT(0)
|
|
62
|
+
expect(r.isInfinity()).toBe(true)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('matches regular mul for small scalar', () => {
|
|
66
|
+
const k = 5
|
|
67
|
+
const r1 = G.mul(k)
|
|
68
|
+
const r2 = G.mulCT(k)
|
|
69
|
+
|
|
70
|
+
expect(r2.eq(r1)).toBe(true)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('matches regular mul for large scalar', () => {
|
|
74
|
+
const k =
|
|
75
|
+
'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141'
|
|
76
|
+
|
|
77
|
+
const r1 = G.mul(k)
|
|
78
|
+
const r2 = G.mulCT(k)
|
|
79
|
+
|
|
80
|
+
expect(r2.eq(r1)).toBe(true)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('works with non-generator base point', () => {
|
|
84
|
+
const base = G.mul(3)
|
|
85
|
+
const k = 11
|
|
86
|
+
|
|
87
|
+
const r1 = base.mul(k)
|
|
88
|
+
const r2 = base.mulCT(k)
|
|
89
|
+
|
|
90
|
+
expect(r2.eq(r1)).toBe(true)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('handles alternating bit patterns (ctSwap exercised)', () => {
|
|
94
|
+
// 101010... pattern forces both swap paths
|
|
95
|
+
const k = BigInt(
|
|
96
|
+
'0b101010101010101010101010101010101010101010101010101010101010101'
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
const r1 = G.mul(k.toString(10))
|
|
100
|
+
const r2 = G.mulCT(k.toString(10))
|
|
101
|
+
|
|
102
|
+
expect(r2.eq(r1)).toBe(true)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('handles negative scalars correctly', () => {
|
|
106
|
+
const k = new BigNumber('123456', 16)
|
|
107
|
+
const r1 = G.mul(k.neg())
|
|
108
|
+
const r2 = G.mulCT(k.neg())
|
|
109
|
+
expect(r2.eq(r1)).toBe(true)
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
|