"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.trackClientHellos = exports.getTlsFingerprintAsJa3 = exports.calculateJa3FromFingerprintData = exports.readTlsClientHello = exports.NonTlsError = void 0; const stream = require("stream"); const crypto = require("crypto"); const collectBytes = (stream, byteLength) => { if (byteLength === 0) return Buffer.from([]); return new Promise((resolve, reject) => __awaiter(void 0, void 0, void 0, function* () { var _a; const closeReject = () => reject(new Error('Stream closed before expected data could be read')); const data = []; try { stream.on('error', reject); stream.on('close', closeReject); let dataLength = 0; let readNull = false; do { if (!stream.readable || readNull) yield new Promise((resolve) => stream.once('readable', resolve)); const nextData = (_a = stream.read(byteLength - dataLength)) !== null && _a !== void 0 ? _a : stream.read(); // If less than wanted data is available, at least read what we can get if (nextData === null) { // Still null => tried to read, not enough data readNull = true; continue; } data.push(nextData); dataLength += nextData.byteLength; } while (dataLength < byteLength); return resolve(Buffer.concat(data, byteLength)); } catch (e) { Object.assign(e, { consumedData: data }); reject(e); } finally { stream.removeListener('error', reject); stream.removeListener('close', closeReject); } })); }; const getUint16BE = (buffer, offset) => (buffer[offset] << 8) + buffer[offset + 1]; // https://datatracker.ietf.org/doc/html/draft-davidben-tls-grease-01 defines GREASE values for various // TLS fields, reserving 0a0a, 1a1a, 2a2a, etc for ciphers, extension ids & supported groups. const isGREASE = (value) => (value & 0x0f0f) == 0x0a0a; /** * Seperate error class. If you want to detect TLS parsing errors, but ignore TLS fingerprint * issues from definitely-not-TLS traffic, you can ignore all instances of this error. */ class NonTlsError extends Error { constructor(message) { super(message); // Fix prototypes (required for custom error types): const actualProto = new.target.prototype; Object.setPrototypeOf(this, actualProto); } } exports.NonTlsError = NonTlsError; function extractTlsHello(inputStream) { return __awaiter(this, void 0, void 0, function* () { const consumedData = []; try { consumedData.push(yield collectBytes(inputStream, 1)); const [recordType] = consumedData[0]; if (recordType !== 0x16) throw new Error("Can't calculate TLS fingerprint - not a TLS stream"); consumedData.push(yield collectBytes(inputStream, 2)); const recordLengthBytes = yield collectBytes(inputStream, 2); consumedData.push(recordLengthBytes); const recordLength = recordLengthBytes.readUint16BE(); consumedData.push(yield collectBytes(inputStream, recordLength)); // Put all the bytes back, so that this stream can still be used to create a real TLS session return Buffer.concat(consumedData); } catch (error) { if (error.consumedData) { // This happens if there's an error inside collectBytes with a partial read. error.consumedData.consumedData = Buffer.concat([ ...consumedData, error.consumedData ]); } else { Object.assign(error, { consumedData: Buffer.concat(consumedData) }); } throw error; } }); } function parseSniData(data) { // SNI is almost always just one value - and is arguably required to be, since there's only one type // in the RFC and you're only allowed one name per type, but it's still structured as a list: let offset = 0; while (offset < data.byteLength) { const entryLength = data.readUInt16BE(offset); offset += 2; const entryType = data[offset]; offset += 1; const nameLength = data.readUInt16BE(offset); offset += 2; if (nameLength !== entryLength - 3) { throw new Error('Invalid length in SNI entry'); } const name = data.slice(offset, offset + nameLength).toString('ascii'); offset += nameLength; if (entryType === 0x0) return name; } // No data, or no names with DNS hostname type. return undefined; } function parseAlpnData(data) { const protocols = []; const listLength = data.readUInt16BE(); if (listLength !== data.byteLength - 2) { throw new Error('Invalid length for ALPN list'); } let offset = 2; while (offset < data.byteLength) { const nameLength = data[offset]; offset += 1; const name = data.slice(offset, offset + nameLength).toString('ascii'); offset += nameLength; protocols.push(name); } return protocols; } function readTlsClientHello(inputStream) { var _a, _b, _c, _d, _e, _f; return __awaiter(this, void 0, void 0, function* () { const wasFlowing = inputStream.readableFlowing; if (wasFlowing) inputStream.pause(); // Pause other readers, so we have time to precisely get the data we need. let clientHelloRecordData; try { clientHelloRecordData = yield extractTlsHello(inputStream); } catch (error) { if ('consumedData' in error) { inputStream.unshift(error.consumedData); } if (wasFlowing) inputStream.resume(); // If there were other readers, resume and let them continue throw new NonTlsError(error.message); } // Put all the bytes back, so that this stream can still be used to create a real TLS session inputStream.unshift(clientHelloRecordData); if (wasFlowing) inputStream.resume(); // If there were other readers, resume and let them continue // Collect all the hello bytes, and then give us a stream of exactly only those bytes, so we can // still process them step by step in order: const clientHello = clientHelloRecordData.slice(5); // Strip TLS record prefix const helloDataStream = stream.Readable.from(clientHello, { objectMode: false }); const [helloType] = (yield collectBytes(helloDataStream, 1)); if (helloType !== 0x1) throw new Error("Can't calculate TLS fingerprint - not a TLS client hello"); const helloLength = (yield collectBytes(helloDataStream, 3)).readIntBE(0, 3); if (helloLength !== clientHello.byteLength - 4) throw new Error(`Unexpected client hello length: ${helloLength} (of ${clientHello.byteLength})`); const clientTlsVersion = yield collectBytes(helloDataStream, 2); const clientRandom = yield collectBytes(helloDataStream, 32); const [sessionIdLength] = yield collectBytes(helloDataStream, 1); const sessionId = yield collectBytes(helloDataStream, sessionIdLength); const cipherSuitesLength = (yield collectBytes(helloDataStream, 2)).readUint16BE(); const cipherSuites = yield collectBytes(helloDataStream, cipherSuitesLength); const [compressionMethodsLength] = yield collectBytes(helloDataStream, 1); const compressionMethods = yield collectBytes(helloDataStream, compressionMethodsLength); const extensionsLength = (yield collectBytes(helloDataStream, 2)).readUint16BE(); let readExtensionsDataLength = 0; const extensions = []; while (readExtensionsDataLength < extensionsLength) { const extensionId = yield collectBytes(helloDataStream, 2); const extensionLength = (yield collectBytes(helloDataStream, 2)).readUint16BE(); const extensionData = yield collectBytes(helloDataStream, extensionLength); extensions.push({ id: extensionId, data: extensionData }); readExtensionsDataLength += 4 + extensionLength; } // All data received & parsed! Now turn it into the fingerprint format: //SSLVersion,Cipher,SSLExtension,EllipticCurve,EllipticCurvePointFormat const tlsVersionFingerprint = clientTlsVersion.readUint16BE(); const cipherFingerprint = []; for (let i = 0; i < cipherSuites.length; i += 2) { const cipherId = getUint16BE(cipherSuites, i); if (isGREASE(cipherId)) continue; cipherFingerprint.push(cipherId); } const extensionsFingerprint = extensions .map(({ id }) => getUint16BE(id, 0)) .filter(id => !isGREASE(id)); const supportedGroupsData = ((_b = (_a = extensions.find(({ id }) => id.equals(Buffer.from([0x0, 0x0a])))) === null || _a === void 0 ? void 0 : _a.data) !== null && _b !== void 0 ? _b : Buffer.from([])).slice(2); // Drop the length prefix const groupsFingerprint = []; for (let i = 0; i < supportedGroupsData.length; i += 2) { const groupId = getUint16BE(supportedGroupsData, i); if (isGREASE(groupId)) continue; groupsFingerprint.push(groupId); } const curveFormatsData = (_d = (_c = extensions.find(({ id }) => id.equals(Buffer.from([0x0, 0x0b])))) === null || _c === void 0 ? void 0 : _c.data) !== null && _d !== void 0 ? _d : Buffer.from([]); const curveFormatsFingerprint = Array.from(curveFormatsData.slice(1)); // Drop length prefix const fingerprintData = [ tlsVersionFingerprint, cipherFingerprint, extensionsFingerprint, groupsFingerprint, curveFormatsFingerprint ]; // And capture other client hello data that might be interesting: const sniExtensionData = (_e = extensions.find(({ id }) => id.equals(Buffer.from([0x0, 0x0])))) === null || _e === void 0 ? void 0 : _e.data; const serverName = sniExtensionData ? parseSniData(sniExtensionData) : undefined; const alpnExtensionData = (_f = extensions.find(({ id }) => id.equals(Buffer.from([0x0, 0x10])))) === null || _f === void 0 ? void 0 : _f.data; const alpnProtocols = alpnExtensionData ? parseAlpnData(alpnExtensionData) : undefined; return { serverName, alpnProtocols, fingerprintData }; }); } exports.readTlsClientHello = readTlsClientHello; function calculateJa3FromFingerprintData(fingerprintData) { const fingerprintString = [ fingerprintData[0], fingerprintData[1].join('-'), fingerprintData[2].join('-'), fingerprintData[3].join('-'), fingerprintData[4].join('-') ].join(','); return crypto.createHash('md5').update(fingerprintString).digest('hex'); } exports.calculateJa3FromFingerprintData = calculateJa3FromFingerprintData; function getTlsFingerprintAsJa3(rawStream) { return __awaiter(this, void 0, void 0, function* () { return calculateJa3FromFingerprintData((yield readTlsClientHello(rawStream)).fingerprintData); }); } exports.getTlsFingerprintAsJa3 = getTlsFingerprintAsJa3; /** * Modify a TLS server, so that the TLS client hello is always parsed and the result is * attached to all sockets at the point when the 'secureConnection' event fires. * * This method mutates and returns the TLS server provided. TLS client hello data is * available from all TLS sockets afterwards in the `socket.tlsClientHello` property. * * This will work for all standard uses of a TLS server or similar (e.g. an HTTPS server) * but may behave unpredictably for advanced use cases, e.g. if you are already * manually injecting connections, hooking methods or events or otherwise doing something * funky & complicated. In those cases you probably want to use the fingerprint * calculation methods directly inside your funky logic instead. */ function trackClientHellos(tlsServer) { // Disable the normal TLS 'connection' event listener that triggers TLS setup: const tlsConnectionListener = tlsServer.listeners('connection')[0]; if (!tlsConnectionListener) throw new Error('TLS server is not listening for connection events'); tlsServer.removeListener('connection', tlsConnectionListener); // Listen ourselves for connections, get the fingerprint first, then let TLS setup resume: tlsServer.on('connection', (socket) => __awaiter(this, void 0, void 0, function* () { var _a, _b; try { const helloData = yield readTlsClientHello(socket); socket.tlsClientHello = Object.assign(Object.assign({}, helloData), { ja3: calculateJa3FromFingerprintData(helloData.fingerprintData) }); } catch (e) { if (!(e instanceof NonTlsError)) { // Ignore totally non-TLS traffic console.warn(`TLS client hello data not available for TLS connection from ${(_a = socket.remoteAddress) !== null && _a !== void 0 ? _a : 'unknown address'}: ${(_b = e.message) !== null && _b !== void 0 ? _b : e}`); } } // Once we have a fingerprint, TLS handshakes can continue as normal: tlsConnectionListener.call(tlsServer, socket); })); tlsServer.prependListener('secureConnection', (tlsSocket) => { var _a; const fingerprint = (_a = tlsSocket._parent) === null || _a === void 0 ? void 0 : _a.tlsClientHello; tlsSocket.tlsClientHello = fingerprint; }); return tlsServer; } exports.trackClientHellos = trackClientHellos; //# sourceMappingURL=index.js.map