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 thiserror::Error;
6use wacore::libsignal::protocol::{
7    CiphertextMessage, DecryptionResult, IdentityChange, PreKeyBundle, PreKeySignalMessage,
8    PublicKey, SENDERKEY_MESSAGE_CURRENT_VERSION, SenderKeyDistributionMessage, SenderKeyStore,
9    SignalMessage, SignalProtocolError, UsePQRatchet, message_decrypt, message_encrypt,
10    process_sender_key_distribution_message,
11};
12use wacore::message_processing::EncType;
13use wacore::messages::MessageUtils;
14use wacore::types::jid::{JidExt, make_sender_key_name};
15use wacore_binary::Jid;
16use wacore_binary::Node;
17
18use crate::client::Client;
19
20/// Error returned by the low-level Signal protocol operations.
21#[derive(Debug, Error)]
22#[non_exhaustive]
23pub enum SignalError {
24    /// A Signal protocol primitive (encrypt/decrypt/session) failed.
25    #[error("{0}")]
26    Protocol(#[from] SignalProtocolError),
27    /// The requested operation is not valid for this input (e.g. a sender-key
28    /// or message-secret envelope passed to the pairwise decrypt path).
29    #[error("unsupported signal operation: {0}")]
30    Unsupported(String),
31    /// The operation is supported but one of its inputs is malformed.
32    #[error("invalid signal input: {0}")]
33    InvalidInput(String),
34    /// Catch-all for internal failures (device resolution, cache flush).
35    #[error("{0}")]
36    Internal(#[from] anyhow::Error),
37}
38
39impl From<crate::client::SignalMaintenanceError> for SignalError {
40    fn from(err: crate::client::SignalMaintenanceError) -> Self {
41        match err {
42            crate::client::SignalMaintenanceError::Signal(e) => SignalError::Protocol(e),
43            other => SignalError::Internal(other.into()),
44        }
45    }
46}
47
48/// Read-only information from a currently open pairwise session.
49#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct SignalSessionInfo {
51    /// Local base key identifying the active session state.
52    pub base_key: Vec<u8>,
53    /// Remote registration identifier recorded by the session.
54    pub registration_id: u32,
55}
56
57/// Result of moving pairwise session state between address namespaces.
58#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
59#[non_exhaustive]
60pub struct SignalSessionMigration {
61    /// Pairwise sessions moved to the destination namespace.
62    pub migrated: usize,
63    /// Pairwise session lookups skipped after a storage error.
64    pub skipped: usize,
65    /// Pairwise sessions found or unsuccessfully queried.
66    pub total: usize,
67    /// Identity records moved when the destination had no identity.
68    pub migrated_identities: usize,
69    /// Source identity records removed in favor of an existing destination.
70    pub discarded_identities: usize,
71    /// Identity lookups skipped after a storage error.
72    pub skipped_identities: usize,
73}
74
75impl SignalSessionMigration {
76    /// Whether any source state was moved or removed.
77    pub fn has_state_changes(self) -> bool {
78        self.migrated != 0 || self.migrated_identities != 0 || self.discarded_identities != 0
79    }
80}
81
82fn decode_sender_key_distribution(
83    bytes: &[u8],
84) -> Result<SenderKeyDistributionMessage, SignalError> {
85    match SenderKeyDistributionMessage::try_from(bytes) {
86        Ok(message) => Ok(message),
87        Err(primary_error) => {
88            let fallback = waproto::codec::sender_key_distribution_message_decode(bytes)
89                .map_err(|fallback_error| {
90                    SignalError::InvalidInput(format!(
91                        "sender-key distribution decode failed: primary={primary_error}; fallback={fallback_error}"
92                    ))
93                })?;
94            let signing_key = fallback.signing_key.ok_or_else(|| {
95                SignalError::InvalidInput("sender-key distribution is missing signing_key".into())
96            })?;
97            let id = fallback.id.ok_or_else(|| {
98                SignalError::InvalidInput("sender-key distribution is missing id".into())
99            })?;
100            let iteration = fallback.iteration.ok_or_else(|| {
101                SignalError::InvalidInput("sender-key distribution is missing iteration".into())
102            })?;
103            let chain_key: [u8; 32] = fallback
104                .chain_key
105                .ok_or_else(|| {
106                    SignalError::InvalidInput("sender-key distribution is missing chain_key".into())
107                })?
108                .try_into()
109                .map_err(|value: Vec<u8>| {
110                    SignalError::InvalidInput(format!(
111                        "sender-key distribution chain_key must be 32 bytes, got {}",
112                        value.len()
113                    ))
114                })?;
115            let signing_key =
116                PublicKey::from_djb_public_key_bytes(&signing_key).map_err(|error| {
117                    SignalError::InvalidInput(format!(
118                        "sender-key distribution signing_key is invalid: {error}"
119                    ))
120                })?;
121            Ok(SenderKeyDistributionMessage::new(
122                SENDERKEY_MESSAGE_CURRENT_VERSION,
123                id,
124                iteration,
125                chain_key,
126                signing_key,
127            )?)
128        }
129    }
130}
131
132/// Feature handle for Signal protocol operations.
133pub struct Signal<'a> {
134    client: &'a Client,
135}
136
137impl<'a> Signal<'a> {
138    pub(crate) fn new(client: &'a Client) -> Self {
139        Self { client }
140    }
141
142    async fn session_info_at(&self, jid: &Jid) -> Result<Option<SignalSessionInfo>, SignalError> {
143        let address = jid.to_protocol_address();
144        let session_mutex = self.client.session_lock_for(address.as_str()).await;
145        let _session_guard = session_mutex.lock().await;
146        let device = self.client.persistence_manager.get_device_snapshot();
147        let Some(session) = self
148            .client
149            .signal_cache
150            .peek_session(&address, &*device.backend)
151            .await?
152        else {
153            return Ok(None);
154        };
155        let base_key = session.alice_base_key()?;
156        let registration_id = session.remote_registration_id()?;
157        Ok(Some(SignalSessionInfo {
158            base_key: base_key.to_vec(),
159            registration_id,
160        }))
161    }
162
163    /// Move a legacy source-namespace session only after a resolved lookup
164    /// misses. Successful steady-state operations therefore pay no migration
165    /// preflight, while startup state stored under PN remains recoverable.
166    async fn migrate_legacy_pairwise_state(
167        &self,
168        source: &Jid,
169        resolved: &Jid,
170    ) -> Result<bool, SignalError> {
171        if source.server == resolved.server {
172            return Ok(false);
173        }
174        Ok(self.migrate_sessions(source, resolved).await?.migrated != 0)
175    }
176
177    async fn encrypt_pairwise_at(
178        &self,
179        jid: &Jid,
180        plaintext: &[u8],
181    ) -> Result<CiphertextMessage, SignalError> {
182        let address = jid.to_protocol_address();
183        let lock = self.client.session_lock_for(address.as_str()).await;
184        let _guard = lock.lock().await;
185        let mut adapter = self.client.signal_adapter().await;
186        Ok(message_encrypt(
187            plaintext,
188            &address,
189            &mut adapter.session_store,
190            &mut adapter.identity_store,
191        )
192        .await?)
193    }
194
195    async fn decrypt_pairwise_at(
196        &self,
197        jid: &Jid,
198        parsed: &CiphertextMessage,
199    ) -> Result<DecryptionResult, SignalError> {
200        let address = jid.to_protocol_address();
201        let lock = self.client.session_lock_for(address.as_str()).await;
202        let _guard = lock.lock().await;
203        let mut adapter = self.client.signal_adapter().await;
204        let mut rng = rand::make_rng::<rand::rngs::StdRng>();
205        let decrypted = message_decrypt(
206            parsed,
207            &address,
208            &mut adapter.session_store,
209            &mut adapter.identity_store,
210            &mut adapter.pre_key_store,
211            &adapter.signed_pre_key_store,
212            &mut rng,
213            UsePQRatchet::No,
214        )
215        .await?;
216
217        // A pkmsg consumed prekey is reported, not deleted by the decrypt;
218        // buffer it so the caller's flush removes it atomically with the
219        // promoted session.
220        if let Some(prekey_id) = decrypted.consumed_prekey_id {
221            adapter
222                .pre_key_store
223                .buffer_consumed_prekey(prekey_id, &address)
224                .await;
225        }
226        Ok(decrypted)
227    }
228
229    async fn delete_pairwise_state_at(&self, jid: &Jid) {
230        let address = jid.to_protocol_address();
231        let lock = self.client.session_lock_for(address.as_str()).await;
232        let _guard = lock.lock().await;
233        self.client.signal_cache.delete_session(&address).await;
234        self.client.signal_cache.delete_identity(&address).await;
235    }
236
237    /// Install a supplied pairwise pre-key bundle and durably expose the new
238    /// session before returning.
239    pub async fn install_prekey_bundle(
240        &self,
241        jid: &Jid,
242        bundle: &PreKeyBundle,
243    ) -> Result<IdentityChange, SignalError> {
244        let resolved = self.client.resolve_encryption_jid(jid).await;
245        let mut adapter = self.client.signal_adapter().await;
246        let mut rng = rand::make_rng::<rand::rngs::StdRng>();
247        let identity_change = self
248            .client
249            .install_prekey_bundle_cached(&resolved, bundle, &mut adapter, &mut rng)
250            .await?;
251        self.client.flush_signal_cache_batch_safe().await?;
252        Ok(identity_change)
253    }
254
255    /// Process a sender-key distribution and durably expose it before return.
256    pub async fn process_sender_key_distribution(
257        &self,
258        group_jid: &Jid,
259        sender_jid: &Jid,
260        distribution: &[u8],
261    ) -> Result<(), SignalError> {
262        self.process_sender_key_distribution_cached(group_jid, sender_jid, distribution)
263            .await?;
264        self.client.flush_signal_cache_batch_safe().await?;
265        Ok(())
266    }
267
268    /// Cache-only variant for the inbound message pipeline, whose enclosing
269    /// commit owns the batched durability flush.
270    pub(crate) async fn process_sender_key_distribution_cached(
271        &self,
272        group_jid: &Jid,
273        sender_jid: &Jid,
274        distribution: &[u8],
275    ) -> Result<(), SignalError> {
276        let distribution = decode_sender_key_distribution(distribution)?;
277        let sender_address = sender_jid.to_non_ad().to_protocol_address();
278        let sender_key_name = make_sender_key_name(group_jid, &sender_address);
279        let mut store = self.client.sender_key_adapter().await;
280        let chain_lock = store.sender_key_lock(&sender_key_name).await;
281        let chain_guard = chain_lock.lock().await;
282
283        process_sender_key_distribution_message(&sender_key_name, &distribution, &mut store)
284            .await?;
285        drop(chain_guard);
286        Ok(())
287    }
288
289    /// Create the current sender-key distribution for a group.
290    pub async fn sender_key_distribution(
291        &self,
292        group_jid: &Jid,
293        sender_jid: &Jid,
294    ) -> Result<Vec<u8>, SignalError> {
295        let sender_address = sender_jid.to_non_ad().to_protocol_address();
296        let sender_key_name = make_sender_key_name(group_jid, &sender_address);
297        let mut store = self.client.sender_key_adapter().await;
298        let chain_lock = store.sender_key_lock(&sender_key_name).await;
299        let chain_guard = chain_lock.lock().await;
300        let distribution = wacore::send::create_sender_key_distribution_message_for_group(
301            &mut store,
302            &sender_key_name,
303        )
304        .await?;
305        drop(chain_guard);
306        self.client.persist_signal_state_pre_wire().await?;
307        Ok(distribution)
308    }
309
310    /// Check whether sender-key state exists for a group and sender.
311    pub async fn has_sender_key(
312        &self,
313        group_jid: &Jid,
314        sender_jid: &Jid,
315    ) -> Result<bool, SignalError> {
316        let sender_address = sender_jid.to_non_ad().to_protocol_address();
317        let sender_key_name = make_sender_key_name(group_jid, &sender_address);
318        let device = self.client.persistence_manager.get_device_snapshot();
319        Ok(self
320            .client
321            .signal_cache
322            .get_sender_key(&sender_key_name, &*device.backend)
323            .await?
324            .is_some())
325    }
326
327    /// Delete one sender-key chain and make the removal durable before returning.
328    pub async fn delete_sender_key(
329        &self,
330        group_jid: &Jid,
331        sender_jid: &Jid,
332    ) -> Result<(), SignalError> {
333        let sender_address = sender_jid.to_non_ad().to_protocol_address();
334        let sender_key_name = make_sender_key_name(group_jid, &sender_address);
335        let backend = self.client.persistence_manager.backend();
336        self.client
337            .signal_cache
338            .delete_sender_key_durable(&sender_key_name, backend.as_ref())
339            .await?;
340        Ok(())
341    }
342
343    /// Inspect the currently open pairwise session for a JID.
344    pub async fn session_info(&self, jid: &Jid) -> Result<Option<SignalSessionInfo>, SignalError> {
345        let resolved = self.client.resolve_encryption_jid(jid).await;
346        let info = self.session_info_at(&resolved).await?;
347        if info.is_some() || !self.migrate_legacy_pairwise_state(jid, &resolved).await? {
348            return Ok(info);
349        }
350        self.session_info_at(&resolved).await
351    }
352
353    /// Move pairwise session state from a phone-number namespace to its linked
354    /// identifier namespace across known device slots.
355    pub async fn migrate_sessions(
356        &self,
357        from: &Jid,
358        to: &Jid,
359    ) -> Result<SignalSessionMigration, SignalError> {
360        if !matches!(
361            (from.server, to.server),
362            (wacore_binary::Server::Pn, wacore_binary::Server::Lid)
363                | (
364                    wacore_binary::Server::Hosted,
365                    wacore_binary::Server::HostedLid
366                )
367        ) {
368            return Err(SignalError::InvalidInput(
369                "source and destination must be matching phone and linked-identifier namespaces"
370                    .into(),
371            ));
372        }
373        let outcome = self.client.migrate_signal_sessions(from, to).await;
374        if outcome.has_state_changes()
375            || self
376                .client
377                .signal_cache
378                .has_pending_pairwise_writes_for_user(&from.user)
379                .await
380        {
381            self.client.flush_signal_cache_batch_safe().await?;
382        }
383        Ok(outcome)
384    }
385
386    /// Encrypt plaintext for a single recipient using the Signal protocol.
387    ///
388    /// Returns `(EncType, ciphertext_bytes)`. The caller is responsible
389    /// for padding if needed; this method encrypts raw bytes.
390    ///
391    /// PN JIDs are resolved to LID, with a legacy PN session migrated lazily
392    /// if the resolved lookup misses.
393    pub async fn encrypt_message(
394        &self,
395        jid: &Jid,
396        plaintext: &[u8],
397    ) -> Result<(EncType, Vec<u8>), SignalError> {
398        let encryption_jid = self.client.resolve_encryption_jid(jid).await;
399        let encrypted = match self.encrypt_pairwise_at(&encryption_jid, plaintext).await {
400            Ok(encrypted) => encrypted,
401            Err(error @ SignalError::Protocol(SignalProtocolError::SessionNotFound(_))) => {
402                if !self
403                    .migrate_legacy_pairwise_state(jid, &encryption_jid)
404                    .await?
405                {
406                    return Err(error);
407                }
408                self.encrypt_pairwise_at(&encryption_jid, plaintext).await?
409            }
410            Err(error) => return Err(error),
411        };
412
413        // Same pre-wire gate as the send path: the caller transmits these
414        // bytes, so a raised lease must be durable before they leave here.
415        self.client.persist_signal_state_pre_wire().await?;
416
417        let (_, is_prekey, bytes) = wacore::send::extract_ciphertext(encrypted)
418            .ok_or_else(|| SignalError::Unsupported("unexpected ciphertext variant".into()))?;
419        let enc_type = if is_prekey {
420            EncType::PreKeyMessage
421        } else {
422            EncType::Message
423        };
424        Ok((enc_type, bytes.into_vec()))
425    }
426
427    /// Decrypt a Signal protocol message from a sender.
428    ///
429    /// Returns raw padded plaintext. Use [`MessageUtils::unpad_message_ref`]
430    /// with the stanza's `v` attribute if WhatsApp message unpadding is needed.
431    ///
432    /// PN JIDs are resolved to LID, with a legacy PN session migrated lazily
433    /// if the resolved lookup misses.
434    pub async fn decrypt_message(
435        &self,
436        jid: &Jid,
437        enc_type: EncType,
438        ciphertext: &[u8],
439    ) -> Result<Vec<u8>, SignalError> {
440        let parsed = match enc_type {
441            EncType::PreKeyMessage => {
442                CiphertextMessage::PreKeySignalMessage(PreKeySignalMessage::try_from(ciphertext)?)
443            }
444            EncType::Message => {
445                CiphertextMessage::SignalMessage(SignalMessage::try_from(ciphertext)?)
446            }
447            EncType::SenderKey => {
448                return Err(SignalError::Unsupported(
449                    "use decrypt_group_message for sender-key messages".into(),
450                ));
451            }
452            EncType::MessageSecret => {
453                return Err(SignalError::Unsupported(
454                    "msmsg envelopes are not Signal messages; use the bot_message path".into(),
455                ));
456            }
457        };
458
459        let encryption_jid = self.client.resolve_encryption_jid(jid).await;
460        let decrypted = match self.decrypt_pairwise_at(&encryption_jid, &parsed).await {
461            Ok(decrypted) => decrypted,
462            Err(error @ SignalError::Protocol(SignalProtocolError::SessionNotFound(_))) => {
463                if !self
464                    .migrate_legacy_pairwise_state(jid, &encryption_jid)
465                    .await?
466                {
467                    return Err(error);
468                }
469                self.decrypt_pairwise_at(&encryption_jid, &parsed).await?
470            }
471            Err(error) => return Err(error),
472        };
473
474        self.client.flush_signal_cache_batch_safe().await?;
475
476        Ok(decrypted.plaintext)
477    }
478
479    /// Encrypt plaintext for a group using sender keys.
480    ///
481    /// Returns `(Option<skdm_bytes>, ciphertext_bytes)`. The SKDM is `Some`
482    /// while a new sender key still requires distribution (first encrypt for
483    /// this group, after key rotation, or a retry whose earlier durability
484    /// gate failed). Callers must distribute the SKDM to all group participants
485    /// when present.
486    ///
487    /// Concurrent calls for the same `(group, sender)` are serialized on the
488    /// sender-key chain, so the SKDM and the skmsg can't be split across keys.
489    pub async fn encrypt_group_message(
490        &self,
491        group_jid: &Jid,
492        plaintext: &[u8],
493    ) -> Result<(Option<Vec<u8>>, Vec<u8>), SignalError> {
494        let own_jid = self.client.get_own_jid_for_group(group_jid).await?;
495        let sender_addr = own_jid.to_protocol_address();
496        let sender_key_name = make_sender_key_name(group_jid, &sender_addr);
497
498        // Serialize the key-existence check + SKDM creation + encrypt for this chain.
499        let chain_lock = self
500            .client
501            .signal_cache
502            .sender_key_lock(&sender_key_name)
503            .await;
504        let _chain_guard = chain_lock.lock().await;
505
506        // Only create SKDM when no sender key exists (matches WA Web behavior)
507        let device_snapshot = self.client.persistence_manager.get_device_snapshot();
508        let key_exists = self
509            .client
510            .signal_cache
511            .get_sender_key(&sender_key_name, &*device_snapshot.backend)
512            .await?
513            .is_some();
514
515        let mut store = self.client.sender_key_adapter().await;
516        let mut rng = rand::make_rng::<rand::rngs::StdRng>();
517
518        let pending_distribution = self
519            .client
520            .signal_cache
521            .pending_sender_key_distribution(&sender_key_name)
522            .await;
523        let skdm_bytes = if let Some(distribution) = pending_distribution {
524            Some(distribution.as_ref().to_vec())
525        } else if !key_exists {
526            let distribution = wacore::send::create_sender_key_distribution_message_for_group(
527                &mut store,
528                &sender_key_name,
529            )
530            .await?;
531            self.client
532                .signal_cache
533                .cache_pending_sender_key_distribution(
534                    &sender_key_name,
535                    std::sync::Arc::from(distribution.clone()),
536                )
537                .await;
538            Some(distribution)
539        } else {
540            None
541        };
542
543        let ciphertext =
544            wacore::send::encrypt_group_message(&mut store, &sender_key_name, plaintext, &mut rng)
545                .await?;
546
547        // The durability gate can need the processing permit, whose holder may
548        // need this chain lock.
549        drop(_chain_guard);
550        self.client.persist_signal_state_pre_wire().await?;
551
552        if let Some(distribution) = &skdm_bytes {
553            self.client
554                .signal_cache
555                .clear_pending_sender_key_distribution(&sender_key_name, distribution)
556                .await;
557        }
558
559        Ok((skdm_bytes, ciphertext.into_serialized().into_vec()))
560    }
561
562    /// Decrypt a group (sender-key) message.
563    ///
564    /// Returns raw padded plaintext. Use [`MessageUtils::unpad_message_ref`]
565    /// with the stanza's `v` attribute if WhatsApp message unpadding is needed.
566    ///
567    /// Concurrent mutations of the same sender-key chain are serialized.
568    pub async fn decrypt_group_message(
569        &self,
570        group_jid: &Jid,
571        sender_jid: &Jid,
572        ciphertext: &[u8],
573    ) -> Result<Vec<u8>, SignalError> {
574        let sender_key_name =
575            make_sender_key_name(group_jid, &sender_jid.to_non_ad().to_protocol_address());
576
577        let mut store = self.client.sender_key_adapter().await;
578        let chain_lock = store.sender_key_lock(&sender_key_name).await;
579        let _chain_guard = chain_lock.lock().await;
580
581        let plaintext =
582            wacore::libsignal::protocol::group_decrypt(ciphertext, &mut store, &sender_key_name)
583                .await?;
584
585        drop(_chain_guard);
586        self.client.flush_signal_cache_batch_safe().await?;
587
588        Ok(plaintext.to_vec())
589    }
590
591    /// Check whether a Signal session exists for `jid`.
592    ///
593    /// PN JIDs are resolved to LID when a LID mapping exists, matching
594    /// the encrypt/decrypt paths.
595    pub async fn validate_session(&self, jid: &Jid) -> Result<bool, SignalError> {
596        let resolved = self.client.resolve_encryption_jid(jid).await;
597        let signal_addr = resolved.to_protocol_address();
598        let device_snapshot = self.client.persistence_manager.get_device_snapshot();
599        let exists = self
600            .client
601            .signal_cache
602            .has_session(&signal_addr, &*device_snapshot.backend)
603            .await
604            .map_err(|e| SignalError::Internal(e.context("session check failed")))?;
605        if exists || !self.migrate_legacy_pairwise_state(jid, &resolved).await? {
606            return Ok(exists);
607        }
608        self.client
609            .signal_cache
610            .has_session(&signal_addr, &*device_snapshot.backend)
611            .await
612            .map_err(|e| SignalError::Internal(e.context("session check failed")))
613    }
614
615    /// Delete Signal sessions and identity keys for the given JIDs.
616    ///
617    /// Matches WA Web's `deleteRemoteSession` which removes both session
618    /// and identity as a paired operation. Changes are flushed to the
619    /// persistent backend before returning.
620    ///
621    /// When a supplied PN JID resolves to LID, both namespace representations
622    /// are removed so legacy PN state cannot be migrated back after deletion.
623    pub async fn delete_sessions(&self, jids: &[Jid]) -> Result<(), SignalError> {
624        for jid in jids {
625            let resolved = self.client.resolve_encryption_jid(jid).await;
626            self.delete_pairwise_state_at(jid).await;
627            if resolved != *jid {
628                self.delete_pairwise_state_at(&resolved).await;
629            }
630        }
631
632        self.client.flush_signal_cache_batch_safe().await?;
633        Ok(())
634    }
635
636    /// Create encrypted participant `<to>` nodes for the given recipient JIDs.
637    ///
638    /// Resolves devices, ensures Signal sessions, encrypts the message for
639    /// each device, and returns the resulting XML nodes.
640    ///
641    /// Returns `(nodes, should_include_device_identity)`.
642    pub async fn create_participant_nodes(
643        &self,
644        recipient_jids: &[Jid],
645        message: &waproto::whatsapp::Message,
646    ) -> Result<(Vec<Node>, bool), SignalError> {
647        let device_jids = self.client.get_user_devices(recipient_jids).await?;
648        self.client.ensure_e2e_sessions(&device_jids).await?;
649
650        // Acquire per-device session locks before encrypting (matches DM send path)
651        let lock_jids = self.client.build_session_lock_keys(&device_jids).await;
652        let _session_guards = self.client.session_guards_for(&lock_jids).await;
653
654        let plaintext = MessageUtils::encode_and_pad(message);
655        let mut adapter = self.client.signal_adapter().await;
656        let mediatype = wacore::send::media_type_from_message(message);
657        let hide_decrypt_fail = wacore::send::should_hide_decrypt_fail(message);
658
659        let mut stores = adapter.as_signal_stores();
660        let result = wacore::send::encrypt_for_devices(
661            &*self.client.runtime,
662            &mut stores,
663            self.client,
664            &device_jids,
665            &plaintext,
666            hide_decrypt_fail,
667            mediatype,
668        )
669        .await?;
670
671        drop(_session_guards);
672        self.client.persist_signal_state_pre_wire().await?;
673
674        Ok((result.participant_nodes, result.includes_prekey_message))
675    }
676
677    /// Ensure E2E sessions exist for the given JIDs.
678    pub async fn assert_sessions(&self, jids: &[Jid]) -> Result<(), SignalError> {
679        self.client.ensure_e2e_sessions(jids).await?;
680        Ok(())
681    }
682
683    /// Get all known device JIDs for the given user JIDs via usync.
684    pub async fn get_user_devices(&self, jids: &[Jid]) -> Result<Vec<Jid>, SignalError> {
685        Ok(self.client.get_user_devices(jids).await?)
686    }
687}
688
689impl Client {
690    /// Access low-level Signal protocol operations.
691    pub fn signal(&self) -> Signal<'_> {
692        Signal::new(self)
693    }
694}
695
696#[cfg(test)]
697mod tests {
698    use super::*;
699    use std::sync::Arc;
700    use std::sync::atomic::Ordering;
701
702    use wacore::store::in_memory::InMemoryBackend;
703    use wacore::store::traits::{DeviceInfo, DeviceListRecord, SignalStore};
704    use wacore_binary::Server;
705
706    use crate::lid_pn_cache::LearningSource;
707    use crate::test_utils::seed_peer_session;
708
709    async fn memory_client() -> (Arc<Client>, Arc<InMemoryBackend>) {
710        let backend = Arc::new(InMemoryBackend::new());
711        let client = crate::test_utils::create_test_client_with_backend(backend.clone()).await;
712        client
713            .persistence_manager
714            .process_command(crate::store::commands::DeviceCommand::SetId(Some(
715                Jid::new("15550001000", Server::Pn),
716            )))
717            .await;
718        (client, backend)
719    }
720
721    fn peer_prekey_bundle(registration_id: u32, device_id: u32) -> PreKeyBundle {
722        use wacore::libsignal::protocol::{IdentityKeyPair, KeyPair};
723
724        let mut rng = rand::make_rng::<rand::rngs::StdRng>();
725        let identity = IdentityKeyPair::generate(&mut rng);
726        let signed_prekey = KeyPair::generate(&mut rng);
727        let prekey = KeyPair::generate(&mut rng);
728        let signature = identity
729            .private_key()
730            .calculate_signature(&signed_prekey.public_key.serialize(), &mut rng)
731            .expect("signed prekey signature");
732        PreKeyBundle::new(
733            registration_id,
734            device_id.into(),
735            Some((7u32.into(), prekey.public_key)),
736            9u32.into(),
737            signed_prekey.public_key,
738            signature.to_vec(),
739            *identity.identity_key(),
740        )
741        .expect("prekey bundle")
742    }
743
744    async fn seed_legacy_pn_session_with_mapping(
745        client: &Arc<Client>,
746        pn: &Jid,
747        lid: &Jid,
748        registration_id: u32,
749    ) {
750        client
751            .signal()
752            .install_prekey_bundle(
753                pn,
754                &peer_prekey_bundle(registration_id, u32::from(pn.device)),
755            )
756            .await
757            .expect("install legacy PN session");
758        client
759            .lid_pn_cache
760            .warm_up([crate::lid_pn_cache::LidPnEntry::new(
761                lid.user.to_string(),
762                pn.user.to_string(),
763                LearningSource::Other,
764            )])
765            .await;
766    }
767
768    #[tokio::test]
769    async fn supplied_prekey_bundle_exposes_session_info() {
770        let (client, _) = memory_client().await;
771        let peer = Jid::pn_device("15550002000", 2);
772        let bundle = peer_prekey_bundle(4242, u32::from(peer.device));
773
774        client
775            .signal()
776            .install_prekey_bundle(&peer, &bundle)
777            .await
778            .expect("install bundle");
779
780        assert!(client.signal().validate_session(&peer).await.unwrap());
781        let info = client
782            .signal()
783            .session_info(&peer)
784            .await
785            .unwrap()
786            .expect("open session info");
787        assert_eq!(info.registration_id, 4242);
788        assert!(!info.base_key.is_empty());
789    }
790
791    #[tokio::test]
792    async fn supplied_prekey_bundle_uses_known_lid_namespace() {
793        let (client, backend) = memory_client().await;
794        let pn = Jid::pn_device("15550002002", 2);
795        let lid = Jid::lid_device("100000000000002", 2);
796        client
797            .add_lid_pn_mapping(&lid.user, &pn.user, LearningSource::PeerPnMessage)
798            .await
799            .unwrap();
800
801        client
802            .signal()
803            .install_prekey_bundle(&pn, &peer_prekey_bundle(4244, u32::from(pn.device)))
804            .await
805            .expect("install mapped bundle");
806
807        assert!(client.signal().validate_session(&pn).await.unwrap());
808        assert!(
809            backend
810                .get_session(pn.to_protocol_address().as_str())
811                .await
812                .unwrap()
813                .is_none(),
814            "the obsolete phone-number slot must stay empty"
815        );
816        assert!(
817            backend
818                .get_session(lid.to_protocol_address().as_str())
819                .await
820                .unwrap()
821                .is_some(),
822            "the installed session must be durable in the resolved namespace"
823        );
824    }
825
826    #[tokio::test]
827    async fn facade_lookup_migrates_a_legacy_pn_session_on_lid_miss() {
828        let (client, backend) = memory_client().await;
829        let pn = Jid::pn_device("15550002003", 3);
830        let lid = Jid::lid_device("100000000000003", 3);
831        seed_legacy_pn_session_with_mapping(&client, &pn, &lid, 4245).await;
832
833        assert!(
834            backend
835                .get_session(lid.to_protocol_address().as_str())
836                .await
837                .unwrap()
838                .is_none(),
839            "the fixture must begin with state only in the PN namespace"
840        );
841        assert!(client.signal().validate_session(&pn).await.unwrap());
842        assert_eq!(
843            client
844                .signal()
845                .session_info(&pn)
846                .await
847                .unwrap()
848                .expect("migrated session info")
849                .registration_id,
850            4245
851        );
852        assert!(
853            backend
854                .get_session(pn.to_protocol_address().as_str())
855                .await
856                .unwrap()
857                .is_none()
858        );
859        assert!(
860            backend
861                .get_session(lid.to_protocol_address().as_str())
862                .await
863                .unwrap()
864                .is_some()
865        );
866    }
867
868    #[tokio::test]
869    async fn facade_encrypt_retries_after_migrating_a_legacy_pn_session() {
870        let (client, backend) = memory_client().await;
871        let pn = Jid::pn_device("15550002004", 4);
872        let lid = Jid::lid_device("100000000000004", 4);
873        seed_legacy_pn_session_with_mapping(&client, &pn, &lid, 4246).await;
874
875        let (_, ciphertext) = client
876            .signal()
877            .encrypt_message(&pn, b"legacy namespace")
878            .await
879            .expect("encrypt after lazy migration");
880        assert!(!ciphertext.is_empty());
881        assert!(
882            backend
883                .get_session(pn.to_protocol_address().as_str())
884                .await
885                .unwrap()
886                .is_none()
887        );
888        assert!(
889            backend
890                .get_session(lid.to_protocol_address().as_str())
891                .await
892                .unwrap()
893                .is_some()
894        );
895    }
896
897    #[tokio::test]
898    async fn delete_sessions_removes_legacy_and_resolved_namespaces() {
899        let (client, backend) = memory_client().await;
900        let pn = Jid::pn_device("15550002005", 5);
901        let lid = Jid::lid_device("100000000000005", 5);
902        seed_legacy_pn_session_with_mapping(&client, &pn, &lid, 4247).await;
903        let pn_address = pn.to_protocol_address();
904        let lid_address = lid.to_protocol_address();
905
906        assert!(
907            backend
908                .get_session(pn_address.as_str())
909                .await
910                .unwrap()
911                .is_some()
912        );
913        assert!(
914            backend
915                .load_identity(pn_address.as_str())
916                .await
917                .unwrap()
918                .is_some()
919        );
920
921        client
922            .signal()
923            .delete_sessions(std::slice::from_ref(&pn))
924            .await
925            .expect("delete both known namespaces");
926
927        for address in [&pn_address, &lid_address] {
928            assert!(
929                backend
930                    .get_session(address.as_str())
931                    .await
932                    .unwrap()
933                    .is_none()
934            );
935            assert!(
936                backend
937                    .load_identity(address.as_str())
938                    .await
939                    .unwrap()
940                    .is_none()
941            );
942        }
943        assert!(!client.signal().validate_session(&pn).await.unwrap());
944    }
945
946    #[tokio::test]
947    async fn session_info_waits_for_pairwise_mutations() {
948        let (client, _) = memory_client().await;
949        let peer = Jid::pn_device("15550002001", 2);
950        let bundle = peer_prekey_bundle(4243, u32::from(peer.device));
951
952        client
953            .signal()
954            .install_prekey_bundle(&peer, &bundle)
955            .await
956            .expect("install bundle");
957
958        let address = peer.to_protocol_address();
959        let session_mutex = client.session_lock_for(address.as_str()).await;
960        let session_guard = session_mutex.lock().await;
961        assert!(
962            tokio::time::timeout(
963                std::time::Duration::from_millis(100),
964                client.signal().session_info(&peer),
965            )
966            .await
967            .is_err(),
968            "inspection must not observe a session while a pairwise mutation owns it"
969        );
970
971        drop(session_guard);
972        assert!(client.signal().session_info(&peer).await.unwrap().is_some());
973    }
974
975    #[tokio::test]
976    async fn session_migration_reports_moves_for_both_user_namespaces() {
977        for (from_server, to_server) in [
978            (Server::Pn, Server::Lid),
979            (Server::Hosted, Server::HostedLid),
980        ] {
981            let (client, _) = memory_client().await;
982            let from = Jid::new("15550003000", from_server).with_device(3);
983            let to = Jid::new("100000000000003", to_server).with_device(3);
984            let bundle = peer_prekey_bundle(4343, 3);
985            client
986                .signal()
987                .install_prekey_bundle(&from, &bundle)
988                .await
989                .expect("install source session");
990
991            let outcome = client
992                .signal()
993                .migrate_sessions(&from, &to)
994                .await
995                .expect("migrate session");
996            assert_eq!(outcome.migrated, 1);
997            assert_eq!(outcome.skipped, 0);
998            assert_eq!(outcome.total, 1);
999            assert_eq!(outcome.migrated_identities, 1);
1000            assert_eq!(outcome.discarded_identities, 0);
1001            assert_eq!(outcome.skipped_identities, 0);
1002            assert!(outcome.has_state_changes());
1003            assert!(client.signal().session_info(&from).await.unwrap().is_none());
1004            assert!(client.signal().session_info(&to).await.unwrap().is_some());
1005        }
1006    }
1007
1008    #[tokio::test]
1009    async fn session_migration_rejects_mismatched_namespaces() {
1010        let (client, _) = memory_client().await;
1011        for (from, to) in [
1012            (
1013                Jid::new("15550003000", Server::Pn),
1014                Jid::new("100000000000003", Server::HostedLid),
1015            ),
1016            (
1017                Jid::new("15550003000", Server::Hosted),
1018                Jid::new("100000000000003", Server::Lid),
1019            ),
1020        ] {
1021            assert!(
1022                matches!(
1023                    client.signal().migrate_sessions(&from, &to).await,
1024                    Err(SignalError::InvalidInput(_))
1025                ),
1026                "mismatched namespace pair {from} -> {to} must be rejected"
1027            );
1028        }
1029    }
1030
1031    #[tokio::test]
1032    async fn session_migration_retries_pending_durability_after_flush_failure() {
1033        let (client, backend) = memory_client().await;
1034        let from = Jid::new("15550003001", Server::Pn);
1035        let to = Jid::new("100000000000004", Server::Lid);
1036        let from_device = from.with_device(4);
1037        let to_device = to.with_device(4);
1038        client
1039            .signal()
1040            .install_prekey_bundle(&from_device, &peer_prekey_bundle(4344, 4))
1041            .await
1042            .expect("install source session");
1043
1044        backend.set_fail_session_writes(true);
1045        assert!(
1046            client.signal().migrate_sessions(&from, &to).await.is_err(),
1047            "the injected durability failure must reach the caller"
1048        );
1049        assert!(
1050            client
1051                .signal_cache
1052                .has_pending_pairwise_writes_for_user(&from.user)
1053                .await
1054        );
1055
1056        backend.set_fail_session_writes(false);
1057        let attempts_before_retry = backend.session_batch_write_count();
1058        let retry = client
1059            .signal()
1060            .migrate_sessions(&from, &to)
1061            .await
1062            .expect("retry pending migration flush");
1063
1064        assert!(
1065            !retry.has_state_changes(),
1066            "the cache already reflects the move"
1067        );
1068        assert!(backend.session_batch_write_count() > attempts_before_retry);
1069        assert!(
1070            backend
1071                .get_session(from_device.to_protocol_address().as_str())
1072                .await
1073                .unwrap()
1074                .is_none()
1075        );
1076        assert!(
1077            backend
1078                .get_session(to_device.to_protocol_address().as_str())
1079                .await
1080                .unwrap()
1081                .is_some()
1082        );
1083        assert!(
1084            !client
1085                .signal_cache
1086                .has_pending_pairwise_writes_for_user(&from.user)
1087                .await
1088        );
1089    }
1090
1091    #[tokio::test]
1092    async fn sender_key_distribution_roundtrip_uses_shared_store() {
1093        let (sender, sender_backend) = memory_client().await;
1094        let (receiver, _) = memory_client().await;
1095        let group = Jid::new("120363000000000001", Server::Group);
1096        let author = Jid::new("15550001000", Server::Pn);
1097
1098        assert!(
1099            !sender
1100                .signal()
1101                .has_sender_key(&group, &author)
1102                .await
1103                .unwrap()
1104        );
1105
1106        let distribution = sender
1107            .signal()
1108            .sender_key_distribution(&group, &author)
1109            .await
1110            .expect("create distribution");
1111        assert!(!distribution.is_empty());
1112        assert!(
1113            sender
1114                .signal()
1115                .has_sender_key(&group, &author)
1116                .await
1117                .unwrap()
1118        );
1119
1120        receiver
1121            .signal()
1122            .process_sender_key_distribution(&group, &author, &distribution)
1123            .await
1124            .expect("process distribution");
1125
1126        let (_, ciphertext) = sender
1127            .signal()
1128            .encrypt_group_message(&group, b"sender-key payload")
1129            .await
1130            .expect("group encrypt");
1131        let plaintext = receiver
1132            .signal()
1133            .decrypt_group_message(&group, &author, &ciphertext)
1134            .await
1135            .expect("group decrypt");
1136        assert_eq!(plaintext, b"sender-key payload");
1137
1138        let sender_key_name = make_sender_key_name(&group, &author.to_protocol_address());
1139        let chain_lock = sender.signal_cache.sender_key_lock(&sender_key_name).await;
1140        let chain_guard = chain_lock.lock().await;
1141        let signal = sender.signal();
1142        let mut deletion = Box::pin(signal.delete_sender_key(&group, &author));
1143        assert!(
1144            tokio::time::timeout(std::time::Duration::from_millis(100), &mut deletion)
1145                .await
1146                .is_err(),
1147            "deletion must wait for an in-flight chain mutation"
1148        );
1149        drop(chain_guard);
1150        tokio::time::timeout(std::time::Duration::from_secs(5), deletion)
1151            .await
1152            .expect("delete must finish after the chain unlocks")
1153            .expect("delete sender key");
1154        assert!(
1155            !sender
1156                .signal()
1157                .has_sender_key(&group, &author)
1158                .await
1159                .unwrap()
1160        );
1161        assert!(
1162            sender_backend
1163                .get_sender_key(sender_key_name.cache_key())
1164                .await
1165                .unwrap()
1166                .is_none(),
1167            "delete must be durable before returning"
1168        );
1169    }
1170
1171    #[tokio::test]
1172    async fn group_encrypt_flushes_only_at_sender_key_lease_boundaries() {
1173        use wacore::libsignal::protocol::consts::SENDER_CHAIN_RESERVATION_BATCH;
1174
1175        let (client, backend) = memory_client().await;
1176        let group = Jid::new("120363000000000000", Server::Group);
1177
1178        let (skdm, _) = client
1179            .signal()
1180            .encrypt_group_message(&group, b"first")
1181            .await
1182            .expect("first group encrypt");
1183        assert!(skdm.is_some(), "the first encrypt must create an SKDM");
1184        assert_eq!(backend.sender_key_batch_write_count(), 1);
1185
1186        client
1187            .signal_flush_test_block
1188            .store(true, Ordering::Release);
1189        for _ in 1..SENDER_CHAIN_RESERVATION_BATCH {
1190            let (skdm, _) = client
1191                .signal()
1192                .encrypt_group_message(&group, b"warm")
1193                .await
1194                .expect("lease-covered group encrypt");
1195            assert!(skdm.is_none(), "a warm chain must reuse its SKDM");
1196        }
1197        assert_eq!(
1198            backend.sender_key_batch_write_count(),
1199            1,
1200            "lease-covered iterations must not flush synchronously"
1201        );
1202
1203        client
1204            .signal()
1205            .encrypt_group_message(&group, b"boundary")
1206            .await
1207            .expect("boundary group encrypt");
1208        assert_eq!(
1209            backend.sender_key_batch_write_count(),
1210            2,
1211            "raising the next lease must flush before returning ciphertext"
1212        );
1213        client
1214            .signal_flush_test_block
1215            .store(false, Ordering::Release);
1216    }
1217
1218    #[tokio::test]
1219    async fn group_encrypt_retry_preserves_distribution_after_flush_failure() {
1220        let (sender, backend) = memory_client().await;
1221        let (receiver, _) = memory_client().await;
1222        let group = Jid::new("120363000000000002", Server::Group);
1223        let author = Jid::new("15550001000", Server::Pn);
1224
1225        backend.set_fail_sender_key_writes(true);
1226        assert!(
1227            sender
1228                .signal()
1229                .encrypt_group_message(&group, b"failed attempt")
1230                .await
1231                .is_err(),
1232            "the injected durability failure must reach the caller"
1233        );
1234
1235        backend.set_fail_sender_key_writes(false);
1236        let (distribution, ciphertext) = sender
1237            .signal()
1238            .encrypt_group_message(&group, b"retry payload")
1239            .await
1240            .expect("retry group encryption");
1241        let distribution = distribution.expect("retry must retain the pending distribution");
1242        receiver
1243            .signal()
1244            .process_sender_key_distribution(&group, &author, &distribution)
1245            .await
1246            .expect("process retained distribution");
1247        assert_eq!(
1248            receiver
1249                .signal()
1250                .decrypt_group_message(&group, &author, &ciphertext)
1251                .await
1252                .expect("decrypt retry ciphertext"),
1253            b"retry payload"
1254        );
1255
1256        let (distribution, _) = sender
1257            .signal()
1258            .encrypt_group_message(&group, b"warm payload")
1259            .await
1260            .expect("warm group encryption");
1261        assert!(
1262            distribution.is_none(),
1263            "the retained distribution must clear after a successful retry"
1264        );
1265    }
1266
1267    #[tokio::test]
1268    async fn participant_fanout_reuses_durable_session_leases() {
1269        let (client, backend) = memory_client().await;
1270        let recipient = Jid::new("15550002000", Server::Pn);
1271        client
1272            .update_device_list(DeviceListRecord {
1273                user: recipient.user.to_string(),
1274                devices: vec![DeviceInfo::new(0, None), DeviceInfo::new(1, None)],
1275                timestamp: wacore::time::now_secs(),
1276                phash: None,
1277                raw_id: None,
1278            })
1279            .await
1280            .expect("device registry");
1281
1282        let devices = [recipient.with_device(0), recipient.with_device(1)];
1283        for device in &devices {
1284            seed_peer_session(&client, device).await;
1285            client
1286                .signal()
1287                .encrypt_message(device, b"warm lease")
1288                .await
1289                .expect("lease warmup");
1290        }
1291        let writes_before = backend.session_batch_write_count();
1292
1293        client
1294            .signal_flush_test_block
1295            .store(true, Ordering::Release);
1296        let message = waproto::whatsapp::Message {
1297            conversation: Some("fanout".into()),
1298            ..Default::default()
1299        };
1300        let (nodes, _) = client
1301            .signal()
1302            .create_participant_nodes(std::slice::from_ref(&recipient), &message)
1303            .await
1304            .expect("participant fanout");
1305        assert_eq!(nodes.len(), devices.len());
1306        assert_eq!(
1307            backend.session_batch_write_count(),
1308            writes_before,
1309            "a warm fanout must not flush durable leases synchronously"
1310        );
1311        client
1312            .signal_flush_test_block
1313            .store(false, Ordering::Release);
1314    }
1315}