Transparency without asterisks. What we encrypt, what we don't, and who can see what.
Your messages are encrypted on your device before they leave. Only the intended recipient can decrypt them — not us, not our hosting provider, not anyone who intercepts the traffic. We use the server as a mailbox, not as an eye.
Your device generates a completely random cryptographic key pair. The private key never leaves your device unencrypted. So that you can sign in from other devices, your device seals the private key with a key derived from your recovery code — a code shown to you exactly once — and only that sealed copy is stored on our server. We cannot open it. The code never reaches us.
What this means in practice:
Until July 2026, your keys were derived from your username + password. Convenient — but your password reaches our server every time you log in (that's how logging in works). In principle, we — or anyone who compromised our server — could have used it to reconstruct your private key. You should never have to rely on our good intentions, so we redesigned it: keys are now random, and the only thing that unlocks them is a code we never see. Existing accounts keep the old scheme until they accept the in-app upgrade — when the app offers it, we recommend saying yes.
Source: assets/js/crypto.js → generateIdentityKeyPair(), generateRecoveryCode(), wrapSecretKey() · api/keys.php → set_recovery (stores only sealed material). We use TweetNaCl, an audited cryptography library by independent researchers.
For each person in the room, your computer/phone separately encrypts the message using that recipient's public key. The server receives N locked boxes — it has the key to none of them.
Each "box" is encrypted with XSalsa20-Poly1305 — an algorithm that guarantees if anyone changes even one byte, decryption will fail. A message cannot be tampered with without the recipient noticing.
Source: assets/js/crypto.js → encryptMessage(), decryptMessage(). A fresh random nonce per message.
Your client asks the server periodically: "anything new for me?". If yes, the server returns the encrypted messages addressed to you, and your computer/phone unlocks them locally using your secret key.
Once you've received them, you send a "got it" ack to the server — and the server immediately deletes the encrypted copy. We don't keep them around.
Source: api/poll.php (receive) · api/ack.php (delete).
Unlike WhatsApp/Signal/Telegram, ChatNorris does not store messages for later pickup. If the recipient isn't actively in the room, the sender is notified ("X was not in the room — tap to resend").
This is deliberate: it dramatically shrinks the time window in which an encrypted message exists on the server. If you weren't present, the message isn't sent — so there's nothing to steal from us, even if we (or someone of us) wanted to do that.
Because a message that was never sent can never leak. Our model favors presence over convenience — a deliberate privacy choice.
On the server we store:
An earlier version of this page said we keep "only your latest IP". That was incomplete, and we'd rather correct ourselves than round in our favor. Like almost any web service, our operational logs record the IP a request came from. Here's exactly what exists and how long it lives — everything below is pruned automatically by a scheduled job:
Verify: cron/cleanup.php — every retention window above is enforced there, in plain sight.
We never store:
Calls use WebRTC peer-to-peer — audio travels directly between devices, never through our server. The WebRTC protocol encrypts audio with DTLS-SRTP — an industry standard.
Our server only mediates call setup (who is calling whom, in which room) — not the call content.
Honestly: an admin with server access can see:
An admin cannot see:
We keep working on making the app safer. The changes below address two scenarios we cared about: a malicious member inside a room, and a device that was lost or stolen while unlocked. All of these landed as silent upgrades — you didn't have to re-login or change anything.
The biggest change on this page: encryption keys are now random, unlocked only by a recovery code that never reaches us — see section 1. Before, they were derived from your password, which does reach us at login. We closed that gap because a promise you have to take on trust is not a guarantee. New accounts get this automatically; existing accounts are offered the upgrade at login.
If a contact's encryption key changes, you now see a persistent warning banner in the conversation — in your language — until you either verify their Safety Number or dismiss it. It also catches changes that happened while you were offline. Previously this was a brief English-only toast that was easy to miss. A key change can be a legitimate upgrade (see above) or an impersonation attempt; now you actually get to decide.
Blocking someone now cuts off every path they had to reach you: messages, calls (including through shared rooms), ring notifications, pokes, and screenshot alerts — and they can no longer re-join a private room with you. Previously some of these paths were left open. Bans and moderation actions now also take effect immediately, on live sessions, not at next login.
When you send a photo, your device now scrubs the metadata before encrypting: GPS, camera serial number, capture timestamp, comments. Only the visible pixels remain. So if the recipient screenshots or forwards the photo elsewhere, your location does not travel with it.
Certain image formats (like SVG) can technically contain executable code. We no longer render those inline in the chat — if someone sends such a file, it shows up as a plain attachment with a download button. No automatic code execution from a message.
If you leave your device unlocked and walk away, the app locks itself after 10 minutes of inactivity. It wipes the key from memory and asks for your password again. A forgotten device no longer gives the finder access to your conversations.
When you tap Log Out, all cryptographic material is now wiped from your device — not just the session. So if you lose your device after logging out, there's no encrypted key left for anyone to try to crack offline.
When you open an image or video in full screen, the file is rendered inside an isolated frame (a sandbox) that has no access to the app's data. Defense in depth: even if something got past the earlier checks, it cannot read your keys.
Verify: assets/js/chat.js → stripImageMetadata(), SAFE_INLINE_IMAGE_MIMES, lockSession(), openMediaViewer(). Also assets/js/app.js → logout().
Your identity key is long-lived. If someone obtains your private key or your recovery code in the future and has saved old encrypted traffic, they could decrypt it retroactively. Apps like Signal use ephemeral per-message keys that are destroyed right after — we don't, yet. (This got better with the July 2026 key change: your key can no longer be derived from your password, so a leaked password alone no longer exposes old traffic. But it is still not per-message forward secrecy, and we won't pretend otherwise.)
This is a deliberate product choice: an admin needs to help users who forget a room's password. This is not your account password (that's hashed). It's only the optional extra password some rooms have. If this bothers you, don't set a room password.
…and we can't. If you wanted "search past messages", we couldn't offer it without breaking the model described here. It's a feature that's missing on purpose.
Nobody can. When a message is decrypted on the recipient's screen, they can screenshot it, photograph the screen with another device, or copy the text. We try to detect screenshots on iOS via a heuristic (the shutter animation hides the screen briefly) and notify the other party, but it's best-effort, not a guarantee.
The fastest way: open your browser's DevTools → Network tab → send a message → look at the POST request to /api/send.php. You'll see only encrypted bytes (Base64).
For deeper verification, interested parties can read the source code — especially:
assets/js/crypto.js — all the client-side encryption logic.api/send.php, api/poll.php, api/ack.php — how the server handles delivery.includes/auth.php → register() — how the password is stored (Argon2id).TweetNaCl, the cryptography library we use, has had an independent audit and is widely accepted in the cryptography community.
If you find something that concerns you — a bug, a suspected vulnerability, an ambiguity on this page — let us know. We don't pretend to be perfect. We'd rather hear about it.