@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
package/src/error.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright © 2025 Signal Messenger, LLC
|
|
3
|
+
* Copyright © 2026 Parity Technologies
|
|
4
|
+
*
|
|
5
|
+
* Error types for the SPQR protocol.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export class SpqrError extends Error {
|
|
9
|
+
constructor(
|
|
10
|
+
message: string,
|
|
11
|
+
public readonly code: SpqrErrorCode,
|
|
12
|
+
public readonly detail?: unknown,
|
|
13
|
+
) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = "SpqrError";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export enum SpqrErrorCode {
|
|
20
|
+
StateDecode = "STATE_DECODE",
|
|
21
|
+
NotImplemented = "NOT_IMPLEMENTED",
|
|
22
|
+
MsgDecode = "MSG_DECODE",
|
|
23
|
+
MacVerifyFailed = "MAC_VERIFY_FAILED",
|
|
24
|
+
EpochOutOfRange = "EPOCH_OUT_OF_RANGE",
|
|
25
|
+
EncodingDecoding = "ENCODING_DECODING",
|
|
26
|
+
Serialization = "SERIALIZATION",
|
|
27
|
+
VersionMismatch = "VERSION_MISMATCH",
|
|
28
|
+
MinimumVersion = "MINIMUM_VERSION",
|
|
29
|
+
KeyJump = "KEY_JUMP",
|
|
30
|
+
KeyTrimmed = "KEY_TRIMMED",
|
|
31
|
+
KeyAlreadyRequested = "KEY_ALREADY_REQUESTED",
|
|
32
|
+
ErroneousDataReceived = "ERRONEOUS_DATA_RECEIVED",
|
|
33
|
+
SendKeyEpochDecreased = "SEND_KEY_EPOCH_DECREASED",
|
|
34
|
+
InvalidParams = "INVALID_PARAMS",
|
|
35
|
+
ChainNotAvailable = "CHAIN_NOT_AVAILABLE",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class EncodingError extends Error {
|
|
39
|
+
constructor(
|
|
40
|
+
message: string,
|
|
41
|
+
public readonly inner?: PolynomialError,
|
|
42
|
+
) {
|
|
43
|
+
super(message);
|
|
44
|
+
this.name = "EncodingError";
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export class PolynomialError extends Error {
|
|
49
|
+
constructor(
|
|
50
|
+
message: string,
|
|
51
|
+
public readonly code:
|
|
52
|
+
| "MESSAGE_LENGTH_EVEN"
|
|
53
|
+
| "MESSAGE_LENGTH_TOO_LONG"
|
|
54
|
+
| "SERIALIZATION_INVALID",
|
|
55
|
+
) {
|
|
56
|
+
super(message);
|
|
57
|
+
this.name = "PolynomialError";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export class AuthenticatorError extends Error {
|
|
62
|
+
constructor(
|
|
63
|
+
message: string,
|
|
64
|
+
public readonly code:
|
|
65
|
+
| "INVALID_CT_MAC"
|
|
66
|
+
| "INVALID_HDR_MAC"
|
|
67
|
+
| "ROOT_KEY_PRESENT"
|
|
68
|
+
| "ROOT_KEY_MISSING"
|
|
69
|
+
| "MAC_KEY_PRESENT"
|
|
70
|
+
| "MAC_KEY_MISSING",
|
|
71
|
+
) {
|
|
72
|
+
super(message);
|
|
73
|
+
this.name = "AuthenticatorError";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright © 2025 Signal Messenger, LLC
|
|
3
|
+
* Copyright © 2026 Parity Technologies
|
|
4
|
+
*
|
|
5
|
+
* True incremental ML-KEM-768 implementation.
|
|
6
|
+
*
|
|
7
|
+
* Implements the libcrux-compatible incremental encapsulation split:
|
|
8
|
+
*
|
|
9
|
+
* - generate(): Splits the ML-KEM-768 public key into:
|
|
10
|
+
* hdr (pk1, 64 bytes) = rho(32) + H(ek)(32) -- where H = SHA3-256
|
|
11
|
+
* ek (pk2, 1152 bytes) = ByteEncode12(tHat) -- the NTT vector
|
|
12
|
+
*
|
|
13
|
+
* - encaps1(hdr, rng): Uses only rho and H(ek) from the header to produce
|
|
14
|
+
* a REAL ct1 (960 bytes), shared secret (32 bytes), and
|
|
15
|
+
* encapsulation state (2080 bytes) for encaps2.
|
|
16
|
+
*
|
|
17
|
+
* - encaps2(ek, es): Completes the encapsulation using the tHat from pk2
|
|
18
|
+
* and the stored NTT randomness. Returns ct2 (128 bytes).
|
|
19
|
+
*
|
|
20
|
+
* - decaps(dk, ct1, ct2): Standard ML-KEM-768 decapsulation.
|
|
21
|
+
*
|
|
22
|
+
* Wire-compatible with Signal's Rust libcrux incremental implementation.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { sha3_256, sha3_512, shake256 } from "@noble/hashes/sha3.js";
|
|
26
|
+
import { u32 } from "@noble/hashes/utils.js";
|
|
27
|
+
import { genCrystals, XOF128 } from "@noble/post-quantum/_crystals.js";
|
|
28
|
+
import { ml_kem768 } from "@noble/post-quantum/ml-kem.js";
|
|
29
|
+
import type { RandomBytes } from "./types.js";
|
|
30
|
+
|
|
31
|
+
// ---- ML-KEM-768 constants ----
|
|
32
|
+
|
|
33
|
+
const N = 256;
|
|
34
|
+
const Q = 3329;
|
|
35
|
+
const F = 3303;
|
|
36
|
+
const ROOT_OF_UNITY = 17;
|
|
37
|
+
const K = 3;
|
|
38
|
+
const ETA1 = 2;
|
|
39
|
+
const ETA2 = 2;
|
|
40
|
+
const DU = 10;
|
|
41
|
+
const DV = 4;
|
|
42
|
+
|
|
43
|
+
// ---- Size constants ----
|
|
44
|
+
|
|
45
|
+
/** Size of the public key header: rho(32) + H(ek)(32) */
|
|
46
|
+
export const HEADER_SIZE = 64;
|
|
47
|
+
|
|
48
|
+
/** Size of the encapsulation key: ByteEncode12(tHat) = 3 * 384 = 1152 bytes */
|
|
49
|
+
export const EK_SIZE = 1152;
|
|
50
|
+
|
|
51
|
+
/** Size of the decapsulation (secret) key */
|
|
52
|
+
export const DK_SIZE = 2400;
|
|
53
|
+
|
|
54
|
+
/** Size of the first ciphertext fragment (ct[0..960]) */
|
|
55
|
+
export const CT1_SIZE = 960;
|
|
56
|
+
|
|
57
|
+
/** Size of the second ciphertext fragment (ct[960..1088]) */
|
|
58
|
+
export const CT2_SIZE = 128;
|
|
59
|
+
|
|
60
|
+
/** Size of the KEM shared secret */
|
|
61
|
+
export const SS_SIZE = 32;
|
|
62
|
+
|
|
63
|
+
/** Standard ML-KEM-768 full public key size */
|
|
64
|
+
export const FULL_PK_SIZE = 1184;
|
|
65
|
+
|
|
66
|
+
/** Standard ML-KEM-768 full ciphertext size */
|
|
67
|
+
export const FULL_CT_SIZE = 1088;
|
|
68
|
+
|
|
69
|
+
/** Size of the keygen seed */
|
|
70
|
+
export const KEYGEN_SEED_SIZE = 64;
|
|
71
|
+
|
|
72
|
+
/** Size of the encapsulation randomness (message m) */
|
|
73
|
+
export const ENCAPS_SEED_SIZE = 32;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Size of the encapsulation state:
|
|
77
|
+
* r_as_ntt: K(3) * N(256) * 2 = 1536 bytes
|
|
78
|
+
* error2: N(256) * 2 = 512 bytes
|
|
79
|
+
* randomness: 32 bytes
|
|
80
|
+
* Total: 2080 bytes
|
|
81
|
+
*/
|
|
82
|
+
export const ES_SIZE = 2080;
|
|
83
|
+
|
|
84
|
+
// ---- Initialize crystals NTT machinery ----
|
|
85
|
+
|
|
86
|
+
const { mod, nttZetas, NTT, bitsCoder } = genCrystals({
|
|
87
|
+
N,
|
|
88
|
+
Q,
|
|
89
|
+
F,
|
|
90
|
+
ROOT_OF_UNITY,
|
|
91
|
+
newPoly: (n: number): Uint16Array => new Uint16Array(n),
|
|
92
|
+
brvBits: 7,
|
|
93
|
+
isKyber: true,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// ---- Polynomial operations (copied from noble ml-kem.ts, closure-scoped) ----
|
|
97
|
+
|
|
98
|
+
type Poly = Uint16Array;
|
|
99
|
+
|
|
100
|
+
function polyAdd(a: Poly, b: Poly): void {
|
|
101
|
+
for (let i = 0; i < N; i++) a[i] = mod(a[i] + b[i]);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function BaseCaseMultiply(
|
|
105
|
+
a0: number,
|
|
106
|
+
a1: number,
|
|
107
|
+
b0: number,
|
|
108
|
+
b1: number,
|
|
109
|
+
zeta: number,
|
|
110
|
+
): { c0: number; c1: number } {
|
|
111
|
+
const c0 = mod(a1 * b1 * zeta + a0 * b0);
|
|
112
|
+
const c1 = mod(a0 * b1 + a1 * b0);
|
|
113
|
+
return { c0, c1 };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function MultiplyNTTs(f: Poly, g: Poly): Poly {
|
|
117
|
+
for (let i = 0; i < N / 2; i++) {
|
|
118
|
+
let z = nttZetas[64 + (i >> 1)];
|
|
119
|
+
if ((i & 1) !== 0) z = -z;
|
|
120
|
+
const { c0, c1 } = BaseCaseMultiply(f[2 * i + 0], f[2 * i + 1], g[2 * i + 0], g[2 * i + 1], z);
|
|
121
|
+
f[2 * i + 0] = c0;
|
|
122
|
+
f[2 * i + 1] = c1;
|
|
123
|
+
}
|
|
124
|
+
return f;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
type XofGet = ReturnType<ReturnType<typeof XOF128>["get"]>;
|
|
128
|
+
|
|
129
|
+
function SampleNTT(xof: XofGet): Poly {
|
|
130
|
+
const r: Poly = new Uint16Array(N);
|
|
131
|
+
for (let j = 0; j < N; ) {
|
|
132
|
+
const b = xof();
|
|
133
|
+
if (b.length % 3 !== 0) throw new Error("SampleNTT: unaligned block");
|
|
134
|
+
for (let i = 0; j < N && i + 3 <= b.length; i += 3) {
|
|
135
|
+
const d1 = ((b[i + 0] >> 0) | (b[i + 1] << 8)) & 0xfff;
|
|
136
|
+
const d2 = ((b[i + 1] >> 4) | (b[i + 2] << 4)) & 0xfff;
|
|
137
|
+
if (d1 < Q) r[j++] = d1;
|
|
138
|
+
if (j < N && d2 < Q) r[j++] = d2;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return r;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function sampleCBD(seed: Uint8Array, nonce: number, eta: number): Poly {
|
|
145
|
+
const len = (eta * N) / 4;
|
|
146
|
+
const buf = shake256
|
|
147
|
+
.create({ dkLen: len })
|
|
148
|
+
.update(seed)
|
|
149
|
+
.update(new Uint8Array([nonce]))
|
|
150
|
+
.digest();
|
|
151
|
+
const r: Poly = new Uint16Array(N);
|
|
152
|
+
const b32 = u32(buf);
|
|
153
|
+
let bitLen = 0;
|
|
154
|
+
let p = 0;
|
|
155
|
+
let bb = 0;
|
|
156
|
+
let t0 = 0;
|
|
157
|
+
for (let b of b32) {
|
|
158
|
+
for (let j = 0; j < 32; j++) {
|
|
159
|
+
bb += b & 1;
|
|
160
|
+
b >>= 1;
|
|
161
|
+
bitLen += 1;
|
|
162
|
+
if (bitLen === eta) {
|
|
163
|
+
t0 = bb;
|
|
164
|
+
bb = 0;
|
|
165
|
+
} else if (bitLen === 2 * eta) {
|
|
166
|
+
r[p++] = mod(t0 - bb);
|
|
167
|
+
bb = 0;
|
|
168
|
+
bitLen = 0;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (bitLen !== 0) throw new Error(`sampleCBD: leftover bits: ${bitLen}`);
|
|
173
|
+
return r;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ---- Compress/decompress coders ----
|
|
177
|
+
|
|
178
|
+
const compress = (d: number): { encode: (i: number) => number; decode: (i: number) => number } => {
|
|
179
|
+
if (d >= 12) return { encode: (i: number) => i, decode: (i: number) => i };
|
|
180
|
+
const a = 2 ** (d - 1);
|
|
181
|
+
return {
|
|
182
|
+
encode: (i: number) => ((i << d) + Q / 2) / Q,
|
|
183
|
+
decode: (i: number) => (i * Q + a) >>> d,
|
|
184
|
+
};
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const polyCoder = (d: number): ReturnType<typeof bitsCoder> => bitsCoder(d, compress(d));
|
|
188
|
+
|
|
189
|
+
// Coders for encoding/decoding polynomials
|
|
190
|
+
const poly12 = polyCoder(12); // for tHat encoding (ByteEncode12)
|
|
191
|
+
const polyDU = polyCoder(DU); // for u compression (du=10)
|
|
192
|
+
const polyDV = polyCoder(DV); // for v compression (dv=4)
|
|
193
|
+
const poly1 = polyCoder(1); // for message encoding (1-bit)
|
|
194
|
+
|
|
195
|
+
// ---- Key material ----
|
|
196
|
+
|
|
197
|
+
/** Generated ML-KEM-768 key material, split for incremental protocol */
|
|
198
|
+
export interface Keys {
|
|
199
|
+
/** Public key header: rho(32) + SHA3-256(full_pk)(32) */
|
|
200
|
+
hdr: Uint8Array;
|
|
201
|
+
/** Encapsulation key: ByteEncode12(tHat) = 1152 bytes */
|
|
202
|
+
ek: Uint8Array;
|
|
203
|
+
/** Decapsulation (secret) key: full 2400-byte ML-KEM-768 secret key */
|
|
204
|
+
dk: Uint8Array;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Result of encaps1: REAL ct1, shared secret, and encapsulation state */
|
|
208
|
+
export interface Encaps1Result {
|
|
209
|
+
/** REAL ct1 (960 bytes) */
|
|
210
|
+
ct1: Uint8Array;
|
|
211
|
+
/** Encapsulation state (2080 bytes): r_as_ntt(1536) + error2(512) + m(32) */
|
|
212
|
+
es: Uint8Array;
|
|
213
|
+
/** REAL shared secret (32 bytes) */
|
|
214
|
+
sharedSecret: Uint8Array;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ---- Encapsulation state encoding ----
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Encode the encapsulation state as 2080 bytes:
|
|
221
|
+
* r_as_ntt: K polys, each 256 coefficients as uint16 LE = 1536 bytes
|
|
222
|
+
* error2: 1 poly, 256 coefficients as uint16 LE = 512 bytes
|
|
223
|
+
* randomness: 32 bytes (the message m)
|
|
224
|
+
*/
|
|
225
|
+
function encodeState(rHat: Poly[], e2: Poly, m: Uint8Array): Uint8Array {
|
|
226
|
+
const state = new Uint8Array(ES_SIZE);
|
|
227
|
+
let offset = 0;
|
|
228
|
+
|
|
229
|
+
// r_as_ntt: K polynomials
|
|
230
|
+
for (let k = 0; k < K; k++) {
|
|
231
|
+
const poly = rHat[k];
|
|
232
|
+
for (let i = 0; i < N; i++) {
|
|
233
|
+
const val = poly[i];
|
|
234
|
+
state[offset++] = val & 0xff;
|
|
235
|
+
state[offset++] = (val >> 8) & 0xff;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// error2: 1 polynomial
|
|
240
|
+
for (let i = 0; i < N; i++) {
|
|
241
|
+
const val = e2[i];
|
|
242
|
+
state[offset++] = val & 0xff;
|
|
243
|
+
state[offset++] = (val >> 8) & 0xff;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// randomness (message m)
|
|
247
|
+
state.set(m, offset);
|
|
248
|
+
|
|
249
|
+
return state;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Decode the encapsulation state from 2080 bytes.
|
|
254
|
+
* Includes the Issue 1275 endianness workaround.
|
|
255
|
+
*/
|
|
256
|
+
function decodeState(state: Uint8Array): {
|
|
257
|
+
rHat: Poly[];
|
|
258
|
+
e2: Poly;
|
|
259
|
+
m: Uint8Array;
|
|
260
|
+
} {
|
|
261
|
+
// Apply Issue 1275 workaround
|
|
262
|
+
const fixedState = fixIssue1275(state);
|
|
263
|
+
const st = fixedState ?? state;
|
|
264
|
+
|
|
265
|
+
let offset = 0;
|
|
266
|
+
|
|
267
|
+
// r_as_ntt: K polynomials
|
|
268
|
+
const rHat: Poly[] = [];
|
|
269
|
+
for (let k = 0; k < K; k++) {
|
|
270
|
+
const poly = new Uint16Array(N);
|
|
271
|
+
for (let i = 0; i < N; i++) {
|
|
272
|
+
poly[i] = st[offset] | (st[offset + 1] << 8);
|
|
273
|
+
offset += 2;
|
|
274
|
+
}
|
|
275
|
+
rHat.push(poly);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// error2: 1 polynomial
|
|
279
|
+
const e2 = new Uint16Array(N);
|
|
280
|
+
for (let i = 0; i < N; i++) {
|
|
281
|
+
e2[i] = st[offset] | (st[offset + 1] << 8);
|
|
282
|
+
offset += 2;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// randomness (message m)
|
|
286
|
+
const m = st.slice(offset, offset + 32);
|
|
287
|
+
|
|
288
|
+
return { rHat, e2, m };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ---- Issue 1275 Endianness Workaround ----
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Port of Rust's potentially_fix_state_incorrectly_encoded_by_libcrux_issue_1275.
|
|
295
|
+
*
|
|
296
|
+
* Due to https://github.com/cryspen/libcrux/issues/1275, the encapsulation
|
|
297
|
+
* state may contain error2 coefficients with wrong endianness.
|
|
298
|
+
*
|
|
299
|
+
* Error2 values should be in [-2, 2] (ETA2=2 for ML-KEM-768).
|
|
300
|
+
* As uint16 LE, valid values are: 0x0000, 0x0001, 0x0002, 0xFFFF (-1), 0xFFFE (-2).
|
|
301
|
+
* Bad-endian equivalents: 0x0100, 0x0200, 0xFEFF.
|
|
302
|
+
*
|
|
303
|
+
* Returns a fixed copy if endianness is wrong, or null if state is OK.
|
|
304
|
+
*/
|
|
305
|
+
function fixIssue1275(es: Uint8Array): Uint8Array | null {
|
|
306
|
+
// error2 is at bytes [1536..2048] (after r_as_ntt, before randomness)
|
|
307
|
+
const E2_START = K * N * 2; // 1536
|
|
308
|
+
const E2_END = E2_START + N * 2; // 2048
|
|
309
|
+
|
|
310
|
+
for (let i = E2_START; i < E2_END; i += 2) {
|
|
311
|
+
const lo = es[i];
|
|
312
|
+
const hi = es[i + 1];
|
|
313
|
+
const val = lo | (hi << 8); // interpret as i16 LE
|
|
314
|
+
|
|
315
|
+
// 0x0000 and 0xFFFF have same representation in both endiannesses
|
|
316
|
+
if (val === 0x0000 || val === 0xffff) continue;
|
|
317
|
+
|
|
318
|
+
// Good LE values: 0x0001, 0x0002, 0xFFFE
|
|
319
|
+
if (val === 0x0001 || val === 0x0002 || val === 0xfffe) {
|
|
320
|
+
return null; // Already correct
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Bad (big-endian) values: 0x0100, 0x0200, 0xFEFF
|
|
324
|
+
if (val === 0x0100 || val === 0x0200 || val === 0xfeff) {
|
|
325
|
+
return flipEndianness(es);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Unknown value -- return null (use as-is)
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Flip the endianness of all i16 values in the encapsulation state.
|
|
337
|
+
* The last 32 bytes (randomness) are NOT flipped.
|
|
338
|
+
*/
|
|
339
|
+
function flipEndianness(es: Uint8Array): Uint8Array {
|
|
340
|
+
const fixed = new Uint8Array(es);
|
|
341
|
+
const coeffEnd = es.length - 32; // don't flip the last 32 bytes (randomness)
|
|
342
|
+
for (let i = 0; i < coeffEnd; i += 2) {
|
|
343
|
+
const tmp = fixed[i];
|
|
344
|
+
fixed[i] = fixed[i + 1];
|
|
345
|
+
fixed[i + 1] = tmp;
|
|
346
|
+
}
|
|
347
|
+
return fixed;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ---- Key generation ----
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Generate an ML-KEM-768 keypair and split the public key.
|
|
354
|
+
*
|
|
355
|
+
* The ML-KEM-768 public key (1184 bytes) is:
|
|
356
|
+
* tHat = pk[0..1152] (ByteEncode12 of NTT vector)
|
|
357
|
+
* rho = pk[1152..1184] (32-byte seed)
|
|
358
|
+
*
|
|
359
|
+
* The incremental split produces:
|
|
360
|
+
* hdr (pk1) = rho(32) + SHA3-256(pk)(32) = 64 bytes
|
|
361
|
+
* ek (pk2) = tHat(1152) = ByteEncode12(tHat) = 1152 bytes
|
|
362
|
+
*
|
|
363
|
+
* @param rng - Random byte generator; must provide at least 64 bytes
|
|
364
|
+
* @returns Split key material
|
|
365
|
+
*/
|
|
366
|
+
export function generate(rng: RandomBytes): Keys {
|
|
367
|
+
const seed = rng(KEYGEN_SEED_SIZE);
|
|
368
|
+
const { publicKey, secretKey } = ml_kem768.keygen(seed);
|
|
369
|
+
|
|
370
|
+
// Standard pk layout: tHat(1152) || rho(32)
|
|
371
|
+
const tHat = publicKey.slice(0, EK_SIZE); // 1152 bytes
|
|
372
|
+
const rho = publicKey.slice(EK_SIZE); // 32 bytes
|
|
373
|
+
const hEk = sha3_256(publicKey); // H(ek) = SHA3-256(full pk)
|
|
374
|
+
|
|
375
|
+
// pk1 (header) = rho(32) || H(ek)(32)
|
|
376
|
+
const hdr = new Uint8Array(HEADER_SIZE);
|
|
377
|
+
hdr.set(rho, 0);
|
|
378
|
+
hdr.set(hEk, 32);
|
|
379
|
+
|
|
380
|
+
return {
|
|
381
|
+
hdr,
|
|
382
|
+
ek: tHat,
|
|
383
|
+
dk: secretKey,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// ---- Encapsulation Phase 1 ----
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Phase 1 of true incremental encapsulation.
|
|
391
|
+
*
|
|
392
|
+
* Uses only rho and H(ek) from the 64-byte header to produce:
|
|
393
|
+
* - REAL ct1 (960 bytes): compress_du(u) where u = NTT^-1(A^T * rHat) + e1
|
|
394
|
+
* - REAL shared secret: K-hat from SHA3-512(m || H(ek))
|
|
395
|
+
* - Encapsulation state (2080 bytes): r_as_ntt + error2 + m
|
|
396
|
+
*
|
|
397
|
+
* @param hdr - The 64-byte header: rho(32) + H(ek)(32)
|
|
398
|
+
* @param rng - Random byte generator
|
|
399
|
+
* @returns Real ct1, encapsulation state, and real shared secret
|
|
400
|
+
*/
|
|
401
|
+
export function encaps1(hdr: Uint8Array, rng: RandomBytes): Encaps1Result {
|
|
402
|
+
const rho = hdr.slice(0, 32);
|
|
403
|
+
const hEk = hdr.slice(32, 64);
|
|
404
|
+
|
|
405
|
+
// Step 1: m = random 32 bytes
|
|
406
|
+
const m = rng(ENCAPS_SEED_SIZE);
|
|
407
|
+
|
|
408
|
+
// Step 2: kr = SHA3-512(m || H(ek)) -> K-hat(32) || r(32)
|
|
409
|
+
const kr = sha3_512.create().update(m).update(hEk).digest();
|
|
410
|
+
const kHat = kr.slice(0, 32);
|
|
411
|
+
const r = kr.slice(32, 64);
|
|
412
|
+
|
|
413
|
+
// Step 3: Sample rHat[i] = NTT(CBD(r, i, ETA1))
|
|
414
|
+
const rHat: Poly[] = [];
|
|
415
|
+
for (let i = 0; i < K; i++) {
|
|
416
|
+
rHat.push(NTT.encode(sampleCBD(r, i, ETA1)));
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Step 4: Generate A from rho via XOF128, compute u
|
|
420
|
+
const x = XOF128(rho);
|
|
421
|
+
const u: Poly[] = [];
|
|
422
|
+
for (let i = 0; i < K; i++) {
|
|
423
|
+
const e1 = sampleCBD(r, K + i, ETA2);
|
|
424
|
+
const tmp = new Uint16Array(N);
|
|
425
|
+
for (let j = 0; j < K; j++) {
|
|
426
|
+
const aij = SampleNTT(x.get(i, j));
|
|
427
|
+
polyAdd(tmp, MultiplyNTTs(aij, rHat[j].slice() as Poly));
|
|
428
|
+
}
|
|
429
|
+
polyAdd(e1, NTT.decode(tmp));
|
|
430
|
+
u.push(e1);
|
|
431
|
+
}
|
|
432
|
+
x.clean();
|
|
433
|
+
|
|
434
|
+
// Step 5: ct1 = compress_du(u) = encode each u[i] with du=10
|
|
435
|
+
const ct1 = new Uint8Array(CT1_SIZE);
|
|
436
|
+
for (let i = 0; i < K; i++) {
|
|
437
|
+
const encoded = polyDU.encode(u[i]);
|
|
438
|
+
ct1.set(encoded, i * polyDU.bytesLen);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Step 6: e2 = CBD(r, 2*K, ETA2)
|
|
442
|
+
const e2 = sampleCBD(r, 2 * K, ETA2);
|
|
443
|
+
|
|
444
|
+
// Step 7: Encode state
|
|
445
|
+
const es = encodeState(rHat, e2, m);
|
|
446
|
+
|
|
447
|
+
return {
|
|
448
|
+
ct1,
|
|
449
|
+
es,
|
|
450
|
+
sharedSecret: kHat,
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// ---- Encapsulation Phase 2 ----
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Phase 2 of true incremental encapsulation.
|
|
458
|
+
*
|
|
459
|
+
* Uses the tHat from pk2 (ek) and the stored NTT randomness to compute ct2.
|
|
460
|
+
*
|
|
461
|
+
* @param ek - The 1152-byte encapsulation key (ByteEncode12(tHat))
|
|
462
|
+
* @param es - The 2080-byte encapsulation state from encaps1
|
|
463
|
+
* @returns ct2 (128 bytes) ONLY
|
|
464
|
+
*/
|
|
465
|
+
export function encaps2(ek: Uint8Array, es: Uint8Array): Uint8Array {
|
|
466
|
+
const { rHat, e2, m } = decodeState(es);
|
|
467
|
+
|
|
468
|
+
// Decode tHat from ek (ByteDecode12)
|
|
469
|
+
const tHat: Poly[] = [];
|
|
470
|
+
for (let i = 0; i < K; i++) {
|
|
471
|
+
const slice = ek.subarray(i * poly12.bytesLen, (i + 1) * poly12.bytesLen);
|
|
472
|
+
tHat.push(poly12.decode(slice));
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Compute v = NTT^-1(sum(tHat[i] * rHat[i])) + e2 + Decompress1(m)
|
|
476
|
+
const tmp = new Uint16Array(N);
|
|
477
|
+
for (let i = 0; i < K; i++) {
|
|
478
|
+
polyAdd(tmp, MultiplyNTTs(tHat[i].slice() as Poly, rHat[i].slice() as Poly));
|
|
479
|
+
}
|
|
480
|
+
const v = NTT.decode(tmp);
|
|
481
|
+
polyAdd(v, e2);
|
|
482
|
+
|
|
483
|
+
// Decompress1(m) = decode m as 1-bit polynomial
|
|
484
|
+
const mPoly = poly1.decode(m);
|
|
485
|
+
polyAdd(v, mPoly);
|
|
486
|
+
|
|
487
|
+
// ct2 = compress_dv(v)
|
|
488
|
+
return polyDV.encode(v);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// ---- Decapsulation ----
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Decapsulate a split ciphertext using the decapsulation key.
|
|
495
|
+
*
|
|
496
|
+
* Concatenates ct1 (960 bytes) and ct2 (128 bytes) into a standard
|
|
497
|
+
* 1088-byte ML-KEM-768 ciphertext, then performs standard decapsulation.
|
|
498
|
+
*
|
|
499
|
+
* @param dk - The 2400-byte decapsulation (secret) key
|
|
500
|
+
* @param ct1 - First ciphertext fragment (960 bytes)
|
|
501
|
+
* @param ct2 - Second ciphertext fragment (128 bytes)
|
|
502
|
+
* @returns The 32-byte shared secret
|
|
503
|
+
*/
|
|
504
|
+
export function decaps(dk: Uint8Array, ct1: Uint8Array, ct2: Uint8Array): Uint8Array {
|
|
505
|
+
const ct = new Uint8Array(FULL_CT_SIZE);
|
|
506
|
+
ct.set(ct1.subarray(0, CT1_SIZE), 0);
|
|
507
|
+
ct.set(ct2.subarray(0, CT2_SIZE), CT1_SIZE);
|
|
508
|
+
return ml_kem768.decapsulate(ct, dk);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// ---- Validation ----
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Check whether an encapsulation key is consistent with a header.
|
|
515
|
+
*
|
|
516
|
+
* Reconstructs the full public key from pk2 (tHat) and pk1[0..32] (rho),
|
|
517
|
+
* computes SHA3-256 of the full pk, and compares with H(ek) in the header.
|
|
518
|
+
*
|
|
519
|
+
* @param ek - Encapsulation key (1152 bytes = ByteEncode12(tHat))
|
|
520
|
+
* @param hdr - Header (64 bytes = rho(32) + H(ek)(32))
|
|
521
|
+
* @returns true if ek is consistent with the header
|
|
522
|
+
*/
|
|
523
|
+
export function ekMatchesHeader(ek: Uint8Array, hdr: Uint8Array): boolean {
|
|
524
|
+
if (hdr.length < HEADER_SIZE || ek.length < EK_SIZE) {
|
|
525
|
+
return false;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const rho = hdr.slice(0, 32);
|
|
529
|
+
const expectedHash = hdr.slice(32, 64);
|
|
530
|
+
|
|
531
|
+
// Reconstruct full pk: tHat(1152) || rho(32)
|
|
532
|
+
const pk = new Uint8Array(FULL_PK_SIZE);
|
|
533
|
+
pk.set(ek.subarray(0, EK_SIZE), 0);
|
|
534
|
+
pk.set(rho, EK_SIZE);
|
|
535
|
+
|
|
536
|
+
// Compute H(pk) and compare
|
|
537
|
+
const actualHash = sha3_256(pk);
|
|
538
|
+
|
|
539
|
+
// Constant-time comparison
|
|
540
|
+
if (actualHash.length !== expectedHash.length) return false;
|
|
541
|
+
let diff = 0;
|
|
542
|
+
for (let i = 0; i < actualHash.length; i++) {
|
|
543
|
+
diff |= actualHash[i] ^ expectedHash[i];
|
|
544
|
+
}
|
|
545
|
+
return diff === 0;
|
|
546
|
+
}
|