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/README.md +308 -70
- package/index.cjs +45 -0
- package/index.d.ts +158 -0
- package/index.js +46 -2631
- package/package.json +69 -14
- package/src/agent.js +275 -0
- package/src/client.js +439 -0
- package/src/crypto.js +519 -0
- package/src/h3.js +911 -0
- package/src/h3_client.js +317 -0
- package/src/h3_server.js +523 -0
- package/src/quic_connection.js +1238 -0
- package/src/server.js +346 -0
- package/src/streams.js +471 -0
- package/src/tls_bridge.js +211 -0
- package/src/transport.js +651 -0
- package/src/utils.js +290 -0
- package/src/webtransport.js +708 -0
- package/CHANGELOG.md +0 -25
- package/examples/webtransport_datagram_echo_server/client.html +0 -58
- package/examples/webtransport_datagram_echo_server/server.js +0 -29
- package/libs/crypto.js +0 -2107
- package/libs/h3.js +0 -1214
- package/libs/utils.js +0 -404
- package/quico.svg +0 -1
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
|
+
};
|