Tokens retention policy
This commit is contained in:
@@ -16,12 +16,10 @@
|
||||
|
||||
use rand::Rng;
|
||||
use rand::os::OsRng;
|
||||
use std::io;
|
||||
use std::io::{Read, Write};
|
||||
use std::fs;
|
||||
use std::io::{self, Read, Write};
|
||||
use std::path::Path;
|
||||
use std::time;
|
||||
use util::{H256, Hashable};
|
||||
use std::{fs, time, mem};
|
||||
use util::{H256, Hashable, Itertools};
|
||||
|
||||
/// Providing current time in seconds
|
||||
pub trait TimeProvider {
|
||||
@@ -47,12 +45,35 @@ impl TimeProvider for DefaultTimeProvider {
|
||||
|
||||
/// No of seconds the hash is valid
|
||||
const TIME_THRESHOLD: u64 = 7;
|
||||
/// minimal length of hash
|
||||
const TOKEN_LENGTH: usize = 16;
|
||||
/// special "initial" token used for authorization when there are no tokens yet.
|
||||
const INITIAL_TOKEN: &'static str = "initial";
|
||||
/// Separator between fields in serialized tokens file.
|
||||
const SEPARATOR: &'static str = ";";
|
||||
/// Number of seconds to keep unused tokens.
|
||||
const UNUSED_TOKEN_TIMEOUT: u64 = 3600 * 24; // a day
|
||||
|
||||
struct Code {
|
||||
code: String,
|
||||
/// Duration since unix_epoch
|
||||
created_at: time::Duration,
|
||||
/// Duration since unix_epoch
|
||||
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())
|
||||
}
|
||||
|
||||
/// Manages authorization codes for `SignerUIs`
|
||||
pub struct AuthCodes<T: TimeProvider = DefaultTimeProvider> {
|
||||
codes: Vec<String>,
|
||||
codes: Vec<Code>,
|
||||
now: T,
|
||||
}
|
||||
|
||||
@@ -69,13 +90,32 @@ impl AuthCodes<DefaultTimeProvider> {
|
||||
"".into()
|
||||
}
|
||||
};
|
||||
let time_provider = DefaultTimeProvider::default();
|
||||
|
||||
let codes = content.lines()
|
||||
.filter(|f| f.len() >= TOKEN_LENGTH)
|
||||
.map(String::from)
|
||||
.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: codes,
|
||||
now: DefaultTimeProvider::default(),
|
||||
now: time_provider,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -86,19 +126,30 @@ impl<T: TimeProvider> AuthCodes<T> {
|
||||
/// Writes all `AuthCodes` to a disk.
|
||||
pub fn to_file(&self, file: &Path) -> io::Result<()> {
|
||||
let mut file = try!(fs::File::create(file));
|
||||
let content = self.codes.join("\n");
|
||||
let content = self.codes.iter().map(|code| {
|
||||
let mut data = vec![code.code.clone(), encode_time(code.created_at.clone())];
|
||||
if let Some(used_at) = code.last_used_at.clone() {
|
||||
data.push(encode_time(used_at));
|
||||
}
|
||||
data.join(SEPARATOR)
|
||||
}).join("\n");
|
||||
file.write_all(content.as_bytes())
|
||||
}
|
||||
|
||||
/// Creates a new `AuthCodes` store with given `TimeProvider`.
|
||||
pub fn new(codes: Vec<String>, now: T) -> Self {
|
||||
AuthCodes {
|
||||
codes: codes,
|
||||
codes: codes.into_iter().map(|code| Code {
|
||||
code: code,
|
||||
created_at: time::Duration::from_secs(now.now()),
|
||||
last_used_at: None,
|
||||
}).collect(),
|
||||
now: now,
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if given hash is correct identifier of `SignerUI`
|
||||
/// Checks if given hash is correct authcode of `SignerUI`
|
||||
/// Updates this hash last used field in case it's valid.
|
||||
#[cfg_attr(feature="dev", allow(wrong_self_convention))]
|
||||
pub fn is_valid(&mut self, hash: &H256, time: u64) -> bool {
|
||||
let now = self.now.now();
|
||||
@@ -121,8 +172,14 @@ impl<T: TimeProvider> AuthCodes<T> {
|
||||
}
|
||||
|
||||
// look for code
|
||||
self.codes.iter()
|
||||
.any(|code| &as_token(code) == hash)
|
||||
for mut code in &mut self.codes {
|
||||
if &as_token(&code.code) == hash {
|
||||
code.last_used_at = Some(time::Duration::from_secs(now));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Generates and returns a new code that can be used by `SignerUIs`
|
||||
@@ -135,7 +192,11 @@ impl<T: TimeProvider> AuthCodes<T> {
|
||||
.collect::<Vec<String>>()
|
||||
.join("-");
|
||||
trace!(target: "signer", "New authentication token generated.");
|
||||
self.codes.push(code);
|
||||
self.codes.push(Code {
|
||||
code: code,
|
||||
created_at: time::Duration::from_secs(self.now.now()),
|
||||
last_used_at: None,
|
||||
});
|
||||
Ok(readable_code)
|
||||
}
|
||||
|
||||
@@ -143,12 +204,31 @@ impl<T: TimeProvider> AuthCodes<T> {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.codes.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes old tokens that have not been used since creation.
|
||||
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 {
|
||||
// Skip codes that are old and were never used.
|
||||
if code.last_used_at.is_none() && code.created_at <= threshold {
|
||||
continue;
|
||||
}
|
||||
self.codes.push(code);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use devtools;
|
||||
use std::io::{Read, Write};
|
||||
use std::{time, fs};
|
||||
use std::cell::Cell;
|
||||
|
||||
use util::{H256, Hashable};
|
||||
use super::*;
|
||||
|
||||
@@ -217,6 +297,54 @@ mod tests {
|
||||
assert_eq!(res2, false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_read_old_format_from_file() {
|
||||
// given
|
||||
let path = devtools::RandomTempPath::new();
|
||||
let code = "23521352asdfasdfadf";
|
||||
{
|
||||
let mut file = fs::File::create(&path).unwrap();
|
||||
file.write_all(b"a\n23521352asdfasdfadf\nb\n").unwrap();
|
||||
}
|
||||
|
||||
// when
|
||||
let mut authcodes = AuthCodes::from_file(&path).unwrap();
|
||||
let time = time::UNIX_EPOCH.elapsed().unwrap().as_secs();
|
||||
|
||||
// then
|
||||
assert!(authcodes.is_valid(&generate_hash(code, time), time), "Code should be read from file");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_remove_old_unused_tokens() {
|
||||
// given
|
||||
let path = devtools::RandomTempPath::new();
|
||||
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());
|
||||
// `code2` should not be removed (we never remove tokens that were used)
|
||||
codes.is_valid(&generate_hash(code2, time.get()), time.get());
|
||||
|
||||
// when
|
||||
time.set(100 + 10_000_000);
|
||||
// mark `code1` as used now
|
||||
codes.is_valid(&generate_hash(code1, time.get()), time.get());
|
||||
|
||||
let new_code = codes.generate_new().unwrap().replace('-', "");
|
||||
codes.clear_garbage();
|
||||
codes.to_file(&path).unwrap();
|
||||
|
||||
// then
|
||||
let mut content = String::new();
|
||||
let mut file = fs::File::open(&path).unwrap();
|
||||
file.read_to_string(&mut content).unwrap();
|
||||
|
||||
assert_eq!(content, format!("{};100;10000100\n{};100;100\n{};10000100", code1, code2, new_code));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -94,6 +94,9 @@ fn auth_is_valid(codes_path: &Path, protocols: ws::Result<Vec<&str>>) -> bool {
|
||||
// Check if the code is valid
|
||||
AuthCodes::from_file(codes_path)
|
||||
.map(|mut codes| {
|
||||
// remove old tokens
|
||||
codes.clear_garbage();
|
||||
|
||||
let res = codes.is_valid(&auth, time);
|
||||
// make sure to save back authcodes - it might have been modified
|
||||
if let Err(_) = codes.to_file(codes_path) {
|
||||
|
||||
Reference in New Issue
Block a user