@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/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