hardware_enclave/internal/app_storage/
platform.rs1#![allow(dead_code, unused_imports, unused_qualifications, unreachable_patterns)]
6
7#[allow(unused_imports)]
8use std::path::PathBuf;
9use zeroize::Zeroizing;
10
11pub fn meta_hmac_key(app_name: &str) -> Option<Zeroizing<Vec<u8>>> {
31 #[cfg(target_os = "macos")]
32 {
33 match crate::internal::apple::meta_hmac::load_or_create(app_name) {
34 Ok(key) => key,
35 Err(e) => {
36 tracing::warn!(
37 error = %e,
38 "macOS meta-HMAC key load failed; falling back to no-HMAC mode"
39 );
40 None
41 }
42 }
43 }
44 #[cfg(target_os = "windows")]
45 {
46 match crate::internal::windows::meta_hmac::load_or_create(app_name) {
47 Ok(key) => key,
48 Err(e) => {
49 tracing::warn!(
50 error = %e,
51 "Windows DPAPI meta-HMAC key load failed; falling back to no-HMAC mode"
52 );
53 None
54 }
55 }
56 }
57 #[cfg(target_os = "linux")]
58 {
59 crate::internal::keyring::meta_hmac_key(app_name)
60 }
61 #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
62 {
63 let _ = app_name;
64 None
65 }
66}
67
68pub fn verify_meta_integrity(
89 app_name: &str,
90 keys_dir: &std::path::Path,
91 label: &str,
92) -> crate::internal::app_storage::error::Result<()> {
93 let meta_path = keys_dir.join(format!("{label}.meta"));
101 if !meta_path.exists() {
102 return Ok(());
103 }
104 let hmac_key = match meta_hmac_key(app_name) {
105 Some(k) => k,
106 None => {
107 return Ok(());
112 }
113 };
114 match crate::internal::core::metadata::load_meta_with_hmac(
115 keys_dir,
116 label,
117 hmac_key.as_slice(),
118 crate::internal::core::metadata::MetaIntegrityMode::RequireSidecar,
119 ) {
120 Ok(_) => Ok(()),
121 Err(e) => {
122 let msg = e.to_string();
123 if msg.contains(crate::internal::core::metadata::META_HMAC_VERIFY_OP) {
124 return Err(crate::internal::app_storage::error::StorageError::KeyInitFailed(msg));
125 }
126 if msg.contains(crate::internal::core::metadata::META_HMAC_MISSING_OP) {
127 tracing::warn!(
128 label = %label,
129 "`.meta.hmac` sidecar missing — migrating from existing meta. \
130 If you did not just upgrade, treat this as suspicious and \
131 regenerate the key."
132 );
133 if let Err(migrate_err) = crate::internal::core::metadata::migrate_meta_to_hmac(
134 keys_dir,
135 label,
136 hmac_key.as_slice(),
137 ) {
138 return Err(
139 crate::internal::app_storage::error::StorageError::KeyInitFailed(
140 migrate_err.to_string(),
141 ),
142 );
143 }
144 return Ok(());
145 }
146 Ok(())
150 }
151 }
152}
153
154pub fn check_meta_integrity(
181 app_name: &str,
182 label: &str,
183 keys_dir: &std::path::Path,
184) -> crate::internal::app_storage::error::Result<()> {
185 let meta_path = keys_dir.join(format!("{label}.meta"));
191 if !meta_path.exists() {
192 return Ok(());
193 }
194 #[cfg(target_os = "macos")]
195 {
196 let hmac_key = match crate::internal::apple::meta_hmac::load_existing(app_name) {
201 Ok(Some(k)) => k,
202 _ => return Ok(()),
203 };
204 let outcome = crate::internal::apple::meta_tag::verify(
205 app_name,
206 label,
207 keys_dir,
208 hmac_key.as_slice(),
209 )
210 .map_err(|e| {
211 crate::internal::app_storage::error::StorageError::KeyInitFailed(e.to_string())
212 })?;
213 match outcome {
214 crate::internal::apple::meta_tag::VerifyOutcome::Match
215 | crate::internal::apple::meta_tag::VerifyOutcome::NoMeta
216 | crate::internal::apple::meta_tag::VerifyOutcome::KeychainUnavailable => Ok(()),
217 crate::internal::apple::meta_tag::VerifyOutcome::Tamper => Err(
218 crate::internal::app_storage::error::StorageError::KeyInitFailed(format!(
219 "key '{label}': metadata integrity check failed. The on-disk meta does \
220 not match the keychain-stored tag — meta may have been tampered with. \
221 Refusing to proceed. Regenerate the key to restore a known-good state."
222 )),
223 ),
224 crate::internal::apple::meta_tag::VerifyOutcome::Legacy => Err(
225 crate::internal::app_storage::error::StorageError::KeyInitFailed(format!(
226 "key '{label}' has no integrity tag. This is a one-time migration \
227 required by upgrading to a build that introduces meta integrity tags, \
228 and is not something future upgrades will repeat. If you have already \
229 run `{app_name} migrate-meta` on this machine, treat this as a tamper \
230 signal — do not run it again. Regenerate the affected key instead. \
231 Before migrating, verify the key's current policy looks correct: \
232 `{app_name} inspect {label}`. To migrate: `{app_name} migrate-meta`."
233 )),
234 ),
235 }
236 }
237 #[cfg(target_os = "windows")]
238 {
239 let hmac_key = match crate::internal::windows::meta_hmac::load_existing(app_name) {
245 Ok(Some(k)) => k,
246 _ => return Ok(()),
247 };
248 let outcome = crate::internal::windows::meta_tag::verify(
249 app_name,
250 label,
251 keys_dir,
252 hmac_key.as_slice(),
253 )
254 .map_err(|e| {
255 crate::internal::app_storage::error::StorageError::KeyInitFailed(e.to_string())
256 })?;
257 match outcome {
258 crate::internal::windows::meta_tag::VerifyOutcome::Match
259 | crate::internal::windows::meta_tag::VerifyOutcome::NoMeta
260 | crate::internal::windows::meta_tag::VerifyOutcome::KeychainUnavailable => Ok(()),
261 crate::internal::windows::meta_tag::VerifyOutcome::Tamper => Err(
262 crate::internal::app_storage::error::StorageError::KeyInitFailed(format!(
263 "key '{label}': metadata integrity check failed. The on-disk meta does \
264 not match the keychain-stored tag — meta may have been tampered with. \
265 Refusing to proceed. Regenerate the key to restore a known-good state."
266 )),
267 ),
268 crate::internal::windows::meta_tag::VerifyOutcome::Legacy => Err(
269 crate::internal::app_storage::error::StorageError::KeyInitFailed(format!(
270 "key '{label}' has no integrity tag. This is a one-time migration \
271 required by upgrading to a build that introduces meta integrity tags, \
272 and is not something future upgrades will repeat. If you have already \
273 run `{app_name} migrate-meta` on this machine, treat this as a tamper \
274 signal — do not run it again. Regenerate the affected key instead. \
275 Before migrating, verify the key's current policy looks correct: \
276 `{app_name} inspect {label}`. To migrate: `{app_name} migrate-meta`."
277 )),
278 ),
279 }
280 }
281 #[cfg(target_os = "linux")]
282 {
283 let hmac_key = match crate::internal::keyring::meta_hmac_key_existing(app_name) {
289 Ok(Some(k)) => k,
290 _ => return Ok(()),
291 };
292 let outcome = crate::internal::keyring::meta_tag::verify(
293 app_name,
294 label,
295 keys_dir,
296 hmac_key.as_slice(),
297 )
298 .map_err(|e| {
299 crate::internal::app_storage::error::StorageError::KeyInitFailed(e.to_string())
300 })?;
301 match outcome {
302 crate::internal::keyring::meta_tag::VerifyOutcome::Match
303 | crate::internal::keyring::meta_tag::VerifyOutcome::NoMeta
304 | crate::internal::keyring::meta_tag::VerifyOutcome::KeychainUnavailable => Ok(()),
305 crate::internal::keyring::meta_tag::VerifyOutcome::Tamper => Err(
306 crate::internal::app_storage::error::StorageError::KeyInitFailed(format!(
307 "key '{label}': metadata integrity check failed. The on-disk meta does \
308 not match the keychain-stored tag — meta may have been tampered with. \
309 Refusing to proceed. Regenerate the key to restore a known-good state."
310 )),
311 ),
312 crate::internal::keyring::meta_tag::VerifyOutcome::Legacy => Err(
313 crate::internal::app_storage::error::StorageError::KeyInitFailed(format!(
314 "key '{label}' has no integrity tag. This is a one-time migration \
315 required by upgrading to a build that introduces meta integrity tags, \
316 and is not something future upgrades will repeat. If you have already \
317 run `{app_name} migrate-meta` on this machine, treat this as a tamper \
318 signal — do not run it again. Regenerate the affected key instead. \
319 Before migrating, verify the key's current policy looks correct: \
320 `{app_name} inspect {label}`. To migrate: `{app_name} migrate-meta`."
321 )),
322 ),
323 }
324 }
325 #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
326 {
327 let _ = (app_name, label, keys_dir);
328 Ok(())
329 }
330}
331
332pub fn store_file_tag(
341 app_name: &str,
342 path_label: &str,
343 tag: &[u8; 32],
344) -> crate::internal::core::Result<()> {
345 #[cfg(target_os = "macos")]
346 {
347 crate::internal::apple::meta_tag::store(app_name, path_label, tag)
348 }
349 #[cfg(target_os = "windows")]
350 {
351 crate::internal::windows::meta_tag::store(app_name, path_label, tag)
352 }
353 #[cfg(target_os = "linux")]
354 {
355 crate::internal::keyring::meta_tag::store(app_name, path_label, tag)
356 }
357 #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
358 {
359 let _ = (app_name, path_label, tag);
360 Ok(())
361 }
362}
363
364pub fn load_file_tag(
373 app_name: &str,
374 path_label: &str,
375) -> crate::internal::core::Result<Option<[u8; 32]>> {
376 #[cfg(target_os = "macos")]
377 {
378 crate::internal::apple::meta_tag::load(app_name, path_label)
379 }
380 #[cfg(target_os = "windows")]
381 {
382 crate::internal::windows::meta_tag::load(app_name, path_label)
383 }
384 #[cfg(target_os = "linux")]
385 {
386 crate::internal::keyring::meta_tag::load(app_name, path_label)
387 }
388 #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
389 {
390 let _ = (app_name, path_label);
391 Ok(None)
392 }
393}
394
395pub fn delete_file_tag(app_name: &str, path_label: &str) -> crate::internal::core::Result<()> {
398 #[cfg(target_os = "macos")]
399 {
400 crate::internal::apple::meta_tag::delete(app_name, path_label)
401 }
402 #[cfg(target_os = "windows")]
403 {
404 crate::internal::windows::meta_tag::delete(app_name, path_label)
405 }
406 #[cfg(target_os = "linux")]
407 {
408 crate::internal::keyring::meta_tag::delete(app_name, path_label)
409 }
410 #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
411 {
412 let _ = (app_name, path_label);
413 Ok(())
414 }
415}
416
417pub fn delete_meta_hmac_key(app_name: &str) -> crate::internal::core::Result<()> {
426 #[cfg(target_os = "macos")]
427 {
428 crate::internal::apple::meta_hmac::delete(app_name)
429 }
430 #[cfg(target_os = "windows")]
431 {
432 crate::internal::windows::meta_hmac::delete(app_name)
433 }
434 #[cfg(target_os = "linux")]
435 {
436 let _ = app_name;
442 Ok(())
443 }
444 #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
445 {
446 let _ = app_name;
447 Ok(())
448 }
449}
450
451#[derive(Debug, Clone, Copy, PartialEq, Eq)]
453pub enum BackendKind {
454 SecureEnclave,
456 Tpm,
458 WindowsDpapi,
460 TpmBridge,
462 Keyring,
464}
465
466impl std::fmt::Display for BackendKind {
467 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
468 match self {
469 BackendKind::SecureEnclave => write!(f, "Secure Enclave"),
470 BackendKind::Tpm => write!(f, "TPM 2.0"),
471 BackendKind::WindowsDpapi => write!(f, "Windows DPAPI"),
472 BackendKind::TpmBridge => write!(f, "TPM 2.0 (WSL Bridge)"),
473 BackendKind::Keyring => write!(f, "Keyring"),
474 }
475 }
476}
477
478#[cfg(target_os = "linux")]
486pub fn find_bridge_executable(app_name: &str, extra_paths: &[String]) -> Option<PathBuf> {
487 if let Some(path) = crate::internal::bridge::find_bridge(app_name) {
489 return Some(path);
490 }
491
492 let auto_paths = [
494 format!("/mnt/c/Program Files/{app_name}/{app_name}-tpm-bridge.exe"),
495 format!("/mnt/c/ProgramData/{app_name}/{app_name}-tpm-bridge.exe"),
496 ];
497
498 for path_str in &auto_paths {
499 let path = std::path::Path::new(path_str);
500 if path.exists() {
501 return Some(path.to_path_buf());
502 }
503 }
504
505 for path_str in extra_paths {
506 let path = std::path::Path::new(path_str);
507 if path.is_absolute() && path.exists() {
508 return Some(path.to_path_buf());
509 }
510 }
511
512 None
513}
514
515#[cfg(test)]
516#[allow(clippy::unwrap_used)]
517mod tests {
518 use super::*;
519
520 #[test]
521 fn backend_kind_display() {
522 assert_eq!(BackendKind::SecureEnclave.to_string(), "Secure Enclave");
523 assert_eq!(BackendKind::Tpm.to_string(), "TPM 2.0");
524 assert_eq!(BackendKind::WindowsDpapi.to_string(), "Windows DPAPI");
525 assert_eq!(BackendKind::TpmBridge.to_string(), "TPM 2.0 (WSL Bridge)");
526 assert_eq!(BackendKind::Keyring.to_string(), "Keyring");
527 }
528
529 #[test]
530 fn backend_kind_eq() {
531 assert_eq!(BackendKind::SecureEnclave, BackendKind::SecureEnclave);
532 assert_ne!(BackendKind::SecureEnclave, BackendKind::Tpm);
533 }
534
535 #[test]
536 fn backend_kind_clone() {
537 let kind = BackendKind::Tpm;
538 let cloned = kind;
539 assert_eq!(kind, cloned);
540 }
541
542 #[test]
543 fn backend_kind_debug_nonempty() {
544 for kind in [
545 BackendKind::SecureEnclave,
546 BackendKind::Tpm,
547 BackendKind::WindowsDpapi,
548 BackendKind::TpmBridge,
549 BackendKind::Keyring,
550 ] {
551 let s = format!("{kind:?}");
552 assert!(!s.is_empty());
553 }
554 }
555
556 #[test]
557 fn backend_kind_all_variants_display_nonempty() {
558 for kind in [
559 BackendKind::SecureEnclave,
560 BackendKind::Tpm,
561 BackendKind::WindowsDpapi,
562 BackendKind::TpmBridge,
563 BackendKind::Keyring,
564 ] {
565 assert!(!kind.to_string().is_empty());
566 }
567 }
568
569 #[test]
570 fn backend_kind_all_pairs_not_equal() {
571 let kinds = [
572 BackendKind::SecureEnclave,
573 BackendKind::Tpm,
574 BackendKind::WindowsDpapi,
575 BackendKind::TpmBridge,
576 BackendKind::Keyring,
577 ];
578 for i in 0..kinds.len() {
579 for j in 0..kinds.len() {
580 if i != j {
581 assert_ne!(kinds[i], kinds[j]);
582 }
583 }
584 }
585 }
586
587 #[test]
588 fn verify_meta_integrity_succeeds_when_meta_file_absent() {
589 let dir = std::env::temp_dir().join(format!(
590 "enclaveapp-platform-test-{}-{}",
591 std::process::id(),
592 std::time::SystemTime::now()
593 .duration_since(std::time::UNIX_EPOCH)
594 .unwrap_or_default()
595 .as_nanos()
596 ));
597 std::fs::create_dir_all(&dir).unwrap();
598 let result = verify_meta_integrity("test-app", &dir, "nonexistent-label");
600 assert!(result.is_ok());
601 drop(std::fs::remove_dir_all(&dir));
602 }
603
604 #[test]
605 fn check_meta_integrity_succeeds_when_meta_file_absent() {
606 let dir = std::env::temp_dir().join(format!(
607 "enclaveapp-platform-test2-{}-{}",
608 std::process::id(),
609 std::time::SystemTime::now()
610 .duration_since(std::time::UNIX_EPOCH)
611 .unwrap_or_default()
612 .as_nanos()
613 ));
614 std::fs::create_dir_all(&dir).unwrap();
615 let result = check_meta_integrity("test-app", "nonexistent-label", &dir);
616 assert!(result.is_ok());
617 drop(std::fs::remove_dir_all(&dir));
618 }
619
620 #[cfg(target_os = "linux")]
621 #[test]
622 fn find_bridge_executable_returns_none_on_dev_machine() {
623 drop(find_bridge_executable("test-app", &[]));
625 }
626}