⭐ 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