pulseengine_mcp_auth/crypto/
keys.rs

1//! Secure key generation and derivation
2//!
3//! This module provides secure key generation similar to Loxone's
4//! approach, with URL-safe encoding and proper randomness.
5
6use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
7use rand::{Rng, RngCore, distributions::Alphanumeric};
8
9/// Key derivation errors
10#[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
19/// Generate a secure API key
20///
21/// This generates a URL-safe base64 encoded random key,
22/// similar to Loxone's generate_api_key function.
23pub fn generate_secure_key() -> String {
24    // Generate 32 bytes of randomness (256 bits)
25    let mut key_bytes = [0u8; 32];
26    rand::thread_rng().fill_bytes(&mut key_bytes);
27
28    // Encode as URL-safe base64 without padding
29    URL_SAFE_NO_PAD.encode(&key_bytes)
30}
31
32/// Generate a secure key with custom length
33pub 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
40/// Generate a human-friendly API key prefix
41///
42/// Format: lmcp_{role}_{timestamp}_{random}
43/// This matches Loxone's key ID format
44pub 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
55/// Derive a key from user input using PBKDF2
56///
57/// This is for cases where we need to derive a key from a password
58/// or other user input, with proper key stretching.
59pub 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
87/// Generate a master key from environment or secure storage
88///
89/// This is used to derive all other encryption keys
90pub fn generate_master_key() -> Result<[u8; 32], KeyDerivationError> {
91    generate_master_key_for_application(None)
92}
93
94/// Generate an application-specific master key from environment or secure storage
95///
96/// This checks for app-specific environment variables first, then falls back to generic ones
97pub fn generate_master_key_for_application(
98    app_name: Option<&str>,
99) -> Result<[u8; 32], KeyDerivationError> {
100    // In production, this should come from secure storage (HSM, vault, etc.)
101    // For now, we'll check environment variable or generate a new one
102
103    // First try app-specific environment variable if app_name is provided
104    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    // Fall back to generic environment variable
115    if let Ok(master_key_b64) = std::env::var("PULSEENGINE_MCP_MASTER_KEY") {
116        return decode_master_key(&master_key_b64);
117    }
118
119    // Generate a new master key
120    let mut key = [0u8; 32];
121    rand::thread_rng().fill_bytes(&mut key);
122
123    // Log warning about using generated key
124    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
132/// Decode a base64-encoded master key
133fn 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        // Keys should be different
160        assert_ne!(key1, key2);
161
162        // Keys should be URL-safe base64 (43 chars for 32 bytes without padding)
163        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        // IDs should be different (different timestamp/random)
175        assert_ne!(id1, id2);
176
177        // Check format
178        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        // Same input should produce same key
191        assert_eq!(key1, key2);
192
193        // Different salt should produce different key
194        let key3 = derive_key(password, b"different-salt", 1000).unwrap();
195        assert_ne!(key1, key3);
196
197        // Different iterations should produce different key
198        let key4 = derive_key(password, salt, 2000).unwrap();
199        assert_ne!(key1, key4);
200    }
201
202    #[test]
203    fn test_derive_key_validation() {
204        // Empty input should fail
205        assert!(derive_key("", b"salt", 1000).is_err());
206
207        // Empty salt should fail
208        assert!(derive_key("password", b"", 1000).is_err());
209
210        // Zero iterations should fail
211        assert!(derive_key("password", b"salt", 0).is_err());
212    }
213}