⭐ new: add code for scraper keys to expire and have limited durations
parent
bf8e483d16
commit
0eb1e7e38f
|
@ -187,6 +187,7 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"num-integer",
|
"num-integer",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
|
"serde",
|
||||||
"time",
|
"time",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"winapi 0.3.9",
|
"winapi 0.3.9",
|
||||||
|
|
|
@ -10,7 +10,7 @@ license = "AGPL-3.0"
|
||||||
|
|
||||||
base64 = "0.12.3"
|
base64 = "0.12.3"
|
||||||
blake3 = "0.3.7"
|
blake3 = "0.3.7"
|
||||||
chrono = "0.4.19"
|
chrono = {version = "0.4.19", features = ["serde"]}
|
||||||
dashmap = "3.11.10"
|
dashmap = "3.11.10"
|
||||||
futures = "0.3.7"
|
futures = "0.3.7"
|
||||||
handlebars = "3.5.1"
|
handlebars = "3.5.1"
|
||||||
|
|
|
@ -3,78 +3,20 @@
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
convert::{TryFrom, TryInto},
|
convert::{TryFrom},
|
||||||
fmt,
|
|
||||||
iter::FromIterator,
|
iter::FromIterator,
|
||||||
ops::Deref,
|
|
||||||
path::Path,
|
path::Path,
|
||||||
};
|
};
|
||||||
|
|
||||||
use serde::{
|
|
||||||
Deserialize,
|
|
||||||
Deserializer,
|
|
||||||
de::{
|
|
||||||
self,
|
|
||||||
Visitor,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::errors::ConfigError;
|
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
|
// Stuff we need to load from the config file and use to
|
||||||
// set up the HTTP server
|
// set up the HTTP server
|
||||||
|
|
||||||
pub mod file {
|
pub mod file {
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use super::*;
|
use crate::key_validity::*;
|
||||||
|
|
||||||
#[derive (Deserialize)]
|
#[derive (Deserialize)]
|
||||||
pub struct Server {
|
pub struct Server {
|
||||||
|
@ -84,16 +26,21 @@ pub mod file {
|
||||||
pub display_name: Option <String>,
|
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
|
// Stuff that's identical between the file and the runtime structures
|
||||||
|
|
||||||
#[derive (Default, Deserialize)]
|
#[derive (Default, Deserialize)]
|
||||||
pub struct Isomorphic {
|
pub struct Isomorphic {
|
||||||
#[serde (default)]
|
|
||||||
pub enable_dev_mode: bool,
|
|
||||||
#[serde (default)]
|
#[serde (default)]
|
||||||
pub enable_scraper_auth: bool,
|
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)]
|
#[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 config;
|
||||||
pub mod errors;
|
pub mod errors;
|
||||||
pub mod git_version;
|
pub mod git_version;
|
||||||
|
pub mod key_validity;
|
||||||
|
|
||||||
mod server_endpoint;
|
mod server_endpoint;
|
||||||
|
|
||||||
pub use config::Config;
|
pub use config::Config;
|
||||||
|
@ -495,10 +497,9 @@ async fn reload_config (
|
||||||
(*config) = new_config;
|
(*config) = new_config;
|
||||||
|
|
||||||
debug! ("Loaded {} server configs", config.servers.len ());
|
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);
|
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!");
|
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) Add feature flags to ptth_relay.toml for dev mode and scrapers
|
||||||
- (X) Make sure Docker release CAN build
|
- (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
|
- ( ) Add hash of 1 scraper key to ptth_relay.toml, with 1 week expiration
|
||||||
- ( ) (POC) Test with curl
|
- ( ) (POC) Test with curl
|
||||||
- ( ) Manually create SQLite DB for scraper keys, add 1 hash
|
- ( ) Manually create SQLite DB for scraper keys, add 1 hash
|
||||||
|
|
|
@ -18,7 +18,7 @@ fn end_to_end () {
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use tracing::{debug, info};
|
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
|
// Prefer this form for tests, since all tests share one process
|
||||||
// and we don't care if another test already installed a subscriber.
|
// and we don't care if another test already installed a subscriber.
|
||||||
|
|
Loading…
Reference in New Issue