Skip to main content

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}