Threat model
Who we defend against — and what we don’t.
A security product that only lists its strengths is marketing. This is the honest version: the adversaries we design against, the guarantees we make, and the things we deliberately leave out of scope.
This is the plain-English mirror of our internal engineering threat model.
Scope
What we do — and don’t — guarantee
In scope — guarantees this product makes:
- Confidentiality of your vault contents (documents, notes, metadata) against a server operator with full database access.
- Plausible deniability via the decoy vault — a coercer who obtains the decoy password cannot prove the real vault exists.
- Recovery through secret-shared trusted contacts, without the server ever learning your recovery key.
- Sharing to non-Inktally recipients without leaking the shared resource to the server.
- Triggered release with server-assisted delivery: resources designated for release are held under our key between arming and delivery so recipients who have never used Inktally can still receive them. Every delivery is audit-logged; everything else in your vault stays end-to-end encrypted. (For fully server-blind delivery, the executor-assisted mode — coming soon — keeps our servers out of the loop entirely.)
- A tamper-evident audit log that detects server-side edits to your history.
Out of scope — what we do NOT defend against:
- An adversary who controls your device at the moment you unlock. If your endpoint is compromised, no server-side design can save the plaintext you just decrypted.
- Side-channel attacks against your hardware (cache timing, Spectre/Meltdown, fault injection).
- Quantum adversaries — XChaCha20-Poly1305 and X25519 are not post-quantum.
- Availability — we are a confidentiality and integrity product, not a high-availability one. DoS and outages are not in this model.
- Legal compulsion of the server to start logging new data going forward. Past data stays encrypted; future collection is a surface this product can’t close.
Adversaries
The four classes we design against
Ordered by capability — each tier can do everything the one before it can.
1. Passive network observer
Can: Sees your TLS-encrypted traffic to and from the server.
Defended by: TLS in transit, no plaintext personal data on the wire, and login responses that are constant-shape regardless of whether an account exists.
2. Honest-but-curious server operator
Can: Reads the database, storage, and logs in real time — but doesn’t inject rows or tamper.
Defended by: Zero-knowledge encryption: every sensitive field is ciphertext under a key derived on your device. Every account holds exactly two vault rows, so the operator can’t tell which password opens which. The audit chain detects after-the-fact edits.
3. Malicious server operator
Can: Injects arbitrary rows, modifies code paths, and — critically — can serve a backdoored client.
Defended by: Stored data stays useless without your client-derived key. But a backdoored web client that exfiltrates your key after derivation is NOT defended against in the current version — an inherent limit of any web-served crypto product without an out-of-band integrity check. We say so plainly (see limitations).
4. Coercive third party
Can: Physically or legally compels you to reveal a vault password — and may want proof you aren’t hiding a second vault.
Defended by: The decoy vault. You reveal the decoy password; the coercer unlocks a functional but separate vault and cannot distinguish it from 'the real one'. Limits are documented below.
Decoy invariants
What keeps the decoy believable
The decoy vault carries the highest-capability adversary, so it has the strictest invariants. Each is enforced in code and covered by tests:
- Constant-shape login. The login endpoint always returns exactly two vault blobs, in randomised order — whether or not the email is registered, whether or not a decoy was set up, and whether or not it was later disabled. Verifier comparison runs in constant time against both rows.
- No surface reveals the vault kind. No header, body field, status code, or URL differs between a real and a decoy session for the same operation. Cross-vault access collapses to a 404 (“not found”), never a 403 — a 403 would confirm the resource exists.
- One-transaction disable. Disabling the decoy deletes every decoy-scoped resource in a single database transaction and resets the decoy row to a deterministic placeholder.
- Manual-only triggers in a decoy session. A decoy session can’t configure automatic (dead-man’s-switch or scheduled) triggers — only owner-initiated ones — so the decoy can never auto-release and expose its own existence.
- Indistinguishable timing. We continuously measure how long the server takes on the real-password path vs the decoy-password path; the medians must stay within 1.5× or the release is blocked. In our benchmarks the ratio is ~1.06×.
Known limitations
The honest caveats
These do not break the current security contract, but you deserve to know them. We’d rather state them than have you discover them.
- 2FA and recovery contacts are shared across vaults. A coercer who unlocks the decoy can see that you have recovery contacts and 2FA — which hints a real vault exists behind it.
- Notification preferences are user-scoped. Quiet-hours and channel settings are visible from either vault. They don’t reveal contents — only rough usage patterns.
- Account existence can be enumerated by login timing. An unknown email is answered faster than a registered one. This confirms whether an address is registered — it does not leak vault contents or the existence of a decoy.
- Storage cleanup after decoy disable is lazy. Orphaned encrypted blobs are swept by a background worker rather than deleted instantly. They’re already unreachable (no row, no envelope); the added surface is zero.
- Client integrity. The web client is served from the same origin as the API, so a malicious or compelled operator could push a client that exfiltrates your key after you derive it. There is no out-of-band integrity check in the current version.
Quick surfaces
Recovery, sharing, and triggers
- Recovery. Shamir secret sharing with a threshold you choose: any k of your n trusted contacts (k ≥ 2) can authorize a reset; fewer cannot, and neither can we. The server only ever stores ciphertext shares; the quorum coordinates out-of-band.
- Sharing. The claim link is single-use, time-bounded, and stored only as a hash. The actual file key never sits in plaintext on the server — your client re-wraps it to the recipient’s public key; their client unwraps it with their private key.
- Triggers. When a release fires, Inktally delivers a sealed envelope to the recipient — not plaintext. For owners who pre-armed escrow, the server briefly holds an encrypted copy of the resource key between arming and delivery, then re-seals it to the recipient's own key. After delivery only the recipient can decrypt; the server retains no useful key material. Manual shares from a living owner follow the same server-blind re-wrap pattern as ordinary sharing above.
Want the cryptographic detail?
The security whitepaper walks through the key hierarchy and primitives, and the security overview is the plain-English summary.