@bsv/sdk 1.9.30 → 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.
Files changed (30) hide show
  1. package/dist/cjs/package.json +3 -2
  2. package/dist/cjs/src/messages/EncryptedMessage.js +19 -0
  3. package/dist/cjs/src/messages/EncryptedMessage.js.map +1 -1
  4. package/dist/cjs/src/primitives/AESGCM.js +72 -27
  5. package/dist/cjs/src/primitives/AESGCM.js.map +1 -1
  6. package/dist/cjs/src/primitives/PrivateKey.js +27 -0
  7. package/dist/cjs/src/primitives/PrivateKey.js.map +1 -1
  8. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  9. package/dist/esm/src/messages/EncryptedMessage.js +19 -0
  10. package/dist/esm/src/messages/EncryptedMessage.js.map +1 -1
  11. package/dist/esm/src/primitives/AESGCM.js +71 -26
  12. package/dist/esm/src/primitives/AESGCM.js.map +1 -1
  13. package/dist/esm/src/primitives/PrivateKey.js +27 -0
  14. package/dist/esm/src/primitives/PrivateKey.js.map +1 -1
  15. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  16. package/dist/types/src/messages/EncryptedMessage.d.ts +19 -0
  17. package/dist/types/src/messages/EncryptedMessage.d.ts.map +1 -1
  18. package/dist/types/src/primitives/AESGCM.d.ts +18 -0
  19. package/dist/types/src/primitives/AESGCM.d.ts.map +1 -1
  20. package/dist/types/src/primitives/PrivateKey.d.ts +27 -0
  21. package/dist/types/src/primitives/PrivateKey.d.ts.map +1 -1
  22. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  23. package/dist/umd/bundle.js +3 -3
  24. package/dist/umd/bundle.js.map +1 -1
  25. package/docs/reference/messages.md +24 -0
  26. package/package.json +3 -2
  27. package/src/messages/EncryptedMessage.ts +19 -0
  28. package/src/primitives/AESGCM.ts +75 -34
  29. package/src/primitives/PrivateKey.ts +27 -0
  30. package/src/primitives/__tests/AESGCM.test.ts +31 -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.30",
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,
@@ -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
- mixColumns(state)
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
- if ((block0[i] & (1 << j)) !== 0) {
291
- xorInto(z, v)
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
- if ((v[15] & 1) !== 0) {
295
- rightShift(v)
296
- xorInto(v, R)
297
- } else {
298
- rightShift(v)
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
 
@@ -355,6 +355,33 @@ export default class PrivateKey extends BigNumber {
355
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
+