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:
- A leaker who notices the watermark crops the image before posting it.
- A visible stamp is ugly enough that product teams remove it.
- 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 (
.lenientconfidence) 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.
Comments
Nothing yet. Say the first thing.
Sign in to join the conversation.