Security Dispatch · No. 002

Zero-Knowledge
by Design

How Planless encrypts your workspace — and how we verified it. Key derivation, storage topology, threat model, and a full test suite run against the cryptographic core.

Brainsless Research Lab · February 2026 · Security

Orbis 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 isn't a normal threat model. It requires a different kind of security — one where even we cannot read what it knows about you.

This document explains the zero-knowledge architecture that makes that true. The server holds 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.

flowchart TD A(["🔑 Passphrase\n(browser only)"]) S(["32-byte Salt\n(server — not secret)"]) B["PBKDF2-SHA256\n600,000 iterations"] C(["AES-256-GCM Key\n(CryptoKey — memory only)"]) D["sessionStorage\n{ jwk, ts }\n30 min TTL"] E["AES-256-GCM\nencrypt / decrypt\nUnique 12-byte IV per call"] F[("Cloud\n{ encrypted, v:1, iv, ct }")] G[("localStorage\n{ encrypted, v:1, iv, ct }")] A --> B S --> B B --> C C -->|"exportKey → JWK"| D D -->|"inactivity → auto-lock"| X(["🔒 Locked"]) C --> E E --> F E --> G style A fill:#F2E4DA,stroke:#CC785C,color:#1A1714 style S fill:#F2E4DA,stroke:#CC785C,color:#1A1714 style C fill:#F2E4DA,stroke:#CC785C,color:#1A1714 style X fill:#FAEAE9,stroke:#C0392B,color:#C0392B style D fill:#FEF3C7,stroke:#B45309,color:#1A1714 style F fill:#E8F5EE,stroke:#2D7A55,color:#1A1714 style G fill:#E8F5EE,stroke:#2D7A55,color:#1A1714

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.

Location Contents Server 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's 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
Suite What it tests Tests
1. Cryptographic Correctness Salt entropy, IV uniqueness across 50 calls, GCM auth tag on bit-flip (ciphertext and IV), cross-key rejection, PBKDF2 salt independence 10
2. Zero-Knowledge Properties Passphrase never in stored metadata, no key material on server, verifyBlob opacity, lock/unlock state machine, wrong passphrase returns false 9
3. Edge Cases Minimum length enforcement, null/empty rejection, missing blob fields, null value roundtrip, 1 MB payload, unicode/emoji, all byte values 0–255, corrupted blobs 13
4. Recovery Code Format validation, uniqueness across 100 runs, 80-bit entropy, correct code unlocks, wrong code returns false, cross-session decrypt 8
5. Session Storage Save/restore roundtrip, corrupted entry cleared, lock removes key, inactivity expiry, session scoping per user 6
6. Vault Reset Clears all state, sessionStorage, and in-memory key; throws on invalid input 2
7. State Listeners Locked/unlocked events, unsubscribe removes callback, listener exception does not crash the vault 4
8. Blob Detection Correct identification of encrypted vs. plaintext vs. null vs. malformed blobs 5
9. Attack Resistance Truncated ciphertext, empty ciphertext, cross-user ciphertext rejection, post-reset unreadability, session key isolation 11
10. Performance PBKDF2 brute-force cost (>20 ms per attempt), 100 encrypts under 1 s, full unlock under 5 s 3
11. Multi-User Isolation Same passphrase, different salts → different keys → cross-decrypt fails, status correctly scoped to userId 2
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 — it is a known property of browser-based cryptography, not a flaw in the design.

Finding Component Status
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 ZK claim is scoped. It holds against specific threats and doesn't hold against others. Both are worth being precise about.

Cloud breach / server read
Server holds salt + verifyBlob + ciphertext. No key. Cannot decrypt. PBKDF2 at 600k iterations makes offline passphrase guessing expensive.
Backend admin access
Same as above. Full backend access yields only opaque ciphertext. No key escrow exists anywhere.
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 ZK 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 (Content Security Policy, strict input handling), shorten the attack window (inactivity TTL, we now do 30 minutes), and be honest about what the boundary is.

Rating

Why the bar is higher here than anywhere else.

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. Orbis has access to your email, calendar, documents, spreadsheets, task graph, and persistent memory across every session. The ZK 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 your entire professional life: every project, every relationship, every decision pattern Orbis has learned. The stakes justify a different standard — and this implementation meets it.

"Zero-knowledge for an AI that knows everything 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 honestly accounted for. Everything else is correct by design, verified by 75 tests, and audited end-to-end.

Notes

[1] PBKDF2-SHA256 at 600,000 iterations matches the OWASP Cheat Sheet recommendation for 2024. On modern hardware this takes approximately 50–300ms per derivation attempt, making offline brute-force of a reasonable passphrase impractical at scale.

[2] The AES-256-GCM authentication tag is 128 bits appended to the ciphertext. Any modification to the ciphertext, IV, or additional authenticated data causes decryption to throw a DOMException before any plaintext is returned. Verified directly in test suite suite 1.

[3] The 30-minute session inactivity TTL was chosen to balance UX (not re-prompting mid-session) with security (limiting XSS window). It resets on every encrypt() or decrypt() call. Sessions are also cleared on tab close (sessionStorage) and on explicit lock.

[4] Tier-1 covers all primary workspace data: spaces, connection graphs, chat history, and relationship indexes. UI preferences (theme, zoom, interaction speed) are not encrypted — they contain no sensitive content and encrypting them adds latency with no security benefit.

Back to Brainsless Lab Brainsless · Security Dispatch No. 002