Skip to main content

auths_core/
error.rs

1//! Error types for agent and core operations.
2
3use thiserror::Error;
4
5/// Trait for error metadata providing structured error codes and actionable suggestions.
6///
7/// All Auths error types implement this trait to provide:
8/// - A unique error code for programmatic handling (e.g., "AUTHS_KEY_NOT_FOUND")
9/// - An optional human-readable suggestion for how to resolve the error
10pub trait AuthsErrorInfo {
11    /// Returns a unique error code string following the AUTHS_* naming convention.
12    ///
13    /// Error codes are stable identifiers that can be used for:
14    /// - Programmatic error handling in scripts
15    /// - Internationalization of error messages
16    /// - Logging and debugging
17    fn error_code(&self) -> &'static str;
18
19    /// Returns an optional actionable suggestion for resolving the error.
20    ///
21    /// Suggestions should be clear, concise commands or instructions
22    /// that help the user fix the problem.
23    fn suggestion(&self) -> Option<&'static str>;
24}
25
26/// Errors from the Auths agent and core operations.
27#[derive(Debug, Error)]
28#[non_exhaustive]
29pub enum AgentError {
30    /// The requested key was not found.
31    #[error("Key not found")]
32    KeyNotFound,
33
34    /// The provided passphrase is incorrect.
35    #[error("Incorrect passphrase")]
36    IncorrectPassphrase,
37
38    /// A passphrase is required but was not provided.
39    #[error("Missing Passphrase")]
40    MissingPassphrase,
41
42    /// A platform security framework error occurred.
43    #[error("Security error: {0}")]
44    SecurityError(String),
45
46    /// A cryptographic operation failed.
47    #[error("Crypto error: {0}")]
48    CryptoError(String),
49
50    /// Failed to deserialize a key.
51    #[error("Key deserialization error: {0}")]
52    KeyDeserializationError(String),
53
54    /// Signing operation failed.
55    #[error("Signing failed: {0}")]
56    SigningFailed(String),
57
58    /// A protocol error occurred.
59    #[error("Protocol error: {0}")]
60    Proto(String),
61
62    /// An I/O error occurred.
63    #[error("IO error: {0}")]
64    IO(#[from] std::io::Error),
65
66    /// A Git operation failed.
67    #[error("git error: {0}")]
68    GitError(String),
69
70    /// Invalid input was provided.
71    #[error("Invalid input: {0}")]
72    InvalidInput(String),
73
74    /// A mutex lock was poisoned.
75    #[error("Mutex lock poisoned: {0}")]
76    MutexError(String),
77
78    /// A storage operation failed.
79    #[error("Storage error: {0}")]
80    StorageError(String),
81
82    /// The user cancelled an interactive prompt.
83    #[error("User input cancelled")]
84    UserInputCancelled,
85
86    // --- Platform backend errors ---
87    /// Backend is not available on this platform or configuration
88    #[error("Keychain backend unavailable: {backend} - {reason}")]
89    BackendUnavailable {
90        /// Name of the failing backend.
91        backend: &'static str,
92        /// Reason the backend is unavailable.
93        reason: String,
94    },
95
96    /// Storage is locked and requires authentication
97    #[error("Storage is locked, authentication required")]
98    StorageLocked,
99
100    /// Backend initialization failed
101    #[error("Failed to initialize keychain backend: {backend} - {error}")]
102    BackendInitFailed {
103        /// Name of the failing backend.
104        backend: &'static str,
105        /// Initialization error message.
106        error: String,
107    },
108
109    /// Credential size exceeds platform limit
110    #[error("Credential too large for backend (max {max_bytes} bytes, got {actual_bytes})")]
111    CredentialTooLarge {
112        /// Maximum credential size in bytes.
113        max_bytes: usize,
114        /// Actual credential size in bytes.
115        actual_bytes: usize,
116    },
117
118    /// Agent is locked due to idle timeout
119    #[error("Agent is locked. Unlock with 'auths agent unlock' or restart the agent.")]
120    AgentLocked,
121
122    /// The passphrase does not meet strength requirements.
123    #[error("Passphrase too weak: {0}")]
124    WeakPassphrase(String),
125
126    // --- HSM / PKCS#11 errors ---
127    /// HSM PIN is locked after too many failed attempts.
128    #[error("HSM PIN is locked — reset required")]
129    HsmPinLocked,
130
131    /// HSM device was removed during operation.
132    #[error("HSM device removed")]
133    HsmDeviceRemoved,
134
135    /// HSM session expired or was closed unexpectedly.
136    #[error("HSM session expired")]
137    HsmSessionExpired,
138
139    /// HSM does not support the requested cryptographic mechanism.
140    #[error("HSM does not support mechanism: {0}")]
141    HsmUnsupportedMechanism(String),
142}
143
144impl AuthsErrorInfo for AgentError {
145    fn error_code(&self) -> &'static str {
146        match self {
147            Self::KeyNotFound => "AUTHS_KEY_NOT_FOUND",
148            Self::IncorrectPassphrase => "AUTHS_INCORRECT_PASSPHRASE",
149            Self::MissingPassphrase => "AUTHS_MISSING_PASSPHRASE",
150            Self::SecurityError(_) => "AUTHS_SECURITY_ERROR",
151            Self::CryptoError(_) => "AUTHS_CRYPTO_ERROR",
152            Self::KeyDeserializationError(_) => "AUTHS_KEY_DESERIALIZATION_ERROR",
153            Self::SigningFailed(_) => "AUTHS_SIGNING_FAILED",
154            Self::Proto(_) => "AUTHS_PROTOCOL_ERROR",
155            Self::IO(_) => "AUTHS_IO_ERROR",
156            Self::GitError(_) => "AUTHS_GIT_ERROR",
157            Self::InvalidInput(_) => "AUTHS_INVALID_INPUT",
158            Self::MutexError(_) => "AUTHS_MUTEX_ERROR",
159            Self::StorageError(_) => "AUTHS_STORAGE_ERROR",
160            Self::UserInputCancelled => "AUTHS_USER_CANCELLED",
161            Self::BackendUnavailable { .. } => "AUTHS_BACKEND_UNAVAILABLE",
162            Self::StorageLocked => "AUTHS_STORAGE_LOCKED",
163            Self::BackendInitFailed { .. } => "AUTHS_BACKEND_INIT_FAILED",
164            Self::CredentialTooLarge { .. } => "AUTHS_CREDENTIAL_TOO_LARGE",
165            Self::AgentLocked => "AUTHS_AGENT_LOCKED",
166            Self::WeakPassphrase(_) => "AUTHS_WEAK_PASSPHRASE",
167            Self::HsmPinLocked => "AUTHS_HSM_PIN_LOCKED",
168            Self::HsmDeviceRemoved => "AUTHS_HSM_DEVICE_REMOVED",
169            Self::HsmSessionExpired => "AUTHS_HSM_SESSION_EXPIRED",
170            Self::HsmUnsupportedMechanism(_) => "AUTHS_HSM_UNSUPPORTED_MECHANISM",
171        }
172    }
173
174    fn suggestion(&self) -> Option<&'static str> {
175        match self {
176            Self::KeyNotFound => Some("Run `auths key list` to see available keys"),
177            Self::IncorrectPassphrase => Some("Check your passphrase and try again"),
178            Self::MissingPassphrase => {
179                Some("Provide a passphrase with --passphrase or set AUTHS_PASSPHRASE")
180            }
181            Self::BackendUnavailable { .. } => {
182                Some("Run `auths doctor` to diagnose keychain issues")
183            }
184            Self::StorageLocked => Some("Authenticate with your platform keychain"),
185            Self::BackendInitFailed { .. } => {
186                Some("Run `auths doctor` to diagnose keychain issues")
187            }
188            Self::GitError(_) => Some("Ensure you're in a Git repository"),
189            Self::AgentLocked => {
190                Some("Run `auths agent unlock` or restart with `auths agent start`")
191            }
192            Self::UserInputCancelled => {
193                Some("Run the command again and provide the required input")
194            }
195            Self::StorageError(_) => Some("Check file permissions and disk space"),
196            // These errors typically don't have actionable suggestions
197            Self::SecurityError(_)
198            | Self::CryptoError(_)
199            | Self::KeyDeserializationError(_)
200            | Self::SigningFailed(_)
201            | Self::Proto(_)
202            | Self::IO(_)
203            | Self::InvalidInput(_)
204            | Self::MutexError(_)
205            | Self::CredentialTooLarge { .. } => None,
206            Self::WeakPassphrase(_) => {
207                Some("Use at least 12 characters with uppercase, lowercase, and a digit or symbol")
208            }
209            Self::HsmPinLocked => Some("Reset the HSM PIN using your HSM vendor's admin tools"),
210            Self::HsmDeviceRemoved => Some("Reconnect the HSM device and try again"),
211            Self::HsmSessionExpired => Some("Retry the operation — a new session will be opened"),
212            Self::HsmUnsupportedMechanism(_) => {
213                Some("Check that your HSM supports Ed25519 (CKM_EDDSA)")
214            }
215        }
216    }
217}
218
219/// Errors from trust resolution and identity pinning.
220#[derive(Debug, Error)]
221#[non_exhaustive]
222pub enum TrustError {
223    /// An I/O error occurred.
224    #[error("I/O error: {0}")]
225    Io(#[from] std::io::Error),
226    /// Invalid data encountered (corrupt pin, bad hex, wrong format).
227    #[error("{0}")]
228    InvalidData(String),
229    /// A required resource was not found.
230    #[error("not found: {0}")]
231    NotFound(String),
232    /// JSON serialization/deserialization failed.
233    #[error("serialization error: {0}")]
234    Serialization(#[from] serde_json::Error),
235    /// Attempted to create something that already exists.
236    #[error("already exists: {0}")]
237    AlreadyExists(String),
238    /// Advisory file lock could not be acquired.
239    #[error("lock acquisition failed: {0}")]
240    Lock(String),
241    /// Trust policy rejected the identity.
242    #[error("policy rejected: {0}")]
243    PolicyRejected(String),
244}
245
246impl AuthsErrorInfo for TrustError {
247    fn error_code(&self) -> &'static str {
248        match self {
249            Self::Io(_) => "AUTHS_TRUST_IO_ERROR",
250            Self::InvalidData(_) => "AUTHS_TRUST_INVALID_DATA",
251            Self::NotFound(_) => "AUTHS_TRUST_NOT_FOUND",
252            Self::Serialization(_) => "AUTHS_TRUST_SERIALIZATION_ERROR",
253            Self::AlreadyExists(_) => "AUTHS_TRUST_ALREADY_EXISTS",
254            Self::Lock(_) => "AUTHS_TRUST_LOCK_FAILED",
255            Self::PolicyRejected(_) => "AUTHS_TRUST_POLICY_REJECTED",
256        }
257    }
258
259    fn suggestion(&self) -> Option<&'static str> {
260        match self {
261            Self::NotFound(_) => Some("Run `auths trust list` to see pinned identities"),
262            Self::PolicyRejected(_) => Some("Run `auths trust add` to pin this identity"),
263            Self::Lock(_) => Some("Check file permissions and try again"),
264            Self::Io(_) => Some("Check disk space and file permissions"),
265            Self::AlreadyExists(_) => Some("Run `auths trust list` to see existing entries"),
266            Self::InvalidData(_) | Self::Serialization(_) => None,
267        }
268    }
269}
270
271impl From<AgentError> for ssh_agent_lib::error::AgentError {
272    fn from(err: AgentError) -> Self {
273        match err {
274            AgentError::KeyNotFound => Self::Failure,
275            AgentError::IncorrectPassphrase => Self::Failure,
276            _ => Self::Failure,
277        }
278    }
279}