Skip to main content

hardware_enclave/
error.rs

1// Copyright 2026 Jay Gowdy
2// SPDX-License-Identifier: MIT
3
4#[non_exhaustive]
5#[derive(Debug, thiserror::Error)]
6pub enum Error {
7    /// The hardware security module is absent, not enrolled, or unreachable.
8    #[error("hardware security module not available")]
9    NotAvailable,
10    /// No key with the given label exists in this app's key store.
11    #[error("key not found: {label}")]
12    KeyNotFound { label: String },
13    /// A key with this label already exists.
14    #[error("duplicate key label: {label}")]
15    DuplicateLabel { label: String },
16    /// The label is syntactically invalid (empty, too long, or contains illegal characters).
17    #[error("invalid key label: {reason}")]
18    InvalidLabel { reason: String },
19    /// The signing operation failed.
20    #[error("signing failed: {detail}")]
21    SignFailed { detail: String },
22    /// The encryption operation failed.
23    #[error("encryption failed: {detail}")]
24    EncryptFailed { detail: String },
25    /// The decryption operation failed; the ciphertext may be corrupt or have been tampered with.
26    #[error("decryption failed: {detail}")]
27    DecryptFailed { detail: String },
28    /// The OS keychain / TPM ACL has a Deny entry for this binary.
29    #[error("authentication denied for '{label}'")]
30    AuthDenied { label: String },
31    /// User authentication is required but the device is locked or no GUI session is available.
32    #[error("authentication required for '{label}': {detail}")]
33    AuthRequired { label: String, detail: String },
34    /// The user dismissed the biometric or PIN prompt.
35    #[error("user cancelled authentication for '{label}'")]
36    UserCancelled { label: String },
37    /// A lower-level key operation failed.
38    #[error("key operation failed — {operation}: {detail}")]
39    KeyOperation { operation: String, detail: String },
40    /// File HMAC mismatch — the file has been modified outside the API.
41    #[error("tamper detected: {path}")]
42    TamperDetected { path: String },
43    /// Returned from factory construction (not first use) when a config option
44    /// requires a code-signed binary with the named entitlement/feature.
45    ///
46    /// The requested configuration requires a code-signed binary with a specific entitlement.
47    #[error("feature '{feature}' requires a code-signed binary")]
48    RequiresSigning { feature: String },
49    /// The backend cannot enforce the requested `AccessPolicy` (e.g. `BiometricOnly` on Linux).
50    ///
51    /// Returned from `generate_key()` when the backend cannot enforce the
52    /// requested `AccessPolicy` (e.g. `BiometricOnly` on Linux keyring/TPM).
53    #[error("access policy '{policy}' is not supported by the current backend")]
54    PolicyNotSupported { policy: String },
55    /// `sign_with_presence(Strict, ...)` was called on a platform without biometric support.
56    ///
57    /// Returned from `sign_with_presence()` when `PresenceMode::Strict` is
58    /// requested but the platform has no user-presence support.
59    #[error("user presence is not available on this platform")]
60    PresenceNotAvailable,
61    /// This API is not yet fully implemented on this platform. Check the `feature` string.
62    #[error("not implemented: {feature}")]
63    NotImplemented { feature: String },
64    /// The key's stored access policy does not match. Regenerate the key.
65    ///
66    /// This typically indicates the key was generated with a different policy
67    /// and should be regenerated.
68    #[error("access policy mismatch: {detail}")]
69    PolicyMismatch { detail: String },
70    /// A configuration value is invalid.
71    #[error("config error: {0}")]
72    Config(String),
73    /// An I/O error occurred.
74    #[error("I/O error: {0}")]
75    Io(#[from] std::io::Error),
76    /// An in-process memory protection operation failed (guard-page allocation, mlock, etc.).
77    #[error("memory error: {0}")]
78    Memory(String),
79}
80
81/// Shorthand `Result` type for this crate.
82pub type Result<T> = std::result::Result<T, Error>;
83
84/// Conversions from internal error types — only available when key management features are active.
85#[cfg(any(feature = "signing", feature = "encryption"))]
86impl From<crate::internal::core::Error> for Error {
87    #[allow(unreachable_patterns)]
88    fn from(e: crate::internal::core::Error) -> Self {
89        use crate::internal::core::Error as CE;
90        match e {
91            CE::NotAvailable => Error::NotAvailable,
92            CE::KeyNotFound { label } => Error::KeyNotFound { label },
93            CE::DuplicateLabel { label } => Error::DuplicateLabel { label },
94            CE::InvalidLabel { reason } => Error::InvalidLabel { reason },
95            CE::SignFailed { detail } => Error::SignFailed { detail },
96            CE::EncryptFailed { detail } => Error::EncryptFailed { detail },
97            CE::DecryptFailed { detail } => Error::DecryptFailed { detail },
98            CE::KeychainAuthDenied { label } => Error::AuthDenied { label },
99            CE::KeychainInteractionRequired { label } => Error::AuthRequired {
100                label,
101                detail: "screen may be locked; unlock and retry".into(),
102            },
103            CE::KeychainNoWindowServer { label } => Error::AuthRequired {
104                label,
105                detail: "no GUI session; restart agent via launchd".into(),
106            },
107            CE::UserCancelled { label } => Error::UserCancelled { label },
108            CE::KeyOperation { operation, detail } => Error::KeyOperation { operation, detail },
109            CE::GenerateFailed { detail } => Error::KeyOperation {
110                operation: "generate".into(),
111                detail,
112            },
113            CE::Config(s) | CE::Serialization(s) => Error::Config(s),
114            CE::Io(e) => Error::Io(e),
115            // non_exhaustive fallback: catches variants added to core::Error in the future.
116            // IMPORTANT — when adding new variants to crate::internal::core::Error, add
117            // explicit arms here BEFORE this catch-all so callers see the right Error variant.
118            // High-impact candidates: any new auth/presence error (→ AuthDenied/AuthRequired),
119            // any new availability error (→ NotAvailable), IO variants (→ Error::Io).
120            other => Error::KeyOperation {
121                operation: "unknown".into(),
122                detail: other.to_string(),
123            },
124        }
125    }
126}
127
128#[cfg(any(feature = "signing", feature = "encryption"))]
129impl From<crate::internal::app_storage::StorageError> for Error {
130    #[allow(unreachable_patterns)]
131    fn from(e: crate::internal::app_storage::StorageError) -> Self {
132        use crate::internal::app_storage::StorageError as SE;
133        match e {
134            SE::NotAvailable => Error::NotAvailable,
135            SE::EncryptionFailed(s) => Error::EncryptFailed { detail: s },
136            SE::DecryptionFailed(s) => Error::DecryptFailed { detail: s },
137            SE::SigningFailed(s) => Error::SignFailed { detail: s },
138            SE::KeyInitFailed(s) => Error::KeyOperation {
139                operation: "init".into(),
140                detail: s,
141            },
142            SE::KeyNotFound(s) => Error::KeyNotFound { label: s },
143            SE::PolicyMismatch(s) => Error::PolicyMismatch { detail: s },
144            SE::PlatformError(s) => Error::KeyOperation {
145                operation: "platform".into(),
146                detail: s,
147            },
148            // non_exhaustive fallback: catches variants added to StorageError in the future.
149            // IMPORTANT — when adding new variants to StorageError, add explicit arms here
150            // BEFORE this catch-all. High-impact candidates: any new availability error
151            // (→ NotAvailable), any new auth error (→ AuthDenied/AuthRequired).
152            other => Error::KeyOperation {
153                operation: "unknown".into(),
154                detail: other.to_string(),
155            },
156        }
157    }
158}
159
160#[cfg(all(test, any(feature = "signing", feature = "encryption")))]
161#[allow(clippy::unwrap_used, clippy::panic)]
162mod tests {
163    use super::*;
164    use crate::internal::app_storage::StorageError;
165
166    #[test]
167    fn from_storage_error_policy_mismatch_preserves_detail() {
168        let e: Error = StorageError::PolicyMismatch("None vs BiometricOnly".into()).into();
169        match e {
170            Error::PolicyMismatch { detail } => {
171                assert!(detail.contains("BiometricOnly"));
172            }
173            other => panic!("expected PolicyMismatch, got {other:?}"),
174        }
175    }
176
177    #[test]
178    fn from_storage_error_all_variants_convert() {
179        // Verify none of the conversions panic
180        let variants: Vec<StorageError> = vec![
181            StorageError::NotAvailable,
182            StorageError::EncryptionFailed("e".into()),
183            StorageError::DecryptionFailed("d".into()),
184            StorageError::SigningFailed("s".into()),
185            StorageError::KeyInitFailed("k".into()),
186            StorageError::KeyNotFound("n".into()),
187            StorageError::PolicyMismatch("p".into()),
188            StorageError::PlatformError("pl".into()),
189        ];
190        for v in variants {
191            drop(Error::from(v));
192        }
193    }
194}