From 4f06b562950359ab2ed85f7c4fa0b5234cad0577 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Mon, 29 Dec 2025 16:36:20 +0100 Subject: [PATCH] Improve error handling --- Cargo.lock | 23 ++++++++++++++++++++++- Cargo.toml | 3 ++- src/basic_state_manager.rs | 27 ++++++--------------------- src/client.rs | 8 ++++---- src/crypto_wrapper.rs | 15 ++++++--------- src/errors.rs | 24 ++++++++++++++++++++++++ src/lib.rs | 5 +++++ 7 files changed, 69 insertions(+), 36 deletions(-) create mode 100644 src/errors.rs diff --git a/Cargo.lock b/Cargo.lock index b69d930..4b02438 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -632,7 +632,7 @@ checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" [[package]] name = "light-openid" -version = "1.0.4" +version = "1.1.0" dependencies = [ "aes-gcm", "base64", @@ -642,6 +642,7 @@ dependencies = [ "rkyv", "serde", "serde_json", + "thiserror", "urlencoding", ] @@ -1273,6 +1274,26 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tinystr" version = "0.8.2" diff --git a/Cargo.toml b/Cargo.toml index 0fd46ac..c11ea4c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "light-openid" -version = "1.0.4" +version = "1.1.0" edition = "2021" repository = "https://gitea.communiquons.org/pierre/light-openid" authors = ["Pierre HUBERT "] @@ -17,6 +17,7 @@ base64 = "0.22.0" serde = { version = "1.0.198", features = ["derive"] } serde_json = "1.0.115" urlencoding = "2.1.3" +thiserror = "2" # Dependencies for crypto wrapper rkyv = { version = "0.8.12", optional = true } diff --git a/src/basic_state_manager.rs b/src/basic_state_manager.rs index a26540c..ad2a84d 100644 --- a/src/basic_state_manager.rs +++ b/src/basic_state_manager.rs @@ -6,11 +6,10 @@ //! 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::errors::OpenIdError; use crate::time_utils::time; +use crate::Res; use std::net::IpAddr; #[derive(Debug, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)] @@ -28,20 +27,6 @@ impl State { } } -#[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); @@ -59,22 +44,22 @@ impl BasicStateManager { } /// Generate a new state - pub fn gen_state(&self, ip: IpAddr) -> Result> { + pub fn gen_state(&self, ip: IpAddr) -> Res { 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> { + pub fn validate_state(&self, ip: IpAddr, state: &str) -> Res { let state: State = self.0.decrypt(state)?; if state.ip != ip { - return Err(Box::new(StateError::InvalidIp)); + return Err(OpenIdError::StateErrorInvalidIP); } if state.expire < time() { - return Err(Box::new(StateError::Expired)); + return Err(OpenIdError::StateErrorExpired); } Ok(()) diff --git a/src/client.rs b/src/client.rs index d61c763..8491791 100644 --- a/src/client.rs +++ b/src/client.rs @@ -3,13 +3,13 @@ use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; use base64::Engine; use std::collections::HashMap; -use std::error::Error; use crate::primitives::{OpenIDConfig, OpenIDTokenResponse, OpenIDUserInfo}; +use crate::Res; impl OpenIDConfig { /// Load OpenID configuration from a given .well-known/openid-configuration URL - pub async fn load_from_url(url: &str) -> Result> { + pub async fn load_from_url(url: &str) -> Res { Ok(reqwest::get(url).await?.json().await?) } @@ -37,7 +37,7 @@ impl OpenIDConfig { client_secret: &str, code: &str, redirect_uri: &str, - ) -> Result<(OpenIDTokenResponse, String), Box> { + ) -> Res<(OpenIDTokenResponse, String)> { let authorization = BASE64_STANDARD.encode(format!("{client_id}:{client_secret}")); let mut params = HashMap::new(); @@ -66,7 +66,7 @@ impl OpenIDConfig { pub async fn request_user_info( &self, token: &OpenIDTokenResponse, - ) -> Result<(OpenIDUserInfo, String), Box> { + ) -> Res<(OpenIDUserInfo, String)> { let response = reqwest::Client::new() .get(self.userinfo_endpoint.as_ref().expect( "This client only support information retrieval through userinfo endpoint!", diff --git a/src/crypto_wrapper.rs b/src/crypto_wrapper.rs index d1e95bc..79cde45 100644 --- a/src/crypto_wrapper.rs +++ b/src/crypto_wrapper.rs @@ -1,3 +1,5 @@ +use crate::errors::OpenIdError; +use crate::Res; use aes_gcm::aead::{Aead, OsRng}; use aes_gcm::{Aes256Gcm, Key, KeyInit, Nonce}; use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; @@ -10,7 +12,6 @@ use rkyv::rancor::Strategy; use rkyv::ser::allocator::ArenaHandle; use rkyv::util::AlignedVec; use rkyv::{Archive, Deserialize, Serialize}; -use std::error::Error; /// The length of the nonce used to initialize encryption const NONCE_LEN: usize = 12; @@ -33,7 +34,7 @@ impl CryptoWrapper { pub fn encrypt( &self, data: &impl for<'a> Serialize, rkyv::rancor::Error>>, - ) -> Result> { + ) -> Res { let aes_key = Aes256Gcm::new(&self.key); let nonce_bytes = rand::rng().random::<[u8; NONCE_LEN]>(); @@ -48,7 +49,7 @@ impl CryptoWrapper { } /// Decrypt some data previously encrypted using the [`CryptoWrapper::encrypt`] method - pub fn decrypt(&self, input: &str) -> Result> + pub fn decrypt(&self, input: &str) -> Res where T: Archive, T::Archived: for<'a> CheckBytes> @@ -57,9 +58,7 @@ impl CryptoWrapper { let bytes = BASE64_STANDARD.decode(input)?; if bytes.len() < NONCE_LEN { - return Err(Box::new(std::io::Error::other( - "Input string is smaller than nonce!", - ))); + return Err(OpenIdError::DecInputStringSmallerThanNonce); } let (enc, nonce) = bytes.split_at(bytes.len() - NONCE_LEN); @@ -71,9 +70,7 @@ impl CryptoWrapper { Ok(d) => d, Err(e) => { log::error!("Failed to decrypt wrapped data! {e:#?}"); - return Err(Box::new(std::io::Error::other( - "Failed to decrypt wrapped data!", - ))); + return Err(OpenIdError::DecryptWrappedData); } }; diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 0000000..2f5c92b --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,24 @@ +#[derive(thiserror::Error, Debug)] +pub enum OpenIdError { + #[error("Reqwest error: {0}")] + Reqwest(#[from] reqwest::Error), + #[error("Serde json error: {0}")] + SerdeJson(#[from] serde_json::Error), + #[error("Base64 decoding error: {0}")] + Base64Decode(#[from] base64::DecodeError), + #[cfg(feature = "crypto-wrapper")] + #[error("State error: invalid IP")] + StateErrorInvalidIP, + #[cfg(feature = "crypto-wrapper")] + #[error("State error: expired")] + StateErrorExpired, + #[cfg(feature = "crypto-wrapper")] + #[error("Rkyv error: {0}")] + RkyvError(#[from] rkyv::rancor::Error), + #[cfg(feature = "crypto-wrapper")] + #[error("Input string is smaller than nonce!")] + DecInputStringSmallerThanNonce, + #[cfg(feature = "crypto-wrapper")] + #[error("Failed to decrypt wrapped data!")] + DecryptWrappedData, +} diff --git a/src/lib.rs b/src/lib.rs index 8007649..1ce3540 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,9 +4,14 @@ //! //! See https://gitea.communiquons.org/pierre/oidc-test-client for an example of usage of this library +use crate::errors::OpenIdError; + pub mod client; +pub mod errors; pub mod primitives; +pub type Res = Result; + #[cfg(feature = "crypto-wrapper")] mod time_utils;