1use crate::AppStateError;
8use crate::decode::{Mutation, decode_record};
9use crate::hash::{HashState, generate_patch_mac};
10use crate::keys::ExpandedAppStateKeys;
11use log::{debug, trace};
12use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14use std::sync::Arc;
15use waproto::whatsapp as wa;
16
17fn known_op(
21 op: Option<buffa::EnumValue<wa::syncd_mutation::SyncdOperation>>,
22) -> Result<wa::syncd_mutation::SyncdOperation, AppStateError> {
23 match op {
24 None => Ok(wa::syncd_mutation::SyncdOperation::SET),
25 Some(v) => v
26 .as_known()
27 .ok_or(AppStateError::UnsupportedSyncdOperation(v.to_i32())),
28 }
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct AppStateMutationMAC {
33 pub index_mac: Vec<u8>,
34 pub value_mac: Vec<u8>,
35}
36
37#[derive(Debug, Clone)]
39pub struct ProcessedSnapshot {
40 pub state: HashState,
42 pub mutations: Vec<Mutation>,
44 pub mutation_macs: Vec<AppStateMutationMAC>,
46}
47
48#[derive(Debug, Clone)]
50pub struct PatchProcessingResult {
51 pub state: HashState,
53 pub mutations: Vec<Mutation>,
55 pub added_macs: Vec<AppStateMutationMAC>,
57 pub removed_index_macs: Vec<Vec<u8>>,
59}
60
61pub fn process_snapshot<F>(
76 snapshot: &wa::SyncdSnapshot,
77 initial_state: &mut HashState,
78 mut get_keys: F,
79 validate_macs: bool,
80 collection_name: &str,
81) -> Result<ProcessedSnapshot, AppStateError>
82where
83 F: FnMut(&[u8]) -> Result<Arc<ExpandedAppStateKeys>, AppStateError>,
84{
85 let version = snapshot.version.version.unwrap_or(0);
86 initial_state.version = version;
87
88 initial_state.update_hash_from_records(&snapshot.records);
90
91 debug!(
92 target: "AppState",
93 "Snapshot {} v{}: {} records, ltHash ends with ...{}",
94 collection_name,
95 version,
96 snapshot.records.len(),
97 hex::encode(&initial_state.hash[120..])
98 );
99
100 if validate_macs {
105 let (Some(mac_expected), Some(key_id)) = (
106 snapshot.mac.as_ref(),
107 snapshot.key_id.as_option().and_then(|k| k.id.as_deref()),
108 ) else {
109 return Err(AppStateError::SnapshotMACMismatch);
110 };
111 let keys = get_keys(key_id)?;
112 let computed = initial_state.generate_snapshot_mac(collection_name, &keys.snapshot_mac);
113 trace!(
114 target: "AppState",
115 "Snapshot {} v{} MAC validation: computed={}, expected={}",
116 collection_name,
117 version,
118 hex::encode(&computed),
119 hex::encode(mac_expected)
120 );
121 if computed != *mac_expected {
122 return Err(AppStateError::SnapshotMACMismatch);
123 }
124 }
125
126 let mut mutations = Vec::with_capacity(snapshot.records.len());
128 let mut mutation_macs = Vec::with_capacity(snapshot.records.len());
129
130 for rec in &snapshot.records {
131 let key_id = rec.key_id.id.as_ref().ok_or(AppStateError::MissingKeyId)?;
132 let keys = get_keys(key_id)?;
133
134 let (mutation, macs) = decode_record(
135 wa::syncd_mutation::SyncdOperation::SET,
136 rec,
137 &keys,
138 key_id,
139 validate_macs,
140 )?;
141
142 mutation_macs.push(AppStateMutationMAC {
143 index_mac: macs.index_mac,
144 value_mac: macs.value_mac,
145 });
146
147 mutations.push(mutation);
148 }
149
150 Ok(ProcessedSnapshot {
151 state: initial_state.clone(),
152 mutations,
153 mutation_macs,
154 })
155}
156
157pub fn process_patch<F, G>(
173 patch: &wa::SyncdPatch,
174 state: &mut HashState,
175 mut get_keys: F,
176 mut get_prev_value_mac: G,
177 validate_macs: bool,
178 collection_name: &str,
179) -> Result<PatchProcessingResult, AppStateError>
180where
181 F: FnMut(&[u8]) -> Result<Arc<ExpandedAppStateKeys>, AppStateError>,
182 G: FnMut(&[u8]) -> Result<Option<Vec<u8>>, AppStateError>,
183{
184 let original_version = state.version;
189 let original_hash_is_empty = state.hash == [0u8; 128];
190 let had_no_prior_state = original_version == 0 && original_hash_is_empty;
191
192 let patch_version = patch.version.version.unwrap_or(0);
193
194 let expected_version = original_version.saturating_add(1);
200 if !had_no_prior_state && patch_version != expected_version {
201 return Err(AppStateError::PatchVersionMismatch {
202 expected: expected_version,
203 got: patch_version,
204 });
205 }
206
207 for m in &patch.mutations {
212 known_op(m.operation)?;
213 }
214
215 state.version = patch_version;
216
217 let mut in_patch: HashMap<&[u8], &[u8]> = HashMap::with_capacity(patch.mutations.len());
223 let (hash_update_result, result) = state.update_hash(&patch.mutations, |index_mac, idx| {
224 let is_remove = patch.mutations[idx]
229 .operation
230 .is_some_and(|op| op == wa::syncd_mutation::SyncdOperation::REMOVE);
231 let prev = if !is_remove && let Some(value_mac) = in_patch.get(index_mac) {
232 Some(value_mac.to_vec())
233 } else {
234 get_prev_value_mac(index_mac).map_err(|e| anyhow::anyhow!(e))?
235 };
236 if let Some(rec) = patch.mutations[idx].record.as_option()
237 && let Some(index) = rec.index.as_option().and_then(|i| i.blob.as_deref())
238 && let Some(value) = rec.value.as_option().and_then(|v| v.blob.as_deref())
239 && value.len() >= 32
240 {
241 in_patch.insert(index, &value[value.len() - 32..]);
242 }
243 Ok(prev)
244 });
245 result.map_err(|_| AppStateError::MismatchingLTHash)?;
246
247 debug!(
248 target: "AppState",
249 "Patch {} v{}: {} mutations, ltHash ends with ...{}, hasMissingRemove={}",
250 collection_name,
251 state.version,
252 patch.mutations.len(),
253 hex::encode(&state.hash[120..]),
254 hash_update_result.has_missing_remove
255 );
256
257 if validate_macs && let Some(key_id) = patch.key_id.id.as_ref() {
259 let keys = get_keys(key_id)?;
260 let verdict = validate_patch_macs(
261 patch,
262 state,
263 &keys,
264 collection_name,
265 had_no_prior_state,
266 hash_update_result.has_missing_remove,
267 )?;
268 if verdict.snapshot_mac_diverged && !state.mac_mismatch_fatal {
269 log::warn!(
270 target: "AppState",
271 "Collection {collection_name} ltHash diverged at v{}: the patch is authentic \
272 (patchMac valid) but its snapshotMac cannot match again. Applying it and \
273 skipping the aggregate comparison from here, as WA Web does.",
274 state.version
275 );
276 state.mac_mismatch_fatal = true;
277 }
278 }
279
280 if validate_macs {
285 detect_duplicate_index_in_patch(&patch.mutations)?;
286 }
287
288 let mut mutations = Vec::with_capacity(patch.mutations.len());
290 let mut added_macs = Vec::with_capacity(patch.mutations.len());
291 let mut removed_index_macs = Vec::with_capacity(patch.mutations.len());
292
293 for m in &patch.mutations {
294 if m.record.is_set() {
295 let op = known_op(m.operation)?;
296
297 let key_id = m
298 .record
299 .key_id
300 .id
301 .as_ref()
302 .ok_or(AppStateError::MissingKeyId)?;
303 let keys = get_keys(key_id)?;
304
305 let (mutation, macs) = decode_record(op, &m.record, &keys, key_id, validate_macs)?;
306
307 match op {
308 wa::syncd_mutation::SyncdOperation::SET => {
309 added_macs.push(AppStateMutationMAC {
310 index_mac: macs.index_mac,
311 value_mac: macs.value_mac,
312 });
313 }
314 wa::syncd_mutation::SyncdOperation::REMOVE => {
315 removed_index_macs.push(macs.index_mac);
316 }
317 }
318
319 mutations.push(mutation);
320 }
321 }
322
323 Ok(PatchProcessingResult {
324 state: state.clone(),
325 mutations,
326 added_macs,
327 removed_index_macs,
328 })
329}
330
331fn detect_duplicate_index_in_patch(mutations: &[wa::SyncdMutation]) -> Result<(), AppStateError> {
339 let mut seen_set: Vec<&[u8]> = Vec::new();
345 let mut seen_remove: Vec<&[u8]> = Vec::new();
346 for m in mutations {
347 let Some(rec) = m.record.as_option() else {
348 continue;
349 };
350 let Some(index_mac) = rec.index.as_option().and_then(|i| i.blob.as_deref()) else {
351 continue;
352 };
353 let op = known_op(m.operation)?;
354 let seen = match op {
355 wa::syncd_mutation::SyncdOperation::SET => &mut seen_set,
356 wa::syncd_mutation::SyncdOperation::REMOVE => &mut seen_remove,
357 };
358 if seen.contains(&index_mac) {
359 return Err(AppStateError::DuplicateIndexInPatch);
360 }
361 seen.push(index_mac);
362 }
363 Ok(())
364}
365
366#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
372pub struct PatchMacVerdict {
373 pub snapshot_mac_diverged: bool,
376}
377
378pub fn validate_patch_macs(
416 patch: &wa::SyncdPatch,
417 state: &HashState,
418 keys: &ExpandedAppStateKeys,
419 collection_name: &str,
420 had_no_prior_state: bool,
421 has_missing_remove: bool,
422) -> Result<PatchMacVerdict, AppStateError> {
423 match patch.patch_mac.as_ref() {
424 Some(patch_mac) => {
425 let version = patch.version.version.unwrap_or(0);
426 let computed_patch =
427 generate_patch_mac(patch, collection_name, &keys.patch_mac, version);
428 if computed_patch != *patch_mac {
429 debug!(
430 target: "AppState",
431 "Patch {} v{} patchMAC MISMATCH, hasMissingRemove={}",
432 collection_name,
433 state.version,
434 has_missing_remove
435 );
436 return Err(AppStateError::PatchMACMismatch);
437 }
438 }
439 None if had_no_prior_state => return Err(AppStateError::PatchMACMismatch),
444 None => {}
445 }
446
447 if state.mac_mismatch_fatal {
449 return Ok(PatchMacVerdict {
450 snapshot_mac_diverged: false,
451 });
452 }
453
454 if let Some(snap_mac) = patch.snapshot_mac.as_ref() {
455 let computed_snap = state.generate_snapshot_mac(collection_name, &keys.snapshot_mac);
456 trace!(
457 target: "AppState",
458 "Patch {} v{} snapshotMAC: computed={}, expected={}",
459 collection_name,
460 state.version,
461 hex::encode(&computed_snap),
462 hex::encode(snap_mac)
463 );
464 if computed_snap != *snap_mac {
465 debug!(
466 target: "AppState",
467 "Patch {} v{} snapshotMAC MISMATCH! ltHash=...{}, hasMissingRemove={}",
468 collection_name,
469 state.version,
470 hex::encode(&state.hash[120..]),
471 has_missing_remove
472 );
473 return Ok(PatchMacVerdict {
474 snapshot_mac_diverged: true,
475 });
476 }
477 } else if had_no_prior_state {
478 return Err(AppStateError::PatchSnapshotMACMismatch);
481 }
482
483 Ok(PatchMacVerdict::default())
484}
485
486pub fn validate_snapshot_mac(
490 snapshot: &wa::SyncdSnapshot,
491 state: &HashState,
492 keys: &ExpandedAppStateKeys,
493 collection_name: &str,
494) -> Result<(), AppStateError> {
495 let Some(mac_expected) = snapshot.mac.as_ref() else {
498 return Err(AppStateError::SnapshotMACMismatch);
499 };
500 let computed = state.generate_snapshot_mac(collection_name, &keys.snapshot_mac);
501 if computed != *mac_expected {
502 return Err(AppStateError::SnapshotMACMismatch);
503 }
504 Ok(())
505}
506
507#[cfg(test)]
508#[allow(clippy::disallowed_methods)]
509mod tests {
510 use super::*;
511 use crate::hash::{generate_content_mac, generate_index_mac};
512 use crate::keys::expand_app_state_keys;
513 use crate::lthash::WAPATCH_INTEGRITY;
514 use buffa::Message;
515 use wacore_libsignal::crypto::aes_256_cbc_encrypt_into;
516
517 fn sign_genesis_patch(
521 patch: &mut wa::SyncdPatch,
522 keys: &ExpandedAppStateKeys,
523 collection: &str,
524 ) {
525 let mut probe = HashState::default();
526 let gk = |_: &[u8]| Ok(Arc::new(keys.clone()));
527 let gp = |_: &[u8]| Ok(None);
528 process_patch(patch, &mut probe, gk, gp, false, collection).expect("probe apply");
529 patch.snapshot_mac = Some(probe.generate_snapshot_mac(collection, &keys.snapshot_mac));
530 let version = patch.version.version.unwrap_or(0);
531 patch.patch_mac = Some(generate_patch_mac(
532 patch,
533 collection,
534 &keys.patch_mac,
535 version,
536 ));
537 }
538
539 fn create_encrypted_record(
540 op: wa::syncd_mutation::SyncdOperation,
541 index_mac: &[u8],
542 keys: &ExpandedAppStateKeys,
543 key_id: &[u8],
544 timestamp: i64,
545 ) -> wa::SyncdRecord {
546 let action_data = wa::SyncActionData {
549 index: Some(index_mac.to_vec()),
550 value: buffa::MessageField::some(wa::SyncActionValue {
551 timestamp: Some(timestamp),
552 ..Default::default()
553 }),
554 ..Default::default()
555 };
556 let plaintext = action_data.encode_to_vec();
557
558 let iv = vec![0u8; 16];
559 let mut ciphertext = Vec::new();
560 aes_256_cbc_encrypt_into(&plaintext, &keys.value_encryption, &iv, &mut ciphertext)
561 .expect("test data should be valid");
562
563 let mut value_with_iv = iv;
564 value_with_iv.extend_from_slice(&ciphertext);
565 let value_mac = generate_content_mac(op, &value_with_iv, key_id, &keys.value_mac);
566 let mut value_blob = value_with_iv;
567 value_blob.extend_from_slice(&value_mac);
568
569 wa::SyncdRecord {
570 index: buffa::MessageField::some(wa::SyncdIndex {
571 blob: Some(generate_index_mac(index_mac, &keys.index)),
572 }),
573 value: buffa::MessageField::some(wa::SyncdValue {
574 blob: Some(value_blob),
575 }),
576 key_id: buffa::MessageField::some(wa::KeyId {
577 id: Some(key_id.to_vec()),
578 }),
579 }
580 }
581
582 #[test]
583 fn test_process_snapshot_basic() {
584 let master_key = [7u8; 32];
585 let keys = expand_app_state_keys(&master_key);
586 let key_id = b"test_key_id".to_vec();
587 let index_mac = vec![1; 32];
588
589 let record = create_encrypted_record(
590 wa::syncd_mutation::SyncdOperation::SET,
591 &index_mac,
592 &keys,
593 &key_id,
594 1234567890,
595 );
596
597 let snapshot = wa::SyncdSnapshot {
598 version: buffa::MessageField::some(wa::SyncdVersion { version: Some(1) }),
599 records: vec![record],
600 key_id: buffa::MessageField::some(wa::KeyId {
601 id: Some(key_id.clone()),
602 }),
603 ..Default::default()
604 };
605
606 let get_keys = |_: &[u8]| Ok(Arc::new(keys.clone()));
607
608 let mut state = HashState::default();
609 let result = process_snapshot(&snapshot, &mut state, get_keys, false, "regular")
610 .expect("test data should be valid");
611
612 assert_eq!(result.state.version, 1);
613 assert_eq!(result.mutations.len(), 1);
614 assert_eq!(result.mutation_macs.len(), 1);
615 assert_eq!(
617 result.mutation_macs[0].index_mac,
618 generate_index_mac(&index_mac, &keys.index)
619 );
620 assert!(!result.mutation_macs[0].value_mac.is_empty());
621 assert_ne!(
622 result.mutation_macs[0].index_mac,
623 result.mutation_macs[0].value_mac
624 );
625 assert_eq!(
626 result.mutations[0]
627 .action_value
628 .as_ref()
629 .and_then(|v| v.timestamp),
630 Some(1234567890)
631 );
632 }
633
634 #[test]
635 fn process_snapshot_rejects_missing_mac_when_validating() {
636 let master_key = [7u8; 32];
637 let keys = expand_app_state_keys(&master_key);
638 let key_id = b"test_key_id".to_vec();
639 let record = create_encrypted_record(
640 wa::syncd_mutation::SyncdOperation::SET,
641 &[1u8; 32],
642 &keys,
643 &key_id,
644 1234567890,
645 );
646 let snapshot = wa::SyncdSnapshot {
648 version: buffa::MessageField::some(wa::SyncdVersion { version: Some(1) }),
649 records: vec![record],
650 key_id: buffa::MessageField::some(wa::KeyId {
651 id: Some(key_id.clone()),
652 }),
653 ..Default::default()
654 };
655 let get_keys = |_: &[u8]| Ok(Arc::new(keys.clone()));
656 let mut state = HashState::default();
657 let err = process_snapshot(&snapshot, &mut state, get_keys, true, "regular")
658 .expect_err("missing snapshot mac must fail when validating");
659 assert!(matches!(err, AppStateError::SnapshotMACMismatch));
660 }
661
662 #[test]
663 fn process_snapshot_rejects_missing_key_id_when_validating() {
664 let master_key = [7u8; 32];
665 let keys = expand_app_state_keys(&master_key);
666 let key_id = b"test_key_id".to_vec();
667 let record = create_encrypted_record(
668 wa::syncd_mutation::SyncdOperation::SET,
669 &[1u8; 32],
670 &keys,
671 &key_id,
672 1234567890,
673 );
674 let snapshot = wa::SyncdSnapshot {
676 version: buffa::MessageField::some(wa::SyncdVersion { version: Some(1) }),
677 records: vec![record],
678 mac: Some(vec![9u8; 32]),
679 key_id: buffa::MessageField::none(),
680 };
681 let get_keys = |_: &[u8]| Ok(Arc::new(keys.clone()));
682 let mut state = HashState::default();
683 let err = process_snapshot(&snapshot, &mut state, get_keys, true, "regular")
684 .expect_err("missing snapshot key_id must fail when validating");
685 assert!(matches!(err, AppStateError::SnapshotMACMismatch));
686 }
687
688 #[test]
701 fn critical_snapshot_fails_key_not_found_until_key_share_lands() {
702 let master_key = [7u8; 32];
703 let keys = expand_app_state_keys(&master_key);
704 let key_id = b"appstate-sync-key-1".to_vec();
705
706 let record = create_encrypted_record(
708 wa::syncd_mutation::SyncdOperation::SET,
709 &[1u8; 32],
710 &keys,
711 &key_id,
712 1_700_000_000,
713 );
714 let snapshot = wa::SyncdSnapshot {
715 version: buffa::MessageField::some(wa::SyncdVersion { version: Some(3) }),
716 records: vec![record],
717 key_id: buffa::MessageField::some(wa::KeyId {
718 id: Some(key_id.clone()),
719 }),
720 ..Default::default()
721 };
722
723 let key_missing = |_: &[u8]| -> Result<Arc<ExpandedAppStateKeys>, AppStateError> {
726 Err(AppStateError::KeyNotFound)
727 };
728 let mut state = HashState::default();
729 let err = process_snapshot(
730 &snapshot,
731 &mut state,
732 key_missing,
733 false,
734 "critical_unblock_low",
735 )
736 .expect_err("must fail while the key-share is still in flight");
737 assert!(
738 matches!(err, AppStateError::KeyNotFound),
739 "expected KeyNotFound (the 'didn't find app state key' failure), got {err:?}"
740 );
741
742 let key_present = |_: &[u8]| Ok(Arc::new(keys.clone()));
746 let mut state2 = HashState::default();
747 let result = process_snapshot(
748 &snapshot,
749 &mut state2,
750 key_present,
751 false,
752 "critical_unblock_low",
753 )
754 .expect("the same snapshot must decode once the key is present");
755 assert_eq!(result.state.version, 3);
756 assert_eq!(result.mutations.len(), 1, "the contact record must apply");
757 }
758
759 #[test]
760 fn test_process_patch_basic() {
761 let master_key = [7u8; 32];
762 let keys = expand_app_state_keys(&master_key);
763 let key_id = b"test_key_id".to_vec();
764 let index_mac = vec![1; 32];
765
766 let record = create_encrypted_record(
767 wa::syncd_mutation::SyncdOperation::SET,
768 &index_mac,
769 &keys,
770 &key_id,
771 1234567890,
772 );
773
774 let patch = wa::SyncdPatch {
775 version: buffa::MessageField::some(wa::SyncdVersion { version: Some(2) }),
776 mutations: vec![wa::SyncdMutation {
777 operation: Some(wa::syncd_mutation::SyncdOperation::SET.into()),
778 record: buffa::MessageField::some(record),
779 }],
780 key_id: buffa::MessageField::some(wa::KeyId {
781 id: Some(key_id.clone()),
782 }),
783 ..Default::default()
784 };
785
786 let get_keys = |_: &[u8]| Ok(Arc::new(keys.clone()));
787 let get_prev = |_: &[u8]| Ok(None);
788
789 let mut state = HashState::default();
790 let result = process_patch(&patch, &mut state, get_keys, get_prev, false, "regular")
791 .expect("test data should be valid");
792
793 assert_eq!(result.state.version, 2);
794 assert_eq!(result.mutations.len(), 1);
795 assert_eq!(result.added_macs.len(), 1);
796 assert_eq!(
798 result.added_macs[0].index_mac,
799 generate_index_mac(&index_mac, &keys.index)
800 );
801 assert!(!result.added_macs[0].value_mac.is_empty());
802 assert_ne!(
803 result.added_macs[0].index_mac,
804 result.added_macs[0].value_mac
805 );
806 assert!(result.removed_index_macs.is_empty());
807 }
808
809 #[test]
813 fn test_process_patch_rejects_unknown_operation() {
814 let master_key = [7u8; 32];
815 let keys = expand_app_state_keys(&master_key);
816 let key_id = b"test_key_id".to_vec();
817 let index_mac = vec![1; 32];
818
819 let record = create_encrypted_record(
820 wa::syncd_mutation::SyncdOperation::SET,
821 &index_mac,
822 &keys,
823 &key_id,
824 1234567890,
825 );
826
827 let patch = wa::SyncdPatch {
828 version: buffa::MessageField::some(wa::SyncdVersion { version: Some(2) }),
829 mutations: vec![wa::SyncdMutation {
830 operation: Some(buffa::EnumValue::Unknown(7)),
831 record: buffa::MessageField::some(record),
832 }],
833 key_id: buffa::MessageField::some(wa::KeyId {
834 id: Some(key_id.clone()),
835 }),
836 ..Default::default()
837 };
838
839 let get_keys = |_: &[u8]| Ok(Arc::new(keys.clone()));
840 let get_prev = |_: &[u8]| Ok(None);
841
842 let mut state = HashState::default();
843 let before = state.hash;
844 let err = process_patch(&patch, &mut state, get_keys, get_prev, false, "regular")
845 .expect_err("unknown operation must be rejected");
846
847 assert!(matches!(err, AppStateError::UnsupportedSyncdOperation(7)));
848 assert_eq!(state.hash, before, "hash must be untouched on rejection");
849 assert_eq!(state.version, 0, "version must be untouched on rejection");
850 }
851
852 fn state_at(version: u64, hash: u8) -> HashState {
853 HashState {
854 version,
855 hash: [hash; 128],
856 index_value_map: HashMap::new(),
857 mac_mismatch_fatal: false,
858 }
859 }
860
861 #[test]
865 fn validate_patch_macs_reports_snapshot_divergence_instead_of_failing() {
866 let keys = expand_app_state_keys(&[7u8; 32]);
867 let mut patch = wa::SyncdPatch {
868 version: buffa::MessageField::some(wa::SyncdVersion { version: Some(2) }),
869 snapshot_mac: Some(vec![0u8; 32]),
870 ..Default::default()
871 };
872 patch.patch_mac = Some(generate_patch_mac(&patch, "regular", &keys.patch_mac, 2));
873 let state = state_at(2, 3);
874
875 let verdict = validate_patch_macs(&patch, &state, &keys, "regular", false, true)
876 .expect("an authentic patch must not fail on the aggregate ltHash");
877
878 assert!(verdict.snapshot_mac_diverged);
879 }
880
881 #[test]
884 fn validate_patch_macs_skips_snapshot_comparison_once_latched() {
885 let keys = expand_app_state_keys(&[7u8; 32]);
886 let mut patch = wa::SyncdPatch {
887 version: buffa::MessageField::some(wa::SyncdVersion { version: Some(3) }),
888 snapshot_mac: Some(vec![0u8; 32]),
889 ..Default::default()
890 };
891 patch.patch_mac = Some(generate_patch_mac(&patch, "regular", &keys.patch_mac, 3));
892 let mut state = state_at(3, 3);
893 state.mac_mismatch_fatal = true;
894
895 let verdict = validate_patch_macs(&patch, &state, &keys, "regular", false, false)
896 .expect("a latched collection must not re-raise the mismatch");
897
898 assert!(
899 !verdict.snapshot_mac_diverged,
900 "a latched collection must not re-report divergence it already acted on"
901 );
902 }
903
904 #[test]
907 fn validate_patch_macs_rejects_patch_mismatch_even_when_latched() {
908 let keys = expand_app_state_keys(&[7u8; 32]);
909 let patch = wa::SyncdPatch {
910 version: buffa::MessageField::some(wa::SyncdVersion { version: Some(2) }),
911 patch_mac: Some(vec![0u8; 32]),
912 ..Default::default()
913 };
914 let mut state = state_at(2, 5);
915 state.mac_mismatch_fatal = true;
916
917 let err = validate_patch_macs(&patch, &state, &keys, "regular", false, true)
918 .expect_err("neither latching nor hasMissingRemove is a patchMAC bypass");
919
920 assert!(matches!(err, AppStateError::PatchMACMismatch));
921 }
922
923 #[test]
928 fn validate_patch_macs_rejects_genesis_tampered_patch_mac() {
929 let keys = expand_app_state_keys(&[7u8; 32]);
930 let patch = wa::SyncdPatch {
931 version: buffa::MessageField::some(wa::SyncdVersion { version: Some(1) }),
932 patch_mac: Some(vec![0u8; 32]),
933 ..Default::default()
934 };
935 let err = validate_patch_macs(&patch, &state_at(1, 5), &keys, "regular", true, false)
936 .expect_err("genesis patchMAC must be validated, not skipped");
937 assert!(matches!(err, AppStateError::PatchMACMismatch));
938 }
939
940 #[test]
941 fn validate_patch_macs_rejects_genesis_missing_patch_mac() {
942 let keys = expand_app_state_keys(&[7u8; 32]);
943 let patch = wa::SyncdPatch {
947 version: buffa::MessageField::some(wa::SyncdVersion { version: Some(1) }),
948 ..Default::default()
949 };
950 let err = validate_patch_macs(&patch, &state_at(1, 5), &keys, "regular", true, false)
951 .expect_err("genesis patch without patchMAC must be rejected");
952 assert!(matches!(err, AppStateError::PatchMACMismatch));
953 }
954
955 #[test]
956 fn validate_patch_macs_rejects_genesis_missing_snapshot_mac() {
957 let keys = expand_app_state_keys(&[7u8; 32]);
958 let mut patch = wa::SyncdPatch {
961 version: buffa::MessageField::some(wa::SyncdVersion { version: Some(1) }),
962 ..Default::default()
963 };
964 patch.patch_mac = Some(generate_patch_mac(&patch, "regular", &keys.patch_mac, 1));
965 let err = validate_patch_macs(&patch, &state_at(1, 3), &keys, "regular", true, false)
966 .expect_err("genesis patch without snapshotMAC must be rejected");
967 assert!(matches!(err, AppStateError::PatchSnapshotMACMismatch));
968 }
969
970 #[test]
971 fn validate_patch_macs_accepts_genesis_valid_macs() {
972 let keys = expand_app_state_keys(&[7u8; 32]);
975 let state = state_at(1, 3);
976 let mut patch = wa::SyncdPatch {
977 version: buffa::MessageField::some(wa::SyncdVersion { version: Some(1) }),
978 ..Default::default()
979 };
980 patch.snapshot_mac = Some(state.generate_snapshot_mac("regular", &keys.snapshot_mac));
981 patch.patch_mac = Some(generate_patch_mac(&patch, "regular", &keys.patch_mac, 1));
982 let verdict = validate_patch_macs(&patch, &state, &keys, "regular", true, false)
983 .expect("legitimate genesis patch with correct MACs must be accepted");
984 assert!(!verdict.snapshot_mac_diverged);
985 }
986
987 #[test]
990 fn process_patch_latches_divergence_and_keeps_applying() {
991 let keys = expand_app_state_keys(&[7u8; 32]);
992 let key_id = b"test_key_id".to_vec();
993 let mut patch = wa::SyncdPatch {
994 version: buffa::MessageField::some(wa::SyncdVersion { version: Some(6) }),
995 mutations: vec![wa::SyncdMutation {
996 operation: Some(wa::syncd_mutation::SyncdOperation::SET.into()),
997 record: buffa::MessageField::some(create_encrypted_record(
998 wa::syncd_mutation::SyncdOperation::SET,
999 &[1u8; 32],
1000 &keys,
1001 &key_id,
1002 1,
1003 )),
1004 }],
1005 key_id: buffa::MessageField::some(wa::KeyId {
1006 id: Some(key_id.clone()),
1007 }),
1008 snapshot_mac: Some(
1010 state_at(6, 0xEE).generate_snapshot_mac("regular", &keys.snapshot_mac),
1011 ),
1012 ..Default::default()
1013 };
1014 patch.patch_mac = Some(generate_patch_mac(&patch, "regular", &keys.patch_mac, 6));
1015
1016 let gk = |_: &[u8]| Ok(Arc::new(keys.clone()));
1017 let gp = |_: &[u8]| Ok(None);
1018 let mut state = state_at(5, 0x11);
1019 let result = process_patch(&patch, &mut state, gk, gp, true, "regular")
1020 .expect("an authentic patch must apply over a diverged base");
1021
1022 assert_eq!(result.mutations.len(), 1);
1023 assert_eq!(result.state.version, 6);
1024 assert!(
1025 result.state.mac_mismatch_fatal,
1026 "the divergence must be latched so it is not re-detected every patch"
1027 );
1028 }
1029
1030 #[test]
1031 fn process_patch_rejects_duplicate_set_index() {
1032 let master_key = [7u8; 32];
1033 let keys = expand_app_state_keys(&master_key);
1034 let key_id = b"test_key_id".to_vec();
1035 let index_mac = vec![1u8; 32];
1036
1037 let mk = |ts| wa::SyncdMutation {
1039 operation: Some(wa::syncd_mutation::SyncdOperation::SET.into()),
1040 record: buffa::MessageField::some(create_encrypted_record(
1041 wa::syncd_mutation::SyncdOperation::SET,
1042 &index_mac,
1043 &keys,
1044 &key_id,
1045 ts,
1046 )),
1047 };
1048 let mut patch = wa::SyncdPatch {
1049 version: buffa::MessageField::some(wa::SyncdVersion { version: Some(1) }),
1050 mutations: vec![mk(1), mk(2)],
1051 key_id: buffa::MessageField::some(wa::KeyId {
1052 id: Some(key_id.clone()),
1053 }),
1054 ..Default::default()
1055 };
1056 sign_genesis_patch(&mut patch, &keys, "regular");
1057
1058 let get_keys = |_: &[u8]| Ok(Arc::new(keys.clone()));
1059 let get_prev = |_: &[u8]| Ok(None);
1060 let mut state = HashState::default();
1061 let err = process_patch(&patch, &mut state, get_keys, get_prev, true, "regular")
1062 .expect_err("duplicate SET index must be rejected when validating");
1063 assert!(matches!(err, AppStateError::DuplicateIndexInPatch));
1064 }
1065
1066 #[test]
1067 fn process_patch_allows_same_index_across_set_and_remove() {
1068 let master_key = [7u8; 32];
1069 let keys = expand_app_state_keys(&master_key);
1070 let key_id = b"test_key_id".to_vec();
1071 let index_mac = vec![2u8; 32];
1072
1073 let set = wa::SyncdMutation {
1076 operation: Some(wa::syncd_mutation::SyncdOperation::SET.into()),
1077 record: buffa::MessageField::some(create_encrypted_record(
1078 wa::syncd_mutation::SyncdOperation::SET,
1079 &index_mac,
1080 &keys,
1081 &key_id,
1082 1,
1083 )),
1084 };
1085 let remove = wa::SyncdMutation {
1086 operation: Some(wa::syncd_mutation::SyncdOperation::REMOVE.into()),
1087 record: buffa::MessageField::some(create_encrypted_record(
1088 wa::syncd_mutation::SyncdOperation::REMOVE,
1089 &index_mac,
1090 &keys,
1091 &key_id,
1092 2,
1093 )),
1094 };
1095 let mut patch = wa::SyncdPatch {
1096 version: buffa::MessageField::some(wa::SyncdVersion { version: Some(1) }),
1097 mutations: vec![set, remove],
1098 key_id: buffa::MessageField::some(wa::KeyId {
1099 id: Some(key_id.clone()),
1100 }),
1101 ..Default::default()
1102 };
1103 sign_genesis_patch(&mut patch, &keys, "regular");
1104
1105 let get_keys = |_: &[u8]| Ok(Arc::new(keys.clone()));
1106 let get_prev = |_: &[u8]| Ok(None);
1107 let mut state = HashState::default();
1108 let result = process_patch(&patch, &mut state, get_keys, get_prev, true, "regular");
1109 assert!(
1110 result.is_ok(),
1111 "SET+REMOVE on the same index must be allowed: {result:?}"
1112 );
1113 }
1114
1115 #[test]
1116 fn process_patch_allows_distinct_indices_when_validating() {
1117 let master_key = [7u8; 32];
1118 let keys = expand_app_state_keys(&master_key);
1119 let key_id = b"test_key_id".to_vec();
1120
1121 let mk = |index: &[u8], ts| wa::SyncdMutation {
1122 operation: Some(wa::syncd_mutation::SyncdOperation::SET.into()),
1123 record: buffa::MessageField::some(create_encrypted_record(
1124 wa::syncd_mutation::SyncdOperation::SET,
1125 index,
1126 &keys,
1127 &key_id,
1128 ts,
1129 )),
1130 };
1131 let mut patch = wa::SyncdPatch {
1132 version: buffa::MessageField::some(wa::SyncdVersion { version: Some(1) }),
1133 mutations: vec![mk(&[3u8; 32], 1), mk(&[4u8; 32], 2)],
1134 key_id: buffa::MessageField::some(wa::KeyId {
1135 id: Some(key_id.clone()),
1136 }),
1137 ..Default::default()
1138 };
1139 sign_genesis_patch(&mut patch, &keys, "regular");
1140
1141 let get_keys = |_: &[u8]| Ok(Arc::new(keys.clone()));
1142 let get_prev = |_: &[u8]| Ok(None);
1143 let mut state = HashState::default();
1144 let result = process_patch(&patch, &mut state, get_keys, get_prev, true, "regular");
1145 assert!(result.is_ok(), "distinct indices must pass: {result:?}");
1146 }
1147
1148 #[test]
1149 fn test_process_patch_with_overwrite() {
1150 let master_key = [7u8; 32];
1151 let keys = expand_app_state_keys(&master_key);
1152 let key_id = b"test_key_id".to_vec();
1153 let index_mac = vec![1; 32];
1154
1155 let initial_record = create_encrypted_record(
1157 wa::syncd_mutation::SyncdOperation::SET,
1158 &index_mac,
1159 &keys,
1160 &key_id,
1161 1000,
1162 );
1163 let initial_value_blob = initial_record
1164 .value
1165 .blob
1166 .as_ref()
1167 .expect("test data should be valid");
1168 let initial_value_mac = initial_value_blob[initial_value_blob.len() - 32..].to_vec();
1169
1170 let snapshot = wa::SyncdSnapshot {
1172 version: buffa::MessageField::some(wa::SyncdVersion { version: Some(1) }),
1173 records: vec![initial_record],
1174 key_id: buffa::MessageField::some(wa::KeyId {
1175 id: Some(key_id.clone()),
1176 }),
1177 ..Default::default()
1178 };
1179
1180 let get_keys = |_: &[u8]| Ok(Arc::new(keys.clone()));
1181 let mut snapshot_state = HashState::default();
1182 let snapshot_result =
1183 process_snapshot(&snapshot, &mut snapshot_state, get_keys, false, "regular")
1184 .expect("test data should be valid");
1185
1186 let overwrite_record = create_encrypted_record(
1188 wa::syncd_mutation::SyncdOperation::SET,
1189 &index_mac,
1190 &keys,
1191 &key_id,
1192 2000,
1193 );
1194
1195 let patch = wa::SyncdPatch {
1196 version: buffa::MessageField::some(wa::SyncdVersion { version: Some(2) }),
1197 mutations: vec![wa::SyncdMutation {
1198 operation: Some(wa::syncd_mutation::SyncdOperation::SET.into()),
1199 record: buffa::MessageField::some(overwrite_record.clone()),
1200 }],
1201 key_id: buffa::MessageField::some(wa::KeyId {
1202 id: Some(key_id.clone()),
1203 }),
1204 ..Default::default()
1205 };
1206
1207 let get_keys = |_: &[u8]| Ok(Arc::new(keys.clone()));
1208 let stored_index_mac = generate_index_mac(&index_mac, &keys.index);
1210 let get_prev = |idx: &[u8]| {
1211 if idx == stored_index_mac.as_slice() {
1212 Ok(Some(initial_value_mac.clone()))
1213 } else {
1214 Ok(None)
1215 }
1216 };
1217
1218 let mut patch_state = snapshot_result.state.clone();
1219 let result = process_patch(
1220 &patch,
1221 &mut patch_state,
1222 get_keys,
1223 get_prev,
1224 false,
1225 "regular",
1226 )
1227 .expect("test data should be valid");
1228
1229 assert_eq!(result.state.version, 2);
1230 assert_eq!(result.mutations.len(), 1);
1231 assert_eq!(
1232 result.mutations[0]
1233 .action_value
1234 .as_ref()
1235 .and_then(|v| v.timestamp),
1236 Some(2000)
1237 );
1238
1239 let new_value_blob = overwrite_record
1241 .value
1242 .into_option()
1243 .expect("test data should be valid")
1244 .blob
1245 .expect("test data should be valid");
1246 let new_value_mac = new_value_blob[new_value_blob.len() - 32..].to_vec();
1247
1248 let expected_hash = WAPATCH_INTEGRITY.subtract_then_add(
1249 &snapshot_result.state.hash,
1250 &[initial_value_mac],
1251 &[new_value_mac],
1252 );
1253
1254 assert_eq!(result.state.hash.as_slice(), expected_hash.as_slice());
1255 }
1256
1257 #[test]
1263 fn test_process_patch_in_patch_overwrite_last_write_wins() {
1264 let master_key = [7u8; 32];
1265 let keys = expand_app_state_keys(&master_key);
1266 let key_id = b"test_key_id".to_vec();
1267 let index_mac = vec![1; 32];
1268
1269 let first = create_encrypted_record(
1270 wa::syncd_mutation::SyncdOperation::SET,
1271 &index_mac,
1272 &keys,
1273 &key_id,
1274 1000,
1275 );
1276 let second = create_encrypted_record(
1277 wa::syncd_mutation::SyncdOperation::SET,
1278 &index_mac,
1279 &keys,
1280 &key_id,
1281 2000,
1282 );
1283
1284 let tail = |rec: &wa::SyncdRecord| {
1285 let blob = rec.value.as_option().unwrap().blob.as_ref().unwrap();
1286 blob[blob.len() - 32..].to_vec()
1287 };
1288 let first_tail = tail(&first);
1289 let second_tail = tail(&second);
1290 assert_ne!(
1291 first_tail, second_tail,
1292 "distinct timestamps must yield distinct value MACs"
1293 );
1294
1295 let patch = wa::SyncdPatch {
1296 version: buffa::MessageField::some(wa::SyncdVersion { version: Some(1) }),
1297 mutations: vec![
1298 wa::SyncdMutation {
1299 operation: Some(wa::syncd_mutation::SyncdOperation::SET.into()),
1300 record: buffa::MessageField::some(first),
1301 },
1302 wa::SyncdMutation {
1303 operation: Some(wa::syncd_mutation::SyncdOperation::SET.into()),
1304 record: buffa::MessageField::some(second),
1305 },
1306 ],
1307 key_id: buffa::MessageField::some(wa::KeyId {
1308 id: Some(key_id.clone()),
1309 }),
1310 ..Default::default()
1311 };
1312
1313 let get_keys = |_: &[u8]| Ok(Arc::new(keys.clone()));
1314 let get_prev = |_: &[u8]| Ok(None);
1315
1316 let mut state = HashState::default();
1318 let result = process_patch(&patch, &mut state, get_keys, get_prev, false, "regular")
1319 .expect("two in-patch SETs should process");
1320
1321 assert_eq!(result.mutations.len(), 2);
1322 assert_eq!(result.added_macs.len(), 2);
1323
1324 const EMPTY: &[Vec<u8>] = &[];
1326 let expected = WAPATCH_INTEGRITY.subtract_then_add(
1327 &[0u8; 128],
1328 EMPTY,
1329 std::slice::from_ref(&second_tail),
1330 );
1331 assert_eq!(
1332 result.state.hash.as_slice(),
1333 expected.as_slice(),
1334 "in-patch overwrite must leave only the second SET's value in the ltHash"
1335 );
1336
1337 let both_kept =
1339 WAPATCH_INTEGRITY.subtract_then_add(&[0u8; 128], EMPTY, &[first_tail, second_tail]);
1340 assert_ne!(
1341 result.state.hash.as_slice(),
1342 both_kept.as_slice(),
1343 "both SET values must not remain: in-patch overwrite regressed"
1344 );
1345 }
1346
1347 #[test]
1354 fn test_process_patch_set_plus_remove_same_index_wa_web_index_mode() {
1355 let master_key = [7u8; 32];
1356 let keys = expand_app_state_keys(&master_key);
1357 let key_id = b"test_key_id".to_vec();
1358 let index_mac = vec![3; 32];
1359 let store_prev = vec![9u8; 32];
1360
1361 let set = create_encrypted_record(
1362 wa::syncd_mutation::SyncdOperation::SET,
1363 &index_mac,
1364 &keys,
1365 &key_id,
1366 2000,
1367 );
1368 let remove = create_encrypted_record(
1369 wa::syncd_mutation::SyncdOperation::REMOVE,
1370 &index_mac,
1371 &keys,
1372 &key_id,
1373 1000,
1374 );
1375
1376 let tail = |rec: &wa::SyncdRecord| {
1377 let blob = rec.value.as_option().unwrap().blob.as_ref().unwrap();
1378 blob[blob.len() - 32..].to_vec()
1379 };
1380 let set_tail = tail(&set);
1381
1382 let build_patch = |mutations: Vec<wa::SyncdMutation>| wa::SyncdPatch {
1383 version: buffa::MessageField::some(wa::SyncdVersion { version: Some(1) }),
1384 mutations,
1385 key_id: buffa::MessageField::some(wa::KeyId {
1386 id: Some(key_id.clone()),
1387 }),
1388 ..Default::default()
1389 };
1390 let set_mutation = wa::SyncdMutation {
1391 operation: Some(wa::syncd_mutation::SyncdOperation::SET.into()),
1392 record: buffa::MessageField::some(set),
1393 };
1394 let remove_mutation = wa::SyncdMutation {
1395 operation: Some(wa::syncd_mutation::SyncdOperation::REMOVE.into()),
1396 record: buffa::MessageField::some(remove),
1397 };
1398
1399 let get_keys = |_: &[u8]| Ok(Arc::new(keys.clone()));
1400 let get_prev = |_: &[u8]| Ok(Some(store_prev.clone()));
1401
1402 let expected = WAPATCH_INTEGRITY.subtract_then_add(
1403 &[0u8; 128],
1404 std::slice::from_ref(&store_prev),
1405 std::slice::from_ref(&set_tail),
1406 );
1407
1408 for (label, mutations) in [
1411 (
1412 "set-then-remove",
1413 vec![set_mutation.clone(), remove_mutation.clone()],
1414 ),
1415 ("remove-then-set", vec![remove_mutation, set_mutation]),
1416 ] {
1417 let patch = build_patch(mutations);
1418 let mut state = HashState::default();
1419 let result = process_patch(&patch, &mut state, get_keys, get_prev, false, "regular")
1420 .unwrap_or_else(|e| panic!("{label} should process: {e:?}"));
1421
1422 assert_eq!(
1423 result.state.hash.as_slice(),
1424 expected.as_slice(),
1425 "{label}: net must be base + set_tail - store_prev (WA Web index-mode)"
1426 );
1427
1428 let wire_index_mac = generate_index_mac(&index_mac, &keys.index);
1432 assert_eq!(result.added_macs.len(), 1, "{label}");
1433 assert_eq!(result.added_macs[0].index_mac, wire_index_mac, "{label}");
1434 assert_eq!(result.added_macs[0].value_mac, set_tail, "{label}");
1435 assert_eq!(
1436 result.removed_index_macs,
1437 vec![wire_index_mac.clone()],
1438 "{label}"
1439 );
1440 }
1441 }
1442
1443 #[test]
1447 fn test_patch_version_rollback_rejected() {
1448 let master_key = [7u8; 32];
1449 let keys = expand_app_state_keys(&master_key);
1450 let key_id = b"test_key_id".to_vec();
1451 let index_mac = vec![99; 32];
1452
1453 let record = create_encrypted_record(
1454 wa::syncd_mutation::SyncdOperation::SET,
1455 &index_mac,
1456 &keys,
1457 &key_id,
1458 5000,
1459 );
1460
1461 let mut state = HashState {
1463 version: 5,
1464 ..Default::default()
1465 };
1466
1467 let patch = wa::SyncdPatch {
1469 version: buffa::MessageField::some(wa::SyncdVersion { version: Some(3) }),
1470 mutations: vec![wa::SyncdMutation {
1471 operation: Some(wa::syncd_mutation::SyncdOperation::SET.into()),
1472 record: buffa::MessageField::some(record),
1473 }],
1474 key_id: buffa::MessageField::some(wa::KeyId {
1475 id: Some(key_id.clone()),
1476 }),
1477 ..Default::default()
1478 };
1479
1480 let get_keys = |_: &[u8]| Ok(Arc::new(keys.clone()));
1481 let get_prev = |_: &[u8]| -> Result<Option<Vec<u8>>, AppStateError> { Ok(None) };
1482
1483 let err = process_patch(&patch, &mut state, get_keys, get_prev, false, "regular")
1484 .expect_err("rollback patch should be rejected");
1485
1486 assert!(
1487 matches!(
1488 err,
1489 AppStateError::PatchVersionMismatch {
1490 expected: 6,
1491 got: 3
1492 }
1493 ),
1494 "expected PatchVersionMismatch {{ expected: 6, got: 3 }}, got: {err:?}"
1495 );
1496 }
1497
1498 #[test]
1501 fn test_patch_version_gap_rejected() {
1502 let master_key = [7u8; 32];
1503 let keys = expand_app_state_keys(&master_key);
1504 let key_id = b"test_key_id".to_vec();
1505 let index_mac = vec![99; 32];
1506
1507 let record = create_encrypted_record(
1508 wa::syncd_mutation::SyncdOperation::SET,
1509 &index_mac,
1510 &keys,
1511 &key_id,
1512 6000,
1513 );
1514
1515 let mut state = HashState {
1517 version: 5,
1518 ..Default::default()
1519 };
1520
1521 let patch = wa::SyncdPatch {
1523 version: buffa::MessageField::some(wa::SyncdVersion { version: Some(8) }),
1524 mutations: vec![wa::SyncdMutation {
1525 operation: Some(wa::syncd_mutation::SyncdOperation::SET.into()),
1526 record: buffa::MessageField::some(record),
1527 }],
1528 key_id: buffa::MessageField::some(wa::KeyId {
1529 id: Some(key_id.clone()),
1530 }),
1531 ..Default::default()
1532 };
1533
1534 let get_keys = |_: &[u8]| Ok(Arc::new(keys.clone()));
1535 let get_prev = |_: &[u8]| -> Result<Option<Vec<u8>>, AppStateError> { Ok(None) };
1536
1537 let err = process_patch(&patch, &mut state, get_keys, get_prev, false, "regular")
1538 .expect_err("version gap should be rejected");
1539
1540 assert!(
1541 matches!(
1542 err,
1543 AppStateError::PatchVersionMismatch {
1544 expected: 6,
1545 got: 8
1546 }
1547 ),
1548 "expected PatchVersionMismatch {{ expected: 6, got: 8 }}, got: {err:?}"
1549 );
1550 }
1551
1552 #[test]
1554 fn test_patch_version_consecutive_accepted() {
1555 let master_key = [7u8; 32];
1556 let keys = expand_app_state_keys(&master_key);
1557 let key_id = b"test_key_id".to_vec();
1558 let index_mac = vec![99; 32];
1559
1560 let record = create_encrypted_record(
1561 wa::syncd_mutation::SyncdOperation::SET,
1562 &index_mac,
1563 &keys,
1564 &key_id,
1565 7000,
1566 );
1567
1568 let mut state = HashState {
1570 version: 5,
1571 ..Default::default()
1572 };
1573
1574 let patch = wa::SyncdPatch {
1576 version: buffa::MessageField::some(wa::SyncdVersion { version: Some(6) }),
1577 mutations: vec![wa::SyncdMutation {
1578 operation: Some(wa::syncd_mutation::SyncdOperation::SET.into()),
1579 record: buffa::MessageField::some(record),
1580 }],
1581 key_id: buffa::MessageField::some(wa::KeyId {
1582 id: Some(key_id.clone()),
1583 }),
1584 ..Default::default()
1585 };
1586
1587 let get_keys = |_: &[u8]| Ok(Arc::new(keys.clone()));
1588 let get_prev = |_: &[u8]| -> Result<Option<Vec<u8>>, AppStateError> { Ok(None) };
1589
1590 let result = process_patch(&patch, &mut state, get_keys, get_prev, false, "regular")
1591 .expect("consecutive version should be accepted");
1592 assert_eq!(result.state.version, 6);
1593 }
1594
1595 #[test]
1599 fn test_patch_version_check_skipped_when_no_prior_state() {
1600 let master_key = [7u8; 32];
1601 let keys = expand_app_state_keys(&master_key);
1602 let key_id = b"test_key_id".to_vec();
1603 let index_mac = vec![99; 32];
1604
1605 let record = create_encrypted_record(
1606 wa::syncd_mutation::SyncdOperation::SET,
1607 &index_mac,
1608 &keys,
1609 &key_id,
1610 8000,
1611 );
1612
1613 let mut state = HashState::default();
1615
1616 let patch = wa::SyncdPatch {
1618 version: buffa::MessageField::some(wa::SyncdVersion { version: Some(42) }),
1619 mutations: vec![wa::SyncdMutation {
1620 operation: Some(wa::syncd_mutation::SyncdOperation::SET.into()),
1621 record: buffa::MessageField::some(record),
1622 }],
1623 key_id: buffa::MessageField::some(wa::KeyId {
1624 id: Some(key_id.clone()),
1625 }),
1626 ..Default::default()
1627 };
1628
1629 let get_keys = |_: &[u8]| Ok(Arc::new(keys.clone()));
1630 let get_prev = |_: &[u8]| -> Result<Option<Vec<u8>>, AppStateError> { Ok(None) };
1631
1632 let result = process_patch(&patch, &mut state, get_keys, get_prev, false, "regular")
1633 .expect("no-prior-state should skip version check");
1634 assert_eq!(result.state.version, 42);
1635 }
1636}