Я пишу собственную реализацию TOTP на Rust, но не могу получить правильный код 2FA с заданным секретом.
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);
}
Я попробовал все. И найдите документацию RFC о TOTP и HOTP, перепишите методы и поищите в Интернете другие реализации. По этим данным у меня 832112, а должно 041374
Во-первых, я проверяю вывод одновременно с помощью библиотеки Python pyotp. Во-вторых, когда я устанавливаю время на 1000000 секунд, у меня есть «832112», а должно быть «041374».
Необходимость использования to_string возникла из-за плохой практики использования &String вместо &str в качестве типа параметра, чего практически никогда не следует делать.

Обычно ключ HMAC (секрет TOTP) задается в виде строки base32, что и происходит в данном случае. Вам нужно декодировать это в реальную строку байтов, а не использовать ее в качестве ключа во время кодирования.
Вот слегка измененная версия вашего кода, которая использует крейт HMAC и реализует код totp как чистую функцию, что позволяет нам использовать тестовые векторы из 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
);
}
}
Я также позволил себе использовать встроенные функции проверки порядка байтов, предоставляемые Rust, вместо того, чтобы реализовывать их вручную. Это легче понять, а также это может быть быстрее, поскольку во многих системах существуют отдельные инструкции для замены и преобразования байтов.
Реализация реализаций на основе SHA-256 и SHA-512, если они вам понадобятся, оставлена в качестве упражнения для читателя. Google Authenticator их не поддерживает, но Authy и некоторые другие реализации поддерживают.
Можете ли вы указать ожидаемый и фактический результат за фиксированное время вместо
SystemTime::now()? Отсутствие ничего для тестирования не очень удобно для отладки.