zeropoint-node 1.0.3 → 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.
- package/CHANGELOG.md +153 -1
- package/CITATION.cff +1 -1
- package/README.md +1 -1
- package/package.json +5 -2
- package/src/crypto/ml-kem-768-acvp.json +492 -0
- package/src/crypto/ml-kem.test.ts +140 -0
- package/src/crypto/ml-kem.ts +554 -0
- package/src/quantum/error-correction.ts +69 -16
- package/src/quantum/hybrid.ts +2 -2
- package/src/quantum/simulator.ts +19 -0
- package/src/quantum/tomography.ts +4 -4
- package/src/verification/lean-bridge.test.ts +109 -228
- package/src/verification/lean-bridge.ts +401 -153
- package/src/crypto/kyber-real.test.ts +0 -125
- package/src/crypto/kyber-real.ts +0 -492
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Real Kyber-768 Test: Actual NIST Implementation
|
|
3
|
-
*
|
|
4
|
-
* Tests polynomial arithmetic, NTT, key generation, encapsulation, decapsulation.
|
|
5
|
-
* NOT simulation - real cryptographic operations.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { generateKeyPair, encapsulate, decapsulate } from './kyber-real.ts'
|
|
9
|
-
|
|
10
|
-
function testKeyGeneration(): void {
|
|
11
|
-
console.log('Test: Kyber-768 key pair generation...')
|
|
12
|
-
|
|
13
|
-
const { publicKey, secretKey } = generateKeyPair()
|
|
14
|
-
|
|
15
|
-
if (publicKey.length !== 1184) {
|
|
16
|
-
throw new Error(`Public key size mismatch: expected 1184, got ${publicKey.length}`)
|
|
17
|
-
}
|
|
18
|
-
if (secretKey.length !== 2400) {
|
|
19
|
-
throw new Error(`Secret key size mismatch: expected 2400, got ${secretKey.length}`)
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
console.log(` ✓ Public key: ${publicKey.length} bytes`)
|
|
23
|
-
console.log(` ✓ Secret key: ${secretKey.length} bytes`)
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function testEncapsulationDecapsulation(): void {
|
|
27
|
-
console.log('Test: Kyber-768 encapsulation & decapsulation...')
|
|
28
|
-
|
|
29
|
-
const { publicKey, secretKey } = generateKeyPair()
|
|
30
|
-
const { ciphertext, sharedSecret: ss1 } = encapsulate(publicKey)
|
|
31
|
-
const ss2 = decapsulate(secretKey, ciphertext)
|
|
32
|
-
|
|
33
|
-
if (ciphertext.length !== 1088) {
|
|
34
|
-
throw new Error(`Ciphertext size mismatch: expected 1088, got ${ciphertext.length}`)
|
|
35
|
-
}
|
|
36
|
-
if (ss1.length !== 32) {
|
|
37
|
-
throw new Error(`Shared secret size mismatch: expected 32, got ${ss1.length}`)
|
|
38
|
-
}
|
|
39
|
-
if (ss2.length !== 32) {
|
|
40
|
-
throw new Error(`Decapsulated secret size mismatch: expected 32, got ${ss2.length}`)
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Both parties should derive same shared secret
|
|
44
|
-
if (!ss1.equals(ss2)) {
|
|
45
|
-
console.log(' ✗ Encapsulated SS: ' + ss1.toString('hex').slice(0, 32) + '...')
|
|
46
|
-
console.log(' ✗ Decapsulated SS: ' + ss2.toString('hex').slice(0, 32) + '...')
|
|
47
|
-
throw new Error('Encapsulation/decapsulation mismatch: shared secrets do not match')
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
console.log(` ✓ Ciphertext: ${ciphertext.length} bytes`)
|
|
51
|
-
console.log(` ✓ Encapsulated SS: ${ss1.toString('hex').slice(0, 16)}...`)
|
|
52
|
-
console.log(` ✓ Decapsulated SS: ${ss2.toString('hex').slice(0, 16)}...`)
|
|
53
|
-
console.log(` ✓ Shared secrets match: VERIFIED`)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function testMultipleRounds(): void {
|
|
57
|
-
console.log('Test: Multiple encapsulation/decapsulation rounds...')
|
|
58
|
-
|
|
59
|
-
const { publicKey, secretKey } = generateKeyPair()
|
|
60
|
-
const secrets: Buffer[] = []
|
|
61
|
-
|
|
62
|
-
for (let i = 0; i < 5; i++) {
|
|
63
|
-
const { ciphertext, sharedSecret } = encapsulate(publicKey)
|
|
64
|
-
const recovered = decapsulate(secretKey, ciphertext)
|
|
65
|
-
|
|
66
|
-
if (!sharedSecret.equals(recovered)) {
|
|
67
|
-
throw new Error(`Round ${i + 1}: shared secrets do not match`)
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
secrets.push(sharedSecret)
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Each encapsulation should produce different ciphertext (randomness)
|
|
74
|
-
const ciphertexts = new Set<string>()
|
|
75
|
-
for (let i = 0; i < 5; i++) {
|
|
76
|
-
const { ciphertext } = encapsulate(publicKey)
|
|
77
|
-
ciphertexts.add(ciphertext.toString('hex'))
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (ciphertexts.size < 3) {
|
|
81
|
-
console.warn(' ⚠ Warning: encapsulations not sufficiently random')
|
|
82
|
-
} else {
|
|
83
|
-
console.log(` ✓ Randomness verified: ${ciphertexts.size}/5 unique ciphertexts`)
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
console.log(` ✓ ${secrets.length} rounds: all shared secrets recovered`)
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function testErrorHandling(): void {
|
|
90
|
-
console.log('Test: Error handling...')
|
|
91
|
-
|
|
92
|
-
const { publicKey } = generateKeyPair()
|
|
93
|
-
const invalid_pk = Buffer.alloc(100) // Wrong size
|
|
94
|
-
|
|
95
|
-
let caught = false
|
|
96
|
-
try {
|
|
97
|
-
encapsulate(invalid_pk)
|
|
98
|
-
} catch (e) {
|
|
99
|
-
caught = true
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (!caught) {
|
|
103
|
-
throw new Error('Should reject invalid public key size')
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
console.log(' ✓ Invalid public key size rejected')
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
async function runTests(): Promise<void> {
|
|
110
|
-
console.log('🔐 Kyber-768 Real Implementation Tests\n')
|
|
111
|
-
|
|
112
|
-
try {
|
|
113
|
-
testKeyGeneration()
|
|
114
|
-
testEncapsulationDecapsulation()
|
|
115
|
-
testMultipleRounds()
|
|
116
|
-
testErrorHandling()
|
|
117
|
-
|
|
118
|
-
console.log('\n✅ All Kyber-768 tests passed! (NIST FIPS 203 compliant)')
|
|
119
|
-
} catch (error) {
|
|
120
|
-
console.error(`\n❌ Test failed: ${error instanceof Error ? error.message : String(error)}`)
|
|
121
|
-
process.exit(1)
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
runTests()
|
package/src/crypto/kyber-real.ts
DELETED
|
@@ -1,492 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Kyber-768: Real NIST FIPS 203 Implementation
|
|
3
|
-
*
|
|
4
|
-
* Proper polynomial arithmetic, NTT, noise sampling.
|
|
5
|
-
* NOT a toy version - actual cryptographic implementation.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { randomBytes, createHash } from 'node:crypto'
|
|
9
|
-
import { abs, round, floor, sqrt } from '../0/algebra.ts'
|
|
10
|
-
|
|
11
|
-
// ============================================================================
|
|
12
|
-
// KYBER-768 PARAMETERS (NIST FIPS 203)
|
|
13
|
-
// ============================================================================
|
|
14
|
-
|
|
15
|
-
const KYBER_N = 256 // Polynomial degree
|
|
16
|
-
const KYBER_Q = 3329 // Prime modulus
|
|
17
|
-
const KYBER_K = 3 // Module dimension for Kyber-768
|
|
18
|
-
const KYBER_ETA1 = 2 // Noise parameter for key generation
|
|
19
|
-
const KYBER_ETA2 = 1 // Noise parameter for encapsulation
|
|
20
|
-
const KYBER_DU = 10 // Compression parameter for u
|
|
21
|
-
const KYBER_DV = 4 // Compression parameter for v
|
|
22
|
-
const KYBER_PUBLIC_KEY_SIZE = 1184 // (k * 384 + 32) = (3 * 384 + 32)
|
|
23
|
-
const KYBER_SECRET_KEY_SIZE = 2400 // (k * 384 + 128 + 64)
|
|
24
|
-
const KYBER_CIPHERTEXT_SIZE = 1088 // (du * k * 32 + dv * 32)
|
|
25
|
-
const KYBER_SHARED_SECRET_SIZE = 32
|
|
26
|
-
|
|
27
|
-
// ============================================================================
|
|
28
|
-
// POLYNOMIAL ARITHMETIC (mod Q)
|
|
29
|
-
// ============================================================================
|
|
30
|
-
|
|
31
|
-
export type Polynomial = Uint16Array<ArrayBuffer> // 256 coefficients mod 3329
|
|
32
|
-
|
|
33
|
-
// Create polynomial from bytes using CBD sampling
|
|
34
|
-
export function polyFromBytes(seed: Buffer, nonce: number): Polynomial {
|
|
35
|
-
const poly = new Uint16Array(KYBER_N)
|
|
36
|
-
|
|
37
|
-
// Expand seed to 64 bytes (need 512 bits for 256 coefficients with eta=2)
|
|
38
|
-
const shake = createHash('sha256')
|
|
39
|
-
shake.update(seed)
|
|
40
|
-
shake.update(Buffer.from([nonce]))
|
|
41
|
-
let bytes = shake.digest()
|
|
42
|
-
|
|
43
|
-
// For 256 coefficients with 2 bits each, we need 64 bytes
|
|
44
|
-
if (bytes.length < 64) {
|
|
45
|
-
// Expand by hashing again with incremented nonce
|
|
46
|
-
const shake2 = createHash('sha256')
|
|
47
|
-
shake2.update(seed)
|
|
48
|
-
shake2.update(Buffer.from([nonce + 256]))
|
|
49
|
-
bytes = Buffer.concat([bytes, shake2.digest()])
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Centered binomial distribution: sample from {-KYBER_ETA1, ..., +KYBER_ETA1}
|
|
53
|
-
for (let i = 0; i < KYBER_N; i++) {
|
|
54
|
-
const byte_idx = floor((i * 2) / 8)
|
|
55
|
-
const bit_offset = (i * 2) % 8
|
|
56
|
-
|
|
57
|
-
const a = (bytes[byte_idx] >> bit_offset) & 1
|
|
58
|
-
const b = (bytes[byte_idx] >> (bit_offset + 1)) & 1
|
|
59
|
-
poly[i] = (a - b + KYBER_Q) % KYBER_Q
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return poly
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Polynomial addition mod Q
|
|
66
|
-
export function polyAdd(a: Polynomial, b: Polynomial): Polynomial {
|
|
67
|
-
const result = new Uint16Array(KYBER_N)
|
|
68
|
-
for (let i = 0; i < KYBER_N; i++) {
|
|
69
|
-
result[i] = (a[i] + b[i]) % KYBER_Q
|
|
70
|
-
}
|
|
71
|
-
return result
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Polynomial multiplication via NTT (Number Theoretic Transform)
|
|
75
|
-
export function polyMultiply(a: Polynomial, b: Polynomial): Polynomial {
|
|
76
|
-
const aNTT = ntt(a)
|
|
77
|
-
const bNTT = ntt(b)
|
|
78
|
-
|
|
79
|
-
const cNTT = new Uint16Array(KYBER_N)
|
|
80
|
-
for (let i = 0; i < KYBER_N; i++) {
|
|
81
|
-
cNTT[i] = (aNTT[i] * bNTT[i]) % KYBER_Q
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return inverseNTT(cNTT)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Number Theoretic Transform (NTT)
|
|
88
|
-
function ntt(poly: Polynomial): Polynomial {
|
|
89
|
-
const result = new Uint16Array(poly)
|
|
90
|
-
const zeta = 17 // Primitive root of unity modulo Q
|
|
91
|
-
|
|
92
|
-
for (let len = 128; len >= 1; len >>>= 1) {
|
|
93
|
-
for (let start = 0; start < KYBER_N; start += 2 * len) {
|
|
94
|
-
const zeta_pow = modExp(zeta, start / (2 * len), KYBER_Q)
|
|
95
|
-
|
|
96
|
-
for (let i = start; i < start + len; i++) {
|
|
97
|
-
const t = (result[i + len] * zeta_pow) % KYBER_Q
|
|
98
|
-
result[i + len] = (result[i] - t + KYBER_Q) % KYBER_Q
|
|
99
|
-
result[i] = (result[i] + t) % KYBER_Q
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return result
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Inverse NTT
|
|
108
|
-
function inverseNTT(poly: Polynomial): Polynomial {
|
|
109
|
-
const result = new Uint16Array(poly)
|
|
110
|
-
const inv = modInverse(KYBER_N, KYBER_Q)
|
|
111
|
-
|
|
112
|
-
for (let len = 1; len < KYBER_N; len <<= 1) {
|
|
113
|
-
for (let start = 0; start < KYBER_N; start += 2 * len) {
|
|
114
|
-
const zeta_pow = modExp(17, -(start / len + 1), KYBER_Q)
|
|
115
|
-
|
|
116
|
-
for (let i = start; i < start + len; i++) {
|
|
117
|
-
const t = (result[i + len] * zeta_pow) % KYBER_Q
|
|
118
|
-
result[i + len] = (result[i] - t + KYBER_Q) % KYBER_Q
|
|
119
|
-
result[i] = (result[i] + t) % KYBER_Q
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
for (let i = 0; i < KYBER_N; i++) {
|
|
125
|
-
result[i] = (result[i] * inv) % KYBER_Q
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return result
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Modular exponentiation
|
|
132
|
-
function modExp(base: number, exp: number, mod: number): number {
|
|
133
|
-
if (exp < 0) {
|
|
134
|
-
// For negative exponent: compute base^exp = (base^(-exp))^(-1)
|
|
135
|
-
// Using Fermat's little theorem: a^(-1) ≡ a^(p-2) mod p
|
|
136
|
-
const pos_exp = modExp(base, -exp, mod)
|
|
137
|
-
return modExp(pos_exp, mod - 2, mod)
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
let result = 1
|
|
141
|
-
base = base % mod
|
|
142
|
-
|
|
143
|
-
while (exp > 0) {
|
|
144
|
-
if (exp % 2 === 1) {
|
|
145
|
-
result = (result * base) % mod
|
|
146
|
-
}
|
|
147
|
-
exp = floor(exp / 2)
|
|
148
|
-
base = (base * base) % mod
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
return result
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Modular inverse via extended Euclidean algorithm
|
|
155
|
-
function modInverse(a: number, m: number): number {
|
|
156
|
-
let [old_r, r] = [a, m]
|
|
157
|
-
let [old_s, s] = [1, 0]
|
|
158
|
-
|
|
159
|
-
while (r !== 0) {
|
|
160
|
-
const quotient = floor(old_r / r)
|
|
161
|
-
;[old_r, r] = [r, old_r - quotient * r]
|
|
162
|
-
;[old_s, s] = [s, old_s - quotient * s]
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
return (old_s + m) % m
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// ============================================================================
|
|
169
|
-
// KYBER-768 KEY GENERATION
|
|
170
|
-
// ============================================================================
|
|
171
|
-
|
|
172
|
-
export interface KyberKeyPair {
|
|
173
|
-
readonly publicKey: Buffer
|
|
174
|
-
readonly secretKey: Buffer
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
export function generateKeyPair(): KyberKeyPair {
|
|
178
|
-
const d = randomBytes(32) // Seed for pseudorandom generation
|
|
179
|
-
const z = randomBytes(32) // Decapsulation seed
|
|
180
|
-
|
|
181
|
-
// Generate matrix A and secret vectors s, e
|
|
182
|
-
const A: Polynomial[][] = []
|
|
183
|
-
for (let i = 0; i < KYBER_K; i++) {
|
|
184
|
-
A[i] = []
|
|
185
|
-
for (let j = 0; j < KYBER_K; j++) {
|
|
186
|
-
A[i]![j] = polyFromBytes(d, i * KYBER_K + j)
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const seed_se = randomBytes(64)
|
|
191
|
-
const s: Polynomial[] = []
|
|
192
|
-
const e: Polynomial[] = []
|
|
193
|
-
|
|
194
|
-
for (let i = 0; i < KYBER_K; i++) {
|
|
195
|
-
s[i] = polyFromBytes(seed_se.slice(0, 32), i)
|
|
196
|
-
e[i] = polyFromBytes(seed_se.slice(32), i)
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// Compute t = A * s + e
|
|
200
|
-
const t: Polynomial[] = []
|
|
201
|
-
for (let i = 0; i < KYBER_K; i++) {
|
|
202
|
-
let ti = new Uint16Array(KYBER_N)
|
|
203
|
-
for (let j = 0; j < KYBER_K; j++) {
|
|
204
|
-
const prod = polyMultiply(A[i]![j]!, s[j]!)
|
|
205
|
-
ti = polyAdd(ti, prod)
|
|
206
|
-
}
|
|
207
|
-
t[i] = polyAdd(ti, e[i]!)
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// Encode public key: t || seed
|
|
211
|
-
const publicKey = Buffer.concat([
|
|
212
|
-
Buffer.from(polynomialsToBytes(t)),
|
|
213
|
-
d,
|
|
214
|
-
])
|
|
215
|
-
|
|
216
|
-
// Encode secret key: s || e || d || z || pk_hash (NIST FIPS 203)
|
|
217
|
-
const secretKey = Buffer.concat([
|
|
218
|
-
Buffer.from(polynomialsToBytes(s)),
|
|
219
|
-
Buffer.from(polynomialsToBytes(e)),
|
|
220
|
-
d,
|
|
221
|
-
z,
|
|
222
|
-
createHash('sha256').update(publicKey).digest(),
|
|
223
|
-
])
|
|
224
|
-
|
|
225
|
-
return { publicKey, secretKey }
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// ============================================================================
|
|
229
|
-
// KYBER-768 ENCAPSULATION
|
|
230
|
-
// ============================================================================
|
|
231
|
-
|
|
232
|
-
export interface Encapsulation {
|
|
233
|
-
readonly ciphertext: Buffer
|
|
234
|
-
readonly sharedSecret: Buffer
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
export function encapsulate(publicKey: Buffer): Encapsulation {
|
|
238
|
-
if (publicKey.length !== KYBER_PUBLIC_KEY_SIZE) {
|
|
239
|
-
throw new Error(`Invalid public key size: ${publicKey.length}`)
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// Extract t from public key
|
|
243
|
-
const t_bytes = publicKey.slice(0, KYBER_PUBLIC_KEY_SIZE - 32)
|
|
244
|
-
const seed = publicKey.slice(KYBER_PUBLIC_KEY_SIZE - 32)
|
|
245
|
-
const t = bytesToPolynomials(t_bytes, KYBER_K)
|
|
246
|
-
|
|
247
|
-
// Sample random message
|
|
248
|
-
const m = randomBytes(32)
|
|
249
|
-
|
|
250
|
-
// Encode message to polynomial
|
|
251
|
-
const r_seed = createHash('sha256')
|
|
252
|
-
.update(m)
|
|
253
|
-
.update(createHash('sha256').update(publicKey).digest())
|
|
254
|
-
.digest()
|
|
255
|
-
|
|
256
|
-
const r: Polynomial[] = []
|
|
257
|
-
for (let i = 0; i < KYBER_K; i++) {
|
|
258
|
-
r[i] = polyFromBytes(r_seed, i)
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// Compute u = A^T * r + e1 and v = t^T * r + e2 + msg
|
|
262
|
-
const u: Polynomial[] = []
|
|
263
|
-
const A: Polynomial[][] = []
|
|
264
|
-
for (let i = 0; i < KYBER_K; i++) {
|
|
265
|
-
A[i] = []
|
|
266
|
-
for (let j = 0; j < KYBER_K; j++) {
|
|
267
|
-
A[i]![j] = polyFromBytes(seed, i * KYBER_K + j)
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
for (let i = 0; i < KYBER_K; i++) {
|
|
272
|
-
let ui = new Uint16Array(KYBER_N)
|
|
273
|
-
for (let j = 0; j < KYBER_K; j++) {
|
|
274
|
-
const prod = polyMultiply(A[j]![i]!, r[j]!)
|
|
275
|
-
ui = polyAdd(ui, prod)
|
|
276
|
-
}
|
|
277
|
-
u[i] = ui
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
let v = new Uint16Array(KYBER_N)
|
|
281
|
-
for (let i = 0; i < KYBER_K; i++) {
|
|
282
|
-
const prod = polyMultiply(t[i]!, r[i]!)
|
|
283
|
-
v = polyAdd(v, prod)
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// Add message to v
|
|
287
|
-
const msg_poly = messageToPoly(m)
|
|
288
|
-
v = polyAdd(v, msg_poly)
|
|
289
|
-
|
|
290
|
-
// Compress u and v
|
|
291
|
-
const c1 = compressPolynomials(u, KYBER_DU)
|
|
292
|
-
const c2 = compressPolynomial(v, KYBER_DV)
|
|
293
|
-
|
|
294
|
-
const ciphertext = Buffer.concat([c1, c2])
|
|
295
|
-
|
|
296
|
-
// Derive shared secret
|
|
297
|
-
const sharedSecret = createHash('sha256')
|
|
298
|
-
.update(m)
|
|
299
|
-
.update(ciphertext)
|
|
300
|
-
.digest()
|
|
301
|
-
|
|
302
|
-
return { ciphertext, sharedSecret }
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// ============================================================================
|
|
306
|
-
// KYBER-768 DECAPSULATION
|
|
307
|
-
// ============================================================================
|
|
308
|
-
|
|
309
|
-
export function decapsulate(secretKey: Buffer, ciphertext: Buffer): Buffer {
|
|
310
|
-
if (secretKey.length !== KYBER_SECRET_KEY_SIZE) {
|
|
311
|
-
throw new Error(`Invalid secret key size: ${secretKey.length}`)
|
|
312
|
-
}
|
|
313
|
-
if (ciphertext.length !== KYBER_CIPHERTEXT_SIZE) {
|
|
314
|
-
throw new Error(`Invalid ciphertext size: ${ciphertext.length}`)
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// Extract s from secret key (first k*384 bytes)
|
|
318
|
-
const s_bytes = secretKey.slice(0, KYBER_K * 384)
|
|
319
|
-
const s = bytesToPolynomials(s_bytes, KYBER_K)
|
|
320
|
-
|
|
321
|
-
// Note: e, d, z, and pk_hash follow s in the secret key but aren't needed for basic decapsulation
|
|
322
|
-
// z would be used for implicit rejection (FO transform variant)
|
|
323
|
-
|
|
324
|
-
// Decompress u and v from ciphertext
|
|
325
|
-
const c1_size = KYBER_DU * KYBER_K * 32
|
|
326
|
-
const u = decompressPolynomials(ciphertext.slice(0, c1_size), KYBER_K, KYBER_DU)
|
|
327
|
-
const v = decompressPolynomial(ciphertext.slice(c1_size), KYBER_DV)
|
|
328
|
-
|
|
329
|
-
// Recover message: m' = v - s^T * u
|
|
330
|
-
let m_prime = new Uint16Array(v)
|
|
331
|
-
for (let i = 0; i < KYBER_K; i++) {
|
|
332
|
-
const prod = polyMultiply(s[i]!, u[i]!)
|
|
333
|
-
m_prime = polySubtract(m_prime, prod)
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
const m = polyToMessage(m_prime)
|
|
337
|
-
|
|
338
|
-
// Derive shared secret
|
|
339
|
-
const sharedSecret = createHash('sha256')
|
|
340
|
-
.update(m)
|
|
341
|
-
.update(ciphertext)
|
|
342
|
-
.digest()
|
|
343
|
-
|
|
344
|
-
return sharedSecret
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
// ============================================================================
|
|
348
|
-
// HELPER FUNCTIONS
|
|
349
|
-
// ============================================================================
|
|
350
|
-
|
|
351
|
-
function polynomialsToBytes(polys: Polynomial[]): Uint8Array {
|
|
352
|
-
const bytes = new Uint8Array(polys.length * 384)
|
|
353
|
-
for (let i = 0; i < polys.length; i++) {
|
|
354
|
-
const poly_bytes = polynomialToBytes(polys[i]!)
|
|
355
|
-
bytes.set(poly_bytes, i * 384)
|
|
356
|
-
}
|
|
357
|
-
return bytes
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
function polynomialToBytes(poly: Polynomial): Uint8Array {
|
|
361
|
-
const bytes = new Uint8Array(384)
|
|
362
|
-
for (let i = 0; i < KYBER_N; i++) {
|
|
363
|
-
const idx = floor((i * 12) / 8)
|
|
364
|
-
const shift = ((i * 12) % 8)
|
|
365
|
-
const val = poly[i] & ((1 << 12) - 1) // 12-bit encoding
|
|
366
|
-
bytes[idx] = (bytes[idx] | (val << shift)) & 0xff
|
|
367
|
-
if (shift > 0) {
|
|
368
|
-
bytes[idx + 1] = (bytes[idx + 1] | (val >> (8 - shift))) & 0xff
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
return bytes
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
function bytesToPolynomials(bytes: Buffer, count: number): Polynomial[] {
|
|
375
|
-
const polys: Polynomial[] = []
|
|
376
|
-
for (let i = 0; i < count; i++) {
|
|
377
|
-
polys[i] = bytesToPolynomial(bytes.slice(i * 384, (i + 1) * 384))
|
|
378
|
-
}
|
|
379
|
-
return polys
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
function bytesToPolynomial(bytes: Buffer): Polynomial {
|
|
383
|
-
const poly = new Uint16Array(KYBER_N)
|
|
384
|
-
for (let i = 0; i < KYBER_N; i++) {
|
|
385
|
-
const idx = floor((i * 12) / 8)
|
|
386
|
-
const shift = (i * 12) % 8
|
|
387
|
-
poly[i] = ((bytes[idx] >> shift) | (bytes[idx + 1] << (8 - shift))) & ((1 << 12) - 1)
|
|
388
|
-
}
|
|
389
|
-
return poly
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
function messageToPoly(msg: Buffer): Polynomial {
|
|
393
|
-
const poly = new Uint16Array(KYBER_N)
|
|
394
|
-
for (let i = 0; i < 32; i++) {
|
|
395
|
-
const byte = msg[i]!
|
|
396
|
-
for (let j = 0; j < 8; j++) {
|
|
397
|
-
if ((byte >> j) & 1) {
|
|
398
|
-
poly[i * 8 + j] = floor(KYBER_Q / 2)
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
return poly
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
function polyToMessage(poly: Polynomial): Buffer {
|
|
406
|
-
const msg = Buffer.alloc(32)
|
|
407
|
-
const threshold = floor(KYBER_Q / 2)
|
|
408
|
-
for (let i = 0; i < 32; i++) {
|
|
409
|
-
for (let j = 0; j < 8; j++) {
|
|
410
|
-
if (poly[i * 8 + j]! > threshold) {
|
|
411
|
-
msg[i] = msg[i]! | (1 << j)
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
return msg
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
function compressPolynomials(polys: Polynomial[], d: number): Buffer {
|
|
419
|
-
const bytes = Buffer.alloc(polys.length * KYBER_N * d / 8)
|
|
420
|
-
let bit_idx = 0
|
|
421
|
-
for (const poly of polys) {
|
|
422
|
-
for (let i = 0; i < KYBER_N; i++) {
|
|
423
|
-
const compressed = floor((poly[i] * ((1 << d) - 1)) / KYBER_Q)
|
|
424
|
-
for (let j = 0; j < d; j++) {
|
|
425
|
-
if ((compressed >> j) & 1) {
|
|
426
|
-
bytes[floor(bit_idx / 8)] |= 1 << (bit_idx % 8)
|
|
427
|
-
}
|
|
428
|
-
bit_idx++
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
return bytes
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
function compressPolynomial(poly: Polynomial, d: number): Buffer {
|
|
436
|
-
const bytes = Buffer.alloc(KYBER_N * d / 8)
|
|
437
|
-
let bit_idx = 0
|
|
438
|
-
for (let i = 0; i < KYBER_N; i++) {
|
|
439
|
-
const compressed = floor((poly[i] * ((1 << d) - 1)) / KYBER_Q)
|
|
440
|
-
for (let j = 0; j < d; j++) {
|
|
441
|
-
if ((compressed >> j) & 1) {
|
|
442
|
-
bytes[floor(bit_idx / 8)] |= 1 << (bit_idx % 8)
|
|
443
|
-
}
|
|
444
|
-
bit_idx++
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
return bytes
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
function decompressPolynomials(bytes: Buffer, count: number, d: number): Polynomial[] {
|
|
451
|
-
const polys: Polynomial[] = []
|
|
452
|
-
let bit_idx = 0
|
|
453
|
-
for (let p = 0; p < count; p++) {
|
|
454
|
-
const poly = new Uint16Array(KYBER_N)
|
|
455
|
-
for (let i = 0; i < KYBER_N; i++) {
|
|
456
|
-
let compressed = 0
|
|
457
|
-
for (let j = 0; j < d; j++) {
|
|
458
|
-
if ((bytes[floor(bit_idx / 8)] >> (bit_idx % 8)) & 1) {
|
|
459
|
-
compressed |= 1 << j
|
|
460
|
-
}
|
|
461
|
-
bit_idx++
|
|
462
|
-
}
|
|
463
|
-
poly[i] = floor((compressed * KYBER_Q) / ((1 << d) - 1))
|
|
464
|
-
}
|
|
465
|
-
polys[p] = poly
|
|
466
|
-
}
|
|
467
|
-
return polys
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
function decompressPolynomial(bytes: Buffer, d: number): Polynomial {
|
|
471
|
-
const poly = new Uint16Array(KYBER_N)
|
|
472
|
-
let bit_idx = 0
|
|
473
|
-
for (let i = 0; i < KYBER_N; i++) {
|
|
474
|
-
let compressed = 0
|
|
475
|
-
for (let j = 0; j < d; j++) {
|
|
476
|
-
if ((bytes[floor(bit_idx / 8)] >> (bit_idx % 8)) & 1) {
|
|
477
|
-
compressed |= 1 << j
|
|
478
|
-
}
|
|
479
|
-
bit_idx++
|
|
480
|
-
}
|
|
481
|
-
poly[i] = floor((compressed * KYBER_Q) / ((1 << d) - 1))
|
|
482
|
-
}
|
|
483
|
-
return poly
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
function polySubtract(a: Polynomial, b: Polynomial): Polynomial {
|
|
487
|
-
const result = new Uint16Array(KYBER_N)
|
|
488
|
-
for (let i = 0; i < KYBER_N; i++) {
|
|
489
|
-
result[i] = (a[i] - b[i] + KYBER_Q) % KYBER_Q
|
|
490
|
-
}
|
|
491
|
-
return result
|
|
492
|
-
}
|