#![deny(missing_docs)]
#![deny(rustdoc::broken_intra_doc_links)]
mod base32;
mod hmac;
mod hotp;
mod otpauth;
mod sha1;
mod totp;
use std::time::{SystemTime, UNIX_EPOCH};
pub const DEFAULT_STEP: u64 = totp::DEFAULT_STEP;
pub const DEFAULT_DIGITS: u32 = totp::DEFAULT_DIGITS;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Error {
InvalidBase32(String),
InvalidUri(&'static str),
}
impl core::fmt::Display for Error {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Error::InvalidBase32(s) => write!(f, "invalid base32: {s}"),
Error::InvalidUri(s) => write!(f, "invalid otpauth URI: {s}"),
}
}
}
impl std::error::Error for Error {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Algorithm {
#[default]
Sha1,
}
#[derive(Clone, PartialEq, Eq)]
pub struct Secret(Vec<u8>);
impl Secret {
pub fn from_base32(s: &str) -> Result<Self, Error> {
base32::decode(s).map(Secret).map_err(Error::InvalidBase32)
}
pub fn from_bytes(bytes: impl Into<Vec<u8>>) -> Self {
Secret(bytes.into())
}
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
pub fn to_base32(&self) -> String {
let s = base32::encode(&self.0);
s.trim_end_matches('=').to_string()
}
pub fn to_base32_grouped(&self) -> String {
base32::encode_grouped(&self.0)
}
}
impl core::fmt::Debug for Secret {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "Secret(<{} bytes>)", self.0.len())
}
}
#[derive(Debug, Clone)]
pub struct Hotp {
secret: Secret,
digits: u32,
algorithm: Algorithm,
}
impl Hotp {
pub fn new(secret: Secret) -> Self {
Self {
secret,
digits: DEFAULT_DIGITS,
algorithm: Algorithm::default(),
}
}
pub fn digits(mut self, digits: u32) -> Self {
self.digits = digits;
self
}
pub fn code_at(&self, counter: u64) -> String {
let _ = self.algorithm; hotp::hotp(self.secret.as_bytes(), counter, self.digits)
}
}
#[derive(Debug, Clone)]
pub struct Totp {
secret: Secret,
issuer: String,
account: String,
digits: u32,
step: u64,
algorithm: Algorithm,
}
impl Totp {
pub fn builder(secret: Secret) -> TotpBuilder {
TotpBuilder {
secret,
issuer: String::new(),
account: String::new(),
digits: DEFAULT_DIGITS,
step: DEFAULT_STEP,
algorithm: Algorithm::default(),
}
}
pub fn code_at(&self, unix_time: u64) -> String {
let _ = self.algorithm;
totp::totp(self.secret.as_bytes(), unix_time, self.step, self.digits)
}
pub fn code_now(&self) -> String {
self.code_at(unix_now())
}
pub fn seconds_remaining_at(&self, unix_time: u64) -> u64 {
totp::seconds_remaining(unix_time, self.step)
}
pub fn seconds_remaining_now(&self) -> u64 {
self.seconds_remaining_at(unix_now())
}
pub fn verify(&self, code: &str, unix_time: u64, window: u32) -> Option<i64> {
totp::verify(
self.secret.as_bytes(),
code,
unix_time,
self.step,
self.digits,
window as i64,
)
}
pub fn issuer(&self) -> &str {
&self.issuer
}
pub fn account(&self) -> &str {
&self.account
}
pub fn secret(&self) -> &Secret {
&self.secret
}
pub fn uri(&self) -> String {
otpauth::build_uri(&self.issuer, &self.account, self.secret.as_bytes())
}
}
pub struct TotpBuilder {
secret: Secret,
issuer: String,
account: String,
digits: u32,
step: u64,
algorithm: Algorithm,
}
impl TotpBuilder {
pub fn issuer(mut self, s: impl Into<String>) -> Self {
self.issuer = s.into();
self
}
pub fn account(mut self, s: impl Into<String>) -> Self {
self.account = s.into();
self
}
pub fn digits(mut self, digits: u32) -> Self {
self.digits = digits;
self
}
pub fn step(mut self, step: u64) -> Self {
self.step = step;
self
}
pub fn algorithm(mut self, algorithm: Algorithm) -> Self {
self.algorithm = algorithm;
self
}
pub fn build(self) -> Totp {
Totp {
secret: self.secret,
issuer: self.issuer,
account: self.account,
digits: self.digits,
step: self.step,
algorithm: self.algorithm,
}
}
}
fn unix_now() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system clock before UNIX epoch")
.as_secs()
}