1
- import * as stream from " stream" ;
1
+ import * as stream from ' stream' ;
2
2
3
- export async function getTlsFingerprint ( data : stream . Readable ) {
4
- const firstData : any = await new Promise ( ( resolve ) => data . on ( 'data' , resolve ) ) ;
5
- return firstData [ 0 ] . toString ( ) ;
3
+ const collectBytes = ( stream : stream . Readable , byteLength : number ) => {
4
+ if ( byteLength === 0 ) return Buffer . from ( [ ] ) ;
5
+
6
+ return new Promise < Buffer > ( async ( resolve , reject ) => {
7
+ const closeReject = ( ) => reject ( new Error ( 'Stream closed before expected data could be read' ) ) ;
8
+
9
+ try {
10
+ stream . on ( 'error' , reject ) ;
11
+ stream . on ( 'close' , closeReject ) ;
12
+
13
+ const data : Buffer [ ] = [ ] ;
14
+ let dataLength = 0 ;
15
+ let readNull = false ;
16
+ do {
17
+ if ( ! stream . readable || readNull ) await new Promise < Buffer > ( ( resolve ) => stream . once ( 'readable' , resolve ) ) ;
18
+
19
+ const nextData = stream . read ( byteLength - dataLength )
20
+ ?? stream . read ( ) ; // If less than wanted data is available, at least read what we can get
21
+
22
+ if ( nextData === null ) {
23
+ // Still null => tried to read, not enough data
24
+ readNull = true ;
25
+ continue ;
26
+ }
27
+
28
+ data . push ( nextData ) ;
29
+ dataLength += nextData . byteLength ;
30
+ } while ( dataLength < byteLength )
31
+
32
+ return resolve ( Buffer . concat ( data , byteLength ) ) ;
33
+ } catch ( e ) {
34
+ reject ( e ) ;
35
+ } finally {
36
+ stream . removeListener ( 'error' , reject ) ;
37
+ stream . removeListener ( 'close' , closeReject ) ;
38
+ }
39
+ } ) ;
40
+ } ;
41
+
42
+ const getUint16BE = ( buffer : Buffer , offset : number ) =>
43
+ ( buffer [ offset ] << 8 ) + buffer [ offset + 1 ] ;
44
+
45
+ export async function getTlsFingerprint ( rawStream : stream . Readable ) {
46
+ // Create a separate stream, which isn't flowing, so we can read byte-by-byte regardless of how else
47
+ // the stream is being used.
48
+ const inputStream = new stream . PassThrough ( ) ;
49
+ rawStream . pipe ( inputStream ) ;
50
+
51
+ const [ recordType ] = await collectBytes ( inputStream , 1 ) ;
52
+ if ( recordType !== 0x16 ) throw new Error ( "Can't calculate TLS fingerprint - not a TLS stream" ) ;
53
+
54
+ const tlsRecordVersion = await collectBytes ( inputStream , 2 ) ;
55
+ const recordLength = ( await collectBytes ( inputStream , 2 ) ) . readUint16BE ( ) ;
56
+
57
+ // Collect all the hello bytes, and then give us a stream of exactly only those bytes, so we can
58
+ // still process them step by step in order:
59
+ const helloDataStream = stream . Readable . from ( await collectBytes ( inputStream , recordLength ) , { objectMode : false } ) ;
60
+ rawStream . unpipe ( inputStream ) ; // Don't need any more data now, thanks.
61
+
62
+ const [ helloType ] = ( await collectBytes ( helloDataStream , 1 ) ) ;
63
+ if ( helloType !== 0x1 ) throw new Error ( "Can't calculate TLS fingerprint - not a TLS client hello" ) ;
64
+
65
+ const helloLength = ( await collectBytes ( helloDataStream , 3 ) ) . readIntBE ( 0 , 3 ) ;
66
+ if ( helloLength !== recordLength - 4 ) throw new Error (
67
+ `Unexpected client hello length: ${ helloLength } (or ${ recordLength } )`
68
+ ) ;
69
+
70
+ const clientTlsVersion = await collectBytes ( helloDataStream , 2 ) ;
71
+ const clientRandom = await collectBytes ( helloDataStream , 32 ) ;
72
+
73
+ const [ sessionIdLength ] = await collectBytes ( helloDataStream , 1 ) ;
74
+ const sessionId = await collectBytes ( helloDataStream , sessionIdLength ) ;
75
+
76
+ const cipherSuitesLength = ( await collectBytes ( helloDataStream , 2 ) ) . readUint16BE ( ) ;
77
+ const cipherSuites = await collectBytes ( helloDataStream , cipherSuitesLength ) ;
78
+
79
+ const [ compressionMethodsLength ] = await collectBytes ( helloDataStream , 1 ) ;
80
+ const compressionMethods = await collectBytes ( helloDataStream , compressionMethodsLength ) ;
81
+
82
+ const extensionsLength = ( await collectBytes ( helloDataStream , 2 ) ) . readUint16BE ( ) ;
83
+ let readExtensionsDataLength = 0 ;
84
+ const extensions : Array < { id : Buffer , data : Buffer } > = [ ] ;
85
+
86
+ while ( readExtensionsDataLength < extensionsLength ) {
87
+ const extensionId = await collectBytes ( helloDataStream , 2 ) ;
88
+ const extensionLength = ( await collectBytes ( helloDataStream , 2 ) ) . readUint16BE ( ) ;
89
+ const extensionData = await collectBytes ( helloDataStream , extensionLength ) ;
90
+
91
+ extensions . push ( { id : extensionId , data : extensionData } ) ;
92
+ readExtensionsDataLength += 4 + extensionLength ;
93
+ }
94
+
95
+ // All data parsed! Now turn it into the fingerprint format:
96
+ //SSLVersion,Cipher,SSLExtension,EllipticCurve,EllipticCurvePointFormat
97
+
98
+ const tlsVersionFingerprint = clientTlsVersion . readUint16BE ( )
99
+
100
+ const cipherFingerprint : number [ ] = [ ] ;
101
+ for ( let i = 0 ; i < cipherSuites . length ; i += 2 ) {
102
+ cipherFingerprint . push ( getUint16BE ( cipherSuites , i ) ) ;
103
+ }
104
+
105
+ const extensionsFingerprint : number [ ] = extensions . map ( ( { id } ) => getUint16BE ( id , 0 ) ) ;
106
+
107
+ const supportedGroupsData = (
108
+ extensions . find ( ( { id } ) => id . equals ( Buffer . from ( [ 0x0 , 0x0a ] ) ) ) ?. data
109
+ ?? Buffer . from ( [ ] )
110
+ ) . slice ( 2 ) // Drop the length prefix
111
+
112
+ const groupsFingerprint : number [ ] = [ ] ;
113
+ for ( let i = 0 ; i < supportedGroupsData . length ; i += 2 ) {
114
+ groupsFingerprint . push ( getUint16BE ( supportedGroupsData , i ) ) ;
115
+ }
116
+
117
+ const curveFormatsData = extensions . find ( ( { id } ) => id . equals ( Buffer . from ( [ 0x0 , 0x0b ] ) ) ) ?. data
118
+ ?? Buffer . from ( [ ] ) ;
119
+ const curveFormatsFingerprint : number [ ] = Array . from ( curveFormatsData . slice ( 1 ) ) ; // Drop length prefix
120
+
121
+ return [
122
+ tlsVersionFingerprint ,
123
+ cipherFingerprint ,
124
+ extensionsFingerprint ,
125
+ groupsFingerprint ,
126
+ curveFormatsFingerprint
127
+ ] as const ;
6
128
}
0 commit comments