use super::*;
pub const HOTP_DEFAULT_COUNTER_VALUE: u64 = 0;
pub const HOTP_DEFAULT_RESYNC_VALUE: u16 = 2;
#[derive(Default)]
pub struct HOTPBuilder {
alg: Option<HashesAlgorithm>,
counter: Option<u64>,
resync: Option<u16>,
digits: Option<usize>,
secret: Option<Vec<u8>>,
}
impl HOTPBuilder {
pub fn new() -> Self {
HOTPBuilder::default()
}
pub fn algorithm(mut self, alg: HashesAlgorithm) -> Self {
self.alg = Some(alg);
self
}
pub fn counter(mut self, c: u64) -> Self {
self.counter = Some(c);
self
}
pub fn re_sync_parameter(mut self, s: u16) -> Self {
self.resync = Some(s);
self
}
pub fn digits(mut self, d: usize) -> Self {
self.digits = Some(d);
self
}
pub fn secret(mut self, secret: &[u8]) -> Self {
self.secret = Some(secret.to_vec());
self
}
pub fn build(self) -> HOTPContext {
let HOTPBuilder {
alg,
counter,
resync,
digits,
secret,
} = self;
let alg = alg.unwrap_or(OTP_DEFAULT_ALG_VALUE);
let secret = secret.unwrap_or_default();
let secret_key = alg.to_mac_hash_key(secret.as_slice());
HOTPContext {
alg,
counter: counter.unwrap_or(HOTP_DEFAULT_COUNTER_VALUE),
resync: resync.unwrap_or(HOTP_DEFAULT_RESYNC_VALUE),
digits: digits.unwrap_or(OTP_DEFAULT_DIGITS_VALUE),
secret,
secret_key,
}
}
}
pub struct HOTPContext {
alg: HashesAlgorithm,
counter: u64,
resync: u16,
digits: usize,
secret: Vec<u8>,
secret_key: MacHashKey,
}
impl HOTPContext {
pub fn builder() -> HOTPBuilder {
HOTPBuilder::new()
}
pub fn gen(&self) -> String {
self.gen_at(self.counter)
}
pub fn inc(&mut self) -> &mut Self {
self.counter += 1;
self
}
pub fn validate_current(&self, value: &str) -> bool {
if value.len() != self.digits {
return false;
}
self.gen().as_str().eq(value)
}
pub fn verify(&mut self, value: &str) -> bool {
if value.len() != self.digits {
return false;
}
for i in self.counter..(self.counter + self.resync as u64) {
if self.gen_at(i).as_str().eq(value) {
self.counter += i - self.counter + 1;
return true;
}
}
false
}
fn gen_at(&self, c: u64) -> String {
let c_b_e = c.to_be_bytes();
let hs_sig = self
.secret_key
.sign(&c_b_e[..])
.expect("This should not happen since HMAC can take key of any size")
.into_vec();
let s_bits = dt(hs_sig.as_ref());
let s_num = s_bits % 10_u32.pow(self.digits as u32);
format!("{:0>6}", s_num)
}
}
impl OtpAuth for HOTPContext {
fn to_uri(&self, label: Option<&str>, issuer: Option<&str>) -> String {
let mut uri = format!(
"otpauth://hotp/{}?secret={}&algorithm={}&digits={}&counter={}",
label.unwrap_or("slauth"),
base32::encode(base32::Alphabet::Rfc4648 { padding: false }, self.secret.as_slice()),
self.alg,
self.digits,
self.counter
);
if let Some(iss) = issuer {
uri.push_str("&issuer=");
uri.push_str(iss);
}
uri
}
fn from_uri(uri: &str) -> Result<Self, String>
where
Self: Sized,
{
let mut uri_it = uri.split("://");
uri_it
.next()
.filter(|scheme| scheme.eq(&"otpauth"))
.ok_or_else(|| "Otpauth uri is malformed".to_string())?;
let type_label_it_opt = uri_it.next().map(|type_label_param| type_label_param.split('/'));
if let Some(mut type_label_it) = type_label_it_opt {
type_label_it
.next()
.filter(|otp_type| otp_type.eq(&"hotp"))
.ok_or_else(|| "Otpauth uri is malformed, bad type".to_string())?;
let param_it_opt = type_label_it
.next()
.and_then(|label_param| label_param.split('?').next_back().map(|s| s.split('&')));
param_it_opt
.ok_or_else(|| "Otpauth uri is malformed, missing parameters".to_string())
.and_then(|param_it| {
let mut secret = Vec::<u8>::new();
let mut counter = u64::MAX;
let mut alg = OTP_DEFAULT_ALG_VALUE;
let mut digits = OTP_DEFAULT_DIGITS_VALUE;
for s_param in param_it {
let mut s_param_it = s_param.split('=');
match s_param_it.next() {
Some("secret") => {
secret = s_param_it
.next()
.and_then(decode_hex_or_base_32)
.ok_or_else(|| "Otpauth uri is malformed, missing secret value".to_string())?;
continue;
}
Some("algorithm") => {
alg = match s_param_it
.next()
.ok_or_else(|| "Otpauth uri is malformed, missing algorithm value".to_string())?
{
"SHA256" => HashesAlgorithm::SHA256,
"SHA512" => HashesAlgorithm::SHA512,
_ => HashesAlgorithm::SHA1,
};
continue;
}
Some("digits") => {
digits = s_param_it
.next()
.ok_or_else(|| "Otpauth uri is malformed, missing digits value".to_string())?
.parse::<usize>()
.map_err(|_| "Otpauth uri is malformed, bad digits value".to_string())?;
continue;
}
Some("counter") => {
counter = s_param_it
.next()
.ok_or_else(|| "Otpauth uri is malformed, missing counter value".to_string())?
.parse::<u64>()
.map_err(|_| "Otpauth uri is malformed, bad counter value".to_string())?;
continue;
}
_ => {}
}
}
if secret.is_empty() || counter == u64::MAX {
return Err("Otpauth uri is malformed".to_string());
}
let secret_key = alg.to_mac_hash_key(secret.as_slice());
Ok(HOTPContext {
alg,
counter,
resync: HOTP_DEFAULT_RESYNC_VALUE,
digits,
secret,
secret_key,
})
})
} else {
Err("Otpauth uri is malformed, missing parts".to_string())
}
}
}
#[cfg(feature = "native-bindings")]
mod native_bindings {
use std::{os::raw::c_char, ptr::null_mut};
use super::*;
use crate::strings;
#[no_mangle]
pub unsafe extern "C" fn hotp_from_uri(uri: *const c_char) -> *mut HOTPContext {
let uri_str = strings::c_char_to_string(uri);
let hotp = HOTPContext::from_uri(&uri_str).map(Box::new);
match hotp {
Ok(hotp) => Box::into_raw(hotp),
Err(_) => null_mut(),
}
}
#[no_mangle]
pub unsafe extern "C" fn hotp_free(hotp: *mut HOTPContext) {
let _ = Box::from_raw(hotp);
}
#[no_mangle]
pub unsafe extern "C" fn hotp_to_uri(hotp: *mut HOTPContext, label: *const c_char, issuer: *const c_char) -> *mut c_char {
let hotp = &*hotp;
let label = strings::c_char_to_string(label);
let label_opt = if !label.is_empty() { Some(label.as_str()) } else { None };
let issuer = strings::c_char_to_string(issuer);
let issuer_opt = if !issuer.is_empty() { Some(issuer.as_str()) } else { None };
strings::string_to_c_char(hotp.to_uri(label_opt, issuer_opt))
}
#[no_mangle]
pub unsafe extern "C" fn hotp_gen(hotp: *mut HOTPContext) -> *mut c_char {
let hotp = &*hotp;
strings::string_to_c_char(hotp.gen())
}
#[no_mangle]
pub unsafe extern "C" fn hotp_inc(hotp: *mut HOTPContext) {
let hotp = &mut *hotp;
hotp.inc();
}
#[no_mangle]
pub unsafe extern "C" fn hotp_verify(hotp: *mut HOTPContext, code: *const c_char) -> bool {
let hotp = &mut *hotp;
let value = strings::c_char_to_string(code);
hotp.verify(&value)
}
#[no_mangle]
pub unsafe extern "C" fn hotp_validate_current(hotp: *mut HOTPContext, code: *const c_char) -> bool {
let hotp = &*hotp;
let value = strings::c_char_to_string(code);
hotp.validate_current(&value)
}
}
#[test]
fn hotp_from_uri() {
const MK_ULTRA: &str = "patate";
let server = HOTPBuilder::new()
.counter(102)
.re_sync_parameter(3)
.secret(MK_ULTRA.as_bytes())
.build();
let uri = server.to_uri(Some("Lucid:test@devolutions.net"), Some("Lucid"));
let client = HOTPContext::from_uri(uri.as_ref()).expect("oh no");
assert!(server.validate_current(client.gen().as_str()));
}
#[test]
fn hotp_multiple() {
const MK_ULTRA: &str = "patate";
let mut server = HOTPBuilder::new()
.counter(102)
.re_sync_parameter(3)
.secret(MK_ULTRA.as_bytes())
.build();
let uri = server.to_uri(Some("Lucid:test@devolutions.net"), Some("Lucid"));
let mut client = HOTPContext::from_uri(uri.as_ref()).expect("oh no");
assert!(server.verify(client.gen().as_str()));
assert!(server.verify(client.inc().gen().as_str()));
assert!(server.verify(client.inc().gen().as_str()));
assert!(server.verify(client.inc().gen().as_str()));
assert!(server.verify(client.inc().gen().as_str()));
}
#[test]
fn hotp_multiple_resync() {
const MK_ULTRA: &str = "patate";
let mut server = HOTPBuilder::new()
.counter(102)
.re_sync_parameter(3)
.secret(MK_ULTRA.as_bytes())
.build();
let uri = server.to_uri(Some("Lucid:test@devolutions.net"), Some("Lucid"));
let mut client = HOTPContext::from_uri(uri.as_ref()).expect("oh no");
assert!(server.verify(client.gen().as_str()));
assert!(server.verify(client.inc().gen().as_str()));
assert!(server.verify(client.inc().inc().gen().as_str()));
assert!(server.verify(client.inc().gen().as_str()));
assert!(server.verify(client.inc().inc().inc().gen().as_str()));
}