Skip to main content

platform/realm/
secret.rs

1//! Realm secret 管理
2//!
3//! 用于 Admin 分配、AIS 校验 realm secret。
4//! Secret 数据直接存储在 Realm 表的 secret_current / secret_previous 字段中。
5
6use 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
12/// signaling -> AIS 传递 realm secret 的 HTTP 头
13pub const REALM_SECRET_HEADER: &str = "x-actrix-realm-secret";
14
15/// 旧 secret 的默认兼容窗口(4 小时)
16pub const DEFAULT_REALM_SECRET_PREVIOUS_GRACE_SECS: u64 = 4 * 3600;
17
18/// Realm secret 校验结果
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub enum RealmSecretCheck {
21    /// realm 未配置 secret(兼容历史 realm)
22    NotConfigured,
23    /// 命中当前 secret
24    ValidCurrent,
25    /// 命中上一代 secret(且仍在兼容窗口内)
26    ValidPrevious,
27    /// realm 已配置 secret,但请求未提供
28    MissingRequired,
29    /// 请求提供了 secret,但不匹配当前/可用上一代
30    Invalid,
31}
32
33/// 轮转结果(仅返回明文给调用方一次)
34#[derive(Debug, Clone)]
35pub struct RealmSecretRotation {
36    pub new_secret: String,
37    pub previous_valid_until: Option<u64>,
38}
39
40/// Secret 状态元数据
41#[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
48/// 生成用于加入 realm 的 secret(URL-safe,便于 CLI / URL 透传)
49pub 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
55/// Hash a realm secret with SHA256.
56pub 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
62/// 轮转 realm secret(旧 secret 进入兼容窗口)
63///
64/// 加载 Realm → current→previous → 生成新 hash → save
65pub 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    // Move current to previous (if current is non-empty)
77    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    // Generate new secret
84    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
96/// 获取 realm secret 状态元数据(仅 hash,不含明文)
97pub 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
118/// 按 realm id 校验请求 secret。
119pub 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    // 历史 realm 未配置 secret:保持兼容
126    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    // Check previous secret within grace window
141    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        // Create realm with empty secret
204        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}