use totp_rs::{Algorithm, TOTP, Secret};
use slauth::oath::{hotp, HashesAlgorithm};
use serde::{Deserialize, Serialize};
use sled::IVec;
use url::Url;
use std::time::{SystemTime, UNIX_EPOCH};
use crate::securecrypto::SecureCrypto;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum XOTPAlgorithm {
SHA1,
SHA256,
SHA512,
}
impl XOTPAlgorithm {
pub fn to_totp(&self) -> Algorithm {
match self {
XOTPAlgorithm::SHA1 => Algorithm::SHA1,
XOTPAlgorithm::SHA256 => Algorithm::SHA256,
XOTPAlgorithm::SHA512 => Algorithm::SHA512,
}
}
pub fn to_hotp(&self) -> HashesAlgorithm {
match self {
XOTPAlgorithm::SHA1 => HashesAlgorithm::SHA1,
XOTPAlgorithm::SHA256 => HashesAlgorithm::SHA256,
XOTPAlgorithm::SHA512 => HashesAlgorithm::SHA512,
}
}
pub fn from_str(s: &str) -> Option<Self> {
match s.to_uppercase().as_str() {
"SHA1" | "SHA-1" => Some(XOTPAlgorithm::SHA1),
"SHA256" | "SHA-256" => Some(XOTPAlgorithm::SHA256),
"SHA512" | "SHA-512" => Some(XOTPAlgorithm::SHA512),
_ => None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum XOTPType {
HOTP,
TOTP,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum OtpAlgo {
Hotp { counter: u64 }, Totp { interval: u64 }, }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct XOTP {
pub otptype: XOTPType,
pub secret: Vec<u8>, pub algorithm: XOTPAlgorithm,
pub digits: u8, pub algo: OtpAlgo,
pub issuer_label: Option<String>, }
impl Default for XOTP {
fn default() -> Self {
Self {
otptype: XOTPType::TOTP,
secret: Vec::new(),
algorithm: XOTPAlgorithm::SHA1,
digits: 6,
algo: OtpAlgo::Totp { interval: 30 },
issuer_label: None,
}
}
}
impl XOTP {
pub fn from_text(text: &str, crypto: &SecureCrypto) -> Self {
if is_uri(text) {
Self::from_uri(text, crypto)
} else {
Self::from_secret(text, crypto)
}
}
fn from_secret(secret: &str, crypto: &SecureCrypto) -> Self {
let cleaned_secret = secret.trim().replace(' ', "");
let encrypted_secret = crypto.encrypt_string(&cleaned_secret)
.expect("Failed to encrypt secret");
XOTP {
otptype: XOTPType::TOTP,
secret: encrypted_secret,
algorithm: XOTPAlgorithm::SHA1,
digits: 6,
algo: OtpAlgo::Totp { interval: 30 },
issuer_label: None,
}
}
fn from_uri(uri: &str, crypto: &SecureCrypto) -> Self {
let url = Url::parse(uri).expect("Invalid URI");
let mut xotp = XOTP::default();
let path = url.path();
xotp.issuer_label = Some(path[1..].to_string());
if let Some(host) = url.host_str() {
if host.contains("hotp") {
xotp.otptype = XOTPType::HOTP;
} else if host.contains("totp") {
xotp.otptype = XOTPType::TOTP;
}
}
let query_pairs = url.query_pairs();
for (key, value) in query_pairs {
match key.as_ref() {
"secret" => {
let encrypted_secret = crypto.encrypt_string(&value)
.expect("Failed to encrypt secret");
xotp.secret = encrypted_secret;
}
"algorithm" => {
xotp.algorithm = XOTPAlgorithm::from_str(&value).unwrap_or(XOTPAlgorithm::SHA1);
}
"digits" => {
xotp.digits = value.parse().unwrap_or(6);
}
"period" => {
xotp.algo = OtpAlgo::Totp { interval: value.parse().unwrap_or(30) };
}
"counter" => {
xotp.algo = OtpAlgo::Hotp { counter: value.parse().unwrap_or(0) };
}
_ => {}
}
}
xotp
}
pub fn generate_totp_code(&self, crypto: &SecureCrypto) -> String {
let secret = crypto.decrypt_string(&IVec::from(self.secret.as_slice()))
.expect("Failed to decrypt secret");
let totp = TOTP::new(
self.algorithm.to_totp(),
self.digits.into(),
1,
match &self.algo {
OtpAlgo::Totp { interval } => *interval,
_ => 30,
},
Secret::Encoded(secret).to_bytes().expect("Failed to convert secret"),
).expect("Failed to create TOTP");
totp.generate_current().unwrap_or_default()
}
pub fn generate_hotp_code(&mut self, crypto: &SecureCrypto) -> String {
let secret = crypto.decrypt_string(&IVec::from(self.secret.as_slice()))
.expect("Failed to decrypt secret");
let counter = match &self.algo {
OtpAlgo::Hotp { counter } => *counter,
_ => 1,
};
let hotp_builder = hotp::HOTPBuilder::new()
.algorithm(self.algorithm.to_hotp())
.digits(self.digits.into())
.counter(counter)
.secret(&secret.as_bytes())
.build();
self.increment_counter();
hotp_builder.r#gen()
}
pub fn get_remaining_seconds(&self) -> Option<u32> {
if let XOTPType::TOTP = self.otptype {
let interval = match &self.algo {
OtpAlgo::Totp { interval } => *interval,
_ => 30,
}.max(30) as u64;
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let elapsed = now % interval;
Some((interval - elapsed) as u32)
} else {
None
}
}
pub fn increment_counter(&mut self) -> bool {
if let OtpAlgo::Hotp { ref mut counter } = self.algo {
*counter += 1;
true
} else {
false
}
}
}
fn is_uri(text: &str) -> bool {
text.starts_with("otpauth://")
}
pub fn generate_code(xotp: &mut XOTP, crypto: &SecureCrypto) -> String {
match xotp.otptype {
XOTPType::TOTP => {
xotp.generate_totp_code(crypto)
},
XOTPType::HOTP => {
xotp.generate_hotp_code(crypto)
}
}
}