@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.
- 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/PrivateKey.js +27 -0
- package/dist/cjs/src/primitives/PrivateKey.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/PrivateKey.js +27 -0
- package/dist/esm/src/primitives/PrivateKey.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/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/PrivateKey.ts +27 -0
- 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.
|
|
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
|
|
|
@@ -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
|
+
|