Threat model
A zero-knowledge claim is only useful if its limits are stated as clearly as its protections. This page walks the common attacker positions; each section says what holds, what does not, and which mitigation applies. The cryptographic facts are documented on the enclave page: AES-256-GCM per record (random 96-bit IV per operation), PBKDF2-SHA256 at 600,000 iterations for key derivation, AES-KW key wrapping, all through the platform's Web Crypto API with zero npm crypto dependencies.
The storage or server is compromised
This is the design case: record content is protected in full. A store — disk, IndexedDB, cloud bucket, database — holds only ciphertext envelopes; it cannot read records, cannot run queries (queries execute inside the hub after decryption), and a tampered envelope fails AES-GCM authentication with a TAMPERED error rather than decrypting to something wrong. A cloud admin, a stolen backup, a subpoenaed bucket: all get the same ciphertext. Verify it yourself in one minute: See the ciphertext.
What it does not hide: envelope metadata the store needs to operate — record count, ids, sizes, write timestamps, version numbers. An observer of the store learns that you write and roughly how much, never what — unless a collection opts a field into equality search, a separately-acknowledged trade that reveals which records share a value for that field without revealing the value itself. Two forms exist, each behind its own explicit gate: deterministic fields (acknowledgeDeterministicRisk), and classified fields marked equatable (acknowledgeEquatableRisk).
A network observer
Envelopes cross the wire in the same encrypted form they're stored in, so a passive observer of your traffic to a store or sync target reads nothing — independent of (and additional to) transport TLS. The metadata caveat above applies equally here.
The device is stolen (vault locked)
The at-rest protection is the secret-derived key hierarchy: PBKDF2 at 600,000 iterations makes each guess cost ~200 ms, which makes brute force infeasible against a 12+ character or 4+ word diceware secret — and that quality is on you; a weak secret is the single easiest way to lose the whole property. The keyring on disk holds only wrapped keys.
If the vault was unlocked when the device was taken, the session's keys are in process memory until db.close() wipes them — see the next section, because a live unlocked session is the real attack surface.
A malicious browser extension, or compromised client
The question evaluators ask most, so here is the honest answer first: noy-db cannot protect an unlocked session from code that gets into the page. Any XSS or malicious dependency runs in the session's own JS context and reads what the session reads. A malicious extension's content scripts run in an isolated world — they can't reach the page's JS memory directly, but they can read and modify everything the page renders, capture what's typed (including a secret), and, where the page's CSP permits script injection, escalate into the page context itself. In practice, treat an attacker inside the browser profile as able to observe the unlocked session. No client-side encryption library can prevent this; one that claims to should worry you.
What noy-db provides is a set of ways to shrink the window and the blast radius:
on-webauthn— unlock with a platform passkey instead of a typed secret: there is nothing to keylog, and the credential lives in the platform secure enclave; the wrapped key material is encrypted under the WebAuthn credential and never appears as a typeable secret in the page.- Session hygiene —
withSession()issues session tokens that expire on their TTL, and defines asessionPolicy(idle/absolute timeouts, re-auth for sensitive operations) that your app enforces by driving acreateEnforcer()at its own call sites — the policy is not applied automatically.db.close()wipes the in-memory keys (the memory-dump mitigation from the security policy). A short-lived session means a shorter window for anything watching it. - Scoped grants — in a multi-user vault (team), stealing a viewer session yields read-only access to what that keyring covers, not the vault; tiered records keep higher-tier fields sealed even inside an unlocked session until elevation.
- Duress and lockout signals —
on-threatadds lockout and duress behaviors around the unlock ceremony (showcase31). - App-level hygiene still matters — a strict CSP, dependency auditing, and not embedding untrusted scripts are your app's job; noy-db runs inside whatever page you give it.
A phishing page imitating the unlock screen
Tier 2 already resists this: a passkey is bound to an origin, so a look-alike page cannot make it sign. The exposed surface is tier 1 — a phrase the user types into a field, on the rare occasions the app asks for it. A page that reproduces your unlock screen convincingly enough gets that phrase, and with it the vault.
Echo mode (secretMode: 'echo') is the answer to that specific attack. The tier-1 secret becomes one sentence enrolled as three parts — the user types the prompt, the vault reveals the echo (the middle of the user's own sentence), the user recognizes it and types the key. The vault has to prove it holds the secret before the user finishes handing over theirs.
Grading the attacker by what they actually hold:
| Attacker | Holds | Outcome |
|---|---|---|
| A — static clone | nothing but your look-alike page | Defeated. It cannot produce the echo, and the user is told to expect one. |
| B — file-holder | a copy of the keyring or pod file, including any reveal blob it carries | Defeated only by a sealed reveal, whose blob needs the enrolling device's key store to open. A portable reveal is decryptable by anyone who holds the file and knows the prompt — that is a deliberate trade for a reveal that travels with a shared pod. |
| C — flow substitution | nothing; asks the user to "enter your full secret instead" | Defeated structurally. No single typed string can ever derive an echo vault's key, so no legitimate single-field flow exists to imitate. The key-derivation input is a length-prefixed encoding of the three parts starting with a byte that text encoding can never produce, and the key-derivation dispatch refuses a plain string on an echo keyring outright. |
| Malicious player | the pod and the user interface | Out of scope. Nothing survives typing a secret into hostile code. Echo mode targets remote phishing pages and honest-but-lazy viewers, not an application the user has been persuaded to trust. |
What echo mode does not hide
Stated plainly, because they are the price of the design:
- The prompt and the echo are each an offline guessing oracle for a file-holder. The keyring stores a verifier for both (they are what gate the reveal, and what verify a typed echo on the degraded path). Anyone holding the file can test candidate prompts and candidate echoes offline — at 600,000 PBKDF2 iterations per guess, but without limit. The prompt therefore carries its own minimum-length floor. The echo has no entropy floor at all: it is a free-form human phrase, so its resistance is whatever the user gave it. "Attacker-B resistance" for a
sealedreveal means B cannot read the echo — never that B cannot grind for it. - A sealed reveal blob is unpadded, so its length discloses the echo's byte length. Not the echo, but its size.
- The revealed echo is on-screen material — shoulder-surfable, and captured by anything that can read what the page renders (see the previous section). Masking is a user-interface mitigation, hinted at by the keyring's optional mask hint; it is not a cryptographic one.
- Recognition checks fail when they become routine. This is why the same idea failed as the "site recognition image" of the 2000s: shown a familiar image on every login, users stop noticing when it is missing. Echo mode survives that only because tier 1 is rare — daily use runs on tier 2 and tier 3, so the ceremony fires on high-attention events (enrollment, recovery, a new device) and never becomes background noise. An application that puts the echo ceremony in the daily path throws away the property it was buying.
- The content of the secret is the user's job, and it is the part most often got wrong. An echo drawn from public biography — a school, a nickname, a birthplace — is guessable by an acquaintance and often by a search engine, which hands attacker B the oracle for free. The enrollment recipe is the guidance; the hub cannot validate it, because no validator can tell a private memory from a published fact.
Reverse engineering the client code
Reading the code reveals no secret — by design, there is nothing secret in it. noy-db's source is readable, its algorithms are the standard ones named above, and its security reduces entirely to key material, which the code never contains: keys are derived from the user's secret, held by the platform authenticator (on-*), or sealed at a host you control (at-* — the one family that is deliberately not zero-knowledge: a host you run can decrypt the scoped slice it unseals, which is its purpose). Decompiling the app tells an attacker how the lock works, not where the key is. Kerckhoffs' principle, applied.
A revoked user
Revocation always re-encrypts under new data keys, so a revoked member's cached key material stops opening anything new. What no system can do: un-read what they already read while authorized — treat revocation as stopping future access, not as recalling the past.
Reporting a vulnerability
Never a public issue: report it privately through GitHub's private vulnerability reporting on noy-db/core (Report a vulnerability).
See also
- Core 02 — Client (Enclave) — the key hierarchy behind every claim above.
- session-tiers — the three-tier ladder, the three tier-1 modes, and the policy gates that guard sensitive actions.
- Composing an echo secret — choosing three parts an acquaintance cannot guess.
- See the ciphertext — the one-minute verification.
- Where can noy-db run? — how the trust boundary moves per topology.