Skip to content

Commit 2287adb

Browse files
committed
Calculate the full JA3 hash
1 parent a758553 commit 2287adb

File tree

2 files changed

+48
-4
lines changed

2 files changed

+48
-4
lines changed

src/index.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as stream from 'stream';
2+
import * as crypto from 'crypto';
23

34
const collectBytes = (stream: stream.Readable, byteLength: number) => {
45
if (byteLength === 0) return Buffer.from([]);
@@ -42,7 +43,7 @@ const collectBytes = (stream: stream.Readable, byteLength: number) => {
4243
const getUint16BE = (buffer: Buffer, offset: number) =>
4344
(buffer[offset] << 8) + buffer[offset+1];
4445

45-
export async function getTlsFingerprint(rawStream: stream.Readable) {
46+
export async function getTlsFingerprintData(rawStream: stream.Readable) {
4647
// Create a separate stream, which isn't flowing, so we can read byte-by-byte regardless of how else
4748
// the stream is being used.
4849
const inputStream = new stream.PassThrough();
@@ -125,4 +126,18 @@ export async function getTlsFingerprint(rawStream: stream.Readable) {
125126
groupsFingerprint,
126127
curveFormatsFingerprint
127128
] as const;
129+
}
130+
131+
export async function getTlsFingerprintAsJa3(rawStream: stream.Readable) {
132+
const fingerprintData = await getTlsFingerprintData(rawStream);
133+
134+
const fingerprintString = [
135+
fingerprintData[0],
136+
fingerprintData[1].join('-'),
137+
fingerprintData[2].join('-'),
138+
fingerprintData[3].join('-'),
139+
fingerprintData[4].join('-')
140+
].join(',');
141+
142+
return crypto.createHash('md5').update(fingerprintString).digest('hex');
128143
}

test/test.spec.ts

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import * as net from 'net';
22
import * as tls from 'tls';
3+
import * as https from 'https';
34
import { makeDestroyable, DestroyableServer } from 'destroyable-server';
45

56
import { expect } from 'chai';
67
import { getDeferred } from './test-util';
78

8-
import { getTlsFingerprint } from '../src/index';
9+
import {
10+
getTlsFingerprintData,
11+
getTlsFingerprintAsJa3
12+
} from '../src/index';
913

1014
const nodeMajorVersion = parseInt(process.version.slice(1).split('.')[0], 10);
1115

@@ -15,7 +19,7 @@ describe("Read-TLS-Fingerprint", () => {
1519

1620
afterEach(() => server?.destroy());
1721

18-
it("can read Node's fingerprint", async () => {
22+
it("can read Node's fingerprint data", async () => {
1923
server = makeDestroyable(new net.Server());
2024

2125
server.listen();
@@ -31,7 +35,7 @@ describe("Read-TLS-Fingerprint", () => {
3135
}).on('error', () => {}); // Socket will fail, since server never responds, that's OK
3236

3337
const incomingSocket = await incomingSocketPromise;
34-
const fingerprint = await getTlsFingerprint(incomingSocket);
38+
const fingerprint = await getTlsFingerprintData(incomingSocket);
3539

3640
const [
3741
tlsVersion,
@@ -60,4 +64,29 @@ describe("Read-TLS-Fingerprint", () => {
6064
]);
6165
expect(curveFormats).to.deep.equal([0, 1, 2]);
6266
});
67+
68+
it("can read Node's JA3 fingerprint", async () => {
69+
server = makeDestroyable(new net.Server());
70+
71+
server.listen();
72+
await new Promise((resolve) => server.on('listening', resolve));
73+
74+
let incomingSocketPromise = getDeferred<net.Socket>();
75+
server.on('connection', (socket) => incomingSocketPromise.resolve(socket));
76+
77+
const port = (server.address() as net.AddressInfo).port;
78+
https.request({
79+
host: 'localhost',
80+
port
81+
}).on('error', () => {}); // Socket will fail, since server never responds, that's OK
82+
83+
const incomingSocket = await incomingSocketPromise;
84+
const fingerprint = await getTlsFingerprintAsJa3(incomingSocket);
85+
86+
expect(fingerprint).to.be.oneOf([
87+
'398430069e0a8ecfbc8db0778d658d77', // Node 12 - 16
88+
'0cce74b0d9b7f8528fb2181588d23793' // Node 17+
89+
]);
90+
});
91+
6392
});

0 commit comments

Comments
 (0)