Manifest is a zero-knowledge platform for the marine protection & indemnity (P&I) renewal book. Every piece of business data is encrypted in your browser before it is sent anywhere, using keys that never leave your device. As a result the servers only ever hold ciphertext and cannot read your renewal data. This page explains, in plain terms, how that is built and enforced in the source code.
The core idea: the server can't read your data
The whole design follows from treating the server as untrusted for confidentiality. It authenticates people and stores ciphertext, but the confidentiality of the renewal data lives entirely in keys held in your browser. The guarantee we design for: a full compromise of the database, or a malicious server operator, yields only ciphertext — no renewal data and no keys.
- Your browser is the only place plaintext and keys ever exist, and only for the length of a session.
- The server is trusted to authenticate users and store ciphertext, and to enforce access rules — but it physically cannot read the business data, because it has no key.
- The network is assumed hostile (defended by HTTPS and strict headers).
- Other users can reach only the data their role and grant allow.
The zero-knowledge key hierarchy
Access to the encrypted book unwraps in a chain that starts from a secret only you know and ends at the data key — every step happening in your browser:
Manifest Key (human-readable, shown once, never stored)
│ Argon2id (memory-hard, salted)
▼
Wrapping Key (256-bit, in memory only)
│ unwraps your private key (AES-256-GCM)
▼
Your RSA private key
│ decrypts your access grant (RSA-OAEP)
▼
Organisation Data Key (DEK, 256-bit, in memory)
│ AES-256-GCM
▼
Every business record (ciphertext in the database)What the server stores is all non-secret: your public key, your encrypted private key and its salt, your grant (the organisation data key encrypted to your public key), and ciphertext records. It never stores your Manifest Key, the wrapping key, your private key, or the data key.
Cryptographic building blocks
All cryptography uses the platform Web Crypto API — no home-grown algorithms. All randomness comes from the cryptographically-secure generator; Math.random is never used for anything security-relevant.
- AES-256-GCM for record and key encryption — 256-bit keys, a fresh random 96-bit nonce per message, a 128-bit authentication tag, and keys imported as non-extractable.
- Argon2id (memory-hard, run as WebAssembly) to derive the wrapping key from your Manifest Key — 256 MiB of memory by default, with a fresh random salt. The cost parameters are stored with each user so they can be raised over time, and are bounds-checked before use so a tampered value can't exhaust or hang the browser.
- RSA-OAEP, 3072-bit for the per-user keypair that shares the data key between users.
- A random 256-bit data key — generated by the secure generator, never derived from the human key.
- HMAC-SHA-256 / HKDF-SHA-256 to derive independent, purpose-specific sub-keys from the data key, so no two purposes ever share key material.
Tamper-evidence: ciphertext bound to its context
Each ciphertext is cryptographically bound to where it belongs, so the untrusted server cannot relocate, duplicate or replay encrypted blobs:
- Each record's ciphertext is bound to its own row identity — a blob moved to a different row simply fails to decrypt.
- Each wrapped key is bound to its owner, and each access grant is bound to its grantee and the current key generation — so a grant can't be relocated to another user or replayed after a re-key.
Any wrong key, wrong context, or tampering makes decryption throw — failures are unambiguous, never a silent wrong answer.
The key vault in your browser
Once you unlock, the organisation data key is held only in memory for the life of the browser tab. It is never written to permanent storage, cookies, URLs, or logs, and never leaves the device. To avoid re-prompting on every page reload it is additionally cached in tab-scoped session storage, which the browser clears automatically when the tab closes. That cache:
- is scoped to the single tab and never shared across tabs;
- expires daily (a hard cut-off each morning — “unlock once per day”);
- is cleared on explicit lock, on idle auto-lock (after an hour of inactivity), on sign-out, and whenever the sign-in page is revisited.
Key bytes are best-effort zeroed on lock. Locking always forces the Manifest Key to be re-entered — the identity session can outlive the data key, but never substitutes for it.
The Manifest Key & recovery
- Your Manifest Key is six memorable words in a random order. It is shown once and offered to your browser's own password manager, which stores it locally on your device under your control. Manifest itself never stores it — not in the database, not on the server, not in any app-controlled storage.
- The key's own entropy is modest by design; the memory-hard Argon2id derivation is the deliberate backstop that makes brute-forcing a stolen encrypted key infeasible.
- A higher-entropy break-glass key (developer-only) allows organisation-level recovery if all normal keys are lost. It is generated and shown once and, like the Manifest Key, is never stored by Manifest.
- Keys rotate every 60 days: past that, a user must re-roll to a fresh keypair before continuing.
- Repeated failed unlock attempts trigger a temporary lockout. This is a usability speed-bump only — the real protection against offline attack is the Argon2id memory-hardness above.
Multi-user access without shared secrets
A single organisation data key encrypts everything, yet no secret is ever shared between people. Access is a grant: the data key encrypted to a user's public key.
- Granting needs nothing secret from the new user — an existing key-holder encrypts the data key to their public key. This is how access is restored after a lost key with no data loss.
- A new user first provisions their own keypair from their own Manifest Key; until an authorised user grants them the data key, they can sign in but decrypt nothing.
- Grant changes are tier-bounded: a user may only manage their own grant or that of a strictly lower-privileged user, so no one can lock out a peer or a superior. A user's encrypted key material is readable only by themselves; others can retrieve only the public key needed to grant access.
Signing in
Sign-in is a passwordless one-time code emailed to your work address, which you type back on the page (a typed code rather than a clickable link, so corporate email security that pre-fetches links can't consume a one-time login). The identity provider is the authentication authority.
- Every request is checked against an explicit account allow-list, enforced both in the server route and in the database itself — so no account outside the allow-list can ever be created, even by calling the identity service directly.
- The unauthenticated link-request endpoint is rate-limited by email and by source IP before any account or token is created.
- There is no account-enumeration oracle: an unknown email gets the same generic response as a real send.
- An optional CAPTCHA can be enabled in front of the endpoint.
- Microsoft Entra ID single sign-on is also supported. Any SSO login is re-checked against the allow-list before a session is granted. Sign-in carries no key material, so it never weakens the zero-knowledge model.
Access control in the database
Roles form a strict hierarchy (Developer, Managing Director, Editor, Viewer). On top of encryption, the database enforces who can do what:
- Row-Level Security is enabled and forced on every table, and anonymous access is never granted anything. Reading records requires a valid data-key grant and membership in that record's department; writing requires an editor membership.
- All privileged actions — role changes, access grants, key resets — happen only through hardened database functions that re-check the caller's authority server-side. There is no client-side write path to roles or grants.
- Key-reset and recovery actions are written to an append-only audit log that clients cannot forge.
- The manager panel is gated twice: a server-side authority check and Row-Level Security as the real backstop.
Because every stored business column is ciphertext, even a mistake in an access rule cannot leak readable data — the access rules are defence-in-depth on top of the encryption, not the only line of defence.
Safe data handling
- Spreadsheet import/export and document extraction run entirely in your browser — files are parsed locally and the plaintext never leaves your device unencrypted.
- Imported and edited values are standardised against one controlled vocabulary and sanity-checked (for example, premium equal to rate × tonnage, plausible build years, consistent dates) — records that don't compute are flagged with the exact conflicting fields.
- The renewal book is an append-only, encrypted event log: every change is a new encrypted entry, giving a full, tamper-evident history and the ability to view the book as it was at any earlier point.
- Third-party libraries that touch untrusted files are pinned to patched versions.
Programmatic access
A read-only software interface lets other tools query the book while preserving the zero-knowledge boundary exactly: all decryption happens in the caller's own process. The server still returns only ciphertext plus the caller's own encrypted grant; the Manifest Key, private key and data key never leave the caller.
Transport & browser hardening
- All traffic is HTTPS, with HTTP Strict Transport Security (preloaded).
- A strict Content-Security-Policy allows scripts only from the app itself via a fresh per-request nonce (no arbitrary inline scripts), pins network connections to the exact backend origin, and forbids the page from being framed.
- Additional headers block content-type sniffing, suppress the referrer, deny camera/microphone/geolocation, and isolate the key-holding tab from other windows.
- Authentication and application pages are served no-store so sensitive responses are never cached.
- The privileged server credential is read from a server-only variable and never shipped to the browser, and no secrets are committed to the source repository.
Honest limitations
For a fair assessment, the accepted trade-offs of the current design:
- Zeroing keys is best-effort. JavaScript cannot guarantee the engine hasn't copied key bytes; zeroing shrinks the exposure window rather than guaranteeing erasure.
- The Manifest Key's entropy is modest by design, offset by memory-hard Argon2id. A deployment that needs a higher bar can raise the word count or the derivation cost.
- The guarantee is against a compromised server, not a compromised client. Malware or a malicious browser extension on an unlocked device could read plaintext while the session is open — mitigated by the strict content-security policy, the idle auto-lock, and continuous adversarial testing.
- The reload cache keeps the data key in tab-scoped session storage between page reloads; while valid it is readable by scripts running on the page. It is tab-scoped, expires daily, and never touches permanent storage — and does not weaken the zero-knowledge guarantee toward the server.
