How Zitrone works
Zero-knowledge architecture means the server never sees, stores, or logs plaintext message content under any circumstances. Not as a policy. As a property of the design. Here's the whole thing, in plain language.
Signal Protocol
Zitrone uses the Signal Protocol — the X3DH key agreement for establishing a shared secret with someone you've never messaged before, and the Double Ratchet for everything after. It's the most scrutinized messaging cryptography in existence. We didn't invent our own. You should be suspicious of anyone who does.
Every single message is encrypted with its own AES-256-GCM message key, derived by the ratchet and discarded after use. Keys rotate with every message, which gives you forward secrecy: if a key were ever compromised, it unlocks one message, not your history — and your history is probably already gone anyway.
The implementations are established libraries, not homemade: the shipped Android client uses libsignal-android, and the reference crypto for the in-development clients is built on libsodium.
Key generation and storage
Your identity is a Curve25519 keypair generated on your device. The private half never leaves it. There is no account recovery, because there is nothing on our side to recover.
- Identity key — long-term Curve25519 keypair, generated on device, never leaves the device.
- Signed prekey — Curve25519, rotated every 7 days, signed by your identity key.
- One-time prekeys — uploaded in batches of 100 (public keys only), each consumed exactly once.
- Session keys — derived via X3DH on first contact, then advanced by the Double Ratchet.
- Message keys — AES-256-GCM, one per message, discarded after use.
Where your keys live depends on your platform:
- Android (shipping) — everything lives inside the passphrase-sealed vault described in the next section. Biometric unlock, where enabled, wraps the vault key through the Android Keystore System, hardware-backed where the device supports it.
- iOS (in development, not yet released) — planned to use the Secure Enclave and Keychain, biometric-protected.
- Linux (in development, not yet released) — planned to use the Secret Service API (GNOME Keyring on GNOME, KWallet on KDE), with an Argon2id+AES-256-GCM encrypted-file fallback on minimal desktops. Either way the vault is encrypted before it reaches the storage layer.
The vault — and plausible deniability
On Android, everything Zitrone holds — keys, contacts, messages — lives inside a single encrypted vault image, sealed under a passphrase you set at first launch. The unlock key is derived on-device with Argon2id; nothing about the vault ever reaches a server, which is also why the passphrase is unrecoverable: there is no reset and nothing on our side to recover.
The vault image has a fixed number of slots, and an unused slot is uniformly random filler — byte-indistinguishable from a used one. The number of vaults on a device is never stored anywhere. That structure exists for a reason: Zitrone supports a second, plausibly-deniable vault — a fully independent identity behind a different passphrase, with no cryptographic evidence that it exists. There is deliberately no UI for it anywhere in the app; the complete instructions, and the sharp edges you must read before relying on it, live on the how-to page.
Pucker Burn — the duress password
A separate password that, entered at the lock screen, erases everything Zitrone holds on the device — every vault, preferences, keystore entries, caches — and closes the app. Set it under Settings → Account. Two properties are deliberate: the app offers no readback anywhere — it cannot tell you whether a burn password is set, because that answer would itself prove a duress credential exists — and consequently, forgetting it is unrecoverable. A burn is device-local: it does not delete your account on the relay. An armed device is byte-indistinguishable from an unarmed one.
What the server stores (and doesn't)
The server is a relay, not an archive. Store-and-forward only: a message sits on the server as an opaque encrypted blob until your recipient's device confirms delivery, at which point it is deleted immediately. Undelivered messages are purged after 72 hours and the sender is told so.
The complete list of what the server stores:
- Your account ID — a random UUID, not a username
- Your public identity key (Curve25519)
- Your public prekeys (one-time and signed)
- Encrypted message envelopes in transit — blob only
- Encrypted attachment blobs — opaque, held under a token hash (see below)
- Delivery receipts — a hash of the message ID, nothing else
- Account creation timestamp
And what it never stores:
- Plaintext messages, or message content of any kind
- IP addresses
- Device identifiers
- Contact lists
- Read receipts linked to identity
- Any logs that identify users — access logs are disabled outright
Attachments are blind blobs: a file is encrypted with its own key on your device and uploaded to the relay, which stores it under a hash of a token it never sees. The token and the key travel inside the ratchet-encrypted message — the relay holds ciphertext it cannot decrypt and cannot even look up without the token.
Transport is WebSocket over TLS 1.3, with certificate pinning in the shipped Android client. Typing indicators and read receipts travel as encrypted signals too — the server can't even tell whether you've read something. Deleting your account purges everything: prekeys, pending envelopes, the account record. Irreversibly.
Lemon drops — QR dead drops
A message can be sealed into a one-time QR "drop" hosted on the relay instead of sent to a contact. The drop is sealed to one recipient's keys, and the QR is a pointer to it, not a key: anyone can scan the image, but only the device it was sealed for can open it — anyone else gets an explanatory screen and can neither read the message nor destroy it (the fetch is non-destructive on the relay, and burning requires a token that only successful decryption reveals). That is what lets a drop hide in plain sight — printed, posted, passed through a channel you don't trust. Opening consumes it, and a drop unclaimed by its sender-chosen deadline is destroyed. Sealing a drop solves a small proof-of-work deposit, so the relay can't be flooded with them for free. The feature is off by default; the how-to page covers enabling and using it.
Cover traffic
The Android client emits synthetic traffic so that a passive network observer — an ISP, a hostile Wi-Fi network — cannot pick out a real send by its timing. Every real send is paired with a cover frame of the same length, in unpredictable order, with a randomized delay. Two things are deliberate about it. First, it has no UI at all — no toggle, no indicator, nothing in Settings — because a visible switch would itself be a signal. Second, a real message is never delayed, reordered, blocked, or made less durable to produce cover; under any contention, cover yields and the real send proceeds. Honest limits: cover is paired with real sends, so periods when you send nothing are not covered — dead air is not disguised. And it does not hide who talks to whom from the relay itself, which sees envelope routing regardless.
Screenshot protection, by platform
Each platform allows a different level of protection, so we're specific about it. Zitrone ships on Android today — the iOS and Linux clients below are in development and not yet available for download.
- Android (shipping) —
FLAG_SECUREon every screen with message content. This is an OS-level hard block: screenshots and screen recordings come out black. The strongest protection of the three. On top of that, every chat carries anidentity watermark: a faint, tiled lattice of your own identity fingerprint painted behind the messages, so anything photographed off the screen is visibly marked as yours. It is deliberately visible — a deterrent nobody can see deters nobody — and it is always on, with no toggle. It is drawn on your device and reported nowhere: it marks a leak for whoever later looks at the image, not for us. We have no telemetry and no way to know a screenshot happened. - iOS (in development, not yet released) — screen recording is detected in real time and the message list is blurred immediately. Screenshots can't be prevented on iOS — the API only fires after the fact — so we detect them, warn you, and log the event locally on your device.
- Linux (desktop app) (in development, not yet released) — a focus-loss blur overlay: the moment the window loses focus or visibility, the message list is blurred and desaturated, plus the same identity watermark described under Android. This is best-effort: Linux exposes no universal API to hard-block screen capture on either Wayland or X11, and we won't pretend otherwise. Android remains the platform with a true OS-level hard block.
Honesty clause: a compromised device with an OS-level keylogger, or someone pointing a second camera at the screen, is outside any app's power. We don't pretend otherwise.
The network layer: I2P, Tor, clearnet
Network-level metadata — who connected, from where — is the hardest thing for any messenger to hide. Zitrone keeps stored metadata minimal, and on the wire it resolves its transport in a fixed order:
- I2P first, on by default. When the official I2P app is installed, the Android client routes through its local proxy automatically. Without the I2P app the setting is inert — Settings shows an install link when it's missing.
- Tor, opt-in. Routing through Orbot's local SOCKS proxy. Slower, more private than clearnet; requires Orbot.
- Clearnet, as a warned fallback. When neither router is available, the client connects directly — and the connection status in Settings says so explicitly, including that clearnet exposes your IP address. Message content stays end-to-end encrypted on every transport; what the transport changes is who can see that you connected.
Open source and audit history
Everything — the encryption, the server, the shipped Android app and the in-development clients — is open source under AGPL-3.0 at github.com/jackofall1232/zitrone. The AGPL means anyone running a modified Zitrone as a service must publish their changes. No silent forks with weakened crypto.
Audit history: no third-party audits have been conducted yet. We'd rather tell you that plainly than imply otherwise. The audit log updates as audits complete, and researchers who want to conduct one — or who find a vulnerability — should follow the responsible disclosure policy: acknowledgement within 48 hours, fix target within 90 days, good-faith research explicitly authorized.
For the full technical treatment — threat model, transport details, the works — read the security model documentation. For a hands-on walkthrough of every shipped feature, see the how-to guide.