238 lines
5.4 KiB
Rust
238 lines
5.4 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 trait MaxValidDuration {
|
|
fn dur () -> Duration;
|
|
}
|
|
|
|
#[derive (Deserialize)]
|
|
pub struct ScraperKey {
|
|
pub name: String,
|
|
|
|
not_before: DateTime <Utc>,
|
|
not_after: DateTime <Utc>,
|
|
pub hash: BlakeHashWrapper,
|
|
}
|
|
|
|
#[derive (Copy, Clone, Debug, PartialEq)]
|
|
pub enum KeyValidity {
|
|
Valid,
|
|
|
|
WrongKey (BlakeHashWrapper),
|
|
ClockIsBehind,
|
|
Expired,
|
|
DurationTooLong (Duration),
|
|
DurationNegative,
|
|
}
|
|
|
|
impl ScraperKey {
|
|
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 + Duration::days (30),
|
|
hash: BlakeHashWrapper::from_key (input),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ScraperKey {
|
|
#[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;
|
|
}
|
|
|
|
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 {
|
|
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 ()),
|
|
};
|
|
|
|
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 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 + 60),
|
|
hash: BlakeHashWrapper::from_key ("bad_password".as_bytes ()),
|
|
};
|
|
|
|
for (input, expected) in &[
|
|
(zero_time + Duration::days (0), ClockIsBehind),
|
|
(zero_time + Duration::days (2), Valid),
|
|
(zero_time + Duration::days (60 - 1), Valid),
|
|
(zero_time + Duration::days (60 + 1), 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 ()),
|
|
};
|
|
|
|
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"),
|
|
}
|
|
}
|
|
}
|
|
}
|