Skip to main content

yggdrasil/mcp/
crypto.rs

1//! MCP token 静态加密(AES-GCM-256)。
2//!
3//! 设计要点:
4//! - 明文 bearer token 永不裸存于 DB。存储列 `token_enc` 是 AES-GCM 密文
5//!   (nonce ‖ ciphertext ‖ tag)的 hex 编码;管理员可在后台解密重查明文。
6//! - `token_hash`(明文 SHA-256 hex)用于每请求 O(1) 常量查找,见 `auth.rs`。
7//! - 主密钥来自环境变量 `MCP_TOKEN_ENC_KEY`(hex 编码的 32 字节,64 个 hex 字符;
8//!   可用 `openssl rand -hex 32` 生成)。缺失或非法时 `mcp_enc_key()` 返回 None,
9//!   调用方按「MCP 不可用」降级(拒绝签发/认证),不 panic——符合 AGENTS.md §16。
10//!
11//! AES-GCM-256:12 字节随机 nonce(每次加密独立生成),认证标签隐含在密文尾部。
12//! nonce 复用是 AES-GCM 的唯一致命风险;这里每次 `encrypt_token` 都新取 nonce,
13//! 且 nonce 与密文一同存储,解密时无需额外恢复。仅用 hex(已是直接依赖)编码,
14//! 避免把 base64 由传递依赖提升为直接依赖。
15
16use aes_gcm::aead::{Aead, KeyInit, OsRng};
17use aes_gcm::{AeadCore, Aes256Gcm, Key, Nonce};
18
19/// 从环境读取并解码主密钥;缺失或非法返回 None(降级,不 panic)。
20///
21/// 接受 hex 编码的 32 字节(64 个 hex 字符)。解码后必须正好 32 字节(AES-256)。
22pub fn mcp_enc_key() -> Option<[u8; 32]> {
23    let raw = std::env::var("MCP_TOKEN_ENC_KEY").ok()?;
24    let trimmed = raw.trim();
25    if trimmed.is_empty() {
26        return None;
27    }
28    let bytes = hex::decode(trimmed).ok()?;
29    if bytes.len() == 32 {
30        let mut arr = [0u8; 32];
31        arr.copy_from_slice(&bytes);
32        Some(arr)
33    } else {
34        None
35    }
36}
37
38/// 加密明文 token,返回 `nonce‖ct‖tag` 的 hex 字符串(存入 `token_enc`)。
39///
40/// 失败仅在不持有有效主密钥时(调用方应在签发前已检查 `mcp_enc_key().is_some()`)。
41pub fn encrypt_token(plaintext: &str) -> Option<String> {
42    let key_bytes = mcp_enc_key()?;
43    let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(&key_bytes));
44    let nonce = Aes256Gcm::generate_nonce(&mut OsRng); // 12 字节,每次独立
45    let ct = cipher.encrypt(&nonce, plaintext.as_bytes()).ok()?;
46    let mut buf = Vec::with_capacity(nonce.len() + ct.len());
47    buf.extend_from_slice(nonce.as_slice());
48    buf.extend_from_slice(&ct);
49    Some(hex::encode(buf))
50}
51
52/// 解密 `token_enc`(`nonce‖ct‖tag` 的 hex)还原明文 token。
53///
54/// 失败(密钥缺失、hex 非法、密文被篡改、nonce 长度不符)统一返回 None——
55/// 调用方无法区分具体原因,按「该 token 不可解密」处理(等同于失效)。
56pub fn decrypt_token(enc_hex: &str) -> Option<String> {
57    let key_bytes = mcp_enc_key()?;
58    let buf = hex::decode(enc_hex).ok()?;
59    // AES-GCM-256 nonce 固定 12 字节;短于此必然损坏。
60    if buf.len() < 12 {
61        return None;
62    }
63    let (nonce_bytes, ct) = buf.split_at(12);
64    let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(&key_bytes));
65    let nonce = Nonce::from_slice(nonce_bytes);
66    let pt = cipher.decrypt(nonce, ct).ok()?;
67    String::from_utf8(pt).ok()
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73
74    /// 测试用:把任意 32 字节映射到临时 hex 密钥环境,跑完闭包后还原 env。
75    /// 返回闭包的结果,便于 `let enc = with_key(&key, || encrypt_token(...))`。
76    /// 串行化(serial_test)避免跨用例污染进程级 env。
77    fn with_key<F: FnOnce() -> R, R>(key: &[u8; 32], body: F) -> R {
78        let prev = std::env::var("MCP_TOKEN_ENC_KEY").ok();
79        std::env::set_var("MCP_TOKEN_ENC_KEY", hex::encode(key));
80        // scope guard: 还原 env
81        struct Restore(Option<String>);
82        impl Drop for Restore {
83            fn drop(&mut self) {
84                match &self.0 {
85                    Some(v) => std::env::set_var("MCP_TOKEN_ENC_KEY", v),
86                    None => std::env::remove_var("MCP_TOKEN_ENC_KEY"),
87                }
88            }
89        }
90        let _r = Restore(prev);
91        body()
92    }
93
94    #[test]
95    #[serial_test::serial]
96    fn roundtrip_basic() {
97        let key = [42u8; 32];
98        with_key(&key, || {
99            let enc = encrypt_token("ygg_secret_token_value").expect("encrypt");
100            let dec = decrypt_token(&enc).expect("decrypt");
101            assert_eq!(dec, "ygg_secret_token_value");
102        });
103    }
104
105    #[test]
106    #[serial_test::serial]
107    fn roundtrip_unicode_and_long() {
108        let key = [7u8; 32];
109        let plaintext = "ygg_".to_string() + &"中文标题-漢字-🔑 ".repeat(500);
110        with_key(&key, || {
111            let enc = encrypt_token(&plaintext).expect("encrypt");
112            assert_eq!(decrypt_token(&enc).expect("decrypt"), plaintext);
113        });
114    }
115
116    #[test]
117    #[serial_test::serial]
118    fn distinct_nonces_per_encryption() {
119        // 相同明文加密两次,密文必须不同(nonce 随机);都可解回同一明文。
120        let key = [9u8; 32];
121        with_key(&key, || {
122            let a = encrypt_token("same").expect("encrypt");
123            let b = encrypt_token("same").expect("encrypt");
124            assert_ne!(a, b, "nonce must differ → ciphertext differs");
125            assert_eq!(decrypt_token(&a).unwrap(), "same");
126            assert_eq!(decrypt_token(&b).unwrap(), "same");
127        });
128    }
129
130    #[test]
131    #[serial_test::serial]
132    fn tampered_ciphertext_fails() {
133        let key = [1u8; 32];
134        with_key(&key, || {
135            let mut enc = encrypt_token("payload").expect("encrypt");
136            // 翻转最后一个字节(GCM tag 区域)→ 认证失败
137            let mut bytes = hex::decode(&enc).unwrap();
138            let last = bytes.len() - 1;
139            bytes[last] ^= 0xff;
140            enc = hex::encode(&bytes);
141            assert_eq!(decrypt_token(&enc), None);
142        });
143    }
144
145    #[test]
146    #[serial_test::serial]
147    fn missing_key_returns_none() {
148        std::env::remove_var("MCP_TOKEN_ENC_KEY");
149        assert_eq!(mcp_enc_key(), None);
150        assert_eq!(encrypt_token("x"), None);
151        assert_eq!(decrypt_token("deadbeef"), None);
152    }
153
154    #[test]
155    #[serial_test::serial]
156    fn wrong_key_cannot_decrypt() {
157        let enc = {
158            let key = [1u8; 32];
159            with_key(&key, || encrypt_token("secret").unwrap())
160        };
161        // 换另一个密钥:解密必须失败(认证标签不匹配)
162        with_key(&[2u8; 32], || {
163            assert_eq!(decrypt_token(&enc), None);
164        });
165    }
166
167    #[test]
168    #[serial_test::serial]
169    fn accepts_hex_encoding() {
170        let key = [5u8; 32];
171        std::env::set_var("MCP_TOKEN_ENC_KEY", hex::encode(key));
172        assert_eq!(mcp_enc_key(), Some(key));
173        std::env::remove_var("MCP_TOKEN_ENC_KEY");
174    }
175
176    #[test]
177    #[serial_test::serial]
178    fn rejects_odd_length_hex() {
179        // 奇数长度 hex 非法 → None
180        std::env::set_var("MCP_TOKEN_ENC_KEY", "abc");
181        assert_eq!(mcp_enc_key(), None);
182        std::env::remove_var("MCP_TOKEN_ENC_KEY");
183    }
184
185    #[test]
186    #[serial_test::serial]
187    fn rejects_short_key() {
188        // 16 字节(32 hex 字符)非 AES-256 密钥,必须拒绝
189        std::env::set_var("MCP_TOKEN_ENC_KEY", hex::encode([0u8; 16]));
190        assert_eq!(mcp_enc_key(), None, "16 字节非 AES-256 密钥,必须拒绝");
191        std::env::remove_var("MCP_TOKEN_ENC_KEY");
192    }
193}