@@ -43,7 +43,19 @@ const collectBytes = (stream: stream.Readable, byteLength: number) => {
43
43
const getUint16BE = ( buffer : Buffer , offset : number ) =>
44
44
( buffer [ offset ] << 8 ) + buffer [ offset + 1 ] ;
45
45
46
- export async function getTlsFingerprintData ( rawStream : stream . Readable ) {
46
+ // https://datatracker.ietf.org/doc/html/draft-davidben-tls-grease-01 defines GREASE values for various
47
+ // TLS fields, reserving 0a0a, 1a1a, 2a2a, etc for ciphers, extension ids & supported groups.
48
+ const isGREASE = ( value : number ) => ( value & 0x0f0f ) == 0x0a0a ;
49
+
50
+ export type TlsFingerprintData = [
51
+ tlsVersion : number ,
52
+ ciphers : number [ ] ,
53
+ extensions : number [ ] ,
54
+ groups : number [ ] ,
55
+ curveFormats : number [ ]
56
+ ] ;
57
+
58
+ export async function getTlsFingerprintData ( rawStream : stream . Readable ) : Promise < TlsFingerprintData > {
47
59
// Create a separate stream, which isn't flowing, so we can read byte-by-byte regardless of how else
48
60
// the stream is being used.
49
61
const inputStream = new stream . PassThrough ( ) ;
@@ -100,10 +112,14 @@ export async function getTlsFingerprintData(rawStream: stream.Readable) {
100
112
101
113
const cipherFingerprint : number [ ] = [ ] ;
102
114
for ( let i = 0 ; i < cipherSuites . length ; i += 2 ) {
103
- cipherFingerprint . push ( getUint16BE ( cipherSuites , i ) ) ;
115
+ const cipherId = getUint16BE ( cipherSuites , i ) ;
116
+ if ( isGREASE ( cipherId ) ) continue ;
117
+ cipherFingerprint . push ( cipherId ) ;
104
118
}
105
119
106
- const extensionsFingerprint : number [ ] = extensions . map ( ( { id } ) => getUint16BE ( id , 0 ) ) ;
120
+ const extensionsFingerprint : number [ ] = extensions
121
+ . map ( ( { id } ) => getUint16BE ( id , 0 ) )
122
+ . filter ( id => ! isGREASE ( id ) ) ;
107
123
108
124
const supportedGroupsData = (
109
125
extensions . find ( ( { id } ) => id . equals ( Buffer . from ( [ 0x0 , 0x0a ] ) ) ) ?. data
@@ -112,7 +128,9 @@ export async function getTlsFingerprintData(rawStream: stream.Readable) {
112
128
113
129
const groupsFingerprint : number [ ] = [ ] ;
114
130
for ( let i = 0 ; i < supportedGroupsData . length ; i += 2 ) {
115
- groupsFingerprint . push ( getUint16BE ( supportedGroupsData , i ) ) ;
131
+ const groupId = getUint16BE ( supportedGroupsData , i )
132
+ if ( isGREASE ( groupId ) ) continue ;
133
+ groupsFingerprint . push ( groupId ) ;
116
134
}
117
135
118
136
const curveFormatsData = extensions . find ( ( { id } ) => id . equals ( Buffer . from ( [ 0x0 , 0x0b ] ) ) ) ?. data
@@ -125,12 +143,10 @@ export async function getTlsFingerprintData(rawStream: stream.Readable) {
125
143
extensionsFingerprint ,
126
144
groupsFingerprint ,
127
145
curveFormatsFingerprint
128
- ] as const ;
146
+ ] ;
129
147
}
130
148
131
- export async function getTlsFingerprintAsJa3 ( rawStream : stream . Readable ) {
132
- const fingerprintData = await getTlsFingerprintData ( rawStream ) ;
133
-
149
+ export function calculateJa3FromFingerprintData ( fingerprintData : TlsFingerprintData ) {
134
150
const fingerprintString = [
135
151
fingerprintData [ 0 ] ,
136
152
fingerprintData [ 1 ] . join ( '-' ) ,
@@ -140,4 +156,8 @@ export async function getTlsFingerprintAsJa3(rawStream: stream.Readable) {
140
156
] . join ( ',' ) ;
141
157
142
158
return crypto . createHash ( 'md5' ) . update ( fingerprintString ) . digest ( 'hex' ) ;
159
+ }
160
+
161
+ export async function getTlsFingerprintAsJa3 ( rawStream : stream . Readable ) {
162
+ return calculateJa3FromFingerprintData ( await getTlsFingerprintData ( rawStream ) ) ;
143
163
}
0 commit comments