@bcts/spqr 1.0.0-alpha.21
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/LICENSE +661 -0
- package/README.md +11 -0
- package/dist/index.cjs +4321 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +115 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +115 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.iife.js +4318 -0
- package/dist/index.iife.js.map +1 -0
- package/dist/index.mjs +4312 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +74 -0
- package/src/authenticator.ts +163 -0
- package/src/chain.ts +522 -0
- package/src/constants.ts +90 -0
- package/src/encoding/gf.ts +190 -0
- package/src/encoding/index.ts +15 -0
- package/src/encoding/polynomial.ts +657 -0
- package/src/error.ts +75 -0
- package/src/incremental-mlkem768.ts +546 -0
- package/src/index.ts +415 -0
- package/src/kdf.ts +34 -0
- package/src/proto/index.ts +1376 -0
- package/src/proto/pq-ratchet-types.ts +195 -0
- package/src/types.ts +81 -0
- package/src/util.ts +61 -0
- package/src/v1/chunked/index.ts +60 -0
- package/src/v1/chunked/message.ts +257 -0
- package/src/v1/chunked/send-ct.ts +352 -0
- package/src/v1/chunked/send-ek.ts +285 -0
- package/src/v1/chunked/serialize.ts +278 -0
- package/src/v1/chunked/states.ts +399 -0
- package/src/v1/index.ts +9 -0
- package/src/v1/unchunked/index.ts +20 -0
- package/src/v1/unchunked/send-ct.ts +231 -0
- package/src/v1/unchunked/send-ek.ts +177 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,4312 @@
|
|
|
1
|
+
import { hkdf } from "@noble/hashes/hkdf.js";
|
|
2
|
+
import { sha256 } from "@noble/hashes/sha2.js";
|
|
3
|
+
import { hmac } from "@noble/hashes/hmac.js";
|
|
4
|
+
import { sha3_256, sha3_512, shake256 } from "@noble/hashes/sha3.js";
|
|
5
|
+
import { u32 } from "@noble/hashes/utils.js";
|
|
6
|
+
import { XOF128, genCrystals } from "@noble/post-quantum/_crystals.js";
|
|
7
|
+
import { ml_kem768 } from "@noble/post-quantum/ml-kem.js";
|
|
8
|
+
|
|
9
|
+
//#region src/kdf.ts
|
|
10
|
+
/**
|
|
11
|
+
* Copyright © 2025 Signal Messenger, LLC
|
|
12
|
+
* Copyright © 2026 Parity Technologies
|
|
13
|
+
*
|
|
14
|
+
* KDF wrappers for SPQR (HKDF-SHA256 and HMAC-SHA256).
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* HKDF-SHA256 key derivation.
|
|
18
|
+
* @param ikm Input key material
|
|
19
|
+
* @param salt Salt (use ZERO_SALT for empty)
|
|
20
|
+
* @param info Context info string or bytes
|
|
21
|
+
* @param length Output length in bytes
|
|
22
|
+
*/
|
|
23
|
+
function hkdfSha256(ikm, salt, info, length) {
|
|
24
|
+
return hkdf(sha256, ikm, salt, typeof info === "string" ? new TextEncoder().encode(info) : info, length);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* HMAC-SHA256 computation.
|
|
28
|
+
*/
|
|
29
|
+
function hmacSha256(key, data) {
|
|
30
|
+
return hmac(sha256, key, data);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
//#endregion
|
|
34
|
+
//#region src/util.ts
|
|
35
|
+
/**
|
|
36
|
+
* Copyright © 2025 Signal Messenger, LLC
|
|
37
|
+
* Copyright © 2026 Parity Technologies
|
|
38
|
+
*
|
|
39
|
+
* Low-level utility functions for SPQR.
|
|
40
|
+
*/
|
|
41
|
+
/**
|
|
42
|
+
* Constant-time comparison of two byte arrays.
|
|
43
|
+
* Returns true if they are equal, false otherwise.
|
|
44
|
+
*
|
|
45
|
+
* Uses a XOR accumulator to avoid early-exit timing leaks.
|
|
46
|
+
*
|
|
47
|
+
* Limitation: Unlike Rust (#[inline(never)] + black_box), JavaScript
|
|
48
|
+
* cannot fully prevent JIT optimizations. In Node.js environments,
|
|
49
|
+
* callers requiring stronger guarantees should use
|
|
50
|
+
* crypto.timingSafeEqual directly.
|
|
51
|
+
*/
|
|
52
|
+
function constantTimeEqual(a, b) {
|
|
53
|
+
if (a.length !== b.length) return false;
|
|
54
|
+
let diff = 0;
|
|
55
|
+
for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i];
|
|
56
|
+
return diff === 0;
|
|
57
|
+
}
|
|
58
|
+
/** Convert a bigint to 8-byte big-endian Uint8Array */
|
|
59
|
+
function bigintToBE8(value) {
|
|
60
|
+
const buf = new Uint8Array(8);
|
|
61
|
+
new DataView(buf.buffer).setBigUint64(0, value, false);
|
|
62
|
+
return buf;
|
|
63
|
+
}
|
|
64
|
+
/** Convert a number to 4-byte big-endian Uint8Array */
|
|
65
|
+
function uint32ToBE4(value) {
|
|
66
|
+
const buf = new Uint8Array(4);
|
|
67
|
+
new DataView(buf.buffer).setUint32(0, value, false);
|
|
68
|
+
return buf;
|
|
69
|
+
}
|
|
70
|
+
/** Concatenate multiple Uint8Arrays */
|
|
71
|
+
function concat(...arrays) {
|
|
72
|
+
let total = 0;
|
|
73
|
+
for (const a of arrays) total += a.length;
|
|
74
|
+
const result = new Uint8Array(total);
|
|
75
|
+
let offset = 0;
|
|
76
|
+
for (const a of arrays) {
|
|
77
|
+
result.set(a, offset);
|
|
78
|
+
offset += a.length;
|
|
79
|
+
}
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
//#endregion
|
|
84
|
+
//#region src/constants.ts
|
|
85
|
+
/** HMAC-SHA256 MAC size */
|
|
86
|
+
const MAC_SIZE = 32;
|
|
87
|
+
/** Default max key index jump */
|
|
88
|
+
const DEFAULT_MAX_JUMP = 25e3;
|
|
89
|
+
/** Default max out-of-order keys stored */
|
|
90
|
+
const DEFAULT_MAX_OOO_KEYS = 2e3;
|
|
91
|
+
/** Number of epochs to keep prior to the current send epoch */
|
|
92
|
+
const EPOCHS_TO_KEEP_PRIOR_TO_SEND_EPOCH = 1;
|
|
93
|
+
/** Size of a key entry in KeyHistory: 4 bytes (index BE32) + 32 bytes (key) */
|
|
94
|
+
const KEY_ENTRY_SIZE = 36;
|
|
95
|
+
/** Chain initialization label (NOTE: two spaces before "Start") */
|
|
96
|
+
const LABEL_CHAIN_START = "Signal PQ Ratchet V1 Chain Start";
|
|
97
|
+
/** Chain epoch advancement label */
|
|
98
|
+
const LABEL_CHAIN_ADD_EPOCH = "Signal PQ Ratchet V1 Chain Add Epoch";
|
|
99
|
+
/** Per-message key derivation label */
|
|
100
|
+
const LABEL_CHAIN_NEXT = "Signal PQ Ratchet V1 Chain Next";
|
|
101
|
+
/** Authenticator key ratchet label */
|
|
102
|
+
const LABEL_AUTH_UPDATE = "Signal_PQCKA_V1_MLKEM768:Authenticator Update";
|
|
103
|
+
/** Ciphertext MAC label */
|
|
104
|
+
const LABEL_CT_MAC = "Signal_PQCKA_V1_MLKEM768:ciphertext";
|
|
105
|
+
/** Header MAC label */
|
|
106
|
+
const LABEL_HDR_MAC = "Signal_PQCKA_V1_MLKEM768:ekheader";
|
|
107
|
+
/** Epoch secret derivation label */
|
|
108
|
+
const LABEL_SCKA_KEY = "Signal_PQCKA_V1_MLKEM768:SCKA Key";
|
|
109
|
+
/** 32-byte zero salt used in HKDF */
|
|
110
|
+
const ZERO_SALT = new Uint8Array(32);
|
|
111
|
+
|
|
112
|
+
//#endregion
|
|
113
|
+
//#region src/incremental-mlkem768.ts
|
|
114
|
+
/**
|
|
115
|
+
* Copyright © 2025 Signal Messenger, LLC
|
|
116
|
+
* Copyright © 2026 Parity Technologies
|
|
117
|
+
*
|
|
118
|
+
* True incremental ML-KEM-768 implementation.
|
|
119
|
+
*
|
|
120
|
+
* Implements the libcrux-compatible incremental encapsulation split:
|
|
121
|
+
*
|
|
122
|
+
* - generate(): Splits the ML-KEM-768 public key into:
|
|
123
|
+
* hdr (pk1, 64 bytes) = rho(32) + H(ek)(32) -- where H = SHA3-256
|
|
124
|
+
* ek (pk2, 1152 bytes) = ByteEncode12(tHat) -- the NTT vector
|
|
125
|
+
*
|
|
126
|
+
* - encaps1(hdr, rng): Uses only rho and H(ek) from the header to produce
|
|
127
|
+
* a REAL ct1 (960 bytes), shared secret (32 bytes), and
|
|
128
|
+
* encapsulation state (2080 bytes) for encaps2.
|
|
129
|
+
*
|
|
130
|
+
* - encaps2(ek, es): Completes the encapsulation using the tHat from pk2
|
|
131
|
+
* and the stored NTT randomness. Returns ct2 (128 bytes).
|
|
132
|
+
*
|
|
133
|
+
* - decaps(dk, ct1, ct2): Standard ML-KEM-768 decapsulation.
|
|
134
|
+
*
|
|
135
|
+
* Wire-compatible with Signal's Rust libcrux incremental implementation.
|
|
136
|
+
*/
|
|
137
|
+
const N = 256;
|
|
138
|
+
const Q = 3329;
|
|
139
|
+
const F = 3303;
|
|
140
|
+
const ROOT_OF_UNITY = 17;
|
|
141
|
+
const K = 3;
|
|
142
|
+
const ETA1 = 2;
|
|
143
|
+
const ETA2 = 2;
|
|
144
|
+
const DU = 10;
|
|
145
|
+
const DV = 4;
|
|
146
|
+
/** Size of the public key header: rho(32) + H(ek)(32) */
|
|
147
|
+
const HEADER_SIZE = 64;
|
|
148
|
+
/** Size of the encapsulation key: ByteEncode12(tHat) = 3 * 384 = 1152 bytes */
|
|
149
|
+
const EK_SIZE = 1152;
|
|
150
|
+
/** Size of the first ciphertext fragment (ct[0..960]) */
|
|
151
|
+
const CT1_SIZE = 960;
|
|
152
|
+
/** Size of the second ciphertext fragment (ct[960..1088]) */
|
|
153
|
+
const CT2_SIZE = 128;
|
|
154
|
+
/** Standard ML-KEM-768 full public key size */
|
|
155
|
+
const FULL_PK_SIZE = 1184;
|
|
156
|
+
/** Standard ML-KEM-768 full ciphertext size */
|
|
157
|
+
const FULL_CT_SIZE = 1088;
|
|
158
|
+
/** Size of the keygen seed */
|
|
159
|
+
const KEYGEN_SEED_SIZE = 64;
|
|
160
|
+
/** Size of the encapsulation randomness (message m) */
|
|
161
|
+
const ENCAPS_SEED_SIZE = 32;
|
|
162
|
+
/**
|
|
163
|
+
* Size of the encapsulation state:
|
|
164
|
+
* r_as_ntt: K(3) * N(256) * 2 = 1536 bytes
|
|
165
|
+
* error2: N(256) * 2 = 512 bytes
|
|
166
|
+
* randomness: 32 bytes
|
|
167
|
+
* Total: 2080 bytes
|
|
168
|
+
*/
|
|
169
|
+
const ES_SIZE = 2080;
|
|
170
|
+
const { mod, nttZetas, NTT, bitsCoder } = genCrystals({
|
|
171
|
+
N,
|
|
172
|
+
Q,
|
|
173
|
+
F,
|
|
174
|
+
ROOT_OF_UNITY,
|
|
175
|
+
newPoly: (n) => new Uint16Array(n),
|
|
176
|
+
brvBits: 7,
|
|
177
|
+
isKyber: true
|
|
178
|
+
});
|
|
179
|
+
function polyAdd(a, b) {
|
|
180
|
+
for (let i = 0; i < N; i++) a[i] = mod(a[i] + b[i]);
|
|
181
|
+
}
|
|
182
|
+
function BaseCaseMultiply(a0, a1, b0, b1, zeta) {
|
|
183
|
+
return {
|
|
184
|
+
c0: mod(a1 * b1 * zeta + a0 * b0),
|
|
185
|
+
c1: mod(a0 * b1 + a1 * b0)
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
function MultiplyNTTs(f, g) {
|
|
189
|
+
for (let i = 0; i < N / 2; i++) {
|
|
190
|
+
let z = nttZetas[64 + (i >> 1)];
|
|
191
|
+
if ((i & 1) !== 0) z = -z;
|
|
192
|
+
const { c0, c1 } = BaseCaseMultiply(f[2 * i + 0], f[2 * i + 1], g[2 * i + 0], g[2 * i + 1], z);
|
|
193
|
+
f[2 * i + 0] = c0;
|
|
194
|
+
f[2 * i + 1] = c1;
|
|
195
|
+
}
|
|
196
|
+
return f;
|
|
197
|
+
}
|
|
198
|
+
function SampleNTT(xof) {
|
|
199
|
+
const r = new Uint16Array(N);
|
|
200
|
+
for (let j = 0; j < N;) {
|
|
201
|
+
const b = xof();
|
|
202
|
+
if (b.length % 3 !== 0) throw new Error("SampleNTT: unaligned block");
|
|
203
|
+
for (let i = 0; j < N && i + 3 <= b.length; i += 3) {
|
|
204
|
+
const d1 = (b[i + 0] >> 0 | b[i + 1] << 8) & 4095;
|
|
205
|
+
const d2 = (b[i + 1] >> 4 | b[i + 2] << 4) & 4095;
|
|
206
|
+
if (d1 < Q) r[j++] = d1;
|
|
207
|
+
if (j < N && d2 < Q) r[j++] = d2;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return r;
|
|
211
|
+
}
|
|
212
|
+
function sampleCBD(seed, nonce, eta) {
|
|
213
|
+
const len = eta * N / 4;
|
|
214
|
+
const buf = shake256.create({ dkLen: len }).update(seed).update(new Uint8Array([nonce])).digest();
|
|
215
|
+
const r = new Uint16Array(N);
|
|
216
|
+
const b32 = u32(buf);
|
|
217
|
+
let bitLen = 0;
|
|
218
|
+
let p = 0;
|
|
219
|
+
let bb = 0;
|
|
220
|
+
let t0 = 0;
|
|
221
|
+
for (let b of b32) for (let j = 0; j < 32; j++) {
|
|
222
|
+
bb += b & 1;
|
|
223
|
+
b >>= 1;
|
|
224
|
+
bitLen += 1;
|
|
225
|
+
if (bitLen === eta) {
|
|
226
|
+
t0 = bb;
|
|
227
|
+
bb = 0;
|
|
228
|
+
} else if (bitLen === 2 * eta) {
|
|
229
|
+
r[p++] = mod(t0 - bb);
|
|
230
|
+
bb = 0;
|
|
231
|
+
bitLen = 0;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (bitLen !== 0) throw new Error(`sampleCBD: leftover bits: ${bitLen}`);
|
|
235
|
+
return r;
|
|
236
|
+
}
|
|
237
|
+
const compress = (d) => {
|
|
238
|
+
if (d >= 12) return {
|
|
239
|
+
encode: (i) => i,
|
|
240
|
+
decode: (i) => i
|
|
241
|
+
};
|
|
242
|
+
const a = 2 ** (d - 1);
|
|
243
|
+
return {
|
|
244
|
+
encode: (i) => ((i << d) + Q / 2) / Q,
|
|
245
|
+
decode: (i) => i * Q + a >>> d
|
|
246
|
+
};
|
|
247
|
+
};
|
|
248
|
+
const polyCoder = (d) => bitsCoder(d, compress(d));
|
|
249
|
+
const poly12 = polyCoder(12);
|
|
250
|
+
const polyDU = polyCoder(DU);
|
|
251
|
+
const polyDV = polyCoder(DV);
|
|
252
|
+
const poly1 = polyCoder(1);
|
|
253
|
+
/**
|
|
254
|
+
* Encode the encapsulation state as 2080 bytes:
|
|
255
|
+
* r_as_ntt: K polys, each 256 coefficients as uint16 LE = 1536 bytes
|
|
256
|
+
* error2: 1 poly, 256 coefficients as uint16 LE = 512 bytes
|
|
257
|
+
* randomness: 32 bytes (the message m)
|
|
258
|
+
*/
|
|
259
|
+
function encodeState(rHat, e2, m) {
|
|
260
|
+
const state = new Uint8Array(ES_SIZE);
|
|
261
|
+
let offset = 0;
|
|
262
|
+
for (let k = 0; k < K; k++) {
|
|
263
|
+
const poly = rHat[k];
|
|
264
|
+
for (let i = 0; i < N; i++) {
|
|
265
|
+
const val = poly[i];
|
|
266
|
+
state[offset++] = val & 255;
|
|
267
|
+
state[offset++] = val >> 8 & 255;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
for (let i = 0; i < N; i++) {
|
|
271
|
+
const val = e2[i];
|
|
272
|
+
state[offset++] = val & 255;
|
|
273
|
+
state[offset++] = val >> 8 & 255;
|
|
274
|
+
}
|
|
275
|
+
state.set(m, offset);
|
|
276
|
+
return state;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Decode the encapsulation state from 2080 bytes.
|
|
280
|
+
* Includes the Issue 1275 endianness workaround.
|
|
281
|
+
*/
|
|
282
|
+
function decodeState(state) {
|
|
283
|
+
const st = fixIssue1275(state) ?? state;
|
|
284
|
+
let offset = 0;
|
|
285
|
+
const rHat = [];
|
|
286
|
+
for (let k = 0; k < K; k++) {
|
|
287
|
+
const poly = new Uint16Array(N);
|
|
288
|
+
for (let i = 0; i < N; i++) {
|
|
289
|
+
poly[i] = st[offset] | st[offset + 1] << 8;
|
|
290
|
+
offset += 2;
|
|
291
|
+
}
|
|
292
|
+
rHat.push(poly);
|
|
293
|
+
}
|
|
294
|
+
const e2 = new Uint16Array(N);
|
|
295
|
+
for (let i = 0; i < N; i++) {
|
|
296
|
+
e2[i] = st[offset] | st[offset + 1] << 8;
|
|
297
|
+
offset += 2;
|
|
298
|
+
}
|
|
299
|
+
return {
|
|
300
|
+
rHat,
|
|
301
|
+
e2,
|
|
302
|
+
m: st.slice(offset, offset + 32)
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Port of Rust's potentially_fix_state_incorrectly_encoded_by_libcrux_issue_1275.
|
|
307
|
+
*
|
|
308
|
+
* Due to https://github.com/cryspen/libcrux/issues/1275, the encapsulation
|
|
309
|
+
* state may contain error2 coefficients with wrong endianness.
|
|
310
|
+
*
|
|
311
|
+
* Error2 values should be in [-2, 2] (ETA2=2 for ML-KEM-768).
|
|
312
|
+
* As uint16 LE, valid values are: 0x0000, 0x0001, 0x0002, 0xFFFF (-1), 0xFFFE (-2).
|
|
313
|
+
* Bad-endian equivalents: 0x0100, 0x0200, 0xFEFF.
|
|
314
|
+
*
|
|
315
|
+
* Returns a fixed copy if endianness is wrong, or null if state is OK.
|
|
316
|
+
*/
|
|
317
|
+
function fixIssue1275(es) {
|
|
318
|
+
const E2_START = K * N * 2;
|
|
319
|
+
const E2_END = E2_START + N * 2;
|
|
320
|
+
for (let i = E2_START; i < E2_END; i += 2) {
|
|
321
|
+
const val = es[i] | es[i + 1] << 8;
|
|
322
|
+
if (val === 0 || val === 65535) continue;
|
|
323
|
+
if (val === 1 || val === 2 || val === 65534) return null;
|
|
324
|
+
if (val === 256 || val === 512 || val === 65279) return flipEndianness(es);
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Flip the endianness of all i16 values in the encapsulation state.
|
|
331
|
+
* The last 32 bytes (randomness) are NOT flipped.
|
|
332
|
+
*/
|
|
333
|
+
function flipEndianness(es) {
|
|
334
|
+
const fixed = new Uint8Array(es);
|
|
335
|
+
const coeffEnd = es.length - 32;
|
|
336
|
+
for (let i = 0; i < coeffEnd; i += 2) {
|
|
337
|
+
const tmp = fixed[i];
|
|
338
|
+
fixed[i] = fixed[i + 1];
|
|
339
|
+
fixed[i + 1] = tmp;
|
|
340
|
+
}
|
|
341
|
+
return fixed;
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Generate an ML-KEM-768 keypair and split the public key.
|
|
345
|
+
*
|
|
346
|
+
* The ML-KEM-768 public key (1184 bytes) is:
|
|
347
|
+
* tHat = pk[0..1152] (ByteEncode12 of NTT vector)
|
|
348
|
+
* rho = pk[1152..1184] (32-byte seed)
|
|
349
|
+
*
|
|
350
|
+
* The incremental split produces:
|
|
351
|
+
* hdr (pk1) = rho(32) + SHA3-256(pk)(32) = 64 bytes
|
|
352
|
+
* ek (pk2) = tHat(1152) = ByteEncode12(tHat) = 1152 bytes
|
|
353
|
+
*
|
|
354
|
+
* @param rng - Random byte generator; must provide at least 64 bytes
|
|
355
|
+
* @returns Split key material
|
|
356
|
+
*/
|
|
357
|
+
function generate(rng) {
|
|
358
|
+
const seed = rng(KEYGEN_SEED_SIZE);
|
|
359
|
+
const { publicKey, secretKey } = ml_kem768.keygen(seed);
|
|
360
|
+
const tHat = publicKey.slice(0, EK_SIZE);
|
|
361
|
+
const rho = publicKey.slice(EK_SIZE);
|
|
362
|
+
const hEk = sha3_256(publicKey);
|
|
363
|
+
const hdr = new Uint8Array(HEADER_SIZE);
|
|
364
|
+
hdr.set(rho, 0);
|
|
365
|
+
hdr.set(hEk, 32);
|
|
366
|
+
return {
|
|
367
|
+
hdr,
|
|
368
|
+
ek: tHat,
|
|
369
|
+
dk: secretKey
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Phase 1 of true incremental encapsulation.
|
|
374
|
+
*
|
|
375
|
+
* Uses only rho and H(ek) from the 64-byte header to produce:
|
|
376
|
+
* - REAL ct1 (960 bytes): compress_du(u) where u = NTT^-1(A^T * rHat) + e1
|
|
377
|
+
* - REAL shared secret: K-hat from SHA3-512(m || H(ek))
|
|
378
|
+
* - Encapsulation state (2080 bytes): r_as_ntt + error2 + m
|
|
379
|
+
*
|
|
380
|
+
* @param hdr - The 64-byte header: rho(32) + H(ek)(32)
|
|
381
|
+
* @param rng - Random byte generator
|
|
382
|
+
* @returns Real ct1, encapsulation state, and real shared secret
|
|
383
|
+
*/
|
|
384
|
+
function encaps1(hdr, rng) {
|
|
385
|
+
const rho = hdr.slice(0, 32);
|
|
386
|
+
const hEk = hdr.slice(32, 64);
|
|
387
|
+
const m = rng(ENCAPS_SEED_SIZE);
|
|
388
|
+
const kr = sha3_512.create().update(m).update(hEk).digest();
|
|
389
|
+
const kHat = kr.slice(0, 32);
|
|
390
|
+
const r = kr.slice(32, 64);
|
|
391
|
+
const rHat = [];
|
|
392
|
+
for (let i = 0; i < K; i++) rHat.push(NTT.encode(sampleCBD(r, i, ETA1)));
|
|
393
|
+
const x = XOF128(rho);
|
|
394
|
+
const u = [];
|
|
395
|
+
for (let i = 0; i < K; i++) {
|
|
396
|
+
const e1 = sampleCBD(r, K + i, ETA2);
|
|
397
|
+
const tmp = new Uint16Array(N);
|
|
398
|
+
for (let j = 0; j < K; j++) polyAdd(tmp, MultiplyNTTs(SampleNTT(x.get(i, j)), rHat[j].slice()));
|
|
399
|
+
polyAdd(e1, NTT.decode(tmp));
|
|
400
|
+
u.push(e1);
|
|
401
|
+
}
|
|
402
|
+
x.clean();
|
|
403
|
+
const ct1 = new Uint8Array(CT1_SIZE);
|
|
404
|
+
for (let i = 0; i < K; i++) {
|
|
405
|
+
const encoded = polyDU.encode(u[i]);
|
|
406
|
+
ct1.set(encoded, i * polyDU.bytesLen);
|
|
407
|
+
}
|
|
408
|
+
return {
|
|
409
|
+
ct1,
|
|
410
|
+
es: encodeState(rHat, sampleCBD(r, 2 * K, ETA2), m),
|
|
411
|
+
sharedSecret: kHat
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Phase 2 of true incremental encapsulation.
|
|
416
|
+
*
|
|
417
|
+
* Uses the tHat from pk2 (ek) and the stored NTT randomness to compute ct2.
|
|
418
|
+
*
|
|
419
|
+
* @param ek - The 1152-byte encapsulation key (ByteEncode12(tHat))
|
|
420
|
+
* @param es - The 2080-byte encapsulation state from encaps1
|
|
421
|
+
* @returns ct2 (128 bytes) ONLY
|
|
422
|
+
*/
|
|
423
|
+
function encaps2(ek, es) {
|
|
424
|
+
const { rHat, e2, m } = decodeState(es);
|
|
425
|
+
const tHat = [];
|
|
426
|
+
for (let i = 0; i < K; i++) {
|
|
427
|
+
const slice = ek.subarray(i * poly12.bytesLen, (i + 1) * poly12.bytesLen);
|
|
428
|
+
tHat.push(poly12.decode(slice));
|
|
429
|
+
}
|
|
430
|
+
const tmp = new Uint16Array(N);
|
|
431
|
+
for (let i = 0; i < K; i++) polyAdd(tmp, MultiplyNTTs(tHat[i].slice(), rHat[i].slice()));
|
|
432
|
+
const v = NTT.decode(tmp);
|
|
433
|
+
polyAdd(v, e2);
|
|
434
|
+
polyAdd(v, poly1.decode(m));
|
|
435
|
+
return polyDV.encode(v);
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Decapsulate a split ciphertext using the decapsulation key.
|
|
439
|
+
*
|
|
440
|
+
* Concatenates ct1 (960 bytes) and ct2 (128 bytes) into a standard
|
|
441
|
+
* 1088-byte ML-KEM-768 ciphertext, then performs standard decapsulation.
|
|
442
|
+
*
|
|
443
|
+
* @param dk - The 2400-byte decapsulation (secret) key
|
|
444
|
+
* @param ct1 - First ciphertext fragment (960 bytes)
|
|
445
|
+
* @param ct2 - Second ciphertext fragment (128 bytes)
|
|
446
|
+
* @returns The 32-byte shared secret
|
|
447
|
+
*/
|
|
448
|
+
function decaps(dk, ct1, ct2) {
|
|
449
|
+
const ct = new Uint8Array(FULL_CT_SIZE);
|
|
450
|
+
ct.set(ct1.subarray(0, CT1_SIZE), 0);
|
|
451
|
+
ct.set(ct2.subarray(0, CT2_SIZE), CT1_SIZE);
|
|
452
|
+
return ml_kem768.decapsulate(ct, dk);
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Check whether an encapsulation key is consistent with a header.
|
|
456
|
+
*
|
|
457
|
+
* Reconstructs the full public key from pk2 (tHat) and pk1[0..32] (rho),
|
|
458
|
+
* computes SHA3-256 of the full pk, and compares with H(ek) in the header.
|
|
459
|
+
*
|
|
460
|
+
* @param ek - Encapsulation key (1152 bytes = ByteEncode12(tHat))
|
|
461
|
+
* @param hdr - Header (64 bytes = rho(32) + H(ek)(32))
|
|
462
|
+
* @returns true if ek is consistent with the header
|
|
463
|
+
*/
|
|
464
|
+
function ekMatchesHeader(ek, hdr) {
|
|
465
|
+
if (hdr.length < HEADER_SIZE || ek.length < EK_SIZE) return false;
|
|
466
|
+
const rho = hdr.slice(0, 32);
|
|
467
|
+
const expectedHash = hdr.slice(32, 64);
|
|
468
|
+
const pk = new Uint8Array(FULL_PK_SIZE);
|
|
469
|
+
pk.set(ek.subarray(0, EK_SIZE), 0);
|
|
470
|
+
pk.set(rho, EK_SIZE);
|
|
471
|
+
const actualHash = sha3_256(pk);
|
|
472
|
+
if (actualHash.length !== expectedHash.length) return false;
|
|
473
|
+
let diff = 0;
|
|
474
|
+
for (let i = 0; i < actualHash.length; i++) diff |= actualHash[i] ^ expectedHash[i];
|
|
475
|
+
return diff === 0;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
//#endregion
|
|
479
|
+
//#region src/error.ts
|
|
480
|
+
/**
|
|
481
|
+
* Copyright © 2025 Signal Messenger, LLC
|
|
482
|
+
* Copyright © 2026 Parity Technologies
|
|
483
|
+
*
|
|
484
|
+
* Error types for the SPQR protocol.
|
|
485
|
+
*/
|
|
486
|
+
var SpqrError = class extends Error {
|
|
487
|
+
constructor(message, code, detail) {
|
|
488
|
+
super(message);
|
|
489
|
+
this.code = code;
|
|
490
|
+
this.detail = detail;
|
|
491
|
+
this.name = "SpqrError";
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
let SpqrErrorCode = /* @__PURE__ */ function(SpqrErrorCode) {
|
|
495
|
+
SpqrErrorCode["StateDecode"] = "STATE_DECODE";
|
|
496
|
+
SpqrErrorCode["NotImplemented"] = "NOT_IMPLEMENTED";
|
|
497
|
+
SpqrErrorCode["MsgDecode"] = "MSG_DECODE";
|
|
498
|
+
SpqrErrorCode["MacVerifyFailed"] = "MAC_VERIFY_FAILED";
|
|
499
|
+
SpqrErrorCode["EpochOutOfRange"] = "EPOCH_OUT_OF_RANGE";
|
|
500
|
+
SpqrErrorCode["EncodingDecoding"] = "ENCODING_DECODING";
|
|
501
|
+
SpqrErrorCode["Serialization"] = "SERIALIZATION";
|
|
502
|
+
SpqrErrorCode["VersionMismatch"] = "VERSION_MISMATCH";
|
|
503
|
+
SpqrErrorCode["MinimumVersion"] = "MINIMUM_VERSION";
|
|
504
|
+
SpqrErrorCode["KeyJump"] = "KEY_JUMP";
|
|
505
|
+
SpqrErrorCode["KeyTrimmed"] = "KEY_TRIMMED";
|
|
506
|
+
SpqrErrorCode["KeyAlreadyRequested"] = "KEY_ALREADY_REQUESTED";
|
|
507
|
+
SpqrErrorCode["ErroneousDataReceived"] = "ERRONEOUS_DATA_RECEIVED";
|
|
508
|
+
SpqrErrorCode["SendKeyEpochDecreased"] = "SEND_KEY_EPOCH_DECREASED";
|
|
509
|
+
SpqrErrorCode["InvalidParams"] = "INVALID_PARAMS";
|
|
510
|
+
SpqrErrorCode["ChainNotAvailable"] = "CHAIN_NOT_AVAILABLE";
|
|
511
|
+
return SpqrErrorCode;
|
|
512
|
+
}({});
|
|
513
|
+
var AuthenticatorError = class extends Error {
|
|
514
|
+
constructor(message, code) {
|
|
515
|
+
super(message);
|
|
516
|
+
this.code = code;
|
|
517
|
+
this.name = "AuthenticatorError";
|
|
518
|
+
}
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
//#endregion
|
|
522
|
+
//#region src/v1/unchunked/send-ct.ts
|
|
523
|
+
const SCKA_KEY_LABEL$1 = new TextEncoder().encode(LABEL_SCKA_KEY);
|
|
524
|
+
/**
|
|
525
|
+
* Derive the epoch secret from the KEM shared secret.
|
|
526
|
+
*
|
|
527
|
+
* HKDF-SHA256(ikm=sharedSecret, salt=ZERO_SALT,
|
|
528
|
+
* info=LABEL_SCKA_KEY || epoch_be8, length=32)
|
|
529
|
+
*
|
|
530
|
+
* Matches Rust: info = [b"Signal_PQCKA_V1_MLKEM768:SCKA Key", epoch.to_be_bytes()].concat()
|
|
531
|
+
*/
|
|
532
|
+
function deriveEpochSecret$1(epoch, sharedSecret) {
|
|
533
|
+
return {
|
|
534
|
+
epoch,
|
|
535
|
+
secret: hkdfSha256(sharedSecret, ZERO_SALT, concat(SCKA_KEY_LABEL$1, bigintToBE8(epoch)), 32)
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Waiting to receive the header from the send_ek peer.
|
|
540
|
+
*/
|
|
541
|
+
var NoHeaderReceived$1 = class {
|
|
542
|
+
constructor(epoch, auth) {
|
|
543
|
+
this.epoch = epoch;
|
|
544
|
+
this.auth = auth;
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Receive the header and verify its MAC.
|
|
548
|
+
*
|
|
549
|
+
* @param epoch - The epoch for this exchange (must match current epoch)
|
|
550
|
+
* @param hdr - The 64-byte public key header
|
|
551
|
+
* @param mac - The 32-byte HMAC-SHA256 MAC over the header
|
|
552
|
+
* @returns Next state
|
|
553
|
+
* @throws {AuthenticatorError} If the header MAC is invalid
|
|
554
|
+
*/
|
|
555
|
+
recvHeader(epoch, hdr, mac) {
|
|
556
|
+
this.auth.verifyHdr(epoch, hdr, mac);
|
|
557
|
+
return new HeaderReceived$1(this.epoch, this.auth, hdr);
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
/**
|
|
561
|
+
* The header has been received and verified. Ready to produce ct1.
|
|
562
|
+
*
|
|
563
|
+
* In the true incremental ML-KEM approach, sendCt1 performs encaps1
|
|
564
|
+
* using only the header (rho + H(ek)), producing REAL ct1 and shared
|
|
565
|
+
* secret. The epoch secret is derived here.
|
|
566
|
+
*/
|
|
567
|
+
var HeaderReceived$1 = class {
|
|
568
|
+
constructor(epoch, auth, hdr) {
|
|
569
|
+
this.epoch = epoch;
|
|
570
|
+
this.auth = auth;
|
|
571
|
+
this.hdr = hdr;
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Generate encapsulation randomness and produce REAL ct1.
|
|
575
|
+
*
|
|
576
|
+
* Performs encaps1 using the header to produce:
|
|
577
|
+
* - Real ct1 (960 bytes)
|
|
578
|
+
* - Real shared secret -> epoch secret
|
|
579
|
+
* - Encapsulation state for later encaps2
|
|
580
|
+
*
|
|
581
|
+
* The authenticator is updated with the derived epoch secret.
|
|
582
|
+
*
|
|
583
|
+
* @param rng - Random byte generator
|
|
584
|
+
* @returns [nextState, real_ct1, epochSecret]
|
|
585
|
+
*/
|
|
586
|
+
sendCt1(rng) {
|
|
587
|
+
const { ct1, es, sharedSecret } = encaps1(this.hdr, rng);
|
|
588
|
+
const epochSecret = deriveEpochSecret$1(this.epoch, sharedSecret);
|
|
589
|
+
const auth = this.auth.clone();
|
|
590
|
+
auth.update(this.epoch, epochSecret.secret);
|
|
591
|
+
return [
|
|
592
|
+
new Ct1Sent(this.epoch, auth, this.hdr, es, ct1),
|
|
593
|
+
ct1,
|
|
594
|
+
epochSecret
|
|
595
|
+
];
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
/**
|
|
599
|
+
* Real ct1 has been produced. Waiting for the encapsulation key.
|
|
600
|
+
*
|
|
601
|
+
* Stores hdr, es(2080), and ct1(960) for the encaps2 phase.
|
|
602
|
+
*/
|
|
603
|
+
var Ct1Sent = class {
|
|
604
|
+
constructor(epoch, auth, hdr, es, ct1) {
|
|
605
|
+
this.epoch = epoch;
|
|
606
|
+
this.auth = auth;
|
|
607
|
+
this.hdr = hdr;
|
|
608
|
+
this.es = es;
|
|
609
|
+
this.ct1 = ct1;
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Receive the encapsulation key and validate it against the header.
|
|
613
|
+
*
|
|
614
|
+
* In the true incremental approach, this simply validates and stores
|
|
615
|
+
* the ek for later use in sendCt2. No encapsulation happens here.
|
|
616
|
+
*
|
|
617
|
+
* @param ek - The 1152-byte encapsulation key from the send_ek peer
|
|
618
|
+
* @returns Next state
|
|
619
|
+
* @throws {SpqrError} If the ek does not match the header
|
|
620
|
+
*/
|
|
621
|
+
recvEk(ek) {
|
|
622
|
+
if (!ekMatchesHeader(ek, this.hdr)) throw new SpqrError("Encapsulation key does not match header", SpqrErrorCode.ErroneousDataReceived);
|
|
623
|
+
return new Ct1SentEkReceived(this.epoch, this.auth, this.es, ek, this.ct1);
|
|
624
|
+
}
|
|
625
|
+
};
|
|
626
|
+
/**
|
|
627
|
+
* The encapsulation key has been received and validated.
|
|
628
|
+
* Ready to send ct2.
|
|
629
|
+
*
|
|
630
|
+
* Stores es(2080), ek(1152), and ct1(960) for encaps2 + MAC.
|
|
631
|
+
*/
|
|
632
|
+
var Ct1SentEkReceived = class {
|
|
633
|
+
constructor(epoch, auth, es, ek, ct1) {
|
|
634
|
+
this.epoch = epoch;
|
|
635
|
+
this.auth = auth;
|
|
636
|
+
this.es = es;
|
|
637
|
+
this.ek = ek;
|
|
638
|
+
this.ct1 = ct1;
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Produce ct2 by calling encaps2, then MAC over ct1 || ct2.
|
|
642
|
+
*
|
|
643
|
+
* @returns Result with next state, ct2, and MAC
|
|
644
|
+
*/
|
|
645
|
+
sendCt2() {
|
|
646
|
+
const ct2 = encaps2(this.ek, this.es);
|
|
647
|
+
const fullCt = concat(this.ct1, ct2);
|
|
648
|
+
const mac = this.auth.macCt(this.epoch, fullCt);
|
|
649
|
+
return {
|
|
650
|
+
state: new Ct2Sent(this.epoch, this.auth),
|
|
651
|
+
ct2,
|
|
652
|
+
mac
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
};
|
|
656
|
+
/**
|
|
657
|
+
* Terminal state for this epoch's send_ct exchange.
|
|
658
|
+
*
|
|
659
|
+
* The caller is responsible for creating the next epoch's
|
|
660
|
+
* send_ek::KeysUnsampled state from the epoch and auth.
|
|
661
|
+
*/
|
|
662
|
+
var Ct2Sent = class {
|
|
663
|
+
constructor(epoch, auth) {
|
|
664
|
+
this.epoch = epoch;
|
|
665
|
+
this.auth = auth;
|
|
666
|
+
}
|
|
667
|
+
/** The next epoch for the send_ek side */
|
|
668
|
+
get nextEpoch() {
|
|
669
|
+
return this.epoch + 1n;
|
|
670
|
+
}
|
|
671
|
+
};
|
|
672
|
+
|
|
673
|
+
//#endregion
|
|
674
|
+
//#region src/encoding/gf.ts
|
|
675
|
+
/**
|
|
676
|
+
* Copyright © 2025 Signal Messenger, LLC
|
|
677
|
+
* Copyright © 2026 Parity Technologies
|
|
678
|
+
*
|
|
679
|
+
* GF(2^16) Galois field arithmetic.
|
|
680
|
+
* Ported from the Rust SPQR implementation.
|
|
681
|
+
*
|
|
682
|
+
* Primitive polynomial: x^16 + x^12 + x^3 + x + 1 (0x1100b)
|
|
683
|
+
*/
|
|
684
|
+
/** Primitive polynomial for GF(2^16): x^16 + x^12 + x^3 + x + 1 */
|
|
685
|
+
const POLY = 69643;
|
|
686
|
+
/**
|
|
687
|
+
* Build one entry of the reduction table.
|
|
688
|
+
*
|
|
689
|
+
* Given a single byte `a` that appears in positions [16..23] or [24..31] of
|
|
690
|
+
* a 32-bit product, compute the 16-bit XOR mask needed to reduce those bits
|
|
691
|
+
* modulo POLY.
|
|
692
|
+
*/
|
|
693
|
+
function reduceFromByte(a) {
|
|
694
|
+
let byte = a;
|
|
695
|
+
let out = 0;
|
|
696
|
+
for (let i = 7; i >= 0; i--) if ((1 << i & byte) !== 0) {
|
|
697
|
+
out ^= POLY << i;
|
|
698
|
+
byte ^= POLY << i >>> 16 & 255;
|
|
699
|
+
}
|
|
700
|
+
return out & 65535;
|
|
701
|
+
}
|
|
702
|
+
/** Precomputed 256-entry lookup table for polynomial reduction. */
|
|
703
|
+
const REDUCE_BYTES = /* @__PURE__ */ (() => {
|
|
704
|
+
const table = new Uint16Array(256);
|
|
705
|
+
for (let i = 0; i < 256; i++) table[i] = reduceFromByte(i);
|
|
706
|
+
return table;
|
|
707
|
+
})();
|
|
708
|
+
/**
|
|
709
|
+
* Polynomial multiplication in GF(2)[x] (no reduction).
|
|
710
|
+
*
|
|
711
|
+
* Both `a` and `b` must be in the range [0, 0xffff].
|
|
712
|
+
* The result may be up to 31 bits wide.
|
|
713
|
+
*/
|
|
714
|
+
function polyMul(a, b) {
|
|
715
|
+
let acc = 0;
|
|
716
|
+
for (let shift = 0; shift < 16; shift++) if ((b & 1 << shift) !== 0) acc ^= a << shift;
|
|
717
|
+
return acc >>> 0;
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Reduce a 32-bit polynomial product modulo POLY, yielding a 16-bit result.
|
|
721
|
+
*
|
|
722
|
+
* Uses the precomputed REDUCE_BYTES table to process the top two bytes.
|
|
723
|
+
*/
|
|
724
|
+
function polyReduce(v) {
|
|
725
|
+
let r = v >>> 0;
|
|
726
|
+
r ^= REDUCE_BYTES[r >>> 24 & 255] << 8;
|
|
727
|
+
r ^= REDUCE_BYTES[r >>> 16 & 255];
|
|
728
|
+
return r & 65535;
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Full GF(2^16) multiplication of two raw u16 values.
|
|
732
|
+
* Returns a u16 result.
|
|
733
|
+
*/
|
|
734
|
+
function mulRaw(a, b) {
|
|
735
|
+
return polyReduce(polyMul(a, b));
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* An element of GF(2^16).
|
|
739
|
+
*
|
|
740
|
+
* The `value` field holds a 16-bit unsigned integer in [0, 65535].
|
|
741
|
+
*/
|
|
742
|
+
var GF16 = class GF16 {
|
|
743
|
+
value;
|
|
744
|
+
constructor(value) {
|
|
745
|
+
this.value = value & 65535;
|
|
746
|
+
}
|
|
747
|
+
static ZERO = new GF16(0);
|
|
748
|
+
static ONE = new GF16(1);
|
|
749
|
+
/** Addition in GF(2^n) is XOR. */
|
|
750
|
+
add(other) {
|
|
751
|
+
return new GF16(this.value ^ other.value);
|
|
752
|
+
}
|
|
753
|
+
/** Subtraction in GF(2^n) is the same as addition (XOR). */
|
|
754
|
+
sub(other) {
|
|
755
|
+
return this.add(other);
|
|
756
|
+
}
|
|
757
|
+
/** Multiplication in GF(2^16) using polynomial long-multiplication + reduction. */
|
|
758
|
+
mul(other) {
|
|
759
|
+
return new GF16(mulRaw(this.value, other.value));
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* Division in GF(2^16).
|
|
763
|
+
*
|
|
764
|
+
* Computes `this / other` via Fermat's little theorem:
|
|
765
|
+
* other^(-1) = other^(2^16 - 2)
|
|
766
|
+
*
|
|
767
|
+
* The loop accumulates the inverse through repeated squaring:
|
|
768
|
+
* After 15 iterations (i = 1..15):
|
|
769
|
+
* out = this * other^(2^16 - 2) = this * other^(-1)
|
|
770
|
+
*
|
|
771
|
+
* Throws if `other` is zero.
|
|
772
|
+
*/
|
|
773
|
+
div(other) {
|
|
774
|
+
if (other.value === 0) throw new Error("GF16: division by zero");
|
|
775
|
+
let sqVal = mulRaw(other.value, other.value);
|
|
776
|
+
let outVal = this.value;
|
|
777
|
+
for (let i = 1; i < 16; i++) {
|
|
778
|
+
const newSqVal = mulRaw(sqVal, sqVal);
|
|
779
|
+
const newOutVal = mulRaw(sqVal, outVal);
|
|
780
|
+
sqVal = newSqVal;
|
|
781
|
+
outVal = newOutVal;
|
|
782
|
+
}
|
|
783
|
+
return new GF16(outVal);
|
|
784
|
+
}
|
|
785
|
+
equals(other) {
|
|
786
|
+
return this.value === other.value;
|
|
787
|
+
}
|
|
788
|
+
toString() {
|
|
789
|
+
return `GF16(0x${this.value.toString(16).padStart(4, "0")})`;
|
|
790
|
+
}
|
|
791
|
+
};
|
|
792
|
+
/**
|
|
793
|
+
* Multiply every element of `into` by `a` in-place.
|
|
794
|
+
*
|
|
795
|
+
* This is the TypeScript equivalent of Rust's `parallel_mult` which benefits
|
|
796
|
+
* from SIMD on native platforms. Here we just iterate.
|
|
797
|
+
*/
|
|
798
|
+
function parallelMult(a, into) {
|
|
799
|
+
const av = a.value;
|
|
800
|
+
for (let i = 0; i < into.length; i++) into[i] = new GF16(mulRaw(av, into[i].value));
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
//#endregion
|
|
804
|
+
//#region src/encoding/polynomial.ts
|
|
805
|
+
/**
|
|
806
|
+
* Copyright © 2025 Signal Messenger, LLC
|
|
807
|
+
* Copyright © 2026 Parity Technologies
|
|
808
|
+
*
|
|
809
|
+
* Polynomial erasure coding over GF(2^16).
|
|
810
|
+
* Ported from the Rust SPQR implementation.
|
|
811
|
+
*
|
|
812
|
+
* The encoder splits a message into 16 parallel polynomials and can produce
|
|
813
|
+
* an unlimited number of coded chunks. Any sufficient subset of chunks
|
|
814
|
+
* allows the decoder to reconstruct the original message via Lagrange
|
|
815
|
+
* interpolation.
|
|
816
|
+
*/
|
|
817
|
+
var PolynomialError = class extends Error {
|
|
818
|
+
constructor(message) {
|
|
819
|
+
super(message);
|
|
820
|
+
this.name = "PolynomialError";
|
|
821
|
+
}
|
|
822
|
+
};
|
|
823
|
+
const NUM_POLYS = 16;
|
|
824
|
+
const CHUNK_DATA_SIZE$1 = 32;
|
|
825
|
+
const MAX_MESSAGE_LENGTH = 65536 * NUM_POLYS;
|
|
826
|
+
/**
|
|
827
|
+
* A polynomial over GF(2^16) in coefficient form.
|
|
828
|
+
*
|
|
829
|
+
* Coefficients are stored in little-endian order:
|
|
830
|
+
* coefficients[0] = constant term (x^0)
|
|
831
|
+
* coefficients[1] = linear term (x^1)
|
|
832
|
+
* ...
|
|
833
|
+
*/
|
|
834
|
+
var Poly = class Poly {
|
|
835
|
+
coefficients;
|
|
836
|
+
constructor(coefficients) {
|
|
837
|
+
this.coefficients = coefficients;
|
|
838
|
+
}
|
|
839
|
+
/** Create a zero polynomial of a given length. */
|
|
840
|
+
static zeros(len) {
|
|
841
|
+
const coeffs = new Array(len);
|
|
842
|
+
for (let i = 0; i < len; i++) coeffs[i] = GF16.ZERO;
|
|
843
|
+
return new Poly(coeffs);
|
|
844
|
+
}
|
|
845
|
+
/** Number of coefficients. */
|
|
846
|
+
get length() {
|
|
847
|
+
return this.coefficients.length;
|
|
848
|
+
}
|
|
849
|
+
/**
|
|
850
|
+
* Evaluate the polynomial at a given point using a divide-and-conquer
|
|
851
|
+
* approach for computing powers of x, then a dot product.
|
|
852
|
+
*
|
|
853
|
+
* xs[0] = 1, xs[1] = x, xs[i] = xs[floor(i/2)] * xs[floor(i/2) + (i%2)]
|
|
854
|
+
*/
|
|
855
|
+
computeAt(x) {
|
|
856
|
+
const n = this.coefficients.length;
|
|
857
|
+
if (n === 0) return GF16.ZERO;
|
|
858
|
+
if (n === 1) return this.coefficients[0];
|
|
859
|
+
const xs = new Array(n);
|
|
860
|
+
xs[0] = GF16.ONE;
|
|
861
|
+
if (n > 1) xs[1] = x;
|
|
862
|
+
for (let i = 2; i < n; i++) {
|
|
863
|
+
const half = i >>> 1;
|
|
864
|
+
const rem = i & 1;
|
|
865
|
+
xs[i] = xs[half].mul(xs[half + rem]);
|
|
866
|
+
}
|
|
867
|
+
let result = GF16.ZERO;
|
|
868
|
+
for (let i = 0; i < n; i++) result = result.add(this.coefficients[i].mul(xs[i]));
|
|
869
|
+
return result;
|
|
870
|
+
}
|
|
871
|
+
/** Add another polynomial to this one in-place. */
|
|
872
|
+
addAssign(other) {
|
|
873
|
+
while (this.coefficients.length < other.coefficients.length) this.coefficients.push(GF16.ZERO);
|
|
874
|
+
for (let i = 0; i < other.coefficients.length; i++) this.coefficients[i] = this.coefficients[i].add(other.coefficients[i]);
|
|
875
|
+
}
|
|
876
|
+
/** Multiply all coefficients by a scalar in-place. */
|
|
877
|
+
multAssign(m) {
|
|
878
|
+
parallelMult(m, this.coefficients);
|
|
879
|
+
}
|
|
880
|
+
/** Serialize coefficients as big-endian u16 pairs. */
|
|
881
|
+
serialize() {
|
|
882
|
+
const out = new Uint8Array(this.coefficients.length * 2);
|
|
883
|
+
for (let i = 0; i < this.coefficients.length; i++) {
|
|
884
|
+
const v = this.coefficients[i].value;
|
|
885
|
+
out[i * 2] = v >>> 8 & 255;
|
|
886
|
+
out[i * 2 + 1] = v & 255;
|
|
887
|
+
}
|
|
888
|
+
return out;
|
|
889
|
+
}
|
|
890
|
+
/** Deserialize from big-endian u16 pairs. */
|
|
891
|
+
static deserialize(data) {
|
|
892
|
+
if (data.length % 2 !== 0) throw new PolynomialError("Poly data length must be even");
|
|
893
|
+
const n = data.length / 2;
|
|
894
|
+
const coeffs = new Array(n);
|
|
895
|
+
for (let i = 0; i < n; i++) coeffs[i] = new GF16(data[i * 2] << 8 | data[i * 2 + 1]);
|
|
896
|
+
return new Poly(coeffs);
|
|
897
|
+
}
|
|
898
|
+
/**
|
|
899
|
+
* Lagrange interpolation over a set of points.
|
|
900
|
+
*
|
|
901
|
+
* Given N points (x_i, y_i), produces the unique polynomial of degree < N
|
|
902
|
+
* passing through all of them.
|
|
903
|
+
*/
|
|
904
|
+
static lagrangeInterpolate(pts) {
|
|
905
|
+
const n = pts.length;
|
|
906
|
+
if (n === 0) return new Poly([]);
|
|
907
|
+
let product = new Poly([pts[0].x, GF16.ONE]);
|
|
908
|
+
for (let i = 1; i < n; i++) product = polyMultiply(product, new Poly([pts[i].x, GF16.ONE]));
|
|
909
|
+
const result = Poly.zeros(n);
|
|
910
|
+
for (let i = 0; i < n; i++) {
|
|
911
|
+
const basis = syntheticDivide(product, pts[i].x);
|
|
912
|
+
const denom = basis.computeAt(pts[i].x);
|
|
913
|
+
const scale = pts[i].y.div(denom);
|
|
914
|
+
const scaled = clonePoly(basis);
|
|
915
|
+
scaled.multAssign(scale);
|
|
916
|
+
result.addAssign(scaled);
|
|
917
|
+
}
|
|
918
|
+
return result;
|
|
919
|
+
}
|
|
920
|
+
};
|
|
921
|
+
/** Multiply two polynomials (convolution over GF(2^16)). */
|
|
922
|
+
function polyMultiply(a, b) {
|
|
923
|
+
if (a.length === 0 || b.length === 0) return new Poly([]);
|
|
924
|
+
const result = Poly.zeros(a.length + b.length - 1);
|
|
925
|
+
for (let i = 0; i < a.length; i++) for (let j = 0; j < b.length; j++) result.coefficients[i + j] = result.coefficients[i + j].add(a.coefficients[i].mul(b.coefficients[j]));
|
|
926
|
+
return result;
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Synthetic division: divide poly by (x - root) = (x + root) in GF(2^n).
|
|
930
|
+
*
|
|
931
|
+
* Returns the quotient polynomial (degree one less than input).
|
|
932
|
+
*/
|
|
933
|
+
function syntheticDivide(poly, root) {
|
|
934
|
+
const n = poly.length;
|
|
935
|
+
if (n <= 1) return new Poly([]);
|
|
936
|
+
const quotient = new Array(n - 1);
|
|
937
|
+
let carry = GF16.ZERO;
|
|
938
|
+
for (let i = n - 1; i >= 1; i--) {
|
|
939
|
+
const coeff = poly.coefficients[i].add(carry);
|
|
940
|
+
quotient[i - 1] = coeff;
|
|
941
|
+
carry = coeff.mul(root);
|
|
942
|
+
}
|
|
943
|
+
return new Poly(quotient);
|
|
944
|
+
}
|
|
945
|
+
/** Clone a polynomial (shallow copy of coefficient array). */
|
|
946
|
+
function clonePoly(p) {
|
|
947
|
+
return new Poly(p.coefficients.slice());
|
|
948
|
+
}
|
|
949
|
+
var SortedPtSet = class SortedPtSet {
|
|
950
|
+
pts = [];
|
|
951
|
+
get length() {
|
|
952
|
+
return this.pts.length;
|
|
953
|
+
}
|
|
954
|
+
/** Insert a point, maintaining sorted order. Returns false if duplicate x. */
|
|
955
|
+
insert(pt) {
|
|
956
|
+
const idx = this.findIndex(pt.x.value);
|
|
957
|
+
if (idx < this.pts.length && this.pts[idx].x.value === pt.x.value) return false;
|
|
958
|
+
this.pts.splice(idx, 0, pt);
|
|
959
|
+
return true;
|
|
960
|
+
}
|
|
961
|
+
/** Binary search for the insertion point of a given x value. */
|
|
962
|
+
findIndex(xVal) {
|
|
963
|
+
let lo = 0;
|
|
964
|
+
let hi = this.pts.length;
|
|
965
|
+
while (lo < hi) {
|
|
966
|
+
const mid = lo + hi >>> 1;
|
|
967
|
+
if (this.pts[mid].x.value < xVal) lo = mid + 1;
|
|
968
|
+
else hi = mid;
|
|
969
|
+
}
|
|
970
|
+
return lo;
|
|
971
|
+
}
|
|
972
|
+
/** Look up a point by x value. Returns undefined if not found. */
|
|
973
|
+
findByX(xVal) {
|
|
974
|
+
const idx = this.findIndex(xVal);
|
|
975
|
+
if (idx < this.pts.length && this.pts[idx].x.value === xVal) return this.pts[idx];
|
|
976
|
+
}
|
|
977
|
+
/** Return all points as an array (for interpolation). */
|
|
978
|
+
toArray() {
|
|
979
|
+
return this.pts.slice();
|
|
980
|
+
}
|
|
981
|
+
/** Serialize all points as big-endian u16 pairs (x then y). */
|
|
982
|
+
serialize() {
|
|
983
|
+
const out = new Uint8Array(this.pts.length * 4);
|
|
984
|
+
for (let i = 0; i < this.pts.length; i++) {
|
|
985
|
+
const pt = this.pts[i];
|
|
986
|
+
out[i * 4] = pt.x.value >>> 8 & 255;
|
|
987
|
+
out[i * 4 + 1] = pt.x.value & 255;
|
|
988
|
+
out[i * 4 + 2] = pt.y.value >>> 8 & 255;
|
|
989
|
+
out[i * 4 + 3] = pt.y.value & 255;
|
|
990
|
+
}
|
|
991
|
+
return out;
|
|
992
|
+
}
|
|
993
|
+
/** Deserialize from big-endian u16 pairs (x then y). */
|
|
994
|
+
static deserialize(data) {
|
|
995
|
+
if (data.length % 4 !== 0) throw new PolynomialError("SortedPtSet data length must be multiple of 4");
|
|
996
|
+
const set = new SortedPtSet();
|
|
997
|
+
const n = data.length / 4;
|
|
998
|
+
for (let i = 0; i < n; i++) {
|
|
999
|
+
const x = new GF16(data[i * 4] << 8 | data[i * 4 + 1]);
|
|
1000
|
+
const y = new GF16(data[i * 4 + 2] << 8 | data[i * 4 + 3]);
|
|
1001
|
+
set.insert({
|
|
1002
|
+
x,
|
|
1003
|
+
y
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
return set;
|
|
1007
|
+
}
|
|
1008
|
+
};
|
|
1009
|
+
var PolyEncoder = class PolyEncoder {
|
|
1010
|
+
idx;
|
|
1011
|
+
state;
|
|
1012
|
+
constructor(idx, state) {
|
|
1013
|
+
this.idx = idx;
|
|
1014
|
+
this.state = state;
|
|
1015
|
+
}
|
|
1016
|
+
/**
|
|
1017
|
+
* Create an encoder from a message byte array.
|
|
1018
|
+
*
|
|
1019
|
+
* The message is split into 16 interleaved streams of GF16 values.
|
|
1020
|
+
* Each pair of consecutive bytes becomes one GF16 element. If the message
|
|
1021
|
+
* length is odd, it is padded with a zero byte.
|
|
1022
|
+
*/
|
|
1023
|
+
static encodeBytes(msg) {
|
|
1024
|
+
if (msg.length % 2 !== 0) throw new PolynomialError("Message length must be even");
|
|
1025
|
+
if (msg.length > MAX_MESSAGE_LENGTH) throw new PolynomialError("Message length exceeds maximum");
|
|
1026
|
+
const totalValues = msg.length / 2;
|
|
1027
|
+
const points = new Array(NUM_POLYS);
|
|
1028
|
+
for (let p = 0; p < NUM_POLYS; p++) points[p] = [];
|
|
1029
|
+
for (let i = 0; i < totalValues; i++) {
|
|
1030
|
+
const poly = i % NUM_POLYS;
|
|
1031
|
+
const value = msg[i * 2] << 8 | msg[i * 2 + 1];
|
|
1032
|
+
points[poly].push(new GF16(value));
|
|
1033
|
+
}
|
|
1034
|
+
return new PolyEncoder(0, {
|
|
1035
|
+
kind: "points",
|
|
1036
|
+
points
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
/** Return the next chunk and advance the index. */
|
|
1040
|
+
nextChunk() {
|
|
1041
|
+
const chunk = this.chunkAt(this.idx);
|
|
1042
|
+
this.idx++;
|
|
1043
|
+
return chunk;
|
|
1044
|
+
}
|
|
1045
|
+
/** Compute the chunk at a specific index. */
|
|
1046
|
+
chunkAt(idx) {
|
|
1047
|
+
const data = new Uint8Array(CHUNK_DATA_SIZE$1);
|
|
1048
|
+
for (let i = 0; i < NUM_POLYS; i++) {
|
|
1049
|
+
const totalIdx = idx * NUM_POLYS + i;
|
|
1050
|
+
const poly = totalIdx % NUM_POLYS;
|
|
1051
|
+
const polyIdx = Math.floor(totalIdx / NUM_POLYS);
|
|
1052
|
+
const val = this.pointAt(poly, polyIdx);
|
|
1053
|
+
data[i * 2] = val.value >>> 8 & 255;
|
|
1054
|
+
data[i * 2 + 1] = val.value & 255;
|
|
1055
|
+
}
|
|
1056
|
+
return {
|
|
1057
|
+
index: idx & 65535,
|
|
1058
|
+
data
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
1061
|
+
/**
|
|
1062
|
+
* Get the GF16 value for polynomial `poly` at index `idx`.
|
|
1063
|
+
*
|
|
1064
|
+
* If we are in Points state and the index is within range, return directly.
|
|
1065
|
+
* Otherwise, convert to Polys state and evaluate.
|
|
1066
|
+
*/
|
|
1067
|
+
pointAt(poly, idx) {
|
|
1068
|
+
if (this.state.kind === "points") {
|
|
1069
|
+
const pts = this.state.points[poly];
|
|
1070
|
+
if (idx < pts.length) return pts[idx];
|
|
1071
|
+
this.convertToPolys();
|
|
1072
|
+
}
|
|
1073
|
+
return this.state.polys[poly].computeAt(new GF16(idx));
|
|
1074
|
+
}
|
|
1075
|
+
/** Convert from Points state to Polys state via Lagrange interpolation. */
|
|
1076
|
+
convertToPolys() {
|
|
1077
|
+
if (this.state.kind === "polys") return;
|
|
1078
|
+
const { points } = this.state;
|
|
1079
|
+
const polys = new Array(NUM_POLYS);
|
|
1080
|
+
for (let p = 0; p < NUM_POLYS; p++) {
|
|
1081
|
+
const pts = points[p].map((y, i) => ({
|
|
1082
|
+
x: new GF16(i),
|
|
1083
|
+
y
|
|
1084
|
+
}));
|
|
1085
|
+
polys[p] = Poly.lagrangeInterpolate(pts);
|
|
1086
|
+
}
|
|
1087
|
+
this.state = {
|
|
1088
|
+
kind: "polys",
|
|
1089
|
+
polys
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
/** Serialize encoder state for protobuf transport. */
|
|
1093
|
+
toProto() {
|
|
1094
|
+
if (this.state.kind === "points") {
|
|
1095
|
+
const pts = this.state.points.map((arr) => {
|
|
1096
|
+
const buf = new Uint8Array(arr.length * 2);
|
|
1097
|
+
for (let i = 0; i < arr.length; i++) {
|
|
1098
|
+
buf[i * 2] = arr[i].value >>> 8 & 255;
|
|
1099
|
+
buf[i * 2 + 1] = arr[i].value & 255;
|
|
1100
|
+
}
|
|
1101
|
+
return buf;
|
|
1102
|
+
});
|
|
1103
|
+
return {
|
|
1104
|
+
idx: this.idx,
|
|
1105
|
+
pts,
|
|
1106
|
+
polys: []
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
const polys = this.state.polys.map((p) => p.serialize());
|
|
1110
|
+
return {
|
|
1111
|
+
idx: this.idx,
|
|
1112
|
+
pts: [],
|
|
1113
|
+
polys
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
/** Restore encoder from protobuf data. */
|
|
1117
|
+
static fromProto(data) {
|
|
1118
|
+
if (data.polys.length > 0) {
|
|
1119
|
+
const polys = data.polys.map((buf) => Poly.deserialize(buf));
|
|
1120
|
+
return new PolyEncoder(data.idx, {
|
|
1121
|
+
kind: "polys",
|
|
1122
|
+
polys
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
const points = data.pts.map((buf) => {
|
|
1126
|
+
const arr = [];
|
|
1127
|
+
for (let i = 0; i < buf.length; i += 2) arr.push(new GF16(buf[i] << 8 | buf[i + 1]));
|
|
1128
|
+
return arr;
|
|
1129
|
+
});
|
|
1130
|
+
return new PolyEncoder(data.idx, {
|
|
1131
|
+
kind: "points",
|
|
1132
|
+
points
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
};
|
|
1136
|
+
var PolyDecoder = class PolyDecoder {
|
|
1137
|
+
/** Total number of GF16 values needed (= message byte length / 2, rounded up). */
|
|
1138
|
+
ptsNeeded;
|
|
1139
|
+
pts;
|
|
1140
|
+
_isComplete;
|
|
1141
|
+
constructor(ptsNeeded, pts, isComplete) {
|
|
1142
|
+
this.ptsNeeded = ptsNeeded;
|
|
1143
|
+
this.pts = pts;
|
|
1144
|
+
this._isComplete = isComplete;
|
|
1145
|
+
}
|
|
1146
|
+
/**
|
|
1147
|
+
* Create a decoder for a message of `lenBytes` bytes.
|
|
1148
|
+
*
|
|
1149
|
+
* The caller must know the original message length to know when enough
|
|
1150
|
+
* chunks have been received.
|
|
1151
|
+
*/
|
|
1152
|
+
static create(lenBytes) {
|
|
1153
|
+
if (lenBytes % 2 !== 0) throw new PolynomialError("Message length must be even");
|
|
1154
|
+
const ptsNeeded = lenBytes / 2;
|
|
1155
|
+
const pts = new Array(NUM_POLYS);
|
|
1156
|
+
for (let i = 0; i < NUM_POLYS; i++) pts[i] = new SortedPtSet();
|
|
1157
|
+
return new PolyDecoder(ptsNeeded, pts, false);
|
|
1158
|
+
}
|
|
1159
|
+
/** Whether all polynomial sets have enough points to decode. */
|
|
1160
|
+
get isComplete() {
|
|
1161
|
+
return this._isComplete;
|
|
1162
|
+
}
|
|
1163
|
+
/**
|
|
1164
|
+
* Number of points necessary for polynomial `poly`.
|
|
1165
|
+
*
|
|
1166
|
+
* The total points (ptsNeeded) are distributed across 16 polynomials
|
|
1167
|
+
* round-robin, so some may need one more point than others.
|
|
1168
|
+
*/
|
|
1169
|
+
necessaryPoints(poly) {
|
|
1170
|
+
const base = Math.floor(this.ptsNeeded / NUM_POLYS);
|
|
1171
|
+
return poly < this.ptsNeeded % NUM_POLYS ? base + 1 : base;
|
|
1172
|
+
}
|
|
1173
|
+
/** Add a chunk to the decoder. */
|
|
1174
|
+
addChunk(chunk) {
|
|
1175
|
+
if (this._isComplete) return;
|
|
1176
|
+
for (let i = 0; i < NUM_POLYS; i++) {
|
|
1177
|
+
const totalIdx = chunk.index * NUM_POLYS + i;
|
|
1178
|
+
const poly = totalIdx % NUM_POLYS;
|
|
1179
|
+
const polyIdx = Math.floor(totalIdx / NUM_POLYS);
|
|
1180
|
+
const needed = this.necessaryPoints(poly);
|
|
1181
|
+
if (this.pts[poly].length >= needed) continue;
|
|
1182
|
+
const y = new GF16(chunk.data[i * 2] << 8 | chunk.data[i * 2 + 1]);
|
|
1183
|
+
const pt = {
|
|
1184
|
+
x: new GF16(polyIdx),
|
|
1185
|
+
y
|
|
1186
|
+
};
|
|
1187
|
+
this.pts[poly].insert(pt);
|
|
1188
|
+
}
|
|
1189
|
+
this._isComplete = this.checkComplete();
|
|
1190
|
+
}
|
|
1191
|
+
/** Check whether all 16 polynomial sets have enough points. */
|
|
1192
|
+
checkComplete() {
|
|
1193
|
+
for (let p = 0; p < NUM_POLYS; p++) if (this.pts[p].length < this.necessaryPoints(p)) return false;
|
|
1194
|
+
return true;
|
|
1195
|
+
}
|
|
1196
|
+
/**
|
|
1197
|
+
* Attempt to decode the message.
|
|
1198
|
+
*
|
|
1199
|
+
* Returns the decoded byte array if enough chunks have been received,
|
|
1200
|
+
* or null if more chunks are needed.
|
|
1201
|
+
*/
|
|
1202
|
+
decodedMessage() {
|
|
1203
|
+
if (!this._isComplete) return null;
|
|
1204
|
+
const result = new Uint8Array(this.ptsNeeded * 2);
|
|
1205
|
+
for (let i = 0; i < this.ptsNeeded; i++) {
|
|
1206
|
+
const poly = i % NUM_POLYS;
|
|
1207
|
+
const xVal = Math.floor(i / NUM_POLYS);
|
|
1208
|
+
let val;
|
|
1209
|
+
const direct = this.pts[poly].findByX(xVal);
|
|
1210
|
+
if (direct !== void 0) val = direct.y;
|
|
1211
|
+
else {
|
|
1212
|
+
const allPts = this.pts[poly].toArray();
|
|
1213
|
+
val = Poly.lagrangeInterpolate(allPts).computeAt(new GF16(xVal));
|
|
1214
|
+
}
|
|
1215
|
+
result[i * 2] = val.value >>> 8 & 255;
|
|
1216
|
+
result[i * 2 + 1] = val.value & 255;
|
|
1217
|
+
}
|
|
1218
|
+
return result;
|
|
1219
|
+
}
|
|
1220
|
+
/** Serialize decoder state for protobuf transport. */
|
|
1221
|
+
toProto() {
|
|
1222
|
+
return {
|
|
1223
|
+
ptsNeeded: this.ptsNeeded,
|
|
1224
|
+
polys: NUM_POLYS,
|
|
1225
|
+
pts: this.pts.map((s) => s.serialize()),
|
|
1226
|
+
isComplete: this._isComplete
|
|
1227
|
+
};
|
|
1228
|
+
}
|
|
1229
|
+
/** Restore decoder from protobuf data. */
|
|
1230
|
+
static fromProto(data) {
|
|
1231
|
+
const pts = data.pts.map((buf) => SortedPtSet.deserialize(buf));
|
|
1232
|
+
while (pts.length < NUM_POLYS) pts.push(new SortedPtSet());
|
|
1233
|
+
return new PolyDecoder(data.ptsNeeded, pts, data.isComplete);
|
|
1234
|
+
}
|
|
1235
|
+
};
|
|
1236
|
+
|
|
1237
|
+
//#endregion
|
|
1238
|
+
//#region src/authenticator.ts
|
|
1239
|
+
/**
|
|
1240
|
+
* Copyright © 2025 Signal Messenger, LLC
|
|
1241
|
+
* Copyright © 2026 Parity Technologies
|
|
1242
|
+
*
|
|
1243
|
+
* SPQR Authenticator -- HMAC-SHA256 MAC for KEM exchanges.
|
|
1244
|
+
*
|
|
1245
|
+
* Ported from Signal's spqr crate: authenticator.rs
|
|
1246
|
+
*
|
|
1247
|
+
* The Authenticator produces and verifies MACs over ciphertext and header
|
|
1248
|
+
* data using HMAC-SHA256. The internal rootKey and macKey are updated
|
|
1249
|
+
* via HKDF at each epoch transition.
|
|
1250
|
+
*
|
|
1251
|
+
* All info strings and data formats MUST match the Rust implementation exactly.
|
|
1252
|
+
*/
|
|
1253
|
+
const enc$1 = new TextEncoder();
|
|
1254
|
+
const AUTH_UPDATE_INFO = enc$1.encode(LABEL_AUTH_UPDATE);
|
|
1255
|
+
const CT_MAC_PREFIX = enc$1.encode(LABEL_CT_MAC);
|
|
1256
|
+
const HDR_MAC_PREFIX = enc$1.encode(LABEL_HDR_MAC);
|
|
1257
|
+
/**
|
|
1258
|
+
* Authenticator manages root_key and mac_key state for producing
|
|
1259
|
+
* and verifying MACs over KEM ciphertext and headers.
|
|
1260
|
+
*
|
|
1261
|
+
* The update operation uses HKDF:
|
|
1262
|
+
* IKM = [rootKey || key]
|
|
1263
|
+
* Salt = ZERO_SALT (32 zeros)
|
|
1264
|
+
* Info = LABEL_AUTH_UPDATE + epoch_be8
|
|
1265
|
+
* Output: 64 bytes -> [0..32] = new rootKey, [32..64] = new macKey
|
|
1266
|
+
*
|
|
1267
|
+
* MAC operations use HMAC-SHA256:
|
|
1268
|
+
* Key = macKey
|
|
1269
|
+
* Data = [label_prefix || epoch_be8 || payload]
|
|
1270
|
+
*/
|
|
1271
|
+
var Authenticator = class Authenticator {
|
|
1272
|
+
rootKey;
|
|
1273
|
+
macKey;
|
|
1274
|
+
constructor(rootKey, macKey) {
|
|
1275
|
+
this.rootKey = rootKey;
|
|
1276
|
+
this.macKey = macKey;
|
|
1277
|
+
}
|
|
1278
|
+
/**
|
|
1279
|
+
* Create a new Authenticator from a root key and initial epoch.
|
|
1280
|
+
* Matches Rust: `Authenticator::new(root_key, ep)`
|
|
1281
|
+
*
|
|
1282
|
+
* Initializes with zero keys, then immediately updates with
|
|
1283
|
+
* the provided root key and epoch.
|
|
1284
|
+
*/
|
|
1285
|
+
static create(rootKey, epoch) {
|
|
1286
|
+
const auth = new Authenticator(new Uint8Array(32), new Uint8Array(32));
|
|
1287
|
+
auth.update(epoch, rootKey);
|
|
1288
|
+
return auth;
|
|
1289
|
+
}
|
|
1290
|
+
/**
|
|
1291
|
+
* Update the authenticator with a new epoch and key material.
|
|
1292
|
+
*
|
|
1293
|
+
* HKDF(IKM=[rootKey||key], salt=ZERO_SALT, info=[label||epoch_be8], length=64)
|
|
1294
|
+
*
|
|
1295
|
+
* Output split: rootKey = [0..32], macKey = [32..64]
|
|
1296
|
+
*/
|
|
1297
|
+
update(epoch, key) {
|
|
1298
|
+
const derived = hkdfSha256(concat(this.rootKey, key), ZERO_SALT, concat(AUTH_UPDATE_INFO, bigintToBE8(epoch)), 64);
|
|
1299
|
+
this.rootKey = derived.slice(0, 32);
|
|
1300
|
+
this.macKey = derived.slice(32, 64);
|
|
1301
|
+
}
|
|
1302
|
+
/**
|
|
1303
|
+
* Compute MAC over ciphertext.
|
|
1304
|
+
*
|
|
1305
|
+
* HMAC-SHA256(macKey, [LABEL_CT_MAC || epoch_be8 || ct])
|
|
1306
|
+
*/
|
|
1307
|
+
macCt(epoch, ct) {
|
|
1308
|
+
const data = concat(CT_MAC_PREFIX, bigintToBE8(epoch), ct);
|
|
1309
|
+
return hmacSha256(this.macKey, data);
|
|
1310
|
+
}
|
|
1311
|
+
/**
|
|
1312
|
+
* Verify ciphertext MAC (constant-time comparison).
|
|
1313
|
+
* Throws AuthenticatorError if the MAC does not match.
|
|
1314
|
+
*/
|
|
1315
|
+
verifyCt(epoch, ct, expectedMac) {
|
|
1316
|
+
if (!constantTimeEqual(this.macCt(epoch, ct), expectedMac)) throw new AuthenticatorError("Ciphertext MAC is invalid", "INVALID_CT_MAC");
|
|
1317
|
+
}
|
|
1318
|
+
/**
|
|
1319
|
+
* Compute MAC over header (encapsulation key header).
|
|
1320
|
+
*
|
|
1321
|
+
* HMAC-SHA256(macKey, [LABEL_HDR_MAC || epoch_be8 || hdr])
|
|
1322
|
+
*/
|
|
1323
|
+
macHdr(epoch, hdr) {
|
|
1324
|
+
const data = concat(HDR_MAC_PREFIX, bigintToBE8(epoch), hdr);
|
|
1325
|
+
return hmacSha256(this.macKey, data);
|
|
1326
|
+
}
|
|
1327
|
+
/**
|
|
1328
|
+
* Verify header MAC (constant-time comparison).
|
|
1329
|
+
* Throws AuthenticatorError if the MAC does not match.
|
|
1330
|
+
*/
|
|
1331
|
+
verifyHdr(epoch, hdr, expectedMac) {
|
|
1332
|
+
if (!constantTimeEqual(this.macHdr(epoch, hdr), expectedMac)) throw new AuthenticatorError("Encapsulation key MAC is invalid", "INVALID_HDR_MAC");
|
|
1333
|
+
}
|
|
1334
|
+
/**
|
|
1335
|
+
* Deep clone this authenticator.
|
|
1336
|
+
*/
|
|
1337
|
+
clone() {
|
|
1338
|
+
return new Authenticator(Uint8Array.from(this.rootKey), Uint8Array.from(this.macKey));
|
|
1339
|
+
}
|
|
1340
|
+
/**
|
|
1341
|
+
* Serialize to protobuf representation.
|
|
1342
|
+
* Matches Rust Authenticator::into_pb().
|
|
1343
|
+
*/
|
|
1344
|
+
toProto() {
|
|
1345
|
+
return {
|
|
1346
|
+
rootKey: Uint8Array.from(this.rootKey),
|
|
1347
|
+
macKey: Uint8Array.from(this.macKey)
|
|
1348
|
+
};
|
|
1349
|
+
}
|
|
1350
|
+
/**
|
|
1351
|
+
* Deserialize from protobuf representation.
|
|
1352
|
+
* Matches Rust Authenticator::from_pb().
|
|
1353
|
+
*/
|
|
1354
|
+
static fromProto(pb) {
|
|
1355
|
+
return new Authenticator(Uint8Array.from(pb.rootKey), Uint8Array.from(pb.macKey));
|
|
1356
|
+
}
|
|
1357
|
+
};
|
|
1358
|
+
|
|
1359
|
+
//#endregion
|
|
1360
|
+
//#region src/v1/chunked/send-ek.ts
|
|
1361
|
+
/**
|
|
1362
|
+
* Initial chunked send_ek state. No keypair generated yet.
|
|
1363
|
+
* Wraps unchunked.KeysUnsampled.
|
|
1364
|
+
*/
|
|
1365
|
+
var KeysUnsampled$1 = class {
|
|
1366
|
+
constructor(uc) {
|
|
1367
|
+
this.uc = uc;
|
|
1368
|
+
}
|
|
1369
|
+
get epoch() {
|
|
1370
|
+
return this.uc.epoch;
|
|
1371
|
+
}
|
|
1372
|
+
/**
|
|
1373
|
+
* Generate keypair, produce header + MAC, encode into PolyEncoder,
|
|
1374
|
+
* and return the first header chunk.
|
|
1375
|
+
*/
|
|
1376
|
+
sendHdrChunk(rng) {
|
|
1377
|
+
const [headerSent, hdr, mac] = this.uc.sendHeader(rng);
|
|
1378
|
+
const hdrPayload = concat(hdr, mac);
|
|
1379
|
+
const sendingHdr = PolyEncoder.encodeBytes(hdrPayload);
|
|
1380
|
+
const chunk = sendingHdr.nextChunk();
|
|
1381
|
+
return [new KeysSampled(headerSent, sendingHdr), chunk];
|
|
1382
|
+
}
|
|
1383
|
+
};
|
|
1384
|
+
/**
|
|
1385
|
+
* Header has been generated and encoding started. Still sending header chunks.
|
|
1386
|
+
* Can also begin receiving ct1 chunks.
|
|
1387
|
+
*/
|
|
1388
|
+
var KeysSampled = class {
|
|
1389
|
+
constructor(uc, sendingHdr) {
|
|
1390
|
+
this.uc = uc;
|
|
1391
|
+
this.sendingHdr = sendingHdr;
|
|
1392
|
+
}
|
|
1393
|
+
get epoch() {
|
|
1394
|
+
return this.uc.epoch;
|
|
1395
|
+
}
|
|
1396
|
+
/** Produce the next header chunk. */
|
|
1397
|
+
sendHdrChunk() {
|
|
1398
|
+
const chunk = this.sendingHdr.nextChunk();
|
|
1399
|
+
return [this, chunk];
|
|
1400
|
+
}
|
|
1401
|
+
/**
|
|
1402
|
+
* Receive a ct1 chunk from the send_ct peer.
|
|
1403
|
+
* This triggers sending the encapsulation key.
|
|
1404
|
+
*/
|
|
1405
|
+
recvCt1Chunk(epoch, chunk) {
|
|
1406
|
+
if (epoch !== this.uc.epoch) throw new SpqrError(`Epoch mismatch: expected ${this.uc.epoch}, got ${epoch}`, SpqrErrorCode.EpochOutOfRange);
|
|
1407
|
+
const receivingCt1 = PolyDecoder.create(CT1_SIZE);
|
|
1408
|
+
receivingCt1.addChunk(chunk);
|
|
1409
|
+
const [ekSent, ek] = this.uc.sendEk();
|
|
1410
|
+
return new HeaderSent$1(ekSent, PolyEncoder.encodeBytes(ek), receivingCt1);
|
|
1411
|
+
}
|
|
1412
|
+
};
|
|
1413
|
+
/**
|
|
1414
|
+
* Sending ek chunks while receiving ct1 chunks.
|
|
1415
|
+
*/
|
|
1416
|
+
var HeaderSent$1 = class {
|
|
1417
|
+
constructor(uc, sendingEk, receivingCt1) {
|
|
1418
|
+
this.uc = uc;
|
|
1419
|
+
this.sendingEk = sendingEk;
|
|
1420
|
+
this.receivingCt1 = receivingCt1;
|
|
1421
|
+
}
|
|
1422
|
+
get epoch() {
|
|
1423
|
+
return this.uc.epoch;
|
|
1424
|
+
}
|
|
1425
|
+
/** Produce the next ek chunk. */
|
|
1426
|
+
sendEkChunk() {
|
|
1427
|
+
const chunk = this.sendingEk.nextChunk();
|
|
1428
|
+
return [this, chunk];
|
|
1429
|
+
}
|
|
1430
|
+
/** Receive a ct1 chunk. Returns done when ct1 is fully decoded. */
|
|
1431
|
+
recvCt1Chunk(epoch, chunk) {
|
|
1432
|
+
if (epoch !== this.uc.epoch) throw new SpqrError(`Epoch mismatch: expected ${this.uc.epoch}, got ${epoch}`, SpqrErrorCode.EpochOutOfRange);
|
|
1433
|
+
this.receivingCt1.addChunk(chunk);
|
|
1434
|
+
const decoded = this.receivingCt1.decodedMessage();
|
|
1435
|
+
if (decoded !== null) return {
|
|
1436
|
+
done: true,
|
|
1437
|
+
state: new Ct1Received(this.uc.recvCt1(decoded), this.sendingEk)
|
|
1438
|
+
};
|
|
1439
|
+
return {
|
|
1440
|
+
done: false,
|
|
1441
|
+
state: this
|
|
1442
|
+
};
|
|
1443
|
+
}
|
|
1444
|
+
};
|
|
1445
|
+
/**
|
|
1446
|
+
* ct1 has been fully decoded. Still sending ek chunks.
|
|
1447
|
+
* Can begin receiving ct2 chunks.
|
|
1448
|
+
*/
|
|
1449
|
+
var Ct1Received = class {
|
|
1450
|
+
constructor(uc, sendingEk) {
|
|
1451
|
+
this.uc = uc;
|
|
1452
|
+
this.sendingEk = sendingEk;
|
|
1453
|
+
}
|
|
1454
|
+
get epoch() {
|
|
1455
|
+
return this.uc.epoch;
|
|
1456
|
+
}
|
|
1457
|
+
/** Produce the next ek chunk. */
|
|
1458
|
+
sendEkChunk() {
|
|
1459
|
+
const chunk = this.sendingEk.nextChunk();
|
|
1460
|
+
return [this, chunk];
|
|
1461
|
+
}
|
|
1462
|
+
/**
|
|
1463
|
+
* Receive a ct2 chunk. Creates the ct2 decoder.
|
|
1464
|
+
* ct2 payload is CT2_SIZE + MAC_SIZE = 160 bytes
|
|
1465
|
+
* (carries ct2 + mac).
|
|
1466
|
+
*/
|
|
1467
|
+
recvCt2Chunk(epoch, chunk) {
|
|
1468
|
+
if (epoch !== this.uc.epoch) throw new SpqrError(`Epoch mismatch: expected ${this.uc.epoch}, got ${epoch}`, SpqrErrorCode.EpochOutOfRange);
|
|
1469
|
+
const receivingCt2 = PolyDecoder.create(CT2_SIZE + MAC_SIZE);
|
|
1470
|
+
receivingCt2.addChunk(chunk);
|
|
1471
|
+
return new EkSentCt1Received$1(this.uc, receivingCt2);
|
|
1472
|
+
}
|
|
1473
|
+
};
|
|
1474
|
+
/**
|
|
1475
|
+
* Both ek sending and ct1 receiving are done. Now receiving ct2 chunks.
|
|
1476
|
+
* When ct2 is fully decoded, extract ct2 and mac, then perform decapsulation
|
|
1477
|
+
* to derive the epoch secret.
|
|
1478
|
+
*/
|
|
1479
|
+
var EkSentCt1Received$1 = class {
|
|
1480
|
+
constructor(uc, receivingCt2) {
|
|
1481
|
+
this.uc = uc;
|
|
1482
|
+
this.receivingCt2 = receivingCt2;
|
|
1483
|
+
}
|
|
1484
|
+
get epoch() {
|
|
1485
|
+
return this.uc.epoch;
|
|
1486
|
+
}
|
|
1487
|
+
/**
|
|
1488
|
+
* Receive a ct2 chunk. Returns done when ct2 is fully decoded and
|
|
1489
|
+
* epoch secret is derived.
|
|
1490
|
+
*/
|
|
1491
|
+
recvCt2Chunk(epoch, chunk) {
|
|
1492
|
+
if (epoch !== this.uc.epoch) throw new SpqrError(`Epoch mismatch: expected ${this.uc.epoch}, got ${epoch}`, SpqrErrorCode.EpochOutOfRange);
|
|
1493
|
+
this.receivingCt2.addChunk(chunk);
|
|
1494
|
+
const decoded = this.receivingCt2.decodedMessage();
|
|
1495
|
+
if (decoded !== null) {
|
|
1496
|
+
const ct2 = decoded.slice(0, CT2_SIZE);
|
|
1497
|
+
const mac = decoded.slice(CT2_SIZE);
|
|
1498
|
+
const result = this.uc.recvCt2(ct2, mac);
|
|
1499
|
+
const nextUcSendCt = new NoHeaderReceived$1(result.nextEpoch, result.auth);
|
|
1500
|
+
const receivingHdr = PolyDecoder.create(HEADER_SIZE + MAC_SIZE);
|
|
1501
|
+
return {
|
|
1502
|
+
done: true,
|
|
1503
|
+
state: createSendCtNoHeaderReceived(nextUcSendCt, receivingHdr),
|
|
1504
|
+
epochSecret: result.epochSecret
|
|
1505
|
+
};
|
|
1506
|
+
}
|
|
1507
|
+
return {
|
|
1508
|
+
done: false,
|
|
1509
|
+
state: this
|
|
1510
|
+
};
|
|
1511
|
+
}
|
|
1512
|
+
};
|
|
1513
|
+
/** @internal Set by the chunked index module to break circular imports. */
|
|
1514
|
+
let createSendCtNoHeaderReceived;
|
|
1515
|
+
/** @internal Called by the index module to wire up the factory. */
|
|
1516
|
+
function _setCreateSendCtNoHeaderReceived(factory) {
|
|
1517
|
+
createSendCtNoHeaderReceived = factory;
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
//#endregion
|
|
1521
|
+
//#region src/v1/unchunked/send-ek.ts
|
|
1522
|
+
const SCKA_KEY_LABEL = new TextEncoder().encode(LABEL_SCKA_KEY);
|
|
1523
|
+
/**
|
|
1524
|
+
* Derive the epoch secret from the KEM shared secret.
|
|
1525
|
+
*
|
|
1526
|
+
* HKDF-SHA256(ikm=sharedSecret, salt=ZERO_SALT,
|
|
1527
|
+
* info=LABEL_SCKA_KEY || epoch_be8, length=32)
|
|
1528
|
+
*
|
|
1529
|
+
* Matches Rust: info = [b"Signal_PQCKA_V1_MLKEM768:SCKA Key", epoch.to_be_bytes()].concat()
|
|
1530
|
+
*/
|
|
1531
|
+
function deriveEpochSecret(epoch, sharedSecret) {
|
|
1532
|
+
return {
|
|
1533
|
+
epoch,
|
|
1534
|
+
secret: hkdfSha256(sharedSecret, ZERO_SALT, concat(SCKA_KEY_LABEL, bigintToBE8(epoch)), 32)
|
|
1535
|
+
};
|
|
1536
|
+
}
|
|
1537
|
+
/**
|
|
1538
|
+
* Initial send_ek state. No keypair has been generated yet.
|
|
1539
|
+
*/
|
|
1540
|
+
var KeysUnsampled = class {
|
|
1541
|
+
constructor(epoch, auth) {
|
|
1542
|
+
this.epoch = epoch;
|
|
1543
|
+
this.auth = auth;
|
|
1544
|
+
}
|
|
1545
|
+
/**
|
|
1546
|
+
* Generate an ML-KEM-768 keypair and produce the header + MAC.
|
|
1547
|
+
*
|
|
1548
|
+
* @param rng - Random byte generator
|
|
1549
|
+
* @returns [nextState, hdr, hdrMac]
|
|
1550
|
+
*/
|
|
1551
|
+
sendHeader(rng) {
|
|
1552
|
+
const keys = generate(rng);
|
|
1553
|
+
const mac = this.auth.macHdr(this.epoch, keys.hdr);
|
|
1554
|
+
return [
|
|
1555
|
+
new HeaderSent(this.epoch, this.auth, keys.ek, keys.dk),
|
|
1556
|
+
keys.hdr,
|
|
1557
|
+
mac
|
|
1558
|
+
];
|
|
1559
|
+
}
|
|
1560
|
+
};
|
|
1561
|
+
/**
|
|
1562
|
+
* The header has been sent to the peer. Ready to send the encapsulation key.
|
|
1563
|
+
*/
|
|
1564
|
+
var HeaderSent = class {
|
|
1565
|
+
constructor(epoch, auth, ek, dk) {
|
|
1566
|
+
this.epoch = epoch;
|
|
1567
|
+
this.auth = auth;
|
|
1568
|
+
this.ek = ek;
|
|
1569
|
+
this.dk = dk;
|
|
1570
|
+
}
|
|
1571
|
+
/**
|
|
1572
|
+
* Produce the encapsulation key to send to the peer.
|
|
1573
|
+
*
|
|
1574
|
+
* @returns [nextState, ek]
|
|
1575
|
+
*/
|
|
1576
|
+
sendEk() {
|
|
1577
|
+
return [new EkSent(this.epoch, this.auth, this.dk), this.ek];
|
|
1578
|
+
}
|
|
1579
|
+
};
|
|
1580
|
+
/**
|
|
1581
|
+
* Both the header and encapsulation key have been sent.
|
|
1582
|
+
* Waiting to receive ct1 from the send_ct peer.
|
|
1583
|
+
*/
|
|
1584
|
+
var EkSent = class {
|
|
1585
|
+
constructor(epoch, auth, dk) {
|
|
1586
|
+
this.epoch = epoch;
|
|
1587
|
+
this.auth = auth;
|
|
1588
|
+
this.dk = dk;
|
|
1589
|
+
}
|
|
1590
|
+
/**
|
|
1591
|
+
* Receive the first ciphertext fragment from the peer.
|
|
1592
|
+
*
|
|
1593
|
+
* @param ct1 - The 960-byte first ciphertext fragment
|
|
1594
|
+
* @returns Next state
|
|
1595
|
+
*/
|
|
1596
|
+
recvCt1(ct1) {
|
|
1597
|
+
return new EkSentCt1Received(this.epoch, this.auth, this.dk, ct1);
|
|
1598
|
+
}
|
|
1599
|
+
};
|
|
1600
|
+
/**
|
|
1601
|
+
* ct1 has been received. Waiting for ct2 to complete decapsulation.
|
|
1602
|
+
*/
|
|
1603
|
+
var EkSentCt1Received = class {
|
|
1604
|
+
constructor(epoch, auth, dk, ct1) {
|
|
1605
|
+
this.epoch = epoch;
|
|
1606
|
+
this.auth = auth;
|
|
1607
|
+
this.dk = dk;
|
|
1608
|
+
this.ct1 = ct1;
|
|
1609
|
+
}
|
|
1610
|
+
/**
|
|
1611
|
+
* Receive ct2 and MAC, perform decapsulation, verify the MAC,
|
|
1612
|
+
* and derive the epoch secret.
|
|
1613
|
+
*
|
|
1614
|
+
* The caller constructs the next send_ct::NoHeaderReceived state from
|
|
1615
|
+
* the returned nextEpoch and auth.
|
|
1616
|
+
*
|
|
1617
|
+
* @param ct2 - The 128-byte second ciphertext fragment
|
|
1618
|
+
* @param mac - The 32-byte HMAC-SHA256 MAC over the full ciphertext
|
|
1619
|
+
* @returns Result containing next epoch, updated auth, and epoch secret
|
|
1620
|
+
* @throws {AuthenticatorError} If the ciphertext MAC is invalid
|
|
1621
|
+
*/
|
|
1622
|
+
recvCt2(ct2, mac) {
|
|
1623
|
+
const sharedSecret = decaps(this.dk, this.ct1, ct2);
|
|
1624
|
+
const epochSecret = deriveEpochSecret(this.epoch, sharedSecret);
|
|
1625
|
+
const auth = this.auth.clone();
|
|
1626
|
+
auth.update(this.epoch, epochSecret.secret);
|
|
1627
|
+
const fullCt = concat(this.ct1, ct2);
|
|
1628
|
+
auth.verifyCt(this.epoch, fullCt, mac);
|
|
1629
|
+
return {
|
|
1630
|
+
nextEpoch: this.epoch + 1n,
|
|
1631
|
+
auth,
|
|
1632
|
+
epochSecret
|
|
1633
|
+
};
|
|
1634
|
+
}
|
|
1635
|
+
};
|
|
1636
|
+
|
|
1637
|
+
//#endregion
|
|
1638
|
+
//#region src/v1/chunked/send-ct.ts
|
|
1639
|
+
/**
|
|
1640
|
+
* Copyright © 2025 Signal Messenger, LLC
|
|
1641
|
+
* Copyright © 2026 Parity Technologies
|
|
1642
|
+
*
|
|
1643
|
+
* Chunked send_ct state machine for SPQR V1.
|
|
1644
|
+
*
|
|
1645
|
+
* Wraps the unchunked send_ct states with PolyEncoder/PolyDecoder erasure
|
|
1646
|
+
* coding, enabling chunk-by-chunk data transfer.
|
|
1647
|
+
*
|
|
1648
|
+
* True incremental ML-KEM (Phase 9):
|
|
1649
|
+
* - send_ct produces REAL ct1 chunks from the start.
|
|
1650
|
+
* - ct2 payload carries only ct2(128) + mac(32) = 160 bytes.
|
|
1651
|
+
* - Epoch secret is derived in sendCt1 (when encaps1 is performed).
|
|
1652
|
+
*
|
|
1653
|
+
* States:
|
|
1654
|
+
* NoHeaderReceived -- recvHdrChunk(epoch, chunk) --> NoHeaderReceived | HeaderReceived
|
|
1655
|
+
* HeaderReceived -- sendCt1Chunk(rng) --> [Ct1Sampled, Chunk, EpochSecret]
|
|
1656
|
+
* Ct1Sampled -- sendCt1Chunk() --> [Ct1Sampled, Chunk]
|
|
1657
|
+
* -- recvEkChunk(epoch, chunk, ct1Ack) --> 4 outcomes
|
|
1658
|
+
* EkReceivedCt1Sampled -- sendCt1Chunk() --> [EkReceivedCt1Sampled, Chunk]
|
|
1659
|
+
* -- recvCt1Ack(epoch) --> Ct2Sampled
|
|
1660
|
+
* Ct1Acknowledged -- recvEkChunk(epoch, chunk) --> Ct1Acknowledged | Ct2Sampled
|
|
1661
|
+
* Ct2Sampled -- sendCt2Chunk() --> [Ct2Sampled, Chunk]
|
|
1662
|
+
* -- recvNextEpoch(epoch) --> sendEk.KeysUnsampled
|
|
1663
|
+
*/
|
|
1664
|
+
/**
|
|
1665
|
+
* Waiting to receive header chunks from the send_ek peer.
|
|
1666
|
+
*/
|
|
1667
|
+
var NoHeaderReceived = class NoHeaderReceived {
|
|
1668
|
+
constructor(uc, receivingHdr) {
|
|
1669
|
+
this.uc = uc;
|
|
1670
|
+
this.receivingHdr = receivingHdr;
|
|
1671
|
+
}
|
|
1672
|
+
get epoch() {
|
|
1673
|
+
return this.uc.epoch;
|
|
1674
|
+
}
|
|
1675
|
+
/**
|
|
1676
|
+
* Create the initial NoHeaderReceived from an auth key.
|
|
1677
|
+
*/
|
|
1678
|
+
static create(authKey) {
|
|
1679
|
+
const auth = Authenticator.create(authKey, 1n);
|
|
1680
|
+
return new NoHeaderReceived(new NoHeaderReceived$1(1n, auth), PolyDecoder.create(HEADER_SIZE + MAC_SIZE));
|
|
1681
|
+
}
|
|
1682
|
+
/** Receive a header chunk. Returns done when header is fully decoded. */
|
|
1683
|
+
recvHdrChunk(epoch, chunk) {
|
|
1684
|
+
if (epoch !== this.uc.epoch) throw new SpqrError(`Epoch mismatch: expected ${this.uc.epoch}, got ${epoch}`, SpqrErrorCode.EpochOutOfRange);
|
|
1685
|
+
this.receivingHdr.addChunk(chunk);
|
|
1686
|
+
const decoded = this.receivingHdr.decodedMessage();
|
|
1687
|
+
if (decoded !== null) {
|
|
1688
|
+
const hdr = decoded.slice(0, HEADER_SIZE);
|
|
1689
|
+
const mac = decoded.slice(HEADER_SIZE);
|
|
1690
|
+
return {
|
|
1691
|
+
done: true,
|
|
1692
|
+
state: new HeaderReceived(this.uc.recvHeader(epoch, hdr, mac), PolyDecoder.create(EK_SIZE))
|
|
1693
|
+
};
|
|
1694
|
+
}
|
|
1695
|
+
return {
|
|
1696
|
+
done: false,
|
|
1697
|
+
state: this
|
|
1698
|
+
};
|
|
1699
|
+
}
|
|
1700
|
+
};
|
|
1701
|
+
/**
|
|
1702
|
+
* Header has been received and verified. Ready to produce ct1 chunks.
|
|
1703
|
+
*/
|
|
1704
|
+
var HeaderReceived = class {
|
|
1705
|
+
constructor(uc, receivingEk) {
|
|
1706
|
+
this.uc = uc;
|
|
1707
|
+
this.receivingEk = receivingEk;
|
|
1708
|
+
}
|
|
1709
|
+
get epoch() {
|
|
1710
|
+
return this.uc.epoch;
|
|
1711
|
+
}
|
|
1712
|
+
/**
|
|
1713
|
+
* Generate encapsulation randomness, produce REAL ct1, encode it,
|
|
1714
|
+
* and return the first ct1 chunk.
|
|
1715
|
+
*
|
|
1716
|
+
* Returns [Ct1Sampled, Chunk, EpochSecret] -- real epoch secret.
|
|
1717
|
+
*/
|
|
1718
|
+
sendCt1Chunk(rng) {
|
|
1719
|
+
const [ct1Sent, realCt1, epochSecret] = this.uc.sendCt1(rng);
|
|
1720
|
+
const sendingCt1 = PolyEncoder.encodeBytes(realCt1);
|
|
1721
|
+
const chunk = sendingCt1.nextChunk();
|
|
1722
|
+
return [
|
|
1723
|
+
new Ct1Sampled(ct1Sent, sendingCt1, this.receivingEk),
|
|
1724
|
+
chunk,
|
|
1725
|
+
epochSecret
|
|
1726
|
+
];
|
|
1727
|
+
}
|
|
1728
|
+
};
|
|
1729
|
+
/**
|
|
1730
|
+
* Real ct1 has been produced and encoding started.
|
|
1731
|
+
* Sending ct1 chunks while receiving ek chunks.
|
|
1732
|
+
*/
|
|
1733
|
+
var Ct1Sampled = class {
|
|
1734
|
+
constructor(uc, sendingCt1, receivingEk) {
|
|
1735
|
+
this.uc = uc;
|
|
1736
|
+
this.sendingCt1 = sendingCt1;
|
|
1737
|
+
this.receivingEk = receivingEk;
|
|
1738
|
+
}
|
|
1739
|
+
get epoch() {
|
|
1740
|
+
return this.uc.epoch;
|
|
1741
|
+
}
|
|
1742
|
+
/** Produce the next ct1 chunk. */
|
|
1743
|
+
sendCt1Chunk() {
|
|
1744
|
+
const chunk = this.sendingCt1.nextChunk();
|
|
1745
|
+
return [this, chunk];
|
|
1746
|
+
}
|
|
1747
|
+
/**
|
|
1748
|
+
* Receive an ek chunk, with optional ct1 acknowledgement from peer.
|
|
1749
|
+
*
|
|
1750
|
+
* Four possible outcomes depending on whether ek is complete and
|
|
1751
|
+
* whether the peer acknowledged ct1:
|
|
1752
|
+
* - Both: done (Ct2Sampled + epochSecret)
|
|
1753
|
+
* - ek complete, no ack: stillSending (EkReceivedCt1Sampled + epochSecret)
|
|
1754
|
+
* - ek incomplete, ack: stillReceiving (Ct1Acknowledged)
|
|
1755
|
+
* - Neither: stillReceivingStillSending (Ct1Sampled)
|
|
1756
|
+
*/
|
|
1757
|
+
recvEkChunk(epoch, chunk, ct1Ack) {
|
|
1758
|
+
if (epoch !== this.uc.epoch) throw new SpqrError(`Epoch mismatch: expected ${this.uc.epoch}, got ${epoch}`, SpqrErrorCode.EpochOutOfRange);
|
|
1759
|
+
this.receivingEk.addChunk(chunk);
|
|
1760
|
+
const ekDecoded = this.receivingEk.decodedMessage();
|
|
1761
|
+
if (ekDecoded !== null && ct1Ack) {
|
|
1762
|
+
const { state: ct2SentUc, ct2, mac } = this.uc.recvEk(ekDecoded).sendCt2();
|
|
1763
|
+
const ct2Payload = concat(ct2, mac);
|
|
1764
|
+
return {
|
|
1765
|
+
tag: "done",
|
|
1766
|
+
state: new Ct2Sampled(ct2SentUc, PolyEncoder.encodeBytes(ct2Payload)),
|
|
1767
|
+
epochSecret: null
|
|
1768
|
+
};
|
|
1769
|
+
}
|
|
1770
|
+
if (ekDecoded !== null && !ct1Ack) return {
|
|
1771
|
+
tag: "stillSending",
|
|
1772
|
+
state: new EkReceivedCt1Sampled(this.uc.recvEk(ekDecoded), this.sendingCt1),
|
|
1773
|
+
epochSecret: null
|
|
1774
|
+
};
|
|
1775
|
+
if (ekDecoded === null && ct1Ack) return {
|
|
1776
|
+
tag: "stillReceiving",
|
|
1777
|
+
state: new Ct1Acknowledged(this.uc, this.receivingEk)
|
|
1778
|
+
};
|
|
1779
|
+
return {
|
|
1780
|
+
tag: "stillReceivingStillSending",
|
|
1781
|
+
state: this
|
|
1782
|
+
};
|
|
1783
|
+
}
|
|
1784
|
+
};
|
|
1785
|
+
/**
|
|
1786
|
+
* ek has been received and validated.
|
|
1787
|
+
* Still sending ct1 chunks, waiting for ct1 acknowledgement.
|
|
1788
|
+
*/
|
|
1789
|
+
var EkReceivedCt1Sampled = class {
|
|
1790
|
+
constructor(uc, sendingCt1) {
|
|
1791
|
+
this.uc = uc;
|
|
1792
|
+
this.sendingCt1 = sendingCt1;
|
|
1793
|
+
}
|
|
1794
|
+
get epoch() {
|
|
1795
|
+
return this.uc.epoch;
|
|
1796
|
+
}
|
|
1797
|
+
/** Produce the next ct1 chunk. */
|
|
1798
|
+
sendCt1Chunk() {
|
|
1799
|
+
const chunk = this.sendingCt1.nextChunk();
|
|
1800
|
+
return [this, chunk];
|
|
1801
|
+
}
|
|
1802
|
+
/** Peer has acknowledged ct1. Produce ct2 and encode it. */
|
|
1803
|
+
recvCt1Ack(epoch) {
|
|
1804
|
+
if (epoch !== this.uc.epoch) throw new SpqrError(`Epoch mismatch: expected ${this.uc.epoch}, got ${epoch}`, SpqrErrorCode.EpochOutOfRange);
|
|
1805
|
+
const { state: ct2SentUc, ct2, mac } = this.uc.sendCt2();
|
|
1806
|
+
const ct2Payload = concat(ct2, mac);
|
|
1807
|
+
return new Ct2Sampled(ct2SentUc, PolyEncoder.encodeBytes(ct2Payload));
|
|
1808
|
+
}
|
|
1809
|
+
};
|
|
1810
|
+
/**
|
|
1811
|
+
* ct1 has been acknowledged by the peer. Still receiving ek chunks.
|
|
1812
|
+
* When ek arrives, validate it and produce ct2.
|
|
1813
|
+
*/
|
|
1814
|
+
var Ct1Acknowledged = class {
|
|
1815
|
+
constructor(uc, receivingEk) {
|
|
1816
|
+
this.uc = uc;
|
|
1817
|
+
this.receivingEk = receivingEk;
|
|
1818
|
+
}
|
|
1819
|
+
get epoch() {
|
|
1820
|
+
return this.uc.epoch;
|
|
1821
|
+
}
|
|
1822
|
+
/** Receive an ek chunk. Returns done when ek is fully decoded. */
|
|
1823
|
+
recvEkChunk(epoch, chunk) {
|
|
1824
|
+
if (epoch !== this.uc.epoch) throw new SpqrError(`Epoch mismatch: expected ${this.uc.epoch}, got ${epoch}`, SpqrErrorCode.EpochOutOfRange);
|
|
1825
|
+
this.receivingEk.addChunk(chunk);
|
|
1826
|
+
const decoded = this.receivingEk.decodedMessage();
|
|
1827
|
+
if (decoded !== null) {
|
|
1828
|
+
const { state: ct2SentUc, ct2, mac } = this.uc.recvEk(decoded).sendCt2();
|
|
1829
|
+
const ct2Payload = concat(ct2, mac);
|
|
1830
|
+
return {
|
|
1831
|
+
done: true,
|
|
1832
|
+
state: new Ct2Sampled(ct2SentUc, PolyEncoder.encodeBytes(ct2Payload)),
|
|
1833
|
+
epochSecret: null
|
|
1834
|
+
};
|
|
1835
|
+
}
|
|
1836
|
+
return {
|
|
1837
|
+
done: false,
|
|
1838
|
+
state: this
|
|
1839
|
+
};
|
|
1840
|
+
}
|
|
1841
|
+
};
|
|
1842
|
+
/**
|
|
1843
|
+
* ct2 payload (ct2 + mac) has been encoded. Sending ct2 chunks.
|
|
1844
|
+
* Terminal for this epoch once all chunks sent and next epoch begins.
|
|
1845
|
+
*/
|
|
1846
|
+
var Ct2Sampled = class {
|
|
1847
|
+
constructor(uc, sendingCt2) {
|
|
1848
|
+
this.uc = uc;
|
|
1849
|
+
this.sendingCt2 = sendingCt2;
|
|
1850
|
+
}
|
|
1851
|
+
get epoch() {
|
|
1852
|
+
return this.uc.epoch;
|
|
1853
|
+
}
|
|
1854
|
+
/** Produce the next ct2 chunk. */
|
|
1855
|
+
sendCt2Chunk() {
|
|
1856
|
+
const chunk = this.sendingCt2.nextChunk();
|
|
1857
|
+
return [this, chunk];
|
|
1858
|
+
}
|
|
1859
|
+
/**
|
|
1860
|
+
* Transition to the next epoch's send_ek KeysUnsampled.
|
|
1861
|
+
*/
|
|
1862
|
+
recvNextEpoch(_epoch) {
|
|
1863
|
+
const nextEpoch = this.uc.nextEpoch;
|
|
1864
|
+
const nextUc = new KeysUnsampled(nextEpoch, this.uc.auth);
|
|
1865
|
+
return new KeysUnsampled$1(nextUc);
|
|
1866
|
+
}
|
|
1867
|
+
};
|
|
1868
|
+
|
|
1869
|
+
//#endregion
|
|
1870
|
+
//#region src/v1/chunked/states.ts
|
|
1871
|
+
function getEpoch(s) {
|
|
1872
|
+
return s.state.epoch;
|
|
1873
|
+
}
|
|
1874
|
+
/**
|
|
1875
|
+
* Initialize Alice (send_ek side) at epoch 1.
|
|
1876
|
+
*/
|
|
1877
|
+
function initA(authKey) {
|
|
1878
|
+
const auth = Authenticator.create(authKey, 1n);
|
|
1879
|
+
const ucState = new KeysUnsampled(1n, auth);
|
|
1880
|
+
return {
|
|
1881
|
+
tag: "keysUnsampled",
|
|
1882
|
+
state: new KeysUnsampled$1(ucState)
|
|
1883
|
+
};
|
|
1884
|
+
}
|
|
1885
|
+
/**
|
|
1886
|
+
* Initialize Bob (send_ct side) at epoch 1.
|
|
1887
|
+
*/
|
|
1888
|
+
function initB(authKey) {
|
|
1889
|
+
return {
|
|
1890
|
+
tag: "noHeaderReceived",
|
|
1891
|
+
state: NoHeaderReceived.create(authKey)
|
|
1892
|
+
};
|
|
1893
|
+
}
|
|
1894
|
+
/**
|
|
1895
|
+
* Produce the next outgoing message from the current state.
|
|
1896
|
+
*/
|
|
1897
|
+
function send$1(current, rng) {
|
|
1898
|
+
const epoch = getEpoch(current);
|
|
1899
|
+
switch (current.tag) {
|
|
1900
|
+
case "keysUnsampled": {
|
|
1901
|
+
const [next, chunk] = current.state.sendHdrChunk(rng);
|
|
1902
|
+
return {
|
|
1903
|
+
msg: {
|
|
1904
|
+
epoch,
|
|
1905
|
+
payload: {
|
|
1906
|
+
type: "hdr",
|
|
1907
|
+
chunk
|
|
1908
|
+
}
|
|
1909
|
+
},
|
|
1910
|
+
key: null,
|
|
1911
|
+
state: {
|
|
1912
|
+
tag: "keysSampled",
|
|
1913
|
+
state: next
|
|
1914
|
+
}
|
|
1915
|
+
};
|
|
1916
|
+
}
|
|
1917
|
+
case "keysSampled": {
|
|
1918
|
+
const [next, chunk] = current.state.sendHdrChunk();
|
|
1919
|
+
return {
|
|
1920
|
+
msg: {
|
|
1921
|
+
epoch,
|
|
1922
|
+
payload: {
|
|
1923
|
+
type: "hdr",
|
|
1924
|
+
chunk
|
|
1925
|
+
}
|
|
1926
|
+
},
|
|
1927
|
+
key: null,
|
|
1928
|
+
state: {
|
|
1929
|
+
tag: "keysSampled",
|
|
1930
|
+
state: next
|
|
1931
|
+
}
|
|
1932
|
+
};
|
|
1933
|
+
}
|
|
1934
|
+
case "headerSent": {
|
|
1935
|
+
const [next, chunk] = current.state.sendEkChunk();
|
|
1936
|
+
return {
|
|
1937
|
+
msg: {
|
|
1938
|
+
epoch,
|
|
1939
|
+
payload: {
|
|
1940
|
+
type: "ek",
|
|
1941
|
+
chunk
|
|
1942
|
+
}
|
|
1943
|
+
},
|
|
1944
|
+
key: null,
|
|
1945
|
+
state: {
|
|
1946
|
+
tag: "headerSent",
|
|
1947
|
+
state: next
|
|
1948
|
+
}
|
|
1949
|
+
};
|
|
1950
|
+
}
|
|
1951
|
+
case "ct1Received": {
|
|
1952
|
+
const [next, chunk] = current.state.sendEkChunk();
|
|
1953
|
+
return {
|
|
1954
|
+
msg: {
|
|
1955
|
+
epoch,
|
|
1956
|
+
payload: {
|
|
1957
|
+
type: "ekCt1Ack",
|
|
1958
|
+
chunk
|
|
1959
|
+
}
|
|
1960
|
+
},
|
|
1961
|
+
key: null,
|
|
1962
|
+
state: {
|
|
1963
|
+
tag: "ct1Received",
|
|
1964
|
+
state: next
|
|
1965
|
+
}
|
|
1966
|
+
};
|
|
1967
|
+
}
|
|
1968
|
+
case "ekSentCt1Received": return {
|
|
1969
|
+
msg: {
|
|
1970
|
+
epoch,
|
|
1971
|
+
payload: { type: "ct1Ack" }
|
|
1972
|
+
},
|
|
1973
|
+
key: null,
|
|
1974
|
+
state: current
|
|
1975
|
+
};
|
|
1976
|
+
case "noHeaderReceived": return {
|
|
1977
|
+
msg: {
|
|
1978
|
+
epoch,
|
|
1979
|
+
payload: { type: "none" }
|
|
1980
|
+
},
|
|
1981
|
+
key: null,
|
|
1982
|
+
state: current
|
|
1983
|
+
};
|
|
1984
|
+
case "headerReceived": {
|
|
1985
|
+
const [next, chunk, epochSecret] = current.state.sendCt1Chunk(rng);
|
|
1986
|
+
return {
|
|
1987
|
+
msg: {
|
|
1988
|
+
epoch,
|
|
1989
|
+
payload: {
|
|
1990
|
+
type: "ct1",
|
|
1991
|
+
chunk
|
|
1992
|
+
}
|
|
1993
|
+
},
|
|
1994
|
+
key: epochSecret,
|
|
1995
|
+
state: {
|
|
1996
|
+
tag: "ct1Sampled",
|
|
1997
|
+
state: next
|
|
1998
|
+
}
|
|
1999
|
+
};
|
|
2000
|
+
}
|
|
2001
|
+
case "ct1Sampled": {
|
|
2002
|
+
const [next, chunk] = current.state.sendCt1Chunk();
|
|
2003
|
+
return {
|
|
2004
|
+
msg: {
|
|
2005
|
+
epoch,
|
|
2006
|
+
payload: {
|
|
2007
|
+
type: "ct1",
|
|
2008
|
+
chunk
|
|
2009
|
+
}
|
|
2010
|
+
},
|
|
2011
|
+
key: null,
|
|
2012
|
+
state: {
|
|
2013
|
+
tag: "ct1Sampled",
|
|
2014
|
+
state: next
|
|
2015
|
+
}
|
|
2016
|
+
};
|
|
2017
|
+
}
|
|
2018
|
+
case "ekReceivedCt1Sampled": {
|
|
2019
|
+
const [next, chunk] = current.state.sendCt1Chunk();
|
|
2020
|
+
return {
|
|
2021
|
+
msg: {
|
|
2022
|
+
epoch,
|
|
2023
|
+
payload: {
|
|
2024
|
+
type: "ct1",
|
|
2025
|
+
chunk
|
|
2026
|
+
}
|
|
2027
|
+
},
|
|
2028
|
+
key: null,
|
|
2029
|
+
state: {
|
|
2030
|
+
tag: "ekReceivedCt1Sampled",
|
|
2031
|
+
state: next
|
|
2032
|
+
}
|
|
2033
|
+
};
|
|
2034
|
+
}
|
|
2035
|
+
case "ct1Acknowledged": return {
|
|
2036
|
+
msg: {
|
|
2037
|
+
epoch,
|
|
2038
|
+
payload: { type: "none" }
|
|
2039
|
+
},
|
|
2040
|
+
key: null,
|
|
2041
|
+
state: current
|
|
2042
|
+
};
|
|
2043
|
+
case "ct2Sampled": {
|
|
2044
|
+
const [next, chunk] = current.state.sendCt2Chunk();
|
|
2045
|
+
return {
|
|
2046
|
+
msg: {
|
|
2047
|
+
epoch,
|
|
2048
|
+
payload: {
|
|
2049
|
+
type: "ct2",
|
|
2050
|
+
chunk
|
|
2051
|
+
}
|
|
2052
|
+
},
|
|
2053
|
+
key: null,
|
|
2054
|
+
state: {
|
|
2055
|
+
tag: "ct2Sampled",
|
|
2056
|
+
state: next
|
|
2057
|
+
}
|
|
2058
|
+
};
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
/**
|
|
2063
|
+
* Process an incoming message and transition the state.
|
|
2064
|
+
*/
|
|
2065
|
+
function recv$1(current, msg) {
|
|
2066
|
+
const stateEpoch = getEpoch(current);
|
|
2067
|
+
if (msg.epoch < stateEpoch) return {
|
|
2068
|
+
key: null,
|
|
2069
|
+
state: current
|
|
2070
|
+
};
|
|
2071
|
+
if (msg.epoch > stateEpoch) {
|
|
2072
|
+
if (current.tag === "ct2Sampled" && msg.epoch === stateEpoch + 1n) return {
|
|
2073
|
+
key: null,
|
|
2074
|
+
state: {
|
|
2075
|
+
tag: "keysUnsampled",
|
|
2076
|
+
state: current.state.recvNextEpoch(msg.epoch)
|
|
2077
|
+
}
|
|
2078
|
+
};
|
|
2079
|
+
throw new SpqrError(`Epoch too far ahead: state=${stateEpoch}, msg=${msg.epoch}`, SpqrErrorCode.EpochOutOfRange);
|
|
2080
|
+
}
|
|
2081
|
+
const payload = msg.payload;
|
|
2082
|
+
switch (current.tag) {
|
|
2083
|
+
case "keysUnsampled": return {
|
|
2084
|
+
key: null,
|
|
2085
|
+
state: current
|
|
2086
|
+
};
|
|
2087
|
+
case "keysSampled":
|
|
2088
|
+
if (payload.type === "ct1") return {
|
|
2089
|
+
key: null,
|
|
2090
|
+
state: {
|
|
2091
|
+
tag: "headerSent",
|
|
2092
|
+
state: current.state.recvCt1Chunk(msg.epoch, payload.chunk)
|
|
2093
|
+
}
|
|
2094
|
+
};
|
|
2095
|
+
return {
|
|
2096
|
+
key: null,
|
|
2097
|
+
state: current
|
|
2098
|
+
};
|
|
2099
|
+
case "headerSent":
|
|
2100
|
+
if (payload.type === "ct1") {
|
|
2101
|
+
const result = current.state.recvCt1Chunk(msg.epoch, payload.chunk);
|
|
2102
|
+
if (result.done) return {
|
|
2103
|
+
key: null,
|
|
2104
|
+
state: {
|
|
2105
|
+
tag: "ct1Received",
|
|
2106
|
+
state: result.state
|
|
2107
|
+
}
|
|
2108
|
+
};
|
|
2109
|
+
return {
|
|
2110
|
+
key: null,
|
|
2111
|
+
state: {
|
|
2112
|
+
tag: "headerSent",
|
|
2113
|
+
state: result.state
|
|
2114
|
+
}
|
|
2115
|
+
};
|
|
2116
|
+
}
|
|
2117
|
+
return {
|
|
2118
|
+
key: null,
|
|
2119
|
+
state: current
|
|
2120
|
+
};
|
|
2121
|
+
case "ct1Received":
|
|
2122
|
+
if (payload.type === "ct2") return {
|
|
2123
|
+
key: null,
|
|
2124
|
+
state: {
|
|
2125
|
+
tag: "ekSentCt1Received",
|
|
2126
|
+
state: current.state.recvCt2Chunk(msg.epoch, payload.chunk)
|
|
2127
|
+
}
|
|
2128
|
+
};
|
|
2129
|
+
return {
|
|
2130
|
+
key: null,
|
|
2131
|
+
state: current
|
|
2132
|
+
};
|
|
2133
|
+
case "ekSentCt1Received":
|
|
2134
|
+
if (payload.type === "ct2") {
|
|
2135
|
+
const result = current.state.recvCt2Chunk(msg.epoch, payload.chunk);
|
|
2136
|
+
if (result.done) return {
|
|
2137
|
+
key: result.epochSecret,
|
|
2138
|
+
state: {
|
|
2139
|
+
tag: "noHeaderReceived",
|
|
2140
|
+
state: result.state
|
|
2141
|
+
}
|
|
2142
|
+
};
|
|
2143
|
+
return {
|
|
2144
|
+
key: null,
|
|
2145
|
+
state: {
|
|
2146
|
+
tag: "ekSentCt1Received",
|
|
2147
|
+
state: result.state
|
|
2148
|
+
}
|
|
2149
|
+
};
|
|
2150
|
+
}
|
|
2151
|
+
return {
|
|
2152
|
+
key: null,
|
|
2153
|
+
state: current
|
|
2154
|
+
};
|
|
2155
|
+
case "noHeaderReceived":
|
|
2156
|
+
if (payload.type === "hdr") {
|
|
2157
|
+
const result = current.state.recvHdrChunk(msg.epoch, payload.chunk);
|
|
2158
|
+
if (result.done) return {
|
|
2159
|
+
key: null,
|
|
2160
|
+
state: {
|
|
2161
|
+
tag: "headerReceived",
|
|
2162
|
+
state: result.state
|
|
2163
|
+
}
|
|
2164
|
+
};
|
|
2165
|
+
return {
|
|
2166
|
+
key: null,
|
|
2167
|
+
state: {
|
|
2168
|
+
tag: "noHeaderReceived",
|
|
2169
|
+
state: result.state
|
|
2170
|
+
}
|
|
2171
|
+
};
|
|
2172
|
+
}
|
|
2173
|
+
return {
|
|
2174
|
+
key: null,
|
|
2175
|
+
state: current
|
|
2176
|
+
};
|
|
2177
|
+
case "headerReceived": return {
|
|
2178
|
+
key: null,
|
|
2179
|
+
state: current
|
|
2180
|
+
};
|
|
2181
|
+
case "ct1Sampled":
|
|
2182
|
+
if (payload.type === "ek") return mapCt1SampledResult(current.state.recvEkChunk(msg.epoch, payload.chunk, false));
|
|
2183
|
+
if (payload.type === "ekCt1Ack") return mapCt1SampledResult(current.state.recvEkChunk(msg.epoch, payload.chunk, true));
|
|
2184
|
+
return {
|
|
2185
|
+
key: null,
|
|
2186
|
+
state: current
|
|
2187
|
+
};
|
|
2188
|
+
case "ekReceivedCt1Sampled":
|
|
2189
|
+
if (payload.type === "ct1Ack" || payload.type === "ekCt1Ack") return {
|
|
2190
|
+
key: null,
|
|
2191
|
+
state: {
|
|
2192
|
+
tag: "ct2Sampled",
|
|
2193
|
+
state: current.state.recvCt1Ack(msg.epoch)
|
|
2194
|
+
}
|
|
2195
|
+
};
|
|
2196
|
+
return {
|
|
2197
|
+
key: null,
|
|
2198
|
+
state: current
|
|
2199
|
+
};
|
|
2200
|
+
case "ct1Acknowledged":
|
|
2201
|
+
if (payload.type === "ek" || payload.type === "ekCt1Ack") {
|
|
2202
|
+
const chunk = payload.type === "ek" ? payload.chunk : payload.chunk;
|
|
2203
|
+
const result = current.state.recvEkChunk(msg.epoch, chunk);
|
|
2204
|
+
if (result.done) return {
|
|
2205
|
+
key: result.epochSecret,
|
|
2206
|
+
state: {
|
|
2207
|
+
tag: "ct2Sampled",
|
|
2208
|
+
state: result.state
|
|
2209
|
+
}
|
|
2210
|
+
};
|
|
2211
|
+
return {
|
|
2212
|
+
key: null,
|
|
2213
|
+
state: {
|
|
2214
|
+
tag: "ct1Acknowledged",
|
|
2215
|
+
state: result.state
|
|
2216
|
+
}
|
|
2217
|
+
};
|
|
2218
|
+
}
|
|
2219
|
+
return {
|
|
2220
|
+
key: null,
|
|
2221
|
+
state: current
|
|
2222
|
+
};
|
|
2223
|
+
case "ct2Sampled": return {
|
|
2224
|
+
key: null,
|
|
2225
|
+
state: current
|
|
2226
|
+
};
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
function mapCt1SampledResult(result) {
|
|
2230
|
+
switch (result.tag) {
|
|
2231
|
+
case "done": return {
|
|
2232
|
+
key: result.epochSecret,
|
|
2233
|
+
state: {
|
|
2234
|
+
tag: "ct2Sampled",
|
|
2235
|
+
state: result.state
|
|
2236
|
+
}
|
|
2237
|
+
};
|
|
2238
|
+
case "stillSending": return {
|
|
2239
|
+
key: result.epochSecret,
|
|
2240
|
+
state: {
|
|
2241
|
+
tag: "ekReceivedCt1Sampled",
|
|
2242
|
+
state: result.state
|
|
2243
|
+
}
|
|
2244
|
+
};
|
|
2245
|
+
case "stillReceiving": return {
|
|
2246
|
+
key: null,
|
|
2247
|
+
state: {
|
|
2248
|
+
tag: "ct1Acknowledged",
|
|
2249
|
+
state: result.state
|
|
2250
|
+
}
|
|
2251
|
+
};
|
|
2252
|
+
case "stillReceivingStillSending": return {
|
|
2253
|
+
key: null,
|
|
2254
|
+
state: {
|
|
2255
|
+
tag: "ct1Sampled",
|
|
2256
|
+
state: result.state
|
|
2257
|
+
}
|
|
2258
|
+
};
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
//#endregion
|
|
2263
|
+
//#region src/v1/chunked/message.ts
|
|
2264
|
+
var MessageType = /* @__PURE__ */ function(MessageType) {
|
|
2265
|
+
MessageType[MessageType["None"] = 0] = "None";
|
|
2266
|
+
MessageType[MessageType["Hdr"] = 1] = "Hdr";
|
|
2267
|
+
MessageType[MessageType["Ek"] = 2] = "Ek";
|
|
2268
|
+
MessageType[MessageType["EkCt1Ack"] = 3] = "EkCt1Ack";
|
|
2269
|
+
MessageType[MessageType["Ct1Ack"] = 4] = "Ct1Ack";
|
|
2270
|
+
MessageType[MessageType["Ct1"] = 5] = "Ct1";
|
|
2271
|
+
MessageType[MessageType["Ct2"] = 6] = "Ct2";
|
|
2272
|
+
return MessageType;
|
|
2273
|
+
}(MessageType || {});
|
|
2274
|
+
/**
|
|
2275
|
+
* Encode a bigint as LEB128 varint into the output array.
|
|
2276
|
+
*/
|
|
2277
|
+
function encodeVarint(value, into) {
|
|
2278
|
+
let v = value;
|
|
2279
|
+
if (v < 0n) v = 0n;
|
|
2280
|
+
do {
|
|
2281
|
+
let byte = Number(v & 127n);
|
|
2282
|
+
v >>= 7n;
|
|
2283
|
+
if (v > 0n) byte |= 128;
|
|
2284
|
+
into.push(byte);
|
|
2285
|
+
} while (v > 0n);
|
|
2286
|
+
}
|
|
2287
|
+
/**
|
|
2288
|
+
* Decode a LEB128 varint from a Uint8Array at the given offset.
|
|
2289
|
+
* Updates offset.offset in place.
|
|
2290
|
+
*/
|
|
2291
|
+
function decodeVarint(from, at) {
|
|
2292
|
+
let result = 0n;
|
|
2293
|
+
let shift = 0n;
|
|
2294
|
+
while (shift < 70n) {
|
|
2295
|
+
if (at.offset >= from.length) throw new Error("Varint: unexpected end of data");
|
|
2296
|
+
const byte = from[at.offset++];
|
|
2297
|
+
result |= BigInt(byte & 127) << shift;
|
|
2298
|
+
if ((byte & 128) === 0) return result;
|
|
2299
|
+
shift += 7n;
|
|
2300
|
+
}
|
|
2301
|
+
throw new Error("Varint: too many bytes");
|
|
2302
|
+
}
|
|
2303
|
+
/**
|
|
2304
|
+
* Encode a number (u32) as LEB128 varint into the output array.
|
|
2305
|
+
*/
|
|
2306
|
+
function encodeVarint32(value, into) {
|
|
2307
|
+
let v = value >>> 0;
|
|
2308
|
+
do {
|
|
2309
|
+
let byte = v & 127;
|
|
2310
|
+
v >>>= 7;
|
|
2311
|
+
if (v > 0) byte |= 128;
|
|
2312
|
+
into.push(byte);
|
|
2313
|
+
} while (v > 0);
|
|
2314
|
+
}
|
|
2315
|
+
/**
|
|
2316
|
+
* Decode a LEB128 varint as a u32 number.
|
|
2317
|
+
*/
|
|
2318
|
+
function decodeVarint32(from, at) {
|
|
2319
|
+
let result = 0;
|
|
2320
|
+
let shift = 0;
|
|
2321
|
+
while (shift < 35) {
|
|
2322
|
+
if (at.offset >= from.length) throw new Error("Varint32: unexpected end of data");
|
|
2323
|
+
const byte = from[at.offset++];
|
|
2324
|
+
result |= (byte & 127) << shift;
|
|
2325
|
+
if ((byte & 128) === 0) return result >>> 0;
|
|
2326
|
+
shift += 7;
|
|
2327
|
+
}
|
|
2328
|
+
throw new Error("Varint32: too many bytes");
|
|
2329
|
+
}
|
|
2330
|
+
const CHUNK_DATA_SIZE = 32;
|
|
2331
|
+
/** Encode a chunk (index varint + 32 data bytes) into the output array. */
|
|
2332
|
+
function encodeChunk(chunk, into) {
|
|
2333
|
+
encodeVarint32(chunk.index, into);
|
|
2334
|
+
for (let i = 0; i < CHUNK_DATA_SIZE; i++) into.push(chunk.data[i]);
|
|
2335
|
+
}
|
|
2336
|
+
/** Decode a chunk from a Uint8Array at the given offset. */
|
|
2337
|
+
function decodeChunk(from, at) {
|
|
2338
|
+
const index = decodeVarint32(from, at);
|
|
2339
|
+
if (at.offset + CHUNK_DATA_SIZE > from.length || index > 65535) throw new Error("Chunk: invalid chunk (data too short or index exceeds u16)");
|
|
2340
|
+
const data = from.slice(at.offset, at.offset + CHUNK_DATA_SIZE);
|
|
2341
|
+
at.offset += CHUNK_DATA_SIZE;
|
|
2342
|
+
return {
|
|
2343
|
+
index,
|
|
2344
|
+
data
|
|
2345
|
+
};
|
|
2346
|
+
}
|
|
2347
|
+
/** Protocol version */
|
|
2348
|
+
const V1 = 1;
|
|
2349
|
+
/**
|
|
2350
|
+
* Serialize a Message with a given sequence index into binary wire format.
|
|
2351
|
+
*/
|
|
2352
|
+
function serializeMessage(msg, index) {
|
|
2353
|
+
const out = [];
|
|
2354
|
+
out.push(V1);
|
|
2355
|
+
encodeVarint(msg.epoch, out);
|
|
2356
|
+
encodeVarint32(index, out);
|
|
2357
|
+
const payload = msg.payload;
|
|
2358
|
+
switch (payload.type) {
|
|
2359
|
+
case "none":
|
|
2360
|
+
out.push(MessageType.None);
|
|
2361
|
+
break;
|
|
2362
|
+
case "hdr":
|
|
2363
|
+
out.push(MessageType.Hdr);
|
|
2364
|
+
encodeChunk(payload.chunk, out);
|
|
2365
|
+
break;
|
|
2366
|
+
case "ek":
|
|
2367
|
+
out.push(MessageType.Ek);
|
|
2368
|
+
encodeChunk(payload.chunk, out);
|
|
2369
|
+
break;
|
|
2370
|
+
case "ekCt1Ack":
|
|
2371
|
+
out.push(MessageType.EkCt1Ack);
|
|
2372
|
+
encodeChunk(payload.chunk, out);
|
|
2373
|
+
break;
|
|
2374
|
+
case "ct1Ack":
|
|
2375
|
+
out.push(MessageType.Ct1Ack);
|
|
2376
|
+
break;
|
|
2377
|
+
case "ct1":
|
|
2378
|
+
out.push(MessageType.Ct1);
|
|
2379
|
+
encodeChunk(payload.chunk, out);
|
|
2380
|
+
break;
|
|
2381
|
+
case "ct2":
|
|
2382
|
+
out.push(MessageType.Ct2);
|
|
2383
|
+
encodeChunk(payload.chunk, out);
|
|
2384
|
+
break;
|
|
2385
|
+
}
|
|
2386
|
+
return new Uint8Array(out);
|
|
2387
|
+
}
|
|
2388
|
+
/**
|
|
2389
|
+
* Deserialize a Message from binary wire format.
|
|
2390
|
+
*/
|
|
2391
|
+
function deserializeMessage(from) {
|
|
2392
|
+
const at = { offset: 0 };
|
|
2393
|
+
if (at.offset >= from.length) throw new Error("Message: empty data");
|
|
2394
|
+
const version = from[at.offset++];
|
|
2395
|
+
if (version !== V1) throw new Error(`Message: unsupported version ${version}`);
|
|
2396
|
+
const epoch = decodeVarint(from, at);
|
|
2397
|
+
if (epoch === 0n) throw new Error("Message: epoch must be > 0");
|
|
2398
|
+
const index = decodeVarint32(from, at);
|
|
2399
|
+
if (at.offset >= from.length) throw new Error("Message: missing message type");
|
|
2400
|
+
const msgType = from[at.offset++];
|
|
2401
|
+
let payload;
|
|
2402
|
+
switch (msgType) {
|
|
2403
|
+
case MessageType.None:
|
|
2404
|
+
payload = { type: "none" };
|
|
2405
|
+
break;
|
|
2406
|
+
case MessageType.Hdr:
|
|
2407
|
+
payload = {
|
|
2408
|
+
type: "hdr",
|
|
2409
|
+
chunk: decodeChunk(from, at)
|
|
2410
|
+
};
|
|
2411
|
+
break;
|
|
2412
|
+
case MessageType.Ek:
|
|
2413
|
+
payload = {
|
|
2414
|
+
type: "ek",
|
|
2415
|
+
chunk: decodeChunk(from, at)
|
|
2416
|
+
};
|
|
2417
|
+
break;
|
|
2418
|
+
case MessageType.EkCt1Ack:
|
|
2419
|
+
payload = {
|
|
2420
|
+
type: "ekCt1Ack",
|
|
2421
|
+
chunk: decodeChunk(from, at)
|
|
2422
|
+
};
|
|
2423
|
+
break;
|
|
2424
|
+
case MessageType.Ct1Ack:
|
|
2425
|
+
payload = { type: "ct1Ack" };
|
|
2426
|
+
break;
|
|
2427
|
+
case MessageType.Ct1:
|
|
2428
|
+
payload = {
|
|
2429
|
+
type: "ct1",
|
|
2430
|
+
chunk: decodeChunk(from, at)
|
|
2431
|
+
};
|
|
2432
|
+
break;
|
|
2433
|
+
case MessageType.Ct2:
|
|
2434
|
+
payload = {
|
|
2435
|
+
type: "ct2",
|
|
2436
|
+
chunk: decodeChunk(from, at)
|
|
2437
|
+
};
|
|
2438
|
+
break;
|
|
2439
|
+
default: throw new Error(`Message: unknown message type ${msgType}`);
|
|
2440
|
+
}
|
|
2441
|
+
return {
|
|
2442
|
+
msg: {
|
|
2443
|
+
epoch,
|
|
2444
|
+
payload
|
|
2445
|
+
},
|
|
2446
|
+
index,
|
|
2447
|
+
bytesRead: at.offset
|
|
2448
|
+
};
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
//#endregion
|
|
2452
|
+
//#region src/v1/chunked/index.ts
|
|
2453
|
+
/**
|
|
2454
|
+
* Copyright © 2025 Signal Messenger, LLC
|
|
2455
|
+
* Copyright © 2026 Parity Technologies
|
|
2456
|
+
*
|
|
2457
|
+
* Chunked state machine for SPQR V1.
|
|
2458
|
+
*
|
|
2459
|
+
* Provides erasure-coded chunk-by-chunk data transfer wrapping the
|
|
2460
|
+
* unchunked V1 state machine.
|
|
2461
|
+
*/
|
|
2462
|
+
_setCreateSendCtNoHeaderReceived((uc, receivingHdr) => new NoHeaderReceived(uc, receivingHdr));
|
|
2463
|
+
|
|
2464
|
+
//#endregion
|
|
2465
|
+
//#region src/v1/chunked/serialize.ts
|
|
2466
|
+
/**
|
|
2467
|
+
* Serialize a runtime States object into PbV1State for protobuf encoding.
|
|
2468
|
+
* Stores the epoch as field 12 so it can be recovered on deserialization.
|
|
2469
|
+
*/
|
|
2470
|
+
function statesToPb(s) {
|
|
2471
|
+
const epoch = s.state.epoch;
|
|
2472
|
+
return {
|
|
2473
|
+
innerState: chunkedStateToPb(s),
|
|
2474
|
+
epoch
|
|
2475
|
+
};
|
|
2476
|
+
}
|
|
2477
|
+
function chunkedStateToPb(s) {
|
|
2478
|
+
switch (s.tag) {
|
|
2479
|
+
case "keysUnsampled": return {
|
|
2480
|
+
type: "keysUnsampled",
|
|
2481
|
+
uc: { auth: s.state.uc.auth.toProto() }
|
|
2482
|
+
};
|
|
2483
|
+
case "keysSampled": {
|
|
2484
|
+
const st = s.state;
|
|
2485
|
+
return {
|
|
2486
|
+
type: "keysSampled",
|
|
2487
|
+
uc: {
|
|
2488
|
+
auth: st.uc.auth.toProto(),
|
|
2489
|
+
ek: Uint8Array.from(st.uc.ek),
|
|
2490
|
+
dk: Uint8Array.from(st.uc.dk),
|
|
2491
|
+
hdr: new Uint8Array(0),
|
|
2492
|
+
hdrMac: new Uint8Array(0)
|
|
2493
|
+
},
|
|
2494
|
+
sendingHdr: st.sendingHdr.toProto()
|
|
2495
|
+
};
|
|
2496
|
+
}
|
|
2497
|
+
case "headerSent": {
|
|
2498
|
+
const st = s.state;
|
|
2499
|
+
return {
|
|
2500
|
+
type: "headerSent",
|
|
2501
|
+
uc: {
|
|
2502
|
+
auth: st.uc.auth.toProto(),
|
|
2503
|
+
ek: new Uint8Array(0),
|
|
2504
|
+
dk: Uint8Array.from(st.uc.dk)
|
|
2505
|
+
},
|
|
2506
|
+
sendingEk: st.sendingEk.toProto(),
|
|
2507
|
+
receivingCt1: st.receivingCt1.toProto()
|
|
2508
|
+
};
|
|
2509
|
+
}
|
|
2510
|
+
case "ct1Received": {
|
|
2511
|
+
const st = s.state;
|
|
2512
|
+
return {
|
|
2513
|
+
type: "ct1Received",
|
|
2514
|
+
uc: {
|
|
2515
|
+
auth: st.uc.auth.toProto(),
|
|
2516
|
+
dk: Uint8Array.from(st.uc.dk),
|
|
2517
|
+
ct1: Uint8Array.from(st.uc.ct1)
|
|
2518
|
+
},
|
|
2519
|
+
sendingEk: st.sendingEk.toProto()
|
|
2520
|
+
};
|
|
2521
|
+
}
|
|
2522
|
+
case "ekSentCt1Received": {
|
|
2523
|
+
const st = s.state;
|
|
2524
|
+
return {
|
|
2525
|
+
type: "ekSentCt1Received",
|
|
2526
|
+
uc: {
|
|
2527
|
+
auth: st.uc.auth.toProto(),
|
|
2528
|
+
dk: Uint8Array.from(st.uc.dk),
|
|
2529
|
+
ct1: Uint8Array.from(st.uc.ct1)
|
|
2530
|
+
},
|
|
2531
|
+
receivingCt2: st.receivingCt2.toProto()
|
|
2532
|
+
};
|
|
2533
|
+
}
|
|
2534
|
+
case "noHeaderReceived": {
|
|
2535
|
+
const st = s.state;
|
|
2536
|
+
return {
|
|
2537
|
+
type: "noHeaderReceived",
|
|
2538
|
+
uc: { auth: st.uc.auth.toProto() },
|
|
2539
|
+
receivingHdr: st.receivingHdr.toProto()
|
|
2540
|
+
};
|
|
2541
|
+
}
|
|
2542
|
+
case "headerReceived": {
|
|
2543
|
+
const st = s.state;
|
|
2544
|
+
return {
|
|
2545
|
+
type: "headerReceived",
|
|
2546
|
+
uc: {
|
|
2547
|
+
auth: st.uc.auth.toProto(),
|
|
2548
|
+
hdr: Uint8Array.from(st.uc.hdr),
|
|
2549
|
+
es: new Uint8Array(0),
|
|
2550
|
+
ct1: new Uint8Array(0),
|
|
2551
|
+
ss: new Uint8Array(0)
|
|
2552
|
+
},
|
|
2553
|
+
receivingEk: st.receivingEk.toProto()
|
|
2554
|
+
};
|
|
2555
|
+
}
|
|
2556
|
+
case "ct1Sampled": {
|
|
2557
|
+
const st = s.state;
|
|
2558
|
+
return {
|
|
2559
|
+
type: "ct1Sampled",
|
|
2560
|
+
uc: {
|
|
2561
|
+
auth: st.uc.auth.toProto(),
|
|
2562
|
+
hdr: Uint8Array.from(st.uc.hdr),
|
|
2563
|
+
es: Uint8Array.from(st.uc.es),
|
|
2564
|
+
ct1: Uint8Array.from(st.uc.ct1)
|
|
2565
|
+
},
|
|
2566
|
+
sendingCt1: st.sendingCt1.toProto(),
|
|
2567
|
+
receivingEk: st.receivingEk.toProto()
|
|
2568
|
+
};
|
|
2569
|
+
}
|
|
2570
|
+
case "ekReceivedCt1Sampled": {
|
|
2571
|
+
const st = s.state;
|
|
2572
|
+
return {
|
|
2573
|
+
type: "ekReceivedCt1Sampled",
|
|
2574
|
+
uc: {
|
|
2575
|
+
auth: st.uc.auth.toProto(),
|
|
2576
|
+
hdr: new Uint8Array(0),
|
|
2577
|
+
es: Uint8Array.from(st.uc.es),
|
|
2578
|
+
ek: Uint8Array.from(st.uc.ek),
|
|
2579
|
+
ct1: Uint8Array.from(st.uc.ct1)
|
|
2580
|
+
},
|
|
2581
|
+
sendingCt1: st.sendingCt1.toProto()
|
|
2582
|
+
};
|
|
2583
|
+
}
|
|
2584
|
+
case "ct1Acknowledged": {
|
|
2585
|
+
const st = s.state;
|
|
2586
|
+
return {
|
|
2587
|
+
type: "ct1Acknowledged",
|
|
2588
|
+
uc: {
|
|
2589
|
+
auth: st.uc.auth.toProto(),
|
|
2590
|
+
hdr: Uint8Array.from(st.uc.hdr),
|
|
2591
|
+
es: Uint8Array.from(st.uc.es),
|
|
2592
|
+
ct1: Uint8Array.from(st.uc.ct1)
|
|
2593
|
+
},
|
|
2594
|
+
receivingEk: st.receivingEk.toProto()
|
|
2595
|
+
};
|
|
2596
|
+
}
|
|
2597
|
+
case "ct2Sampled": {
|
|
2598
|
+
const st = s.state;
|
|
2599
|
+
return {
|
|
2600
|
+
type: "ct2Sampled",
|
|
2601
|
+
uc: { auth: st.uc.auth.toProto() },
|
|
2602
|
+
sendingCt2: st.sendingCt2.toProto()
|
|
2603
|
+
};
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
/**
|
|
2608
|
+
* Deserialize a PbV1State back into a runtime States object.
|
|
2609
|
+
*
|
|
2610
|
+
* @param pb - The protobuf V1 state
|
|
2611
|
+
* @returns The reconstructed runtime States
|
|
2612
|
+
*/
|
|
2613
|
+
function statesFromPb(pb) {
|
|
2614
|
+
if (pb.innerState === void 0) throw new Error("PbV1State has no innerState");
|
|
2615
|
+
const epoch = pb.epoch ?? 1n;
|
|
2616
|
+
return chunkedStateFromPb(pb.innerState, epoch);
|
|
2617
|
+
}
|
|
2618
|
+
function chunkedStateFromPb(cs, epoch) {
|
|
2619
|
+
switch (cs.type) {
|
|
2620
|
+
case "keysUnsampled": {
|
|
2621
|
+
const auth = authFromPb(cs.uc.auth);
|
|
2622
|
+
const ucState = new KeysUnsampled(epoch, auth);
|
|
2623
|
+
return {
|
|
2624
|
+
tag: "keysUnsampled",
|
|
2625
|
+
state: new KeysUnsampled$1(ucState)
|
|
2626
|
+
};
|
|
2627
|
+
}
|
|
2628
|
+
case "keysSampled": {
|
|
2629
|
+
const auth = authFromPb(cs.uc.auth);
|
|
2630
|
+
const ucState = new HeaderSent(epoch, auth, cs.uc.ek, cs.uc.dk);
|
|
2631
|
+
const encoder = PolyEncoder.fromProto(cs.sendingHdr);
|
|
2632
|
+
return {
|
|
2633
|
+
tag: "keysSampled",
|
|
2634
|
+
state: new KeysSampled(ucState, encoder)
|
|
2635
|
+
};
|
|
2636
|
+
}
|
|
2637
|
+
case "headerSent": {
|
|
2638
|
+
const auth = authFromPb(cs.uc.auth);
|
|
2639
|
+
const ucState = new EkSent(epoch, auth, cs.uc.dk);
|
|
2640
|
+
const encoder = PolyEncoder.fromProto(cs.sendingEk);
|
|
2641
|
+
const decoder = PolyDecoder.fromProto(cs.receivingCt1);
|
|
2642
|
+
return {
|
|
2643
|
+
tag: "headerSent",
|
|
2644
|
+
state: new HeaderSent$1(ucState, encoder, decoder)
|
|
2645
|
+
};
|
|
2646
|
+
}
|
|
2647
|
+
case "ct1Received": {
|
|
2648
|
+
const auth = authFromPb(cs.uc.auth);
|
|
2649
|
+
const ucState = new EkSentCt1Received(epoch, auth, cs.uc.dk, cs.uc.ct1);
|
|
2650
|
+
const encoder = PolyEncoder.fromProto(cs.sendingEk);
|
|
2651
|
+
return {
|
|
2652
|
+
tag: "ct1Received",
|
|
2653
|
+
state: new Ct1Received(ucState, encoder)
|
|
2654
|
+
};
|
|
2655
|
+
}
|
|
2656
|
+
case "ekSentCt1Received": {
|
|
2657
|
+
const auth = authFromPb(cs.uc.auth);
|
|
2658
|
+
const ucState = new EkSentCt1Received(epoch, auth, cs.uc.dk, cs.uc.ct1);
|
|
2659
|
+
const decoder = PolyDecoder.fromProto(cs.receivingCt2);
|
|
2660
|
+
return {
|
|
2661
|
+
tag: "ekSentCt1Received",
|
|
2662
|
+
state: new EkSentCt1Received$1(ucState, decoder)
|
|
2663
|
+
};
|
|
2664
|
+
}
|
|
2665
|
+
case "noHeaderReceived": {
|
|
2666
|
+
const auth = authFromPb(cs.uc.auth);
|
|
2667
|
+
const ucState = new NoHeaderReceived$1(epoch, auth);
|
|
2668
|
+
const decoder = PolyDecoder.fromProto(cs.receivingHdr);
|
|
2669
|
+
return {
|
|
2670
|
+
tag: "noHeaderReceived",
|
|
2671
|
+
state: new NoHeaderReceived(ucState, decoder)
|
|
2672
|
+
};
|
|
2673
|
+
}
|
|
2674
|
+
case "headerReceived": {
|
|
2675
|
+
const auth = authFromPb(cs.uc.auth);
|
|
2676
|
+
const ucState = new HeaderReceived$1(epoch, auth, cs.uc.hdr);
|
|
2677
|
+
const decoder = PolyDecoder.fromProto(cs.receivingEk);
|
|
2678
|
+
return {
|
|
2679
|
+
tag: "headerReceived",
|
|
2680
|
+
state: new HeaderReceived(ucState, decoder)
|
|
2681
|
+
};
|
|
2682
|
+
}
|
|
2683
|
+
case "ct1Sampled": {
|
|
2684
|
+
const auth = authFromPb(cs.uc.auth);
|
|
2685
|
+
const ucState = new Ct1Sent(epoch, auth, cs.uc.hdr, cs.uc.es, cs.uc.ct1);
|
|
2686
|
+
const encoder = PolyEncoder.fromProto(cs.sendingCt1);
|
|
2687
|
+
const decoder = PolyDecoder.fromProto(cs.receivingEk);
|
|
2688
|
+
return {
|
|
2689
|
+
tag: "ct1Sampled",
|
|
2690
|
+
state: new Ct1Sampled(ucState, encoder, decoder)
|
|
2691
|
+
};
|
|
2692
|
+
}
|
|
2693
|
+
case "ekReceivedCt1Sampled": {
|
|
2694
|
+
const auth = authFromPb(cs.uc.auth);
|
|
2695
|
+
const ucState = new Ct1SentEkReceived(epoch, auth, cs.uc.es, cs.uc.ek, cs.uc.ct1);
|
|
2696
|
+
const encoder = PolyEncoder.fromProto(cs.sendingCt1);
|
|
2697
|
+
return {
|
|
2698
|
+
tag: "ekReceivedCt1Sampled",
|
|
2699
|
+
state: new EkReceivedCt1Sampled(ucState, encoder)
|
|
2700
|
+
};
|
|
2701
|
+
}
|
|
2702
|
+
case "ct1Acknowledged": {
|
|
2703
|
+
const auth = authFromPb(cs.uc.auth);
|
|
2704
|
+
const ucState = new Ct1Sent(epoch, auth, cs.uc.hdr, cs.uc.es, cs.uc.ct1);
|
|
2705
|
+
const decoder = PolyDecoder.fromProto(cs.receivingEk);
|
|
2706
|
+
return {
|
|
2707
|
+
tag: "ct1Acknowledged",
|
|
2708
|
+
state: new Ct1Acknowledged(ucState, decoder)
|
|
2709
|
+
};
|
|
2710
|
+
}
|
|
2711
|
+
case "ct2Sampled": {
|
|
2712
|
+
const auth = authFromPb(cs.uc.auth);
|
|
2713
|
+
const ucState = new Ct2Sent(epoch, auth);
|
|
2714
|
+
const encoder = PolyEncoder.fromProto(cs.sendingCt2);
|
|
2715
|
+
return {
|
|
2716
|
+
tag: "ct2Sampled",
|
|
2717
|
+
state: new Ct2Sampled(ucState, encoder)
|
|
2718
|
+
};
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
function authFromPb(pb) {
|
|
2723
|
+
if (pb === void 0) return Authenticator.fromProto({
|
|
2724
|
+
rootKey: new Uint8Array(32),
|
|
2725
|
+
macKey: new Uint8Array(32)
|
|
2726
|
+
});
|
|
2727
|
+
return Authenticator.fromProto(pb);
|
|
2728
|
+
}
|
|
2729
|
+
|
|
2730
|
+
//#endregion
|
|
2731
|
+
//#region src/types.ts
|
|
2732
|
+
/** Protocol version */
|
|
2733
|
+
let Version = /* @__PURE__ */ function(Version) {
|
|
2734
|
+
Version[Version["V0"] = 0] = "V0";
|
|
2735
|
+
Version[Version["V1"] = 1] = "V1";
|
|
2736
|
+
return Version;
|
|
2737
|
+
}({});
|
|
2738
|
+
/** Communication direction */
|
|
2739
|
+
let Direction = /* @__PURE__ */ function(Direction) {
|
|
2740
|
+
Direction[Direction["A2B"] = 0] = "A2B";
|
|
2741
|
+
Direction[Direction["B2A"] = 1] = "B2A";
|
|
2742
|
+
return Direction;
|
|
2743
|
+
}({});
|
|
2744
|
+
|
|
2745
|
+
//#endregion
|
|
2746
|
+
//#region src/chain.ts
|
|
2747
|
+
/**
|
|
2748
|
+
* Copyright © 2025 Signal Messenger, LLC
|
|
2749
|
+
* Copyright © 2026 Parity Technologies
|
|
2750
|
+
*
|
|
2751
|
+
* SPQR Symmetric Chain -- epoch-based key derivation with forward/backward secrecy.
|
|
2752
|
+
*
|
|
2753
|
+
* Ported from Signal's spqr crate: chain.rs
|
|
2754
|
+
*
|
|
2755
|
+
* The Chain manages send/receive keys across epochs. Each epoch has two
|
|
2756
|
+
* directional chains (A2B and B2A). Keys are derived using HKDF-SHA256
|
|
2757
|
+
* with specific info strings that MUST match the Rust implementation
|
|
2758
|
+
* exactly (including the double space in "Chain Start").
|
|
2759
|
+
*/
|
|
2760
|
+
const enc = new TextEncoder();
|
|
2761
|
+
const CHAIN_START_INFO = enc.encode(LABEL_CHAIN_START);
|
|
2762
|
+
const CHAIN_NEXT_INFO = enc.encode(LABEL_CHAIN_NEXT);
|
|
2763
|
+
const CHAIN_ADD_EPOCH_INFO = enc.encode(LABEL_CHAIN_ADD_EPOCH);
|
|
2764
|
+
function resolveMaxJump(params) {
|
|
2765
|
+
return params.maxJump > 0 ? params.maxJump : DEFAULT_MAX_JUMP;
|
|
2766
|
+
}
|
|
2767
|
+
function resolveMaxOooKeys(params) {
|
|
2768
|
+
return params.maxOooKeys > 0 ? params.maxOooKeys : DEFAULT_MAX_OOO_KEYS;
|
|
2769
|
+
}
|
|
2770
|
+
function trimSize(params) {
|
|
2771
|
+
const maxOoo = resolveMaxOooKeys(params);
|
|
2772
|
+
return Math.floor(maxOoo * 11 / 10) + 1;
|
|
2773
|
+
}
|
|
2774
|
+
function switchDirection(d) {
|
|
2775
|
+
return d === Direction.A2B ? Direction.B2A : Direction.A2B;
|
|
2776
|
+
}
|
|
2777
|
+
/**
|
|
2778
|
+
* Stores out-of-order keys as packed [index_be32 (4 bytes)][key (32 bytes)] entries.
|
|
2779
|
+
* Matches Rust KeyHistory data layout exactly.
|
|
2780
|
+
*/
|
|
2781
|
+
var KeyHistory = class {
|
|
2782
|
+
data;
|
|
2783
|
+
length;
|
|
2784
|
+
constructor(data) {
|
|
2785
|
+
this.data = data !== void 0 ? Uint8Array.from(data) : new Uint8Array(0);
|
|
2786
|
+
this.length = this.data.length;
|
|
2787
|
+
}
|
|
2788
|
+
add(index, key, _params) {
|
|
2789
|
+
const entry = new Uint8Array(KEY_ENTRY_SIZE);
|
|
2790
|
+
new DataView(entry.buffer).setUint32(0, index, false);
|
|
2791
|
+
entry.set(key, 4);
|
|
2792
|
+
const newData = new Uint8Array(this.length + KEY_ENTRY_SIZE);
|
|
2793
|
+
newData.set(this.data.subarray(0, this.length));
|
|
2794
|
+
newData.set(entry, this.length);
|
|
2795
|
+
this.data = newData;
|
|
2796
|
+
this.length += KEY_ENTRY_SIZE;
|
|
2797
|
+
}
|
|
2798
|
+
gc(currentKey, params) {
|
|
2799
|
+
const maxOoo = resolveMaxOooKeys(params);
|
|
2800
|
+
if (this.length >= trimSize(params) * KEY_ENTRY_SIZE) {
|
|
2801
|
+
if (currentKey < maxOoo) throw new Error("KeyHistory.gc: currentKey < maxOooKeys (corrupted state)");
|
|
2802
|
+
const trimHorizon = currentKey - maxOoo;
|
|
2803
|
+
let i = 0;
|
|
2804
|
+
while (i < this.length) if (trimHorizon > new DataView(this.data.buffer, this.data.byteOffset + i, 4).getUint32(0, false)) this.removeAt(i);
|
|
2805
|
+
else i += KEY_ENTRY_SIZE;
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2808
|
+
clear() {
|
|
2809
|
+
this.data = new Uint8Array(0);
|
|
2810
|
+
this.length = 0;
|
|
2811
|
+
}
|
|
2812
|
+
get(at, currentCtr, params) {
|
|
2813
|
+
if (at + resolveMaxOooKeys(params) < currentCtr) throw new SpqrError(`Key trimmed: ${at}`, SpqrErrorCode.KeyTrimmed);
|
|
2814
|
+
const want = new Uint8Array(4);
|
|
2815
|
+
new DataView(want.buffer).setUint32(0, at, false);
|
|
2816
|
+
for (let i = 0; i < this.length; i += KEY_ENTRY_SIZE) if (this.data[i] === want[0] && this.data[i + 1] === want[1] && this.data[i + 2] === want[2] && this.data[i + 3] === want[3]) {
|
|
2817
|
+
const key = this.data.slice(i + 4, i + KEY_ENTRY_SIZE);
|
|
2818
|
+
this.removeAt(i);
|
|
2819
|
+
return key;
|
|
2820
|
+
}
|
|
2821
|
+
throw new SpqrError(`Key already requested: ${at}`, SpqrErrorCode.KeyAlreadyRequested);
|
|
2822
|
+
}
|
|
2823
|
+
removeAt(index) {
|
|
2824
|
+
if (index + KEY_ENTRY_SIZE < this.length) {
|
|
2825
|
+
const lastStart = this.length - KEY_ENTRY_SIZE;
|
|
2826
|
+
this.data.copyWithin(index, lastStart, this.length);
|
|
2827
|
+
}
|
|
2828
|
+
this.length -= KEY_ENTRY_SIZE;
|
|
2829
|
+
}
|
|
2830
|
+
/** Serialize to raw bytes for protobuf storage */
|
|
2831
|
+
serialize() {
|
|
2832
|
+
return this.data.slice(0, this.length);
|
|
2833
|
+
}
|
|
2834
|
+
};
|
|
2835
|
+
/**
|
|
2836
|
+
* One directional chain within an epoch (send or recv).
|
|
2837
|
+
* Manages a ratcheting key derivation with HKDF.
|
|
2838
|
+
*/
|
|
2839
|
+
var ChainEpochDirection = class ChainEpochDirection {
|
|
2840
|
+
ctr;
|
|
2841
|
+
next;
|
|
2842
|
+
prev;
|
|
2843
|
+
constructor(key, ctr, prev) {
|
|
2844
|
+
this.ctr = ctr ?? 0;
|
|
2845
|
+
this.next = Uint8Array.from(key);
|
|
2846
|
+
this.prev = new KeyHistory(prev);
|
|
2847
|
+
}
|
|
2848
|
+
/**
|
|
2849
|
+
* Derive the next key in the chain.
|
|
2850
|
+
* Returns [index, key].
|
|
2851
|
+
*/
|
|
2852
|
+
nextKey() {
|
|
2853
|
+
this.ctr += 1;
|
|
2854
|
+
const info = concat(uint32ToBE4(this.ctr), CHAIN_NEXT_INFO);
|
|
2855
|
+
const gen = hkdfSha256(this.next, ZERO_SALT, info, 64);
|
|
2856
|
+
this.next = gen.slice(0, 32);
|
|
2857
|
+
const key = gen.slice(32, 64);
|
|
2858
|
+
return [this.ctr, key];
|
|
2859
|
+
}
|
|
2860
|
+
/**
|
|
2861
|
+
* Internal: derive next key without consuming it publicly.
|
|
2862
|
+
* Used for skipping ahead to build out-of-order key history.
|
|
2863
|
+
*/
|
|
2864
|
+
static nextKeyInternal(next, ctr) {
|
|
2865
|
+
ctr += 1;
|
|
2866
|
+
const gen = hkdfSha256(next, ZERO_SALT, concat(uint32ToBE4(ctr), CHAIN_NEXT_INFO), 64);
|
|
2867
|
+
return {
|
|
2868
|
+
next: gen.slice(0, 32),
|
|
2869
|
+
ctr,
|
|
2870
|
+
index: ctr,
|
|
2871
|
+
key: gen.slice(32, 64)
|
|
2872
|
+
};
|
|
2873
|
+
}
|
|
2874
|
+
/**
|
|
2875
|
+
* Get the key at a specific counter position.
|
|
2876
|
+
* Supports out-of-order access via KeyHistory.
|
|
2877
|
+
*/
|
|
2878
|
+
key(at, params) {
|
|
2879
|
+
const maxJump = resolveMaxJump(params);
|
|
2880
|
+
const maxOoo = resolveMaxOooKeys(params);
|
|
2881
|
+
if (at > this.ctr) {
|
|
2882
|
+
if (at - this.ctr > maxJump) throw new SpqrError(`Key jump: ${this.ctr} - ${at}`, SpqrErrorCode.KeyJump);
|
|
2883
|
+
} else if (at < this.ctr) return this.prev.get(at, this.ctr, params);
|
|
2884
|
+
else throw new SpqrError(`Key already requested: ${at}`, SpqrErrorCode.KeyAlreadyRequested);
|
|
2885
|
+
if (at > this.ctr + maxOoo) this.prev.clear();
|
|
2886
|
+
while (at > this.ctr + 1) {
|
|
2887
|
+
const result = ChainEpochDirection.nextKeyInternal(this.next, this.ctr);
|
|
2888
|
+
this.next = result.next;
|
|
2889
|
+
this.ctr = result.ctr;
|
|
2890
|
+
if (this.ctr + maxOoo >= at) this.prev.add(result.index, result.key, params);
|
|
2891
|
+
}
|
|
2892
|
+
this.prev.gc(this.ctr, params);
|
|
2893
|
+
const result = ChainEpochDirection.nextKeyInternal(this.next, this.ctr);
|
|
2894
|
+
this.next = result.next;
|
|
2895
|
+
this.ctr = result.ctr;
|
|
2896
|
+
return result.key;
|
|
2897
|
+
}
|
|
2898
|
+
clearNext() {
|
|
2899
|
+
this.next = new Uint8Array(0);
|
|
2900
|
+
}
|
|
2901
|
+
/** Serialize to protobuf EpochDirection */
|
|
2902
|
+
toProto() {
|
|
2903
|
+
return {
|
|
2904
|
+
ctr: this.ctr,
|
|
2905
|
+
next: Uint8Array.from(this.next),
|
|
2906
|
+
prev: this.prev.serialize()
|
|
2907
|
+
};
|
|
2908
|
+
}
|
|
2909
|
+
static fromProto(pb) {
|
|
2910
|
+
return new ChainEpochDirection(pb.next, pb.ctr, pb.prev);
|
|
2911
|
+
}
|
|
2912
|
+
};
|
|
2913
|
+
/**
|
|
2914
|
+
* The main Chain manages send/receive keys across all epochs.
|
|
2915
|
+
*
|
|
2916
|
+
* Port of Rust's Chain struct from chain.rs.
|
|
2917
|
+
* Uses bigint for epoch values (matching Rust u64).
|
|
2918
|
+
*/
|
|
2919
|
+
var Chain = class Chain {
|
|
2920
|
+
dir;
|
|
2921
|
+
currentEpoch;
|
|
2922
|
+
sendEpoch;
|
|
2923
|
+
links;
|
|
2924
|
+
nextRoot;
|
|
2925
|
+
params;
|
|
2926
|
+
constructor(dir, currentEpoch, sendEpoch, links, nextRoot, params) {
|
|
2927
|
+
this.dir = dir;
|
|
2928
|
+
this.currentEpoch = currentEpoch;
|
|
2929
|
+
this.sendEpoch = sendEpoch;
|
|
2930
|
+
this.links = links;
|
|
2931
|
+
this.nextRoot = nextRoot;
|
|
2932
|
+
this.params = params;
|
|
2933
|
+
}
|
|
2934
|
+
/**
|
|
2935
|
+
* Create a new chain from an initial key and direction.
|
|
2936
|
+
* HKDF info: "Signal PQ Ratchet V1 Chain Start" (TWO spaces before Start!)
|
|
2937
|
+
*
|
|
2938
|
+
* The 96-byte HKDF output is split as:
|
|
2939
|
+
* [0..32]: nextRoot
|
|
2940
|
+
* [32..64]: A2B chain seed
|
|
2941
|
+
* [64..96]: B2A chain seed
|
|
2942
|
+
*
|
|
2943
|
+
* Direction determines which half is send vs recv.
|
|
2944
|
+
*/
|
|
2945
|
+
static create(initialKey, dir, params = {
|
|
2946
|
+
maxJump: DEFAULT_MAX_JUMP,
|
|
2947
|
+
maxOooKeys: DEFAULT_MAX_OOO_KEYS
|
|
2948
|
+
}) {
|
|
2949
|
+
const gen = hkdfSha256(initialKey, ZERO_SALT, CHAIN_START_INFO, 96);
|
|
2950
|
+
const switchedDir = switchDirection(dir);
|
|
2951
|
+
const sendKey = cedForDirection(gen, dir);
|
|
2952
|
+
const recvKey = cedForDirection(gen, switchedDir);
|
|
2953
|
+
return new Chain(dir, 0n, 0n, [{
|
|
2954
|
+
send: new ChainEpochDirection(sendKey),
|
|
2955
|
+
recv: new ChainEpochDirection(recvKey)
|
|
2956
|
+
}], gen.slice(0, 32), params);
|
|
2957
|
+
}
|
|
2958
|
+
/**
|
|
2959
|
+
* Add a new epoch to the chain with a shared secret.
|
|
2960
|
+
* HKDF info: "Signal PQ Ratchet V1 Chain Add Epoch"
|
|
2961
|
+
*
|
|
2962
|
+
* Salt = current nextRoot, IKM = epochSecret.secret
|
|
2963
|
+
*/
|
|
2964
|
+
addEpoch(epochSecret) {
|
|
2965
|
+
if (epochSecret.epoch !== this.currentEpoch + 1n) throw new SpqrError(`Expected epoch ${this.currentEpoch + 1n}, got ${epochSecret.epoch}`, SpqrErrorCode.EpochOutOfRange);
|
|
2966
|
+
const gen = hkdfSha256(epochSecret.secret, this.nextRoot, CHAIN_ADD_EPOCH_INFO, 96);
|
|
2967
|
+
this.currentEpoch = epochSecret.epoch;
|
|
2968
|
+
this.nextRoot = gen.slice(0, 32);
|
|
2969
|
+
const sendKey = cedForDirection(gen, this.dir);
|
|
2970
|
+
const recvKey = cedForDirection(gen, switchDirection(this.dir));
|
|
2971
|
+
this.links.push({
|
|
2972
|
+
send: new ChainEpochDirection(sendKey),
|
|
2973
|
+
recv: new ChainEpochDirection(recvKey)
|
|
2974
|
+
});
|
|
2975
|
+
}
|
|
2976
|
+
epochIdx(epoch) {
|
|
2977
|
+
if (epoch > this.currentEpoch) throw new SpqrError(`Epoch not in valid range: ${epoch}`, SpqrErrorCode.EpochOutOfRange);
|
|
2978
|
+
const back = Number(this.currentEpoch - epoch);
|
|
2979
|
+
if (back >= this.links.length) throw new SpqrError(`Epoch not in valid range: ${epoch}`, SpqrErrorCode.EpochOutOfRange);
|
|
2980
|
+
return this.links.length - 1 - back;
|
|
2981
|
+
}
|
|
2982
|
+
/**
|
|
2983
|
+
* Get the next send key for a given epoch.
|
|
2984
|
+
* Returns [index, key].
|
|
2985
|
+
*/
|
|
2986
|
+
sendKey(epoch) {
|
|
2987
|
+
if (epoch < this.sendEpoch) throw new SpqrError(`Send key epoch decreased (${this.sendEpoch} -> ${epoch})`, SpqrErrorCode.SendKeyEpochDecreased);
|
|
2988
|
+
let epochIndex = this.epochIdx(epoch);
|
|
2989
|
+
if (this.sendEpoch !== epoch) {
|
|
2990
|
+
this.sendEpoch = epoch;
|
|
2991
|
+
while (epochIndex > EPOCHS_TO_KEEP_PRIOR_TO_SEND_EPOCH) {
|
|
2992
|
+
this.links.shift();
|
|
2993
|
+
epochIndex -= 1;
|
|
2994
|
+
}
|
|
2995
|
+
for (let i = 0; i < epochIndex; i++) this.links[i].send.clearNext();
|
|
2996
|
+
}
|
|
2997
|
+
return this.links[epochIndex].send.nextKey();
|
|
2998
|
+
}
|
|
2999
|
+
/**
|
|
3000
|
+
* Get a receive key for a given epoch and counter index.
|
|
3001
|
+
*/
|
|
3002
|
+
recvKey(epoch, index) {
|
|
3003
|
+
const epochIndex = this.epochIdx(epoch);
|
|
3004
|
+
return this.links[epochIndex].recv.key(index, this.params);
|
|
3005
|
+
}
|
|
3006
|
+
/**
|
|
3007
|
+
* Serialize the chain to its protobuf representation.
|
|
3008
|
+
* Matches Rust Chain::into_pb().
|
|
3009
|
+
*/
|
|
3010
|
+
toProto() {
|
|
3011
|
+
return {
|
|
3012
|
+
direction: this.dir,
|
|
3013
|
+
currentEpoch: this.currentEpoch,
|
|
3014
|
+
sendEpoch: this.sendEpoch,
|
|
3015
|
+
nextRoot: Uint8Array.from(this.nextRoot),
|
|
3016
|
+
links: this.links.map((link) => ({
|
|
3017
|
+
send: link.send.toProto(),
|
|
3018
|
+
recv: link.recv.toProto()
|
|
3019
|
+
})),
|
|
3020
|
+
params: chainParamsToProto(this.params)
|
|
3021
|
+
};
|
|
3022
|
+
}
|
|
3023
|
+
/**
|
|
3024
|
+
* Deserialize a chain from its protobuf representation.
|
|
3025
|
+
* Matches Rust Chain::from_pb().
|
|
3026
|
+
*/
|
|
3027
|
+
static fromProto(pb) {
|
|
3028
|
+
const links = pb.links.map((link) => ({
|
|
3029
|
+
send: ChainEpochDirection.fromProto(link.send ?? {
|
|
3030
|
+
ctr: 0,
|
|
3031
|
+
next: new Uint8Array(0),
|
|
3032
|
+
prev: new Uint8Array(0)
|
|
3033
|
+
}),
|
|
3034
|
+
recv: ChainEpochDirection.fromProto(link.recv ?? {
|
|
3035
|
+
ctr: 0,
|
|
3036
|
+
next: new Uint8Array(0),
|
|
3037
|
+
prev: new Uint8Array(0)
|
|
3038
|
+
})
|
|
3039
|
+
}));
|
|
3040
|
+
const params = chainParamsFromProto(pb.params);
|
|
3041
|
+
return new Chain(pb.direction, pb.currentEpoch, pb.sendEpoch, links, Uint8Array.from(pb.nextRoot), params);
|
|
3042
|
+
}
|
|
3043
|
+
};
|
|
3044
|
+
/**
|
|
3045
|
+
* Select the chain key for a given direction from a 96-byte HKDF output.
|
|
3046
|
+
* A2B uses bytes [32..64], B2A uses bytes [64..96].
|
|
3047
|
+
*/
|
|
3048
|
+
function cedForDirection(gen, dir) {
|
|
3049
|
+
return dir === Direction.A2B ? gen.slice(32, 64) : gen.slice(64, 96);
|
|
3050
|
+
}
|
|
3051
|
+
/**
|
|
3052
|
+
* Convert ChainParams to protobuf format.
|
|
3053
|
+
* Default values are stored as 0 (proto3 convention).
|
|
3054
|
+
*/
|
|
3055
|
+
function chainParamsToProto(params) {
|
|
3056
|
+
return {
|
|
3057
|
+
maxJump: params.maxJump === DEFAULT_MAX_JUMP ? 0 : params.maxJump,
|
|
3058
|
+
maxOooKeys: params.maxOooKeys === DEFAULT_MAX_OOO_KEYS ? 0 : params.maxOooKeys
|
|
3059
|
+
};
|
|
3060
|
+
}
|
|
3061
|
+
/**
|
|
3062
|
+
* Convert protobuf ChainParams to runtime ChainParams.
|
|
3063
|
+
* Zero values are interpreted as defaults.
|
|
3064
|
+
*/
|
|
3065
|
+
function chainParamsFromProto(pb) {
|
|
3066
|
+
if (pb === void 0) return {
|
|
3067
|
+
maxJump: DEFAULT_MAX_JUMP,
|
|
3068
|
+
maxOooKeys: DEFAULT_MAX_OOO_KEYS
|
|
3069
|
+
};
|
|
3070
|
+
return {
|
|
3071
|
+
maxJump: pb.maxJump > 0 ? pb.maxJump : DEFAULT_MAX_JUMP,
|
|
3072
|
+
maxOooKeys: pb.maxOooKeys > 0 ? pb.maxOooKeys : DEFAULT_MAX_OOO_KEYS
|
|
3073
|
+
};
|
|
3074
|
+
}
|
|
3075
|
+
|
|
3076
|
+
//#endregion
|
|
3077
|
+
//#region src/proto/index.ts
|
|
3078
|
+
const WIRE_VARINT = 0;
|
|
3079
|
+
const WIRE_LENGTH_DELIMITED = 2;
|
|
3080
|
+
const EMPTY_BYTES = new Uint8Array(0);
|
|
3081
|
+
var ProtoWriter = class {
|
|
3082
|
+
parts = [];
|
|
3083
|
+
writeVarint(fieldNumber, value) {
|
|
3084
|
+
if (typeof value === "bigint") {
|
|
3085
|
+
if (value === 0n) return;
|
|
3086
|
+
this.writeTag(fieldNumber, WIRE_VARINT);
|
|
3087
|
+
this.writeRawVarint64(value);
|
|
3088
|
+
} else {
|
|
3089
|
+
if (value === 0) return;
|
|
3090
|
+
this.writeTag(fieldNumber, WIRE_VARINT);
|
|
3091
|
+
this.writeRawVarint(value);
|
|
3092
|
+
}
|
|
3093
|
+
}
|
|
3094
|
+
/** Write a varint field even when the value is zero (for required fields). */
|
|
3095
|
+
writeVarintAlways(fieldNumber, value) {
|
|
3096
|
+
if (typeof value === "bigint") {
|
|
3097
|
+
this.writeTag(fieldNumber, WIRE_VARINT);
|
|
3098
|
+
this.writeRawVarint64(value);
|
|
3099
|
+
} else {
|
|
3100
|
+
this.writeTag(fieldNumber, WIRE_VARINT);
|
|
3101
|
+
this.writeRawVarint(value);
|
|
3102
|
+
}
|
|
3103
|
+
}
|
|
3104
|
+
writeBool(fieldNumber, value) {
|
|
3105
|
+
if (!value) return;
|
|
3106
|
+
this.writeTag(fieldNumber, WIRE_VARINT);
|
|
3107
|
+
this.writeRawVarint(1);
|
|
3108
|
+
}
|
|
3109
|
+
writeBytes(fieldNumber, data) {
|
|
3110
|
+
if (data.length === 0) return;
|
|
3111
|
+
this.writeTag(fieldNumber, WIRE_LENGTH_DELIMITED);
|
|
3112
|
+
this.writeRawVarint(data.length);
|
|
3113
|
+
this.parts.push(new Uint8Array(data));
|
|
3114
|
+
}
|
|
3115
|
+
writeMessage(fieldNumber, writer) {
|
|
3116
|
+
const data = writer.finish();
|
|
3117
|
+
if (data.length === 0) return;
|
|
3118
|
+
this.writeTag(fieldNumber, WIRE_LENGTH_DELIMITED);
|
|
3119
|
+
this.writeRawVarint(data.length);
|
|
3120
|
+
this.parts.push(data);
|
|
3121
|
+
}
|
|
3122
|
+
/** For oneof fields, write even if the sub-message is empty. */
|
|
3123
|
+
writeMessageAlways(fieldNumber, writer) {
|
|
3124
|
+
const data = writer.finish();
|
|
3125
|
+
this.writeTag(fieldNumber, WIRE_LENGTH_DELIMITED);
|
|
3126
|
+
this.writeRawVarint(data.length);
|
|
3127
|
+
if (data.length > 0) this.parts.push(data);
|
|
3128
|
+
}
|
|
3129
|
+
writeEnum(fieldNumber, value) {
|
|
3130
|
+
this.writeVarint(fieldNumber, value);
|
|
3131
|
+
}
|
|
3132
|
+
finish() {
|
|
3133
|
+
let total = 0;
|
|
3134
|
+
for (const p of this.parts) total += p.length;
|
|
3135
|
+
const result = new Uint8Array(total);
|
|
3136
|
+
let offset = 0;
|
|
3137
|
+
for (const p of this.parts) {
|
|
3138
|
+
result.set(p, offset);
|
|
3139
|
+
offset += p.length;
|
|
3140
|
+
}
|
|
3141
|
+
return result;
|
|
3142
|
+
}
|
|
3143
|
+
writeTag(fieldNumber, wireType) {
|
|
3144
|
+
this.writeRawVarint(fieldNumber << 3 | wireType);
|
|
3145
|
+
}
|
|
3146
|
+
writeRawVarint(value) {
|
|
3147
|
+
const buf = [];
|
|
3148
|
+
let v = value >>> 0;
|
|
3149
|
+
while (v > 127) {
|
|
3150
|
+
buf.push(v & 127 | 128);
|
|
3151
|
+
v >>>= 7;
|
|
3152
|
+
}
|
|
3153
|
+
buf.push(v & 127);
|
|
3154
|
+
this.parts.push(new Uint8Array(buf));
|
|
3155
|
+
}
|
|
3156
|
+
writeRawVarint64(value) {
|
|
3157
|
+
const buf = [];
|
|
3158
|
+
let v = value;
|
|
3159
|
+
while (v > 127n) {
|
|
3160
|
+
buf.push(Number(v & 127n) | 128);
|
|
3161
|
+
v >>= 7n;
|
|
3162
|
+
}
|
|
3163
|
+
buf.push(Number(v & 127n));
|
|
3164
|
+
this.parts.push(new Uint8Array(buf));
|
|
3165
|
+
}
|
|
3166
|
+
};
|
|
3167
|
+
var ProtoReader = class ProtoReader {
|
|
3168
|
+
data;
|
|
3169
|
+
pos;
|
|
3170
|
+
constructor(data) {
|
|
3171
|
+
this.data = data;
|
|
3172
|
+
this.pos = 0;
|
|
3173
|
+
}
|
|
3174
|
+
get remaining() {
|
|
3175
|
+
return this.data.length - this.pos;
|
|
3176
|
+
}
|
|
3177
|
+
get done() {
|
|
3178
|
+
return this.pos >= this.data.length;
|
|
3179
|
+
}
|
|
3180
|
+
readField() {
|
|
3181
|
+
if (this.pos >= this.data.length) return null;
|
|
3182
|
+
const tag = this.readRawVarint();
|
|
3183
|
+
return {
|
|
3184
|
+
fieldNumber: tag >>> 3,
|
|
3185
|
+
wireType: tag & 7
|
|
3186
|
+
};
|
|
3187
|
+
}
|
|
3188
|
+
readVarint() {
|
|
3189
|
+
return this.readRawVarint();
|
|
3190
|
+
}
|
|
3191
|
+
readVarint64() {
|
|
3192
|
+
return this.readRawVarint64();
|
|
3193
|
+
}
|
|
3194
|
+
readBool() {
|
|
3195
|
+
return this.readRawVarint() !== 0;
|
|
3196
|
+
}
|
|
3197
|
+
readBytes() {
|
|
3198
|
+
const len = this.readRawVarint();
|
|
3199
|
+
const data = this.data.slice(this.pos, this.pos + len);
|
|
3200
|
+
this.pos += len;
|
|
3201
|
+
return data;
|
|
3202
|
+
}
|
|
3203
|
+
readMessage() {
|
|
3204
|
+
return new ProtoReader(this.readBytes());
|
|
3205
|
+
}
|
|
3206
|
+
readEnum() {
|
|
3207
|
+
return this.readRawVarint();
|
|
3208
|
+
}
|
|
3209
|
+
skip(wireType) {
|
|
3210
|
+
switch (wireType) {
|
|
3211
|
+
case 0:
|
|
3212
|
+
this.readRawVarint();
|
|
3213
|
+
break;
|
|
3214
|
+
case 1:
|
|
3215
|
+
this.pos += 8;
|
|
3216
|
+
break;
|
|
3217
|
+
case 2: {
|
|
3218
|
+
const len = this.readRawVarint();
|
|
3219
|
+
this.pos += len;
|
|
3220
|
+
break;
|
|
3221
|
+
}
|
|
3222
|
+
case 5:
|
|
3223
|
+
this.pos += 4;
|
|
3224
|
+
break;
|
|
3225
|
+
default: throw new Error(`Unknown wire type: ${wireType}`);
|
|
3226
|
+
}
|
|
3227
|
+
}
|
|
3228
|
+
readRawVarint() {
|
|
3229
|
+
let result = 0;
|
|
3230
|
+
let shift = 0;
|
|
3231
|
+
while (shift < 35) {
|
|
3232
|
+
const b = this.data[this.pos++];
|
|
3233
|
+
result |= (b & 127) << shift;
|
|
3234
|
+
if ((b & 128) === 0) return result >>> 0;
|
|
3235
|
+
shift += 7;
|
|
3236
|
+
}
|
|
3237
|
+
throw new Error("Varint too long");
|
|
3238
|
+
}
|
|
3239
|
+
readRawVarint64() {
|
|
3240
|
+
let result = 0n;
|
|
3241
|
+
let shift = 0n;
|
|
3242
|
+
while (shift < 70n) {
|
|
3243
|
+
const b = this.data[this.pos++];
|
|
3244
|
+
result |= BigInt(b & 127) << shift;
|
|
3245
|
+
if ((b & 128) === 0) return result;
|
|
3246
|
+
shift += 7n;
|
|
3247
|
+
}
|
|
3248
|
+
throw new Error("Varint64 too long");
|
|
3249
|
+
}
|
|
3250
|
+
};
|
|
3251
|
+
function decodeChainParams(data) {
|
|
3252
|
+
const r = new ProtoReader(data);
|
|
3253
|
+
let maxJump = 0;
|
|
3254
|
+
let maxOooKeys = 0;
|
|
3255
|
+
while (!r.done) {
|
|
3256
|
+
const field = r.readField();
|
|
3257
|
+
if (field === null) break;
|
|
3258
|
+
switch (field.fieldNumber) {
|
|
3259
|
+
case 1:
|
|
3260
|
+
maxJump = r.readVarint();
|
|
3261
|
+
break;
|
|
3262
|
+
case 2:
|
|
3263
|
+
maxOooKeys = r.readVarint();
|
|
3264
|
+
break;
|
|
3265
|
+
default: r.skip(field.wireType);
|
|
3266
|
+
}
|
|
3267
|
+
}
|
|
3268
|
+
return {
|
|
3269
|
+
maxJump,
|
|
3270
|
+
maxOooKeys
|
|
3271
|
+
};
|
|
3272
|
+
}
|
|
3273
|
+
function encodeVersionNegotiation(msg) {
|
|
3274
|
+
const w = new ProtoWriter();
|
|
3275
|
+
w.writeBytes(1, msg.authKey);
|
|
3276
|
+
w.writeEnum(2, msg.direction);
|
|
3277
|
+
w.writeEnum(3, msg.minVersion);
|
|
3278
|
+
if (msg.chainParams !== void 0) {
|
|
3279
|
+
const sub = new ProtoWriter();
|
|
3280
|
+
sub.writeVarint(1, msg.chainParams.maxJump);
|
|
3281
|
+
sub.writeVarint(2, msg.chainParams.maxOooKeys);
|
|
3282
|
+
w.writeMessage(4, sub);
|
|
3283
|
+
}
|
|
3284
|
+
return w.finish();
|
|
3285
|
+
}
|
|
3286
|
+
function decodeVersionNegotiation(data) {
|
|
3287
|
+
const r = new ProtoReader(data);
|
|
3288
|
+
let authKey = EMPTY_BYTES;
|
|
3289
|
+
let direction = 0;
|
|
3290
|
+
let minVersion = 0;
|
|
3291
|
+
let chainParams;
|
|
3292
|
+
while (!r.done) {
|
|
3293
|
+
const field = r.readField();
|
|
3294
|
+
if (field === null) break;
|
|
3295
|
+
switch (field.fieldNumber) {
|
|
3296
|
+
case 1:
|
|
3297
|
+
authKey = r.readBytes();
|
|
3298
|
+
break;
|
|
3299
|
+
case 2:
|
|
3300
|
+
direction = r.readEnum();
|
|
3301
|
+
break;
|
|
3302
|
+
case 3:
|
|
3303
|
+
minVersion = r.readEnum();
|
|
3304
|
+
break;
|
|
3305
|
+
case 4:
|
|
3306
|
+
chainParams = decodeChainParams(r.readBytes());
|
|
3307
|
+
break;
|
|
3308
|
+
default: r.skip(field.wireType);
|
|
3309
|
+
}
|
|
3310
|
+
}
|
|
3311
|
+
return {
|
|
3312
|
+
authKey,
|
|
3313
|
+
direction,
|
|
3314
|
+
minVersion,
|
|
3315
|
+
chainParams
|
|
3316
|
+
};
|
|
3317
|
+
}
|
|
3318
|
+
function encodeEpochDirection(w, msg) {
|
|
3319
|
+
w.writeVarint(1, msg.ctr);
|
|
3320
|
+
w.writeBytes(2, msg.next);
|
|
3321
|
+
w.writeBytes(3, msg.prev);
|
|
3322
|
+
}
|
|
3323
|
+
function decodeEpochDirection(r) {
|
|
3324
|
+
let ctr = 0;
|
|
3325
|
+
let next = EMPTY_BYTES;
|
|
3326
|
+
let prev = EMPTY_BYTES;
|
|
3327
|
+
while (!r.done) {
|
|
3328
|
+
const field = r.readField();
|
|
3329
|
+
if (field === null) break;
|
|
3330
|
+
switch (field.fieldNumber) {
|
|
3331
|
+
case 1:
|
|
3332
|
+
ctr = r.readVarint();
|
|
3333
|
+
break;
|
|
3334
|
+
case 2:
|
|
3335
|
+
next = r.readBytes();
|
|
3336
|
+
break;
|
|
3337
|
+
case 3:
|
|
3338
|
+
prev = r.readBytes();
|
|
3339
|
+
break;
|
|
3340
|
+
default: r.skip(field.wireType);
|
|
3341
|
+
}
|
|
3342
|
+
}
|
|
3343
|
+
return {
|
|
3344
|
+
ctr,
|
|
3345
|
+
next,
|
|
3346
|
+
prev
|
|
3347
|
+
};
|
|
3348
|
+
}
|
|
3349
|
+
function encodeEpoch(msg) {
|
|
3350
|
+
const w = new ProtoWriter();
|
|
3351
|
+
if (msg.send !== void 0) {
|
|
3352
|
+
const sub = new ProtoWriter();
|
|
3353
|
+
encodeEpochDirection(sub, msg.send);
|
|
3354
|
+
w.writeMessage(1, sub);
|
|
3355
|
+
}
|
|
3356
|
+
if (msg.recv !== void 0) {
|
|
3357
|
+
const sub = new ProtoWriter();
|
|
3358
|
+
encodeEpochDirection(sub, msg.recv);
|
|
3359
|
+
w.writeMessage(2, sub);
|
|
3360
|
+
}
|
|
3361
|
+
return w.finish();
|
|
3362
|
+
}
|
|
3363
|
+
function decodeEpoch(data) {
|
|
3364
|
+
const r = new ProtoReader(data);
|
|
3365
|
+
let send;
|
|
3366
|
+
let recv;
|
|
3367
|
+
while (!r.done) {
|
|
3368
|
+
const field = r.readField();
|
|
3369
|
+
if (field === null) break;
|
|
3370
|
+
switch (field.fieldNumber) {
|
|
3371
|
+
case 1:
|
|
3372
|
+
send = decodeEpochDirection(r.readMessage());
|
|
3373
|
+
break;
|
|
3374
|
+
case 2:
|
|
3375
|
+
recv = decodeEpochDirection(r.readMessage());
|
|
3376
|
+
break;
|
|
3377
|
+
default: r.skip(field.wireType);
|
|
3378
|
+
}
|
|
3379
|
+
}
|
|
3380
|
+
return {
|
|
3381
|
+
send,
|
|
3382
|
+
recv
|
|
3383
|
+
};
|
|
3384
|
+
}
|
|
3385
|
+
function encodeChain(msg) {
|
|
3386
|
+
const w = new ProtoWriter();
|
|
3387
|
+
w.writeEnum(1, msg.direction);
|
|
3388
|
+
w.writeVarint(2, msg.currentEpoch);
|
|
3389
|
+
for (const link of msg.links) w.writeBytes(3, encodeEpoch(link));
|
|
3390
|
+
w.writeBytes(4, msg.nextRoot);
|
|
3391
|
+
w.writeVarint(5, msg.sendEpoch);
|
|
3392
|
+
if (msg.params !== void 0) {
|
|
3393
|
+
const sub = new ProtoWriter();
|
|
3394
|
+
sub.writeVarint(1, msg.params.maxJump);
|
|
3395
|
+
sub.writeVarint(2, msg.params.maxOooKeys);
|
|
3396
|
+
w.writeMessage(6, sub);
|
|
3397
|
+
}
|
|
3398
|
+
return w.finish();
|
|
3399
|
+
}
|
|
3400
|
+
function decodeChain(data) {
|
|
3401
|
+
const r = new ProtoReader(data);
|
|
3402
|
+
let direction = 0;
|
|
3403
|
+
let currentEpoch = 0n;
|
|
3404
|
+
const links = [];
|
|
3405
|
+
let nextRoot = EMPTY_BYTES;
|
|
3406
|
+
let sendEpoch = 0n;
|
|
3407
|
+
let params;
|
|
3408
|
+
while (!r.done) {
|
|
3409
|
+
const field = r.readField();
|
|
3410
|
+
if (field === null) break;
|
|
3411
|
+
switch (field.fieldNumber) {
|
|
3412
|
+
case 1:
|
|
3413
|
+
direction = r.readEnum();
|
|
3414
|
+
break;
|
|
3415
|
+
case 2:
|
|
3416
|
+
currentEpoch = r.readVarint64();
|
|
3417
|
+
break;
|
|
3418
|
+
case 3:
|
|
3419
|
+
links.push(decodeEpoch(r.readBytes()));
|
|
3420
|
+
break;
|
|
3421
|
+
case 4:
|
|
3422
|
+
nextRoot = r.readBytes();
|
|
3423
|
+
break;
|
|
3424
|
+
case 5:
|
|
3425
|
+
sendEpoch = r.readVarint64();
|
|
3426
|
+
break;
|
|
3427
|
+
case 6:
|
|
3428
|
+
params = decodeChainParams(r.readBytes());
|
|
3429
|
+
break;
|
|
3430
|
+
default: r.skip(field.wireType);
|
|
3431
|
+
}
|
|
3432
|
+
}
|
|
3433
|
+
return {
|
|
3434
|
+
direction,
|
|
3435
|
+
currentEpoch,
|
|
3436
|
+
links,
|
|
3437
|
+
nextRoot,
|
|
3438
|
+
sendEpoch,
|
|
3439
|
+
params
|
|
3440
|
+
};
|
|
3441
|
+
}
|
|
3442
|
+
function decodeAuthenticator(data) {
|
|
3443
|
+
const r = new ProtoReader(data);
|
|
3444
|
+
let rootKey = EMPTY_BYTES;
|
|
3445
|
+
let macKey = EMPTY_BYTES;
|
|
3446
|
+
while (!r.done) {
|
|
3447
|
+
const field = r.readField();
|
|
3448
|
+
if (field === null) break;
|
|
3449
|
+
switch (field.fieldNumber) {
|
|
3450
|
+
case 1:
|
|
3451
|
+
rootKey = r.readBytes();
|
|
3452
|
+
break;
|
|
3453
|
+
case 2:
|
|
3454
|
+
macKey = r.readBytes();
|
|
3455
|
+
break;
|
|
3456
|
+
default: r.skip(field.wireType);
|
|
3457
|
+
}
|
|
3458
|
+
}
|
|
3459
|
+
return {
|
|
3460
|
+
rootKey,
|
|
3461
|
+
macKey
|
|
3462
|
+
};
|
|
3463
|
+
}
|
|
3464
|
+
function encodePolynomialEncoder(msg) {
|
|
3465
|
+
const w = new ProtoWriter();
|
|
3466
|
+
w.writeVarint(1, msg.idx);
|
|
3467
|
+
for (const pt of msg.pts) w.writeBytes(2, pt);
|
|
3468
|
+
for (const poly of msg.polys) w.writeBytes(3, poly);
|
|
3469
|
+
return w.finish();
|
|
3470
|
+
}
|
|
3471
|
+
function decodePolynomialEncoder(data) {
|
|
3472
|
+
const r = new ProtoReader(data);
|
|
3473
|
+
let idx = 0;
|
|
3474
|
+
const pts = [];
|
|
3475
|
+
const polys = [];
|
|
3476
|
+
while (!r.done) {
|
|
3477
|
+
const field = r.readField();
|
|
3478
|
+
if (field === null) break;
|
|
3479
|
+
switch (field.fieldNumber) {
|
|
3480
|
+
case 1:
|
|
3481
|
+
idx = r.readVarint();
|
|
3482
|
+
break;
|
|
3483
|
+
case 2:
|
|
3484
|
+
pts.push(r.readBytes());
|
|
3485
|
+
break;
|
|
3486
|
+
case 3:
|
|
3487
|
+
polys.push(r.readBytes());
|
|
3488
|
+
break;
|
|
3489
|
+
default: r.skip(field.wireType);
|
|
3490
|
+
}
|
|
3491
|
+
}
|
|
3492
|
+
return {
|
|
3493
|
+
idx,
|
|
3494
|
+
pts,
|
|
3495
|
+
polys
|
|
3496
|
+
};
|
|
3497
|
+
}
|
|
3498
|
+
function encodePolynomialDecoder(msg) {
|
|
3499
|
+
const w = new ProtoWriter();
|
|
3500
|
+
w.writeVarint(1, msg.ptsNeeded);
|
|
3501
|
+
w.writeVarint(2, msg.polys);
|
|
3502
|
+
for (const pt of msg.pts) w.writeBytes(3, pt);
|
|
3503
|
+
w.writeBool(4, msg.isComplete);
|
|
3504
|
+
return w.finish();
|
|
3505
|
+
}
|
|
3506
|
+
function decodePolynomialDecoder(data) {
|
|
3507
|
+
const r = new ProtoReader(data);
|
|
3508
|
+
let ptsNeeded = 0;
|
|
3509
|
+
let polys = 0;
|
|
3510
|
+
const pts = [];
|
|
3511
|
+
let isComplete = false;
|
|
3512
|
+
while (!r.done) {
|
|
3513
|
+
const field = r.readField();
|
|
3514
|
+
if (field === null) break;
|
|
3515
|
+
switch (field.fieldNumber) {
|
|
3516
|
+
case 1:
|
|
3517
|
+
ptsNeeded = r.readVarint();
|
|
3518
|
+
break;
|
|
3519
|
+
case 2:
|
|
3520
|
+
polys = r.readVarint();
|
|
3521
|
+
break;
|
|
3522
|
+
case 3:
|
|
3523
|
+
pts.push(r.readBytes());
|
|
3524
|
+
break;
|
|
3525
|
+
case 4:
|
|
3526
|
+
isComplete = r.readBool();
|
|
3527
|
+
break;
|
|
3528
|
+
default: r.skip(field.wireType);
|
|
3529
|
+
}
|
|
3530
|
+
}
|
|
3531
|
+
return {
|
|
3532
|
+
ptsNeeded,
|
|
3533
|
+
polys,
|
|
3534
|
+
pts,
|
|
3535
|
+
isComplete
|
|
3536
|
+
};
|
|
3537
|
+
}
|
|
3538
|
+
function encodeAuthOptional(w, fieldNumber, auth) {
|
|
3539
|
+
if (auth !== void 0) {
|
|
3540
|
+
const sub = new ProtoWriter();
|
|
3541
|
+
sub.writeBytes(1, auth.rootKey);
|
|
3542
|
+
sub.writeBytes(2, auth.macKey);
|
|
3543
|
+
w.writeMessage(fieldNumber, sub);
|
|
3544
|
+
}
|
|
3545
|
+
}
|
|
3546
|
+
function decodeAuthOptional(r) {
|
|
3547
|
+
return decodeAuthenticator(r.readBytes());
|
|
3548
|
+
}
|
|
3549
|
+
/**
|
|
3550
|
+
* Detect whether an unchunked sub-message uses the Rust field layout
|
|
3551
|
+
* (epoch at field 1, auth at field 2, data fields shifted +1) or the
|
|
3552
|
+
* TS layout (auth at field 1, data fields as-is).
|
|
3553
|
+
*
|
|
3554
|
+
* Returns the field number offset: 0 for TS layout, 1 for Rust layout.
|
|
3555
|
+
* Detection is based on field 1's wire type:
|
|
3556
|
+
* - varint (wire type 0) => Rust layout (field 1 = epoch)
|
|
3557
|
+
* - length-delimited (wire type 2) => TS layout (field 1 = auth)
|
|
3558
|
+
*/
|
|
3559
|
+
function detectUcLayout(data) {
|
|
3560
|
+
if (data.length === 0) return 0;
|
|
3561
|
+
return (data[0] & 7) === WIRE_VARINT ? 1 : 0;
|
|
3562
|
+
}
|
|
3563
|
+
function encodeUcKeysUnsampled(msg) {
|
|
3564
|
+
const w = new ProtoWriter();
|
|
3565
|
+
encodeAuthOptional(w, 1, msg.auth);
|
|
3566
|
+
return w.finish();
|
|
3567
|
+
}
|
|
3568
|
+
function decodeUcKeysUnsampled(data) {
|
|
3569
|
+
const r = new ProtoReader(data);
|
|
3570
|
+
const offset = detectUcLayout(data);
|
|
3571
|
+
let auth;
|
|
3572
|
+
while (!r.done) {
|
|
3573
|
+
const field = r.readField();
|
|
3574
|
+
if (field === null) break;
|
|
3575
|
+
switch (field.fieldNumber - offset) {
|
|
3576
|
+
case 1:
|
|
3577
|
+
auth = decodeAuthOptional(r);
|
|
3578
|
+
break;
|
|
3579
|
+
default: r.skip(field.wireType);
|
|
3580
|
+
}
|
|
3581
|
+
}
|
|
3582
|
+
return { auth };
|
|
3583
|
+
}
|
|
3584
|
+
function encodeUcKeysSampled(msg) {
|
|
3585
|
+
const w = new ProtoWriter();
|
|
3586
|
+
encodeAuthOptional(w, 1, msg.auth);
|
|
3587
|
+
w.writeBytes(2, msg.ek);
|
|
3588
|
+
w.writeBytes(3, msg.dk);
|
|
3589
|
+
w.writeBytes(4, msg.hdr);
|
|
3590
|
+
w.writeBytes(5, msg.hdrMac);
|
|
3591
|
+
return w.finish();
|
|
3592
|
+
}
|
|
3593
|
+
function decodeUcKeysSampled(data) {
|
|
3594
|
+
const r = new ProtoReader(data);
|
|
3595
|
+
const offset = detectUcLayout(data);
|
|
3596
|
+
let auth;
|
|
3597
|
+
let ek = EMPTY_BYTES;
|
|
3598
|
+
let dk = EMPTY_BYTES;
|
|
3599
|
+
let hdr = EMPTY_BYTES;
|
|
3600
|
+
let hdrMac = EMPTY_BYTES;
|
|
3601
|
+
while (!r.done) {
|
|
3602
|
+
const field = r.readField();
|
|
3603
|
+
if (field === null) break;
|
|
3604
|
+
switch (field.fieldNumber - offset) {
|
|
3605
|
+
case 1:
|
|
3606
|
+
auth = decodeAuthOptional(r);
|
|
3607
|
+
break;
|
|
3608
|
+
case 2:
|
|
3609
|
+
ek = r.readBytes();
|
|
3610
|
+
break;
|
|
3611
|
+
case 3:
|
|
3612
|
+
dk = r.readBytes();
|
|
3613
|
+
break;
|
|
3614
|
+
case 4:
|
|
3615
|
+
hdr = r.readBytes();
|
|
3616
|
+
break;
|
|
3617
|
+
case 5:
|
|
3618
|
+
hdrMac = r.readBytes();
|
|
3619
|
+
break;
|
|
3620
|
+
default: r.skip(field.wireType);
|
|
3621
|
+
}
|
|
3622
|
+
}
|
|
3623
|
+
return {
|
|
3624
|
+
auth,
|
|
3625
|
+
ek,
|
|
3626
|
+
dk,
|
|
3627
|
+
hdr,
|
|
3628
|
+
hdrMac
|
|
3629
|
+
};
|
|
3630
|
+
}
|
|
3631
|
+
function encodeUcHeaderSent(msg) {
|
|
3632
|
+
const w = new ProtoWriter();
|
|
3633
|
+
encodeAuthOptional(w, 1, msg.auth);
|
|
3634
|
+
w.writeBytes(2, msg.ek);
|
|
3635
|
+
w.writeBytes(3, msg.dk);
|
|
3636
|
+
return w.finish();
|
|
3637
|
+
}
|
|
3638
|
+
function decodeUcHeaderSent(data) {
|
|
3639
|
+
const r = new ProtoReader(data);
|
|
3640
|
+
const offset = detectUcLayout(data);
|
|
3641
|
+
let auth;
|
|
3642
|
+
let ek = EMPTY_BYTES;
|
|
3643
|
+
let dk = EMPTY_BYTES;
|
|
3644
|
+
while (!r.done) {
|
|
3645
|
+
const field = r.readField();
|
|
3646
|
+
if (field === null) break;
|
|
3647
|
+
switch (field.fieldNumber - offset) {
|
|
3648
|
+
case 1:
|
|
3649
|
+
auth = decodeAuthOptional(r);
|
|
3650
|
+
break;
|
|
3651
|
+
case 2:
|
|
3652
|
+
ek = r.readBytes();
|
|
3653
|
+
break;
|
|
3654
|
+
case 3:
|
|
3655
|
+
dk = r.readBytes();
|
|
3656
|
+
break;
|
|
3657
|
+
default: r.skip(field.wireType);
|
|
3658
|
+
}
|
|
3659
|
+
}
|
|
3660
|
+
return {
|
|
3661
|
+
auth,
|
|
3662
|
+
ek,
|
|
3663
|
+
dk
|
|
3664
|
+
};
|
|
3665
|
+
}
|
|
3666
|
+
function encodeUcDkCt1(msg) {
|
|
3667
|
+
const w = new ProtoWriter();
|
|
3668
|
+
encodeAuthOptional(w, 1, msg.auth);
|
|
3669
|
+
w.writeBytes(2, msg.dk);
|
|
3670
|
+
w.writeBytes(3, msg.ct1);
|
|
3671
|
+
return w.finish();
|
|
3672
|
+
}
|
|
3673
|
+
function decodeUcDkCt1(data) {
|
|
3674
|
+
const r = new ProtoReader(data);
|
|
3675
|
+
const offset = detectUcLayout(data);
|
|
3676
|
+
let auth;
|
|
3677
|
+
let dk = EMPTY_BYTES;
|
|
3678
|
+
let ct1 = EMPTY_BYTES;
|
|
3679
|
+
while (!r.done) {
|
|
3680
|
+
const field = r.readField();
|
|
3681
|
+
if (field === null) break;
|
|
3682
|
+
switch (field.fieldNumber - offset) {
|
|
3683
|
+
case 1:
|
|
3684
|
+
auth = decodeAuthOptional(r);
|
|
3685
|
+
break;
|
|
3686
|
+
case 2:
|
|
3687
|
+
dk = r.readBytes();
|
|
3688
|
+
break;
|
|
3689
|
+
case 3:
|
|
3690
|
+
ct1 = r.readBytes();
|
|
3691
|
+
break;
|
|
3692
|
+
default: r.skip(field.wireType);
|
|
3693
|
+
}
|
|
3694
|
+
}
|
|
3695
|
+
return {
|
|
3696
|
+
auth,
|
|
3697
|
+
dk,
|
|
3698
|
+
ct1
|
|
3699
|
+
};
|
|
3700
|
+
}
|
|
3701
|
+
function encodeUcHeaderReceived(msg) {
|
|
3702
|
+
const w = new ProtoWriter();
|
|
3703
|
+
encodeAuthOptional(w, 1, msg.auth);
|
|
3704
|
+
w.writeBytes(2, msg.hdr);
|
|
3705
|
+
w.writeBytes(3, msg.es);
|
|
3706
|
+
w.writeBytes(4, msg.ct1);
|
|
3707
|
+
w.writeBytes(5, msg.ss);
|
|
3708
|
+
return w.finish();
|
|
3709
|
+
}
|
|
3710
|
+
function decodeUcHeaderReceived(data) {
|
|
3711
|
+
const r = new ProtoReader(data);
|
|
3712
|
+
const offset = detectUcLayout(data);
|
|
3713
|
+
let auth;
|
|
3714
|
+
let hdr = EMPTY_BYTES;
|
|
3715
|
+
let es = EMPTY_BYTES;
|
|
3716
|
+
let ct1 = EMPTY_BYTES;
|
|
3717
|
+
let ss = EMPTY_BYTES;
|
|
3718
|
+
while (!r.done) {
|
|
3719
|
+
const field = r.readField();
|
|
3720
|
+
if (field === null) break;
|
|
3721
|
+
switch (field.fieldNumber - offset) {
|
|
3722
|
+
case 1:
|
|
3723
|
+
auth = decodeAuthOptional(r);
|
|
3724
|
+
break;
|
|
3725
|
+
case 2:
|
|
3726
|
+
hdr = r.readBytes();
|
|
3727
|
+
break;
|
|
3728
|
+
case 3:
|
|
3729
|
+
es = r.readBytes();
|
|
3730
|
+
break;
|
|
3731
|
+
case 4:
|
|
3732
|
+
ct1 = r.readBytes();
|
|
3733
|
+
break;
|
|
3734
|
+
case 5:
|
|
3735
|
+
ss = r.readBytes();
|
|
3736
|
+
break;
|
|
3737
|
+
default: r.skip(field.wireType);
|
|
3738
|
+
}
|
|
3739
|
+
}
|
|
3740
|
+
return {
|
|
3741
|
+
auth,
|
|
3742
|
+
hdr,
|
|
3743
|
+
es,
|
|
3744
|
+
ct1,
|
|
3745
|
+
ss
|
|
3746
|
+
};
|
|
3747
|
+
}
|
|
3748
|
+
function encodeUcAuthHdrEsCt1(msg) {
|
|
3749
|
+
const w = new ProtoWriter();
|
|
3750
|
+
encodeAuthOptional(w, 1, msg.auth);
|
|
3751
|
+
w.writeBytes(2, msg.hdr);
|
|
3752
|
+
w.writeBytes(3, msg.es);
|
|
3753
|
+
w.writeBytes(4, msg.ct1);
|
|
3754
|
+
return w.finish();
|
|
3755
|
+
}
|
|
3756
|
+
function decodeUcAuthHdrEsCt1(data) {
|
|
3757
|
+
const r = new ProtoReader(data);
|
|
3758
|
+
const offset = detectUcLayout(data);
|
|
3759
|
+
let auth;
|
|
3760
|
+
let hdr = EMPTY_BYTES;
|
|
3761
|
+
let es = EMPTY_BYTES;
|
|
3762
|
+
let ct1 = EMPTY_BYTES;
|
|
3763
|
+
while (!r.done) {
|
|
3764
|
+
const field = r.readField();
|
|
3765
|
+
if (field === null) break;
|
|
3766
|
+
switch (field.fieldNumber - offset) {
|
|
3767
|
+
case 1:
|
|
3768
|
+
auth = decodeAuthOptional(r);
|
|
3769
|
+
break;
|
|
3770
|
+
case 2:
|
|
3771
|
+
hdr = r.readBytes();
|
|
3772
|
+
break;
|
|
3773
|
+
case 3:
|
|
3774
|
+
es = r.readBytes();
|
|
3775
|
+
break;
|
|
3776
|
+
case 4:
|
|
3777
|
+
ct1 = r.readBytes();
|
|
3778
|
+
break;
|
|
3779
|
+
default: r.skip(field.wireType);
|
|
3780
|
+
}
|
|
3781
|
+
}
|
|
3782
|
+
return {
|
|
3783
|
+
auth,
|
|
3784
|
+
hdr,
|
|
3785
|
+
es,
|
|
3786
|
+
ct1
|
|
3787
|
+
};
|
|
3788
|
+
}
|
|
3789
|
+
function encodeUcEkReceivedCt1Sampled(msg) {
|
|
3790
|
+
const w = new ProtoWriter();
|
|
3791
|
+
encodeAuthOptional(w, 1, msg.auth);
|
|
3792
|
+
w.writeBytes(2, msg.hdr);
|
|
3793
|
+
w.writeBytes(3, msg.es);
|
|
3794
|
+
w.writeBytes(4, msg.ek);
|
|
3795
|
+
w.writeBytes(5, msg.ct1);
|
|
3796
|
+
return w.finish();
|
|
3797
|
+
}
|
|
3798
|
+
function decodeUcEkReceivedCt1Sampled(data) {
|
|
3799
|
+
const r = new ProtoReader(data);
|
|
3800
|
+
const offset = detectUcLayout(data);
|
|
3801
|
+
let auth;
|
|
3802
|
+
let hdr = EMPTY_BYTES;
|
|
3803
|
+
let es = EMPTY_BYTES;
|
|
3804
|
+
let ek = EMPTY_BYTES;
|
|
3805
|
+
let ct1 = EMPTY_BYTES;
|
|
3806
|
+
while (!r.done) {
|
|
3807
|
+
const field = r.readField();
|
|
3808
|
+
if (field === null) break;
|
|
3809
|
+
switch (field.fieldNumber - offset) {
|
|
3810
|
+
case 1:
|
|
3811
|
+
auth = decodeAuthOptional(r);
|
|
3812
|
+
break;
|
|
3813
|
+
case 2:
|
|
3814
|
+
hdr = r.readBytes();
|
|
3815
|
+
break;
|
|
3816
|
+
case 3:
|
|
3817
|
+
es = r.readBytes();
|
|
3818
|
+
break;
|
|
3819
|
+
case 4:
|
|
3820
|
+
ek = r.readBytes();
|
|
3821
|
+
break;
|
|
3822
|
+
case 5:
|
|
3823
|
+
ct1 = r.readBytes();
|
|
3824
|
+
break;
|
|
3825
|
+
default: r.skip(field.wireType);
|
|
3826
|
+
}
|
|
3827
|
+
}
|
|
3828
|
+
return {
|
|
3829
|
+
auth,
|
|
3830
|
+
hdr,
|
|
3831
|
+
es,
|
|
3832
|
+
ek,
|
|
3833
|
+
ct1
|
|
3834
|
+
};
|
|
3835
|
+
}
|
|
3836
|
+
/**
|
|
3837
|
+
* Encode a PbChunkedState into a sub-message for V1State.
|
|
3838
|
+
* The field number in V1State determines which variant this is:
|
|
3839
|
+
* 1=keysUnsampled, 2=keysSampled, ..., 11=ct2Sampled
|
|
3840
|
+
*
|
|
3841
|
+
* Each chunked state has:
|
|
3842
|
+
* field 1 = unchunked data
|
|
3843
|
+
* field 2 = encoder (if present)
|
|
3844
|
+
* field 3 = decoder (if present)
|
|
3845
|
+
*/
|
|
3846
|
+
function encodeChunkedStateInner(state) {
|
|
3847
|
+
const w = new ProtoWriter();
|
|
3848
|
+
switch (state.type) {
|
|
3849
|
+
case "keysUnsampled":
|
|
3850
|
+
w.writeBytes(1, encodeUcKeysUnsampled(state.uc));
|
|
3851
|
+
return {
|
|
3852
|
+
fieldNumber: 1,
|
|
3853
|
+
data: w.finish()
|
|
3854
|
+
};
|
|
3855
|
+
case "keysSampled":
|
|
3856
|
+
w.writeBytes(1, encodeUcKeysSampled(state.uc));
|
|
3857
|
+
w.writeBytes(2, encodePolynomialEncoder(state.sendingHdr));
|
|
3858
|
+
return {
|
|
3859
|
+
fieldNumber: 2,
|
|
3860
|
+
data: w.finish()
|
|
3861
|
+
};
|
|
3862
|
+
case "headerSent":
|
|
3863
|
+
w.writeBytes(1, encodeUcHeaderSent(state.uc));
|
|
3864
|
+
w.writeBytes(2, encodePolynomialEncoder(state.sendingEk));
|
|
3865
|
+
w.writeBytes(3, encodePolynomialDecoder(state.receivingCt1));
|
|
3866
|
+
return {
|
|
3867
|
+
fieldNumber: 3,
|
|
3868
|
+
data: w.finish()
|
|
3869
|
+
};
|
|
3870
|
+
case "ct1Received":
|
|
3871
|
+
w.writeBytes(1, encodeUcDkCt1(state.uc));
|
|
3872
|
+
w.writeBytes(2, encodePolynomialEncoder(state.sendingEk));
|
|
3873
|
+
return {
|
|
3874
|
+
fieldNumber: 4,
|
|
3875
|
+
data: w.finish()
|
|
3876
|
+
};
|
|
3877
|
+
case "ekSentCt1Received":
|
|
3878
|
+
w.writeBytes(1, encodeUcDkCt1(state.uc));
|
|
3879
|
+
w.writeBytes(3, encodePolynomialDecoder(state.receivingCt2));
|
|
3880
|
+
return {
|
|
3881
|
+
fieldNumber: 5,
|
|
3882
|
+
data: w.finish()
|
|
3883
|
+
};
|
|
3884
|
+
case "noHeaderReceived":
|
|
3885
|
+
w.writeBytes(1, encodeUcKeysUnsampled(state.uc));
|
|
3886
|
+
w.writeBytes(2, encodePolynomialDecoder(state.receivingHdr));
|
|
3887
|
+
return {
|
|
3888
|
+
fieldNumber: 6,
|
|
3889
|
+
data: w.finish()
|
|
3890
|
+
};
|
|
3891
|
+
case "headerReceived":
|
|
3892
|
+
w.writeBytes(1, encodeUcHeaderReceived(state.uc));
|
|
3893
|
+
w.writeBytes(2, encodePolynomialDecoder(state.receivingEk));
|
|
3894
|
+
return {
|
|
3895
|
+
fieldNumber: 7,
|
|
3896
|
+
data: w.finish()
|
|
3897
|
+
};
|
|
3898
|
+
case "ct1Sampled":
|
|
3899
|
+
w.writeBytes(1, encodeUcAuthHdrEsCt1(state.uc));
|
|
3900
|
+
w.writeBytes(2, encodePolynomialEncoder(state.sendingCt1));
|
|
3901
|
+
w.writeBytes(3, encodePolynomialDecoder(state.receivingEk));
|
|
3902
|
+
return {
|
|
3903
|
+
fieldNumber: 8,
|
|
3904
|
+
data: w.finish()
|
|
3905
|
+
};
|
|
3906
|
+
case "ekReceivedCt1Sampled":
|
|
3907
|
+
w.writeBytes(1, encodeUcEkReceivedCt1Sampled(state.uc));
|
|
3908
|
+
w.writeBytes(2, encodePolynomialEncoder(state.sendingCt1));
|
|
3909
|
+
return {
|
|
3910
|
+
fieldNumber: 9,
|
|
3911
|
+
data: w.finish()
|
|
3912
|
+
};
|
|
3913
|
+
case "ct1Acknowledged":
|
|
3914
|
+
w.writeBytes(1, encodeUcAuthHdrEsCt1(state.uc));
|
|
3915
|
+
w.writeBytes(2, encodePolynomialDecoder(state.receivingEk));
|
|
3916
|
+
return {
|
|
3917
|
+
fieldNumber: 10,
|
|
3918
|
+
data: w.finish()
|
|
3919
|
+
};
|
|
3920
|
+
case "ct2Sampled":
|
|
3921
|
+
w.writeBytes(1, encodeUcKeysUnsampled(state.uc));
|
|
3922
|
+
w.writeBytes(2, encodePolynomialEncoder(state.sendingCt2));
|
|
3923
|
+
return {
|
|
3924
|
+
fieldNumber: 11,
|
|
3925
|
+
data: w.finish()
|
|
3926
|
+
};
|
|
3927
|
+
}
|
|
3928
|
+
}
|
|
3929
|
+
function decodeChunkedRaw(data) {
|
|
3930
|
+
const r = new ProtoReader(data);
|
|
3931
|
+
let uc = EMPTY_BYTES;
|
|
3932
|
+
let encoder;
|
|
3933
|
+
let decoder;
|
|
3934
|
+
while (!r.done) {
|
|
3935
|
+
const field = r.readField();
|
|
3936
|
+
if (field === null) break;
|
|
3937
|
+
switch (field.fieldNumber) {
|
|
3938
|
+
case 1:
|
|
3939
|
+
uc = r.readBytes();
|
|
3940
|
+
break;
|
|
3941
|
+
case 2:
|
|
3942
|
+
encoder = r.readBytes();
|
|
3943
|
+
break;
|
|
3944
|
+
case 3:
|
|
3945
|
+
decoder = r.readBytes();
|
|
3946
|
+
break;
|
|
3947
|
+
default: r.skip(field.wireType);
|
|
3948
|
+
}
|
|
3949
|
+
}
|
|
3950
|
+
return {
|
|
3951
|
+
uc,
|
|
3952
|
+
encoder,
|
|
3953
|
+
decoder
|
|
3954
|
+
};
|
|
3955
|
+
}
|
|
3956
|
+
function requireField(data, name) {
|
|
3957
|
+
if (data === void 0) throw new Error(`Protobuf: missing required field '${name}'`);
|
|
3958
|
+
return data;
|
|
3959
|
+
}
|
|
3960
|
+
function decodeChunkedState(fieldNumber, data) {
|
|
3961
|
+
const raw = decodeChunkedRaw(data);
|
|
3962
|
+
switch (fieldNumber) {
|
|
3963
|
+
case 1: return {
|
|
3964
|
+
type: "keysUnsampled",
|
|
3965
|
+
uc: decodeUcKeysUnsampled(raw.uc)
|
|
3966
|
+
};
|
|
3967
|
+
case 2: return {
|
|
3968
|
+
type: "keysSampled",
|
|
3969
|
+
uc: decodeUcKeysSampled(raw.uc),
|
|
3970
|
+
sendingHdr: decodePolynomialEncoder(requireField(raw.encoder, "encoder"))
|
|
3971
|
+
};
|
|
3972
|
+
case 3: return {
|
|
3973
|
+
type: "headerSent",
|
|
3974
|
+
uc: decodeUcHeaderSent(raw.uc),
|
|
3975
|
+
sendingEk: decodePolynomialEncoder(requireField(raw.encoder, "encoder")),
|
|
3976
|
+
receivingCt1: decodePolynomialDecoder(requireField(raw.decoder, "decoder"))
|
|
3977
|
+
};
|
|
3978
|
+
case 4: return {
|
|
3979
|
+
type: "ct1Received",
|
|
3980
|
+
uc: decodeUcDkCt1(raw.uc),
|
|
3981
|
+
sendingEk: decodePolynomialEncoder(requireField(raw.encoder, "encoder"))
|
|
3982
|
+
};
|
|
3983
|
+
case 5: return {
|
|
3984
|
+
type: "ekSentCt1Received",
|
|
3985
|
+
uc: decodeUcDkCt1(raw.uc),
|
|
3986
|
+
receivingCt2: decodePolynomialDecoder(requireField(raw.decoder, "decoder"))
|
|
3987
|
+
};
|
|
3988
|
+
case 6: return {
|
|
3989
|
+
type: "noHeaderReceived",
|
|
3990
|
+
uc: decodeUcKeysUnsampled(raw.uc),
|
|
3991
|
+
receivingHdr: decodePolynomialDecoder(requireField(raw.encoder, "encoder"))
|
|
3992
|
+
};
|
|
3993
|
+
case 7: return {
|
|
3994
|
+
type: "headerReceived",
|
|
3995
|
+
uc: decodeUcHeaderReceived(raw.uc),
|
|
3996
|
+
receivingEk: decodePolynomialDecoder(requireField(raw.encoder, "encoder"))
|
|
3997
|
+
};
|
|
3998
|
+
case 8: return {
|
|
3999
|
+
type: "ct1Sampled",
|
|
4000
|
+
uc: decodeUcAuthHdrEsCt1(raw.uc),
|
|
4001
|
+
sendingCt1: decodePolynomialEncoder(requireField(raw.encoder, "encoder")),
|
|
4002
|
+
receivingEk: decodePolynomialDecoder(requireField(raw.decoder, "decoder"))
|
|
4003
|
+
};
|
|
4004
|
+
case 9: return {
|
|
4005
|
+
type: "ekReceivedCt1Sampled",
|
|
4006
|
+
uc: decodeUcEkReceivedCt1Sampled(raw.uc),
|
|
4007
|
+
sendingCt1: decodePolynomialEncoder(requireField(raw.encoder, "encoder"))
|
|
4008
|
+
};
|
|
4009
|
+
case 10: return {
|
|
4010
|
+
type: "ct1Acknowledged",
|
|
4011
|
+
uc: decodeUcAuthHdrEsCt1(raw.uc),
|
|
4012
|
+
receivingEk: decodePolynomialDecoder(requireField(raw.encoder, "encoder"))
|
|
4013
|
+
};
|
|
4014
|
+
case 11: return {
|
|
4015
|
+
type: "ct2Sampled",
|
|
4016
|
+
uc: decodeUcKeysUnsampled(raw.uc),
|
|
4017
|
+
sendingCt2: decodePolynomialEncoder(requireField(raw.encoder, "encoder"))
|
|
4018
|
+
};
|
|
4019
|
+
default: throw new Error(`Unknown chunked state field number: ${fieldNumber}`);
|
|
4020
|
+
}
|
|
4021
|
+
}
|
|
4022
|
+
function encodeV1State(msg) {
|
|
4023
|
+
if (msg.innerState === void 0) return EMPTY_BYTES;
|
|
4024
|
+
const { fieldNumber, data } = encodeChunkedStateInner(msg.innerState);
|
|
4025
|
+
const w = new ProtoWriter();
|
|
4026
|
+
w.writeBytes(fieldNumber, data);
|
|
4027
|
+
if (msg.epoch !== void 0) w.writeVarint(12, msg.epoch);
|
|
4028
|
+
return w.finish();
|
|
4029
|
+
}
|
|
4030
|
+
function decodeV1State(data) {
|
|
4031
|
+
const r = new ProtoReader(data);
|
|
4032
|
+
let innerState;
|
|
4033
|
+
let epoch;
|
|
4034
|
+
while (!r.done) {
|
|
4035
|
+
const field = r.readField();
|
|
4036
|
+
if (field === null) break;
|
|
4037
|
+
if (field.fieldNumber >= 1 && field.fieldNumber <= 11) innerState = decodeChunkedState(field.fieldNumber, r.readBytes());
|
|
4038
|
+
else if (field.fieldNumber === 12) epoch = r.readVarint64();
|
|
4039
|
+
else r.skip(field.wireType);
|
|
4040
|
+
}
|
|
4041
|
+
return {
|
|
4042
|
+
innerState,
|
|
4043
|
+
epoch
|
|
4044
|
+
};
|
|
4045
|
+
}
|
|
4046
|
+
function encodePqRatchetState(msg) {
|
|
4047
|
+
const w = new ProtoWriter();
|
|
4048
|
+
if (msg.versionNegotiation !== void 0) w.writeBytes(1, encodeVersionNegotiation(msg.versionNegotiation));
|
|
4049
|
+
if (msg.chain !== void 0) w.writeBytes(2, encodeChain(msg.chain));
|
|
4050
|
+
if (msg.v1 !== void 0) w.writeBytes(3, encodeV1State(msg.v1));
|
|
4051
|
+
return w.finish();
|
|
4052
|
+
}
|
|
4053
|
+
function decodePqRatchetState(data) {
|
|
4054
|
+
const r = new ProtoReader(data);
|
|
4055
|
+
let versionNegotiation;
|
|
4056
|
+
let chain;
|
|
4057
|
+
let v1;
|
|
4058
|
+
while (!r.done) {
|
|
4059
|
+
const field = r.readField();
|
|
4060
|
+
if (field === null) break;
|
|
4061
|
+
switch (field.fieldNumber) {
|
|
4062
|
+
case 1:
|
|
4063
|
+
versionNegotiation = decodeVersionNegotiation(r.readBytes());
|
|
4064
|
+
break;
|
|
4065
|
+
case 2:
|
|
4066
|
+
chain = decodeChain(r.readBytes());
|
|
4067
|
+
break;
|
|
4068
|
+
case 3:
|
|
4069
|
+
v1 = decodeV1State(r.readBytes());
|
|
4070
|
+
break;
|
|
4071
|
+
default: r.skip(field.wireType);
|
|
4072
|
+
}
|
|
4073
|
+
}
|
|
4074
|
+
return {
|
|
4075
|
+
versionNegotiation,
|
|
4076
|
+
chain,
|
|
4077
|
+
v1
|
|
4078
|
+
};
|
|
4079
|
+
}
|
|
4080
|
+
|
|
4081
|
+
//#endregion
|
|
4082
|
+
//#region src/index.ts
|
|
4083
|
+
/**
|
|
4084
|
+
* Copyright © 2025 Signal Messenger, LLC
|
|
4085
|
+
* Copyright © 2026 Parity Technologies
|
|
4086
|
+
*
|
|
4087
|
+
* Top-level public API for the SPQR protocol.
|
|
4088
|
+
*
|
|
4089
|
+
* Matches Signal's Rust `lib.rs` interface. All state is serialized as
|
|
4090
|
+
* opaque protobuf bytes (Uint8Array) so that callers never need to touch
|
|
4091
|
+
* internal types.
|
|
4092
|
+
*
|
|
4093
|
+
* Exported functions:
|
|
4094
|
+
* - emptyState() -> empty serialized state (V0)
|
|
4095
|
+
* - initialState(p) -> create initial serialized state
|
|
4096
|
+
* - send(state, rng) -> produce next message + advance state
|
|
4097
|
+
* - recv(state, msg) -> consume incoming message + advance state
|
|
4098
|
+
* - currentVersion(s) -> inspect version negotiation status
|
|
4099
|
+
*/
|
|
4100
|
+
/**
|
|
4101
|
+
* Return an empty (V0) serialized state.
|
|
4102
|
+
*/
|
|
4103
|
+
function emptyState() {
|
|
4104
|
+
return new Uint8Array(0);
|
|
4105
|
+
}
|
|
4106
|
+
/**
|
|
4107
|
+
* Create an initial serialized state from parameters.
|
|
4108
|
+
*
|
|
4109
|
+
* For V0, returns an empty state. For V1+, initializes the inner V1
|
|
4110
|
+
* state machine and version negotiation.
|
|
4111
|
+
*/
|
|
4112
|
+
function initialState(params) {
|
|
4113
|
+
if (params.version === Version.V0) return emptyState();
|
|
4114
|
+
const inner = initInner(params.version, params.direction, params.authKey);
|
|
4115
|
+
return encodePqRatchetState({
|
|
4116
|
+
versionNegotiation: {
|
|
4117
|
+
authKey: Uint8Array.from(params.authKey),
|
|
4118
|
+
direction: params.direction,
|
|
4119
|
+
minVersion: params.minVersion,
|
|
4120
|
+
chainParams: {
|
|
4121
|
+
maxJump: params.chainParams.maxJump,
|
|
4122
|
+
maxOooKeys: params.chainParams.maxOooKeys
|
|
4123
|
+
}
|
|
4124
|
+
},
|
|
4125
|
+
chain: void 0,
|
|
4126
|
+
v1: inner
|
|
4127
|
+
});
|
|
4128
|
+
}
|
|
4129
|
+
/**
|
|
4130
|
+
* Produce the next outgoing message from the current state.
|
|
4131
|
+
*
|
|
4132
|
+
* Returns the updated state, serialized message, and optional message key.
|
|
4133
|
+
*/
|
|
4134
|
+
function send(state, rng) {
|
|
4135
|
+
if (state.length === 0) return {
|
|
4136
|
+
state: new Uint8Array(0),
|
|
4137
|
+
msg: new Uint8Array(0),
|
|
4138
|
+
key: null
|
|
4139
|
+
};
|
|
4140
|
+
const statePb = decodePqRatchetState(state);
|
|
4141
|
+
if (statePb.v1 === void 0) return {
|
|
4142
|
+
state: new Uint8Array(0),
|
|
4143
|
+
msg: new Uint8Array(0),
|
|
4144
|
+
key: null
|
|
4145
|
+
};
|
|
4146
|
+
const sendResult = send$1(statesFromPb(statePb.v1), rng);
|
|
4147
|
+
let chain;
|
|
4148
|
+
if (statePb.chain !== void 0) chain = Chain.fromProto(statePb.chain);
|
|
4149
|
+
else if (statePb.versionNegotiation !== void 0) {
|
|
4150
|
+
const vn = statePb.versionNegotiation;
|
|
4151
|
+
if (vn.minVersion > Version.V0) chain = chainFromVersionNegotiation(vn);
|
|
4152
|
+
} else throw new SpqrError("Chain not available and no version negotiation", SpqrErrorCode.ChainNotAvailable);
|
|
4153
|
+
let index;
|
|
4154
|
+
let msgKey;
|
|
4155
|
+
let chainPb;
|
|
4156
|
+
if (chain === void 0) {
|
|
4157
|
+
if (sendResult.key !== null) throw new SpqrError("Unexpected epoch secret without chain", SpqrErrorCode.ChainNotAvailable);
|
|
4158
|
+
index = 0;
|
|
4159
|
+
msgKey = new Uint8Array(0);
|
|
4160
|
+
chainPb = void 0;
|
|
4161
|
+
} else {
|
|
4162
|
+
if (sendResult.key !== null) chain.addEpoch(sendResult.key);
|
|
4163
|
+
const msgEpoch = sendResult.msg.epoch - 1n;
|
|
4164
|
+
const [sendIndex, sendKey] = chain.sendKey(msgEpoch);
|
|
4165
|
+
index = sendIndex;
|
|
4166
|
+
msgKey = sendKey;
|
|
4167
|
+
chainPb = chain.toProto();
|
|
4168
|
+
}
|
|
4169
|
+
const serializedMsg = serializeMessage(sendResult.msg, index);
|
|
4170
|
+
const v1Pb = statesToPb(sendResult.state);
|
|
4171
|
+
return {
|
|
4172
|
+
state: encodePqRatchetState({
|
|
4173
|
+
versionNegotiation: statePb.versionNegotiation,
|
|
4174
|
+
chain: chainPb,
|
|
4175
|
+
v1: v1Pb
|
|
4176
|
+
}),
|
|
4177
|
+
msg: serializedMsg,
|
|
4178
|
+
key: msgKey.length === 0 ? null : msgKey
|
|
4179
|
+
};
|
|
4180
|
+
}
|
|
4181
|
+
/**
|
|
4182
|
+
* Process an incoming message and transition the state.
|
|
4183
|
+
*
|
|
4184
|
+
* Returns the updated state and optional message key.
|
|
4185
|
+
*/
|
|
4186
|
+
function recv(state, msg) {
|
|
4187
|
+
if (state.length === 0 && msg.length === 0) return {
|
|
4188
|
+
state: new Uint8Array(0),
|
|
4189
|
+
key: null
|
|
4190
|
+
};
|
|
4191
|
+
const prenegotiatedPb = state.length === 0 ? {
|
|
4192
|
+
v1: void 0,
|
|
4193
|
+
chain: void 0,
|
|
4194
|
+
versionNegotiation: void 0
|
|
4195
|
+
} : decodePqRatchetState(state);
|
|
4196
|
+
const msgVer = msgVersion(msg);
|
|
4197
|
+
if (msgVer === void 0) return {
|
|
4198
|
+
state: Uint8Array.from(state),
|
|
4199
|
+
key: null
|
|
4200
|
+
};
|
|
4201
|
+
const stateVer = stateVersion(prenegotiatedPb);
|
|
4202
|
+
let statePb;
|
|
4203
|
+
if (msgVer >= stateVer) statePb = prenegotiatedPb;
|
|
4204
|
+
else {
|
|
4205
|
+
const vn = prenegotiatedPb.versionNegotiation;
|
|
4206
|
+
if (vn === void 0) throw new SpqrError(`Version mismatch: state=${stateVer}, msg=${msgVer}, no negotiation available`, SpqrErrorCode.VersionMismatch);
|
|
4207
|
+
if (msgVer < vn.minVersion) throw new SpqrError(`Minimum version not met: min=${vn.minVersion}, msg=${msgVer}`, SpqrErrorCode.MinimumVersion);
|
|
4208
|
+
statePb = {
|
|
4209
|
+
v1: initInner(msgVer, vn.direction, Uint8Array.from(vn.authKey)),
|
|
4210
|
+
versionNegotiation: void 0,
|
|
4211
|
+
chain: chainFrom(prenegotiatedPb.chain, vn)
|
|
4212
|
+
};
|
|
4213
|
+
}
|
|
4214
|
+
if (statePb.v1 === void 0) return {
|
|
4215
|
+
state: new Uint8Array(0),
|
|
4216
|
+
key: null
|
|
4217
|
+
};
|
|
4218
|
+
const { msg: sckaMsg, index } = deserializeMessage(msg);
|
|
4219
|
+
const recvResult = recv$1(statesFromPb(statePb.v1), sckaMsg);
|
|
4220
|
+
const msgKeyEpoch = sckaMsg.epoch - 1n;
|
|
4221
|
+
const chainObj = chainFromState(statePb.chain, statePb.versionNegotiation);
|
|
4222
|
+
if (recvResult.key !== null) chainObj.addEpoch(recvResult.key);
|
|
4223
|
+
let msgKey;
|
|
4224
|
+
if (msgKeyEpoch === 0n && index === 0) msgKey = new Uint8Array(0);
|
|
4225
|
+
else msgKey = chainObj.recvKey(msgKeyEpoch, index);
|
|
4226
|
+
const v1Pb = statesToPb(recvResult.state);
|
|
4227
|
+
return {
|
|
4228
|
+
state: encodePqRatchetState({
|
|
4229
|
+
versionNegotiation: void 0,
|
|
4230
|
+
chain: chainObj.toProto(),
|
|
4231
|
+
v1: v1Pb
|
|
4232
|
+
}),
|
|
4233
|
+
key: msgKey.length === 0 ? null : msgKey
|
|
4234
|
+
};
|
|
4235
|
+
}
|
|
4236
|
+
/**
|
|
4237
|
+
* Inspect the current version negotiation status of a serialized state.
|
|
4238
|
+
*/
|
|
4239
|
+
function currentVersion(state) {
|
|
4240
|
+
if (state.length === 0) return {
|
|
4241
|
+
type: "negotiation_complete",
|
|
4242
|
+
version: Version.V0
|
|
4243
|
+
};
|
|
4244
|
+
const statePb = decodePqRatchetState(state);
|
|
4245
|
+
const version = statePb.v1 !== void 0 ? Version.V1 : Version.V0;
|
|
4246
|
+
if (statePb.versionNegotiation !== void 0) return {
|
|
4247
|
+
type: "still_negotiating",
|
|
4248
|
+
version,
|
|
4249
|
+
minVersion: statePb.versionNegotiation.minVersion
|
|
4250
|
+
};
|
|
4251
|
+
return {
|
|
4252
|
+
type: "negotiation_complete",
|
|
4253
|
+
version
|
|
4254
|
+
};
|
|
4255
|
+
}
|
|
4256
|
+
/**
|
|
4257
|
+
* Initialize the V1 inner state based on version and direction.
|
|
4258
|
+
*/
|
|
4259
|
+
function initInner(version, direction, authKey) {
|
|
4260
|
+
if (version === Version.V0) return;
|
|
4261
|
+
let states;
|
|
4262
|
+
if (direction === Direction.A2B) states = initA(authKey);
|
|
4263
|
+
else states = initB(authKey);
|
|
4264
|
+
return statesToPb(states);
|
|
4265
|
+
}
|
|
4266
|
+
/**
|
|
4267
|
+
* Extract version from a serialized message.
|
|
4268
|
+
* Empty msg -> V0. msg[0]: 0 -> V0, 1 -> V1, else undefined.
|
|
4269
|
+
*/
|
|
4270
|
+
function msgVersion(msg) {
|
|
4271
|
+
if (msg.length === 0) return Version.V0;
|
|
4272
|
+
const v = msg[0];
|
|
4273
|
+
if (v === 0) return Version.V0;
|
|
4274
|
+
if (v === 1) return Version.V1;
|
|
4275
|
+
}
|
|
4276
|
+
/**
|
|
4277
|
+
* Extract version from the decoded state.
|
|
4278
|
+
* No v1 inner -> V0. Has v1 inner -> V1.
|
|
4279
|
+
*/
|
|
4280
|
+
function stateVersion(state) {
|
|
4281
|
+
return state.v1 !== void 0 ? Version.V1 : Version.V0;
|
|
4282
|
+
}
|
|
4283
|
+
/**
|
|
4284
|
+
* Create a Chain from version negotiation parameters.
|
|
4285
|
+
*/
|
|
4286
|
+
function chainFromVersionNegotiation(vn) {
|
|
4287
|
+
const chainParams = vn.chainParams ?? {
|
|
4288
|
+
maxJump: 25e3,
|
|
4289
|
+
maxOooKeys: 2e3
|
|
4290
|
+
};
|
|
4291
|
+
return Chain.create(Uint8Array.from(vn.authKey), vn.direction, chainParams);
|
|
4292
|
+
}
|
|
4293
|
+
/**
|
|
4294
|
+
* Get or create a Chain from the existing chain proto and version negotiation.
|
|
4295
|
+
* Prefers existing chain, falls back to creating from version negotiation.
|
|
4296
|
+
*/
|
|
4297
|
+
function chainFrom(chainPb, vn) {
|
|
4298
|
+
if (chainPb !== void 0) return chainPb;
|
|
4299
|
+
if (vn !== void 0) return chainFromVersionNegotiation(vn).toProto();
|
|
4300
|
+
}
|
|
4301
|
+
/**
|
|
4302
|
+
* Get a Chain object from state, creating from vn if needed.
|
|
4303
|
+
*/
|
|
4304
|
+
function chainFromState(chainPb, vn) {
|
|
4305
|
+
if (chainPb !== void 0) return Chain.fromProto(chainPb);
|
|
4306
|
+
if (vn !== void 0) return chainFromVersionNegotiation(vn);
|
|
4307
|
+
throw new SpqrError("Chain not available and no version negotiation", SpqrErrorCode.ChainNotAvailable);
|
|
4308
|
+
}
|
|
4309
|
+
|
|
4310
|
+
//#endregion
|
|
4311
|
+
export { Direction, SpqrError, SpqrErrorCode, Version, currentVersion, emptyState, initialState, recv, send };
|
|
4312
|
+
//# sourceMappingURL=index.mjs.map
|