use crate::appstate::hash::HashState;
use crate::store::error::Result;
use async_trait::async_trait;
use bytes::Bytes;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use wacore_appstate::processor::AppStateMutationMAC;
use wacore_binary::Jid;
pub type MessageSecret = [u8; crate::reporting_token::MESSAGE_SECRET_SIZE];
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct AppStateSyncKey {
pub key_data: Vec<u8>,
pub fingerprint: Vec<u8>,
pub timestamp: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LidPnMappingEntry {
pub lid: String,
pub phone_number: String,
pub created_at: i64,
pub updated_at: i64,
pub learning_source: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TcTokenEntry {
pub token: Vec<u8>,
pub token_timestamp: i64,
pub sender_timestamp: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MsgSecretEntry {
pub chat: Arc<str>,
pub sender: Arc<str>,
pub msg_id: Arc<str>,
pub secret: MessageSecret,
#[serde(default)]
pub expires_at: i64,
#[serde(default)]
pub message_ts: i64,
}
impl MsgSecretEntry {
pub fn sender_id_for(chat: &Jid, chat_id: &Arc<str>, sender: &Jid) -> Arc<str> {
if sender.is_same_chat_as(chat) {
Arc::clone(chat_id)
} else {
sender.to_non_ad_arc_str()
}
}
pub fn new(
chat: &Jid,
sender: &Jid,
msg_id: &str,
secret: MessageSecret,
expires_at: i64,
message_ts: i64,
) -> Self {
let chat_id = chat.to_non_ad_arc_str();
let sender_id = Self::sender_id_for(chat, &chat_id, sender);
Self {
chat: chat_id,
sender: sender_id,
msg_id: Arc::from(msg_id),
secret,
expires_at,
message_ts,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeviceInfo {
pub device_id: u32,
pub key_index: Option<u32>,
#[serde(default)]
pub is_hosted: bool,
}
impl DeviceInfo {
pub const fn new(device_id: u32, key_index: Option<u32>) -> Self {
Self {
device_id,
key_index,
is_hosted: false,
}
}
pub const fn with_hosting(mut self, is_hosted: bool) -> Self {
self.is_hosted = is_hosted;
self
}
}
#[cfg(test)]
mod msg_secret_entry_tests {
use super::{Jid, MsgSecretEntry};
use std::sync::Arc;
fn jid(s: &str) -> Jid {
s.parse().unwrap_or_else(|e| panic!("parse {s}: {e}"))
}
#[test]
fn direct_message_shares_one_identifier_allocation() {
let entry = MsgSecretEntry::new(
&jid("5511987650001@s.whatsapp.net"),
&jid("5511987650001:33@s.whatsapp.net"),
"3EB0AABBCCDDEEFF0011",
[7u8; 32],
0,
0,
);
assert_eq!(&*entry.chat, "5511987650001@s.whatsapp.net");
assert_eq!(&*entry.sender, "5511987650001@s.whatsapp.net");
assert!(
Arc::ptr_eq(&entry.chat, &entry.sender),
"chat and sender must share the allocation when they are the same user"
);
assert_eq!(&*entry.msg_id, "3EB0AABBCCDDEEFF0011");
}
#[test]
fn distinct_users_keep_separate_identifiers() {
let entry = MsgSecretEntry::new(
&jid("120363021033254949@g.us"),
&jid("5511987650001:2@s.whatsapp.net"),
"M1",
[0u8; 32],
123,
456,
);
assert_eq!(&*entry.chat, "120363021033254949@g.us");
assert_eq!(&*entry.sender, "5511987650001@s.whatsapp.net");
assert!(!Arc::ptr_eq(&entry.chat, &entry.sender));
assert_eq!((entry.expires_at, entry.message_ts), (123, 456));
}
#[test]
fn same_user_across_namespaces_is_not_aliased() {
let chat = jid("100000012345678@lid");
let entry = MsgSecretEntry::new(
&chat,
&jid("100000012345678@s.whatsapp.net"),
"",
[0u8; 32],
0,
0,
);
assert_eq!(&*entry.chat, "100000012345678@lid");
assert_eq!(&*entry.sender, "100000012345678@s.whatsapp.net");
assert!(!Arc::ptr_eq(&entry.chat, &entry.sender));
assert_eq!(&*entry.msg_id, "");
let chat_id: Arc<str> = Arc::from("100000012345678@lid");
let aliased = MsgSecretEntry::sender_id_for(&chat, &chat_id, &jid("100000012345678:9@lid"));
assert!(Arc::ptr_eq(&aliased, &chat_id));
}
}
#[cfg(test)]
mod device_info_tests {
use super::DeviceInfo;
#[test]
fn hosted_flag_is_backward_compatible_with_persisted_json() {
let legacy: DeviceInfo = serde_json::from_str(r#"{"device_id":7,"key_index":3}"#).unwrap();
assert!(!legacy.is_hosted);
let hosted = DeviceInfo::new(7, Some(3)).with_hosting(true);
let roundtrip: DeviceInfo =
serde_json::from_str(&serde_json::to_string(&hosted).unwrap()).unwrap();
assert!(roundtrip.is_hosted);
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeviceListRecord {
pub user: String,
pub devices: Vec<DeviceInfo>,
pub timestamp: i64,
pub phash: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub raw_id: Option<u32>,
}
impl crate::stats::HeapSize for DeviceListRecord {
fn heap_bytes(&self) -> usize {
self.user.capacity()
+ self.devices.capacity() * size_of::<DeviceInfo>()
+ self.phash.as_ref().map_or(0, |p| p.capacity())
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait SignalStore: Send + Sync {
async fn put_identity(&self, address: &str, key: [u8; 32]) -> Result<()>;
async fn put_identities_batch(&self, identities: &[(Arc<str>, [u8; 32])]) -> Result<()> {
for (address, key) in identities {
self.put_identity(address, *key).await?;
}
Ok(())
}
async fn load_identity(&self, address: &str) -> Result<Option<[u8; 32]>>;
async fn delete_identity(&self, address: &str) -> Result<()>;
async fn get_session(&self, address: &str) -> Result<Option<Bytes>>;
async fn put_session(&self, address: &str, session: &[u8]) -> Result<()>;
async fn put_sessions_batch(&self, sessions: &[(Arc<str>, Bytes)]) -> Result<()> {
for (address, session) in sessions {
self.put_session(address, session).await?;
}
Ok(())
}
async fn delete_session(&self, address: &str) -> Result<()>;
async fn has_session(&self, address: &str) -> Result<bool> {
Ok(self.get_session(address).await?.is_some())
}
async fn has_signal_state_for_user(&self, user: &str) -> Result<bool> {
let _ = user;
Ok(true)
}
async fn store_prekey(&self, id: u32, record: &[u8], uploaded: bool) -> Result<()>;
async fn store_prekeys_batch(&self, keys: &[(u32, Bytes)], uploaded: bool) -> Result<()> {
for (id, record) in keys {
self.store_prekey(*id, record, uploaded).await?;
}
Ok(())
}
async fn load_prekey(&self, id: u32) -> Result<Option<Bytes>>;
async fn load_prekeys_batch(&self, ids: &[u32]) -> Result<Vec<(u32, Bytes)>> {
let mut result = Vec::with_capacity(ids.len());
for &id in ids {
if let Some(record) = self.load_prekey(id).await? {
result.push((id, record));
}
}
Ok(result)
}
async fn mark_prekeys_uploaded(&self, ids: &[u32]) -> Result<()>;
async fn remove_prekey(&self, id: u32) -> Result<()>;
async fn get_max_prekey_id(&self) -> Result<u32>;
async fn store_signed_prekey(&self, id: u32, record: &[u8]) -> Result<()>;
async fn load_signed_prekey(&self, id: u32) -> Result<Option<Vec<u8>>>;
async fn load_all_signed_prekeys(&self) -> Result<Vec<(u32, Vec<u8>)>>;
async fn remove_signed_prekey(&self, id: u32) -> Result<()>;
async fn put_sender_key(&self, address: &str, record: &[u8]) -> Result<()>;
async fn put_sender_keys_batch(&self, sender_keys: &[(Arc<str>, Bytes)]) -> Result<()> {
for (address, record) in sender_keys {
self.put_sender_key(address, record).await?;
}
Ok(())
}
async fn get_sender_key(&self, address: &str) -> Result<Option<Vec<u8>>>;
async fn delete_sender_key(&self, address: &str) -> Result<()>;
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait AppSyncStore: Send + Sync {
async fn get_sync_key(&self, key_id: &[u8]) -> Result<Option<AppStateSyncKey>>;
async fn set_sync_key(&self, key_id: &[u8], key: AppStateSyncKey) -> Result<()>;
async fn get_version(&self, name: &str) -> Result<HashState>;
async fn set_version(&self, name: &str, state: HashState) -> Result<()>;
async fn put_mutation_macs(
&self,
name: &str,
version: u64,
mutations: &[AppStateMutationMAC],
) -> Result<()>;
async fn get_mutation_mac(&self, name: &str, index_mac: &[u8]) -> Result<Option<Vec<u8>>>;
async fn get_mutation_macs(
&self,
name: &str,
index_macs: &[[u8; 32]],
) -> Result<std::collections::HashMap<[u8; 32], Vec<u8>>> {
let mut out = std::collections::HashMap::with_capacity(index_macs.len());
for index_mac in index_macs {
if let Some(mac) = self.get_mutation_mac(name, index_mac.as_slice()).await? {
out.insert(*index_mac, mac);
}
}
Ok(out)
}
async fn delete_mutation_macs(&self, name: &str, index_macs: &[Vec<u8>]) -> Result<()>;
async fn clear_mutation_macs(&self, name: &str) -> Result<()>;
async fn get_latest_sync_key_id(&self) -> Result<Option<Vec<u8>>>;
}
fn unsupported_pending_inbound() -> crate::store::error::StoreError {
crate::store::error::StoreError::Validation(
"backend does not support the pending inbound buffer required by the durability hook"
.to_string(),
)
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait ProtocolStore: Send + Sync {
async fn get_sender_key_devices(&self, group_jid: &str) -> Result<Vec<(String, bool)>>;
async fn set_sender_key_status(&self, group_jid: &str, entries: &[(&str, bool)]) -> Result<()>;
async fn clear_sender_key_devices(&self, group_jid: &str) -> Result<()>;
async fn delete_sender_key_device_rows(&self, device_jids: &[&str]) -> Result<()>;
async fn clear_all_sender_key_devices(&self) -> Result<()>;
async fn get_lid_mapping(&self, lid: &str) -> Result<Option<LidPnMappingEntry>>;
async fn get_pn_mapping(&self, phone: &str) -> Result<Option<LidPnMappingEntry>>;
async fn put_lid_mapping(&self, entry: &LidPnMappingEntry) -> Result<()>;
async fn put_lid_mappings(&self, entries: &[LidPnMappingEntry]) -> Result<()> {
for entry in entries {
self.put_lid_mapping(entry).await?;
}
Ok(())
}
async fn get_all_lid_mappings(&self) -> Result<Vec<LidPnMappingEntry>>;
async fn save_base_key(&self, address: &str, message_id: &str, base_key: &[u8]) -> Result<()>;
async fn has_same_base_key(
&self,
address: &str,
message_id: &str,
current_base_key: &[u8],
) -> Result<bool>;
async fn delete_base_key(&self, address: &str, message_id: &str) -> Result<()>;
async fn update_device_list(&self, record: DeviceListRecord) -> Result<()>;
async fn update_device_lists(&self, records: Vec<DeviceListRecord>) -> Result<()> {
for record in records {
self.update_device_list(record).await?;
}
Ok(())
}
async fn get_devices(&self, user: &str) -> Result<Option<DeviceListRecord>>;
async fn delete_devices(&self, user: &str) -> Result<()>;
async fn get_group_metadata(&self, _group_jid: &str) -> Result<Option<Vec<u8>>> {
Ok(None)
}
async fn put_group_metadata(&self, _group_jid: &str, _blob: &[u8]) -> Result<()> {
Ok(())
}
async fn delete_group_metadata(&self, _group_jid: &str) -> Result<()> {
Ok(())
}
async fn get_tc_token(&self, jid: &str) -> Result<Option<TcTokenEntry>>;
async fn put_tc_token(&self, jid: &str, entry: &TcTokenEntry) -> Result<()>;
async fn delete_tc_token(&self, jid: &str) -> Result<()>;
async fn get_all_tc_token_jids(&self) -> Result<Vec<String>>;
async fn delete_expired_tc_tokens(&self, token_cutoff: i64, sender_cutoff: i64) -> Result<u32>;
async fn touch_tc_token_sender_timestamp(
&self,
jid: &str,
sender_timestamp: i64,
) -> Result<()> {
let entry = match self.get_tc_token(jid).await? {
Some(existing) => TcTokenEntry {
sender_timestamp: Some(
existing
.sender_timestamp
.map_or(sender_timestamp, |e| e.max(sender_timestamp)),
),
..existing
},
None => TcTokenEntry {
token: Vec::new(),
token_timestamp: sender_timestamp,
sender_timestamp: Some(sender_timestamp),
},
};
self.put_tc_token(jid, &entry).await
}
async fn store_received_tc_token(
&self,
jid: &str,
token: &[u8],
token_timestamp: i64,
) -> Result<()> {
let existing = self.get_tc_token(jid).await?;
if let Some(existing) = &existing
&& !existing.token.is_empty()
&& token_timestamp < existing.token_timestamp
{
return Ok(());
}
let sender_timestamp = existing.and_then(|existing| existing.sender_timestamp);
self.put_tc_token(
jid,
&TcTokenEntry {
token: token.to_vec(),
token_timestamp,
sender_timestamp,
},
)
.await
}
async fn store_sent_message(
&self,
chat_jid: &str,
message_id: &str,
payload: &[u8],
) -> Result<()>;
async fn take_sent_message(&self, chat_jid: &str, message_id: &str) -> Result<Option<Vec<u8>>>;
async fn delete_expired_sent_messages(&self, cutoff_timestamp: i64) -> Result<u32>;
async fn store_pending_inbound(
&self,
_chat: &str,
_sender: &str,
_id: &str,
_message: &[u8],
) -> Result<()> {
Err(unsupported_pending_inbound())
}
async fn get_pending_inbound(
&self,
_chat: &str,
_sender: &str,
_id: &str,
) -> Result<Option<Vec<u8>>> {
Err(unsupported_pending_inbound())
}
async fn delete_pending_inbound(&self, _chat: &str, _sender: &str, _id: &str) -> Result<()> {
Err(unsupported_pending_inbound())
}
async fn delete_expired_pending_inbound(&self, _cutoff_timestamp: i64) -> Result<u32> {
Ok(0)
}
async fn store_pending_inbound_batch(&self, rows: &[PendingInboundRow<'_>]) -> Result<()> {
for row in rows {
self.store_pending_inbound(row.chat, row.sender, row.id, row.message)
.await?;
}
Ok(())
}
async fn delete_pending_inbound_batch(&self, keys: &[PendingInboundKey<'_>]) -> Result<()> {
for key in keys {
self.delete_pending_inbound(key.chat, key.sender, key.id)
.await?;
}
Ok(())
}
}
#[derive(Debug, Clone, Copy)]
pub struct PendingInboundRow<'a> {
pub chat: &'a str,
pub sender: &'a str,
pub id: &'a str,
pub message: &'a [u8],
}
#[derive(Debug, Clone, Copy)]
pub struct PendingInboundKey<'a> {
pub chat: &'a str,
pub sender: &'a str,
pub id: &'a str,
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait DeviceStore: Send + Sync {
async fn save(&self, device: &crate::store::Device) -> Result<()>;
async fn load(&self) -> Result<Option<crate::store::Device>>;
async fn exists(&self) -> Result<bool>;
async fn create(&self) -> Result<i32>;
async fn snapshot_db(&self, _name: &str, _extra_content: Option<&[u8]>) -> Result<()> {
Ok(())
}
async fn resource_report(&self) -> crate::stats::StorageResourceReport {
crate::stats::StorageResourceReport::default()
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait MsgSecretStore: Send + Sync {
async fn put_msg_secret(
&self,
chat: &str,
sender: &str,
msg_id: &str,
secret: &[u8; crate::reporting_token::MESSAGE_SECRET_SIZE],
) -> Result<()> {
self.put_msg_secrets(vec![MsgSecretEntry {
chat: Arc::from(chat),
sender: Arc::from(sender),
msg_id: Arc::from(msg_id),
secret: *secret,
expires_at: 0,
message_ts: 0,
}])
.await?;
Ok(())
}
async fn put_msg_secrets(&self, entries: Vec<MsgSecretEntry>) -> Result<usize>;
async fn get_msg_secret(
&self,
chat: &str,
sender: &str,
msg_id: &str,
) -> Result<Option<Vec<u8>>>;
async fn get_msg_secret_with_ts(
&self,
chat: &str,
sender: &str,
msg_id: &str,
) -> Result<Option<(Vec<u8>, i64)>> {
Ok(self
.get_msg_secret(chat, sender, msg_id)
.await?
.map(|secret| (secret, 0)))
}
async fn delete_expired_msg_secrets(&self, cutoff_timestamp: i64) -> Result<u32>;
}
pub fn merge_msg_secret_expiry(existing: i64, incoming: i64) -> i64 {
if existing == 0 || incoming == 0 {
0
} else {
existing.max(incoming)
}
}
pub fn merge_msg_secret_message_ts(existing: i64, incoming: i64) -> i64 {
existing.max(incoming)
}
pub trait Backend:
SignalStore + AppSyncStore + ProtocolStore + MsgSecretStore + DeviceStore + Send + Sync
{
}
impl<T> Backend for T where
T: SignalStore + AppSyncStore + ProtocolStore + MsgSecretStore + DeviceStore + Send + Sync
{
}