use anyhow::{anyhow, Result};
use base64::{engine::general_purpose, Engine as _};
use data_encoding::BASE32;
use qrcode::QrCode;
use sha1::Sha1;
use totp_lite::totp_custom;
fn current_unix_timestamp() -> Result<u64> {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.map_err(|_| anyhow!("System clock is set before Unix epoch"))
}
pub fn generate_secret() -> Result<String> {
use ring::rand::{SecureRandom, SystemRandom};
let rng = SystemRandom::new();
let mut secret_bytes = [0u8; 20]; rng.fill(&mut secret_bytes)
.map_err(|_| anyhow!("Cryptographic random number generator failed"))?;
Ok(BASE32.encode(&secret_bytes))
}
pub fn generate_totp_code(secret: &str, timestamp: Option<u64>) -> Result<String> {
let secret_bytes = BASE32
.decode(secret.as_bytes())
.map_err(|e| anyhow!("Invalid base32 secret: {}", e))?;
let time = match timestamp {
Some(t) => t,
None => current_unix_timestamp()?,
};
let code = totp_custom::<Sha1>(
30, 6, &secret_bytes,
time,
);
Ok(code)
}
pub fn verify_totp_code(secret: &str, code: &str, window: Option<u64>) -> Result<bool> {
let window = window.unwrap_or(1);
let current_time = current_unix_timestamp()?;
for i in 0..=(window * 2) {
let time_offset = if i < window {
current_time.saturating_sub((window - i) * 30)
} else {
current_time + ((i - window) * 30)
};
let expected_code = generate_totp_code(secret, Some(time_offset))?;
if expected_code == code {
return Ok(true);
}
}
Ok(false)
}
pub fn generate_qr_code_data_url(secret: &str, account_name: &str, issuer: &str) -> Result<String> {
let uri = format!(
"otpauth://totp/{}:{}?secret={}&issuer={}&algorithm=SHA1&digits=6&period=30",
issuer, account_name, secret, issuer
);
let qr =
QrCode::new(uri.as_bytes()).map_err(|e| anyhow!("Failed to generate QR code: {}", e))?;
let svg = qr.render::<qrcode::render::svg::Color>().max_dimensions(200, 200).build();
Ok(format!(
"data:image/svg+xml;base64,{}",
general_purpose::STANDARD.encode(svg.as_bytes())
))
}
pub fn generate_backup_codes(count: usize) -> Result<Vec<String>> {
use ring::rand::{SecureRandom, SystemRandom};
let rng = SystemRandom::new();
let mut codes = Vec::new();
for _ in 0..count {
let mut bytes = [0u8; 4];
rng.fill(&mut bytes)
.map_err(|_| anyhow!("Cryptographic random number generator failed"))?;
let code = format!("{:08}", u32::from_be_bytes(bytes) % 100_000_000);
codes.push(code);
}
Ok(codes)
}
pub fn hash_backup_code(code: &str) -> Result<String> {
Ok(bcrypt::hash(code, bcrypt::DEFAULT_COST)?)
}
pub fn verify_backup_code(code: &str, hash: &str) -> Result<bool> {
Ok(bcrypt::verify(code, hash)?)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_totp_generation_and_verification() {
let secret = generate_secret().unwrap();
let code = generate_totp_code(&secret, None).unwrap();
assert_eq!(code.len(), 6);
assert!(code.chars().all(|c| c.is_ascii_digit()));
assert!(verify_totp_code(&secret, &code, Some(1)).unwrap());
}
#[test]
fn test_backup_code_generation() {
let codes = generate_backup_codes(10).unwrap();
assert_eq!(codes.len(), 10);
for code in &codes {
assert_eq!(code.len(), 8);
assert!(code.chars().all(|c| c.is_ascii_digit()));
}
}
#[test]
fn test_backup_code_hashing() {
let code = "12345678";
let hash = hash_backup_code(code).unwrap();
assert!(verify_backup_code(code, &hash).unwrap());
assert!(!verify_backup_code("87654321", &hash).unwrap());
}
}