storage.rs (26351B)
1 use std::borrow::Cow; 2 use std::fs; 3 use std::path::{Path, PathBuf}; 4 5 use radroots_protected_store::{ 6 RadrootsProtectedFileKeySource, RadrootsProtectedStoreEnvelope, sidecar_path, 7 }; 8 use radroots_secret_vault::RadrootsSecretVaultAccessError; 9 10 use crate::{IdentityError, RadrootsIdentity, RadrootsIdentityFile, RadrootsIdentityPublic}; 11 12 pub const RADROOTS_ENCRYPTED_IDENTITY_DEFAULT_KEY_SLOT: &str = "radroots_identity"; 13 pub const RADROOTS_ENCRYPTED_IDENTITY_KEY_SUFFIX: &str = ".key"; 14 15 #[derive(Debug, Clone)] 16 pub struct RadrootsEncryptedIdentityFile { 17 path: PathBuf, 18 key_slot: Cow<'static, str>, 19 } 20 21 impl RadrootsEncryptedIdentityFile { 22 #[must_use] 23 pub fn new(path: impl AsRef<Path>) -> Self { 24 Self::new_path(path.as_ref()) 25 } 26 27 #[must_use] 28 fn new_path(path: &Path) -> Self { 29 Self::with_key_slot_path(path, RADROOTS_ENCRYPTED_IDENTITY_DEFAULT_KEY_SLOT) 30 } 31 32 #[must_use] 33 pub fn with_key_slot(path: impl AsRef<Path>, key_slot: impl Into<Cow<'static, str>>) -> Self { 34 Self::with_key_slot_path(path.as_ref(), key_slot) 35 } 36 37 #[must_use] 38 fn with_key_slot_path(path: &Path, key_slot: impl Into<Cow<'static, str>>) -> Self { 39 Self { 40 path: path.to_path_buf(), 41 key_slot: key_slot.into(), 42 } 43 } 44 45 #[must_use] 46 pub fn path(&self) -> &Path { 47 self.path.as_path() 48 } 49 50 #[must_use] 51 pub fn key_slot(&self) -> &str { 52 self.key_slot.as_ref() 53 } 54 55 #[must_use] 56 pub fn wrapping_key_path(&self) -> PathBuf { 57 encrypted_identity_wrapping_key_path(&self.path) 58 } 59 60 pub fn store(&self, identity: &RadrootsIdentity) -> Result<(), IdentityError> { 61 if let Some(parent) = self.path.parent() 62 && !parent.as_os_str().is_empty() 63 { 64 fs::create_dir_all(parent) 65 .map_err(|source| IdentityError::CreateDir(parent.to_path_buf(), source))?; 66 } 67 68 let payload = identity_file_payload(identity); 69 let key_source = RadrootsProtectedFileKeySource::from_sidecar_suffix( 70 &self.path, 71 RADROOTS_ENCRYPTED_IDENTITY_KEY_SUFFIX, 72 ); 73 let envelope = RadrootsProtectedStoreEnvelope::seal_with_wrapped_key( 74 &key_source, 75 self.key_slot(), 76 &payload, 77 ) 78 .map_err(|error| { 79 protected_storage_message(&self.path, "seal encrypted identity", &error) 80 })?; 81 let encoded = encode_encrypted_identity(&envelope); 82 fs::write(&self.path, encoded) 83 .map_err(|source| IdentityError::Write(self.path.clone(), source))?; 84 apply_secret_permissions(&self.path)?; 85 Ok(()) 86 } 87 88 pub fn load(&self) -> Result<RadrootsIdentity, IdentityError> { 89 let encoded = fs::read(&self.path).map_err(|source| { 90 if source.kind() == std::io::ErrorKind::NotFound { 91 IdentityError::NotFound(self.path.clone()) 92 } else { 93 IdentityError::Read(self.path.clone(), source) 94 } 95 })?; 96 let key_source = RadrootsProtectedFileKeySource::from_sidecar_suffix( 97 &self.path, 98 RADROOTS_ENCRYPTED_IDENTITY_KEY_SUFFIX, 99 ); 100 let envelope = RadrootsProtectedStoreEnvelope::decode_json(&encoded).map_err(|error| { 101 protected_storage_message(&self.path, "decode encrypted identity", &error) 102 })?; 103 let plaintext = envelope 104 .open_with_wrapped_key(&key_source) 105 .map_err(|error| { 106 protected_storage_message(&self.path, "open encrypted identity", &error) 107 })?; 108 let file: RadrootsIdentityFile = serde_json::from_slice(&plaintext)?; 109 RadrootsIdentity::try_from(file) 110 } 111 112 pub fn rotate(&self) -> Result<(), IdentityError> { 113 let identity = self.load()?; 114 let backup = self.rotation_backup()?; 115 116 if let Err(error) = self.store(&identity) { 117 let _ = fs::write(&self.path, &backup.envelope); 118 let _ = set_secret_permissions(&self.path); 119 let _ = fs::write(&backup.key_path, &backup.key); 120 let _ = set_secret_permissions(&backup.key_path); 121 return Err(error); 122 } 123 124 Ok(()) 125 } 126 127 #[cfg_attr(coverage_nightly, coverage(off))] 128 fn rotation_backup(&self) -> Result<EncryptedIdentityRotationBackup, IdentityError> { 129 let envelope = fs::read(&self.path) 130 .map_err(|source| IdentityError::Read(self.path.clone(), source))?; 131 let key_path = self.wrapping_key_path(); 132 let key = 133 fs::read(&key_path).map_err(|source| IdentityError::Read(key_path.clone(), source))?; 134 135 fs::remove_file(&key_path) 136 .map_err(|source| IdentityError::Write(key_path.clone(), source))?; 137 138 Ok(EncryptedIdentityRotationBackup { 139 envelope, 140 key_path, 141 key, 142 }) 143 } 144 } 145 146 struct EncryptedIdentityRotationBackup { 147 envelope: Vec<u8>, 148 key_path: PathBuf, 149 key: Vec<u8>, 150 } 151 152 #[must_use] 153 pub fn encrypted_identity_wrapping_key_path(path: impl AsRef<Path>) -> PathBuf { 154 encrypted_identity_wrapping_key_path_ref(path.as_ref()) 155 } 156 157 fn encrypted_identity_wrapping_key_path_ref(path: &Path) -> PathBuf { 158 sidecar_path(path, RADROOTS_ENCRYPTED_IDENTITY_KEY_SUFFIX) 159 } 160 161 pub fn store_encrypted_identity( 162 path: impl AsRef<Path>, 163 identity: &RadrootsIdentity, 164 ) -> Result<(), IdentityError> { 165 store_encrypted_identity_path(path.as_ref(), identity) 166 } 167 168 fn store_encrypted_identity_path( 169 path: &Path, 170 identity: &RadrootsIdentity, 171 ) -> Result<(), IdentityError> { 172 RadrootsEncryptedIdentityFile::new_path(path).store(identity) 173 } 174 175 pub fn store_encrypted_identity_with_key_slot( 176 path: impl AsRef<Path>, 177 key_slot: impl Into<Cow<'static, str>>, 178 identity: &RadrootsIdentity, 179 ) -> Result<(), IdentityError> { 180 store_encrypted_identity_with_key_slot_path(path.as_ref(), key_slot, identity) 181 } 182 183 fn store_encrypted_identity_with_key_slot_path( 184 path: &Path, 185 key_slot: impl Into<Cow<'static, str>>, 186 identity: &RadrootsIdentity, 187 ) -> Result<(), IdentityError> { 188 RadrootsEncryptedIdentityFile::with_key_slot_path(path, key_slot).store(identity) 189 } 190 191 pub fn rotate_encrypted_identity(path: impl AsRef<Path>) -> Result<(), IdentityError> { 192 rotate_encrypted_identity_path(path.as_ref()) 193 } 194 195 fn rotate_encrypted_identity_path(path: &Path) -> Result<(), IdentityError> { 196 RadrootsEncryptedIdentityFile::new_path(path).rotate() 197 } 198 199 pub fn rotate_encrypted_identity_with_key_slot( 200 path: impl AsRef<Path>, 201 key_slot: impl Into<Cow<'static, str>>, 202 ) -> Result<(), IdentityError> { 203 rotate_encrypted_identity_with_key_slot_path(path.as_ref(), key_slot) 204 } 205 206 fn rotate_encrypted_identity_with_key_slot_path( 207 path: &Path, 208 key_slot: impl Into<Cow<'static, str>>, 209 ) -> Result<(), IdentityError> { 210 RadrootsEncryptedIdentityFile::with_key_slot_path(path, key_slot).rotate() 211 } 212 213 pub fn load_encrypted_identity(path: impl AsRef<Path>) -> Result<RadrootsIdentity, IdentityError> { 214 load_encrypted_identity_path(path.as_ref()) 215 } 216 217 fn load_encrypted_identity_path(path: &Path) -> Result<RadrootsIdentity, IdentityError> { 218 RadrootsEncryptedIdentityFile::new_path(path).load() 219 } 220 221 pub fn load_encrypted_identity_with_key_slot( 222 path: impl AsRef<Path>, 223 key_slot: impl Into<Cow<'static, str>>, 224 ) -> Result<RadrootsIdentity, IdentityError> { 225 load_encrypted_identity_with_key_slot_path(path.as_ref(), key_slot) 226 } 227 228 fn load_encrypted_identity_with_key_slot_path( 229 path: &Path, 230 key_slot: impl Into<Cow<'static, str>>, 231 ) -> Result<RadrootsIdentity, IdentityError> { 232 RadrootsEncryptedIdentityFile::with_key_slot_path(path, key_slot).load() 233 } 234 235 pub fn store_identity_profile( 236 path: impl AsRef<Path>, 237 identity: &RadrootsIdentity, 238 ) -> Result<(), IdentityError> { 239 store_identity_profile_path(path.as_ref(), identity) 240 } 241 242 fn store_identity_profile_path( 243 path: &Path, 244 identity: &RadrootsIdentity, 245 ) -> Result<(), IdentityError> { 246 if let Some(parent) = path.parent() 247 && !parent.as_os_str().is_empty() 248 { 249 fs::create_dir_all(parent) 250 .map_err(|source| IdentityError::CreateDir(parent.to_path_buf(), source))?; 251 } 252 253 let encoded = identity_profile_payload(identity); 254 fs::write(path, encoded).map_err(|source| IdentityError::Write(path.to_path_buf(), source))?; 255 apply_secret_permissions(path)?; 256 Ok(()) 257 } 258 259 pub fn load_identity_profile( 260 path: impl AsRef<Path>, 261 ) -> Result<RadrootsIdentityPublic, IdentityError> { 262 load_identity_profile_path(path.as_ref()) 263 } 264 265 fn load_identity_profile_path(path: &Path) -> Result<RadrootsIdentityPublic, IdentityError> { 266 let encoded = match fs::read(path) { 267 Ok(encoded) => encoded, 268 Err(source) if source.kind() == std::io::ErrorKind::NotFound => { 269 return Err(IdentityError::NotFound(path.to_path_buf())); 270 } 271 Err(source) => return Err(IdentityError::Read(path.to_path_buf(), source)), 272 }; 273 if let Ok(public_identity) = serde_json::from_slice::<RadrootsIdentityPublic>(&encoded) { 274 return Ok(public_identity); 275 } 276 RadrootsIdentity::load_from_path_auto(path).map(|identity| identity.to_public()) 277 } 278 279 fn identity_file_payload(identity: &RadrootsIdentity) -> Vec<u8> { 280 serde_json::to_vec(&identity.to_file()).expect("identity file serialization is infallible") 281 } 282 283 fn identity_profile_payload(identity: &RadrootsIdentity) -> Vec<u8> { 284 serde_json::to_vec_pretty(&identity.to_public()) 285 .expect("identity profile serialization is infallible") 286 } 287 288 fn encode_encrypted_identity(envelope: &RadrootsProtectedStoreEnvelope) -> Vec<u8> { 289 envelope 290 .encode_json() 291 .expect("protected-store envelope serialization is infallible") 292 } 293 294 #[cfg_attr(coverage_nightly, coverage(off))] 295 fn apply_secret_permissions(path: &Path) -> Result<(), IdentityError> { 296 set_secret_permissions(path).map_err(|error| secret_permission_error(path, error)) 297 } 298 299 fn protected_storage_message( 300 path: &Path, 301 action: &str, 302 message: &dyn core::fmt::Display, 303 ) -> IdentityError { 304 IdentityError::ProtectedStorage { 305 path: path.to_path_buf(), 306 message: format!("failed to {action}: {message}"), 307 } 308 } 309 310 fn secret_permission_error(path: &Path, error: RadrootsSecretVaultAccessError) -> IdentityError { 311 protected_storage_message(path, "update secret-file permissions", &error) 312 } 313 314 #[cfg(unix)] 315 #[cfg_attr(coverage_nightly, coverage(off))] 316 fn set_secret_permissions(path: &Path) -> Result<(), RadrootsSecretVaultAccessError> { 317 use std::os::unix::fs::PermissionsExt; 318 319 let permissions = std::fs::Permissions::from_mode(0o600); 320 fs::set_permissions(path, permissions) 321 .map_err(|source| RadrootsSecretVaultAccessError::Backend(source.to_string())) 322 } 323 324 #[cfg(not(unix))] 325 #[cfg_attr(coverage_nightly, coverage(off))] 326 fn set_secret_permissions(_path: &Path) -> Result<(), RadrootsSecretVaultAccessError> { 327 Ok(()) 328 } 329 330 #[cfg(test)] 331 mod tests { 332 use super::*; 333 334 #[cfg_attr(coverage_nightly, coverage(off))] 335 #[test] 336 fn encrypted_identity_round_trips() { 337 let temp = tempfile::tempdir().expect("tempdir"); 338 let path = temp.path().join("identity.enc.json"); 339 let identity = RadrootsIdentity::from_secret_key_str( 340 "1111111111111111111111111111111111111111111111111111111111111111", 341 ) 342 .expect("identity"); 343 344 store_encrypted_identity(&path, &identity).expect("store encrypted identity"); 345 346 let loaded = load_encrypted_identity(&path).expect("load encrypted identity"); 347 assert_eq!(loaded.id(), identity.id()); 348 assert_eq!(loaded.secret_key_hex(), identity.secret_key_hex()); 349 assert!(encrypted_identity_wrapping_key_path(&path).is_file()); 350 } 351 352 #[cfg_attr(coverage_nightly, coverage(off))] 353 #[test] 354 fn encrypted_identity_rotation_rewraps_key() { 355 let temp = tempfile::tempdir().expect("tempdir"); 356 let path = temp.path().join("identity.enc.json"); 357 let identity = RadrootsIdentity::from_secret_key_str( 358 "1111111111111111111111111111111111111111111111111111111111111111", 359 ) 360 .expect("identity"); 361 362 store_encrypted_identity(&path, &identity).expect("store encrypted identity"); 363 let key_path = encrypted_identity_wrapping_key_path(&path); 364 let before = fs::read(&key_path).expect("key before"); 365 366 rotate_encrypted_identity(&path).expect("rotate encrypted identity"); 367 368 let after = fs::read(&key_path).expect("key after"); 369 assert_ne!(before, after); 370 let loaded = load_encrypted_identity(&path).expect("load rotated identity"); 371 assert_eq!(loaded.secret_key_hex(), identity.secret_key_hex()); 372 } 373 374 #[cfg_attr(coverage_nightly, coverage(off))] 375 #[test] 376 fn encrypted_identity_supports_custom_key_slot() { 377 let temp = tempfile::tempdir().expect("tempdir"); 378 let path = temp.path().join("identity.enc.json"); 379 let identity = RadrootsIdentity::from_secret_key_str( 380 "1111111111111111111111111111111111111111111111111111111111111111", 381 ) 382 .expect("identity"); 383 384 store_encrypted_identity_with_key_slot(&path, "myc_identity", &identity) 385 .expect("store encrypted identity"); 386 let loaded = load_encrypted_identity_with_key_slot(&path, "myc_identity") 387 .expect("load encrypted identity"); 388 assert_eq!(loaded.secret_key_hex(), identity.secret_key_hex()); 389 } 390 391 #[cfg_attr(coverage_nightly, coverage(off))] 392 #[test] 393 fn identity_profile_round_trips() { 394 let temp = tempfile::tempdir().expect("tempdir"); 395 let path = temp.path().join("profile.json"); 396 let mut identity = RadrootsIdentity::from_secret_key_str( 397 "1111111111111111111111111111111111111111111111111111111111111111", 398 ) 399 .expect("identity"); 400 identity.set_profile(crate::RadrootsIdentityProfile::default()); 401 402 store_identity_profile(&path, &identity).expect("store profile"); 403 404 let loaded = load_identity_profile(&path).expect("load profile"); 405 assert_eq!(loaded.id, identity.id()); 406 } 407 408 #[cfg_attr(coverage_nightly, coverage(off))] 409 #[test] 410 fn encrypted_identity_file_accessors_and_wrappers_use_expected_paths() { 411 let temp = tempfile::tempdir().expect("tempdir"); 412 let path = temp.path().join("identity.enc.json"); 413 let identity = RadrootsIdentity::from_secret_key_str( 414 "1111111111111111111111111111111111111111111111111111111111111111", 415 ) 416 .expect("identity"); 417 418 let default_file = RadrootsEncryptedIdentityFile::new(path.as_path()); 419 assert_eq!(default_file.path(), path.as_path()); 420 assert_eq!( 421 default_file.key_slot(), 422 RADROOTS_ENCRYPTED_IDENTITY_DEFAULT_KEY_SLOT 423 ); 424 assert_eq!( 425 default_file.wrapping_key_path(), 426 encrypted_identity_wrapping_key_path(path.as_path()) 427 ); 428 429 let custom_file = 430 RadrootsEncryptedIdentityFile::with_key_slot(path.as_path(), "custom_identity"); 431 assert_eq!(custom_file.key_slot(), "custom_identity"); 432 433 store_encrypted_identity(path.as_path(), &identity).expect("store encrypted identity"); 434 rotate_encrypted_identity(path.as_path()).expect("rotate encrypted identity"); 435 let loaded = load_encrypted_identity(path.as_path()).expect("load encrypted identity"); 436 assert_eq!(loaded.secret_key_hex(), identity.secret_key_hex()); 437 438 store_encrypted_identity_with_key_slot(path.as_path(), "custom_identity", &identity) 439 .expect("store encrypted identity with slot"); 440 rotate_encrypted_identity_with_key_slot(path.as_path(), "custom_identity") 441 .expect("rotate encrypted identity with slot"); 442 let loaded = load_encrypted_identity_with_key_slot(path.as_path(), "custom_identity") 443 .expect("load encrypted identity with slot"); 444 assert_eq!(loaded.secret_key_hex(), identity.secret_key_hex()); 445 } 446 447 #[cfg_attr(coverage_nightly, coverage(off))] 448 #[test] 449 fn encrypted_identity_load_reports_read_decode_and_open_errors() { 450 let temp = tempfile::tempdir().expect("tempdir"); 451 let missing = temp.path().join("missing.enc.json"); 452 let missing_error = load_encrypted_identity(missing.as_path()).expect_err("missing"); 453 assert!(matches!(missing_error, IdentityError::NotFound(path) if path == missing)); 454 455 let read_error = load_encrypted_identity(temp.path()).expect_err("directory read"); 456 assert!(matches!(read_error, IdentityError::Read(path, _) if path == temp.path())); 457 458 let invalid = temp.path().join("invalid.enc.json"); 459 fs::write(&invalid, b"not-json").expect("write invalid envelope"); 460 let decode_error = load_encrypted_identity(invalid.as_path()).expect_err("decode error"); 461 assert!(matches!( 462 decode_error, 463 IdentityError::ProtectedStorage { path, message } 464 if path == invalid && message.contains("decode encrypted identity") 465 )); 466 467 let invalid_plaintext = temp.path().join("invalid-plaintext.enc.json"); 468 let key_source = RadrootsProtectedFileKeySource::from_sidecar_suffix( 469 invalid_plaintext.as_path(), 470 RADROOTS_ENCRYPTED_IDENTITY_KEY_SUFFIX, 471 ); 472 let envelope = RadrootsProtectedStoreEnvelope::seal_with_wrapped_key( 473 &key_source, 474 RADROOTS_ENCRYPTED_IDENTITY_DEFAULT_KEY_SLOT, 475 b"not identity json", 476 ) 477 .expect("seal invalid plaintext"); 478 fs::write( 479 &invalid_plaintext, 480 envelope.encode_json().expect("encode invalid plaintext"), 481 ) 482 .expect("write invalid plaintext envelope"); 483 let invalid_plaintext_error = 484 load_encrypted_identity(invalid_plaintext.as_path()).expect_err("invalid plaintext"); 485 assert!(matches!( 486 invalid_plaintext_error, 487 IdentityError::InvalidJson(_) 488 )); 489 490 let path = temp.path().join("identity.enc.json"); 491 let identity = RadrootsIdentity::from_secret_key_str( 492 "1111111111111111111111111111111111111111111111111111111111111111", 493 ) 494 .expect("identity"); 495 store_encrypted_identity_with_key_slot(path.as_path(), "right_slot", &identity) 496 .expect("store encrypted identity"); 497 fs::write( 498 encrypted_identity_wrapping_key_path(path.as_path()), 499 b"short", 500 ) 501 .expect("corrupt wrapping key"); 502 let open_error = load_encrypted_identity(path.as_path()).expect_err("open"); 503 assert!(matches!( 504 open_error, 505 IdentityError::ProtectedStorage { path: error_path, message } 506 if error_path == path && message.contains("open encrypted identity") 507 )); 508 } 509 510 #[cfg_attr(coverage_nightly, coverage(off))] 511 #[test] 512 fn encrypted_identity_store_reports_create_write_and_seal_errors() { 513 let temp = tempfile::tempdir().expect("tempdir"); 514 let identity = RadrootsIdentity::from_secret_key_str( 515 "1111111111111111111111111111111111111111111111111111111111111111", 516 ) 517 .expect("identity"); 518 519 let blocked_parent = temp.path().join("blocked-parent"); 520 fs::write(&blocked_parent, b"not-a-directory").expect("blocked parent"); 521 let create_path = blocked_parent.join("identity.enc.json"); 522 let create_error = 523 store_encrypted_identity(create_path.as_path(), &identity).expect_err("create dir"); 524 assert!( 525 matches!(create_error, IdentityError::CreateDir(path, _) if path == blocked_parent) 526 ); 527 528 let directory_path = temp.path().join("identity-as-directory.enc.json"); 529 fs::create_dir(&directory_path).expect("identity directory"); 530 let write_error = 531 store_encrypted_identity(directory_path.as_path(), &identity).expect_err("write dir"); 532 assert!(matches!(write_error, IdentityError::Write(path, _) if path == directory_path)); 533 534 let sealed_path = temp.path().join("seal-error.enc.json"); 535 fs::create_dir(encrypted_identity_wrapping_key_path(sealed_path.as_path())) 536 .expect("blocking key directory"); 537 let seal_error = 538 store_encrypted_identity(sealed_path.as_path(), &identity).expect_err("seal"); 539 assert!(matches!( 540 seal_error, 541 IdentityError::ProtectedStorage { path, message } 542 if path == sealed_path && message.contains("seal encrypted identity") 543 )); 544 } 545 546 #[cfg(unix)] 547 #[cfg_attr(coverage_nightly, coverage(off))] 548 #[test] 549 fn encrypted_identity_rotation_restores_wrapping_key_after_store_failure() { 550 use std::os::unix::fs::PermissionsExt; 551 552 let temp = tempfile::tempdir().expect("tempdir"); 553 let path = temp.path().join("identity.enc.json"); 554 let identity = RadrootsIdentity::from_secret_key_str( 555 "1111111111111111111111111111111111111111111111111111111111111111", 556 ) 557 .expect("identity"); 558 559 store_encrypted_identity(path.as_path(), &identity).expect("store encrypted identity"); 560 let key_path = encrypted_identity_wrapping_key_path(path.as_path()); 561 let key_before = fs::read(&key_path).expect("key before"); 562 563 fs::set_permissions(&path, fs::Permissions::from_mode(0o400)).expect("read only"); 564 let error = rotate_encrypted_identity(path.as_path()).expect_err("rotate failure"); 565 fs::set_permissions(&path, fs::Permissions::from_mode(0o600)).expect("writable"); 566 567 assert!(matches!(error, IdentityError::Write(error_path, _) if error_path == path)); 568 assert_eq!(fs::read(&key_path).expect("restored key"), key_before); 569 let loaded = load_encrypted_identity(path.as_path()).expect("load restored identity"); 570 assert_eq!(loaded.secret_key_hex(), identity.secret_key_hex()); 571 } 572 573 #[cfg_attr(coverage_nightly, coverage(off))] 574 #[test] 575 fn identity_profile_storage_reports_errors_and_private_fallback() { 576 let temp = tempfile::tempdir().expect("tempdir"); 577 let identity = RadrootsIdentity::from_secret_key_str( 578 "1111111111111111111111111111111111111111111111111111111111111111", 579 ) 580 .expect("identity"); 581 582 let blocked_parent = temp.path().join("blocked-profile-parent"); 583 fs::write(&blocked_parent, b"not-a-directory").expect("blocked parent"); 584 let create_path = blocked_parent.join("profile.json"); 585 let create_error = 586 store_identity_profile(create_path.as_path(), &identity).expect_err("create dir"); 587 assert!( 588 matches!(create_error, IdentityError::CreateDir(path, _) if path == blocked_parent) 589 ); 590 591 let directory_path = temp.path().join("profile-as-directory.json"); 592 fs::create_dir(&directory_path).expect("profile directory"); 593 let write_error = 594 store_identity_profile(directory_path.as_path(), &identity).expect_err("write dir"); 595 assert!(matches!(write_error, IdentityError::Write(path, _) if path == directory_path)); 596 597 let missing = temp.path().join("missing-profile.json"); 598 let missing_error = load_identity_profile(missing.as_path()).expect_err("missing"); 599 assert!(matches!(missing_error, IdentityError::NotFound(path) if path == missing)); 600 601 let read_error = load_identity_profile(temp.path()).expect_err("directory read"); 602 assert!(matches!(read_error, IdentityError::Read(path, _) if path == temp.path())); 603 604 let private_profile = temp.path().join("private-profile.json"); 605 fs::write( 606 &private_profile, 607 serde_json::to_vec(&identity.to_file()).expect("identity file"), 608 ) 609 .expect("write private profile"); 610 let loaded = load_identity_profile(private_profile.as_path()).expect("load fallback"); 611 assert_eq!(loaded.id, identity.id()); 612 } 613 614 #[cfg_attr(coverage_nightly, coverage(off))] 615 #[test] 616 fn protected_storage_permission_message_uses_operator_action() { 617 let path = Path::new("missing-secret-file"); 618 let error = secret_permission_error( 619 path, 620 RadrootsSecretVaultAccessError::Backend("permission denied".into()), 621 ); 622 623 assert!(matches!( 624 error, 625 IdentityError::ProtectedStorage { path: error_path, message } 626 if error_path == path 627 && message.contains("update secret-file permissions") 628 && message.contains("permission denied") 629 )); 630 } 631 632 #[cfg_attr(coverage_nightly, coverage(off))] 633 #[test] 634 fn storage_supports_parentless_relative_files() { 635 let temp = tempfile::tempdir().expect("tempdir"); 636 let previous = std::env::current_dir().expect("current dir"); 637 std::env::set_current_dir(temp.path()).expect("set temp cwd"); 638 639 let identity = RadrootsIdentity::from_secret_key_str( 640 "1111111111111111111111111111111111111111111111111111111111111111", 641 ) 642 .expect("identity"); 643 let encrypted_path = Path::new("identity.enc.json"); 644 store_encrypted_identity(encrypted_path, &identity).expect("store encrypted"); 645 let loaded = load_encrypted_identity(encrypted_path).expect("load encrypted"); 646 assert_eq!(loaded.secret_key_hex(), identity.secret_key_hex()); 647 648 let profile_path = Path::new("profile.json"); 649 store_identity_profile(profile_path, &identity).expect("store profile"); 650 let loaded = load_identity_profile(profile_path).expect("load profile"); 651 assert_eq!(loaded.id, identity.id()); 652 653 let empty_path = Path::new(""); 654 let encrypted_error = 655 store_encrypted_identity(empty_path, &identity).expect_err("empty encrypted path"); 656 assert!(matches!(encrypted_error, IdentityError::Write(_, _))); 657 let profile_error = 658 store_identity_profile(empty_path, &identity).expect_err("empty profile path"); 659 assert!(matches!(profile_error, IdentityError::Write(_, _))); 660 661 std::env::set_current_dir(previous).expect("restore cwd"); 662 } 663 }