diff --git a/Cargo.lock b/Cargo.lock index 2f835cc..307451e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -478,7 +478,7 @@ checksum = "6a987beff54b60ffa6d51982e1aa1146bc42f19bd26be28b0586f252fccf5317" [[package]] name = "light-openid" -version = "0.2.1-alpha" +version = "0.3.0-alpha" dependencies = [ "aes-gcm", "base64", diff --git a/Cargo.toml b/Cargo.toml index a083cd8..082366c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "light-openid" -version = "0.2.1-alpha" +version = "0.3.0-alpha" edition = "2021" repository = "https://gitea.communiquons.org/pierre/light-openid" authors = ["Pierre HUBERT "] diff --git a/src/basic_state_manager.rs b/src/basic_state_manager.rs new file mode 100644 index 0000000..117fa8b --- /dev/null +++ b/src/basic_state_manager.rs @@ -0,0 +1,112 @@ +//! # Basic state manager +//! +//! The state manager included in this module can be used to +//! generate basic and stateless states for applications with +//! minimum security requirements. The states contains the IP +//! address of the client in an encrypted way, and expires 15 +//! minutes after issuance. + +use std::error::Error; +use std::fmt; + +use crate::crypto_wrapper::CryptoWrapper; +use crate::time_utils::time; +use bincode::{Decode, Encode}; +use std::net::IpAddr; + +#[derive(Encode, Decode, Debug)] +struct State { + ip: IpAddr, + expire: u64, +} + +impl State { + pub fn new(ip: IpAddr) -> Self { + Self { + ip, + expire: time() + 15 * 60, + } + } +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +enum StateError { + InvalidIp, + Expired, +} + +impl Error for StateError {} + +impl fmt::Display for StateError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "StateManager error {:?}", self) + } +} + +/// Basic state manager. Can be used to prevent CRSF by encrypting +/// a token containing a lifetime and the IP address of the user +pub struct BasicStateManager(CryptoWrapper); + +impl BasicStateManager { + /// Initialize the state manager by creating a random encryption key. This function + /// should be called only one, ideally in the main function of the application + pub fn new() -> Self { + Self(CryptoWrapper::new_random()) + } + + /// Initialize state manager with a given CryptoWrapper + pub fn new_with_wrapper(wrapper: CryptoWrapper) -> Self { + Self(wrapper) + } + + /// Generate a new state + pub fn gen_state(&self, ip: IpAddr) -> Result> { + let state = State::new(ip); + + self.0.encrypt(&state) + } + + /// Validate given state on callback URL + pub fn validate_state(&self, ip: IpAddr, state: &str) -> Result<(), Box> { + let state: State = self.0.decrypt(state)?; + + if state.ip != ip { + return Err(Box::new(StateError::InvalidIp)); + } + + if state.expire < time() { + return Err(Box::new(StateError::Expired)); + } + + Ok(()) + } +} + +impl Default for BasicStateManager { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod test { + use crate::basic_state_manager::BasicStateManager; + use std::net::{IpAddr, Ipv4Addr}; + + const IP_1: IpAddr = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)); + const IP_2: IpAddr = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 2)); + + #[test] + fn valid_state() { + let manager = BasicStateManager::new(); + let state = manager.gen_state(IP_1).unwrap(); + assert!(manager.validate_state(IP_1, &state).is_ok()); + } + + #[test] + fn invalid_ip() { + let manager = BasicStateManager::new(); + let state = manager.gen_state(IP_1).unwrap(); + assert!(manager.validate_state(IP_2, &state).is_err()); + } +} diff --git a/src/lib.rs b/src/lib.rs index ef3892e..422131a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,10 @@ pub mod client; pub mod primitives; +mod time_utils; #[cfg(feature = "crypto-wrapper")] pub mod crypto_wrapper; + +#[cfg(feature = "crypto-wrapper")] +pub mod basic_state_manager; diff --git a/src/time_utils.rs b/src/time_utils.rs new file mode 100644 index 0000000..d741fb1 --- /dev/null +++ b/src/time_utils.rs @@ -0,0 +1,19 @@ +use std::time::{SystemTime, UNIX_EPOCH}; + +/// Get current time since epoch, in seconds +pub fn time() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() +} + +#[cfg(test)] +mod test { + use crate::time_utils::time; + + #[test] + fn time_is_recent_enough() { + assert!(time() > 1682750570); + } +}