Compare commits
1 Commits
master
...
581babb22b
| Author | SHA1 | Date | |
|---|---|---|---|
| 581babb22b |
904
Cargo.lock
generated
904
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "light-openid"
|
name = "light-openid"
|
||||||
version = "1.1.0"
|
version = "1.0.4"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
repository = "https://gitea.communiquons.org/pierre/light-openid"
|
repository = "https://gitea.communiquons.org/pierre/light-openid"
|
||||||
authors = ["Pierre HUBERT <pierre.git@communiquons.org>"]
|
authors = ["Pierre HUBERT <pierre.git@communiquons.org>"]
|
||||||
@@ -12,17 +12,16 @@ license = "GPL-2.0-or-later"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
log = "0.4.21"
|
log = "0.4.21"
|
||||||
reqwest = { version = "0.12.14", features = ["json"] }
|
reqwest = { version = "0.12.14", features = ["json"] }
|
||||||
base64 = "0.22.0"
|
base64 = "0.22.0"
|
||||||
serde = { version = "1.0.198", features = ["derive"] }
|
serde = { version = "1.0.198", features = ["derive"] }
|
||||||
serde_json = "1.0.115"
|
serde_json = "1.0.115"
|
||||||
urlencoding = "2.1.3"
|
urlencoding = "2.1.3"
|
||||||
thiserror = "2"
|
|
||||||
|
|
||||||
# Dependencies for crypto wrapper
|
# Dependencies for crypto wrapper
|
||||||
rkyv = { version = "0.8.12", optional = true }
|
bincode = { version = "2.0.0-rc.3", optional = true }
|
||||||
aes-gcm = { version = "0.10.3", optional = true }
|
aes-gcm = { version = "0.10.3", optional = true }
|
||||||
rand = { version = "0.9.0", optional = true }
|
rand = { version = "0.9.0", optional = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
crypto-wrapper = ["rkyv", "aes-gcm", "rand"]
|
crypto-wrapper = ["bincode", "aes-gcm", "rand"]
|
||||||
|
|||||||
@@ -6,13 +6,15 @@
|
|||||||
//! address of the client in an encrypted way, and expires 15
|
//! address of the client in an encrypted way, and expires 15
|
||||||
//! minutes after issuance.
|
//! minutes after issuance.
|
||||||
|
|
||||||
|
use std::error::Error;
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
use crate::crypto_wrapper::CryptoWrapper;
|
use crate::crypto_wrapper::CryptoWrapper;
|
||||||
use crate::errors::OpenIdError;
|
|
||||||
use crate::time_utils::time;
|
use crate::time_utils::time;
|
||||||
use crate::Res;
|
use bincode::{Decode, Encode};
|
||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
|
|
||||||
#[derive(Debug, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
|
#[derive(Encode, Decode, Debug)]
|
||||||
struct State {
|
struct State {
|
||||||
ip: IpAddr,
|
ip: IpAddr,
|
||||||
expire: u64,
|
expire: u64,
|
||||||
@@ -27,13 +29,27 @@ 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
|
/// Basic state manager. Can be used to prevent CRSF by encrypting
|
||||||
/// a token containing a lifetime and the IP address of the user
|
/// a token containing a lifetime and the IP address of the user
|
||||||
pub struct BasicStateManager(CryptoWrapper);
|
pub struct BasicStateManager(CryptoWrapper);
|
||||||
|
|
||||||
impl BasicStateManager {
|
impl BasicStateManager {
|
||||||
/// Initialize the state manager by creating a random encryption key. This function
|
/// Initialize the state manager by creating a random encryption key. This function
|
||||||
/// should be called only once, ideally in the main function of the application
|
/// should be called only one, ideally in the main function of the application
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self(CryptoWrapper::new_random())
|
Self(CryptoWrapper::new_random())
|
||||||
}
|
}
|
||||||
@@ -44,22 +60,22 @@ impl BasicStateManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Generate a new state
|
/// Generate a new state
|
||||||
pub fn gen_state(&self, ip: IpAddr) -> Res<String> {
|
pub fn gen_state(&self, ip: IpAddr) -> Result<String, Box<dyn Error>> {
|
||||||
let state = State::new(ip);
|
let state = State::new(ip);
|
||||||
|
|
||||||
self.0.encrypt(&state)
|
self.0.encrypt(&state)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Validate given state on callback URL
|
/// Validate given state on callback URL
|
||||||
pub fn validate_state(&self, ip: IpAddr, state: &str) -> Res {
|
pub fn validate_state(&self, ip: IpAddr, state: &str) -> Result<(), Box<dyn Error>> {
|
||||||
let state: State = self.0.decrypt(state)?;
|
let state: State = self.0.decrypt(state)?;
|
||||||
|
|
||||||
if state.ip != ip {
|
if state.ip != ip {
|
||||||
return Err(OpenIdError::StateErrorInvalidIP);
|
return Err(Box::new(StateError::InvalidIp));
|
||||||
}
|
}
|
||||||
|
|
||||||
if state.expire < time() {
|
if state.expire < time() {
|
||||||
return Err(OpenIdError::StateErrorExpired);
|
return Err(Box::new(StateError::Expired));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
|
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::error::Error;
|
||||||
|
|
||||||
use crate::primitives::{OpenIDConfig, OpenIDTokenResponse, OpenIDUserInfo};
|
use crate::primitives::{OpenIDConfig, OpenIDTokenResponse, OpenIDUserInfo};
|
||||||
use crate::Res;
|
|
||||||
|
|
||||||
impl OpenIDConfig {
|
impl OpenIDConfig {
|
||||||
/// Load OpenID configuration from a given .well-known/openid-configuration URL
|
/// Load OpenID configuration from a given .well-known/openid-configuration URL
|
||||||
pub async fn load_from_url(url: &str) -> Res<Self> {
|
pub async fn load_from_url(url: &str) -> Result<Self, Box<dyn Error>> {
|
||||||
Ok(reqwest::get(url).await?.json().await?)
|
Ok(reqwest::get(url).await?.json().await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,8 +37,8 @@ impl OpenIDConfig {
|
|||||||
client_secret: &str,
|
client_secret: &str,
|
||||||
code: &str,
|
code: &str,
|
||||||
redirect_uri: &str,
|
redirect_uri: &str,
|
||||||
) -> Res<(OpenIDTokenResponse, String)> {
|
) -> Result<(OpenIDTokenResponse, String), Box<dyn Error>> {
|
||||||
let authorization = BASE64_STANDARD.encode(format!("{client_id}:{client_secret}"));
|
let authorization = BASE64_STANDARD.encode(format!("{}:{}", client_id, client_secret));
|
||||||
|
|
||||||
let mut params = HashMap::new();
|
let mut params = HashMap::new();
|
||||||
params.insert("grant_type", "authorization_code");
|
params.insert("grant_type", "authorization_code");
|
||||||
@@ -66,7 +66,7 @@ impl OpenIDConfig {
|
|||||||
pub async fn request_user_info(
|
pub async fn request_user_info(
|
||||||
&self,
|
&self,
|
||||||
token: &OpenIDTokenResponse,
|
token: &OpenIDTokenResponse,
|
||||||
) -> Res<(OpenIDUserInfo, String)> {
|
) -> Result<(OpenIDUserInfo, String), Box<dyn Error>> {
|
||||||
let response = reqwest::Client::new()
|
let response = reqwest::Client::new()
|
||||||
.get(self.userinfo_endpoint.as_ref().expect(
|
.get(self.userinfo_endpoint.as_ref().expect(
|
||||||
"This client only support information retrieval through userinfo endpoint!",
|
"This client only support information retrieval through userinfo endpoint!",
|
||||||
|
|||||||
@@ -1,23 +1,18 @@
|
|||||||
use crate::errors::OpenIdError;
|
use std::error::Error;
|
||||||
use crate::Res;
|
use std::io::ErrorKind;
|
||||||
|
|
||||||
use aes_gcm::aead::{Aead, OsRng};
|
use aes_gcm::aead::{Aead, OsRng};
|
||||||
use aes_gcm::{Aes256Gcm, Key, KeyInit, Nonce};
|
use aes_gcm::{Aes256Gcm, Key, KeyInit, Nonce};
|
||||||
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
|
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
|
||||||
use base64::Engine as _;
|
use base64::Engine as _;
|
||||||
|
use bincode::{Decode, Encode};
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use rkyv::api::high::{HighSerializer, HighValidator};
|
|
||||||
use rkyv::bytecheck::CheckBytes;
|
|
||||||
use rkyv::de::Pool;
|
|
||||||
use rkyv::rancor::Strategy;
|
|
||||||
use rkyv::ser::allocator::ArenaHandle;
|
|
||||||
use rkyv::util::AlignedVec;
|
|
||||||
use rkyv::{Archive, Deserialize, Serialize};
|
|
||||||
|
|
||||||
/// The length of the nonce used to initialize encryption
|
/// The lenght of the nonce used to initialize encryption
|
||||||
const NONCE_LEN: usize = 12;
|
const NONCE_LEN: usize = 12;
|
||||||
|
|
||||||
/// CryptoWrapper is a library that can be used to encrypt and decrypt some data marked
|
/// CryptoWrapper is a library that can be used to encrypt and decrypt some data marked
|
||||||
/// that derives [SchemaWrite] and [SchemaRead] traits using AES encryption
|
/// that derives [Encode] and [Decode] traits using AES encryption
|
||||||
pub struct CryptoWrapper {
|
pub struct CryptoWrapper {
|
||||||
key: Key<Aes256Gcm>,
|
key: Key<Aes256Gcm>,
|
||||||
}
|
}
|
||||||
@@ -31,14 +26,11 @@ impl CryptoWrapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Encrypt some data, returning the result as a base64-encoded string
|
/// Encrypt some data, returning the result as a base64-encoded string
|
||||||
pub fn encrypt(
|
pub fn encrypt<T: Encode + Decode<()>>(&self, data: &T) -> Result<String, Box<dyn Error>> {
|
||||||
&self,
|
|
||||||
data: &impl for<'a> Serialize<HighSerializer<AlignedVec, ArenaHandle<'a>, rkyv::rancor::Error>>,
|
|
||||||
) -> Res<String> {
|
|
||||||
let aes_key = Aes256Gcm::new(&self.key);
|
let aes_key = Aes256Gcm::new(&self.key);
|
||||||
let nonce_bytes = rand::rng().random::<[u8; NONCE_LEN]>();
|
let nonce_bytes = rand::rng().random::<[u8; NONCE_LEN]>();
|
||||||
|
|
||||||
let serialized_data = rkyv::to_bytes(data)?;
|
let serialized_data = bincode::encode_to_vec(data, bincode::config::standard())?;
|
||||||
|
|
||||||
let mut enc = aes_key
|
let mut enc = aes_key
|
||||||
.encrypt(Nonce::from_slice(&nonce_bytes), serialized_data.as_slice())
|
.encrypt(Nonce::from_slice(&nonce_bytes), serialized_data.as_slice())
|
||||||
@@ -49,16 +41,14 @@ impl CryptoWrapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Decrypt some data previously encrypted using the [`CryptoWrapper::encrypt`] method
|
/// Decrypt some data previously encrypted using the [`CryptoWrapper::encrypt`] method
|
||||||
pub fn decrypt<T>(&self, input: &str) -> Res<T>
|
pub fn decrypt<T: Decode<()>>(&self, input: &str) -> Result<T, Box<dyn Error>> {
|
||||||
where
|
|
||||||
T: Archive,
|
|
||||||
T::Archived: for<'a> CheckBytes<HighValidator<'a, rkyv::rancor::Error>>
|
|
||||||
+ Deserialize<T, Strategy<Pool, rkyv::rancor::Error>>,
|
|
||||||
{
|
|
||||||
let bytes = BASE64_STANDARD.decode(input)?;
|
let bytes = BASE64_STANDARD.decode(input)?;
|
||||||
|
|
||||||
if bytes.len() < NONCE_LEN {
|
if bytes.len() < NONCE_LEN {
|
||||||
return Err(OpenIdError::DecInputStringSmallerThanNonce);
|
return Err(Box::new(std::io::Error::new(
|
||||||
|
ErrorKind::Other,
|
||||||
|
"Input string is smaller than nonce!",
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let (enc, nonce) = bytes.split_at(bytes.len() - NONCE_LEN);
|
let (enc, nonce) = bytes.split_at(bytes.len() - NONCE_LEN);
|
||||||
@@ -69,20 +59,24 @@ impl CryptoWrapper {
|
|||||||
let dec = match aes_key.decrypt(Nonce::from_slice(nonce), enc) {
|
let dec = match aes_key.decrypt(Nonce::from_slice(nonce), enc) {
|
||||||
Ok(d) => d,
|
Ok(d) => d,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Failed to decrypt wrapped data! {e:#?}");
|
log::error!("Failed to decrypt wrapped data! {:#?}", e);
|
||||||
return Err(OpenIdError::DecryptWrappedData);
|
return Err(Box::new(std::io::Error::new(
|
||||||
|
ErrorKind::Other,
|
||||||
|
"Failed to decrypt wrapped data!",
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(rkyv::from_bytes(&dec)?)
|
Ok(bincode::decode_from_slice(&dec, bincode::config::standard())?.0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use crate::crypto_wrapper::CryptoWrapper;
|
use crate::crypto_wrapper::CryptoWrapper;
|
||||||
|
use bincode::{Decode, Encode};
|
||||||
|
|
||||||
#[derive(Eq, PartialEq, Debug, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
|
#[derive(Encode, Decode, Eq, PartialEq, Debug)]
|
||||||
struct Message(String);
|
struct Message(String);
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
#[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,
|
|
||||||
}
|
|
||||||
@@ -4,14 +4,9 @@
|
|||||||
//!
|
//!
|
||||||
//! See https://gitea.communiquons.org/pierre/oidc-test-client for an example of usage of this library
|
//! 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 client;
|
||||||
pub mod errors;
|
|
||||||
pub mod primitives;
|
pub mod primitives;
|
||||||
|
|
||||||
pub type Res<T = ()> = Result<T, OpenIdError>;
|
|
||||||
|
|
||||||
#[cfg(feature = "crypto-wrapper")]
|
#[cfg(feature = "crypto-wrapper")]
|
||||||
mod time_utils;
|
mod time_utils;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user