use constant_time_eq::constant_time_eq;
use hmac::{Hmac, Mac};
use rand::RngCore;
use std::time::SystemTime;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Error {
SystemTimeError(String),
Base32DecodeError,
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match &self {
Error::SystemTimeError(e) => write!(f, "System time error: {}", e),
Error::Base32DecodeError => write!(f, "Base32 decode error"),
}
}
}
impl std::error::Error for Error {}
pub type Result<T> = std::result::Result<T, Error>;
fn system_time() -> Result<u64> {
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map(|d| d.as_secs())
.map_err(|e| Error::SystemTimeError(e.to_string()))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Algorithm {
HmacSha1,
HmacSha256,
HmacSha512,
}
impl Default for Algorithm {
fn default() -> Self {
Algorithm::HmacSha1
}
}
impl Algorithm {
fn digest<M: Mac>(mut mac: M, data: &[u8]) -> Vec<u8> {
mac.update(data);
mac.finalize().into_bytes().to_vec()
}
fn sign(&self, key: &[u8], data: &[u8]) -> Vec<u8> {
match self {
Algorithm::HmacSha1 => {
Algorithm::digest(Hmac::<sha1::Sha1>::new_from_slice(key).unwrap(), data)
}
Algorithm::HmacSha256 => {
Algorithm::digest(Hmac::<sha2::Sha256>::new_from_slice(key).unwrap(), data)
}
Algorithm::HmacSha512 => {
Algorithm::digest(Hmac::<sha2::Sha512>::new_from_slice(key).unwrap(), data)
}
}
}
}
#[derive(Debug, Clone)]
pub struct Secret {
key: Vec<u8>,
}
impl PartialEq for Secret {
fn eq(&self, other: &Self) -> bool {
constant_time_eq(&self.key, &other.key)
}
}
impl Eq for Secret {}
impl Secret {
pub fn new() -> Self {
let mut key = vec![0u8; 16];
rand::thread_rng().fill_bytes(&mut key);
Self { key }
}
pub fn from_base32(key: &str) -> Result<Self> {
let key = base32::decode(base32::Alphabet::RFC4648 { padding: false }, key);
if key.is_none() {
return Err(Error::Base32DecodeError);
}
Ok(Self { key: key.unwrap() })
}
pub fn from_raw(key: &str) -> Self {
Self {
key: key.as_bytes().to_vec(),
}
}
pub fn from_slice(key: &[u8]) -> Self {
Self { key: key.to_vec() }
}
pub fn raw(&self) -> String {
String::from_utf8(self.key.clone()).unwrap()
}
pub fn base32(&self) -> String {
base32::encode(base32::Alphabet::RFC4648 { padding: false }, &self.key)
}
pub fn as_slice(&self) -> &[u8] {
&self.key
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TOTP {
algorithm: Algorithm,
digits: usize,
window: u64,
time_step: u64,
key: Secret,
}
impl TOTP {
pub fn new(
algorithm: Algorithm,
digits: usize,
window: u64,
time_step: u64,
key: Secret,
) -> Self {
Self::new_unchecked(algorithm, digits, window, time_step, key)
}
pub fn new_unchecked(
algorithm: Algorithm,
digits: usize,
window: u64,
time_step: u64,
key: Secret,
) -> Self {
Self {
algorithm,
digits,
window,
time_step,
key,
}
}
pub fn generate(&self, time: u64) -> String {
let hash = self.algorithm.sign(
self.key.as_slice(),
(time / self.time_step).to_be_bytes().as_ref(),
);
let offset = (hash.last().unwrap() & 0xf) as usize;
let result = u32::from_be_bytes(hash[offset..offset + 4].try_into().unwrap()) & 0x7fffffff;
format!(
"{1:00$}",
self.digits,
result % 10_u32.pow(self.digits as u32)
)
}
pub fn generate_now(&self) -> Result<String> {
let time = system_time()?;
Ok(self.generate(time))
}
pub fn next_time_step(&self, time: u64) -> u64 {
let time_step = time / self.time_step;
(time_step + 1) * self.time_step
}
pub fn ttl(&self) -> Result<u64> {
let time = system_time()?;
let remain = self.time_step - (time % self.time_step);
Ok(remain)
}
pub fn verify(&self, token: &str) -> Result<bool> {
let time = system_time()?;
Ok(self.verify_with_time(token, time))
}
pub fn verify_with_time(&self, token: &str, time: u64) -> bool {
let step = time / self.time_step - self.window;
for i in 0..self.window * 2 + 1 {
let t = (step + i) * self.time_step;
let code = self.generate(t);
if constant_time_eq(token.as_bytes(), code.as_bytes()) {
return true;
}
}
false
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_totp() {
let secret =
Secret::from_base32("KLOKQRZDX46UDXHSOUUM32PKUPPKGI7LM4U3G4GEDVUTSNXLTLNA").unwrap();
let totp = TOTP::new(Algorithm::HmacSha1, 6, 1, 30, secret);
let code = totp.generate_now().unwrap();
assert_eq!(code.len(), 6);
assert!(totp.verify(&code).unwrap());
}
}