use crate::error::MfaError;
use base32::Alphabet;
use totp_lite::{totp_custom, Sha1};
const TIME_STEP_SECS: u64 = 30;
const CODE_DIGITS: u32 = 6;
#[derive(Clone)]
pub struct TotpSecret {
secret_base32: String,
}
impl std::fmt::Debug for TotpSecret {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TotpSecret")
.field("secret_base32", &"[redacted]")
.finish()
}
}
impl Drop for TotpSecret {
fn drop(&mut self) {
zeroize::Zeroize::zeroize(&mut self.secret_base32);
}
}
impl TotpSecret {
#[must_use]
pub fn new(secret_base32: impl Into<String>) -> Self {
Self {
secret_base32: secret_base32.into(),
}
}
pub fn current_code(&self) -> Result<String, MfaError> {
let step = current_time_step()?;
totp_code_at(&self.secret_base32, step)
}
}
pub fn totp_code_at(secret_base32: &str, time_step: u64) -> Result<String, MfaError> {
let key = base32::decode(Alphabet::Rfc4648 { padding: false }, secret_base32.trim())
.ok_or_else(|| MfaError::Totp("invalid base32 secret".into()))?;
let code = totp_custom::<Sha1>(TIME_STEP_SECS, CODE_DIGITS, &key, time_step);
Ok(format!("{code:0width$}", width = CODE_DIGITS as usize))
}
fn current_time_step() -> Result<u64, MfaError> {
use std::time::{SystemTime, UNIX_EPOCH};
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|e| MfaError::Totp(e.to_string()))?;
Ok(now.as_secs() / TIME_STEP_SECS)
}