@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/src/chain.ts ADDED
@@ -0,0 +1,522 @@
1
+ /**
2
+ * Copyright © 2025 Signal Messenger, LLC
3
+ * Copyright © 2026 Parity Technologies
4
+ *
5
+ * SPQR Symmetric Chain -- epoch-based key derivation with forward/backward secrecy.
6
+ *
7
+ * Ported from Signal's spqr crate: chain.rs
8
+ *
9
+ * The Chain manages send/receive keys across epochs. Each epoch has two
10
+ * directional chains (A2B and B2A). Keys are derived using HKDF-SHA256
11
+ * with specific info strings that MUST match the Rust implementation
12
+ * exactly (including the double space in "Chain Start").
13
+ */
14
+
15
+ import { hkdfSha256 } from "./kdf.js";
16
+ import { concat, uint32ToBE4 } from "./util.js";
17
+ import {
18
+ ZERO_SALT,
19
+ LABEL_CHAIN_START,
20
+ LABEL_CHAIN_NEXT,
21
+ LABEL_CHAIN_ADD_EPOCH,
22
+ DEFAULT_MAX_JUMP,
23
+ DEFAULT_MAX_OOO_KEYS,
24
+ EPOCHS_TO_KEEP_PRIOR_TO_SEND_EPOCH,
25
+ KEY_ENTRY_SIZE,
26
+ } from "./constants.js";
27
+ import { SpqrError, SpqrErrorCode } from "./error.js";
28
+ import type { Epoch, EpochSecret, ChainParams } from "./types.js";
29
+ import { Direction } from "./types.js";
30
+ import type {
31
+ PbChain,
32
+ PbChainParams,
33
+ PbEpoch,
34
+ PbEpochDirection,
35
+ } from "./proto/pq-ratchet-types.js";
36
+
37
+ // Re-export Direction from types
38
+ export { Direction } from "./types.js";
39
+
40
+ // ---- Pre-encoded info labels ----
41
+ const enc = new TextEncoder();
42
+ const CHAIN_START_INFO = enc.encode(LABEL_CHAIN_START);
43
+ const CHAIN_NEXT_INFO = enc.encode(LABEL_CHAIN_NEXT);
44
+ const CHAIN_ADD_EPOCH_INFO = enc.encode(LABEL_CHAIN_ADD_EPOCH);
45
+
46
+ // ---- Helper functions ----
47
+
48
+ function resolveMaxJump(params: ChainParams): number {
49
+ return params.maxJump > 0 ? params.maxJump : DEFAULT_MAX_JUMP;
50
+ }
51
+
52
+ function resolveMaxOooKeys(params: ChainParams): number {
53
+ return params.maxOooKeys > 0 ? params.maxOooKeys : DEFAULT_MAX_OOO_KEYS;
54
+ }
55
+
56
+ function trimSize(params: ChainParams): number {
57
+ const maxOoo = resolveMaxOooKeys(params);
58
+ return Math.floor((maxOoo * 11) / 10) + 1;
59
+ }
60
+
61
+ function switchDirection(d: Direction): Direction {
62
+ // Direction.A2B = 0, Direction.B2A = 1
63
+ return d === Direction.A2B ? Direction.B2A : Direction.A2B;
64
+ }
65
+
66
+ // ---- KeyHistory ----
67
+
68
+ /**
69
+ * Stores out-of-order keys as packed [index_be32 (4 bytes)][key (32 bytes)] entries.
70
+ * Matches Rust KeyHistory data layout exactly.
71
+ */
72
+ class KeyHistory {
73
+ data: Uint8Array;
74
+ private length: number;
75
+
76
+ constructor(data?: Uint8Array) {
77
+ this.data = data !== undefined ? Uint8Array.from(data) : new Uint8Array(0);
78
+ this.length = this.data.length;
79
+ }
80
+
81
+ add(index: number, key: Uint8Array, _params: ChainParams): void {
82
+ const entry = new Uint8Array(KEY_ENTRY_SIZE);
83
+ const view = new DataView(entry.buffer);
84
+ view.setUint32(0, index, false); // big-endian
85
+ entry.set(key, 4);
86
+
87
+ const newData = new Uint8Array(this.length + KEY_ENTRY_SIZE);
88
+ newData.set(this.data.subarray(0, this.length));
89
+ newData.set(entry, this.length);
90
+ this.data = newData;
91
+ this.length += KEY_ENTRY_SIZE;
92
+ }
93
+
94
+ gc(currentKey: number, params: ChainParams): void {
95
+ const maxOoo = resolveMaxOooKeys(params);
96
+ if (this.length >= trimSize(params) * KEY_ENTRY_SIZE) {
97
+ if (currentKey < maxOoo) {
98
+ throw new Error("KeyHistory.gc: currentKey < maxOooKeys (corrupted state)");
99
+ }
100
+ const trimHorizon = currentKey - maxOoo;
101
+
102
+ let i = 0;
103
+ while (i < this.length) {
104
+ const view = new DataView(this.data.buffer, this.data.byteOffset + i, 4);
105
+ const idx = view.getUint32(0, false);
106
+ if (trimHorizon > idx) {
107
+ this.removeAt(i);
108
+ // Don't advance i -- replacement might also be old
109
+ } else {
110
+ i += KEY_ENTRY_SIZE;
111
+ }
112
+ }
113
+ }
114
+ }
115
+
116
+ clear(): void {
117
+ this.data = new Uint8Array(0);
118
+ this.length = 0;
119
+ }
120
+
121
+ get(at: number, currentCtr: number, params: ChainParams): Uint8Array {
122
+ const maxOoo = resolveMaxOooKeys(params);
123
+ if (at + maxOoo < currentCtr) {
124
+ throw new SpqrError(`Key trimmed: ${at}`, SpqrErrorCode.KeyTrimmed);
125
+ }
126
+
127
+ const want = new Uint8Array(4);
128
+ new DataView(want.buffer).setUint32(0, at, false);
129
+
130
+ for (let i = 0; i < this.length; i += KEY_ENTRY_SIZE) {
131
+ if (
132
+ this.data[i] === want[0] &&
133
+ this.data[i + 1] === want[1] &&
134
+ this.data[i + 2] === want[2] &&
135
+ this.data[i + 3] === want[3]
136
+ ) {
137
+ const key = this.data.slice(i + 4, i + KEY_ENTRY_SIZE);
138
+ this.removeAt(i);
139
+ return key;
140
+ }
141
+ }
142
+
143
+ throw new SpqrError(`Key already requested: ${at}`, SpqrErrorCode.KeyAlreadyRequested);
144
+ }
145
+
146
+ private removeAt(index: number): void {
147
+ if (index + KEY_ENTRY_SIZE < this.length) {
148
+ // Swap-remove: move last entry to this position
149
+ const lastStart = this.length - KEY_ENTRY_SIZE;
150
+ this.data.copyWithin(index, lastStart, this.length);
151
+ }
152
+ this.length -= KEY_ENTRY_SIZE;
153
+ }
154
+
155
+ /** Serialize to raw bytes for protobuf storage */
156
+ serialize(): Uint8Array {
157
+ return this.data.slice(0, this.length);
158
+ }
159
+ }
160
+
161
+ // ---- ChainEpochDirection ----
162
+
163
+ /**
164
+ * One directional chain within an epoch (send or recv).
165
+ * Manages a ratcheting key derivation with HKDF.
166
+ */
167
+ class ChainEpochDirection {
168
+ ctr: number;
169
+ next: Uint8Array;
170
+ prev: KeyHistory;
171
+
172
+ constructor(key: Uint8Array, ctr?: number, prev?: Uint8Array) {
173
+ this.ctr = ctr ?? 0;
174
+ this.next = Uint8Array.from(key);
175
+ this.prev = new KeyHistory(prev);
176
+ }
177
+
178
+ /**
179
+ * Derive the next key in the chain.
180
+ * Returns [index, key].
181
+ */
182
+ nextKey(): [number, Uint8Array] {
183
+ this.ctr += 1;
184
+
185
+ const ctrBe = uint32ToBE4(this.ctr);
186
+ const info = concat(ctrBe, CHAIN_NEXT_INFO);
187
+
188
+ const gen = hkdfSha256(this.next, ZERO_SALT, info, 64);
189
+ this.next = gen.slice(0, 32);
190
+ const key = gen.slice(32, 64);
191
+
192
+ return [this.ctr, key];
193
+ }
194
+
195
+ /**
196
+ * Internal: derive next key without consuming it publicly.
197
+ * Used for skipping ahead to build out-of-order key history.
198
+ */
199
+ private static nextKeyInternal(
200
+ next: Uint8Array,
201
+ ctr: number,
202
+ ): { next: Uint8Array; ctr: number; index: number; key: Uint8Array } {
203
+ ctr += 1;
204
+
205
+ const ctrBe = uint32ToBE4(ctr);
206
+ const info = concat(ctrBe, CHAIN_NEXT_INFO);
207
+
208
+ const gen = hkdfSha256(next, ZERO_SALT, info, 64);
209
+
210
+ return {
211
+ next: gen.slice(0, 32),
212
+ ctr,
213
+ index: ctr,
214
+ key: gen.slice(32, 64),
215
+ };
216
+ }
217
+
218
+ /**
219
+ * Get the key at a specific counter position.
220
+ * Supports out-of-order access via KeyHistory.
221
+ */
222
+ key(at: number, params: ChainParams): Uint8Array {
223
+ const maxJump = resolveMaxJump(params);
224
+ const maxOoo = resolveMaxOooKeys(params);
225
+
226
+ if (at > this.ctr) {
227
+ if (at - this.ctr > maxJump) {
228
+ throw new SpqrError(`Key jump: ${this.ctr} - ${at}`, SpqrErrorCode.KeyJump);
229
+ }
230
+ } else if (at < this.ctr) {
231
+ return this.prev.get(at, this.ctr, params);
232
+ } else {
233
+ // at === this.ctr: already returned
234
+ throw new SpqrError(`Key already requested: ${at}`, SpqrErrorCode.KeyAlreadyRequested);
235
+ }
236
+
237
+ if (at > this.ctr + maxOoo) {
238
+ // About to make all currently-held keys obsolete
239
+ this.prev.clear();
240
+ }
241
+
242
+ while (at > this.ctr + 1) {
243
+ const result = ChainEpochDirection.nextKeyInternal(this.next, this.ctr);
244
+ this.next = result.next;
245
+ this.ctr = result.ctr;
246
+ // Only add keys into history if we're not going to immediately GC them
247
+ if (this.ctr + maxOoo >= at) {
248
+ this.prev.add(result.index, result.key, params);
249
+ }
250
+ }
251
+
252
+ // GC after potentially adding new keys
253
+ this.prev.gc(this.ctr, params);
254
+
255
+ // Get the requested key
256
+ const result = ChainEpochDirection.nextKeyInternal(this.next, this.ctr);
257
+ this.next = result.next;
258
+ this.ctr = result.ctr;
259
+ return result.key;
260
+ }
261
+
262
+ clearNext(): void {
263
+ this.next = new Uint8Array(0);
264
+ }
265
+
266
+ /** Serialize to protobuf EpochDirection */
267
+ toProto(): PbEpochDirection {
268
+ return {
269
+ ctr: this.ctr,
270
+ next: Uint8Array.from(this.next),
271
+ prev: this.prev.serialize(),
272
+ };
273
+ }
274
+
275
+ static fromProto(pb: PbEpochDirection): ChainEpochDirection {
276
+ return new ChainEpochDirection(pb.next, pb.ctr, pb.prev);
277
+ }
278
+ }
279
+
280
+ // ---- ChainEpoch ----
281
+
282
+ interface ChainEpoch {
283
+ send: ChainEpochDirection;
284
+ recv: ChainEpochDirection;
285
+ }
286
+
287
+ // ---- Chain ----
288
+
289
+ /**
290
+ * The main Chain manages send/receive keys across all epochs.
291
+ *
292
+ * Port of Rust's Chain struct from chain.rs.
293
+ * Uses bigint for epoch values (matching Rust u64).
294
+ */
295
+ export class Chain {
296
+ private readonly dir: Direction;
297
+ private currentEpoch: Epoch;
298
+ private sendEpoch: Epoch;
299
+ private readonly links: ChainEpoch[];
300
+ private nextRoot: Uint8Array;
301
+ private readonly params: ChainParams;
302
+
303
+ private constructor(
304
+ dir: Direction,
305
+ currentEpoch: Epoch,
306
+ sendEpoch: Epoch,
307
+ links: ChainEpoch[],
308
+ nextRoot: Uint8Array,
309
+ params: ChainParams,
310
+ ) {
311
+ this.dir = dir;
312
+ this.currentEpoch = currentEpoch;
313
+ this.sendEpoch = sendEpoch;
314
+ this.links = links;
315
+ this.nextRoot = nextRoot;
316
+ this.params = params;
317
+ }
318
+
319
+ /**
320
+ * Create a new chain from an initial key and direction.
321
+ * HKDF info: "Signal PQ Ratchet V1 Chain Start" (TWO spaces before Start!)
322
+ *
323
+ * The 96-byte HKDF output is split as:
324
+ * [0..32]: nextRoot
325
+ * [32..64]: A2B chain seed
326
+ * [64..96]: B2A chain seed
327
+ *
328
+ * Direction determines which half is send vs recv.
329
+ */
330
+ static create(
331
+ initialKey: Uint8Array,
332
+ dir: Direction,
333
+ params: ChainParams = { maxJump: DEFAULT_MAX_JUMP, maxOooKeys: DEFAULT_MAX_OOO_KEYS },
334
+ ): Chain {
335
+ const gen = hkdfSha256(initialKey, ZERO_SALT, CHAIN_START_INFO, 96);
336
+
337
+ const switchedDir = switchDirection(dir);
338
+
339
+ const sendKey = cedForDirection(gen, dir);
340
+ const recvKey = cedForDirection(gen, switchedDir);
341
+
342
+ return new Chain(
343
+ dir,
344
+ 0n,
345
+ 0n,
346
+ [
347
+ {
348
+ send: new ChainEpochDirection(sendKey),
349
+ recv: new ChainEpochDirection(recvKey),
350
+ },
351
+ ],
352
+ gen.slice(0, 32),
353
+ params,
354
+ );
355
+ }
356
+
357
+ /**
358
+ * Add a new epoch to the chain with a shared secret.
359
+ * HKDF info: "Signal PQ Ratchet V1 Chain Add Epoch"
360
+ *
361
+ * Salt = current nextRoot, IKM = epochSecret.secret
362
+ */
363
+ addEpoch(epochSecret: EpochSecret): void {
364
+ if (epochSecret.epoch !== this.currentEpoch + 1n) {
365
+ throw new SpqrError(
366
+ `Expected epoch ${this.currentEpoch + 1n}, got ${epochSecret.epoch}`,
367
+ SpqrErrorCode.EpochOutOfRange,
368
+ );
369
+ }
370
+
371
+ const gen = hkdfSha256(epochSecret.secret, this.nextRoot, CHAIN_ADD_EPOCH_INFO, 96);
372
+
373
+ this.currentEpoch = epochSecret.epoch;
374
+ this.nextRoot = gen.slice(0, 32);
375
+
376
+ const sendKey = cedForDirection(gen, this.dir);
377
+ const recvKey = cedForDirection(gen, switchDirection(this.dir));
378
+
379
+ this.links.push({
380
+ send: new ChainEpochDirection(sendKey),
381
+ recv: new ChainEpochDirection(recvKey),
382
+ });
383
+ }
384
+
385
+ private epochIdx(epoch: Epoch): number {
386
+ if (epoch > this.currentEpoch) {
387
+ throw new SpqrError(`Epoch not in valid range: ${epoch}`, SpqrErrorCode.EpochOutOfRange);
388
+ }
389
+ const back = Number(this.currentEpoch - epoch);
390
+ if (back >= this.links.length) {
391
+ throw new SpqrError(`Epoch not in valid range: ${epoch}`, SpqrErrorCode.EpochOutOfRange);
392
+ }
393
+ return this.links.length - 1 - back;
394
+ }
395
+
396
+ /**
397
+ * Get the next send key for a given epoch.
398
+ * Returns [index, key].
399
+ */
400
+ sendKey(epoch: Epoch): [number, Uint8Array] {
401
+ if (epoch < this.sendEpoch) {
402
+ throw new SpqrError(
403
+ `Send key epoch decreased (${this.sendEpoch} -> ${epoch})`,
404
+ SpqrErrorCode.SendKeyEpochDecreased,
405
+ );
406
+ }
407
+
408
+ let epochIndex = this.epochIdx(epoch);
409
+
410
+ if (this.sendEpoch !== epoch) {
411
+ this.sendEpoch = epoch;
412
+
413
+ while (epochIndex > EPOCHS_TO_KEEP_PRIOR_TO_SEND_EPOCH) {
414
+ this.links.shift();
415
+ epochIndex -= 1;
416
+ }
417
+
418
+ for (let i = 0; i < epochIndex; i++) {
419
+ this.links[i].send.clearNext();
420
+ }
421
+ }
422
+
423
+ return this.links[epochIndex].send.nextKey();
424
+ }
425
+
426
+ /**
427
+ * Get a receive key for a given epoch and counter index.
428
+ */
429
+ recvKey(epoch: Epoch, index: number): Uint8Array {
430
+ const epochIndex = this.epochIdx(epoch);
431
+ return this.links[epochIndex].recv.key(index, this.params);
432
+ }
433
+
434
+ // ---- Protobuf serialization ----
435
+
436
+ /**
437
+ * Serialize the chain to its protobuf representation.
438
+ * Matches Rust Chain::into_pb().
439
+ */
440
+ toProto(): PbChain {
441
+ return {
442
+ direction: this.dir,
443
+ currentEpoch: this.currentEpoch,
444
+ sendEpoch: this.sendEpoch,
445
+ nextRoot: Uint8Array.from(this.nextRoot),
446
+ links: this.links.map(
447
+ (link): PbEpoch => ({
448
+ send: link.send.toProto(),
449
+ recv: link.recv.toProto(),
450
+ }),
451
+ ),
452
+ params: chainParamsToProto(this.params),
453
+ };
454
+ }
455
+
456
+ /**
457
+ * Deserialize a chain from its protobuf representation.
458
+ * Matches Rust Chain::from_pb().
459
+ */
460
+ static fromProto(pb: PbChain): Chain {
461
+ const links = pb.links.map(
462
+ (link): ChainEpoch => ({
463
+ send: ChainEpochDirection.fromProto(
464
+ link.send ?? { ctr: 0, next: new Uint8Array(0), prev: new Uint8Array(0) },
465
+ ),
466
+ recv: ChainEpochDirection.fromProto(
467
+ link.recv ?? { ctr: 0, next: new Uint8Array(0), prev: new Uint8Array(0) },
468
+ ),
469
+ }),
470
+ );
471
+
472
+ const params = chainParamsFromProto(pb.params);
473
+
474
+ return new Chain(
475
+ pb.direction as Direction,
476
+ pb.currentEpoch,
477
+ pb.sendEpoch,
478
+ links,
479
+ Uint8Array.from(pb.nextRoot),
480
+ params,
481
+ );
482
+ }
483
+ }
484
+
485
+ // ---- Direction helpers ----
486
+
487
+ /**
488
+ * Select the chain key for a given direction from a 96-byte HKDF output.
489
+ * A2B uses bytes [32..64], B2A uses bytes [64..96].
490
+ */
491
+ function cedForDirection(gen: Uint8Array, dir: Direction): Uint8Array {
492
+ // Direction.A2B = 0 -> [32..64]
493
+ // Direction.B2A = 1 -> [64..96]
494
+ return dir === Direction.A2B ? gen.slice(32, 64) : gen.slice(64, 96);
495
+ }
496
+
497
+ // ---- ChainParams proto helpers ----
498
+
499
+ /**
500
+ * Convert ChainParams to protobuf format.
501
+ * Default values are stored as 0 (proto3 convention).
502
+ */
503
+ function chainParamsToProto(params: ChainParams): PbChainParams {
504
+ return {
505
+ maxJump: params.maxJump === DEFAULT_MAX_JUMP ? 0 : params.maxJump,
506
+ maxOooKeys: params.maxOooKeys === DEFAULT_MAX_OOO_KEYS ? 0 : params.maxOooKeys,
507
+ };
508
+ }
509
+
510
+ /**
511
+ * Convert protobuf ChainParams to runtime ChainParams.
512
+ * Zero values are interpreted as defaults.
513
+ */
514
+ function chainParamsFromProto(pb?: PbChainParams): ChainParams {
515
+ if (pb === undefined) {
516
+ return { maxJump: DEFAULT_MAX_JUMP, maxOooKeys: DEFAULT_MAX_OOO_KEYS };
517
+ }
518
+ return {
519
+ maxJump: pb.maxJump > 0 ? pb.maxJump : DEFAULT_MAX_JUMP,
520
+ maxOooKeys: pb.maxOooKeys > 0 ? pb.maxOooKeys : DEFAULT_MAX_OOO_KEYS,
521
+ };
522
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Copyright © 2025 Signal Messenger, LLC
3
+ * Copyright © 2026 Parity Technologies
4
+ *
5
+ * Protocol constants for SPQR.
6
+ * Values must match the Rust implementation exactly.
7
+ */
8
+
9
+ /** Size of each erasure-coded chunk in bytes */
10
+ export const CHUNK_SIZE = 32;
11
+
12
+ /** Number of parallel polynomials per chunk (32 bytes / 2 bytes per GF16) */
13
+ export const NUM_POLYS = 16;
14
+
15
+ /** GF(2^16) irreducible polynomial */
16
+ export const GF_POLY = 0x1100b;
17
+
18
+ /** ML-KEM-768 split public key header size */
19
+ export const HEADER_SIZE = 64;
20
+
21
+ /** ML-KEM-768 remaining public key (encapsulation key) */
22
+ export const ENCAPSULATION_KEY_SIZE = 1152;
23
+
24
+ /** ML-KEM-768 secret key size */
25
+ export const DECAPSULATION_KEY_SIZE = 2400;
26
+
27
+ /** ML-KEM-768 first ciphertext size */
28
+ export const CIPHERTEXT1_SIZE = 960;
29
+
30
+ /** ML-KEM-768 second ciphertext size */
31
+ export const CIPHERTEXT2_SIZE = 128;
32
+
33
+ /** ML-KEM-768 full public key size */
34
+ export const PUBLIC_KEY_SIZE = 1184;
35
+
36
+ /** ML-KEM-768 full ciphertext size */
37
+ export const FULL_CIPHERTEXT_SIZE = 1088;
38
+
39
+ /** libcrux incremental encapsulation state size */
40
+ export const ENCAPSULATION_STATE_SIZE = 2080;
41
+
42
+ /** KEM shared secret size */
43
+ export const SHARED_SECRET_SIZE = 32;
44
+
45
+ /** HMAC-SHA256 MAC size */
46
+ export const MAC_SIZE = 32;
47
+
48
+ /** Default max key index jump */
49
+ export const DEFAULT_MAX_JUMP = 25000;
50
+
51
+ /** Default max out-of-order keys stored */
52
+ export const DEFAULT_MAX_OOO_KEYS = 2000;
53
+
54
+ /** Default chain parameters */
55
+ export const DEFAULT_CHAIN_PARAMS = {
56
+ maxJump: DEFAULT_MAX_JUMP,
57
+ maxOooKeys: DEFAULT_MAX_OOO_KEYS,
58
+ } as const;
59
+
60
+ /** Number of epochs to keep prior to the current send epoch */
61
+ export const EPOCHS_TO_KEEP_PRIOR_TO_SEND_EPOCH = 1;
62
+
63
+ /** Size of a key entry in KeyHistory: 4 bytes (index BE32) + 32 bytes (key) */
64
+ export const KEY_ENTRY_SIZE = 36;
65
+
66
+ // ---- HKDF Labels (must match Rust exactly) ----
67
+
68
+ /** Chain initialization label (NOTE: two spaces before "Start") */
69
+ export const LABEL_CHAIN_START = "Signal PQ Ratchet V1 Chain Start";
70
+
71
+ /** Chain epoch advancement label */
72
+ export const LABEL_CHAIN_ADD_EPOCH = "Signal PQ Ratchet V1 Chain Add Epoch";
73
+
74
+ /** Per-message key derivation label */
75
+ export const LABEL_CHAIN_NEXT = "Signal PQ Ratchet V1 Chain Next";
76
+
77
+ /** Authenticator key ratchet label */
78
+ export const LABEL_AUTH_UPDATE = "Signal_PQCKA_V1_MLKEM768:Authenticator Update";
79
+
80
+ /** Ciphertext MAC label */
81
+ export const LABEL_CT_MAC = "Signal_PQCKA_V1_MLKEM768:ciphertext";
82
+
83
+ /** Header MAC label */
84
+ export const LABEL_HDR_MAC = "Signal_PQCKA_V1_MLKEM768:ekheader";
85
+
86
+ /** Epoch secret derivation label */
87
+ export const LABEL_SCKA_KEY = "Signal_PQCKA_V1_MLKEM768:SCKA Key";
88
+
89
+ /** 32-byte zero salt used in HKDF */
90
+ export const ZERO_SALT = new Uint8Array(32);