fn sha1(data: &[u8]) -> [u8; 20] {
let mut h0: u32 = 0x6745_2301;
let mut h1: u32 = 0xEFCD_AB89;
let mut h2: u32 = 0x98BA_DCFE;
let mut h3: u32 = 0x1032_5476;
let mut h4: u32 = 0xC3D2_E1F0;
let bit_len = (data.len() as u64) * 8;
let pad_len = 64 - ((data.len() + 9) % 64);
let total_len = data.len() + 1 + pad_len + 8;
let mut padded = vec![0u8; total_len];
padded[..data.len()].copy_from_slice(data);
padded[data.len()] = 0x80;
padded[total_len - 8..].copy_from_slice(&bit_len.to_be_bytes());
for chunk in padded.chunks_exact(64) {
let mut w = [0u32; 80];
for i in 0..16 {
w[i] = u32::from_be_bytes([
chunk[i * 4],
chunk[i * 4 + 1],
chunk[i * 4 + 2],
chunk[i * 4 + 3],
]);
}
for i in 16..80 {
w[i] = (w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16]).rotate_left(1);
}
let (mut a, mut b, mut c, mut d, mut e) = (h0, h1, h2, h3, h4);
for i in 0..80u32 {
let (f, k) = match i {
0..=19 => ((b & c) | ((!b) & d), 0x5A82_7999u32),
20..=39 => (b ^ c ^ d, 0x6ED9_EBA1u32),
40..=59 => ((b & c) | (b & d) | (c & d), 0x8F1B_BCDCu32),
_ => (b ^ c ^ d, 0xCA62_C1D6u32),
};
let temp = a
.rotate_left(5)
.wrapping_add(f)
.wrapping_add(e)
.wrapping_add(k)
.wrapping_add(w[i as usize]);
e = d;
d = c;
c = b.rotate_left(30);
b = a;
a = temp;
}
h0 = h0.wrapping_add(a);
h1 = h1.wrapping_add(b);
h2 = h2.wrapping_add(c);
h3 = h3.wrapping_add(d);
h4 = h4.wrapping_add(e);
}
let mut out = [0u8; 20];
out[0..4].copy_from_slice(&h0.to_be_bytes());
out[4..8].copy_from_slice(&h1.to_be_bytes());
out[8..12].copy_from_slice(&h2.to_be_bytes());
out[12..16].copy_from_slice(&h3.to_be_bytes());
out[16..20].copy_from_slice(&h4.to_be_bytes());
out
}
fn hmac_sha1(key: &[u8], message: &[u8]) -> [u8; 20] {
const BLOCK_SIZE: usize = 64;
let mut k = [0u8; BLOCK_SIZE];
if key.len() > BLOCK_SIZE {
let h = sha1(key);
k[..20].copy_from_slice(&h);
} else {
k[..key.len()].copy_from_slice(key);
}
let mut ipad = [0x36u8; BLOCK_SIZE];
let mut opad = [0x5Cu8; BLOCK_SIZE];
for i in 0..BLOCK_SIZE {
ipad[i] ^= k[i];
opad[i] ^= k[i];
}
let mut inner_data = Vec::with_capacity(BLOCK_SIZE + message.len());
inner_data.extend_from_slice(&ipad);
inner_data.extend_from_slice(message);
let inner = sha1(&inner_data);
let mut outer_data = [0u8; BLOCK_SIZE + 20];
outer_data[..BLOCK_SIZE].copy_from_slice(&opad);
outer_data[BLOCK_SIZE..].copy_from_slice(&inner);
sha1(&outer_data)
}
#[derive(Debug, Clone, Copy)]
pub struct TotpConfig {
pub digits: u32,
pub period_secs: u64,
pub skew: u32,
}
impl Default for TotpConfig {
fn default() -> Self {
Self {
digits: 6,
period_secs: 30,
skew: 1,
}
}
}
#[must_use]
pub fn generate_hotp(secret: &[u8], counter: u64) -> u32 {
generate_hotp_digits(secret, counter, 6)
}
#[must_use]
pub fn generate_hotp_digits(secret: &[u8], counter: u64, digits: u32) -> u32 {
let mac = hmac_sha1(secret, &counter.to_be_bytes());
let offset = (mac[19] & 0x0F) as usize;
let code = u32::from_be_bytes([
mac[offset] & 0x7F,
mac[offset + 1],
mac[offset + 2],
mac[offset + 3],
]);
code % 10u32.pow(digits)
}
#[must_use]
pub fn generate_totp(secret: &[u8], timestamp: u64, config: &TotpConfig) -> u32 {
let counter = timestamp / config.period_secs;
generate_hotp_digits(secret, counter, config.digits)
}
#[must_use]
pub fn verify_totp(secret: &[u8], timestamp: u64, config: &TotpConfig, code: u32) -> bool {
let counter = timestamp / config.period_secs;
let start = counter.saturating_sub(u64::from(config.skew));
let end = counter + u64::from(config.skew);
for c in start..=end {
if generate_hotp_digits(secret, c, config.digits) == code {
return true;
}
}
false
}
#[must_use]
pub fn verify_hotp(secret: &[u8], counter: u64, code: u32) -> bool {
generate_hotp(secret, counter) == code
}
#[cfg(test)]
mod tests {
use super::*;
const RFC_SECRET: &[u8] = b"12345678901234567890";
#[test]
fn sha1_empty() {
let h = sha1(b"");
let expected = [
0xda, 0x39, 0xa3, 0xee, 0x5e, 0x6b, 0x4b, 0x0d, 0x32, 0x55, 0xbf, 0xef, 0x95, 0x60,
0x18, 0x90, 0xaf, 0xd8, 0x07, 0x09,
];
assert_eq!(h, expected);
}
#[test]
fn sha1_abc() {
let h = sha1(b"abc");
let expected = [
0xa9, 0x99, 0x3e, 0x36, 0x47, 0x06, 0x81, 0x6a, 0xba, 0x3e, 0x25, 0x71, 0x78, 0x50,
0xc2, 0x6c, 0x9c, 0xd0, 0xd8, 0x9d,
];
assert_eq!(h, expected);
}
#[test]
fn hotp_rfc4226_test_vectors() {
let expected = [
755_224, 287_082, 359_152, 969_429, 338_314, 254_676, 287_922, 162_583, 399_871,
520_489,
];
for (counter, &exp) in expected.iter().enumerate() {
let code = generate_hotp(RFC_SECRET, counter as u64);
assert_eq!(code, exp, "HOTP mismatch at counter {counter}");
}
}
#[test]
fn totp_rfc6238_test_vector_sha1() {
let config = TotpConfig {
digits: 6,
period_secs: 30,
skew: 0,
};
let code = generate_totp(RFC_SECRET, 59, &config);
assert_eq!(code, 287_082);
}
#[test]
fn totp_verify_exact() {
let config = TotpConfig {
digits: 6,
period_secs: 30,
skew: 0,
};
let code = generate_totp(RFC_SECRET, 90, &config); assert!(verify_totp(RFC_SECRET, 90, &config, code));
}
#[test]
fn totp_verify_with_skew() {
let config = TotpConfig {
digits: 6,
period_secs: 30,
skew: 1,
};
let code = generate_totp(RFC_SECRET, 90, &config);
assert!(verify_totp(RFC_SECRET, 120, &config, code));
}
#[test]
fn totp_reject_wrong_code() {
let config = TotpConfig::default();
assert!(!verify_totp(RFC_SECRET, 90, &config, 999_999));
}
#[test]
fn totp_reject_out_of_skew() {
let config = TotpConfig {
digits: 6,
period_secs: 30,
skew: 0,
};
let code = generate_totp(RFC_SECRET, 0, &config); assert!(!verify_totp(RFC_SECRET, 60, &config, code));
}
#[test]
fn totp_8_digits() {
let config = TotpConfig {
digits: 8,
period_secs: 30,
skew: 0,
};
let code = generate_totp(RFC_SECRET, 59, &config);
assert!(code < 100_000_000);
assert!(verify_totp(RFC_SECRET, 59, &config, code));
}
#[test]
fn hotp_verify() {
let code = generate_hotp(RFC_SECRET, 0);
assert!(verify_hotp(RFC_SECRET, 0, code));
assert!(!verify_hotp(RFC_SECRET, 0, code + 1));
}
#[test]
fn totp_config_default() {
let cfg = TotpConfig::default();
assert_eq!(cfg.digits, 6);
assert_eq!(cfg.period_secs, 30);
assert_eq!(cfg.skew, 1);
}
#[test]
fn hmac_sha1_basic() {
let key = b"Jefe";
let data = b"what do ya want for nothing?";
let mac = hmac_sha1(key, data);
let expected = [
0xef, 0xfc, 0xdf, 0x6a, 0xe5, 0xeb, 0x2f, 0xa2, 0xd2, 0x74, 0x16, 0xd5, 0xf1, 0x84,
0xdf, 0x9c, 0x25, 0x9a, 0x7c, 0x79,
];
assert_eq!(mac, expected);
}
#[test]
fn hotp_different_counters_differ() {
let c0 = generate_hotp(RFC_SECRET, 0);
let c1 = generate_hotp(RFC_SECRET, 1);
assert_ne!(c0, c1);
}
#[test]
fn hotp_deterministic() {
let a = generate_hotp(RFC_SECRET, 42);
let b = generate_hotp(RFC_SECRET, 42);
assert_eq!(a, b);
}
#[test]
fn totp_code_range() {
let config = TotpConfig::default();
for t in (0..300).step_by(30) {
let code = generate_totp(RFC_SECRET, t, &config);
assert!(code < 1_000_000, "6-digit code should be < 1000000");
}
}
}