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#[derive(Debug, Clone)]
10pub struct Mutation {
11 pub action_value: Option<wa::SyncActionValue>,
13 pub index_mac: Vec<u8>,
15 pub value_mac: Vec<u8>,
17 pub index: Vec<String>,
19 pub operation: wa::syncd_mutation::SyncdOperation,
21}
22
23pub 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
104pub 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 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, )
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 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 assert_eq!(key_ids.len(), 1);
331 assert_eq!(key_ids[0], key_id);
332 }
333}