Skip to main content
Signal Android implements the Signal Protocol through a combination of the libsignal-android library and Android-specific storage and session management code. This page explains the cryptographic building blocks, where they live in the codebase, and how they interact.

X3DH: session establishment

Before two users can exchange encrypted messages for the first time, they must establish a shared secret without ever communicating directly. Signal uses the Extended Triple Diffie-Hellman (X3DH) key agreement protocol.

Key bundles

Each account publishes a prekey bundle to the Signal server containing:

The X3DH handshake

When Alice sends the first message to Bob, she:
  1. Fetches Bob’s prekey bundle from the server.
  2. Computes four Diffie-Hellman operations using her ephemeral key pair, Bob’s identity key, Bob’s signed prekey, and Bob’s one-time prekey.
  3. Derives a shared master secret by feeding the concatenated DH outputs through HKDF.
  4. Discards her ephemeral private key.
Bob can reconstruct the same master secret when he receives Alice’s initial message, which carries her identity key and ephemeral public key. After this point both parties initialize a Double Ratchet session using the derived secret. The Android implementation delegates all X3DH math to libsignal. The storage layer is in crypto/storage/:

Double Ratchet Algorithm

Once a session is established via X3DH, all subsequent messages are protected by the Double Ratchet Algorithm. The ratchet provides both forward secrecy (compromising today’s key does not reveal past messages) and break-in recovery (future messages recover their secrecy after a compromise).

The two ratchets

Symmetric-key ratchet

A KDF chain that advances with every message. Each step produces a new message key and deletes the previous one. An attacker who obtains one message key cannot derive prior keys.

Diffie-Hellman ratchet

Each reply includes a new DH ratchet public key. Both parties advance the DH ratchet on receiving a new key, deriving a fresh chain key that reseeds the symmetric ratchet.

Message key derivation

Skipped message keys are stored temporarily so out-of-order messages can still be decrypted. After a configurable threshold they are discarded. All ratchet state is serialized into a SessionRecord protobuf and stored in the sessions table (see Database).

Pre-keys

Pre-keys allow asynchronous session establishment: Bob does not need to be online for Alice to send him a first message.

One-time EC prekeys

Generated in batches of 100 by PreKeyUtil.generateAndStoreOneTimeEcPreKeys():
Once a one-time prekey is consumed by an incoming session it is deleted from OneTimePreKeyTable. PreKeysSyncJob periodically replenishes the server’s supply.

Signed prekeys

A signed prekey is a medium-term EC key pair signed by the account’s identity key. It is rotated approximately every 48 hours via PreKeysSyncJob. The previous signed prekey is retained for a 30-day archive window (ARCHIVE_AGE) so that in-flight messages encrypted to the old key can still be decrypted.

Kyber (post-quantum) prekeys

Signal added post-quantum resistance with CRYSTALS-Kyber KEM prekeys, stored in KyberPreKeyTable:
The LAST_RESORT flag distinguishes one-time Kyber prekeys (consumed on use) from the last-resort key (retained as fallback when one-time keys run out). SignalKyberPreKeyStore handles all read/write operations through this table.

Sender keys for group messaging

Encrypting a message separately for every group member would be expensive for large groups. Signal solves this with sender keys. When a device first sends to a group, it generates a sender key — a symmetric ratchet state associated with a DistributionId. The sender key is distributed to each group member encrypted under their individual Signal Protocol session. Subsequent messages to that group are encrypted once using the sender key ratchet and broadcast to all members. Key rotation is explicit. SenderKeyUtil.rotateOurKey() clears the stored sender key state, forcing re-distribution to all members on the next send:
Two tables back sender keys: SenderKeyTable stores your own sender key state; SenderKeySharedTable tracks which recipients have already received your sender key for a given distribution.

Sealed sender

Sealed sender hides the sender’s identity from the Signal server. Normally, even with end-to-end encryption the server sees who is sending to whom. Sealed sender wraps the ciphertext in an additional encryption layer so the server only learns the recipient, not the sender.

How it works

  1. The sender obtains a sender certificate from the Signal server. The certificate binds the sender’s ACI (account identifier) to their identity key and is signed by Signal’s server key. The certificate may optionally omit the sender’s phone number depending on their phone number privacy settings.
  2. The sender certificate and the sealed message ciphertext are bundled together and encrypted to the recipient’s profile key-derived unidentified access key.
  3. The server forwards the sealed message to the recipient without being able to read the sender certificate.
  4. The recipient decrypts using their unidentified access key, then validates the sender certificate to learn who sent the message.
CertificateType controls whether the certificate includes the sender’s phone number (ACI_AND_E164) or only the ACI (ACI_ONLY), determined by the sender’s phone number privacy setting.

Session management in Android

All Signal Protocol session state is persisted in the sessions table via SessionTable:
Each row holds a serialized SessionRecord protobuf keyed by (account_id, address, device_id). This supports multi-device: one account can have sessions with each of a recipient’s linked devices simultaneously. TextSecureSessionStore wraps SessionTable and is accessed through SignalServiceDataStoreImpl, which provides the full SignalProtocolStore interface expected by libsignal.
Session access is guarded by ReentrantSessionLock — a JVM-level reentrant lock — to serialize concurrent session operations across threads. Always acquire this lock when reading or modifying session state outside of libsignal’s normal message processing path.

Safety numbers and fingerprint verification

Safety numbers let users verify they share the same cryptographic keys out-of-band, protecting against a compromised server performing a man-in-the-middle attack. Signal computes safety numbers by combining both parties’ identity keys and phone numbers (or ACIs) through a series of SHA-512 iterations and encoding the result as a 60-digit decimal string, displayed in five-digit groups. Users compare this string by reading it aloud or scanning each other’s QR codes. Identity trust decisions live in SignalBaseIdentityKeyStore and IdentityTable. When an identity key changes (e.g., the remote party reinstalled Signal), the app marks the session as needing verification and surfaces a “safety number changed” notification.