Overview
Vault attachments let you store encrypted files alongside your credentials and secure notes. Documents, recovery codes, license keys, and identity scans can be attached to any vault item. All files are encrypted client-side before upload — the server only ever stores ciphertext. The attachment system uses the same envelope encryption model as the rest of the vault: each file gets its own random AES-256 key, which is wrapped by your vault’sencKey. The server cannot decrypt attachments under any circumstances.
Encryption model
Each attachment encrypts four pieces of data independently, each with its own random 96-bit IV:| Component | Encryption | Purpose |
|---|---|---|
| File content | AES-256-GCM (per-file key) | The actual file bytes |
| Filename | AES-256-GCM (per-file key) | Original filename (e.g. recovery-codes.pdf) |
| MIME type | AES-256-GCM (per-file key) | Content type (e.g. application/pdf) |
| File key | AES-256-GCM (vault encKey) | The per-file key itself, wrapped for storage |
The filename and MIME type are encrypted because they can reveal sensitive information about the attachment’s purpose. The server stores only opaque ciphertext for all metadata fields.
Encrypted content format
The encrypted file content is stored as a single base64-encoded blob with the IV prepended:Wrapped file key format
The per-file key is wrapped using AES-256-GCM with the vault’sencKey and stored as a JSON object:
Upload flow
Select file
The user selects a file from their device to attach to a vault item (password, note, or generic item).
Client-side encryption
The browser calls
encryptAttachment(file, encKey), which generates a per-file AES-256 key using crypto.subtle.generateKey(), encrypts the content, filename, and MIME type, then wraps the file key with the vault’s encKey.Upload encrypted payload
The client sends a
POST /api/vault/attachments request containing the base64-encoded encrypted content, wrapped file key, encrypted metadata, and the parent item reference.Server-side storage
The server validates the request, checks storage quotas, verifies parent item ownership, uploads the encrypted blob to Supabase Storage, and saves metadata to the
vault_attachments table.If the database insert fails after the file has been uploaded to storage, the server automatically cleans up the orphaned storage object to prevent storage leaks.
Download flow
Request attachment
The client sends
GET /api/vault/attachments/{id}. The server verifies ownership and returns the encrypted content, wrapped file key, and encrypted metadata.Unwrap file key
The client decrypts the wrapped file key using the vault’s
encKey via crypto.subtle.decrypt(), then imports the raw key material as an AES-256-GCM CryptoKey.Decrypt content
The IV is extracted from the first 12 bytes of the encrypted content. The remaining bytes are decrypted with AES-256-GCM using the unwrapped file key.
Decrypt metadata
The filename and MIME type are decrypted separately using the same file key, each with their own stored IV.
Size limits and quotas
| Limit | Value |
|---|---|
| Max file size | 25 MB per file |
| Max base64 payload | 35,000,000 characters (~25 MB encoded) |
| Total storage (free tier) | 100 MB per user |
| Upload rate limit | 10 uploads per hour |
| Download rate limit | 30 downloads per minute |
| Read (list) rate limit | 30 requests per minute |
| Delete rate limit | 30 deletes per minute |
Parent item types
Attachments are linked to a parent vault item. Theparent_type field determines which table is queried for ownership verification:
| Parent type | Database table | Example use case |
|---|---|---|
password | passwords | Recovery codes, security key backups |
note | notes | Identity documents, license scans |
item | vault_items | Generic file storage |
API endpoints
| Method | Endpoint | Description |
|---|---|---|
GET | /api/vault/attachments?parent_id=X&parent_type=Y | List attachments for a vault item |
POST | /api/vault/attachments | Upload an encrypted attachment |
GET | /api/vault/attachments/{id} | Download an encrypted attachment |
DELETE | /api/vault/attachments/{id} | Delete an attachment |
Request schema (upload)
Response schema (download)
Client-side API
Thevault-attachment-crypto module exposes two functions for use in the browser:
- encryptAttachment
- decryptAttachment
Security properties
- Zero-knowledge storage: the server stores only ciphertext. File content, filenames, and MIME types are never visible server-side.
- Per-file key isolation: compromising one file key does not expose other attachments. Each file has an independent random AES-256 key.
- Authenticated encryption: AES-256-GCM provides both confidentiality and integrity. Any tampering with the ciphertext or IV causes decryption to fail.
- Ownership enforcement: every API operation verifies that the authenticated user owns both the attachment and its parent item.
- Audit trail: uploads and deletes are recorded in the audit log with parent item context and file size.