Ownly.

Security proof.

We do not prevent screenshots — that claim is not verifiable. What follows is what we do instead, proved by regenerable artifacts from automated tests. Glossary.

16

Attestations

13

Passing

1

In progress

2

Under investigation

commit c8c5e14refreshed 2026-04-17

A per-Recipient invisible payload is embedded at serve time. Any leaked copy maps back to the specific Recipient whose Share produced it — without requiring cooperation from the platform it leaked on.

Forensic Watermarking

upload→share→serve→trace returns matched:true with recipient_label matching the share-time value AND trace receipt verifies VALID

Pass
watermarked.jpgdownload
Forensic Watermarking — watermarked.jpg
trace-response.json
{
  "matched": true,
  "hash": "1445fce",
  "recipient_label": "Recipient A"
}
receipt.json
{
  "receipt": {
    "version": 1,
    "kid": "v1",
    "signed_at": "2026-04-17",
    "server_identity": "ownly-test",
    "signature": "8b382d643e785faeeb6a773087d71a069ae1623375b397cc5df734a40cdbfcde"
  },
  "payload": {
    "hash": "1445fce",
    "recipient_label": "Recipient A"
  }
}

Trace Receipt

exit 0 AND stdout contains Verdict VALID

Pass
Test outputs ↓
{
  "exit_code": 0,
  "stdout_first_500_chars": "Verdict: VALID\nReason:  none\n\nContent ID:    20260415-95ca9058\nRecipient:     prod-receipt-fixture\nHash:          634df1b\nGrant ID:      670fefdf-e39e-43d0-b3a0-b33e6821a177\nShared at:     2026-04-15T19:21:18.235618+00:00\nSigned at:     2026-04-15T20:01:08.984Z\nSigned by:     ownly-test (kid=v1)\nSignature:     4efb0edec9707f84…\n",
  "stderr_first_500_chars": ""
}

Verify offline

node scripts/verify-receipt.mjs web/public/proof/snapshots/trace-receipt/

Honeypot Variations

Different grant_ids produce different variation_seeds AND different served bytes (SHA-256 differs per-Recipient); re-query returns same seed (determinism).

Investigation pending

Investigation open — /trace · contact us for status.

Test outputs ↓
{
  "hash_alice_prefix": null,
  "hash_bob_prefix": null,
  "bytes_differ": false,
  "seeds_differ": true,
  "determinism_holds": false
}

Canvas Fingerprint

Posting a different fingerprint hash to /api/grant/fingerprint sets fingerprint_mismatch=true AND does NOT overwrite device_fingerprint (first-seen preserved, route.ts:125-130); POST /api/grant/fingerprint payload matches view_grants.device_fingerprint (64-char SHA-256 hex); repeat-view mismatch detection documented

Pass
Test outputs ↓
{
  "mismatch_flag_set": true,
  "device_fingerprint_preserved": true,
  "mismatch_flag": false,
  "round_trip_match": true
}

Client-side mechanisms shipped as JavaScript that every Recipient already receives. Attribution Overlays, Hold-to-View gating, Temporal Noise, and Content-Aware Noise raise the effort required to produce a clean, redistributable screenshot.

Hold-to-View

initial state shows blurred placeholder; mousedown reveals content; mouseup blacks out within 50ms

Pass
initial.pngdownload
Hold-to-View — initial.png
holding.pngdownload
Hold-to-View — holding.png
released.pngdownload
Hold-to-View — released.png

Attribution Overlay

DOM timer-bar contains recipient_label AND canvas screenshot captured

Pass
canvas.pngdownload
Attribution Overlay — canvas.png

Temporal Noise

≥30% of pixels differ by ≥5 between consecutive noise frames when hold_to_view_enabled=false (noise loop starts automatically after auto-render); ≥30% of pixels differ by ≥5 between consecutive noise frames (noise present); 2-frame average has lower or equal variance than single frame (noise cancels)

Pass
frame1.pngdownload
Temporal Noise — frame1.png
frame2.pngdownload
Temporal Noise — frame2.png
frame3.pngdownload
Temporal Noise — frame3.png

Content-Aware Noise

mean noise magnitude in top-30% saliency bin ≥ 1.2× mean noise magnitude in bottom-30% saliency bin (saliency-modulated amplitude confirmed active)

Pass
frame1.pngdownload
Content-Aware Noise — frame1.png
frame2.pngdownload
Content-Aware Noise — frame2.png
saliency-map.pngdownload
Content-Aware Noise — saliency-map.png
diff-heatmap.pngdownload
Content-Aware Noise — diff-heatmap.png

View Treatment

Share with custom view_treatment (opacity=0.05, amplitude=5, hold=true) succeeds (200) and viewer holds-to-view correctly; view_treatment wiring from /api/share to Secure Viewer confirmed; POST /api/share with attribution_overlay_opacity=0.5 (above [0, 0.3] range) returns 400 with error array

Pass
viewer-canvas.pngdownload
View Treatment — viewer-canvas.png

Tooling that activates after a Leak is identified. Both mechanisms are in progress — their current status is surfaced honestly rather than hidden.

Capture Response

canvas pixel sample changes on synthetic visibilitychange event

Investigation pending

Investigation open — see spec · /trace · contact us for status.

DMCA Notice

Given a successful Leak Trace (with Trace Receipt) and a Share Capability Receipt, /api/trace/notice generates a §512(c)(3)-compliant DMCA Notice with all required fields populated and both cryptographic signatures embedded; the HTML output contains the Recipient label, watermark hash, and both receipt signatures.

In progress

Not yet implemented. See spec.

Test outputs ↓
{
  "skip_reason": "trace returned matched:false — watermark service cold or sample image has no embedded watermark. Use a watermarked copy from the share URL."
}

Metadata scrubbing, access quotas, authentication gates, signed Receipts, and Protection Profiles. Less visible than the layers above — load-bearing for every claim on this page.

Metadata Scrub

no tag from the denylist present in exiftool -j output on the stored Blob

Pass

Access Control

first view status==200 AND second view status==403 with body 'View limit reached'

Pass

Trace Bearer Auth

status==401 AND bearer token is not leaked in response body

Pass

Capability Receipt

Share creation emits a Capability Receipt with a valid HMAC-SHA-256 signature over the Protection Profile payload; the verify endpoint confirms valid: true; a corrupted signature is rejected with valid: false.

Pass

Protection Profile

Share creation for an image/jpeg Content resulted in protection_profile_id='standard-image' on the view_grants row.

Pass