Private
Public Access
1
0
Files
linux_patch_manager/crates/pm-auth/src/mfa_totp.rs
Echo 6c72dc3ac6
Some checks failed
CI Pipeline / Rust Format Check (push) Failing after 2s
CI Pipeline / Clippy Lints (push) Failing after 1s
CI Pipeline / Rust Unit Tests (push) Failing after 2s
CI Pipeline / Security Audit (push) Failing after 2s
CI Pipeline / Frontend Lint & Type Check (push) Failing after 3s
CI Pipeline / Build .deb & Release (push) Has been skipped
feat: populate os_family, os_name, arch, agent_version from health poller and enrollment
- health_poller: persist agent_version from HealthData.version
- health_poller: call /system/info to update os_family, os_name, arch
- enrollment: set os_family and arch from os_details during approval
- enrollment: build os_name from os+os_version when name field absent
- COALESCE in UPDATE preserves existing values when new data unavailable
- version bump 0.1.7 -> 0.1.8
2026-05-21 00:09:57 +00:00

104 lines
3.2 KiB
Rust
Executable File

//! TOTP (Time-based One-Time Password) MFA implementation.
//!
//! Uses TOTP-rs with HMAC-SHA1, 6-digit codes, 30-second window.
//! Compatible with Google Authenticator, Authy, and standard TOTP apps.
use serde::{Deserialize, Serialize};
use thiserror::Error;
use totp_rs::{Algorithm, Secret, TOTP};
/// TOTP issuer label shown in authenticator apps.
const ISSUER: &str = "Linux Patch Manager";
#[derive(Debug, Error)]
pub enum TotpError {
#[error("Failed to create TOTP: {0}")]
Creation(String),
#[error("Invalid TOTP secret")]
InvalidSecret,
#[error("TOTP code verification failed")]
VerificationFailed,
}
/// TOTP setup response returned to the user during MFA enrollment.
#[derive(Debug, Serialize, Deserialize)]
pub struct TotpSetup {
/// Base32-encoded secret for manual entry in authenticator apps.
pub secret_base32: String,
/// OTP Auth URI for QR code generation (otpauth://totp/...).
pub otp_uri: String,
}
/// Generate a new TOTP secret and return setup information.
///
/// The caller should store `secret_base32` in the database after
/// the user verifies the first code.
pub fn generate_setup(username: &str) -> Result<TotpSetup, TotpError> {
let secret = Secret::generate_secret();
let secret_base32 = secret.to_encoded().to_string();
let totp = build_totp(username, &secret_base32)?;
let otp_uri = totp.get_url();
Ok(TotpSetup {
secret_base32,
otp_uri,
})
}
/// Verify a TOTP code against the stored secret.
///
/// Accepts codes within a ±1 step window (±30 seconds) to handle clock skew.
pub fn verify_code(username: &str, secret_base32: &str, code: &str) -> Result<bool, TotpError> {
let totp = build_totp(username, secret_base32)?;
let valid = totp
.check_current(code)
.map_err(|_| TotpError::VerificationFailed)?;
Ok(valid)
}
/// Build a TOTP instance from a base32 secret.
fn build_totp(username: &str, secret_base32: &str) -> Result<TOTP, TotpError> {
let secret = Secret::Encoded(secret_base32.to_string());
let secret_bytes = secret.to_bytes().map_err(|_| TotpError::InvalidSecret)?;
// With the `otpauth` feature, TOTP::new signature is:
// new(issuer, account_name, algorithm, digits, skew, step, secret)
TOTP::new(
Algorithm::SHA1,
6, // digits
1, // skew
30, // step (seconds)
secret_bytes,
Some(ISSUER.to_string()),
username.to_string(),
)
.map_err(|e| TotpError::Creation(e.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn generate_setup_produces_valid_uri() {
let setup = generate_setup("testuser").unwrap();
assert!(!setup.secret_base32.is_empty());
assert!(setup.otp_uri.starts_with("otpauth://totp/"));
}
#[test]
fn verify_with_current_code() {
let setup = generate_setup("testuser").unwrap();
let totp = build_totp("testuser", &setup.secret_base32).unwrap();
let code = totp.generate_current().unwrap();
assert!(verify_code("testuser", &setup.secret_base32, &code).unwrap());
}
#[test]
fn wrong_code_fails() {
let setup = generate_setup("testuser").unwrap();
assert!(!verify_code("testuser", &setup.secret_base32, "000000").unwrap());
}
}