1use 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#[derive(Debug, Error)]
22#[non_exhaustive]
23pub enum SignalError {
24 #[error("{0}")]
26 Protocol(#[from] SignalProtocolError),
27 #[error("unsupported signal operation: {0}")]
30 Unsupported(String),
31 #[error("invalid signal input: {0}")]
33 InvalidInput(String),
34 #[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#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct SignalSessionInfo {
51 pub base_key: Vec<u8>,
53 pub registration_id: u32,
55}
56
57#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
59#[non_exhaustive]
60pub struct SignalSessionMigration {
61 pub migrated: usize,
63 pub skipped: usize,
65 pub total: usize,
67 pub migrated_identities: usize,
69 pub discarded_identities: usize,
71 pub skipped_identities: usize,
73}
74
75impl SignalSessionMigration {
76 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
132pub 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 pub async fn assert_sessions(&self, jids: &[Jid]) -> Result<(), SignalError> {
679 self.client.ensure_e2e_sessions(jids).await?;
680 Ok(())
681 }
682
683 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 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}