TOTP Generator with HMAC-SHA1

Derive RFC 6238 time-based one-time codes from a Base32 secret, with counter packing, dynamic truncation, and modulo digits.

RustSecurity Boundaries

Code

snippet
use hmac::{Hmac, Mac};
use sha1::Sha1;

type HmacSha1 = Hmac<Sha1>;

const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";

fn base32_decode(input: &str) -> Option<Vec<u8>> {
    let mut buffer: u32 = 0;
    let mut bits: u8 = 0;
    let mut out = Vec::new();
    for ch in input.bytes().filter(|byte| *byte != b'=') {
        let value = ALPHABET
            .iter()
            .position(|candidate| *candidate == ch.to_ascii_uppercase())? as u32;
        buffer = (buffer << 5) | value;
        bits += 5;
        if bits >= 8 {
            bits -= 8;
            out.push((buffer >> bits) as u8);
            buffer &= (1 << bits) - 1;
        }
    }
    Some(out)
}

fn hotp(secret: &[u8], counter: u64, digits: u32) -> u32 {
    let mut mac = HmacSha1::new_from_slice(secret).expect("HMAC accepts any key length");
    mac.update(&counter.to_be_bytes());
    let digest = mac.finalize().into_bytes();
    let offset = (digest[19] & 0x0f) as usize;
    let code = u32::from_be_bytes([
        digest[offset],
        digest[offset + 1],
        digest[offset + 2],
        digest[offset + 3],
    ]) & 0x7fff_ffff;
    code % 10u32.pow(digits)
}

fn totp(secret: &[u8], unix_time: u64, step: u64, digits: u32) -> u32 {
    hotp(secret, unix_time / step, digits)
}

fn main() {
    let secret = base32_decode("JBSWY3DPEHPK3PXP").expect("valid base32");
    let code = totp(&secret, 1_700_000_000, 30, 6);
    println!("code: {:06}", code);
}