#!/usr/bin/env -S rust-script -c --debug
#![cfg_attr(feature = "_docs_examples", allow(unexpected_cfgs, reason = "example script"))]
use ::devela::{CryptoError, impl_trait, is, unwrap, whilst};
::devela::_use_or_shim![_doc_location, _doc_vendor, _tags];
#[doc = _tags!(crypto hash)]
#[doc = _doc_location!("data/codec/crypto")]
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[must_use]
pub struct Otp {
code: u32,
digits: u32,
}
impl_trait! { fmt::Display for Otp |self, f|
write!(f, "{:0width$}", self.code, width = self.digits as usize)
}
impl Otp {
pub const DEFAULT_DIGITS: u32 = 6;
pub const MIN_DIGITS: u32 = 1;
pub const MAX_DIGITS: u32 = 10;
pub const DEFAULT_EPOCH: u64 = 0;
pub const DEFAULT_PERIOD: u64 = 30;
}
impl Otp {
const fn validate_digits(digits: u32) -> Result<(), CryptoError> {
if digits < Self::MIN_DIGITS || digits > Self::MAX_DIGITS {
Err(CryptoError::InvalidLength)
} else {
Ok(())
}
}
pub const fn new(code: u32, digits: u32) -> Result<Self, CryptoError> {
let modulo = unwrap![ok? Self::modulo(digits)];
is![code as u64 >= modulo, Err(CryptoError::InvalidParameter), Ok(Self { code, digits })]
}
pub const fn new_reduced(code: u64, digits: u32) -> Result<Self, CryptoError> {
let modulo = unwrap![ok? Self::modulo(digits)];
Ok(Self { code: (code % modulo) as u32, digits })
}
#[must_use]
pub const fn code(self) -> u32 {
self.code
}
#[must_use]
pub const fn digits(self) -> u32 {
self.digits
}
pub const fn modulo(digits: u32) -> Result<u64, CryptoError> {
unwrap![ok? Self::validate_digits(digits)];
let mut n = 1u64;
whilst! { i in 0..digits; { n *= 10; }}
Ok(n)
}
pub const fn time_counter(
unix_seconds: u64,
epoch: u64,
period: u64,
) -> Result<u64, CryptoError> {
if period == 0 || unix_seconds < epoch {
Err(CryptoError::InvalidParameter)
} else {
Ok((unix_seconds - epoch) / period)
}
}
}
#[allow(unused, reason = "example script")]
#[cfg(all(feature = "std", feature = "time"))]
fn main() {
use ::devela::{Base32, Otp, TimeUnixU32, digest};
digest![struct Sha1: Sha1];
const SECRET: &str = "I65VU7K5ZQL7WB4E";
const PERIOD: u64 = 30;
let mut key = [0u8; Base32::decoded_len_stripped(SECRET.as_bytes())];
let key_len = Base32::decode_from_slice(SECRET.as_bytes(), &mut key).unwrap();
let now = TimeUnixU32::now().seconds as u64;
let otp = Sha1::totp(&key[..key_len], now, Otp::DEFAULT_DIGITS).unwrap();
println!("{otp}");
}
#[cfg(test)]
mod tests {
use crate::{_hex, Otp, Sha1};
#[test]
fn otp_rejects_invalid_digits() {
let key = _hex::<20>("3132333435363738393031323334353637383930");
assert!(Sha1::hotp(&key, 0, 0).unwrap_err().is_invalid_length());
assert!(Sha1::hotp(&key, 0, 11).unwrap_err().is_invalid_length());
}
#[test]
fn otp_rejects_invalid_time_counter_parameters() {
assert!(Otp::time_counter(100, 0, 0).unwrap_err().is_invalid_parameter());
assert!(Otp::time_counter(99, 100, 30).unwrap_err().is_invalid_parameter());
}
}