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/README.md +2489 -147
- package/index.js +56 -34
- package/package.json +3 -3
- package/src/agent.js +364 -36
- package/src/client.js +474 -118
- package/src/client_profile.js +266 -0
- package/src/connect_udp.js +578 -0
- package/src/connect_udp_client.js +770 -0
- package/src/crypto.js +257 -140
- package/src/h1.js +953 -0
- package/src/h1_client.js +731 -0
- package/src/h1_server.js +624 -0
- package/src/h2.js +1325 -0
- package/src/h2_client.js +675 -0
- package/src/h2_fingerprint.js +403 -0
- package/src/h2_frames.js +582 -0
- package/src/h2_server.js +862 -0
- package/src/h3.js +1966 -469
- package/src/h3_client.js +476 -48
- package/src/h3_server.js +1349 -94
- package/src/hpack.js +455 -0
- package/src/hpack_primitives.js +520 -0
- package/src/http_common.js +901 -0
- package/src/ja4h.js +256 -0
- package/src/qpack.js +555 -0
- package/src/quic_connection.js +6666 -632
- package/src/quic_server.js +822 -69
- package/src/quic_socket.js +183 -7
- package/src/quic_version.js +143 -0
- package/src/server.js +843 -115
- package/src/server_stats.js +93 -0
- package/src/streams.js +376 -36
- package/src/tcp_listener.js +557 -0
- package/src/tls_bridge.js +516 -75
- package/src/tls_options.js +108 -0
- package/src/transport.js +541 -39
- package/src/utils.js +94 -12
- package/src/webtransport.js +433 -436
- package/src/ws.js +624 -0
- package/src/ws_client.js +641 -0
- package/src/ws_server.js +833 -0
- package/src/wt_stream.js +618 -0
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
77
|
-
var
|
|
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,
|
|
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,
|
|
89
|
-
iv: hkdf_expand_label(hashName, side_secret,
|
|
90
|
-
hp: hkdf_expand_label(hashName, side_secret,
|
|
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,
|
|
109
|
-
iv: hkdf_expand_label(hashName, traffic_secret,
|
|
110
|
-
hp: hkdf_expand_label(hashName, traffic_secret,
|
|
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
|
-
|
|
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,
|
|
131
|
-
iv: hkdf_expand_label(hashName, next_secret,
|
|
132
|
-
hp: hkdf_expand_label(hashName, next_secret,
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
182
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
201
|
-
|
|
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 =
|
|
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.
|
|
217
|
-
|
|
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
|
-
|
|
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 (
|
|
295
|
-
|
|
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([
|
|
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
|
-
//
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
var fullPacket =
|
|
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
|
-
|
|
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
|
-
|
|
390
|
-
|
|
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
|
-
|
|
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.
|
|
418
|
-
tag = payload.
|
|
419
|
-
aad = array.
|
|
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.
|
|
597
|
+
var payload = array.subarray(pnOffset + pnLength);
|
|
438
598
|
if (payload.length < 16) return null;
|
|
439
599
|
|
|
440
|
-
ciphertext = payload.
|
|
441
|
-
tag = payload.
|
|
442
|
-
aad = array.
|
|
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
|
-
//
|
|
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,
|