Docs

Security Workflow

Back to docs

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 -> view

LOCAL_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 -> view

TEAM_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 encryptedKey locally.
  • 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 -> passphrase

SERVER_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_SECRET and stores it in DB.

Storage:

  • S3/MinIO: encrypted files + encrypted manifest.
  • DB: Project.passphraseEnc encrypted with server secret.

Retrieval:

  • Viewer calls /api/projects/[id]/secret.
  • Server decrypts passphraseEnc and 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 files

PUBLIC_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.passphraseEnc encrypted 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 files

Key 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 demand

Troubleshooting

  • 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.