Skip to content

Commit 3172a06

Browse files
committed
Extend to cover a few missed JA4 details
1 parent 6c716ae commit 3172a06

File tree

1 file changed

+26
-17
lines changed

1 file changed

+26
-17
lines changed

src/index.ts

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -305,9 +305,9 @@ export async function getTlsFingerprintAsJa3(rawStream: stream.Readable) {
305305
}
306306

307307
export interface Ja4Data {
308-
protocol: 't' | 'q' | 'd'; // TLS, QUIC, DTLS
309-
version: '12' | '13'; // TLS 1.2 or 1.3
310-
sni: string; // SNI value or 'i' for IP
308+
protocol: 't' | 'q' | 'd'; // TLS, QUIC, DTLS (only TLS supported for now)
309+
version: '10' | ' 11' | '12' | '13'; // TLS version
310+
sni: 'd' | 'i'; // 'd' if a domain was provided via SNI, 'i' otherwise (for IP)
311311
cipherCount: number;
312312
extensionCount: number;
313313
alpn: string; // First and last character of the ALPN value or '00' if none
@@ -319,23 +319,30 @@ export interface Ja4Data {
319319
export function calculateJa4FromHelloData(
320320
{ serverName, alpnProtocols, fingerprintData }: TlsHelloData
321321
): string {
322-
const [ , ciphers, extensions, , , sigAlgorithms] = fingerprintData;
322+
const [tlsVersion, ciphers, extensions, , , sigAlgorithms] = fingerprintData;
323323

324324
// Part A: Protocol info
325325
const protocol = 't'; // We only handle TCP for now
326-
const version = extensions.includes(43) ? '13' : '12'; // Extension 43 is supported_versions
326+
327+
const version = extensions.includes(0x002B)
328+
? '13' // TLS 1.3 uses the supported versions extension, and 1.4+ doesn't exist (yet)
329+
: { // Previous TLS sets the version in the handshake up front:
330+
0x0303: '12',
331+
0x0302: '11',
332+
0x0301: '10'
333+
}[tlsVersion]
334+
?? '00'; // Other unknown version
335+
327336
const sni = !serverName ? 'i' : 'd'; // 'i' for IP (no SNI), 'd' for domain
328337

329338
// Handle different ALPN protocols
330339
let alpn = '00';
331-
if (alpnProtocols && alpnProtocols.length > 0) {
332-
const firstProtocol = alpnProtocols[0];
333-
if (firstProtocol) {
334-
// Take first and last character of the protocol string
335-
alpn = firstProtocol.length >= 2
336-
? `${firstProtocol[0]}${firstProtocol[firstProtocol.length - 1]}`
337-
: '00';
338-
}
340+
const firstProtocol = alpnProtocols?.[0];
341+
if (firstProtocol && firstProtocol.length >= 1) {
342+
// Take first and last character of the protocol string
343+
alpn = firstProtocol.length >= 2
344+
? `${firstProtocol[0]}${firstProtocol[firstProtocol.length - 1]}`
345+
: `${firstProtocol[0]}${firstProtocol[0]}`;
339346
}
340347

341348
// Format numbers as fixed-width hex
@@ -350,10 +357,12 @@ export function calculateJa4FromHelloData(
350357
.filter(c => !isGREASE(c))
351358
.map(c => c.toString(16).padStart(4, '0'));
352359
const sortedCiphers = [...cipherHexValues].sort().join(',');
353-
const cipherHash = crypto.createHash('sha256')
354-
.update(sortedCiphers)
355-
.digest('hex')
356-
.slice(0, 12);
360+
const cipherHash = ciphers.length
361+
? crypto.createHash('sha256')
362+
.update(sortedCiphers)
363+
.digest('hex')
364+
.slice(0, 12)
365+
: '000000000000'; // No ciphers provided
357366

358367
// Part C: Truncated SHA256 of extensions + sig algorithms
359368
// Get extensions (excluding SNI and ALPN)

0 commit comments

Comments
 (0)