Peer-to-peer encrypted messaging that leaves no trace. No accounts. No logs. No server memory.
Messages travel directly between browsers over WebRTC, encrypted with a hybrid post-quantum + classical key that the server never possesses.
Rooms self-destruct after 15 minutes. Close the tab and it's gone.
Why nullroom?
Most encrypted messengers still require a phone number, an app install, and an account — creating a permanent identity tied to your conversations. nullroom takes a different approach:
| | Traditional E2EE Apps | nullroom |
|---|---|---|
| Identity | Phone number or email required | None — no accounts, ever |
| Installation | App store download | Browser tab — nothing to install |
| Persistence | Messages stored (encrypted) on servers | Messages exist only in browser memory |
| Metadata | Server logs connections, contacts, timestamps | Server logs disabled; Redis-only ephemeral state |
| Post-quantum | Rarely | ML-KEM-768 hybrid key exchange by default |
| | Account persists, contact graph remains | Close the tab. No trace on the device or server |
After the conversation
nullroom is built for conversations that shouldn't leave a footprint: sharing production secrets, sensitive legal consultations, journalist-source communication, or any exchange where the existence of the conversation is as sensitive as its content.
Security Architecture
Hybrid Post-Quantum Encryption
Every session is protected by two independent layers of encryption, fused into a single hybrid key:
Classical Layer Quantum Layer
───────────── ─────────────
AES-GCM-256 key ML-KEM-768 (FIPS 203)
generated in browser key exchange over DataChannel
lives in URL fragment (#) 1184-byte public key
never sent to server 1088-byte ciphertext
32-byte shared secret
│ │
└──────────┐ ┌────────────────┘
▼ ▼
HKDF-SHA-256 derivation
salt = classical key
ikm = quantum secret
info = "nullroom-hybrid-v1"
│
▼
Hybrid Session Key (AES-GCM-256)
══════════════════════════════════
Used for all message encryption
Neither key alone is sufficient. An attacker would need to compromise both the URL fragment and break the lattice-based ML-KEM scheme — which is designed to resist quantum computers (NIST Security Level 3).
The post-quantum upgrade follows a 3-message protocol over the WebRTC data channel with mutual HMAC-SHA-256 confirmation using role-specific labels (nullroom-pq-confirm-initiator / nullroom-pq-confirm-responder), preventing reflection and man-in-the-middle attacks.
Zero-Knowledge Signaling
The server is a dumb relay. It never possesses encryption keys and cannot decrypt any messages:
The encryption key is generated client-side via the Web Crypto API and placed in the URL fragment (#), which browsers never send to servers.
ActionCable relays WebRTC signaling payloads (SDP offers/answers, ICE candidates) without parsing or storing them.
The quantum key exchange happens directly over the peer-to-peer data channel — the server is not involved.
Production logging is disabled (Logger.new(nil) + :fatal log level). The no-referrer meta tag prevents URL leakage to third parties.
Ephemeral State
No database writes for room or message data. The only server-side state is volatile Redis:
| Key | TTL | Purpose |
|---|---|---|
| room:<uuid> | 15 min | Room existence marker |
| room:<uuid>:count | 16 min | Participant counter (max 2) |
Both keys auto-expire. When a peer disconnects, the other peer's UI is immediately scrubbed and the room can optionally be destroyed in Redis on the spot.
Verifiability
nullroom is built deterministically. Every push to main produces a cryptographically signed attestation via GitHub Actions — proving the code you receive matches the code in this repository.
In-browser audit: Look for the small dot in the bottom-left corner of any page. Click it to see how many loaded modules match the build manifest, along with a direct link to the GitHub Attestation for the running commit. This check uses zero network calls — it compares data already in your browser.
For the full technical explanation — why the build is deterministic, how to reproduce it locally, and what's attested — see VERIFICATION.md.
How It Works
1. Create a room
Click "Create Secure Room" → browser sends POST /rooms → server creates a UUID in Redis and returns TURN credentials → browser generates an AES-GCM-256 key locally → redirects to /rooms/<uuid>#<key>.
The server knows the room exists. It never knows the key.
2. Invite your peer
Three ways to share the room — pick the one that fits your threat model:
| Method | How | Security |
|---|---|---|
| Direct Link | Copy the full URL (with #key) and send via a trusted channel | Key travels with the link |
| QR Code | Peer scans the on-screen QR code in person | Air-gapped — key never touches the network |
| 4-Word Phrase | Read a phrase like neon-zebra-piano-rocket over a voice call | PBKDF2-derived AES key (100K iterations) encrypts the room URL; SHA-256 of the phrase serves as a one-time Redis lookup key; blob auto-deletes after retrieval or 3 minutes |
3. Connect and upgrade
Once both peers are in the room:
WebRTC signaling — SDP offer/answer and ICE candidates relay through ActionCable
Data channel opens — direct peer-to-peer connection established
Post-quantum upgrade — ML-KEM-768 key exchange runs automatically over the data channel
Mutual confirmation — both peers verify role-specific HMACs before activating the hybrid key
Status transitions: Signaling… → Upgrading… → Secure P2P
4. Chat and transfer files
Every message is encrypted with the hybrid key (AES-GCM-256, random 12-byte IV per message) before leaving the browser.
File transfers run over a dedicated nullroom-files data channel with per-chunk encryption (64 KB chunks, backpressure control at 16 MB buffer). Size limits are connection-aware: 1 GB for direct P2P, 100 MB when relayed through TURN. The authorization gate sees only the file size — never the filename, connection type, or content.
Messages and files travel directly between browsers — the server is not in the data path.
5. Room termination
When either peer closes the tab:
The remaining peer's message history is immediately scrubbed from the DOM.
Input is disabled and a termination modal is shown.
The Redis room key auto-expires (or is deleted immediately if DESTROY_ROOM_ON_PEER_LEAVE is enabled).
Refreshing the URL shows "Invalid room link." There is nothing to recover.
Features
| Feature | Details |
|---|---|
| E2EE Messaging | AES-GCM-256 via Web Crypto API, per-message random IV |
| Post-Quantum Key Exchange | ML-KEM-768 (NIST FIPS 203, Security Level 3) with hybrid HKDF derivation |
| P2P File Transfer | Chunked, per-chunk encrypted, dedicated data channel; connection-aware limits (1 GB direct / 100 MB relay) |
| 4-Word Handshake | BIP-39 wordlist, PBKDF2 (100K iterations), one-time encrypted Redis blob (180s TTL) |
| QR Code Sharing | In-person room joining without transmitting the key over the network |
| Anonymous Donations | Monero (XMR) payments with RSA blind signatures — payment identity is cryptographically decoupled from chat sessions |
| Zero-Trace Termination | Peer disconnect triggers immediate UI scrub + optional Redis room destruction |
| NAT Traversal | Self-hosted TURN relay (coturn) with short-lived HMAC credentials over TLS |
| 2-Person Room Lock | Server-enforced capacity — third connection attempts are rejected |
| No-Log Production | Logger disabled, no-referrer policy, filtered parameters |
| CSP Hardened | Per-request nonces, frame-ancestors: none, no unsafe-inline |
By default, rooms remain active until their TTL expires, allowing peers to reconnect via browser history. To destroy rooms immediately when any peer leaves: