hardware_enclave/
capabilities.rs1#[cfg(target_os = "macos")]
5use std::collections::HashMap;
6#[cfg(target_os = "macos")]
7use std::sync::{Mutex, OnceLock};
8
9use crate::types::{AccessPolicy, BackendKind};
10
11pub use crate::internal::core::signing::is_binary_signed;
12
13pub fn has_keychain_entitlement(group: &str) -> bool {
20 #[cfg(target_os = "macos")]
21 {
22 static CACHE: OnceLock<Mutex<HashMap<String, bool>>> = OnceLock::new();
23 let cache = CACHE.get_or_init(|| Mutex::new(HashMap::new()));
24 let mut guard = cache.lock().unwrap_or_else(|e| e.into_inner());
25 if let Some(&result) = guard.get(group) {
26 return result;
27 }
28 let result = check_entitlement_macos(group);
29 guard.insert(group.to_string(), result);
30 result
31 }
32 #[cfg(not(target_os = "macos"))]
33 {
34 let _ = group;
35 false
36 }
37}
38
39#[cfg(target_os = "macos")]
40fn check_entitlement_macos(group: &str) -> bool {
41 let exe = match std::env::current_exe() {
42 Ok(e) => e,
43 Err(_) => return false,
44 };
45 let output = std::process::Command::new("/usr/bin/codesign")
51 .args(["-d", "--entitlements", "-", "--xml"])
52 .arg(&exe)
53 .output();
54 match output {
55 Ok(o) => {
56 let stdout = String::from_utf8_lossy(&o.stdout);
57 let stderr = String::from_utf8_lossy(&o.stderr);
58 stdout.contains(group) || stderr.contains(group)
59 }
60 Err(_) => false,
61 }
62}
63
64#[derive(Debug, Clone)]
66pub struct SecurityCapabilities {
67 pub binary_signed: bool,
69 pub backend: BackendKind,
71 pub effective_keychain_group: Option<String>,
73 pub code_signature_binding: bool,
75 pub keychain_user_presence: bool,
77 pub hardware_presence: bool,
79 pub presence_caching: bool,
81 pub effective_app_name: String,
83 pub downgraded_features: Vec<String>,
85 pub recommended_access_policy: AccessPolicy,
87}
88
89pub fn security_capabilities(app_name: &str) -> SecurityCapabilities {
91 let signed = is_binary_signed();
92 let effective_app_name = crate::internal::core::signing::ensure_safe_app_name(app_name);
93 let backend = detect_backend();
94
95 #[cfg(target_os = "macos")]
96 let hardware_presence = crate::internal::apple::touch_id_available();
97 #[cfg(target_os = "windows")]
98 let hardware_presence = true;
99 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
100 let hardware_presence = false;
101
102 let presence_caching = cfg!(target_os = "macos");
103
104 let recommended_access_policy = if signed {
105 AccessPolicy::None
106 } else {
107 AccessPolicy::Any
108 };
109
110 SecurityCapabilities {
111 binary_signed: signed,
112 backend,
113 effective_keychain_group: None,
114 code_signature_binding: false,
115 keychain_user_presence: false,
116 hardware_presence,
117 presence_caching,
118 effective_app_name,
119 downgraded_features: Vec::new(),
120 recommended_access_policy,
121 }
122}
123
124#[allow(clippy::needless_return, unreachable_code)]
125fn detect_backend() -> BackendKind {
126 #[cfg(target_os = "macos")]
127 {
128 return BackendKind::SecureEnclave;
129 }
130 #[cfg(target_os = "windows")]
131 {
132 return BackendKind::Tpm;
133 }
134 #[cfg(target_os = "linux")]
135 {
136 if crate::internal::wsl::is_wsl() {
137 return BackendKind::TpmBridge;
138 }
139 return BackendKind::Keyring;
140 }
141 #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
142 BackendKind::Keyring
143}
144
145#[cfg(test)]
146#[allow(clippy::unwrap_used)]
147mod tests {
148 use super::*;
149
150 #[test]
151 fn is_binary_signed_returns_false_in_cargo_test() {
152 assert!(!is_binary_signed());
154 }
155
156 #[test]
157 fn has_keychain_entitlement_returns_false_for_unknown_group() {
158 assert!(!has_keychain_entitlement("com.example.nonexistent.group"));
160 }
161
162 #[test]
163 fn security_capabilities_does_not_panic() {
164 let caps = security_capabilities("testapp");
165 assert!(!caps.effective_app_name.is_empty());
166 assert!(
168 caps.effective_app_name.ends_with("-unsigned"),
169 "unsigned binary should have -unsigned suffix, got: {}",
170 caps.effective_app_name
171 );
172 assert!(!caps.binary_signed);
173 }
174}