use crate::constants::{DEFAULT_ALGORITHM, DEFAULT_BREADTH, DEFAULT_COUNTER, DEFAULT_DIGITS};
use hmacsha::{HmacSha, ShaTypes};
const fn u64_to_8_length_u8_array(input: u64) -> [u8; 8] {
input.to_be_bytes()
}
fn make_opt(secret: &[u8], digits: u32, counter: u64, algorithm: &ShaTypes) -> String {
let counter_bytes = u64_to_8_length_u8_array(counter);
let mut hash = HmacSha::new(secret, &counter_bytes, algorithm);
let digest = hash.compute_digest();
let offset = usize::from(digest.last().unwrap() & 0xf);
let value = (u32::from(digest[offset]) & 0x7f) << 24
| (u32::from(digest[offset + 1]) & 0xff) << 16
| (u32::from(digest[offset + 2]) & 0xff) << 8
| (u32::from(digest[offset + 3]) & 0xff);
let mut code = (value % 10_u32.pow(digits)).to_string();
if code.len() != (digits as usize) {
code = "0".repeat((digits - (code.len() as u32)) as usize) + &code;
}
code
}
#[derive(Clone, Copy)]
pub enum MakeOption<'a> {
Default,
Counter(u64),
Digits(u32),
Algorithm(&'a ShaTypes),
Full {
counter: u64,
digits: u32,
algorithm: &'a ShaTypes,
},
}
#[derive(Clone, Copy)]
pub enum CheckOption<'a> {
Default,
Counter(u64),
Breadth(u64),
Full {
counter: u64,
breadth: u64,
algorithm: &'a ShaTypes,
},
Algorithm(&'a ShaTypes),
}
pub struct Hotp<'a> {
secret: &'a str,
}
impl<'a> Hotp<'a> {
pub const fn new(secret: &'a str) -> Self {
Self { secret }
}
pub fn make(&self, options: MakeOption) -> String {
match options {
MakeOption::Default => make_opt(
self.secret().as_bytes(),
DEFAULT_DIGITS,
DEFAULT_COUNTER,
DEFAULT_ALGORITHM,
),
MakeOption::Counter(counter) => make_opt(
self.secret().as_bytes(),
DEFAULT_DIGITS,
counter,
DEFAULT_ALGORITHM,
),
MakeOption::Digits(digits) => make_opt(
self.secret().as_bytes(),
digits,
DEFAULT_COUNTER,
DEFAULT_ALGORITHM,
),
MakeOption::Full {
counter,
digits,
algorithm,
} => make_opt(self.secret().as_bytes(), digits, counter, algorithm),
MakeOption::Algorithm(algorithm) => make_opt(
self.secret().as_bytes(),
DEFAULT_DIGITS,
DEFAULT_COUNTER,
algorithm,
),
}
}
pub fn check(&self, otp: &str, options: CheckOption) -> bool {
let (counter, breadth, algorithm) = match options {
CheckOption::Default => (DEFAULT_COUNTER, DEFAULT_BREADTH, DEFAULT_ALGORITHM),
CheckOption::Counter(counter) => (counter, DEFAULT_BREADTH, DEFAULT_ALGORITHM),
CheckOption::Breadth(breadth) => (DEFAULT_COUNTER, breadth, DEFAULT_ALGORITHM),
CheckOption::Full {
counter,
breadth,
algorithm,
} => (counter, breadth, algorithm),
CheckOption::Algorithm(algorithm) => (DEFAULT_COUNTER, DEFAULT_BREADTH, algorithm),
};
for i in (counter - breadth)..=(counter + breadth) {
let code = self.make(MakeOption::Full {
counter: i,
digits: otp.len() as u32,
algorithm,
});
if code == otp {
return true;
}
}
false
}
pub const fn secret(&self) -> &&'a str {
&self.secret
}
}
#[cfg(test)]
mod tests {
use hmacsha::ShaTypes;
use super::{u64_to_8_length_u8_array, CheckOption, Hotp, MakeOption};
use crate::constants::DEFAULT_ALGORITHM;
#[test]
fn make_test() {
let hotp = Hotp::new("A strong shared secret");
let code1 = hotp.make(MakeOption::Default);
let code2 = hotp.make(MakeOption::Default);
assert_eq!(code1, code2);
}
#[test]
fn make_test_sha2() {
let hotp = Hotp::new("A strong shared secret");
let code1 = hotp.make(MakeOption::Algorithm(&ShaTypes::Sha2_256));
let code2 = hotp.make(MakeOption::Algorithm(&ShaTypes::Sha2_256));
assert_eq!(code1, code2);
}
#[test]
fn make_test_correcteness() {
use hex;
let secret = "12345678901234567890";
let hotp = Hotp::new(secret);
let hex_string = hex::encode(secret);
assert_eq!(
format!("0x{}", hex_string),
"0x3132333435363738393031323334353637383930"
);
let code = hotp.make(MakeOption::Counter(0));
assert_eq!(code, "755224");
let code = hotp.make(MakeOption::Counter(1));
assert_eq!(code, "287082");
let code = hotp.make(MakeOption::Counter(2));
assert_eq!(code, "359152");
let code = hotp.make(MakeOption::Counter(3));
assert_eq!(code, "969429");
let code = hotp.make(MakeOption::Counter(4));
assert_eq!(code, "338314");
let code = hotp.make(MakeOption::Counter(5));
assert_eq!(code, "254676");
let code = hotp.make(MakeOption::Counter(6));
assert_eq!(code, "287922");
let code = hotp.make(MakeOption::Counter(7));
assert_eq!(code, "162583");
let code = hotp.make(MakeOption::Counter(8));
assert_eq!(code, "399871");
let code = hotp.make(MakeOption::Counter(9));
assert_eq!(code, "520489");
}
#[test]
fn check_test() {
let hotp = Hotp::new("A strong shared secret");
let code = hotp.make(MakeOption::Default);
let check = hotp.check(code.as_str(), CheckOption::Default);
assert!(check);
}
#[test]
fn check_test_sha2() {
let hotp = Hotp::new("A strong shared secret");
let code = hotp.make(MakeOption::Algorithm(&ShaTypes::Sha2_256));
let check = hotp.check(code.as_str(), CheckOption::Algorithm(&ShaTypes::Sha2_256));
assert!(check);
}
#[test]
fn check_test_counter() {
let hotp = Hotp::new("A strong shared secret");
let code = hotp.make(MakeOption::Counter(42));
let check = hotp.check(code.as_str(), CheckOption::Counter(42));
assert!(check);
}
#[test]
fn check_test_breadth() {
let hotp = Hotp::new("A strong shared secret");
let code = hotp.make(MakeOption::Counter(42));
let check = hotp.check(
code.as_str(),
CheckOption::Full {
counter: 42,
breadth: 6,
algorithm: DEFAULT_ALGORITHM,
},
);
assert!(check);
}
#[test]
fn check_test_counter_and_breadth() {
let hotp = Hotp::new("A strong shared secret");
let code = hotp.make(MakeOption::Counter(42));
let check = hotp.check(
code.as_str(),
CheckOption::Full {
counter: 42,
breadth: 6,
algorithm: DEFAULT_ALGORITHM,
},
);
assert!(check);
}
#[test]
fn check_u64_to_8_length_u8_array() {
let value = 1024_u64;
let result = u64_to_8_length_u8_array(value);
let expected = [00_u8, 00_u8, 00_u8, 00_u8, 00_u8, 00_u8, 4_u8, 00_u8];
assert_eq!(result, expected)
}
#[test]
fn check_max_u64_to_8_length_u8_array() {
let value = u64::MAX;
let result = u64_to_8_length_u8_array(value);
let expected = [
255_u8, 255_u8, 255_u8, 255_u8, 255_u8, 255_u8, 255_u8, 255_u8,
];
assert_eq!(result, expected)
}
}