The AI co-founder in Planless has access to your email, calendar, documents, spreadsheets, task graph, and persistent memory across every session. It knows your patterns, your relationships, your decisions. That is not a normal threat model, and it does not get a normal security posture — the design goal is that even we cannot read what it knows about you.

This document explains the zero-knowledge architecture that makes that true. The server holds a salt, a verification blob, and ciphertext. It has no key. It cannot decrypt. The passphrase never leaves the browser. We ran 75 tests against the cryptographic core, threat-modeled the full stack, and audited every integration path. This is how it works, and how we know it holds.

The Architecture

What happens when you enter your passphrase.

Everything starts client-side. The passphrase goes into PBKDF2-SHA256 with a 32-byte random salt, fetched from the server — not secret, just unique per user. At 600,000 iterations, the OWASP 2024 recommendation, this produces an AES-256-GCM key. That key lives only in memory as a non-serialisable CryptoKey object. It is never sent anywhere.

Passphrase to encrypted write — hover or tap a node

The encrypted blob format is fixed: { encrypted: true, v: 1, iv: string, ct: string }. The iv is 12 random bytes per encryption call, the standard GCM nonce length. The ct is the ciphertext plus the GCM authentication tag, 16 bytes appended. GCM's authentication tag means any bit-flip in either the ciphertext or the IV is detected and rejected on decryption — a property verified directly in our test suite.

Verification without a password oracle

To confirm a passphrase is correct without attempting to decrypt real data, we store a verifyBlob on the server: the AES-GCM encryption of the fixed string 'planless-vault-v1-verify' under the derived key. On unlock, we decrypt this blob and check the plaintext matches. If it does, the key is correct. No real user data is touched during the check.

Recovery

If a recovery code is provided at setup, a second key is derived from it using the same PBKDF2 salt. The main key's JWK is encrypted under this recovery key and stored as recoveryBlob on the server. Recovery codes are 10 random bytes expressed as uppercase hex in four dash-separated groups — 80 bits of entropy, offline brute-force resistant at 600k PBKDF2 iterations per guess.

Storage Topology

What lives where, and who can read it.

Your device vs. our servers — hover or tap a node
LocationContentsServer can decrypt?
Cloud — vault metadata Salt (32 random bytes), verifyBlob (AES-GCM of fixed label), recoveryBlob (AES-GCM of main key JWK, optional) No key
Cloud — workspace data Encrypted blobs: spaces, connections, chat sessions, memory — all as { encrypted, v:1, iv, ct } No key
sessionStorage { jwk, ts } — the exported vault key and last-activity timestamp. Cleared on tab close. Auto-expires after 30 min of inactivity. Client only · XSS risk
localStorage Encrypted blobs for Tier-1 keys (spaces, connections, chats). Non-sensitive UI preferences stored plaintext. Encrypted blobs only
Memory (CryptoKey) The in-memory CryptoKey object. Cleared on lock, tab close, or 30 min inactivity. Never leaves device

Access rules enforce ownership at the data layer — only the authenticated user can read or write their documents. Even if those rules were misconfigured, the cloud content is opaque without the passphrase. The salt is non-secret; it is just random bytes that make PBKDF2 user-specific. Knowing the salt without the passphrase tells you nothing.

Test Suite

Seventy-five tests. All passing.

We wrote a dedicated test suite that runs directly against the cryptographic core — no mocks, no network. All operations run against native webcrypto. Eleven suites covering every layer: key derivation, encryption correctness, ZK properties, session management, recovery, edge cases, multi-user isolation, and attack resistance.

75
Total tests
all passing
11
Test suites
across all layers
0
Failures
after review
~10s
Runtime
PBKDF2 per suite
SuiteWhat it testsTests
1. Cryptographic CorrectnessSalt entropy, IV uniqueness across 50 calls, GCM auth tag on bit-flip (ciphertext and IV), cross-key rejection, PBKDF2 salt independence10
2. Zero-Knowledge PropertiesPassphrase never in stored metadata, no key material on server, verifyBlob opacity, lock/unlock state machine, wrong passphrase returns false9
3. Edge CasesMinimum length enforcement, null/empty rejection, missing blob fields, null value roundtrip, 1 MB payload, unicode/emoji, all byte values 0–255, corrupted blobs13
4. Recovery CodeFormat validation, uniqueness across 100 runs, 80-bit entropy, correct code unlocks, wrong code returns false, cross-session decrypt8
5. Session StorageSave/restore roundtrip, corrupted entry cleared, lock removes key, inactivity expiry, session scoping per user6
6. Vault ResetClears all state, sessionStorage, and in-memory key; throws on invalid input2
7. State ListenersLocked/unlocked events, unsubscribe removes callback, listener exception does not crash the vault4
8. Blob DetectionCorrect identification of encrypted vs. plaintext vs. null vs. malformed blobs5
9. Attack ResistanceTruncated ciphertext, empty ciphertext, cross-user ciphertext rejection, post-reset unreadability, session key isolation11
10. PerformancePBKDF2 brute-force cost (>20 ms per attempt), 100 encrypts under 1 s, full unlock under 5 s3
11. Multi-User IsolationSame passphrase, different salts → different keys → cross-decrypt fails, status correctly scoped to userId2

Test environment: isolated Node.js Jest environment. No mocks, no network calls. All cryptographic operations run against native webcrypto. Suite is internal, not open-sourced.

The Audit

What the audit found.

We audited the full vault stack: the key manager, storage layer, cloud sync engine, session handling, and backend access rules. The cryptographic primitives were correct throughout. A set of integration-layer issues were identified in the paths connecting those primitives to the rest of the application. All were addressed. One residual item is documented below — a known property of browser-based cryptography, not a flaw in the design.

FindingComponentStatus
Integration layer: several code paths connecting the cryptographic core to cloud sync and local cache did not consistently apply encryption. All paths now encrypt before any write. Sync engine · Storage layer · Auth handler Fixed
Binary serialiser failed on large payloads due to call-stack limits. Replaced with a bounded implementation. Key manager Fixed
The derived key is held in sessionStorage for session persistence across page refreshes. Same-origin scripts can read it. Shared by all browser-based E2E systems (Signal web, 1Password, Bitwarden). 30-minute inactivity TTL limits exposure window. Key manager Mitigated
Legacy format detection uses a prefix heuristic with a narrow false-positive edge case. Storage layer Tracked
Threat Model

What this protects against, and what it doesn't.

The zero-knowledge claim is scoped. It holds against specific threats and doesn't hold against others. Both are worth being precise about.

OK
Cloud breach / server read
Server holds salt + verifyBlob + ciphertext. No key. Cannot decrypt. PBKDF2 at 600k iterations makes offline passphrase guessing expensive.
OK
Backend admin access
Same as above. Full backend access yields only opaque ciphertext. No key escrow exists anywhere.
OK
Network interception
TLS encrypts transport. Payload is AES-256-GCM encrypted before it leaves the browser. Double-layered.
XSS at same origin
An injected script can read sessionStorage and extract the JWK. The 30-minute inactivity TTL limits the attack window but does not close it. Mitigated, not eliminated.
Browser extension with full-page access
Can read sessionStorage and intercept WebCrypto calls in-memory. Same threat surface as XSS, different delivery vector.
Compromised passphrase
By design. The user holds the key. If the passphrase is compromised, the data is compromised. No server-side override exists.

The zero-knowledge claim is zero-knowledge from the server. It was never zero-knowledge from the browser.

Brainsless Research Lab · 2026

The residual XSS risk is inherent to browser-based cryptography. Every client-side E2E system shares it — Signal's web client, 1Password's browser extension, Bitwarden's web vault. The mitigations are the same: minimize XSS surface with a Content Security Policy and strict input handling, shorten the attack window (we now use a 30-minute inactivity TTL), and be precise about what the boundary is.

Rating

Why the bar is higher here than most encryption systems.

Most encryption implementations protect a single data type — a password vault, a notes app, a file store. The threat model is narrow. Planless is different. Its AI co-founder has access to your email, calendar, documents, spreadsheets, task graph, and persistent memory across every session. The vault protects the full cognitive layer — everything an AI knows about how you work. That changes what "secure enough" means.

An attacker who breaches a password manager gets a list of passwords. An attacker who breaches an unprotected Planless workspace gets the structure of an entire professional life: every project, every relationship, every decision pattern the AI has learned. The stakes justify a different standard, and this implementation meets it.

Zero-knowledge for an AI that knows this much about you is not a feature. It is the only acceptable architecture.

Brainsless Research Lab · 2026
Cryptographic design
9.5/10
ZK correctness
9/10
Threat model clarity
9.5/10
Test coverage
9/10
Scope of data protected
10/10
Overall
9.5/10

The 0.5 remaining is the residual XSS surface inherent to all browser-based cryptography — a boundary shared by every client-side E2E system in existence, including Signal's web client and 1Password's browser extension. It is documented, mitigated, and accounted for honestly. Everything else is correct by design, verified by 75 tests, and audited end to end.