291 lines
6.7 KiB
Rust
291 lines
6.7 KiB
Rust
use std::{
|
|
convert::TryInto,
|
|
fmt::{self, Debug, Formatter},
|
|
ops::Deref,
|
|
};
|
|
|
|
use chrono::{DateTime, Duration, Utc};
|
|
use serde::{
|
|
de::{
|
|
self,
|
|
Visitor,
|
|
},
|
|
Deserialize,
|
|
Deserializer,
|
|
Serialize,
|
|
};
|
|
|
|
#[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 {
|
|
#[must_use]
|
|
pub fn from_key (bytes: &[u8]) -> Self {
|
|
Self (blake3::hash (bytes))
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn encode_base64 (&self) -> String {
|
|
base64::encode (self.as_bytes ())
|
|
}
|
|
}
|
|
|
|
impl Deref for BlakeHashWrapper {
|
|
type Target = blake3::Hash;
|
|
|
|
fn deref (&self) -> &<Self as Deref>::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 <E: de::Error> (self, value: &str)
|
|
-> Result <Self::Value, E>
|
|
{
|
|
let bytes: Vec <u8> = 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 <D: Deserializer <'de>> (deserializer: D) -> Result <Self, D::Error> {
|
|
Ok (BlakeHashWrapper (deserializer.deserialize_str (BlakeHashVisitor)?))
|
|
}
|
|
}
|
|
|
|
impl Serialize for BlakeHashWrapper {
|
|
fn serialize <S: serde::Serializer> (&self, serializer: S) -> Result <S::Ok, S::Error>
|
|
{
|
|
serializer.serialize_str (&self.encode_base64 ())
|
|
}
|
|
}
|
|
|
|
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 <V: MaxValidDuration> {
|
|
name: String,
|
|
|
|
not_before: DateTime <Utc>,
|
|
not_after: DateTime <Utc>,
|
|
pub hash: BlakeHashWrapper,
|
|
|
|
#[serde (default)]
|
|
_phantom: std::marker::PhantomData <V>,
|
|
}
|
|
|
|
#[derive (Copy, Clone, Debug, PartialEq)]
|
|
pub enum KeyValidity {
|
|
Valid,
|
|
|
|
WrongKey (BlakeHashWrapper),
|
|
ClockIsBehind,
|
|
Expired,
|
|
DurationTooLong (Duration),
|
|
DurationNegative,
|
|
}
|
|
|
|
impl <V: MaxValidDuration> ScraperKey <V> {
|
|
pub fn new_30_day <S: Into <String>> (name: S, input: &[u8]) -> Self {
|
|
let now = Utc::now ();
|
|
|
|
Self {
|
|
name: name.into (),
|
|
not_before: now,
|
|
not_after: now + V::dur (),
|
|
hash: BlakeHashWrapper::from_key (input),
|
|
_phantom: Default::default (),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl <V: MaxValidDuration> ScraperKey <V> {
|
|
#[must_use]
|
|
pub fn is_valid (&self, now: DateTime <Utc>, 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;
|
|
}
|
|
|
|
Valid
|
|
}
|
|
}
|
|
|
|
#[cfg (test)]
|
|
mod tests {
|
|
use chrono::{Utc};
|
|
use serde_json::json;
|
|
use super::*;
|
|
use KeyValidity::*;
|
|
|
|
#[test]
|
|
fn roundtrip_tripcode () {
|
|
let tripcode = "m8vG/sQnsn/87CQ5Ob6wMJeAnMKtXYfCBLNZ1SrSkvI=";
|
|
|
|
let j = json! ({
|
|
"tripcode": tripcode,
|
|
});
|
|
|
|
let s = serde_json::to_string (&j).unwrap ();
|
|
|
|
let j = serde_json::from_str::<serde_json::Value> (&s).unwrap ();
|
|
assert_eq! (j, json! ({"tripcode": tripcode}));
|
|
}
|
|
|
|
#[test]
|
|
fn duration_negative () {
|
|
let zero_time = Utc::now ();
|
|
|
|
let key = ScraperKey::<Valid30Days> {
|
|
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::<Valid30Days> {
|
|
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::<Valid30Days> {
|
|
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 (29), 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::<Valid30Days> {
|
|
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"),
|
|
}
|
|
}
|
|
}
|
|
}
|