Skip to main content

auths_core/
error.rs

1//! Error types for agent and core operations.
2
3use thiserror::Error;
4
5pub use auths_crypto::AuthsErrorInfo;
6
7/// Errors from the Auths agent and core operations.
8#[derive(Debug, Error)]
9#[non_exhaustive]
10pub enum AgentError {
11    /// The requested key was not found.
12    #[error("Key not found")]
13    KeyNotFound,
14
15    /// The provided passphrase is incorrect.
16    #[error("Incorrect passphrase")]
17    IncorrectPassphrase,
18
19    /// A passphrase is required but was not provided.
20    #[error("Missing Passphrase")]
21    MissingPassphrase,
22
23    /// A platform security framework error occurred.
24    #[error("Security error: {0}")]
25    SecurityError(String),
26
27    /// A cryptographic operation failed.
28    #[error("Crypto error: {0}")]
29    CryptoError(String),
30
31    /// Failed to deserialize a key.
32    #[error("Key deserialization error: {0}")]
33    KeyDeserializationError(String),
34
35    /// Signing operation failed.
36    #[error("Signing failed: {0}")]
37    SigningFailed(String),
38
39    /// A protocol error occurred.
40    #[error("Protocol error: {0}")]
41    Proto(String),
42
43    /// An I/O error occurred.
44    #[error("IO error: {0}")]
45    IO(#[from] std::io::Error),
46
47    /// A Git operation failed.
48    #[error("git error: {0}")]
49    GitError(String),
50
51    /// Invalid input was provided.
52    #[error("Invalid input: {0}")]
53    InvalidInput(String),
54
55    /// A mutex lock was poisoned.
56    #[error("Mutex lock poisoned: {0}")]
57    MutexError(String),
58
59    /// A storage operation failed.
60    #[error("Storage error: {0}")]
61    StorageError(String),
62
63    /// The user cancelled an interactive prompt.
64    #[error("User input cancelled")]
65    UserInputCancelled,
66
67    // --- Platform backend errors ---
68    /// Backend is not available on this platform or configuration
69    #[error("Keychain backend unavailable: {backend} - {reason}")]
70    BackendUnavailable {
71        /// Name of the failing backend.
72        backend: &'static str,
73        /// Reason the backend is unavailable.
74        reason: String,
75    },
76
77    /// Storage is locked and requires authentication
78    #[error("Storage is locked, authentication required")]
79    StorageLocked,
80
81    /// Backend initialization failed
82    #[error("Failed to initialize keychain backend: {backend} - {error}")]
83    BackendInitFailed {
84        /// Name of the failing backend.
85        backend: &'static str,
86        /// Initialization error message.
87        error: String,
88    },
89
90    /// Credential size exceeds platform limit
91    #[error("Credential too large for backend (max {max_bytes} bytes, got {actual_bytes})")]
92    CredentialTooLarge {
93        /// Maximum credential size in bytes.
94        max_bytes: usize,
95        /// Actual credential size in bytes.
96        actual_bytes: usize,
97    },
98
99    /// Agent is locked due to idle timeout
100    #[error("Agent is locked. Unlock with 'auths agent unlock' or restart the agent.")]
101    AgentLocked,
102
103    /// The passphrase does not meet strength requirements.
104    #[error("Passphrase too weak: {0}")]
105    WeakPassphrase(String),
106
107    // --- HSM / PKCS#11 errors ---
108    /// HSM PIN is locked after too many failed attempts.
109    #[error("HSM PIN is locked — reset required")]
110    HsmPinLocked,
111
112    /// HSM device was removed during operation.
113    #[error("HSM device removed")]
114    HsmDeviceRemoved,
115
116    /// HSM session expired or was closed unexpectedly.
117    #[error("HSM session expired")]
118    HsmSessionExpired,
119
120    /// HSM does not support the requested cryptographic mechanism.
121    #[error("HSM does not support mechanism: {0}")]
122    HsmUnsupportedMechanism(String),
123
124    /// Operation cannot be completed because the key is hardware-backed (SE/HSM)
125    /// and the operation requires raw key material.
126    #[error(
127        "Operation '{operation}' requires a software-backed key; hardware-backed keys (e.g. Secure Enclave) cannot export raw material"
128    )]
129    HardwareKeyNotExportable {
130        /// Name of the operation that requires raw key material.
131        operation: String,
132    },
133}
134
135impl AuthsErrorInfo for AgentError {
136    fn error_code(&self) -> &'static str {
137        match self {
138            Self::KeyNotFound => "AUTHS-E3001",
139            Self::IncorrectPassphrase => "AUTHS-E3002",
140            Self::MissingPassphrase => "AUTHS-E3003",
141            Self::SecurityError(_) => "AUTHS-E3004",
142            Self::CryptoError(_) => "AUTHS-E3005",
143            Self::KeyDeserializationError(_) => "AUTHS-E3006",
144            Self::SigningFailed(_) => "AUTHS-E3007",
145            Self::Proto(_) => "AUTHS-E3008",
146            Self::IO(_) => "AUTHS-E3009",
147            Self::GitError(_) => "AUTHS-E3010",
148            Self::InvalidInput(_) => "AUTHS-E3011",
149            Self::MutexError(_) => "AUTHS-E3012",
150            Self::StorageError(_) => "AUTHS-E3013",
151            Self::UserInputCancelled => "AUTHS-E3014",
152            Self::BackendUnavailable { .. } => "AUTHS-E3015",
153            Self::StorageLocked => "AUTHS-E3016",
154            Self::BackendInitFailed { .. } => "AUTHS-E3017",
155            Self::CredentialTooLarge { .. } => "AUTHS-E3018",
156            Self::AgentLocked => "AUTHS-E3019",
157            Self::WeakPassphrase(_) => "AUTHS-E3020",
158            Self::HsmPinLocked => "AUTHS-E3021",
159            Self::HsmDeviceRemoved => "AUTHS-E3022",
160            Self::HsmSessionExpired => "AUTHS-E3023",
161            Self::HsmUnsupportedMechanism(_) => "AUTHS-E3024",
162            Self::HardwareKeyNotExportable { .. } => "AUTHS-E3025",
163        }
164    }
165
166    fn suggestion(&self) -> Option<&'static str> {
167        match self {
168            Self::KeyNotFound => Some("Run `auths key list` to see available keys"),
169            Self::IncorrectPassphrase => Some(
170                "Check your passphrase and try again. Set AUTHS_PASSPHRASE for automation, or run `auths agent start` for session caching",
171            ),
172            Self::MissingPassphrase => {
173                Some("Provide a passphrase with --passphrase or set AUTHS_PASSPHRASE")
174            }
175            Self::BackendUnavailable { .. } => {
176                Some("Run `auths doctor` to diagnose keychain issues")
177            }
178            Self::StorageLocked => Some("Authenticate with your platform keychain"),
179            Self::BackendInitFailed { .. } => {
180                Some("Run `auths doctor` to diagnose keychain issues")
181            }
182            Self::GitError(_) => Some("Ensure you're in a Git repository"),
183            Self::AgentLocked => {
184                Some("Run `auths agent unlock` or restart with `auths agent start`")
185            }
186            Self::UserInputCancelled => {
187                Some("Run the command again and provide the required input")
188            }
189            Self::StorageError(_) => Some("Check file permissions and disk space"),
190            Self::SecurityError(_) => Some(
191                "Run `auths doctor` to check system keychain access and security configuration",
192            ),
193            Self::CryptoError(_) => {
194                Some("A cryptographic operation failed; check key material with `auths key list`")
195            }
196            Self::KeyDeserializationError(_) => {
197                Some("The stored key is corrupted; re-import with `auths key import`")
198            }
199            Self::SigningFailed(_) => Some(
200                "The signing operation failed; verify your key is accessible with `auths key list`",
201            ),
202            Self::Proto(_) => Some(
203                "A protocol error occurred; check that both sides are running compatible versions",
204            ),
205            Self::IO(_) => Some("Check file permissions and that the filesystem is not read-only"),
206            Self::InvalidInput(_) => Some("Check the command arguments and try again"),
207            Self::MutexError(_) => Some("A concurrency error occurred; restart the operation"),
208            Self::CredentialTooLarge { .. } => Some(
209                "Reduce the credential size or use file-based storage with AUTHS_KEYCHAIN_BACKEND=file",
210            ),
211            Self::WeakPassphrase(_) => {
212                Some("Use at least 12 characters with uppercase, lowercase, and a digit or symbol")
213            }
214            Self::HsmPinLocked => Some("Reset the HSM PIN using your HSM vendor's admin tools"),
215            Self::HsmDeviceRemoved => Some("Reconnect the HSM device and try again"),
216            Self::HsmSessionExpired => Some("Retry the operation — a new session will be opened"),
217            Self::HsmUnsupportedMechanism(_) => {
218                Some("Check that your HSM supports Ed25519 (CKM_EDDSA)")
219            }
220            Self::HardwareKeyNotExportable { .. } => Some(
221                "Use a software-backed keychain backend for this operation, or re-initialize your identity without Secure Enclave",
222            ),
223        }
224    }
225}
226
227/// Errors from trust resolution and identity pinning.
228#[derive(Debug, Error)]
229#[non_exhaustive]
230pub enum TrustError {
231    /// An I/O error occurred.
232    #[error("I/O error: {0}")]
233    Io(#[from] std::io::Error),
234    /// Invalid data encountered (corrupt pin, bad hex, wrong format).
235    #[error("{0}")]
236    InvalidData(String),
237    /// A required resource was not found.
238    #[error("not found: {0}")]
239    NotFound(String),
240    /// JSON serialization/deserialization failed.
241    #[error("serialization error: {0}")]
242    Serialization(#[from] serde_json::Error),
243    /// Attempted to create something that already exists.
244    #[error("already exists: {0}")]
245    AlreadyExists(String),
246    /// Advisory file lock could not be acquired.
247    #[error("lock acquisition failed: {0}")]
248    Lock(String),
249    /// Trust policy rejected the identity.
250    #[error("policy rejected: {0}")]
251    PolicyRejected(String),
252}
253
254impl AuthsErrorInfo for TrustError {
255    fn error_code(&self) -> &'static str {
256        match self {
257            Self::Io(_) => "AUTHS-E3101",
258            Self::InvalidData(_) => "AUTHS-E3102",
259            Self::NotFound(_) => "AUTHS-E3103",
260            Self::Serialization(_) => "AUTHS-E3104",
261            Self::AlreadyExists(_) => "AUTHS-E3105",
262            Self::Lock(_) => "AUTHS-E3106",
263            Self::PolicyRejected(_) => "AUTHS-E3107",
264        }
265    }
266
267    fn suggestion(&self) -> Option<&'static str> {
268        match self {
269            Self::NotFound(_) => Some("Run `auths trust list` to see pinned identities"),
270            Self::PolicyRejected(_) => Some("Run `auths trust pin` to pin this identity"),
271            Self::Lock(_) => Some("Check file permissions and try again"),
272            Self::Io(_) => Some("Check disk space and file permissions"),
273            Self::AlreadyExists(_) => Some("Run `auths trust list` to see existing entries"),
274            Self::InvalidData(_) => {
275                Some("The trust store may be corrupted; delete and re-pin with `auths trust pin`")
276            }
277            Self::Serialization(_) => {
278                Some("The trust store data is corrupted; delete and re-pin with `auths trust pin`")
279            }
280        }
281    }
282}
283
284impl From<AgentError> for ssh_agent_lib::error::AgentError {
285    fn from(err: AgentError) -> Self {
286        match err {
287            AgentError::KeyNotFound => Self::Failure,
288            AgentError::IncorrectPassphrase => Self::Failure,
289            _ => Self::Failure,
290        }
291    }
292}
293
294#[cfg(test)]
295mod tests {
296    use super::*;
297
298    #[test]
299    fn hardware_key_not_exportable_has_actionable_display() {
300        let err = AgentError::HardwareKeyNotExportable {
301            operation: "pairing".into(),
302        };
303        let msg = err.to_string();
304        assert!(msg.contains("hardware"), "msg={msg}");
305        assert!(msg.contains("pairing"), "msg={msg}");
306    }
307}