This document describes how each security policy works end-to-end in Preview Vault, including key generation, storage locations, and retrieval logic. Zero-trust is the default posture: keys stay client-side unless the selected policy explicitly allows server-managed access.
Shared Concepts.
- Project passphrase: A user-provided (or generated) secret used to derive a symmetric key for AES-GCM encryption/decryption of files and the manifest.
- Salt: Random 16-byte salt generated at project creation; used with the passphrase to derive the symmetric key.
- Encrypted files: All uploaded files are encrypted client-side before upload; ciphertext is stored in S3-compatible storage.
- Manifest: A JSON index of file metadata; encrypted client-side and stored in S3-compatible storage.
- Viewer: The
/p/[projectId]page retrieves project metadata + manifest and decrypts locally.
System Diagram (All Policies)
┌───────────────┐
│ Browser/CLI │
│ (encrypts) │
└──────┬────────┘
│ ciphertext + encrypted manifest
▼
┌───────────────┐ ┌──────────────────┐
│ S3-Compatible │<--------│ App API (DB + │
│ Storage │ │ access control) │
└───────────────┘ └──────────────────┘
▲ ▲
│ decrypt in viewer │ policy-specific keys
└──────────────┬───────────┘
▼
┌──────────┐
│ Viewer │
└──────────┘Security Levels
ZERO_TRUST
Intent: No passphrase storage anywhere. The user must supply it every time.
Creation:
- User enters a passphrase in the client.
- Client derives a key from passphrase + salt and encrypts files/manifest.
- Passphrase is NOT saved to server or localStorage.
Storage:
- S3/MinIO: encrypted files + encrypted manifest.
- DB: project metadata only (no passphrase, no encrypted key).
Retrieval:
- Viewer prompts for passphrase.
- User types it; key is derived client-side to decrypt the manifest.
Risk profile:
- Highest confidentiality. If passphrase is lost, data is unrecoverable.
Diagram:
User -> passphrase -> derive key -> encrypt -> storage
User -> passphrase -> derive key -> decrypt -> viewLOCAL_DEVICE
Intent: Convenience for a single browser/device without sharing the passphrase.
Creation:
- Same as ZERO_TRUST, but the passphrase is saved in browser localStorage.
Storage:
- S3/MinIO: encrypted files + encrypted manifest.
- Browser localStorage:
previewvault:<projectId>:passphrase. - DB: project metadata only (no passphrase, no encrypted key).
Retrieval:
- Viewer checks localStorage for passphrase and auto-unlocks if found.
- If localStorage is missing, user must re-enter passphrase.
Risk profile:
- Passphrase exists on the device. Loss of device or browser profile exposes access.
Diagram:
localStorage(passphrase) -> derive key -> decrypt -> viewTEAM_SHARED
Intent: Multi-user access without sharing passphrases. Each user has their own encrypted copy of the passphrase.
Creation:
- Owner creates project and passphrase (same as ZERO_TRUST).
- Owner device generates a keypair locally (libsodium) if not present.
- Owner stores their public key in the server (DB) and encrypts the passphrase for themselves.
Key Generation:
- Per device keypair generated in the browser using libsodium.
- Private key never leaves the device.
- Public key is uploaded to the server with a device ID.
Storage:
- S3/MinIO: encrypted files + encrypted manifest.
- Browser localStorage:
- previewvault:<userId>:device-id (device ID) - previewvault:<userId>:device:<deviceId> (public + private key)
- DB:
- UserDevice row per device with public key + metadata. - ProjectDeviceKey row per device with encryptedKey (passphrase encrypted to that device's public key).
- Passphrase is not cached in localStorage for TEAM_SHARED.
Sharing Flow:
- Owner/admin adds collaborator by email.
- Collaborator signs in at least once to register their public key.
- Owner selects collaborator device in “Share key”; owner encrypts passphrase to the device public key and stores it in
ProjectDeviceKey.
Retrieval:
- Viewer requests a device challenge from server.
- Server returns a sealed challenge encrypted to the device public key.
- Client decrypts the challenge with the device private key and returns the plaintext.
- Server verifies the response and then returns the encrypted project key for that device.
- Client decrypts
encryptedKeylocally. - Passphrase is never transmitted in plaintext.
Risk profile:
- Strongest multi-user confidentiality. Server never stores passphrase, only encrypted copies.
- Single-device enforcement (optional): per project, the owner can require a single active device per collaborator. Other devices are blocked and reported.
Diagram:
Owner device keys: (public, private)
passphrase --encrypt--> ProjectDeviceKey(owner_device_pub) -> DB
Collaborator:
device keys created -> public key saved -> DB
Owner shares:
passphrase --encrypt--> ProjectDeviceKey(collab_device_pub) -> DB
Viewer:
challenge -> decrypt -> verify -> fetch ProjectDeviceKey -> decrypt -> passphraseSERVER_MANAGED
Intent: Simplest multi-user access. Server stores an encrypted passphrase and returns it to members.
Creation:
- Passphrase is generated or provided client-side.
- Client sends passphrase to server for storage.
- Server encrypts passphrase using
PROJECT_KEY_SECRETand stores it in DB.
Storage:
- S3/MinIO: encrypted files + encrypted manifest.
- DB:
Project.passphraseEncencrypted with server secret.
Retrieval:
- Viewer calls
/api/projects/[id]/secret. - Server decrypts
passphraseEncand returns passphrase to authorized members (owner/admin/member). - Viewer auto-unlocks using the returned passphrase.
Risk profile:
- Simpler UX but server holds the ability to retrieve passphrase for any member.
Diagram:
passphrase -> server encrypts with PROJECT_KEY_SECRET -> DB
member -> /secret -> passphrase -> decrypt filesPUBLIC_LINK
Intent: Link-only access. No auth required; passphrase is still used but publicly retrievable.
Creation:
- Passphrase auto-generated if user does not provide one.
- Passphrase is stored on server (same as SERVER_MANAGED).
Storage:
- S3/MinIO: encrypted files + encrypted manifest.
- DB:
Project.passphraseEncencrypted with server secret.
Retrieval:
- Viewer calls
/api/projects/[id]/secret/public. - Server returns passphrase without requiring auth.
- Viewer auto-unlocks.
Risk profile:
- Lowest confidentiality. Anyone with the link can retrieve passphrase and access data.
Diagram:
public link -> /secret/public -> passphrase -> decrypt filesKey Materials and Where They Live
- Passphrase (plain):
- ZERO_TRUST: not stored. - LOCAL_DEVICE: browser localStorage. - TEAM_SHARED: never stored server-side; exists only on owner’s device and in encrypted form for each member. - SERVER_MANAGED / PUBLIC_LINK: stored encrypted on server.
- Project encryption key (derived):
- Always derived client-side from passphrase + salt. - Never stored; created in memory for encrypt/decrypt.
- Device keypair (TEAM_SHARED):
- Public + private keys stored in browser localStorage per device. - Public key stored on server for device registration. - Private key never leaves device.
Security Notes
- All file encryption/decryption happens client-side.
- Server never sees plaintext file content.
- Server can only access passphrase in SERVER_MANAGED or PUBLIC_LINK.
- TEAM_SHARED is best for multi-user trust without passphrase sharing.
- All non-public projects require authenticated project members to access; only PUBLIC_LINK allows anonymous access.
- Device metadata (user agent, platform, timezone, language, screen size) is captured for anomaly detection only. It is not a hard security boundary and can change due to updates or privacy tools.
- Suspicious metadata changes are reported even when the device challenge verifies successfully.
Data Flow Diagram (Creation vs Viewing)
Creation:
files -> encrypt -> upload ciphertext
manifest -> encrypt -> upload
(optional) passphrase -> server (policy-dependent)
Viewing:
download manifest -> decrypt
download file -> decrypt on demandTroubleshooting
- Team-shared collaborator cannot receive key:
- They must sign in once to register their public key. - Owner should refresh collaborators and then share key.
- Passphrase not auto-filled:
- LocalStorage might be blocked or cleared (incognito clears on close). - For TEAM_SHARED, user needs local keys and a shared key entry in DB.