pulseengine_mcp_auth/crypto/
keys.rs1use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
7use rand::{Rng, RngCore, distributions::Alphanumeric};
8
9#[derive(Debug, thiserror::Error)]
11pub enum KeyDerivationError {
12 #[error("Invalid input: {0}")]
13 InvalidInput(String),
14
15 #[error("Derivation failed: {0}")]
16 DerivationFailed(String),
17}
18
19pub fn generate_secure_key() -> String {
24 let mut key_bytes = [0u8; 32];
26 rand::thread_rng().fill_bytes(&mut key_bytes);
27
28 URL_SAFE_NO_PAD.encode(&key_bytes)
30}
31
32pub fn generate_secure_key_with_length(bytes: usize) -> String {
34 let mut key_bytes = vec![0u8; bytes];
35 rand::thread_rng().fill_bytes(&mut key_bytes);
36
37 URL_SAFE_NO_PAD.encode(&key_bytes)
38}
39
40pub fn generate_key_id(role: &str) -> String {
45 let timestamp = chrono::Utc::now().timestamp();
46 let random: String = rand::thread_rng()
47 .sample_iter(&Alphanumeric)
48 .take(8)
49 .map(char::from)
50 .collect();
51
52 format!("lmcp_{}_{timestamp}_{random}", role.to_lowercase())
53}
54
55pub fn derive_key(
60 input: &str,
61 salt: &[u8],
62 iterations: u32,
63) -> Result<[u8; 32], KeyDerivationError> {
64 use pbkdf2::pbkdf2_hmac;
65 use sha2::Sha256;
66
67 if input.is_empty() {
68 return Err(KeyDerivationError::InvalidInput("Empty input".to_string()));
69 }
70
71 if salt.is_empty() {
72 return Err(KeyDerivationError::InvalidInput("Empty salt".to_string()));
73 }
74
75 if iterations == 0 {
76 return Err(KeyDerivationError::InvalidInput(
77 "Iterations must be > 0".to_string(),
78 ));
79 }
80
81 let mut key = [0u8; 32];
82 pbkdf2_hmac::<Sha256>(input.as_bytes(), salt, iterations, &mut key);
83
84 Ok(key)
85}
86
87pub fn generate_master_key() -> Result<[u8; 32], KeyDerivationError> {
91 generate_master_key_for_application(None)
92}
93
94pub fn generate_master_key_for_application(
98 app_name: Option<&str>,
99) -> Result<[u8; 32], KeyDerivationError> {
100 if let Some(app) = app_name {
105 let app_specific_var = format!(
106 "PULSEENGINE_MCP_MASTER_KEY_{}",
107 app.to_uppercase().replace('-', "_")
108 );
109 if let Ok(master_key_b64) = std::env::var(&app_specific_var) {
110 return decode_master_key(&master_key_b64);
111 }
112 }
113
114 if let Ok(master_key_b64) = std::env::var("PULSEENGINE_MCP_MASTER_KEY") {
116 return decode_master_key(&master_key_b64);
117 }
118
119 let mut key = [0u8; 32];
121 rand::thread_rng().fill_bytes(&mut key);
122
123 tracing::warn!(
125 "Generated new master key. Set PULSEENGINE_MCP_MASTER_KEY={} for persistence",
126 URL_SAFE_NO_PAD.encode(&key)
127 );
128
129 Ok(key)
130}
131
132fn decode_master_key(master_key_b64: &str) -> Result<[u8; 32], KeyDerivationError> {
134 let key_bytes = URL_SAFE_NO_PAD
135 .decode(master_key_b64)
136 .map_err(|e| KeyDerivationError::InvalidInput(format!("Invalid master key: {}", e)))?;
137
138 if key_bytes.len() != 32 {
139 return Err(KeyDerivationError::InvalidInput(format!(
140 "Master key must be 32 bytes, got {}",
141 key_bytes.len()
142 )));
143 }
144
145 let mut key = [0u8; 32];
146 key.copy_from_slice(&key_bytes);
147 Ok(key)
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 #[test]
155 fn test_generate_secure_key() {
156 let key1 = generate_secure_key();
157 let key2 = generate_secure_key();
158
159 assert_ne!(key1, key2);
161
162 assert_eq!(key1.len(), 43);
164 assert!(!key1.contains('+'));
165 assert!(!key1.contains('/'));
166 assert!(!key1.contains('='));
167 }
168
169 #[test]
170 fn test_generate_key_id() {
171 let id1 = generate_key_id("admin");
172 let id2 = generate_key_id("admin");
173
174 assert_ne!(id1, id2);
176
177 assert!(id1.starts_with("lmcp_admin_"));
179 assert!(id1.matches('_').count() == 3);
180 }
181
182 #[test]
183 fn test_derive_key() {
184 let password = "test-password";
185 let salt = b"test-salt-1234567890";
186
187 let key1 = derive_key(password, salt, 1000).unwrap();
188 let key2 = derive_key(password, salt, 1000).unwrap();
189
190 assert_eq!(key1, key2);
192
193 let key3 = derive_key(password, b"different-salt", 1000).unwrap();
195 assert_ne!(key1, key3);
196
197 let key4 = derive_key(password, salt, 2000).unwrap();
199 assert_ne!(key1, key4);
200 }
201
202 #[test]
203 fn test_derive_key_validation() {
204 assert!(derive_key("", b"salt", 1000).is_err());
206
207 assert!(derive_key("password", b"", 1000).is_err());
209
210 assert!(derive_key("password", b"salt", 0).is_err());
212 }
213}