use totp_rs::{Algorithm, Secret, TOTP};
const ISSUER: &str = "kanade";
const DIGITS: usize = 6;
const SKEW: u8 = 1;
const STEP: u64 = 30;
pub fn generate_secret_base32() -> String {
Secret::generate_secret().to_encoded().to_string()
}
fn totp(secret_base32: &str, account: &str) -> Result<TOTP, String> {
let bytes = Secret::Encoded(secret_base32.to_string())
.to_bytes()
.map_err(|e| format!("decode totp secret: {e}"))?;
TOTP::new(
Algorithm::SHA1,
DIGITS,
SKEW,
STEP,
bytes,
Some(ISSUER.to_string()),
account.to_string(),
)
.map_err(|e| format!("build totp: {e}"))
}
pub fn otpauth_url(secret_base32: &str, account: &str) -> Result<String, String> {
Ok(totp(secret_base32, account)?.get_url())
}
pub fn verify(secret_base32: &str, account: &str, code: &str) -> Result<bool, String> {
totp(secret_base32, account)?
.check_current(code)
.map_err(|e| format!("totp check: {e}"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn generated_secret_roundtrips_and_current_code_verifies() {
let secret = generate_secret_base32();
assert!(!secret.is_empty());
let url = otpauth_url(&secret, "alice").unwrap();
assert!(url.starts_with("otpauth://totp/"));
assert!(url.contains("kanade"));
let bytes = Secret::Encoded(secret.clone()).to_bytes().unwrap();
let t = TOTP::new(
Algorithm::SHA1,
DIGITS,
SKEW,
STEP,
bytes,
Some(ISSUER.to_string()),
"alice".to_string(),
)
.unwrap();
let code = t.generate_current().unwrap();
assert!(verify(&secret, "alice", &code).unwrap());
assert!(!verify(&secret, "alice", "000000").unwrap());
}
}