Skip to main content

hardware_enclave/
factory.rs

1// Copyright 2026 Jay Gowdy
2// SPDX-License-Identifier: MIT
3
4use crate::internal::app_storage::BackendKind;
5
6use crate::auth::AuthHandle;
7#[cfg(target_os = "macos")]
8use crate::capabilities::has_keychain_entitlement;
9use crate::security_key::SecurityKeyHandle;
10
11use crate::config::EnclaveConfig;
12use crate::encryption::EncryptorHandle;
13use crate::error::{Error, Result};
14use crate::integrity::TamperEvidentHandle;
15use crate::signing::SignerHandle;
16
17/// Create a signing handle for the current platform.
18///
19/// Validates the config against the binary's signing state:
20/// - `wrapping_key_user_presence: true` + no access group + unsigned -> `Error::RequiresSigning`
21/// - `keychain_access_group` set but entitlement absent -> downgrade (no error)
22pub fn create_signer(config: &EnclaveConfig) -> Result<SignerHandle> {
23    let storage_config = validate_and_resolve_config(config)?;
24    let backend = crate::internal::app_storage::AppSigningBackend::init(storage_config)
25        .map_err(Error::from)?;
26    let kind = backend.backend_kind();
27    Ok(SignerHandle::new(backend, kind))
28}
29
30/// Create an encryption handle for the current platform.
31pub fn create_encryptor(config: &EnclaveConfig) -> Result<EncryptorHandle> {
32    let storage_config = validate_and_resolve_config(config)?;
33    let kind = resolve_backend_kind();
34    let storage = crate::internal::app_storage::AppEncryptionStorage::init(storage_config)
35        .map_err(Error::from)?;
36    Ok(EncryptorHandle::new(storage, kind))
37}
38
39/// Create an auth handle for the current platform.
40///
41/// The `config` parameter is accepted for API consistency with the other factory
42/// functions but is not currently used — `AuthHandle` only requires platform
43/// detection. It is reserved for Phase 2 when access-group entitlement validation
44/// will be wired in.
45pub fn create_auth(config: &EnclaveConfig) -> Result<AuthHandle> {
46    let _ = config; // reserved for Phase 2 entitlement validation
47    let kind = resolve_backend_kind();
48    Ok(AuthHandle::new(kind))
49}
50
51/// Create a [`SecurityKeyHandle`] for the current platform.
52///
53/// Unlike the other factory functions, this is **infallible** — it always
54/// returns a handle regardless of whether the platform authenticator is
55/// available. Call [`SecurityKeyHandle::is_available()`] to check at runtime
56/// whether Windows Hello is reachable before calling
57/// [`generate`][SecurityKeyHandle::generate] or [`sign`][SecurityKeyHandle::sign].
58///
59/// This design allows the handle to be constructed once at startup and
60/// re-used across multiple operations without repeating the availability check.
61pub fn create_security_key(config: &EnclaveConfig) -> SecurityKeyHandle {
62    crate::security_key::make_security_key_handle(config)
63}
64
65/// Create a tamper-evident handle for the given app.
66///
67/// The per-app HMAC key is loaded from the platform secure store
68/// (Keychain on macOS, DPAPI on Windows, D-Bus Secret Service on Linux).
69/// On first use the key is created, which on macOS may prompt for the
70/// login keychain password if the binary is unsigned.
71///
72/// **For testing and development** where no interactive prompt is acceptable,
73/// use [`create_tamper_evident_ephemeral`] instead, which uses a random
74/// in-memory key and never touches the platform secure store.
75pub fn create_tamper_evident(app_name: &str) -> Result<TamperEvidentHandle> {
76    let effective = crate::internal::core::signing::ensure_safe_app_name(app_name);
77    Ok(TamperEvidentHandle::new(effective))
78}
79
80/// Create a tamper-evident handle with an ephemeral random HMAC key.
81///
82/// The key is generated from `OsRng` and held in memory only — no platform
83/// secure store (Keychain / DPAPI / Secret Service) is accessed. This means:
84///
85/// - **No interactive prompts.** Safe to call from CI, tests, and examples.
86/// - **Key is not persisted.** Files written with this handle cannot be
87///   verified after the process restarts. Use [`create_tamper_evident`] for
88///   persistent integrity protection.
89///
90/// Suitable for: automated tests, CI pipelines, development examples, and
91/// any non-production scenario where prompt-free operation is required.
92pub fn create_tamper_evident_ephemeral(app_name: &str) -> TamperEvidentHandle {
93    let effective = crate::internal::core::signing::ensure_safe_app_name(app_name);
94    TamperEvidentHandle::new_ephemeral(effective)
95}
96
97// ── internal helpers ──────────────────────────────────────────────────
98
99fn validate_and_resolve_config(
100    config: &EnclaveConfig,
101) -> Result<crate::internal::app_storage::StorageConfig> {
102    // `mut` is only used by the macOS cfg block below; suppress the lint
103    // on platforms where the mutation code is compiled out.
104    #[cfg_attr(not(target_os = "macos"), allow(unused_mut))]
105    let mut sc = config.to_storage_config();
106
107    // Hard error: user_presence without access_group on unsigned binary.
108    // The legacy keychain rejects the userPresence ACL with errSecParam.
109    #[cfg(target_os = "macos")]
110    if sc.wrapping_key_user_presence
111        && sc.keychain_access_group.is_none()
112        && !crate::internal::core::signing::is_binary_signed()
113    {
114        return Err(Error::RequiresSigning {
115            feature: "wrapping_key_user_presence (requires keychain_access_group + entitlement)"
116                .into(),
117        });
118    }
119
120    // Log when macOS-specific config options are set on non-macOS platforms.
121    #[cfg(not(target_os = "macos"))]
122    if sc.wrapping_key_user_presence || sc.keychain_access_group.is_some() {
123        tracing::debug!(
124            app = %sc.app_name,
125            wrapping_key_user_presence = sc.wrapping_key_user_presence,
126            keychain_access_group = ?sc.keychain_access_group,
127            "macOS-specific config options set on non-macOS platform; they will be ignored"
128        );
129    }
130
131    // Downgrade: access_group requested but entitlement absent -> use legacy keychain.
132    #[cfg(target_os = "macos")]
133    if let Some(ref group) = sc.keychain_access_group.clone() {
134        if !has_keychain_entitlement(group) {
135            tracing::warn!(
136                app = %sc.app_name,
137                group = %group,
138                "keychain_access_group requested but entitlement is absent; \
139                 downgrading to legacy keychain (no user_presence gate)"
140            );
141            sc.keychain_access_group = None;
142            sc.wrapping_key_user_presence = false;
143        }
144    }
145
146    Ok(sc)
147}
148
149#[allow(clippy::needless_return, unreachable_code)]
150fn resolve_backend_kind() -> BackendKind {
151    #[cfg(target_os = "macos")]
152    {
153        return BackendKind::SecureEnclave;
154    }
155    #[cfg(target_os = "windows")]
156    {
157        return BackendKind::Tpm;
158    }
159    #[cfg(target_os = "linux")]
160    {
161        if crate::internal::wsl::is_wsl() {
162            return BackendKind::TpmBridge;
163        }
164        return BackendKind::Keyring;
165    }
166    #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
167    BackendKind::Keyring
168}
169
170#[cfg(test)]
171#[allow(clippy::unwrap_used)]
172mod tests {
173    use super::*;
174
175    #[test]
176    fn create_auth_does_not_panic() {
177        let config = EnclaveConfig::new("testapp", "default");
178        let _handle = create_auth(&config).unwrap();
179    }
180
181    #[cfg(target_os = "macos")]
182    #[test]
183    fn user_presence_without_access_group_unsigned_returns_requires_signing() {
184        use crate::config::{MacOsConfig, PlatformConfig};
185        use std::time::Duration;
186        let config = EnclaveConfig {
187            app_name: "testapp".into(),
188            default_key_label: "key".into(),
189            access_policy: None,
190            keys_dir: None,
191            platform: PlatformConfig::MacOs(MacOsConfig {
192                wrapping_key_user_presence: true,
193                wrapping_key_cache_ttl: Duration::ZERO,
194                keychain_access_group: None,
195                extra_bridge_paths: Vec::new(),
196            }),
197        };
198        // In test env the binary is unsigned, so this must return RequiresSigning.
199        let result = create_signer(&config);
200        assert!(
201            result.is_err(),
202            "unsigned binary with user_presence must return an error"
203        );
204        assert!(
205            matches!(result, Err(Error::RequiresSigning { .. })),
206            "expected RequiresSigning, got: {:?}",
207            result
208        );
209    }
210}