#![allow(clippy::disallowed_methods)]
use buffa::Message;
use wacore::appstate::WAPATCH_INTEGRITY;
use wacore::appstate::hash::{HashState, generate_content_mac};
use wacore::appstate::keys::expand_app_state_keys;
use wacore::appstate::processor::validate_patch_macs;
use waproto::whatsapp as wa;
fn make_mutation(
op: wa::syncd_mutation::SyncdOperation,
key_id: &[u8],
index_mac: Vec<u8>,
value_blob: Option<Vec<u8>>,
) -> wa::SyncdMutation {
wa::SyncdMutation {
operation: Some(op.into()),
record: wa::SyncdRecord {
index: wa::SyncdIndex {
blob: Some(index_mac),
}
.into(),
value: match value_blob {
Some(b) => wa::SyncdValue { blob: Some(b) }.into(),
None => Default::default(),
},
key_id: wa::KeyId {
id: Some(key_id.to_vec()),
}
.into(),
}
.into(),
}
}
fn create_value_blob(
keys: &wacore::appstate::keys::ExpandedAppStateKeys,
key_id: &[u8],
) -> Vec<u8> {
let iv = [1u8; 16];
let ciphertext = b"test_content".to_vec();
let mut content = iv.to_vec();
content.extend_from_slice(&ciphertext);
let value_mac = generate_content_mac(
wa::syncd_mutation::SyncdOperation::SET,
&content,
key_id,
&keys.value_mac,
);
let mut value_blob = content;
value_blob.extend_from_slice(&value_mac);
value_blob
}
#[test]
fn test_has_missing_remove_flag_set_on_remove_without_previous_value() {
let mut state = HashState::default();
let key_id = b"test_key";
let index_mac = vec![1u8; 32];
let mutations = vec![make_mutation(
wa::syncd_mutation::SyncdOperation::REMOVE,
key_id,
index_mac,
None,
)];
let (result, err) = state.update_hash(&mutations, |_, _| Ok(None));
assert!(err.is_ok());
assert!(
result.has_missing_remove,
"has_missing_remove should be true when REMOVE targets missing entry"
);
}
#[test]
fn test_has_missing_remove_flag_not_set_on_set_without_previous_value() {
let mut state = HashState::default();
let key_id = b"test_key";
let index_mac = vec![1u8; 32];
let master_key = [7u8; 32];
let keys = expand_app_state_keys(&master_key);
let value_blob = create_value_blob(&keys, key_id);
let mutations = vec![make_mutation(
wa::syncd_mutation::SyncdOperation::SET,
key_id,
index_mac,
Some(value_blob),
)];
let (result, err) = state.update_hash(&mutations, |_, _| Ok(None));
assert!(err.is_ok());
assert!(
!result.has_missing_remove,
"has_missing_remove should be false for SET without previous value"
);
}
#[test]
fn test_has_missing_remove_flag_not_set_when_previous_value_exists() {
let mut state = HashState::default();
let key_id = b"test_key";
let index_mac = vec![1u8; 32];
let previous_value_mac = vec![99u8; 32];
let mutations = vec![make_mutation(
wa::syncd_mutation::SyncdOperation::REMOVE,
key_id,
index_mac.clone(),
None,
)];
let (result, err) = state.update_hash(&mutations, |idx, _| {
if idx == index_mac {
Ok(Some(previous_value_mac.clone()))
} else {
Ok(None)
}
});
assert!(err.is_ok());
assert!(
!result.has_missing_remove,
"has_missing_remove should be false when previous value exists"
);
}
#[test]
fn test_lthash_diverges_on_missing_remove() {
let master_key = [7u8; 32];
let keys = expand_app_state_keys(&master_key);
let key_id = b"test_key";
let index_mac_1 = vec![1u8; 32];
let value_blob_1 = create_value_blob(&keys, key_id);
let value_mac_1 = value_blob_1[value_blob_1.len() - 32..].to_vec();
let mut state = HashState::default();
let set_mutation = make_mutation(
wa::syncd_mutation::SyncdOperation::SET,
key_id,
index_mac_1.clone(),
Some(value_blob_1),
);
let (_, err) = state.update_hash(&[set_mutation], |_, _| Ok(None));
assert!(err.is_ok());
let expected_hash_with_entry = WAPATCH_INTEGRITY.subtract_then_add(
&[0u8; 128],
&[] as &[Vec<u8>],
std::slice::from_ref(&value_mac_1),
);
assert_eq!(state.hash.as_slice(), expected_hash_with_entry.as_slice());
let index_mac_2 = vec![2u8; 32];
let remove_mutation = make_mutation(
wa::syncd_mutation::SyncdOperation::REMOVE,
key_id,
index_mac_2,
None,
);
let (result, err) = state.update_hash(&[remove_mutation], |_, _| Ok(None));
assert!(err.is_ok());
assert!(result.has_missing_remove);
assert_eq!(
state.hash.as_slice(),
expected_hash_with_entry.as_slice(),
"Hash should remain unchanged when we can't subtract a missing value"
);
}
#[test]
fn test_external_mutations_decode_from_syncd_mutations() {
let key_id = b"test_key";
let master_key = [7u8; 32];
let keys = expand_app_state_keys(&master_key);
let mutation1 = make_mutation(
wa::syncd_mutation::SyncdOperation::SET,
key_id,
vec![1u8; 32],
Some(create_value_blob(&keys, key_id)),
);
let mutation2 = make_mutation(
wa::syncd_mutation::SyncdOperation::REMOVE,
key_id,
vec![2u8; 32],
None,
);
let syncd_mutations = wa::SyncdMutations {
mutations: vec![mutation1, mutation2],
};
let encoded = syncd_mutations.encode_to_vec();
let decoded = wa::SyncdMutations::decode_from_slice(&encoded).expect("should decode");
assert_eq!(decoded.mutations.len(), 2);
assert_eq!(
decoded.mutations[0].operation,
Some(wa::syncd_mutation::SyncdOperation::SET.into())
);
assert_eq!(
decoded.mutations[1].operation,
Some(wa::syncd_mutation::SyncdOperation::REMOVE.into())
);
}
#[test]
fn test_validate_patch_macs_reports_divergence_regardless_of_has_missing_remove() {
let master_key = [7u8; 32];
let keys = expand_app_state_keys(&master_key);
let key_id = b"test_key";
let collection_name = "regular_low";
let state = HashState {
version: 1,
hash: [42u8; 128], ..Default::default()
};
let patch = wa::SyncdPatch {
version: wa::SyncdVersion { version: Some(2) }.into(),
snapshot_mac: Some(vec![0u8; 32]), key_id: wa::KeyId {
id: Some(key_id.to_vec()),
}
.into(),
..Default::default()
};
for has_missing_remove in [false, true] {
let verdict = validate_patch_macs(
&patch,
&state,
&keys,
collection_name,
false,
has_missing_remove,
)
.expect("a snapshotMAC mismatch is divergence, not a rejected patch");
assert!(
verdict.snapshot_mac_diverged,
"divergence must be reported with has_missing_remove={has_missing_remove}"
);
}
}
#[test]
fn test_mixed_set_and_remove_with_missing_remove() {
let master_key = [7u8; 32];
let keys = expand_app_state_keys(&master_key);
let key_id = b"test_key";
let mut state = HashState::default();
let index_mac_known = vec![1u8; 32];
let value_blob_known = create_value_blob(&keys, key_id);
let value_mac_known = value_blob_known[value_blob_known.len() - 32..].to_vec();
let set_known = make_mutation(
wa::syncd_mutation::SyncdOperation::SET,
key_id,
index_mac_known.clone(),
Some(value_blob_known),
);
let (_, err) = state.update_hash(&[set_known], |_, _| Ok(None));
assert!(err.is_ok());
let index_mac_new = vec![3u8; 32];
let value_blob_new = create_value_blob(&keys, key_id);
let index_mac_unknown = vec![99u8; 32];
let mutations = vec![
make_mutation(
wa::syncd_mutation::SyncdOperation::SET,
key_id,
index_mac_new,
Some(value_blob_new),
),
make_mutation(
wa::syncd_mutation::SyncdOperation::REMOVE,
key_id,
index_mac_known.clone(),
None,
),
make_mutation(
wa::syncd_mutation::SyncdOperation::REMOVE,
key_id,
index_mac_unknown,
None,
),
];
let (result, err) = state.update_hash(&mutations, |idx, _| {
if idx == index_mac_known {
Ok(Some(value_mac_known.clone()))
} else {
Ok(None)
}
});
assert!(err.is_ok());
assert!(
result.has_missing_remove,
"Should have has_missing_remove due to unknown REMOVE"
);
}