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, Generate, KeyInit};
17use aes_gcm::{Aes256Gcm, 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/// 密钥缺失或非法、系统随机源失败、加密失败时返回 None。
41pub fn encrypt_token(plaintext: &str) -> Option<String> {
42    let key_bytes = mcp_enc_key()?;
43    let cipher = Aes256Gcm::new((&key_bytes).into());
44    let nonce = Nonce::try_generate().ok()?; // 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_bytes).into());
65    let nonce = nonce_bytes.try_into().ok()?;
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 decrypt_legacy_nist_empty_plaintext() {
97        // AES-GCM 0.10.3 tests/aes256gcm.rs: first NIST CAVS vector,
98        // gcmEncryptExtIV256.rsp, empty plaintext and AAD.
99        // https://github.com/RustCrypto/AEADs/blob/aes-gcm-v0.10.3/aes-gcm/tests/aes256gcm.rs
100        let key: [u8; 32] =
101            hex::decode("b52c505a37d78eda5dd34f20c22540ea1b58963cf8e5bf8ffa85f9f2492505b4")
102                .unwrap()
103                .try_into()
104                .unwrap();
105        let enc = concat!(
106            "516c33929df5a3284ff463d7",
107            "bdc1ac884d332457a1d2664f168c76f0"
108        );
109        with_key(&key, || {
110            assert_eq!(decrypt_token(enc).as_deref(), Some(""));
111        });
112    }
113
114    #[test]
115    #[serial_test::serial]
116    fn roundtrip_basic() {
117        let key = [42u8; 32];
118        with_key(&key, || {
119            let enc = encrypt_token("ygg_secret_token_value").expect("encrypt");
120            let dec = decrypt_token(&enc).expect("decrypt");
121            assert_eq!(dec, "ygg_secret_token_value");
122        });
123    }
124
125    #[test]
126    #[serial_test::serial]
127    fn roundtrip_unicode_and_long() {
128        let key = [7u8; 32];
129        let plaintext = "ygg_".to_string() + &"中文标题-漢字-🔑 ".repeat(500);
130        with_key(&key, || {
131            let enc = encrypt_token(&plaintext).expect("encrypt");
132            assert_eq!(decrypt_token(&enc).expect("decrypt"), plaintext);
133        });
134    }
135
136    #[test]
137    #[serial_test::serial]
138    fn distinct_nonces_per_encryption() {
139        // 相同明文加密两次,密文必须不同(nonce 随机);都可解回同一明文。
140        let key = [9u8; 32];
141        with_key(&key, || {
142            let a = encrypt_token("same").expect("encrypt");
143            let b = encrypt_token("same").expect("encrypt");
144            assert_ne!(a, b, "nonce must differ → ciphertext differs");
145            assert_eq!(decrypt_token(&a).unwrap(), "same");
146            assert_eq!(decrypt_token(&b).unwrap(), "same");
147        });
148    }
149
150    #[test]
151    #[serial_test::serial]
152    fn tampered_ciphertext_fails() {
153        let key = [1u8; 32];
154        with_key(&key, || {
155            let mut enc = encrypt_token("payload").expect("encrypt");
156            // 翻转最后一个字节(GCM tag 区域)→ 认证失败
157            let mut bytes = hex::decode(&enc).unwrap();
158            let last = bytes.len() - 1;
159            bytes[last] ^= 0xff;
160            enc = hex::encode(&bytes);
161            assert_eq!(decrypt_token(&enc), None);
162        });
163    }
164
165    #[test]
166    #[serial_test::serial]
167    fn missing_key_returns_none() {
168        std::env::remove_var("MCP_TOKEN_ENC_KEY");
169        assert_eq!(mcp_enc_key(), None);
170        assert_eq!(encrypt_token("x"), None);
171        assert_eq!(decrypt_token("deadbeef"), None);
172    }
173
174    #[test]
175    #[serial_test::serial]
176    fn wrong_key_cannot_decrypt() {
177        let enc = {
178            let key = [1u8; 32];
179            with_key(&key, || encrypt_token("secret").unwrap())
180        };
181        // 换另一个密钥:解密必须失败(认证标签不匹配)
182        with_key(&[2u8; 32], || {
183            assert_eq!(decrypt_token(&enc), None);
184        });
185    }
186
187    #[test]
188    #[serial_test::serial]
189    fn accepts_hex_encoding() {
190        let key = [5u8; 32];
191        std::env::set_var("MCP_TOKEN_ENC_KEY", hex::encode(key));
192        assert_eq!(mcp_enc_key(), Some(key));
193        std::env::remove_var("MCP_TOKEN_ENC_KEY");
194    }
195
196    #[test]
197    #[serial_test::serial]
198    fn rejects_odd_length_hex() {
199        // 奇数长度 hex 非法 → None
200        std::env::set_var("MCP_TOKEN_ENC_KEY", "abc");
201        assert_eq!(mcp_enc_key(), None);
202        std::env::remove_var("MCP_TOKEN_ENC_KEY");
203    }
204
205    #[test]
206    #[serial_test::serial]
207    fn rejects_short_key() {
208        // 16 字节(32 hex 字符)非 AES-256 密钥,必须拒绝
209        std::env::set_var("MCP_TOKEN_ENC_KEY", hex::encode([0u8; 16]));
210        assert_eq!(mcp_enc_key(), None, "16 字节非 AES-256 密钥,必须拒绝");
211        std::env::remove_var("MCP_TOKEN_ENC_KEY");
212    }
213}