diff --git a/common/src/types.rs b/common/src/types.rs index 9e86f31..3d752e5 100644 --- a/common/src/types.rs +++ b/common/src/types.rs @@ -72,6 +72,15 @@ pub struct RecoveryCodeSummary { pub expires_unix: u64, } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, zvariant::Type)] +pub struct TotpEnrollment { + pub user: String, + /// Base32-encoded shared secret, no padding. ~32 chars for 160 bits. + pub secret_b32: String, + /// Full `otpauth://totp/...` URI suitable for QR encoding. + pub otpauth_uri: String, +} + #[cfg(test)] mod tests { use super::*; diff --git a/daemon/src/dbus.rs b/daemon/src/dbus.rs index 2368559..4aa1420 100644 --- a/daemon/src/dbus.rs +++ b/daemon/src/dbus.rs @@ -257,6 +257,8 @@ central_path = "{}" policy_dir, pending_dir: tmp.path().join("pending"), recovery_dir: tmp.path().join("recovery"), + #[cfg(feature = "totp")] + totp_dir: tmp.path().join("totp"), userdb_path: tmp.path().join("users.db"), pam_profile_path: tmp.path().join("authforge-pamconf"), pam_auth_update: shim, diff --git a/daemon/src/state.rs b/daemon/src/state.rs index 2691d6f..bae68e4 100644 --- a/daemon/src/state.rs +++ b/daemon/src/state.rs @@ -5,6 +5,8 @@ use crate::storage::credentials::{CredEntry, CredentialsStore, CredsError, Creds use crate::storage::pending::{PendingError, PendingStore}; use crate::storage::policy::PolicyStore; use crate::storage::recovery::{RecoveryEntry, RecoveryStore, RecoveryStoreError}; +#[cfg(feature = "totp")] +use crate::storage::totp::TotpStore; use crate::storage::userdb::{UserDb, UserDbError}; use authforge_common::policy::{Policy, PolicyError}; use authforge_common::types::{ @@ -32,12 +34,17 @@ pub enum StateError { Apply(#[from] ApplyError), #[error("authenticator: {0}")] Authn(String), + #[allow(dead_code)] // wired through D-Bus in Task 5. + #[error("storage: {0}")] + Storage(String), } pub struct StorageConfig { pub policy_dir: PathBuf, pub pending_dir: PathBuf, pub recovery_dir: PathBuf, + #[cfg(feature = "totp")] + pub totp_dir: PathBuf, pub userdb_path: PathBuf, /// Where pam-auth-update reads the AuthForge profile from. Defaults to /// `/usr/share/pam-configs/authforge`; tests / non-root runs override. @@ -58,6 +65,8 @@ impl StorageConfig { policy_dir: env("AUTHFORGE_POLICY_DIR", "/etc/authforge/policy.d"), pending_dir: env("AUTHFORGE_PENDING_DIR", "/var/lib/authforge/pending"), recovery_dir: env("AUTHFORGE_RECOVERY_DIR", "/var/lib/authforge/recovery"), + #[cfg(feature = "totp")] + totp_dir: env("AUTHFORGE_TOTP_DIR", "/etc/google-authenticator"), userdb_path: env("AUTHFORGE_USERDB", "/var/lib/authforge/users.db"), pam_profile_path: env("AUTHFORGE_PAMCONF_PATH", "/usr/share/pam-configs/authforge"), pam_auth_update: env("AUTHFORGE_PAM_AUTH_UPDATE", "/usr/sbin/pam-auth-update"), @@ -70,6 +79,9 @@ pub struct AppState { pending: PendingStore, #[allow(dead_code)] // wired through D-Bus in Task 6. recovery: RecoveryStore, + #[cfg(feature = "totp")] + #[allow(dead_code)] // wired through D-Bus in Task 5. + totp: TotpStore, userdb: Mutex, authn: Arc, applier: PolicyApplier, @@ -80,12 +92,16 @@ impl AppState { let policy = PolicyStore::new(cfg.policy_dir); let pending = PendingStore::new(cfg.pending_dir); let recovery = RecoveryStore::new(cfg.recovery_dir); + #[cfg(feature = "totp")] + let totp = TotpStore::new(cfg.totp_dir); let userdb = Mutex::new(UserDb::open(&cfg.userdb_path)?); let applier = PolicyApplier::new(cfg.pam_profile_path, cfg.pam_auth_update); Ok(Self { policy, pending, recovery, + #[cfg(feature = "totp")] + totp, userdb, authn, applier, @@ -273,6 +289,37 @@ impl AppState { } } +#[cfg(feature = "totp")] +#[allow(dead_code)] // wired through D-Bus in Task 5. +impl AppState { + pub async fn enroll_totp( + &self, + user: &str, + ) -> Result { + let inner = self + .totp + .enroll(user, "AuthForge") + .map_err(|e| StateError::Storage(e.to_string()))?; + Ok(authforge_common::types::TotpEnrollment { + user: inner.user, + secret_b32: inner.secret_b32, + otpauth_uri: inner.otpauth_uri, + }) + } + + pub async fn is_totp_enrolled(&self, user: &str) -> Result { + self.totp + .is_enrolled(user) + .map_err(|e| StateError::Storage(e.to_string())) + } + + pub async fn revoke_totp(&self, user: &str) -> Result { + self.totp + .revoke(user) + .map_err(|e| StateError::Storage(e.to_string())) + } +} + #[cfg(test)] mod tests { use super::*; @@ -293,6 +340,8 @@ mod tests { policy_dir: d.join("policy.d"), pending_dir: d.join("pending"), recovery_dir: d.join("recovery"), + #[cfg(feature = "totp")] + totp_dir: d.join("totp"), userdb_path: d.join("users.db"), pam_profile_path: d.join("authforge-pamconf"), pam_auth_update: shim, @@ -353,6 +402,8 @@ mod tests { policy_dir, pending_dir: d.path().join("pending"), recovery_dir: d.path().join("recovery"), + #[cfg(feature = "totp")] + totp_dir: d.path().join("totp"), userdb_path: d.path().join("users.db"), pam_profile_path: d.path().join("authforge-pamconf"), pam_auth_update: shim,