Security

Verifiable, not just stated.

This page documents what protects your work on Shotdrive — the cryptographic primitives, the threat model, the sub-processors, and how to report a vulnerability. Every claim has a source in the public repo.

Cryptography

What's encrypted, and how.

AES-256-GCM, browser-side

File content is encrypted with AES-256-GCM in 50 MB chunks inside your browser before any byte reaches our servers. The key is generated client-side and never transmitted.

apps/shotdrive/src/lib/crypto/aes-gcm.ts

Key lives in URL fragment

The decryption key is appended to the share URL after #key=. Browsers never send fragments to servers — we can't see it, can't log it, can't recover it for you.

apps/shotdrive/src/lib/crypto/key-encoding.ts

HKDF-SHA-256 IV derivation

Each chunk gets a unique nonce derived from HKDF-SHA-256 of (key, chunk_index). The Worker never sees a reused IV; the client doesn't need to store one.

apps/shotdrive/src/lib/crypto/aes-gcm.ts

Encrypted file metadata

File names, gallery titles, descriptions, comment text, captions, branding watermark configs — all encrypted with a per-bundle key (K_bundle) using AES-GCM with subkey derivation for per-field domain separation.

apps/shotdrive/src/lib/crypto/metadata.ts

Password-protected links

Password derivation uses PBKDF2-SHA-256 at 310,000 iterations (current OWASP recommendation). The server stores a verifier; the password itself is never sent.

apps/shotdrive/src/lib/crypto/tokens.ts

Cross-device Vault

Owner-side vault wraps your gallery keys with a per-device passkey (Face ID / Touch ID / Windows Hello) plus a recovery phrase. Add a device, the wrapped key follows; remove a device, the wrap is revoked. We see only the wrapped blobs.

apps/shotdrive/src/lib/crypto/vault.ts

Threat model

What we protect against.

A database breach

D1 contains only ciphertext for file content and content-bearing metadata. An attacker with full D1 read access cannot recover file bodies, file names, gallery titles, comment text, or captions.

A staff member with curiosity

No one at Shotdrive can read your files. We don't have plaintext on the server — the key is in the URL fragment, never sent. Support can see workflow metadata (bundle exists, expiry, owner) but not content.

A subpoena for content

We can hand over ciphertext, account email, plan, bundle shape metadata, and download logs — those exist. We cannot hand over the content of any file or the decryption key. We never had them.

A malicious sub-processor

Cloudflare R2 stores file ciphertext but not the key. Supabase stores user records but not gallery keys. Brevo sees email addresses and the share link URL (we made the choice to share the URL, including #key=, with Brevo — see /faq for the trade-off).

Third-party scripts on /g, /d, /b

Strict Content-Security-Policy with `script-src 'self' 'unsafe-inline'` and no third-party origins. We do not load analytics, fonts, or any external resource on delivery surfaces.

A compromised recipient device

If a recipient's device is compromised after they've decrypted, we can't help — the content lived there in plaintext after decrypt. We mitigate by recommending password-protected links + custom expiry for sensitive deliveries.

Transparency

What we see and what we don't.

We follow Pragmatic E2EE — content is end-to-end encrypted; workflow metadata is server-readable under three named carve-outs. The full doc is in our privacy page and the design Principles in the repo.

We never see

  • File content (any byte of any uploaded file)
  • File display names
  • Gallery titles & descriptions
  • Comment text & captions
  • Track titles, version labels, album metadata
  • Section text bodies & spec field values
  • EXIF metadata (stripped client-side before thumbnail gen)
  • Vault recovery phrase entropy

We see

  • Bundle existence (public token)
  • Bundle kind (delivery / press / music / stills / visual)
  • File count + total ciphertext size
  • Created-at + expires-at + last-accessed timestamps
  • Recipient list (for inbox + per-recipient key wrapping)
  • Workspace branding (logo, name, colors — Carve-out 1)
  • Aggregate download counts (for analytics)
  • Owner plan tier + Polar customer ID (for billing)

Sub-processors

Six EU vendors, six narrow responsibilities.

Cloudflare

Workers (compute), R2 (object storage, EU jurisdiction), D1 (database), KV (rate limits), Pages (frontend), Analytics Engine

Sees: Encrypted file ciphertext, request metadata (route, status, latency, country)

Does not see: Decryption keys, plaintext file content

Supabase

Authentication, account database (Frankfurt)

Sees: User email, opaque user ID, session tokens

Does not see: Gallery contents, gallery keys

Polar

Billing (EU Merchant of Record)

Sees: Customer email, plan tier, payment instrument (via downstream PSP)

Does not see: Gallery contents, gallery keys, file metadata

Brevo

Transactional email (France)

Sees: Recipient email, share-link URL including #key= fragment

Does not see: File content; emails are content-free triggers per our email policy

Sentry

Error monitoring (EU region)

Sees: Stack traces, browser version, error context (PII-scrubbed before send)

Does not see: File content, gallery keys

Betterstack

Uptime monitoring

Sees: HTTP response codes from public endpoints

Does not see: Any user data

Formal DPA at /legal/dpa. Privacy notice at /legal/privacy.

Delivery surface

The recipient-facing pages are locked down.

Strict CSP — no third-party scripts

`/d/*`, `/g/*`, `/b/*` ship with `script-src 'self' 'unsafe-inline'`. No analytics SDK, no font CDN, no embed scripts. Browser-extension injections fire a console warning; the page still works.

No referrer leakage

`Referrer-Policy: no-referrer` on all delivery routes. Clicking an external link from a gallery does not leak the source URL or its `#key=` fragment.

X-Frame-Options: DENY

Delivery pages cannot be framed by any third-party site. Click-jacking via iframe is not possible.

No PII in Worker logs

Worker request logs contain route, status, latency, country, and structured request IDs only. No emails, no filenames, no IPs (for authenticated uploads — anonymous uploads store IP for 30 days for abuse enforcement, then purge).

Coordinated disclosure

Found something? Tell us first.

Contact

security@shotdrive.io

PGP key on request.

Response SLA

  • Acknowledgement within 48 hours.
  • Triage and remediation plan within 7 days.
  • Coordinated disclosure window: 90 days.

Safe harbour

We will not pursue legal action against researchers who:

  • Disclose privately and follow this policy
  • Don't access or modify other users' data
  • Don't disrupt the service for other users
  • Don't publicly disclose before the 90-day window closes

Canonical text at /.well-known/security.txt.

Verifying these claims

Trust us is not a security argument.

The cryptographic implementation lives in the public source tree under apps/shotdrive/src/lib/crypto/. If you find a way to read content we say is encrypted, or a metadata field we list as ciphertext leaking in plaintext, write to security@shotdrive.io.