Implement Android TOTP cached discovery and search (#110)
Android ARM verification / rust (push) Failing after 49m58s
Android ARM verification / android (push) Failing after 8s
Dependency security audit / rustsec (push) Failing after 22s

This commit is contained in:
hugo committed 2026-10-12 20:38:33 +02:00
1 parent 0c8366d34f
commit f4a6edbc68
18 files changed
+1070 -165

No files matched your search

+116
View File
@@ -228,6 +228,14 @@ fn totp_cache_reuses_ciphertext_hashes_and_removes_deleted_entries() -> TestResu
assert_eq!(warm_secrets.requests, 0);
assert_eq!(warm.progress().cache_hits(), warm.progress().total());
assert_eq!(warm_page.rows(), page.rows());
let locked = MobileTotpOperation::default();
assert_eq!(service.refresh_cached(&cache, &locked)?.rows(), page.rows());
assert_eq!(locked.progress().cache_hits(), locked.progress().total());
locked.cancel();
assert_eq!(
locked.progress().phase(),
MobileTotpDiscoveryPhase::Complete
);
assert_eq!(service.cache_status(&cache).0 as usize, page.rows().len());
let rebuild = MobileTotpOperation::default();
let mut rebuild_secrets = CountingSecrets::all(&fixture);
@@ -269,6 +277,17 @@ fn totp_cache_reuses_ciphertext_hashes_and_removes_deleted_entries() -> TestResu
"ordinary",
b"otpauth://totp/New:new@example.com?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ&issuer=New\n",
)?;
let preserved = fs::read(&cache)?;
let requires_authentication = MobileTotpOperation::default();
assert!(matches!(
service.refresh_cached(&cache, &requires_authentication),
Err(MobileTotpError::AuthenticationRequired)
));
assert_eq!(
requires_authentication.progress().phase(),
MobileTotpDiscoveryPhase::Preparing
);
assert_eq!(fs::read(&cache)?, preserved);
let changed = MobileTotpOperation::default();
let mut changed_secrets = CountingSecrets::all(&fixture);
let changed_page =
@@ -327,6 +346,10 @@ fn totp_cache_reuses_ciphertext_hashes_and_removes_deleted_entries() -> TestResu
let cancelled = MobileTotpOperation::default();
cancelled.cancel();
assert!(matches!(
service.search_cached_page_cancellable(&BTreeSet::new(), &cache, "team", &cancelled),
Err(MobileTotpError::Otp(OtpError::Cancelled))
));
let mut cancelled_secrets = CountingSecrets::all(&fixture);
assert!(matches!(
service.discover(&BTreeSet::new(), &mut cancelled_secrets, &cache, &cancelled,),
@@ -490,6 +513,99 @@ fn cancelled_discovery_checkpoints_completed_entries() -> TestResult {
service.discover(&BTreeSet::new(), &mut resumed_secrets, &cache, &resumed)?;
assert!(resumed.progress().cache_hits() >= 1);
assert!(resumed_secrets.requests < resumed.progress().total() as usize);
let deleted = repository
.snapshot()?
.entries()
.next()
.unwrap()
.path()
.clone();
repository.remove_entry(&deleted)?;
write_plaintext(
&repository,
&keys,
"new-checkpoint",
b"otpauth://totp/Test?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ\n",
)?;
let interrupted = MobileTotpOperation::default();
let mut secrets = CancellingSecrets {
inner: FixtureSecrets::all(&fixture),
operation: &interrupted,
requests: 0,
};
assert!(matches!(
service.discover(&BTreeSet::new(), &mut secrets, &cache, &interrupted),
Err(MobileTotpError::Otp(OtpError::Cancelled))
));
let checkpoint: toml::Value = toml::from_str(&fs::read_to_string(&cache)?)?;
assert!(checkpoint["entries"].get(deleted.to_string()).is_some());
service.discover(
&BTreeSet::new(),
&mut FixtureSecrets::all(&fixture),
&cache,
&MobileTotpOperation::default(),
)?;
let completed: toml::Value = toml::from_str(&fs::read_to_string(&cache)?)?;
assert!(completed["entries"].get(deleted.to_string()).is_none());
Ok(())
}
#[test]
fn large_cached_search_filters_paths_without_reading_secrets() -> 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,
"seed",
b"otpauth://totp/Test?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ\n",
)?;
let directory = tempfile::tempdir()?;
let cache = directory.path().join("totp-catalog.toml");
let service = MobileTotpService::new(&repository, &keys);
service.discover(
&BTreeSet::new(),
&mut FixtureSecrets::all(&fixture),
&cache,
&MobileTotpOperation::default(),
)?;
let ciphertext = repository.read_entry(&EntryPath::parse("seed")?)?;
let mut catalog: toml::Value = toml::from_str(&fs::read_to_string(&cache)?)?;
let entries = catalog["entries"].as_table_mut().unwrap();
let record = entries["seed"].clone();
for index in 0..1_000 {
let path = format!("large/entry-{index:04}");
repository.write_entry(&EntryPath::parse(&path)?, &ciphertext)?;
entries.insert(path, record.clone());
}
fs::write(&cache, toml::to_string(&catalog)?)?;
let result = service
.search_cached_page_cancellable(
&BTreeSet::new(),
&cache,
" ENTRY-0999 ",
&MobileTotpOperation::default(),
)?
.unwrap();
assert_eq!(
result
.rows()
.iter()
.map(|row| row.path())
.collect::<Vec<_>>(),
["large/entry-0999"]
);
let all = service
.search_cached_page_cancellable(
&BTreeSet::new(),
&cache,
"large/",
&MobileTotpOperation::default(),
)?
.unwrap();
assert_eq!(all.rows().len(), 1_000);
Ok(())
}