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 Security
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
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 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 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 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 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 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
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.
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.
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.
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).
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.
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
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
We see
Sub-processors
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
Authentication, account database (Frankfurt)
Sees: User email, opaque user ID, session tokens
Does not see: Gallery contents, gallery keys
Billing (EU Merchant of Record)
Sees: Customer email, plan tier, payment instrument (via downstream PSP)
Does not see: Gallery contents, gallery keys, file metadata
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
Error monitoring (EU region)
Sees: Stack traces, browser version, error context (PII-scrubbed before send)
Does not see: File content, gallery keys
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
`/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.
`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.
Delivery pages cannot be framed by any third-party site. Click-jacking via iframe is not possible.
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
Contact
PGP key on request.
Response SLA
Safe harbour
We will not pursue legal action against researchers who:
Canonical text at /.well-known/security.txt.
Verifying these claims
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.