#![forbid(unsafe_code)]
extern crate binascii;
extern crate ring;
#[cfg(test)]
mod tests;
mod utils;
#[derive(Copy, Clone)]
pub enum HOTPAlgorithm {
HMACSHA1,
HMACSHA256,
HMACSHA512,
}
impl HOTPAlgorithm {
pub fn from_buffer_len(buffer_length: usize) -> Option<HOTPAlgorithm> {
match buffer_length {
ring::digest::SHA1_OUTPUT_LEN => Option::Some(HOTPAlgorithm::HMACSHA1),
ring::digest::SHA256_OUTPUT_LEN => Option::Some(HOTPAlgorithm::HMACSHA256),
ring::digest::SHA512_OUTPUT_LEN => Option::Some(HOTPAlgorithm::HMACSHA512),
_ => Option::None,
}
}
pub fn get_algorithm<'a>(&self) -> ring::hmac::Algorithm {
match *self {
HOTPAlgorithm::HMACSHA1 => ring::hmac::HMAC_SHA1_FOR_LEGACY_USE_ONLY,
HOTPAlgorithm::HMACSHA256 => ring::hmac::HMAC_SHA256,
HOTPAlgorithm::HMACSHA512 => ring::hmac::HMAC_SHA512,
}
}
}
pub struct HOTP {
secret: Vec<u8>,
algorithm: HOTPAlgorithm,
}
impl HOTP {
pub fn new(algorithm: HOTPAlgorithm) -> Result<HOTP, ring::error::Unspecified> {
let algo = algorithm.get_algorithm();
match HOTP::generate_secret(algo.digest_algorithm().output_len()) {
Ok(secret) => Result::Ok(HOTP { secret, algorithm }),
Err(err) => Result::Err(err),
}
}
pub fn from_base32(data: &str) -> Result<HOTP, ()> {
let mut buffer = [0u8; 1024];
let secret = match binascii::b32decode(data.as_bytes(), &mut buffer) {
Ok(v) => v,
Err(_) => {
return Err(());
}
};
let algorithm = HOTPAlgorithm::from_buffer_len(secret.len());
match algorithm {
Some(algorithm) => Result::Ok(HOTP {
secret: Vec::from(secret),
algorithm,
}),
None => Result::Err(()),
}
}
pub fn from_bin(data: &[u8]) -> Result<HOTP, ()> {
let algorithm = HOTPAlgorithm::from_buffer_len(data.len());
if algorithm.is_none() {
return Result::Err(());
}
Result::Ok(HOTP {
secret: Vec::from(data),
algorithm: algorithm.unwrap(),
})
}
fn generate_secret(size: usize) -> Result<Vec<u8>, ring::error::Unspecified> {
use ring::rand::SecureRandom;
let mut secret: Vec<u8> = vec![0; size];
let rand = ring::rand::SystemRandom::new();
match rand.fill(secret.as_mut()) {
Ok(_) => Result::Ok(secret),
Err(err) => Result::Err(err),
}
}
pub fn get_secret_base32(&self) -> String {
let mut buffer = Box::new([0u8; 1024]);
match binascii::b32encode(self.secret.as_slice(), buffer.as_mut()) {
Ok(v) => {
let vec = Vec::from(v);
String::from_utf8(vec).unwrap()
}
Err(_) => unreachable!(),
}
}
pub fn get_otp(&self, counter: &[u8], digits: u32) -> u32 {
let key = ring::hmac::Key::new(self.algorithm.get_algorithm(), &self.secret);
let hmac = ring::hmac::sign(&key, counter);
let block = hmac.as_ref();
let num = HOTP::get_hotp_value(block);
return num % 10u32.pow(digits);
}
fn get_hotp_value(data: &[u8]) -> u32 {
let offset: usize = (data[data.len() - 1] & 0x0f) as usize;
let result: u32 = (((data[offset] & 0x7f) as u32) << 24)
| (((data[offset + 1] & 0xff) as u32) << 16)
| (((data[offset + 2] & 0xff) as u32) << 8)
| ((data[offset + 3] & 0xff) as u32);
return result;
}
pub fn validate(&self, counter: &[u8], digits: u32, guess: u32) -> bool {
self.get_otp(counter, digits) == guess
}
}
pub struct TOTP {
secret: HOTP,
time_step: u64,
start_time: u64,
}
impl TOTP {
pub fn new(secret: HOTP, time_step: u64, start_time: u64) -> TOTP {
assert!(time_step > 0);
TOTP {
secret,
time_step,
start_time,
}
}
fn get_time(&self) -> u64 {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap();
return (now.as_secs() + self.start_time) / self.time_step;
}
pub fn get_otp(&self, digits: u32, offset: i32) -> u32 {
let buf: &[u8] = &utils::num_to_buffer(((self.get_time() as i64) + (offset as i64)) as u64);
return self.secret.get_otp(buf, digits);
}
pub fn validate(&self, digits: u32, guess: u32, buffer: u32) -> bool {
for offset in -(buffer as i32)..((buffer + 1) as i32) {
if self.get_otp(digits, offset) == guess {
return true;
}
}
return false;
}
}
pub fn hotp(counter: u64, secret: &str, digits: u32) -> Option<u32> {
match HOTP::from_base32(secret) {
Ok(otp) => {
let counter_bytes = &utils::num_to_buffer(counter);
Option::Some(otp.get_otp(counter_bytes, digits))
}
Err(_) => Option::None,
}
}
pub fn totp(secret: &str, digits: u32, time_step: u64, time_start: u64) -> Option<u32> {
match HOTP::from_base32(secret) {
Ok(otp) => Option::Some(TOTP::new(otp, time_step, time_start).get_otp(digits, 0)),
Err(_) => Option::None,
}
}
pub fn validate_hotp(
input: u32,
validation_margin: i32,
counter: u64,
secret: &str,
digits: u32,
) -> Option<bool> {
match HOTP::from_base32(secret) {
Ok(hotp) => {
for i in (-validation_margin)..(validation_margin + 1) {
let current_counter = (counter as i64) + (i as i64);
if hotp.get_otp(&utils::num_to_buffer(current_counter as u64), digits) == input {
return Option::Some(true);
}
}
Option::Some(false)
}
Err(_) => Option::None,
}
}
pub fn validate_totp(
input: u32,
validation_margin: u32,
secret: &str,
digits: u32,
time_step: u64,
time_start: u64,
) -> Option<bool> {
match HOTP::from_base32(secret) {
Ok(hotp) => {
let totp = TOTP::new(hotp, time_step, time_start);
Option::Some(totp.validate(digits, input, validation_margin))
}
Err(_) => Option::None,
}
}