Security overview
Zentalk implements the Signal Protocol (X3DH + Double Ratchet) with a post-quantum hybrid handshake (enabled by default) and layered metadata protection. Contacts are authenticated out of band with safety numbers; a public key transparency log is being built, and client-side verification against it is not yet enabled. This page maps the full stack; each layer has a dedicated page with wire-level detail.
Protocol stack
Section titled “Protocol stack”| Layer | Mechanism | Primitives | Details |
|---|---|---|---|
| Key agreement | X3DH (Extended Triple Diffie-Hellman) | X25519, Ed25519, HKDF-SHA256 | X3DH key agreement |
| Post-quantum hardening | Hybrid X3DH | X25519 + ML-KEM-768 (Kyber-768), HKDF combiner | Post-quantum hybrid encryption |
| Session encryption | Double Ratchet | X25519 DH ratchet, HMAC-SHA256 chains, AES-256-GCM | Double Ratchet |
| Groups and channels | Epoch-based symmetric key ratchet | HKDF-SHA256, AES-256-GCM | Group and channel encryption |
| Metadata protection | Sealed sender envelopes, address hashing, bucket routing | X25519 ECDH, HKDF-SHA256, AES-256-GCM, HMAC-SHA256 | Sealed sender |
| User verification | Safety numbers and SAS codes — the check that authenticates a contact today | Iterated SHA-512, HKDF-SHA256 | Contact verification |
| Key transparency | Append-only Merkle log. Roots are unsigned and client verification is not enabled in the closed Beta | SHA-256 Merkle tree, inclusion proofs | Key Transparency Log |
The layer pages build on one another; the table lists them in recommended reading order.
Two interoperable implementations exist:
- TypeScript (web client):
zentalk/src/services/e2ee/— the reference for everything documented in this section. - Go (mesh node):
zentalk-node/pkg/protocol/x3dh.goandratchet.go— parity implementation sharing the same protocol structure and HKDF info strings.
For programmatic access to the APIs referenced throughout, see the REST API and the TypeScript SDK.
Core primitives
Section titled “Core primitives”| Purpose | Primitive | Parameters |
|---|---|---|
| Diffie-Hellman | X25519 | 32-byte keys |
| Signatures | Ed25519 | 32-byte public keys, 64-byte signatures |
| Post-quantum KEM | ML-KEM-768 (Kyber-768, NIST FIPS 203) | 1184-byte public key, 1088-byte ciphertext, 32-byte shared secret |
| Authenticated encryption | AES-256-GCM | 32-byte key, 12-byte IV, 16-byte tag |
| Key derivation | HKDF-SHA256 (RFC 5869) | Protocol-specific salts and info strings |
| Chain advancement / MACs | HMAC-SHA256 | 32-byte output |
What is end-to-end encrypted
Section titled “What is end-to-end encrypted”Encrypted with keys the server never holds:
- Direct message content (Double Ratchet, AES-256-GCM)
- Group and channel message content (epoch keys distributed over pairwise E2EE)
- Group key distribution messages
- Sender identity inside sealed-sender envelopes (X25519-sealed sender certificate)
What the server and relays can observe
Section titled “What the server and relays can observe”Zentalk’s metadata posture is summarized below; the relay-visibility details are documented in Sealed sender. In summary:
| Metadata | Current state |
|---|---|
| Sender address (sealed envelopes) | Hidden — encrypted inside the sender certificate |
| Recipient address (sealed envelopes) | Hashed (32 bytes) in v2. In v3 the 32-byte slot carries per-envelope randomness and routing uses a 2-byte bucket id — but at closed-Beta population a bucket usually identifies at most one recipient, so it is not an anonymity set (details) |
| Mesh routing addresses | Salted SHA-256 hashes (zentalk-mesh-v1 salt) — obfuscation, not anonymity: anyone who knows an address can recompute its hash |
| Payload size | Exact. Padding helpers exist on both the API and the client but have no callers, so no size bucketing is in effect on any message path |
| Timing | Exact |
| Presence, typing indicators, read receipts | Encrypted variants (encrypted_presence_update, encrypted_typing, encrypted_read_receipt) are implemented; mesh-only deployments enforce them by blocking the plaintext variants. Plaintext is visible to backend and relays only on non-mesh-only deployments |
| X3DH initial messages | Contain senderAddress in plaintext for new sessions only; established sessions do not leak it |
Threat model
Section titled “Threat model”The project maintains a full threat model defining nine adversary classes and six trust boundaries. In summary:
- Passive network observers (A1) — mitigated by TLS 1.3 and E2EE. Message sizes are not padded, so size and timing remain observable. A dedicated “store now, decrypt later” sub-class (A1*) is addressed by the hybrid post-quantum handshake.
- Active MITM (A2) — mitigated by signed-prekey verification in X3DH, local TOFU pinning of a peer’s identity key, and out-of-band SAS verification. The Key Transparency Log is not a mitigation in the closed Beta: roots are unsigned and the client check is disabled in every shipped build, so it is a residual, not coverage. Out-of-band verification is what closes this class today.
- Malicious mesh validators (A3) — content is E2EE; membership tokens fail closed; replication factor of 3 or more tolerates Byzantine drop. Residual: routing tags reveal partial social-graph information, and v3 bucketing does not remove it at this deployment’s size (why).
- Compromised relay (A4) — sealed sender hides sender identity; sender certificates expire after 1 hour, limiting the impact of a compromise.
- Malicious clients (A5) — DPoP-bound refresh tokens, per-endpoint rate limits, and Double Ratchet replay protection (persistent IV tracking).
- Compelled access and insiders (A6–A8) — the operator can only produce ciphertext and hashed identifiers; message content keys never leave user devices.
- Device compromise (A9) — key material is wrapped with AES-256-GCM in secure browser storage; an unlocked-device adversary is outside the threat envelope, as is the case for messaging applications generally.
Where keys live
Section titled “Where keys live”All long-term and session key material is generated client-side and stored in IndexedDB via a secure key storage layer. The server stores only public key bundles (identity keys, signed prekeys, one-time prekeys) published through the keys API — see X3DH key agreement for the publish/claim flow and Authentication for how requests are authorized.
