Skip to main content

wacore_appstate/
encode.rs

1use 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
7/// Encode and encrypt a mutation into a SyncdRecord.
8///
9/// This is the reverse of `decode_record` — it takes plaintext data and produces
10/// an encrypted record ready for sending.
11///
12/// # Returns
13/// A tuple of (SyncdMutation, value_mac_bytes) where value_mac is needed for
14/// hash state updates and persistence.
15pub 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    // 1. Build SyncActionData
24    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    // 2. AES-256-CBC encrypt
33    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    // 3. Build IV || ciphertext
38    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    // 4. Generate content MAC
43    let value_mac = generate_content_mac(operation, &iv_and_cipher, key_id, &keys.value_mac);
44
45    // 5. Complete value blob: IV || ciphertext || MAC
46    let mut value_blob = iv_and_cipher;
47    value_blob.extend_from_slice(&value_mac);
48
49    // 6. Generate index MAC
50    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    // 7. Build the record
58    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        // Decode the encoded record
110        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, // validate MACs
117        )
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}