Comprehensive adversarial audit covering network, application, metadata, browser, infrastructure, and cryptographic layers. All fixable issues are addressed in code. Residual infrastructure limitations are documented in the in-app security advisory.
security
sender identity removed from message packets
Previously every encrypted_message packet included a cleartext sender_id field visible to relays and the coordinator. Recipients now identify the sender from the ratchet header's ephemeral DH public key. A ratchet_pub_to_contact map provides O(1) lookup for known contacts; unknown DH keys fall back to O(n) speculative decryption across all contacts. Sender identity is only present inside the AES-256-GCM ciphertext that only the recipient can open.
Before: { type:'encrypted_message', sender_id:'abc123', data:{...} }
After: { type:'encrypted_message', data:{header,iv,ciphertext} }
security
file encryption key moved inside ratchet channel
File transfers previously embedded file_key, name, mime, size, and total in cleartext outer packets, exposing metadata to relays. Now all file metadata travels in a file_start message inside the ratchet-encrypted channel. Outer file_chunk packets carry only routing data and the encrypted chunk payload. Chunks arriving before file_start are queued until the metadata arrives.
Before: file_chunk → { file_key, name, mime, size, total, sender_id, ... }
After: encrypted_message → { type:'file_start', file_key, name, ... } ← ratchet only
file_chunk → { file_id, idx, iv, ciphertext, hash } ← no metadata
security
sender_id removed from group message broadcasts
Group packets (group_msg, group_member_left, group_invite, and all other group types) previously carried a cleartext sender_id. The sender field is now read from the decrypted inner AES-256-GCM payload (payload.from), which is encrypted under the group key that only members hold.
security
IV replay protection on relay nodes
Relay nodes now maintain a seen_ivs set. Any packet whose AES-GCM IV was processed within the last 60 seconds is rejected with a replay error. This blocks replay attacks where an attacker re-submits captured onion packets.
Before: relay re-processes duplicate IV → packet delivered again
After: relay checks seen_ivs → duplicate thrown, not forwarded
security
relay registration restricted to localhost + shared secret
The POST /api/relay/register endpoint previously accepted registrations from any host. It now requires both: (1) the request originates from 127.0.0.1 / ::1, and (2) a RELAY_SECRET that is generated at startup and passed only to forked relay child processes. External relay registration is impossible.
security
/api/deliver restricted to localhost
The delivery endpoint that injects packets into WebSocket sessions was reachable from the public internet. It now checks req.socket.remoteAddress and returns 403 for any non-loopback caller. Only relay processes running on the same machine can deliver packets.
security
direct_deliver WebSocket handler removed
A direct_deliver message type on the WebSocket allowed any connected client to bypass onion routing entirely and inject a packet directly into any other client's session. This handler is removed. All packet delivery now goes through relay nodes only.
security
WebSocket payload size cap
The WebSocket server now enforces a 128 KB maxPayload limit. Oversized frames are rejected before processing, preventing memory exhaustion attacks via malformed giant packets.
security
CSP connect-src locked to same origin
The Content-Security-Policy previously allowed WebSocket connections to any host (ws: wss:). It is now restricted to 'self', preventing exfiltration via injected scripts that open WebSockets to attacker-controlled servers.
security
HSTS, COOP, and COEP headers added
Strict-Transport-Security (1 year, includeSubDomains) forces HTTPS on all future visits. Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp isolate the browsing context, enabling SharedArrayBuffer and mitigating Spectre-style cross-origin leaks.
fix
autoauth namespace fallback was static
autoauth_module.get_ns() fell back to the static string 'd' when no session namespace existed. All users who loaded without a namespace shared the same IndexedDB key prefix, meaning one user's encrypted identity blob could be loaded by another user on the same device. The fallback now generates a fresh crypto.randomUUID() per session.
new
in-app security advisory
A one-per-session modal now surfaces on first load, informing users of limitations that cannot be fixed in code: relay co-location, browser fingerprinting, DNS leaks, WebRTC IP exposure, and cover traffic statistical distinguishability. The advisory does not appear again until the tab is closed and reopened.