use anyhow::{Result, ensure};
use waproto::whatsapp as wa;
use crate::secret_enc_addon::{AddonContext, ModificationType, decrypt_addon, encrypt_addon};
const GCM_IV_SIZE: usize = 12;
const MESSAGE_SECRET_SIZE: usize = 32;
fn comment_addon_ctx<'a>(
parent_msg_id: &'a str,
parent_sender_jid: &'a str,
commenter_jid: &'a str,
) -> AddonContext<'a> {
AddonContext {
stanza_id: parent_msg_id,
parent_msg_original_sender: parent_sender_jid,
modification_sender: commenter_jid,
modification_type: ModificationType::EncComment,
}
}
pub fn encrypt_comment_with_secret(
inner: &wa::Message,
message_secret: &[u8],
parent_msg_id: &str,
parent_sender_jid: &str,
commenter_jid: &str,
) -> Result<(Vec<u8>, [u8; GCM_IV_SIZE])> {
ensure!(
message_secret.len() == MESSAGE_SECRET_SIZE,
"message_secret must be {MESSAGE_SECRET_SIZE} bytes, got {}",
message_secret.len()
);
let plaintext = waproto::codec::message_to_vec(inner);
encrypt_addon(
&plaintext,
message_secret,
&comment_addon_ctx(parent_msg_id, parent_sender_jid, commenter_jid),
)
}
pub fn decrypt_comment_with_secret(
enc_payload: &[u8],
iv: &[u8],
message_secret: &[u8],
parent_msg_id: &str,
parent_sender_jid: &str,
commenter_jid: &str,
) -> Result<wa::Message> {
ensure!(
message_secret.len() == MESSAGE_SECRET_SIZE,
"message_secret must be {MESSAGE_SECRET_SIZE} bytes, got {}",
message_secret.len()
);
let plaintext = decrypt_addon(
enc_payload,
iv,
message_secret,
&comment_addon_ctx(parent_msg_id, parent_sender_jid, commenter_jid),
)?;
Ok(waproto::codec::message_decode(&plaintext[..])?)
}
#[cfg(test)]
mod tests {
use super::*;
const SECRET: [u8; 32] = [0x24; 32];
const PARENT_ID: &str = "3EB0POST";
const AUTHOR: &str = "111111111111111@lid";
const COMMENTER: &str = "222222222222222@lid";
fn body(text: &str) -> wa::Message {
wa::Message {
extended_text_message: buffa::MessageField::some(wa::message::ExtendedTextMessage {
text: Some(text.to_string()),
..Default::default()
}),
..Default::default()
}
}
#[test]
fn roundtrip_extended_text_body() {
let inner = body("nice post");
let (enc, iv) =
encrypt_comment_with_secret(&inner, &SECRET, PARENT_ID, AUTHOR, COMMENTER).unwrap();
let out =
decrypt_comment_with_secret(&enc, &iv, &SECRET, PARENT_ID, AUTHOR, COMMENTER).unwrap();
assert_eq!(
out.extended_text_message
.as_option()
.and_then(|m| m.text.as_deref()),
Some("nice post")
);
}
#[test]
fn use_case_differs_from_reaction() {
let inner = body("hi");
let (enc, iv) =
encrypt_comment_with_secret(&inner, &SECRET, PARENT_ID, AUTHOR, COMMENTER).unwrap();
assert!(
crate::reaction::decrypt_reaction_with_secret(
&enc, &iv, &SECRET, PARENT_ID, AUTHOR, COMMENTER
)
.is_err()
);
}
#[test]
fn invalid_secret_size_rejected() {
let inner = body("x");
assert!(
encrypt_comment_with_secret(&inner, &[0u8; 31], PARENT_ID, AUTHOR, COMMENTER).is_err()
);
}
}