⭐ new: add code for scraper keys to expire and have limited durations
parent
bf8e483d16
commit
0eb1e7e38f
|
@ -187,6 +187,7 @@ dependencies = [
|
|||
"libc",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"time",
|
||||
"wasm-bindgen",
|
||||
"winapi 0.3.9",
|
||||
|
|
|
@ -10,7 +10,7 @@ license = "AGPL-3.0"
|
|||
|
||||
base64 = "0.12.3"
|
||||
blake3 = "0.3.7"
|
||||
chrono = "0.4.19"
|
||||
chrono = {version = "0.4.19", features = ["serde"]}
|
||||
dashmap = "3.11.10"
|
||||
futures = "0.3.7"
|
||||
handlebars = "3.5.1"
|
||||
|
|
|
@ -3,78 +3,20 @@
|
|||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
convert::{TryFrom, TryInto},
|
||||
fmt,
|
||||
convert::{TryFrom},
|
||||
iter::FromIterator,
|
||||
ops::Deref,
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use serde::{
|
||||
Deserialize,
|
||||
Deserializer,
|
||||
de::{
|
||||
self,
|
||||
Visitor,
|
||||
},
|
||||
};
|
||||
|
||||
use crate::errors::ConfigError;
|
||||
|
||||
pub struct BlakeHashWrapper (blake3::Hash);
|
||||
|
||||
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) -> &<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)?))
|
||||
}
|
||||
}
|
||||
|
||||
// Stuff we need to load from the config file and use to
|
||||
// set up the HTTP server
|
||||
|
||||
pub mod file {
|
||||
use serde::Deserialize;
|
||||
|
||||
use super::*;
|
||||
use crate::key_validity::*;
|
||||
|
||||
#[derive (Deserialize)]
|
||||
pub struct Server {
|
||||
|
@ -84,16 +26,21 @@ pub mod file {
|
|||
pub display_name: Option <String>,
|
||||
}
|
||||
|
||||
#[derive (Deserialize)]
|
||||
pub struct DevMode {
|
||||
pub scraper_key: Option <ScraperKey <Valid7Days>>,
|
||||
}
|
||||
|
||||
// Stuff that's identical between the file and the runtime structures
|
||||
|
||||
#[derive (Default, Deserialize)]
|
||||
pub struct Isomorphic {
|
||||
#[serde (default)]
|
||||
pub enable_dev_mode: bool,
|
||||
#[serde (default)]
|
||||
pub enable_scraper_auth: bool,
|
||||
|
||||
pub dev_scraper_key: Option <BlakeHashWrapper>,
|
||||
// If any of these fields are used, we are in dev mode and have to
|
||||
// show extra warnings, since some auth may be weakened
|
||||
pub dev_mode: Option <DevMode>,
|
||||
}
|
||||
|
||||
#[derive (Deserialize)]
|
||||
|
|
|
@ -0,0 +1,222 @@
|
|||
use std::{
|
||||
convert::TryInto,
|
||||
fmt,
|
||||
ops::Deref,
|
||||
};
|
||||
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use serde::{
|
||||
de::{
|
||||
self,
|
||||
Visitor,
|
||||
},
|
||||
Deserialize,
|
||||
Deserializer,
|
||||
};
|
||||
|
||||
pub struct BlakeHashWrapper (blake3::Hash);
|
||||
|
||||
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) -> &<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)?))
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive (Deserialize)]
|
||||
pub struct ScraperKey <V: MaxValidDuration> {
|
||||
pub not_before: DateTime <Utc>,
|
||||
pub not_after: DateTime <Utc>,
|
||||
pub hash: BlakeHashWrapper,
|
||||
_phantom: std::marker::PhantomData <V>,
|
||||
}
|
||||
|
||||
#[derive (Copy, Clone, Debug, PartialEq)]
|
||||
pub enum KeyValidity {
|
||||
Valid,
|
||||
|
||||
WrongKey,
|
||||
ClockIsBehind,
|
||||
Expired,
|
||||
DurationTooLong (Duration),
|
||||
DurationNegative,
|
||||
}
|
||||
|
||||
impl <V: MaxValidDuration> ScraperKey <V> {
|
||||
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.
|
||||
if blake3::hash (input) != *self.hash {
|
||||
return WrongKey;
|
||||
}
|
||||
|
||||
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::<Valid7Days> {
|
||||
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::<Valid7Days> {
|
||||
not_before: zero_time + Duration::days (1),
|
||||
not_after: zero_time + Duration::days (1 + 8),
|
||||
hash: BlakeHashWrapper::from_key ("bad_password".as_bytes ()),
|
||||
_phantom: Default::default (),
|
||||
};
|
||||
|
||||
let err = DurationTooLong (Duration::days (7));
|
||||
|
||||
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::<Valid7Days> {
|
||||
not_before: zero_time + Duration::days (1),
|
||||
not_after: zero_time + Duration::days (1 + 7),
|
||||
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 + 7), 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::<Valid7Days> {
|
||||
not_before: zero_time + Duration::days (1),
|
||||
not_after: zero_time + Duration::days (1 + 7),
|
||||
hash: BlakeHashWrapper::from_key ("bad_password".as_bytes ()),
|
||||
_phantom: Default::default (),
|
||||
};
|
||||
|
||||
for (input, expected) in &[
|
||||
(zero_time + Duration::days (0), WrongKey),
|
||||
(zero_time + Duration::days (2), WrongKey),
|
||||
(zero_time + Duration::days (1 + 7), WrongKey),
|
||||
(zero_time + Duration::days (100), WrongKey),
|
||||
] {
|
||||
assert_eq! (key.is_valid (*input, "badder_password".as_bytes ()), *expected);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -62,6 +62,8 @@ use ptth_core::{
|
|||
pub mod config;
|
||||
pub mod errors;
|
||||
pub mod git_version;
|
||||
pub mod key_validity;
|
||||
|
||||
mod server_endpoint;
|
||||
|
||||
pub use config::Config;
|
||||
|
@ -495,10 +497,9 @@ async fn reload_config (
|
|||
(*config) = new_config;
|
||||
|
||||
debug! ("Loaded {} server configs", config.servers.len ());
|
||||
debug! ("enable_dev_mode: {}", config.iso.enable_dev_mode);
|
||||
debug! ("enable_scraper_auth: {}", config.iso.enable_scraper_auth);
|
||||
|
||||
if config.iso.enable_dev_mode {
|
||||
if config.iso.dev_mode.is_some () {
|
||||
error! ("Dev mode is enabled! This might turn off some security features. If you see this in production, escalate it to someone!");
|
||||
}
|
||||
|
||||
|
|
|
@ -33,8 +33,6 @@ stronger is ready.
|
|||
|
||||
- (X) Add feature flags to ptth_relay.toml for dev mode and scrapers
|
||||
- (X) Make sure Docker release CAN build
|
||||
- ( ) Add failing test to block releases
|
||||
- ( ) Make sure `cargo test` fails and Docker release can NOT build
|
||||
- ( ) Add hash of 1 scraper key to ptth_relay.toml, with 1 week expiration
|
||||
- ( ) (POC) Test with curl
|
||||
- ( ) Manually create SQLite DB for scraper keys, add 1 hash
|
||||
|
|
|
@ -18,7 +18,7 @@ fn end_to_end () {
|
|||
use reqwest::Client;
|
||||
use tracing::{debug, info};
|
||||
|
||||
use ptth_relay::config::BlakeHashWrapper;
|
||||
use ptth_relay::key_validity::BlakeHashWrapper;
|
||||
|
||||
// Prefer this form for tests, since all tests share one process
|
||||
// and we don't care if another test already installed a subscriber.
|
||||
|
|
Loading…
Reference in New Issue