Overview
whatsapp-rust implements the Signal Protocol for end-to-end encryption of both one-on-one and group messages. The implementation is based on Signal’s libsignal library, adapted for WhatsApp’s specific protocol requirements.
The Signal Protocol implementation handles cryptographic primitives. Any modifications to this code require expert-level understanding of cryptographic protocols to avoid security vulnerabilities.
Architecture
The Signal Protocol implementation is split across two main locations:
wacore/libsignal/ - Platform-agnostic Signal Protocol core (Rust port of libsignal)
src/store/signal*.rs - WhatsApp-specific storage integration with Diesel/SQLite
Key Components
wacore/libsignal/src/
├── protocol/
│ ├── session_cipher.rs # Encryption/decryption for 1:1 messages
│ ├── group_cipher.rs # Encryption/decryption for group messages
│ ├── ratchet.rs # Double Ratchet implementation
│ ├── sender_keys.rs # Sender Key protocol for groups
│ └── state/ # Session state management
└── crypto/
├── aes_cbc.rs # AES-256-CBC for message content
├── aes_gcm.rs # AES-GCM for media encryption
└── hash.rs # HKDF and HMAC primitives
Double Ratchet Protocol
The Double Ratchet algorithm provides forward secrecy and post-compromise security for 1:1 messages.
Session Initialization
Two participants initialize a session using Diffie-Hellman key exchange:
// Alice initiates the session (sender)
pub fn initialize_alice_session<R: Rng + CryptoRng>(
parameters: &AliceSignalProtocolParameters,
csprng: &mut R,
) -> Result<SessionState>
// Bob receives the session (recipient)
pub fn initialize_bob_session(
parameters: &BobSignalProtocolParameters
) -> Result<SessionState>
Key Derivation:
- Compute shared secrets from ephemeral key exchanges
- Derive root key and chain key using HKDF-SHA256:
HKDF(discontinuity_bytes || DH1 || DH2 || DH3 [|| DH4])
→ (RootKey[32], ChainKey[32], PQRKey[32])
- Initialize sender and receiver chains
Location: wacore/libsignal/src/protocol/ratchet.rs:41-172
Message Encryption
Each message advances the sender chain and derives ephemeral message keys:
// From wacore/libsignal/src/protocol/session_cipher.rs:65-183
pub async fn message_encrypt(
ptext: &[u8],
remote_address: &ProtocolAddress,
session_store: &mut dyn SessionStore,
identity_store: &mut dyn IdentityKeyStore,
) -> Result<CiphertextMessage>
Process:
- Load current session state
- Get sender chain key and derive message keys:
let (message_keys_gen, next_chain_key) = chain_key.step_with_message_keys();
let message_keys = message_keys_gen.generate_keys();
// message_keys contains: cipher_key, mac_key, iv
- Encrypt plaintext with AES-256-CBC:
aes_256_cbc_encrypt_into(ptext, message_keys.cipher_key(),
message_keys.iv(), &mut buf)
- Create SignalMessage with MAC for authentication
- Advance chain key and save session state
Message Format:
- SignalMessage: Standard encrypted message
- PreKeySignalMessage: Includes prekey bundle for session establishment
Message Decryption
Decryption handles out-of-order delivery and tries multiple session states:
// From wacore/libsignal/src/protocol/session_cipher.rs:292-363
pub async fn message_decrypt_signal<R: Rng + CryptoRng>(
ciphertext: &SignalMessage,
remote_address: &ProtocolAddress,
session_store: &mut dyn SessionStore,
identity_store: &mut dyn IdentityKeyStore,
csprng: &mut R,
) -> Result<Vec<u8>>
Process:
- Try current session state first
- If MAC verification fails, try previous (archived) sessions
- Derive/retrieve message keys for the counter
- Verify MAC:
ciphertext.verify_mac(&their_identity_key, &local_identity_key,
message_keys.mac_key())
- Decrypt with AES-256-CBC
- Promote successful session to current if needed
The implementation optimizes memory by using take/restore patterns to avoid cloning session states during decryption attempts (see session_cipher.rs:495-619).
Chain Key Ratcheting
Message keys are derived from chain keys, which advance with each message:
pub struct ChainKey {
key: [u8; 32],
index: u32,
}
impl ChainKey {
pub fn step_with_message_keys(self) -> (MessageKeyGenerator, ChainKey) {
let message_key_gen = MessageKeyGenerator::new(self.key, self.index);
let next_chain_key = self.next_chain_key();
(message_key_gen, next_chain_key)
}
}
Location: wacore/libsignal/src/protocol/ratchet/keys.rs
Forward Jumps
The protocol tolerates out-of-order messages up to a limit:
const MAX_FORWARD_JUMPS: usize = 25000;
if jump > MAX_FORWARD_JUMPS {
return Err(SignalProtocolError::InvalidMessage(
original_message_type,
"message from too far into the future",
));
}
Location: wacore/libsignal/src/protocol/session_cipher.rs:832-847
Sender Keys (Group Encryption)
Groups use the Sender Key protocol for efficient multi-recipient encryption.
Sender Key Distribution
Each participant generates and distributes a sender key:
// From wacore/libsignal/src/protocol/group_cipher.rs:283-336
pub async fn create_sender_key_distribution_message<R: Rng + CryptoRng>(
sender_key_name: &SenderKeyName,
sender_key_store: &mut dyn SenderKeyStore,
csprng: &mut R,
) -> Result<SenderKeyDistributionMessage>
Structure:
- Chain ID: Random 31-bit identifier for this sender key session
- Iteration: Message counter (starts at 0)
- Chain Key: 32-byte seed for deriving message keys
- Signing Key: Ed25519 public key for message authentication
Group Encryption
Messages are encrypted with the sender’s current chain key:
// From wacore/libsignal/src/protocol/group_cipher.rs:53-116
pub async fn group_encrypt<R: Rng + CryptoRng>(
sender_key_store: &mut dyn SenderKeyStore,
sender_key_name: &SenderKeyName,
plaintext: &[u8],
csprng: &mut R,
) -> Result<SenderKeyMessage>
Process:
- Load sender key state for the group
- Derive message keys from current chain key
- Encrypt with AES-256-CBC
- Sign message with Ed25519 private key
- Advance chain key
Group Decryption
Recipients decrypt using the sender’s distributed key:
// From wacore/libsignal/src/protocol/group_cipher.rs:162-250
pub async fn group_decrypt(
skm_bytes: &[u8],
sender_key_store: &mut dyn SenderKeyStore,
sender_key_name: &SenderKeyName,
) -> Result<Vec<u8>>
Process:
- Parse SenderKeyMessage
- Look up sender key state by chain ID
- Verify Ed25519 signature
- Derive message keys for iteration (handling out-of-order)
- Decrypt with AES-256-CBC
Group decryption maintains up to MAX_FORWARD_JUMPS (25000) cached message keys per sender. This prevents resource exhaustion attacks but limits tolerance for extreme out-of-order delivery.
Cryptographic Primitives
AES-256-CBC (Message Content)
Used for encrypting message bodies in both 1:1 and group messages:
pub fn aes_256_cbc_encrypt_into(
plaintext: &[u8],
key: &[u8], // 32 bytes
iv: &[u8], // 16 bytes
output: &mut Vec<u8>,
) -> Result<()>
Location: wacore/libsignal/src/crypto/aes_cbc.rs
Thread-Local Buffers
The implementation uses thread-local buffers to reduce allocations:
thread_local! {
static ENCRYPTION_BUFFER: RefCell<EncryptionBuffer> = ...;
static DECRYPTION_BUFFER: RefCell<EncryptionBuffer> = ...;
}
// Usage in session_cipher.rs:99-111
let ctext = ENCRYPTION_BUFFER.with(|buffer| {
let mut buf_wrapper = buffer.borrow_mut();
let buf = buf_wrapper.get_buffer();
aes_256_cbc_encrypt_into(ptext, message_keys.cipher_key(),
message_keys.iv(), buf)?;
let result = std::mem::take(buf);
buf.reserve(EncryptionBuffer::INITIAL_CAPACITY);
Ok::<Vec<u8>, SignalProtocolError>(result)
})?;
Location: wacore/libsignal/src/protocol/session_cipher.rs:14-54
HKDF-SHA256
Used for key derivation in session initialization:
pub fn derive_keys(secret_input: &[u8]) -> (RootKey, ChainKey, InitialPQRKey) {
let mut secrets = [0; 96];
hkdf::Hkdf::<sha2::Sha256>::new(None, secret_input)
.expand(b"WhisperText", &mut secrets)
.expect("valid length");
// Split into RootKey[32], ChainKey[32], PQRKey[32]
}
Location: wacore/libsignal/src/protocol/ratchet.rs:18-39
Storage Integration
whatsapp-rust integrates Signal Protocol storage with SQLite via Diesel:
src/store/
├── signal_identity.rs # Identity key storage
├── signal_prekey.rs # PreKey storage
├── signal_session.rs # Session state storage
└── signal_sender_key.rs # Sender key storage
Each storage trait from wacore/libsignal/src/protocol/storage/traits.rs is implemented using SQL queries:
#[async_trait]
impl SessionStore for SqliteStore {
async fn load_session(
&mut self,
address: &ProtocolAddress,
) -> Result<Option<SessionRecord>> {
// SQL: SELECT record FROM signal_sessions
// WHERE our_jid = ? AND their_jid = ?
}
}
Security Considerations
Identity Key Trust
The implementation verifies identity keys before encryption/decryption:
if !identity_store
.is_trusted_identity(remote_address, &their_identity_key,
Direction::Sending)
.await?
{
return Err(SignalProtocolError::UntrustedIdentity(
remote_address.clone(),
));
}
Location: wacore/libsignal/src/protocol/session_cipher.rs:160-172
Duplicate Message Detection
The protocol detects and rejects duplicate messages:
if chain_index > counter {
return match state.get_message_keys(their_ephemeral, counter)? {
Some(keys) => Ok(keys),
None => Err(SignalProtocolError::DuplicatedMessage(chain_index, counter)),
};
}
Location: wacore/libsignal/src/protocol/session_cipher.rs:822-827
Session State Corruption
Detailed logging helps diagnose crypto failures:
fn create_decryption_failure_log(
remote_address: &ProtocolAddress,
errs: &[SignalProtocolError],
record: &SessionRecord,
ciphertext: &SignalMessage,
) -> Result<String>
This generates comprehensive error logs showing:
- All attempted session states
- Receiver chain information
- Message metadata (sender ratchet key, counter)
Location: wacore/libsignal/src/protocol/session_cipher.rs:365-454
Take/Restore Pattern
Avoids cloning session states during decryption attempts:
// Take ownership instead of cloning
if let Some(mut current_state) = record.take_session_state() {
let result = decrypt_message_with_state(&mut current_state, ...);
match result {
Ok(ptext) => {
record.set_session_state(current_state);
return Ok(ptext);
}
Err(e) => {
record.set_session_state(current_state); // Restore
}
}
}
Location: wacore/libsignal/src/protocol/session_cipher.rs:495-564
Buffer Reuse
Thread-local buffers eliminate per-message allocations:
struct EncryptionBuffer {
buffer: Vec<u8>,
usage_count: usize,
}
const INITIAL_CAPACITY: usize = 1024;
const MAX_CAPACITY: usize = 16 * 1024;
const SHRINK_THRESHOLD: usize = 100;
fn get_buffer(&mut self) -> &mut Vec<u8> {
self.usage_count += 1;
if self.usage_count.is_multiple_of(SHRINK_THRESHOLD) {
if self.buffer.capacity() > MAX_CAPACITY {
self.buffer = Vec::with_capacity(INITIAL_CAPACITY);
}
}
&mut self.buffer
}
Location: wacore/libsignal/src/protocol/session_cipher.rs:20-54
References