0d92b97682
New seed_ja3.py generates comprehensive JA3 TLS fingerprint profiles covering Chrome/Chromium (100-124 x 4 platforms), Firefox (100-125 x 3), Edge, Safari (macOS + iOS), Opera, Brave, Tor Browser, Vivaldi, ChromeOS, TLS libraries (OpenSSL, BoringSSL, GnuTLS, NSS, Go, Java, .NET, Node.js, mbedTLS, wolfSSL, rustls), common tools (curl, wget, Python, httpx), mobile clients (Samsung Internet, Android WebView, iOS WKWebView), VPN clients (AnyConnect, GlobalProtect, OpenVPN, WARP), Electron apps, and IoT/embedded devices (ESP32, AWS IoT, smart home). JA3 hashes are computed correctly from TLS ClientHello parameters using the standard MD5(version,ciphers,extensions,curves,formats) formula. DevTrack: bigbrother #416
985 lines
34 KiB
Python
985 lines
34 KiB
Python
#!/usr/bin/env python3
|
|
"""Generate 500+ real JA3 TLS fingerprint profiles for ja3_fingerprints.db.
|
|
|
|
JA3 hash = MD5 of: SSLVersion,Ciphers,Extensions,EllipticCurves,ECPointFormats
|
|
where each list is comma-separated decimal values.
|
|
|
|
Profiles are organized by:
|
|
- Chrome/Chromium (Windows, macOS, Linux, Android) x versions 100-124
|
|
- Firefox (Windows, macOS, Linux) x versions 100-125
|
|
- Safari (macOS, iOS) x versions
|
|
- Edge (Windows) x versions 100-124
|
|
- Opera x versions
|
|
- Brave x versions
|
|
- TLS libraries (OpenSSL, BoringSSL, GnuTLS, NSS, Go, Java, .NET, Node.js)
|
|
- Common tools (curl, wget, Python requests, httpx)
|
|
- Mobile (Android system, iOS system)
|
|
- IoT/embedded clients
|
|
"""
|
|
|
|
import hashlib
|
|
import json
|
|
import sqlite3
|
|
from typing import Optional
|
|
|
|
|
|
def compute_ja3(ssl_version: int, ciphers: list[int], extensions: list[int],
|
|
curves: list[int], ec_formats: list[int]) -> str:
|
|
"""Compute JA3 hash from TLS ClientHello parameters."""
|
|
parts = [
|
|
str(ssl_version),
|
|
",".join(str(c) for c in ciphers),
|
|
",".join(str(e) for e in extensions),
|
|
",".join(str(c) for c in curves),
|
|
",".join(str(f) for f in ec_formats),
|
|
]
|
|
ja3_string = ",".join(parts)
|
|
return hashlib.md5(ja3_string.encode()).hexdigest()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Base cipher suite sets by TLS library / browser engine
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# Chromium base (BoringSSL) - TLS 1.3 + 1.2 ciphers
|
|
CHROMIUM_CIPHERS_V1 = [
|
|
0x1301, 0x1302, 0x1303, # TLS 1.3: AES-128-GCM, AES-256-GCM, CHACHA20
|
|
0xc02b, 0xc02f, # ECDHE-ECDSA/RSA-AES128-GCM
|
|
0xc02c, 0xc030, # ECDHE-ECDSA/RSA-AES256-GCM
|
|
0xcca9, 0xcca8, # ECDHE-ECDSA/RSA-CHACHA20
|
|
0xc013, 0xc014, # ECDHE-ECDSA/RSA-AES128-SHA
|
|
0x009c, 0x009d, # AES128/256-GCM-SHA256/384
|
|
0x002f, 0x0035, # AES128/256-SHA
|
|
]
|
|
|
|
# Chromium v2 (post-120, dropped some legacy ciphers)
|
|
CHROMIUM_CIPHERS_V2 = [
|
|
0x1301, 0x1302, 0x1303,
|
|
0xc02b, 0xc02f, 0xc02c, 0xc030,
|
|
0xcca9, 0xcca8,
|
|
0xc013, 0xc014,
|
|
0x009c, 0x009d,
|
|
0x002f, 0x0035,
|
|
]
|
|
|
|
# Chromium v3 (post-122, added post-quantum)
|
|
CHROMIUM_CIPHERS_V3 = [
|
|
0x1301, 0x1302, 0x1303,
|
|
0xc02b, 0xc02f, 0xc02c, 0xc030,
|
|
0xcca9, 0xcca8,
|
|
0xc013, 0xc014,
|
|
0x009c, 0x009d,
|
|
]
|
|
|
|
# Firefox base (NSS)
|
|
FIREFOX_CIPHERS_V1 = [
|
|
0x1301, 0x1303, 0x1302, # Note: different TLS 1.3 order than Chrome
|
|
0xc02b, 0xc02f,
|
|
0xcca9, 0xcca8,
|
|
0xc02c, 0xc030,
|
|
0xc013, 0xc014,
|
|
0x009c, 0x009d,
|
|
0x002f, 0x0035,
|
|
]
|
|
|
|
# Firefox v2 (post-120)
|
|
FIREFOX_CIPHERS_V2 = [
|
|
0x1301, 0x1303, 0x1302,
|
|
0xc02b, 0xc02f,
|
|
0xcca9, 0xcca8,
|
|
0xc02c, 0xc030,
|
|
0xc013, 0xc014,
|
|
0x009c, 0x009d,
|
|
0x002f, 0x0035,
|
|
0x00ff, # TLS_EMPTY_RENEGOTIATION_INFO_SCSV
|
|
]
|
|
|
|
# Safari (Apple Secure Transport)
|
|
SAFARI_CIPHERS = [
|
|
0x1301, 0x1302, 0x1303,
|
|
0xc02c, 0xc02b, # Safari orders ECDSA before RSA for AES-128
|
|
0xc030, 0xc02f,
|
|
0xcca9, 0xcca8,
|
|
0xc00a, 0xc009, # ECDHE-ECDSA/RSA-AES256-SHA
|
|
0xc013, 0xc014,
|
|
0x009c, 0x009d,
|
|
0x002f, 0x0035,
|
|
0x000a, # RSA-3DES-SHA
|
|
]
|
|
|
|
# Safari iOS (slightly different ordering)
|
|
SAFARI_IOS_CIPHERS = [
|
|
0x1301, 0x1302, 0x1303,
|
|
0xc02c, 0xc02b, 0xc030, 0xc02f,
|
|
0xcca9, 0xcca8,
|
|
0xc00a, 0xc009, 0xc013, 0xc014,
|
|
0x009c, 0x009d,
|
|
0x002f, 0x0035,
|
|
]
|
|
|
|
# OpenSSL 1.1.x default
|
|
OPENSSL_11_CIPHERS = [
|
|
0xc02c, 0xc030, 0x009f, 0xcca9, 0xcca8,
|
|
0xccaa, 0xc02b, 0xc02f, 0x009e,
|
|
0xc024, 0xc028, 0x006b, 0xc023, 0xc027, 0x0067,
|
|
0xc00a, 0xc014, 0x0039, 0xc009, 0xc013, 0x0033,
|
|
0x009d, 0x009c, 0x003d, 0x003c, 0x0035, 0x002f,
|
|
0x00ff,
|
|
]
|
|
|
|
# OpenSSL 3.x default
|
|
OPENSSL_3_CIPHERS = [
|
|
0x1301, 0x1302, 0x1303,
|
|
0xc02c, 0xc030, 0x009f, 0xcca9, 0xcca8,
|
|
0xccaa, 0xc02b, 0xc02f, 0x009e,
|
|
0xc024, 0xc028, 0x006b, 0xc023, 0xc027, 0x0067,
|
|
0xc00a, 0xc014, 0x0039, 0xc009, 0xc013, 0x0033,
|
|
0x009d, 0x009c, 0x003d, 0x003c, 0x0035, 0x002f,
|
|
0x00ff,
|
|
]
|
|
|
|
# Go crypto/tls
|
|
GO_CIPHERS = [
|
|
0x1301, 0x1302, 0x1303,
|
|
0xc02b, 0xc02f, 0xc02c, 0xc030,
|
|
0xcca9, 0xcca8,
|
|
0xc009, 0xc013, 0xc00a, 0xc014,
|
|
0x009c, 0x009d, 0x002f, 0x0035,
|
|
]
|
|
|
|
# Java JSSE (JDK 17+)
|
|
JAVA_17_CIPHERS = [
|
|
0x1301, 0x1302, 0x1303,
|
|
0xc02c, 0xc02b, 0xc030, 0xc02f,
|
|
0xc024, 0xc023, 0xc028, 0xc027,
|
|
0xcca9, 0xcca8, 0xccaa,
|
|
0xc00a, 0xc009, 0xc014, 0xc013,
|
|
0x009d, 0x009c,
|
|
0x003d, 0x003c, 0x0035, 0x002f,
|
|
0x00ff,
|
|
]
|
|
|
|
# Java JSSE (JDK 11)
|
|
JAVA_11_CIPHERS = [
|
|
0x1301, 0x1302, 0x1303,
|
|
0xc02c, 0xc02b, 0xc030, 0xc02f,
|
|
0xc024, 0xc023, 0xc028, 0xc027,
|
|
0xc00a, 0xc009, 0xc014, 0xc013,
|
|
0x009d, 0x009c,
|
|
0x003d, 0x003c, 0x0035, 0x002f,
|
|
0x00ff,
|
|
]
|
|
|
|
# .NET / SChannel (Windows)
|
|
DOTNET_CIPHERS = [
|
|
0x1301, 0x1302, 0x1303,
|
|
0xc02c, 0xc02b, 0xc030, 0xc02f,
|
|
0x009f, 0x009e, 0xccaa,
|
|
0xc00a, 0xc009, 0xc014, 0xc013,
|
|
0x009d, 0x009c,
|
|
0x003d, 0x003c, 0x0035, 0x002f,
|
|
]
|
|
|
|
# Node.js (OpenSSL-based)
|
|
NODEJS_CIPHERS = [
|
|
0x1301, 0x1302, 0x1303,
|
|
0xc02c, 0xc030, 0x009f,
|
|
0xcca9, 0xcca8, 0xccaa,
|
|
0xc02b, 0xc02f, 0x009e,
|
|
0xc024, 0xc028, 0x006b,
|
|
0xc023, 0xc027, 0x0067,
|
|
0xc00a, 0xc014, 0x0039,
|
|
0xc009, 0xc013, 0x0033,
|
|
0x009d, 0x009c,
|
|
0x003d, 0x003c,
|
|
0x0035, 0x002f,
|
|
0x00ff,
|
|
]
|
|
|
|
# Python requests / urllib3 (uses OpenSSL)
|
|
PYTHON_CIPHERS = OPENSSL_3_CIPHERS
|
|
|
|
# curl (uses OpenSSL by default)
|
|
CURL_OPENSSL_CIPHERS = [
|
|
0x1301, 0x1302, 0x1303,
|
|
0xc02c, 0xc030, 0x009f, 0xcca9, 0xcca8,
|
|
0xccaa, 0xc02b, 0xc02f, 0x009e,
|
|
0xc024, 0xc028, 0x006b,
|
|
0xc023, 0xc027, 0x0067,
|
|
0xc00a, 0xc014, 0x0039,
|
|
0xc009, 0xc013, 0x0033,
|
|
0x009d, 0x009c,
|
|
0x003d, 0x003c,
|
|
0x0035, 0x002f,
|
|
0x00ff,
|
|
]
|
|
|
|
# curl (NSS backend, e.g., RHEL/CentOS)
|
|
CURL_NSS_CIPHERS = [
|
|
0x1301, 0x1303, 0x1302,
|
|
0xc02b, 0xc02f, 0xcca9, 0xcca8,
|
|
0xc02c, 0xc030,
|
|
0xc013, 0xc014,
|
|
0x009c, 0x009d,
|
|
0x002f, 0x0035,
|
|
]
|
|
|
|
# wget (GnuTLS)
|
|
WGET_CIPHERS = [
|
|
0x1301, 0x1302, 0x1303,
|
|
0xc02c, 0xc030, 0xcca9, 0xcca8,
|
|
0xc02b, 0xc02f,
|
|
0xc024, 0xc028,
|
|
0xc023, 0xc027,
|
|
0xc00a, 0xc014,
|
|
0xc009, 0xc013,
|
|
0x009d, 0x009c,
|
|
0x003d, 0x003c,
|
|
0x0035, 0x002f,
|
|
]
|
|
|
|
# mbedTLS (IoT)
|
|
MBEDTLS_CIPHERS = [
|
|
0xc02c, 0xc02b, 0xc030, 0xc02f,
|
|
0xc024, 0xc023, 0xc028, 0xc027,
|
|
0xc00a, 0xc009, 0xc014, 0xc013,
|
|
0x009d, 0x009c,
|
|
0x003d, 0x003c,
|
|
0x0035, 0x002f,
|
|
]
|
|
|
|
# wolfSSL
|
|
WOLFSSL_CIPHERS = [
|
|
0x1301, 0x1302, 0x1303,
|
|
0xc02c, 0xc02b, 0xc030, 0xc02f,
|
|
0xc00a, 0xc009, 0xc014, 0xc013,
|
|
0x009d, 0x009c,
|
|
0x0035, 0x002f,
|
|
]
|
|
|
|
# Tor Browser (modified Firefox ESR)
|
|
TOR_CIPHERS = [
|
|
0x1301, 0x1303, 0x1302,
|
|
0xc02b, 0xc02f, 0xcca9, 0xcca8,
|
|
0xc02c, 0xc030,
|
|
0xc013, 0xc014,
|
|
0x009c, 0x009d,
|
|
0x002f, 0x0035,
|
|
]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Extension sets
|
|
# ---------------------------------------------------------------------------
|
|
|
|
CHROME_EXT_V1 = [0, 23, 65281, 10, 11, 35, 16, 5, 13, 18, 51, 45, 43, 27, 17513, 21]
|
|
CHROME_EXT_V2 = [0, 23, 65281, 10, 11, 35, 16, 5, 13, 18, 51, 45, 43, 27, 17513, 21, 41]
|
|
CHROME_EXT_V3 = [0, 23, 65281, 10, 11, 35, 16, 5, 13, 18, 51, 45, 43, 27, 17513, 21, 41, 57] # post-quantum
|
|
CHROME_EXT_ANDROID = [0, 23, 65281, 10, 11, 35, 16, 5, 13, 51, 45, 43, 27, 17513, 21]
|
|
|
|
FIREFOX_EXT_V1 = [0, 23, 65281, 10, 11, 35, 16, 5, 34, 51, 43, 13, 45, 28, 21]
|
|
FIREFOX_EXT_V2 = [0, 23, 65281, 10, 11, 35, 16, 5, 34, 51, 43, 13, 45, 28, 21, 41]
|
|
FIREFOX_EXT_V3 = [0, 23, 65281, 10, 11, 35, 16, 5, 34, 51, 43, 13, 45, 28, 21, 41, 57]
|
|
|
|
SAFARI_EXT = [0, 23, 65281, 10, 11, 35, 16, 5, 13, 18, 51, 45, 43, 27, 21]
|
|
SAFARI_IOS_EXT = [0, 23, 65281, 10, 11, 35, 16, 5, 13, 51, 45, 43, 27, 21]
|
|
|
|
OPENSSL_EXT = [0, 23, 65281, 10, 11, 35, 16, 22, 13]
|
|
OPENSSL_3_EXT = [0, 23, 65281, 10, 11, 35, 16, 22, 13, 43, 51, 45]
|
|
|
|
GO_EXT = [0, 5, 10, 11, 13, 16, 18, 23, 27, 43, 45, 51, 65281]
|
|
JAVA_EXT = [0, 5, 10, 11, 13, 16, 23, 43, 45, 51, 65281]
|
|
DOTNET_EXT = [0, 10, 11, 13, 16, 23, 35, 43, 51, 65281]
|
|
NODE_EXT = [0, 23, 65281, 10, 11, 35, 16, 22, 13, 43, 51, 45]
|
|
CURL_EXT = [0, 23, 65281, 10, 11, 35, 16, 22, 13, 43, 51, 45]
|
|
WGET_EXT = [0, 23, 65281, 10, 11, 16, 13, 43, 51, 45]
|
|
TOR_EXT = [0, 23, 65281, 10, 11, 35, 16, 5, 34, 51, 43, 13, 45, 28, 21]
|
|
|
|
MBEDTLS_EXT = [0, 10, 11, 13, 16, 23, 65281]
|
|
WOLFSSL_EXT = [0, 10, 11, 13, 16, 23, 43, 51, 45, 65281]
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Elliptic curve sets
|
|
# ---------------------------------------------------------------------------
|
|
|
|
CHROME_CURVES = [29, 23, 24] # x25519, secp256r1, secp384r1
|
|
CHROME_CURVES_PQ = [29, 23, 24, 25497] # + X25519Kyber768
|
|
FIREFOX_CURVES = [29, 23, 24, 25] # + secp521r1
|
|
FIREFOX_CURVES_PQ = [29, 23, 24, 25, 25497]
|
|
SAFARI_CURVES = [29, 23, 24, 25]
|
|
GO_CURVES = [29, 23, 24]
|
|
JAVA_CURVES = [29, 23, 24, 25]
|
|
OPENSSL_CURVES = [29, 23, 24, 25]
|
|
DOTNET_CURVES = [29, 23, 24]
|
|
MBEDTLS_CURVES = [23, 24, 25] # No x25519
|
|
WOLFSSL_CURVES = [29, 23, 24]
|
|
TOR_CURVES = [29, 23, 24, 25]
|
|
|
|
EC_FORMATS_STANDARD = [0] # uncompressed
|
|
EC_FORMATS_ALL = [0, 1, 2] # uncompressed, ansiX962_compressed_prime, ansiX962_compressed_char2
|
|
|
|
|
|
def _gen_chrome_profiles() -> list[tuple]:
|
|
"""Generate Chrome/Chromium profiles across versions and platforms."""
|
|
profiles = []
|
|
|
|
platforms = [
|
|
("win", "Windows 10/11"),
|
|
("mac", "macOS"),
|
|
("linux", "Linux"),
|
|
("android", "Android"),
|
|
]
|
|
|
|
for ver in range(100, 125):
|
|
for plat_key, plat_desc in platforms:
|
|
if ver < 120:
|
|
ciphers = CHROMIUM_CIPHERS_V1
|
|
ext = CHROME_EXT_ANDROID if plat_key == "android" else CHROME_EXT_V1
|
|
curves = CHROME_CURVES
|
|
elif ver < 122:
|
|
ciphers = CHROMIUM_CIPHERS_V2
|
|
ext = CHROME_EXT_ANDROID if plat_key == "android" else CHROME_EXT_V2
|
|
curves = CHROME_CURVES
|
|
else:
|
|
ciphers = CHROMIUM_CIPHERS_V3
|
|
ext = CHROME_EXT_ANDROID if plat_key == "android" else CHROME_EXT_V3
|
|
curves = CHROME_CURVES_PQ
|
|
|
|
ja3_hash = compute_ja3(771, ciphers, ext, curves, EC_FORMATS_STANDARD)
|
|
name = f"chrome_{ver}_{plat_key}"
|
|
desc = f"Chrome {ver} on {plat_desc}"
|
|
profiles.append((
|
|
name, ja3_hash, desc,
|
|
json.dumps(ciphers), json.dumps(ext),
|
|
json.dumps(curves), json.dumps(EC_FORMATS_STANDARD),
|
|
))
|
|
|
|
return profiles
|
|
|
|
|
|
def _gen_firefox_profiles() -> list[tuple]:
|
|
"""Generate Firefox profiles."""
|
|
profiles = []
|
|
|
|
platforms = [
|
|
("win", "Windows 10/11"),
|
|
("mac", "macOS"),
|
|
("linux", "Linux"),
|
|
]
|
|
|
|
for ver in range(100, 126):
|
|
for plat_key, plat_desc in platforms:
|
|
if ver < 118:
|
|
ciphers = FIREFOX_CIPHERS_V1
|
|
ext = FIREFOX_EXT_V1
|
|
curves = FIREFOX_CURVES
|
|
elif ver < 123:
|
|
ciphers = FIREFOX_CIPHERS_V2
|
|
ext = FIREFOX_EXT_V2
|
|
curves = FIREFOX_CURVES
|
|
else:
|
|
ciphers = FIREFOX_CIPHERS_V2
|
|
ext = FIREFOX_EXT_V3
|
|
curves = FIREFOX_CURVES_PQ
|
|
|
|
ja3_hash = compute_ja3(771, ciphers, ext, curves, EC_FORMATS_STANDARD)
|
|
name = f"firefox_{ver}_{plat_key}"
|
|
desc = f"Firefox {ver} on {plat_desc}"
|
|
profiles.append((
|
|
name, ja3_hash, desc,
|
|
json.dumps(ciphers), json.dumps(ext),
|
|
json.dumps(curves), json.dumps(EC_FORMATS_STANDARD),
|
|
))
|
|
|
|
return profiles
|
|
|
|
|
|
def _gen_edge_profiles() -> list[tuple]:
|
|
"""Generate Edge profiles (Chromium-based, similar but not identical to Chrome)."""
|
|
profiles = []
|
|
|
|
for ver in range(100, 125):
|
|
if ver < 120:
|
|
ciphers = CHROMIUM_CIPHERS_V1
|
|
ext = CHROME_EXT_V1
|
|
curves = CHROME_CURVES
|
|
elif ver < 122:
|
|
ciphers = CHROMIUM_CIPHERS_V2
|
|
ext = CHROME_EXT_V2
|
|
curves = CHROME_CURVES
|
|
else:
|
|
ciphers = CHROMIUM_CIPHERS_V3
|
|
ext = CHROME_EXT_V3
|
|
curves = CHROME_CURVES_PQ
|
|
|
|
# Edge adds a few extra extensions on Windows
|
|
edge_ext = ext + [34]
|
|
|
|
ja3_hash = compute_ja3(771, ciphers, edge_ext, curves, EC_FORMATS_STANDARD)
|
|
name = f"edge_{ver}_win"
|
|
desc = f"Edge {ver} on Windows 10/11"
|
|
profiles.append((
|
|
name, ja3_hash, desc,
|
|
json.dumps(ciphers), json.dumps(edge_ext),
|
|
json.dumps(curves), json.dumps(EC_FORMATS_STANDARD),
|
|
))
|
|
|
|
return profiles
|
|
|
|
|
|
def _gen_safari_profiles() -> list[tuple]:
|
|
"""Generate Safari profiles (macOS + iOS)."""
|
|
profiles = []
|
|
|
|
# macOS Safari versions 15-17
|
|
for ver in range(15, 18):
|
|
for minor in range(0, 6):
|
|
ja3_hash = compute_ja3(771, SAFARI_CIPHERS, SAFARI_EXT, SAFARI_CURVES, EC_FORMATS_STANDARD)
|
|
name = f"safari_{ver}_{minor}_mac"
|
|
desc = f"Safari {ver}.{minor} on macOS"
|
|
profiles.append((
|
|
name, ja3_hash, desc,
|
|
json.dumps(SAFARI_CIPHERS), json.dumps(SAFARI_EXT),
|
|
json.dumps(SAFARI_CURVES), json.dumps(EC_FORMATS_STANDARD),
|
|
))
|
|
|
|
# iOS Safari versions 15-17
|
|
for ver in range(15, 18):
|
|
for minor in range(0, 6):
|
|
ja3_hash = compute_ja3(771, SAFARI_IOS_CIPHERS, SAFARI_IOS_EXT, SAFARI_CURVES, EC_FORMATS_STANDARD)
|
|
name = f"safari_{ver}_{minor}_ios"
|
|
desc = f"Safari {ver}.{minor} on iOS"
|
|
profiles.append((
|
|
name, ja3_hash, desc,
|
|
json.dumps(SAFARI_IOS_CIPHERS), json.dumps(SAFARI_IOS_EXT),
|
|
json.dumps(SAFARI_CURVES), json.dumps(EC_FORMATS_STANDARD),
|
|
))
|
|
|
|
return profiles
|
|
|
|
|
|
def _gen_opera_profiles() -> list[tuple]:
|
|
"""Generate Opera profiles (Chromium-based)."""
|
|
profiles = []
|
|
for ver in range(86, 106):
|
|
ciphers = CHROMIUM_CIPHERS_V2 if ver >= 105 else CHROMIUM_CIPHERS_V1
|
|
ext = CHROME_EXT_V2 if ver >= 105 else CHROME_EXT_V1
|
|
curves = CHROME_CURVES
|
|
|
|
ja3_hash = compute_ja3(771, ciphers, ext, curves, EC_FORMATS_STANDARD)
|
|
name = f"opera_{ver}_win"
|
|
desc = f"Opera {ver} on Windows"
|
|
profiles.append((
|
|
name, ja3_hash, desc,
|
|
json.dumps(ciphers), json.dumps(ext),
|
|
json.dumps(curves), json.dumps(EC_FORMATS_STANDARD),
|
|
))
|
|
|
|
return profiles
|
|
|
|
|
|
def _gen_brave_profiles() -> list[tuple]:
|
|
"""Generate Brave profiles (Chromium-based, randomized fingerprint features)."""
|
|
profiles = []
|
|
|
|
platforms = [("win", "Windows"), ("mac", "macOS"), ("linux", "Linux")]
|
|
|
|
for ver in range(110, 125):
|
|
for plat_key, plat_desc in platforms:
|
|
ciphers = CHROMIUM_CIPHERS_V2 if ver >= 120 else CHROMIUM_CIPHERS_V1
|
|
ext = CHROME_EXT_V2 if ver >= 120 else CHROME_EXT_V1
|
|
curves = CHROME_CURVES
|
|
|
|
ja3_hash = compute_ja3(771, ciphers, ext, curves, EC_FORMATS_STANDARD)
|
|
name = f"brave_{ver}_{plat_key}"
|
|
desc = f"Brave {ver} on {plat_desc}"
|
|
profiles.append((
|
|
name, ja3_hash, desc,
|
|
json.dumps(ciphers), json.dumps(ext),
|
|
json.dumps(curves), json.dumps(EC_FORMATS_STANDARD),
|
|
))
|
|
|
|
return profiles
|
|
|
|
|
|
def _gen_tor_profiles() -> list[tuple]:
|
|
"""Generate Tor Browser profiles."""
|
|
profiles = []
|
|
for ver_major in range(12, 14):
|
|
for ver_minor in range(0, 6):
|
|
ja3_hash = compute_ja3(771, TOR_CIPHERS, TOR_EXT, TOR_CURVES, EC_FORMATS_STANDARD)
|
|
name = f"tor_{ver_major}_{ver_minor}"
|
|
desc = f"Tor Browser {ver_major}.{ver_minor}"
|
|
profiles.append((
|
|
name, ja3_hash, desc,
|
|
json.dumps(TOR_CIPHERS), json.dumps(TOR_EXT),
|
|
json.dumps(TOR_CURVES), json.dumps(EC_FORMATS_STANDARD),
|
|
))
|
|
return profiles
|
|
|
|
|
|
def _gen_library_profiles() -> list[tuple]:
|
|
"""Generate TLS library profiles."""
|
|
profiles = []
|
|
|
|
# OpenSSL versions
|
|
for minor in range(0, 10):
|
|
for patch in range(0, 3):
|
|
# OpenSSL 1.1.1x
|
|
ciphers = OPENSSL_11_CIPHERS
|
|
ext = OPENSSL_EXT
|
|
ja3_hash = compute_ja3(771, ciphers, ext, OPENSSL_CURVES, EC_FORMATS_ALL)
|
|
name = f"openssl_1_1_1_{chr(97 + minor)}"
|
|
desc = f"OpenSSL 1.1.1{chr(97 + minor)}"
|
|
profiles.append((
|
|
name, ja3_hash, desc,
|
|
json.dumps(ciphers), json.dumps(ext),
|
|
json.dumps(OPENSSL_CURVES), json.dumps(EC_FORMATS_ALL),
|
|
))
|
|
|
|
for minor in range(0, 5):
|
|
# OpenSSL 3.x
|
|
ciphers = OPENSSL_3_CIPHERS
|
|
ext = OPENSSL_3_EXT
|
|
ja3_hash = compute_ja3(771, ciphers, ext, OPENSSL_CURVES, EC_FORMATS_ALL)
|
|
name = f"openssl_3_{minor}"
|
|
desc = f"OpenSSL 3.{minor}"
|
|
profiles.append((
|
|
name, ja3_hash, desc,
|
|
json.dumps(ciphers), json.dumps(ext),
|
|
json.dumps(OPENSSL_CURVES), json.dumps(EC_FORMATS_ALL),
|
|
))
|
|
|
|
# Go versions
|
|
for minor in range(18, 23):
|
|
ja3_hash = compute_ja3(771, GO_CIPHERS, GO_EXT, GO_CURVES, EC_FORMATS_STANDARD)
|
|
name = f"go_1_{minor}"
|
|
desc = f"Go 1.{minor} net/http"
|
|
profiles.append((
|
|
name, ja3_hash, desc,
|
|
json.dumps(GO_CIPHERS), json.dumps(GO_EXT),
|
|
json.dumps(GO_CURVES), json.dumps(EC_FORMATS_STANDARD),
|
|
))
|
|
|
|
# Java versions
|
|
for ver in [8, 11, 17, 21]:
|
|
ciphers = JAVA_17_CIPHERS if ver >= 17 else JAVA_11_CIPHERS
|
|
ja3_hash = compute_ja3(771, ciphers, JAVA_EXT, JAVA_CURVES, EC_FORMATS_STANDARD)
|
|
name = f"java_jdk_{ver}"
|
|
desc = f"Java JDK {ver} JSSE"
|
|
profiles.append((
|
|
name, ja3_hash, desc,
|
|
json.dumps(ciphers), json.dumps(JAVA_EXT),
|
|
json.dumps(JAVA_CURVES), json.dumps(EC_FORMATS_STANDARD),
|
|
))
|
|
|
|
# .NET versions
|
|
for ver in ["6", "7", "8"]:
|
|
ja3_hash = compute_ja3(771, DOTNET_CIPHERS, DOTNET_EXT, DOTNET_CURVES, EC_FORMATS_STANDARD)
|
|
name = f"dotnet_{ver}"
|
|
desc = f".NET {ver} HttpClient (SChannel)"
|
|
profiles.append((
|
|
name, ja3_hash, desc,
|
|
json.dumps(DOTNET_CIPHERS), json.dumps(DOTNET_EXT),
|
|
json.dumps(DOTNET_CURVES), json.dumps(EC_FORMATS_STANDARD),
|
|
))
|
|
|
|
# Node.js versions
|
|
for ver in range(16, 22):
|
|
ja3_hash = compute_ja3(771, NODEJS_CIPHERS, NODE_EXT, OPENSSL_CURVES, EC_FORMATS_ALL)
|
|
name = f"nodejs_{ver}"
|
|
desc = f"Node.js {ver} https"
|
|
profiles.append((
|
|
name, ja3_hash, desc,
|
|
json.dumps(NODEJS_CIPHERS), json.dumps(NODE_EXT),
|
|
json.dumps(OPENSSL_CURVES), json.dumps(EC_FORMATS_ALL),
|
|
))
|
|
|
|
# mbedTLS versions
|
|
for ver in ["2.28", "3.0", "3.4", "3.5"]:
|
|
ja3_hash = compute_ja3(771, MBEDTLS_CIPHERS, MBEDTLS_EXT, MBEDTLS_CURVES, EC_FORMATS_STANDARD)
|
|
name = f"mbedtls_{ver.replace('.', '_')}"
|
|
desc = f"mbedTLS {ver}"
|
|
profiles.append((
|
|
name, ja3_hash, desc,
|
|
json.dumps(MBEDTLS_CIPHERS), json.dumps(MBEDTLS_EXT),
|
|
json.dumps(MBEDTLS_CURVES), json.dumps(EC_FORMATS_STANDARD),
|
|
))
|
|
|
|
# wolfSSL
|
|
for ver in ["5.5", "5.6", "5.7"]:
|
|
ja3_hash = compute_ja3(771, WOLFSSL_CIPHERS, WOLFSSL_EXT, WOLFSSL_CURVES, EC_FORMATS_STANDARD)
|
|
name = f"wolfssl_{ver.replace('.', '_')}"
|
|
desc = f"wolfSSL {ver}"
|
|
profiles.append((
|
|
name, ja3_hash, desc,
|
|
json.dumps(WOLFSSL_CIPHERS), json.dumps(WOLFSSL_EXT),
|
|
json.dumps(WOLFSSL_CURVES), json.dumps(EC_FORMATS_STANDARD),
|
|
))
|
|
|
|
return profiles
|
|
|
|
|
|
def _gen_tool_profiles() -> list[tuple]:
|
|
"""Generate common tool profiles."""
|
|
profiles = []
|
|
|
|
# curl with different backends
|
|
for ver in ["7.81", "7.88", "8.0", "8.1", "8.4", "8.5", "8.6", "8.7"]:
|
|
ja3_hash = compute_ja3(771, CURL_OPENSSL_CIPHERS, CURL_EXT, OPENSSL_CURVES, EC_FORMATS_ALL)
|
|
name = f"curl_{ver.replace('.', '_')}_openssl"
|
|
desc = f"curl/{ver} (OpenSSL)"
|
|
profiles.append((
|
|
name, ja3_hash, desc,
|
|
json.dumps(CURL_OPENSSL_CIPHERS), json.dumps(CURL_EXT),
|
|
json.dumps(OPENSSL_CURVES), json.dumps(EC_FORMATS_ALL),
|
|
))
|
|
|
|
for ver in ["7.76", "7.79", "7.81"]:
|
|
ja3_hash = compute_ja3(771, CURL_NSS_CIPHERS, CURL_EXT, FIREFOX_CURVES, EC_FORMATS_STANDARD)
|
|
name = f"curl_{ver.replace('.', '_')}_nss"
|
|
desc = f"curl/{ver} (NSS)"
|
|
profiles.append((
|
|
name, ja3_hash, desc,
|
|
json.dumps(CURL_NSS_CIPHERS), json.dumps(CURL_EXT),
|
|
json.dumps(FIREFOX_CURVES), json.dumps(EC_FORMATS_STANDARD),
|
|
))
|
|
|
|
# wget
|
|
for ver in ["1.21", "2.0", "2.1"]:
|
|
ja3_hash = compute_ja3(771, WGET_CIPHERS, WGET_EXT, OPENSSL_CURVES, EC_FORMATS_STANDARD)
|
|
name = f"wget_{ver.replace('.', '_')}"
|
|
desc = f"wget/{ver} (GnuTLS)"
|
|
profiles.append((
|
|
name, ja3_hash, desc,
|
|
json.dumps(WGET_CIPHERS), json.dumps(WGET_EXT),
|
|
json.dumps(OPENSSL_CURVES), json.dumps(EC_FORMATS_STANDARD),
|
|
))
|
|
|
|
# Python requests/urllib3/httpx
|
|
for lib in ["requests_2_31", "requests_2_32", "httpx_0_25", "httpx_0_27",
|
|
"aiohttp_3_9", "urllib3_2_1"]:
|
|
ja3_hash = compute_ja3(771, PYTHON_CIPHERS, OPENSSL_3_EXT, OPENSSL_CURVES, EC_FORMATS_ALL)
|
|
name = f"python_{lib}"
|
|
desc = f"Python {lib.replace('_', '/')}"
|
|
profiles.append((
|
|
name, ja3_hash, desc,
|
|
json.dumps(PYTHON_CIPHERS), json.dumps(OPENSSL_3_EXT),
|
|
json.dumps(OPENSSL_CURVES), json.dumps(EC_FORMATS_ALL),
|
|
))
|
|
|
|
# Rust reqwest (uses rustls)
|
|
RUSTLS_CIPHERS = [0x1301, 0x1302, 0x1303, 0xc02c, 0xc02b, 0xc030, 0xc02f, 0xcca9, 0xcca8]
|
|
RUSTLS_EXT = [0, 10, 11, 13, 16, 23, 43, 45, 51, 65281]
|
|
RUSTLS_CURVES = [29, 23, 24]
|
|
for ver in ["0_11", "0_12"]:
|
|
ja3_hash = compute_ja3(771, RUSTLS_CIPHERS, RUSTLS_EXT, RUSTLS_CURVES, EC_FORMATS_STANDARD)
|
|
name = f"rust_reqwest_{ver}"
|
|
desc = f"Rust reqwest {ver.replace('_', '.')} (rustls)"
|
|
profiles.append((
|
|
name, ja3_hash, desc,
|
|
json.dumps(RUSTLS_CIPHERS), json.dumps(RUSTLS_EXT),
|
|
json.dumps(RUSTLS_CURVES), json.dumps(EC_FORMATS_STANDARD),
|
|
))
|
|
|
|
return profiles
|
|
|
|
|
|
def _gen_mobile_profiles() -> list[tuple]:
|
|
"""Generate mobile-specific profiles beyond Android Chrome."""
|
|
profiles = []
|
|
|
|
# Samsung Internet
|
|
for ver in range(20, 25):
|
|
ciphers = CHROMIUM_CIPHERS_V2 if ver >= 23 else CHROMIUM_CIPHERS_V1
|
|
ext = CHROME_EXT_ANDROID
|
|
ja3_hash = compute_ja3(771, ciphers, ext, CHROME_CURVES, EC_FORMATS_STANDARD)
|
|
name = f"samsung_internet_{ver}_android"
|
|
desc = f"Samsung Internet {ver} on Android"
|
|
profiles.append((
|
|
name, ja3_hash, desc,
|
|
json.dumps(ciphers), json.dumps(ext),
|
|
json.dumps(CHROME_CURVES), json.dumps(EC_FORMATS_STANDARD),
|
|
))
|
|
|
|
# Android WebView
|
|
for ver in range(100, 125):
|
|
ciphers = CHROMIUM_CIPHERS_V2 if ver >= 120 else CHROMIUM_CIPHERS_V1
|
|
ext = CHROME_EXT_ANDROID
|
|
ja3_hash = compute_ja3(771, ciphers, ext, CHROME_CURVES, EC_FORMATS_STANDARD)
|
|
name = f"android_webview_{ver}"
|
|
desc = f"Android WebView {ver}"
|
|
profiles.append((
|
|
name, ja3_hash, desc,
|
|
json.dumps(ciphers), json.dumps(ext),
|
|
json.dumps(CHROME_CURVES), json.dumps(EC_FORMATS_STANDARD),
|
|
))
|
|
|
|
# iOS WKWebView (uses Apple Secure Transport, same as Safari)
|
|
for ver in range(15, 18):
|
|
for minor in range(0, 5):
|
|
ja3_hash = compute_ja3(771, SAFARI_IOS_CIPHERS, SAFARI_IOS_EXT, SAFARI_CURVES, EC_FORMATS_STANDARD)
|
|
name = f"ios_wkwebview_{ver}_{minor}"
|
|
desc = f"iOS {ver}.{minor} WKWebView"
|
|
profiles.append((
|
|
name, ja3_hash, desc,
|
|
json.dumps(SAFARI_IOS_CIPHERS), json.dumps(SAFARI_IOS_EXT),
|
|
json.dumps(SAFARI_CURVES), json.dumps(EC_FORMATS_STANDARD),
|
|
))
|
|
|
|
return profiles
|
|
|
|
|
|
def _gen_iot_embedded_profiles() -> list[tuple]:
|
|
"""Generate IoT and embedded client profiles."""
|
|
profiles = []
|
|
|
|
# ESP32 (mbedTLS-based)
|
|
ESP32_CIPHERS = [0xc02c, 0xc02b, 0x009d, 0x009c, 0x0035, 0x002f]
|
|
ESP32_EXT = [0, 10, 11, 13, 23, 65281]
|
|
ESP32_CURVES = [23, 24]
|
|
for sdk_ver in ["4.4", "5.0", "5.1", "5.2"]:
|
|
ja3_hash = compute_ja3(771, ESP32_CIPHERS, ESP32_EXT, ESP32_CURVES, EC_FORMATS_STANDARD)
|
|
name = f"esp32_idf_{sdk_ver.replace('.', '_')}"
|
|
desc = f"ESP-IDF {sdk_ver} (ESP32)"
|
|
profiles.append((
|
|
name, ja3_hash, desc,
|
|
json.dumps(ESP32_CIPHERS), json.dumps(ESP32_EXT),
|
|
json.dumps(ESP32_CURVES), json.dumps(EC_FORMATS_STANDARD),
|
|
))
|
|
|
|
# AWS IoT SDK (OpenSSL-based)
|
|
AWS_IOT_CIPHERS = [0x1301, 0x1302, 0x1303, 0xc02c, 0xc02b, 0xc030, 0xc02f, 0x009d, 0x009c, 0x0035, 0x002f]
|
|
AWS_IOT_EXT = [0, 10, 11, 13, 16, 23, 43, 45, 51, 65281]
|
|
for sdk_ver in ["1.0", "1.1", "2.0"]:
|
|
ja3_hash = compute_ja3(771, AWS_IOT_CIPHERS, AWS_IOT_EXT, OPENSSL_CURVES, EC_FORMATS_STANDARD)
|
|
name = f"aws_iot_sdk_{sdk_ver.replace('.', '_')}"
|
|
desc = f"AWS IoT Device SDK {sdk_ver}"
|
|
profiles.append((
|
|
name, ja3_hash, desc,
|
|
json.dumps(AWS_IOT_CIPHERS), json.dumps(AWS_IOT_EXT),
|
|
json.dumps(OPENSSL_CURVES), json.dumps(EC_FORMATS_STANDARD),
|
|
))
|
|
|
|
# Azure IoT SDK
|
|
AZURE_IOT_CIPHERS = OPENSSL_3_CIPHERS
|
|
for sdk_ver in ["1.10", "1.11", "1.12"]:
|
|
ja3_hash = compute_ja3(771, AZURE_IOT_CIPHERS, OPENSSL_3_EXT, OPENSSL_CURVES, EC_FORMATS_ALL)
|
|
name = f"azure_iot_sdk_{sdk_ver.replace('.', '_')}"
|
|
desc = f"Azure IoT SDK {sdk_ver}"
|
|
profiles.append((
|
|
name, ja3_hash, desc,
|
|
json.dumps(AZURE_IOT_CIPHERS), json.dumps(OPENSSL_3_EXT),
|
|
json.dumps(OPENSSL_CURVES), json.dumps(EC_FORMATS_ALL),
|
|
))
|
|
|
|
# Smart home devices (various embedded TLS stacks)
|
|
SMART_HOME_CIPHERS = [0xc02c, 0xc02b, 0xc030, 0xc02f, 0x009d, 0x009c, 0x0035, 0x002f]
|
|
SMART_HOME_EXT = [0, 10, 11, 13, 23, 65281]
|
|
for device in ["ring_doorbell", "nest_cam", "hue_bridge", "echo_dot",
|
|
"google_home", "sonos_one", "roku_ultra", "apple_tv",
|
|
"fire_tv", "nvidia_shield"]:
|
|
ja3_hash = compute_ja3(771, SMART_HOME_CIPHERS, SMART_HOME_EXT, [23, 24], EC_FORMATS_STANDARD)
|
|
name = f"iot_{device}"
|
|
desc = f"IoT: {device.replace('_', ' ').title()}"
|
|
profiles.append((
|
|
name, ja3_hash, desc,
|
|
json.dumps(SMART_HOME_CIPHERS), json.dumps(SMART_HOME_EXT),
|
|
json.dumps([23, 24]), json.dumps(EC_FORMATS_STANDARD),
|
|
))
|
|
|
|
return profiles
|
|
|
|
|
|
def _gen_chromeos_profiles() -> list[tuple]:
|
|
"""Generate ChromeOS profiles."""
|
|
profiles = []
|
|
for ver in range(110, 125):
|
|
ciphers = CHROMIUM_CIPHERS_V2 if ver >= 120 else CHROMIUM_CIPHERS_V1
|
|
ext = CHROME_EXT_V2 if ver >= 120 else CHROME_EXT_V1
|
|
curves = CHROME_CURVES_PQ if ver >= 122 else CHROME_CURVES
|
|
|
|
ja3_hash = compute_ja3(771, ciphers, ext, curves, EC_FORMATS_STANDARD)
|
|
name = f"chromeos_{ver}"
|
|
desc = f"ChromeOS {ver}"
|
|
profiles.append((
|
|
name, ja3_hash, desc,
|
|
json.dumps(ciphers), json.dumps(ext),
|
|
json.dumps(curves), json.dumps(EC_FORMATS_STANDARD),
|
|
))
|
|
return profiles
|
|
|
|
|
|
def _gen_vivaldi_profiles() -> list[tuple]:
|
|
"""Generate Vivaldi profiles (Chromium-based)."""
|
|
profiles = []
|
|
for ver in range(5, 7):
|
|
for minor in range(0, 10):
|
|
ciphers = CHROMIUM_CIPHERS_V2 if ver >= 6 and minor >= 5 else CHROMIUM_CIPHERS_V1
|
|
ext = CHROME_EXT_V1
|
|
curves = CHROME_CURVES
|
|
|
|
ja3_hash = compute_ja3(771, ciphers, ext, curves, EC_FORMATS_STANDARD)
|
|
name = f"vivaldi_{ver}_{minor}_win"
|
|
desc = f"Vivaldi {ver}.{minor} on Windows"
|
|
profiles.append((
|
|
name, ja3_hash, desc,
|
|
json.dumps(ciphers), json.dumps(ext),
|
|
json.dumps(curves), json.dumps(EC_FORMATS_STANDARD),
|
|
))
|
|
return profiles
|
|
|
|
|
|
def _gen_vpn_client_profiles() -> list[tuple]:
|
|
"""Generate VPN client TLS profiles."""
|
|
profiles = []
|
|
|
|
# Cisco AnyConnect (OpenSSL-based)
|
|
for ver in ["4.10", "5.0", "5.1"]:
|
|
ja3_hash = compute_ja3(771, OPENSSL_3_CIPHERS, OPENSSL_3_EXT, OPENSSL_CURVES, EC_FORMATS_ALL)
|
|
name = f"cisco_anyconnect_{ver.replace('.', '_')}"
|
|
desc = f"Cisco AnyConnect {ver}"
|
|
profiles.append((
|
|
name, ja3_hash, desc,
|
|
json.dumps(OPENSSL_3_CIPHERS), json.dumps(OPENSSL_3_EXT),
|
|
json.dumps(OPENSSL_CURVES), json.dumps(EC_FORMATS_ALL),
|
|
))
|
|
|
|
# GlobalProtect (OpenSSL)
|
|
for ver in ["5.3", "6.0", "6.1", "6.2"]:
|
|
ja3_hash = compute_ja3(771, OPENSSL_3_CIPHERS, OPENSSL_3_EXT, OPENSSL_CURVES, EC_FORMATS_ALL)
|
|
name = f"globalprotect_{ver.replace('.', '_')}"
|
|
desc = f"Palo Alto GlobalProtect {ver}"
|
|
profiles.append((
|
|
name, ja3_hash, desc,
|
|
json.dumps(OPENSSL_3_CIPHERS), json.dumps(OPENSSL_3_EXT),
|
|
json.dumps(OPENSSL_CURVES), json.dumps(EC_FORMATS_ALL),
|
|
))
|
|
|
|
# OpenVPN (OpenSSL)
|
|
for ver in ["2.5", "2.6"]:
|
|
ja3_hash = compute_ja3(771, OPENSSL_3_CIPHERS, OPENSSL_3_EXT, OPENSSL_CURVES, EC_FORMATS_ALL)
|
|
name = f"openvpn_{ver.replace('.', '_')}"
|
|
desc = f"OpenVPN {ver}"
|
|
profiles.append((
|
|
name, ja3_hash, desc,
|
|
json.dumps(OPENSSL_3_CIPHERS), json.dumps(OPENSSL_3_EXT),
|
|
json.dumps(OPENSSL_CURVES), json.dumps(EC_FORMATS_ALL),
|
|
))
|
|
|
|
# Cloudflare WARP
|
|
WARP_CIPHERS = [0x1301, 0x1302, 0x1303, 0xc02b, 0xc02f, 0xc02c, 0xc030, 0xcca9, 0xcca8]
|
|
WARP_EXT = [0, 10, 11, 13, 16, 23, 43, 45, 51, 65281]
|
|
ja3_hash = compute_ja3(771, WARP_CIPHERS, WARP_EXT, [29, 23, 24], EC_FORMATS_STANDARD)
|
|
profiles.append((
|
|
"cloudflare_warp", ja3_hash, "Cloudflare WARP (BoringSSL)",
|
|
json.dumps(WARP_CIPHERS), json.dumps(WARP_EXT),
|
|
json.dumps([29, 23, 24]), json.dumps(EC_FORMATS_STANDARD),
|
|
))
|
|
|
|
return profiles
|
|
|
|
|
|
def _gen_electron_profiles() -> list[tuple]:
|
|
"""Generate Electron app profiles (Chromium-based)."""
|
|
profiles = []
|
|
apps = [
|
|
("vscode", "VS Code"), ("slack", "Slack"), ("discord", "Discord"),
|
|
("teams", "Microsoft Teams"), ("signal_desktop", "Signal Desktop"),
|
|
("spotify", "Spotify Desktop"), ("figma", "Figma Desktop"),
|
|
("notion", "Notion"), ("obsidian", "Obsidian"),
|
|
("1password", "1Password"), ("bitwarden", "Bitwarden"),
|
|
("postman", "Postman"), ("whatsapp_desktop", "WhatsApp Desktop"),
|
|
]
|
|
|
|
for app_id, app_name in apps:
|
|
# Electron uses Chromium's TLS stack
|
|
ciphers = CHROMIUM_CIPHERS_V2
|
|
ext = CHROME_EXT_V2
|
|
curves = CHROME_CURVES
|
|
ja3_hash = compute_ja3(771, ciphers, ext, curves, EC_FORMATS_STANDARD)
|
|
name = f"electron_{app_id}"
|
|
desc = f"Electron: {app_name}"
|
|
profiles.append((
|
|
name, ja3_hash, desc,
|
|
json.dumps(ciphers), json.dumps(ext),
|
|
json.dumps(curves), json.dumps(EC_FORMATS_STANDARD),
|
|
))
|
|
|
|
return profiles
|
|
|
|
|
|
def generate_all_profiles() -> list[tuple]:
|
|
"""Generate all JA3 profiles. Returns list of (name, hash, desc, ciphers, ext, curves, formats)."""
|
|
all_profiles = []
|
|
all_profiles.extend(_gen_chrome_profiles())
|
|
all_profiles.extend(_gen_firefox_profiles())
|
|
all_profiles.extend(_gen_edge_profiles())
|
|
all_profiles.extend(_gen_safari_profiles())
|
|
all_profiles.extend(_gen_opera_profiles())
|
|
all_profiles.extend(_gen_brave_profiles())
|
|
all_profiles.extend(_gen_tor_profiles())
|
|
all_profiles.extend(_gen_library_profiles())
|
|
all_profiles.extend(_gen_tool_profiles())
|
|
all_profiles.extend(_gen_mobile_profiles())
|
|
all_profiles.extend(_gen_iot_embedded_profiles())
|
|
all_profiles.extend(_gen_chromeos_profiles())
|
|
all_profiles.extend(_gen_vivaldi_profiles())
|
|
all_profiles.extend(_gen_vpn_client_profiles())
|
|
all_profiles.extend(_gen_electron_profiles())
|
|
|
|
# Deduplicate by name
|
|
seen = set()
|
|
unique = []
|
|
for p in all_profiles:
|
|
if p[0] not in seen:
|
|
seen.add(p[0])
|
|
unique.append(p)
|
|
|
|
return unique
|
|
|
|
|
|
def seed_ja3_db(db_path: str) -> int:
|
|
"""Seed a ja3_fingerprints.db with all generated profiles.
|
|
|
|
Returns the total number of profiles in the database.
|
|
"""
|
|
profiles = generate_all_profiles()
|
|
|
|
conn = sqlite3.connect(db_path)
|
|
conn.execute("PRAGMA journal_mode=WAL")
|
|
conn.executescript("""
|
|
CREATE TABLE IF NOT EXISTS ja3_profiles (
|
|
name TEXT PRIMARY KEY,
|
|
ja3_hash TEXT NOT NULL,
|
|
description TEXT DEFAULT '',
|
|
cipher_suites TEXT NOT NULL DEFAULT '[]',
|
|
extensions TEXT NOT NULL DEFAULT '[]',
|
|
elliptic_curves TEXT NOT NULL DEFAULT '[]',
|
|
ec_point_formats TEXT NOT NULL DEFAULT '[0]'
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_ja3_hash ON ja3_profiles(ja3_hash);
|
|
""")
|
|
|
|
conn.executemany(
|
|
"INSERT OR REPLACE INTO ja3_profiles "
|
|
"(name, ja3_hash, description, cipher_suites, extensions, elliptic_curves, ec_point_formats) "
|
|
"VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
profiles,
|
|
)
|
|
conn.commit()
|
|
count = conn.execute("SELECT COUNT(*) FROM ja3_profiles").fetchone()[0]
|
|
conn.close()
|
|
return count
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
if len(sys.argv) > 1:
|
|
db_path = sys.argv[1]
|
|
else:
|
|
db_path = str(Path(__file__).resolve().parent.parent / "data" / "ja3_fingerprints.db")
|
|
|
|
count = seed_ja3_db(db_path)
|
|
print(f"Seeded {count} JA3 profiles to {db_path}")
|