Show TOTP codes in password details (#75)

This commit is contained in:
2026-08-11 23:40:24 +02:00
parent 9759162eff
commit 2026637a0e
9 changed files with 640 additions and 110 deletions

View File

@@ -9,6 +9,7 @@ use std::{
use ironstorage::{
crypto::{KeyInfo, KeyStore, SecretProvider, SecretProviderError},
document::EntryDocumentService,
mobile_totp::{
MobileTotpDiscoveryPhase, MobileTotpError, MobileTotpOperation, MobileTotpService,
MobileWatchSnapshotState,
@@ -305,6 +306,72 @@ fn totp_cache_reuses_ciphertext_hashes_and_removes_deleted_entries() -> TestResu
Ok(())
}
#[test]
fn decrypted_entry_details_reconcile_totp_cache_without_persisting_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"))?;
let path = EntryPath::parse("otp/detail")?;
write_plaintext(
&repository,
&keys,
"otp/detail",
b"password\notpauth://totp/Acme:detail@example.com?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ&issuer=Acme&digits=8&period=30\n",
)?;
let cache_directory = tempfile::tempdir()?;
let cache = cache_directory.path().join("totp-catalog.toml");
let service = MobileTotpService::new(&repository, &keys);
let mut secrets = FixtureSecrets::all(&fixture);
let document =
EntryDocumentService::new(&repository, &keys).open("otp/detail", &mut secrets)?;
let ciphertext = repository.read_entry(&path)?;
let reconciliation =
service.reconcile_document(&document, &ciphertext, Some(59), &BTreeSet::new(), &cache)?;
let detail = reconciliation.detail().expect("valid TOTP detail");
assert_eq!(detail.code().expose(), b"94287082");
assert_eq!(detail.valid_until(), 60);
assert!(
service
.cached_page(&BTreeSet::new(), &cache)
.expect("detail created cache")
.rows()
.iter()
.any(|row| row.path() == "otp/detail")
);
let encoded = fs::read_to_string(&cache)?;
assert!(encoded.contains("otp/detail"));
assert!(encoded.contains("is_totp = true"));
for secret in ["otpauth://", "secret=", "detail@example.com", "password"] {
assert!(!encoded.contains(secret), "cache leaked {secret}");
}
write_plaintext(
&repository,
&keys,
"otp/detail",
b"password\nlogin: detail\n",
)?;
let mut secrets = FixtureSecrets::all(&fixture);
let document =
EntryDocumentService::new(&repository, &keys).open("otp/detail", &mut secrets)?;
let ciphertext = repository.read_entry(&path)?;
let reconciliation =
service.reconcile_document(&document, &ciphertext, Some(59), &BTreeSet::new(), &cache)?;
assert!(reconciliation.detail().is_none());
assert!(
service
.cached_page(&BTreeSet::new(), &cache)
.expect("detail updated cache")
.rows()
.iter()
.all(|row| row.path() != "otp/detail")
);
assert!(fs::read_to_string(cache)?.contains("is_totp = false"));
Ok(())
}
#[test]
fn cancelled_discovery_checkpoints_completed_entries() -> TestResult {
let fixture = FixtureSet::load()?;