1use std::fs;
16use std::path::{Path, PathBuf};
17
18#[cfg(unix)]
19use tracing::warn;
20use tracing::{debug, info};
21
22use crate::sealed::{RecipientPrivateKey, RecipientPublicKey};
23use crate::{EncryptionKey, Result, SecretsError};
24
25const NODE_SECRETS_KEY_FILE: &str = "node_secrets.key";
27
28#[must_use]
31pub fn node_secrets_key_path(base_dir: &Path) -> PathBuf {
32 base_dir.join(NODE_SECRETS_KEY_FILE)
33}
34
35pub fn load_or_generate_node_keypair(
48 base_dir: &Path,
49) -> std::result::Result<(RecipientPrivateKey, RecipientPublicKey), SecretsError> {
50 fs::create_dir_all(base_dir).map_err(|e| {
52 SecretsError::Storage(format!(
53 "Failed to create node key directory {}: {e}",
54 base_dir.display()
55 ))
56 })?;
57
58 let path = node_secrets_key_path(base_dir);
59
60 if path.exists() {
62 debug!("Loading node X25519 keypair from {}", path.display());
63 let buf = fs::read(&path).map_err(|e| {
64 SecretsError::Storage(format!(
65 "Failed to read node key file {}: {e}",
66 path.display()
67 ))
68 })?;
69
70 if buf.len() != 32 {
71 return Err(SecretsError::Storage(format!(
72 "node_secrets.key has wrong length: expected 32, got {}",
73 buf.len()
74 )));
75 }
76
77 let mut bytes = [0u8; 32];
78 bytes.copy_from_slice(&buf);
79 let private = RecipientPrivateKey::from_bytes(bytes);
80 let public = private.public_key();
81 return Ok((private, public));
82 }
83
84 info!("Generating new node X25519 keypair at {}", path.display());
86 let (private, public) = RecipientPrivateKey::generate();
87
88 write_node_key_file(&path, &private)?;
89
90 Ok((private, public))
91}
92
93fn write_node_key_file(
96 path: &Path,
97 private: &RecipientPrivateKey,
98) -> std::result::Result<(), SecretsError> {
99 use base64::engine::general_purpose::STANDARD as B64;
104 use base64::Engine as _;
105 let raw = B64
106 .decode(private.to_base64())
107 .map_err(|e| SecretsError::Storage(format!("Failed to encode node private key: {e}")))?;
108 debug_assert_eq!(raw.len(), 32);
109
110 #[cfg(unix)]
113 {
114 use std::fs::OpenOptions;
115 use std::io::Write;
116 use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
117
118 let mut file = OpenOptions::new()
119 .write(true)
120 .create(true)
121 .truncate(true)
122 .mode(0o600)
123 .open(path)
124 .map_err(|e| {
125 SecretsError::Storage(format!(
126 "Failed to create node key file {}: {e}",
127 path.display()
128 ))
129 })?;
130
131 file.write_all(&raw).map_err(|e| {
132 SecretsError::Storage(format!(
133 "Failed to write node key file {}: {e}",
134 path.display()
135 ))
136 })?;
137
138 let permissions = fs::Permissions::from_mode(0o600);
141 if let Err(e) = fs::set_permissions(path, permissions) {
142 warn!(
143 "Failed to set permissions on node key file {}: {e}",
144 path.display()
145 );
146 return Err(SecretsError::Storage(format!(
147 "Failed to set permissions on node key file {}: {e}",
148 path.display()
149 )));
150 }
151 }
152
153 #[cfg(not(unix))]
154 {
155 fs::write(path, &raw).map_err(|e| {
156 SecretsError::Storage(format!(
157 "Failed to write node key file {}: {e}",
158 path.display()
159 ))
160 })?;
161 }
162
163 Ok(())
164}
165
166const ENV_KEY: &str = "ZLAYER_SECRETS_KEY";
168
169const ENV_PASSWORD: &str = "ZLAYER_SECRETS_PASSWORD";
171
172#[derive(Debug, Clone)]
186pub struct KeyManager {
187 base_dir: PathBuf,
188}
189
190impl Default for KeyManager {
191 fn default() -> Self {
192 Self::new()
193 }
194}
195
196impl KeyManager {
197 #[must_use]
201 pub fn new() -> Self {
202 Self {
203 base_dir: zlayer_paths::ZLayerDirs::system_default().secrets(),
204 }
205 }
206
207 #[must_use]
212 pub fn with_base_dir(base_dir: impl AsRef<Path>) -> Self {
213 Self {
214 base_dir: base_dir.as_ref().to_path_buf(),
215 }
216 }
217
218 fn key_file_path(&self, deployment: &str) -> PathBuf {
220 self.base_dir.join(format!("secrets_{deployment}.key"))
221 }
222
223 pub fn get_or_create_key(&self, deployment: &str) -> Result<EncryptionKey> {
265 if let Ok(hex_key) = std::env::var(ENV_KEY) {
267 debug!("Using encryption key from {ENV_KEY} environment variable");
268 return Self::key_from_hex(&hex_key);
269 }
270
271 if let Ok(password) = std::env::var(ENV_PASSWORD) {
273 debug!("Deriving encryption key from {ENV_PASSWORD} environment variable");
274 return Self::key_from_password(&password, deployment);
275 }
276
277 let key_path = self.key_file_path(deployment);
279 if key_path.exists() {
280 debug!("Loading encryption key from file: {}", key_path.display());
281 return Self::load_key_from_file(&key_path);
282 }
283
284 info!(
286 "Generating new encryption key for deployment '{}' at {}",
287 deployment,
288 key_path.display()
289 );
290 Self::generate_and_save_key(&key_path)
291 }
292
293 fn key_from_hex(hex_key: &str) -> Result<EncryptionKey> {
295 let key_bytes = hex::decode(hex_key.trim()).map_err(|e| {
296 SecretsError::Encryption(format!("Invalid hex-encoded key in {ENV_KEY}: {e}"))
297 })?;
298
299 EncryptionKey::from_bytes(&key_bytes)
300 }
301
302 fn key_from_password(password: &str, deployment: &str) -> Result<EncryptionKey> {
304 EncryptionKey::derive_from_password(password, deployment.as_bytes())
307 }
308
309 fn load_key_from_file(path: &Path) -> Result<EncryptionKey> {
311 let key_bytes = fs::read(path).map_err(|e| {
312 SecretsError::Encryption(format!("Failed to read key file {}: {e}", path.display()))
313 })?;
314
315 EncryptionKey::from_bytes(&key_bytes)
316 }
317
318 fn generate_and_save_key(path: &Path) -> Result<EncryptionKey> {
322 if let Some(parent) = path.parent() {
324 fs::create_dir_all(parent).map_err(|e| {
325 SecretsError::Encryption(format!(
326 "Failed to create key directory {}: {e}",
327 parent.display()
328 ))
329 })?;
330 }
331
332 let key = EncryptionKey::generate();
334
335 fs::write(path, key.as_bytes()).map_err(|e| {
337 SecretsError::Encryption(format!("Failed to write key file {}: {e}", path.display()))
338 })?;
339
340 #[cfg(unix)]
342 {
343 use std::os::unix::fs::PermissionsExt;
344 let permissions = fs::Permissions::from_mode(0o600);
345 if let Err(e) = fs::set_permissions(path, permissions) {
346 warn!(
347 "Failed to set permissions on key file {}: {e}",
348 path.display()
349 );
350 }
351 }
352
353 info!("Created new encryption key at {}", path.display());
354 Ok(key)
355 }
356}
357
358#[cfg(test)]
359mod tests {
360 use super::*;
361 use serial_test::serial;
362 use std::env;
363 use tempfile::TempDir;
364
365 struct EnvGuard;
367
368 impl EnvGuard {
369 fn new() -> Self {
370 env::remove_var(ENV_KEY);
372 env::remove_var(ENV_PASSWORD);
373 Self
374 }
375 }
376
377 impl Drop for EnvGuard {
378 fn drop(&mut self) {
379 env::remove_var(ENV_KEY);
380 env::remove_var(ENV_PASSWORD);
381 }
382 }
383
384 fn setup_manager() -> (KeyManager, TempDir) {
385 let temp_dir = TempDir::new().unwrap();
386 let manager = KeyManager::with_base_dir(temp_dir.path());
387 (manager, temp_dir)
388 }
389
390 #[test]
391 fn test_new_uses_default_dir() {
392 let manager = KeyManager::new();
393 let expected = zlayer_paths::ZLayerDirs::system_default().secrets();
394 assert_eq!(manager.base_dir, expected);
395 }
396
397 #[test]
398 fn test_with_base_dir() {
399 let manager = KeyManager::with_base_dir("/custom/path");
400 assert_eq!(manager.base_dir, PathBuf::from("/custom/path"));
401 }
402
403 #[test]
404 fn test_key_file_path() {
405 let dirs = zlayer_paths::ZLayerDirs::system_default();
406 let manager = KeyManager::with_base_dir(dirs.secrets());
407 let path = manager.key_file_path("production");
408 assert_eq!(path, dirs.secrets().join("secrets_production.key"));
409 }
410
411 #[test]
414 #[serial]
415 fn test_auto_generate_key() {
416 let _guard = EnvGuard::new();
417 let (manager, _temp) = setup_manager();
418
419 let key = manager.get_or_create_key("test-deployment").unwrap();
420 assert_eq!(key.as_bytes().len(), 32);
421
422 let key_path = manager.key_file_path("test-deployment");
424 assert!(key_path.exists());
425 }
426
427 #[test]
428 #[serial]
429 fn test_load_existing_key() {
430 let _guard = EnvGuard::new();
431 let (manager, _temp) = setup_manager();
432
433 let key1 = manager.get_or_create_key("test-deployment").unwrap();
435
436 let key2 = manager.get_or_create_key("test-deployment").unwrap();
438
439 assert_eq!(key1.as_bytes(), key2.as_bytes());
440 }
441
442 #[test]
443 #[serial]
444 fn test_different_deployments_get_different_keys() {
445 let _guard = EnvGuard::new();
446 let (manager, _temp) = setup_manager();
447
448 let key1 = manager.get_or_create_key("deployment-a").unwrap();
449 let key2 = manager.get_or_create_key("deployment-b").unwrap();
450
451 assert_ne!(key1.as_bytes(), key2.as_bytes());
452 }
453
454 #[test]
455 #[serial]
456 fn test_env_key_takes_priority() {
457 let _guard = EnvGuard::new();
458 let (manager, _temp) = setup_manager();
459
460 let known_key = [42u8; 32];
462 let hex_key = hex::encode(known_key);
463
464 env::set_var(ENV_KEY, &hex_key);
466
467 let key = manager.get_or_create_key("any-deployment").unwrap();
468 assert_eq!(key.as_bytes(), &known_key);
469 }
470
471 #[test]
472 #[serial]
473 fn test_env_password_takes_priority_over_file() {
474 let _guard = EnvGuard::new();
475 let (manager, _temp) = setup_manager();
476
477 let file_key = manager.get_or_create_key("test-deployment").unwrap();
479
480 env::set_var(ENV_PASSWORD, "my-secret-password");
482
483 let password_key = manager.get_or_create_key("test-deployment").unwrap();
485 assert_ne!(file_key.as_bytes(), password_key.as_bytes());
486 }
487
488 #[test]
489 #[serial]
490 fn test_password_derivation_is_deterministic() {
491 let _guard = EnvGuard::new();
492 let (manager, _temp) = setup_manager();
493
494 env::set_var(ENV_PASSWORD, "test-password");
495
496 let key1 = manager.get_or_create_key("deployment").unwrap();
497 let key2 = manager.get_or_create_key("deployment").unwrap();
498
499 assert_eq!(key1.as_bytes(), key2.as_bytes());
500 }
501
502 #[test]
503 #[serial]
504 fn test_password_with_different_deployments() {
505 let _guard = EnvGuard::new();
506 let (manager, _temp) = setup_manager();
507
508 env::set_var(ENV_PASSWORD, "same-password");
509
510 let key1 = manager.get_or_create_key("deployment-a").unwrap();
512 let key2 = manager.get_or_create_key("deployment-b").unwrap();
513
514 assert_ne!(key1.as_bytes(), key2.as_bytes());
515 }
516
517 #[test]
518 #[serial]
519 fn test_invalid_hex_key_error() {
520 let _guard = EnvGuard::new();
521 let (manager, _temp) = setup_manager();
522
523 env::set_var(ENV_KEY, "not-valid-hex!!");
524
525 let result = manager.get_or_create_key("test");
526 assert!(result.is_err());
527 }
528
529 #[test]
530 #[serial]
531 fn test_hex_key_wrong_length_error() {
532 let _guard = EnvGuard::new();
533 let (manager, _temp) = setup_manager();
534
535 env::set_var(ENV_KEY, "0011223344556677889900112233445566778899");
537
538 let result = manager.get_or_create_key("test");
539 assert!(result.is_err());
540 }
541
542 #[cfg(unix)]
543 #[test]
544 #[serial]
545 fn test_key_file_permissions() {
546 use std::os::unix::fs::PermissionsExt;
547
548 let _guard = EnvGuard::new();
549 let (manager, _temp) = setup_manager();
550
551 manager.get_or_create_key("secure-deployment").unwrap();
552
553 let key_path = manager.key_file_path("secure-deployment");
554 let metadata = fs::metadata(&key_path).unwrap();
555 let permissions = metadata.permissions();
556
557 assert_eq!(permissions.mode() & 0o777, 0o600);
559 }
560
561 #[test]
566 fn node_keypair_round_trip_generate_then_load() {
567 let temp = TempDir::new().unwrap();
568
569 let (_priv1, pub1) = load_or_generate_node_keypair(temp.path()).unwrap();
570 let (_priv2, pub2) = load_or_generate_node_keypair(temp.path()).unwrap();
571
572 assert_eq!(pub1, pub2);
574
575 assert!(node_secrets_key_path(temp.path()).exists());
577 }
578
579 #[cfg(unix)]
580 #[test]
581 fn node_keypair_perms_0600_on_unix() {
582 use std::os::unix::fs::PermissionsExt;
583
584 let temp = TempDir::new().unwrap();
585 let _ = load_or_generate_node_keypair(temp.path()).unwrap();
586
587 let path = node_secrets_key_path(temp.path());
588 let mode = fs::metadata(&path).unwrap().permissions().mode();
589 assert_eq!(mode & 0o777, 0o600, "expected mode 0600, got {mode:o}");
590 }
591
592 #[test]
593 fn node_keypair_rejects_wrong_length() {
594 let temp = TempDir::new().unwrap();
595 let path = node_secrets_key_path(temp.path());
596
597 fs::create_dir_all(temp.path()).unwrap();
599 fs::write(&path, [0u8; 16]).unwrap();
600
601 let result = load_or_generate_node_keypair(temp.path());
605 match result {
606 Ok(_) => panic!("expected SecretsError::Storage, got Ok(_)"),
607 Err(SecretsError::Storage(msg)) => {
608 assert!(
609 msg.contains("length") || msg.contains("expected 32"),
610 "expected length error message, got: {msg}"
611 );
612 }
613 Err(other) => panic!("expected SecretsError::Storage, got {other:?}"),
614 }
615 }
616
617 #[test]
618 fn node_keypair_pubkey_matches_private() {
619 let temp = TempDir::new().unwrap();
620 let (private, public) = load_or_generate_node_keypair(temp.path()).unwrap();
621
622 let derived = private.public_key();
625 assert_eq!(derived, public);
626 }
627}