1use thiserror::Error;
4
5pub use auths_crypto::AuthsErrorInfo;
6
7#[derive(Debug, Error)]
9#[non_exhaustive]
10pub enum AgentError {
11 #[error("Key not found")]
13 KeyNotFound,
14
15 #[error("Incorrect passphrase")]
17 IncorrectPassphrase,
18
19 #[error("Missing Passphrase")]
21 MissingPassphrase,
22
23 #[error("Security error: {0}")]
25 SecurityError(String),
26
27 #[error("Crypto error: {0}")]
29 CryptoError(String),
30
31 #[error("Key deserialization error: {0}")]
33 KeyDeserializationError(String),
34
35 #[error("Signing failed: {0}")]
37 SigningFailed(String),
38
39 #[error("Protocol error: {0}")]
41 Proto(String),
42
43 #[error("IO error: {0}")]
45 IO(#[from] std::io::Error),
46
47 #[error("git error: {0}")]
49 GitError(String),
50
51 #[error("Invalid input: {0}")]
53 InvalidInput(String),
54
55 #[error("Mutex lock poisoned: {0}")]
57 MutexError(String),
58
59 #[error("Storage error: {0}")]
61 StorageError(String),
62
63 #[error("User input cancelled")]
65 UserInputCancelled,
66
67 #[error("Keychain backend unavailable: {backend} - {reason}")]
70 BackendUnavailable {
71 backend: &'static str,
73 reason: String,
75 },
76
77 #[error("Storage is locked, authentication required")]
79 StorageLocked,
80
81 #[error("Failed to initialize keychain backend: {backend} - {error}")]
83 BackendInitFailed {
84 backend: &'static str,
86 error: String,
88 },
89
90 #[error("Credential too large for backend (max {max_bytes} bytes, got {actual_bytes})")]
92 CredentialTooLarge {
93 max_bytes: usize,
95 actual_bytes: usize,
97 },
98
99 #[error("Agent is locked. Unlock with 'auths agent unlock' or restart the agent.")]
101 AgentLocked,
102
103 #[error("Passphrase too weak: {0}")]
105 WeakPassphrase(String),
106
107 #[error("HSM PIN is locked — reset required")]
110 HsmPinLocked,
111
112 #[error("HSM device removed")]
114 HsmDeviceRemoved,
115
116 #[error("HSM session expired")]
118 HsmSessionExpired,
119
120 #[error("HSM does not support mechanism: {0}")]
122 HsmUnsupportedMechanism(String),
123
124 #[error(
127 "Operation '{operation}' requires a software-backed key; hardware-backed keys (e.g. Secure Enclave) cannot export raw material"
128 )]
129 HardwareKeyNotExportable {
130 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#[derive(Debug, Error)]
229#[non_exhaustive]
230pub enum TrustError {
231 #[error("I/O error: {0}")]
233 Io(#[from] std::io::Error),
234 #[error("{0}")]
236 InvalidData(String),
237 #[error("not found: {0}")]
239 NotFound(String),
240 #[error("serialization error: {0}")]
242 Serialization(#[from] serde_json::Error),
243 #[error("already exists: {0}")]
245 AlreadyExists(String),
246 #[error("lock acquisition failed: {0}")]
248 Lock(String),
249 #[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}