wacore_appstate/
encode.rs1use crate::hash::generate_content_mac;
2use crate::keys::ExpandedAppStateKeys;
3use prost::Message;
4use wacore_libsignal::crypto::{CryptographicMac, aes_256_cbc_encrypt_into};
5use waproto::whatsapp as wa;
6
7pub fn encode_record(
16 operation: wa::syncd_mutation::SyncdOperation,
17 index: &[u8],
18 value: &wa::SyncActionValue,
19 keys: &ExpandedAppStateKeys,
20 key_id: &[u8],
21 iv: &[u8; 16],
22) -> (wa::SyncdMutation, Vec<u8>) {
23 let action_data = wa::SyncActionData {
25 index: Some(index.to_vec()),
26 value: Some(value.clone()),
27 padding: Some(vec![]),
28 version: Some(1),
29 };
30 let plaintext = action_data.encode_to_vec();
31
32 let mut ciphertext = Vec::new();
34 aes_256_cbc_encrypt_into(&plaintext, &keys.value_encryption, iv, &mut ciphertext)
35 .expect("AES encryption should not fail with valid 32-byte key and 16-byte IV");
36
37 let mut iv_and_cipher = Vec::with_capacity(16 + ciphertext.len());
39 iv_and_cipher.extend_from_slice(iv);
40 iv_and_cipher.extend_from_slice(&ciphertext);
41
42 let value_mac = generate_content_mac(operation, &iv_and_cipher, key_id, &keys.value_mac);
44
45 let mut value_blob = iv_and_cipher;
47 value_blob.extend_from_slice(&value_mac);
48
49 let index_mac = {
51 let mut mac = CryptographicMac::new("HmacSha256", &keys.index)
52 .expect("HmacSha256 is a valid algorithm");
53 mac.update(index);
54 mac.finalize()
55 };
56
57 let record = wa::SyncdRecord {
59 index: Some(wa::SyncdIndex {
60 blob: Some(index_mac),
61 }),
62 value: Some(wa::SyncdValue {
63 blob: Some(value_blob),
64 }),
65 key_id: Some(wa::KeyId {
66 id: Some(key_id.to_vec()),
67 }),
68 };
69
70 let mutation = wa::SyncdMutation {
71 operation: Some(operation as i32),
72 record: Some(record),
73 };
74
75 (mutation, value_mac)
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81 use crate::decode::decode_record;
82 use crate::keys::expand_app_state_keys;
83
84 #[test]
85 fn test_encode_then_decode_roundtrip() {
86 let master_key = [7u8; 32];
87 let keys = expand_app_state_keys(&master_key);
88 let key_id = b"test_key_id";
89 let iv = [0u8; 16];
90
91 let index = b"[\"setting_pushName\"]";
92 let value = wa::SyncActionValue {
93 push_name_setting: Some(wa::sync_action_value::PushNameSetting {
94 name: Some("Test User".to_string()),
95 }),
96 timestamp: Some(1234567890),
97 ..Default::default()
98 };
99
100 let (mutation, _value_mac) = encode_record(
101 wa::syncd_mutation::SyncdOperation::Set,
102 index,
103 &value,
104 &keys,
105 key_id,
106 &iv,
107 );
108
109 let record = mutation.record.as_ref().unwrap();
111 let decoded = decode_record(
112 wa::syncd_mutation::SyncdOperation::Set,
113 record,
114 &keys,
115 key_id,
116 true, )
118 .expect("roundtrip decode should succeed");
119
120 assert_eq!(
121 decoded.action_value.as_ref().and_then(|v| v.timestamp),
122 Some(1234567890)
123 );
124 assert_eq!(
125 decoded
126 .action_value
127 .as_ref()
128 .and_then(|v| v.push_name_setting.as_ref())
129 .and_then(|p| p.name.as_deref()),
130 Some("Test User")
131 );
132 assert_eq!(decoded.index, vec!["setting_pushName"]);
133 assert_eq!(decoded.operation, wa::syncd_mutation::SyncdOperation::Set);
134 }
135}