1use super::{Realm, RealmError};
7use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
8use chrono::Utc;
9use rand::RngCore;
10use sha2::{Digest, Sha256};
11
12pub const REALM_SECRET_HEADER: &str = "x-actrix-realm-secret";
14
15pub const DEFAULT_REALM_SECRET_PREVIOUS_GRACE_SECS: u64 = 4 * 3600;
17
18#[derive(Debug, Clone, PartialEq, Eq)]
20pub enum RealmSecretCheck {
21 NotConfigured,
23 ValidCurrent,
25 ValidPrevious,
27 MissingRequired,
29 Invalid,
31}
32
33#[derive(Debug, Clone)]
35pub struct RealmSecretRotation {
36 pub new_secret: String,
37 pub previous_valid_until: Option<u64>,
38}
39
40#[derive(Debug, Clone, Default)]
42pub struct RealmSecretState {
43 pub current_hash: Option<String>,
44 pub previous_hash: Option<String>,
45 pub previous_valid_until: Option<u64>,
46}
47
48pub fn generate_realm_secret() -> String {
50 let mut bytes = [0u8; 24];
51 rand::thread_rng().fill_bytes(&mut bytes);
52 format!("rs_{}", URL_SAFE_NO_PAD.encode(bytes))
53}
54
55pub fn hash_realm_secret(secret: &str) -> String {
57 let mut hasher = Sha256::new();
58 hasher.update(secret.as_bytes());
59 hex::encode(hasher.finalize())
60}
61
62pub async fn rotate_realm_secret(
66 realm_id: u32,
67 previous_grace_secs: Option<u64>,
68) -> Result<RealmSecretRotation, RealmError> {
69 let grace_secs = previous_grace_secs.unwrap_or(DEFAULT_REALM_SECRET_PREVIOUS_GRACE_SECS);
70
71 let mut realm = Realm::get(realm_id).await?.ok_or(RealmError::NotFound)?;
72
73 let now = Utc::now().timestamp() as u64;
74 let previous_valid_until = now.saturating_add(grace_secs);
75
76 if !realm.secret_current.is_empty() {
78 realm.secret_previous = Some((realm.secret_current.clone(), previous_valid_until));
79 } else {
80 realm.secret_previous = None;
81 }
82
83 let new_secret = generate_realm_secret();
85 let new_hash = hash_realm_secret(&new_secret);
86 realm.secret_current = new_hash;
87
88 realm.save().await?;
89
90 Ok(RealmSecretRotation {
91 new_secret,
92 previous_valid_until: Some(previous_valid_until),
93 })
94}
95
96pub async fn get_realm_secret_state(realm_id: u32) -> Result<RealmSecretState, RealmError> {
98 let realm = Realm::get(realm_id).await?.ok_or(RealmError::NotFound)?;
99
100 let current_hash = if realm.secret_current.is_empty() {
101 None
102 } else {
103 Some(realm.secret_current.clone())
104 };
105
106 let (previous_hash, previous_valid_until) = match &realm.secret_previous {
107 Some((hash, valid_until)) => (Some(hash.clone()), Some(*valid_until)),
108 None => (None, None),
109 };
110
111 Ok(RealmSecretState {
112 current_hash,
113 previous_hash,
114 previous_valid_until,
115 })
116}
117
118pub async fn verify_realm_secret(
120 realm_id: u32,
121 provided_secret: Option<&str>,
122) -> Result<RealmSecretCheck, RealmError> {
123 let realm = Realm::get(realm_id).await?.ok_or(RealmError::NotFound)?;
124
125 if realm.secret_current.is_empty() {
127 return Ok(RealmSecretCheck::NotConfigured);
128 }
129
130 let provided = provided_secret.map(str::trim).filter(|v| !v.is_empty());
131 let Some(provided) = provided else {
132 return Ok(RealmSecretCheck::MissingRequired);
133 };
134
135 let provided_hash = hash_realm_secret(provided);
136 if provided_hash == realm.secret_current {
137 return Ok(RealmSecretCheck::ValidCurrent);
138 }
139
140 if let Some((prev_hash, valid_until)) = &realm.secret_previous {
142 let now = Utc::now().timestamp() as u64;
143 if now <= *valid_until && provided_hash == *prev_hash {
144 return Ok(RealmSecretCheck::ValidPrevious);
145 }
146 }
147
148 Ok(RealmSecretCheck::Invalid)
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154 use crate::util::test_utils::utils::setup_test_db;
155 use serial_test::serial;
156
157 #[tokio::test]
158 #[serial]
159 async fn test_create_and_verify_secret() -> anyhow::Result<()> {
160 setup_test_db().await?;
161
162 let secret = generate_realm_secret();
163 let hash = hash_realm_secret(&secret);
164 let realm = Realm::create("test-secret-realm".to_string(), hash).await?;
165
166 let result = verify_realm_secret(realm.id, Some(&secret)).await?;
167 assert_eq!(result, RealmSecretCheck::ValidCurrent);
168
169 let missing = verify_realm_secret(realm.id, None).await?;
170 assert_eq!(missing, RealmSecretCheck::MissingRequired);
171
172 let invalid = verify_realm_secret(realm.id, Some("wrong-secret")).await?;
173 assert_eq!(invalid, RealmSecretCheck::Invalid);
174
175 Ok(())
176 }
177
178 #[tokio::test]
179 #[serial]
180 async fn test_rotate_secret_keeps_previous_temporarily_valid() -> anyhow::Result<()> {
181 setup_test_db().await?;
182
183 let old_secret = generate_realm_secret();
184 let old_hash = hash_realm_secret(&old_secret);
185 let realm = Realm::create("test-rotate-realm".to_string(), old_hash).await?;
186
187 let rotated = rotate_realm_secret(realm.id, Some(60)).await?;
188
189 let new_ok = verify_realm_secret(realm.id, Some(&rotated.new_secret)).await?;
190 assert_eq!(new_ok, RealmSecretCheck::ValidCurrent);
191
192 let old_ok = verify_realm_secret(realm.id, Some(&old_secret)).await?;
193 assert_eq!(old_ok, RealmSecretCheck::ValidPrevious);
194
195 Ok(())
196 }
197
198 #[tokio::test]
199 #[serial]
200 async fn test_not_configured_secret() -> anyhow::Result<()> {
201 setup_test_db().await?;
202
203 let realm = Realm::create("test-no-secret".to_string(), String::new()).await?;
205
206 let result = verify_realm_secret(realm.id, None).await?;
207 assert_eq!(result, RealmSecretCheck::NotConfigured);
208
209 Ok(())
210 }
211}