Cryptography in detail
Ground rule: there is no home-made cipher code. All primitives come from established, vetted libraries.
Methods in use
Section titled “Methods in use”| Purpose | Method | Parameters |
|---|---|---|
| Asymmetric key / DH | X25519 | Curve25519 |
| Key derivation | HKDF-SHA256 | 32-byte outputs, dedicated domain labels |
| Symmetric AEAD | AES-256-GCM | 96-bit nonce, 128-bit tag |
| Chain keys in the ratchet | HMAC-SHA256 | Signal constants |
| Post-quantum KEM | ML-KEM-1024 (FIPS 203) | category 5, 32-byte secret |
| Password-authenticated pairing | J-PAKE (RFC 8236) | NIST 3072 group |
| Password KDF, pairing | Argon2id | 32 MB, 3 passes |
| Password KDF, backup | Argon2id | 64 MB, 4 passes, parameters in the file header |
| Second factor | TOTP (RFC 6238) or biometrics | 6 digits, 30 s, ±1 window |
| Database at rest | SQLCipher (AES-256) | passphrase only in the keystore |
| Key wrapping | AES-256-GCM in the Android keystore | StrongBox preferred, TEE as fallback |
Key hierarchy
Section titled “Key hierarchy”Every device owns an X25519 identity key pair. The private part is stored encrypted with AES-256-GCM; the wrapping key lives in the hardware keystore (StrongBox preferred).
Derived from it:
- a static session key per contact (ECDH → HKDF) for bootstrap and control traffic as well as the post-quantum exchange,
- the initial ratchet root key per contact, into which the ML-KEM secret is mixed,
- an ephemeral message key per message (ratchet), or a one-time subkey on the static paths.
The database passphrase and the attachment key are independent random keys, likewise wrapped.
The static session key carries the most frequent messages there are – presence, confirmations, profile details, the post-quantum exchange and all call signalling. Its Diffie-Hellman part is therefore cached process-wide, capped at 256 entries and cleared on an identity change. That puts no additional secret into memory: the value depends solely on your own private identity key, which is in the process anyway, and on the other side’s public key. If they change theirs, a new entry is created – a stale one is never used.
Exactly one master secret stays hardware-wrapped; everything else is derived from it via HKDF and wrapped in software. On StrongBox devices that saves several seconds of cold start without giving up the hardware binding: without the secure element of that device the master secret cannot be unwrapped, and without it none of the others can either.
Double Ratchet for one-to-one messages
Section titled “Double Ratchet for one-to-one messages”Signal-conformant: a DH ratchet on every change of direction, a fresh message key per message, retention of skipped keys with a hard cap (200). The result is forward secrecy and post-compromise security.
The ratchet state is saved only after successful authentication; access to it is serialised. Retained message keys expire after seven days – previously they sat there indefinitely; the sweep runs at most hourly so that the check costs nothing.
Self-healing
Section titled “Self-healing”Sessions can fall out of step: one side reinstalls, a backup is restored, a key changes. Three mechanisms handle that:
- On receipt, bootstrap candidates are checked for the first sending chain of a session even when a session already exists. That is safe because every candidate has to decrypt authenticated – strangers cannot slip a session in.
- On the sending side, after many unconfirmed attempts a throttled rebuild happens.
- If the sender is more than 200 steps ahead, the chain is fast-forwarded in a controlled way – capped at 25,000 steps so an absurd figure cannot create computational load.
A second case that occurred for real: every delivery attempt used to re-encrypt and consume a fresh message key. After hours without a connection the sender was hundreds of steps ahead of the recipient, whose skip limit then made every further message unreadable. Today retries send the same ciphertext again.
X3DH-lite: forward secrecy from the first message
Section titled “X3DH-lite: forward secrecy from the first message”Without a prekey server the first root key of a session would be derived deterministically from the two identities – and therefore be constant for each pair of contacts until the ratchet takes a step. Anyone who later obtained a private identity key could retroactively decrypt the first messages.
TalX solves this without a server: the sender bootstraps the ratchet against a rotating prekey of the recipient, distributed through their profile. Rotation happens every 30 days; the previous prekey is kept for one more round so that messages in flight do not fall into a void – together, a grace window of roughly 60 days. After that the private part is deleted. This gives the very first message forward secrecy.
Hybrid post-quantum exchange
Section titled “Hybrid post-quantum exchange”After connecting, both sides negotiate an ML-KEM-1024 secret. Who is initiator and who is responder follows deterministically from the fingerprints; the encapsulation is idempotent.
The secret is mixed into the root key in addition to the classical X25519 part (HKDF over both, with a label of its own). The classical method therefore stays in place – the post-quantum share comes on top and cannot make anything worse.
An important design point: the exchange itself runs over the static session key, not over the ratchet it is only just establishing. Otherwise a chicken-and-egg problem arises – which is exactly what happened and is documented as finding F-15.
Static paths and one-time subkeys
Section titled “Static paths and one-time subkeys”Control traffic (presence, profile, synchronisation, confirmations, call signals, the
post-quantum exchange, group fallbacks), attachments at rest and the WebRTC connection
details each use a one-time subkey per message, file or blob:
HKDF(static key, 16-byte salt, label), with the salt carried in the header.
Every combination of key and nonce is therefore independent, and a nonce repetition would be inconsequential.
Groups: sender keys
Section titled “Groups: sender keys”Every member has a chain of their own, distributed over the one-to-one ratchet. Rotation happens after 100 of your own messages, additionally on a time basis – a quiet group must not leave its chain standing for months – and mandatorily on every membership change. After a rotation, 16 keys of the old chain remain valid so that reordered messages already in flight are not lost. Skipped group keys are likewise capped at 200.
Safety number
Section titled “Safety number”A comparable sequence of digits is formed from both public identities. It is identical regardless of device – an earlier bug, where it came out differently depending on the language setting and thereby raised a false alarm, has been fixed.
What goes on the wire
Section titled “What goes on the wire”Every chunk carries its sender’s protocol version – currently 13. Since protocol version
13 the chunk contains a marker that changes per transfer instead of the sender
identifier: HMAC(shared key, transfer identifier). Only the intended recipient can
attribute it; two messages from the same sender are not recognisable as related from the
outside.
That protects against third parties – a stranger’s device within radio range, someone reading the payload at the relay. Against the relay operator it does not help; they tie every delivery to the registered connection.
