@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,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright © 2025 Signal Messenger, LLC
|
|
3
|
+
* Copyright © 2026 Parity Technologies
|
|
4
|
+
*
|
|
5
|
+
* TypeScript interfaces matching pq_ratchet.proto exactly.
|
|
6
|
+
*
|
|
7
|
+
* NOTE: All optional properties use `| undefined` to satisfy
|
|
8
|
+
* `exactOptionalPropertyTypes: true` in tsconfig.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** PqRatchetState - top level state (proto field numbers in comments) */
|
|
12
|
+
export interface PbPqRatchetState {
|
|
13
|
+
versionNegotiation?: PbVersionNegotiation | undefined; // field 1
|
|
14
|
+
chain?: PbChain | undefined; // field 2
|
|
15
|
+
v1?: PbV1State | undefined; // field 3 (oneof inner)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface PbVersionNegotiation {
|
|
19
|
+
authKey: Uint8Array; // field 1
|
|
20
|
+
direction: number; // field 2 (Direction enum)
|
|
21
|
+
minVersion: number; // field 3 (Version enum)
|
|
22
|
+
chainParams?: PbChainParams | undefined; // field 4
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface PbChainParams {
|
|
26
|
+
maxJump: number; // field 1 (uint32)
|
|
27
|
+
maxOooKeys: number; // field 2 (uint32)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface PbChain {
|
|
31
|
+
direction: number; // field 1 (Direction enum)
|
|
32
|
+
currentEpoch: bigint; // field 2 (uint64)
|
|
33
|
+
links: PbEpoch[]; // field 3 (repeated)
|
|
34
|
+
nextRoot: Uint8Array; // field 4
|
|
35
|
+
sendEpoch: bigint; // field 5 (uint64)
|
|
36
|
+
params?: PbChainParams | undefined; // field 6
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface PbEpoch {
|
|
40
|
+
send?: PbEpochDirection | undefined; // field 1
|
|
41
|
+
recv?: PbEpochDirection | undefined; // field 2
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface PbEpochDirection {
|
|
45
|
+
ctr: number; // field 1 (uint32)
|
|
46
|
+
next: Uint8Array; // field 2 (bytes)
|
|
47
|
+
prev: Uint8Array; // field 3 (bytes)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface PbAuthenticator {
|
|
51
|
+
rootKey: Uint8Array; // field 1
|
|
52
|
+
macKey: Uint8Array; // field 2
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface PbPolynomialEncoder {
|
|
56
|
+
idx: number; // field 1 (uint32)
|
|
57
|
+
pts: Uint8Array[]; // field 2 (repeated bytes)
|
|
58
|
+
polys: Uint8Array[]; // field 3 (repeated bytes)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface PbPolynomialDecoder {
|
|
62
|
+
ptsNeeded: number; // field 1 (uint32)
|
|
63
|
+
polys: number; // field 2 (uint32, always 16)
|
|
64
|
+
pts: Uint8Array[]; // field 3 (repeated bytes)
|
|
65
|
+
isComplete: boolean; // field 4 (bool)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** V1Msg - wire message format */
|
|
69
|
+
export interface PbV1Msg {
|
|
70
|
+
epoch: bigint; // field 1 (uint64)
|
|
71
|
+
index: number; // field 2 (uint32)
|
|
72
|
+
innerMsg?: PbV1MsgInner | undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export type PbV1MsgInner =
|
|
76
|
+
| { type: "hdr"; chunk: PbChunk } // field 3
|
|
77
|
+
| { type: "ek"; chunk: PbChunk } // field 4
|
|
78
|
+
| { type: "ekCt1Ack"; chunk: PbChunk } // field 5
|
|
79
|
+
| { type: "ct1Ack"; value: boolean } // field 6
|
|
80
|
+
| { type: "ct1"; chunk: PbChunk } // field 7
|
|
81
|
+
| { type: "ct2"; chunk: PbChunk }; // field 8
|
|
82
|
+
|
|
83
|
+
export interface PbChunk {
|
|
84
|
+
index: number; // field 1 (uint32)
|
|
85
|
+
data: Uint8Array; // field 2 (bytes, 32 bytes)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// V1State contains the oneof with 11 chunked states
|
|
89
|
+
export interface PbV1State {
|
|
90
|
+
innerState?: PbChunkedState | undefined;
|
|
91
|
+
/** Epoch of the current state (field 12 in proto, used for deserialization) */
|
|
92
|
+
epoch?: bigint | undefined;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export type PbChunkedState =
|
|
96
|
+
// Send_EK states (fields 1-5 in V1State.Chunked)
|
|
97
|
+
| { type: "keysUnsampled"; uc: PbUnchunkedKeysUnsampled }
|
|
98
|
+
| { type: "keysSampled"; uc: PbUnchunkedKeysSampled; sendingHdr: PbPolynomialEncoder }
|
|
99
|
+
| {
|
|
100
|
+
type: "headerSent";
|
|
101
|
+
uc: PbUnchunkedHeaderSent;
|
|
102
|
+
sendingEk: PbPolynomialEncoder;
|
|
103
|
+
receivingCt1: PbPolynomialDecoder;
|
|
104
|
+
}
|
|
105
|
+
| { type: "ct1Received"; uc: PbUnchunkedCt1Received; sendingEk: PbPolynomialEncoder }
|
|
106
|
+
| {
|
|
107
|
+
type: "ekSentCt1Received";
|
|
108
|
+
uc: PbUnchunkedEkSentCt1Received;
|
|
109
|
+
receivingCt2: PbPolynomialDecoder;
|
|
110
|
+
}
|
|
111
|
+
// Send_CT states (fields 6-11 in V1State.Chunked)
|
|
112
|
+
| { type: "noHeaderReceived"; uc: PbUnchunkedNoHeaderReceived; receivingHdr: PbPolynomialDecoder }
|
|
113
|
+
| { type: "headerReceived"; uc: PbUnchunkedHeaderReceived; receivingEk: PbPolynomialDecoder }
|
|
114
|
+
| {
|
|
115
|
+
type: "ct1Sampled";
|
|
116
|
+
uc: PbUnchunkedCt1Sampled;
|
|
117
|
+
sendingCt1: PbPolynomialEncoder;
|
|
118
|
+
receivingEk: PbPolynomialDecoder;
|
|
119
|
+
}
|
|
120
|
+
| {
|
|
121
|
+
type: "ekReceivedCt1Sampled";
|
|
122
|
+
uc: PbUnchunkedEkReceivedCt1Sampled;
|
|
123
|
+
sendingCt1: PbPolynomialEncoder;
|
|
124
|
+
}
|
|
125
|
+
| { type: "ct1Acknowledged"; uc: PbUnchunkedCt1Acknowledged; receivingEk: PbPolynomialDecoder }
|
|
126
|
+
| { type: "ct2Sampled"; uc: PbUnchunkedCt2Sampled; sendingCt2: PbPolynomialEncoder };
|
|
127
|
+
|
|
128
|
+
// Unchunked state data (carry actual KEM keys/ciphertexts)
|
|
129
|
+
export interface PbUnchunkedKeysUnsampled {
|
|
130
|
+
auth?: PbAuthenticator | undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface PbUnchunkedKeysSampled {
|
|
134
|
+
auth?: PbAuthenticator | undefined;
|
|
135
|
+
ek: Uint8Array;
|
|
136
|
+
dk: Uint8Array;
|
|
137
|
+
hdr: Uint8Array;
|
|
138
|
+
hdrMac: Uint8Array;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface PbUnchunkedHeaderSent {
|
|
142
|
+
auth?: PbAuthenticator | undefined;
|
|
143
|
+
ek: Uint8Array;
|
|
144
|
+
dk: Uint8Array;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface PbUnchunkedCt1Received {
|
|
148
|
+
auth?: PbAuthenticator | undefined;
|
|
149
|
+
dk: Uint8Array;
|
|
150
|
+
ct1: Uint8Array;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export interface PbUnchunkedEkSentCt1Received {
|
|
154
|
+
auth?: PbAuthenticator | undefined;
|
|
155
|
+
dk: Uint8Array;
|
|
156
|
+
ct1: Uint8Array;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export interface PbUnchunkedNoHeaderReceived {
|
|
160
|
+
auth?: PbAuthenticator | undefined;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export interface PbUnchunkedHeaderReceived {
|
|
164
|
+
auth?: PbAuthenticator | undefined;
|
|
165
|
+
hdr: Uint8Array;
|
|
166
|
+
es: Uint8Array;
|
|
167
|
+
ct1: Uint8Array;
|
|
168
|
+
ss: Uint8Array;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export interface PbUnchunkedCt1Sampled {
|
|
172
|
+
auth?: PbAuthenticator | undefined;
|
|
173
|
+
hdr: Uint8Array;
|
|
174
|
+
es: Uint8Array;
|
|
175
|
+
ct1: Uint8Array;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export interface PbUnchunkedEkReceivedCt1Sampled {
|
|
179
|
+
auth?: PbAuthenticator | undefined;
|
|
180
|
+
hdr: Uint8Array;
|
|
181
|
+
es: Uint8Array;
|
|
182
|
+
ek: Uint8Array;
|
|
183
|
+
ct1: Uint8Array;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export interface PbUnchunkedCt1Acknowledged {
|
|
187
|
+
auth?: PbAuthenticator | undefined;
|
|
188
|
+
hdr: Uint8Array;
|
|
189
|
+
es: Uint8Array;
|
|
190
|
+
ct1: Uint8Array;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export interface PbUnchunkedCt2Sampled {
|
|
194
|
+
auth?: PbAuthenticator | undefined;
|
|
195
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright © 2025 Signal Messenger, LLC
|
|
3
|
+
* Copyright © 2026 Parity Technologies
|
|
4
|
+
*
|
|
5
|
+
* Shared types for the SPQR protocol.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Epoch identifier (u64 in Rust, bigint in TypeScript) */
|
|
9
|
+
export type Epoch = bigint;
|
|
10
|
+
|
|
11
|
+
/** Secret key material (32 bytes) */
|
|
12
|
+
export type Secret = Uint8Array;
|
|
13
|
+
|
|
14
|
+
/** Message key output from send/recv */
|
|
15
|
+
export type MessageKey = Uint8Array | null;
|
|
16
|
+
|
|
17
|
+
/** Opaque serialized state (protobuf bytes) */
|
|
18
|
+
export type SerializedState = Uint8Array;
|
|
19
|
+
|
|
20
|
+
/** Opaque serialized message (protobuf V1Msg bytes) */
|
|
21
|
+
export type SerializedMessage = Uint8Array;
|
|
22
|
+
|
|
23
|
+
/** Interface for random byte generation */
|
|
24
|
+
export type RandomBytes = (length: number) => Uint8Array;
|
|
25
|
+
|
|
26
|
+
/** Result of send() */
|
|
27
|
+
export interface Send {
|
|
28
|
+
state: SerializedState;
|
|
29
|
+
msg: SerializedMessage;
|
|
30
|
+
key: Secret | null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Result of recv() */
|
|
34
|
+
export interface Recv {
|
|
35
|
+
state: SerializedState;
|
|
36
|
+
key: Secret | null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Epoch secret output from state machine */
|
|
40
|
+
export interface EpochSecret {
|
|
41
|
+
epoch: Epoch;
|
|
42
|
+
secret: Secret;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Parameters for initializing SPQR */
|
|
46
|
+
export interface Params {
|
|
47
|
+
direction: Direction;
|
|
48
|
+
version: Version;
|
|
49
|
+
minVersion: Version;
|
|
50
|
+
authKey: Uint8Array;
|
|
51
|
+
chainParams: ChainParams;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Chain configuration parameters */
|
|
55
|
+
export interface ChainParams {
|
|
56
|
+
maxJump: number;
|
|
57
|
+
maxOooKeys: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Protocol version */
|
|
61
|
+
export enum Version {
|
|
62
|
+
V0 = 0,
|
|
63
|
+
V1 = 1,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Communication direction */
|
|
67
|
+
export enum Direction {
|
|
68
|
+
A2B = 0,
|
|
69
|
+
B2A = 1,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Current version negotiation status */
|
|
73
|
+
export type CurrentVersion =
|
|
74
|
+
| { type: "still_negotiating"; version: Version; minVersion: Version }
|
|
75
|
+
| { type: "negotiation_complete"; version: Version };
|
|
76
|
+
|
|
77
|
+
/** Internal secret output from state transitions */
|
|
78
|
+
export type SecretOutput =
|
|
79
|
+
| { type: "none" }
|
|
80
|
+
| { type: "send"; secret: Secret }
|
|
81
|
+
| { type: "recv"; secret: Secret };
|
package/src/util.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright © 2025 Signal Messenger, LLC
|
|
3
|
+
* Copyright © 2026 Parity Technologies
|
|
4
|
+
*
|
|
5
|
+
* Low-level utility functions for SPQR.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Constant-time comparison of two byte arrays.
|
|
10
|
+
* Returns true if they are equal, false otherwise.
|
|
11
|
+
*
|
|
12
|
+
* Uses a XOR accumulator to avoid early-exit timing leaks.
|
|
13
|
+
*
|
|
14
|
+
* Limitation: Unlike Rust (#[inline(never)] + black_box), JavaScript
|
|
15
|
+
* cannot fully prevent JIT optimizations. In Node.js environments,
|
|
16
|
+
* callers requiring stronger guarantees should use
|
|
17
|
+
* crypto.timingSafeEqual directly.
|
|
18
|
+
*/
|
|
19
|
+
export function constantTimeEqual(a: Uint8Array, b: Uint8Array): boolean {
|
|
20
|
+
if (a.length !== b.length) return false;
|
|
21
|
+
let diff = 0;
|
|
22
|
+
for (let i = 0; i < a.length; i++) {
|
|
23
|
+
diff |= a[i] ^ b[i];
|
|
24
|
+
}
|
|
25
|
+
return diff === 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Convert a bigint to 8-byte big-endian Uint8Array */
|
|
29
|
+
export function bigintToBE8(value: bigint): Uint8Array {
|
|
30
|
+
const buf = new Uint8Array(8);
|
|
31
|
+
const view = new DataView(buf.buffer);
|
|
32
|
+
view.setBigUint64(0, value, false);
|
|
33
|
+
return buf;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Convert an 8-byte big-endian Uint8Array to bigint */
|
|
37
|
+
export function be8ToBigint(buf: Uint8Array): bigint {
|
|
38
|
+
const view = new DataView(buf.buffer, buf.byteOffset, 8);
|
|
39
|
+
return view.getBigUint64(0, false);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Convert a number to 4-byte big-endian Uint8Array */
|
|
43
|
+
export function uint32ToBE4(value: number): Uint8Array {
|
|
44
|
+
const buf = new Uint8Array(4);
|
|
45
|
+
const view = new DataView(buf.buffer);
|
|
46
|
+
view.setUint32(0, value, false);
|
|
47
|
+
return buf;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Concatenate multiple Uint8Arrays */
|
|
51
|
+
export function concat(...arrays: Uint8Array[]): Uint8Array {
|
|
52
|
+
let total = 0;
|
|
53
|
+
for (const a of arrays) total += a.length;
|
|
54
|
+
const result = new Uint8Array(total);
|
|
55
|
+
let offset = 0;
|
|
56
|
+
for (const a of arrays) {
|
|
57
|
+
result.set(a, offset);
|
|
58
|
+
offset += a.length;
|
|
59
|
+
}
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright © 2025 Signal Messenger, LLC
|
|
3
|
+
* Copyright © 2026 Parity Technologies
|
|
4
|
+
*
|
|
5
|
+
* Chunked state machine for SPQR V1.
|
|
6
|
+
*
|
|
7
|
+
* Provides erasure-coded chunk-by-chunk data transfer wrapping the
|
|
8
|
+
* unchunked V1 state machine.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// Wire up the circular dependency factory before any re-exports
|
|
12
|
+
import { _setCreateSendCtNoHeaderReceived } from "./send-ek.js";
|
|
13
|
+
import { NoHeaderReceived } from "./send-ct.js";
|
|
14
|
+
import type { PolyDecoder } from "../../encoding/polynomial.js";
|
|
15
|
+
import type * as unchunkedSendCt from "../unchunked/send-ct.js";
|
|
16
|
+
|
|
17
|
+
_setCreateSendCtNoHeaderReceived(
|
|
18
|
+
(uc: unchunkedSendCt.NoHeaderReceived, receivingHdr: PolyDecoder) =>
|
|
19
|
+
new NoHeaderReceived(uc, receivingHdr),
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
// States dispatcher
|
|
23
|
+
export {
|
|
24
|
+
type States,
|
|
25
|
+
type Message,
|
|
26
|
+
type MessagePayload,
|
|
27
|
+
type SendResult,
|
|
28
|
+
type RecvResult,
|
|
29
|
+
initA,
|
|
30
|
+
initB,
|
|
31
|
+
send,
|
|
32
|
+
recv,
|
|
33
|
+
} from "./states.js";
|
|
34
|
+
|
|
35
|
+
// Message serialization
|
|
36
|
+
export { serializeMessage, deserializeMessage } from "./message.js";
|
|
37
|
+
|
|
38
|
+
// Chunked send_ek states
|
|
39
|
+
export {
|
|
40
|
+
KeysUnsampled,
|
|
41
|
+
KeysSampled,
|
|
42
|
+
HeaderSent,
|
|
43
|
+
Ct1Received,
|
|
44
|
+
EkSentCt1Received,
|
|
45
|
+
type HeaderSentRecvChunk,
|
|
46
|
+
type EkSentCt1ReceivedRecvChunk,
|
|
47
|
+
} from "./send-ek.js";
|
|
48
|
+
|
|
49
|
+
// Chunked send_ct states
|
|
50
|
+
export {
|
|
51
|
+
NoHeaderReceived,
|
|
52
|
+
HeaderReceived,
|
|
53
|
+
Ct1Sampled,
|
|
54
|
+
EkReceivedCt1Sampled,
|
|
55
|
+
Ct1Acknowledged,
|
|
56
|
+
Ct2Sampled,
|
|
57
|
+
type NoHeaderReceivedRecvChunk,
|
|
58
|
+
type Ct1SampledRecvChunk,
|
|
59
|
+
type Ct1AcknowledgedRecvChunk,
|
|
60
|
+
} from "./send-ct.js";
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright © 2025 Signal Messenger, LLC
|
|
3
|
+
* Copyright © 2026 Parity Technologies
|
|
4
|
+
*
|
|
5
|
+
* Custom binary V1 message serialization for chunked SPQR.
|
|
6
|
+
*
|
|
7
|
+
* Wire format:
|
|
8
|
+
* [version: u8] 1 byte (V1 = 1)
|
|
9
|
+
* [epoch: varint] 1-10 bytes (LEB128)
|
|
10
|
+
* [index: varint] 1-5 bytes (LEB128)
|
|
11
|
+
* [msg_type: u8] 1 byte (0=None, 1=Hdr, 2=Ek, 3=EkCt1Ack, 4=Ct1Ack, 5=Ct1, 6=Ct2)
|
|
12
|
+
* [chunk_index: varint] + [chunk_data: 32 bytes] (optional, if msg_type has a chunk)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { Chunk } from "../../encoding/polynomial.js";
|
|
16
|
+
import type { Message, MessagePayload } from "./states.js";
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Message type enum
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
const enum MessageType {
|
|
23
|
+
None = 0,
|
|
24
|
+
Hdr = 1,
|
|
25
|
+
Ek = 2,
|
|
26
|
+
EkCt1Ack = 3,
|
|
27
|
+
Ct1Ack = 4,
|
|
28
|
+
Ct1 = 5,
|
|
29
|
+
Ct2 = 6,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Varint encoding/decoding (LEB128, matching protobuf varint)
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Encode a bigint as LEB128 varint into the output array.
|
|
38
|
+
*/
|
|
39
|
+
export function encodeVarint(value: bigint, into: number[]): void {
|
|
40
|
+
let v = value;
|
|
41
|
+
if (v < 0n) v = 0n;
|
|
42
|
+
do {
|
|
43
|
+
let byte = Number(v & 0x7fn);
|
|
44
|
+
v >>= 7n;
|
|
45
|
+
if (v > 0n) {
|
|
46
|
+
byte |= 0x80;
|
|
47
|
+
}
|
|
48
|
+
into.push(byte);
|
|
49
|
+
} while (v > 0n);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Decode a LEB128 varint from a Uint8Array at the given offset.
|
|
54
|
+
* Updates offset.offset in place.
|
|
55
|
+
*/
|
|
56
|
+
export function decodeVarint(from: Uint8Array, at: { offset: number }): bigint {
|
|
57
|
+
let result = 0n;
|
|
58
|
+
let shift = 0n;
|
|
59
|
+
while (shift < 70n) {
|
|
60
|
+
if (at.offset >= from.length) {
|
|
61
|
+
throw new Error("Varint: unexpected end of data");
|
|
62
|
+
}
|
|
63
|
+
const byte = from[at.offset++];
|
|
64
|
+
result |= BigInt(byte & 0x7f) << shift;
|
|
65
|
+
if ((byte & 0x80) === 0) {
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
shift += 7n;
|
|
69
|
+
}
|
|
70
|
+
throw new Error("Varint: too many bytes");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Encode a number (u32) as LEB128 varint into the output array.
|
|
75
|
+
*/
|
|
76
|
+
function encodeVarint32(value: number, into: number[]): void {
|
|
77
|
+
let v = value >>> 0;
|
|
78
|
+
do {
|
|
79
|
+
let byte = v & 0x7f;
|
|
80
|
+
v >>>= 7;
|
|
81
|
+
if (v > 0) {
|
|
82
|
+
byte |= 0x80;
|
|
83
|
+
}
|
|
84
|
+
into.push(byte);
|
|
85
|
+
} while (v > 0);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Decode a LEB128 varint as a u32 number.
|
|
90
|
+
*/
|
|
91
|
+
function decodeVarint32(from: Uint8Array, at: { offset: number }): number {
|
|
92
|
+
let result = 0;
|
|
93
|
+
let shift = 0;
|
|
94
|
+
while (shift < 35) {
|
|
95
|
+
if (at.offset >= from.length) {
|
|
96
|
+
throw new Error("Varint32: unexpected end of data");
|
|
97
|
+
}
|
|
98
|
+
const byte = from[at.offset++];
|
|
99
|
+
result |= (byte & 0x7f) << shift;
|
|
100
|
+
if ((byte & 0x80) === 0) {
|
|
101
|
+
return result >>> 0;
|
|
102
|
+
}
|
|
103
|
+
shift += 7;
|
|
104
|
+
}
|
|
105
|
+
throw new Error("Varint32: too many bytes");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// Chunk serialization
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
const CHUNK_DATA_SIZE = 32;
|
|
113
|
+
|
|
114
|
+
/** Encode a chunk (index varint + 32 data bytes) into the output array. */
|
|
115
|
+
export function encodeChunk(chunk: Chunk, into: number[]): void {
|
|
116
|
+
encodeVarint32(chunk.index, into);
|
|
117
|
+
for (let i = 0; i < CHUNK_DATA_SIZE; i++) {
|
|
118
|
+
into.push(chunk.data[i]);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Decode a chunk from a Uint8Array at the given offset. */
|
|
123
|
+
export function decodeChunk(from: Uint8Array, at: { offset: number }): Chunk {
|
|
124
|
+
const index = decodeVarint32(from, at);
|
|
125
|
+
if (at.offset + CHUNK_DATA_SIZE > from.length || index > 0xffff) {
|
|
126
|
+
throw new Error("Chunk: invalid chunk (data too short or index exceeds u16)");
|
|
127
|
+
}
|
|
128
|
+
const data = from.slice(at.offset, at.offset + CHUNK_DATA_SIZE);
|
|
129
|
+
at.offset += CHUNK_DATA_SIZE;
|
|
130
|
+
return { index, data };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
// Message serialization
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
/** Protocol version */
|
|
138
|
+
const V1 = 1;
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Serialize a Message with a given sequence index into binary wire format.
|
|
142
|
+
*/
|
|
143
|
+
export function serializeMessage(msg: Message, index: number): Uint8Array {
|
|
144
|
+
const out: number[] = [];
|
|
145
|
+
|
|
146
|
+
// Version byte
|
|
147
|
+
out.push(V1);
|
|
148
|
+
|
|
149
|
+
// Epoch (bigint varint)
|
|
150
|
+
encodeVarint(msg.epoch, out);
|
|
151
|
+
|
|
152
|
+
// Index (u32 varint)
|
|
153
|
+
encodeVarint32(index, out);
|
|
154
|
+
|
|
155
|
+
// Message type + payload
|
|
156
|
+
const payload = msg.payload;
|
|
157
|
+
switch (payload.type) {
|
|
158
|
+
case "none":
|
|
159
|
+
out.push(MessageType.None);
|
|
160
|
+
break;
|
|
161
|
+
case "hdr":
|
|
162
|
+
out.push(MessageType.Hdr);
|
|
163
|
+
encodeChunk(payload.chunk, out);
|
|
164
|
+
break;
|
|
165
|
+
case "ek":
|
|
166
|
+
out.push(MessageType.Ek);
|
|
167
|
+
encodeChunk(payload.chunk, out);
|
|
168
|
+
break;
|
|
169
|
+
case "ekCt1Ack":
|
|
170
|
+
out.push(MessageType.EkCt1Ack);
|
|
171
|
+
encodeChunk(payload.chunk, out);
|
|
172
|
+
break;
|
|
173
|
+
case "ct1Ack":
|
|
174
|
+
out.push(MessageType.Ct1Ack);
|
|
175
|
+
// No value byte -- matches Rust wire format (Ct1Ack has no payload)
|
|
176
|
+
break;
|
|
177
|
+
case "ct1":
|
|
178
|
+
out.push(MessageType.Ct1);
|
|
179
|
+
encodeChunk(payload.chunk, out);
|
|
180
|
+
break;
|
|
181
|
+
case "ct2":
|
|
182
|
+
out.push(MessageType.Ct2);
|
|
183
|
+
encodeChunk(payload.chunk, out);
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return new Uint8Array(out);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Deserialize a Message from binary wire format.
|
|
192
|
+
*/
|
|
193
|
+
export function deserializeMessage(from: Uint8Array): {
|
|
194
|
+
msg: Message;
|
|
195
|
+
index: number;
|
|
196
|
+
bytesRead: number;
|
|
197
|
+
} {
|
|
198
|
+
const at = { offset: 0 };
|
|
199
|
+
|
|
200
|
+
// Version byte
|
|
201
|
+
if (at.offset >= from.length) {
|
|
202
|
+
throw new Error("Message: empty data");
|
|
203
|
+
}
|
|
204
|
+
const version = from[at.offset++];
|
|
205
|
+
if (version !== V1) {
|
|
206
|
+
throw new Error(`Message: unsupported version ${version}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Epoch
|
|
210
|
+
const epoch = decodeVarint(from, at);
|
|
211
|
+
if (epoch === 0n) {
|
|
212
|
+
throw new Error("Message: epoch must be > 0");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Index
|
|
216
|
+
const index = decodeVarint32(from, at);
|
|
217
|
+
|
|
218
|
+
// Message type
|
|
219
|
+
if (at.offset >= from.length) {
|
|
220
|
+
throw new Error("Message: missing message type");
|
|
221
|
+
}
|
|
222
|
+
const msgType = from[at.offset++];
|
|
223
|
+
|
|
224
|
+
let payload: MessagePayload;
|
|
225
|
+
switch (msgType as MessageType) {
|
|
226
|
+
case MessageType.None:
|
|
227
|
+
payload = { type: "none" };
|
|
228
|
+
break;
|
|
229
|
+
case MessageType.Hdr:
|
|
230
|
+
payload = { type: "hdr", chunk: decodeChunk(from, at) };
|
|
231
|
+
break;
|
|
232
|
+
case MessageType.Ek:
|
|
233
|
+
payload = { type: "ek", chunk: decodeChunk(from, at) };
|
|
234
|
+
break;
|
|
235
|
+
case MessageType.EkCt1Ack:
|
|
236
|
+
payload = { type: "ekCt1Ack", chunk: decodeChunk(from, at) };
|
|
237
|
+
break;
|
|
238
|
+
case MessageType.Ct1Ack:
|
|
239
|
+
// No value byte -- matches Rust (hardcoded true, no data after type byte)
|
|
240
|
+
payload = { type: "ct1Ack" };
|
|
241
|
+
break;
|
|
242
|
+
case MessageType.Ct1:
|
|
243
|
+
payload = { type: "ct1", chunk: decodeChunk(from, at) };
|
|
244
|
+
break;
|
|
245
|
+
case MessageType.Ct2:
|
|
246
|
+
payload = { type: "ct2", chunk: decodeChunk(from, at) };
|
|
247
|
+
break;
|
|
248
|
+
default:
|
|
249
|
+
throw new Error(`Message: unknown message type ${msgType}`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
msg: { epoch, payload },
|
|
254
|
+
index,
|
|
255
|
+
bytesRead: at.offset,
|
|
256
|
+
};
|
|
257
|
+
}
|