@bcts/spqr 1.0.0-alpha.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +11 -0
- package/dist/index.cjs +4321 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +115 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +115 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.iife.js +4318 -0
- package/dist/index.iife.js.map +1 -0
- package/dist/index.mjs +4312 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +74 -0
- package/src/authenticator.ts +163 -0
- package/src/chain.ts +522 -0
- package/src/constants.ts +90 -0
- package/src/encoding/gf.ts +190 -0
- package/src/encoding/index.ts +15 -0
- package/src/encoding/polynomial.ts +657 -0
- package/src/error.ts +75 -0
- package/src/incremental-mlkem768.ts +546 -0
- package/src/index.ts +415 -0
- package/src/kdf.ts +34 -0
- package/src/proto/index.ts +1376 -0
- package/src/proto/pq-ratchet-types.ts +195 -0
- package/src/types.ts +81 -0
- package/src/util.ts +61 -0
- package/src/v1/chunked/index.ts +60 -0
- package/src/v1/chunked/message.ts +257 -0
- package/src/v1/chunked/send-ct.ts +352 -0
- package/src/v1/chunked/send-ek.ts +285 -0
- package/src/v1/chunked/serialize.ts +278 -0
- package/src/v1/chunked/states.ts +399 -0
- package/src/v1/index.ts +9 -0
- package/src/v1/unchunked/index.ts +20 -0
- package/src/v1/unchunked/send-ct.ts +231 -0
- package/src/v1/unchunked/send-ek.ts +177 -0
|
@@ -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
|
+
}
|