Back to home

How ChatNorris works

Transparency without asterisks. What we encrypt, what we don't, and who can see what.

TL;DR

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.

1. Your keys

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:

┌──────────────────────────────────────────────────────────────┐ │ │ │ Random generator (on YOUR device) │ │ │ │ │ ▼ │ │ ┌──────────────────────────┐ │ │ │ Private key (32 bytes) │ ← never leaves unencrypted │ │ └──────────────────────────┘ │ │ │ │ │ │ ▼ (X25519) ▼ sealed under your recovery code │ │ ┌────────────┐ ┌───────────────────────────┐ │ │ │ Public key │ │ Sealed box → the server │ │ │ │ → friends │ │ stores it but CANNOT │ │ │ │ see this │ │ open it │ │ │ └────────────┘ └───────────────────────────┘ │ │ │ │ Recovery code: XXXX-XXXX-… → stays with YOU, never sent │ └──────────────────────────────────────────────────────────────┘
We changed this — here's the honest why

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.

2. How a message is sent

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.

┌─────────────┐ ┌─────────────┐ │ YOU │ │ Server │ │ │ │ │ │ "Hi Niki" │ │ │ │ │ │ │ │ │ ▼ │ │ │ │ ┌─────────┐ │ nacl.box(msg, nonce, │ │ │ │ Box 1 │ │ Niki.pubKey, your.secKey) │ │ │ │ for Niki│─┼───────────────────────────────►│ ┌─────────┐ │ │ └─────────┘ │ │ │ ✉ #1 │ │ │ ┌─────────┐ │ │ │ for Niki│ │ │ │ Box 2 │ │ │ └─────────┘ │ │ │ for Anna│─┼───────────────────────────────►│ ┌─────────┐ │ │ └─────────┘ │ │ │ ✉ #2 │ │ │ │ │ │ for Anna│ │ │ │ │ └─────────┘ │ │ │ │ │ │ │ │ ❌ The server │ │ │ │ CANNOT see │ │ │ │ the content. │ └─────────────┘ └─────────────┘

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.

3. How a message reaches you

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.

Recipient Server Sender │ │ │ │ "anything new?" │ │ │──────────────────────►│ │ │ │ │ │ ✉ encrypted │ │ │◄──────────────────────│ │ │ │ │ │ feed to NaCl │ │ │ ─► "Hi Niki" │ │ │ │ │ │ "got it" │ │ │──────────────────────►│ │ │ │ ✉ DELETED │ │ │ ❌ Gone for good │ │ │ │

Source: api/poll.php (receive) · api/ack.php (delete).

4. Delivery policy — "must be in the room"

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.

Why this way?

Because a message that was never sent can never leak. Our model favors presence over convenience — a deliberate privacy choice.

5. What we as a provider can see

On the server we store:

IP addresses — the honest version

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:

6. Voice calls

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.

7. Admin access — what an admin can see

Honestly: an admin with server access can see:

An admin cannot see:

8. What we improved recently

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.

🔑 Keys the server can no longer derive (July 2026)

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.

⚠️ Key changes now raise a real, persistent warning

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 now blocks everywhere

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.

🛡️ Photo metadata is now stripped automatically

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.

🚫 Dangerous file formats are blocked from inline preview

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.

🔒 Auto-lock after inactivity

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.

🧹 Stricter logout

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.

🧪 Media preview runs in a sandbox

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().

9. What we DON'T claim

We don't have forward secrecy

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.)

Room passwords are stored in cleartext

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.

We don't search your message content

…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.

We can't prevent screenshots

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.

10. How to verify

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:

TweetNaCl, the cryptography library we use, has had an independent audit and is widely accepted in the cryptography community.

11. Contact us

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.