use hmacsha1::hmac_sha1;
use std::{error, fmt, result};
use std::time::{SystemTime, UNIX_EPOCH};
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
#[cfg(feature = "with-qrcode")]
use qrcode::render::svg;
#[cfg(feature = "with-qrcode")]
use qrcode::{EcLevel, QrCode};
#[cfg(any(feature = "with-qrcode"))]
use qrcode::types::QrError;
const SECRET_MAX_LEN: usize = 128;
const SECRET_MIN_LEN: usize = 16;
#[derive(Copy, Clone)]
pub enum ErrorCorrectionLevel {
Low,
Medium,
Quartile,
High,
}
use self::ErrorCorrectionLevel::*;
impl fmt::Display for ErrorCorrectionLevel {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let result = match self {
Low => 'L',
Medium => 'M',
Quartile => 'Q',
High => 'H',
};
write!(f, "{}", result)
}
}
#[cfg(feature = "with-qrcode")]
impl From<ErrorCorrectionLevel> for qrcode::EcLevel {
fn from(level: ErrorCorrectionLevel) -> Self {
match level {
ErrorCorrectionLevel::High => EcLevel::H,
ErrorCorrectionLevel::Medium => EcLevel::M,
ErrorCorrectionLevel::Quartile => EcLevel::Q,
ErrorCorrectionLevel::Low => EcLevel::L,
}
}
}
const ALPHABET: [char; 33] = [
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '2', '3', '4', '5', '6', '7', '=',
];
pub struct GoogleAuthenticator {
code_len: usize,
}
impl Default for GoogleAuthenticator {
fn default() -> Self {
Self { code_len: 6 }
}
}
impl GoogleAuthenticator {
pub fn new() -> Self {
Self::default()
}
pub fn with_code_length(mut self, code_length: usize) -> Self {
self.code_len = code_length;
self
}
pub fn create_secret(&self, length: u8) -> String {
let mut secret = Vec::<char>::new();
let mut index: usize;
for _ in 0..length {
index = (rand::random::<u8>() & 0x1F) as usize;
secret.push(ALPHABET[index]);
}
secret.into_iter().collect()
}
pub fn get_code(&self, secret: &str, times_slice: u64) -> Result<String> {
if secret.len() < SECRET_MIN_LEN || secret.len() > SECRET_MAX_LEN {
return Err(GAError::Error(
"bad secret length. must be less than 128 and more than 16, recommend 32",
));
}
let message = if times_slice == 0 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs()
/ 30
} else {
times_slice
};
let key = Self::base32_decode(secret)?;
let msg_bytes = message.to_be_bytes();
let hash = hmac_sha1(&key, &msg_bytes);
let offset = hash[hash.len() - 1] & 0x0F;
let mut truncated_hash: [u8; 4] = Default::default();
truncated_hash.copy_from_slice(&hash[offset as usize..(offset + 4) as usize]);
let mut code = i32::from_be_bytes(truncated_hash);
code &= 0x7FFFFFFF;
code %= 1_000_000;
let mut code_str = code.to_string();
for i in 0..(self.code_len - code_str.len()) {
code_str.insert(i, '0');
}
Ok(code_str)
}
pub fn verify_code(&self, secret: &str, code: &str, discrepancy: u64, time_slice: u64) -> bool {
if code.len() != self.code_len {
return false;
}
let curr_time_slice = if time_slice == 0 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs()
/ 30
} else {
time_slice
};
let start_time = curr_time_slice.saturating_sub(discrepancy);
let end_time = curr_time_slice.saturating_add(discrepancy + 1);
for _time_slice in start_time..end_time {
if let Ok(c) = self.get_code(secret, _time_slice) {
if code == c {
return true;
}
}
}
false
}
pub fn qr_code_url(
&self,
secret: &str,
name: &str,
title: &str,
width: u32,
height: u32,
level: ErrorCorrectionLevel,
) -> String {
let width = if width == 0 { 200 } else { width };
let height = if height == 0 { 200 } else { height };
let scheme = Self::create_scheme(name, secret, title);
let scheme = utf8_percent_encode(&scheme, NON_ALPHANUMERIC);
format!(
"https://chart.googleapis.com/chart?chs={}x{}&chld={}|0&cht=qr&chl={}",
width, height, level, scheme
)
}
#[cfg(feature = "with-qrcode")]
pub fn qr_code(
&self,
secret: &str,
name: &str,
title: &str,
width: u32,
height: u32,
level: ErrorCorrectionLevel,
) -> Result<String> {
let width = if width == 0 { 200 } else { width };
let height = if height == 0 { 200 } else { height };
let scheme = Self::create_scheme(name, secret, title);
let code = QrCode::with_error_correction_level(scheme.as_bytes(), level.into())?;
Ok(code
.render()
.min_dimensions(width, height)
.dark_color(svg::Color("#000000"))
.light_color(svg::Color("#ffffff"))
.build())
}
fn create_scheme(name: &str, secret: &str, title: &str) -> String {
let name = utf8_percent_encode(name, NON_ALPHANUMERIC);
let title = utf8_percent_encode(title, NON_ALPHANUMERIC);
format!("otpauth://totp/{}?secret={}&issuer={}", name, secret, title)
}
fn base32_decode(secret: &str) -> Result<Vec<u8>> {
match base32::decode(base32::Alphabet::RFC4648 { padding: true }, secret) {
Some(_decode_str) => Ok(_decode_str),
_ => Err(GAError::Error("secret must be base32 decodeable.")),
}
}
}
#[derive(Debug)]
pub enum GAError {
Error(&'static str),
#[cfg(any(feature = "with-qrcode"))]
QrError(QrError),
}
impl error::Error for GAError {
fn description(&self) -> &str {
match *self {
GAError::Error(description) => description,
#[cfg(any(feature = "with-qrcode"))]
GAError::QrError(ref _err) => "",
}
}
fn cause(&self) -> Option<&dyn error::Error> {
match *self {
#[cfg(any(feature = "with-qrcode"))]
GAError::QrError(ref _err) => None,
GAError::Error(_) => None,
}
}
}
#[cfg(any(feature = "with-qrcode"))]
impl From<QrError> for GAError {
fn from(err: QrError) -> GAError {
GAError::QrError(err)
}
}
impl fmt::Display for GAError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
GAError::Error(desc) => f.write_str(desc),
#[cfg(any(feature = "with-qrcode"))]
GAError::QrError(ref err) => fmt::Display::fmt(err, f),
}
}
}
pub type Result<T> = result::Result<T, GAError>;