Initial public release

Full BigBrother network implant - passive SOC + active exploitation.
Personal identifiers removed; all capabilities intact.
See README.md for setup and docs/deployment.md for detailed deployment.
This commit is contained in:
n0mad1k
2026-06-26 09:52:50 -04:00
commit ccc6b729de
175 changed files with 47941 additions and 0 deletions
+984
View File
@@ -0,0 +1,984 @@
#!/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}")