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: catches variants added to core::Error in the future.
114            // IMPORTANT — when adding new variants to crate::internal::core::Error, add
115            // explicit arms here BEFORE this catch-all so callers see the right Error variant.
116            // High-impact candidates: any new auth/presence error (→ AuthDenied/AuthRequired),
117            // any new availability error (→ NotAvailable), IO variants (→ Error::Io).
118            other => Error::KeyOperation {
119                operation: "unknown".into(),
120                detail: other.to_string(),
121            },
122        }
123    }
124}
125
126impl From<crate::internal::app_storage::StorageError> for Error {
127    #[allow(unreachable_patterns)]
128    fn from(e: crate::internal::app_storage::StorageError) -> Self {
129        use crate::internal::app_storage::StorageError as SE;
130        match e {
131            SE::NotAvailable => Error::NotAvailable,
132            SE::EncryptionFailed(s) => Error::EncryptFailed { detail: s },
133            SE::DecryptionFailed(s) => Error::DecryptFailed { detail: s },
134            SE::SigningFailed(s) => Error::SignFailed { detail: s },
135            SE::KeyInitFailed(s) => Error::KeyOperation {
136                operation: "init".into(),
137                detail: s,
138            },
139            SE::KeyNotFound(s) => Error::KeyNotFound { label: s },
140            SE::PolicyMismatch(s) => Error::PolicyMismatch { detail: s },
141            SE::PlatformError(s) => Error::KeyOperation {
142                operation: "platform".into(),
143                detail: s,
144            },
145            // non_exhaustive fallback: catches variants added to StorageError in the future.
146            // IMPORTANT — when adding new variants to StorageError, add explicit arms here
147            // BEFORE this catch-all. High-impact candidates: any new availability error
148            // (→ NotAvailable), any new auth error (→ AuthDenied/AuthRequired).
149            other => Error::KeyOperation {
150                operation: "unknown".into(),
151                detail: other.to_string(),
152            },
153        }
154    }
155}
156
157#[cfg(test)]
158#[allow(clippy::unwrap_used, clippy::panic)]
159mod tests {
160    use super::*;
161    use crate::internal::app_storage::StorageError;
162
163    #[test]
164    fn from_storage_error_policy_mismatch_preserves_detail() {
165        let e: Error = StorageError::PolicyMismatch("None vs BiometricOnly".into()).into();
166        match e {
167            Error::PolicyMismatch { detail } => {
168                assert!(detail.contains("BiometricOnly"));
169            }
170            other => panic!("expected PolicyMismatch, got {other:?}"),
171        }
172    }
173
174    #[test]
175    fn from_storage_error_all_variants_convert() {
176        // Verify none of the conversions panic
177        let variants: Vec<StorageError> = vec![
178            StorageError::NotAvailable,
179            StorageError::EncryptionFailed("e".into()),
180            StorageError::DecryptionFailed("d".into()),
181            StorageError::SigningFailed("s".into()),
182            StorageError::KeyInitFailed("k".into()),
183            StorageError::KeyNotFound("n".into()),
184            StorageError::PolicyMismatch("p".into()),
185            StorageError::PlatformError("pl".into()),
186        ];
187        for v in variants {
188            drop(Error::from(v));
189        }
190    }
191}