use bytes::Bytes;
use thiserror::Error;
use wacore_binary::zlib_pool::decompress_zlib_pooled;
#[derive(Debug, Error)]
pub enum HistorySyncError {
#[error("Failed to decompress history sync data: {0}")]
DecompressionError(#[from] std::io::Error),
#[error("Failed to decode HistorySync protobuf: {0}")]
ProtobufDecodeError(#[from] prost::DecodeError),
#[error("Malformed protobuf: {0}")]
MalformedProtobuf(String),
}
#[derive(Debug)]
pub struct HistorySyncResult {
pub own_pushname: Option<String>,
pub nct_salt: Option<Vec<u8>>,
pub conversations_processed: usize,
pub tc_token_candidates: Vec<TcTokenCandidate>,
pub decompressed_bytes: Option<Bytes>,
}
mod wire_type {
pub const VARINT: u32 = 0;
pub const FIXED64: u32 = 1;
pub const LENGTH_DELIMITED: u32 = 2;
pub const FIXED32: u32 = 5;
}
pub fn process_history_sync(
compressed_data: Vec<u8>,
own_user: Option<&str>,
retain_blob: bool,
_compressed_size_hint: Option<u64>,
) -> Result<HistorySyncResult, HistorySyncError> {
const MAX_DECOMPRESSED: u64 = 64 * 1024 * 1024;
let decompressed = decompress_zlib_pooled(&compressed_data, MAX_DECOMPRESSED)
.map_err(HistorySyncError::DecompressionError)?;
drop(compressed_data);
let buf = Bytes::from(decompressed);
let mut pos = 0;
let mut result = HistorySyncResult {
own_pushname: None,
nct_salt: None,
conversations_processed: 0,
tc_token_candidates: Vec::new(),
decompressed_bytes: if retain_blob { Some(buf.clone()) } else { None },
};
while pos < buf.len() {
let (tag, bytes_read) = read_varint(&buf[pos..])?;
pos += bytes_read;
let field_number = (tag >> 3) as u32;
let wire_type_raw = (tag & 0x7) as u32;
match field_number {
2 if wire_type_raw == wire_type::LENGTH_DELIMITED => {
let (len, vlen) = read_varint(&buf[pos..])?;
pos += vlen;
let end = checked_end(pos, len, buf.len(), "conversation")?;
result.conversations_processed += 1;
if let Some(candidate) = extract_tc_token_fields(&buf[pos..end]) {
result.tc_token_candidates.push(candidate);
}
pos = end;
}
7 if let Some(own) = own_user
&& result.own_pushname.is_none()
&& wire_type_raw == wire_type::LENGTH_DELIMITED =>
{
let (len, vlen) = read_varint(&buf[pos..])?;
pos += vlen;
let end = checked_end(pos, len, buf.len(), "pushname")?;
if let Some(name) = extract_own_pushname(&buf[pos..end], own) {
result.own_pushname = Some(name);
}
pos = end;
}
19 if wire_type_raw == wire_type::LENGTH_DELIMITED => {
let (len, vlen) = read_varint(&buf[pos..])?;
pos += vlen;
let end = checked_end(pos, len, buf.len(), "nctSalt")?;
let salt = buf[pos..end].to_vec();
if !salt.is_empty() {
result.nct_salt = Some(salt);
}
pos = end;
}
_ => {
pos = skip_field(wire_type_raw, &buf, pos)?;
}
}
}
Ok(result)
}
#[inline]
fn checked_end(
pos: usize,
len: u64,
buf_len: usize,
field: &str,
) -> Result<usize, HistorySyncError> {
let len = usize::try_from(len).map_err(|_| {
HistorySyncError::MalformedProtobuf(format!("{field} length overflows usize: {len}"))
})?;
let end = pos.checked_add(len).ok_or_else(|| {
HistorySyncError::MalformedProtobuf(format!(
"{field} field overflows: pos={pos}, len={len}"
))
})?;
if end > buf_len {
return Err(HistorySyncError::MalformedProtobuf(format!(
"{field} field overflows buffer: pos={pos}, len={len}, buf={buf_len}"
)));
}
Ok(end)
}
#[inline]
fn read_varint(data: &[u8]) -> Result<(u64, usize), HistorySyncError> {
let mut value: u64 = 0;
let mut shift = 0u32;
for (i, &byte) in data.iter().enumerate() {
value |= ((byte & 0x7F) as u64) << shift;
if byte & 0x80 == 0 {
return Ok((value, i + 1));
}
shift += 7;
if shift >= 64 {
return Err(HistorySyncError::MalformedProtobuf(
"varint too long".into(),
));
}
}
Err(HistorySyncError::MalformedProtobuf(
"unexpected end of data in varint".into(),
))
}
#[inline]
fn skip_field(wire_type: u32, buf: &[u8], pos: usize) -> Result<usize, HistorySyncError> {
match wire_type {
wire_type::VARINT => {
let (_, vlen) = read_varint(&buf[pos..])?;
Ok(pos + vlen)
}
wire_type::FIXED64 => checked_end(pos, 8, buf.len(), "fixed64"),
wire_type::LENGTH_DELIMITED => {
let (len, vlen) = read_varint(&buf[pos..])?;
checked_end(pos + vlen, len, buf.len(), "length-delimited")
}
wire_type::FIXED32 => checked_end(pos, 4, buf.len(), "fixed32"),
_ => {
log::warn!("Unknown wire type {wire_type} in history sync, cannot skip");
Err(HistorySyncError::MalformedProtobuf(format!(
"unknown wire type {wire_type}"
)))
}
}
}
fn extract_own_pushname(data: &[u8], own_user: &str) -> Option<String> {
let mut pos = 0;
let mut id_match = false;
let mut pushname: Option<String> = None;
while pos < data.len() {
let (tag, bytes_read) = read_varint(data.get(pos..)?).ok()?;
pos += bytes_read;
let field_number = (tag >> 3) as u32;
let wt = (tag & 0x7) as u32;
match field_number {
1 if wt == wire_type::LENGTH_DELIMITED => {
let (len, vlen) = read_varint(data.get(pos..)?).ok()?;
pos += vlen;
let len = usize::try_from(len).ok()?;
let end = pos.checked_add(len).filter(|&e| e <= data.len())?;
let id = std::str::from_utf8(data.get(pos..end)?).ok()?;
id_match = id == own_user;
if !id_match {
return None; }
pos = end;
}
2 if wt == wire_type::LENGTH_DELIMITED => {
let (len, vlen) = read_varint(data.get(pos..)?).ok()?;
pos += vlen;
let len = usize::try_from(len).ok()?;
let end = pos.checked_add(len).filter(|&e| e <= data.len())?;
let name = std::str::from_utf8(data.get(pos..end)?).ok()?;
pushname = Some(name.to_string());
pos = end;
}
_ => {
pos = skip_field(wt, data, pos).ok()?;
}
}
}
if id_match { pushname } else { None }
}
#[derive(Clone, PartialEq, prost::Message)]
pub(crate) struct ConversationTcTokenFields {
#[prost(string, required, tag = "1")]
pub id: String,
#[prost(bytes = "vec", optional, tag = "21")]
pub tc_token: Option<Vec<u8>>,
#[prost(uint64, optional, tag = "22")]
pub tc_token_timestamp: Option<u64>,
#[prost(uint64, optional, tag = "28")]
pub tc_token_sender_timestamp: Option<u64>,
}
pub(crate) fn extract_tc_token_fields(data: &[u8]) -> Option<TcTokenCandidate> {
use prost::Message;
let conv = ConversationTcTokenFields::decode(data).ok()?;
if let Some(parts) = wacore_binary::jid::parse_jid_fast(&conv.id)
&& (parts.server == "g.us" || parts.server == "newsletter" || parts.server == "bot")
{
return None;
}
let tc_token = conv.tc_token.filter(|t| !t.is_empty())?;
let tc_token_timestamp = conv.tc_token_timestamp?;
Some(TcTokenCandidate {
id: conv.id,
tc_token,
tc_token_timestamp,
tc_token_sender_timestamp: conv.tc_token_sender_timestamp,
})
}
#[derive(Debug)]
pub struct TcTokenCandidate {
pub id: String,
pub tc_token: Vec<u8>,
pub tc_token_timestamp: u64,
pub tc_token_sender_timestamp: Option<u64>,
}
#[cfg(test)]
mod tests {
use super::*;
use flate2::Compression;
use flate2::write::ZlibEncoder;
use prost::Message;
use std::io::Write;
use waproto::whatsapp as wa;
fn encode_and_compress(hs: &wa::HistorySync) -> Vec<u8> {
let proto_bytes = hs.encode_to_vec();
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
encoder.write_all(&proto_bytes).unwrap();
encoder.finish().unwrap()
}
#[test]
fn test_nct_salt_extracted_from_history_sync() {
let salt = vec![0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF];
let hs = wa::HistorySync {
sync_type: wa::history_sync::HistorySyncType::InitialBootstrap as i32,
nct_salt: Some(salt.clone()),
..Default::default()
};
let compressed = encode_and_compress(&hs);
let result = process_history_sync(compressed, None, false, None).unwrap();
assert_eq!(result.nct_salt, Some(salt));
}
#[test]
fn test_nct_salt_none_when_absent() {
let hs = wa::HistorySync {
sync_type: wa::history_sync::HistorySyncType::InitialBootstrap as i32,
..Default::default()
};
let compressed = encode_and_compress(&hs);
let result = process_history_sync(compressed, None, false, None).unwrap();
assert!(result.nct_salt.is_none());
}
#[test]
fn test_nct_salt_and_pushname_coexist() {
let salt = vec![0x01, 0x02, 0x03];
let hs = wa::HistorySync {
sync_type: wa::history_sync::HistorySyncType::InitialBootstrap as i32,
nct_salt: Some(salt.clone()),
pushnames: vec![wa::Pushname {
id: Some("0000000000".into()),
pushname: Some("TestUser".into()),
}],
..Default::default()
};
let compressed = encode_and_compress(&hs);
let result = process_history_sync(compressed, Some("0000000000"), false, None).unwrap();
assert_eq!(result.nct_salt, Some(salt));
assert_eq!(result.own_pushname.as_deref(), Some("TestUser"));
}
}