# SPDX-License-Identifier: AGPL-3.0-or-later """HTML fingerprint canary — plausible-looking page with an obfuscated browser-fingerprinting payload inlined at the bottom of ````. The visible content is a deliberately mundane "internal directory" table — the kind of file a curious attacker pulls off a decky's filesystem and opens locally to triage. When the file is opened in *any* network-connected browser the obfuscated payload runs and beacons to ``/c/``: first a bare-open pixel, then a chunked fingerprint dump (canvas, audio, fonts, WebGL, WebRTC local IPs, timing jitter, permissions, composite identity hash). Determinism: the mint UUID is derived from the callback token via :func:`uuid.uuid5` so the same ``ctx`` always produces byte-identical output, satisfying the generator contract in :mod:`decnet.canary.base`. The obfuscator's seed and polymorphic config bits are likewise callback-token-derived (see :mod:`decnet.canary.obfuscator`). """ from __future__ import annotations import hashlib import uuid from decnet.canary.base import CanaryArtifact, CanaryContext, CanaryGenerator from decnet.canary.obfuscator import render_fingerprint_js, nonce_for _MINT_NAMESPACE = uuid.UUID("a3f7c821-9d1e-4b6a-8c2d-1e4f9a7b3c5d") def _mint_uuid_for(callback_token: str) -> str: return str(uuid.uuid5(_MINT_NAMESPACE, callback_token)) def _stable_int(callback_token: str, salt: str = "") -> int: """Deterministic non-negative int derived from the callback token. ``builtins.hash`` is salted per-process — useless for a generator that must be byte-identical across runs. SHA-256 prefix is overkill but free. """ h = hashlib.sha256((callback_token + "|" + salt).encode("utf-8")).digest() return int.from_bytes(h[:4], "big") _PAGE_TEMPLATE = """ Internal Asset Directory

Internal Asset Directory

last sync: {sync_label} · {row_count} entries · CONFIDENTIAL
{rows}
HostnameOwnerRoleVLANNotes
page generated by directory-sync v2.4.1 — do not redistribute
""" _ROW_POOL = ( ("ny-app-01.corp.local", "k.tanaka", "app server", "vlan20", "primary"), ("ny-db-01.corp.local", "ops", "postgres primary", "vlan30", "backup nightly"), ("ny-build-02.corp.local", "ci-bot", "jenkins agent", "vlan40", ""), ("sf-vpn-01.corp.local", "netsec", "wireguard endpoint", "vlan10", "external"), ("ldn-mail-03.corp.local", "j.weber", "exchange edge", "vlan50", ""), ("hk-cache-01.corp.local", "ops", "redis replica", "vlan30", "lag <1s"), ("br-dev-04.corp.local", "m.silva", "dev sandbox", "vlan60", "ephemeral"), ("eu-bastion-02.corp.local", "secops", "ssh jump host", "vlan10", "mfa required"), ("us-archive-01.corp.local", "compliance", "log archive", "vlan70", "retain 7y"), ) def _build_rows(callback_token: str) -> tuple[str, int]: pick = _stable_int(callback_token, "pick") % len(_ROW_POOL) take = 5 + (_stable_int(callback_token, "take") % 4) selected = [_ROW_POOL[(pick + i) % len(_ROW_POOL)] for i in range(take)] cells = "\n".join( "" + "".join(f"{c}" for c in row) + "" for row in selected ) return cells, len(selected) def _sync_label(callback_token: str) -> str: day = _stable_int(callback_token, "day") % 28 + 1 hour = _stable_int(callback_token, "hour") % 24 return f"2026-04-{day:02d} {hour:02d}:14 UTC" class FingerprintHtmlGenerator(CanaryGenerator): """Synthesise an HTML page that fingerprints the browser opening it.""" name = "fingerprint_html" def generate(self, ctx: CanaryContext) -> CanaryArtifact: mint_uuid = _mint_uuid_for(ctx.callback_token) nonce = nonce_for(ctx.callback_token, mint_uuid) payload = render_fingerprint_js( callback_token=ctx.callback_token, http_base=ctx.http_base, mint_uuid=mint_uuid, nonce=nonce, ) rows, row_count = _build_rows(ctx.callback_token) body = _PAGE_TEMPLATE.format( sync_label=_sync_label(ctx.callback_token), row_count=row_count, rows=rows, payload=payload, ) beacon = f"{ctx.http_base.rstrip('/')}/c/{ctx.callback_token}" return CanaryArtifact( path="", content=body.encode("utf-8"), mode=0o644, mtime_offset=-86400 * 14, generator=self.name, fingerprint_nonce=nonce, notes=[ f"obfuscated fingerprinter beacons={beacon}", f"mint_uuid={mint_uuid}", ], )