use crate::Array;
#[cfg(not(feature = "only-gauth"))]
use crate::*;
#[cfg(feature = "alloc")]
use alloc::{
string::String,
vec::Vec,
};
use ring::hmac::{
sign,
Key,
};
#[cfg(feature = "only-gauth")]
use ring::hmac::HMAC_SHA1_FOR_LEGACY_USE_ONLY;
#[derive(Debug, Clone)]
pub struct HOTP<A>
where
A: Array,
{
pub(crate) sec: A,
#[cfg(not(feature = "only-gauth"))]
pub(crate) len: u8,
#[cfg(not(feature = "only-gauth"))]
pub(crate) alg: Algorithm,
}
#[cfg(test)]
mod test {
use super::{
HOTP,
TRUNCATE_TO,
};
const DATA: &[u8] = b"12345678901234567890";
const OUTPUT: [(u32, u32); 10] = [
(0x4c93cf18, 755224),
(0x41397eea, 287082),
(0x082fef30, 359152),
(0x66ef7655, 969429),
(0x61c5938a, 338314),
(0x33c083d4, 254676),
(0x7256c032, 287922),
(0x04e5b397, 162583),
(0x2823443f, 399871),
(0x2679dc69, 520489),
];
#[test]
fn counting_works() {
let hotp = HOTP::new(DATA);
let z = (0u64..10).into_iter().zip(OUTPUT.iter());
for (i, (long, expected)) in z {
let otp = hotp.generate(i);
assert_eq!(otp, *expected);
#[cfg(not(feature = "only-gauth"))]
{
let otp = hotp.clone().set_len(9).generate(i);
assert_eq!(otp, long % TRUNCATE_TO[9]);
}
#[cfg(feature = "only-gauth")]
{
assert_eq!(otp, long % TRUNCATE_TO[6]);
}
}
}
}
#[doc(hidden)]
impl<A: Array, B: Array> PartialEq<HOTP<B>> for HOTP<A> {
fn eq(
&self,
other: &HOTP<B>,
) -> bool {
#[cfg(feature = "only-gauth")]
let stuff = true;
#[cfg(not(feature = "only-gauth"))]
let stuff = self.len == other.len && self.alg == other.alg;
let all_xor = self
.sec
.as_slice()
.iter()
.zip(other.sec.as_slice())
.fold(0u8, |v, (a, b)| v | (a ^ b));
self.sec.as_slice().len() == other.sec.as_slice().len()
&& 0 == all_xor
&& stuff
}
}
pub const TRUNCATE_TO: [u32; 10] = [
1,
10,
100,
1_000,
10_000,
100_000,
1_000_000,
10_000_000,
100_000_000,
1_000_000_000,
];
impl<A: Array> HOTP<A> {
pub fn new(sec: A) -> Self {
#[cfg(not(feature = "only-gauth"))]
let new = Self {
sec,
len: 6,
alg: SHA1,
};
#[cfg(feature = "only-gauth")]
let new = Self { sec };
new
}
pub fn verify_range_default(
self,
counter: u64,
input: u32,
) -> bool {
self.verify_range(counter, input, -1..=1)
}
pub fn generate(
&self,
counter: u64,
) -> u32 {
let key = self.sec.as_slice();
#[cfg(feature = "only-gauth")]
let key = Key::new(HMAC_SHA1_FOR_LEGACY_USE_ONLY, key);
#[cfg(not(feature = "only-gauth"))]
let key = Key::new(self.alg.into(), key);
let counter = counter.to_be_bytes();
let result = sign(&key, &counter);
let digest = result.as_ref();
#[cfg(feature = "only-gauth")]
let last = 19;
#[cfg(not(feature = "only-gauth"))]
let last = match self.alg {
SHA1 => 19,
SHA256 => 31,
SHA512 => 63,
};
let offset = (unsafe { digest.get_unchecked(last) } & 0x0F) as usize;
let value = u32::from_be_bytes(unsafe {
[
0x7F & *digest.get_unchecked(offset),
*digest.get_unchecked(offset + 1),
*digest.get_unchecked(offset + 2),
*digest.get_unchecked(offset + 3),
]
});
#[cfg(feature = "only-gauth")]
let len = 6;
#[cfg(not(feature = "only-gauth"))]
let len = self.len as usize;
value % TRUNCATE_TO[len]
}
pub fn verify(
&self,
counter: u64,
input: u32,
) -> bool {
let value = self.generate(counter);
(input ^ value) == 0
}
pub fn verify_range<I>(
self,
counter: u64,
input: u32,
diffs: I,
) -> bool
where
I: IntoIterator<Item = i64>,
{
let mut ok = false;
for diff in diffs {
let new_count = if diff.is_negative() {
counter + (diff.abs() as u64)
} else {
counter - diff as u64
};
ok |= self.verify(new_count, input);
}
ok
}
}
#[cfg(not(feature = "only-gauth"))]
impl<A: Array> HOTP<A> {
pub fn set_alg(
mut self,
alg: crate::alg::Algorithm,
) -> Self {
self.alg = alg;
self
}
pub fn set_len(
mut self,
len: u8,
) -> Self {
self.len = len;
self
}
}
#[cfg(all(feature = "base32", any(feature = "std", feature = "alloc")))]
mod b32 {
use crate::{
Array,
Error,
B32A,
HOTP,
};
#[cfg(feature = "alloc")]
use alloc::{
str::from_utf8_unchecked,
string::String,
vec::Vec,
};
#[cfg(feature = "cstr")]
use std::ffi::CStr;
impl<A: Array> HOTP<A> {
pub fn base32_secret(&self) -> String {
base32::encode(B32A, self.sec.as_slice())
}
pub fn base32_segs(
&self,
chunk_size: usize,
) -> crate::Segs {
let sec = self.base32_secret();
crate::Segs {
sec,
chunk_size,
curr: 0,
}
}
pub fn to_uri<S: AsRef<str>>(
&self,
label: S,
issuer: S,
counter: u64,
) -> String {
#[cfg(feature = "only-gauth")]
let (alg, len) = ("SHA1", 6);
#[cfg(not(feature = "only-gauth"))]
let (alg, len) = (self.alg, self.len);
format!(
"otpauth://hotp/{lbl}?secret={sec}&issuer={iss}&counter={cnt}&algorithm={alg}&digits={len}",
sec = self.base32_secret(),
lbl = label.as_ref(),
iss = issuer.as_ref(),
cnt = counter,
alg = alg,
len = len
)
}
}
impl HOTP<Vec<u8>> {
pub fn from_base32<S: AsRef<str>>(s: S) -> Result<Self, Error> {
let s = s.as_ref();
base32::decode(B32A, s)
.map(|vec| Self::new(vec))
.ok_or_else(|| Error::BadEnc)
}
#[cfg(feature = "cstr")]
pub fn from_base32c<C: AsRef<CStr>>(s: C) -> Result<Self, Error> {
let s = s.as_ref().to_str()?;
base32::decode(B32A, s)
.ok_or_else(|| Error::BadEnc)
.map(|v| Self::new(v))
}
pub fn from_base32b<'a, I: IntoIterator<Item = &'a u8>>(
i: I
) -> Result<Self, Error> {
let key: Vec<u8> = i
.into_iter()
.copied()
.take_while(u8::is_ascii_graphic)
.collect();
let key = unsafe { String::from_utf8_unchecked(key) };
Self::from_base32(key)
}
}
}
#[cfg(any(feature = "std", feature = "alloc"))]
impl<A: Array> HOTP<A> {
pub fn generate_str(
&self,
counter: u64,
) -> String {
let value = self.generate(counter);
#[cfg(feature = "only-gauth")]
let len = 6;
#[cfg(not(feature = "only-gauth"))]
let len = self.len;
format!("{v:0<len$}", len = len, v = value)
}
}