quico 0.1.3 → 0.3.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,534 @@
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 an Initial-packet secret + keys from the client's original DCID.
64
+ *
65
+ * `direction` selects WHICH endpoint's Initial secret to derive — it is NOT
66
+ * "the keys I read/write with" in the obvious sense:
67
+ * 'read' → the client's secret (TLS label "client in")
68
+ * 'write' → the server's secret (TLS label "server in")
69
+ * Each role then maps these to its own read/write keys: a client WRITES with
70
+ * the "client in" secret and READS with "server in"; a server is the mirror.
71
+ * So callers pass 'read' to get the client-side keys and 'write' for the
72
+ * server-side keys, and assign them to initial_read / initial_write per role.
73
+ */
74
+ function quic_derive_init_secrets(client_dcid, version, direction) {
75
+ var hashName = 'sha256';
76
+ var hashFn = getHashFn(hashName);
77
+ var salt = INITIAL_SALTS[version];
78
+ if (!salt) throw new Error('Unsupported QUIC version: 0x' + version.toString(16));
79
+
80
+ var label = direction === 'read' ? 'client in' : 'server in';
81
+ var initial_secret = hkdf_extract(hashName, salt, client_dcid);
82
+
83
+ var side_secret = hkdf_expand_label(
84
+ hashName, initial_secret, label, new Uint8Array(0), 32
85
+ );
86
+
87
+ return {
88
+ key: hkdf_expand_label(hashName, side_secret, 'quic key', new Uint8Array(0), 16),
89
+ iv: hkdf_expand_label(hashName, side_secret, 'quic iv', new Uint8Array(0), 12),
90
+ hp: hkdf_expand_label(hashName, side_secret, 'quic hp', new Uint8Array(0), 16)
91
+ };
92
+ }
93
+
94
+
95
+ /**
96
+ * Derive QUIC keys from a TLS traffic secret (handshake or app).
97
+ * cipher: TLS cipher suite code (e.g. 0x1301). Used to determine key length.
98
+ */
99
+ function quic_derive_from_tls_secrets(traffic_secret, hashName, cipher) {
100
+ if (!traffic_secret) return null;
101
+ var keyLen = 16; // default AES-128
102
+ if (cipher && TLS_CIPHER_SUITES[cipher]) {
103
+ keyLen = TLS_CIPHER_SUITES[cipher].keylen;
104
+ } else if (hashName === 'sha384') {
105
+ keyLen = 32; // AES-256 for SHA-384 based cipher suites
106
+ }
107
+ return {
108
+ key: hkdf_expand_label(hashName, traffic_secret, 'quic key', new Uint8Array(0), keyLen),
109
+ iv: hkdf_expand_label(hashName, traffic_secret, 'quic iv', new Uint8Array(0), 12),
110
+ hp: hkdf_expand_label(hashName, traffic_secret, 'quic hp', new Uint8Array(0), keyLen)
111
+ };
112
+ }
113
+
114
+
115
+ /**
116
+ * Key Update (RFC 9001 §6): derive next secret + keys from current secret.
117
+ * next_secret = HKDF-Expand-Label(current_secret, "quic ku", "", hash_len)
118
+ */
119
+ function quic_derive_key_update(current_secret, hashName, cipher) {
120
+ var hashLen = getHashLen(hashName);
121
+ var keyLen = 16;
122
+ if (cipher && TLS_CIPHER_SUITES[cipher]) {
123
+ keyLen = TLS_CIPHER_SUITES[cipher].keylen;
124
+ } else if (hashName === 'sha384') {
125
+ keyLen = 32;
126
+ }
127
+ var next_secret = hkdf_expand_label(hashName, current_secret, 'quic ku', new Uint8Array(0), hashLen);
128
+ return {
129
+ secret: next_secret,
130
+ key: hkdf_expand_label(hashName, next_secret, 'quic key', new Uint8Array(0), keyLen),
131
+ iv: hkdf_expand_label(hashName, next_secret, 'quic iv', new Uint8Array(0), 12),
132
+ hp: hkdf_expand_label(hashName, next_secret, 'quic hp', new Uint8Array(0), keyLen)
133
+ };
134
+ }
135
+
136
+
137
+ // ============================================================
138
+ // AEAD (AES-GCM)
139
+ // ============================================================
140
+
141
+ function compute_nonce(iv, packetNumber) {
142
+ var nonce = new Uint8Array(iv);
143
+ var n = packetNumber;
144
+ for (var i = 11; n > 0 && i >= 0; i--) {
145
+ nonce[i] ^= n & 0xff;
146
+ n = Math.floor(n / 256);
147
+ }
148
+ return nonce;
149
+ }
150
+
151
+ function aead_algo(keyLen) {
152
+ if (keyLen === 16) return 'aes-128-gcm';
153
+ if (keyLen === 32) return 'aes-256-gcm';
154
+ throw new Error('Unsupported key length: ' + keyLen);
155
+ }
156
+
157
+ function aead_encrypt(key, iv, packetNumber, plaintext, aad) {
158
+ try {
159
+ var nonce = compute_nonce(iv, packetNumber);
160
+ var cipher = crypto.createCipheriv(aead_algo(key.length), key, nonce);
161
+ cipher.setAAD(aad);
162
+
163
+ var encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
164
+ var tag = cipher.getAuthTag();
165
+
166
+ var result = new Uint8Array(encrypted.length + tag.length);
167
+ result.set(encrypted, 0);
168
+ result.set(tag, encrypted.length);
169
+ return result;
170
+ } catch (e) {
171
+ return null;
172
+ }
173
+ }
174
+
175
+ function aead_decrypt(key, nonce, ciphertext, tag, aad) {
176
+ try {
177
+ var decipher = crypto.createDecipheriv(aead_algo(key.length), key, nonce);
178
+ decipher.setAuthTag(tag);
179
+ decipher.setAAD(aad);
180
+
181
+ var decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
182
+ return new Uint8Array(decrypted);
183
+ } catch (e) {
184
+ return null;
185
+ }
186
+ }
187
+
188
+
189
+ // ============================================================
190
+ // Header protection (AES-ECB mask)
191
+ // ============================================================
192
+
193
+ function aes_ecb_encrypt(key, plaintext) {
194
+ var algo = key.length === 32 ? 'aes-256-ecb' : 'aes-128-ecb';
195
+ var cipher = crypto.createCipheriv(algo, key, null);
196
+ cipher.setAutoPadding(false);
197
+ return new Uint8Array(Buffer.concat([cipher.update(plaintext), cipher.final()]));
198
+ }
199
+
200
+ function apply_header_protection(packet, pnOffset, hpKey, pnLength) {
201
+ var sample = packet.slice(pnOffset + 4, pnOffset + 4 + 16);
202
+ if (sample.length < 16) throw new Error('Not enough bytes for HP sample');
203
+
204
+ var mask = aes_ecb_encrypt(hpKey, sample);
205
+ var isLong = (packet[0] & 0x80) !== 0;
206
+
207
+ packet[0] ^= mask[0] & (isLong ? 0x0f : 0x1f);
208
+
209
+ for (var i = 0; i < pnLength; i++) {
210
+ packet[pnOffset + i] ^= mask[1 + i];
211
+ }
212
+ return packet;
213
+ }
214
+
215
+ function remove_header_protection(array, pnOffset, hpKey, isShort) {
216
+ var sample = array.slice(pnOffset + 4, pnOffset + 4 + 16);
217
+ var mask = aes_ecb_encrypt(hpKey, sample);
218
+
219
+ if (isShort) {
220
+ array[0] ^= mask[0] & 0x1f;
221
+ } else {
222
+ array[0] ^= mask[0] & 0x0f;
223
+ }
224
+
225
+ var pnLength = (array[0] & 0x03) + 1;
226
+
227
+ for (var i = 0; i < pnLength; i++) {
228
+ array[pnOffset + i] ^= mask[1 + i];
229
+ }
230
+ return pnLength;
231
+ }
232
+
233
+
234
+ // ============================================================
235
+ // Packet number encoding
236
+ // ============================================================
237
+
238
+ function expandPacketNumber(truncated, pnLen, largestReceived) {
239
+ // Use Math.pow instead of << to avoid JavaScript 32-bit overflow (1 << 32 === 1)
240
+ var pnWin = Math.pow(2, pnLen * 8);
241
+ var pnHalf = pnWin / 2;
242
+ var expected = largestReceived + 1;
243
+ return truncated + pnWin * Math.floor((expected - truncated + pnHalf) / pnWin);
244
+ }
245
+
246
+ function decode_packet_number(array, offset, pnLength) {
247
+ var value = 0;
248
+ // Use *256 (not <<8): a 4-byte packet number whose leading byte has the high
249
+ // bit set would overflow JS's 32-bit signed bitwise math and go negative.
250
+ for (var i = 0; i < pnLength; i++) {
251
+ value = value * 256 + array[offset + i];
252
+ }
253
+ return value;
254
+ }
255
+
256
+ function encode_packet_number(packetNumber) {
257
+ var pnLength;
258
+ if (packetNumber <= 0xff) pnLength = 1;
259
+ else if (packetNumber <= 0xffff) pnLength = 2;
260
+ else if (packetNumber <= 0xffffff) pnLength = 3;
261
+ else pnLength = 4;
262
+
263
+ var buf = new Uint8Array(4);
264
+ buf[0] = (packetNumber >>> 24) & 0xff;
265
+ buf[1] = (packetNumber >>> 16) & 0xff;
266
+ buf[2] = (packetNumber >>> 8) & 0xff;
267
+ buf[3] = packetNumber & 0xff;
268
+
269
+ return { bytes: buf.slice(4 - pnLength), length: pnLength };
270
+ }
271
+
272
+
273
+ // ============================================================
274
+ // Packet-level encrypt / decrypt
275
+ // ============================================================
276
+
277
+ /**
278
+ * Build QUIC header for a packet (before encryption).
279
+ */
280
+ function build_quic_header(packetType, dcid, scid, token, lengthField, pnLen, keyPhase) {
281
+ var hdr = [];
282
+ var firstByte;
283
+
284
+ if (packetType === '1rtt') {
285
+ firstByte = 0x40 | ((pnLen - 1) & 0x03);
286
+ if (keyPhase) firstByte |= 0x04; // key_phase bit
287
+ hdr.push(Uint8Array.of(firstByte));
288
+ hdr.push(dcid);
289
+ var header = concatUint8Arrays(hdr);
290
+ return { header: header, pnOffset: header.length };
291
+ }
292
+
293
+ // Long header
294
+ if (packetType === 'initial') {
295
+ firstByte = 0xc0 | ((pnLen - 1) & 0x03);
296
+ } else if (packetType === 'handshake') {
297
+ firstByte = 0xe0 | ((pnLen - 1) & 0x03);
298
+ } else if (packetType === '0rtt') {
299
+ firstByte = 0xd0 | ((pnLen - 1) & 0x03);
300
+ } else {
301
+ throw new Error('Unsupported packet type: ' + packetType);
302
+ }
303
+
304
+ hdr.push(Uint8Array.of(firstByte));
305
+ hdr.push(new Uint8Array([0x00, 0x00, 0x00, 0x01])); // version 1
306
+ hdr.push(writeVarInt(dcid.length), dcid);
307
+ hdr.push(writeVarInt(scid.length), scid);
308
+
309
+ if (packetType === 'initial') {
310
+ if (!token) token = new Uint8Array(0);
311
+ hdr.push(writeVarInt(token.length), token);
312
+ }
313
+
314
+ hdr.push(lengthField);
315
+
316
+ var header = concatUint8Arrays(hdr);
317
+ return { header: header, pnOffset: header.length };
318
+ }
319
+
320
+
321
+ /**
322
+ * Encrypt a QUIC packet (frames → encrypted packet with HP).
323
+ */
324
+ function encrypt_quic_packet(packetType, encodedFrames, writeKey, writeIv, writeHp, packetNumber, dcid, scid, token, keyPhase) {
325
+ var pn = encode_packet_number(packetNumber);
326
+ var pnLength = pn.length;
327
+ var pnBytes = pn.bytes;
328
+
329
+ var payloadLen = encodedFrames.length + pnLength + 16; // 16 = GCM tag
330
+ var lengthField = writeVarInt(payloadLen);
331
+ var hdrInfo = build_quic_header(packetType, dcid, scid, token, lengthField, pnLength, keyPhase);
332
+
333
+ var header = hdrInfo.header;
334
+ var pnOffset = hdrInfo.pnOffset;
335
+
336
+ // Ensure minimum packet size for HP sample (pnOffset + pnLength + 20 for sample)
337
+ var minTotal = pnOffset + pnLength + 20;
338
+ var fullLen = header.length + pnLength + encodedFrames.length + 16;
339
+
340
+ if (fullLen < minTotal) {
341
+ var extra = minTotal - fullLen;
342
+ var padded = new Uint8Array(encodedFrames.length + extra);
343
+ padded.set(encodedFrames, 0);
344
+ encodedFrames = padded;
345
+ // Rebuild header with new length
346
+ payloadLen = encodedFrames.length + pnLength + 16;
347
+ lengthField = writeVarInt(payloadLen);
348
+ hdrInfo = build_quic_header(packetType, dcid, scid, token, lengthField, pnLength, keyPhase);
349
+ header = hdrInfo.header;
350
+ pnOffset = hdrInfo.pnOffset;
351
+ }
352
+
353
+ // AAD = header + packet number bytes (unprotected)
354
+ var aad = concatUint8Arrays([header, pnBytes]);
355
+
356
+ var ciphertext = aead_encrypt(writeKey, writeIv, packetNumber, encodedFrames, aad);
357
+ if (ciphertext === null) return null;
358
+
359
+ var fullPacket = concatUint8Arrays([header, pnBytes, ciphertext]);
360
+
361
+ return apply_header_protection(fullPacket, pnOffset, writeHp, pnLength);
362
+ }
363
+
364
+
365
+ /**
366
+ * Decrypt a QUIC packet (raw bytes → plaintext frames).
367
+ *
368
+ * `ownCid` is the connection ID by which the receiver is addressed. It is used
369
+ * ONLY for short (1-RTT) headers, which carry no DCID-length field — the
370
+ * receiver must already know its own CID length to locate the packet number.
371
+ * Long headers encode the DCID length on the wire, so `ownCid` is ignored there.
372
+ */
373
+ function decrypt_quic_packet(array, readKey, readIv, readHp, ownCid, largestPn) {
374
+ if (!(array instanceof Uint8Array)) throw new Error('Invalid input');
375
+
376
+ var firstByte = array[0];
377
+ var isShort = (firstByte & 0x80) === 0;
378
+
379
+ var pnOffset, pnLength, aad, ciphertext, tag, packetNumber, nonce, keyPhase = false;
380
+
381
+ if (!isShort) {
382
+ // Long Header
383
+ var offset = 6;
384
+ var dcidLen = array[5];
385
+ offset += dcidLen;
386
+ var scidLen = array[offset++];
387
+ offset += scidLen;
388
+
389
+ var typeBits = (firstByte & 0x30) >> 4;
390
+ if (typeBits === 0) { // Initial — read token
391
+ var tokenLen = readVarInt(array, offset);
392
+ offset += tokenLen.byteLength + tokenLen.value;
393
+ }
394
+
395
+ var len = readVarInt(array, offset);
396
+ offset += len.byteLength;
397
+
398
+ pnOffset = offset;
399
+ pnLength = remove_header_protection(array, pnOffset, readHp, false);
400
+ if (pnLength === null) return null;
401
+
402
+ packetNumber = expandPacketNumber(
403
+ decode_packet_number(array, pnOffset, pnLength),
404
+ pnLength, largestPn
405
+ );
406
+ nonce = compute_nonce(readIv, packetNumber);
407
+
408
+ var payloadStart = pnOffset + pnLength;
409
+ var payloadLength = len.value - pnLength;
410
+ var payloadEnd = payloadStart + payloadLength;
411
+
412
+ if (payloadEnd > array.length) return null;
413
+
414
+ var payload = array.slice(payloadStart, payloadEnd);
415
+ if (payload.length < 16) return null;
416
+
417
+ ciphertext = payload.slice(0, payload.length - 16);
418
+ tag = payload.slice(payload.length - 16);
419
+ aad = array.slice(0, pnOffset + pnLength);
420
+
421
+ } else {
422
+ // Short Header
423
+ var dcidLen = ownCid.length;
424
+ pnOffset = 1 + dcidLen;
425
+
426
+ pnLength = remove_header_protection(array, pnOffset, readHp, true);
427
+ if (pnLength === null) return null;
428
+
429
+ keyPhase = Boolean((array[0] & 0x04) >>> 2);
430
+
431
+ packetNumber = expandPacketNumber(
432
+ decode_packet_number(array, pnOffset, pnLength),
433
+ pnLength, largestPn
434
+ );
435
+ nonce = compute_nonce(readIv, packetNumber);
436
+
437
+ var payload = array.slice(pnOffset + pnLength);
438
+ if (payload.length < 16) return null;
439
+
440
+ ciphertext = payload.slice(0, payload.length - 16);
441
+ tag = payload.slice(payload.length - 16);
442
+ aad = array.slice(0, pnOffset + pnLength);
443
+ }
444
+
445
+ var plaintext = aead_decrypt(readKey, nonce, ciphertext, tag, aad);
446
+
447
+ return {
448
+ packet_number: packetNumber,
449
+ key_phase: keyPhase,
450
+ plaintext: plaintext
451
+ };
452
+ }
453
+
454
+
455
+ // ============================================================
456
+ // TLS CRYPTO message reassembly from CRYPTO frame chunks
457
+ // ============================================================
458
+
459
+ function extract_tls_messages_from_chunks(chunks, from_offset) {
460
+ var offset = from_offset;
461
+ var buffers = [];
462
+
463
+ while (chunks[offset]) {
464
+ buffers.push(chunks[offset]);
465
+ offset += chunks[offset].length;
466
+ }
467
+
468
+ if (buffers.length === 0) return false;
469
+
470
+ var combined = concatUint8Arrays(buffers);
471
+ var tls_messages = [];
472
+ var i = 0;
473
+
474
+ while (i + 4 <= combined.length) {
475
+ var msgType = combined[i];
476
+ var length = (combined[i + 1] << 16) | (combined[i + 2] << 8) | combined[i + 3];
477
+ if (i + 4 + length > combined.length) break;
478
+
479
+ tls_messages.push(combined.slice(i, i + 4 + length));
480
+ i += 4 + length;
481
+ }
482
+
483
+ if (i > 0) {
484
+ // Delete all consumed chunks
485
+ var cleanupOffset = from_offset;
486
+ while (cleanupOffset < offset) {
487
+ var c = chunks[cleanupOffset];
488
+ if (!c) break;
489
+ var nextOffset = cleanupOffset + c.length;
490
+ delete chunks[cleanupOffset];
491
+ cleanupOffset = nextOffset;
492
+ }
493
+ // Store remaining data at the correct position
494
+ var newFromOffset = from_offset + i;
495
+ if (i < combined.length) {
496
+ chunks[newFromOffset] = combined.slice(i);
497
+ }
498
+ from_offset = newFromOffset;
499
+ }
500
+
501
+ return { tls_messages: tls_messages, new_from_offset: from_offset };
502
+ }
503
+
504
+
505
+ // ============================================================
506
+ // Exports
507
+ // ============================================================
508
+
509
+ export {
510
+ // Re-exports from LemonTLS
511
+ TLS_CIPHER_SUITES,
512
+ hkdf_expand_label,
513
+ getHashFn,
514
+ getHashLen,
515
+
516
+ // QUIC key derivation
517
+ quic_derive_init_secrets,
518
+ quic_derive_from_tls_secrets,
519
+ quic_derive_key_update,
520
+
521
+ // Packet encrypt/decrypt
522
+ encrypt_quic_packet,
523
+ decrypt_quic_packet,
524
+
525
+ // TLS message reassembly
526
+ extract_tls_messages_from_chunks,
527
+
528
+ // Low-level (for testing)
529
+ compute_nonce,
530
+ aead_encrypt,
531
+ aead_decrypt,
532
+ apply_header_protection,
533
+ remove_header_protection
534
+ };