Skip to main content

securitydept_token_set_context/backend_oidc_mode/
refresh_material.rs

1use std::fmt;
2
3use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
4use serde::{Deserialize, Serialize};
5use snafu::Snafu;
6
7#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
8#[serde(transparent)]
9pub struct SealedRefreshMaterial(String);
10
11impl SealedRefreshMaterial {
12    pub fn new(value: impl Into<String>) -> Self {
13        Self(value.into())
14    }
15
16    pub fn expose(&self) -> &str {
17        &self.0
18    }
19}
20
21impl fmt::Debug for SealedRefreshMaterial {
22    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
23        f.write_str("SealedRefreshMaterial(REDACTED)")
24    }
25}
26
27pub trait RefreshMaterialProtector: Send + Sync {
28    fn seal(&self, refresh_token: &str) -> Result<SealedRefreshMaterial, RefreshMaterialError>;
29    fn unseal(&self, material: &SealedRefreshMaterial) -> Result<String, RefreshMaterialError>;
30}
31
32#[derive(Debug, Snafu)]
33pub enum RefreshMaterialError {
34    #[snafu(display("refresh material protector is misconfigured: {message}"))]
35    InvalidConfig { message: String },
36    #[snafu(display("refresh material is invalid: {message}"))]
37    InvalidMaterial { message: String },
38    #[snafu(display("refresh material cryptographic operation failed: {message}"))]
39    Cryptography { message: String },
40}
41
42#[derive(Debug, Default, Clone, Copy)]
43pub struct PassthroughRefreshMaterialProtector;
44
45impl RefreshMaterialProtector for PassthroughRefreshMaterialProtector {
46    fn seal(&self, refresh_token: &str) -> Result<SealedRefreshMaterial, RefreshMaterialError> {
47        Ok(SealedRefreshMaterial::new(refresh_token))
48    }
49
50    fn unseal(&self, material: &SealedRefreshMaterial) -> Result<String, RefreshMaterialError> {
51        Ok(material.expose().to_string())
52    }
53}
54
55pub struct AeadRefreshMaterialProtector {
56    master_key: orion::aead::SecretKey,
57}
58
59impl fmt::Debug for AeadRefreshMaterialProtector {
60    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61        f.write_str("AeadRefreshMaterialProtector(REDACTED)")
62    }
63}
64
65impl AeadRefreshMaterialProtector {
66    pub fn from_master_key(master_key: &str) -> Result<Self, RefreshMaterialError> {
67        let master_key =
68            orion::aead::SecretKey::from_slice(master_key.as_bytes()).map_err(|e| {
69                RefreshMaterialError::InvalidConfig {
70                    message: format!("failed to parse master key: {e}"),
71                }
72            })?;
73
74        Ok(Self { master_key })
75    }
76}
77
78impl RefreshMaterialProtector for AeadRefreshMaterialProtector {
79    fn seal(&self, refresh_token: &str) -> Result<SealedRefreshMaterial, RefreshMaterialError> {
80        let ciphertext =
81            orion::aead::seal(&self.master_key, refresh_token.as_bytes()).map_err(|e| {
82                RefreshMaterialError::Cryptography {
83                    message: format!("failed to seal refresh token: {e}"),
84                }
85            })?;
86
87        Ok(SealedRefreshMaterial::new(
88            URL_SAFE_NO_PAD.encode(ciphertext),
89        ))
90    }
91
92    fn unseal(&self, material: &SealedRefreshMaterial) -> Result<String, RefreshMaterialError> {
93        let ciphertext = URL_SAFE_NO_PAD.decode(material.expose()).map_err(|e| {
94            RefreshMaterialError::InvalidMaterial {
95                message: format!("failed to decode sealed refresh token: {e}"),
96            }
97        })?;
98
99        let plaintext = orion::aead::open(&self.master_key, &ciphertext).map_err(|e| {
100            RefreshMaterialError::Cryptography {
101                message: format!("failed to unseal refresh token: {e}"),
102            }
103        })?;
104
105        String::from_utf8(plaintext).map_err(|e| RefreshMaterialError::InvalidMaterial {
106            message: format!("unsealed refresh token is not valid UTF-8: {e}"),
107        })
108    }
109}