use anyhow::{Result, anyhow};
use crate::libsignal::crypto::{aes_256_gcm_decrypt, aes_256_gcm_encrypt};
const GCM_IV_SIZE: usize = 12;
const GCM_TAG_SIZE: usize = 16;
const KEY_SIZE: usize = 32;
const BOT_MESSAGE_INFO: &[u8] = b"Bot Message";
#[derive(Debug, Clone, Copy)]
pub struct BotMessageContext<'a> {
pub msg_id: &'a str,
pub target_sender_user_jid: &'a str,
pub bot_user_jid: &'a str,
}
fn derive_base_bot_key(message_secret: &[u8]) -> Result<[u8; KEY_SIZE]> {
if message_secret.len() != KEY_SIZE {
return Err(anyhow!(
"invalid messageSecret length: expected {KEY_SIZE}, got {}",
message_secret.len()
));
}
let mut out = [0u8; KEY_SIZE];
crate::crypto::hkdf_sha256_into(message_secret, None, BOT_MESSAGE_INFO, &mut out)
.map_err(|e| anyhow!("HKDF expand failed: {e}"))?;
Ok(out)
}
fn derive_per_message_key(
base_key: &[u8; KEY_SIZE],
ctx: &BotMessageContext<'_>,
) -> [u8; KEY_SIZE] {
let mut info = Vec::with_capacity(
ctx.msg_id.len() + ctx.target_sender_user_jid.len() + ctx.bot_user_jid.len(),
);
info.extend_from_slice(ctx.msg_id.as_bytes());
info.extend_from_slice(ctx.target_sender_user_jid.as_bytes());
info.extend_from_slice(ctx.bot_user_jid.as_bytes());
let mut out = [0u8; KEY_SIZE];
crate::crypto::hkdf_sha256_into(base_key, None, &info, &mut out)
.expect("HKDF expand with 32-byte output never fails");
out
}
fn build_aad(ctx: &BotMessageContext<'_>) -> Vec<u8> {
let mut aad = Vec::with_capacity(ctx.msg_id.len() + 1 + ctx.bot_user_jid.len());
aad.extend_from_slice(ctx.msg_id.as_bytes());
aad.push(0);
aad.extend_from_slice(ctx.bot_user_jid.as_bytes());
aad
}
pub fn decrypt_bot_message(
message_secret: &[u8],
enc_iv: &[u8],
enc_payload: &[u8],
ctx: &BotMessageContext<'_>,
) -> Result<Vec<u8>> {
let nonce: &[u8; GCM_IV_SIZE] = enc_iv.try_into().map_err(|_| {
anyhow!(
"invalid enc_iv length: expected {GCM_IV_SIZE}, got {}",
enc_iv.len()
)
})?;
if enc_payload.len() < GCM_TAG_SIZE {
return Err(anyhow!(
"enc_payload too short: need at least {GCM_TAG_SIZE} bytes for tag, got {}",
enc_payload.len()
));
}
let base = derive_base_bot_key(message_secret)?;
let key = derive_per_message_key(&base, ctx);
let aad = build_aad(ctx);
let mut out = Vec::with_capacity(enc_payload.len().saturating_sub(GCM_TAG_SIZE));
aes_256_gcm_decrypt(&key, nonce, &aad, enc_payload, &mut out)
.map_err(|_| anyhow!("bot message GCM tag verification failed"))?;
Ok(out)
}
#[allow(dead_code)]
pub fn encrypt_bot_message(
plaintext: &[u8],
message_secret: &[u8],
ctx: &BotMessageContext<'_>,
) -> Result<(Vec<u8>, [u8; GCM_IV_SIZE])> {
use rand::Rng;
let base = derive_base_bot_key(message_secret)?;
let key = derive_per_message_key(&base, ctx);
let aad = build_aad(ctx);
let mut iv = [0u8; GCM_IV_SIZE];
rand::make_rng::<rand::rngs::StdRng>().fill_bytes(&mut iv);
let mut payload = Vec::with_capacity(plaintext.len() + GCM_TAG_SIZE);
aes_256_gcm_encrypt(&key, &iv, &aad, plaintext, &mut payload)
.map_err(|e| anyhow!("AES-GCM encrypt failed: {e}"))?;
Ok((payload, iv))
}
#[cfg(test)]
mod tests {
use super::*;
fn vec_secret(byte: u8) -> [u8; 32] {
[byte; 32]
}
fn sample_ctx() -> BotMessageContext<'static> {
BotMessageContext {
msg_id: "ABCDEF1234567890",
target_sender_user_jid: "11122233344@s.whatsapp.net",
bot_user_jid: "867051314767696@bot",
}
}
#[test]
fn derive_base_bot_key_is_deterministic_and_distinct_from_secret() {
let secret = vec_secret(0x11);
let k1 = derive_base_bot_key(&secret).unwrap();
let k2 = derive_base_bot_key(&secret).unwrap();
assert_eq!(k1, k2, "HKDF must be deterministic");
assert_ne!(
k1[..],
secret[..],
"base key must differ from raw messageSecret"
);
}
#[test]
fn derive_base_bot_key_rejects_wrong_secret_size() {
let small = [0u8; 16];
assert!(derive_base_bot_key(&small).is_err());
let big = [0u8; 64];
assert!(derive_base_bot_key(&big).is_err());
}
#[test]
fn per_message_key_is_sensitive_to_each_input() {
let base = vec_secret(0x22);
let ctx = sample_ctx();
let baseline = derive_per_message_key(&base, &ctx);
let mut altered = ctx;
altered.msg_id = "DIFFERENT_MSG_ID";
assert_ne!(baseline, derive_per_message_key(&base, &altered));
let mut altered = ctx;
altered.target_sender_user_jid = "99988877766@s.whatsapp.net";
assert_ne!(baseline, derive_per_message_key(&base, &altered));
let mut altered = ctx;
altered.bot_user_jid = "999999999999@bot";
assert_ne!(baseline, derive_per_message_key(&base, &altered));
}
#[test]
fn build_aad_matches_wa_web_layout() {
let ctx = sample_ctx();
let aad = build_aad(&ctx);
let expected = b"ABCDEF1234567890\x00867051314767696@bot";
assert_eq!(aad, expected);
}
#[test]
fn encrypt_decrypt_round_trip() {
let secret = vec_secret(0x33);
let ctx = sample_ctx();
let plaintext = b"hello bot reply";
let (payload, iv) = encrypt_bot_message(plaintext, &secret, &ctx).unwrap();
let decrypted = decrypt_bot_message(&secret, &iv, &payload, &ctx).unwrap();
assert_eq!(decrypted, plaintext);
}
#[test]
fn decrypt_rejects_tag_tampering() {
let secret = vec_secret(0x44);
let ctx = sample_ctx();
let plaintext = b"sensitive";
let (mut payload, iv) = encrypt_bot_message(plaintext, &secret, &ctx).unwrap();
let last = payload.len() - 1;
payload[last] ^= 0x01;
assert!(decrypt_bot_message(&secret, &iv, &payload, &ctx).is_err());
}
#[test]
fn decrypt_rejects_ciphertext_tampering() {
let secret = vec_secret(0x55);
let ctx = sample_ctx();
let (mut payload, iv) = encrypt_bot_message(b"plain", &secret, &ctx).unwrap();
payload[0] ^= 0xFF;
assert!(decrypt_bot_message(&secret, &iv, &payload, &ctx).is_err());
}
#[test]
fn decrypt_rejects_wrong_secret() {
let ctx = sample_ctx();
let (payload, iv) = encrypt_bot_message(b"x", &vec_secret(0x66), &ctx).unwrap();
assert!(decrypt_bot_message(&vec_secret(0x67), &iv, &payload, &ctx).is_err());
}
#[test]
fn decrypt_rejects_wrong_msg_id() {
let secret = vec_secret(0x77);
let ctx = sample_ctx();
let (payload, iv) = encrypt_bot_message(b"x", &secret, &ctx).unwrap();
let mut other = ctx;
other.msg_id = "OTHER";
assert!(decrypt_bot_message(&secret, &iv, &payload, &other).is_err());
}
#[test]
fn decrypt_rejects_wrong_bot_jid() {
let secret = vec_secret(0x88);
let ctx = sample_ctx();
let (payload, iv) = encrypt_bot_message(b"x", &secret, &ctx).unwrap();
let mut other = ctx;
other.bot_user_jid = "9876543210@bot";
assert!(decrypt_bot_message(&secret, &iv, &payload, &other).is_err());
}
#[test]
fn decrypt_rejects_short_iv() {
let secret = vec_secret(0x99);
let ctx = sample_ctx();
let payload = vec![0u8; 32];
let short_iv = [0u8; 8];
let err = decrypt_bot_message(&secret, &short_iv, &payload, &ctx).unwrap_err();
assert!(format!("{err}").contains("enc_iv"));
}
#[test]
fn decrypt_rejects_short_payload() {
let secret = vec_secret(0xAA);
let ctx = sample_ctx();
let iv = [0u8; 12];
let short_payload = [0u8; 8];
let err = decrypt_bot_message(&secret, &iv, &short_payload, &ctx).unwrap_err();
assert!(format!("{err}").contains("too short"));
}
#[test]
fn vector_round_trip_known_inputs() {
let secret = [
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c,
0x1d, 0x1e, 0x1f, 0x20,
];
let ctx = BotMessageContext {
msg_id: "3EB0FAB0BAE1234567",
target_sender_user_jid: "5511999998888@s.whatsapp.net",
bot_user_jid: "867051314767696@bot",
};
let plaintext = b"the quick brown fox jumps over the lazy dog";
let (payload, iv) = encrypt_bot_message(plaintext, &secret, &ctx).unwrap();
let mutations: &[fn(&mut BotMessageContext<'_>)] = &[
|c| c.msg_id = "X",
|c| c.target_sender_user_jid = "X@s.whatsapp.net",
|c| c.bot_user_jid = "X@bot",
];
for mutate in mutations {
let mut bad = ctx;
mutate(&mut bad);
assert!(
decrypt_bot_message(&secret, &iv, &payload, &bad).is_err(),
"mutating any context field must break decryption"
);
}
let got = decrypt_bot_message(&secret, &iv, &payload, &ctx).unwrap();
assert_eq!(got, plaintext);
}
}