#![forbid(unsafe_code)] use std::fs; use ironstorage::{ mobile_watch::{ MobileWatchSnapshotState, WatchSnapshotApply, WatchSnapshotEntry, WatchSnapshotReceiver, WatchSnapshotSender, }, otp::OtpAlgorithm, repository::{EntryPath, SecretBytes}, }; type TestResult = Result<(), Box>; fn entry(path: &str, issuer: &str, account: &str, secret: &[u8]) -> WatchSnapshotEntry { WatchSnapshotEntry::new( EntryPath::parse(path).expect("fixture path"), Some(issuer.to_owned()), account.to_owned(), OtpAlgorithm::Sha256, 8, 30, SecretBytes::new(secret.to_vec()), ) } #[test] fn replacement_snapshots_reject_replays_conflicts_and_pairing_changes() -> TestResult { let directory = tempfile::tempdir()?; let journal = directory.path().join("watch-snapshot.toml"); let mut sender = WatchSnapshotSender::load(journal.clone()); let first = sender.prepare( "paired-watch-a", vec![entry("otp/alice", "Acme", "alice", b"first-secret")], )?; assert_eq!(first.revision(), 1); assert_eq!(sender.status().state(), MobileWatchSnapshotState::Pending); let mut receiver = WatchSnapshotReceiver::default(); assert_eq!( receiver.apply(SecretBytes::new(first.snapshot().expose().to_vec()))?, WatchSnapshotApply::Replaced ); assert_eq!(receiver.current().expect("snapshot").entries().len(), 1); assert_eq!( sender.acknowledge(first.delivered_receipt())?.state(), MobileWatchSnapshotState::Delivered ); assert_eq!( sender .acknowledge(&receiver.current_receipt().expect("accepted receipt"))? .state(), MobileWatchSnapshotState::Current ); let duplicate = sender.prepare( "paired-watch-a", vec![entry("otp/alice", "Acme", "alice", b"first-secret")], )?; assert_eq!(duplicate.revision(), 1); assert_eq!(duplicate.snapshot().expose(), first.snapshot().expose()); assert_eq!( receiver.apply(SecretBytes::new(duplicate.snapshot().expose().to_vec()))?, WatchSnapshotApply::Duplicate ); let replacement = sender.prepare( "paired-watch-a", vec![entry("otp/bob", "Acme", "bob", b"second-secret")], )?; assert_eq!(replacement.revision(), 2); assert_eq!( receiver.apply(SecretBytes::new(replacement.snapshot().expose().to_vec()))?, WatchSnapshotApply::Replaced ); assert_eq!( receiver.current().expect("replacement").entries()[0].account(), "bob" ); assert_eq!( receiver.apply(SecretBytes::new(first.snapshot().expose().to_vec()))?, WatchSnapshotApply::Stale ); assert_eq!( receiver.current().expect("stale ignored").entries()[0].account(), "bob" ); let revoked = sender.prepare("paired-watch-a", Vec::new())?; assert_eq!(revoked.revision(), 3); assert_eq!( receiver.apply(SecretBytes::new(revoked.snapshot().expose().to_vec()))?, WatchSnapshotApply::Revoked ); assert!( receiver .current() .expect("revocation marker") .is_revocation() ); assert_eq!( receiver.apply(SecretBytes::new(replacement.snapshot().expose().to_vec()))?, WatchSnapshotApply::Stale ); let changed_watch = sender.prepare( "paired-watch-b", vec![entry("otp/carol", "Acme", "carol", b"third-secret")], )?; assert_eq!(changed_watch.revision(), 4); assert_eq!( receiver.apply(SecretBytes::new(changed_watch.snapshot().expose().to_vec()))?, WatchSnapshotApply::PairingChanged ); assert_eq!(receiver.current().expect("new pairing").revision(), 4); let mut fresh_watch = WatchSnapshotReceiver::default(); assert_eq!( fresh_watch.apply(SecretBytes::new(changed_watch.snapshot().expose().to_vec()))?, WatchSnapshotApply::Replaced ); let mut damaged = changed_watch.snapshot().expose().to_vec(); damaged[20] ^= 0x55; assert!(fresh_watch.apply(SecretBytes::new(damaged)).is_err()); assert!(fresh_watch.current().is_none()); assert_eq!( fresh_watch.apply(SecretBytes::new(first.snapshot().expose().to_vec()))?, WatchSnapshotApply::Stale ); assert_eq!( fresh_watch.apply(SecretBytes::new(changed_watch.snapshot().expose().to_vec()))?, WatchSnapshotApply::Replaced ); let persisted = fs::read_to_string(journal)?; for forbidden in [ "first-secret", "second-secret", "third-secret", "otpauth://", "94287082", ] { assert!(!persisted.contains(forbidden), "journal leaked {forbidden}"); } Ok(()) } #[test] fn journal_keeps_revisions_monotonic_across_sender_reloads() -> TestResult { let directory = tempfile::tempdir()?; let journal = directory.path().join("watch-snapshot.toml"); let first = WatchSnapshotSender::load(journal.clone()).prepare( "paired-watch", vec![entry("otp/alice", "Acme", "alice", b"secret")], )?; assert_eq!(first.revision(), 1); let same = WatchSnapshotSender::load(journal.clone()).prepare( "paired-watch", vec![entry("otp/alice", "Acme", "alice", b"secret")], )?; assert_eq!(same.revision(), 1); let changed = WatchSnapshotSender::load(journal).prepare("paired-watch", Vec::new())?; assert_eq!(changed.revision(), 2); Ok(()) }