1use crate::client::Client;
8use anyhow;
9use anyhow::Context as _;
10use log;
11
12use std::sync::atomic::Ordering;
13use wacore::iq::prekeys::{
14 DigestKeyBundleSpec, PreKeyCountSpec, PreKeyFetchReason, PreKeyFetchSpec, PreKeyUploadSpec,
15};
16use wacore::libsignal::protocol::{KeyPair, PublicKey};
17use wacore::libsignal::store::record_helpers::encode_pre_key_record_to;
18use wacore::store::commands::DeviceCommand;
19use wacore_binary::Jid;
20
21pub use wacore::prekeys::PreKeyUtils;
22
23pub(crate) const DEFAULT_WANTED_PRE_KEY_COUNT: usize = 812;
26
27const MIN_PRE_KEY_COUNT: usize = 5;
28
29fn should_upload_pre_keys(force: bool, server_count: usize) -> bool {
33 force || server_count < MIN_PRE_KEY_COUNT
34}
35
36const MAX_PREKEY_ID: u32 = 16_777_215;
38
39fn start_prekey_id(next_pre_key_id: u32, max_store_id: u32) -> u32 {
44 let raw = if next_pre_key_id > 0 {
45 std::cmp::max(next_pre_key_id as u64, max_store_id as u64 + 1)
46 } else {
47 max_store_id as u64 + 1
48 };
49 ((raw - 1) % MAX_PREKEY_ID as u64) as u32 + 1
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57struct PreKeyUploadPlan {
58 window_start: u32,
61 available: u32,
63 gen_count: u32,
65 gen_start: u32,
67 new_next: u32,
69}
70
71fn plan_prekey_upload(
81 first_unupload: u32,
82 next: u32,
83 max_store_id: u32,
84 wanted: usize,
85) -> PreKeyUploadPlan {
86 let wanted = wanted as u64;
87 let first = first_unupload as u64;
88 let next_eff = next as u64;
89
90 let fresh_window = |start: u64| {
91 let start = if start + wanted - 1 > MAX_PREKEY_ID as u64 {
92 1
93 } else {
94 start
95 };
96 PreKeyUploadPlan {
97 window_start: start as u32,
98 available: 0,
99 gen_count: wanted as u32,
100 gen_start: start as u32,
101 new_next: (start + wanted) as u32,
102 }
103 };
104
105 if first == 0 || first > next_eff {
106 return fresh_window(start_prekey_id(next, max_store_id) as u64);
107 }
108 let available = (next_eff - first).min(wanted);
113 let gen_count = wanted - available;
114 if first + wanted - 1 > MAX_PREKEY_ID as u64
115 || (gen_count > 0 && next_eff + gen_count - 1 > MAX_PREKEY_ID as u64)
116 {
117 return fresh_window(1);
121 }
122 PreKeyUploadPlan {
123 window_start: first as u32,
124 available: available as u32,
125 gen_count: gen_count as u32,
126 gen_start: next_eff as u32,
127 new_next: (next_eff + gen_count) as u32,
128 }
129}
130
131const MAX_PRE_KEY_UPLOAD_BATCH: usize = u16::MAX as usize;
136
137fn clamp_wanted_pre_key_count(n: usize) -> usize {
141 n.clamp(MIN_PRE_KEY_COUNT, MAX_PRE_KEY_UPLOAD_BATCH)
142}
143
144impl Client {
145 #[cfg_attr(feature = "tracing", tracing::instrument(name = "wa.session.fetch_pre_keys", level = "debug", skip_all, fields(count = jids.len()), err(Debug)))]
146 pub(crate) async fn fetch_pre_keys(
147 &self,
148 jids: &[Jid],
149 reason: Option<PreKeyFetchReason>,
150 ) -> Result<wacore::prekeys::PreKeyFetchOutcome, anyhow::Error> {
151 let spec = match reason {
152 Some(r) => PreKeyFetchSpec::with_reason(jids.to_vec(), r),
153 None => PreKeyFetchSpec::new(jids.to_vec()),
154 };
155
156 let spec = spec.with_account_identities(self.collect_account_identities(jids).await);
163
164 let outcome = self.execute(spec).await?;
165
166 for jid in outcome.bundles.keys() {
167 log::debug!("Successfully parsed pre-key bundle for {}", jid.observe());
168 }
169
170 Ok(outcome)
171 }
172
173 async fn collect_account_identities(
178 &self,
179 jids: &[Jid],
180 ) -> std::collections::HashMap<Jid, [u8; 32]> {
181 use futures::StreamExt;
182 const COMPANION_IDENTITY_LOAD_CONCURRENCY: usize = 16;
186 let companions: Vec<Jid> = jids.iter().filter(|j| j.device != 0).cloned().collect();
187 futures::stream::iter(companions)
188 .map(|jid| async move { self.load_account_identity(&jid).await.map(|id| (jid, id)) })
189 .buffer_unordered(COMPANION_IDENTITY_LOAD_CONCURRENCY)
190 .filter_map(|entry| async move { entry })
191 .collect()
192 .await
193 }
194
195 pub(crate) async fn load_account_identity(&self, companion_jid: &Jid) -> Option<[u8; 32]> {
201 use wacore::types::jid::JidExt;
202
203 let account_jid = companion_jid.with_device(0);
204 let addr = account_jid.to_protocol_address();
205 let backend = self
206 .persistence_manager
207 .get_device_snapshot()
208 .backend
209 .clone();
210 match self.signal_cache.get_identity(&addr, &*backend).await {
211 Ok(Some(id)) if id.len() == 32 => {
212 let mut arr = [0u8; 32];
213 arr.copy_from_slice(&id);
214 Some(arr)
215 }
216 Ok(_) => None,
217 Err(e) => {
218 log::debug!(
219 "ADV fallback: failed to load account identity for {}: {}",
220 account_jid.observe(),
221 e
222 );
223 None
224 }
225 }
226 }
227
228 #[cfg_attr(
230 feature = "tracing",
231 tracing::instrument(
232 name = "wa.session.server_pre_key_count",
233 level = "debug",
234 skip_all,
235 err(Debug)
236 )
237 )]
238 pub(crate) async fn get_server_pre_key_count(&self) -> Result<usize, crate::request::IqError> {
239 let response = self.execute(PreKeyCountSpec::new()).await?;
240 Ok(response.count)
241 }
242
243 #[cfg_attr(
246 feature = "tracing",
247 tracing::instrument(
248 name = "wa.session.upload_pre_keys_at_login",
249 level = "debug",
250 skip_all,
251 err(Debug)
252 )
253 )]
254 pub(crate) async fn upload_pre_keys_at_login(&self) -> Result<(), anyhow::Error> {
255 let has_prekeys = self
256 .persistence_manager
257 .get_device_snapshot()
258 .server_has_prekeys;
259
260 if has_prekeys {
261 log::debug!("Server has prekeys (persisted flag), skipping login upload.");
262 return Ok(());
263 }
264
265 let _guard = self.prekey_upload_lock.lock().await;
267
268 if self
270 .persistence_manager
271 .get_device_snapshot()
272 .server_has_prekeys
273 {
274 return Ok(());
275 }
276
277 log::info!("Server missing prekeys (persisted flag), uploading.");
278 let r = self.upload_pre_keys_inner().await;
280 wacore::telemetry::prekey_upload(if r.is_ok() { "ok" } else { "fail" });
281 r
282 }
283
284 #[cfg_attr(
285 feature = "tracing",
286 tracing::instrument(
287 name = "wa.session.upload_pre_keys",
288 level = "debug",
289 skip_all,
290 fields(force = force, wanted = ?wanted),
291 err(Debug)
292 )
293 )]
294 async fn upload_pre_keys_with_count(
295 &self,
296 force: bool,
297 wanted: Option<usize>,
298 ) -> Result<(), anyhow::Error> {
299 if !force {
304 let server_count = self
305 .get_server_pre_key_count()
306 .await
307 .map_err(|e| anyhow::anyhow!(e))?;
308
309 if !should_upload_pre_keys(force, server_count) {
310 log::debug!("Server has {server_count} pre-keys, no upload needed.");
311 return Ok(());
312 }
313
314 log::debug!("Server has {server_count} pre-keys, uploading.");
315 }
316
317 match wanted {
318 Some(wanted) => self.upload_pre_keys_inner_with_count(wanted).await,
319 None => self.upload_pre_keys_inner().await,
320 }
321 }
322
323 #[cfg_attr(
331 feature = "tracing",
332 tracing::instrument(
333 name = "wa.session.get_or_gen_prekey",
334 level = "debug",
335 skip_all,
336 err(Debug)
337 )
338 )]
339 pub(crate) async fn get_or_gen_single_pre_key(
340 &self,
341 ) -> Result<(u32, PublicKey), anyhow::Error> {
342 let device_snapshot = self.persistence_manager.get_device_snapshot();
343 let backend = device_snapshot.backend.clone();
344 let max_id = backend.get_max_prekey_id().await?;
345 let plan = plan_prekey_upload(
346 device_snapshot.first_unupload_pre_key_id,
347 device_snapshot.next_pre_key_id,
348 max_id,
349 1,
350 );
351
352 if plan.gen_count == 0 {
353 let window_ids: Vec<u32> =
360 (plan.window_start..device_snapshot.next_pre_key_id).collect();
361 let mut rows = backend.load_prekeys_batch(&window_ids).await?;
362 rows.sort_unstable_by_key(|(id, _)| *id);
363 if let Some((id, record)) = rows.into_iter().next() {
364 if id != plan.window_start {
365 log::warn!(
366 "prekey window head {} missing from store; advancing to {id}",
367 plan.window_start
368 );
369 self.persistence_manager
370 .process_command(DeviceCommand::SetPreKeyWatermarks {
371 next_pre_key_id: device_snapshot.next_pre_key_id,
372 first_unupload_pre_key_id: id,
373 })
374 .await;
375 }
376 let structure = waproto::codec::pre_key_record_decode(&record)?;
377 let record = wacore::libsignal::store::record_helpers::prekey_structure_to_record(
378 structure,
379 )?;
380 return Ok((id, record.key_pair()?.public_key));
381 }
382 log::warn!(
383 "prekey window [{}, {}) fully consumed; generating fresh",
384 plan.window_start,
385 device_snapshot.next_pre_key_id
386 );
387 }
388
389 let id = if plan.gen_count > 0 {
390 plan.gen_start
391 } else {
392 let raw = device_snapshot
396 .next_pre_key_id
397 .max(plan.window_start.saturating_add(1));
398 if raw > MAX_PREKEY_ID { 1 } else { raw }
399 };
400 let key_pair = KeyPair::generate(&mut rand::make_rng::<rand::rngs::StdRng>());
401 let mut encoded_record = Vec::new();
402 encode_pre_key_record_to(id, &key_pair, &mut encoded_record);
403 backend.store_prekey(id, &encoded_record, false).await?;
404 self.persistence_manager
405 .process_command(DeviceCommand::SetPreKeyWatermarks {
406 next_pre_key_id: id.saturating_add(1),
407 first_unupload_pre_key_id: if plan.gen_count > 0 {
408 plan.window_start
409 } else {
410 id
411 },
412 })
413 .await;
414 self.persistence_manager
418 .flush()
419 .await
420 .context("failed to flush prekey watermarks")?;
421 Ok((id, key_pair.public_key))
422 }
423
424 pub(crate) async fn mark_single_prekey_uploaded(
431 &self,
432 _guard: &async_lock::MutexGuard<'_, ()>,
433 id: u32,
434 ) -> Result<(), anyhow::Error> {
435 let device_snapshot = self.persistence_manager.get_device_snapshot();
436 if device_snapshot.first_unupload_pre_key_id != id {
437 return Ok(());
438 }
439 let next_first = if id >= MAX_PREKEY_ID { 1 } else { id + 1 };
440 let next_pre_key_id =
446 if id >= MAX_PREKEY_ID && device_snapshot.next_pre_key_id > MAX_PREKEY_ID {
447 next_first
448 } else {
449 device_snapshot.next_pre_key_id
450 };
451 self.persistence_manager
452 .process_command(DeviceCommand::SetPreKeyWatermarks {
453 next_pre_key_id,
454 first_unupload_pre_key_id: next_first,
455 })
456 .await;
457 self.persistence_manager
458 .flush()
459 .await
460 .context("failed to flush prekey watermark after mark")?;
461 Ok(())
462 }
463
464 #[cfg_attr(
468 feature = "tracing",
469 tracing::instrument(
470 name = "wa.session.upload_pre_keys_inner",
471 level = "debug",
472 skip_all,
473 err(Debug)
474 )
475 )]
476 async fn upload_pre_keys_inner(&self) -> Result<(), anyhow::Error> {
477 let wanted = self.wanted_pre_key_count.load(Ordering::Relaxed);
478 self.upload_pre_keys_inner_with_count(wanted).await
479 }
480
481 async fn upload_pre_keys_inner_with_count(&self, wanted: usize) -> Result<(), anyhow::Error> {
482 self.upload_pre_keys_pass(true, wanted).await
483 }
484
485 async fn upload_pre_keys_pass(
491 &self,
492 allow_collapse_retry: bool,
493 configured: usize,
494 ) -> Result<(), anyhow::Error> {
495 let device_snapshot = self.persistence_manager.get_device_snapshot();
499 let backend = device_snapshot.backend.clone();
500
501 let wanted = clamp_wanted_pre_key_count(configured);
502 if wanted != configured {
503 log::warn!("wanted_pre_key_count {configured} out of range, clamped to {wanted}");
504 }
505
506 let max_id = backend.get_max_prekey_id().await?;
509 if device_snapshot.first_unupload_pre_key_id == 0 {
510 log::info!(
511 "Initialising prekey upload window (legacy counter = {}, MAX(key_id) = {})",
512 device_snapshot.next_pre_key_id,
513 max_id
514 );
515 }
516 let plan = plan_prekey_upload(
517 device_snapshot.first_unupload_pre_key_id,
518 device_snapshot.next_pre_key_id,
519 max_id,
520 wanted,
521 );
522
523 let mut fresh_pre_keys: Vec<(u32, PublicKey)> = Vec::new();
528 if plan.gen_count > 0 {
529 let gen_start = plan.gen_start;
530 let gen_count = plan.gen_count as usize;
531 let (encoded_batch, generated) = wacore::runtime::blocking(&*self.runtime, move || {
536 let mut rng = rand::make_rng::<rand::rngs::StdRng>();
539 const MAX_RECORD_LEN: usize = 74;
547 let mut pubkeys = Vec::with_capacity(gen_count);
548 let mut offsets = Vec::with_capacity(gen_count);
549 let mut buf = Vec::with_capacity(gen_count * MAX_RECORD_LEN);
550 for i in 0..gen_count {
551 let pre_key_id = gen_start + i as u32;
552 let key_pair = KeyPair::generate(&mut rng);
553 let start = buf.len();
554 encode_pre_key_record_to(pre_key_id, &key_pair, &mut buf);
555 offsets.push((pre_key_id, start..buf.len()));
556 pubkeys.push((pre_key_id, key_pair.public_key));
557 }
558 let shared = bytes::Bytes::from(buf);
559 let encoded_batch: Vec<(u32, bytes::Bytes)> = offsets
560 .into_iter()
561 .map(|(id, range)| (id, shared.slice(range)))
562 .collect();
563 (encoded_batch, pubkeys)
564 })
565 .await;
566
567 backend.store_prekeys_batch(&encoded_batch, false).await?;
573 fresh_pre_keys = generated;
574 }
575
576 self.persistence_manager
581 .process_command(DeviceCommand::SetPreKeyWatermarks {
582 next_pre_key_id: plan.new_next,
583 first_unupload_pre_key_id: plan.window_start,
584 })
585 .await;
586 self.persistence_manager
592 .flush()
593 .await
594 .context("failed to flush prekey watermarks")?;
595
596 let leftover_ids: Vec<u32> = (0..plan.available).map(|i| plan.window_start + i).collect();
601 let mut leftover_rows = if leftover_ids.is_empty() {
602 Vec::new()
603 } else {
604 backend.load_prekeys_batch(&leftover_ids).await?
605 };
606 leftover_rows.sort_unstable_by_key(|(id, _)| *id);
607
608 if plan.gen_count == 0 && leftover_rows.is_empty() {
609 self.persistence_manager
613 .process_command(DeviceCommand::SetPreKeyWatermarks {
614 next_pre_key_id: plan.new_next,
615 first_unupload_pre_key_id: plan.new_next,
616 })
617 .await;
618 if allow_collapse_retry {
619 log::warn!(
620 "prekey window [{}, {}) fully missing; collapsed, regenerating",
621 plan.window_start,
622 plan.new_next
623 );
624 return Box::pin(self.upload_pre_keys_pass(false, configured)).await;
625 }
626 anyhow::bail!("no prekey available to upload");
627 }
628
629 let pre_key_pairs = {
630 let mut pairs: Vec<(u32, PublicKey)> =
631 Vec::with_capacity(leftover_rows.len() + fresh_pre_keys.len());
632 for (id, record) in &leftover_rows {
637 let public_key = waproto::codec::pre_key_record_decode(&record[..])
638 .map_err(anyhow::Error::from)
639 .and_then(|s| {
640 let raw = s
641 .public_key
642 .ok_or_else(|| anyhow::anyhow!("record missing public key"))?;
643 PublicKey::from_djb_public_key_bytes(&raw).map_err(anyhow::Error::from)
644 });
645 match public_key {
646 Ok(public_key) => pairs.push((*id, public_key)),
647 Err(e) => log::warn!("skipping undecodable prekey record {id}: {e:?}"),
648 }
649 }
650 pairs.extend(fresh_pre_keys);
653 if pairs.is_empty() {
654 anyhow::bail!("no decodable prekey available to upload");
655 }
656 pairs
657 };
658 let last_id = pre_key_pairs
659 .last()
660 .map(|(id, _)| *id)
661 .expect("non-empty checked above");
662 let uploaded_count = pre_key_pairs.len();
663 let pre_key_ids: Vec<u32> = pre_key_pairs.iter().map(|(id, _)| *id).collect();
664
665 let spec = PreKeyUploadSpec::new(
666 device_snapshot.registration_id,
667 device_snapshot.identity_key.public_key,
668 device_snapshot.signed_pre_key_id,
669 device_snapshot.signed_pre_key.public_key,
670 device_snapshot.signed_pre_key_signature.to_vec(),
671 pre_key_pairs,
672 );
673
674 self.persistence_manager
681 .process_command(DeviceCommand::SetPreKeyWatermarks {
682 next_pre_key_id: plan.new_next,
683 first_unupload_pre_key_id: plan.window_start.max(last_id.saturating_add(1)),
684 })
685 .await;
686 self.persistence_manager
691 .flush()
692 .await
693 .context("failed to flush abandon watermark")?;
694
695 self.execute(spec).await?;
696
697 let uploaded_ids: Vec<u32> = pre_key_ids;
703 if let Err(e) = backend.mark_prekeys_uploaded(&uploaded_ids).await {
704 log::warn!("Failed to mark prekeys as uploaded: {:?}", e);
705 }
706
707 self.persistence_manager
709 .modify_device(|d| d.server_has_prekeys = true)
710 .await;
711
712 log::debug!(
713 "Successfully uploaded {} pre-keys ({} reused from the window) starting from {}.",
714 uploaded_count,
715 plan.available,
716 plan.window_start
717 );
718
719 Ok(())
720 }
721
722 pub(crate) async fn upload_pre_keys_with_retry(
729 &self,
730 force: bool,
731 ) -> Result<(), anyhow::Error> {
732 self.upload_pre_keys_with_retry_count(force, None).await
733 }
734
735 #[cfg_attr(
736 feature = "tracing",
737 tracing::instrument(
738 name = "wa.session.upload_pre_keys_retry",
739 level = "debug",
740 skip_all,
741 fields(force = force, wanted = ?wanted),
742 err(Debug)
743 )
744 )]
745 async fn upload_pre_keys_with_retry_count(
746 &self,
747 force: bool,
748 wanted: Option<usize>,
749 ) -> Result<(), anyhow::Error> {
750 let mut delay_a: u64 = 1;
751 let mut delay_b: u64 = 2;
752 const MAX_DELAY_SECS: u64 = 610;
753
754 loop {
755 let result = self.upload_pre_keys_with_count(force, wanted).await;
756 match result {
757 Ok(()) => {
758 log::info!("Pre-key upload succeeded");
759 wacore::telemetry::prekey_upload("ok");
761 return Ok(());
762 }
763 Err(e) => {
764 let delay = delay_a.min(MAX_DELAY_SECS);
765 log::warn!("Pre-key upload failed, retrying in {}s: {:?}", delay, e);
766
767 self.runtime
768 .sleep(std::time::Duration::from_secs(delay))
769 .await;
770
771 if !self.is_logged_in.load(Ordering::Relaxed) {
773 wacore::telemetry::prekey_upload("fail");
774 return Err(anyhow::anyhow!(
775 "Connection lost during pre-key upload retry"
776 ));
777 }
778
779 let next = delay_a + delay_b;
780 delay_a = delay_b;
781 delay_b = next;
782 }
783 }
784 }
785 }
786
787 #[cfg_attr(
800 feature = "tracing",
801 tracing::instrument(
802 name = "wa.session.refresh_pre_keys",
803 level = "debug",
804 skip_all,
805 err(Debug)
806 )
807 )]
808 pub async fn refresh_pre_keys(&self) -> Result<(), anyhow::Error> {
809 let _guard = self.prekey_upload_lock.lock().await;
810 self.upload_pre_keys_with_retry(true).await
811 }
812
813 #[cfg_attr(
817 feature = "tracing",
818 tracing::instrument(
819 name = "wa.session.refresh_pre_keys_with_count",
820 level = "debug",
821 skip_all,
822 fields(count = count),
823 err(Debug)
824 )
825 )]
826 pub async fn refresh_pre_keys_with_count(&self, count: usize) -> Result<(), anyhow::Error> {
827 let _guard = self.prekey_upload_lock.lock().await;
828 self.upload_pre_keys_with_retry_count(true, Some(count))
829 .await
830 }
831
832 #[cfg_attr(
834 feature = "tracing",
835 tracing::instrument(
836 name = "wa.session.ensure_pre_keys",
837 level = "debug",
838 skip_all,
839 err(Debug)
840 )
841 )]
842 pub async fn ensure_pre_keys(&self) -> Result<(), anyhow::Error> {
843 let _guard = self.prekey_upload_lock.lock().await;
844 self.upload_pre_keys_with_retry(false).await
845 }
846
847 #[cfg_attr(
858 feature = "tracing",
859 tracing::instrument(
860 name = "wa.session.validate_digest_key",
861 level = "debug",
862 skip_all,
863 err(Debug)
864 )
865 )]
866 pub async fn validate_digest_key(&self) -> Result<(), anyhow::Error> {
867 let _guard = self.prekey_upload_lock.lock().await;
871
872 let response = match self.execute(DigestKeyBundleSpec::new()).await {
873 Ok(resp) => resp,
874 Err(crate::request::IqError::ServerError { code: 404, .. }) => {
875 log::warn!("digestKey: no record found for current user, re-uploading");
876 return self.upload_pre_keys_with_retry(true).await;
877 }
878 Err(crate::request::IqError::ServerError { code: 406, .. }) => {
879 log::warn!("digestKey: malformed request");
880 return Ok(());
881 }
882 Err(crate::request::IqError::ServerError { code: 503, .. }) => {
883 log::warn!("digestKey: service unavailable");
884 return Ok(());
885 }
886 Err(crate::request::IqError::ParseError(e)) => {
887 log::debug!("digestKey: unparseable digest response ({e}), skipping");
889 return Ok(());
890 }
891 Err(e) => {
892 if !self.is_shutting_down() {
893 log::warn!("digestKey: server error: {:?}", e);
894 }
895 return Ok(());
896 }
897 };
898
899 let device_snapshot = self.persistence_manager.get_device_snapshot();
903 if response.reg_id != device_snapshot.registration_id {
904 log::warn!(
905 "digestKey: registration ID mismatch (server={}, local={}), skipping",
906 response.reg_id,
907 device_snapshot.registration_id
908 );
909 return Ok(());
910 }
911
912 let identity_bytes = device_snapshot.identity_key.public_key.public_key_bytes();
915 let skey_pub_bytes = device_snapshot.signed_pre_key.public_key.public_key_bytes();
916 let skey_sig_bytes = &device_snapshot.signed_pre_key_signature;
917
918 let backend = self
919 .persistence_manager
920 .get_device_snapshot()
921 .backend
922 .clone();
923
924 let loaded = match backend.load_prekeys_batch(&response.prekey_ids).await {
926 Ok(v) => v,
927 Err(e) => {
928 log::warn!("digestKey: failed to batch-load prekeys: {:?}, skipping", e);
929 return Ok(());
930 }
931 };
932
933 let loaded_map: std::collections::HashMap<u32, bytes::Bytes> = loaded.into_iter().collect();
936 let unique_requested: std::collections::HashSet<&u32> =
937 response.prekey_ids.iter().collect();
938
939 if loaded_map.len() < unique_requested.len() {
940 log::warn!(
941 "digestKey: missing {} local prekeys, skipping",
942 unique_requested.len() - loaded_map.len()
943 );
944 return Ok(());
945 }
946
947 let mut prekey_pubkeys = Vec::with_capacity(response.prekey_ids.len());
949 for prekey_id in &response.prekey_ids {
950 let Some(record_bytes) = loaded_map.get(prekey_id) else {
951 log::warn!("digestKey: missing local prekey {}, skipping", prekey_id);
952 return Ok(());
953 };
954 match wacore::prekeys::extract_prekey_public_key(record_bytes) {
955 Some(pk) => prekey_pubkeys.push(pk),
956 None => {
957 log::warn!(
958 "digestKey: prekey {} has no public key, skipping",
959 prekey_id
960 );
961 return Ok(());
962 }
963 }
964 }
965
966 let local_hash = wacore::prekeys::compute_key_bundle_digest(
967 identity_bytes,
968 skey_pub_bytes,
969 skey_sig_bytes,
970 &prekey_pubkeys,
971 );
972
973 if local_hash.as_slice() != response.hash.as_slice() {
974 log::warn!(
975 "digestKey: hash mismatch (server={}, local={}), skipping",
976 hex::encode(&response.hash),
977 hex::encode(local_hash)
978 );
979 return Ok(());
980 }
981
982 log::debug!("digestKey: key bundle validation successful");
983 Ok(())
984 }
985}
986
987#[cfg(test)]
988mod tests {
989 use super::{
990 DEFAULT_WANTED_PRE_KEY_COUNT, MAX_PRE_KEY_UPLOAD_BATCH, MAX_PREKEY_ID, MIN_PRE_KEY_COUNT,
991 clamp_wanted_pre_key_count, plan_prekey_upload, should_upload_pre_keys, start_prekey_id,
992 };
993
994 #[test]
995 fn plan_initialises_window_for_legacy_device() {
996 let p = plan_prekey_upload(0, 7, 20, 812);
999 assert_eq!(p.window_start, 21, "legacy start skips stored rows");
1000 assert_eq!(p.available, 0);
1001 assert_eq!(p.gen_count, 812);
1002 assert_eq!(p.gen_start, 21);
1003 assert_eq!(p.new_next, 833);
1004 }
1005
1006 #[test]
1007 fn plan_generates_full_batch_on_empty_window() {
1008 let p = plan_prekey_upload(100, 100, 99, 812);
1009 assert_eq!(p.window_start, 100);
1010 assert_eq!(p.available, 0);
1011 assert_eq!(p.gen_count, 812);
1012 assert_eq!(p.gen_start, 100);
1013 assert_eq!(p.new_next, 912);
1014 }
1015
1016 #[test]
1017 fn plan_reuses_leftovers_and_tops_up() {
1018 let p = plan_prekey_upload(100, 150, 149, 812);
1020 assert_eq!(p.window_start, 100);
1021 assert_eq!(p.available, 50);
1022 assert_eq!(p.gen_count, 762);
1023 assert_eq!(p.gen_start, 150, "generation starts at NEXT (savePreKeys)");
1024 assert_eq!(p.new_next, 912);
1025 }
1026
1027 #[test]
1028 fn plan_full_window_generates_nothing_and_keeps_next() {
1029 let p = plan_prekey_upload(100, 1500, 1499, 812);
1032 assert_eq!(p.window_start, 100);
1033 assert_eq!(p.available, 812);
1034 assert_eq!(p.gen_count, 0);
1035 assert_eq!(p.new_next, 1500, "a capped window must not regress NEXT");
1036 }
1037
1038 #[test]
1039 fn plan_heals_corrupt_first_past_next() {
1040 let p = plan_prekey_upload(500, 100, 600, 812);
1041 assert_eq!(p.window_start, 601, "heals via the legacy-safe start");
1042 assert_eq!(p.available, 0);
1043 assert_eq!(p.gen_count, 812);
1044 }
1045
1046 #[test]
1047 fn plan_collapses_window_at_id_boundary() {
1048 let p = plan_prekey_upload(
1050 MAX_PREKEY_ID - 10,
1051 MAX_PREKEY_ID - 5,
1052 MAX_PREKEY_ID - 6,
1053 812,
1054 );
1055 assert_eq!(p.window_start, 1);
1056 assert_eq!(p.available, 0);
1057 assert_eq!(p.gen_count, 812);
1058 assert_eq!(p.new_next, 813);
1059 }
1060
1061 #[test]
1062 fn plan_single_key_reuses_window_head() {
1063 let p = plan_prekey_upload(10, 12, 11, 1);
1066 assert_eq!(p.window_start, 10);
1067 assert_eq!(p.available, 1);
1068 assert_eq!(p.gen_count, 0);
1069 assert_eq!(p.new_next, 12);
1070 }
1071
1072 #[test]
1073 fn default_matches_wa_web_upload_keys_count() {
1074 assert_eq!(DEFAULT_WANTED_PRE_KEY_COUNT, 812);
1076 }
1077
1078 #[test]
1079 fn clamp_wanted_pre_key_count_bounds() {
1080 assert_eq!(clamp_wanted_pre_key_count(0), MIN_PRE_KEY_COUNT);
1081 assert_eq!(clamp_wanted_pre_key_count(2), MIN_PRE_KEY_COUNT);
1082 assert_eq!(clamp_wanted_pre_key_count(4), MIN_PRE_KEY_COUNT);
1083 assert_eq!(
1084 clamp_wanted_pre_key_count(MIN_PRE_KEY_COUNT),
1085 MIN_PRE_KEY_COUNT
1086 );
1087 assert_eq!(clamp_wanted_pre_key_count(812), 812);
1088 assert_eq!(
1089 clamp_wanted_pre_key_count(MAX_PRE_KEY_UPLOAD_BATCH),
1090 MAX_PRE_KEY_UPLOAD_BATCH
1091 );
1092 assert_eq!(
1093 clamp_wanted_pre_key_count(MAX_PRE_KEY_UPLOAD_BATCH + 1),
1094 MAX_PRE_KEY_UPLOAD_BATCH
1095 );
1096 assert_eq!(
1097 clamp_wanted_pre_key_count(usize::MAX),
1098 MAX_PRE_KEY_UPLOAD_BATCH
1099 );
1100 }
1101
1102 #[test]
1103 fn start_prekey_id_uses_counter_and_wraps() {
1104 assert_eq!(start_prekey_id(10, 5), 10);
1106 assert_eq!(start_prekey_id(3, 100), 101);
1108 assert_eq!(start_prekey_id(0, 5), 6);
1110 assert_eq!(start_prekey_id(MAX_PREKEY_ID + 1, 0), 1);
1112 assert_eq!(start_prekey_id(MAX_PREKEY_ID, 0), MAX_PREKEY_ID);
1113 }
1114
1115 #[test]
1116 fn force_upload_bypasses_count_guard() {
1117 assert!(should_upload_pre_keys(true, 1000), "force always uploads");
1120 assert!(
1121 !should_upload_pre_keys(false, MIN_PRE_KEY_COUNT),
1122 "count guard skips when not forced and at/above threshold"
1123 );
1124 assert!(
1125 should_upload_pre_keys(false, MIN_PRE_KEY_COUNT - 1),
1126 "below threshold uploads even without force"
1127 );
1128 }
1129}
1130
1131#[cfg(test)]
1132#[allow(clippy::disallowed_methods)]
1133mod window_tests {
1134 use wacore::libsignal::protocol::PublicKey;
1135
1136 fn snapshot(client: &crate::client::Client) -> (u32, u32) {
1137 let d = client.persistence_manager.get_device_snapshot();
1138 (d.next_pre_key_id, d.first_unupload_pre_key_id)
1139 }
1140
1141 fn backend(
1142 client: &crate::client::Client,
1143 ) -> std::sync::Arc<dyn crate::store::traits::Backend> {
1144 client.persistence_manager.backend()
1145 }
1146
1147 #[tokio::test]
1148 async fn explicit_upload_count_does_not_change_background_configuration() {
1149 let client = crate::test_utils::create_test_client_with_name("prekey_explicit_count").await;
1150 client.set_wanted_pre_key_count(5);
1151
1152 let _ = client.upload_pre_keys_inner_with_count(7).await;
1153
1154 assert_eq!(client.wanted_pre_key_count(), 5);
1155 assert_eq!(snapshot(&client), (8, 8));
1156 assert_eq!(
1157 backend(&client)
1158 .load_prekeys_batch(&[1, 2, 3, 4, 5, 6, 7])
1159 .await
1160 .unwrap()
1161 .len(),
1162 7
1163 );
1164 }
1165
1166 #[tokio::test]
1171 async fn failed_upload_abandons_window_and_never_remints_ids() {
1172 let client = crate::test_utils::create_test_client_with_name("prekey_window_fail").await;
1173 client.set_wanted_pre_key_count(5);
1174
1175 let err = client.upload_pre_keys_inner().await;
1176 assert!(err.is_err(), "IQ must fail on a disconnected client");
1177
1178 let (next, first) = snapshot(&client);
1179 assert_eq!(next, 6, "NEXT advances at generation time");
1180 assert_eq!(first, 6, "FIRST is marked past the window before the send");
1181
1182 let rows = backend(&client)
1183 .load_prekeys_batch(&[1, 2, 3, 4, 5])
1184 .await
1185 .expect("load");
1186 assert_eq!(rows.len(), 5, "the generated window stays stored");
1187 let before: Vec<_> = rows.into_iter().collect();
1188
1189 let _ = client.upload_pre_keys_inner().await;
1190 let (next, first) = snapshot(&client);
1191 assert_eq!(next, 11, "second attempt mints fresh ids 6..=10");
1192 assert_eq!(first, 11);
1193
1194 let rows2 = backend(&client)
1195 .load_prekeys_batch(&[6, 7, 8, 9, 10])
1196 .await
1197 .expect("load");
1198 assert_eq!(rows2.len(), 5);
1199
1200 let after = backend(&client)
1201 .load_prekeys_batch(&[1, 2, 3, 4, 5])
1202 .await
1203 .expect("load");
1204 assert_eq!(
1205 before, after,
1206 "abandoned rows must never be regenerated (collision class)"
1207 );
1208 }
1209
1210 #[tokio::test]
1214 async fn single_prekey_is_reused_until_consumed() {
1215 let client = crate::test_utils::create_test_client_with_name("prekey_single_reuse").await;
1216
1217 let (id1, pk1) = client.get_or_gen_single_pre_key().await.expect("gen");
1218 let (id2, pk2) = client.get_or_gen_single_pre_key().await.expect("reuse");
1219 assert_eq!(id1, id2, "window head must be reused");
1220 assert_eq!(
1221 pk1.serialize(),
1222 pk2.serialize(),
1223 "same stored key, not a regenerated one"
1224 );
1225 let (next, first) = snapshot(&client);
1226 assert_eq!(first, id1);
1227 assert_eq!(next, id1 + 1);
1228
1229 backend(&client).remove_prekey(id1).await.expect("remove");
1231 let (id3, pk3) = client.get_or_gen_single_pre_key().await.expect("heal");
1232 assert_eq!(id3, id1 + 1, "dead slot skipped, fresh id minted");
1233 assert_ne!(pk3.serialize(), pk1.serialize());
1234 let (next, first) = snapshot(&client);
1235 assert_eq!(first, id3);
1236 assert_eq!(next, id3 + 1);
1237 }
1238
1239 #[tokio::test]
1243 async fn marking_retry_prekey_uploaded_excludes_it_from_reuse() {
1244 let client = crate::test_utils::create_test_client_with_name("prekey_mark_uploaded").await;
1245
1246 let (id1, _) = client.get_or_gen_single_pre_key().await.expect("gen");
1247 let (next, first) = snapshot(&client);
1248 assert_eq!(first, id1);
1249 assert_eq!(next, id1 + 1);
1250
1251 let guard = client.prekey_upload_lock.lock().await;
1252 client
1253 .mark_single_prekey_uploaded(&guard, id1)
1254 .await
1255 .expect("mark");
1256 drop(guard);
1257 let (next2, first2) = snapshot(&client);
1258 assert_eq!(
1259 first2,
1260 id1 + 1,
1261 "low watermark advances past the marked key"
1262 );
1263 assert_eq!(next2, id1 + 1, "window is now empty");
1264
1265 let (id2, _) = client.get_or_gen_single_pre_key().await.expect("fresh");
1267 assert_ne!(id2, id1, "marked key must not be reused");
1268
1269 let guard = client.prekey_upload_lock.lock().await;
1271 client
1272 .mark_single_prekey_uploaded(&guard, id1)
1273 .await
1274 .expect("noop");
1275 drop(guard);
1276 let (_, first3) = snapshot(&client);
1277 assert_eq!(first3, id2, "stale mark leaves the current head untouched");
1278 }
1279
1280 #[tokio::test]
1285 async fn marking_near_boundary_head_preserves_terminal_window_key() {
1286 use wacore::store::commands::DeviceCommand;
1287
1288 let client =
1289 crate::test_utils::create_test_client_with_name("prekey_mark_near_boundary").await;
1290 client
1293 .persistence_manager
1294 .process_command(DeviceCommand::SetPreKeyWatermarks {
1295 next_pre_key_id: super::MAX_PREKEY_ID + 1,
1296 first_unupload_pre_key_id: super::MAX_PREKEY_ID - 1,
1297 })
1298 .await;
1299
1300 let guard = client.prekey_upload_lock.lock().await;
1301 client
1302 .mark_single_prekey_uploaded(&guard, super::MAX_PREKEY_ID - 1)
1303 .await
1304 .expect("mark");
1305 drop(guard);
1306
1307 let (next, first) = snapshot(&client);
1308 assert_eq!(
1309 first,
1310 super::MAX_PREKEY_ID,
1311 "head advances onto the surviving key"
1312 );
1313 assert_eq!(
1314 next,
1315 super::MAX_PREKEY_ID + 1,
1316 "NEXT untouched: terminal key MAX is still in the window"
1317 );
1318 }
1319
1320 #[tokio::test]
1323 async fn marking_terminal_head_collapses_wrapped_window() {
1324 use wacore::store::commands::DeviceCommand;
1325
1326 let client = crate::test_utils::create_test_client_with_name("prekey_mark_terminal").await;
1327 client
1328 .persistence_manager
1329 .process_command(DeviceCommand::SetPreKeyWatermarks {
1330 next_pre_key_id: super::MAX_PREKEY_ID + 1,
1331 first_unupload_pre_key_id: super::MAX_PREKEY_ID,
1332 })
1333 .await;
1334
1335 let guard = client.prekey_upload_lock.lock().await;
1336 client
1337 .mark_single_prekey_uploaded(&guard, super::MAX_PREKEY_ID)
1338 .await
1339 .expect("mark");
1340 drop(guard);
1341
1342 let (next, first) = snapshot(&client);
1343 assert_eq!(first, 1, "head wraps to the low watermark");
1344 assert_eq!(
1345 next, 1,
1346 "NEXT collapses onto the wrapped head: window empty"
1347 );
1348 }
1349
1350 #[tokio::test]
1353 async fn consumed_head_advances_to_next_live_window_key() {
1354 use buffa::Message;
1355 use wacore::libsignal::protocol::KeyPair;
1356 use wacore::libsignal::store::record_helpers::new_pre_key_record;
1357 use wacore::store::commands::DeviceCommand;
1358
1359 let client = crate::test_utils::create_test_client_with_name("prekey_window_heal").await;
1360 let mut rng = rand::make_rng::<rand::rngs::StdRng>();
1361 let mut publics = std::collections::HashMap::new();
1362 for id in 10u32..13 {
1363 let kp = KeyPair::generate(&mut rng);
1364 publics.insert(id, kp.public_key);
1365 backend(&client)
1366 .store_prekey(id, &new_pre_key_record(id, &kp).encode_to_vec(), false)
1367 .await
1368 .expect("store");
1369 }
1370 client
1371 .persistence_manager
1372 .process_command(DeviceCommand::SetPreKeyWatermarks {
1373 next_pre_key_id: 13,
1374 first_unupload_pre_key_id: 10,
1375 })
1376 .await;
1377
1378 backend(&client).remove_prekey(10).await.expect("consume");
1379 let (id, pk) = client.get_or_gen_single_pre_key().await.expect("heal");
1380 assert_eq!(id, 11, "heal must advance to the next LIVE window key");
1381 assert_eq!(pk.serialize(), publics[&11].serialize());
1382 let (next, first) = snapshot(&client);
1383 assert_eq!(first, 11, "FIRST lands on the surviving key");
1384 assert_eq!(next, 13, "NEXT untouched: 12 is still in the window");
1385
1386 backend(&client).remove_prekey(11).await.expect("consume");
1388 let (id, _) = client.get_or_gen_single_pre_key().await.expect("heal 2");
1389 assert_eq!(id, 12);
1390 }
1391
1392 #[tokio::test]
1396 async fn fully_missing_window_collapses_and_regenerates_in_one_pass() {
1397 use wacore::store::commands::DeviceCommand;
1398
1399 let client =
1400 crate::test_utils::create_test_client_with_name("prekey_window_collapse").await;
1401 client.set_wanted_pre_key_count(5);
1402 client
1404 .persistence_manager
1405 .process_command(DeviceCommand::SetPreKeyWatermarks {
1406 next_pre_key_id: 15,
1407 first_unupload_pre_key_id: 10,
1408 })
1409 .await;
1410
1411 let _ = client.upload_pre_keys_inner().await;
1414 let (next, first) = snapshot(&client);
1415 assert_eq!(next, 20, "fresh batch minted at the collapsed NEXT");
1416 assert_eq!(first, 20, "marked past the window before the send");
1417 let rows = backend(&client)
1418 .load_prekeys_batch(&[15, 16, 17, 18, 19])
1419 .await
1420 .expect("load");
1421 assert_eq!(rows.len(), 5, "regeneration happened within the pass");
1422 }
1423
1424 #[tokio::test]
1428 async fn upload_window_includes_retry_single_key() {
1429 let client = crate::test_utils::create_test_client_with_name("prekey_window_topup").await;
1430 client.set_wanted_pre_key_count(5);
1431
1432 let (retry_id, retry_pk) = client.get_or_gen_single_pre_key().await.expect("gen");
1433 let before = backend(&client)
1434 .load_prekeys_batch(&[retry_id])
1435 .await
1436 .expect("load");
1437 assert_eq!(before.len(), 1);
1438
1439 let _ = client.upload_pre_keys_inner().await;
1440 let (next, _) = snapshot(&client);
1441 assert_eq!(
1442 next,
1443 retry_id + 5,
1444 "only wanted - available new keys are generated"
1445 );
1446
1447 let after = backend(&client)
1448 .load_prekeys_batch(&[retry_id])
1449 .await
1450 .expect("load");
1451 assert_eq!(
1452 before, after,
1453 "the retry key is re-offered, not regenerated"
1454 );
1455 let window = backend(&client)
1456 .load_prekeys_batch(&[
1457 retry_id,
1458 retry_id + 1,
1459 retry_id + 2,
1460 retry_id + 3,
1461 retry_id + 4,
1462 ])
1463 .await
1464 .expect("load");
1465 assert_eq!(window.len(), 5, "window = retry key + top-up");
1466
1467 use buffa::Message;
1468 let structure =
1469 waproto::whatsapp::PreKeyRecordStructure::decode_from_slice(&after[0].1[..])
1470 .expect("decode structure");
1471 let reloaded = PublicKey::from_djb_public_key_bytes(
1472 structure.public_key.as_deref().expect("public key"),
1473 )
1474 .expect("pub");
1475 assert_eq!(
1476 reloaded.serialize(),
1477 retry_pk.serialize(),
1478 "stored record matches the key shipped in the receipt"
1479 );
1480 }
1481}