use crate::otp::algorithm::Algorithm;
use crate::otp::base::otp;
use crate::{
InvalidSecretError, OtpResult, Radix, Secret, UnsupportedAlgorithmError, UnsupportedLengthError,
UnsupportedRadixError,
};
use base32ct::{Base32, Encoding};
use std::num::NonZeroU8;
#[derive(Debug, PartialEq)]
pub struct HOTP {
pub(crate) algorithm: Algorithm,
pub(crate) secret: Secret,
pub(crate) length: NonZeroU8,
pub(crate) radix: Radix,
}
impl HOTP {
pub fn new(algorithm: Algorithm, secret: Secret, length: NonZeroU8, radix: Radix) -> HOTP {
Self {
algorithm,
secret,
length,
radix,
}
}
pub fn default(secret: Secret) -> HOTP {
Self::new(Algorithm::SHA1, secret, NonZeroU8::new(6).unwrap(), Radix::new(10).unwrap())
}
pub fn rfc4226_default(secret: Secret) -> HOTP {
Self::default(secret)
}
pub fn generate(&self, counter: u64) -> OtpResult<String> {
otp(&self.algorithm, self.secret.clone().get(), self.length.get(), self.radix.get(), counter)
}
pub fn verify(&self, otp: &str, counter: u64, retries: u64) -> OtpResult<Option<u64>> {
if self.length.get() != otp.len() as u8 {
Ok(None)
} else {
for i in counter..=(counter + retries) {
match self.generate(i) {
Ok(generated_otp) => {
if otp == generated_otp {
return Ok(Some(i));
}
}
Err(e) => return Err(e),
}
}
Ok(None)
}
}
pub fn provisioning_uri(&self, issuer: &str, user: &str, counter: u64) -> OtpResult<String> {
if self.length.get() != 6 {
Err(Box::new(UnsupportedLengthError(self.length.get())))
} else if self.radix.get() != 10 {
Err(Box::new(UnsupportedRadixError(self.radix.get())))
} else if self.algorithm != Algorithm::SHA1 {
Err(Box::new(UnsupportedAlgorithmError(self.algorithm)))
} else {
Ok(format!(
"otpauth://hotp/{}?secret={}&counter={}&issuer={}",
urlencoding::encode(&format!("{}:{}", issuer, user)),
urlencoding::encode(Base32::encode_string(&self.secret.clone().get()).as_str()),
urlencoding::encode(&counter.to_string()),
urlencoding::encode(issuer)
))
}
}
pub fn from_uri(uri: &str) -> OtpResult<HOTP> {
let params = uri.split('?').next_back().unwrap().split('&');
let mut secret: Option<Secret> = None;
for param in params {
if let Some((key, value)) = param.split_once('=') {
if key == "secret" && !value.is_empty() {
secret = Some(Secret::new_from_vec(
Base32::decode_vec(urlencoding::decode(value).unwrap().trim()).unwrap(),
))
}
}
}
if secret.is_none() {
return Err(Box::new(InvalidSecretError()));
}
Ok(HOTP::default(secret.ok_or(InvalidSecretError()).unwrap()))
}
}