#![allow(dead_code)]
use cfx_types::H256;
use itertools::Itertools;
use keccak_hash::keccak;
use log::{trace, warn};
use rand::{distributions::Alphanumeric, rngs::OsRng, Rng};
use std::{
fs,
io::{self, Read, Write},
mem,
path::Path,
time,
};
pub trait TimeProvider {
fn now(&self) -> u64;
}
impl<F: Fn() -> u64> TimeProvider for F {
fn now(&self) -> u64 { self() }
}
#[derive(Default)]
pub struct DefaultTimeProvider;
impl TimeProvider for DefaultTimeProvider {
fn now(&self) -> u64 {
time::UNIX_EPOCH
.elapsed()
.expect("Valid time has to be set in your system.")
.as_secs()
}
}
const TIME_THRESHOLD: u64 = 7;
const TOKEN_LENGTH: usize = 16;
const SEPARATOR: &str = ";";
const UNUSED_TOKEN_TIMEOUT: u64 = 3600 * 24; struct Code {
code: String,
created_at: time::Duration,
last_used_at: Option<time::Duration>,
}
fn decode_time(val: &str) -> Option<time::Duration> {
let time = val.parse::<u64>().ok();
time.map(time::Duration::from_secs)
}
fn encode_time(time: time::Duration) -> String { format!("{}", time.as_secs()) }
pub struct AuthCodes<T: TimeProvider = DefaultTimeProvider> {
codes: Vec<Code>,
now: T,
}
impl AuthCodes<DefaultTimeProvider> {
pub fn from_file(file: &Path) -> io::Result<AuthCodes> {
let content = {
if let Ok(mut file) = fs::File::open(file) {
let mut s = String::new();
let _ = file.read_to_string(&mut s)?;
s
} else {
"".into()
}
};
let time_provider = DefaultTimeProvider::default();
let codes = content
.lines()
.filter_map(|line| {
let mut parts = line.split(SEPARATOR);
let token = parts.next();
let created = parts.next();
let used = parts.next();
match token {
None => None,
Some(token) if token.len() < TOKEN_LENGTH => None,
Some(token) => Some(Code {
code: token.into(),
last_used_at: used.and_then(decode_time),
created_at: created
.and_then(decode_time)
.unwrap_or_else(|| {
time::Duration::from_secs(time_provider.now())
}),
}),
}
})
.collect();
Ok(AuthCodes {
codes,
now: time_provider,
})
}
}
impl<T: TimeProvider> AuthCodes<T> {
pub fn to_file(&self, file: &Path) -> io::Result<()> {
let mut file = fs::File::create(file)?;
let content = self
.codes
.iter()
.map(|code| {
let mut data =
vec![code.code.clone(), encode_time(code.created_at)];
if let Some(used_at) = code.last_used_at {
data.push(encode_time(used_at));
}
data.join(SEPARATOR)
})
.join("\n");
file.write_all(content.as_bytes())
}
pub fn new(codes: Vec<String>, now: T) -> Self {
AuthCodes {
codes: codes
.into_iter()
.map(|code| Code {
code,
created_at: time::Duration::from_secs(now.now()),
last_used_at: None,
})
.collect(),
now,
}
}
pub fn is_valid(&mut self, hash: &H256, time: u64) -> bool {
let now = self.now.now();
if time >= now + TIME_THRESHOLD || time <= now - TIME_THRESHOLD {
warn!(target: "signer", "Received old authentication request. ({} vs {})", now, time);
return false;
}
let as_token = |code| keccak(format!("{}:{}", code, time));
for code in &mut self.codes {
if &as_token(&code.code) == hash {
code.last_used_at = Some(time::Duration::from_secs(now));
return true;
}
}
false
}
pub fn generate_new(&mut self) -> io::Result<String> {
let code = OsRng
.sample_iter(&Alphanumeric)
.take(TOKEN_LENGTH)
.collect::<String>();
let readable_code = code
.as_bytes()
.chunks(4)
.filter_map(|f| String::from_utf8(f.to_vec()).ok())
.collect::<Vec<String>>()
.join("-");
trace!(target: "signer", "New authentication token generated.");
self.codes.push(Code {
code,
created_at: time::Duration::from_secs(self.now.now()),
last_used_at: None,
});
Ok(readable_code)
}
pub fn is_empty(&self) -> bool { self.codes.is_empty() }
pub fn clear_garbage(&mut self) {
let now = self.now.now();
let threshold =
time::Duration::from_secs(now.saturating_sub(UNUSED_TOKEN_TIMEOUT));
let codes = mem::replace(&mut self.codes, Vec::new());
for code in codes {
if code.last_used_at.is_none() && code.created_at <= threshold {
continue;
}
self.codes.push(code);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use cfx_types::H256;
use keccak_hash::keccak;
use std::{
cell::Cell,
fs,
io::{Read, Write},
time,
};
use tempdir::TempDir;
fn generate_hash(val: &str, time: u64) -> H256 {
keccak(format!("{}:{}", val, time))
}
#[test]
fn should_return_false_even_if_code_is_initial_and_store_is_empty() {
let code = "initial";
let time = 99;
let mut codes = AuthCodes::new(vec![], || 100);
let res1 = codes.is_valid(&generate_hash(code, time), time);
let res2 = codes.is_valid(&generate_hash(code, time), time);
assert_eq!(res1, false);
assert_eq!(res2, false);
}
#[test]
fn should_return_true_if_hash_is_valid() {
let code = "23521352asdfasdfadf";
let time = 99;
let mut codes = AuthCodes::new(vec![code.into()], || 100);
let res = codes.is_valid(&generate_hash(code, time), time);
assert_eq!(res, true);
}
#[test]
fn should_return_false_if_code_is_unknown() {
let code = "23521352asdfasdfadf";
let time = 99;
let mut codes = AuthCodes::new(vec!["1".into()], || 100);
let res = codes.is_valid(&generate_hash(code, time), time);
assert_eq!(res, false);
}
#[test]
fn should_return_false_if_hash_is_valid_but_time_is_invalid() {
let code = "23521352asdfasdfadf";
let time = 107;
let time2 = 93;
let mut codes = AuthCodes::new(vec![code.into()], || 100);
let res1 = codes.is_valid(&generate_hash(code, time), time);
let res2 = codes.is_valid(&generate_hash(code, time2), time2);
assert_eq!(res1, false);
assert_eq!(res2, false);
}
#[test]
fn should_read_old_format_from_file() {
let tempdir = TempDir::new("").unwrap();
let file_path = tempdir.path().join("file");
let code = "23521352asdfasdfadf";
{
let mut file = fs::File::create(&file_path).unwrap();
file.write_all(b"a\n23521352asdfasdfadf\nb\n").unwrap();
}
let mut authcodes = AuthCodes::from_file(&file_path).unwrap();
let time = time::UNIX_EPOCH.elapsed().unwrap().as_secs();
assert!(
authcodes.is_valid(&generate_hash(code, time), time),
"Code should be read from file"
);
}
#[test]
fn should_remove_old_unused_tokens() {
let tempdir = TempDir::new("").unwrap();
let file_path = tempdir.path().join("file");
let code1 = "11111111asdfasdf111";
let code2 = "22222222asdfasdf222";
let code3 = "33333333asdfasdf333";
let time = Cell::new(100);
let mut codes = AuthCodes::new(
vec![code1.into(), code2.into(), code3.into()],
|| time.get(),
);
codes.is_valid(&generate_hash(code2, time.get()), time.get());
time.set(100 + 10_000_000);
codes.is_valid(&generate_hash(code1, time.get()), time.get());
let new_code = codes.generate_new().unwrap().replace('-', "");
codes.clear_garbage();
codes.to_file(&file_path).unwrap();
let mut content = String::new();
let mut file = fs::File::open(&file_path).unwrap();
file.read_to_string(&mut content).unwrap();
assert_eq!(
content,
format!(
"{};100;10000100\n{};100;100\n{};10000100",
code1, code2, new_code
)
);
}
}