securitydept-token-set-context 0.2.0

Token Set Context of SecurityDept, a layered authentication and authorization toolkit built as reusable Rust crates.
Documentation
use std::fmt;

use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
use serde::{Deserialize, Serialize};
use snafu::Snafu;

#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(transparent)]
pub struct SealedRefreshMaterial(String);

impl SealedRefreshMaterial {
    pub fn new(value: impl Into<String>) -> Self {
        Self(value.into())
    }

    pub fn expose(&self) -> &str {
        &self.0
    }
}

impl fmt::Debug for SealedRefreshMaterial {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str("SealedRefreshMaterial(REDACTED)")
    }
}

pub trait RefreshMaterialProtector: Send + Sync {
    fn seal(&self, refresh_token: &str) -> Result<SealedRefreshMaterial, RefreshMaterialError>;
    fn unseal(&self, material: &SealedRefreshMaterial) -> Result<String, RefreshMaterialError>;
}

#[derive(Debug, Snafu)]
pub enum RefreshMaterialError {
    #[snafu(display("refresh material protector is misconfigured: {message}"))]
    InvalidConfig { message: String },
    #[snafu(display("refresh material is invalid: {message}"))]
    InvalidMaterial { message: String },
    #[snafu(display("refresh material cryptographic operation failed: {message}"))]
    Cryptography { message: String },
}

#[derive(Debug, Default, Clone, Copy)]
pub struct PassthroughRefreshMaterialProtector;

impl RefreshMaterialProtector for PassthroughRefreshMaterialProtector {
    fn seal(&self, refresh_token: &str) -> Result<SealedRefreshMaterial, RefreshMaterialError> {
        Ok(SealedRefreshMaterial::new(refresh_token))
    }

    fn unseal(&self, material: &SealedRefreshMaterial) -> Result<String, RefreshMaterialError> {
        Ok(material.expose().to_string())
    }
}

pub struct AeadRefreshMaterialProtector {
    master_key: orion::aead::SecretKey,
}

impl fmt::Debug for AeadRefreshMaterialProtector {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str("AeadRefreshMaterialProtector(REDACTED)")
    }
}

impl AeadRefreshMaterialProtector {
    pub fn from_master_key(master_key: &str) -> Result<Self, RefreshMaterialError> {
        let master_key =
            orion::aead::SecretKey::from_slice(master_key.as_bytes()).map_err(|e| {
                RefreshMaterialError::InvalidConfig {
                    message: format!("failed to parse master key: {e}"),
                }
            })?;

        Ok(Self { master_key })
    }
}

impl RefreshMaterialProtector for AeadRefreshMaterialProtector {
    fn seal(&self, refresh_token: &str) -> Result<SealedRefreshMaterial, RefreshMaterialError> {
        let ciphertext =
            orion::aead::seal(&self.master_key, refresh_token.as_bytes()).map_err(|e| {
                RefreshMaterialError::Cryptography {
                    message: format!("failed to seal refresh token: {e}"),
                }
            })?;

        Ok(SealedRefreshMaterial::new(
            URL_SAFE_NO_PAD.encode(ciphertext),
        ))
    }

    fn unseal(&self, material: &SealedRefreshMaterial) -> Result<String, RefreshMaterialError> {
        let ciphertext = URL_SAFE_NO_PAD.decode(material.expose()).map_err(|e| {
            RefreshMaterialError::InvalidMaterial {
                message: format!("failed to decode sealed refresh token: {e}"),
            }
        })?;

        let plaintext = orion::aead::open(&self.master_key, &ciphertext).map_err(|e| {
            RefreshMaterialError::Cryptography {
                message: format!("failed to unseal refresh token: {e}"),
            }
        })?;

        String::from_utf8(plaintext).map_err(|e| RefreshMaterialError::InvalidMaterial {
            message: format!("unsealed refresh token is not valid UTF-8: {e}"),
        })
    }
}