#![warn(missing_docs)]
extern crate sha_1 as sha1;
extern crate sha2;
extern crate digest;
extern crate hmac;
extern crate rustc_hex;
use sha1::Sha1;
use sha2::Sha256;
use sha2::Sha512;
use hmac::{Hmac, Mac};
use digest::Digest;
use rustc_hex::{FromHex, ToHex};
use std::io::Write as Write_io;
pub enum HashType {
SHA1,
SHA256,
SHA512
}
pub fn from_hex(data: &str) -> Result<Vec<u8>, &str> {
match data.from_hex() {
Ok(d) => Ok(d),
Err(_) => Err("Unable to decode hex")
}
}
#[inline]
#[allow(unknown_lints)] #[allow(identity_op)] fn u64_from_be_bytes_4(bytes: &[u8], start: usize) -> u64 {
let mut val = 0u64;
val += (bytes[start] as u64) << ((3 * 8) as u64);
val += (bytes[start+1] as u64) << ((2 * 8) as u64);
val += (bytes[start+2] as u64) << ((1 * 8) as u64);
val += (bytes[start+3] as u64) << ((0 * 8) as u64);
val
}
#[inline]
fn dynamic_truncation(hs: &[u8]) -> u64 {
let offset_bits = (hs[hs.len()-1] & 0xf) as usize;
let p = u64_from_be_bytes_4(hs, offset_bits);
p & 0x7fffffff
}
fn hmac_and_truncate<D: Digest + Default>(key: &[u8], message: &[u8],
digits: u32) -> u64 {
let mut hmac = Hmac::<D>::new(key);
hmac.input(message);
let result = hmac.result();
let hs = result.code();
dynamic_truncation(hs) % 10_u64.pow(digits)
}
pub fn hotp_raw(key: &[u8], counter: u64, digits: u32) -> u64 {
let message = counter.to_be();
let msg_ptr: &[u8] = unsafe { ::std::slice::from_raw_parts(&message as *const u64 as *const u8, 8) };
hmac_and_truncate::<Sha1>(key, msg_ptr, digits)
}
pub fn hotp(key: &str, counter: u64, digits: u32) -> Result<u64, &str> {
match key.from_hex() {
Ok(bytes) => Ok(hotp_raw(bytes.as_ref(), counter, digits)),
Err(_) => Err("Unable to parse hex.")
}
}
pub fn totp_custom<D: Digest + Default>(key: &[u8], digits: u32, epoch: u64,
time_step: u64, current_time: u64) -> u64 {
let counter: u64 = (current_time - epoch) / time_step;
let message = counter.to_be();
let msg_ptr: &[u8] = unsafe { ::std::slice::from_raw_parts(&message as *const u64 as *const u8, 8) };
hmac_and_truncate::<D>(key, msg_ptr, digits)
}
pub fn totp_raw_custom_time(key: &[u8], digits: u32, epoch: u64, time_step: u64,
timestamp: u64, hash: &HashType) -> u64 {
match *hash {
HashType::SHA1 => totp_custom::<Sha1>(key, digits, epoch, time_step, timestamp),
HashType::SHA256 => totp_custom::<Sha256>(key, digits, epoch, time_step, timestamp),
HashType::SHA512 => totp_custom::<Sha512>(key, digits, epoch, time_step, timestamp),
}
}
pub fn totp_raw_now(key: &[u8], digits: u32, epoch: u64, time_step: u64, hash: &HashType) -> u64 {
use std::time::{UNIX_EPOCH, SystemTime};
let current_time: u64 = SystemTime::now().duration_since(UNIX_EPOCH)
.expect("Earlier than 1970-01-01 00:00:00 UTC").as_secs();
totp_raw_custom_time(key, digits, epoch, time_step, current_time, hash)
}
pub fn totp_custom_time<'a>(key: &str, digits: u32, epoch: u64,
time_step: u64, timestamp: u64, hash: &HashType) -> Result<u64, &'a str> {
match key.from_hex() {
Ok(bytes) => Ok(totp_raw_custom_time(bytes.as_ref(), digits, epoch, time_step, timestamp, hash)),
Err(_) => Err("Unable to parse hex.")
}
}
pub fn totp_now<'a>(key: &str, digits: u32, epoch: u64,
time_step: u64, hash: &HashType) -> Result<u64, &'a str> {
match key.from_hex() {
Ok(bytes) => Ok(totp_raw_now(bytes.as_ref(), digits, epoch, time_step, hash)),
Err(_) => Err("Unable to parse hex.")
}
}
pub fn ocra(suite: &str, key: &[u8], counter: u64, question: &str,
password: &[u8], session_info: &[u8], num_of_time_steps: u64) -> Result<u64, ()> {
ocra_debug(suite, key, counter, question, password, session_info, num_of_time_steps).or(Err(()))
}
pub fn ocra_debug(suite: &str, key: &[u8], counter: u64, question: &str,
password: &[u8], session_info: &[u8], num_of_time_steps: u64) -> Result<u64, String> {
let parsed_suite: Vec<&str> = suite.split(':').collect();
if (parsed_suite.len() != 3) || (parsed_suite[0].to_uppercase() != "OCRA-1") {
return Err("Malformed suite string.".to_string());
}
let crypto_function: Vec<&str> = parsed_suite[1].split('-').collect();
if crypto_function[0].to_uppercase() != "HOTP" {
return Err("Only HOTP crypto function is supported. You requested ".to_string() + crypto_function[0] + ".");
}
let hotp_sha_type: HashType = match crypto_function[1].to_uppercase().as_str() {
"SHA1" => HashType::SHA1,
"SHA256" => HashType::SHA256,
"SHA512" => HashType::SHA512,
_ => return Err("Unknown hash type. Supported: SHA1/SHA256/SHA512. Requested: ".to_string() + crypto_function[1] + "."),
};
let num_of_digits = if crypto_function.len() == 3 {
let temp_num = crypto_function[2].parse().unwrap_or(0);
if temp_num > 10 || temp_num < 4 {
return Err("Number of returned digits should satisfy: 4 <= num <= 10. You requested ".to_string() + crypto_function[2] + ".");
}
temp_num
} else {
0
};
let data_input: Vec<&str> = parsed_suite[2].split('-').collect();
let question_len: usize = 128;
let mut counter_len: usize = 0;
let mut hashed_pin_len: usize = 0;
let mut session_info_len: usize = 0;
let mut timestamp_len: usize = 0;
let mut parsed_question_type: (QType, usize) = (QType::N, 0);
let mut parsed_pin_sha_type: (HashType, usize);
let mut timestamp_parsed: u64 = 0;
for p in data_input {
let setting: &[u8] = p.as_bytes();
match setting[0] {
b'q' | b'Q' => {
match ocra_parse_question(p) {
Ok(expr) => {
parsed_question_type = expr;
},
Err(err_str) => return Err(err_str + " Can't parse question " + p + "."),
};
},
b'c' | b'C' => counter_len = 8,
b'p' | b'P' => {
match parse_pin_sha_type(p) {
Ok(expr) => {
parsed_pin_sha_type = expr;
hashed_pin_len = parsed_pin_sha_type.1;
if password.len() != hashed_pin_len {
return Err("Wrong hashed password length.".to_string());
}
},
Err(err_str) => return Err(err_str + " Can't parse hash " + p + "."),
};
},
b's' | b'S' => {
match parse_session_info_len(p) {
Ok(value) => session_info_len = value,
Err(err_str) => return Err(err_str + " Wrong session info parameter " + p + "."),
};
},
b't' | b'T' => {
match parse_timestamp_format(p) {
Ok(value) => {
timestamp_parsed = num_of_time_steps / (value as u64);
timestamp_len = 8;
},
Err(err_str) => return Err(err_str + " Wrong timestamp parameter " + p + "."),
};
},
_ => return Err("Unknown parameter ".to_string() + p + "."),
}
}
let full_message_len = suite.len() + 1 + counter_len + question_len + hashed_pin_len + session_info_len + timestamp_len;
let mut current_message_len = suite.len() + 1;
let mut message: Vec<u8> = Vec::with_capacity(full_message_len);
message.extend_from_slice(suite.as_bytes());
message.push(0u8); if counter_len > 0 {
let counter_be = counter.to_be();
let msg_ptr: &[u8] = unsafe { ::std::slice::from_raw_parts(&counter_be as *const u64 as *const u8, 8) };
message.extend_from_slice(msg_ptr);
current_message_len += counter_len;
}
if parsed_question_type.1 != 0 {
let push_result = push_correct_question(&mut message, parsed_question_type, question);
match push_result {
Ok(_) => {
current_message_len += question_len;
message.resize(current_message_len, 0)
},
Err(err_str) => return Err(err_str),
}
} else {
return Err("No question parameter specified or question length is 0.".to_string());
}
if hashed_pin_len > 0 {
message.extend_from_slice(password);
current_message_len += hashed_pin_len;
}
if session_info_len > 0 {
let real_len = session_info.len();
message.resize(current_message_len + session_info_len - real_len, 0);
message.extend_from_slice(session_info);
}
if timestamp_len > 0 {
let timestamp_parsed_be = timestamp_parsed.to_be();
let timestamp_ptr: &[u8] = unsafe { ::std::slice::from_raw_parts(×tamp_parsed_be as *const u64 as *const u8, 8) };
message.extend_from_slice(timestamp_ptr);
}
let result: u64 = match hotp_sha_type {
HashType::SHA1 => hmac_and_truncate::<Sha1>(key, message.as_slice(), num_of_digits),
HashType::SHA256 => hmac_and_truncate::<Sha256>(key, message.as_slice(), num_of_digits),
HashType::SHA512 => hmac_and_truncate::<Sha512>(key, message.as_slice(), num_of_digits),
};
Ok(result)
}
fn parse_session_info_len(session_info: &str) -> Result<usize, String> {
let (_, num) = session_info.split_at(1);
match num {
"064" => Ok(64),
"128" => Ok(128),
"256" => Ok(256),
"512" => Ok(512),
_ => Err("Wrong session info length. Possible values: 064, 128, 256, 512.".to_string()),
}
}
fn parse_timestamp_format(timestamp: &str) -> Result<usize, String> {
let (_, time_step) = timestamp.split_at(1);
let (num_s, time_type) = time_step.split_at(time_step.len()-1);
let num = num_s.parse::<usize>().unwrap_or(0);
if num < 1 || num > 59 {
return Err("Wrong timestamp value.".to_string());
}
let coefficient: usize;
match time_type {
"S" => coefficient = num,
"M" => coefficient = num * 60,
"H" => {
if num < 49 {
coefficient = num * 60 * 60;
} else {
return Err("Time interval is too big. Use H <= 48.".to_string());
}
},
_ => return Err("Can't parse timestamp. S/M/H time intervals are supported.".to_string()),
}
Ok(coefficient)
}
fn parse_pin_sha_type(psha: &str) -> Result<(HashType, usize), String> {
let psha_local: String = psha.to_uppercase();
if psha_local.starts_with("PSHA") {
let (_, num) = psha_local.split_at(4);
match num {
"1" => Ok((HashType::SHA1, 20)),
"256" => Ok((HashType::SHA256, 32)),
"512" => Ok((HashType::SHA512, 64)),
_ => Err("Unknown SHA hash mode.".to_string()),
}
} else {
Err("Unknown hashing algorithm.".to_string())
}
}
fn push_correct_question(message: &mut Vec<u8>, q_info: (QType, usize), question: &str) -> Result<(), String> {
let (q_type, q_length) = q_info;
match q_type {
QType::A => {
let hex_representation: String = question.as_bytes().to_hex();
let mut hex_encoded: Vec<u8> = hex_representation.from_hex().unwrap();
message.append(hex_encoded.by_ref());
},
QType::N => {
if question.len() > 19 {
assert!(false, "Temporary limitation question.len() < 20 is exceeded.".to_string());
}
let q_as_u64: u64 = question.parse::<u64>().unwrap();
let mut q_as_hex_str: String = format!("{:X}", q_as_u64);
if q_as_hex_str.len() % 2 == 1 {
q_as_hex_str.push('0');
}
message.append(from_hex(q_as_hex_str.as_str()).unwrap().by_ref());
},
QType::H => {
if q_length % 2 == 0 {
message.append(from_hex(question).unwrap().by_ref());
} else {
let mut question_owned = String::with_capacity(q_length + 1);
question_owned.push_str(question);
question_owned.push('0');
message.append(from_hex(question_owned.as_str()).unwrap().by_ref());
}
},
};
Ok(())
}
enum QType {A, N, H}
fn ocra_parse_question(question: &str) -> Result<(QType, usize), String> {
assert_eq!(question.len(), 4);
let (type_str, len_str) = question.split_at(2);
let data: &[u8] = type_str.as_bytes();
assert!(data[0] == b'Q' || data[0] == b'q');
let q_type_result: Result<QType, String> = match data[1] {
b'a' | b'A' => Ok(QType::A),
b'n' | b'N' => Ok(QType::N),
b'h' | b'H' => Ok(QType::H),
_ => Err("This question type is not supported! Use A/N/H, please.".to_string()),
};
if q_type_result.is_err() {
return Err(q_type_result.err().unwrap().to_string());
}
let q_len_result = len_str.parse::<usize>();
if q_len_result.is_err() {
return Err("Can't parse question length.".to_string());
}
let q_len = q_len_result.unwrap();
if q_len < 4 || q_len > 64 {
return Err("Make sure you request question length such that 4 <= question_length <= 64.".to_string());
}
Ok((q_type_result.unwrap(), q_len))
}