Build secure Rust watchOS TOTP core (#56)

This commit is contained in:
2026-08-16 14:27:21 +02:00
parent a055c93b86
commit df1d49339c
20 changed files with 2672 additions and 137 deletions

View File

@@ -4,8 +4,8 @@ use std::fs;
use ironstorage::{
mobile_watch::{
MobileWatchSnapshotState, WatchSnapshotApply, WatchSnapshotEntry, WatchSnapshotReceiver,
WatchSnapshotSender,
MobileWatchSnapshotState, WatchPersistenceAction, WatchRuntime, WatchSnapshotApply,
WatchSnapshotEntry, WatchSnapshotReceiver, WatchSnapshotSender,
},
otp::OtpAlgorithm,
repository::{EntryPath, SecretBytes},
@@ -15,7 +15,7 @@ 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"),
EntryPath::parse(path).expect("fixture path").to_string(),
Some(issuer.to_owned()),
account.to_owned(),
OtpAlgorithm::Sha256,
@@ -165,3 +165,49 @@ fn journal_keeps_revisions_monotonic_across_sender_reloads() -> TestResult {
assert_eq!(changed.revision(), 2);
Ok(())
}
#[test]
fn watch_runtime_generates_view_ready_totp_and_clears_secrets_when_locked() -> TestResult {
let directory = tempfile::tempdir()?;
let mut sender = WatchSnapshotSender::load(directory.path().join("watch-snapshot.toml"));
let snapshot = sender.prepare(
"paired-watch",
vec![WatchSnapshotEntry::new(
"otp/alice".to_owned(),
Some("Acme".to_owned()),
"alice".to_owned(),
OtpAlgorithm::Sha1,
8,
30,
SecretBytes::new(b"12345678901234567890".to_vec()),
)],
)?;
let mut runtime = WatchRuntime::default();
let update = runtime.apply_snapshot(snapshot.snapshot().expose().to_vec())?;
assert_eq!(update.apply(), WatchSnapshotApply::Replaced);
assert_eq!(update.persistence(), WatchPersistenceAction::Replace);
assert_eq!(update.selected_entries(), 1);
assert!(!update.receipt().is_empty());
let records = runtime.records_at(59)?;
assert_eq!(records.len(), 1);
assert_eq!(records[0].path(), "otp/alice");
assert_eq!(records[0].code().expose(), b"94287082");
assert_eq!(records[0].valid_until(), 60);
assert_eq!(records[0].remaining_at(59), 1);
runtime.protected_data_unavailable();
assert!(runtime.records_at(59).is_err());
let restored = runtime.apply_snapshot(snapshot.snapshot().expose().to_vec())?;
assert_eq!(restored.persistence(), WatchPersistenceAction::Replace);
assert_eq!(runtime.records_at(59)?[0].code().expose(), b"94287082");
let revocation = sender.prepare("paired-watch", Vec::new())?;
let revoked = runtime.apply_snapshot(revocation.snapshot().expose().to_vec())?;
assert_eq!(revoked.apply(), WatchSnapshotApply::Revoked);
assert_eq!(revoked.persistence(), WatchPersistenceAction::Delete);
assert!(runtime.records_at(59)?.is_empty());
Ok(())
}