Skip to main content

whatsapp_rust/features/
signal.rs

1//! Low-level Signal protocol and raw transport APIs.
2//!
3//! Encryption, decryption, session management, and participant node creation.
4
5use anyhow::{Result, anyhow};
6use wacore::libsignal::protocol::{
7    CiphertextMessage, PreKeySignalMessage, SignalMessage, UsePQRatchet, message_decrypt,
8    message_encrypt,
9};
10use wacore::message_processing::EncType;
11use wacore::messages::MessageUtils;
12use wacore::types::jid::{JidExt, make_sender_key_name};
13use wacore_binary::Jid;
14use wacore_binary::Node;
15
16use crate::client::Client;
17
18/// Feature handle for Signal protocol operations.
19pub struct Signal<'a> {
20    client: &'a Client,
21}
22
23impl<'a> Signal<'a> {
24    pub(crate) fn new(client: &'a Client) -> Self {
25        Self { client }
26    }
27
28    /// Encrypt plaintext for a single recipient using the Signal protocol.
29    ///
30    /// Returns `(EncType, ciphertext_bytes)`. The caller is responsible
31    /// for padding if needed; this method encrypts raw bytes.
32    ///
33    /// PN JIDs are resolved to LID when a LID session exists, matching
34    /// the internal send path.
35    pub async fn encrypt_message(&self, jid: &Jid, plaintext: &[u8]) -> Result<(EncType, Vec<u8>)> {
36        // Resolve PN→LID to use the correct Signal session (matches send path)
37        let encryption_jid = self.client.resolve_encryption_jid(jid).await;
38        let signal_addr = encryption_jid.to_protocol_address();
39
40        let lock = self.client.session_lock_for(signal_addr.as_str()).await;
41        let _guard = lock.lock().await;
42        let mut adapter = self.client.signal_adapter().await;
43
44        let encrypted = message_encrypt(
45            plaintext,
46            &signal_addr,
47            &mut adapter.session_store,
48            &mut adapter.identity_store,
49        )
50        .await?;
51
52        drop(_guard);
53        self.client.flush_signal_cache().await?;
54
55        let (_, is_prekey, bytes) = wacore::send::extract_ciphertext(encrypted)
56            .ok_or_else(|| anyhow!("unexpected ciphertext variant"))?;
57        let enc_type = if is_prekey {
58            EncType::PreKeyMessage
59        } else {
60            EncType::Message
61        };
62        Ok((enc_type, bytes.into_vec()))
63    }
64
65    /// Decrypt a Signal protocol message from a sender.
66    ///
67    /// Returns raw padded plaintext. Use [`MessageUtils::unpad_message_ref`]
68    /// with the stanza's `v` attribute if WhatsApp message unpadding is needed.
69    ///
70    /// PN JIDs are resolved to LID when a LID session exists, matching
71    /// the internal receive path.
72    pub async fn decrypt_message(
73        &self,
74        jid: &Jid,
75        enc_type: EncType,
76        ciphertext: &[u8],
77    ) -> Result<Vec<u8>> {
78        let parsed = match enc_type {
79            EncType::PreKeyMessage => {
80                CiphertextMessage::PreKeySignalMessage(PreKeySignalMessage::try_from(ciphertext)?)
81            }
82            EncType::Message => {
83                CiphertextMessage::SignalMessage(SignalMessage::try_from(ciphertext)?)
84            }
85            EncType::SenderKey => {
86                return Err(anyhow!("use decrypt_group_message for sender-key messages"));
87            }
88        };
89
90        let encryption_jid = self.client.resolve_encryption_jid(jid).await;
91        let signal_addr = encryption_jid.to_protocol_address();
92
93        let lock = self.client.session_lock_for(signal_addr.as_str()).await;
94        let _guard = lock.lock().await;
95        let mut adapter = self.client.signal_adapter().await;
96        let mut rng = rand::make_rng::<rand::rngs::StdRng>();
97
98        let plaintext = message_decrypt(
99            &parsed,
100            &signal_addr,
101            &mut adapter.session_store,
102            &mut adapter.identity_store,
103            &mut adapter.pre_key_store,
104            &adapter.signed_pre_key_store,
105            &mut rng,
106            UsePQRatchet::No,
107        )
108        .await?;
109
110        drop(_guard);
111        self.client.flush_signal_cache().await?;
112
113        Ok(plaintext.to_vec())
114    }
115
116    /// Encrypt plaintext for a group using sender keys.
117    ///
118    /// Returns `(Option<skdm_bytes>, ciphertext_bytes)`. The SKDM is `Some`
119    /// only when a new sender key was created (first encrypt for this group
120    /// or after key rotation). Callers must distribute the SKDM to all group
121    /// participants when present. This matches WA Web which only creates
122    /// SKDM on first group encrypt or after sender key rotation.
123    ///
124    /// Not safe to call concurrently with `decrypt_group_message` for the
125    /// same group — sender key state is not internally locked.
126    pub async fn encrypt_group_message(
127        &self,
128        group_jid: &Jid,
129        plaintext: &[u8],
130    ) -> Result<(Option<Vec<u8>>, Vec<u8>)> {
131        let own_jid = self.client.get_own_jid_for_group(group_jid).await?;
132        let sender_addr = own_jid.to_protocol_address();
133        let sender_key_name = make_sender_key_name(group_jid, &sender_addr);
134
135        // Only create SKDM when no sender key exists (matches WA Web behavior)
136        let device_store = self.client.persistence_manager.get_device_arc().await;
137        let device_guard = device_store.read().await;
138        let key_exists = self
139            .client
140            .signal_cache
141            .get_sender_key(&sender_key_name, &*device_guard.backend)
142            .await?
143            .is_some();
144        drop(device_guard);
145
146        let mut adapter = self.client.signal_adapter().await;
147        let mut rng = rand::make_rng::<rand::rngs::StdRng>();
148
149        let skdm_bytes = if !key_exists {
150            Some(
151                wacore::send::create_sender_key_distribution_message_for_group(
152                    &mut adapter.sender_key_store,
153                    group_jid,
154                    &sender_addr,
155                )
156                .await?,
157            )
158        } else {
159            None
160        };
161
162        let ciphertext = wacore::send::encrypt_group_message(
163            &mut adapter.sender_key_store,
164            group_jid,
165            &sender_addr,
166            plaintext,
167            &mut rng,
168        )
169        .await?;
170
171        self.client.flush_signal_cache().await?;
172
173        Ok((skdm_bytes, ciphertext.into_serialized().into_vec()))
174    }
175
176    /// Decrypt a group (sender-key) message.
177    ///
178    /// Returns raw padded plaintext. Use [`MessageUtils::unpad_message_ref`]
179    /// with the stanza's `v` attribute if WhatsApp message unpadding is needed.
180    ///
181    /// Not safe to call concurrently with `encrypt_group_message` for the
182    /// same group — sender key state is not internally locked.
183    pub async fn decrypt_group_message(
184        &self,
185        group_jid: &Jid,
186        sender_jid: &Jid,
187        ciphertext: &[u8],
188    ) -> Result<Vec<u8>> {
189        let sender_key_name =
190            make_sender_key_name(group_jid, &sender_jid.to_non_ad().to_protocol_address());
191
192        let mut adapter = self.client.signal_adapter().await;
193
194        let plaintext = wacore::libsignal::protocol::group_decrypt(
195            ciphertext,
196            &mut adapter.sender_key_store,
197            &sender_key_name,
198        )
199        .await?;
200
201        self.client.flush_signal_cache().await?;
202
203        Ok(plaintext.to_vec())
204    }
205
206    /// Check whether a Signal session exists for `jid`.
207    ///
208    /// PN JIDs are resolved to LID when a LID mapping exists, matching
209    /// the encrypt/decrypt paths.
210    pub async fn validate_session(&self, jid: &Jid) -> Result<bool> {
211        let resolved = self.client.resolve_encryption_jid(jid).await;
212        let signal_addr = resolved.to_protocol_address();
213        let device_store = self.client.persistence_manager.get_device_arc().await;
214        let device_guard = device_store.read().await;
215        self.client
216            .signal_cache
217            .has_session(&signal_addr, &*device_guard.backend)
218            .await
219            .map_err(|e| anyhow!("session check failed: {e}"))
220    }
221
222    /// Delete Signal sessions and identity keys for the given JIDs.
223    ///
224    /// Matches WA Web's `deleteRemoteSession` which removes both session
225    /// and identity as a paired operation. Changes are flushed to the
226    /// persistent backend before returning.
227    ///
228    /// PN JIDs are resolved to LID when a LID mapping exists, matching
229    /// the encrypt/decrypt paths.
230    pub async fn delete_sessions(&self, jids: &[Jid]) -> Result<()> {
231        for jid in jids {
232            let resolved = self.client.resolve_encryption_jid(jid).await;
233            let addr = resolved.to_protocol_address();
234
235            let lock = self.client.session_lock_for(addr.as_str()).await;
236            let _guard = lock.lock().await;
237
238            // WA Web removes session + identity together (deleteRemoteSession)
239            self.client.signal_cache.delete_session(&addr).await;
240            self.client.signal_cache.delete_identity(&addr).await;
241        }
242
243        self.client.flush_signal_cache().await?;
244        Ok(())
245    }
246
247    /// Create encrypted participant `<to>` nodes for the given recipient JIDs.
248    ///
249    /// Resolves devices, ensures Signal sessions, encrypts the message for
250    /// each device, and returns the resulting XML nodes.
251    ///
252    /// Returns `(nodes, should_include_device_identity)`.
253    pub async fn create_participant_nodes(
254        &self,
255        recipient_jids: &[Jid],
256        message: &waproto::whatsapp::Message,
257    ) -> Result<(Vec<Node>, bool)> {
258        let device_jids = self.client.get_user_devices(recipient_jids).await?;
259        self.client.ensure_e2e_sessions(&device_jids).await?;
260
261        // Acquire per-device session locks before encrypting (matches DM send path)
262        let lock_jids = self.client.build_session_lock_keys(&device_jids).await;
263        let session_mutexes = self.client.session_mutexes_for(&lock_jids).await;
264        let mut _session_guards = Vec::with_capacity(session_mutexes.len());
265        for mutex in &session_mutexes {
266            _session_guards.push(mutex.lock().await);
267        }
268
269        let plaintext = MessageUtils::encode_and_pad(message);
270        let mut adapter = self.client.signal_adapter().await;
271        let mediatype = wacore::send::media_type_from_message(message);
272        let hide_decrypt_fail = wacore::send::should_hide_decrypt_fail(message);
273
274        let mut stores = adapter.as_signal_stores();
275        let result = wacore::send::encrypt_for_devices(
276            &*self.client.runtime,
277            &mut stores,
278            self.client,
279            &device_jids,
280            &plaintext,
281            hide_decrypt_fail,
282            mediatype,
283        )
284        .await?;
285
286        drop(_session_guards);
287        self.client.flush_signal_cache().await?;
288
289        Ok((result.participant_nodes, result.includes_prekey_message))
290    }
291
292    /// Ensure E2E sessions exist for the given JIDs.
293    pub async fn assert_sessions(&self, jids: &[Jid]) -> Result<()> {
294        self.client.ensure_e2e_sessions(jids).await
295    }
296
297    /// Get all known device JIDs for the given user JIDs via usync.
298    pub async fn get_user_devices(&self, jids: &[Jid]) -> Result<Vec<Jid>> {
299        self.client.get_user_devices(jids).await
300    }
301}
302
303impl Client {
304    /// Access low-level Signal protocol operations.
305    pub fn signal(&self) -> Signal<'_> {
306        Signal::new(self)
307    }
308}