quico 0.3.0 → 0.7.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 CHANGED
@@ -30,29 +30,14 @@ import {
30
30
  readVarInt,
31
31
  writeVarInt
32
32
  } from './utils.js';
33
+ import { versionProfile, longTypeBits, longPacketType, QUIC_V1 } from './quic_version.js';
33
34
 
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
- };
35
+ // The profile of a version that keys are being derived for, or an error.
36
+ function profileOf(version) {
37
+ var p = versionProfile(version === undefined ? QUIC_V1 : version);
38
+ if (!p) throw new Error('Unsupported QUIC version: 0x' + (version >>> 0).toString(16));
39
+ return p;
40
+ }
56
41
 
57
42
 
58
43
  // ============================================================
@@ -73,21 +58,20 @@ var INITIAL_SALTS = {
73
58
  */
74
59
  function quic_derive_init_secrets(client_dcid, version, direction) {
75
60
  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));
61
+ var profile = profileOf(version);
62
+ var L = profile.labels;
79
63
 
80
64
  var label = direction === 'read' ? 'client in' : 'server in';
81
- var initial_secret = hkdf_extract(hashName, salt, client_dcid);
65
+ var initial_secret = hkdf_extract(hashName, profile.initialSalt, client_dcid);
82
66
 
83
67
  var side_secret = hkdf_expand_label(
84
68
  hashName, initial_secret, label, new Uint8Array(0), 32
85
69
  );
86
70
 
87
71
  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)
72
+ key: hkdf_expand_label(hashName, side_secret, L.key, new Uint8Array(0), 16),
73
+ iv: hkdf_expand_label(hashName, side_secret, L.iv, new Uint8Array(0), 12),
74
+ hp: hkdf_expand_label(hashName, side_secret, L.hp, new Uint8Array(0), 16)
91
75
  };
92
76
  }
93
77
 
@@ -95,9 +79,11 @@ function quic_derive_init_secrets(client_dcid, version, direction) {
95
79
  /**
96
80
  * Derive QUIC keys from a TLS traffic secret (handshake or app).
97
81
  * cipher: TLS cipher suite code (e.g. 0x1301). Used to determine key length.
82
+ * version: the connection's QUIC version (its labels).
98
83
  */
99
- function quic_derive_from_tls_secrets(traffic_secret, hashName, cipher) {
84
+ function quic_derive_from_tls_secrets(traffic_secret, hashName, cipher, version) {
100
85
  if (!traffic_secret) return null;
86
+ var L = profileOf(version).labels;
101
87
  var keyLen = 16; // default AES-128
102
88
  if (cipher && TLS_CIPHER_SUITES[cipher]) {
103
89
  keyLen = TLS_CIPHER_SUITES[cipher].keylen;
@@ -105,9 +91,10 @@ function quic_derive_from_tls_secrets(traffic_secret, hashName, cipher) {
105
91
  keyLen = 32; // AES-256 for SHA-384 based cipher suites
106
92
  }
107
93
  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)
94
+ key: hkdf_expand_label(hashName, traffic_secret, L.key, new Uint8Array(0), keyLen),
95
+ iv: hkdf_expand_label(hashName, traffic_secret, L.iv, new Uint8Array(0), 12),
96
+ hp: hkdf_expand_label(hashName, traffic_secret, L.hp, new Uint8Array(0), keyLen),
97
+ aead: aead_for_suite(cipher, keyLen)
111
98
  };
112
99
  }
113
100
 
@@ -116,7 +103,8 @@ function quic_derive_from_tls_secrets(traffic_secret, hashName, cipher) {
116
103
  * Key Update (RFC 9001 §6): derive next secret + keys from current secret.
117
104
  * next_secret = HKDF-Expand-Label(current_secret, "quic ku", "", hash_len)
118
105
  */
119
- function quic_derive_key_update(current_secret, hashName, cipher) {
106
+ function quic_derive_key_update(current_secret, hashName, cipher, version) {
107
+ var L = profileOf(version).labels;
120
108
  var hashLen = getHashLen(hashName);
121
109
  var keyLen = 16;
122
110
  if (cipher && TLS_CIPHER_SUITES[cipher]) {
@@ -124,12 +112,15 @@ function quic_derive_key_update(current_secret, hashName, cipher) {
124
112
  } else if (hashName === 'sha384') {
125
113
  keyLen = 32;
126
114
  }
127
- var next_secret = hkdf_expand_label(hashName, current_secret, 'quic ku', new Uint8Array(0), hashLen);
115
+ // The header protection key does not change with a key update (RFC 9001
116
+ // §6); derived here only to keep the shape, the caller keeps the old one.
117
+ var next_secret = hkdf_expand_label(hashName, current_secret, L.ku, new Uint8Array(0), hashLen);
128
118
  return {
129
119
  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)
120
+ key: hkdf_expand_label(hashName, next_secret, L.key, new Uint8Array(0), keyLen),
121
+ iv: hkdf_expand_label(hashName, next_secret, L.iv, new Uint8Array(0), 12),
122
+ hp: hkdf_expand_label(hashName, next_secret, L.hp, new Uint8Array(0), keyLen),
123
+ aead: aead_for_suite(cipher, keyLen)
133
124
  };
134
125
  }
135
126
 
@@ -148,16 +139,29 @@ function compute_nonce(iv, packetNumber) {
148
139
  return nonce;
149
140
  }
150
141
 
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);
142
+ // The AEAD of a key set (RFC 9001 §5.3): named by the TLS 1.3 suite that
143
+ // derived it — not guessed from the key's length, which is 32 bytes for both
144
+ // AES-256-GCM and ChaCha20-Poly1305. A set without one (Initial keys, which
145
+ // are always AES-128-GCM — §5.2) goes by its length, AES.
146
+ function aead_for_suite(cipher, keyLen) {
147
+ if (cipher === 0x1303) return 'chacha20-poly1305';
148
+ return keyLen === 32 ? 'aes-256-gcm' : 'aes-128-gcm';
149
+ }
150
+ function aead_algo(key, aead) {
151
+ if (aead) return aead;
152
+ if (key.length === 16) return 'aes-128-gcm';
153
+ if (key.length === 32) return 'aes-256-gcm';
154
+ throw new Error('Unsupported key length: ' + key.length);
155
155
  }
156
156
 
157
- function aead_encrypt(key, iv, packetNumber, plaintext, aad) {
157
+ function aead_encrypt(key, iv, packetNumber, plaintext, aad, aead) {
158
158
  try {
159
159
  var nonce = compute_nonce(iv, packetNumber);
160
- var cipher = crypto.createCipheriv(aead_algo(key.length), key, nonce);
160
+ var algo = aead_algo(key, aead);
161
+ // Node takes the tag length for ChaCha20-Poly1305 explicitly; 16, as GCM.
162
+ var cipher = algo === 'chacha20-poly1305'
163
+ ? crypto.createCipheriv(algo, key, nonce, { authTagLength: 16 })
164
+ : crypto.createCipheriv(algo, key, nonce);
161
165
  cipher.setAAD(aad);
162
166
 
163
167
  var encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
@@ -172,14 +176,25 @@ function aead_encrypt(key, iv, packetNumber, plaintext, aad) {
172
176
  }
173
177
  }
174
178
 
175
- function aead_decrypt(key, nonce, ciphertext, tag, aad) {
179
+ function aead_decrypt(key, nonce, ciphertext, tag, aad, aead) {
176
180
  try {
177
- var decipher = crypto.createDecipheriv(aead_algo(key.length), key, nonce);
181
+ var algo = aead_algo(key, aead);
182
+ var decipher = algo === 'chacha20-poly1305'
183
+ ? crypto.createDecipheriv(algo, key, nonce, { authTagLength: 16 })
184
+ : crypto.createDecipheriv(algo, key, nonce);
178
185
  decipher.setAuthTag(tag);
179
186
  decipher.setAAD(aad);
180
187
 
181
- var decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
182
- return new Uint8Array(decrypted);
188
+ // Stream modes: update() is the whole plaintext, final() checks the tag
189
+ // and gives nothing — unless a build ever does, then they are joined.
190
+ var pt = decipher.update(ciphertext);
191
+ var rest = decipher.final();
192
+ if (rest.length) pt = Buffer.concat([pt, rest]);
193
+ // A Uint8Array over the plaintext's own memory — a copy only if it is a
194
+ // slice of a larger (pooled) buffer, which the stream data handed on
195
+ // from it must not keep alive.
196
+ if (pt.byteOffset === 0 && pt.buffer.byteLength === pt.length) return new Uint8Array(pt.buffer, 0, pt.length);
197
+ return new Uint8Array(pt);
183
198
  } catch (e) {
184
199
  return null;
185
200
  }
@@ -187,7 +202,74 @@ function aead_decrypt(key, nonce, ciphertext, tag, aad) {
187
202
 
188
203
 
189
204
  // ============================================================
190
- // Header protection (AES-ECB mask)
205
+ // Retry packets (RFC 9000 §17.2.5, RFC 9001 §5.8)
206
+ // ============================================================
207
+
208
+ // The Retry Integrity Tag: AES-128-GCM under a key and nonce fixed by each
209
+ // version (RFC 9001 §5.8, RFC 9369 §3.3.3), over nothing, authenticating the
210
+ // "Retry Pseudo-Packet" — the client's original DCID (length-prefixed)
211
+ // followed by the Retry packet itself. The key is public: the tag proves not
212
+ // who sent the Retry but that it answers this client's Initial, which only
213
+ // something that saw that Initial could know the DCID of.
214
+ function retry_integrity_tag(originalDcid, retryWithoutTag, version) {
215
+ var profile = profileOf(version);
216
+ var aad = new Uint8Array(1 + originalDcid.length + retryWithoutTag.length);
217
+ aad[0] = originalDcid.length;
218
+ aad.set(originalDcid, 1);
219
+ aad.set(retryWithoutTag, 1 + originalDcid.length);
220
+ var cipher = crypto.createCipheriv('aes-128-gcm', profile.retryKey, profile.retryNonce);
221
+ cipher.setAAD(aad);
222
+ cipher.final();
223
+ return new Uint8Array(cipher.getAuthTag());
224
+ }
225
+
226
+ /**
227
+ * A Retry packet (§17.2.5): to `dcid` (the client's SCID), from `scid` (the
228
+ * ID the client is to use next), carrying `token`, tagged for the client's
229
+ * original DCID — in `version`, the version of the client's Initial (a Retry
230
+ * is always in it, RFC 9369 §4.1).
231
+ */
232
+ function build_retry_packet(dcid, scid, token, originalDcid, version) {
233
+ if (version === undefined) version = QUIC_V1;
234
+ var head = new Uint8Array(1 + 4 + 1 + dcid.length + 1 + scid.length + token.length);
235
+ var o = 0;
236
+ // Long header, fixed bit, the version's Retry type; the low four bits are
237
+ // unused and random.
238
+ head[o++] = 0xc0 | (longTypeBits(version, 'retry') << 4) | (crypto.randomBytes(1)[0] & 0x0f);
239
+ head[o++] = (version >>> 24) & 0xff; head[o++] = (version >>> 16) & 0xff;
240
+ head[o++] = (version >>> 8) & 0xff; head[o++] = version & 0xff;
241
+ head[o++] = dcid.length; head.set(dcid, o); o += dcid.length;
242
+ head[o++] = scid.length; head.set(scid, o); o += scid.length;
243
+ head.set(token, o);
244
+ var out = new Uint8Array(head.length + 16);
245
+ out.set(head, 0);
246
+ out.set(retry_integrity_tag(originalDcid, head, version), head.length);
247
+ return out;
248
+ }
249
+
250
+ /**
251
+ * A client's view of a Retry packet it received (the whole packet, `raw`):
252
+ * { scid, token } if its tag is right for `originalDcid`, else null.
253
+ */
254
+ function open_retry_packet(raw, originalDcid) {
255
+ if (raw.length < 1 + 4 + 1 + 1 + 16) return null;
256
+ var version = ((raw[1] << 24) | (raw[2] << 16) | (raw[3] << 8) | raw[4]) >>> 0;
257
+ if (!versionProfile(version)) return null;
258
+ var o = 5;
259
+ var dcidLen = raw[o++]; o += dcidLen;
260
+ if (o >= raw.length) return null;
261
+ var scidLen = raw[o++];
262
+ var scid = raw.slice(o, o + scidLen); o += scidLen;
263
+ if (o > raw.length - 16) return null;
264
+ var body = raw.subarray(0, raw.length - 16);
265
+ var expected = retry_integrity_tag(originalDcid, body, version);
266
+ if (!crypto.timingSafeEqual(expected, raw.subarray(raw.length - 16))) return null;
267
+ return { scid: scid, token: raw.slice(o, raw.length - 16) };
268
+ }
269
+
270
+
271
+ // ============================================================
272
+ // Header protection (RFC 9001 §5.4)
191
273
  // ============================================================
192
274
 
193
275
  function aes_ecb_encrypt(key, plaintext) {
@@ -197,11 +279,45 @@ function aes_ecb_encrypt(key, plaintext) {
197
279
  return new Uint8Array(Buffer.concat([cipher.update(plaintext), cipher.final()]));
198
280
  }
199
281
 
200
- function apply_header_protection(packet, pnOffset, hpKey, pnLength) {
201
- var sample = packet.slice(pnOffset + 4, pnOffset + 4 + 16);
282
+ // The AES header-protection cipher of a key, made once and kept with it.
283
+ // ECB carries nothing from one block to the next and, with padding off and a
284
+ // whole block in, update() gives the whole block out — so one cipher serves
285
+ // every packet of the key's life. One was created per packet, on each side:
286
+ // about 3% of all CPU in a bulk transfer, for a 16-byte encryption. Keyed by
287
+ // the key array itself (a key set's `hp`, which lives as long as the keys),
288
+ // and forgotten with it.
289
+ var hpCiphers = new WeakMap();
290
+ function aes_hp_mask(key, sample) {
291
+ var c = hpCiphers.get(key);
292
+ if (!c) {
293
+ c = crypto.createCipheriv(key.length === 32 ? 'aes-256-ecb' : 'aes-128-ecb', key, null);
294
+ c.setAutoPadding(false);
295
+ hpCiphers.set(key, c);
296
+ }
297
+ return c.update(sample);
298
+ }
299
+
300
+ var ZERO5 = new Uint8Array(5);
301
+ // The 5-byte mask from a 16-byte sample. AES (§5.4.3): the sample encrypted
302
+ // under the HP key. ChaCha20 (§5.4.4): the sample's first 4 bytes are the
303
+ // block counter (little-endian), the other 12 the nonce, and the mask is
304
+ // ChaCha20 of five zero bytes — which is what node:crypto's 'chacha20' does
305
+ // with the sample as its 16-byte IV (counter ‖ nonce, the counter LE).
306
+ function hp_mask(hpKey, sample, aead) {
307
+ if (aead === 'chacha20-poly1305') {
308
+ var c = crypto.createCipheriv('chacha20', hpKey, sample); // the sample is the IV: one per packet
309
+ return new Uint8Array(c.update(ZERO5));
310
+ }
311
+ return aes_hp_mask(hpKey, sample);
312
+ }
313
+
314
+ function apply_header_protection(packet, pnOffset, hpKey, pnLength, aead) {
315
+ // A view, not a copy: the mask is computed from it before anything is
316
+ // changed, and nothing it covers is changed.
317
+ var sample = packet.subarray(pnOffset + 4, pnOffset + 4 + 16);
202
318
  if (sample.length < 16) throw new Error('Not enough bytes for HP sample');
203
319
 
204
- var mask = aes_ecb_encrypt(hpKey, sample);
320
+ var mask = hp_mask(hpKey, sample, aead);
205
321
  var isLong = (packet[0] & 0x80) !== 0;
206
322
 
207
323
  packet[0] ^= mask[0] & (isLong ? 0x0f : 0x1f);
@@ -212,9 +328,10 @@ function apply_header_protection(packet, pnOffset, hpKey, pnLength) {
212
328
  return packet;
213
329
  }
214
330
 
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);
331
+ function remove_header_protection(array, pnOffset, hpKey, isShort, aead) {
332
+ var sample = array.subarray(pnOffset + 4, pnOffset + 4 + 16);
333
+ if (sample.length < 16) return null; // too short to carry a sample: not a packet
334
+ var mask = hp_mask(hpKey, sample, aead);
218
335
 
219
336
  if (isShort) {
220
337
  array[0] ^= mask[0] & 0x1f;
@@ -277,12 +394,14 @@ function encode_packet_number(packetNumber) {
277
394
  /**
278
395
  * Build QUIC header for a packet (before encryption).
279
396
  */
280
- function build_quic_header(packetType, dcid, scid, token, lengthField, pnLen, keyPhase) {
397
+ // greaseBit: send the fixed ("QUIC") bit as 0 (RFC 9287) — only to a peer
398
+ // that advertised grease_quic_bit.
399
+ function build_quic_header(packetType, dcid, scid, token, lengthField, pnLen, keyPhase, version, greaseBit) {
281
400
  var hdr = [];
282
401
  var firstByte;
283
402
 
284
403
  if (packetType === '1rtt') {
285
- firstByte = 0x40 | ((pnLen - 1) & 0x03);
404
+ firstByte = (greaseBit ? 0 : 0x40) | ((pnLen - 1) & 0x03);
286
405
  if (keyPhase) firstByte |= 0x04; // key_phase bit
287
406
  hdr.push(Uint8Array.of(firstByte));
288
407
  hdr.push(dcid);
@@ -290,19 +409,15 @@ function build_quic_header(packetType, dcid, scid, token, lengthField, pnLen, ke
290
409
  return { header: header, pnOffset: header.length };
291
410
  }
292
411
 
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 {
412
+ // Long header: the type bits are the version's (RFC 9369 §3.2).
413
+ if (version === undefined) version = QUIC_V1;
414
+ if (packetType !== 'initial' && packetType !== 'handshake' && packetType !== '0rtt') {
301
415
  throw new Error('Unsupported packet type: ' + packetType);
302
416
  }
417
+ firstByte = (greaseBit ? 0x80 : 0xc0) | (longTypeBits(version, packetType) << 4) | ((pnLen - 1) & 0x03);
303
418
 
304
419
  hdr.push(Uint8Array.of(firstByte));
305
- hdr.push(new Uint8Array([0x00, 0x00, 0x00, 0x01])); // version 1
420
+ hdr.push(new Uint8Array([(version >>> 24) & 0xff, (version >>> 16) & 0xff, (version >>> 8) & 0xff, version & 0xff]));
306
421
  hdr.push(writeVarInt(dcid.length), dcid);
307
422
  hdr.push(writeVarInt(scid.length), scid);
308
423
 
@@ -321,14 +436,14 @@ function build_quic_header(packetType, dcid, scid, token, lengthField, pnLen, ke
321
436
  /**
322
437
  * Encrypt a QUIC packet (frames → encrypted packet with HP).
323
438
  */
324
- function encrypt_quic_packet(packetType, encodedFrames, writeKey, writeIv, writeHp, packetNumber, dcid, scid, token, keyPhase) {
439
+ function encrypt_quic_packet(packetType, encodedFrames, writeKey, writeIv, writeHp, packetNumber, dcid, scid, token, keyPhase, aead, version, greaseBit) {
325
440
  var pn = encode_packet_number(packetNumber);
326
441
  var pnLength = pn.length;
327
442
  var pnBytes = pn.bytes;
328
443
 
329
444
  var payloadLen = encodedFrames.length + pnLength + 16; // 16 = GCM tag
330
445
  var lengthField = writeVarInt(payloadLen);
331
- var hdrInfo = build_quic_header(packetType, dcid, scid, token, lengthField, pnLength, keyPhase);
446
+ var hdrInfo = build_quic_header(packetType, dcid, scid, token, lengthField, pnLength, keyPhase, version, greaseBit);
332
447
 
333
448
  var header = hdrInfo.header;
334
449
  var pnOffset = hdrInfo.pnOffset;
@@ -345,20 +460,46 @@ function encrypt_quic_packet(packetType, encodedFrames, writeKey, writeIv, write
345
460
  // Rebuild header with new length
346
461
  payloadLen = encodedFrames.length + pnLength + 16;
347
462
  lengthField = writeVarInt(payloadLen);
348
- hdrInfo = build_quic_header(packetType, dcid, scid, token, lengthField, pnLength, keyPhase);
463
+ hdrInfo = build_quic_header(packetType, dcid, scid, token, lengthField, pnLength, keyPhase, version, greaseBit);
349
464
  header = hdrInfo.header;
350
465
  pnOffset = hdrInfo.pnOffset;
351
466
  }
352
467
 
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]);
468
+ // The packet built in place, in one allocation: header and packet number,
469
+ // which are the AAD as they stand, then the ciphertext and the tag written
470
+ // after them. It took five copies — the AAD, the ciphertext concatenated,
471
+ // ciphertext and tag into one array, all of it into the packet — about 5%
472
+ // of a bulk transfer's CPU with the garbage they left.
473
+ var aadLen = header.length + pnLength;
474
+ var fullPacket = new Uint8Array(aadLen + encodedFrames.length + 16);
475
+ fullPacket.set(header, 0);
476
+ fullPacket.set(pnBytes, header.length);
477
+ if (!aead_seal_into(writeKey, writeIv, packetNumber, encodedFrames, fullPacket, aadLen, aead)) return null;
478
+
479
+ return apply_header_protection(fullPacket, pnOffset, writeHp, pnLength, aead);
480
+ }
360
481
 
361
- return apply_header_protection(fullPacket, pnOffset, writeHp, pnLength);
482
+ // AEAD-seal `plaintext` with out[0..aadLen) as the AAD, writing the
483
+ // ciphertext and then the tag into `out` from aadLen. false on failure.
484
+ // GCM and ChaCha20-Poly1305 are stream modes: update() gives back as many
485
+ // bytes as it is given, and final() none.
486
+ function aead_seal_into(key, iv, packetNumber, plaintext, out, aadLen, aead) {
487
+ try {
488
+ var algo = aead_algo(key, aead);
489
+ var nonce = compute_nonce(iv, packetNumber);
490
+ var cipher = algo === 'chacha20-poly1305'
491
+ ? crypto.createCipheriv(algo, key, nonce, { authTagLength: 16 })
492
+ : crypto.createCipheriv(algo, key, nonce);
493
+ cipher.setAAD(out.subarray(0, aadLen));
494
+ var ct = cipher.update(plaintext);
495
+ var rest = cipher.final();
496
+ out.set(ct, aadLen);
497
+ if (rest.length) out.set(rest, aadLen + ct.length);
498
+ out.set(cipher.getAuthTag(), aadLen + ct.length + rest.length);
499
+ return true;
500
+ } catch (e) {
501
+ return false;
502
+ }
362
503
  }
363
504
 
364
505
 
@@ -370,9 +511,26 @@ function encrypt_quic_packet(packetType, encodedFrames, writeKey, writeIv, write
370
511
  * receiver must already know its own CID length to locate the packet number.
371
512
  * Long headers encode the DCID length on the wire, so `ownCid` is ignored there.
372
513
  */
373
- function decrypt_quic_packet(array, readKey, readIv, readHp, ownCid, largestPn) {
514
+ function decrypt_quic_packet(array, readKey, readIv, readHp, ownCid, largestPn, aead) {
374
515
  if (!(array instanceof Uint8Array)) throw new Error('Invalid input');
375
516
 
517
+ // Work on a private copy — this function must not mutate its input.
518
+ //
519
+ // remove_header_protection XORs the first byte and the packet-number bytes
520
+ // IN PLACE. That is fine for exactly one decryption attempt, but the key
521
+ // update path retries this function on the same packet with derived keys
522
+ // (RFC 9001 §6): the second call re-applies the (unchanged) HP mask to a
523
+ // header that is already unprotected, re-scrambling it — and reads pnLength
524
+ // from the scrambled byte, so it may even XOR the wrong number of bytes.
525
+ // The retry then fails even with the correct keys, every phase-flipped
526
+ // packet is dropped, and the connection goes permanently deaf in that
527
+ // direction. Measured against quic-go (which key-updates shortly after
528
+ // handshake confirmation): ACK processing died at t≈2s, BBR froze mid-
529
+ // startup, and the sender spent 58s retransmitting one 8 KB band 29 times.
530
+ //
531
+ // One ≤1500 B copy per decryption attempt is noise next to the AEAD itself.
532
+ array = array.slice();
533
+
376
534
  var firstByte = array[0];
377
535
  var isShort = (firstByte & 0x80) === 0;
378
536
 
@@ -386,8 +544,9 @@ function decrypt_quic_packet(array, readKey, readIv, readHp, ownCid, largestPn)
386
544
  var scidLen = array[offset++];
387
545
  offset += scidLen;
388
546
 
389
- var typeBits = (firstByte & 0x30) >> 4;
390
- if (typeBits === 0) { // Initial — read token
547
+ // Initial — read token. Which type bits mean Initial is the version's.
548
+ var hdrVersion = ((array[1] << 24) | (array[2] << 16) | (array[3] << 8) | array[4]) >>> 0;
549
+ if (longPacketType(hdrVersion, (firstByte & 0x30) >> 4) === 'initial') {
391
550
  var tokenLen = readVarInt(array, offset);
392
551
  offset += tokenLen.byteLength + tokenLen.value;
393
552
  }
@@ -396,7 +555,7 @@ function decrypt_quic_packet(array, readKey, readIv, readHp, ownCid, largestPn)
396
555
  offset += len.byteLength;
397
556
 
398
557
  pnOffset = offset;
399
- pnLength = remove_header_protection(array, pnOffset, readHp, false);
558
+ pnLength = remove_header_protection(array, pnOffset, readHp, false, aead);
400
559
  if (pnLength === null) return null;
401
560
 
402
561
  packetNumber = expandPacketNumber(
@@ -411,19 +570,20 @@ function decrypt_quic_packet(array, readKey, readIv, readHp, ownCid, largestPn)
411
570
 
412
571
  if (payloadEnd > array.length) return null;
413
572
 
414
- var payload = array.slice(payloadStart, payloadEnd);
573
+ // Views on the private copy above — nothing changes it from here on.
574
+ var payload = array.subarray(payloadStart, payloadEnd);
415
575
  if (payload.length < 16) return null;
416
576
 
417
- ciphertext = payload.slice(0, payload.length - 16);
418
- tag = payload.slice(payload.length - 16);
419
- aad = array.slice(0, pnOffset + pnLength);
577
+ ciphertext = payload.subarray(0, payload.length - 16);
578
+ tag = payload.subarray(payload.length - 16);
579
+ aad = array.subarray(0, pnOffset + pnLength);
420
580
 
421
581
  } else {
422
582
  // Short Header
423
583
  var dcidLen = ownCid.length;
424
584
  pnOffset = 1 + dcidLen;
425
585
 
426
- pnLength = remove_header_protection(array, pnOffset, readHp, true);
586
+ pnLength = remove_header_protection(array, pnOffset, readHp, true, aead);
427
587
  if (pnLength === null) return null;
428
588
 
429
589
  keyPhase = Boolean((array[0] & 0x04) >>> 2);
@@ -434,15 +594,15 @@ function decrypt_quic_packet(array, readKey, readIv, readHp, ownCid, largestPn)
434
594
  );
435
595
  nonce = compute_nonce(readIv, packetNumber);
436
596
 
437
- var payload = array.slice(pnOffset + pnLength);
597
+ var payload = array.subarray(pnOffset + pnLength);
438
598
  if (payload.length < 16) return null;
439
599
 
440
- ciphertext = payload.slice(0, payload.length - 16);
441
- tag = payload.slice(payload.length - 16);
442
- aad = array.slice(0, pnOffset + pnLength);
600
+ ciphertext = payload.subarray(0, payload.length - 16);
601
+ tag = payload.subarray(payload.length - 16);
602
+ aad = array.subarray(0, pnOffset + pnLength);
443
603
  }
444
604
 
445
- var plaintext = aead_decrypt(readKey, nonce, ciphertext, tag, aad);
605
+ var plaintext = aead_decrypt(readKey, nonce, ciphertext, tag, aad, aead);
446
606
 
447
607
  return {
448
608
  packet_number: packetNumber,
@@ -452,54 +612,8 @@ function decrypt_quic_packet(array, readKey, readIv, readHp, ownCid, largestPn)
452
612
  }
453
613
 
454
614
 
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
- }
615
+ // (CRYPTO frames are reassembled into TLS messages in quic_connection.js,
616
+ // processCryptoFrame.)
503
617
 
504
618
 
505
619
  // ============================================================
@@ -522,8 +636,11 @@ export {
522
636
  encrypt_quic_packet,
523
637
  decrypt_quic_packet,
524
638
 
639
+ // Retry
640
+ build_retry_packet,
641
+ open_retry_packet,
642
+
525
643
  // TLS message reassembly
526
- extract_tls_messages_from_chunks,
527
644
 
528
645
  // Low-level (for testing)
529
646
  compute_nonce,