Encryption
Enable optional end-to-end encryption for images requiring privacy beyond standard R2 storage security. Encrypted images are protected with AES-256-GCM and can only be decrypted via time-limited HMAC-signed tokens.
Why It's Available
Some users need images that are unreadable even if storage is compromised. Medical records, legal documents, private photos - anything that requires cryptographic protection beyond access control. Standard R2 storage is already secure, but encryption adds an extra layer where even Cloudflare can't decrypt the images without the user's keys.
Trade-offs:
- Adds decryption latency (50-200ms per request)
- No edge caching (each request decrypts fresh)
- No CDN transformations (variants can't be generated from encrypted originals)
- Tokens expire (images aren't directly linkable like public ones)
- More complex UX (users manage access tokens)
Use encryption sparingly. Most images should be public or private (access-controlled) but not encrypted.
Setup
The ENCRYPTION_ROOT_KEY is auto-generated by Terraform if not provided. This root key wraps per-user TMKs (Tenant Master Keys), which in turn wrap per-image CEKs (Content Encryption Keys).
No manual configuration needed. Encryption is enabled per-user via the API or frontend UI when they're ready to use it.
Key Hierarchy
graph TB
RootKey[ENCRYPTION_ROOT_KEY<br/>Environment Variable<br/>Auto-generated by Terraform]
TMK1[TMK v1<br/>User: alice<br/>Stored encrypted in KV]
TMK2[TMK v2<br/>User: alice<br/>After rotation]
TMK3[TMK v1<br/>User: bob<br/>Stored encrypted in KV]
CEK1[CEK - Image 1<br/>AES-256 key]
CEK2[CEK - Image 2<br/>AES-256 key]
CEK3[CEK - Image 3<br/>AES-256 key]
IMG1[Encrypted Image 1]
IMG2[Encrypted Image 2]
IMG3[Encrypted Image 3]
RootKey -->|Wraps| TMK1
RootKey -->|Wraps| TMK2
RootKey -->|Wraps| TMK3
TMK1 -->|Wraps| CEK1
TMK2 -->|Wraps| CEK2
TMK3 -->|Wraps| CEK3
CEK1 -->|Encrypts| IMG1
CEK2 -->|Encrypts| IMG2
CEK3 -->|Encrypts| IMG3- Root Key: Set once during deployment, wraps all TMKs
- TMK (Tenant Master Key): Per-user, versioned (supports rotation)
- CEK (Content Encryption Key): Per-image, unique 256-bit AES key
Token Signing
Access tokens use UPLOAD_TOKEN_SECRET for HMAC signing. This is also auto-generated by Terraform and prevents token forgery.
How it Works
Users opt in to encryption per-upload. The API generates a unique CEK, encrypts the image, wraps the CEK with the user's TMK, and stores everything in R2. Viewing requires generating a time-limited access token.
Upload flow:
- User enables encryption on upload
- API generates random 32-byte CEK
- API encrypts image with CEK (AES-256-GCM)
- API wraps CEK with user's TMK
- API stores encrypted image + wrapped CEK in R2
- Database records
is_encrypted=1
Access flow:
- User generates token (5 minutes to 90 days)
- Token includes HMAC signature to prevent tampering
- User requests image with token parameter
- API verifies HMAC and expiry
- API unwraps TMK → unwraps CEK → decrypts image
- API serves decrypted bytes
Auto-preview tokens (5 minutes) are generated automatically in the UI. Manual tokens can be created for sharing.
User Management
Users control encryption through API endpoints or the frontend settings page:
Enable encryption:
POST /v1/encryption/enableCreates TMK v1 for the user. Idempotent.
Rotate TMK:
POST /v1/encryption/rotateCreates a new TMK version. Old encrypted images remain accessible with their original TMK version. Rate limited to 5 rotations per hour.
Check status:
GET /v1/encryption/statusReturns enabled status, current TMK version, and count of encrypted images.
Generate access token:
POST /v1/images/{imageId}/token
{
"variant": "original",
"expires_in": 3600 // seconds, max 90 days
}Returns URL with embedded token for temporary access.
Performance Impact
Encrypted images bypass edge caching and transformations:
- No variants: Can't generate w512, thumb, etc. from encrypted originals
- No caching: Each request decrypts fresh (50-200ms latency added)
- No format negotiation: Served as uploaded (AVIF/WebP conversion unavailable)
For high-traffic images, encryption isn't suitable. It's designed for sensitive content accessed infrequently.
Security Considerations
Root key loss = unrecoverable: If ENCRYPTION_ROOT_KEY is lost, all encrypted images become permanently unreadable. Back up secrets securely.
TMK rotation: Rotating TMKs doesn't re-encrypt existing images. Old images use old TMK versions. Both versions remain accessible. To fully rotate all keys, you'd need to re-encrypt images (not currently automated).
Token security: Tokens are bearer tokens. Anyone with a valid token can access the image until expiry. Share tokens carefully. Tokens cannot be revoked early (no token blacklist).
Metadata: Image metadata (filename, alt text, tags, dimensions) is stored unencrypted in D1. Only the image bytes themselves are encrypted.
Disabling
Encryption cannot be disabled once enabled for a user. Users can delete encrypted images or stop uploading with encryption enabled, but existing encrypted images remain encrypted.
To prevent new encryptions instance-wide, you'd need to modify the API to reject encryption requests (no env var toggle exists currently).