diff --git a/daemon/src/dbus.rs b/daemon/src/dbus.rs index 4aa1420..f4d6a97 100644 --- a/daemon/src/dbus.rs +++ b/daemon/src/dbus.rs @@ -4,6 +4,8 @@ use authforge_common::policy::Policy; use authforge_common::types::{ Credential, PendingFlag, PendingStatus, PolicyApplyResult, RecoveryCodeSummary, }; +#[cfg(feature = "totp")] +use authforge_common::types::TotpEnrollment; use std::sync::Arc; pub struct AuthForge { @@ -174,6 +176,35 @@ impl AuthForge { .map_err(|e| zbus::fdo::Error::Failed(e.to_string())) } + #[cfg(feature = "totp")] + async fn enroll_totp(&self, user: String) -> zbus::fdo::Result { + self.authz("io.dangerousthings.AuthForge.enroll-totp") + .await?; + self.state + .enroll_totp(&user) + .await + .map_err(|e| zbus::fdo::Error::Failed(e.to_string())) + } + + #[cfg(feature = "totp")] + async fn is_totp_enrolled(&self, user: String) -> zbus::fdo::Result { + // No polkit gate — pure read; daemon's pattern is gates on writes only. + self.state + .is_totp_enrolled(&user) + .await + .map_err(|e| zbus::fdo::Error::Failed(e.to_string())) + } + + #[cfg(feature = "totp")] + async fn revoke_totp(&self, user: String) -> zbus::fdo::Result { + self.authz("io.dangerousthings.AuthForge.revoke-totp") + .await?; + self.state + .revoke_totp(&user) + .await + .map_err(|e| zbus::fdo::Error::Failed(e.to_string())) + } + /// Emitted by main.rs whenever the inotify watcher on the policy.d/ dir /// reports any change. Subscribers (GUI) reload via `GetPolicy`. #[zbus(signal)] @@ -520,4 +551,42 @@ central_path = "{}" .expect("signal not received within 2s") .expect("stream closed before signal"); } + + #[cfg(feature = "totp")] + #[tokio::test] + async fn enroll_totp_returns_otpauth_uri() { + let (_srv, client, _state, _tmp) = p2p_pair().await; + let p = proxy(&client).await; + let e: TotpEnrollment = p.call("EnrollTotp", &("alice",)).await.unwrap(); + assert_eq!(e.user, "alice"); + assert!(e.otpauth_uri.starts_with("otpauth://totp/AuthForge:alice?")); + assert!(e + .secret_b32 + .chars() + .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit())); + } + + #[cfg(feature = "totp")] + #[tokio::test] + async fn is_totp_enrolled_round_trips() { + let (_srv, client, _state, _tmp) = p2p_pair().await; + let p = proxy(&client).await; + let before: bool = p.call("IsTotpEnrolled", &("alice",)).await.unwrap(); + assert!(!before); + let _: TotpEnrollment = p.call("EnrollTotp", &("alice",)).await.unwrap(); + let after: bool = p.call("IsTotpEnrolled", &("alice",)).await.unwrap(); + assert!(after); + } + + #[cfg(feature = "totp")] + #[tokio::test] + async fn revoke_totp_removes_enrollment() { + let (_srv, client, _state, _tmp) = p2p_pair().await; + let p = proxy(&client).await; + let _: TotpEnrollment = p.call("EnrollTotp", &("alice",)).await.unwrap(); + let removed: bool = p.call("RevokeTotp", &("alice",)).await.unwrap(); + assert!(removed); + let after: bool = p.call("IsTotpEnrolled", &("alice",)).await.unwrap(); + assert!(!after); + } } diff --git a/debian/io.dangerousthings.AuthForge.policy b/debian/io.dangerousthings.AuthForge.policy index 5708116..0a9394b 100644 --- a/debian/io.dangerousthings.AuthForge.policy +++ b/debian/io.dangerousthings.AuthForge.policy @@ -97,4 +97,24 @@ + + Enroll a TOTP secret + Authentication is required to enroll a TOTP secret. + + auth_self_keep + auth_admin_keep + auth_self_keep + + + + + Revoke a TOTP enrollment + Administrator authentication is required to revoke a TOTP enrollment. + + auth_admin_keep + auth_admin_keep + auth_admin_keep + + +