1#![allow(dead_code, unused_imports, unused_qualifications, unreachable_patterns)]
6
7use super::error::{Error, Result};
8use fs2::FileExt;
9use serde::{Deserialize, Serialize};
10use std::collections::BTreeSet;
11use std::io::Write;
12use std::path::{Path, PathBuf};
13
14pub fn meta_warning_default() -> String {
15 "HMAC-verified — do not modify this file directly. Use CLI tools (e.g. sshenc identity)."
16 .to_string()
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct KeyMeta {
22 #[serde(default = "meta_warning_default", rename = "_warning")]
24 pub warning: String,
25 pub label: String,
27 #[serde(default)]
30 pub key_type: crate::internal::core::KeyType,
31 #[serde(default)]
33 pub access_policy: crate::internal::core::AccessPolicy,
34 #[serde(default)]
36 pub created: String,
37 #[serde(default)]
40 pub app_specific: serde_json::Value,
41}
42
43impl KeyMeta {
44 pub fn new(
46 label: &str,
47 key_type: crate::internal::core::KeyType,
48 access_policy: crate::internal::core::AccessPolicy,
49 ) -> Self {
50 let created = std::time::SystemTime::now()
51 .duration_since(std::time::UNIX_EPOCH)
52 .unwrap_or_default()
53 .as_secs()
54 .to_string();
55 KeyMeta {
56 warning: meta_warning_default(),
57 label: label.to_string(),
58 key_type,
59 access_policy,
60 created,
61 app_specific: serde_json::Value::Null,
62 }
63 }
64
65 pub fn set_app_field(&mut self, key: &str, value: impl Into<serde_json::Value>) {
67 if self.app_specific.is_null() {
68 self.app_specific = serde_json::Value::Object(serde_json::Map::new());
69 }
70 if let Some(obj) = self.app_specific.as_object_mut() {
71 obj.insert(key.to_string(), value.into());
72 }
73 }
74
75 pub fn get_app_field(&self, key: &str) -> Option<&str> {
77 self.app_specific.get(key)?.as_str()
78 }
79}
80
81pub fn keys_dir(app_name: &str) -> PathBuf {
85 config_dir(app_name).join("keys")
86}
87
88pub fn config_dir(app_name: &str) -> PathBuf {
92 dirs::config_dir()
93 .unwrap_or_else(|| {
94 dirs::home_dir()
95 .unwrap_or_else(|| PathBuf::from("/tmp"))
96 .join(".config")
97 })
98 .join(app_name)
99}
100
101pub fn atomic_write(path: &Path, data: &[u8]) -> Result<()> {
103 atomic_write_with_sync(path, data, sync_parent_dir)
104}
105
106pub fn read_no_follow(path: &Path) -> Result<Vec<u8>> {
118 #[cfg(unix)]
119 {
120 use std::io::Read;
121 use std::os::unix::fs::OpenOptionsExt;
122 let mut file = std::fs::OpenOptions::new()
123 .read(true)
124 .custom_flags(libc::O_NOFOLLOW)
125 .open(path)?;
126 let mut buf = Vec::new();
127 file.read_to_end(&mut buf)?;
128 Ok(buf)
129 }
130 #[cfg(not(unix))]
131 {
132 let meta = std::fs::symlink_metadata(path)?;
133 if meta.file_type().is_symlink() {
134 return Err(Error::Io(std::io::Error::new(
135 std::io::ErrorKind::InvalidInput,
136 format!("refusing to read symlink at {}", path.display()),
137 )));
138 }
139 std::fs::read(path).map_err(Error::Io)
140 }
141}
142
143pub fn read_to_string_no_follow(path: &Path) -> Result<String> {
145 let bytes = read_no_follow(path)?;
146 String::from_utf8(bytes).map_err(|e| {
147 Error::Io(std::io::Error::new(
148 std::io::ErrorKind::InvalidData,
149 format!("{} is not valid UTF-8: {e}", path.display()),
150 ))
151 })
152}
153
154fn atomic_write_with_sync<F>(path: &Path, data: &[u8], sync_parent: F) -> Result<()>
155where
156 F: Fn(&Path) -> Result<()>,
157{
158 let parent = path.parent().ok_or_else(|| {
159 Error::Io(std::io::Error::new(
160 std::io::ErrorKind::InvalidInput,
161 "atomic_write path has no parent directory",
162 ))
163 })?;
164 let tmp = unique_temp_path(parent, path);
165 let mut file = std::fs::OpenOptions::new()
166 .create_new(true)
167 .write(true)
168 .open(&tmp)?;
169 file.write_all(data)?;
170 file.sync_all()?;
171 drop(file);
172 if let Err(e) = std::fs::rename(&tmp, path) {
173 std::fs::remove_file(&tmp).ok();
174 return Err(e.into());
175 }
176 sync_parent(parent)?;
177 Ok(())
178}
179
180#[cfg(unix)]
181fn sync_parent_dir(path: &Path) -> Result<()> {
182 let dir = std::fs::File::open(path)?;
183 dir.sync_all()?;
184 Ok(())
185}
186
187#[cfg(not(unix))]
188fn sync_parent_dir(_path: &Path) -> Result<()> {
189 Ok(())
190}
191
192fn unique_temp_path(parent: &Path, path: &Path) -> PathBuf {
193 let file_name = path
194 .file_name()
195 .and_then(|name| name.to_str())
196 .unwrap_or("tmp");
197 let pid = std::process::id();
198 let nanos = std::time::SystemTime::now()
199 .duration_since(std::time::UNIX_EPOCH)
200 .unwrap_or_default()
201 .as_nanos();
202 parent.join(format!(".{file_name}.{pid}.{nanos}.tmp"))
203}
204
205#[derive(Debug)]
208pub struct DirLock {
209 _file: std::fs::File,
210}
211
212impl DirLock {
213 pub fn acquire(dir: &Path) -> Result<Self> {
215 let lock_path = dir.join(".lock");
216 let file = std::fs::OpenOptions::new()
217 .create(true)
218 .read(true)
219 .write(true)
220 .truncate(false)
221 .open(&lock_path)?;
222 file.lock_exclusive().map_err(Error::Io)?;
223 Ok(DirLock { _file: file })
224 }
225}
226
227pub fn ensure_dir(dir: &Path) -> Result<()> {
229 std::fs::create_dir_all(dir)?;
230 #[cfg(unix)]
231 {
232 use std::os::unix::fs::PermissionsExt;
233 std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o700))?;
234 }
235 Ok(())
236}
237
238#[cfg_attr(not(unix), allow(unused_variables))]
240pub fn restrict_file_permissions(path: &Path) -> Result<()> {
241 #[cfg(unix)]
242 {
243 use std::os::unix::fs::PermissionsExt;
244 std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))?;
245 }
246 Ok(())
247}
248
249pub fn save_meta(dir: &Path, label: &str, meta: &KeyMeta) -> Result<()> {
260 crate::internal::core::types::validate_label(label)?;
261 let meta_path = dir.join(format!("{label}.meta"));
262 let json =
263 serde_json::to_string_pretty(meta).map_err(|e| Error::Serialization(e.to_string()))?;
264 atomic_write(&meta_path, json.as_bytes())
265}
266
267pub fn save_meta_with_hmac(dir: &Path, label: &str, meta: &KeyMeta, hmac_key: &[u8]) -> Result<()> {
279 crate::internal::core::types::validate_label(label)?;
280 let meta_path = dir.join(format!("{label}.meta"));
281 let json =
282 serde_json::to_string_pretty(meta).map_err(|e| Error::Serialization(e.to_string()))?;
283 atomic_write(&meta_path, json.as_bytes())?;
284
285 let tag = compute_meta_hmac(hmac_key, json.as_bytes());
286 let hmac_path = dir.join(format!("{label}.meta.hmac"));
287 atomic_write(&hmac_path, tag.as_bytes())?;
288 Ok(())
289}
290
291#[derive(Debug, Clone, Copy, PartialEq, Eq)]
302pub enum MetaIntegrityMode {
303 RequireSidecar,
308 AllowLegacyMissingSidecar,
314}
315
316pub const META_HMAC_MISSING_OP: &str = "meta_hmac_missing";
319
320pub const META_HMAC_VERIFY_OP: &str = "meta_hmac_verify";
323
324pub fn load_meta(dir: &Path, label: &str) -> Result<KeyMeta> {
326 crate::internal::core::types::validate_label(label)?;
327 let meta_path = dir.join(format!("{label}.meta"));
328 if !meta_path.exists() {
329 return Ok(KeyMeta {
330 warning: meta_warning_default(),
331 label: label.to_string(),
332 key_type: crate::internal::core::KeyType::Signing,
333 access_policy: crate::internal::core::AccessPolicy::None,
334 created: String::new(),
335 app_specific: serde_json::Value::Null,
336 });
337 }
338 let content = read_to_string_no_follow(&meta_path)?;
339 serde_json::from_str(&content).map_err(|e| Error::Serialization(e.to_string()))
340}
341
342pub fn load_meta_with_hmac(
359 dir: &Path,
360 label: &str,
361 hmac_key: &[u8],
362 mode: MetaIntegrityMode,
363) -> Result<KeyMeta> {
364 crate::internal::core::types::validate_label(label)?;
365 let meta_path = dir.join(format!("{label}.meta"));
366 if !meta_path.exists() {
367 return load_meta(dir, label);
368 }
369 let content = read_to_string_no_follow(&meta_path)?;
370
371 let hmac_path = dir.join(format!("{label}.meta.hmac"));
372 if hmac_path.exists() {
373 let expected_hex = read_to_string_no_follow(&hmac_path)?;
374 let actual_hex = compute_meta_hmac(hmac_key, content.as_bytes());
375 if !constant_time_eq(expected_hex.trim().as_bytes(), actual_hex.as_bytes()) {
376 return Err(Error::KeyOperation {
377 operation: META_HMAC_VERIFY_OP.into(),
378 detail: format!(
379 "`.meta.hmac` does not match the stored `.meta` JSON for label {label}: \
380 metadata was tampered with after save"
381 ),
382 });
383 }
384 } else if mode == MetaIntegrityMode::RequireSidecar {
385 return Err(Error::KeyOperation {
386 operation: META_HMAC_MISSING_OP.into(),
387 detail: format!(
388 "`.meta` is present without a `.meta.hmac` sidecar for label {label}: \
389 either the sidecar was deleted (tamper) or this is a legacy meta \
390 that needs `migrate_meta_to_hmac`"
391 ),
392 });
393 }
394
395 serde_json::from_str(&content).map_err(|e| Error::Serialization(e.to_string()))
396}
397
398pub fn migrate_meta_to_hmac(dir: &Path, label: &str, hmac_key: &[u8]) -> Result<PathBuf> {
408 crate::internal::core::types::validate_label(label)?;
409 let meta_path = dir.join(format!("{label}.meta"));
410 if !meta_path.exists() {
411 return Err(Error::KeyNotFound {
412 label: label.to_string(),
413 });
414 }
415 let content = read_to_string_no_follow(&meta_path)?;
416 let tag = compute_meta_hmac(hmac_key, content.as_bytes());
417 let hmac_path = dir.join(format!("{label}.meta.hmac"));
418 atomic_write(&hmac_path, tag.as_bytes())?;
419 Ok(hmac_path)
420}
421
422fn compute_meta_hmac(key: &[u8], data: &[u8]) -> String {
427 let bytes = compute_meta_hmac_bytes(key, data);
428 let mut out = String::with_capacity(64);
429 for byte in bytes {
430 out.push_str(&format!("{byte:02x}"));
431 }
432 out
433}
434
435pub fn compute_meta_hmac_bytes(key: &[u8], data: &[u8]) -> [u8; 32] {
443 use sha2::{Digest, Sha256};
444
445 const BLOCK_SIZE: usize = 64; let mut k = [0_u8; BLOCK_SIZE];
449 if key.len() > BLOCK_SIZE {
450 let hashed = Sha256::digest(key);
451 k[..hashed.len()].copy_from_slice(&hashed);
452 } else {
453 k[..key.len()].copy_from_slice(key);
454 }
455
456 let mut ipad = [0x36_u8; BLOCK_SIZE];
457 let mut opad = [0x5c_u8; BLOCK_SIZE];
458 for i in 0..BLOCK_SIZE {
459 ipad[i] ^= k[i];
460 opad[i] ^= k[i];
461 }
462
463 let mut inner = Sha256::new();
464 inner.update(ipad);
465 inner.update(data);
466 let inner_digest = inner.finalize();
467
468 let mut outer = Sha256::new();
469 outer.update(opad);
470 outer.update(inner_digest);
471 let outer_digest = outer.finalize();
472
473 let mut out = [0_u8; 32];
474 out.copy_from_slice(&outer_digest);
475 out
476}
477
478fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
480 if a.len() != b.len() {
481 return false;
482 }
483 let mut diff: u8 = 0;
484 for (x, y) in a.iter().zip(b.iter()) {
485 diff |= x ^ y;
486 }
487 diff == 0
488}
489
490pub fn save_pub_key(dir: &Path, label: &str, pub_key: &[u8]) -> Result<()> {
492 crate::internal::core::types::validate_label(label)?;
493 let path = dir.join(format!("{label}.pub"));
494 atomic_write(&path, pub_key)
495}
496
497pub fn load_pub_key(dir: &Path, label: &str) -> Result<Vec<u8>> {
499 crate::internal::core::types::validate_label(label)?;
500 let path = dir.join(format!("{label}.pub"));
501 if !path.exists() {
502 return Err(Error::KeyNotFound {
503 label: label.to_string(),
504 });
505 }
506 read_no_follow(&path)
507}
508
509pub fn sync_pub_key(dir: &Path, label: &str, pub_key: &[u8]) -> Result<Vec<u8>> {
511 crate::internal::core::types::validate_label(label)?;
512 crate::internal::core::types::validate_p256_point(pub_key)?;
513
514 match load_pub_key(dir, label) {
515 Ok(existing) if existing == pub_key => Ok(existing),
516 _ => {
517 save_pub_key(dir, label, pub_key)?;
518 Ok(pub_key.to_vec())
519 }
520 }
521}
522
523pub fn list_labels(dir: &Path) -> Result<Vec<String>> {
525 list_labels_for_extensions(dir, &["meta"])
526}
527
528pub fn list_labels_for_extensions(dir: &Path, extensions: &[&str]) -> Result<Vec<String>> {
530 if !dir.exists() {
531 return Ok(Vec::new());
532 }
533 let mut labels = BTreeSet::new();
534 for entry in std::fs::read_dir(dir)? {
535 let entry = entry?;
536 let path = entry.path();
537 if let Some(extension) = path.extension().and_then(|e| e.to_str()) {
538 if !extensions.contains(&extension) {
539 continue;
540 }
541 if let Some(stem) = path.file_stem() {
542 let label = stem.to_string_lossy().to_string();
543 if crate::internal::core::types::validate_label(&label).is_ok() {
544 labels.insert(label);
545 }
546 }
547 }
548 }
549 Ok(labels.into_iter().collect())
550}
551
552pub fn delete_key_files(dir: &Path, label: &str) -> Result<()> {
554 crate::internal::core::types::validate_label(label)?;
555 let extensions = ["meta", "meta.hmac", "pub", "handle", "ssh.pub"];
556 let mut found_any = false;
557 for ext in &extensions {
558 let path = dir.join(format!("{label}.{ext}"));
559 if path.exists() {
560 std::fs::remove_file(&path)?;
561 found_any = true;
562 }
563 }
564 if !found_any {
565 return Err(Error::KeyNotFound {
566 label: label.to_string(),
567 });
568 }
569 Ok(())
570}
571
572pub fn key_files_exist(dir: &Path, label: &str) -> Result<bool> {
574 crate::internal::core::types::validate_label(label)?;
575 Ok(["meta", "pub", "handle", "ssh.pub"]
576 .into_iter()
577 .any(|ext| dir.join(format!("{label}.{ext}")).exists()))
578}
579
580pub fn rename_key_files(
589 dir: &Path,
590 old_label: &str,
591 new_label: &str,
592 hmac_key: Option<&[u8]>,
593) -> Result<()> {
594 rename_key_files_with_writer(dir, old_label, new_label, hmac_key, atomic_write)
595}
596
597fn rename_key_files_with_writer<F>(
598 dir: &Path,
599 old_label: &str,
600 new_label: &str,
601 hmac_key: Option<&[u8]>,
602 metadata_writer: F,
603) -> Result<()>
604where
605 F: Fn(&Path, &[u8]) -> Result<()>,
606{
607 crate::internal::core::types::validate_label(old_label)?;
608 crate::internal::core::types::validate_label(new_label)?;
609 let old_handle = dir.join(format!("{old_label}.handle"));
610 let old_meta = dir.join(format!("{old_label}.meta"));
611 if !old_handle.exists() && !old_meta.exists() {
612 return Err(Error::KeyNotFound {
613 label: old_label.to_string(),
614 });
615 }
616 if key_files_exist(dir, new_label)? {
617 return Err(Error::DuplicateLabel {
618 label: new_label.to_string(),
619 });
620 }
621 let old_hmac = dir.join(format!("{old_label}.meta.hmac"));
622 if old_hmac.exists() && hmac_key.is_none() {
623 return Err(Error::KeyOperation {
624 operation: "rename_key_files".into(),
625 detail: format!(
626 "`{old_label}.meta.hmac` sidecar exists but no hmac_key was supplied; \
627 rename would leave the sidecar stale or orphaned"
628 ),
629 });
630 }
631 let extensions = ["meta", "pub", "handle", "ssh.pub"];
635 let mut renamed = Vec::new();
636 for ext in &extensions {
637 let old = dir.join(format!("{old_label}.{ext}"));
638 let new = dir.join(format!("{new_label}.{ext}"));
639 if old.exists() {
640 if let Err(err) = std::fs::rename(&old, &new) {
641 rollback_renames(&renamed)?;
642 return Err(err.into());
643 }
644 renamed.push((old, new));
645 }
646 }
647 let new_meta_path = dir.join(format!("{new_label}.meta"));
649 let mut new_meta_json: Option<String> = None;
650 if new_meta_path.exists() {
651 let content = read_to_string_no_follow(&new_meta_path)?;
652 let mut meta: KeyMeta =
653 serde_json::from_str(&content).map_err(|e| Error::Serialization(e.to_string()))?;
654 meta.label = new_label.to_string();
655 let json =
656 serde_json::to_string_pretty(&meta).map_err(|e| Error::Serialization(e.to_string()))?;
657 if let Err(err) = metadata_writer(&new_meta_path, json.as_bytes()) {
658 rollback_renames(&renamed)?;
659 return Err(err);
660 }
661 new_meta_json = Some(json);
662 }
663 if old_hmac.exists() {
667 drop(std::fs::remove_file(&old_hmac));
668 }
669 if let (Some(json), Some(key)) = (new_meta_json.as_ref(), hmac_key) {
670 let new_hmac = dir.join(format!("{new_label}.meta.hmac"));
671 let tag = compute_meta_hmac(key, json.as_bytes());
672 if let Err(err) = metadata_writer(&new_hmac, tag.as_bytes()) {
673 rollback_renames(&renamed)?;
674 return Err(err);
675 }
676 }
677 Ok(())
678}
679
680fn rollback_renames(renamed: &[(PathBuf, PathBuf)]) -> Result<()> {
681 for (old, new) in renamed.iter().rev() {
682 if new.exists() {
683 std::fs::rename(new, old)?;
684 }
685 }
686 Ok(())
687}
688
689#[cfg(test)]
690#[allow(
691 clippy::unwrap_used,
692 clippy::panic,
693 clippy::used_underscore_binding,
694 let_underscore_drop
695)]
696mod tests {
697 use super::*;
698 use crate::internal::core::{AccessPolicy, KeyType};
699 use std::sync::atomic::{AtomicU64, Ordering};
700
701 static TEST_COUNTER: AtomicU64 = AtomicU64::new(0);
702
703 fn test_dir() -> PathBuf {
704 let id = TEST_COUNTER.fetch_add(1, Ordering::SeqCst);
705 let pid = std::process::id();
706 let dir = std::env::temp_dir().join(format!("enclaveapp-core-test-{pid}-{id}"));
707 std::fs::create_dir_all(&dir).unwrap();
708 dir
709 }
710
711 #[test]
712 fn meta_hmac_roundtrip_accepts_unchanged_meta() {
713 let dir = test_dir();
714 let hmac_key = b"test-hmac-key-material-32-bytes!";
715 let meta = KeyMeta::new(
716 "roundtrip",
717 KeyType::Encryption,
718 AccessPolicy::BiometricOnly,
719 );
720 save_meta_with_hmac(&dir, "roundtrip", &meta, hmac_key).unwrap();
721 let loaded = load_meta_with_hmac(
722 &dir,
723 "roundtrip",
724 hmac_key,
725 MetaIntegrityMode::RequireSidecar,
726 )
727 .unwrap();
728 assert_eq!(loaded.access_policy, AccessPolicy::BiometricOnly);
729 assert_eq!(loaded.label, "roundtrip");
730 std::fs::remove_dir_all(&dir).unwrap();
731 }
732
733 #[test]
734 fn meta_hmac_rejects_tampered_meta() {
735 let dir = test_dir();
736 let hmac_key = b"test-hmac-key-material-32-bytes!";
737 let meta = KeyMeta::new("tamper", KeyType::Encryption, AccessPolicy::BiometricOnly);
738 save_meta_with_hmac(&dir, "tamper", &meta, hmac_key).unwrap();
739
740 let meta_path = dir.join("tamper.meta");
742 let raw = std::fs::read_to_string(&meta_path).unwrap();
743 let tampered = raw.replace("biometric_only", "none");
744 std::fs::write(&meta_path, tampered).unwrap();
745
746 let err = load_meta_with_hmac(&dir, "tamper", hmac_key, MetaIntegrityMode::RequireSidecar)
747 .unwrap_err();
748 assert!(
749 err.to_string().contains(META_HMAC_VERIFY_OP),
750 "expected HMAC-verify failure, got: {err}"
751 );
752 std::fs::remove_dir_all(&dir).unwrap();
753 }
754
755 #[test]
756 fn meta_hmac_rejects_wrong_key() {
757 let dir = test_dir();
758 let hmac_key = b"test-hmac-key-material-32-bytes!";
759 let meta = KeyMeta::new("wrongkey", KeyType::Encryption, AccessPolicy::None);
760 save_meta_with_hmac(&dir, "wrongkey", &meta, hmac_key).unwrap();
761
762 let bad_key = b"different-hmac-key-material-32by";
763 let err = load_meta_with_hmac(&dir, "wrongkey", bad_key, MetaIntegrityMode::RequireSidecar)
764 .unwrap_err();
765 assert!(err.to_string().contains(META_HMAC_VERIFY_OP));
766 std::fs::remove_dir_all(&dir).unwrap();
767 }
768
769 #[test]
770 fn meta_hmac_legacy_mode_accepts_missing_sidecar() {
771 let dir = test_dir();
774 let hmac_key = b"test-hmac-key-material-32-bytes!";
775 let meta = KeyMeta::new("legacy", KeyType::Signing, AccessPolicy::None);
776 save_meta(&dir, "legacy", &meta).unwrap(); let loaded = load_meta_with_hmac(
778 &dir,
779 "legacy",
780 hmac_key,
781 MetaIntegrityMode::AllowLegacyMissingSidecar,
782 )
783 .unwrap();
784 assert_eq!(loaded.label, "legacy");
785 std::fs::remove_dir_all(&dir).unwrap();
786 }
787
788 #[test]
789 fn meta_hmac_strict_rejects_missing_sidecar() {
790 let dir = test_dir();
794 let hmac_key = b"test-hmac-key-material-32-bytes!";
795 let meta = KeyMeta::new("legacy", KeyType::Signing, AccessPolicy::None);
796 save_meta(&dir, "legacy", &meta).unwrap();
797 let err = load_meta_with_hmac(&dir, "legacy", hmac_key, MetaIntegrityMode::RequireSidecar)
798 .unwrap_err();
799 assert!(
800 err.to_string().contains(META_HMAC_MISSING_OP),
801 "expected meta_hmac_missing, got: {err}"
802 );
803 std::fs::remove_dir_all(&dir).unwrap();
804 }
805
806 #[test]
807 fn migrate_meta_to_hmac_writes_sidecar_for_legacy_meta() {
808 let dir = test_dir();
809 let hmac_key = b"test-hmac-key-material-32-bytes!";
810 let meta = KeyMeta::new("legacy", KeyType::Signing, AccessPolicy::None);
811 save_meta(&dir, "legacy", &meta).unwrap();
812 assert!(!dir.join("legacy.meta.hmac").exists());
813
814 migrate_meta_to_hmac(&dir, "legacy", hmac_key).unwrap();
815 assert!(dir.join("legacy.meta.hmac").exists());
816
817 let loaded =
819 load_meta_with_hmac(&dir, "legacy", hmac_key, MetaIntegrityMode::RequireSidecar)
820 .unwrap();
821 assert_eq!(loaded.label, "legacy");
822 std::fs::remove_dir_all(&dir).unwrap();
823 }
824
825 #[test]
826 fn migrate_meta_to_hmac_errors_for_missing_meta() {
827 let dir = test_dir();
828 let hmac_key = b"test-hmac-key-material-32-bytes!";
829 let err = migrate_meta_to_hmac(&dir, "ghost", hmac_key).unwrap_err();
830 assert!(matches!(err, Error::KeyNotFound { .. }));
831 std::fs::remove_dir_all(&dir).unwrap();
832 }
833
834 #[test]
835 fn compute_meta_hmac_is_stable() {
836 let key = b"k";
840 let data = b"message";
841 let a = compute_meta_hmac(key, data);
842 let b = compute_meta_hmac(key, data);
843 assert_eq!(a, b);
844 assert_eq!(a.len(), 64); }
846
847 #[test]
848 fn constant_time_eq_rejects_length_mismatch() {
849 assert!(!constant_time_eq(b"abc", b"abcd"));
850 assert!(constant_time_eq(b"abc", b"abc"));
851 assert!(!constant_time_eq(b"abc", b"abd"));
852 }
853
854 #[test]
855 fn constant_time_eq_empty_slices_are_equal() {
856 assert!(constant_time_eq(b"", b""));
857 }
858
859 #[test]
860 fn compute_meta_hmac_bytes_output_is_32_bytes() {
861 let tag = compute_meta_hmac_bytes(b"k", b"d");
862 assert_eq!(tag.len(), 32);
863 }
864
865 #[test]
866 fn compute_meta_hmac_bytes_is_deterministic() {
867 let key = b"stable-key";
868 let data = b"stable-data";
869 let a = compute_meta_hmac_bytes(key, data);
870 let b = compute_meta_hmac_bytes(key, data);
871 assert_eq!(a, b);
872 }
873
874 #[test]
875 fn compute_meta_hmac_bytes_long_key_exercises_hash_path() {
876 let long_key = vec![0x5a_u8; 128];
878 let short_key = &long_key[..8];
879 let data = b"test-data";
880 let long_tag = compute_meta_hmac_bytes(&long_key, data);
881 let short_tag = compute_meta_hmac_bytes(short_key, data);
882 assert_ne!(long_tag, short_tag);
883 assert_eq!(long_tag.len(), 32);
884 }
885
886 #[test]
887 fn compute_meta_hmac_bytes_different_data_produces_different_tag() {
888 let key = b"same-key";
889 let tag_a = compute_meta_hmac_bytes(key, b"data-a");
890 let tag_b = compute_meta_hmac_bytes(key, b"data-b");
891 assert_ne!(tag_a, tag_b);
892 }
893
894 #[test]
895 fn compute_meta_hmac_bytes_different_key_produces_different_tag() {
896 let data = b"same-data";
897 let tag_a = compute_meta_hmac_bytes(b"key-a", data);
898 let tag_b = compute_meta_hmac_bytes(b"key-b", data);
899 assert_ne!(tag_a, tag_b);
900 }
901
902 #[test]
903 fn key_meta_new_sets_timestamp() {
904 let meta = KeyMeta::new("test", KeyType::Signing, AccessPolicy::None);
905 assert_eq!(meta.label, "test");
906 assert_eq!(meta.key_type, KeyType::Signing);
907 assert!(!meta.created.is_empty());
908 let ts: u64 = meta.created.parse().unwrap();
909 assert!(ts > 0);
910 }
911
912 #[test]
913 fn key_meta_clone_preserves_all_fields() {
914 let mut meta = KeyMeta::new(
915 "clone-test",
916 KeyType::Encryption,
917 AccessPolicy::BiometricOnly,
918 );
919 meta.set_app_field("field", "value");
920 let cloned = meta.clone();
921 assert_eq!(cloned.label, meta.label);
922 assert_eq!(cloned.key_type, meta.key_type);
923 assert_eq!(cloned.access_policy, meta.access_policy);
924 assert_eq!(cloned.get_app_field("field"), Some("value"));
925 }
926
927 #[test]
928 fn key_meta_app_field_roundtrip() {
929 let mut meta = KeyMeta::new("test", KeyType::Signing, AccessPolicy::None);
930 assert!(meta.get_app_field("git_email").is_none());
931 meta.set_app_field("git_email", "jay@example.com");
932 assert_eq!(meta.get_app_field("git_email"), Some("jay@example.com"));
933 }
934
935 #[test]
936 fn key_meta_serde_roundtrip() {
937 let mut meta = KeyMeta::new("test", KeyType::Encryption, AccessPolicy::BiometricOnly);
938 meta.set_app_field("profile", "default");
939 let json = serde_json::to_string_pretty(&meta).unwrap();
940 let parsed: KeyMeta = serde_json::from_str(&json).unwrap();
941 assert_eq!(parsed.label, "test");
942 assert_eq!(parsed.key_type, KeyType::Encryption);
943 assert_eq!(parsed.access_policy, AccessPolicy::BiometricOnly);
944 assert_eq!(parsed.get_app_field("profile"), Some("default"));
945 }
946
947 #[test]
948 #[cfg_attr(miri, ignore)] fn atomic_write_creates_file() {
950 let dir = test_dir();
951 let path = dir.join("test.txt");
952 atomic_write(&path, b"hello world").unwrap();
953 assert_eq!(std::fs::read_to_string(&path).unwrap(), "hello world");
954 std::fs::remove_dir_all(&dir).unwrap();
955 }
956
957 #[test]
958 #[cfg_attr(miri, ignore)] fn atomic_write_ignores_preexisting_legacy_tmp_file() {
960 let dir = test_dir();
961 let path = dir.join("test.txt");
962 let legacy_tmp = path.with_extension("tmp");
963 std::fs::write(&legacy_tmp, b"legacy").unwrap();
964
965 atomic_write(&path, b"fresh").unwrap();
966
967 assert_eq!(std::fs::read_to_string(&path).unwrap(), "fresh");
968 assert_eq!(std::fs::read_to_string(&legacy_tmp).unwrap(), "legacy");
969 std::fs::remove_dir_all(&dir).unwrap();
970 }
971
972 #[test]
973 #[cfg_attr(miri, ignore)] fn atomic_write_syncs_parent_directory_after_rename() {
975 use std::sync::atomic::{AtomicBool, Ordering};
976
977 let dir = test_dir();
978 let path = dir.join("test.txt");
979 let synced = AtomicBool::new(false);
980
981 atomic_write_with_sync(&path, b"hello world", |parent| {
982 assert_eq!(parent, dir.as_path());
983 synced.store(true, Ordering::SeqCst);
984 Ok(())
985 })
986 .unwrap();
987
988 assert!(synced.load(Ordering::SeqCst));
989 assert_eq!(std::fs::read_to_string(&path).unwrap(), "hello world");
990 std::fs::remove_dir_all(&dir).unwrap();
991 }
992
993 #[test]
994 #[cfg_attr(miri, ignore)] fn read_no_follow_reads_file_content() {
996 let dir = test_dir();
997 let path = dir.join("data.bin");
998 std::fs::write(&path, b"hello bytes").unwrap();
999 let result = read_no_follow(&path).unwrap();
1000 assert_eq!(result, b"hello bytes");
1001 std::fs::remove_dir_all(&dir).unwrap();
1002 }
1003
1004 #[test]
1005 #[cfg_attr(miri, ignore)] fn read_no_follow_returns_error_for_missing_file() {
1007 let dir = test_dir();
1008 let path = dir.join("nonexistent.bin");
1009 let result = read_no_follow(&path);
1010 assert!(result.is_err());
1011 std::fs::remove_dir_all(&dir).unwrap();
1012 }
1013
1014 #[test]
1015 #[cfg_attr(miri, ignore)] fn save_load_meta_roundtrip() {
1017 let dir = test_dir();
1018 let meta = KeyMeta::new("mykey", KeyType::Signing, AccessPolicy::Any);
1019 save_meta(&dir, "mykey", &meta).unwrap();
1020 let loaded = load_meta(&dir, "mykey").unwrap();
1021 assert_eq!(loaded.label, "mykey");
1022 assert_eq!(loaded.key_type, KeyType::Signing);
1023 assert_eq!(loaded.access_policy, AccessPolicy::Any);
1024 std::fs::remove_dir_all(&dir).unwrap();
1025 }
1026
1027 #[test]
1028 #[cfg_attr(miri, ignore)] fn load_meta_returns_default_for_missing() {
1030 let dir = test_dir();
1031 let meta = load_meta(&dir, "nonexistent").unwrap();
1032 assert_eq!(meta.label, "nonexistent");
1033 assert_eq!(meta.key_type, KeyType::Signing);
1034 std::fs::remove_dir_all(&dir).unwrap();
1035 }
1036
1037 #[test]
1038 #[cfg_attr(miri, ignore)] fn save_load_pub_key_roundtrip() {
1040 let dir = test_dir();
1041 let pub_key = vec![0x04; 65];
1042 save_pub_key(&dir, "mykey", &pub_key).unwrap();
1043 let loaded = load_pub_key(&dir, "mykey").unwrap();
1044 assert_eq!(loaded, pub_key);
1045 std::fs::remove_dir_all(&dir).unwrap();
1046 }
1047
1048 #[test]
1049 #[cfg_attr(miri, ignore)] fn load_pub_key_returns_key_not_found() {
1051 let dir = test_dir();
1052 let err = load_pub_key(&dir, "missing").unwrap_err();
1053 match err {
1054 Error::KeyNotFound { label } => assert_eq!(label, "missing"),
1055 other => panic!("expected KeyNotFound, got: {other}"),
1056 }
1057 std::fs::remove_dir_all(&dir).unwrap();
1058 }
1059
1060 #[test]
1061 #[cfg_attr(miri, ignore)] fn sync_pub_key_writes_missing_cache() {
1063 let dir = test_dir();
1064 let pub_key = vec![0x04; 65];
1065
1066 let synced = sync_pub_key(&dir, "sync", &pub_key).unwrap();
1067 assert_eq!(synced, pub_key);
1068 assert_eq!(load_pub_key(&dir, "sync").unwrap(), pub_key);
1069
1070 std::fs::remove_dir_all(&dir).unwrap();
1071 }
1072
1073 #[test]
1074 #[cfg_attr(miri, ignore)] fn sync_pub_key_repairs_mismatched_cache() {
1076 let dir = test_dir();
1077 let mut authoritative = vec![0x04];
1078 authoritative.extend_from_slice(&[0x11; 64]);
1079
1080 save_pub_key(&dir, "sync", &[0x04; 65]).unwrap();
1081
1082 let synced = sync_pub_key(&dir, "sync", &authoritative).unwrap();
1083 assert_eq!(synced, authoritative);
1084 assert_eq!(load_pub_key(&dir, "sync").unwrap(), authoritative);
1085
1086 std::fs::remove_dir_all(&dir).unwrap();
1087 }
1088
1089 #[test]
1090 #[cfg_attr(miri, ignore)] fn metadata_label_operations_reject_invalid_labels() {
1092 let dir = test_dir();
1093 let meta = KeyMeta::new("valid", KeyType::Signing, AccessPolicy::None);
1094
1095 let err = save_meta(&dir, "../escape", &meta).unwrap_err();
1096 assert!(matches!(err, Error::InvalidLabel { .. }));
1097
1098 let err = load_meta(&dir, "../escape").unwrap_err();
1099 assert!(matches!(err, Error::InvalidLabel { .. }));
1100
1101 let err = save_pub_key(&dir, "../escape", b"pubkey").unwrap_err();
1102 assert!(matches!(err, Error::InvalidLabel { .. }));
1103
1104 let err = load_pub_key(&dir, "../escape").unwrap_err();
1105 assert!(matches!(err, Error::InvalidLabel { .. }));
1106
1107 let err = delete_key_files(&dir, "../escape").unwrap_err();
1108 assert!(matches!(err, Error::InvalidLabel { .. }));
1109
1110 let err = rename_key_files(&dir, "valid", "../escape", None).unwrap_err();
1111 assert!(matches!(err, Error::InvalidLabel { .. }));
1112
1113 std::fs::remove_dir_all(&dir).unwrap();
1114 }
1115
1116 #[test]
1117 #[cfg_attr(miri, ignore)] fn list_labels_empty_for_nonexistent_dir() {
1119 let dir = std::env::temp_dir().join("enclaveapp-core-test-nonexistent-dir");
1120 let _ = std::fs::remove_dir_all(&dir);
1121 let labels = list_labels(&dir).unwrap();
1122 assert!(labels.is_empty());
1123 }
1124
1125 #[test]
1126 #[cfg_attr(miri, ignore)] fn list_labels_finds_meta_files() {
1128 let dir = test_dir();
1129 let meta_a = KeyMeta::new("alpha", KeyType::Signing, AccessPolicy::None);
1130 let meta_b = KeyMeta::new("beta", KeyType::Encryption, AccessPolicy::Any);
1131 save_meta(&dir, "alpha", &meta_a).unwrap();
1132 save_meta(&dir, "beta", &meta_b).unwrap();
1133 std::fs::write(dir.join("alpha.pub"), b"pubkey").unwrap();
1135 let labels = list_labels(&dir).unwrap();
1136 assert_eq!(labels, vec!["alpha", "beta"]);
1137 std::fs::remove_dir_all(&dir).unwrap();
1138 }
1139
1140 #[test]
1141 #[cfg_attr(miri, ignore)] fn list_labels_for_extensions_includes_unique_sorted_stems() {
1143 let dir = test_dir();
1144 std::fs::write(dir.join("alpha.handle"), b"handle").unwrap();
1145 std::fs::write(dir.join("beta.meta"), b"{}").unwrap();
1146 std::fs::write(dir.join("beta.handle"), b"handle").unwrap();
1147 std::fs::write(dir.join("gamma.pub"), b"pub").unwrap();
1148
1149 let labels = list_labels_for_extensions(&dir, &["meta", "handle"]).unwrap();
1150 assert_eq!(labels, vec!["alpha", "beta"]);
1151
1152 std::fs::remove_dir_all(&dir).unwrap();
1153 }
1154
1155 #[test]
1156 #[cfg_attr(miri, ignore)] fn list_labels_for_extensions_skips_invalid_labels() {
1158 let dir = test_dir();
1159 std::fs::write(dir.join("valid.handle"), b"handle").unwrap();
1160 std::fs::write(dir.join("bad label.handle"), b"handle").unwrap();
1161 std::fs::write(dir.join("also.bad.handle"), b"handle").unwrap();
1162
1163 let labels = list_labels_for_extensions(&dir, &["handle"]).unwrap();
1164 assert_eq!(labels, vec!["valid"]);
1165
1166 std::fs::remove_dir_all(&dir).unwrap();
1167 }
1168
1169 #[test]
1170 #[cfg_attr(miri, ignore)] fn delete_key_files_removes_all() {
1172 let dir = test_dir();
1173 std::fs::write(dir.join("mykey.meta"), b"{}").unwrap();
1174 std::fs::write(dir.join("mykey.pub"), b"pub").unwrap();
1175 std::fs::write(dir.join("mykey.handle"), b"handle").unwrap();
1176 delete_key_files(&dir, "mykey").unwrap();
1177 assert!(!dir.join("mykey.meta").exists());
1178 assert!(!dir.join("mykey.pub").exists());
1179 assert!(!dir.join("mykey.handle").exists());
1180 std::fs::remove_dir_all(&dir).unwrap();
1181 }
1182
1183 #[test]
1184 #[cfg_attr(miri, ignore)] fn delete_key_files_returns_key_not_found() {
1186 let dir = test_dir();
1187 let err = delete_key_files(&dir, "ghost").unwrap_err();
1188 match err {
1189 Error::KeyNotFound { label } => assert_eq!(label, "ghost"),
1190 other => panic!("expected KeyNotFound, got: {other}"),
1191 }
1192 std::fs::remove_dir_all(&dir).unwrap();
1193 }
1194
1195 #[test]
1196 #[cfg_attr(miri, ignore)] fn rename_key_files_renames_and_updates_meta() {
1198 let dir = test_dir();
1199 let meta = KeyMeta::new("old-name", KeyType::Signing, AccessPolicy::None);
1200 save_meta(&dir, "old-name", &meta).unwrap();
1201 save_pub_key(&dir, "old-name", b"pubkey").unwrap();
1202
1203 rename_key_files(&dir, "old-name", "new-name", None).unwrap();
1204
1205 assert!(!dir.join("old-name.meta").exists());
1206 assert!(!dir.join("old-name.pub").exists());
1207 assert!(dir.join("new-name.meta").exists());
1208 assert!(dir.join("new-name.pub").exists());
1209
1210 let loaded = load_meta(&dir, "new-name").unwrap();
1211 assert_eq!(loaded.label, "new-name");
1212 std::fs::remove_dir_all(&dir).unwrap();
1213 }
1214
1215 #[test]
1216 #[cfg_attr(miri, ignore)] fn rename_key_files_rejects_existing_target() {
1218 let dir = test_dir();
1219 let meta = KeyMeta::new("src", KeyType::Signing, AccessPolicy::None);
1220 save_meta(&dir, "src", &meta).unwrap();
1221 let meta2 = KeyMeta::new("dst", KeyType::Signing, AccessPolicy::None);
1222 save_meta(&dir, "dst", &meta2).unwrap();
1223
1224 let err = rename_key_files(&dir, "src", "dst", None).unwrap_err();
1225 match err {
1226 Error::DuplicateLabel { label } => assert_eq!(label, "dst"),
1227 other => panic!("expected DuplicateLabel, got: {other}"),
1228 }
1229 std::fs::remove_dir_all(&dir).unwrap();
1230 }
1231
1232 #[test]
1233 #[cfg_attr(miri, ignore)] fn rename_key_files_rejects_existing_target_pub_without_meta() {
1235 let dir = test_dir();
1236 let meta = KeyMeta::new("src", KeyType::Signing, AccessPolicy::None);
1237 save_meta(&dir, "src", &meta).unwrap();
1238 save_pub_key(&dir, "dst", b"existing").unwrap();
1239
1240 let err = rename_key_files(&dir, "src", "dst", None).unwrap_err();
1241 match err {
1242 Error::DuplicateLabel { label } => assert_eq!(label, "dst"),
1243 other => panic!("expected DuplicateLabel, got: {other}"),
1244 }
1245 assert!(dir.join("src.meta").exists());
1246 assert!(dir.join("dst.pub").exists());
1247 std::fs::remove_dir_all(&dir).unwrap();
1248 }
1249
1250 #[test]
1251 #[cfg_attr(miri, ignore)] fn rename_key_files_rolls_back_when_metadata_update_fails() {
1253 let dir = test_dir();
1254 let meta = KeyMeta::new("old-name", KeyType::Signing, AccessPolicy::None);
1255 save_meta(&dir, "old-name", &meta).unwrap();
1256 save_pub_key(&dir, "old-name", b"pubkey").unwrap();
1257
1258 let err = rename_key_files_with_writer(&dir, "old-name", "new-name", None, |_, _| {
1259 Err(Error::Serialization("forced failure".into()))
1260 })
1261 .unwrap_err();
1262 assert!(matches!(err, Error::Serialization(_)));
1263 assert!(dir.join("old-name.meta").exists());
1264 assert!(dir.join("old-name.pub").exists());
1265 assert!(!dir.join("new-name.meta").exists());
1266 assert!(!dir.join("new-name.pub").exists());
1267 let loaded = load_meta(&dir, "old-name").unwrap();
1268 assert_eq!(loaded.label, "old-name");
1269 std::fs::remove_dir_all(&dir).unwrap();
1270 }
1271
1272 #[test]
1273 #[cfg_attr(miri, ignore)] fn rename_key_files_with_sidecar_recomputes_hmac_under_new_label() {
1275 let dir = test_dir();
1276 let hmac_key = b"test-hmac-key-material-32-bytes!";
1277 let meta = KeyMeta::new("old-name", KeyType::Signing, AccessPolicy::None);
1278 save_meta_with_hmac(&dir, "old-name", &meta, hmac_key).unwrap();
1279 save_pub_key(&dir, "old-name", b"pubkey").unwrap();
1280
1281 rename_key_files(&dir, "old-name", "new-name", Some(hmac_key)).unwrap();
1282
1283 assert!(!dir.join("old-name.meta").exists());
1284 assert!(!dir.join("old-name.meta.hmac").exists());
1285 assert!(dir.join("new-name.meta").exists());
1286 assert!(dir.join("new-name.meta.hmac").exists());
1287
1288 let loaded = load_meta_with_hmac(
1292 &dir,
1293 "new-name",
1294 hmac_key,
1295 MetaIntegrityMode::RequireSidecar,
1296 )
1297 .unwrap();
1298 assert_eq!(loaded.label, "new-name");
1299 std::fs::remove_dir_all(&dir).unwrap();
1300 }
1301
1302 #[test]
1303 #[cfg_attr(miri, ignore)] fn rename_key_files_with_sidecar_requires_hmac_key() {
1305 let dir = test_dir();
1309 let hmac_key = b"test-hmac-key-material-32-bytes!";
1310 let meta = KeyMeta::new("old-name", KeyType::Signing, AccessPolicy::None);
1311 save_meta_with_hmac(&dir, "old-name", &meta, hmac_key).unwrap();
1312
1313 let err = rename_key_files(&dir, "old-name", "new-name", None).unwrap_err();
1314 match err {
1315 Error::KeyOperation { operation, .. } => assert_eq!(operation, "rename_key_files"),
1316 other => panic!("expected KeyOperation, got: {other}"),
1317 }
1318 assert!(dir.join("old-name.meta").exists());
1320 assert!(dir.join("old-name.meta.hmac").exists());
1321 assert!(!dir.join("new-name.meta").exists());
1322 std::fs::remove_dir_all(&dir).unwrap();
1323 }
1324
1325 #[test]
1326 #[cfg_attr(miri, ignore)] fn rename_key_files_rejects_missing_source() {
1328 let dir = test_dir();
1329 let err = rename_key_files(&dir, "missing", "new", None).unwrap_err();
1330 match err {
1331 Error::KeyNotFound { label } => assert_eq!(label, "missing"),
1332 other => panic!("expected KeyNotFound, got: {other}"),
1333 }
1334 std::fs::remove_dir_all(&dir).unwrap();
1335 }
1336
1337 #[test]
1338 #[cfg_attr(miri, ignore)] fn keys_dir_returns_absolute_path() {
1340 let dir = keys_dir("test-app");
1341 assert!(dir.is_absolute());
1342 assert!(dir.to_string_lossy().contains("test-app"));
1343 assert!(dir.to_string_lossy().contains("keys"));
1344 }
1345
1346 #[test]
1347 #[cfg_attr(miri, ignore)] fn config_dir_returns_absolute_path() {
1349 let dir = config_dir("test-app");
1350 assert!(dir.is_absolute());
1351 assert!(dir.to_string_lossy().contains("test-app"));
1352 }
1353
1354 #[test]
1355 #[cfg_attr(miri, ignore)] fn ensure_dir_creates_nested() {
1357 let dir = test_dir();
1358 let nested = dir.join("a").join("b").join("c");
1359 ensure_dir(&nested).unwrap();
1360 assert!(nested.exists());
1361 assert!(nested.is_dir());
1362 std::fs::remove_dir_all(&dir).unwrap();
1363 }
1364
1365 #[test]
1366 #[cfg_attr(miri, ignore)] fn dir_lock_acquire_and_drop() {
1368 let dir = test_dir();
1369 std::fs::create_dir_all(&dir).unwrap();
1370 let _lock = DirLock::acquire(&dir).unwrap();
1371 assert!(dir.join(".lock").exists());
1372 drop(_lock);
1373 std::fs::remove_dir_all(&dir).unwrap();
1374 }
1375
1376 #[test]
1377 #[cfg_attr(miri, ignore)] fn dir_lock_blocks_until_first_holder_releases() {
1379 use std::sync::mpsc;
1380 use std::thread;
1381 use std::time::{Duration, Instant};
1382
1383 let dir = test_dir();
1384 std::fs::create_dir_all(&dir).unwrap();
1385 let first = DirLock::acquire(&dir).unwrap();
1386 let (tx, rx) = mpsc::channel();
1387 let thread_dir = dir.clone();
1388
1389 let handle = thread::spawn(move || {
1390 tx.send(Instant::now()).unwrap();
1391 let _second = DirLock::acquire(&thread_dir).unwrap();
1392 tx.send(Instant::now()).unwrap();
1393 });
1394
1395 let start = rx.recv().unwrap();
1396 thread::sleep(Duration::from_millis(150));
1397 drop(first);
1398 let acquired = rx.recv().unwrap();
1399 assert!(acquired.duration_since(start) >= Duration::from_millis(100));
1400 handle.join().unwrap();
1401 std::fs::remove_dir_all(&dir).unwrap();
1402 }
1403
1404 #[test]
1405 #[cfg_attr(miri, ignore)] fn restrict_file_permissions_succeeds() {
1407 let dir = test_dir();
1408 let path = dir.join("secret.txt");
1409 std::fs::write(&path, b"secret").unwrap();
1410 restrict_file_permissions(&path).unwrap();
1411 #[cfg(unix)]
1412 {
1413 use std::os::unix::fs::PermissionsExt;
1414 let mode = std::fs::metadata(&path).unwrap().permissions().mode() & 0o777;
1415 assert_eq!(mode, 0o600);
1416 }
1417 std::fs::remove_dir_all(&dir).unwrap();
1418 }
1419}