securityrusthashtotp

Implementation of TOTP on Rust


I'm writing own implementation of TOTP on Rust, but I can`t get correct 2FA code with given secret.

use std::{process::exit, time::{SystemTime, UNIX_EPOCH}};

use sha1::{Sha1, Digest};

fn hmac(key: Vec<u8>, text: Vec<u8>) -> Vec<u8>{
    let mut key = key;
    const SIZE: usize = 64;
    if key.len() > SIZE {
        key = Sha1::digest(&key).to_vec();
    }
    if key.len() < SIZE {
        for _ in 0..(SIZE-key.len()){
            key.push(0);    
        }
    }
    let ipad = 0x36;
    let opad = 0x5C;
    let mut ikeypad: Vec<u8> = key.iter().map(|val| {val^ipad}).collect();
    let mut okeypad: Vec<u8> = key.iter().map(|val| {val^opad}).collect();
    ikeypad.extend(&text);
    let inner_hash: Vec<u8> = Sha1::digest(&ikeypad).to_vec();
    okeypad.extend(&inner_hash);
    let outer_hash: Vec<u8> = Sha1::digest(okeypad).to_vec();
    return outer_hash;
}

fn totp(key: &String, size: u8, x: u16) {
let time = 1000000;
//SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
    let t = time / (x as u64);
    let mut time_vec: Vec<u8> = Vec::new();
    for i in (0..8).rev(){
        time_vec.push(((t & (0xFF << i*8)) >> i*8) as u8)
    }
    let hmac_vec = hmac(key.as_bytes().to_vec(), time_vec);
    let offset: u8 = hmac_vec[19] & 0xf;
    let hmac_truncated: Vec<u8> = hmac_vec[(offset as usize)..=((offset+3) as usize)].to_vec();
    let code_num = ((hmac_truncated[0] as u64 & 0x7F) << 24) | ((hmac_truncated[1] as u64 & 0xFF) << 16) | ((hmac_truncated[2] as u64 & 0xFF) << 8) | ((hmac_truncated[3] as u64 & 0xFF));
    dbg!(code_num % 10_u64.pow(size as u32));
}
fn main() {
    totp(&"JBSWY3DPEHPK3PXP".to_string(), 6, 30);
    
}

I tried everything. And discover RFC docs about TOTP and HOTP, and rewrite methods, and look through the internet for other implementations. With this data I have 832112, while should 041374


Solution

  • Typically, the HMAC key (the TOTP secret) is given as a base32 string, which appears to be the case here. You need to decode that into an actual byte string instead of using it as the key while it's encoded.

    Here's a slightly modified version of your code which uses the HMAC crate and implements the totp code as a pure function, which allow us to use the test vectors from the RFC:

    use hmac::Mac;
    use sha1::Sha1;
    
    fn totp(key: &str, time: u64, size: u32, x: u16) -> u64 {
        let key = base32::decode(base32::Alphabet::RFC4648 { padding: false }, key).unwrap();
        let t = time / (x as u64);
        let timebuf = t.to_be_bytes();
        let mut h = hmac::Hmac::<Sha1>::new_from_slice(&key).unwrap();
        h.update(&timebuf);
        let hmac_vec = h.finalize().into_bytes();
        let offset: u8 = hmac_vec[19] & 0xf;
        let hmac_truncated: Vec<u8> = hmac_vec[(offset as usize)..=((offset + 3) as usize)].to_vec();
        let code_num = u32::from_be_bytes(hmac_truncated.try_into().unwrap()) & 0x7fffffff;
        (code_num as u64) % 10_u64.pow(size)
    }
    fn main() {
        let res = totp("JBSWY3DPEHPK3PXP", 1000000, 6, 30);
        dbg!(res);
    }
    
    #[cfg(test)]
    mod tests {
        use super::totp;
    
        #[test]
        fn known_values() {
            assert_eq!(
                totp("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ", 59, 8, 30),
                94287082
            );
            assert_eq!(
                totp("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ", 1111111109, 8, 30),
                07081804
            );
        }
    }
    

    I've also taken the liberty of using the native endianness functions that Rust provides instead of implementing them by hand. This is easier to understand, and it can also be faster because on many systems, there are single instructions for byte swaps and conversions.

    Implementation for SHA-256- and SHA-512-based implementations, should you want them, is left as an exercise for the reader. Google Authenticator does not support them, but Authy and some other implementations do.