@@ -305,9 +305,9 @@ export async function getTlsFingerprintAsJa3(rawStream: stream.Readable) {
305
305
}
306
306
307
307
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)
311
311
cipherCount : number ;
312
312
extensionCount : number ;
313
313
alpn : string ; // First and last character of the ALPN value or '00' if none
@@ -319,23 +319,30 @@ export interface Ja4Data {
319
319
export function calculateJa4FromHelloData (
320
320
{ serverName, alpnProtocols, fingerprintData } : TlsHelloData
321
321
) : string {
322
- const [ , ciphers , extensions , , , sigAlgorithms ] = fingerprintData ;
322
+ const [ tlsVersion , ciphers , extensions , , , sigAlgorithms ] = fingerprintData ;
323
323
324
324
// Part A: Protocol info
325
325
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
+
327
336
const sni = ! serverName ? 'i' : 'd' ; // 'i' for IP (no SNI), 'd' for domain
328
337
329
338
// Handle different ALPN protocols
330
339
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 ] } ` ;
339
346
}
340
347
341
348
// Format numbers as fixed-width hex
@@ -350,10 +357,12 @@ export function calculateJa4FromHelloData(
350
357
. filter ( c => ! isGREASE ( c ) )
351
358
. map ( c => c . toString ( 16 ) . padStart ( 4 , '0' ) ) ;
352
359
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
357
366
358
367
// Part C: Truncated SHA256 of extensions + sig algorithms
359
368
// Get extensions (excluding SNI and ALPN)
0 commit comments