Files
IronStorage/crates/storage/tests/mobile_totp.rs

118 lines
3.8 KiB
Rust

#![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<String, Vec<u8>>);
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<SecretBytes, SecretProviderError> {
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(())
}