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
84impl From<crate::internal::core::Error> for Error {
85    #[allow(unreachable_patterns)]
86    fn from(e: crate::internal::core::Error) -> Self {
87        use crate::internal::core::Error as CE;
88        match e {
89            CE::NotAvailable => Error::NotAvailable,
90            CE::KeyNotFound { label } => Error::KeyNotFound { label },
91            CE::DuplicateLabel { label } => Error::DuplicateLabel { label },
92            CE::InvalidLabel { reason } => Error::InvalidLabel { reason },
93            CE::SignFailed { detail } => Error::SignFailed { detail },
94            CE::EncryptFailed { detail } => Error::EncryptFailed { detail },
95            CE::DecryptFailed { detail } => Error::DecryptFailed { detail },
96            CE::KeychainAuthDenied { label } => Error::AuthDenied { label },
97            CE::KeychainInteractionRequired { label } => Error::AuthRequired {
98                label,
99                detail: "screen may be locked; unlock and retry".into(),
100            },
101            CE::KeychainNoWindowServer { label } => Error::AuthRequired {
102                label,
103                detail: "no GUI session; restart agent via launchd".into(),
104            },
105            CE::UserCancelled { label } => Error::UserCancelled { label },
106            CE::KeyOperation { operation, detail } => Error::KeyOperation { operation, detail },
107            CE::GenerateFailed { detail } => Error::KeyOperation {
108                operation: "generate".into(),
109                detail,
110            },
111            CE::Config(s) | CE::Serialization(s) => Error::Config(s),
112            CE::Io(e) => Error::Io(e),
113            // non_exhaustive fallback — add explicit arms for new crate::internal::core::Error
114            // variants as they are introduced
115            other => Error::KeyOperation {
116                operation: "unknown".into(),
117                detail: other.to_string(),
118            },
119        }
120    }
121}
122
123impl From<crate::internal::app_storage::StorageError> for Error {
124    #[allow(unreachable_patterns)]
125    fn from(e: crate::internal::app_storage::StorageError) -> Self {
126        use crate::internal::app_storage::StorageError as SE;
127        match e {
128            SE::NotAvailable => Error::NotAvailable,
129            SE::EncryptionFailed(s) => Error::EncryptFailed { detail: s },
130            SE::DecryptionFailed(s) => Error::DecryptFailed { detail: s },
131            SE::SigningFailed(s) => Error::SignFailed { detail: s },
132            SE::KeyInitFailed(s) => Error::KeyOperation {
133                operation: "init".into(),
134                detail: s,
135            },
136            SE::KeyNotFound(s) => Error::KeyNotFound { label: s },
137            SE::PolicyMismatch(s) => Error::PolicyMismatch { detail: s },
138            SE::PlatformError(s) => Error::KeyOperation {
139                operation: "platform".into(),
140                detail: s,
141            },
142            // non_exhaustive fallback — add explicit arms for new StorageError
143            // variants as they are introduced
144            other => Error::KeyOperation {
145                operation: "unknown".into(),
146                detail: other.to_string(),
147            },
148        }
149    }
150}
151
152#[cfg(test)]
153#[allow(clippy::unwrap_used, clippy::panic)]
154mod tests {
155    use super::*;
156    use crate::internal::app_storage::StorageError;
157
158    #[test]
159    fn from_storage_error_policy_mismatch_preserves_detail() {
160        let e: Error = StorageError::PolicyMismatch("None vs BiometricOnly".into()).into();
161        match e {
162            Error::PolicyMismatch { detail } => {
163                assert!(detail.contains("BiometricOnly"));
164            }
165            other => panic!("expected PolicyMismatch, got {other:?}"),
166        }
167    }
168
169    #[test]
170    fn from_storage_error_all_variants_convert() {
171        // Verify none of the conversions panic
172        let variants: Vec<StorageError> = vec![
173            StorageError::NotAvailable,
174            StorageError::EncryptionFailed("e".into()),
175            StorageError::DecryptionFailed("d".into()),
176            StorageError::SigningFailed("s".into()),
177            StorageError::KeyInitFailed("k".into()),
178            StorageError::KeyNotFound("n".into()),
179            StorageError::PolicyMismatch("p".into()),
180            StorageError::PlatformError("pl".into()),
181        ];
182        for v in variants {
183            drop(Error::from(v));
184        }
185    }
186}