Synchronize selected TOTP entries to Apple Watch (#55)
This commit is contained in:
167
crates/storage/tests/mobile_watch.rs
Normal file
167
crates/storage/tests/mobile_watch.rs
Normal file
@@ -0,0 +1,167 @@
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
use std::fs;
|
||||
|
||||
use ironstorage::{
|
||||
mobile_watch::{
|
||||
MobileWatchSnapshotState, WatchSnapshotApply, WatchSnapshotEntry, WatchSnapshotReceiver,
|
||||
WatchSnapshotSender,
|
||||
},
|
||||
otp::OtpAlgorithm,
|
||||
repository::{EntryPath, SecretBytes},
|
||||
};
|
||||
|
||||
type TestResult = Result<(), Box<dyn std::error::Error>>;
|
||||
|
||||
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(())
|
||||
}
|
||||
Reference in New Issue
Block a user