zeropoint-node 1.0.4 → 1.0.5

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.
@@ -1,198 +0,0 @@
1
- /**
2
- * Kyber-768 shaped KEM: round-trip and structural tests.
3
- * This does NOT test FIPS 203 conformance — see the gaps listed in
4
- * kyber-real.ts. It tests that what is implemented is self-consistent.
5
- *
6
- * Two of these pin defects that shipped silently: 12-bit serialization dropped
7
- * the carry byte for every even coefficient, and message decode used a single
8
- * threshold where the ring requires a centred band. Both survived a passing
9
- * suite because nothing exercised them directly.
10
- */
11
-
12
- import {
13
- generateKeyPair,
14
- encapsulate,
15
- decapsulate,
16
- polynomialToBytes,
17
- bytesToPolynomial,
18
- messageToPoly,
19
- polyToMessage,
20
- type Polynomial,
21
- } from './kyber-real.ts'
22
- import { randomBytes } from 'node:crypto'
23
- import { floor } from '../0/algebra.ts'
24
-
25
- const KYBER_Q = 3329
26
- const KYBER_N = 256
27
-
28
- function testSerializationRoundTrip(): void {
29
- console.log('Test: 12-bit coefficient serialization round trip...')
30
-
31
- // Every coefficient must survive at every position. The carry bug hit only
32
- // EVEN indices and only values above 255, so a sparse probe walked past it.
33
- const poly = new Uint16Array(KYBER_N) as Polynomial
34
- for (let i = 0; i < KYBER_N; i++) poly[i] = (i * 13 + 1000) % KYBER_Q
35
- const back = bytesToPolynomial(Buffer.from(polynomialToBytes(poly)))
36
- for (let i = 0; i < KYBER_N; i++) {
37
- if (poly[i] !== back[i]) {
38
- throw new Error('coefficient ' + i + ' corrupted: wrote ' + poly[i] + ', read ' + back[i])
39
- }
40
- }
41
-
42
- // And the extremes, which is where twelve bits actually matters.
43
- const edge = new Uint16Array(KYBER_N) as Polynomial
44
- for (let i = 0; i < KYBER_N; i++) edge[i] = i % 2 === 0 ? KYBER_Q - 1 : 0
45
- const edgeBack = bytesToPolynomial(Buffer.from(polynomialToBytes(edge)))
46
- for (let i = 0; i < KYBER_N; i++) {
47
- if (edge[i] !== edgeBack[i]) {
48
- throw new Error('edge coefficient ' + i + ' corrupted: wrote ' + edge[i] + ', read ' + edgeBack[i])
49
- }
50
- }
51
-
52
- console.log(' ✓ 256 coefficients exact, both parities, including q-1')
53
- }
54
-
55
- function testMessageDecodeBand(): void {
56
- console.log('Test: message decode survives noise of either sign...')
57
-
58
- const msg = randomBytes(32)
59
- if (!polyToMessage(messageToPoly(msg)).equals(msg)) {
60
- throw new Error('message does not survive a noiseless round trip')
61
- }
62
-
63
- // The decode boundary sits q/4 from each ideal point, so anything strictly
64
- // inside must decode correctly — including NEGATIVE noise on a 0 bit, which
65
- // wraps to just under q. That wrap is what a single mid-point threshold
66
- // misreads as a 1.
67
- const limit = floor(KYBER_Q / 4) - 1
68
- for (const noise of [-limit, -100, -1, 0, 1, 100, limit]) {
69
- const poly = messageToPoly(msg)
70
- for (let i = 0; i < KYBER_N; i++) {
71
- poly[i] = (((poly[i]! + noise) % KYBER_Q) + KYBER_Q) % KYBER_Q
72
- }
73
- if (!polyToMessage(poly).equals(msg)) {
74
- throw new Error('decode failed at noise offset ' + noise)
75
- }
76
- }
77
-
78
- console.log(' ✓ recovered at every offset in ±' + limit + ' (boundary is q/4)')
79
- }
80
-
81
- function testKeyGeneration(): void {
82
- console.log('Test: Kyber-768 key pair generation...')
83
-
84
- const { publicKey, secretKey } = generateKeyPair()
85
-
86
- if (publicKey.length !== 1184) {
87
- throw new Error('Public key size mismatch: expected 1184, got ' + publicKey.length)
88
- }
89
- if (secretKey.length !== 2400) {
90
- throw new Error('Secret key size mismatch: expected 2400, got ' + secretKey.length)
91
- }
92
-
93
- console.log(' ✓ Public key: ' + publicKey.length + ' bytes')
94
- console.log(' ✓ Secret key: ' + secretKey.length + ' bytes')
95
- }
96
-
97
- function testEncapsulationDecapsulation(): void {
98
- console.log('Test: Kyber-768 encapsulation & decapsulation...')
99
-
100
- const { publicKey, secretKey } = generateKeyPair()
101
- const { ciphertext, sharedSecret: ss1 } = encapsulate(publicKey)
102
- const ss2 = decapsulate(secretKey, ciphertext)
103
-
104
- if (ciphertext.length !== 1088) {
105
- throw new Error('Ciphertext size mismatch: expected 1088, got ' + ciphertext.length)
106
- }
107
- if (ss1.length !== 32) throw new Error('Shared secret size mismatch: got ' + ss1.length)
108
- if (ss2.length !== 32) throw new Error('Decapsulated secret size mismatch: got ' + ss2.length)
109
-
110
- if (!ss1.equals(ss2)) {
111
- console.log(' ✗ Encapsulated SS: ' + ss1.toString('hex').slice(0, 32) + '...')
112
- console.log(' ✗ Decapsulated SS: ' + ss2.toString('hex').slice(0, 32) + '...')
113
- throw new Error('Encapsulation/decapsulation mismatch: shared secrets do not match')
114
- }
115
-
116
- console.log(' ✓ Ciphertext: ' + ciphertext.length + ' bytes')
117
- console.log(' ✓ Shared secrets match: ' + ss1.toString('hex').slice(0, 16) + '...')
118
- }
119
-
120
- function testMultipleRounds(): void {
121
- console.log('Test: Multiple encapsulation/decapsulation rounds...')
122
-
123
- const { publicKey, secretKey } = generateKeyPair()
124
- const secrets: Buffer[] = []
125
-
126
- // Decode failure is probabilistic, so a handful of rounds proves little.
127
- // 25 keeps the gate under a third of a second while actually sampling noise.
128
- for (let i = 0; i < 25; i++) {
129
- const { ciphertext, sharedSecret } = encapsulate(publicKey)
130
- const recovered = decapsulate(secretKey, ciphertext)
131
-
132
- if (!sharedSecret.equals(recovered)) {
133
- throw new Error('Round ' + (i + 1) + ': shared secrets do not match')
134
- }
135
-
136
- secrets.push(sharedSecret)
137
- }
138
-
139
- const ciphertexts = new Set<string>()
140
- for (let i = 0; i < 5; i++) {
141
- ciphertexts.add(encapsulate(publicKey).ciphertext.toString('hex'))
142
- }
143
- if (ciphertexts.size < 5) {
144
- throw new Error('encapsulation is not randomised: ' + ciphertexts.size + '/5 unique')
145
- }
146
-
147
- console.log(' ✓ Randomness: ' + ciphertexts.size + '/5 unique ciphertexts')
148
- console.log(' ✓ ' + secrets.length + ' rounds: all shared secrets recovered')
149
- }
150
-
151
- function testWrongKeyDoesNotRecover(): void {
152
- console.log('Test: an unrelated secret key does not recover the secret...')
153
-
154
- const alice = generateKeyPair()
155
- const mallory = generateKeyPair()
156
- const { ciphertext, sharedSecret } = encapsulate(alice.publicKey)
157
-
158
- if (decapsulate(mallory.secretKey, ciphertext).equals(sharedSecret)) {
159
- throw new Error('an unrelated secret key recovered the shared secret')
160
- }
161
-
162
- console.log(' ✓ unrelated key yields a different secret')
163
- }
164
-
165
- function testErrorHandling(): void {
166
- console.log('Test: Error handling...')
167
-
168
- let caught = false
169
- try {
170
- encapsulate(Buffer.alloc(100))
171
- } catch {
172
- caught = true
173
- }
174
- if (!caught) throw new Error('Should reject invalid public key size')
175
-
176
- console.log(' ✓ Invalid public key size rejected')
177
- }
178
-
179
- function runTests(): void {
180
- console.log('🔐 Kyber-768 KEM round-trip tests\n')
181
-
182
- try {
183
- testSerializationRoundTrip()
184
- testMessageDecodeBand()
185
- testKeyGeneration()
186
- testEncapsulationDecapsulation()
187
- testMultipleRounds()
188
- testWrongKeyDoesNotRecover()
189
- testErrorHandling()
190
-
191
- console.log('\n✅ Round trip and structure verified — NOT FIPS 203 conformant')
192
- } catch (error) {
193
- console.error('\n❌ Test failed: ' + (error instanceof Error ? error.message : String(error)))
194
- process.exit(1)
195
- }
196
- }
197
-
198
- runTests()
@@ -1,489 +0,0 @@
1
- /**
2
- * Kyber-768 shaped module-LWE KEM — CORRECT ROUND TRIP, NOT FIPS 203.
3
- *
4
- * encapsulate/decapsulate agree: 2000/2000 round trips recover the shared
5
- * secret, with a measured decode margin of 639 of the 832 available (worst
6
- * coefficient over 76 800 sampled). That is the property this file has.
7
- *
8
- * It is NOT ML-KEM and MUST NOT be used as if it were. Do not use it to
9
- * protect anything. Concretely, against FIPS 203:
10
- *
11
- * - The matrix A is sampled by CBD, so its coefficients are in {-1,0,1}
12
- * rather than uniform mod q. The spec samples A uniformly via SHAKE-128
13
- * rejection sampling. A small A means the module-LWE instance underneath
14
- * is not the hard problem the security argument rests on. This is the
15
- * single most important gap.
16
- * - Noise is CBD with eta = 1. ML-KEM-768 uses eta1 = eta2 = 2. The
17
- * KYBER_ETA constants below record the target, not what the sampler does.
18
- * - SHA-256 stands in for SHAKE-128/SHAKE-256 as XOF and PRF.
19
- * - Encapsulation omits the e1 and e2 error terms entirely.
20
- * - Decapsulation has no Fujisaki-Okamoto step: it never re-encrypts and
21
- * compares, and never uses the stored z, so there is no implicit
22
- * rejection and no IND-CCA2 claim. IND-CPA at best, and not that either
23
- * while A stays small.
24
- * - Arithmetic is schoolbook, not NTT. The math is right; the wire format
25
- * therefore differs from the spec's NTT-domain encoding.
26
- * - No NIST KAT vectors are checked, and nothing here interoperates with a
27
- * conforming implementation.
28
- *
29
- * Closing the first item is what would make this cryptography rather than
30
- * arithmetic that happens to round trip.
31
- */
32
-
33
- import { randomBytes, createHash } from 'node:crypto'
34
- import { abs, round, floor, sqrt } from '../0/algebra.ts'
35
-
36
- // ============================================================================
37
- // KYBER-768 PARAMETERS (NIST FIPS 203)
38
- // ============================================================================
39
-
40
- const KYBER_N = 256 // Polynomial degree
41
- const KYBER_Q = 3329 // Prime modulus
42
- const KYBER_K = 3 // Module dimension for Kyber-768
43
- // Target parameters for ML-KEM-768. The sampler below implements eta = 1 and
44
- // does not read these yet — see the conformance gaps at the top of the file.
45
- const KYBER_ETA1 = 2 // Noise parameter for key generation
46
- const KYBER_ETA2 = 2 // Noise parameter for encapsulation
47
- const KYBER_DU = 10 // Compression parameter for u
48
- const KYBER_DV = 4 // Compression parameter for v
49
- const KYBER_PUBLIC_KEY_SIZE = 1184 // (k * 384 + 32) = (3 * 384 + 32)
50
- const KYBER_SECRET_KEY_SIZE = 2400 // (k * 384 + 128 + 64)
51
- const KYBER_CIPHERTEXT_SIZE = 1088 // (du * k * 32 + dv * 32)
52
- const KYBER_SHARED_SECRET_SIZE = 32
53
-
54
- // ============================================================================
55
- // POLYNOMIAL ARITHMETIC (mod Q)
56
- // ============================================================================
57
-
58
- export type Polynomial = Uint16Array<ArrayBuffer> // 256 coefficients mod 3329
59
-
60
- // Create polynomial from bytes using CBD sampling
61
- export function polyFromBytes(seed: Buffer, nonce: number): Polynomial {
62
- const poly = new Uint16Array(KYBER_N)
63
-
64
- // Expand seed to 64 bytes (need 512 bits for 256 coefficients with eta=2)
65
- const shake = createHash('sha256')
66
- shake.update(seed)
67
- shake.update(Buffer.from([nonce]))
68
- let bytes = shake.digest()
69
-
70
- // For 256 coefficients with 2 bits each, we need 64 bytes
71
- if (bytes.length < 64) {
72
- // Expand by hashing again with incremented nonce
73
- const shake2 = createHash('sha256')
74
- shake2.update(seed)
75
- shake2.update(Buffer.from([nonce + 256]))
76
- bytes = Buffer.concat([bytes, shake2.digest()])
77
- }
78
-
79
- // Centered binomial distribution with eta = 1: one bit for a, one for b,
80
- // so coefficients land in {-1, 0, 1}. ML-KEM-768 wants eta = 2.
81
- for (let i = 0; i < KYBER_N; i++) {
82
- const byte_idx = floor((i * 2) / 8)
83
- const bit_offset = (i * 2) % 8
84
-
85
- const a = (bytes[byte_idx] >> bit_offset) & 1
86
- const b = (bytes[byte_idx] >> (bit_offset + 1)) & 1
87
- poly[i] = (a - b + KYBER_Q) % KYBER_Q
88
- }
89
-
90
- return poly
91
- }
92
-
93
- // Polynomial addition mod Q
94
- export function polyAdd(a: Polynomial, b: Polynomial): Polynomial {
95
- const result = new Uint16Array(KYBER_N)
96
- for (let i = 0; i < KYBER_N; i++) {
97
- result[i] = (a[i] + b[i]) % KYBER_Q
98
- }
99
- return result
100
- }
101
-
102
- // Polynomial multiplication: a * b in Z_Q[x] / (x^256 + 1)
103
- // Using schoolbook multiplication (simpler, correct, not optimized)
104
- export function polyMultiply(a: Polynomial, b: Polynomial): Polynomial {
105
- // Result: c[i] = sum_{j=0}^{255} a[j] * b[(i-j) mod 256]
106
- // But mod (x^256 + 1), so x^256 ≡ -1
107
- // This means c[i] = sum_{j=0}^{i} a[j]*b[i-j] - sum_{j=i+1}^{255} a[j]*b[i+256-j]
108
-
109
- const c = new Uint16Array(KYBER_N)
110
-
111
- for (let i = 0; i < KYBER_N; i++) {
112
- let acc = 0n
113
-
114
- // Regular part: j from 0 to i
115
- for (let j = 0; j <= i; j++) {
116
- acc += BigInt(a[j]) * BigInt(b[i - j])
117
- }
118
-
119
- // Wrapped part (negative due to x^256 = -1): j from i+1 to 255
120
- for (let j = i + 1; j < KYBER_N; j++) {
121
- acc -= BigInt(a[j]) * BigInt(b[KYBER_N + i - j])
122
- }
123
-
124
- c[i] = Number(((acc % BigInt(KYBER_Q)) + BigInt(KYBER_Q)) % BigInt(KYBER_Q))
125
- }
126
-
127
- return c
128
- }
129
-
130
- // ============================================================================
131
- // KYBER-768 KEY GENERATION
132
- // ============================================================================
133
-
134
- export interface KyberKeyPair {
135
- readonly publicKey: Buffer
136
- readonly secretKey: Buffer
137
- }
138
-
139
- export function generateKeyPair(): KyberKeyPair {
140
- const d = randomBytes(32) // Seed for pseudorandom generation
141
- const z = randomBytes(32) // Decapsulation seed
142
-
143
- // Generate matrix A and secret vectors s, e
144
- const A: Polynomial[][] = []
145
- for (let i = 0; i < KYBER_K; i++) {
146
- A[i] = []
147
- for (let j = 0; j < KYBER_K; j++) {
148
- A[i]![j] = polyFromBytes(d, i * KYBER_K + j)
149
- }
150
- }
151
-
152
- const seed_se = randomBytes(64)
153
- const s: Polynomial[] = []
154
- const e: Polynomial[] = []
155
-
156
- for (let i = 0; i < KYBER_K; i++) {
157
- s[i] = polyFromBytes(seed_se.slice(0, 32), i)
158
- e[i] = polyFromBytes(seed_se.slice(32), i)
159
- }
160
-
161
- // Compute t = A * s + e
162
- const t: Polynomial[] = []
163
- for (let i = 0; i < KYBER_K; i++) {
164
- let ti = new Uint16Array(KYBER_N)
165
- for (let j = 0; j < KYBER_K; j++) {
166
- const prod = polyMultiply(A[i]![j]!, s[j]!)
167
- ti = polyAdd(ti, prod)
168
- }
169
- t[i] = polyAdd(ti, e[i]!)
170
- }
171
-
172
- // Encode public key: t || seed
173
- const publicKey = Buffer.concat([
174
- Buffer.from(polynomialsToBytes(t)),
175
- d,
176
- ])
177
-
178
- // Encode secret key: s || e || d || z || pk_hash (NIST FIPS 203)
179
- const secretKey = Buffer.concat([
180
- Buffer.from(polynomialsToBytes(s)),
181
- Buffer.from(polynomialsToBytes(e)),
182
- d,
183
- z,
184
- createHash('sha256').update(publicKey).digest(),
185
- ])
186
-
187
- return { publicKey, secretKey }
188
- }
189
-
190
- // ============================================================================
191
- // KYBER-768 ENCAPSULATION
192
- // ============================================================================
193
-
194
- export interface Encapsulation {
195
- readonly ciphertext: Buffer
196
- readonly sharedSecret: Buffer
197
- }
198
-
199
- export function encapsulate(publicKey: Buffer): Encapsulation {
200
- if (publicKey.length !== KYBER_PUBLIC_KEY_SIZE) {
201
- throw new Error(`Invalid public key size: ${publicKey.length}`)
202
- }
203
-
204
- // Extract t from public key
205
- const t_bytes = publicKey.slice(0, KYBER_PUBLIC_KEY_SIZE - 32)
206
- const seed = publicKey.slice(KYBER_PUBLIC_KEY_SIZE - 32)
207
- const t = bytesToPolynomials(t_bytes, KYBER_K)
208
-
209
- // Sample random message
210
- const m = randomBytes(32)
211
-
212
- // Encode message to polynomial
213
- const r_seed = createHash('sha256')
214
- .update(m)
215
- .update(createHash('sha256').update(publicKey).digest())
216
- .digest()
217
-
218
- const r: Polynomial[] = []
219
- for (let i = 0; i < KYBER_K; i++) {
220
- r[i] = polyFromBytes(r_seed, i)
221
- }
222
-
223
- // Compute u = A^T * r + e1 and v = t^T * r + e2 + msg
224
- const u: Polynomial[] = []
225
- const A: Polynomial[][] = []
226
- for (let i = 0; i < KYBER_K; i++) {
227
- A[i] = []
228
- for (let j = 0; j < KYBER_K; j++) {
229
- A[i]![j] = polyFromBytes(seed, i * KYBER_K + j)
230
- }
231
- }
232
-
233
- for (let i = 0; i < KYBER_K; i++) {
234
- let ui = new Uint16Array(KYBER_N)
235
- for (let j = 0; j < KYBER_K; j++) {
236
- const prod = polyMultiply(A[j]![i]!, r[j]!)
237
- ui = polyAdd(ui, prod)
238
- }
239
- u[i] = ui
240
- }
241
-
242
- let v = new Uint16Array(KYBER_N)
243
- for (let i = 0; i < KYBER_K; i++) {
244
- const prod = polyMultiply(t[i]!, r[i]!)
245
- v = polyAdd(v, prod)
246
- }
247
-
248
- // Add message to v
249
- const msg_poly = messageToPoly(m)
250
- v = polyAdd(v, msg_poly)
251
-
252
- // Compress u and v
253
- const c1 = compressPolynomials(u, KYBER_DU)
254
- const c2 = compressPolynomial(v, KYBER_DV)
255
-
256
- const ciphertext = Buffer.concat([c1, c2])
257
-
258
- // Derive shared secret
259
- const sharedSecret = createHash('sha256')
260
- .update(m)
261
- .update(ciphertext)
262
- .digest()
263
-
264
- return { ciphertext, sharedSecret }
265
- }
266
-
267
- // ============================================================================
268
- // KYBER-768 DECAPSULATION
269
- // ============================================================================
270
-
271
- export function decapsulate(secretKey: Buffer, ciphertext: Buffer): Buffer {
272
- if (secretKey.length !== KYBER_SECRET_KEY_SIZE) {
273
- throw new Error(`Invalid secret key size: ${secretKey.length}`)
274
- }
275
- if (ciphertext.length !== KYBER_CIPHERTEXT_SIZE) {
276
- throw new Error(`Invalid ciphertext size: ${ciphertext.length}`)
277
- }
278
-
279
- // Extract s from secret key (first k*384 bytes)
280
- const s_bytes = secretKey.slice(0, KYBER_K * 384)
281
- const s = bytesToPolynomials(s_bytes, KYBER_K)
282
-
283
- // Note: e, d, z, and pk_hash follow s in the secret key but aren't needed for basic decapsulation
284
- // z would be used for implicit rejection (FO transform variant)
285
-
286
- // Decompress u and v from ciphertext
287
- const c1_size = KYBER_DU * KYBER_K * 32
288
- const u = decompressPolynomials(ciphertext.slice(0, c1_size), KYBER_K, KYBER_DU)
289
- const v = decompressPolynomial(ciphertext.slice(c1_size), KYBER_DV)
290
-
291
- // Recover message: m' = v - s^T * u
292
- let m_prime = new Uint16Array(v)
293
- for (let i = 0; i < KYBER_K; i++) {
294
- const prod = polyMultiply(s[i]!, u[i]!)
295
- m_prime = polySubtract(m_prime, prod)
296
- }
297
-
298
- const m = polyToMessage(m_prime)
299
-
300
- // Derive shared secret
301
- const sharedSecret = createHash('sha256')
302
- .update(m)
303
- .update(ciphertext)
304
- .digest()
305
-
306
- return sharedSecret
307
- }
308
-
309
- // ============================================================================
310
- // HELPER FUNCTIONS
311
- // ============================================================================
312
-
313
- function polynomialsToBytes(polys: Polynomial[]): Uint8Array {
314
- const bytes = new Uint8Array(polys.length * 384)
315
- for (let i = 0; i < polys.length; i++) {
316
- const poly_bytes = polynomialToBytes(polys[i]!)
317
- bytes.set(poly_bytes, i * 384)
318
- }
319
- return bytes
320
- }
321
-
322
- export function polynomialToBytes(poly: Polynomial): Uint8Array {
323
- const bytes = new Uint8Array(384)
324
- for (let i = 0; i < KYBER_N; i++) {
325
- const idx = floor((i * 12) / 8)
326
- const shift = ((i * 12) % 8)
327
- const val = poly[i] & ((1 << 12) - 1) // 12-bit encoding
328
- bytes[idx] = (bytes[idx] | (val << shift)) & 0xff
329
- // The carry byte is ALWAYS needed. A 12-bit value never fits in one byte,
330
- // so at shift 0 the top four bits belong to idx+1 just as much as at
331
- // shift 4 — guarding this on `shift > 0` silently truncated every
332
- // even-indexed coefficient to its low byte, which is 118 of 256 for a
333
- // typical polynomial. The reader was always correct; only the writer lost
334
- // the bits, so the corruption only showed up after a serialize round trip.
335
- bytes[idx + 1] = (bytes[idx + 1] | (val >> (8 - shift))) & 0xff
336
- }
337
- return bytes
338
- }
339
-
340
- function bytesToPolynomials(bytes: Buffer, count: number): Polynomial[] {
341
- const polys: Polynomial[] = []
342
- for (let i = 0; i < count; i++) {
343
- polys[i] = bytesToPolynomial(bytes.slice(i * 384, (i + 1) * 384))
344
- }
345
- return polys
346
- }
347
-
348
- export function bytesToPolynomial(bytes: Buffer): Polynomial {
349
- const poly = new Uint16Array(KYBER_N)
350
- for (let i = 0; i < KYBER_N; i++) {
351
- const idx = floor((i * 12) / 8)
352
- const shift = (i * 12) % 8
353
- poly[i] = ((bytes[idx] >> shift) | (bytes[idx + 1] << (8 - shift))) & ((1 << 12) - 1)
354
- }
355
- return poly
356
- }
357
-
358
- export function messageToPoly(msg: Buffer): Polynomial {
359
- const poly = new Uint16Array(KYBER_N)
360
- for (let i = 0; i < 32; i++) {
361
- const byte = msg[i]!
362
- for (let j = 0; j < 8; j++) {
363
- if ((byte >> j) & 1) {
364
- poly[i * 8 + j] = floor(KYBER_Q / 2)
365
- }
366
- }
367
- }
368
- return poly
369
- }
370
-
371
- export function polyToMessage(poly: Polynomial): Buffer {
372
- const msg = Buffer.alloc(32)
373
- for (let i = 0; i < 32; i++) {
374
- for (let j = 0; j < 8; j++) {
375
- // A coefficient carries a 1 when it lies NEARER q/2 than 0. The ring
376
- // wraps, so this is a band [q/4, 3q/4), never a single threshold: a
377
- // 0-bit nudged negative by noise lands at q-5, and `>= q/2` reads that
378
- // as 1. Half the bits flipped on noise sign alone.
379
- //
380
- // Written as 4c ∈ [q, 3q) so it stays integer — no division, no float.
381
- const c4 = poly[i * 8 + j]! * 4
382
- if (c4 >= KYBER_Q && c4 < 3 * KYBER_Q) {
383
- msg[i] = msg[i]! | (1 << j)
384
- }
385
- }
386
- }
387
- return msg
388
- }
389
-
390
- /**
391
- * Compress / decompress a single coefficient, FIPS 203 §4.2.1.
392
- *
393
- * Compress_d(x) = round(x · 2^d / q) mod 2^d
394
- * Decompress_d(y) = round(y · q / 2^d)
395
- *
396
- * Both were `floor` against a scale of 2^d − 1. Two floors in series bias the
397
- * result the same way every time, and at d = 4 that bias is q/(2·15) ≈ 111 —
398
- * which was the measured MEDIAN decode error, against a boundary of q/4 = 832.
399
- * Rounding to the spec's 2^d scale centres the error and halves it, so the
400
- * noise budget pays for noise rather than for a constant.
401
- *
402
- * The `mod 2^d` on compress is load-bearing: q−1 rounds up to 2^d and must
403
- * wrap to 0, because q−1 is −1 in the centred ring and belongs beside 0.
404
- */
405
- function compressCoefficient(x: number, d: number): number {
406
- const scale = 1 << d
407
- return floor((x * scale + floor(KYBER_Q / 2)) / KYBER_Q) & (scale - 1)
408
- }
409
-
410
- function decompressCoefficient(y: number, d: number): number {
411
- const scale = 1 << d
412
- return floor((y * KYBER_Q + scale / 2) / scale)
413
- }
414
-
415
- function compressPolynomials(polys: Polynomial[], d: number): Buffer {
416
- const bytes = Buffer.alloc(polys.length * KYBER_N * d / 8)
417
- let bit_idx = 0
418
- for (const poly of polys) {
419
- for (let i = 0; i < KYBER_N; i++) {
420
- const compressed = compressCoefficient(poly[i]!, d)
421
- for (let j = 0; j < d; j++) {
422
- if ((compressed >> j) & 1) {
423
- bytes[floor(bit_idx / 8)] |= 1 << (bit_idx % 8)
424
- }
425
- bit_idx++
426
- }
427
- }
428
- }
429
- return bytes
430
- }
431
-
432
- function compressPolynomial(poly: Polynomial, d: number): Buffer {
433
- const bytes = Buffer.alloc(KYBER_N * d / 8)
434
- let bit_idx = 0
435
- for (let i = 0; i < KYBER_N; i++) {
436
- const compressed = compressCoefficient(poly[i]!, d)
437
- for (let j = 0; j < d; j++) {
438
- if ((compressed >> j) & 1) {
439
- bytes[floor(bit_idx / 8)] |= 1 << (bit_idx % 8)
440
- }
441
- bit_idx++
442
- }
443
- }
444
- return bytes
445
- }
446
-
447
- function decompressPolynomials(bytes: Buffer, count: number, d: number): Polynomial[] {
448
- const polys: Polynomial[] = []
449
- let bit_idx = 0
450
- for (let p = 0; p < count; p++) {
451
- const poly = new Uint16Array(KYBER_N)
452
- for (let i = 0; i < KYBER_N; i++) {
453
- let compressed = 0
454
- for (let j = 0; j < d; j++) {
455
- if ((bytes[floor(bit_idx / 8)] >> (bit_idx % 8)) & 1) {
456
- compressed |= 1 << j
457
- }
458
- bit_idx++
459
- }
460
- poly[i] = decompressCoefficient(compressed, d)
461
- }
462
- polys[p] = poly
463
- }
464
- return polys
465
- }
466
-
467
- function decompressPolynomial(bytes: Buffer, d: number): Polynomial {
468
- const poly = new Uint16Array(KYBER_N)
469
- let bit_idx = 0
470
- for (let i = 0; i < KYBER_N; i++) {
471
- let compressed = 0
472
- for (let j = 0; j < d; j++) {
473
- if ((bytes[floor(bit_idx / 8)] >> (bit_idx % 8)) & 1) {
474
- compressed |= 1 << j
475
- }
476
- bit_idx++
477
- }
478
- poly[i] = decompressCoefficient(compressed, d)
479
- }
480
- return poly
481
- }
482
-
483
- function polySubtract(a: Polynomial, b: Polynomial): Polynomial {
484
- const result = new Uint16Array(KYBER_N)
485
- for (let i = 0; i < KYBER_N; i++) {
486
- result[i] = (a[i] - b[i] + KYBER_Q) % KYBER_Q
487
- }
488
- return result
489
- }