|
@@ -0,0 +1,198 @@
|
|
|
+/**
|
|
|
+ * @author n1474335 [n1474335@gmail.com]
|
|
|
+ * @copyright Crown Copyright 2021
|
|
|
+ * @license Apache-2.0
|
|
|
+ */
|
|
|
+
|
|
|
+import Operation from "../Operation.mjs";
|
|
|
+import OperationError from "../errors/OperationError.mjs";
|
|
|
+import Utils from "../Utils.mjs";
|
|
|
+import Stream from "../lib/Stream.mjs";
|
|
|
+import {runHash} from "../lib/Hash.mjs";
|
|
|
+
|
|
|
+/**
|
|
|
+ * TLS JA3 Fingerprint operation
|
|
|
+ */
|
|
|
+class TLSJA3Fingerprint extends Operation {
|
|
|
+
|
|
|
+ /**
|
|
|
+ * TLSJA3Fingerprint constructor
|
|
|
+ */
|
|
|
+ constructor() {
|
|
|
+ super();
|
|
|
+
|
|
|
+ this.name = "TLS JA3 Fingerprint";
|
|
|
+ this.module = "Crypto";
|
|
|
+ this.description = "Generates a JA3 fingerprint to help identify TLS clients based on hashing together values from the Client Hello.<br><br>Input: A hex stream of the TLS Client Hello application layer.";
|
|
|
+ this.infoURL = "https://engineering.salesforce.com/tls-fingerprinting-with-ja3-and-ja3s-247362855967";
|
|
|
+ this.inputType = "string";
|
|
|
+ this.outputType = "string";
|
|
|
+ this.args = [
|
|
|
+ {
|
|
|
+ name: "Input format",
|
|
|
+ type: "option",
|
|
|
+ value: ["Hex", "Base64", "Raw"]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "Output format",
|
|
|
+ type: "option",
|
|
|
+ value: ["Hash digest", "JA3 string", "Full details"]
|
|
|
+ }
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @param {string} input
|
|
|
+ * @param {Object[]} args
|
|
|
+ * @returns {string}
|
|
|
+ */
|
|
|
+ run(input, args) {
|
|
|
+ const [inputFormat, outputFormat] = args;
|
|
|
+
|
|
|
+ input = Utils.convertToByteArray(input, inputFormat);
|
|
|
+ const s = new Stream(new Uint8Array(input));
|
|
|
+
|
|
|
+ const handshake = s.readInt(1);
|
|
|
+ if (handshake !== 0x16)
|
|
|
+ throw new OperationError("Not handshake data.");
|
|
|
+
|
|
|
+ // Version
|
|
|
+ s.moveForwardsBy(2);
|
|
|
+
|
|
|
+ // Length
|
|
|
+ const length = s.readInt(2);
|
|
|
+ if (s.length !== length + 5)
|
|
|
+ throw new OperationError("Incorrect handshake length.");
|
|
|
+
|
|
|
+ // Handshake type
|
|
|
+ const handshakeType = s.readInt(1);
|
|
|
+ if (handshakeType !== 1)
|
|
|
+ throw new OperationError("Not a Client Hello.");
|
|
|
+
|
|
|
+ // Handshake length
|
|
|
+ const handshakeLength = s.readInt(3);
|
|
|
+ if (s.length !== handshakeLength + 9)
|
|
|
+ throw new OperationError("Not enough data in Client Hello.");
|
|
|
+
|
|
|
+ // Hello version
|
|
|
+ const helloVersion = s.readInt(2);
|
|
|
+
|
|
|
+ // Random
|
|
|
+ s.moveForwardsBy(32);
|
|
|
+
|
|
|
+ // Session ID
|
|
|
+ const sessionIDLength = s.readInt(1);
|
|
|
+ s.moveForwardsBy(sessionIDLength);
|
|
|
+
|
|
|
+ // Cipher suites
|
|
|
+ const cipherSuitesLength = s.readInt(2);
|
|
|
+ const cipherSuites = s.getBytes(cipherSuitesLength);
|
|
|
+ const cs = new Stream(cipherSuites);
|
|
|
+ const cipherSegment = parseJA3Segment(cs, 2);
|
|
|
+
|
|
|
+ // Compression Methods
|
|
|
+ const compressionMethodsLength = s.readInt(1);
|
|
|
+ s.moveForwardsBy(compressionMethodsLength);
|
|
|
+
|
|
|
+ // Extensions
|
|
|
+ const extensionsLength = s.readInt(2);
|
|
|
+ const extensions = s.getBytes(extensionsLength);
|
|
|
+ const es = new Stream(extensions);
|
|
|
+ let ecsLen, ecs, ellipticCurves = "", ellipticCurvePointFormats = "";
|
|
|
+ const exts = [];
|
|
|
+ while (es.hasMore()) {
|
|
|
+ const type = es.readInt(2);
|
|
|
+ const length = es.readInt(2);
|
|
|
+ switch (type) {
|
|
|
+ case 0x0a: // Elliptic curves
|
|
|
+ ecsLen = es.readInt(2);
|
|
|
+ ecs = new Stream(es.getBytes(ecsLen));
|
|
|
+ ellipticCurves = parseJA3Segment(ecs, 2);
|
|
|
+ break;
|
|
|
+ case 0x0b: // Elliptic curve point formats
|
|
|
+ ecsLen = es.readInt(1);
|
|
|
+ ecs = new Stream(es.getBytes(ecsLen));
|
|
|
+ ellipticCurvePointFormats = parseJA3Segment(ecs, 1);
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ es.moveForwardsBy(length);
|
|
|
+ }
|
|
|
+ if (!GREASE_CIPHERSUITES.includes(type))
|
|
|
+ exts.push(type);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Output
|
|
|
+ const ja3 = [
|
|
|
+ helloVersion.toString(),
|
|
|
+ cipherSegment,
|
|
|
+ exts.join("-"),
|
|
|
+ ellipticCurves,
|
|
|
+ ellipticCurvePointFormats
|
|
|
+ ];
|
|
|
+ const ja3Str = ja3.join(",");
|
|
|
+ const ja3Hash = runHash("md5", Utils.strToArrayBuffer(ja3Str));
|
|
|
+
|
|
|
+ switch (outputFormat) {
|
|
|
+ case "JA3 string":
|
|
|
+ return ja3Str;
|
|
|
+ case "Full details":
|
|
|
+ return `Hash digest:
|
|
|
+${ja3Hash}
|
|
|
+
|
|
|
+Full JA3 string:
|
|
|
+${ja3Str}
|
|
|
+
|
|
|
+TLS Version:
|
|
|
+${helloVersion.toString()}
|
|
|
+Cipher Suites:
|
|
|
+${cipherSegment}
|
|
|
+Extensions:
|
|
|
+${exts.join("-")}
|
|
|
+Elliptic Curves:
|
|
|
+${ellipticCurves}
|
|
|
+Elliptic Curve Point Formats:
|
|
|
+${ellipticCurvePointFormats}`;
|
|
|
+ case "Hash digest":
|
|
|
+ default:
|
|
|
+ return ja3Hash;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Parses a JA3 segment, returning a "-" separated list
|
|
|
+ *
|
|
|
+ * @param {Stream} stream
|
|
|
+ * @returns {string}
|
|
|
+ */
|
|
|
+function parseJA3Segment(stream, size=2) {
|
|
|
+ const segment = [];
|
|
|
+ while (stream.hasMore()) {
|
|
|
+ const element = stream.readInt(size);
|
|
|
+ if (!GREASE_CIPHERSUITES.includes(element))
|
|
|
+ segment.push(element);
|
|
|
+ }
|
|
|
+ return segment.join("-");
|
|
|
+}
|
|
|
+
|
|
|
+const GREASE_CIPHERSUITES = [
|
|
|
+ 0x0a0a,
|
|
|
+ 0x1a1a,
|
|
|
+ 0x2a2a,
|
|
|
+ 0x3a3a,
|
|
|
+ 0x4a4a,
|
|
|
+ 0x5a5a,
|
|
|
+ 0x6a6a,
|
|
|
+ 0x7a7a,
|
|
|
+ 0x8a8a,
|
|
|
+ 0x9a9a,
|
|
|
+ 0xaaaa,
|
|
|
+ 0xbaba,
|
|
|
+ 0xcaca,
|
|
|
+ 0xdada,
|
|
|
+ 0xeaea,
|
|
|
+ 0xfafa
|
|
|
+];
|
|
|
+
|
|
|
+export default TLSJA3Fingerprint;
|