new: add code for scraper keys to expire and have limited durations

main
_ 2020-12-12 17:11:22 +00:00
parent bf8e483d16
commit 0eb1e7e38f
7 changed files with 238 additions and 69 deletions

1
Cargo.lock generated
View File

@ -187,6 +187,7 @@ dependencies = [
"libc",
"num-integer",
"num-traits",
"serde",
"time",
"wasm-bindgen",
"winapi 0.3.9",

View File

@ -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"

View File

@ -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)]

View File

@ -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);
}
}
}

View File

@ -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!");
}

View File

@ -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

View File

@ -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.