use std::{ convert::TryInto, fmt::{self, Debug, Formatter}, ops::Deref, }; use chrono::{DateTime, Duration, Utc}; use serde::{ de::{ self, Visitor, }, Deserialize, Deserializer, }; #[derive (Copy, Clone, PartialEq, Eq)] pub struct BlakeHashWrapper (blake3::Hash); impl Debug for BlakeHashWrapper { fn fmt (&self, f: &mut Formatter <'_>) -> Result <(), fmt::Error> { write! (f, "{}", self.encode_base64 ()) } } impl BlakeHashWrapper { pub fn from_key (bytes: &[u8]) -> Self { Self (blake3::hash (bytes)) } pub fn encode_base64 (&self) -> String { base64::encode (self.as_bytes ()) } } impl Deref for BlakeHashWrapper { type Target = blake3::Hash; fn deref (&self) -> &::Target { &self.0 } } struct BlakeHashVisitor; impl <'de> Visitor <'de> for BlakeHashVisitor { type Value = blake3::Hash; fn expecting (&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str ("a 32-byte blake3 hash, encoded as base64") } fn visit_str (self, value: &str) -> Result { let bytes: Vec = base64::decode (value).map_err (|_| E::custom (format! ("str is not base64: {}", value)))?; let bytes: [u8; 32] = (&bytes [..]).try_into ().map_err (|_| E::custom (format! ("decode base64 is not 32 bytes long: {}", value)))?; let tripcode = blake3::Hash::from (bytes); Ok (tripcode) } } impl <'de> Deserialize <'de> for BlakeHashWrapper { fn deserialize > (deserializer: D) -> Result { Ok (BlakeHashWrapper (deserializer.deserialize_str (BlakeHashVisitor)?)) } } pub struct Valid7Days; pub struct Valid30Days; //pub struct Valid90Days; pub trait MaxValidDuration { fn dur () -> Duration; } impl MaxValidDuration for Valid7Days { fn dur () -> Duration { Duration::days (7) } } impl MaxValidDuration for Valid30Days { fn dur () -> Duration { Duration::days (30) } } #[derive (Deserialize)] pub struct ScraperKey { name: String, not_before: DateTime , not_after: DateTime , pub hash: BlakeHashWrapper, #[serde (default)] _phantom: std::marker::PhantomData , } #[derive (Copy, Clone, Debug, PartialEq)] pub enum KeyValidity { Valid, WrongKey (BlakeHashWrapper), ClockIsBehind, Expired, DurationTooLong (Duration), DurationNegative, } impl ScraperKey { pub fn new_30_day > (name: S, input: &[u8]) -> Self { let now = Utc::now (); Self { name: name.into (), not_before: now, not_after: now + Duration::days (7), hash: BlakeHashWrapper::from_key (input), _phantom: Default::default (), } } } impl ScraperKey { pub fn is_valid (&self, now: DateTime , input: &[u8]) -> KeyValidity { use KeyValidity::*; // I put this first because I think the constant-time check should run // before anything else. But I'm not a crypto expert, so it's just // guesswork. let input_hash = BlakeHashWrapper::from_key (input); if input_hash != self.hash { return WrongKey (input_hash); } if self.not_after < self.not_before { return DurationNegative; } let max_dur = V::dur (); let actual_dur = self.not_after - self.not_before; if actual_dur > max_dur { return DurationTooLong (max_dur); } if now >= self.not_after { return Expired; } if now < self.not_before { return ClockIsBehind; } return Valid; } } #[cfg (test)] mod tests { use chrono::{Utc}; use super::*; use KeyValidity::*; #[test] fn duration_negative () { let zero_time = Utc::now (); let key = ScraperKey:: { name: "automated testing".to_string (), not_before: zero_time + Duration::days (1 + 2), not_after: zero_time + Duration::days (1), hash: BlakeHashWrapper::from_key ("bad_password".as_bytes ()), _phantom: Default::default (), }; let err = DurationNegative; for (input, expected) in &[ (zero_time + Duration::days (0), err), (zero_time + Duration::days (2), err), (zero_time + Duration::days (100), err), ] { assert_eq! (key.is_valid (*input, "bad_password".as_bytes ()), *expected); } } #[test] fn key_valid_too_long () { let zero_time = Utc::now (); let key = ScraperKey:: { name: "automated testing".to_string (), not_before: zero_time + Duration::days (1), not_after: zero_time + Duration::days (1 + 31), hash: BlakeHashWrapper::from_key ("bad_password".as_bytes ()), _phantom: Default::default (), }; let err = DurationTooLong (Duration::days (30)); for (input, expected) in &[ (zero_time + Duration::days (0), err), (zero_time + Duration::days (2), err), (zero_time + Duration::days (100), err), ] { assert_eq! (key.is_valid (*input, "bad_password".as_bytes ()), *expected); } } #[test] fn normal_key () { let zero_time = Utc::now (); let key = ScraperKey:: { name: "automated testing".to_string (), not_before: zero_time + Duration::days (1), not_after: zero_time + Duration::days (1 + 30), hash: BlakeHashWrapper::from_key ("bad_password".as_bytes ()), _phantom: Default::default (), }; for (input, expected) in &[ (zero_time + Duration::days (0), ClockIsBehind), (zero_time + Duration::days (2), Valid), (zero_time + Duration::days (1 + 30), Expired), (zero_time + Duration::days (100), Expired), ] { assert_eq! (key.is_valid (*input, "bad_password".as_bytes ()), *expected); } } #[test] fn wrong_key () { let zero_time = Utc::now (); let key = ScraperKey:: { name: "automated testing".to_string (), not_before: zero_time + Duration::days (1), not_after: zero_time + Duration::days (1 + 30), hash: BlakeHashWrapper::from_key ("bad_password".as_bytes ()), _phantom: Default::default (), }; for input in &[ zero_time + Duration::days (0), zero_time + Duration::days (2), zero_time + Duration::days (1 + 30), zero_time + Duration::days (100), ] { let validity = key.is_valid (*input, "badder_password".as_bytes ()); match validity { WrongKey (_) => (), _ => panic! ("Expected WrongKey here"), } } } }