diff --git a/Cargo.lock b/Cargo.lock index ad87ae5..dce88da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -187,6 +187,7 @@ dependencies = [ "libc", "num-integer", "num-traits", + "serde", "time", "wasm-bindgen", "winapi 0.3.9", diff --git a/crates/ptth_relay/Cargo.toml b/crates/ptth_relay/Cargo.toml index 938364f..58d03e3 100644 --- a/crates/ptth_relay/Cargo.toml +++ b/crates/ptth_relay/Cargo.toml @@ -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" diff --git a/crates/ptth_relay/src/config.rs b/crates/ptth_relay/src/config.rs index cfcde8a..b444810 100644 --- a/crates/ptth_relay/src/config.rs +++ b/crates/ptth_relay/src/config.rs @@ -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) -> &::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)?)) - } -} - // 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 , } + #[derive (Deserialize)] + pub struct DevMode { + pub scraper_key: Option >, + } + // 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 , + // 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 , } #[derive (Deserialize)] diff --git a/crates/ptth_relay/src/key_validity.rs b/crates/ptth_relay/src/key_validity.rs new file mode 100644 index 0000000..30a314b --- /dev/null +++ b/crates/ptth_relay/src/key_validity.rs @@ -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) -> &::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) + } +} + +#[derive (Deserialize)] +pub struct ScraperKey { + pub not_before: DateTime , + pub not_after: DateTime , + pub hash: BlakeHashWrapper, + _phantom: std::marker::PhantomData , +} + +#[derive (Copy, Clone, Debug, PartialEq)] +pub enum KeyValidity { + Valid, + + WrongKey, + ClockIsBehind, + Expired, + DurationTooLong (Duration), + DurationNegative, +} + +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. + 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:: { + 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:: { + 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:: { + 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:: { + 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); + } + } +} diff --git a/crates/ptth_relay/src/lib.rs b/crates/ptth_relay/src/lib.rs index ee9161b..be4f63f 100644 --- a/crates/ptth_relay/src/lib.rs +++ b/crates/ptth_relay/src/lib.rs @@ -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!"); } diff --git a/issues/2020-12Dec/auth-route-YNQAQKJS.md b/issues/2020-12Dec/auth-route-YNQAQKJS.md index 8307ccf..80bae96 100644 --- a/issues/2020-12Dec/auth-route-YNQAQKJS.md +++ b/issues/2020-12Dec/auth-route-YNQAQKJS.md @@ -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 diff --git a/src/tests.rs b/src/tests.rs index 65d94e5..5de6e56 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -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.