Skip to main content

wacore_appstate/
decode.rs

1use crate::AppStateError;
2use crate::hash::{generate_content_mac, validate_index_mac};
3use crate::keys::ExpandedAppStateKeys;
4use prost::Message;
5use wacore_libsignal::crypto::aes_256_cbc_decrypt_into;
6use waproto::whatsapp as wa;
7
8/// A decoded mutation from an app state record.
9#[derive(Debug, Clone)]
10pub struct Mutation {
11    /// The decoded action value.
12    pub action_value: Option<wa::SyncActionValue>,
13    /// The MAC of the index.
14    pub index_mac: Vec<u8>,
15    /// The MAC of the value.
16    pub value_mac: Vec<u8>,
17    /// The parsed index components (JSON array of strings).
18    pub index: Vec<String>,
19    /// The operation type (Set or Remove).
20    pub operation: wa::syncd_mutation::SyncdOperation,
21}
22
23/// Decode a single encrypted record into a mutation.
24///
25/// This is a pure, synchronous function that takes the expanded keys directly,
26/// avoiding any async key lookup.
27///
28/// # Arguments
29/// * `operation` - The operation type (Set or Remove)
30/// * `record` - The encrypted SyncdRecord to decode
31/// * `keys` - The pre-expanded app state keys for decryption
32/// * `key_id` - The key ID used for MAC validation
33/// * `validate_macs` - Whether to validate MACs during decoding
34///
35/// # Returns
36/// A decoded `Mutation` or an error if decoding/validation fails.
37pub fn decode_record(
38    operation: wa::syncd_mutation::SyncdOperation,
39    record: &wa::SyncdRecord,
40    keys: &ExpandedAppStateKeys,
41    key_id: &[u8],
42    validate_macs: bool,
43) -> Result<Mutation, AppStateError> {
44    let value_blob = record
45        .value
46        .as_ref()
47        .and_then(|v| v.blob.as_ref())
48        .ok_or(AppStateError::MissingValueBlob)?;
49
50    if value_blob.len() < 16 + 32 {
51        return Err(AppStateError::ValueBlobTooShort);
52    }
53
54    let (iv, rest) = value_blob.split_at(16);
55    let (ciphertext, value_mac) = rest.split_at(rest.len() - 32);
56
57    if validate_macs {
58        let expected = generate_content_mac(
59            operation,
60            &value_blob[..value_blob.len() - 32],
61            key_id,
62            &keys.value_mac,
63        );
64        if expected != value_mac {
65            return Err(AppStateError::MismatchingContentMAC);
66        }
67    }
68
69    let mut plaintext = Vec::new();
70    aes_256_cbc_decrypt_into(ciphertext, &keys.value_encryption, iv, &mut plaintext)
71        .map_err(|_| AppStateError::DecryptionFailed)?;
72
73    let action = wa::SyncActionData::decode(plaintext.as_slice())
74        .map_err(|_| AppStateError::DecodeFailed)?;
75
76    let mut index_list: Vec<String> = Vec::new();
77    if let Some(idx_bytes) = action.index.as_ref() {
78        if validate_macs {
79            let stored = record
80                .index
81                .as_ref()
82                .and_then(|i| i.blob.as_ref())
83                .ok_or(AppStateError::MissingIndexMAC)?;
84            validate_index_mac(idx_bytes, stored, &keys.index)?;
85        }
86        if let Ok(parsed) = serde_json::from_slice::<Vec<String>>(idx_bytes) {
87            index_list = parsed;
88        }
89    }
90
91    Ok(Mutation {
92        action_value: action.value.clone(),
93        index_mac: record
94            .index
95            .as_ref()
96            .and_then(|i| i.blob.clone())
97            .unwrap_or_default(),
98        value_mac: value_mac.to_vec(),
99        index: index_list,
100        operation,
101    })
102}
103
104/// Extract all unique key IDs from a patch list that need to be fetched.
105///
106/// This is a pure function that collects key IDs from snapshots and patches
107/// without checking against storage.
108pub fn collect_key_ids_from_patch_list(
109    snapshot: Option<&wa::SyncdSnapshot>,
110    patches: &[wa::SyncdPatch],
111) -> Vec<Vec<u8>> {
112    use std::collections::HashSet;
113
114    let mut seen = HashSet::new();
115    let mut key_ids = Vec::new();
116
117    let mut check = |key_id: Option<&Vec<u8>>| {
118        if let Some(k) = key_id
119            && !seen.contains(k.as_slice())
120        {
121            // Unique key ID: two owned buffers are allocated via k.clone() and
122            // owned.clone() — one stored in `seen` for future dedup checks, one
123            // pushed to `key_ids` as the result. Duplicate key IDs are skipped
124            // by the seen.contains() check above, avoiding any allocation.
125            let owned = k.clone();
126            seen.insert(owned.clone());
127            key_ids.push(owned);
128        }
129    };
130
131    if let Some(snapshot) = snapshot {
132        check(snapshot.key_id.as_ref().and_then(|k| k.id.as_ref()));
133        for rec in &snapshot.records {
134            check(rec.key_id.as_ref().and_then(|k| k.id.as_ref()));
135        }
136    }
137
138    for patch in patches {
139        check(patch.key_id.as_ref().and_then(|k| k.id.as_ref()));
140        for mutation in &patch.mutations {
141            if let Some(record) = &mutation.record {
142                check(record.key_id.as_ref().and_then(|k| k.id.as_ref()));
143            }
144        }
145    }
146
147    key_ids
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153    use crate::hash::generate_content_mac;
154    use crate::keys::expand_app_state_keys;
155    use prost::Message;
156    use wacore_libsignal::crypto::aes_256_cbc_encrypt_into;
157
158    fn create_test_record(
159        op: wa::syncd_mutation::SyncdOperation,
160        keys: &ExpandedAppStateKeys,
161        key_id: &[u8],
162        action_data: &wa::SyncActionData,
163    ) -> wa::SyncdRecord {
164        let plaintext = action_data.encode_to_vec();
165        let iv = vec![0u8; 16];
166        let mut ciphertext = Vec::new();
167        aes_256_cbc_encrypt_into(&plaintext, &keys.value_encryption, &iv, &mut ciphertext)
168            .expect("test encryption should succeed");
169
170        let mut value_with_iv = iv;
171        value_with_iv.extend_from_slice(&ciphertext);
172        let value_mac = generate_content_mac(op, &value_with_iv, key_id, &keys.value_mac);
173        let mut value_blob = value_with_iv;
174        value_blob.extend_from_slice(&value_mac);
175
176        wa::SyncdRecord {
177            index: Some(wa::SyncdIndex {
178                blob: Some(vec![1; 32]),
179            }),
180            value: Some(wa::SyncdValue {
181                blob: Some(value_blob),
182            }),
183            key_id: Some(wa::KeyId {
184                id: Some(key_id.to_vec()),
185            }),
186        }
187    }
188
189    #[test]
190    fn test_decode_record_basic() {
191        let master_key = [7u8; 32];
192        let keys = expand_app_state_keys(&master_key);
193        let key_id = b"test_key_id".to_vec();
194
195        let action_data = wa::SyncActionData {
196            value: Some(wa::SyncActionValue {
197                timestamp: Some(1234567890),
198                ..Default::default()
199            }),
200            ..Default::default()
201        };
202
203        let record = create_test_record(
204            wa::syncd_mutation::SyncdOperation::Set,
205            &keys,
206            &key_id,
207            &action_data,
208        );
209
210        let mutation = decode_record(
211            wa::syncd_mutation::SyncdOperation::Set,
212            &record,
213            &keys,
214            &key_id,
215            false, // skip MAC validation for this test
216        )
217        .expect("test encryption should succeed");
218
219        assert_eq!(
220            mutation.action_value.as_ref().and_then(|v| v.timestamp),
221            Some(1234567890)
222        );
223        assert_eq!(mutation.operation, wa::syncd_mutation::SyncdOperation::Set);
224    }
225
226    #[test]
227    fn test_decode_record_with_mac_validation() {
228        let master_key = [7u8; 32];
229        let keys = expand_app_state_keys(&master_key);
230        let key_id = b"test_key_id".to_vec();
231
232        let action_data = wa::SyncActionData {
233            value: Some(wa::SyncActionValue {
234                timestamp: Some(1234567890),
235                ..Default::default()
236            }),
237            ..Default::default()
238        };
239
240        let record = create_test_record(
241            wa::syncd_mutation::SyncdOperation::Set,
242            &keys,
243            &key_id,
244            &action_data,
245        );
246
247        // With MAC validation enabled but no index in action_data, should succeed
248        let result = decode_record(
249            wa::syncd_mutation::SyncdOperation::Set,
250            &record,
251            &keys,
252            &key_id,
253            true,
254        );
255        assert!(result.is_ok());
256    }
257
258    #[test]
259    fn test_collect_key_ids_from_patch_list() {
260        let key_id_1 = vec![1, 2, 3];
261        let key_id_2 = vec![4, 5, 6];
262        let key_id_3 = vec![7, 8, 9];
263        let key_id_4 = vec![10, 11, 12];
264
265        let snapshot = wa::SyncdSnapshot {
266            key_id: Some(wa::KeyId {
267                id: Some(key_id_1.clone()),
268            }),
269            records: vec![wa::SyncdRecord {
270                key_id: Some(wa::KeyId {
271                    id: Some(key_id_2.clone()),
272                }),
273                ..Default::default()
274            }],
275            ..Default::default()
276        };
277
278        let patches = vec![wa::SyncdPatch {
279            key_id: Some(wa::KeyId {
280                id: Some(key_id_3.clone()),
281            }),
282            mutations: vec![wa::SyncdMutation {
283                record: Some(wa::SyncdRecord {
284                    key_id: Some(wa::KeyId {
285                        id: Some(key_id_4.clone()),
286                    }),
287                    ..Default::default()
288                }),
289                ..Default::default()
290            }],
291            ..Default::default()
292        }];
293
294        let key_ids = collect_key_ids_from_patch_list(Some(&snapshot), &patches);
295
296        assert_eq!(key_ids.len(), 4);
297        assert!(key_ids.contains(&key_id_1));
298        assert!(key_ids.contains(&key_id_2));
299        assert!(key_ids.contains(&key_id_3));
300        assert!(key_ids.contains(&key_id_4));
301    }
302
303    #[test]
304    fn test_collect_key_ids_deduplicates() {
305        let key_id = vec![1, 2, 3];
306
307        let snapshot = wa::SyncdSnapshot {
308            key_id: Some(wa::KeyId {
309                id: Some(key_id.clone()),
310            }),
311            records: vec![wa::SyncdRecord {
312                key_id: Some(wa::KeyId {
313                    id: Some(key_id.clone()),
314                }),
315                ..Default::default()
316            }],
317            ..Default::default()
318        };
319
320        let patches = vec![wa::SyncdPatch {
321            key_id: Some(wa::KeyId {
322                id: Some(key_id.clone()),
323            }),
324            ..Default::default()
325        }];
326
327        let key_ids = collect_key_ids_from_patch_list(Some(&snapshot), &patches);
328
329        // Should only have one entry since all key IDs are the same
330        assert_eq!(key_ids.len(), 1);
331        assert_eq!(key_ids[0], key_id);
332    }
333}