@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,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright © 2025 Signal Messenger, LLC
|
|
3
|
+
* Copyright © 2026 Parity Technologies
|
|
4
|
+
*
|
|
5
|
+
* Serialization bridge between runtime States and PbV1State protobuf.
|
|
6
|
+
*
|
|
7
|
+
* Converts the runtime discriminated-union States objects to/from their
|
|
8
|
+
* protobuf representations so that the top-level API can persist state
|
|
9
|
+
* as opaque bytes.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { PbV1State, PbChunkedState } from "../../proto/pq-ratchet-types.js";
|
|
13
|
+
import { Authenticator } from "../../authenticator.js";
|
|
14
|
+
import { PolyEncoder, PolyDecoder } from "../../encoding/polynomial.js";
|
|
15
|
+
|
|
16
|
+
// Unchunked state classes
|
|
17
|
+
import * as ucSendEk from "../unchunked/send-ek.js";
|
|
18
|
+
import * as ucSendCt from "../unchunked/send-ct.js";
|
|
19
|
+
|
|
20
|
+
// Chunked state classes
|
|
21
|
+
import * as sendEk from "./send-ek.js";
|
|
22
|
+
import * as sendCt from "./send-ct.js";
|
|
23
|
+
|
|
24
|
+
import type { States } from "./states.js";
|
|
25
|
+
import type { Epoch } from "../../types.js";
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// States -> PbV1State
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Serialize a runtime States object into PbV1State for protobuf encoding.
|
|
33
|
+
* Stores the epoch as field 12 so it can be recovered on deserialization.
|
|
34
|
+
*/
|
|
35
|
+
export function statesToPb(s: States): PbV1State {
|
|
36
|
+
const epoch = s.state.epoch;
|
|
37
|
+
return { innerState: chunkedStateToPb(s), epoch };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function chunkedStateToPb(s: States): PbChunkedState {
|
|
41
|
+
switch (s.tag) {
|
|
42
|
+
case "keysUnsampled": {
|
|
43
|
+
const st = s.state;
|
|
44
|
+
return {
|
|
45
|
+
type: "keysUnsampled",
|
|
46
|
+
uc: { auth: st.uc.auth.toProto() },
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
case "keysSampled": {
|
|
50
|
+
const st = s.state;
|
|
51
|
+
return {
|
|
52
|
+
type: "keysSampled",
|
|
53
|
+
uc: {
|
|
54
|
+
auth: st.uc.auth.toProto(),
|
|
55
|
+
ek: Uint8Array.from(st.uc.ek),
|
|
56
|
+
dk: Uint8Array.from(st.uc.dk),
|
|
57
|
+
hdr: new Uint8Array(0),
|
|
58
|
+
hdrMac: new Uint8Array(0),
|
|
59
|
+
},
|
|
60
|
+
sendingHdr: st.sendingHdr.toProto(),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
case "headerSent": {
|
|
64
|
+
const st = s.state;
|
|
65
|
+
return {
|
|
66
|
+
type: "headerSent",
|
|
67
|
+
uc: {
|
|
68
|
+
auth: st.uc.auth.toProto(),
|
|
69
|
+
ek: new Uint8Array(0),
|
|
70
|
+
dk: Uint8Array.from(st.uc.dk),
|
|
71
|
+
},
|
|
72
|
+
sendingEk: st.sendingEk.toProto(),
|
|
73
|
+
receivingCt1: st.receivingCt1.toProto(),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
case "ct1Received": {
|
|
77
|
+
const st = s.state;
|
|
78
|
+
return {
|
|
79
|
+
type: "ct1Received",
|
|
80
|
+
uc: {
|
|
81
|
+
auth: st.uc.auth.toProto(),
|
|
82
|
+
dk: Uint8Array.from(st.uc.dk),
|
|
83
|
+
ct1: Uint8Array.from(st.uc.ct1),
|
|
84
|
+
},
|
|
85
|
+
sendingEk: st.sendingEk.toProto(),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
case "ekSentCt1Received": {
|
|
89
|
+
const st = s.state;
|
|
90
|
+
return {
|
|
91
|
+
type: "ekSentCt1Received",
|
|
92
|
+
uc: {
|
|
93
|
+
auth: st.uc.auth.toProto(),
|
|
94
|
+
dk: Uint8Array.from(st.uc.dk),
|
|
95
|
+
ct1: Uint8Array.from(st.uc.ct1),
|
|
96
|
+
},
|
|
97
|
+
receivingCt2: st.receivingCt2.toProto(),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
case "noHeaderReceived": {
|
|
101
|
+
const st = s.state;
|
|
102
|
+
return {
|
|
103
|
+
type: "noHeaderReceived",
|
|
104
|
+
uc: { auth: st.uc.auth.toProto() },
|
|
105
|
+
receivingHdr: st.receivingHdr.toProto(),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
case "headerReceived": {
|
|
109
|
+
const st = s.state;
|
|
110
|
+
return {
|
|
111
|
+
type: "headerReceived",
|
|
112
|
+
uc: {
|
|
113
|
+
auth: st.uc.auth.toProto(),
|
|
114
|
+
hdr: Uint8Array.from(st.uc.hdr),
|
|
115
|
+
es: new Uint8Array(0),
|
|
116
|
+
ct1: new Uint8Array(0),
|
|
117
|
+
ss: new Uint8Array(0),
|
|
118
|
+
},
|
|
119
|
+
receivingEk: st.receivingEk.toProto(),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
case "ct1Sampled": {
|
|
123
|
+
const st = s.state;
|
|
124
|
+
return {
|
|
125
|
+
type: "ct1Sampled",
|
|
126
|
+
uc: {
|
|
127
|
+
auth: st.uc.auth.toProto(),
|
|
128
|
+
hdr: Uint8Array.from(st.uc.hdr),
|
|
129
|
+
es: Uint8Array.from(st.uc.es),
|
|
130
|
+
ct1: Uint8Array.from(st.uc.ct1),
|
|
131
|
+
},
|
|
132
|
+
sendingCt1: st.sendingCt1.toProto(),
|
|
133
|
+
receivingEk: st.receivingEk.toProto(),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
case "ekReceivedCt1Sampled": {
|
|
137
|
+
const st = s.state;
|
|
138
|
+
return {
|
|
139
|
+
type: "ekReceivedCt1Sampled",
|
|
140
|
+
uc: {
|
|
141
|
+
auth: st.uc.auth.toProto(),
|
|
142
|
+
hdr: new Uint8Array(0),
|
|
143
|
+
es: Uint8Array.from(st.uc.es),
|
|
144
|
+
ek: Uint8Array.from(st.uc.ek),
|
|
145
|
+
ct1: Uint8Array.from(st.uc.ct1),
|
|
146
|
+
},
|
|
147
|
+
sendingCt1: st.sendingCt1.toProto(),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
case "ct1Acknowledged": {
|
|
151
|
+
const st = s.state;
|
|
152
|
+
return {
|
|
153
|
+
type: "ct1Acknowledged",
|
|
154
|
+
uc: {
|
|
155
|
+
auth: st.uc.auth.toProto(),
|
|
156
|
+
hdr: Uint8Array.from(st.uc.hdr),
|
|
157
|
+
es: Uint8Array.from(st.uc.es),
|
|
158
|
+
ct1: Uint8Array.from(st.uc.ct1),
|
|
159
|
+
},
|
|
160
|
+
receivingEk: st.receivingEk.toProto(),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
case "ct2Sampled": {
|
|
164
|
+
const st = s.state;
|
|
165
|
+
return {
|
|
166
|
+
type: "ct2Sampled",
|
|
167
|
+
uc: { auth: st.uc.auth.toProto() },
|
|
168
|
+
sendingCt2: st.sendingCt2.toProto(),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
// PbV1State -> States
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Deserialize a PbV1State back into a runtime States object.
|
|
180
|
+
*
|
|
181
|
+
* @param pb - The protobuf V1 state
|
|
182
|
+
* @returns The reconstructed runtime States
|
|
183
|
+
*/
|
|
184
|
+
export function statesFromPb(pb: PbV1State): States {
|
|
185
|
+
if (pb.innerState === undefined) {
|
|
186
|
+
throw new Error("PbV1State has no innerState");
|
|
187
|
+
}
|
|
188
|
+
const epoch = pb.epoch ?? 1n;
|
|
189
|
+
return chunkedStateFromPb(pb.innerState, epoch);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function chunkedStateFromPb(cs: PbChunkedState, epoch: Epoch): States {
|
|
193
|
+
switch (cs.type) {
|
|
194
|
+
case "keysUnsampled": {
|
|
195
|
+
const auth = authFromPb(cs.uc.auth);
|
|
196
|
+
const ucState = new ucSendEk.KeysUnsampled(epoch, auth);
|
|
197
|
+
return { tag: "keysUnsampled", state: new sendEk.KeysUnsampled(ucState) };
|
|
198
|
+
}
|
|
199
|
+
case "keysSampled": {
|
|
200
|
+
const auth = authFromPb(cs.uc.auth);
|
|
201
|
+
const ucState = new ucSendEk.HeaderSent(epoch, auth, cs.uc.ek, cs.uc.dk);
|
|
202
|
+
const encoder = PolyEncoder.fromProto(cs.sendingHdr);
|
|
203
|
+
return { tag: "keysSampled", state: new sendEk.KeysSampled(ucState, encoder) };
|
|
204
|
+
}
|
|
205
|
+
case "headerSent": {
|
|
206
|
+
const auth = authFromPb(cs.uc.auth);
|
|
207
|
+
const ucState = new ucSendEk.EkSent(epoch, auth, cs.uc.dk);
|
|
208
|
+
const encoder = PolyEncoder.fromProto(cs.sendingEk);
|
|
209
|
+
const decoder = PolyDecoder.fromProto(cs.receivingCt1);
|
|
210
|
+
return { tag: "headerSent", state: new sendEk.HeaderSent(ucState, encoder, decoder) };
|
|
211
|
+
}
|
|
212
|
+
case "ct1Received": {
|
|
213
|
+
const auth = authFromPb(cs.uc.auth);
|
|
214
|
+
const ucState = new ucSendEk.EkSentCt1Received(epoch, auth, cs.uc.dk, cs.uc.ct1);
|
|
215
|
+
const encoder = PolyEncoder.fromProto(cs.sendingEk);
|
|
216
|
+
return { tag: "ct1Received", state: new sendEk.Ct1Received(ucState, encoder) };
|
|
217
|
+
}
|
|
218
|
+
case "ekSentCt1Received": {
|
|
219
|
+
const auth = authFromPb(cs.uc.auth);
|
|
220
|
+
const ucState = new ucSendEk.EkSentCt1Received(epoch, auth, cs.uc.dk, cs.uc.ct1);
|
|
221
|
+
const decoder = PolyDecoder.fromProto(cs.receivingCt2);
|
|
222
|
+
return { tag: "ekSentCt1Received", state: new sendEk.EkSentCt1Received(ucState, decoder) };
|
|
223
|
+
}
|
|
224
|
+
case "noHeaderReceived": {
|
|
225
|
+
const auth = authFromPb(cs.uc.auth);
|
|
226
|
+
const ucState = new ucSendCt.NoHeaderReceived(epoch, auth);
|
|
227
|
+
const decoder = PolyDecoder.fromProto(cs.receivingHdr);
|
|
228
|
+
return { tag: "noHeaderReceived", state: new sendCt.NoHeaderReceived(ucState, decoder) };
|
|
229
|
+
}
|
|
230
|
+
case "headerReceived": {
|
|
231
|
+
const auth = authFromPb(cs.uc.auth);
|
|
232
|
+
const ucState = new ucSendCt.HeaderReceived(epoch, auth, cs.uc.hdr);
|
|
233
|
+
const decoder = PolyDecoder.fromProto(cs.receivingEk);
|
|
234
|
+
return { tag: "headerReceived", state: new sendCt.HeaderReceived(ucState, decoder) };
|
|
235
|
+
}
|
|
236
|
+
case "ct1Sampled": {
|
|
237
|
+
const auth = authFromPb(cs.uc.auth);
|
|
238
|
+
// Ct1Sent stores (epoch, auth, hdr, es, ct1) -- ct1 needed for MAC in sendCt2
|
|
239
|
+
const ucState = new ucSendCt.Ct1Sent(epoch, auth, cs.uc.hdr, cs.uc.es, cs.uc.ct1);
|
|
240
|
+
const encoder = PolyEncoder.fromProto(cs.sendingCt1);
|
|
241
|
+
const decoder = PolyDecoder.fromProto(cs.receivingEk);
|
|
242
|
+
return { tag: "ct1Sampled", state: new sendCt.Ct1Sampled(ucState, encoder, decoder) };
|
|
243
|
+
}
|
|
244
|
+
case "ekReceivedCt1Sampled": {
|
|
245
|
+
const auth = authFromPb(cs.uc.auth);
|
|
246
|
+
// Ct1SentEkReceived stores (epoch, auth, es, ek, ct1) -- ct1 needed for MAC in sendCt2
|
|
247
|
+
const ucState = new ucSendCt.Ct1SentEkReceived(epoch, auth, cs.uc.es, cs.uc.ek, cs.uc.ct1);
|
|
248
|
+
const encoder = PolyEncoder.fromProto(cs.sendingCt1);
|
|
249
|
+
return {
|
|
250
|
+
tag: "ekReceivedCt1Sampled",
|
|
251
|
+
state: new sendCt.EkReceivedCt1Sampled(ucState, encoder),
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
case "ct1Acknowledged": {
|
|
255
|
+
const auth = authFromPb(cs.uc.auth);
|
|
256
|
+
// Ct1Sent stores (epoch, auth, hdr, es, ct1) -- ct1 needed for MAC in sendCt2
|
|
257
|
+
const ucState = new ucSendCt.Ct1Sent(epoch, auth, cs.uc.hdr, cs.uc.es, cs.uc.ct1);
|
|
258
|
+
const decoder = PolyDecoder.fromProto(cs.receivingEk);
|
|
259
|
+
return { tag: "ct1Acknowledged", state: new sendCt.Ct1Acknowledged(ucState, decoder) };
|
|
260
|
+
}
|
|
261
|
+
case "ct2Sampled": {
|
|
262
|
+
const auth = authFromPb(cs.uc.auth);
|
|
263
|
+
const ucState = new ucSendCt.Ct2Sent(epoch, auth);
|
|
264
|
+
const encoder = PolyEncoder.fromProto(cs.sendingCt2);
|
|
265
|
+
return { tag: "ct2Sampled", state: new sendCt.Ct2Sampled(ucState, encoder) };
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function authFromPb(pb: { rootKey: Uint8Array; macKey: Uint8Array } | undefined): Authenticator {
|
|
271
|
+
if (pb === undefined) {
|
|
272
|
+
return Authenticator.fromProto({
|
|
273
|
+
rootKey: new Uint8Array(32),
|
|
274
|
+
macKey: new Uint8Array(32),
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
return Authenticator.fromProto(pb);
|
|
278
|
+
}
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright © 2025 Signal Messenger, LLC
|
|
3
|
+
* Copyright © 2026 Parity Technologies
|
|
4
|
+
*
|
|
5
|
+
* Chunked state machine dispatcher for SPQR V1.
|
|
6
|
+
*
|
|
7
|
+
* Provides the top-level send/recv interface that dispatches to the
|
|
8
|
+
* appropriate chunked send_ek or send_ct state, handling epoch
|
|
9
|
+
* validation and state transitions.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { Chunk } from "../../encoding/polynomial.js";
|
|
13
|
+
import type { Epoch, EpochSecret, RandomBytes } from "../../types.js";
|
|
14
|
+
import { SpqrError, SpqrErrorCode } from "../../error.js";
|
|
15
|
+
import { Authenticator } from "../../authenticator.js";
|
|
16
|
+
import * as unchunked from "../unchunked/send-ek.js";
|
|
17
|
+
import * as sendEk from "./send-ek.js";
|
|
18
|
+
import * as sendCt from "./send-ct.js";
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Message types
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
export type MessagePayload =
|
|
25
|
+
| { type: "none" }
|
|
26
|
+
| { type: "hdr"; chunk: Chunk }
|
|
27
|
+
| { type: "ek"; chunk: Chunk }
|
|
28
|
+
| { type: "ekCt1Ack"; chunk: Chunk }
|
|
29
|
+
| { type: "ct1Ack" }
|
|
30
|
+
| { type: "ct1"; chunk: Chunk }
|
|
31
|
+
| { type: "ct2"; chunk: Chunk };
|
|
32
|
+
|
|
33
|
+
export interface Message {
|
|
34
|
+
epoch: Epoch;
|
|
35
|
+
payload: MessagePayload;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface SendResult {
|
|
39
|
+
msg: Message;
|
|
40
|
+
key: EpochSecret | null;
|
|
41
|
+
state: States;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface RecvResult {
|
|
45
|
+
key: EpochSecret | null;
|
|
46
|
+
state: States;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// States discriminated union
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
export type States =
|
|
54
|
+
| { tag: "keysUnsampled"; state: sendEk.KeysUnsampled }
|
|
55
|
+
| { tag: "keysSampled"; state: sendEk.KeysSampled }
|
|
56
|
+
| { tag: "headerSent"; state: sendEk.HeaderSent }
|
|
57
|
+
| { tag: "ct1Received"; state: sendEk.Ct1Received }
|
|
58
|
+
| { tag: "ekSentCt1Received"; state: sendEk.EkSentCt1Received }
|
|
59
|
+
| { tag: "noHeaderReceived"; state: sendCt.NoHeaderReceived }
|
|
60
|
+
| { tag: "headerReceived"; state: sendCt.HeaderReceived }
|
|
61
|
+
| { tag: "ct1Sampled"; state: sendCt.Ct1Sampled }
|
|
62
|
+
| { tag: "ekReceivedCt1Sampled"; state: sendCt.EkReceivedCt1Sampled }
|
|
63
|
+
| { tag: "ct1Acknowledged"; state: sendCt.Ct1Acknowledged }
|
|
64
|
+
| { tag: "ct2Sampled"; state: sendCt.Ct2Sampled };
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Epoch accessor
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
function getEpoch(s: States): Epoch {
|
|
71
|
+
return s.state.epoch;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Initialization
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Initialize Alice (send_ek side) at epoch 1.
|
|
80
|
+
*/
|
|
81
|
+
export function initA(authKey: Uint8Array): States {
|
|
82
|
+
const auth = Authenticator.create(authKey, 1n);
|
|
83
|
+
const ucState = new unchunked.KeysUnsampled(1n, auth);
|
|
84
|
+
return {
|
|
85
|
+
tag: "keysUnsampled",
|
|
86
|
+
state: new sendEk.KeysUnsampled(ucState),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Initialize Bob (send_ct side) at epoch 1.
|
|
92
|
+
*/
|
|
93
|
+
export function initB(authKey: Uint8Array): States {
|
|
94
|
+
return {
|
|
95
|
+
tag: "noHeaderReceived",
|
|
96
|
+
state: sendCt.NoHeaderReceived.create(authKey),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// Send
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Produce the next outgoing message from the current state.
|
|
106
|
+
*/
|
|
107
|
+
export function send(current: States, rng: RandomBytes): SendResult {
|
|
108
|
+
const epoch = getEpoch(current);
|
|
109
|
+
|
|
110
|
+
switch (current.tag) {
|
|
111
|
+
case "keysUnsampled": {
|
|
112
|
+
const [next, chunk] = current.state.sendHdrChunk(rng);
|
|
113
|
+
return {
|
|
114
|
+
msg: { epoch, payload: { type: "hdr", chunk } },
|
|
115
|
+
key: null,
|
|
116
|
+
state: { tag: "keysSampled", state: next },
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
case "keysSampled": {
|
|
121
|
+
const [next, chunk] = current.state.sendHdrChunk();
|
|
122
|
+
return {
|
|
123
|
+
msg: { epoch, payload: { type: "hdr", chunk } },
|
|
124
|
+
key: null,
|
|
125
|
+
state: { tag: "keysSampled", state: next },
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
case "headerSent": {
|
|
130
|
+
const [next, chunk] = current.state.sendEkChunk();
|
|
131
|
+
return {
|
|
132
|
+
msg: { epoch, payload: { type: "ek", chunk } },
|
|
133
|
+
key: null,
|
|
134
|
+
state: { tag: "headerSent", state: next },
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
case "ct1Received": {
|
|
139
|
+
const [next, chunk] = current.state.sendEkChunk();
|
|
140
|
+
return {
|
|
141
|
+
msg: { epoch, payload: { type: "ekCt1Ack", chunk } },
|
|
142
|
+
key: null,
|
|
143
|
+
state: { tag: "ct1Received", state: next },
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
case "ekSentCt1Received": {
|
|
148
|
+
return {
|
|
149
|
+
msg: { epoch, payload: { type: "ct1Ack" } },
|
|
150
|
+
key: null,
|
|
151
|
+
state: current,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
case "noHeaderReceived": {
|
|
156
|
+
return {
|
|
157
|
+
msg: { epoch, payload: { type: "none" } },
|
|
158
|
+
key: null,
|
|
159
|
+
state: current,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
case "headerReceived": {
|
|
164
|
+
const [next, chunk, epochSecret] = current.state.sendCt1Chunk(rng);
|
|
165
|
+
return {
|
|
166
|
+
msg: { epoch, payload: { type: "ct1", chunk } },
|
|
167
|
+
key: epochSecret,
|
|
168
|
+
state: { tag: "ct1Sampled", state: next },
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
case "ct1Sampled": {
|
|
173
|
+
const [next, chunk] = current.state.sendCt1Chunk();
|
|
174
|
+
return {
|
|
175
|
+
msg: { epoch, payload: { type: "ct1", chunk } },
|
|
176
|
+
key: null,
|
|
177
|
+
state: { tag: "ct1Sampled", state: next },
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
case "ekReceivedCt1Sampled": {
|
|
182
|
+
const [next, chunk] = current.state.sendCt1Chunk();
|
|
183
|
+
return {
|
|
184
|
+
msg: { epoch, payload: { type: "ct1", chunk } },
|
|
185
|
+
key: null,
|
|
186
|
+
state: { tag: "ekReceivedCt1Sampled", state: next },
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
case "ct1Acknowledged": {
|
|
191
|
+
return {
|
|
192
|
+
msg: { epoch, payload: { type: "none" } },
|
|
193
|
+
key: null,
|
|
194
|
+
state: current,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
case "ct2Sampled": {
|
|
199
|
+
const [next, chunk] = current.state.sendCt2Chunk();
|
|
200
|
+
return {
|
|
201
|
+
msg: { epoch, payload: { type: "ct2", chunk } },
|
|
202
|
+
key: null,
|
|
203
|
+
state: { tag: "ct2Sampled", state: next },
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
// Recv
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Process an incoming message and transition the state.
|
|
215
|
+
*/
|
|
216
|
+
export function recv(current: States, msg: Message): RecvResult {
|
|
217
|
+
const stateEpoch = getEpoch(current);
|
|
218
|
+
|
|
219
|
+
// Handle epoch mismatch: messages from the past are ignored
|
|
220
|
+
if (msg.epoch < stateEpoch) {
|
|
221
|
+
return { key: null, state: current };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Messages from the future are an error (except ct2Sampled epoch+1)
|
|
225
|
+
if (msg.epoch > stateEpoch) {
|
|
226
|
+
if (current.tag === "ct2Sampled" && msg.epoch === stateEpoch + 1n) {
|
|
227
|
+
// Next epoch -- transition to KeysUnsampled and return immediately.
|
|
228
|
+
// The incoming message payload is NOT processed in the new state
|
|
229
|
+
// (matches Rust: the peer will retransmit via erasure coding).
|
|
230
|
+
const next = current.state.recvNextEpoch(msg.epoch);
|
|
231
|
+
return { key: null, state: { tag: "keysUnsampled", state: next } };
|
|
232
|
+
}
|
|
233
|
+
throw new SpqrError(
|
|
234
|
+
`Epoch too far ahead: state=${stateEpoch}, msg=${msg.epoch}`,
|
|
235
|
+
SpqrErrorCode.EpochOutOfRange,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// msg.epoch === stateEpoch -- process the payload
|
|
240
|
+
const payload = msg.payload;
|
|
241
|
+
|
|
242
|
+
switch (current.tag) {
|
|
243
|
+
case "keysUnsampled": {
|
|
244
|
+
// Waiting to send -- nothing to receive
|
|
245
|
+
return { key: null, state: current };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
case "keysSampled": {
|
|
249
|
+
if (payload.type === "ct1") {
|
|
250
|
+
const next = current.state.recvCt1Chunk(msg.epoch, payload.chunk);
|
|
251
|
+
return { key: null, state: { tag: "headerSent", state: next } };
|
|
252
|
+
}
|
|
253
|
+
return { key: null, state: current };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
case "headerSent": {
|
|
257
|
+
if (payload.type === "ct1") {
|
|
258
|
+
const result = current.state.recvCt1Chunk(msg.epoch, payload.chunk);
|
|
259
|
+
if (result.done) {
|
|
260
|
+
return {
|
|
261
|
+
key: null,
|
|
262
|
+
state: { tag: "ct1Received", state: result.state },
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
key: null,
|
|
267
|
+
state: { tag: "headerSent", state: result.state },
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
return { key: null, state: current };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
case "ct1Received": {
|
|
274
|
+
if (payload.type === "ct2") {
|
|
275
|
+
const next = current.state.recvCt2Chunk(msg.epoch, payload.chunk);
|
|
276
|
+
return {
|
|
277
|
+
key: null,
|
|
278
|
+
state: { tag: "ekSentCt1Received", state: next },
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
return { key: null, state: current };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
case "ekSentCt1Received": {
|
|
285
|
+
if (payload.type === "ct2") {
|
|
286
|
+
const result = current.state.recvCt2Chunk(msg.epoch, payload.chunk);
|
|
287
|
+
if (result.done) {
|
|
288
|
+
return {
|
|
289
|
+
key: result.epochSecret,
|
|
290
|
+
state: { tag: "noHeaderReceived", state: result.state },
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
return {
|
|
294
|
+
key: null,
|
|
295
|
+
state: { tag: "ekSentCt1Received", state: result.state },
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
return { key: null, state: current };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
case "noHeaderReceived": {
|
|
302
|
+
if (payload.type === "hdr") {
|
|
303
|
+
const result = current.state.recvHdrChunk(msg.epoch, payload.chunk);
|
|
304
|
+
if (result.done) {
|
|
305
|
+
return {
|
|
306
|
+
key: null,
|
|
307
|
+
state: { tag: "headerReceived", state: result.state },
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
return {
|
|
311
|
+
key: null,
|
|
312
|
+
state: { tag: "noHeaderReceived", state: result.state },
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
return { key: null, state: current };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
case "headerReceived": {
|
|
319
|
+
// Waiting to send ct1 -- nothing to receive in this state
|
|
320
|
+
return { key: null, state: current };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
case "ct1Sampled": {
|
|
324
|
+
if (payload.type === "ek") {
|
|
325
|
+
const result = current.state.recvEkChunk(msg.epoch, payload.chunk, false);
|
|
326
|
+
return mapCt1SampledResult(result);
|
|
327
|
+
}
|
|
328
|
+
if (payload.type === "ekCt1Ack") {
|
|
329
|
+
const result = current.state.recvEkChunk(msg.epoch, payload.chunk, true);
|
|
330
|
+
return mapCt1SampledResult(result);
|
|
331
|
+
}
|
|
332
|
+
return { key: null, state: current };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
case "ekReceivedCt1Sampled": {
|
|
336
|
+
if (payload.type === "ct1Ack" || payload.type === "ekCt1Ack") {
|
|
337
|
+
const next = current.state.recvCt1Ack(msg.epoch);
|
|
338
|
+
return {
|
|
339
|
+
key: null,
|
|
340
|
+
state: { tag: "ct2Sampled", state: next },
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
return { key: null, state: current };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
case "ct1Acknowledged": {
|
|
347
|
+
if (payload.type === "ek" || payload.type === "ekCt1Ack") {
|
|
348
|
+
const chunk = payload.type === "ek" ? payload.chunk : payload.chunk;
|
|
349
|
+
const result = current.state.recvEkChunk(msg.epoch, chunk);
|
|
350
|
+
if (result.done) {
|
|
351
|
+
return {
|
|
352
|
+
key: result.epochSecret,
|
|
353
|
+
state: { tag: "ct2Sampled", state: result.state },
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
return {
|
|
357
|
+
key: null,
|
|
358
|
+
state: { tag: "ct1Acknowledged", state: result.state },
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
return { key: null, state: current };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
case "ct2Sampled": {
|
|
365
|
+
// If we receive a message for the current epoch, nothing to do
|
|
366
|
+
// (next epoch handled above)
|
|
367
|
+
return { key: null, state: current };
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// ---------------------------------------------------------------------------
|
|
373
|
+
// Helpers
|
|
374
|
+
// ---------------------------------------------------------------------------
|
|
375
|
+
|
|
376
|
+
function mapCt1SampledResult(result: sendCt.Ct1SampledRecvChunk): RecvResult {
|
|
377
|
+
switch (result.tag) {
|
|
378
|
+
case "done":
|
|
379
|
+
return {
|
|
380
|
+
key: result.epochSecret,
|
|
381
|
+
state: { tag: "ct2Sampled", state: result.state },
|
|
382
|
+
};
|
|
383
|
+
case "stillSending":
|
|
384
|
+
return {
|
|
385
|
+
key: result.epochSecret,
|
|
386
|
+
state: { tag: "ekReceivedCt1Sampled", state: result.state },
|
|
387
|
+
};
|
|
388
|
+
case "stillReceiving":
|
|
389
|
+
return {
|
|
390
|
+
key: null,
|
|
391
|
+
state: { tag: "ct1Acknowledged", state: result.state },
|
|
392
|
+
};
|
|
393
|
+
case "stillReceivingStillSending":
|
|
394
|
+
return {
|
|
395
|
+
key: null,
|
|
396
|
+
state: { tag: "ct1Sampled", state: result.state },
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
}
|
package/src/v1/index.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright © 2025 Signal Messenger, LLC
|
|
3
|
+
* Copyright © 2026 Parity Technologies
|
|
4
|
+
*
|
|
5
|
+
* Unchunked state machine for SPQR V1.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// send_ek states
|
|
9
|
+
export { KeysUnsampled, HeaderSent, EkSent, EkSentCt1Received } from "./send-ek.js";
|
|
10
|
+
export type { RecvCt2Result } from "./send-ek.js";
|
|
11
|
+
|
|
12
|
+
// send_ct states
|
|
13
|
+
export {
|
|
14
|
+
NoHeaderReceived,
|
|
15
|
+
HeaderReceived,
|
|
16
|
+
Ct1Sent,
|
|
17
|
+
Ct1SentEkReceived,
|
|
18
|
+
Ct2Sent,
|
|
19
|
+
} from "./send-ct.js";
|
|
20
|
+
export type { SendCt2Result } from "./send-ct.js";
|