UNPKG

quico

Version:

A pure JavaScript implementation of QUIC, HTTP/3, QPACK, and WebTransport for Node.js

1,537 lines (1,253 loc) 69.4 kB
/* * quico: HTTP/3 and QUIC implementation for Node.js * Copyright 2025 colocohen * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at: * * https://www.apache.org/licenses/LICENSE-2.0 * * This file is part of the open-source project hosted at: * https://github.com/colocohen/quico * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. */ var nobleHashes={ hmac: require("@noble/hashes/hmac.js")['hmac'], hkdf: require("@noble/hashes/hkdf.js")['hkdf'], hkdf_extract: require("@noble/hashes/hkdf.js")['extract'], hkdf_expand: require("@noble/hashes/hkdf.js")['expand'], sha256: require("@noble/hashes/sha2.js")['sha256'], }; var { p256 } = require('@noble/curves/p256'); var { x25519 } = require('@noble/curves/ed25519'); var { sha256, sha384 } = require('@noble/hashes/sha2'); var crypto = require('crypto'); var { AES } = require('@stablelib/aes'); var { GCM } = require('@stablelib/gcm'); var x509 = require('@peculiar/x509'); var { concatUint8Arrays, writeVarInt, readVarInt } = require('./utils'); function get_cipher_info(cipher_suite) { switch (cipher_suite) { case 0x1301: // TLS_AES_128_GCM_SHA256 return { keylen: 16, ivlen: 12, hash: sha256,str: 'sha256' }; case 0x1302: // TLS_AES_256_GCM_SHA384 return { keylen: 32, ivlen: 12, hash: sha384,str: 'sha384' }; case 0x1303: // TLS_CHACHA20_POLY1305_SHA256 return { keylen: 32, ivlen: 12, hash: sha256,str: 'sha256' }; default: throw new Error("Unsupported cipher suite: 0x" + cipher_suite.toString(16)); } } function build_server_hello(server_random, public_key, session_id, cipher_suite, group) { var legacy_version = [0x03, 0x03]; var random = Array.from(server_random); var session_id_bytes = Array.from(session_id); var session_id_length = session_id_bytes.length & 0xff; var cipher_suite_bytes = [(cipher_suite >> 8) & 0xff, cipher_suite & 0xff]; var compression_method = [0x00]; var key = Array.from(public_key); var key_length = [(key.length >> 8) & 0xff, key.length & 0xff]; var group_bytes = [(group >> 8) & 0xff, group & 0xff]; var key_exchange = [...group_bytes, ...key_length, ...key]; var key_share_extension = (() => { var extension_type = [0x00, 0x33]; var extension_length = [(key_exchange.length >> 8) & 0xff, key_exchange.length & 0xff]; return [...extension_type, ...extension_length, ...key_exchange]; })(); var supported_versions_extension = [ 0x00, 0x2b, 0x00, 0x02, 0x03, 0x04 ]; var params_bytes = [ 0x00, 0x01, 0x00, 0x04, 0x00, 0x00, 0x10, 0x00, // initial_max_data = 4096 0x00, 0x03, 0x00, 0x04, 0x00, 0x00, 0x08, 0x00 // max_packet_size = 2048 ]; var extensions = [ ...supported_versions_extension, ...key_share_extension ]; var extensions_length = [(extensions.length >> 8) & 0xff, extensions.length & 0xff]; var handshake_body = [ ...legacy_version, ...random, session_id_length, ...session_id_bytes, ...cipher_suite_bytes, ...compression_method, ...extensions_length, ...extensions ]; var body_length = handshake_body.length; var handshake = [ 0x02, // handshake type: ServerHello (body_length >> 16) & 0xff, (body_length >> 8) & 0xff, body_length & 0xff, ...handshake_body ]; return Uint8Array.from(handshake); // ✔️ מחזיר רק Handshake Message } function build_quic_ext(params) { var out = []; function addParam(id, value) { var id_bytes = writeVarInt(id); var length_bytes, value_bytes; if (typeof value === 'number') { value_bytes = writeVarInt(value); } else if (value instanceof Uint8Array) { value_bytes = Array.from(value); } else if (value === true) { value_bytes = []; // for disable_active_migration } else { throw new Error('Unsupported value type for parameter ' + id); } length_bytes = writeVarInt(value_bytes.length); out.push(...id_bytes, ...length_bytes, ...value_bytes); } if (params.original_destination_connection_id) addParam(0x00, params.original_destination_connection_id); if (params.max_idle_timeout) addParam(0x01, params.max_idle_timeout); if (params.stateless_reset_token) addParam(0x02, params.stateless_reset_token); if (params.max_udp_payload_size) addParam(0x03, params.max_udp_payload_size); if (params.initial_max_data) addParam(0x04, params.initial_max_data); if (params.initial_max_stream_data_bidi_local) addParam(0x05, params.initial_max_stream_data_bidi_local); if (params.initial_max_stream_data_bidi_remote) addParam(0x06, params.initial_max_stream_data_bidi_remote); if (params.initial_max_stream_data_uni) addParam(0x07, params.initial_max_stream_data_uni); if (params.initial_max_streams_bidi) addParam(0x08, params.initial_max_streams_bidi); if (params.initial_max_streams_uni) addParam(0x09, params.initial_max_streams_uni); if (params.ack_delay_exponent !== undefined) addParam(0x0a, params.ack_delay_exponent); if (params.max_ack_delay !== undefined) addParam(0x0b, params.max_ack_delay); if (params.disable_active_migration) addParam(0x0c, true); if (params.active_connection_id_limit) addParam(0x0e, params.active_connection_id_limit); if (params.initial_source_connection_id) addParam(0x0f, params.initial_source_connection_id); if (params.retry_source_connection_id) addParam(0x10, params.retry_source_connection_id); if (params.max_datagram_frame_size) addParam(0x20, params.max_datagram_frame_size); // אין ערך – presence בלבד if (params.web_accepted_origins) { for (var i = 0; i < params.web_accepted_origins.length; i++) { var origin = params.web_accepted_origins[i]; var origin_bytes = new TextEncoder().encode(origin); addParam(0x2b603742, origin_bytes); } } return new Uint8Array(out); } function build_alpn_ext(protocol) { var proto_bytes = new TextEncoder().encode(protocol); var ext = new Uint8Array(2 + 1 + proto_bytes.length); ext[0] = 0x00; ext[1] = proto_bytes.length + 1; ext[2] = proto_bytes.length; ext.set(proto_bytes, 3); return ext; } function build_encrypted_extensions(extensions) { var ext_bytes = []; for (var ext of extensions) { ext_bytes.push((ext.type >> 8) & 0xff, ext.type & 0xff); ext_bytes.push((ext.data.length >> 8) & 0xff, ext.data.length & 0xff); ext_bytes.push(...ext.data); } var ext_len = ext_bytes.length; var ext_len_bytes = [(ext_len >> 8) & 0xff, ext_len & 0xff]; var body = [...ext_len_bytes, ...ext_bytes]; var hs_len = body.length; var header = [0x08, (hs_len >> 16) & 0xff, (hs_len >> 8) & 0xff, hs_len & 0xff]; return Uint8Array.from([...header, ...body]); } function build_certificate(certificates) { var context = [0x00]; var cert_list = []; for (var cert of certificates) { var extensions = cert.extensions instanceof Uint8Array ? cert.extensions : new Uint8Array(0); cert_list.push((cert.cert.length >> 16) & 0xff, (cert.cert.length >> 8) & 0xff, cert.cert.length & 0xff); cert_list.push(...cert.cert); cert_list.push((extensions.length >> 8) & 0xff, extensions.length & 0xff); cert_list.push(...extensions); } var total_len = cert_list.length; var list_len = [(total_len >> 16) & 0xff, (total_len >> 8) & 0xff, total_len & 0xff]; var body = [...context, ...list_len, ...cert_list]; var hs_len = body.length; var header = [0x0b, (hs_len >> 16) & 0xff, (hs_len >> 8) & 0xff, hs_len & 0xff]; return Uint8Array.from([...header, ...body]); } function build_certificate_verify(algorithm, signature) { var sig_len = signature.length; var total_len = 4 + sig_len; var header = [ 0x0f, (total_len >> 16) & 0xff, (total_len >> 8) & 0xff, total_len & 0xff, (algorithm >> 8) & 0xff, algorithm & 0xff, (sig_len >> 8) & 0xff, sig_len & 0xff ]; return Uint8Array.from([...header, ...signature]); } function build_finished(verify_data) { var length = verify_data.length; var header = [0x14, (length >> 16) & 0xff, (length >> 8) & 0xff, length & 0xff]; return Uint8Array.from([...header, ...verify_data]); } function handle_client_hello(parsed) { var supported_groups = [0x001d, 0x0017]; // X25519, secp256r1 var supported_cipher_suites = [0x1301, 0x1302];//0x1303, var selected_alpn=null; var selected_group=null; var selected_cipher=null; var client_public_key=null; var server_private_key=null; var server_public_key=null; var shared_secret=null; for(var i in supported_cipher_suites){ if(parsed.cipher_suites.includes(supported_cipher_suites[i])==true){ selected_cipher=supported_cipher_suites[i]; break; } } for(var i in supported_groups){ if(selected_group==null){ for(var i2 in parsed.key_shares){ if(parsed.key_shares[i2].group==supported_groups[i]){ selected_group=parsed.key_shares[i2].group; client_public_key=parsed.key_shares[i2].pubkey; break; } } } } if(selected_group!==null){ if (selected_group === 0x001d) { // X25519 server_private_key = crypto.randomBytes(32); server_public_key = x25519.getPublicKey(server_private_key); shared_secret = x25519.getSharedSecret(server_private_key, client_public_key); } else if (selected_group === 0x0017) { // secp256r1 (P-256) server_private_key = p256.utils.randomPrivateKey(); server_public_key = p256.getPublicKey(server_private_key, false); var client_point = p256.ProjectivePoint.fromHex(client_public_key); var shared_point = client_point.multiply( BigInt('0x' + Buffer.from(server_private_key).toString('hex')) ); shared_secret = shared_point.toRawBytes().slice(0, 32); } } return { selected_cipher: selected_cipher, selected_group: selected_group, client_public_key: client_public_key, server_private_key: new Uint8Array(server_private_key), server_public_key: server_public_key, shared_secret: shared_secret } } function parse_transport_parameters(buf, start) { if (!(buf instanceof Uint8Array)) throw new Error("Expect Uint8Array"); var offset = start || 0; var end = buf.length; var out = { web_accepted_origins: [] }; while (offset < end) { // ---- מזהה הפרמטר ---- var idVar = readVarInt(buf, offset); if (!idVar) throw new Error("Bad varint (id) at " + offset); offset += idVar.byteLength; var id = idVar.value; // ---- אורך הערך ---- var lenVar = readVarInt(buf, offset); if (!lenVar) throw new Error("Bad varint (len) at " + offset); offset += lenVar.byteLength; var length = lenVar.value; if (offset + length > end) throw new Error("Truncated value for id " + id); var valueBytes = buf.slice(offset, offset + length); offset += length; // ---- פענוח לפי ID ---- switch (id) { case 0x00: out.original_destination_connection_id = valueBytes; break; case 0x01: out.max_idle_timeout = readVarInt(valueBytes, 0).value; break; case 0x02: if (valueBytes.length !== 16) throw new Error("stateless_reset_token len≠16"); out.stateless_reset_token = valueBytes; break; case 0x03: out.max_udp_payload_size = readVarInt(valueBytes, 0).value; break; case 0x04: out.initial_max_data = readVarInt(valueBytes, 0).value; break; case 0x05: out.initial_max_stream_data_bidi_local = readVarInt(valueBytes, 0).value; break; case 0x06: out.initial_max_stream_data_bidi_remote = readVarInt(valueBytes, 0).value; break; case 0x07: out.initial_max_stream_data_uni = readVarInt(valueBytes, 0).value; break; case 0x08: out.initial_max_streams_bidi = readVarInt(valueBytes, 0).value; break; case 0x09: out.initial_max_streams_uni = readVarInt(valueBytes, 0).value; break; case 0x0a: out.ack_delay_exponent = readVarInt(valueBytes, 0).value; break; case 0x0b: out.max_ack_delay = readVarInt(valueBytes, 0).value; break; case 0x0c: if (length !== 0) throw new Error("disable_active_migration must be zero-length"); out.disable_active_migration = true; break; case 0x0e: out.active_connection_id_limit = readVarInt(valueBytes, 0).value; break; case 0x0f: out.initial_source_connection_id = valueBytes; break; case 0x10: out.retry_source_connection_id = valueBytes; break; case 0x20: out.max_datagram_frame_size = readVarInt(valueBytes, 0).value; break; case 0x11: out.server_certificate_hash = valueBytes; break; case 0x2b603742: var origin = new TextDecoder().decode(valueBytes); out.web_accepted_origins.push(origin); break; default: if (!out.unknown) out.unknown = []; out.unknown.push({ id: id, bytes: valueBytes }); } } return out; } function parse_tls_message(data) { var view = new Uint8Array(data); var type = view[0]; var length = (view[1] << 16) | (view[2] << 8) | view[3]; var body = new Uint8Array(view.buffer, view.byteOffset + 4, length); return { type, length, body }; } function parse_tls_client_hello2(body) { var view = new Uint8Array(body); var ptr = 0; var legacy_version = (view[ptr++] << 8) | view[ptr++]; var random = view.slice(ptr, ptr + 32); ptr += 32; var session_id_len = view[ptr++]; var session_id = view.slice(ptr, ptr + session_id_len); ptr += session_id_len; var cipher_suites_len = (view[ptr++] << 8) | view[ptr++]; var cipher_suites = []; for (var i = 0; i < cipher_suites_len; i += 2) { var code = (view[ptr++] << 8) | view[ptr++]; cipher_suites.push(code); } var compression_methods_len = view[ptr++]; var compression_methods = view.slice(ptr, ptr + compression_methods_len); ptr += compression_methods_len; var extensions_len = (view[ptr++] << 8) | view[ptr++]; var extensions = []; var ext_end = ptr + extensions_len; while (ptr < ext_end) { var ext_type = (view[ptr++] << 8) | view[ptr++]; var ext_len = (view[ptr++] << 8) | view[ptr++]; var ext_data = view.slice(ptr, ptr + ext_len); ptr += ext_len; extensions.push({ type: ext_type, data: ext_data }); } var sni = null; var key_shares = []; var supported_versions = []; var supported_groups = []; var signature_algorithms = []; var alpn = []; var max_fragment_length = null; var padding = null; var cookie = null; var psk_key_exchange_modes = []; var pre_shared_key = null; var renegotiation_info = null; var quic_transport_parameters = { original: {}, initial_max_stream_data_bidi_local: undefined, initial_max_data: undefined, initial_max_streams_bidi: undefined, idle_timeout: undefined, max_packet_size: undefined, ack_delay_exponent: undefined, max_datagram_frame_size: undefined, web_accepted_origins: undefined }; for (var ext of extensions) { var ext_view = new Uint8Array(ext.data); if (ext.type === 0x00) { var name_len = (ext_view[3] << 8) | ext_view[4]; sni = new TextDecoder().decode(ext_view.slice(5, 5 + name_len)); } if (ext.type === 0x33) { var ptr2 = 0; var list_len = (ext_view[ptr2++] << 8) | ext_view[ptr2++]; var end = ptr2 + list_len; while (ptr2 < end) { var group = (ext_view[ptr2++] << 8) | ext_view[ptr2++]; var key_len = (ext_view[ptr2++] << 8) | ext_view[ptr2++]; var pubkey = ext_view.slice(ptr2, ptr2 + key_len); ptr2 += key_len; key_shares.push({ group, pubkey }); } } if (ext.type === 0x2b) { var len = ext_view[0]; for (var i = 1; i < 1 + len; i += 2) { supported_versions.push((ext_view[i] << 8) | ext_view[i + 1]); } } if (ext.type === 0x0a) { var len = (ext_view[0] << 8) | ext_view[1]; for (var i = 2; i < 2 + len; i += 2) { supported_groups.push((ext_view[i] << 8) | ext_view[i + 1]); } } if (ext.type === 0x0d) { var len = (ext_view[0] << 8) | ext_view[1]; for (var i = 2; i < 2 + len; i += 2) { signature_algorithms.push((ext_view[i] << 8) | ext_view[i + 1]); } } if (ext.type === 0x10) { var list_len = (ext_view[0] << 8) | ext_view[1]; var i = 2; while (i < 2 + list_len) { var name_len = ext_view[i++]; var proto = new TextDecoder().decode(ext_view.slice(i, i + name_len)); alpn.push(proto); i += name_len; } } if (ext.type === 0x39) { var ext_data = ext.data; var ptr2 = 0; while (ptr2 < ext_data.length) { var idRes = readVarInt(ext_data, ptr2); if (!idRes) break; var id = idRes.value; ptr2 += idRes.byteLength; var lenRes = readVarInt(ext_data, ptr2); if (!lenRes) break; var len = lenRes.value; ptr2 += lenRes.byteLength; var value = ext_data.slice(ptr2, ptr2 + len); ptr2 += len; quic_transport_parameters.original[id] = value; function toNumber(bytes) { var n = 0; for (var i = 0; i < bytes.length; i++) { n = (n << 8) | bytes[i]; } return n; } if (id === 0x00) quic_transport_parameters.original_destination_connection_id = value; if (id === 0x01) quic_transport_parameters.max_idle_timeout = toNumber(value); if (id === 0x03) quic_transport_parameters.max_packet_size = toNumber(value); if (id === 0x04) quic_transport_parameters.initial_max_data = toNumber(value); if (id === 0x05) quic_transport_parameters.initial_max_stream_data_bidi_local = toNumber(value); if (id === 0x08) quic_transport_parameters.initial_max_streams_bidi = toNumber(value); if (id === 0x0a) quic_transport_parameters.ack_delay_exponent = toNumber(value); if (id === 0x20) quic_transport_parameters.max_datagram_frame_size = toNumber(value); if (id === 0x2b603742) { try { quic_transport_parameters.web_accepted_origins = new TextDecoder().decode(value); } catch (e) {} } } } if (ext.type === 0x01) max_fragment_length = ext_view[0]; if (ext.type === 0x15) padding = ext_view; if (ext.type === 0x002a) { var len = (ext_view[0] << 8) | ext_view[1]; cookie = ext_view.slice(2, 2 + len); } if (ext.type === 0x2d) { var len = ext_view[0]; for (var i = 1; i <= len; i++) { psk_key_exchange_modes.push(ext_view[i]); } } if (ext.type === 0x29) pre_shared_key = ext_view; if (ext.type === 0xff01) renegotiation_info = ext_view; } return { type: 'client_hello', legacy_version, random, session_id, cipher_suites, compression_methods, extensions, sni, key_shares, supported_versions, supported_groups, signature_algorithms, alpn, max_fragment_length, padding, cookie, psk_key_exchange_modes, pre_shared_key, renegotiation_info, quic_transport_parameters }; } function parse_tls_client_hello(body) { var view = new Uint8Array(body); var ptr = 0; var legacy_version = (view[ptr++] << 8) | view[ptr++]; var random = view.slice(ptr, ptr + 32); ptr += 32; var session_id_len = view[ptr++]; var session_id = view.slice(ptr, ptr + session_id_len); ptr += session_id_len; var cipher_suites_len = (view[ptr++] << 8) | view[ptr++]; var cipher_suites = []; for (var i = 0; i < cipher_suites_len; i += 2) { var code = (view[ptr++] << 8) | view[ptr++]; cipher_suites.push(code); } var compression_methods_len = view[ptr++]; var compression_methods = view.slice(ptr, ptr + compression_methods_len); ptr += compression_methods_len; var extensions_len = (view[ptr++] << 8) | view[ptr++]; var extensions = []; var ext_end = ptr + extensions_len; while (ptr < ext_end) { var ext_type = (view[ptr++] << 8) | view[ptr++]; var ext_len = (view[ptr++] << 8) | view[ptr++]; var ext_data = view.slice(ptr, ptr + ext_len); ptr += ext_len; extensions.push({ type: ext_type, data: ext_data }); } var sni = null; var key_shares = []; var supported_versions = []; var supported_groups = []; var signature_algorithms = []; var alpn = []; var max_fragment_length = null; var padding = null; var cookie = null; var psk_key_exchange_modes = []; var pre_shared_key = null; var renegotiation_info = null; var quic_transport_parameters_raw = null; for (var ext of extensions) { var ext_view = new Uint8Array(ext.data); if (ext.type === 0x00) { // SNI var list_len = (ext_view[0] << 8) | ext_view[1]; var name_type = ext_view[2]; var name_len = (ext_view[3] << 8) | ext_view[4]; var name = new TextDecoder().decode(ext_view.slice(5, 5 + name_len)); sni = name; } if (ext.type === 0x33) { var ptr2 = 0; var list_len = (ext_view[ptr2++] << 8) | ext_view[ptr2++]; var end = ptr2 + list_len; while (ptr2 < end) { var group = (ext_view[ptr2++] << 8) | ext_view[ptr2++]; var key_len = (ext_view[ptr2++] << 8) | ext_view[ptr2++]; var pubkey = ext_view.slice(ptr2, ptr2 + key_len); ptr2 += key_len; key_shares.push({ group, pubkey }); } } if (ext.type === 0x2b) { // supported_versions var len = ext_view[0]; for (var i = 1; i < 1 + len; i += 2) { var ver = (ext_view[i] << 8) | ext_view[i + 1]; supported_versions.push(ver); } } if (ext.type === 0x0a) { // supported_groups var len = (ext_view[0] << 8) | ext_view[1]; for (var i = 2; i < 2 + len; i += 2) { supported_groups.push((ext_view[i] << 8) | ext_view[i + 1]); } } if (ext.type === 0x0d) { // signature_algorithms var len = (ext_view[0] << 8) | ext_view[1]; for (var i = 2; i < 2 + len; i += 2) { signature_algorithms.push((ext_view[i] << 8) | ext_view[i + 1]); } } if (ext.type === 0x10) { // ALPN var list_len = (ext_view[0] << 8) | ext_view[1]; var i = 2; while (i < 2 + list_len) { var name_len = ext_view[i++]; var proto = new TextDecoder().decode(ext_view.slice(i, i + name_len)); alpn.push(proto); i += name_len; } } if (ext.type === 0x39) { // quic_transport_parameters quic_transport_parameters_raw = ext.data; } if (ext.type === 0x01) { // Max Fragment Length max_fragment_length = ext_view[0]; } if (ext.type === 0x15) { // Padding padding = ext_view; } if (ext.type === 0x002a) { // Cookie var len = (ext_view[0] << 8) | ext_view[1]; cookie = ext_view.slice(2, 2 + len); } if (ext.type === 0x2d) { // PSK Key Exchange Modes var len = ext_view[0]; for (var i = 1; i <= len; i++) { psk_key_exchange_modes.push(ext_view[i]); } } if (ext.type === 0x29) { // PreSharedKey (placeholder) pre_shared_key = ext_view; } if (ext.type === 0xff01) { // Renegotiation Info renegotiation_info = ext_view; } } return { type: 'client_hello', legacy_version, random, session_id, cipher_suites, compression_methods, extensions, sni, key_shares, supported_versions, supported_groups, signature_algorithms, alpn, max_fragment_length, padding, cookie, psk_key_exchange_modes, pre_shared_key, renegotiation_info, quic_transport_parameters_raw }; } //////////////////////////////// function hmac(hash, key, data) { return new Uint8Array(crypto.createHmac(hash, key).update(data).digest()); } function hkdf_extract(salt, ikm, hash_func) { return nobleHashes.hkdf_extract(hash_func, ikm, salt); } function hkdf_expand(prk, info, length, hash_func) { return nobleHashes.hkdf_expand(hash_func, prk, info, length); } function build_hkdf_label(label, context, length) { const prefix = "tls13 "; const full = new TextEncoder().encode(prefix + label); const info = new Uint8Array( 2 + 1 + full.length + 1 + context.length); // length (2-bytes, BE) info[0] = (length >> 8) & 0xff; info[1] = length & 0xff; // label length + bytes info[2] = full.length; info.set(full, 3); // context length + bytes const ctxOfs = 3 + full.length; info[ctxOfs] = context.length; info.set(context, ctxOfs + 1); return info; } function hkdf_expand_label(secret, label, context, length, hash_func) { const info = build_hkdf_label(label, context, length); return hkdf_expand(secret, info, length, hash_func); // hash = sha384/sha256 } function hash_transcript(messages,hash_func) { var total_len = messages.reduce((sum, m) => sum + m.length, 0); var total = new Uint8Array(total_len); var offset = 0; for (var m of messages) { total.set(m, offset); offset += m.length; } return hash_func(total); } function tls_derive_app_secrets(handshake_secret, transcript, hash_func) { const hashLen = hash_func.outputLen; const empty = new Uint8Array(0); var zero = new Uint8Array(hash_func.outputLen); var derived_secret = hkdf_expand_label(handshake_secret, "derived", hash_func(empty), hash_func.outputLen, hash_func); var master_secret = hkdf_extract(derived_secret, zero, hash_func); // שלב 3: חישוב hash של ה־transcript עד server Finished const transcript_hash = hash_transcript(transcript, hash_func); // שלב 4: גזירת סודות התעבורה const client_app = hkdf_expand_label(master_secret, 'c ap traffic', transcript_hash, hashLen, hash_func); const server_app = hkdf_expand_label(master_secret, 's ap traffic', transcript_hash, hashLen, hash_func); return { client_application_traffic_secret: client_app, server_application_traffic_secret: server_app }; } function tls_derive_handshake_secrets(shared_secret, transcript, hash_func) { var zero = new Uint8Array(hash_func.outputLen); var empty = new Uint8Array(); var early_secret = hkdf_extract(empty, zero, hash_func); // salt, ikm var derived_secret = hkdf_expand_label(early_secret, "derived", hash_func(empty), hash_func.outputLen, hash_func); var handshake_secret = hkdf_extract(derived_secret, shared_secret, hash_func); var transcript_hash = hash_transcript(transcript, hash_func); var client_hts = hkdf_expand_label(handshake_secret, "c hs traffic", transcript_hash, hash_func.outputLen, hash_func); var server_hts = hkdf_expand_label(handshake_secret, "s hs traffic", transcript_hash, hash_func.outputLen, hash_func); return { handshake_secret, client_handshake_traffic_secret: client_hts, server_handshake_traffic_secret: server_hts, transcript_hash }; } function aead_decrypt(key, iv, packetNumber, ciphertextWithTag, aad, callback) { try { // יצירת nonce לפי QUIC (IV XOR packetNumber) var nonce = new Uint8Array(iv.length); for (var i = 0; i < iv.length; i++) { var pnIndex = iv.length - 1 - i; var pnByte = (packetNumber >>> (8 * i)) & 0xff; nonce[pnIndex] = iv[pnIndex] ^ pnByte; } var tag = ciphertextWithTag.slice(-16); var ciphertext = ciphertextWithTag.slice(0, -16); var algo = key.length === 32 ? 'aes-256-gcm' : key.length === 16 ? 'aes-128-gcm' : (() => { throw new Error("Unsupported key length: " + key.length); })(); const decipher = crypto.createDecipheriv(algo, key, nonce); decipher.setAuthTag(tag); decipher.setAAD(aad); const decrypted = decipher.update(ciphertext); decipher.final(); callback(null, decrypted); } catch (e) { callback(e); } } function aes_gcm_decrypt(ciphertext, tag, key, nonce, aad) { try { var algo = key.length === 32 ? 'aes-256-gcm' : key.length === 16 ? 'aes-128-gcm' : (() => { throw new Error("Unsupported key length: " + key.length); })(); var decipher = crypto.createDecipheriv( algo, Buffer.from(key), Buffer.from(nonce) ); decipher.setAuthTag(Buffer.from(tag)); decipher.setAAD(Buffer.from(aad)); var decrypted = Buffer.concat([ decipher.update(Buffer.from(ciphertext)), decipher.final() ]); //console.log("✅ Decryption success!"); return new Uint8Array(decrypted); } catch (e) { return null; } } const INITIAL_SALTS = { // QUIC v1 (RFC 9001) 0x00000001: new Uint8Array([ 0x38, 0x76, 0x2c, 0xf7, 0xf5, 0x59, 0x34, 0xb3, 0x4d, 0x17, 0x9a, 0xe6, 0xa4, 0xc8, 0x0c, 0xad, 0xcc, 0xbb, 0x7f, 0x0a ]), // QUIC draft-29 (HTTP/3 version h3-29) 0xff00001d: new Uint8Array([ 0xaf, 0xbf, 0xec, 0x28, 0x99, 0x93, 0xd2, 0x4c, 0x9e, 0x97, 0x86, 0xf1, 0x9c, 0x61, 0x11, 0xe0, 0x43, 0x90, 0xa8, 0x99 ]), // QUIC draft-32 (h3-32) 0xff000020: new Uint8Array([ 0x7f, 0xbc, 0xdb, 0x0e, 0x7c, 0x66, 0xbb, 0x77, 0x7b, 0xe3, 0x0e, 0xbd, 0x5f, 0xa5, 0x15, 0x87, 0x3d, 0x8d, 0x6e, 0x67 ]), // Google QUIC v50 ("Q050") — נדיר יותר אבל נתמך בדפדפנים מסוימים 0x51303530: new Uint8Array([ 0x69, 0x45, 0x6f, 0xbe, 0xf1, 0x6e, 0xd7, 0xdc, 0x48, 0x15, 0x9d, 0x98, 0xd0, 0x7f, 0x5c, 0x3c, 0x3d, 0x5a, 0xa7, 0x0a ]) }; function quic_derive_init_secrets(client_dcid, version, direction) { const hash_func = sha256; //console.log(version); const salt = INITIAL_SALTS[version] || null; if (!salt) throw new Error("Unsupported QUIC version: 0x" + version.toString(16)); const label = direction === 'read' ? 'client in' : 'server in'; const initial_secret = hkdf_extract(salt, client_dcid, hash_func); const initial_secret2 = hkdf_expand_label( initial_secret, label, new Uint8Array(0), 32, hash_func ); const key = hkdf_expand_label(initial_secret2, 'quic key', new Uint8Array(0), 16, hash_func); // AES-128-GCM const iv = hkdf_expand_label(initial_secret2, 'quic iv', new Uint8Array(0), 12, hash_func); const hp = hkdf_expand_label(initial_secret2, 'quic hp', new Uint8Array(0), 16, hash_func); return { key, iv, hp }; } function quic_derive_from_tls_secrets(traffic_secret, hash_func = sha256) { if(traffic_secret){ const key = hkdf_expand_label(traffic_secret, 'quic key', new Uint8Array(0), 16, hash_func); const iv = hkdf_expand_label(traffic_secret, 'quic iv', new Uint8Array(0), 12, hash_func); const hp = hkdf_expand_label(traffic_secret, 'quic hp', new Uint8Array(0), 16, hash_func); return { key, iv, hp }; } } function compute_nonce(iv, packetNumber) { const nonce = new Uint8Array(iv); // עותק של ה־IV המקורי (12 בתים) const pnBuffer = new Uint8Array(12); // 12 בתים, מיושר לימין // הכנס את packetNumber לימין של pnBuffer let n = packetNumber; for (let i = 11; n > 0 && i >= 0; i--) { pnBuffer[i] = n & 0xff; n >>= 8; } // בצע XOR בין ה־IV לבין pnBuffer for (let i = 0; i < 12; i++) { nonce[i] ^= pnBuffer[i]; } return nonce; } function aes_ecb_encrypt(keyBytes, plaintext) { if (keyBytes.length !== 16 && keyBytes.length !== 24 && keyBytes.length !== 32) { throw new Error("Invalid AES key size"); } if (plaintext.length % 16 !== 0) { throw new Error("Plaintext must be a multiple of 16 bytes"); } const cipher = crypto.createCipheriv('aes-' + (keyBytes.length * 8) + '-ecb', keyBytes, null); cipher.setAutoPadding(false); const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]); return new Uint8Array(encrypted); } function aead_encrypt(key, iv, packetNumber, plaintext, aad) { try { const algo = key.length === 32 ? 'aes-256-gcm' : key.length === 16 ? 'aes-128-gcm' : (() => { throw new Error("Unsupported key length: " + key.length); })(); const nonce = compute_nonce(iv, packetNumber); const cipher = crypto.createCipheriv(algo, Buffer.from(key), Buffer.from(nonce)); cipher.setAAD(Buffer.from(aad)); const encrypted = Buffer.concat([ cipher.update(Buffer.from(plaintext)), cipher.final() ]); const tag = cipher.getAuthTag(); const result = new Uint8Array(encrypted.length + tag.length); result.set(encrypted, 0); result.set(tag, encrypted.length); return result; } catch (e) { return null; } } function apply_header_protection(packet, pnOffset, hpKey, pnLength) { var sample = packet.slice(pnOffset + 4, pnOffset + 4 + 16); if (sample.length < 16) throw new Error("Not enough bytes for header protection sample"); var maskFull = aes_ecb_encrypt(hpKey, sample); var mask = maskFull.slice(0, 5); var firstByte = packet[0]; var isLongHeader = (firstByte & 0x80) !== 0; if (isLongHeader) { packet[0] ^= (mask[0] & 0x0f); // רק 4 ביטים אחרונים } else { packet[0] ^= (mask[0] & 0x1f); // ל־Short Header } for (var i = 0; i < pnLength; i++) { packet[pnOffset + i] ^= mask[1 + i]; } return packet; } function aes128ecb(sample,hpKey) { const cipher = crypto.createCipheriv('aes-128-ecb', Buffer.from(hpKey), null); cipher.setAutoPadding(false); const input = Buffer.from(sample); return new Uint8Array(Buffer.concat([cipher.update(input), cipher.final()])); } function expandPacketNumber(truncated, pnLen, largestReceived) { var pnWin = 1 << (pnLen * 8); var pnHalf = pnWin >>> 1; var expected = largestReceived + 1; return truncated + pnWin * Math.floor((expected - truncated + pnHalf) / pnWin); } function decode_packet_number(array, offset, pnLength) { let value = 0; for (let i = 0; i < pnLength; i++) { value = (value << 8) | array[offset + i]; } return value; } function decode_and_expand_packet_number(array, offset, pnLength, largestReceived) { var truncated = decode_packet_number(array, offset, pnLength); return expandPacketNumber(truncated, pnLength, largestReceived); } function remove_header_protection(array, pnOffset, hpKey, isShort) { // Step 1: קח sample של 16 בתים מתוך ה־payload אחרי pnOffset + 4 var sampleOffset = pnOffset + 4; var sample = array.slice(sampleOffset, sampleOffset + 16); var mask = aes128ecb(sample, hpKey).slice(0, 5); // ECB with no IV // Step 2: הסר הגנה מה־first byte var firstByte = array[0]; if (isShort) { // Short Header: רק 5 הביטים הנמוכים מוצפנים array[0] ^= mask[0] & 0x1f; } else { // Long Header: רק 4 הביטים הנמוכים מוצפנים array[0] ^= mask[0] & 0x0f; } // Step 3: הסר הגנה מה־packet number (pnLength נקבע מתוך הביטים עכשיו) var pnLength = (array[0] & 0x03) + 1; for (var i = 0; i < pnLength; i++) { array[pnOffset + i] ^= mask[1 + i]; } return pnLength; } function decrypt_quic_packet(array, read_key, read_iv, read_hp, dcid, largest_pn) { if (!(array instanceof Uint8Array)) throw new Error("Invalid input"); const firstByte = array[0]; const isShort = (firstByte & 0x80) === 0; const isLong = !isShort; let keyPhase = false; let pnOffset = 0; let pnLength = 0; let aad = null; let ciphertext = null; let tag = null; let packetNumber = null; let nonce = null; if (isLong) { // ---------- ניתוח Long Header ---------- const view = new DataView(array.buffer, array.byteOffset, array.byteLength); const version = view.getUint32(1); const dcidLen = array[5]; let offset = 6; const parsed_dcid = array.slice(offset, offset + dcidLen); offset += dcidLen; const scidLen = array[offset++]; const scid = array.slice(offset, offset + scidLen); offset += scidLen; const typeBits = (firstByte & 0x30) >> 4; const typeMap = ['initial', '0rtt', 'handshake', 'retry']; const packetType = typeMap[typeBits]; if (packetType === 'initial') { const tokenLen = readVarInt(array, offset); offset += tokenLen.byteLength + tokenLen.value; } const len = readVarInt(array, offset); offset += len.byteLength; pnOffset = offset; // הסרת הגנת כותרת pnLength = remove_header_protection(array, pnOffset, read_hp, false); if(pnLength!==null){ packetNumber = decode_and_expand_packet_number(array, pnOffset, pnLength, largest_pn); nonce = compute_nonce(read_iv, packetNumber); const payloadStart = pnOffset + pnLength; const payloadLength = len.value - pnLength; const payloadEnd = payloadStart + payloadLength; if (payloadEnd > array.length) throw new Error("Truncated long header packet"); const payload = array.slice(payloadStart, payloadEnd); if (payload.length < 16) throw new Error("Encrypted payload too short"); ciphertext = payload.slice(0, payload.length - 16); tag = payload.slice(payload.length - 16); aad = array.slice(0, pnOffset + pnLength); }else{ return null; } } else { // ---------- ניתוח Short Header ---------- // פורמט: 1 byte header + DCID + Packet Number + Payload const dcidLen = dcid.length; pnOffset = 1 + dcidLen; // הסרת הגנת כותרת pnLength = remove_header_protection(array, pnOffset, read_hp, true); if(pnLength!==null){ keyPhase = Boolean((array[0] & 0x04) >>> 2); packetNumber = decode_and_expand_packet_number(array, pnOffset, pnLength, largest_pn); nonce = compute_nonce(read_iv, packetNumber); const payloadStart = pnOffset + pnLength; const payload = array.slice(payloadStart); if (payload.length < 16) throw new Error("Encrypted payload too short"); ciphertext = payload.slice(0, payload.length - 16); tag = payload.slice(payload.length - 16); aad = array.slice(0, pnOffset + pnLength); }else{ return null; } } const plaintext = aes_gcm_decrypt(ciphertext, tag, read_key, nonce, aad); return { packet_number: packetNumber, key_phase: keyPhase, plaintext }; } function extract_tls_messages_from_chunks(chunks, from_offset) { var offset = from_offset; var buffers = []; // מאחדים רצף שלם של chunks מה־offset הנוכחי while (chunks[offset]) { buffers.push(chunks[offset]); offset += chunks[offset].length; } // אם לא קיבלנו שום דבר – נחזיר ריק if (buffers.length === 0) return []; var combined = concatUint8Arrays(buffers); var tls_messages = []; var i = 0; while (i + 4 <= combined.length) { var msgType = combined[i]; var length = (combined[i + 1] << 16) | (combined[i + 2] << 8) | combined[i + 3]; if (i + 4 + length > combined.length) break; // הודעה לא שלמה – עוצרים var msg = combined.slice(i, i + 4 + length); tls_messages.push(msg); i += 4 + length; } // עדכון offset רק עד איפה שעברנו בפועל if (i > 0) { // מוחקים את החלקים המאוחדים מתוך chunks var cleanupOffset = from_offset; while (cleanupOffset < from_offset + i) { var chunk = chunks[cleanupOffset]; delete chunks[cleanupOffset]; cleanupOffset += chunk.length; } // השארית – אם קיימת – נחזיר אותה כ־chunk חדש if (i < combined.length) { var leftover = combined.slice(i); chunks[cleanupOffset] = leftover; } // נעדכן את currentOffset from_offset += i; } return {tls_messages,new_from_offset: from_offset}; } function encode_version(version) { return new Uint8Array([ (version >>> 24) & 0xff, (version >>> 16) & 0xff, (version >>> 8) & 0xff, version & 0xff ]); } function build_quic_header(packetType, dcid, scid, token, lengthField, pnLen) { var hdr = []; var firstByte; // שלב 1: הגדרת הביט הראשון לפי סוג הפאקט if (packetType === 'initial') { firstByte = 0xC0 | ((pnLen - 1) & 0x03); // Long Header, Initial } else if (packetType === 'handshake') { firstByte = 0xE0 | ((pnLen - 1) & 0x03); // Long Header, Handshake } else if (packetType === '0rtt') { firstByte = 0xD0 | ((pnLen - 1) & 0x03); // Long Header, 0-RTT } else if (packetType === '1rtt') { firstByte = 0x40 | ((pnLen - 1) & 0x03); // Short Header hdr.push(Uint8Array.of(firstByte)); hdr.push(dcid); // ב־short header, זהו ה־Destination CID בלבד return { header: concatUint8Arrays(hdr), packetNumberOffset: hdr.reduce((sum, u8) => sum + u8.length, 0) }; } else { throw new Error('Unsupported packet type: ' + packetType); } // שלב 2: Header בסיסי לכל long header hdr.push(Uint8Array.of(firstByte)); hdr.push(encode_version(0x00000001)); // גרסה (4 בייטים) hdr.push(writeVarInt(dcid.length), dcid); hdr.push(writeVarInt(scid.length), scid); // שלב 3: רק ל־Initial מוסיפים טוקן if (packetType === 'initial') { if (!token) token = new Uint8Array(0); hdr.push(writeVarInt(token.length), token); } // שלב 4: שדה אורך (Length), חובה hdr.push(lengthField); // שלב 5: חישוב נקודת התחלה של packet number (מופיע מיד לאחר header) var header = concatUint8Arrays(hdr); return { header: header, packetNumberOffset: header.length }; } function encrypt_quic_packet(packetType, encodedFrames, writeKey, writeIv, writeHp, packetNumber, dcid, scid, token) { var pnLength; if (packetNumber <= 0xff) pnLength = 1; else if (packetNumber <= 0xffff) pnLength = 2; else if (packetNumber <= 0xffffff) pnLength = 3; else pnLength = 4; var pnFull = new Uint8Array(4); pnFull[0] = (packetNumber >>> 24) & 0xff; pnFull[1] = (packetNumber >>> 16) & 0xff; pnFull[2] = (packetNumber >>> 8) & 0xff; pnFull[3] = packetNumber & 0xff; var packetNumberField = pnFull.slice(4 - pnLength); var unprotectedPayloadLength = encodedFrames.length + pnLength + 16; var lengthField = writeVarInt(unprotectedPayloadLength); var headerInfo = build_quic_header(packetType, dcid, scid, token, lengthField, pnLength); var header = headerInfo.header; var packetNumberOffset = headerInfo.packetNumberOffset; // בונים AAD var fullHeader = concatUint8Arrays([header, packetNumberField]); // ✨ הוספת padding אם צריך כדי לאפשר sample var minSampleLength = 32; // או 32 ל־ChaCha20 var minTotalLength = packetNumberOffset + pnLength + minSampleLength; var fullLength = header.length + pnLength + encodedFrames.length + 16; // 16 = GCM tag if (fullLength < minTotalLength) { var extraPadding = minTotalLength - (header.length + pnLength + encodedFrames.length); var padded = new Uint8Array(encodedFrames.length + extraPadding); padded.set(encodedFrames, 0); encodedFrames = padded; // חשוב! גם unprotectedPayloadLength צריך להתעדכן unprotectedPayloadLength = encodedFrames.length + pnLength + 16; lengthField = writeVarInt(unprotectedPayloadLength); headerInfo = build_quic_header(packetType, dcid, scid, token, lengthField, pnLength); header = headerInfo.header; packetNumberOffset = headerInfo.packetNumberOffset; fullHeader = concatUint8Arrays([header, packetNumberField]); } var ciphertext = aead_encrypt(writeKey, writeIv, packetNumber, encodedFrames, fullHeader); if (ciphertext == null) return null; var fullPacket = concatUint8Arrays([ header, packetNumberField, ciphertext ]); return apply_header_protection(fullPacket, packetNumberOffset, writeHp, pnLength); } function encrypt_quic_packet2(packetType, encodedFrames, writeKey, writeIv, writeHp, packetNumber, dcid, scid, token) { // 2. קביעת אורך packet number var pnLength; if (packetNumber <= 0xff) pnLength = 1; else if (packetNumber <= 0xffff) pnLength = 2; else if (packetNumber <= 0xffffff) pnLength = 3; else pnLength = 4; // 3. חיתוך שדה ה־packet number לבתים var pnFull = new Uint8Array(4); pnFull[0] = (packetNumber >>> 24) & 0xff; pnFull[1] = (packetNumber >>> 16) & 0xff; pnFull[2] = (packetNumber >>> 8) & 0xff; pnFull[3] = packetNumber & 0xff; var packetNumberField = pnFull.slice(4 - pnLength); // 4. נבנה header בלי packet number var unprotectedPayloadLength = encodedFrames.length + pnLength + 16; var lengthField = writeVarInt(unprotectedPayloadLength); var headerInfo = build_quic_header(packetType, dcid, scid, token, lengthField, pnLength); var header = headerInfo.header; // עד לפני packet number var packetNumberOffset = headerInfo.packetNumberOffset; // 5. AAD כולל את header + packet number (לפני ההצפנה) var fullHeader = concatUint8Arrays([header, packetNumberField]); // 6. הצפנת המטען var ciphertext = aead_encrypt(writeKey, writeIv, packetNumber, encodedFrames, fullHeader); if (ciphertext == null) return null; // 7. בניית הפקט המלא לפני header protection var fullPacket = concatUint8Arrays([ header, packetNumberField, ciphertext ]); // 8. החלת הגנת כותרת (XOR) return apply_header_protection(fullPacket, packetNumberOffset, writeHp, pnLength); } function encode_quic_frames(frames) { var parts = []; var i; for (i = 0; i < frames.length; i++) { var frame = frames[i]; if (frame.type === 'padding') { var pad = new Uint8Array(frame.length); for (var j = 0; j < pad.length; j++) pad[j] = 0x00; parts.push(pad); } else if (frame.type === 'ping') { parts.push(new Uint8Array([0x01])); } else if (frame.type === 'ack') { var hasECN = frame.ecn !== null && frame.ecn !== undefined; var typeByte = hasECN ? 0x03 : 0x02; var b1 = writeVarInt(frame.largest); // Largest Acknowledged var b2 = writeVarInt(frame.delay); // ACK Delay var b3 = writeVarInt(frame.ranges.length); // ACK Range Count var b4 = writeVarInt(frame.firstRange != null ? frame.firstRange : 0); var temp = [new Uint8Array([typeByte]), b1, b2, b3, b4]; for (j = 0; j < frame.ranges.length; j++) { var gap = writeVarInt(frame.ranges[j].gap); // Gap to next range var len = writeVarInt(frame.ranges[j].length); // Length of next range temp.push(gap, len); } if (hasECN) { temp.push( writeVarInt(frame.ecn.ect0), writeVarInt(frame.ecn.ect1), writeVarInt(frame.ecn.ce) ); } parts.push(concatUint8Arrays(temp)); } else if (frame.type === 'reset_stream') { var id = writeVarInt(frame.id); var err = new Uint8Array([frame.error >> 8, frame.error & 0xff]); var size = writeVarInt(frame.finalSize); parts.push(concatUint8Arrays([ new Uint8Array([0x04]), id, err, size ])); } else if (frame.type === 'stop_sending') { var id = writeVarInt(frame.id); var err = new Uint8Array([frame.error >> 8, frame.error & 0xff]); parts.push(concatUint8Arrays([ new Uint8Array([0x05]), id, err ])); } else if (frame.type === 'crypto') { var off = writeVarInt(frame.offset); var len = writeVarInt(frame.data.length); parts.push(concatUint8Arrays([ new Uint8Array([0x06]), off, len, frame.data ])); } else if (frame.type === 'new_token') { var len = writeVarInt(frame.token.length); parts.push(concatUint8Arrays([ new Uint8Array([0x07]), len, frame.token ])); } else if (frame.type === 'stream') { var typeByte = 0x08;