@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.
@@ -0,0 +1,352 @@
1
+ /**
2
+ * Copyright © 2025 Signal Messenger, LLC
3
+ * Copyright © 2026 Parity Technologies
4
+ *
5
+ * Chunked send_ct state machine for SPQR V1.
6
+ *
7
+ * Wraps the unchunked send_ct states with PolyEncoder/PolyDecoder erasure
8
+ * coding, enabling chunk-by-chunk data transfer.
9
+ *
10
+ * True incremental ML-KEM (Phase 9):
11
+ * - send_ct produces REAL ct1 chunks from the start.
12
+ * - ct2 payload carries only ct2(128) + mac(32) = 160 bytes.
13
+ * - Epoch secret is derived in sendCt1 (when encaps1 is performed).
14
+ *
15
+ * States:
16
+ * NoHeaderReceived -- recvHdrChunk(epoch, chunk) --> NoHeaderReceived | HeaderReceived
17
+ * HeaderReceived -- sendCt1Chunk(rng) --> [Ct1Sampled, Chunk, EpochSecret]
18
+ * Ct1Sampled -- sendCt1Chunk() --> [Ct1Sampled, Chunk]
19
+ * -- recvEkChunk(epoch, chunk, ct1Ack) --> 4 outcomes
20
+ * EkReceivedCt1Sampled -- sendCt1Chunk() --> [EkReceivedCt1Sampled, Chunk]
21
+ * -- recvCt1Ack(epoch) --> Ct2Sampled
22
+ * Ct1Acknowledged -- recvEkChunk(epoch, chunk) --> Ct1Acknowledged | Ct2Sampled
23
+ * Ct2Sampled -- sendCt2Chunk() --> [Ct2Sampled, Chunk]
24
+ * -- recvNextEpoch(epoch) --> sendEk.KeysUnsampled
25
+ */
26
+
27
+ import * as unchunkedSendCt from "../unchunked/send-ct.js";
28
+ import * as unchunkedSendEk from "../unchunked/send-ek.js";
29
+ import { PolyEncoder, PolyDecoder } from "../../encoding/polynomial.js";
30
+ import type { Chunk } from "../../encoding/polynomial.js";
31
+ import { HEADER_SIZE, EK_SIZE } from "../../incremental-mlkem768.js";
32
+ import { MAC_SIZE } from "../../authenticator.js";
33
+ import { Authenticator } from "../../authenticator.js";
34
+ import { concat } from "../../util.js";
35
+ import { SpqrError, SpqrErrorCode } from "../../error.js";
36
+ import type { Epoch, EpochSecret, RandomBytes } from "../../types.js";
37
+ import * as sendEk from "./send-ek.js";
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // Discriminated union result types
41
+ // ---------------------------------------------------------------------------
42
+
43
+ export type NoHeaderReceivedRecvChunk =
44
+ | { done: false; state: NoHeaderReceived }
45
+ | { done: true; state: HeaderReceived };
46
+
47
+ export type Ct1SampledRecvChunk =
48
+ | { tag: "stillReceivingStillSending"; state: Ct1Sampled }
49
+ | { tag: "stillReceiving"; state: Ct1Acknowledged }
50
+ | { tag: "stillSending"; state: EkReceivedCt1Sampled; epochSecret: EpochSecret | null }
51
+ | { tag: "done"; state: Ct2Sampled; epochSecret: EpochSecret | null };
52
+
53
+ export type Ct1AcknowledgedRecvChunk =
54
+ | { done: false; state: Ct1Acknowledged }
55
+ | { done: true; state: Ct2Sampled; epochSecret: EpochSecret | null };
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // State 1: NoHeaderReceived
59
+ // ---------------------------------------------------------------------------
60
+
61
+ /**
62
+ * Waiting to receive header chunks from the send_ek peer.
63
+ */
64
+ export class NoHeaderReceived {
65
+ constructor(
66
+ public readonly uc: unchunkedSendCt.NoHeaderReceived,
67
+ public readonly receivingHdr: PolyDecoder,
68
+ ) {}
69
+
70
+ get epoch(): Epoch {
71
+ return this.uc.epoch;
72
+ }
73
+
74
+ /**
75
+ * Create the initial NoHeaderReceived from an auth key.
76
+ */
77
+ static create(authKey: Uint8Array): NoHeaderReceived {
78
+ const auth = Authenticator.create(authKey, 1n);
79
+ const uc = new unchunkedSendCt.NoHeaderReceived(1n, auth);
80
+ const receivingHdr = PolyDecoder.create(HEADER_SIZE + MAC_SIZE);
81
+ return new NoHeaderReceived(uc, receivingHdr);
82
+ }
83
+
84
+ /** Receive a header chunk. Returns done when header is fully decoded. */
85
+ recvHdrChunk(epoch: Epoch, chunk: Chunk): NoHeaderReceivedRecvChunk {
86
+ if (epoch !== this.uc.epoch) {
87
+ throw new SpqrError(
88
+ `Epoch mismatch: expected ${this.uc.epoch}, got ${epoch}`,
89
+ SpqrErrorCode.EpochOutOfRange,
90
+ );
91
+ }
92
+
93
+ this.receivingHdr.addChunk(chunk);
94
+ const decoded = this.receivingHdr.decodedMessage();
95
+
96
+ if (decoded !== null) {
97
+ // Split: hdr(64) + mac(32)
98
+ const hdr = decoded.slice(0, HEADER_SIZE);
99
+ const mac = decoded.slice(HEADER_SIZE);
100
+
101
+ // Verify header MAC and transition
102
+ const ucResult = this.uc.recvHeader(epoch, hdr, mac);
103
+
104
+ // Create ek decoder
105
+ const receivingEk = PolyDecoder.create(EK_SIZE);
106
+
107
+ return { done: true, state: new HeaderReceived(ucResult, receivingEk) };
108
+ }
109
+
110
+ return { done: false, state: this };
111
+ }
112
+ }
113
+
114
+ // ---------------------------------------------------------------------------
115
+ // State 2: HeaderReceived
116
+ // ---------------------------------------------------------------------------
117
+
118
+ /**
119
+ * Header has been received and verified. Ready to produce ct1 chunks.
120
+ */
121
+ export class HeaderReceived {
122
+ constructor(
123
+ public readonly uc: unchunkedSendCt.HeaderReceived,
124
+ public readonly receivingEk: PolyDecoder,
125
+ ) {}
126
+
127
+ get epoch(): Epoch {
128
+ return this.uc.epoch;
129
+ }
130
+
131
+ /**
132
+ * Generate encapsulation randomness, produce REAL ct1, encode it,
133
+ * and return the first ct1 chunk.
134
+ *
135
+ * Returns [Ct1Sampled, Chunk, EpochSecret] -- real epoch secret.
136
+ */
137
+ sendCt1Chunk(rng: RandomBytes): [Ct1Sampled, Chunk, EpochSecret] {
138
+ const [ct1Sent, realCt1, epochSecret] = this.uc.sendCt1(rng);
139
+ const sendingCt1 = PolyEncoder.encodeBytes(realCt1);
140
+ const chunk = sendingCt1.nextChunk();
141
+ return [new Ct1Sampled(ct1Sent, sendingCt1, this.receivingEk), chunk, epochSecret];
142
+ }
143
+ }
144
+
145
+ // ---------------------------------------------------------------------------
146
+ // State 3: Ct1Sampled
147
+ // ---------------------------------------------------------------------------
148
+
149
+ /**
150
+ * Real ct1 has been produced and encoding started.
151
+ * Sending ct1 chunks while receiving ek chunks.
152
+ */
153
+ export class Ct1Sampled {
154
+ constructor(
155
+ public readonly uc: unchunkedSendCt.Ct1Sent,
156
+ public readonly sendingCt1: PolyEncoder,
157
+ public readonly receivingEk: PolyDecoder,
158
+ ) {}
159
+
160
+ get epoch(): Epoch {
161
+ return this.uc.epoch;
162
+ }
163
+
164
+ /** Produce the next ct1 chunk. */
165
+ sendCt1Chunk(): [Ct1Sampled, Chunk] {
166
+ const chunk = this.sendingCt1.nextChunk();
167
+ return [this, chunk];
168
+ }
169
+
170
+ /**
171
+ * Receive an ek chunk, with optional ct1 acknowledgement from peer.
172
+ *
173
+ * Four possible outcomes depending on whether ek is complete and
174
+ * whether the peer acknowledged ct1:
175
+ * - Both: done (Ct2Sampled + epochSecret)
176
+ * - ek complete, no ack: stillSending (EkReceivedCt1Sampled + epochSecret)
177
+ * - ek incomplete, ack: stillReceiving (Ct1Acknowledged)
178
+ * - Neither: stillReceivingStillSending (Ct1Sampled)
179
+ */
180
+ recvEkChunk(epoch: Epoch, chunk: Chunk, ct1Ack: boolean): Ct1SampledRecvChunk {
181
+ if (epoch !== this.uc.epoch) {
182
+ throw new SpqrError(
183
+ `Epoch mismatch: expected ${this.uc.epoch}, got ${epoch}`,
184
+ SpqrErrorCode.EpochOutOfRange,
185
+ );
186
+ }
187
+
188
+ this.receivingEk.addChunk(chunk);
189
+ const ekDecoded = this.receivingEk.decodedMessage();
190
+
191
+ if (ekDecoded !== null && ct1Ack) {
192
+ // Both ek complete and ct1 acknowledged
193
+ const ucResult = this.uc.recvEk(ekDecoded);
194
+ const { state: ct2SentUc, ct2, mac } = ucResult.sendCt2();
195
+ // ct2 payload = ct2(128) + mac(32) = 160 bytes
196
+ const ct2Payload = concat(ct2, mac);
197
+ const sendingCt2 = PolyEncoder.encodeBytes(ct2Payload);
198
+ return {
199
+ tag: "done",
200
+ state: new Ct2Sampled(ct2SentUc, sendingCt2),
201
+ epochSecret: null, // Epoch secret already derived in sendCt1
202
+ };
203
+ }
204
+
205
+ if (ekDecoded !== null && !ct1Ack) {
206
+ // ek complete but ct1 not yet acknowledged
207
+ const ucResult = this.uc.recvEk(ekDecoded);
208
+ return {
209
+ tag: "stillSending",
210
+ state: new EkReceivedCt1Sampled(ucResult, this.sendingCt1),
211
+ epochSecret: null, // Epoch secret already derived in sendCt1
212
+ };
213
+ }
214
+
215
+ if (ekDecoded === null && ct1Ack) {
216
+ // ct1 acknowledged but ek not yet complete
217
+ return {
218
+ tag: "stillReceiving",
219
+ state: new Ct1Acknowledged(this.uc, this.receivingEk),
220
+ };
221
+ }
222
+
223
+ // Neither complete
224
+ return {
225
+ tag: "stillReceivingStillSending",
226
+ state: this,
227
+ };
228
+ }
229
+ }
230
+
231
+ // ---------------------------------------------------------------------------
232
+ // State 4: EkReceivedCt1Sampled
233
+ // ---------------------------------------------------------------------------
234
+
235
+ /**
236
+ * ek has been received and validated.
237
+ * Still sending ct1 chunks, waiting for ct1 acknowledgement.
238
+ */
239
+ export class EkReceivedCt1Sampled {
240
+ constructor(
241
+ public readonly uc: unchunkedSendCt.Ct1SentEkReceived,
242
+ public readonly sendingCt1: PolyEncoder,
243
+ ) {}
244
+
245
+ get epoch(): Epoch {
246
+ return this.uc.epoch;
247
+ }
248
+
249
+ /** Produce the next ct1 chunk. */
250
+ sendCt1Chunk(): [EkReceivedCt1Sampled, Chunk] {
251
+ const chunk = this.sendingCt1.nextChunk();
252
+ return [this, chunk];
253
+ }
254
+
255
+ /** Peer has acknowledged ct1. Produce ct2 and encode it. */
256
+ recvCt1Ack(epoch: Epoch): Ct2Sampled {
257
+ if (epoch !== this.uc.epoch) {
258
+ throw new SpqrError(
259
+ `Epoch mismatch: expected ${this.uc.epoch}, got ${epoch}`,
260
+ SpqrErrorCode.EpochOutOfRange,
261
+ );
262
+ }
263
+
264
+ const { state: ct2SentUc, ct2, mac } = this.uc.sendCt2();
265
+ // ct2 payload = ct2(128) + mac(32) = 160 bytes
266
+ const ct2Payload = concat(ct2, mac);
267
+ const sendingCt2 = PolyEncoder.encodeBytes(ct2Payload);
268
+
269
+ return new Ct2Sampled(ct2SentUc, sendingCt2);
270
+ }
271
+ }
272
+
273
+ // ---------------------------------------------------------------------------
274
+ // State 5: Ct1Acknowledged
275
+ // ---------------------------------------------------------------------------
276
+
277
+ /**
278
+ * ct1 has been acknowledged by the peer. Still receiving ek chunks.
279
+ * When ek arrives, validate it and produce ct2.
280
+ */
281
+ export class Ct1Acknowledged {
282
+ constructor(
283
+ public readonly uc: unchunkedSendCt.Ct1Sent,
284
+ public readonly receivingEk: PolyDecoder,
285
+ ) {}
286
+
287
+ get epoch(): Epoch {
288
+ return this.uc.epoch;
289
+ }
290
+
291
+ /** Receive an ek chunk. Returns done when ek is fully decoded. */
292
+ recvEkChunk(epoch: Epoch, chunk: Chunk): Ct1AcknowledgedRecvChunk {
293
+ if (epoch !== this.uc.epoch) {
294
+ throw new SpqrError(
295
+ `Epoch mismatch: expected ${this.uc.epoch}, got ${epoch}`,
296
+ SpqrErrorCode.EpochOutOfRange,
297
+ );
298
+ }
299
+
300
+ this.receivingEk.addChunk(chunk);
301
+ const decoded = this.receivingEk.decodedMessage();
302
+
303
+ if (decoded !== null) {
304
+ const ucResult = this.uc.recvEk(decoded);
305
+ const { state: ct2SentUc, ct2, mac } = ucResult.sendCt2();
306
+ // ct2 payload = ct2(128) + mac(32) = 160 bytes
307
+ const ct2Payload = concat(ct2, mac);
308
+ const sendingCt2 = PolyEncoder.encodeBytes(ct2Payload);
309
+ return {
310
+ done: true,
311
+ state: new Ct2Sampled(ct2SentUc, sendingCt2),
312
+ epochSecret: null, // Epoch secret already derived in sendCt1
313
+ };
314
+ }
315
+
316
+ return { done: false, state: this };
317
+ }
318
+ }
319
+
320
+ // ---------------------------------------------------------------------------
321
+ // State 6: Ct2Sampled
322
+ // ---------------------------------------------------------------------------
323
+
324
+ /**
325
+ * ct2 payload (ct2 + mac) has been encoded. Sending ct2 chunks.
326
+ * Terminal for this epoch once all chunks sent and next epoch begins.
327
+ */
328
+ export class Ct2Sampled {
329
+ constructor(
330
+ public readonly uc: unchunkedSendCt.Ct2Sent,
331
+ public readonly sendingCt2: PolyEncoder,
332
+ ) {}
333
+
334
+ get epoch(): Epoch {
335
+ return this.uc.epoch;
336
+ }
337
+
338
+ /** Produce the next ct2 chunk. */
339
+ sendCt2Chunk(): [Ct2Sampled, Chunk] {
340
+ const chunk = this.sendingCt2.nextChunk();
341
+ return [this, chunk];
342
+ }
343
+
344
+ /**
345
+ * Transition to the next epoch's send_ek KeysUnsampled.
346
+ */
347
+ recvNextEpoch(_epoch: Epoch): sendEk.KeysUnsampled {
348
+ const nextEpoch = this.uc.nextEpoch;
349
+ const nextUc = new unchunkedSendEk.KeysUnsampled(nextEpoch, this.uc.auth);
350
+ return new sendEk.KeysUnsampled(nextUc);
351
+ }
352
+ }
@@ -0,0 +1,285 @@
1
+ /**
2
+ * Copyright © 2025 Signal Messenger, LLC
3
+ * Copyright © 2026 Parity Technologies
4
+ *
5
+ * Chunked send_ek state machine for SPQR V1.
6
+ *
7
+ * Wraps the unchunked send_ek states with PolyEncoder/PolyDecoder erasure
8
+ * coding, enabling chunk-by-chunk data transfer.
9
+ *
10
+ * States:
11
+ * KeysUnsampled -- sendHdrChunk(rng) --> KeysSampled + Chunk
12
+ * KeysSampled -- sendHdrChunk() --> KeysSampled + Chunk
13
+ * -- recvCt1Chunk(epoch, chunk) --> HeaderSent
14
+ * HeaderSent -- sendEkChunk() --> HeaderSent + Chunk
15
+ * -- recvCt1Chunk(epoch, chunk) --> HeaderSent | Ct1Received
16
+ * Ct1Received -- sendEkChunk() --> Ct1Received + Chunk
17
+ * -- recvCt2Chunk(epoch, chunk) --> EkSentCt1Received
18
+ * EkSentCt1Received -- recvCt2Chunk(epoch, chunk) --> EkSentCt1Received | sendCt.NoHeaderReceived
19
+ */
20
+
21
+ import type * as unchunked from "../unchunked/send-ek.js";
22
+ import * as unchunkedSendCt from "../unchunked/send-ct.js";
23
+ import { PolyEncoder, PolyDecoder } from "../../encoding/polynomial.js";
24
+ import type { Chunk } from "../../encoding/polynomial.js";
25
+ import { HEADER_SIZE, CT1_SIZE, CT2_SIZE } from "../../incremental-mlkem768.js";
26
+ import { MAC_SIZE } from "../../authenticator.js";
27
+ import { concat } from "../../util.js";
28
+ import { SpqrError, SpqrErrorCode } from "../../error.js";
29
+ import type { Epoch, EpochSecret, RandomBytes } from "../../types.js";
30
+ import type * as sendCt from "./send-ct.js";
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Discriminated union result types
34
+ // ---------------------------------------------------------------------------
35
+
36
+ export type HeaderSentRecvChunk =
37
+ | { done: false; state: HeaderSent }
38
+ | { done: true; state: Ct1Received };
39
+
40
+ export type EkSentCt1ReceivedRecvChunk =
41
+ | { done: false; state: EkSentCt1Received }
42
+ | { done: true; state: sendCt.NoHeaderReceived; epochSecret: EpochSecret };
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // State 1: KeysUnsampled
46
+ // ---------------------------------------------------------------------------
47
+
48
+ /**
49
+ * Initial chunked send_ek state. No keypair generated yet.
50
+ * Wraps unchunked.KeysUnsampled.
51
+ */
52
+ export class KeysUnsampled {
53
+ constructor(public readonly uc: unchunked.KeysUnsampled) {}
54
+
55
+ get epoch(): Epoch {
56
+ return this.uc.epoch;
57
+ }
58
+
59
+ /**
60
+ * Generate keypair, produce header + MAC, encode into PolyEncoder,
61
+ * and return the first header chunk.
62
+ */
63
+ sendHdrChunk(rng: RandomBytes): [KeysSampled, Chunk] {
64
+ const [headerSent, hdr, mac] = this.uc.sendHeader(rng);
65
+ const hdrPayload = concat(hdr, mac);
66
+ const sendingHdr = PolyEncoder.encodeBytes(hdrPayload);
67
+ const chunk = sendingHdr.nextChunk();
68
+ return [new KeysSampled(headerSent, sendingHdr), chunk];
69
+ }
70
+ }
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // State 2: KeysSampled
74
+ // ---------------------------------------------------------------------------
75
+
76
+ /**
77
+ * Header has been generated and encoding started. Still sending header chunks.
78
+ * Can also begin receiving ct1 chunks.
79
+ */
80
+ export class KeysSampled {
81
+ constructor(
82
+ public readonly uc: unchunked.HeaderSent,
83
+ public readonly sendingHdr: PolyEncoder,
84
+ ) {}
85
+
86
+ get epoch(): Epoch {
87
+ return this.uc.epoch;
88
+ }
89
+
90
+ /** Produce the next header chunk. */
91
+ sendHdrChunk(): [KeysSampled, Chunk] {
92
+ const chunk = this.sendingHdr.nextChunk();
93
+ return [this, chunk];
94
+ }
95
+
96
+ /**
97
+ * Receive a ct1 chunk from the send_ct peer.
98
+ * This triggers sending the encapsulation key.
99
+ */
100
+ recvCt1Chunk(epoch: Epoch, chunk: Chunk): HeaderSent {
101
+ if (epoch !== this.uc.epoch) {
102
+ throw new SpqrError(
103
+ `Epoch mismatch: expected ${this.uc.epoch}, got ${epoch}`,
104
+ SpqrErrorCode.EpochOutOfRange,
105
+ );
106
+ }
107
+
108
+ // Create ct1 decoder
109
+ const receivingCt1 = PolyDecoder.create(CT1_SIZE);
110
+ receivingCt1.addChunk(chunk);
111
+
112
+ // Transition unchunked: send ek
113
+ const [ekSent, ek] = this.uc.sendEk();
114
+ const sendingEk = PolyEncoder.encodeBytes(ek);
115
+
116
+ return new HeaderSent(ekSent, sendingEk, receivingCt1);
117
+ }
118
+ }
119
+
120
+ // ---------------------------------------------------------------------------
121
+ // State 3: HeaderSent
122
+ // ---------------------------------------------------------------------------
123
+
124
+ /**
125
+ * Sending ek chunks while receiving ct1 chunks.
126
+ */
127
+ export class HeaderSent {
128
+ constructor(
129
+ public readonly uc: unchunked.EkSent,
130
+ public readonly sendingEk: PolyEncoder,
131
+ public readonly receivingCt1: PolyDecoder,
132
+ ) {}
133
+
134
+ get epoch(): Epoch {
135
+ return this.uc.epoch;
136
+ }
137
+
138
+ /** Produce the next ek chunk. */
139
+ sendEkChunk(): [HeaderSent, Chunk] {
140
+ const chunk = this.sendingEk.nextChunk();
141
+ return [this, chunk];
142
+ }
143
+
144
+ /** Receive a ct1 chunk. Returns done when ct1 is fully decoded. */
145
+ recvCt1Chunk(epoch: Epoch, chunk: Chunk): HeaderSentRecvChunk {
146
+ if (epoch !== this.uc.epoch) {
147
+ throw new SpqrError(
148
+ `Epoch mismatch: expected ${this.uc.epoch}, got ${epoch}`,
149
+ SpqrErrorCode.EpochOutOfRange,
150
+ );
151
+ }
152
+
153
+ this.receivingCt1.addChunk(chunk);
154
+ const decoded = this.receivingCt1.decodedMessage();
155
+
156
+ if (decoded !== null) {
157
+ // ct1 fully received -- transition unchunked
158
+ const ucResult = this.uc.recvCt1(decoded);
159
+ return { done: true, state: new Ct1Received(ucResult, this.sendingEk) };
160
+ }
161
+
162
+ return { done: false, state: this };
163
+ }
164
+ }
165
+
166
+ // ---------------------------------------------------------------------------
167
+ // State 4: Ct1Received
168
+ // ---------------------------------------------------------------------------
169
+
170
+ /**
171
+ * ct1 has been fully decoded. Still sending ek chunks.
172
+ * Can begin receiving ct2 chunks.
173
+ */
174
+ export class Ct1Received {
175
+ constructor(
176
+ public readonly uc: unchunked.EkSentCt1Received,
177
+ public readonly sendingEk: PolyEncoder,
178
+ ) {}
179
+
180
+ get epoch(): Epoch {
181
+ return this.uc.epoch;
182
+ }
183
+
184
+ /** Produce the next ek chunk. */
185
+ sendEkChunk(): [Ct1Received, Chunk] {
186
+ const chunk = this.sendingEk.nextChunk();
187
+ return [this, chunk];
188
+ }
189
+
190
+ /**
191
+ * Receive a ct2 chunk. Creates the ct2 decoder.
192
+ * ct2 payload is CT2_SIZE + MAC_SIZE = 160 bytes
193
+ * (carries ct2 + mac).
194
+ */
195
+ recvCt2Chunk(epoch: Epoch, chunk: Chunk): EkSentCt1Received {
196
+ if (epoch !== this.uc.epoch) {
197
+ throw new SpqrError(
198
+ `Epoch mismatch: expected ${this.uc.epoch}, got ${epoch}`,
199
+ SpqrErrorCode.EpochOutOfRange,
200
+ );
201
+ }
202
+
203
+ const receivingCt2 = PolyDecoder.create(CT2_SIZE + MAC_SIZE);
204
+ receivingCt2.addChunk(chunk);
205
+
206
+ return new EkSentCt1Received(this.uc, receivingCt2);
207
+ }
208
+ }
209
+
210
+ // ---------------------------------------------------------------------------
211
+ // State 5: EkSentCt1Received
212
+ // ---------------------------------------------------------------------------
213
+
214
+ /**
215
+ * Both ek sending and ct1 receiving are done. Now receiving ct2 chunks.
216
+ * When ct2 is fully decoded, extract ct2 and mac, then perform decapsulation
217
+ * to derive the epoch secret.
218
+ */
219
+ export class EkSentCt1Received {
220
+ constructor(
221
+ public readonly uc: unchunked.EkSentCt1Received,
222
+ public readonly receivingCt2: PolyDecoder,
223
+ ) {}
224
+
225
+ get epoch(): Epoch {
226
+ return this.uc.epoch;
227
+ }
228
+
229
+ /**
230
+ * Receive a ct2 chunk. Returns done when ct2 is fully decoded and
231
+ * epoch secret is derived.
232
+ */
233
+ recvCt2Chunk(epoch: Epoch, chunk: Chunk): EkSentCt1ReceivedRecvChunk {
234
+ if (epoch !== this.uc.epoch) {
235
+ throw new SpqrError(
236
+ `Epoch mismatch: expected ${this.uc.epoch}, got ${epoch}`,
237
+ SpqrErrorCode.EpochOutOfRange,
238
+ );
239
+ }
240
+
241
+ this.receivingCt2.addChunk(chunk);
242
+ const decoded = this.receivingCt2.decodedMessage();
243
+
244
+ if (decoded !== null) {
245
+ // Split the 160-byte payload: ct2(128) + mac(32)
246
+ const ct2 = decoded.slice(0, CT2_SIZE);
247
+ const mac = decoded.slice(CT2_SIZE);
248
+
249
+ // Use the unchunked state directly (ct1 is already the REAL ct1)
250
+ const result = this.uc.recvCt2(ct2, mac);
251
+
252
+ // Construct the next-epoch send_ct NoHeaderReceived state
253
+ const nextUcSendCt = new unchunkedSendCt.NoHeaderReceived(result.nextEpoch, result.auth);
254
+ const receivingHdr = PolyDecoder.create(HEADER_SIZE + MAC_SIZE);
255
+
256
+ return {
257
+ done: true,
258
+ state: createSendCtNoHeaderReceived(nextUcSendCt, receivingHdr),
259
+ epochSecret: result.epochSecret,
260
+ };
261
+ }
262
+
263
+ return { done: false, state: this };
264
+ }
265
+ }
266
+
267
+ // ---------------------------------------------------------------------------
268
+ // Factory for creating send-ct NoHeaderReceived (breaks circular dependency)
269
+ // ---------------------------------------------------------------------------
270
+
271
+ /** @internal Set by the chunked index module to break circular imports. */
272
+ let createSendCtNoHeaderReceived: (
273
+ uc: unchunkedSendCt.NoHeaderReceived,
274
+ receivingHdr: PolyDecoder,
275
+ ) => sendCt.NoHeaderReceived;
276
+
277
+ /** @internal Called by the index module to wire up the factory. */
278
+ export function _setCreateSendCtNoHeaderReceived(
279
+ factory: (
280
+ uc: unchunkedSendCt.NoHeaderReceived,
281
+ receivingHdr: PolyDecoder,
282
+ ) => sendCt.NoHeaderReceived,
283
+ ): void {
284
+ createSendCtNoHeaderReceived = factory;
285
+ }