zlayer_secrets/
key_manager.rs1use std::fs;
16use std::path::{Path, PathBuf};
17
18#[cfg(unix)]
19use tracing::warn;
20use tracing::{debug, info};
21
22use crate::{EncryptionKey, Result, SecretsError};
23
24const ENV_KEY: &str = "ZLAYER_SECRETS_KEY";
26
27const ENV_PASSWORD: &str = "ZLAYER_SECRETS_PASSWORD";
29
30#[derive(Debug, Clone)]
44pub struct KeyManager {
45 base_dir: PathBuf,
46}
47
48impl Default for KeyManager {
49 fn default() -> Self {
50 Self::new()
51 }
52}
53
54impl KeyManager {
55 #[must_use]
59 pub fn new() -> Self {
60 Self {
61 base_dir: zlayer_paths::ZLayerDirs::system_default().secrets(),
62 }
63 }
64
65 #[must_use]
70 pub fn with_base_dir(base_dir: impl AsRef<Path>) -> Self {
71 Self {
72 base_dir: base_dir.as_ref().to_path_buf(),
73 }
74 }
75
76 fn key_file_path(&self, deployment: &str) -> PathBuf {
78 self.base_dir.join(format!("secrets_{deployment}.key"))
79 }
80
81 pub fn get_or_create_key(&self, deployment: &str) -> Result<EncryptionKey> {
123 if let Ok(hex_key) = std::env::var(ENV_KEY) {
125 debug!("Using encryption key from {ENV_KEY} environment variable");
126 return Self::key_from_hex(&hex_key);
127 }
128
129 if let Ok(password) = std::env::var(ENV_PASSWORD) {
131 debug!("Deriving encryption key from {ENV_PASSWORD} environment variable");
132 return Self::key_from_password(&password, deployment);
133 }
134
135 let key_path = self.key_file_path(deployment);
137 if key_path.exists() {
138 debug!("Loading encryption key from file: {}", key_path.display());
139 return Self::load_key_from_file(&key_path);
140 }
141
142 info!(
144 "Generating new encryption key for deployment '{}' at {}",
145 deployment,
146 key_path.display()
147 );
148 Self::generate_and_save_key(&key_path)
149 }
150
151 fn key_from_hex(hex_key: &str) -> Result<EncryptionKey> {
153 let key_bytes = hex::decode(hex_key.trim()).map_err(|e| {
154 SecretsError::Encryption(format!("Invalid hex-encoded key in {ENV_KEY}: {e}"))
155 })?;
156
157 EncryptionKey::from_bytes(&key_bytes)
158 }
159
160 fn key_from_password(password: &str, deployment: &str) -> Result<EncryptionKey> {
162 EncryptionKey::derive_from_password(password, deployment.as_bytes())
165 }
166
167 fn load_key_from_file(path: &Path) -> Result<EncryptionKey> {
169 let key_bytes = fs::read(path).map_err(|e| {
170 SecretsError::Encryption(format!("Failed to read key file {}: {e}", path.display()))
171 })?;
172
173 EncryptionKey::from_bytes(&key_bytes)
174 }
175
176 fn generate_and_save_key(path: &Path) -> Result<EncryptionKey> {
180 if let Some(parent) = path.parent() {
182 fs::create_dir_all(parent).map_err(|e| {
183 SecretsError::Encryption(format!(
184 "Failed to create key directory {}: {e}",
185 parent.display()
186 ))
187 })?;
188 }
189
190 let key = EncryptionKey::generate();
192
193 fs::write(path, key.as_bytes()).map_err(|e| {
195 SecretsError::Encryption(format!("Failed to write key file {}: {e}", path.display()))
196 })?;
197
198 #[cfg(unix)]
200 {
201 use std::os::unix::fs::PermissionsExt;
202 let permissions = fs::Permissions::from_mode(0o600);
203 if let Err(e) = fs::set_permissions(path, permissions) {
204 warn!(
205 "Failed to set permissions on key file {}: {e}",
206 path.display()
207 );
208 }
209 }
210
211 info!("Created new encryption key at {}", path.display());
212 Ok(key)
213 }
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219 use serial_test::serial;
220 use std::env;
221 use tempfile::TempDir;
222
223 struct EnvGuard;
225
226 impl EnvGuard {
227 fn new() -> Self {
228 env::remove_var(ENV_KEY);
230 env::remove_var(ENV_PASSWORD);
231 Self
232 }
233 }
234
235 impl Drop for EnvGuard {
236 fn drop(&mut self) {
237 env::remove_var(ENV_KEY);
238 env::remove_var(ENV_PASSWORD);
239 }
240 }
241
242 fn setup_manager() -> (KeyManager, TempDir) {
243 let temp_dir = TempDir::new().unwrap();
244 let manager = KeyManager::with_base_dir(temp_dir.path());
245 (manager, temp_dir)
246 }
247
248 #[test]
249 fn test_new_uses_default_dir() {
250 let manager = KeyManager::new();
251 let expected = zlayer_paths::ZLayerDirs::system_default().secrets();
252 assert_eq!(manager.base_dir, expected);
253 }
254
255 #[test]
256 fn test_with_base_dir() {
257 let manager = KeyManager::with_base_dir("/custom/path");
258 assert_eq!(manager.base_dir, PathBuf::from("/custom/path"));
259 }
260
261 #[test]
262 fn test_key_file_path() {
263 let dirs = zlayer_paths::ZLayerDirs::system_default();
264 let manager = KeyManager::with_base_dir(dirs.secrets());
265 let path = manager.key_file_path("production");
266 assert_eq!(path, dirs.secrets().join("secrets_production.key"));
267 }
268
269 #[test]
272 #[serial]
273 fn test_auto_generate_key() {
274 let _guard = EnvGuard::new();
275 let (manager, _temp) = setup_manager();
276
277 let key = manager.get_or_create_key("test-deployment").unwrap();
278 assert_eq!(key.as_bytes().len(), 32);
279
280 let key_path = manager.key_file_path("test-deployment");
282 assert!(key_path.exists());
283 }
284
285 #[test]
286 #[serial]
287 fn test_load_existing_key() {
288 let _guard = EnvGuard::new();
289 let (manager, _temp) = setup_manager();
290
291 let key1 = manager.get_or_create_key("test-deployment").unwrap();
293
294 let key2 = manager.get_or_create_key("test-deployment").unwrap();
296
297 assert_eq!(key1.as_bytes(), key2.as_bytes());
298 }
299
300 #[test]
301 #[serial]
302 fn test_different_deployments_get_different_keys() {
303 let _guard = EnvGuard::new();
304 let (manager, _temp) = setup_manager();
305
306 let key1 = manager.get_or_create_key("deployment-a").unwrap();
307 let key2 = manager.get_or_create_key("deployment-b").unwrap();
308
309 assert_ne!(key1.as_bytes(), key2.as_bytes());
310 }
311
312 #[test]
313 #[serial]
314 fn test_env_key_takes_priority() {
315 let _guard = EnvGuard::new();
316 let (manager, _temp) = setup_manager();
317
318 let known_key = [42u8; 32];
320 let hex_key = hex::encode(known_key);
321
322 env::set_var(ENV_KEY, &hex_key);
324
325 let key = manager.get_or_create_key("any-deployment").unwrap();
326 assert_eq!(key.as_bytes(), &known_key);
327 }
328
329 #[test]
330 #[serial]
331 fn test_env_password_takes_priority_over_file() {
332 let _guard = EnvGuard::new();
333 let (manager, _temp) = setup_manager();
334
335 let file_key = manager.get_or_create_key("test-deployment").unwrap();
337
338 env::set_var(ENV_PASSWORD, "my-secret-password");
340
341 let password_key = manager.get_or_create_key("test-deployment").unwrap();
343 assert_ne!(file_key.as_bytes(), password_key.as_bytes());
344 }
345
346 #[test]
347 #[serial]
348 fn test_password_derivation_is_deterministic() {
349 let _guard = EnvGuard::new();
350 let (manager, _temp) = setup_manager();
351
352 env::set_var(ENV_PASSWORD, "test-password");
353
354 let key1 = manager.get_or_create_key("deployment").unwrap();
355 let key2 = manager.get_or_create_key("deployment").unwrap();
356
357 assert_eq!(key1.as_bytes(), key2.as_bytes());
358 }
359
360 #[test]
361 #[serial]
362 fn test_password_with_different_deployments() {
363 let _guard = EnvGuard::new();
364 let (manager, _temp) = setup_manager();
365
366 env::set_var(ENV_PASSWORD, "same-password");
367
368 let key1 = manager.get_or_create_key("deployment-a").unwrap();
370 let key2 = manager.get_or_create_key("deployment-b").unwrap();
371
372 assert_ne!(key1.as_bytes(), key2.as_bytes());
373 }
374
375 #[test]
376 #[serial]
377 fn test_invalid_hex_key_error() {
378 let _guard = EnvGuard::new();
379 let (manager, _temp) = setup_manager();
380
381 env::set_var(ENV_KEY, "not-valid-hex!!");
382
383 let result = manager.get_or_create_key("test");
384 assert!(result.is_err());
385 }
386
387 #[test]
388 #[serial]
389 fn test_hex_key_wrong_length_error() {
390 let _guard = EnvGuard::new();
391 let (manager, _temp) = setup_manager();
392
393 env::set_var(ENV_KEY, "0011223344556677889900112233445566778899");
395
396 let result = manager.get_or_create_key("test");
397 assert!(result.is_err());
398 }
399
400 #[cfg(unix)]
401 #[test]
402 #[serial]
403 fn test_key_file_permissions() {
404 use std::os::unix::fs::PermissionsExt;
405
406 let _guard = EnvGuard::new();
407 let (manager, _temp) = setup_manager();
408
409 manager.get_or_create_key("secure-deployment").unwrap();
410
411 let key_path = manager.key_file_path("secure-deployment");
412 let metadata = fs::metadata(&key_path).unwrap();
413 let permissions = metadata.permissions();
414
415 assert_eq!(permissions.mode() & 0o777, 0o600);
417 }
418}