use super::*;
use anyhow::Context as _;
fn is_exact_dm_sender_device(device_jid: &Jid, own_jid: &Jid, own_lid: Option<&Jid>) -> bool {
(device_jid.is_same_user_as(own_jid) && device_jid.device == own_jid.device)
|| own_lid
.is_some_and(|lid| device_jid.is_same_user_as(lid) && device_jid.device == lid.device)
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(name = "wa.send.dm_partition", level = "debug", skip_all)
)]
pub(crate) fn partition_dm_devices(
mut all_devices: Vec<Jid>,
own_jid: &Jid,
own_lid: Option<&Jid>,
) -> PartitionedDmDevices {
let mut recipient_count = 0;
let mut device_index = 0;
while device_index < all_devices.len() {
if is_exact_dm_sender_device(&all_devices[device_index], own_jid, own_lid) {
all_devices.swap_remove(device_index);
continue;
}
if !all_devices[device_index].matches_user_or_lid(own_jid, own_lid) {
all_devices.swap(device_index, recipient_count);
recipient_count += 1;
}
device_index += 1;
}
PartitionedDmDevices {
devices: all_devices,
recipient_count,
}
}
pub(crate) struct PartitionedDmDevices {
devices: Vec<Jid>,
recipient_count: usize,
}
impl crate::stats::HeapSize for PartitionedDmDevices {
fn heap_bytes(&self) -> usize {
self.devices.capacity() * size_of::<Jid>()
+ self.devices.iter().map(|j| j.heap_bytes()).sum::<usize>()
}
}
impl PartitionedDmDevices {
pub(crate) fn valid_devices(&self) -> &[Jid] {
&self.devices
}
pub(crate) fn recipient_devices(&self) -> &[Jid] {
&self.devices[..self.recipient_count]
}
pub(crate) fn own_other_devices(&self) -> &[Jid] {
&self.devices[self.recipient_count..]
}
}
pub struct PreparedDmStanza {
pub node: Node,
pub phash: Option<CompactString>,
pub message_secret: Option<[u8; crate::reporting_token::MESSAGE_SECRET_SIZE]>,
}
pub struct DmStanzaRequest<'a> {
pub own_jid: &'a Jid,
pub account: Option<&'a wa::ADVSignedDeviceIdentity>,
pub to: &'a Jid,
pub message: &'a wa::Message,
pub message_id: &'a str,
pub edit: Option<&'a crate::types::message::EditAttribute>,
pub extra_nodes: &'a [Node],
pub devices: &'a ResolvedDmDevices,
pub pre_encoded: Option<&'a [u8]>,
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(name = "wa.send.dm_prepare", level = "debug", skip_all, err(Debug))
)]
pub async fn prepare_dm_stanza(
runtime: &dyn Runtime,
stores: &mut SignalStores<'_>,
resolver: &dyn SendContextResolver,
request: DmStanzaRequest<'_>,
) -> Result<PreparedDmStanza> {
let DmStanzaRequest {
own_jid,
account,
to: to_jid,
message,
message_id: request_id,
edit,
extra_nodes: extra_stanza_nodes,
devices: resolved_devices,
pre_encoded,
} = request;
let shared_content = message.message_context_info.is_unset().then(|| {
pre_encoded.map_or_else(
|| std::borrow::Cow::Owned(waproto::codec::message_to_vec(message)),
std::borrow::Cow::Borrowed,
)
});
let existing_secret = crate::reporting_token::extract_message_secret(message);
let reporting_result = match &shared_content {
Some(content) => generate_reporting_token_from_encoded(
message,
content,
request_id,
own_jid,
to_jid,
existing_secret,
),
None => generate_reporting_token(message, request_id, own_jid, to_jid, existing_secret),
};
let extra_context = reporting_result.as_ref().map(reporting_context_info);
let recipient_devices = resolved_devices.recipient_devices();
let own_other_devices = resolved_devices.own_other_devices();
let total_devices = resolved_devices.devices().len();
let phash = resolved_devices.phash();
let crate::messages::DmPlaintexts {
recipient: recipient_plaintext,
own_devices: own_devices_plaintext,
} = match &shared_content {
Some(content) if own_other_devices.is_empty() => crate::messages::DmPlaintexts {
recipient: MessageUtils::pad_with_context_from_encoded(content, extra_context.as_ref()),
own_devices: Vec::new(),
},
Some(content) => {
MessageUtils::dm_plaintexts_from_encoded(content, extra_context.as_ref(), to_jid)
}
None => MessageUtils::encode_dm_plaintexts(message, extra_context.as_ref(), to_jid),
};
let mut participant_nodes = Vec::with_capacity(total_devices);
let mut includes_prekey_message = false;
let hide_decrypt_fail = should_hide_decrypt_fail_for_send(edit, message);
let mediatype = media_type_from_message(message);
if !recipient_devices.is_empty() {
let summary = encrypt_for_devices_into(
runtime,
stores,
resolver,
recipient_devices,
&recipient_plaintext,
hide_decrypt_fail,
mediatype,
&mut participant_nodes,
)
.await?;
includes_prekey_message = includes_prekey_message || summary.includes_prekey_message;
}
if !own_other_devices.is_empty() {
let summary = encrypt_for_devices_into(
runtime,
stores,
resolver,
own_other_devices,
&own_devices_plaintext,
hide_decrypt_fail,
mediatype,
&mut participant_nodes,
)
.await?;
includes_prekey_message = includes_prekey_message || summary.includes_prekey_message;
}
let attempted_devices = total_devices;
if participant_nodes.is_empty() && attempted_devices > 0 {
return Err(anyhow!(
"encryption failed for all {attempted_devices} recipient device(s)"
));
}
let mut message_content_nodes = Vec::with_capacity(3 + extra_stanza_nodes.len());
message_content_nodes.push(
NodeBuilder::new("participants")
.children(participant_nodes)
.build(),
);
if let Some(device_identity_bytes) = needs_device_identity(includes_prekey_message, account)
.ok()
.flatten()
{
message_content_nodes.push(
NodeBuilder::new("device-identity")
.bytes(device_identity_bytes)
.build(),
);
}
if let Some(ref result) = reporting_result {
message_content_nodes.push(build_reporting_node(result));
}
message_content_nodes.extend(extra_stanza_nodes.iter().cloned());
let stanza_type = stanza_type_from_message(message);
let mut stanza_builder = NodeBuilder::new("message")
.attr("to", to_jid)
.attr("id", request_id)
.attr("type", stanza_type);
if let Some(edit_attr) = edit
&& *edit_attr != crate::types::message::EditAttribute::Empty
{
stanza_builder = stanza_builder.attr("edit", edit_attr.to_string_val());
}
let stanza = stanza_builder.children(message_content_nodes).build();
Ok(PreparedDmStanza {
node: stanza,
phash,
message_secret: reporting_result.map(|r| r.message_secret),
})
}
pub async fn pkmsg_would_be_emitted<S>(
session_store: &mut S,
signal_address: &ProtocolAddress,
) -> Result<bool>
where
S: crate::libsignal::protocol::SessionStore,
{
let loaded =
crate::libsignal::protocol::SessionCheckout::load(session_store, signal_address).await?;
let needs_pkmsg = if let Some(session) = loaded.as_ref()
&& let Some(state) = session.record().session_state()
&& let Ok(None) = state.unacknowledged_pre_key_message_items()
{
false
} else {
true
};
if let Some(session) = loaded {
session
.commit()
.await
.context("restoring checked-out session after pairwise retry pre-flight")?;
}
Ok(needs_pkmsg)
}
#[derive(Debug)]
pub enum PairwiseRetryDestination {
Direct {
to: Jid,
recipient: Option<Jid>,
},
Participant {
to: Jid,
participant: Jid,
addressing_mode: Option<crate::types::message::AddressingMode>,
},
}
pub struct PairwiseRetryRequest<'a> {
pub destination: PairwiseRetryDestination,
pub encryption_jid: Jid,
pub message: &'a wa::Message,
pub message_id: String,
pub retry_count: u8,
pub account: Option<&'a wa::ADVSignedDeviceIdentity>,
pub edit: Option<crate::types::message::EditAttribute>,
pub pre_encoded: Option<&'a [u8]>,
}
#[inline]
fn is_pairwise_user(jid: &Jid) -> bool {
!jid.is_empty()
&& matches!(
jid.server,
wacore_binary::Server::Pn
| wacore_binary::Server::Lid
| wacore_binary::Server::Hosted
| wacore_binary::Server::HostedLid
| wacore_binary::Server::Bot
)
}
fn validate_pairwise_retry_route(
destination: &PairwiseRetryDestination,
encryption_jid: &Jid,
) -> Result<()> {
if !is_pairwise_user(encryption_jid) {
bail!("pairwise retry encryption target must be a user device JID");
}
match destination {
PairwiseRetryDestination::Direct { to, recipient } => {
if !is_pairwise_user(to) {
bail!("direct retry destination must be a user JID");
}
if recipient.as_ref().is_some_and(|jid| !is_pairwise_user(jid)) {
bail!("direct retry recipient must be a user JID");
}
}
PairwiseRetryDestination::Participant {
to,
participant,
addressing_mode,
} => {
if !is_pairwise_user(participant) {
bail!("participant retry target must be a user device JID");
}
if to.is_group() {
if addressing_mode.is_none() {
bail!("group retry requires an addressing mode");
}
} else if to.is_broadcast_list() {
if addressing_mode.is_some() {
bail!("broadcast retry must not carry a group addressing mode");
}
} else {
bail!("participant retry destination must be a group or broadcast list");
}
}
}
Ok(())
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(name = "wa.send.pairwise_retry", level = "debug", skip_all, err(Debug))
)]
pub async fn prepare_pairwise_retry_stanza<S, I>(
session_store: &mut S,
identity_store: &mut I,
request: PairwiseRetryRequest<'_>,
) -> Result<Node>
where
S: crate::libsignal::protocol::SessionStore,
I: crate::libsignal::protocol::IdentityKeyStore,
{
let PairwiseRetryRequest {
destination,
encryption_jid,
message,
message_id,
retry_count,
account,
edit,
pre_encoded,
} = request;
if message_id.is_empty() {
bail!("retry message ID must not be empty");
}
if !(1..crate::protocol::retry::MAX_RETRY_COUNT).contains(&retry_count) {
bail!(
"retry count {retry_count} must be in 1..{}",
crate::protocol::retry::MAX_RETRY_COUNT
);
}
validate_pairwise_retry_route(&destination, &encryption_jid)?;
let plaintext = match pre_encoded {
Some(content) => MessageUtils::pad_with_context_from_encoded(content, None),
None => MessageUtils::encode_and_pad(message),
};
let signal_address = encryption_jid.to_protocol_address();
if account.is_none() && pkmsg_would_be_emitted(session_store, &signal_address).await? {
bail!(
"pairwise retry pkmsg requires <device-identity> (account is None); \
refusing before message_encrypt to avoid advancing the sender chain"
);
}
let encrypted =
message_encrypt(&plaintext, &signal_address, session_store, identity_store).await?;
let (enc_type, is_prekey, serialized) = extract_ciphertext(encrypted)
.ok_or_else(|| anyhow!("Unexpected encryption message type for pairwise retry"))?;
let hide_decrypt_fail = should_hide_decrypt_fail_for_send(edit.as_ref(), message);
let mut enc_builder = NodeBuilder::new("enc")
.attr("v", stanza::ENC_VERSION)
.attr("type", enc_type)
.attr("count", retry_count);
if let Some(mt) = media_type_from_message(message) {
enc_builder = enc_builder.attr("mediatype", mt);
}
if hide_decrypt_fail {
enc_builder = enc_builder.attr("decrypt-fail", "hide");
}
let enc_node = enc_builder.bytes(serialized).build();
let mut children = vec![enc_node];
if let Some(device_identity_bytes) = needs_device_identity(is_prekey, account)? {
children.push(
NodeBuilder::new("device-identity")
.bytes(device_identity_bytes)
.build(),
);
}
let mut stanza_builder = NodeBuilder::new("message");
match destination {
PairwiseRetryDestination::Direct { to, recipient } => {
stanza_builder = stanza_builder.attr("to", to);
if let Some(recipient) = recipient {
stanza_builder = stanza_builder.attr("recipient", recipient);
}
}
PairwiseRetryDestination::Participant {
to,
participant,
addressing_mode,
} => {
stanza_builder = stanza_builder
.attr("to", to)
.attr("participant", participant);
if let Some(addressing_mode) = addressing_mode {
stanza_builder = stanza_builder.attr("addressing_mode", addressing_mode.as_str());
}
}
}
stanza_builder = stanza_builder
.attr("id", message_id)
.attr("type", stanza_type_from_message(message));
if let Some(e) = edit
&& e != crate::types::message::EditAttribute::Empty
{
stanza_builder = stanza_builder.attr("edit", e.to_string_val());
}
Ok(stanza_builder.children(children).build())
}
#[cfg(test)]
mod partition_tests {
use super::*;
#[test]
fn partition_dm_devices_reuses_input_allocation() {
let own_jid = Jid::lid_device("123456789".to_owned(), 7);
let devices = vec![
Jid::lid_device("987654321".to_owned(), 0),
Jid::lid_device("123456789".to_owned(), 0),
own_jid.clone(),
Jid::lid_device("987654321".to_owned(), 1),
];
let allocation = devices.as_ptr();
let capacity = devices.capacity();
let partitioned = partition_dm_devices(devices, &own_jid, None);
assert_eq!(partitioned.devices.as_ptr(), allocation);
assert_eq!(partitioned.devices.capacity(), capacity);
assert_eq!(partitioned.valid_devices().len(), 3);
assert_eq!(partitioned.recipient_devices().len(), 2);
assert_eq!(partitioned.own_other_devices().len(), 1);
assert!(
partitioned
.recipient_devices()
.iter()
.all(|device| device.user == "987654321")
);
assert_eq!(partitioned.own_other_devices()[0].device, 0);
}
}