cataclysm 0.5.5

A simple http framework
Documentation
use openssl::symm::{encrypt, decrypt, Cipher};
use crate::{
    Error,
    http::{Request, Response},
    session::{SessionCreator}
};
use rand::Rng;
use super::SameSite;
use std::time::Duration;
use cookie::Cookie;
use chrono::{DateTime, Utc};
use base64::{Engine, engine::general_purpose};

/// Session implementation, based on encrypted cookies.
///
/// This structure needs to be created through [CookieSessionBuilder](super::CookieSessionBuilder).
#[derive(Clone)]
pub struct EncryptedCookieSession {
    pub(crate) cipher: Cipher,
    pub(crate) key: Vec<u8>,
    pub(crate) cookie_name: String,
    pub(crate) path: Option<String>,
    pub(crate) domain: Option<String>,
    pub(crate) expires: Option<DateTime<Utc>>,
    pub(crate) max_age: Option<Duration>,
    pub(crate) secure: Option<bool>,
    pub(crate) http_only: Option<bool>,
    pub(crate) same_site: Option<SameSite>,
    pub(crate) force_failure: bool
}

impl EncryptedCookieSession {
    /// Hands in the inner encryption key.
    ///
    /// Should only be used for debugging purposes. Printing or logging the encryption key could compromise the secrecy that ensures safety.
    pub fn get_encryption_key(&self) -> &Vec<u8> {
        &self.key
    }

    /// Helper function to extract a session from a cookie
    fn build_from_req(&self, req: &Request) -> Result<Option<String>, Error> {
        // we can have multiple cookies, so we try for each one
        let empty = Vec::new();
        let cookie_headers = vec![
            req.header.headers.get("Cookie").unwrap_or_else(|| &empty),
            req.header.headers.get("cookie").unwrap_or_else(|| &empty)
        ].into_iter().flatten();
        for cookie_header in cookie_headers {
            for single_cookie in cookie_header.split("; ") {
                let cookie = Cookie::parse_encoded(single_cookie).map_err(|e| Error::custom(format!("{}", e)))?;
                if cookie.name() == self.cookie_name {
                    let value = cookie.value();
                    // The hmac value is at least 44 bytes
                    if value.len() < 44 {
                        return Err(Error::custom("length of cookie cannot contain even the hmac value"));
                    } else {
                        // I know these unwraps look unsafe, but trust me, they are, TODO FIX with let Some
                        let encoded_iv = value.get(0..44).unwrap();
                        let iv = general_purpose::STANDARD.decode(encoded_iv).map_err(|e| Error::custom(format!("{}", e)))?;
                        let content = value.get(44..value.len()).unwrap();

                        // We need to decode first the base64, then decrypt
                        let decoded = general_purpose::STANDARD.decode(content).map_err(|e| Error::custom(format!("{}", e)))?;
                        let decrypted = decrypt(self.cipher, &self.key[..], Some(&iv[..]), &decoded[..])?;
                        return Ok(Some(String::from_utf8(decrypted).map_err(|e| Error::custom(format!("{}", e)))?))
                    }
                }
            }
        }
        Ok(None)
    }
}

impl SessionCreator for EncryptedCookieSession {
    fn parse(&self, req: &Request) -> Result<Option<String>, Error> {
        match self.build_from_req(req) {
            Ok(Some(content)) => Ok(Some(content)),
            Ok(None) => {
                #[cfg(feature = "full_log")]
                log::debug!("cookie not found among request headers");
                Ok(None)
            },
            Err(e) => {
                if self.force_failure {
                    Err(e)
                } else {
                    #[cfg(feature = "full_log")]
                    log::debug!("error while creating session: {}", e);
                    return Ok(None)
                }
            }
        }
    }

    fn apply(&self, content: String, mut res: Response) -> Result<Response, Error> {
        // We create a random 32 bit iv
        let mut rng = rand::rng();
        let mut iv: Vec<u8> = vec![0;32];
        rng.fill_bytes(iv.as_mut_slice());
        let encoded_iv = general_purpose::STANDARD.encode(&iv);
        // Encode it in base64
        let encrypted_content = encrypt(self.cipher, &self.key[..], Some(&iv[..]), content.as_bytes())?;
        let encoded_content = general_purpose::STANDARD.encode(encrypted_content);
        let cookie = format!("{}{}", encoded_iv, encoded_content);

        let cookie_builder = Cookie::build((&self.cookie_name, cookie));

        let cookie_builder = if let Some(path) = &self.path {
            cookie_builder.path(path)
        } else {
            cookie_builder
        };

        let cookie_builder = if let Some(domain) = &self.domain {
            cookie_builder.domain(domain)
        } else {
            cookie_builder
        };

        let cookie_builder = if let Some(expires) = &self.expires {
            match cookie::time::OffsetDateTime::from_unix_timestamp(expires.timestamp()) {
                Ok(v) => cookie_builder.expires(v),
                Err(e) => {
                    return Err(Error::Session(format!("failed to set expiration to cookie, {}", e)));
                }
            }
        } else {
            cookie_builder
        };

        let cookie_builder = if let Some(max_age) = &self.max_age {
            let max_age = match max_age.clone().try_into() {
                Ok(v) => v,
                Err(e) => {
                    return Err(Error::Session(format!("failed to set max-age to cookie, {}", e)));
                }
            };
            cookie_builder.max_age(max_age)
        } else {
            cookie_builder
        };

        let cookie_builder = if let Some(secure) = &self.secure {
            cookie_builder.secure(*secure)
        } else {
            cookie_builder
        };

        let cookie_builder = if let Some(http_only) = &self.http_only {
            cookie_builder.http_only(*http_only)
        } else {
            cookie_builder
        };

        let cookie_builder = if let Some(same_site) = &self.same_site {
            cookie_builder.same_site(same_site.to_cookie_same_site())
        } else {
            cookie_builder
        };

        let cookie = cookie_builder.build();

        res = res.header("Set-Cookie", format!("{}", cookie.encoded()));
        Ok(res)
    }
}