Security

How Holdfast protects your vault.

Zero-knowledge encryption with independently verifiable code. Your passphrase never leaves your browser. We cannot read your vault. Here is exactly how that works.

The encryption model

When you create or edit your vault, all content is encrypted in your browser before it leaves your device. The encryption key is derived from a passphrase that you choose. That passphrase is never transmitted to our servers, never stored in our database, and never visible to anyone at Nexus-Sec Ltd.

If someone breached our database, they would find encrypted blobs and metadata. They would not find your passphrase, your derived key, or any plaintext vault content. This is what "zero-knowledge" means in our context: the operator cannot read user vaults, even with full database access.

Algorithm parameters

ParameterValue
EncryptionAES-256-GCM (authenticated encryption)
Key derivationPBKDF2-SHA256, 250,000 iterations
Salt128-bit (16 bytes), randomly generated per vault
IV96-bit (12 bytes), fresh random value per encryption operation
Authentication tag128-bit (GCM default)
DependenciesNone. WebCrypto API (crypto.subtle) only

AES-256-GCM provides both confidentiality and integrity. If a single byte of ciphertext is modified, decryption fails entirely rather than producing corrupted plaintext. Each encryption operation uses a fresh random IV, because reusing an IV with the same key under GCM is catastrophic.

PBKDF2 with 250,000 iterations is FIPS-validated and available natively in the WebCrypto API across all browsers without polyfills or third-party libraries. Argon2id (memory-hard, more resistant to GPU/ASIC attacks) is on the migration roadmap.

Open source encryption module

The encryption code that protects your vault is published as open source:

github.com/Nexus-sectech/holdfast-crypto

This module is the only code in Holdfast that touches plaintext. It contains four core functions (deriveKey, encrypt, decrypt, generateSalt) plus two binary functions (encryptBuffer, decryptBuffer) for video messages and file attachments. Nothing else: no API calls, no authentication logic, no UI code, no server-side operations.

On every vault page load, your browser verifies the integrity of this module using Subresource Integrity (SRI). The browser checks a cryptographic hash before executing the file. If a single byte differs from the expected value, the browser refuses to run it. You can inspect this yourself in your browser's developer tools.

Full verification instructions are in the repository README.

Threat model

We design against three primary attackers:

External attacker who breaches Supabase or Vercel

They get encrypted blobs and metadata. No plaintext. No passphrase. No derived key. Every user's vault must be attacked individually via passphrase brute-force, which PBKDF2 at 250k iterations makes expensive.

A malicious or compromised Holdfast operator (us, worst case)

Same outcome. We cannot read vaults. The encryption happens in your browser with a key we never possess. Our database contains only ciphertext.

Targeted attacker who compromises a user's account

They need both the account password AND the vault passphrase to read content. Account access alone does not decrypt the vault. Even with both, they cannot impersonate the user to recipients without also intercepting the offline-shared delivery passphrase.

We do not currently defend against a state-level attacker with full network observability and endpoint compromise. No consumer SaaS does. We are transparent about this boundary.

Operator trust and its limits

Publishing the encryption module and enforcing SRI hashes improves transparency substantially, but it does not eliminate the structural limitation of browser-delivered cryptography: we serve the JavaScript, and could in principle serve a different version to a specific user. This is true of every web application that performs client-side encryption.

The mitigations are non-cryptographic but meaningful:

SRI enforcement means the browser itself verifies the encryption module on every page load against a published hash. The public repository means the source is auditable by anyone at any time. Legal entity exposure means Nexus-Sec Ltd is a UK-registered company (No. 17126982) with a named director, registered with the ICO, at a real address. The small surface area of the module makes a malicious change difficult to hide from anyone reading the code.

For users whose threat model requires verified binaries rather than browser-delivered code, Holdfast may not be the right product. We will say so directly rather than overstate what browser-based zero-knowledge can guarantee.

Infrastructure and hardening

Hosting and data residency

Supabase Postgres and Storage are hosted in their EU region (Frankfurt). Vercel functions are configured for European edge regions. All data subjects are handled under UK GDPR. Nexus-Sec Ltd is registered with the ICO.

Authentication

Supabase Auth handles account login (email/password with email verification, plus magic link). Passwords are hashed by Supabase using bcrypt. Account login is separate from the vault passphrase: your account password gets you into the dashboard, but it does not decrypt your vault. TOTP-based two-factor authentication is on the near-term roadmap.

Access control

Every API endpoint resolves the requesting user's auth_uid first and constrains queries to that user's records. Postgres row-level security policies enforce the same constraint independently at the database level. Both layers must agree for a query to succeed. This dual enforcement protects against IDOR vulnerabilities even if an application-layer bug is introduced.

Web hardening

Strict Content Security Policy with no inline scripts on vault pages. CORS restricted to Holdfast origins. HSTS enforced via Cloudflare. Cookies are HttpOnly, Secure, SameSite=Lax for sessions. Cloudflare Turnstile CAPTCHA on signup, login, and check-in endpoints. Subresource Integrity on the encryption module.

Encryption at rest

Vault data is encrypted twice. Supabase encrypts the underlying storage volumes at the infrastructure level (AES-256). On top of that, the vault blobs stored are themselves AES-256-GCM ciphertext under keys we never possess.

Post-delivery lifecycle

After a vault is delivered to recipients, a 30-day retention window begins. On day 23, every recipient with an active delivery token receives a warning email that vault access expires in 7 days. On day 30, a full purge runs automatically:

The encrypted blob, salt, hash, and passphrase hint are nulled. The vault status is set to purged. The Stripe subscription is cancelled. All files are deleted from storage. Delivery tokens, check-in tokens, and recipient records are deleted. The user row is anonymised. A purged event is logged.

This is a complete teardown. No recoverable data remains after the purge window. The retention period is configurable but defaults to 30 days.

Warrant canary

Last updated: 11 May 2026

As of the date above, Nexus-Sec Ltd has not received any government order, national security letter, or court order requiring the compromise, modification, or backdooring of the Holdfast encryption module, the insertion of surveillance capabilities, or the handover of encryption keys or user passphrases.

Nexus-Sec Ltd has not been required to provide any government or law enforcement agency with access to the plaintext contents of any user vault.

This canary is updated periodically. If this statement is removed or not updated for an extended period, draw your own conclusions.

Incident disclosure commitment

No quiet patches. Any security incident affecting the encryption boundary, user data, or the dead man's switch logic will be disclosed as follows:

Affected users will be notified by direct email within 72 hours of confirmed impact, consistent with UK GDPR Article 33 timelines. A public statement will be published on this page describing what happened, what data was affected, and what remediation was taken. The encryption changelog will record any code change made in response.

We will not silently patch a vulnerability that touches the encryption boundary. If it affects the zero-knowledge property, you will know about it. If we discover a structural flaw we cannot fix while keeping a feature live, we will end the feature and refund users rather than ship a workaround that quietly weakens the property the rest of the product depends on.

Encryption changelog

Every change to the encryption boundary is recorded before it ships to production. The full changelog is maintained in the public repository. Current state:

## [1.1.0] - 2026-05-11
### Added
- encryptBuffer() for binary blobs (video, attachments)
- decryptBuffer() for binary blobs
- Subresource Integrity enforcement on all vault pages
- All crypto.subtle calls removed from HTML files

## [1.0.0] - 2026-05-11
### Initial release
- deriveKey() — PBKDF2-SHA256, 250k iterations
- encrypt() — AES-256-GCM, fresh 96-bit IV
- decrypt() — AES-256-GCM with tag verification
- generateSalt() — 16-byte random salt
- Zero external dependencies; WebCrypto API only

What happens if Holdfast closes

In the event of business closure, we commit to: a minimum 90-day notice to all users, an export tool that returns the encrypted vault blob plus enough metadata to recover the contents with your passphrase, and a documented decryption procedure using standard cryptographic tools.

The decryption procedure is documented publicly in the encryption module repository so that even without our cooperation, a user with their passphrase and their exported blob can recover their data.

Reporting a vulnerability

If you believe you have found a security vulnerability in Holdfast, please report it to security@holdfast-co.uk. We will acknowledge receipt within 24 hours and provide an initial assessment within 72 hours. We do not currently operate a bug bounty programme, but we will credit responsible disclosures publicly if the reporter wishes.