wacore/store/traits.rs
1//! Storage traits for the WhatsApp client.
2//!
3//! This module defines 4 domain-grouped traits that together form the `Backend` trait:
4//!
5//! - [`SignalStore`]: Signal protocol cryptographic operations (identity, sessions, keys)
6//! - [`AppSyncStore`]: WhatsApp app state synchronization
7//! - [`ProtocolStore`]: WhatsApp Web protocol alignment (SKDM, LID mapping, device registry)
8//! - [`DeviceStore`]: Device persistence operations
9
10use crate::appstate::hash::HashState;
11use crate::store::error::Result;
12use async_trait::async_trait;
13use bytes::Bytes;
14use serde::{Deserialize, Serialize};
15use std::sync::Arc;
16use wacore_appstate::processor::AppStateMutationMAC;
17use wacore_binary::Jid;
18
19/// Inline protocol-sized message secret. The array makes invalid lengths
20/// unrepresentable without a heap allocation or pointer indirection per row.
21pub type MessageSecret = [u8; crate::reporting_token::MESSAGE_SECRET_SIZE];
22
23/// App state synchronization key for WhatsApp's app state protocol.
24#[derive(Debug, Clone, Default, Serialize, Deserialize)]
25pub struct AppStateSyncKey {
26 pub key_data: Vec<u8>,
27 pub fingerprint: Vec<u8>,
28 pub timestamp: i64,
29}
30
31/// Entry representing a LID to Phone Number mapping.
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct LidPnMappingEntry {
34 /// The LID user part (e.g., "100000012345678")
35 pub lid: String,
36 /// The phone number user part (e.g., "559980000001")
37 pub phone_number: String,
38 /// Unix timestamp when the mapping was first learned
39 pub created_at: i64,
40 /// Unix timestamp when the mapping was last updated
41 pub updated_at: i64,
42 /// The source from which this mapping was learned (e.g., "usync", "peer_pn_message")
43 pub learning_source: String,
44}
45
46/// Trusted contact privacy token entry.
47///
48/// Matches WhatsApp Web's Chat.tcToken / tcTokenTimestamp / tcTokenSenderTimestamp.
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct TcTokenEntry {
51 /// Raw token bytes received from the server.
52 pub token: Vec<u8>,
53 /// Unix timestamp (seconds) when the token was received.
54 pub token_timestamp: i64,
55 /// Unix timestamp (seconds) when we last issued our token to this contact.
56 pub sender_timestamp: Option<i64>,
57}
58
59/// Message-secret write entry keyed by chat, sender, and message ID.
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct MsgSecretEntry {
62 /// Canonical non-AD chat JID. Shared across entries from the same history
63 /// conversation instead of allocating one identical string per message.
64 pub chat: Arc<str>,
65 /// Canonical non-AD sender JID. Often aliases `chat` for direct messages.
66 pub sender: Arc<str>,
67 /// Message identifier. `Arc<str>` keeps entry clones used by buffered
68 /// persistence cheap without changing the serialized representation.
69 pub msg_id: Arc<str>,
70 pub secret: MessageSecret,
71 /// Absolute unix-seconds retention deadline. `0` means never expire.
72 /// Computed by the caller from the parent message's event time plus a
73 /// per-add-on-kind horizon (see `MsgSecretRetention`). The store prunes
74 /// rows whose deadline has passed; it does not know the horizon itself.
75 #[serde(default)]
76 pub expires_at: i64,
77 /// Parent message event time (unix seconds), or `0` when unknown. Kept so
78 /// the receive path can enforce the edit-processing window
79 /// (`editTs < message_ts + window`) the same way WhatsApp Web does.
80 #[serde(default)]
81 pub message_ts: i64,
82}
83
84impl MsgSecretEntry {
85 /// The canonical non-AD sender identifier for a row whose chat identifier is
86 /// already in hand, sharing that allocation whenever both JIDs address the
87 /// same user. That covers every direct message (chat and sender are the peer)
88 /// and every self-authored history row, which is what the `sender` field doc
89 /// means by "often aliases `chat`".
90 pub fn sender_id_for(chat: &Jid, chat_id: &Arc<str>, sender: &Jid) -> Arc<str> {
91 if sender.is_same_chat_as(chat) {
92 Arc::clone(chat_id)
93 } else {
94 sender.to_non_ad_arc_str()
95 }
96 }
97
98 /// Build a row from the JIDs the send and receive paths already carry.
99 ///
100 /// Single chokepoint for how a row's three identifiers are derived, so the
101 /// inbound capture and the outbound persist cannot drift on either the
102 /// canonicalisation or the aliasing above.
103 pub fn new(
104 chat: &Jid,
105 sender: &Jid,
106 msg_id: &str,
107 secret: MessageSecret,
108 expires_at: i64,
109 message_ts: i64,
110 ) -> Self {
111 let chat_id = chat.to_non_ad_arc_str();
112 let sender_id = Self::sender_id_for(chat, &chat_id, sender);
113 Self {
114 chat: chat_id,
115 sender: sender_id,
116 msg_id: Arc::from(msg_id),
117 secret,
118 expires_at,
119 message_ts,
120 }
121 }
122}
123
124/// Device information for registry tracking.
125#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct DeviceInfo {
127 /// The device ID (0 = primary device, 1+ = companion devices)
128 pub device_id: u32,
129 /// The key index, if known
130 pub key_index: Option<u32>,
131 /// Whether the device uses the hosted PN/LID address space.
132 #[serde(default)]
133 pub is_hosted: bool,
134}
135
136impl DeviceInfo {
137 /// Construct a regular device entry.
138 pub const fn new(device_id: u32, key_index: Option<u32>) -> Self {
139 Self {
140 device_id,
141 key_index,
142 is_hosted: false,
143 }
144 }
145
146 /// Apply the hosted bit reported by the device-list source.
147 pub const fn with_hosting(mut self, is_hosted: bool) -> Self {
148 self.is_hosted = is_hosted;
149 self
150 }
151}
152
153#[cfg(test)]
154mod msg_secret_entry_tests {
155 use super::{Jid, MsgSecretEntry};
156 use std::sync::Arc;
157
158 fn jid(s: &str) -> Jid {
159 s.parse().unwrap_or_else(|e| panic!("parse {s}: {e}"))
160 }
161
162 /// A direct message names the same user as chat and as sender (the sender
163 /// only adds a device suffix), so the row must carry one shared allocation
164 /// rather than two identical strings.
165 #[test]
166 fn direct_message_shares_one_identifier_allocation() {
167 let entry = MsgSecretEntry::new(
168 &jid("5511987650001@s.whatsapp.net"),
169 &jid("5511987650001:33@s.whatsapp.net"),
170 "3EB0AABBCCDDEEFF0011",
171 [7u8; 32],
172 0,
173 0,
174 );
175
176 assert_eq!(&*entry.chat, "5511987650001@s.whatsapp.net");
177 assert_eq!(&*entry.sender, "5511987650001@s.whatsapp.net");
178 assert!(
179 Arc::ptr_eq(&entry.chat, &entry.sender),
180 "chat and sender must share the allocation when they are the same user"
181 );
182 assert_eq!(&*entry.msg_id, "3EB0AABBCCDDEEFF0011");
183 }
184
185 /// A group row (and an outbound row, where the sender is us) names two
186 /// different users, which must stay two distinct identifiers.
187 #[test]
188 fn distinct_users_keep_separate_identifiers() {
189 let entry = MsgSecretEntry::new(
190 &jid("120363021033254949@g.us"),
191 &jid("5511987650001:2@s.whatsapp.net"),
192 "M1",
193 [0u8; 32],
194 123,
195 456,
196 );
197
198 assert_eq!(&*entry.chat, "120363021033254949@g.us");
199 assert_eq!(&*entry.sender, "5511987650001@s.whatsapp.net");
200 assert!(!Arc::ptr_eq(&entry.chat, &entry.sender));
201 assert_eq!((entry.expires_at, entry.message_ts), (123, 456));
202 }
203
204 /// Same user part, different namespace: the LID and PN forms are distinct
205 /// lookup keys and must never be collapsed into one.
206 #[test]
207 fn same_user_across_namespaces_is_not_aliased() {
208 let chat = jid("100000012345678@lid");
209 let entry = MsgSecretEntry::new(
210 &chat,
211 &jid("100000012345678@s.whatsapp.net"),
212 "",
213 [0u8; 32],
214 0,
215 0,
216 );
217
218 assert_eq!(&*entry.chat, "100000012345678@lid");
219 assert_eq!(&*entry.sender, "100000012345678@s.whatsapp.net");
220 assert!(!Arc::ptr_eq(&entry.chat, &entry.sender));
221 // An empty message id is a degenerate but representable key, not a panic.
222 assert_eq!(&*entry.msg_id, "");
223
224 // The standalone helper must make the same call as the constructor.
225 let chat_id: Arc<str> = Arc::from("100000012345678@lid");
226 let aliased = MsgSecretEntry::sender_id_for(&chat, &chat_id, &jid("100000012345678:9@lid"));
227 assert!(Arc::ptr_eq(&aliased, &chat_id));
228 }
229}
230
231#[cfg(test)]
232mod device_info_tests {
233 use super::DeviceInfo;
234
235 #[test]
236 fn hosted_flag_is_backward_compatible_with_persisted_json() {
237 let legacy: DeviceInfo = serde_json::from_str(r#"{"device_id":7,"key_index":3}"#).unwrap();
238 assert!(!legacy.is_hosted);
239
240 let hosted = DeviceInfo::new(7, Some(3)).with_hosting(true);
241 let roundtrip: DeviceInfo =
242 serde_json::from_str(&serde_json::to_string(&hosted).unwrap()).unwrap();
243 assert!(roundtrip.is_hosted);
244 }
245}
246
247/// Device list record matching WhatsApp Web's DeviceListRecord structure.
248#[derive(Debug, Clone, Serialize, Deserialize)]
249pub struct DeviceListRecord {
250 /// The user part of the JID (phone number or LID)
251 pub user: String,
252 /// List of known devices for this user
253 pub devices: Vec<DeviceInfo>,
254 /// Timestamp when this record was last updated
255 pub timestamp: i64,
256 /// Participant hash from usync, if available
257 pub phash: Option<String>,
258 /// ADV raw_id from `ADVKeyIndexList` — used to detect identity changes.
259 /// When this changes, all sessions and sender keys for the user must be cleared.
260 #[serde(default, skip_serializing_if = "Option::is_none")]
261 pub raw_id: Option<u32>,
262}
263
264impl crate::stats::HeapSize for DeviceListRecord {
265 fn heap_bytes(&self) -> usize {
266 self.user.capacity()
267 + self.devices.capacity() * size_of::<DeviceInfo>()
268 + self.phash.as_ref().map_or(0, |p| p.capacity())
269 }
270}
271
272/// Signal protocol cryptographic storage operations.
273///
274/// Handles identity keys, sessions, pre-keys, signed pre-keys, and sender keys
275/// for end-to-end encryption.
276#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
277#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
278pub trait SignalStore: Send + Sync {
279 // --- Identity Operations ---
280
281 /// Store an identity key for a remote address.
282 async fn put_identity(&self, address: &str, key: [u8; 32]) -> Result<()>;
283
284 /// Store multiple identity keys in a single batch operation.
285 /// Default implementation falls back to individual `put_identity` calls.
286 /// Addresses are `Arc<str>` so callers (the flush path) pass shared keys
287 /// without allocating a `String` per entry.
288 async fn put_identities_batch(&self, identities: &[(Arc<str>, [u8; 32])]) -> Result<()> {
289 for (address, key) in identities {
290 self.put_identity(address, *key).await?;
291 }
292 Ok(())
293 }
294
295 /// Load an identity key for a remote address (always 32 bytes).
296 async fn load_identity(&self, address: &str) -> Result<Option<[u8; 32]>>;
297
298 /// Delete an identity key.
299 async fn delete_identity(&self, address: &str) -> Result<()>;
300
301 // --- Session Operations ---
302
303 /// Get an encrypted session for an address.
304 async fn get_session(&self, address: &str) -> Result<Option<Bytes>>;
305
306 /// Store an encrypted session.
307 async fn put_session(&self, address: &str, session: &[u8]) -> Result<()>;
308
309 /// Store multiple encrypted sessions in a single batch operation.
310 /// Default implementation falls back to individual `put_session` calls.
311 async fn put_sessions_batch(&self, sessions: &[(Arc<str>, Bytes)]) -> Result<()> {
312 for (address, session) in sessions {
313 self.put_session(address, session).await?;
314 }
315 Ok(())
316 }
317
318 /// Delete a session.
319 async fn delete_session(&self, address: &str) -> Result<()>;
320
321 /// Check if a session exists. Default implementation uses `get_session`.
322 async fn has_session(&self, address: &str) -> Result<bool> {
323 Ok(self.get_session(address).await?.is_some())
324 }
325
326 /// Whether any session or identity exists for `user` across all device ids.
327 /// Addresses are keyed `user@server` (device 0) or `user:dev@server`. Used
328 /// to skip the per-device PN->LID migration scan for users we've never had
329 /// Signal state with. Default is conservative (`true`) so a backend that
330 /// doesn't implement it keeps the caller's full per-device scan.
331 async fn has_signal_state_for_user(&self, user: &str) -> Result<bool> {
332 let _ = user;
333 Ok(true)
334 }
335
336 // --- PreKey Operations ---
337
338 /// Store a pre-key.
339 async fn store_prekey(&self, id: u32, record: &[u8], uploaded: bool) -> Result<()>;
340
341 /// Store multiple pre-keys in a single batch operation.
342 /// Default implementation falls back to individual `store_prekey` calls.
343 async fn store_prekeys_batch(&self, keys: &[(u32, Bytes)], uploaded: bool) -> Result<()> {
344 for (id, record) in keys {
345 self.store_prekey(*id, record, uploaded).await?;
346 }
347 Ok(())
348 }
349
350 /// Load a pre-key by ID.
351 async fn load_prekey(&self, id: u32) -> Result<Option<Bytes>>;
352
353 /// Load multiple pre-keys by ID in a single batch operation.
354 /// Returns only the keys that exist.
355 async fn load_prekeys_batch(&self, ids: &[u32]) -> Result<Vec<(u32, Bytes)>> {
356 let mut result = Vec::with_capacity(ids.len());
357 for &id in ids {
358 if let Some(record) = self.load_prekey(id).await? {
359 result.push((id, record));
360 }
361 }
362 Ok(result)
363 }
364
365 /// Mark already-stored pre-keys as uploaded WITHOUT inserting. UPDATE
366 /// semantics on purpose: a key consumed (deleted) between the upload
367 /// snapshot and this call must stay deleted, never be resurrected by an
368 /// upsert.
369 async fn mark_prekeys_uploaded(&self, ids: &[u32]) -> Result<()>;
370
371 /// Remove a pre-key.
372 async fn remove_prekey(&self, id: u32) -> Result<()>;
373
374 /// Get the maximum pre-key ID currently stored, or 0 if none exist.
375 /// Used for migration when `next_pre_key_id` counter is not yet initialized.
376 async fn get_max_prekey_id(&self) -> Result<u32>;
377
378 // --- Signed PreKey Operations ---
379
380 /// Store a signed pre-key.
381 async fn store_signed_prekey(&self, id: u32, record: &[u8]) -> Result<()>;
382
383 /// Load a signed pre-key by ID.
384 async fn load_signed_prekey(&self, id: u32) -> Result<Option<Vec<u8>>>;
385
386 /// Load all signed pre-keys. Returns (id, record) pairs.
387 async fn load_all_signed_prekeys(&self) -> Result<Vec<(u32, Vec<u8>)>>;
388
389 /// Remove a signed pre-key.
390 async fn remove_signed_prekey(&self, id: u32) -> Result<()>;
391
392 // --- Sender Key Operations ---
393
394 /// Store a sender key for group messaging.
395 async fn put_sender_key(&self, address: &str, record: &[u8]) -> Result<()>;
396
397 /// Store multiple sender keys in a single batch operation.
398 /// Default implementation falls back to individual `put_sender_key` calls.
399 async fn put_sender_keys_batch(&self, sender_keys: &[(Arc<str>, Bytes)]) -> Result<()> {
400 for (address, record) in sender_keys {
401 self.put_sender_key(address, record).await?;
402 }
403 Ok(())
404 }
405
406 /// Get a sender key.
407 async fn get_sender_key(&self, address: &str) -> Result<Option<Vec<u8>>>;
408
409 /// Delete a sender key.
410 async fn delete_sender_key(&self, address: &str) -> Result<()>;
411}
412
413/// WhatsApp app state synchronization storage.
414///
415/// Handles sync keys, version tracking, and mutation MACs for the app state protocol.
416#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
417#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
418pub trait AppSyncStore: Send + Sync {
419 /// Get an app state sync key by ID.
420 async fn get_sync_key(&self, key_id: &[u8]) -> Result<Option<AppStateSyncKey>>;
421
422 /// Set an app state sync key.
423 async fn set_sync_key(&self, key_id: &[u8], key: AppStateSyncKey) -> Result<()>;
424
425 /// Get the app state version for a collection.
426 async fn get_version(&self, name: &str) -> Result<HashState>;
427
428 /// Set the app state version for a collection.
429 async fn set_version(&self, name: &str, state: HashState) -> Result<()>;
430
431 /// Store mutation MACs for a version.
432 async fn put_mutation_macs(
433 &self,
434 name: &str,
435 version: u64,
436 mutations: &[AppStateMutationMAC],
437 ) -> Result<()>;
438
439 /// Get a mutation MAC by index.
440 async fn get_mutation_mac(&self, name: &str, index_mac: &[u8]) -> Result<Option<Vec<u8>>>;
441
442 /// Batch variant of [`get_mutation_mac`](Self::get_mutation_mac): fetch many previous-MAC values in a
443 /// single backend round-trip. The default delegates to per-item lookups;
444 /// backends with a set-membership query (SQL `IN (...)`) should override to
445 /// avoid an N+1 (one DB round-trip per mutation in appstate sync).
446 ///
447 /// Index MACs are full HMAC-SHA256 outputs, so the batch path passes them as
448 /// inline `[u8; 32]` arrays ([`crate::appstate_sync::IndexMac`]) — no per-MAC
449 /// heap allocation on either side of the call.
450 async fn get_mutation_macs(
451 &self,
452 name: &str,
453 index_macs: &[[u8; 32]],
454 ) -> Result<std::collections::HashMap<[u8; 32], Vec<u8>>> {
455 let mut out = std::collections::HashMap::with_capacity(index_macs.len());
456 for index_mac in index_macs {
457 if let Some(mac) = self.get_mutation_mac(name, index_mac.as_slice()).await? {
458 out.insert(*index_mac, mac);
459 }
460 }
461 Ok(out)
462 }
463
464 /// Delete mutation MACs by their index MACs.
465 async fn delete_mutation_macs(&self, name: &str, index_macs: &[Vec<u8>]) -> Result<()>;
466
467 /// Delete every mutation MAC for a collection. Called on snapshot re-sync so the
468 /// MAC store is rebuilt from the snapshot, matching the ltHash baseline; leftover
469 /// entries would corrupt the next patch's ltHash.
470 async fn clear_mutation_macs(&self, name: &str) -> Result<()>;
471
472 /// Get the most recently stored app state sync key ID.
473 async fn get_latest_sync_key_id(&self) -> Result<Option<Vec<u8>>>;
474}
475
476/// Error returned by the default pending-inbound methods so a backend that does
477/// not implement the durability buffer fails closed (no silent at-most-once).
478fn unsupported_pending_inbound() -> crate::store::error::StoreError {
479 crate::store::error::StoreError::Validation(
480 "backend does not support the pending inbound buffer required by the durability hook"
481 .to_string(),
482 )
483}
484
485/// WhatsApp Web protocol alignment storage.
486///
487/// Handles SKDM tracking, LID-PN mapping, base key collision detection,
488/// device registry, and sender key status.
489#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
490#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
491pub trait ProtocolStore: Send + Sync {
492 // --- Per-Device Sender Key Tracking (matches WA Web's participant.senderKey Map) ---
493
494 /// Get the sender key distribution status for all known devices in a group.
495 /// Returns `(device_jid_string, has_key)` pairs where `has_key` indicates
496 /// whether the device has a valid sender key (`true`) or needs fresh SKDM (`false`).
497 async fn get_sender_key_devices(&self, group_jid: &str) -> Result<Vec<(String, bool)>>;
498
499 /// Set sender key status for devices. Called with `has_key=true` after successful
500 /// SKDM distribution (WA Web: `markHasSenderKey`), or `has_key=false` to mark
501 /// devices as needing fresh SKDM (WA Web: `markForgetSenderKey`).
502 async fn set_sender_key_status(&self, group_jid: &str, entries: &[(&str, bool)]) -> Result<()>;
503
504 /// Clear all sender key device tracking for a group (on sender key rotation).
505 async fn clear_sender_key_devices(&self, group_jid: &str) -> Result<()>;
506
507 /// Delete specific `sender_key_devices` rows by device JID across all groups.
508 /// Mirrors WA Web's per-group `senderKey.delete(deviceJid)` cleanup.
509 async fn delete_sender_key_device_rows(&self, device_jids: &[&str]) -> Result<()>;
510
511 /// Clear all sender key device tracking across ALL groups.
512 /// Called on identity change (raw_id mismatch) to force SKDM redistribution.
513 async fn clear_all_sender_key_devices(&self) -> Result<()>;
514
515 // --- LID-PN Mapping ---
516
517 /// Get a mapping by LID.
518 async fn get_lid_mapping(&self, lid: &str) -> Result<Option<LidPnMappingEntry>>;
519
520 /// Get a mapping by phone number (returns the most recent LID for that phone).
521 async fn get_pn_mapping(&self, phone: &str) -> Result<Option<LidPnMappingEntry>>;
522
523 /// Store or update a LID-PN mapping.
524 async fn put_lid_mapping(&self, entry: &LidPnMappingEntry) -> Result<()>;
525
526 /// Batched variant of `put_lid_mapping`. Backends should override with a
527 /// single transaction; the default loops for correctness. Mirrors WA Web's
528 /// `WAWebDBCreateLidPnMappings.createLidPnMappings({ mappings, … })`.
529 async fn put_lid_mappings(&self, entries: &[LidPnMappingEntry]) -> Result<()> {
530 for entry in entries {
531 self.put_lid_mapping(entry).await?;
532 }
533 Ok(())
534 }
535
536 /// Get all LID-PN mappings (for cache warm-up).
537 async fn get_all_lid_mappings(&self) -> Result<Vec<LidPnMappingEntry>>;
538
539 // --- Base Key Collision Detection ---
540
541 /// Save the base key for a session address during retry collision detection.
542 async fn save_base_key(&self, address: &str, message_id: &str, base_key: &[u8]) -> Result<()>;
543
544 /// Check if the current session has the same base key as the saved one.
545 async fn has_same_base_key(
546 &self,
547 address: &str,
548 message_id: &str,
549 current_base_key: &[u8],
550 ) -> Result<bool>;
551
552 /// Delete a base key entry.
553 async fn delete_base_key(&self, address: &str, message_id: &str) -> Result<()>;
554
555 // --- Device Registry ---
556
557 /// Update the device list for a user (called after usync responses).
558 async fn update_device_list(&self, record: DeviceListRecord) -> Result<()>;
559
560 /// Batched variant of `update_device_list`. Backends should override with
561 /// a single transaction; the default loops for correctness. Important on
562 /// usync of large groups, where the per-row commit + spawn_blocking
563 /// overhead dominates wall-clock time when called once per participant.
564 async fn update_device_lists(&self, records: Vec<DeviceListRecord>) -> Result<()> {
565 for record in records {
566 self.update_device_list(record).await?;
567 }
568 Ok(())
569 }
570
571 /// Get all known devices for a user.
572 async fn get_devices(&self, user: &str) -> Result<Option<DeviceListRecord>>;
573
574 /// Delete a device list record, forcing a network re-fetch on next query.
575 async fn delete_devices(&self, user: &str) -> Result<()>;
576
577 // --- Group Metadata Cache (WA Web participant-phash re-query skip) ---
578
579 /// Get the persisted, opaque serialized group metadata blob for `group_jid`.
580 /// The blob is a caller-serialized GroupInfo snapshot; backends without group
581 /// persistence return `None` (the group is then re-queried in full).
582 async fn get_group_metadata(&self, _group_jid: &str) -> Result<Option<Vec<u8>>> {
583 Ok(None)
584 }
585
586 /// Persist (upsert) the serialized group metadata blob for `group_jid`.
587 /// No-op by default; backends override to enable the phash re-query skip.
588 async fn put_group_metadata(&self, _group_jid: &str, _blob: &[u8]) -> Result<()> {
589 Ok(())
590 }
591
592 /// Remove the persisted group metadata blob for `group_jid` (e.g. on leave),
593 /// so the next query re-fetches in full instead of comparing a stale phash.
594 /// No-op by default.
595 async fn delete_group_metadata(&self, _group_jid: &str) -> Result<()> {
596 Ok(())
597 }
598
599 // --- TcToken Storage ---
600
601 /// Get a trusted contact token for a JID (stored under LID).
602 async fn get_tc_token(&self, jid: &str) -> Result<Option<TcTokenEntry>>;
603
604 /// Store or update a trusted contact token for a JID.
605 async fn put_tc_token(&self, jid: &str, entry: &TcTokenEntry) -> Result<()>;
606
607 /// Delete a trusted contact token for a JID.
608 async fn delete_tc_token(&self, jid: &str) -> Result<()>;
609
610 /// Get all JIDs that have stored tc tokens.
611 async fn get_all_tc_token_jids(&self) -> Result<Vec<String>>;
612
613 /// Delete tc tokens that have no live state left. A row is removed only when
614 /// its received token is expired-or-absent (`token_timestamp < token_cutoff`
615 /// or empty) **and** its sender bucket is expired-or-absent
616 /// (`sender_timestamp < sender_cutoff` or null), so recent sender state is
617 /// never dropped just because the received token expired. Returns count deleted.
618 async fn delete_expired_tc_tokens(&self, token_cutoff: i64, sender_cutoff: i64) -> Result<u32>;
619
620 /// Advance `sender_timestamp` toward `sender_timestamp` for a contact,
621 /// inserting a byte-less placeholder when absent and preserving any existing
622 /// token bytes. The stored value only ever moves forward (max), so
623 /// concurrent writers (post-send issuance, history sync) converge regardless
624 /// of ordering and never regress the sender bucket.
625 ///
626 /// Must be atomic w.r.t. [`put_tc_token`](Self::put_tc_token): the sender-side
627 /// issuance path and the notification writer both touch the same row, so a
628 /// non-atomic read-modify-write could drop a real token for a placeholder.
629 /// The default is a read-modify-write for third-party backends; the built-in
630 /// stores override it with a single atomic upsert.
631 async fn touch_tc_token_sender_timestamp(
632 &self,
633 jid: &str,
634 sender_timestamp: i64,
635 ) -> Result<()> {
636 let entry = match self.get_tc_token(jid).await? {
637 Some(existing) => TcTokenEntry {
638 sender_timestamp: Some(
639 existing
640 .sender_timestamp
641 .map_or(sender_timestamp, |e| e.max(sender_timestamp)),
642 ),
643 ..existing
644 },
645 None => TcTokenEntry {
646 token: Vec::new(),
647 token_timestamp: sender_timestamp,
648 sender_timestamp: Some(sender_timestamp),
649 },
650 };
651 self.put_tc_token(jid, &entry).await
652 }
653
654 /// Store a token received from a contact, preserving any existing
655 /// `sender_timestamp`. The symmetric counterpart of
656 /// [`touch_tc_token_sender_timestamp`](Self::touch_tc_token_sender_timestamp):
657 /// each writer owns its own field, so the notification path never drops a
658 /// sender bucket that the issuance path wrote concurrently.
659 ///
660 /// **Newer-wins**: the token pair is overwritten only when the stored token
661 /// is a byte-less placeholder or the incoming `token_timestamp` is at least
662 /// as new — a stale write must not clobber a fresher real token. Doing this
663 /// in the store (atomically for the built-in backends) is what lets the
664 /// concurrent history-sync and privacy-notification writers converge without
665 /// a lock. Same atomicity requirement as the sender bucket — the default
666 /// read-modify-write here is a best-effort for third-party backends.
667 async fn store_received_tc_token(
668 &self,
669 jid: &str,
670 token: &[u8],
671 token_timestamp: i64,
672 ) -> Result<()> {
673 let existing = self.get_tc_token(jid).await?;
674 // Keep a fresher real token; a placeholder never blocks the first real one.
675 if let Some(existing) = &existing
676 && !existing.token.is_empty()
677 && token_timestamp < existing.token_timestamp
678 {
679 return Ok(());
680 }
681 let sender_timestamp = existing.and_then(|existing| existing.sender_timestamp);
682 self.put_tc_token(
683 jid,
684 &TcTokenEntry {
685 token: token.to_vec(),
686 token_timestamp,
687 sender_timestamp,
688 },
689 )
690 .await
691 }
692
693 // --- Sent Message Store (retry support, matches WA Web's getMessageTable) ---
694
695 /// Store a sent message's serialized payload for retry handling.
696 /// Called after each send_message(); the payload is the protobuf-encoded Message.
697 async fn store_sent_message(
698 &self,
699 chat_jid: &str,
700 message_id: &str,
701 payload: &[u8],
702 ) -> Result<()>;
703
704 /// Retrieve and delete a sent message (atomic take). Returns serialized payload.
705 /// Called when a retry receipt arrives; consuming prevents double-retry.
706 async fn take_sent_message(&self, chat_jid: &str, message_id: &str) -> Result<Option<Vec<u8>>>;
707
708 /// Delete sent messages older than cutoff (unix timestamp seconds). Returns count deleted.
709 async fn delete_expired_sent_messages(&self, cutoff_timestamp: i64) -> Result<u32>;
710
711 // --- Pending Inbound Buffer (inbound durability hook) ---
712 //
713 // Backs the at-least-once inbound durability hook: a decrypted message is
714 // buffered here (keyed by its stanza id) before the Signal ratchet is
715 // flushed, so a crash or failed commit before the hook acks replays the
716 // message on redelivery instead of dropping it. The defaults are non-breaking
717 // for backends that do not implement the hook, but fail CLOSED rather than
718 // no-op: an unsupported backend used with a hook surfaces an error (and the
719 // message stays unacked) instead of silently degrading to at-most-once.
720
721 /// Persist a decrypted inbound message awaiting a durability-hook commit.
722 /// Scoped by `(chat, sender, id)` because stanza ids are only unique within
723 /// a `(chat, sender)`.
724 async fn store_pending_inbound(
725 &self,
726 _chat: &str,
727 _sender: &str,
728 _id: &str,
729 _message: &[u8],
730 ) -> Result<()> {
731 Err(unsupported_pending_inbound())
732 }
733
734 /// Read a buffered inbound message by `(chat, sender, id)` without removing it.
735 async fn get_pending_inbound(
736 &self,
737 _chat: &str,
738 _sender: &str,
739 _id: &str,
740 ) -> Result<Option<Vec<u8>>> {
741 Err(unsupported_pending_inbound())
742 }
743
744 /// Remove a buffered inbound message once its durability hook has committed.
745 async fn delete_pending_inbound(&self, _chat: &str, _sender: &str, _id: &str) -> Result<()> {
746 Err(unsupported_pending_inbound())
747 }
748
749 /// Delete buffered inbound messages older than cutoff (unix seconds). Returns
750 /// count deleted. Unlike the other defaults this is a benign `Ok(0)`: the
751 /// keepalive sweep calls it unconditionally for every backend, so it must not
752 /// error when the buffer is unsupported.
753 async fn delete_expired_pending_inbound(&self, _cutoff_timestamp: i64) -> Result<u32> {
754 Ok(0)
755 }
756
757 /// Batched [`store_pending_inbound`](Self::store_pending_inbound): the
758 /// offline drain buffers one commit-batch of messages per call, so backends
759 /// should override this with a single transaction (the bundled SqliteStore
760 /// does). The default iterates the single-row method, preserving behavior
761 /// for third-party backends.
762 async fn store_pending_inbound_batch(&self, rows: &[PendingInboundRow<'_>]) -> Result<()> {
763 for row in rows {
764 self.store_pending_inbound(row.chat, row.sender, row.id, row.message)
765 .await?;
766 }
767 Ok(())
768 }
769
770 /// Batched [`delete_pending_inbound`](Self::delete_pending_inbound); same
771 /// override guidance as [`store_pending_inbound_batch`](Self::store_pending_inbound_batch).
772 async fn delete_pending_inbound_batch(&self, keys: &[PendingInboundKey<'_>]) -> Result<()> {
773 for key in keys {
774 self.delete_pending_inbound(key.chat, key.sender, key.id)
775 .await?;
776 }
777 Ok(())
778 }
779}
780
781/// One row of a pending-inbound batch write. Fields borrow from the in-flight
782/// commit batch so building a batch allocates nothing per row.
783#[derive(Debug, Clone, Copy)]
784pub struct PendingInboundRow<'a> {
785 pub chat: &'a str,
786 pub sender: &'a str,
787 pub id: &'a str,
788 pub message: &'a [u8],
789}
790
791/// Key of a buffered pending-inbound row, for batched deletes.
792#[derive(Debug, Clone, Copy)]
793pub struct PendingInboundKey<'a> {
794 pub chat: &'a str,
795 pub sender: &'a str,
796 pub id: &'a str,
797}
798
799/// Device data persistence operations.
800#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
801#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
802pub trait DeviceStore: Send + Sync {
803 /// Save device data.
804 async fn save(&self, device: &crate::store::Device) -> Result<()>;
805
806 /// Load device data.
807 async fn load(&self) -> Result<Option<crate::store::Device>>;
808
809 /// Check if a device exists.
810 async fn exists(&self) -> Result<bool>;
811
812 /// Create a new device row and return its generated device_id.
813 async fn create(&self) -> Result<i32>;
814
815 /// Create a snapshot of the database state.
816 /// The argument `name` can be used to label the snapshot file.
817 /// `extra_content` can be used to save a related binary blob (e.g. the message that caused the failure).
818 async fn snapshot_db(&self, _name: &str, _extra_content: Option<&[u8]>) -> Result<()> {
819 Ok(())
820 }
821
822 /// Best-effort process-local memory this backend attributes to the session
823 /// (e.g. a SQLite page cache — often the single largest per-session chunk,
824 /// living entirely outside the `Client`). Defaults to an all-`None`
825 /// [`StorageResourceReport`](crate::stats::StorageResourceReport) ("not reported"); backends that can introspect
826 /// their memory override it, and remote/store-backed backends report
827 /// `memory_bytes: Some(0)` (their data isn't process memory).
828 ///
829 /// This is a defaulted method on `DeviceStore` — an already-implemented,
830 /// non-blanket sub-trait of `Backend` — rather than an inherent method
831 /// (which wouldn't compose through the `Arc<dyn Backend>` the client holds)
832 /// or a new `Backend` supertrait (which would force *every* backend,
833 /// including external ones, to add an impl). The default keeps it fully
834 /// non-breaking, exactly like [`Self::snapshot_db`].
835 async fn resource_report(&self) -> crate::stats::StorageResourceReport {
836 crate::stats::StorageResourceReport::default()
837 }
838}
839
840/// Per-outbound-message secret storage for addon-style decryption.
841///
842/// Persists the 32-byte `MessageContextInfo.messageSecret` we send out so that
843/// later inbound replies (poll votes, reactions, msmsg bot responses, edits)
844/// referencing the original message ID can be decrypted. Mirrors WA Web's
845/// `WAWebMsmsgMsgSecretCache` + the `messageSecret` field on the DB message row.
846#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
847#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
848pub trait MsgSecretStore: Send + Sync {
849 /// Persist the protocol-sized `secret` under the composite key with NO
850 /// expiry (`expires_at = 0`). Convenience wrapper over [`put_msg_secrets`].
851 /// `chat`, `sender`, and `msg_id` are JID strings / message ID strings;
852 /// callers should pass non-AD (no-device) form for the JIDs so lookups
853 /// match regardless of which device echo'd the stanza back.
854 ///
855 /// Real call sites that compute a retention deadline build
856 /// [`MsgSecretEntry`] directly and call [`put_msg_secrets`].
857 ///
858 /// [`put_msg_secrets`]: MsgSecretStore::put_msg_secrets
859 async fn put_msg_secret(
860 &self,
861 chat: &str,
862 sender: &str,
863 msg_id: &str,
864 secret: &[u8; crate::reporting_token::MESSAGE_SECRET_SIZE],
865 ) -> Result<()> {
866 self.put_msg_secrets(vec![MsgSecretEntry {
867 chat: Arc::from(chat),
868 sender: Arc::from(sender),
869 msg_id: Arc::from(msg_id),
870 secret: *secret,
871 expires_at: 0,
872 message_ts: 0,
873 }])
874 .await?;
875 Ok(())
876 }
877
878 /// Batched upsert carrying a per-row `expires_at` deadline. On key conflict
879 /// implementations merge deterministically via [`merge_msg_secret_expiry`]
880 /// (later deadline wins, `0` = "never" = infinity) so a redelivery or edit
881 /// re-persist never shortens a window, and via [`merge_msg_secret_message_ts`]
882 /// (the later non-zero parent time wins; a `0` never clobbers a known one).
883 async fn put_msg_secrets(&self, entries: Vec<MsgSecretEntry>) -> Result<usize>;
884
885 /// Fetch the persisted secret; returns `None` if absent.
886 async fn get_msg_secret(
887 &self,
888 chat: &str,
889 sender: &str,
890 msg_id: &str,
891 ) -> Result<Option<Vec<u8>>>;
892
893 /// Fetch the secret together with the parent message's event time
894 /// (`message_ts`, `0` when unknown), so the receive path can enforce the
895 /// edit-processing window. Default pairs `get_msg_secret` with `0`;
896 /// backends that store `message_ts` override this.
897 async fn get_msg_secret_with_ts(
898 &self,
899 chat: &str,
900 sender: &str,
901 msg_id: &str,
902 ) -> Result<Option<(Vec<u8>, i64)>> {
903 Ok(self
904 .get_msg_secret(chat, sender, msg_id)
905 .await?
906 .map(|secret| (secret, 0)))
907 }
908
909 /// Delete rows whose non-zero `expires_at` is at or before
910 /// `cutoff_timestamp` (absolute unix seconds; callers pass "now"). Rows
911 /// with `expires_at = 0` (never) are kept. Returns the number removed so
912 /// the keepalive cleanup can log/throttle.
913 async fn delete_expired_msg_secrets(&self, cutoff_timestamp: i64) -> Result<u32>;
914}
915
916/// Merge two `expires_at` deadlines on key conflict: `0` ("never") wins,
917/// otherwise the later (larger) deadline is kept so windows never shrink.
918pub fn merge_msg_secret_expiry(existing: i64, incoming: i64) -> i64 {
919 if existing == 0 || incoming == 0 {
920 0
921 } else {
922 existing.max(incoming)
923 }
924}
925
926/// Merge two parent `message_ts` values on key conflict: the later (larger)
927/// non-zero value wins, so a `0` ("unknown") never clobbers a known parent
928/// time. `max` already yields this because every real timestamp is `> 0`.
929pub fn merge_msg_secret_message_ts(existing: i64, incoming: i64) -> i64 {
930 existing.max(incoming)
931}
932
933/// Combined storage backend trait.
934///
935/// Any type implementing all domain traits automatically implements `Backend`.
936pub trait Backend:
937 SignalStore + AppSyncStore + ProtocolStore + MsgSecretStore + DeviceStore + Send + Sync
938{
939}
940
941impl<T> Backend for T where
942 T: SignalStore + AppSyncStore + ProtocolStore + MsgSecretStore + DeviceStore + Send + Sync
943{
944}