#![forbid(unsafe_code)] mod support; use std::collections::{BTreeMap, BTreeSet}; use ironstorage::{ crypto::{KeyInfo, KeyStore, SecretProvider, SecretProviderError}, mobile_totp::{MobileTotpService, MobileWatchSnapshotState}, recipient::RecipientPolicyManager, repository::{EntryPath, Repository, SecretBytes}, }; use support::compatibility::{FixtureSet, TestResult}; struct FixtureSecrets(BTreeMap>); impl FixtureSecrets { fn all(fixture: &FixtureSet) -> Self { Self( fixture .generated .keys .iter() .map(|key| { ( key.primary_fingerprint.clone(), key.passphrase.as_bytes().to_vec(), ) }) .collect(), ) } } impl SecretProvider for FixtureSecrets { fn secret_for(&mut self, key: &KeyInfo) -> Result { self.0 .get(key.fingerprint().as_str()) .cloned() .map(SecretBytes::new) .ok_or(SecretProviderError::Unavailable) } } #[test] fn mobile_totp_catalog_details_and_watch_selection_are_storage_owned() -> TestResult { let fixture = FixtureSet::load()?; let store = fixture.materialize_store("basic")?; let repository = Repository::open(store.path())?; let keys = KeyStore::load(fixture.path("keys"))?; write_plaintext( &repository, &keys, "otp/alice", b"password\notpauth://totp/Acme:alice@example.com?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ&issuer=Acme&digits=8&period=30\n", )?; write_plaintext( &repository, &keys, "otp/counter", b"otpauth://hotp/Counter?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ&counter=0\n", )?; write_plaintext(&repository, &keys, "ordinary", b"password\nlogin: alice\n")?; let service = MobileTotpService::new(&repository, &keys); let mut secrets = FixtureSecrets::all(&fixture); let page = service.page(&BTreeSet::new(), &mut secrets)?; let alice = page .rows() .iter() .find(|row| row.path() == "otp/alice") .expect("inserted TOTP row"); assert_eq!(alice.issuer(), Some("Acme")); assert_eq!(alice.account(), "alice@example.com"); assert!(!alice.shared_with_watch()); assert!(page.rows().iter().all(|row| row.path() != "otp/counter")); assert_eq!(page.watch().state(), MobileWatchSnapshotState::Unavailable); assert!(!format!("{page:?}").contains("94287082")); let detail = service.detail("otp/alice", 59, &BTreeSet::new(), &mut secrets)?; assert_eq!(detail.code().expose(), b"94287082"); assert_eq!(detail.valid_until(), 60); assert_eq!(detail.period(), 30); assert!(!format!("{detail:?}").contains("94287082")); let selected = BTreeSet::from([EntryPath::parse("otp/alice")?]); let page = service.page(&selected, &mut secrets)?; assert!( page.rows() .iter() .find(|row| row.path() == "otp/alice") .expect("selected TOTP row") .shared_with_watch() ); assert_eq!(page.watch().state(), MobileWatchSnapshotState::Pending); let detail = service.detail("otp/alice", 60, &selected, &mut secrets)?; assert!(detail.shared_with_watch()); assert_eq!(detail.valid_until(), 90); Ok(()) } fn write_plaintext( repository: &Repository, keys: &KeyStore, path: &str, plaintext: &[u8], ) -> TestResult { let path = EntryPath::parse(path)?; let recipients = RecipientPolicyManager::new(repository, keys).resolve_for_entry(&path, None)?; let encrypted = keys.encrypt( SecretBytes::new(plaintext.to_vec()), recipients.recipients(), )?; repository.write_entry(&path, &encrypted)?; Ok(()) }