use hmac::{Hmac, KeyInit, Mac};
use sha2::{Sha256, Sha512};
use subtle::{Choice, ConditionallySelectable};
use crate::hotp::{dynamic_truncate, generate_with_sha1};
use crate::{Code, CodeError, Digits, Error, Secret};
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum Algorithm {
#[default]
Sha1,
Sha256,
Sha512,
}
impl Algorithm {
#[must_use]
pub const fn recommended_key_len(self) -> usize {
match self {
Self::Sha1 => 20,
Self::Sha256 => 32,
Self::Sha512 => 64,
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct ValidationWindow {
past: u16,
future: u16,
}
impl ValidationWindow {
pub const CURRENT: Self = Self::new(0, 0);
pub const RFC_RECOMMENDED: Self = Self::new(1, 0);
#[must_use]
pub const fn new(past: u16, future: u16) -> Self {
Self { past, future }
}
#[must_use]
pub const fn past(self) -> u16 {
self.past
}
#[must_use]
pub const fn future(self) -> u16 {
self.future
}
}
impl Default for ValidationWindow {
fn default() -> Self {
Self::RFC_RECOMMENDED
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct Totp {
algorithm: Algorithm,
digits: Digits,
period: u64,
epoch: u64,
}
struct MatchAccumulator {
found: Choice,
counter: u64,
drift: i32,
}
impl MatchAccumulator {
fn new() -> Self {
Self {
found: Choice::from(0),
counter: 0,
drift: 0,
}
}
}
impl Totp {
pub const DEFAULT_PERIOD: u64 = 30;
pub const fn new(
algorithm: Algorithm,
digits: Digits,
period: u64,
epoch: u64,
) -> Result<Self, Error> {
if period == 0 {
return Err(Error::ZeroPeriod);
}
Ok(Self {
algorithm,
digits,
period,
epoch,
})
}
#[must_use]
pub const fn algorithm(self) -> Algorithm {
self.algorithm
}
#[must_use]
pub const fn digits(self) -> Digits {
self.digits
}
#[must_use]
pub const fn period(self) -> u64 {
self.period
}
#[must_use]
pub const fn epoch(self) -> u64 {
self.epoch
}
pub const fn counter_at(self, timestamp: u64) -> Result<u64, Error> {
if timestamp < self.epoch {
return Err(Error::TimestampBeforeEpoch {
timestamp,
epoch: self.epoch,
});
}
Ok((timestamp - self.epoch) / self.period)
}
pub const fn seconds_remaining(self, timestamp: u64) -> Result<u64, Error> {
if timestamp < self.epoch {
return Err(Error::TimestampBeforeEpoch {
timestamp,
epoch: self.epoch,
});
}
Ok(self.period - ((timestamp - self.epoch) % self.period))
}
pub fn generate(&self, secret: &Secret<'_>, timestamp: u64) -> Result<Code, Error> {
let counter = self.counter_at(timestamp)?;
Ok(self.generate_for_counter(secret, counter))
}
pub fn verify(
&self,
secret: &Secret<'_>,
timestamp: u64,
candidate: &str,
) -> Result<bool, VerifyError> {
let candidate = Code::parse(candidate, self.digits)?;
let counter = self.counter_at(timestamp)?;
Ok(self.generate_for_counter(secret, counter).ct_eq(candidate))
}
pub fn verify_window(
&self,
secret: &Secret<'_>,
timestamp: u64,
window: ValidationWindow,
candidate: &str,
) -> Result<Option<TotpMatch>, VerifyError> {
let candidate = Code::parse(candidate, self.digits)?;
let counter = self.counter_at(timestamp)?;
let mut matched = MatchAccumulator::new();
self.consider_match(secret, counter, 0, candidate, &mut matched);
for distance in 1..=window.past {
let Some(current) = counter.checked_sub(u64::from(distance)) else {
continue;
};
self.consider_match(
secret,
current,
-i32::from(distance),
candidate,
&mut matched,
);
}
for distance in 1..=window.future {
let Some(current) = counter.checked_add(u64::from(distance)) else {
continue;
};
self.consider_match(
secret,
current,
i32::from(distance),
candidate,
&mut matched,
);
}
Ok(if bool::from(matched.found) {
Some(TotpMatch {
counter: matched.counter,
drift: matched.drift,
})
} else {
None
})
}
fn consider_match(
&self,
secret: &Secret<'_>,
counter: u64,
drift: i32,
candidate: Code,
matched: &mut MatchAccumulator,
) {
let equal = self
.generate_for_counter(secret, counter)
.ct_eq_choice(candidate);
let select = equal & !matched.found;
matched.counter = u64::conditional_select(&matched.counter, &counter, select);
matched.drift = i32::conditional_select(&matched.drift, &drift, select);
matched.found |= equal;
}
fn generate_for_counter(&self, secret: &Secret<'_>, counter: u64) -> Code {
let message = counter.to_be_bytes();
match self.algorithm {
Algorithm::Sha1 => generate_with_sha1(secret, counter, self.digits),
Algorithm::Sha256 => {
let mut mac = Hmac::<Sha256>::new_from_slice(secret.as_bytes())
.expect("HMAC accepts keys of any length");
mac.update(&message);
dynamic_truncate(mac.finalize().into_bytes().as_slice(), self.digits)
}
Algorithm::Sha512 => {
let mut mac = Hmac::<Sha512>::new_from_slice(secret.as_bytes())
.expect("HMAC accepts keys of any length");
mac.update(&message);
dynamic_truncate(mac.finalize().into_bytes().as_slice(), self.digits)
}
}
}
}
impl Default for Totp {
fn default() -> Self {
Self {
algorithm: Algorithm::Sha1,
digits: Digits::SIX,
period: Self::DEFAULT_PERIOD,
epoch: 0,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum VerifyError {
Code(CodeError),
Parameters(Error),
}
impl core::fmt::Display for VerifyError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Code(error) => write!(f, "invalid OTP code: {error}"),
Self::Parameters(error) => write!(f, "invalid TOTP input: {error}"),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for VerifyError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Code(error) => Some(error),
Self::Parameters(error) => Some(error),
}
}
}
impl From<CodeError> for VerifyError {
fn from(value: CodeError) -> Self {
Self::Code(value)
}
}
impl From<Error> for VerifyError {
fn from(value: Error) -> Self {
Self::Parameters(value)
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct TotpMatch {
counter: u64,
drift: i32,
}
impl TotpMatch {
#[must_use]
pub const fn counter(self) -> u64 {
self.counter
}
#[must_use]
pub const fn drift(self) -> i32 {
self.drift
}
}