Skip to main content

encryption/
guard.rs

1// Copyright (c) 2026 erik <erik@erik.xyz> — https://erik.xyz
2
3//! 请求守卫:交给处理器的一个共享加密句柄。
4//!
5//! 框架集成层(axum / actix-web / rocket / poem / salvo / warp / bee-rust / e-cat)
6//! 全都在这一层之上做薄适配 —— 把 [`Guard`] 按各自框架的惯例注入请求上下文,
7//! 处理器拿到它就能加解密,不必把主密钥或 Manager 一路透传下去。
8//!
9//! ```no_run
10//! use encryption::guard::Guard;
11//!
12//! let master_key = [7u8; 32]; // 生产环境从环境变量 / KMS 读取
13//! let guard = Guard::from_master_key(&master_key, "aes-256-gcm")?;
14//!
15//! // 处理器里
16//! let stored = guard.encrypt("13800138000".as_bytes())?;
17//! assert_eq!(guard.decrypt(&stored)?, "13800138000".as_bytes());
18//! # Ok::<(), encryption::Error>(())
19//! ```
20//!
21//! # 为什么是 `Arc`
22//!
23//! 每个请求都要拿到同一个 Manager,而框架普遍要求注入的状态是 `Send + Sync + 'static`
24//! 且常常要求 `Clone`。Manager 本身是只读的、构造后不再变,所以 `Arc` 共享即可 ——
25//! 子密钥只在构造时派生一次,每请求零成本。
26
27use std::sync::Arc;
28
29use base64::Engine as _;
30use zeroize::Zeroizing;
31
32use crate::error::{Error, Result};
33use crate::manager::EncryptionManager;
34
35/// 主密钥环境变量名。
36const MASTER_KEY_ENV: &str = "ENCRYPTION_MASTER_KEY";
37/// 默认算法环境变量名(可选)。
38const ALGORITHM_ENV: &str = "ENCRYPTION_ALGORITHM";
39/// 未指定 `ENCRYPTION_ALGORITHM` 时使用的算法。
40const DEFAULT_ALGORITHM: &str = "aes-256-gcm";
41
42/// 请求守卫:一个可跨请求共享、可跨线程克隆的加密句柄。
43///
44/// 构造之后的一切加解密都经由它转发到 [`EncryptionManager`];克隆只是
45/// `Arc` 引用计数递增,不含任何密钥拷贝。
46#[derive(Debug, Clone)]
47pub struct Guard {
48    manager: Arc<EncryptionManager>,
49}
50
51impl Guard {
52    /// 从一个已经建好的 Manager 构造。
53    pub fn new(manager: EncryptionManager) -> Self {
54        Self {
55            manager: Arc::new(manager),
56        }
57    }
58
59    /// 从 32 字节主密钥构造(内部走 [`crate::factory::EncryptionManagerFactory`])。
60    ///
61    /// 构造时即完成全部校验(主密钥长度、默认算法是否注册),配置错了就在启动时炸。
62    pub fn from_master_key(master_key: &[u8], default_identifier: &str) -> Result<Self> {
63        Ok(Self::new(
64            crate::factory::EncryptionManagerFactory::from_master_key(
65                master_key,
66                default_identifier,
67            )?,
68        ))
69    }
70
71    /// 从进程环境变量构造。
72    ///
73    /// 读取 `ENCRYPTION_MASTER_KEY`(必需)与 `ENCRYPTION_ALGORITHM`(可选,
74    /// 缺省 `aes-256-gcm`)。主密钥值的格式见 [`Self::from_env_with`]。
75    pub fn from_env() -> Result<Self> {
76        Self::from_env_with(|key| std::env::var(key).ok())
77    }
78
79    /// 从可注入的查找函数构造:测试与自定义配置源(KMS / 配置中心)走这里。
80    ///
81    /// 主密钥取值 `lookup("ENCRYPTION_MASTER_KEY")`,格式为显式前缀
82    /// `hex:<十六进制>` / `base64:<Base64>`;无前缀时自动判定:去掉首尾空白后
83    /// 恰为 64 个十六进制字符按 hex,否则按 Base64。解码后必须恰好 32 字节,
84    /// 否则 [`Error::InvalidMasterKey`];算法名未注册时冒泡
85    /// [`Error::UnknownIdentifier`]。
86    ///
87    /// ```
88    /// use encryption::guard::Guard;
89    ///
90    /// // 假 lookup:真实场景用 std::env::var 或配置中心
91    /// let lookup = |key: &str| match key {
92    ///     "ENCRYPTION_MASTER_KEY" => Some("00".repeat(32)), // 裸 hex(64 位)
93    ///     _ => None,
94    /// };
95    ///
96    /// let guard = Guard::from_env_with(lookup)?;
97    /// assert_eq!(guard.manager().default_identifier(), "aes-256-gcm");
98    ///
99    /// let blob = guard.encrypt("13800138000".as_bytes())?;
100    /// assert_eq!(guard.decrypt(&blob)?, "13800138000".as_bytes());
101    /// # Ok::<(), encryption::Error>(())
102    /// ```
103    pub fn from_env_with<F: Fn(&str) -> Option<String>>(lookup: F) -> Result<Self> {
104        // 环境变量原文即主密钥的编码形态:包进 Zeroizing,用完(含提前返回)即清零。
105        let raw = Zeroizing::new(lookup(MASTER_KEY_ENV).ok_or(Error::MissingConfig {
106            key: MASTER_KEY_ENV,
107        })?);
108        let algorithm = lookup(ALGORITHM_ENV).unwrap_or_else(|| DEFAULT_ALGORITHM.to_owned());
109        // 解码结果(定长主密钥)同样在交给工厂后清零。
110        let master_key = Zeroizing::new(decode_master_key(raw.as_str())?);
111        Self::from_master_key(master_key.as_slice(), &algorithm)
112    }
113
114    /// 共享的 Manager。总是存在。
115    pub fn manager(&self) -> &EncryptionManager {
116        &self.manager
117    }
118
119    /// 用默认算法加密。
120    pub fn encrypt(&self, plaintext: &[u8]) -> Result<Vec<u8>> {
121        self.manager.encrypt(plaintext)
122    }
123
124    /// 用默认算法解密。
125    pub fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>> {
126        self.manager.decrypt(ciphertext)
127    }
128
129    /// 用指定算法加密。
130    pub fn encrypt_with(&self, identifier: &str, plaintext: &[u8]) -> Result<Vec<u8>> {
131        self.manager.encrypt_with(identifier, plaintext)
132    }
133
134    /// 用指定算法解密。
135    pub fn decrypt_with(&self, identifier: &str, ciphertext: &[u8]) -> Result<Vec<u8>> {
136        self.manager.decrypt_with(identifier, ciphertext)
137    }
138
139    /// 已注册算法标识(健康检查 / 日志用)。
140    pub fn identifiers(&self) -> Vec<&str> {
141        self.manager.registry().identifiers()
142    }
143}
144
145/// 解析主密钥取值:显式前缀 `hex:` / `base64:`,无前缀自动判定,
146/// 解码失败或长度不为 32 字节一律 [`Error::InvalidMasterKey`]。
147fn decode_master_key(raw: &str) -> Result<[u8; 32]> {
148    let value = raw.trim();
149    let (is_hex, payload) = if let Some(hex) = value.strip_prefix("hex:") {
150        (true, hex)
151    } else if let Some(b64) = value.strip_prefix("base64:") {
152        (false, b64)
153    } else if value.len() == 64 && value.bytes().all(|b| b.is_ascii_hexdigit()) {
154        (true, value)
155    } else {
156        (false, value)
157    };
158
159    // 解码结果即主密钥本体:包进 Zeroizing,成功与否都在离开作用域时清零
160    // (长度不符的早退路径同样清零,错误分支不做任何额外分配)。
161    let decoded = Zeroizing::new(
162        if is_hex {
163            crate::internal::hex_decode(payload).ok()
164        } else {
165            base64::engine::general_purpose::STANDARD
166                .decode(payload)
167                .ok()
168        }
169        // 解码失败时字节长度未知,got 记 0。
170        .ok_or(Error::InvalidMasterKey { got: 0 })?,
171    );
172
173    decoded
174        .as_slice()
175        .try_into()
176        .map_err(|_| Error::InvalidMasterKey { got: decoded.len() })
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182
183    fn guard() -> Guard {
184        Guard::from_master_key(&[7u8; 32], "aes-256-gcm").unwrap()
185    }
186
187    #[test]
188    fn wraps_the_factory_and_forwards() {
189        let guard = guard();
190        assert_eq!(guard.identifiers().len(), 5);
191        assert_eq!(guard.manager().default_identifier(), "aes-256-gcm");
192
193        let blob = guard.encrypt(b"phone").unwrap();
194        assert_eq!(guard.decrypt(&blob).unwrap(), b"phone");
195
196        let blob = guard.encrypt_with("sm4-cbc", b"phone").unwrap();
197        assert_eq!(guard.decrypt_with("sm4-cbc", &blob).unwrap(), b"phone");
198    }
199
200    #[test]
201    fn clones_share_one_manager() {
202        let guard = guard();
203        let clone = guard.clone();
204        let blob = guard.encrypt(b"shared").unwrap();
205        assert_eq!(clone.decrypt(&blob).unwrap(), b"shared");
206        assert_eq!(Arc::strong_count(&guard.manager), 2);
207    }
208
209    #[test]
210    fn invalid_master_key_fails_at_construction() {
211        assert!(Guard::from_master_key(&[0u8; 16], "aes-256-gcm").is_err());
212        assert!(Guard::from_master_key(&[0u8; 32], "no-such-algorithm").is_err());
213    }
214
215    /// 假 lookup:从一对固定取值里查,不碰 std::env。
216    fn fake_env(entries: Vec<(&str, String)>) -> impl Fn(&str) -> Option<String> {
217        move |key| {
218            entries
219                .iter()
220                .find(|(k, _)| *k == key)
221                .map(|(_, v)| v.clone())
222        }
223    }
224
225    fn roundtrip(guard: &Guard) {
226        let blob = guard.encrypt(b"phone").unwrap();
227        assert_eq!(guard.decrypt(&blob).unwrap(), b"phone");
228    }
229
230    #[test]
231    fn from_env_hex_prefix() {
232        let hex = "07".repeat(32);
233        let lookup = fake_env(vec![(MASTER_KEY_ENV, format!("hex:{hex}"))]);
234        let guard = Guard::from_env_with(lookup).unwrap();
235        assert_eq!(guard.manager().default_identifier(), "aes-256-gcm");
236        roundtrip(&guard);
237    }
238
239    #[test]
240    fn from_env_base64_prefix() {
241        // [7u8; 32] 的 Base64
242        let b64 = "BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc=";
243        let lookup = fake_env(vec![(MASTER_KEY_ENV, format!("base64:{b64}"))]);
244        let guard = Guard::from_env_with(lookup).unwrap();
245        roundtrip(&guard);
246    }
247
248    #[test]
249    fn from_env_bare_hex_auto_detected() {
250        let hex = "00".repeat(32);
251        let lookup = fake_env(vec![(MASTER_KEY_ENV, hex)]);
252        let guard = Guard::from_env_with(lookup).unwrap();
253        roundtrip(&guard);
254    }
255
256    #[test]
257    fn from_env_missing_master_key() {
258        let err = Guard::from_env_with(fake_env(vec![])).unwrap_err();
259        assert_eq!(
260            err,
261            Error::MissingConfig {
262                key: "ENCRYPTION_MASTER_KEY"
263            }
264        );
265    }
266
267    #[test]
268    fn from_env_wrong_length() {
269        // 16 字节(hex: 前缀)→ InvalidMasterKey { got: 16 }
270        let lookup = fake_env(vec![(MASTER_KEY_ENV, format!("hex:{}", "00".repeat(16)))]);
271        assert_eq!(
272            Guard::from_env_with(lookup).unwrap_err(),
273            Error::InvalidMasterKey { got: 16 }
274        );
275
276        // 24 字节(base64: 前缀)→ InvalidMasterKey { got: 24 }
277        let lookup = fake_env(vec![(MASTER_KEY_ENV, format!("base64:{}", "A".repeat(32)))]);
278        assert_eq!(
279            Guard::from_env_with(lookup).unwrap_err(),
280            Error::InvalidMasterKey { got: 24 }
281        );
282
283        // 非法 hex → 解码失败,got 记 0
284        let lookup = fake_env(vec![(MASTER_KEY_ENV, "hex:zz".to_owned())]);
285        assert_eq!(
286            Guard::from_env_with(lookup).unwrap_err(),
287            Error::InvalidMasterKey { got: 0 }
288        );
289    }
290
291    #[test]
292    fn from_env_respects_algorithm() {
293        let lookup = fake_env(vec![
294            (MASTER_KEY_ENV, "07".repeat(32)),
295            (ALGORITHM_ENV, "sm4-cbc".to_owned()),
296        ]);
297        let guard = Guard::from_env_with(lookup).unwrap();
298        assert_eq!(guard.manager().default_identifier(), "sm4-cbc");
299        roundtrip(&guard);
300    }
301
302    #[test]
303    fn from_env_unknown_algorithm_bubbles() {
304        let lookup = fake_env(vec![
305            (MASTER_KEY_ENV, "07".repeat(32)),
306            (ALGORITHM_ENV, "no-such-algorithm".to_owned()),
307        ]);
308        assert!(matches!(
309            Guard::from_env_with(lookup).unwrap_err(),
310            Error::UnknownIdentifier { .. }
311        ));
312    }
313}