securitydept_token_set_context/backend_oidc_mode/
refresh_material.rs1use 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}