Browser Fingerprint Viewer — Swift source
See exactly what websites can learn about your browser without cookies — screen, GPU, fonts, timezone, language, and more. Educational, not tracking.
This is the Swift implementation — the same logic the interactive tool runs, in a shareable, citable form.
// Browser Fingerprint — pure logic (Swift port): the 18-signal privacy-risk
// registry with canonical grouping, risk counts, and a stable SHA-256
// fingerprint hash.
//
// Language: Swift 5.9 — standard library, plus Foundation only for
// String(format:) in the hex encoder.
// Source: CosmoDev polyglot showcase port; canonical = src/lib/browser-fingerprint.ts
// + this tool's python.py / rust.rs.
// License: display source — part of CosmoDev's polyglot tool pages.
//
// Ported from the TypeScript reference. Covers the portable core: the
// 18-signal registry with privacy-risk classifications, canonical category
// grouping, risk counts, and the stable SHA-256 fingerprint hash. The
// browser-API collection layer (canvas / WebGL / font probes) is DOM-bound
// by design and does not port.
//
// SHA-256 note: Apple platforms ship CryptoKit (import Crypto;
// SHA256.hash(data:)) and Linux has the swift-crypto package, but neither
// belongs to the standard library. This snippet hand-rolls FIPS 180-4
// SHA-256 only to stay dependency-free; the "abc" check in the demo below
// pins the implementation to the specification.
import Foundation
/// Privacy-risk classification.
enum RiskLevel: String {
/// Near-unique on its own or part of a rare combination.
case high = "high"
/// Narrows you to a sizable-but-specific population.
case medium = "medium"
/// Coarse; shared by millions of browsers.
case low = "low"
}
/// Groups related signals.
enum FingerprintCategory: String {
case hardware = "Hardware"
case graphics = "Graphics"
case network = "Network"
case browser = "Browser"
case input = "Input"
}
/// Canonical category order (card grid, top to bottom).
let fingerprintCategories: [FingerprintCategory] = [
.hardware, .graphics, .network, .browser, .input,
]
/// One collected, classified signal.
struct FingerprintSignal {
let id: String
let label: String
var value: String
let risk: RiskLevel
let category: FingerprintCategory
}
/// Classifies one signal id.
struct SignalDef {
let label: String
let risk: RiskLevel
let category: FingerprintCategory
}
/// Static registry of every signal the tool collects, with its privacy-risk
/// classification. Single source of truth: buildSignal derives from it, so a
/// signal can never be collected without being classified.
let signalDefinitions: [String: SignalDef] = [
// High risk
"canvas": SignalDef(label: "Canvas fingerprint", risk: .high, category: .graphics),
"webgl-renderer": SignalDef(label: "WebGL renderer", risk: .high, category: .graphics),
"webgl-vendor": SignalDef(label: "WebGL vendor", risk: .high, category: .graphics),
"fonts": SignalDef(label: "Installed fonts", risk: .high, category: .browser),
"timezone": SignalDef(label: "Timezone", risk: .high, category: .browser),
// Medium risk
"screen": SignalDef(label: "Screen & color depth", risk: .medium, category: .hardware),
"device-pixel-ratio": SignalDef(label: "Device pixel ratio", risk: .medium, category: .hardware),
"hardware-concurrency": SignalDef(label: "CPU cores", risk: .medium, category: .hardware),
"device-memory": SignalDef(label: "Device Memory", risk: .medium, category: .hardware),
"platform": SignalDef(label: "Platform", risk: .medium, category: .browser),
"languages": SignalDef(label: "Languages", risk: .medium, category: .browser),
"touch": SignalDef(label: "Touch support", risk: .medium, category: .input),
"connection": SignalDef(label: "Connection type", risk: .medium, category: .network),
// Low risk
"user-agent": SignalDef(label: "User agent", risk: .low, category: .browser),
"do-not-track": SignalDef(label: "Do Not Track", risk: .low, category: .browser),
"cookies-enabled": SignalDef(label: "Cookies enabled", risk: .low, category: .browser),
"online": SignalDef(label: "Online status", risk: .low, category: .network),
"pdf-viewer": SignalDef(label: "PDF viewer", risk: .low, category: .browser),
]
/// Unknown signal id — a typo'd id fails loudly instead of silently
/// rendering an unclassified row.
enum FingerprintError: Error {
case unknownSignal(String)
}
/// Risk level for a known signal id; nil for unknown ids.
func classifySignalRisk(_ id: String) -> RiskLevel? {
signalDefinitions[id]?.risk
}
/// Build a signal from its id and collected value.
func buildSignal(_ id: String, _ value: String) throws -> FingerprintSignal {
guard let def = signalDefinitions[id] else {
throw FingerprintError.unknownSignal(id)
}
return FingerprintSignal(id: id, label: def.label, value: value, risk: def.risk, category: def.category)
}
/// One category and its signals.
struct CategoryGroup {
let category: FingerprintCategory
let signals: [FingerprintSignal]
}
/// Group signals by category in canonical order, omitting empty categories.
func groupByCategory(_ signals: [FingerprintSignal]) -> [CategoryGroup] {
fingerprintCategories.compactMap { category in
let members = signals.filter { $0.category == category }
return members.isEmpty ? nil : CategoryGroup(category: category, signals: members)
}
}
/// Signals per risk level (drives the summary line).
struct RiskCounts {
var low = 0
var medium = 0
var high = 0
}
/// Count signals per risk level.
func countByRisk(_ signals: [FingerprintSignal]) -> RiskCounts {
var counts = RiskCounts()
for signal in signals {
switch signal.risk {
case .low: counts.low += 1
case .medium: counts.medium += 1
case .high: counts.high += 1
}
}
return counts
}
/// The exact byte string the fingerprint hash is computed over: every value
/// joined with "|" in signal order.
func concatSignalValues(_ signals: [FingerprintSignal]) -> String {
signals.map(\.value).joined(separator: "|")
}
// MARK: SHA-256 (FIPS 180-4) — dependency-free, pinned by the "abc" vector
/// Round constants: first 32 bits of the fractional parts of the cube roots
/// of the first 64 primes.
private let sha256K: [UInt32] = [
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
]
/// Initial hash words: first 32 bits of the fractional parts of the square
/// roots of the first 8 primes.
private let sha256H0: [UInt32] = [
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
]
private func rotr(_ x: UInt32, _ n: UInt32) -> UInt32 {
(x >> n) | (x << (32 - n))
}
/// Minimal SHA-256 state machine (single-shot digest over a full message).
private struct Sha256 {
var state: [UInt32] = sha256H0
/// Process one 64-byte block through the 64-round compression function.
private mutating func compress(_ block: ArraySlice<UInt8>) {
var w = [UInt32](repeating: 0, count: 64)
for t in 0..<16 {
let base = block.startIndex + 4 * t
w[t] = (UInt32(block[base]) << 24) | (UInt32(block[base + 1]) << 16)
| (UInt32(block[base + 2]) << 8) | UInt32(block[base + 3])
}
for t in 16..<64 {
let s0 = rotr(w[t - 15], 7) ^ rotr(w[t - 15], 18) ^ (w[t - 15] >> 3)
let s1 = rotr(w[t - 2], 17) ^ rotr(w[t - 2], 19) ^ (w[t - 2] >> 10)
w[t] = w[t - 16] &+ s0 &+ w[t - 7] &+ s1
}
var a = state[0], b = state[1], c = state[2], d = state[3]
var e = state[4], f = state[5], g = state[6], h = state[7]
for t in 0..<64 {
let bigS1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)
let ch = (e & f) ^ (~e & g)
let temp1 = h &+ bigS1 &+ ch &+ sha256K[t] &+ w[t]
let bigS0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)
let maj = (a & b) ^ (a & c) ^ (b & c)
let temp2 = bigS0 &+ maj
h = g; g = f; f = e; e = d &+ temp1
d = c; c = b; b = a; a = temp1 &+ temp2
}
state[0] = state[0] &+ a
state[1] = state[1] &+ b
state[2] = state[2] &+ c
state[3] = state[3] &+ d
state[4] = state[4] &+ e
state[5] = state[5] &+ f
state[6] = state[6] &+ g
state[7] = state[7] &+ h
}
/// Pad (0x80, zeros, 64-bit big-endian bit length) and digest the whole
/// message, returning the 32-byte hash.
mutating func digest(_ message: [UInt8]) -> [UInt8] {
let bitLength = UInt64(message.count) &* 8
var padded = message
padded.append(0x80)
while padded.count % 64 != 56 {
padded.append(0)
}
for shift in stride(from: 56, through: 0, by: -8) {
padded.append(UInt8(truncatingIfNeeded: bitLength >> UInt64(shift)))
}
for start in stride(from: 0, to: padded.count, by: 64) {
compress(padded[start..<(start + 64)])
}
var out = [UInt8]()
out.reserveCapacity(32)
for word in state {
out.append(UInt8(truncatingIfNeeded: word >> 24))
out.append(UInt8(truncatingIfNeeded: word >> 16))
out.append(UInt8(truncatingIfNeeded: word >> 8))
out.append(UInt8(truncatingIfNeeded: word))
}
return out
}
}
/// SHA-256 hex digest of the UTF-8 bytes of input.
func sha256Hex(_ input: String) -> String {
var sha = Sha256()
return sha.digest(Array(input.utf8))
.map { String(format: "%02x", $0) }
.joined()
}
/// Stable fingerprint ID: SHA-256 over every signal value, joined in signal
/// order. Same browser state -> same hash; any single changed value -> new
/// hash.
func hashFingerprint(_ signals: [FingerprintSignal]) -> String {
sha256Hex(concatSignalValues(signals))
}
// Demo (the file is the main file when compiled standalone with
// `swiftc swift.swift`).
do {
// FIPS 180-4 vector pins the hand-rolled implementation to the spec.
if sha256Hex("abc") != "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad" {
FileHandle.standardError.write("SHA-256 self-check failed\n".data(using: .utf8)!)
exit(1)
}
let signals = [
try buildSignal("timezone", "Europe/Paris"),
try buildSignal("screen", "2560×1440 @ 24-bit"),
try buildSignal("user-agent", "Mozilla/5.0 (Macintosh)"),
]
for group in groupByCategory(signals) {
print("[\(group.category.rawValue)]")
for signal in group.signals {
print(" \(signal.label): \(signal.value) (\(signal.risk.rawValue) risk)")
}
}
let counts = countByRisk(signals)
print("risk mix: \(counts.high) high / \(counts.medium) medium / \(counts.low) low")
print("fingerprint: \(hashFingerprint(signals))")
} catch FingerprintError.unknownSignal(let id) {
print("Unknown fingerprint signal id: \(id)")
exit(1)
}
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →