use crate::primitives::hash::{sha1_hmac, sha256_hmac, sha512_hmac};
use std::time::{SystemTime, UNIX_EPOCH};
use subtle::ConstantTimeEq;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Algorithm {
#[default]
Sha1,
Sha256,
Sha512,
}
#[derive(Debug, Clone)]
pub struct TotpOptions {
pub digits: u32,
pub algorithm: Algorithm,
pub period: u64,
pub timestamp: Option<u64>,
}
impl Default for TotpOptions {
fn default() -> Self {
Self {
digits: 6,
algorithm: Algorithm::Sha1,
period: 30,
timestamp: None,
}
}
}
#[derive(Debug, Clone)]
pub struct TotpValidateOptions {
pub options: TotpOptions,
pub skew: u32,
}
impl Default for TotpValidateOptions {
fn default() -> Self {
Self {
options: TotpOptions::default(),
skew: 1,
}
}
}
pub struct Totp;
impl Totp {
pub fn generate(secret: &[u8], options: Option<TotpOptions>) -> String {
let options = options.unwrap_or_default();
let timestamp = options.timestamp.unwrap_or_else(current_unix_seconds);
let counter = timestamp / options.period;
generate_hotp(secret, counter, &options)
}
pub fn validate(secret: &[u8], passcode: &str, options: Option<TotpValidateOptions>) -> bool {
let options = options.unwrap_or_default();
let passcode = passcode.trim();
if passcode.len() != options.options.digits as usize {
return false;
}
let timestamp = options
.options
.timestamp
.unwrap_or_else(current_unix_seconds);
let counter = timestamp / options.options.period;
let mut counters_to_check = Vec::with_capacity(1 + 2 * options.skew as usize);
counters_to_check.push(counter);
for i in 1..=options.skew as u64 {
counters_to_check.push(counter.wrapping_add(i));
if counter >= i {
counters_to_check.push(counter - i);
}
}
for check_counter in counters_to_check {
let expected = generate_hotp(secret, check_counter, &options.options);
if constant_time_eq(passcode, &expected) {
return true;
}
}
false
}
}
fn generate_hotp(secret: &[u8], counter: u64, options: &TotpOptions) -> String {
let counter_bytes = counter.to_be_bytes();
let hmac_result: Vec<u8> = match options.algorithm {
Algorithm::Sha1 => sha1_hmac(secret, &counter_bytes).to_vec(),
Algorithm::Sha256 => sha256_hmac(secret, &counter_bytes).to_vec(),
Algorithm::Sha512 => sha512_hmac(secret, &counter_bytes).to_vec(),
};
let offset = (hmac_result[hmac_result.len() - 1] & 0x0f) as usize;
let code_bytes = [
hmac_result[offset] & 0x7f, hmac_result[offset + 1],
hmac_result[offset + 2],
hmac_result[offset + 3],
];
let code = u32::from_be_bytes(code_bytes);
let divisor = 10u32.pow(options.digits);
let truncated = code % divisor;
format!("{:0>width$}", truncated, width = options.digits as usize)
}
fn current_unix_seconds() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("System time is before Unix epoch")
.as_secs()
}
fn constant_time_eq(a: &str, b: &str) -> bool {
if a.len() != b.len() {
return false;
}
a.as_bytes().ct_eq(b.as_bytes()).into()
}
#[cfg(test)]
mod tests {
use super::*;
const SHA1_SECRET: &[u8] = b"12345678901234567890";
const SHA256_SECRET: &[u8] = b"12345678901234567890123456789012";
const SHA512_SECRET: &[u8] =
b"1234567890123456789012345678901234567890123456789012345678901234";
#[test]
fn test_rfc6238_sha1_time_59() {
let options = TotpOptions {
digits: 8,
algorithm: Algorithm::Sha1,
timestamp: Some(59),
..Default::default()
};
let code = Totp::generate(SHA1_SECRET, Some(options));
assert_eq!(code, "94287082");
}
#[test]
fn test_rfc6238_sha256_time_59() {
let options = TotpOptions {
digits: 8,
algorithm: Algorithm::Sha256,
timestamp: Some(59),
..Default::default()
};
let code = Totp::generate(SHA256_SECRET, Some(options));
assert_eq!(code, "46119246");
}
#[test]
fn test_rfc6238_sha512_time_59() {
let options = TotpOptions {
digits: 8,
algorithm: Algorithm::Sha512,
timestamp: Some(59),
..Default::default()
};
let code = Totp::generate(SHA512_SECRET, Some(options));
assert_eq!(code, "90693936");
}
#[test]
fn test_rfc6238_sha1_time_1111111109() {
let options = TotpOptions {
digits: 8,
algorithm: Algorithm::Sha1,
timestamp: Some(1111111109),
..Default::default()
};
let code = Totp::generate(SHA1_SECRET, Some(options));
assert_eq!(code, "07081804");
}
#[test]
fn test_rfc6238_sha256_time_1111111109() {
let options = TotpOptions {
digits: 8,
algorithm: Algorithm::Sha256,
timestamp: Some(1111111109),
..Default::default()
};
let code = Totp::generate(SHA256_SECRET, Some(options));
assert_eq!(code, "68084774");
}
#[test]
fn test_rfc6238_sha512_time_1111111109() {
let options = TotpOptions {
digits: 8,
algorithm: Algorithm::Sha512,
timestamp: Some(1111111109),
..Default::default()
};
let code = Totp::generate(SHA512_SECRET, Some(options));
assert_eq!(code, "25091201");
}
#[test]
fn test_rfc6238_sha1_time_1111111111() {
let options = TotpOptions {
digits: 8,
algorithm: Algorithm::Sha1,
timestamp: Some(1111111111),
..Default::default()
};
let code = Totp::generate(SHA1_SECRET, Some(options));
assert_eq!(code, "14050471");
}
#[test]
fn test_rfc6238_sha256_time_1111111111() {
let options = TotpOptions {
digits: 8,
algorithm: Algorithm::Sha256,
timestamp: Some(1111111111),
..Default::default()
};
let code = Totp::generate(SHA256_SECRET, Some(options));
assert_eq!(code, "67062674");
}
#[test]
fn test_rfc6238_sha512_time_1111111111() {
let options = TotpOptions {
digits: 8,
algorithm: Algorithm::Sha512,
timestamp: Some(1111111111),
..Default::default()
};
let code = Totp::generate(SHA512_SECRET, Some(options));
assert_eq!(code, "99943326");
}
#[test]
fn test_rfc6238_sha1_time_1234567890() {
let options = TotpOptions {
digits: 8,
algorithm: Algorithm::Sha1,
timestamp: Some(1234567890),
..Default::default()
};
let code = Totp::generate(SHA1_SECRET, Some(options));
assert_eq!(code, "89005924");
}
#[test]
fn test_rfc6238_sha256_time_1234567890() {
let options = TotpOptions {
digits: 8,
algorithm: Algorithm::Sha256,
timestamp: Some(1234567890),
..Default::default()
};
let code = Totp::generate(SHA256_SECRET, Some(options));
assert_eq!(code, "91819424");
}
#[test]
fn test_rfc6238_sha512_time_1234567890() {
let options = TotpOptions {
digits: 8,
algorithm: Algorithm::Sha512,
timestamp: Some(1234567890),
..Default::default()
};
let code = Totp::generate(SHA512_SECRET, Some(options));
assert_eq!(code, "93441116");
}
#[test]
fn test_rfc6238_sha1_time_2000000000() {
let options = TotpOptions {
digits: 8,
algorithm: Algorithm::Sha1,
timestamp: Some(2000000000),
..Default::default()
};
let code = Totp::generate(SHA1_SECRET, Some(options));
assert_eq!(code, "69279037");
}
#[test]
fn test_rfc6238_sha256_time_2000000000() {
let options = TotpOptions {
digits: 8,
algorithm: Algorithm::Sha256,
timestamp: Some(2000000000),
..Default::default()
};
let code = Totp::generate(SHA256_SECRET, Some(options));
assert_eq!(code, "90698825");
}
#[test]
fn test_rfc6238_sha512_time_2000000000() {
let options = TotpOptions {
digits: 8,
algorithm: Algorithm::Sha512,
timestamp: Some(2000000000),
..Default::default()
};
let code = Totp::generate(SHA512_SECRET, Some(options));
assert_eq!(code, "38618901");
}
#[test]
fn test_rfc6238_sha1_time_20000000000() {
let options = TotpOptions {
digits: 8,
algorithm: Algorithm::Sha1,
timestamp: Some(20000000000),
..Default::default()
};
let code = Totp::generate(SHA1_SECRET, Some(options));
assert_eq!(code, "65353130");
}
#[test]
fn test_rfc6238_sha256_time_20000000000() {
let options = TotpOptions {
digits: 8,
algorithm: Algorithm::Sha256,
timestamp: Some(20000000000),
..Default::default()
};
let code = Totp::generate(SHA256_SECRET, Some(options));
assert_eq!(code, "77737706");
}
#[test]
fn test_rfc6238_sha512_time_20000000000() {
let options = TotpOptions {
digits: 8,
algorithm: Algorithm::Sha512,
timestamp: Some(20000000000),
..Default::default()
};
let code = Totp::generate(SHA512_SECRET, Some(options));
assert_eq!(code, "47863826");
}
#[test]
fn test_6_digit_sha1_time_59() {
let options = TotpOptions {
digits: 6,
algorithm: Algorithm::Sha1,
timestamp: Some(59),
..Default::default()
};
let code = Totp::generate(SHA1_SECRET, Some(options));
assert_eq!(code, "287082");
}
#[test]
fn test_6_digit_sha256_time_59() {
let options = TotpOptions {
digits: 6,
algorithm: Algorithm::Sha256,
timestamp: Some(59),
..Default::default()
};
let code = Totp::generate(SHA256_SECRET, Some(options));
assert_eq!(code, "119246");
}
#[test]
fn test_6_digit_sha512_time_59() {
let options = TotpOptions {
digits: 6,
algorithm: Algorithm::Sha512,
timestamp: Some(59),
..Default::default()
};
let code = Totp::generate(SHA512_SECRET, Some(options));
assert_eq!(code, "693936");
}
#[test]
fn test_validate_current_code() {
let options = TotpOptions {
digits: 6,
timestamp: Some(59),
..Default::default()
};
let code = Totp::generate(SHA1_SECRET, Some(options.clone()));
let validate_options = TotpValidateOptions { options, skew: 1 };
assert!(Totp::validate(SHA1_SECRET, &code, Some(validate_options)));
}
#[test]
fn test_validate_rejects_invalid_code() {
let options = TotpOptions {
digits: 6,
timestamp: Some(59),
..Default::default()
};
let validate_options = TotpValidateOptions { options, skew: 1 };
assert!(!Totp::validate(
SHA1_SECRET,
"000000",
Some(validate_options)
));
}
#[test]
fn test_validate_with_skew_previous_period() {
let gen_options = TotpOptions {
digits: 6,
timestamp: Some(59),
..Default::default()
};
let code = Totp::generate(SHA1_SECRET, Some(gen_options));
let validate_options = TotpValidateOptions {
options: TotpOptions {
digits: 6,
timestamp: Some(89),
..Default::default()
},
skew: 1,
};
assert!(Totp::validate(SHA1_SECRET, &code, Some(validate_options)));
}
#[test]
fn test_validate_with_skew_next_period() {
let gen_options = TotpOptions {
digits: 6,
timestamp: Some(59),
..Default::default()
};
let code = Totp::generate(SHA1_SECRET, Some(gen_options));
let validate_options = TotpValidateOptions {
options: TotpOptions {
digits: 6,
timestamp: Some(29),
..Default::default()
},
skew: 1,
};
assert!(Totp::validate(SHA1_SECRET, &code, Some(validate_options)));
}
#[test]
fn test_validate_rejects_outside_skew() {
let gen_options = TotpOptions {
digits: 6,
timestamp: Some(59),
..Default::default()
};
let code = Totp::generate(SHA1_SECRET, Some(gen_options));
let validate_options = TotpValidateOptions {
options: TotpOptions {
digits: 6,
timestamp: Some(119),
..Default::default()
},
skew: 1,
};
assert!(!Totp::validate(SHA1_SECRET, &code, Some(validate_options)));
}
#[test]
fn test_validate_rejects_wrong_length() {
let validate_options = TotpValidateOptions {
options: TotpOptions {
digits: 6,
timestamp: Some(59),
..Default::default()
},
skew: 1,
};
assert!(!Totp::validate(
SHA1_SECRET,
"12345",
Some(validate_options.clone())
));
assert!(!Totp::validate(
SHA1_SECRET,
"1234567",
Some(validate_options)
));
}
#[test]
fn test_validate_trims_whitespace() {
let options = TotpOptions {
digits: 6,
timestamp: Some(59),
..Default::default()
};
let code = Totp::generate(SHA1_SECRET, Some(options.clone()));
let validate_options = TotpValidateOptions { options, skew: 1 };
let padded_code = format!(" {} ", code);
assert!(Totp::validate(
SHA1_SECRET,
&padded_code,
Some(validate_options)
));
}
#[test]
fn test_60_second_period() {
let options_30 = TotpOptions {
digits: 6,
timestamp: Some(59),
period: 30,
..Default::default()
};
let options_60 = TotpOptions {
digits: 6,
timestamp: Some(59),
period: 60,
..Default::default()
};
let code_30 = Totp::generate(SHA1_SECRET, Some(options_30));
let code_60 = Totp::generate(SHA1_SECRET, Some(options_60));
assert_ne!(code_30, code_60);
}
#[test]
fn test_ts_sdk_compat_unix_epoch() {
let secret = hex::decode("48656c6c6f21deadbeef").unwrap();
let options = TotpOptions {
digits: 6,
algorithm: Algorithm::Sha1,
period: 30,
timestamp: Some(0),
};
let code = Totp::generate(&secret, Some(options));
assert_eq!(code, "282760");
}
#[test]
fn test_ts_sdk_compat_2016_timestamp() {
let secret = hex::decode("48656c6c6f21deadbeef").unwrap();
let options = TotpOptions {
digits: 6,
algorithm: Algorithm::Sha1,
period: 30,
timestamp: Some(1465324707),
};
let code = Totp::generate(&secret, Some(options));
assert_eq!(code, "341128");
}
#[test]
fn test_ts_sdk_compat_leading_zero() {
let secret = hex::decode("48656c6c6f21deadbeef").unwrap();
let options = TotpOptions {
digits: 6,
algorithm: Algorithm::Sha1,
period: 30,
timestamp: Some(1365324707),
};
let code = Totp::generate(&secret, Some(options));
assert_eq!(code, "089029");
}
#[test]
fn test_ts_sdk_compat_cycle_boundary_start() {
let secret = hex::decode("48656c6c6f21deadbeef").unwrap();
let options = TotpOptions {
digits: 6,
algorithm: Algorithm::Sha1,
period: 30,
timestamp: Some(1665644340),
};
let code = Totp::generate(&secret, Some(options));
assert_eq!(code, "886842");
}
#[test]
fn test_ts_sdk_compat_cycle_boundary_end() {
let secret = hex::decode("48656c6c6f21deadbeef").unwrap();
let options = TotpOptions {
digits: 6,
algorithm: Algorithm::Sha1,
period: 30,
timestamp: Some(1665644339),
};
let code = Totp::generate(&secret, Some(options));
assert_eq!(code, "134996");
}
#[test]
fn test_default_options() {
let code = Totp::generate(SHA1_SECRET, None);
assert_eq!(code.len(), 6);
}
#[test]
fn test_validate_with_none_options() {
let code = Totp::generate(SHA1_SECRET, None);
assert!(Totp::validate(SHA1_SECRET, &code, None));
}
#[test]
fn test_constant_time_eq() {
assert!(constant_time_eq("123456", "123456"));
assert!(!constant_time_eq("123456", "654321"));
assert!(!constant_time_eq("123", "123456"));
assert!(!constant_time_eq("123456", "123"));
}
#[test]
fn test_constant_time_eq_empty() {
assert!(constant_time_eq("", ""));
}
#[test]
fn test_counter_near_zero() {
let options = TotpOptions {
digits: 6,
timestamp: Some(0), ..Default::default()
};
let code = Totp::generate(SHA1_SECRET, Some(options.clone()));
let validate_options = TotpValidateOptions { options, skew: 1 };
assert!(Totp::validate(SHA1_SECRET, &code, Some(validate_options)));
}
#[test]
fn test_algorithm_enum_default() {
let algo: Algorithm = Default::default();
assert_eq!(algo, Algorithm::Sha1);
}
#[test]
fn test_algorithm_enum_clone_eq() {
let algo1 = Algorithm::Sha256;
let algo2 = algo1;
assert_eq!(algo1, algo2);
}
#[test]
fn test_totp_options_clone() {
let options = TotpOptions {
digits: 8,
algorithm: Algorithm::Sha512,
period: 60,
timestamp: Some(12345),
};
let cloned = options.clone();
assert_eq!(cloned.digits, 8);
assert_eq!(cloned.algorithm, Algorithm::Sha512);
assert_eq!(cloned.period, 60);
assert_eq!(cloned.timestamp, Some(12345));
}
#[test]
fn test_digits_1() {
let options = TotpOptions {
digits: 1,
timestamp: Some(59),
..Default::default()
};
let code = Totp::generate(SHA1_SECRET, Some(options.clone()));
assert_eq!(code.len(), 1);
let validate_options = TotpValidateOptions { options, skew: 0 };
assert!(Totp::validate(SHA1_SECRET, &code, Some(validate_options)));
}
#[test]
fn test_digits_9() {
let options = TotpOptions {
digits: 9,
timestamp: Some(59),
..Default::default()
};
let code = Totp::generate(SHA1_SECRET, Some(options.clone()));
assert_eq!(code.len(), 9);
let validate_options = TotpValidateOptions { options, skew: 0 };
assert!(Totp::validate(SHA1_SECRET, &code, Some(validate_options)));
}
#[test]
fn test_rfc4226_hotp_vectors() {
let expected = [
"755224", "287082", "359152", "969429", "338314", "254676", "287922", "162583", "399871", "520489", ];
for (counter, expected_code) in expected.iter().enumerate() {
let options = TotpOptions {
digits: 6,
algorithm: Algorithm::Sha1,
period: 1, timestamp: Some(counter as u64), };
let code = Totp::generate(SHA1_SECRET, Some(options));
assert_eq!(
&code, expected_code,
"RFC 4226 HOTP vector failed for counter {}",
counter
);
}
}
}