quico 0.1.1 → 0.2.0

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/crypto.js ADDED
@@ -0,0 +1,519 @@
1
+ /*
2
+ * quico: HTTP/3 and QUIC implementation for Node.js
3
+ * Copyright 2025 colocohen
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at:
8
+ *
9
+ * https://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * This file is part of the open-source project hosted at:
12
+ * https://github.com/colocohen/quico
13
+ */
14
+
15
+ import crypto from 'node:crypto';
16
+
17
+ // Import crypto primitives from LemonTLS (already node:crypto based)
18
+ import {
19
+ crypto as tlsCrypto
20
+ } from 'lemon-tls';
21
+
22
+ var hkdf_extract = tlsCrypto.hkdf_extract;
23
+ var hkdf_expand_label = tlsCrypto.hkdf_expand_label;
24
+ var getHashFn = tlsCrypto.getHashFn;
25
+ var getHashLen = tlsCrypto.getHashLen;
26
+ var TLS_CIPHER_SUITES = tlsCrypto.TLS_CIPHER_SUITES;
27
+
28
+ import {
29
+ concatUint8Arrays,
30
+ readVarInt,
31
+ writeVarInt
32
+ } from './utils.js';
33
+
34
+
35
+ // ============================================================
36
+ // Initial salts per QUIC version (RFC 9001 §5.2)
37
+ // ============================================================
38
+
39
+ var INITIAL_SALTS = {
40
+ 0x00000001: new Uint8Array([
41
+ 0x38, 0x76, 0x2c, 0xf7, 0xf5, 0x59, 0x34, 0xb3,
42
+ 0x4d, 0x17, 0x9a, 0xe6, 0xa4, 0xc8, 0x0c, 0xad,
43
+ 0xcc, 0xbb, 0x7f, 0x0a
44
+ ]),
45
+ 0xff00001d: new Uint8Array([
46
+ 0xaf, 0xbf, 0xec, 0x28, 0x99, 0x93, 0xd2, 0x4c,
47
+ 0x9e, 0x97, 0x86, 0xf1, 0x9c, 0x61, 0x11, 0xe0,
48
+ 0x43, 0x90, 0xa8, 0x99
49
+ ]),
50
+ 0xff000020: new Uint8Array([
51
+ 0x7f, 0xbc, 0xdb, 0x0e, 0x7c, 0x66, 0xbb, 0x77,
52
+ 0x7b, 0xe3, 0x0e, 0xbd, 0x5f, 0xa5, 0x15, 0x87,
53
+ 0x3d, 0x8d, 0x6e, 0x67
54
+ ])
55
+ };
56
+
57
+
58
+ // ============================================================
59
+ // Key derivation
60
+ // ============================================================
61
+
62
+ /**
63
+ * Derive Initial encryption keys from the client DCID.
64
+ * direction: 'read' = client keys, 'write' = server keys
65
+ */
66
+ function quic_derive_init_secrets(client_dcid, version, direction) {
67
+ var hashName = 'sha256';
68
+ var hashFn = getHashFn(hashName);
69
+ var salt = INITIAL_SALTS[version];
70
+ if (!salt) throw new Error('Unsupported QUIC version: 0x' + version.toString(16));
71
+
72
+ var label = direction === 'read' ? 'client in' : 'server in';
73
+ var initial_secret = hkdf_extract(hashName, salt, client_dcid);
74
+
75
+ var side_secret = hkdf_expand_label(
76
+ hashName, initial_secret, label, new Uint8Array(0), 32
77
+ );
78
+
79
+ return {
80
+ key: hkdf_expand_label(hashName, side_secret, 'quic key', new Uint8Array(0), 16),
81
+ iv: hkdf_expand_label(hashName, side_secret, 'quic iv', new Uint8Array(0), 12),
82
+ hp: hkdf_expand_label(hashName, side_secret, 'quic hp', new Uint8Array(0), 16)
83
+ };
84
+ }
85
+
86
+
87
+ /**
88
+ * Derive QUIC keys from a TLS traffic secret (handshake or app).
89
+ * cipher: TLS cipher suite code (e.g. 0x1301). Used to determine key length.
90
+ */
91
+ function quic_derive_from_tls_secrets(traffic_secret, hashName, cipher) {
92
+ if (!traffic_secret) return null;
93
+ var keyLen = 16; // default AES-128
94
+ if (cipher && TLS_CIPHER_SUITES[cipher]) {
95
+ keyLen = TLS_CIPHER_SUITES[cipher].keylen;
96
+ } else if (hashName === 'sha384') {
97
+ keyLen = 32; // AES-256 for SHA-384 based cipher suites
98
+ }
99
+ return {
100
+ key: hkdf_expand_label(hashName, traffic_secret, 'quic key', new Uint8Array(0), keyLen),
101
+ iv: hkdf_expand_label(hashName, traffic_secret, 'quic iv', new Uint8Array(0), 12),
102
+ hp: hkdf_expand_label(hashName, traffic_secret, 'quic hp', new Uint8Array(0), keyLen)
103
+ };
104
+ }
105
+
106
+
107
+ /**
108
+ * Key Update (RFC 9001 §6): derive next secret + keys from current secret.
109
+ * next_secret = HKDF-Expand-Label(current_secret, "quic ku", "", hash_len)
110
+ */
111
+ function quic_derive_key_update(current_secret, hashName, cipher) {
112
+ var hashLen = getHashLen(hashName);
113
+ var keyLen = 16;
114
+ if (cipher && TLS_CIPHER_SUITES[cipher]) {
115
+ keyLen = TLS_CIPHER_SUITES[cipher].keylen;
116
+ } else if (hashName === 'sha384') {
117
+ keyLen = 32;
118
+ }
119
+ var next_secret = hkdf_expand_label(hashName, current_secret, 'quic ku', new Uint8Array(0), hashLen);
120
+ return {
121
+ secret: next_secret,
122
+ key: hkdf_expand_label(hashName, next_secret, 'quic key', new Uint8Array(0), keyLen),
123
+ iv: hkdf_expand_label(hashName, next_secret, 'quic iv', new Uint8Array(0), 12),
124
+ hp: hkdf_expand_label(hashName, next_secret, 'quic hp', new Uint8Array(0), keyLen)
125
+ };
126
+ }
127
+
128
+
129
+ // ============================================================
130
+ // AEAD (AES-GCM)
131
+ // ============================================================
132
+
133
+ function compute_nonce(iv, packetNumber) {
134
+ var nonce = new Uint8Array(iv);
135
+ var n = packetNumber;
136
+ for (var i = 11; n > 0 && i >= 0; i--) {
137
+ nonce[i] ^= n & 0xff;
138
+ n = Math.floor(n / 256);
139
+ }
140
+ return nonce;
141
+ }
142
+
143
+ function aead_algo(keyLen) {
144
+ if (keyLen === 16) return 'aes-128-gcm';
145
+ if (keyLen === 32) return 'aes-256-gcm';
146
+ throw new Error('Unsupported key length: ' + keyLen);
147
+ }
148
+
149
+ function aead_encrypt(key, iv, packetNumber, plaintext, aad) {
150
+ try {
151
+ var nonce = compute_nonce(iv, packetNumber);
152
+ var cipher = crypto.createCipheriv(aead_algo(key.length), key, nonce);
153
+ cipher.setAAD(aad);
154
+
155
+ var encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
156
+ var tag = cipher.getAuthTag();
157
+
158
+ var result = new Uint8Array(encrypted.length + tag.length);
159
+ result.set(encrypted, 0);
160
+ result.set(tag, encrypted.length);
161
+ return result;
162
+ } catch (e) {
163
+ return null;
164
+ }
165
+ }
166
+
167
+ function aead_decrypt(key, nonce, ciphertext, tag, aad) {
168
+ try {
169
+ var decipher = crypto.createDecipheriv(aead_algo(key.length), key, nonce);
170
+ decipher.setAuthTag(tag);
171
+ decipher.setAAD(aad);
172
+
173
+ var decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
174
+ return new Uint8Array(decrypted);
175
+ } catch (e) {
176
+ return null;
177
+ }
178
+ }
179
+
180
+
181
+ // ============================================================
182
+ // Header protection (AES-ECB mask)
183
+ // ============================================================
184
+
185
+ function aes_ecb_encrypt(key, plaintext) {
186
+ var algo = key.length === 32 ? 'aes-256-ecb' : 'aes-128-ecb';
187
+ var cipher = crypto.createCipheriv(algo, key, null);
188
+ cipher.setAutoPadding(false);
189
+ return new Uint8Array(Buffer.concat([cipher.update(plaintext), cipher.final()]));
190
+ }
191
+
192
+ function apply_header_protection(packet, pnOffset, hpKey, pnLength) {
193
+ var sample = packet.slice(pnOffset + 4, pnOffset + 4 + 16);
194
+ if (sample.length < 16) throw new Error('Not enough bytes for HP sample');
195
+
196
+ var mask = aes_ecb_encrypt(hpKey, sample);
197
+ var isLong = (packet[0] & 0x80) !== 0;
198
+
199
+ packet[0] ^= mask[0] & (isLong ? 0x0f : 0x1f);
200
+
201
+ for (var i = 0; i < pnLength; i++) {
202
+ packet[pnOffset + i] ^= mask[1 + i];
203
+ }
204
+ return packet;
205
+ }
206
+
207
+ function remove_header_protection(array, pnOffset, hpKey, isShort) {
208
+ var sample = array.slice(pnOffset + 4, pnOffset + 4 + 16);
209
+ var mask = aes_ecb_encrypt(hpKey, sample);
210
+
211
+ if (isShort) {
212
+ array[0] ^= mask[0] & 0x1f;
213
+ } else {
214
+ array[0] ^= mask[0] & 0x0f;
215
+ }
216
+
217
+ var pnLength = (array[0] & 0x03) + 1;
218
+
219
+ for (var i = 0; i < pnLength; i++) {
220
+ array[pnOffset + i] ^= mask[1 + i];
221
+ }
222
+ return pnLength;
223
+ }
224
+
225
+
226
+ // ============================================================
227
+ // Packet number encoding
228
+ // ============================================================
229
+
230
+ function expandPacketNumber(truncated, pnLen, largestReceived) {
231
+ // Use Math.pow instead of << to avoid JavaScript 32-bit overflow (1 << 32 === 1)
232
+ var pnWin = Math.pow(2, pnLen * 8);
233
+ var pnHalf = pnWin / 2;
234
+ var expected = largestReceived + 1;
235
+ return truncated + pnWin * Math.floor((expected - truncated + pnHalf) / pnWin);
236
+ }
237
+
238
+ function decode_packet_number(array, offset, pnLength) {
239
+ var value = 0;
240
+ for (var i = 0; i < pnLength; i++) {
241
+ value = (value << 8) | array[offset + i];
242
+ }
243
+ return value;
244
+ }
245
+
246
+ function encode_packet_number(packetNumber) {
247
+ var pnLength;
248
+ if (packetNumber <= 0xff) pnLength = 1;
249
+ else if (packetNumber <= 0xffff) pnLength = 2;
250
+ else if (packetNumber <= 0xffffff) pnLength = 3;
251
+ else pnLength = 4;
252
+
253
+ var buf = new Uint8Array(4);
254
+ buf[0] = (packetNumber >>> 24) & 0xff;
255
+ buf[1] = (packetNumber >>> 16) & 0xff;
256
+ buf[2] = (packetNumber >>> 8) & 0xff;
257
+ buf[3] = packetNumber & 0xff;
258
+
259
+ return { bytes: buf.slice(4 - pnLength), length: pnLength };
260
+ }
261
+
262
+
263
+ // ============================================================
264
+ // Packet-level encrypt / decrypt
265
+ // ============================================================
266
+
267
+ /**
268
+ * Build QUIC header for a packet (before encryption).
269
+ */
270
+ function build_quic_header(packetType, dcid, scid, token, lengthField, pnLen, keyPhase) {
271
+ var hdr = [];
272
+ var firstByte;
273
+
274
+ if (packetType === '1rtt') {
275
+ firstByte = 0x40 | ((pnLen - 1) & 0x03);
276
+ if (keyPhase) firstByte |= 0x04; // key_phase bit
277
+ hdr.push(Uint8Array.of(firstByte));
278
+ hdr.push(dcid);
279
+ var header = concatUint8Arrays(hdr);
280
+ return { header: header, pnOffset: header.length };
281
+ }
282
+
283
+ // Long header
284
+ if (packetType === 'initial') {
285
+ firstByte = 0xc0 | ((pnLen - 1) & 0x03);
286
+ } else if (packetType === 'handshake') {
287
+ firstByte = 0xe0 | ((pnLen - 1) & 0x03);
288
+ } else if (packetType === '0rtt') {
289
+ firstByte = 0xd0 | ((pnLen - 1) & 0x03);
290
+ } else {
291
+ throw new Error('Unsupported packet type: ' + packetType);
292
+ }
293
+
294
+ hdr.push(Uint8Array.of(firstByte));
295
+ hdr.push(new Uint8Array([0x00, 0x00, 0x00, 0x01])); // version 1
296
+ hdr.push(writeVarInt(dcid.length), dcid);
297
+ hdr.push(writeVarInt(scid.length), scid);
298
+
299
+ if (packetType === 'initial') {
300
+ if (!token) token = new Uint8Array(0);
301
+ hdr.push(writeVarInt(token.length), token);
302
+ }
303
+
304
+ hdr.push(lengthField);
305
+
306
+ var header = concatUint8Arrays(hdr);
307
+ return { header: header, pnOffset: header.length };
308
+ }
309
+
310
+
311
+ /**
312
+ * Encrypt a QUIC packet (frames → encrypted packet with HP).
313
+ */
314
+ function encrypt_quic_packet(packetType, encodedFrames, writeKey, writeIv, writeHp, packetNumber, dcid, scid, token, keyPhase) {
315
+ var pn = encode_packet_number(packetNumber);
316
+ var pnLength = pn.length;
317
+ var pnBytes = pn.bytes;
318
+
319
+ var payloadLen = encodedFrames.length + pnLength + 16; // 16 = GCM tag
320
+ var lengthField = writeVarInt(payloadLen);
321
+ var hdrInfo = build_quic_header(packetType, dcid, scid, token, lengthField, pnLength, keyPhase);
322
+
323
+ var header = hdrInfo.header;
324
+ var pnOffset = hdrInfo.pnOffset;
325
+
326
+ // Ensure minimum packet size for HP sample (pnOffset + pnLength + 20 for sample)
327
+ var minTotal = pnOffset + pnLength + 20;
328
+ var fullLen = header.length + pnLength + encodedFrames.length + 16;
329
+
330
+ if (fullLen < minTotal) {
331
+ var extra = minTotal - fullLen;
332
+ var padded = new Uint8Array(encodedFrames.length + extra);
333
+ padded.set(encodedFrames, 0);
334
+ encodedFrames = padded;
335
+ // Rebuild header with new length
336
+ payloadLen = encodedFrames.length + pnLength + 16;
337
+ lengthField = writeVarInt(payloadLen);
338
+ hdrInfo = build_quic_header(packetType, dcid, scid, token, lengthField, pnLength, keyPhase);
339
+ header = hdrInfo.header;
340
+ pnOffset = hdrInfo.pnOffset;
341
+ }
342
+
343
+ // AAD = header + packet number bytes (unprotected)
344
+ var aad = concatUint8Arrays([header, pnBytes]);
345
+
346
+ var ciphertext = aead_encrypt(writeKey, writeIv, packetNumber, encodedFrames, aad);
347
+ if (ciphertext === null) return null;
348
+
349
+ var fullPacket = concatUint8Arrays([header, pnBytes, ciphertext]);
350
+
351
+ return apply_header_protection(fullPacket, pnOffset, writeHp, pnLength);
352
+ }
353
+
354
+
355
+ /**
356
+ * Decrypt a QUIC packet (raw bytes → plaintext frames).
357
+ */
358
+ function decrypt_quic_packet(array, readKey, readIv, readHp, dcid, largestPn) {
359
+ if (!(array instanceof Uint8Array)) throw new Error('Invalid input');
360
+
361
+ var firstByte = array[0];
362
+ var isShort = (firstByte & 0x80) === 0;
363
+
364
+ var pnOffset, pnLength, aad, ciphertext, tag, packetNumber, nonce, keyPhase = false;
365
+
366
+ if (!isShort) {
367
+ // Long Header
368
+ var offset = 6;
369
+ var dcidLen = array[5];
370
+ offset += dcidLen;
371
+ var scidLen = array[offset++];
372
+ offset += scidLen;
373
+
374
+ var typeBits = (firstByte & 0x30) >> 4;
375
+ if (typeBits === 0) { // Initial — read token
376
+ var tokenLen = readVarInt(array, offset);
377
+ offset += tokenLen.byteLength + tokenLen.value;
378
+ }
379
+
380
+ var len = readVarInt(array, offset);
381
+ offset += len.byteLength;
382
+
383
+ pnOffset = offset;
384
+ pnLength = remove_header_protection(array, pnOffset, readHp, false);
385
+ if (pnLength === null) return null;
386
+
387
+ packetNumber = expandPacketNumber(
388
+ decode_packet_number(array, pnOffset, pnLength),
389
+ pnLength, largestPn
390
+ );
391
+ nonce = compute_nonce(readIv, packetNumber);
392
+
393
+ var payloadStart = pnOffset + pnLength;
394
+ var payloadLength = len.value - pnLength;
395
+ var payloadEnd = payloadStart + payloadLength;
396
+
397
+ if (payloadEnd > array.length) return null;
398
+
399
+ var payload = array.slice(payloadStart, payloadEnd);
400
+ if (payload.length < 16) return null;
401
+
402
+ ciphertext = payload.slice(0, payload.length - 16);
403
+ tag = payload.slice(payload.length - 16);
404
+ aad = array.slice(0, pnOffset + pnLength);
405
+
406
+ } else {
407
+ // Short Header
408
+ var dcidLen = dcid.length;
409
+ pnOffset = 1 + dcidLen;
410
+
411
+ pnLength = remove_header_protection(array, pnOffset, readHp, true);
412
+ if (pnLength === null) return null;
413
+
414
+ keyPhase = Boolean((array[0] & 0x04) >>> 2);
415
+
416
+ packetNumber = expandPacketNumber(
417
+ decode_packet_number(array, pnOffset, pnLength),
418
+ pnLength, largestPn
419
+ );
420
+ nonce = compute_nonce(readIv, packetNumber);
421
+
422
+ var payload = array.slice(pnOffset + pnLength);
423
+ if (payload.length < 16) return null;
424
+
425
+ ciphertext = payload.slice(0, payload.length - 16);
426
+ tag = payload.slice(payload.length - 16);
427
+ aad = array.slice(0, pnOffset + pnLength);
428
+ }
429
+
430
+ var plaintext = aead_decrypt(readKey, nonce, ciphertext, tag, aad);
431
+
432
+ return {
433
+ packet_number: packetNumber,
434
+ key_phase: keyPhase,
435
+ plaintext: plaintext
436
+ };
437
+ }
438
+
439
+
440
+ // ============================================================
441
+ // TLS CRYPTO message reassembly from CRYPTO frame chunks
442
+ // ============================================================
443
+
444
+ function extract_tls_messages_from_chunks(chunks, from_offset) {
445
+ var offset = from_offset;
446
+ var buffers = [];
447
+
448
+ while (chunks[offset]) {
449
+ buffers.push(chunks[offset]);
450
+ offset += chunks[offset].length;
451
+ }
452
+
453
+ if (buffers.length === 0) return false;
454
+
455
+ var combined = concatUint8Arrays(buffers);
456
+ var tls_messages = [];
457
+ var i = 0;
458
+
459
+ while (i + 4 <= combined.length) {
460
+ var msgType = combined[i];
461
+ var length = (combined[i + 1] << 16) | (combined[i + 2] << 8) | combined[i + 3];
462
+ if (i + 4 + length > combined.length) break;
463
+
464
+ tls_messages.push(combined.slice(i, i + 4 + length));
465
+ i += 4 + length;
466
+ }
467
+
468
+ if (i > 0) {
469
+ // Delete all consumed chunks
470
+ var cleanupOffset = from_offset;
471
+ while (cleanupOffset < offset) {
472
+ var c = chunks[cleanupOffset];
473
+ if (!c) break;
474
+ var nextOffset = cleanupOffset + c.length;
475
+ delete chunks[cleanupOffset];
476
+ cleanupOffset = nextOffset;
477
+ }
478
+ // Store remaining data at the correct position
479
+ var newFromOffset = from_offset + i;
480
+ if (i < combined.length) {
481
+ chunks[newFromOffset] = combined.slice(i);
482
+ }
483
+ from_offset = newFromOffset;
484
+ }
485
+
486
+ return { tls_messages: tls_messages, new_from_offset: from_offset };
487
+ }
488
+
489
+
490
+ // ============================================================
491
+ // Exports
492
+ // ============================================================
493
+
494
+ export {
495
+ // Re-exports from LemonTLS
496
+ TLS_CIPHER_SUITES,
497
+ hkdf_expand_label,
498
+ getHashFn,
499
+ getHashLen,
500
+
501
+ // QUIC key derivation
502
+ quic_derive_init_secrets,
503
+ quic_derive_from_tls_secrets,
504
+ quic_derive_key_update,
505
+
506
+ // Packet encrypt/decrypt
507
+ encrypt_quic_packet,
508
+ decrypt_quic_packet,
509
+
510
+ // TLS message reassembly
511
+ extract_tls_messages_from_chunks,
512
+
513
+ // Low-level (for testing)
514
+ compute_nonce,
515
+ aead_encrypt,
516
+ aead_decrypt,
517
+ apply_header_protection,
518
+ remove_header_protection
519
+ };