RSSAmplifier

Sentinel Den · Engineering blog · Mar 30, 2026

Forensic HMAC watermarks on iOS: attributing a leaked screenshot

0
Sign in to vote or save

Muhammad Khan · Sentinel Den

The screen-capture defenses an iOS app can ship are layered: hardware secure canvas (UITextField-backed rendering surface), reactive overlay (blackout on capture-state change), pasteboard guard, share-sheet guard, idle timeout. Each defense reduces the surface area for a leak. None of them eliminate it. The screen will be photographed by a second device at some non-zero rate, and the leak will surface on social media at some non-zero rate, and your incident-response team will be asked the same question: who?

Visible watermarks, “CONFIDENTIAL, User 12345, 2026-03-15” stamped across the screen, are the obvious answer. They are also wrong, for three reasons:

  1. A leaker who notices the watermark crops the image before posting it.
  2. A visible stamp is ugly enough that product teams remove it.
  3. A user who sees the watermark while doing legitimate work is reminded that they are being monitored, which is the wrong UX signal for a healthcare or banking app.

The correct answer is invisible forensic watermarks: text rendered at low opacity, tiled across the protected view, cryptographically signed with a per-user identity. Invisible to the user during normal use. Recoverable from a high-resolution photograph via OCR. Verifiable by a constant-time HMAC compare against a candidate identity list.

This post is the threat model, the rendering primitive, the verifier, and the limits.

The threat model

The watermark answers one specific question: if a leaked photo of this screen shows up later, which user-session was on the screen when the photo was taken? It does not prevent the leak. It does not detect the leak in real time. It enables post-leak attribution, which has two downstream effects:

  • Deterrence. A leaker who knows they will be identified by the leak does the math: probability-of-discovery × consequence-of-identification > value-of-leaking. The math changes the rate of leaking. The deterrent value is the entire point.
  • Investigation. When a leak does happen, the incident-response team has a starting point that is not “interview every user who had access”. Verifier output points at a specific session; the audit log shows that session’s activity; the investigation has signal.

The threat model does not include casual screenshots that never leave the device, programmatic screen capture (covered separately by the hardware-shield path), or insider attackers who exfiltrate the underlying data via a non-screen channel (database dump, network capture). Watermarks address the photograph-the-screen vector and only that vector.

The rendering primitive

The watermark is rendered as a CATiledLayer overlay on top of the protected view. Each tile contains:

  • A short base64url-encoded HMAC fingerprint (24–32 characters).
  • Rendered in mono font at style.alpha (0.08 by default).
  • Rotated -30 degrees, the legacy display label only. The forensic tile that replaces it on a licensed build is not rotated: it has to stay readable from a photo of a screen.
  • Repeated across the view at configurable density (sparse / normal / dense).

The fingerprint is computed at render time from a structured identity:

identity = {
    userIdentifier:    "u_4f9a8b1c",
    sessionIdentifier: "s_2026-03-30T14:22:18Z_8e3f9",
    deviceIdentifier:  "d_4F8A-1C92-...",
    ipAddress:         "203.0.113.45",
    tenantIdentifier:  "t_acme_corp",
    issuedAt:          2026-03-30T14:22:18Z
}
fingerprint = base64url(HMAC-SHA256(serverKey, canonicalEncode(identity)))[:24]

The HMAC key is server-held, never on the device. The watermark on screen is the fingerprint, a value that proves knowledge of the identity without revealing it. The full identity is reconstructed at verification time by the back-office.

Why HMAC and not a signature? HMAC is fast enough to render in real-time on every protected frame without battery cost; signature verification requires asymmetric crypto that’s heavier. HMAC also requires the server to verify, which is the right trust boundary for forensic attribution, the device cannot self-attribute.

What invisible means in practice

At 8% opacity in a mono font, the watermark is designed to sit below the threshold of conscious perception for a user looking at the screen at normal viewing distance. The eye reads it as faint texture; the brain edits it out. The design intent is that a user doing legitimate work never notices it.

But a camera at high resolution captures the watermark with high fidelity. A phone camera photographing a screen at typical distance produces images where the watermark text is OCR-recoverable using Vision framework’s VNRecognizeTextRequest. Even at lower resolutions (older phone cameras, printed-and-rephotographed images), the tiled redundancy means at least one complete watermark survives, and the verifier only needs one intact fingerprint to attribute.

The tile density is the trade-off knob: dense tiling means more opportunities to recover a fingerprint from a partial capture, at the cost of more visible noise. Sparse tiling looks cleaner to the user but offers less recovery surface. ScreenGuard’s three presets (.subtle, .standard, .aggressive) correspond to GDPR / HIPAA / PCI-DSS comfort levels respectively.

The verifier

When a leak surfaces, a screenshot posted to Twitter, an image attached to a support ticket, a printed page on someone’s desk, the incident-response flow is:

import ScreenGuardSDK

let screenGuard = try await ScreenGuard.start(apiKey: licenseKey, environment: .production)
guard let verifier = screenGuard.verifier else { return }  // forensic verify is Professional-tier and up

let report = try await verifier.scan(
    image: leakedScreenshot,
    candidates: activeSessionIdentities  // typically a few hundred entries
)

switch report.outcome {
case .verified:
    let match = report.evaluatedIdentities.first!
    investigation.log(
        leak: leakedScreenshot,
        attributedTo: match.userIdentifier,
        session: match.sessionIdentifier,
        timestamp: match.issuedAt,
        confidence: report.confidence
    )
case .mismatch:
    // OCR recovered fingerprints, but none matched the candidate list.
    // Could be: stale session not in active list, forged watermark,
    // or the image is of a non-ScreenGuard-protected app.
    investigation.logUnattributed(report.extractedFingerprints)
case .signatureNotFound:
    // OCR did not find any watermark-shaped text. Either the image was
    // cropped to exclude all tiles, the image quality is too low, or
    // the source view was not actually protected.
case .invalidIdentity:
    break  // malformed candidate; programming error
}

The verifier runs entirely on the device or your back-office server (no cloud OCR), uses Vision framework’s native text recognition, and runs the HMAC compare in constant time to avoid timing-attack leakage if the verifier is ever exposed via API.

The candidate list is the back-office’s responsibility: it should include every session that was active at or near the timestamp the leak surfaced, typically pulled from the audit log. For a high-DAU app where a leak surfaces hours after the screenshot was taken, the candidate list might be a few thousand entries; an HMAC compare against a few thousand candidates is a single hash per entry, fast enough to run interactively in the incident-response tool, no network call in the loop.

What the watermark does not do

Three limitations to be clear about:

  • Cropped tile-aligned to exclude all watermarks. A sophisticated leaker who knows about the watermark and crops the image to a small area that happens to fall between tiles will defeat attribution for that specific image. Dense tile density makes this very hard but not impossible.
  • Photographed at extreme distance. A photo of the screen taken from across a room at low resolution will not have OCR-recoverable text. The watermark needs at least ~150 DPI capture quality to be readable, which is the standard for any deliberate leak (smartphone cameras at typical distance).
  • Image transformed beyond OCR recovery. Heavy compression artifacts, screenshots of screenshots, low-quality OCRing on the leaker’s side before re-rendering, each transformation degrades the fingerprint. The verifier has degraded modes (.lenient confidence) that tolerate some loss, but at some loss level the signal is gone.

The mitigation for all three is to combine the watermark with the audit log: even if the verifier returns .signatureNotFound, the audit log tells you who had access to that screen during the relevant time window, narrowing the investigation. Watermarks are a strong attribution primitive, not a perfect one.

What HIPAA, PCI, and FINRA actually want from this

The compliance frameworks treat forensic watermarks as a technical safeguard for unauthorized disclosure. They do not specify the watermark technology; they specify that there must be a mechanism to identify the source of a disclosed record. HMAC-signed invisible watermarks meet that bar. The audit log meets the who-had-access bar separately. The combination is what an auditor wants to see.

ScreenGuard’s SGWatermarkGenerator is the primitive, SGCallbackIdentityProvider is the right identity provider for multi-tenant apps where the session ID changes mid-app-lifetime, and SGWatermarkVerifier.scan(image:) is the call the incident-response tool wraps in a button labeled “attribute this leak”.

See /sdk/screenguard for the marketing summary, /docs/screenguard for integration, the full ScreenGuard threat model for the attacker capabilities each defense assumes, and the companion post on HIPAA Technical Safeguards for the mapping from § 164.312 to ScreenGuard’s preset.

Read the original on sentinelden.com

Comments

Nothing yet. Say the first thing.

    Sign in to join the conversation.