Signal: What the Source Shows About Message and Call Encryption
Table of Contents
I checked four repositories together:
libsignalSignal-DesktopSignal-Androidringrtc
The goal was narrow: verify from source whether Signal really encrypts messages on the client, whether the server lacks the message keys, and whether calls are really encrypted.
Main Findings #
- Messages: yes.
Signal-Desktoproutes outgoing plaintext into@signalapp/libsignal-client.signalEncrypt(...)before network send, andlibsignalderives message keys from local identity/session state stored on the client. - Message server knowledge: no message key in the clear. The server can relay ciphertext and public key material, but the message-encryption path in
libsignaluses local private/session state, not a server-provided symmetric message key. - 1:1 calls: yes.
ringrtcgenerates a local ephemeral secret, exchanges only public keys in signaling, derives SRTP keys locally with Diffie-Hellman + HKDF, then injects those SRTP keys into the call setup. - Group calls: yes, but differently. Group calls connect through an SFU, so transport is client-to-SFU. On top of that,
ringrtcenables an extra frame-encryption layer and ratcheted media keys, so the SFU should not be able to decrypt the actual media frames. - Official client wiring is present.
Signal-Desktopdepends on@signalapp/libsignal-clientand@signalapp/ringrtc, andSignal-Androiddepends onlibsignalandringrtctoo. - Scope limit: this proves the open-source code path, not that a particular released binary was reproduced and matched bit-for-bit.
How to Verify Yourself #
Run these from /home/user1/syslab-nosync/other.
1. Confirm the official clients depend on the same crypto/calling libraries #
rg -n "@signalapp/libsignal-client|@signalapp/ringrtc" Signal-Desktop/package.json
rg -n "implementation\(libs\.libsignal\.android\)|implementation\(libs\.signal\.ringrtc\)" Signal-Android/app/build.gradle.kts
2. Confirm Signal Desktop encrypts messages before sending #
rg -n "signalEncrypt\(|content = Bytes.toBase64\(ciphertextMessage\.serialize\(\)\)|sendMessages\(" \
Signal-Desktop/ts/textsecure/OutgoingMessage.preload.ts
3. Confirm libsignal does the encryption locally #
rg -n "get_identity_key_pair|load_session|message_encrypt|aes_256_cbc_encrypt" \
libsignal/rust/protocol/src/storage/traits.rs \
libsignal/rust/protocol/src/session.rs \
libsignal/rust/protocol/src/session_cipher.rs
4. Confirm 1:1 calls derive SRTP keys from client-side Diffie-Hellman #
rg -n "custom Diffie-Hellman|generate_local_secret_and_public_key|negotiate_srtp_keys|disable_dtls_and_set_srtp_key" \
ringrtc/src/rust/src/core/signaling.rs \
ringrtc/src/rust/src/core/connection.rs \
ringrtc/src/rust/src/webrtc/sdp_observer.rs
5. Confirm group calls add end-to-end frame encryption on top of SFU transport #
rg -n "enable_frame_encryption|encrypt_media|decrypt_media|send_media_send_key_to_users_over_signaling|frame_crypto_context" \
ringrtc/src/rust/src/core/group_call.rs \
ringrtc/src/rust/src/webrtc/peer_connection_observer.rs
6. Confirm RingRTC sends call signaling back through Signal’s message layer #
rg -n "type: 'CallingMessage'|sendCallingMessage\(|sendContentMessageToGroup|Proto\.CallMessage" \
Signal-Desktop/ts/services/calling.preload.ts \
Signal-Desktop/ts/jobs/helpers/sendCallingMessage.preload.ts \
Signal-Desktop/ts/textsecure/SendMessage.preload.ts
Detailed Findings #
1. Official clients really use libsignal and ringrtc #
Signal-Desktop/package.json:118-120declares@signalapp/libsignal-clientand@signalapp/ringrtcas dependencies.Signal-Android/app/build.gradle.kts:677-679includeslibs.libsignal.androidandlibs.signal.ringrtc.libsignal/README.md:19-23states that the repository is used by Signal Android, iOS, and Desktop.
That is enough to justify using libsignal as the shared message-crypto implementation and ringrtc as the shared calling implementation for the clients checked here.
2. Message encryption happens on the client before network send #
In Signal-Desktop, the send path imports signalEncrypt directly from @signalapp/libsignal-client:
Signal-Desktop/ts/textsecure/OutgoingMessage.preload.ts:7-18
Then it encrypts the plaintext before building the network payload:
Signal-Desktop/ts/textsecure/OutgoingMessage.preload.ts:377-400callssignalEncrypt(...)Signal-Desktop/ts/textsecure/OutgoingMessage.preload.ts:514-531serializes the resulting ciphertext and stores it incontentSignal-Desktop/ts/textsecure/OutgoingMessage.preload.ts:308-336sends that JSON payload withsendMessages(...)orsendMessagesUnauth(...)
So the Desktop app-side proof is straightforward: plaintext becomes ciphertext first, and only then is the payload handed to the server API.
3. libsignal derives message keys from client-held state, not from the server #
The core evidence is in libsignal.
libsignal/rust/protocol/src/storage/traits.rs:49-52requires the localIdentityKeyStoreto provide the client’s identity key pair, including the private key.libsignal/rust/protocol/src/storage/traits.rs:150-159shows sessions are loaded from a localSessionStore.libsignal/rust/protocol/src/session.rs:180-230creates a session from the remote party’s public prekey bundle plus the local identity key pair and a freshly generated local base key.libsignal/rust/protocol/src/session_cipher.rs:19-27definesmessage_encrypt(...).libsignal/rust/protocol/src/session_cipher.rs:36-45derives message keys from the current sender chain state.libsignal/rust/protocol/src/session_cipher.rs:62-63encrypts plaintext withaes_256_cbc_encrypt(...).libsignal/rust/protocol/src/session_cipher.rs:93-161wraps the ciphertext intoSignalMessageorPreKeySignalMessageand stores updated session state locally.
The important point is not the exact cipher alone. The important point is where the encryption key material comes from: local identity/private/session state and ratchet state, not a server-held symmetric key.
4. 1:1 calls use client-side Diffie-Hellman to derive SRTP keys #
The direct call path is in ringrtc.
ringrtc/src/rust/src/core/signaling.rs:24-30states that V3/V4 signaling replaced DTLS with a custom Diffie-Hellman exchange to derive SRTP keys.ringrtc/src/rust/src/core/connection.rs:635-641generates a local secret/public key pair and places the public key into the outgoing offer.ringrtc/src/rust/src/core/connection.rs:822-858does the same for the answer path.ringrtc/src/rust/src/core/connection.rs:2231-2285definesnegotiate_srtp_keys(...), performs Diffie-Hellman, runs HKDF, and outputs SRTP keys usingAeadAes256Gcm.ringrtc/src/rust/src/core/connection.rs:730-743and:828-838apply those derived keys to offer/answer viadisable_dtls_and_set_srtp_key(...).ringrtc/src/rust/src/webrtc/sdp_observer.rs:174-189is the implementation ofdisable_dtls_and_set_srtp_key(...).ringrtc/src/rust/src/native.rs:454-459creates the 1:1PeerConnectionObserverwithenable_frame_encryptionset tofalse, which means 1:1 calls rely on the SRTP path rather than the separate frame-encryption layer used in group calls.
For 1:1 calls, that means the media path is encrypted with SRTP keys that are derived from endpoint-controlled ephemeral secrets. A signaling relay can see public keys and call metadata, but not the private secret required to derive the SRTP keys.
5. Group calls are encrypted too, but the model is different #
Group calls are not plain peer-to-peer. They are SFU-based. The code shows two separate encryption layers:
- client-to-SFU transport encryption
- participant-to-participant frame encryption over that transport
Transport layer evidence:
ringrtc/src/rust/src/core/group_call.rs:170-198definesSrtpKeysfor the group call transport.ringrtc/src/rust/src/core/group_call.rs:470-509derives those SRTP keys from a Diffie-Hellman exchange with the server public key.ringrtc/src/rust/src/core/group_call.rs:3454-3478sets local and remote group-call session descriptions usingsrtp_keys.clientandsrtp_keys.server.ringrtc/src/rust/src/core/group_call.rs:2992-3057starts the peer connection to the SFU using that transport setup.
End-to-end frame layer evidence:
ringrtc/src/rust/src/core/group_call.rs:4966-4971creates the group callPeerConnectionObserverwithenable_frame_encryptionset totrue.ringrtc/src/rust/src/webrtc/peer_connection_observer.rs:159-185defines the frame-encryption callbacks.ringrtc/src/rust/src/webrtc/peer_connection_observer.rs:450-548shows WebRTC calling back into Rust for media encryption and decryption.ringrtc/src/rust/src/core/group_call.rs:3801-3915implementsencrypt_media(...)anddecrypt_media(...), including a ratchet counter, frame counter, and MAC.ringrtc/src/rust/src/core/group_call.rs:3566-3597advances the media send key ratchet.ringrtc/src/rust/src/core/group_call.rs:3601-3623installs received media keys for decrypting remote frames.ringrtc/src/rust/src/core/group_call.rs:3656-3681sends media frame keys to participants over signaling.
So the precise claim for group calls is:
- the SFU is in the transport path
- the transport is encrypted
- the actual media frames also get an extra application-level encryption layer in the client
- the frame keys are rotated and shared between participants, not exposed as plain media to the SFU
That is the key nuance. Group calls are not “just SRTP to the SFU”. The source shows an extra frame-encryption layer intended to keep the SFU from seeing call contents.
6. Group-call key exchange is routed back through Signal’s messaging layer #
This matters because ringrtc itself decides when to send media keys, but the app decides how to transport those call messages.
Desktop evidence:
Signal-Desktop/ts/services/calling.preload.ts:617-639wires RingRTC callback handlers into the app.Signal-Desktop/ts/services/calling.preload.ts:3288-3347takes RingRTC group-call signaling bytes, wraps them asProto.CallMessage, and queues aCallingMessagejob.Signal-Desktop/ts/jobs/helpers/sendCallingMessage.preload.ts:95-115sends group call messages throughsendContentMessageToGroup(...).Signal-Desktop/ts/jobs/helpers/sendCallingMessage.preload.ts:118-131sends direct call messages throughmessaging.sendCallingMessage(...).Signal-Desktop/ts/textsecure/SendMessage.preload.ts:2411-2444wraps a direct call message insideProto.Contentand sends it throughsendMessageProtoAndWait(...).
That ties the call-signaling path back to Signal’s normal encrypted message transport rather than a plain unauthenticated side channel.
7. What this does and does not prove #
What this source review supports:
- Signal messages are encrypted on the client using
libsignal. - The message server does not appear to hold the message-encryption keys used in that path.
- Direct 1:1 calls derive SRTP keys endpoint-side from Diffie-Hellman inputs.
- Group calls add an extra frame-encryption layer over SFU transport.
What this review does not prove by itself:
- that every released binary exactly matches the audited source
- that iOS was checked in this pass with the same depth as Desktop and Android
- that Signal’s production servers are configured exactly the same as the open-source default expectations
But as a source-based proof of implementation intent and code path, the evidence is strong.