diff --git a/crates/relicario-core/src/error.rs b/crates/relicario-core/src/error.rs index bbe9aa9..f2be80d 100644 --- a/crates/relicario-core/src/error.rs +++ b/crates/relicario-core/src/error.rs @@ -109,6 +109,12 @@ pub enum RelicarioError { /// rotating the passphrase or reference image. #[error("device key error: {0}")] DeviceKey(String), + + /// HOTP requires incrementing and persisting the counter after each use. + /// Without vault-save machinery in compute_totp_code, HOTP would desync + /// immediately. Use TOTP instead. + #[error("HOTP is not supported: counter persistence requires vault save after each use")] + HotpNotSupported, } /// Crate-wide result alias, reducing boilerplate in function signatures. diff --git a/crates/relicario-core/src/item_types/totp.rs b/crates/relicario-core/src/item_types/totp.rs index f645fbe..9fefd0e 100644 --- a/crates/relicario-core/src/item_types/totp.rs +++ b/crates/relicario-core/src/item_types/totp.rs @@ -64,14 +64,14 @@ impl Default for TotpKind { fn default() -> Self { TotpKind::Totp } } -/// Compute a TOTP/HOTP/Steam code for `config` at the given Unix timestamp. +/// Compute a TOTP/Steam code for `config` at the given Unix timestamp. /// /// For TOTP and Steam: counter = `now_unix_seconds / period_seconds`. -/// For HOTP: uses the `counter` carried in the variant. +/// HOTP is not supported — returns [`RelicarioError::HotpNotSupported`]. pub fn compute_totp_code(config: &TotpConfig, now_unix_seconds: u64) -> Result { let counter = match config.kind { TotpKind::Totp => now_unix_seconds / config.period_seconds as u64, - TotpKind::Hotp { counter } => counter, + TotpKind::Hotp { .. } => return Err(RelicarioError::HotpNotSupported), TotpKind::Steam => now_unix_seconds / config.period_seconds as u64, }; let counter_bytes = counter.to_be_bytes(); @@ -165,7 +165,7 @@ mod tests { } #[test] - fn hotp_carries_counter() { + fn hotp_kind_roundtrips_through_json() { let cfg = TotpConfig { kind: TotpKind::Hotp { counter: 42 }, ..TotpConfig::default() }; let json = serde_json::to_string(&cfg).unwrap(); let parsed: TotpConfig = serde_json::from_str(&json).unwrap(); @@ -173,6 +173,18 @@ mod tests { TotpKind::Hotp { counter } => assert_eq!(counter, 42), other => panic!("expected Hotp, got {:?}", other), } + // Note: compute_totp_code will reject this — HOTP not supported + } + + #[test] + fn hotp_returns_not_supported_error() { + let cfg = TotpConfig { + secret: Zeroizing::new(b"12345678901234567890".to_vec()), + kind: TotpKind::Hotp { counter: 0 }, + ..TotpConfig::default() + }; + let result = compute_totp_code(&cfg, 0); + assert!(matches!(result, Err(RelicarioError::HotpNotSupported))); } #[test]