nonce_auth/nonce/error.rs
1use thiserror::Error;
2
3/// Error types that can occur during nonce authentication operations.
4///
5/// This enum represents all possible errors that can occur when using
6/// the nonce authentication library. Each variant corresponds to a
7/// specific failure mode in the authentication process.
8///
9/// # Error Categories
10///
11/// - **Authentication Errors**: `DuplicateNonce`, `InvalidSignature`, `TimestampOutOfWindow`
12/// - **System Errors**: `StorageError`, `CryptoError`
13///
14/// # Error Codes
15///
16/// Each error variant has a stable string code that can be used for programmatic error handling:
17///
18/// ```rust
19/// use nonce_auth::NonceError;
20///
21/// let error = NonceError::DuplicateNonce;
22/// assert_eq!(error.code(), "duplicate_nonce");
23/// ```
24///
25/// # Example
26///
27/// ```rust
28/// use nonce_auth::{CredentialBuilder, CredentialVerifier, NonceError, storage::MemoryStorage};
29/// use hmac::Mac;
30/// use std::sync::Arc;
31///
32/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
33/// let storage = Arc::new(MemoryStorage::new());
34/// let payload = b"test payload";
35/// let credential = CredentialBuilder::new(b"secret").sign(payload)?;
36///
37/// // Handle different error types
38/// match CredentialVerifier::new(storage)
39/// .with_secret(b"secret")
40/// .verify_with(&credential, |mac| {
41/// mac.update(credential.timestamp.to_string().as_bytes());
42/// mac.update(credential.nonce.as_bytes());
43/// mac.update(payload);
44/// })
45/// .await
46/// {
47/// Ok(()) => println!("Request verified"),
48/// Err(NonceError::DuplicateNonce) => println!("Nonce already used"),
49/// Err(NonceError::InvalidSignature) => println!("Invalid signature"),
50/// Err(NonceError::TimestampOutOfWindow) => println!("Request too old"),
51/// Err(e) => println!("Other error: {e}"),
52/// }
53/// # Ok(())
54/// # }
55/// ```
56#[derive(Error, Debug)]
57pub enum NonceError {
58 /// The nonce has already been used and cannot be reused.
59 ///
60 /// This error occurs when attempting to use a nonce that has
61 /// already been consumed. This is the primary mechanism
62 /// for preventing replay attacks.
63 ///
64 /// # When This Occurs
65 ///
66 /// - The same signed request is sent twice
67 /// - A malicious actor attempts to replay a captured request
68 /// - Network issues cause duplicate request delivery
69 ///
70 /// # Resolution
71 ///
72 /// A new signed request should be generated with a fresh nonce.
73 #[error("Nonce already exists")]
74 DuplicateNonce,
75
76 /// The HMAC signature verification failed.
77 ///
78 /// This error occurs when the provided signature doesn't match the
79 /// expected signature calculated during verification. This indicates either
80 /// a tampered request or mismatched secrets.
81 ///
82 /// # When This Occurs
83 ///
84 /// - Different secret keys are being used for signing and verification
85 /// - The request has been tampered with in transit
86 /// - There's a bug in the signature generation/verification logic
87 /// - The timestamp or nonce values have been modified
88 ///
89 /// # Resolution
90 ///
91 /// - Verify that the same secret key is used for signing and verification
92 /// - Check for request tampering or transmission errors
93 /// - Ensure proper signature generation during credential creation
94 #[error("Invalid signature")]
95 InvalidSignature,
96
97 /// The request timestamp is outside the allowed time window.
98 ///
99 /// This error occurs when the timestamp in the signed request is
100 /// either too old or too far in the future compared to the current
101 /// current time, exceeding the configured time window.
102 ///
103 /// # When This Occurs
104 ///
105 /// - System clocks are significantly out of sync
106 /// - Network delays cause old requests to arrive late
107 /// - The time window is configured too strictly
108 /// - A malicious actor attempts to use very old captured requests
109 ///
110 /// # Resolution
111 ///
112 /// - Synchronize system clocks (e.g., using NTP)
113 /// - Increase the time window if appropriate for your use case
114 /// - Generate fresh requests closer to when they'll be sent
115 #[error("Timestamp out of window")]
116 TimestampOutOfWindow,
117
118 /// A storage operation failed.
119 ///
120 /// This error occurs when there's a problem with the underlying
121 /// storage backend operations, such as connection issues, disk space
122 /// problems, or corruption. This applies to all storage backends
123 /// including memory, SQLite, Redis, and others.
124 ///
125 /// # When This Occurs
126 ///
127 /// - Storage backend is unavailable or unreachable
128 /// - Database file is corrupted or inaccessible (SQLite)
129 /// - Redis server connection issues (Redis)
130 /// - Insufficient disk space for storage operations
131 /// - Storage backend is locked by another process
132 /// - File permission issues (file-based storage)
133 ///
134 /// # Resolution
135 ///
136 /// - Check storage backend availability and connectivity
137 /// - Verify storage permissions and disk space
138 /// - Ensure proper storage initialization
139 /// - Check for competing storage access
140 #[error("Storage error: {0}")]
141 StorageError(#[source] Box<dyn std::error::Error + Send + Sync>),
142
143 /// A cryptographic operation failed.
144 ///
145 /// This error occurs when there's a problem with the HMAC signature
146 /// generation or verification process, typically due to invalid
147 /// key material or system-level crypto issues.
148 ///
149 /// # When This Occurs
150 ///
151 /// - Invalid or corrupted secret key
152 /// - System-level cryptographic library issues
153 /// - Memory allocation failures during crypto operations
154 ///
155 /// # Resolution
156 ///
157 /// - Verify the secret key is valid and properly formatted
158 /// - Check system cryptographic library installation
159 /// - Ensure sufficient system resources
160 #[error("Crypto error: {0}")]
161 CryptoError(String),
162}
163
164impl NonceError {
165 /// Returns a stable string code for this error that can be used for programmatic error handling.
166 ///
167 /// The error codes are guaranteed to remain stable across versions, making them
168 /// suitable for use in error handling logic, logging, monitoring, and API responses.
169 ///
170 /// # Error Codes
171 ///
172 /// - `duplicate_nonce`: Nonce has already been used
173 /// - `invalid_signature`: HMAC signature verification failed
174 /// - `timestamp_out_of_window`: Request timestamp is outside allowed window
175 /// - `storage_error`: Storage backend operation failed
176 /// - `crypto_error`: Cryptographic operation failed
177 ///
178 /// # Example
179 ///
180 /// ```rust
181 /// use nonce_auth::NonceError;
182 ///
183 /// let error = NonceError::DuplicateNonce;
184 /// assert_eq!(error.code(), "duplicate_nonce");
185 ///
186 /// match error.code() {
187 /// "duplicate_nonce" => println!("Replay attack detected"),
188 /// "invalid_signature" => println!("Authentication failed"),
189 /// "storage_error" => println!("Storage backend issue"),
190 /// _ => println!("Other error"),
191 /// }
192 /// ```
193 pub fn code(&self) -> &'static str {
194 match self {
195 NonceError::DuplicateNonce => "duplicate_nonce",
196 NonceError::InvalidSignature => "invalid_signature",
197 NonceError::TimestampOutOfWindow => "timestamp_out_of_window",
198 NonceError::StorageError(_) => "storage_error",
199 NonceError::CryptoError(_) => "crypto_error",
200 }
201 }
202
203 /// Creates a new `StorageError` from any error that implements the standard library's `Error` trait.
204 ///
205 /// This is a convenience method for creating storage errors while preserving the original
206 /// error information. The original error will be available through the `source()` method.
207 ///
208 /// # Example
209 ///
210 /// ```rust
211 /// use nonce_auth::NonceError;
212 /// use std::io;
213 /// use std::error::Error;
214 ///
215 /// let io_error = io::Error::new(io::ErrorKind::PermissionDenied, "File access denied");
216 /// let nonce_error = NonceError::from_storage_error(io_error);
217 ///
218 /// assert_eq!(nonce_error.code(), "storage_error");
219 /// assert!(nonce_error.source().is_some());
220 /// ```
221 pub fn from_storage_error<E>(error: E) -> Self
222 where
223 E: std::error::Error + Send + Sync + 'static,
224 {
225 NonceError::StorageError(Box::new(error))
226 }
227
228 /// Creates a new `StorageError` from a string message.
229 ///
230 /// This method is useful when you need to create a storage error from a string
231 /// without an underlying error type.
232 ///
233 /// # Example
234 ///
235 /// ```rust
236 /// use nonce_auth::NonceError;
237 ///
238 /// let error = NonceError::from_storage_message("Connection timeout");
239 /// assert_eq!(error.code(), "storage_error");
240 /// ```
241 pub fn from_storage_message<S: Into<String>>(message: S) -> Self {
242 #[derive(Debug)]
243 struct SimpleError(String);
244
245 impl std::fmt::Display for SimpleError {
246 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
247 write!(f, "{}", self.0)
248 }
249 }
250
251 impl std::error::Error for SimpleError {}
252
253 NonceError::StorageError(Box::new(SimpleError(message.into())))
254 }
255
256 /// Returns true if this is a temporary error that might succeed if retried.
257 ///
258 /// Temporary errors are typically system-level issues like database connectivity
259 /// problems or transient resource constraints. Authentication errors like
260 /// `InvalidSignature` or `DuplicateNonce` are not considered temporary.
261 ///
262 /// # Example
263 ///
264 /// ```rust
265 /// use nonce_auth::NonceError;
266 ///
267 /// let auth_error = NonceError::InvalidSignature;
268 /// let db_error = NonceError::from_storage_message("Connection timeout");
269 ///
270 /// assert!(!auth_error.is_temporary());
271 /// assert!(db_error.is_temporary());
272 /// ```
273 pub fn is_temporary(&self) -> bool {
274 matches!(
275 self,
276 NonceError::StorageError(_) | NonceError::CryptoError(_)
277 )
278 }
279
280 /// Returns true if this is an authentication-related error.
281 ///
282 /// Authentication errors indicate issues with the credential verification
283 /// process, as opposed to system-level errors.
284 ///
285 /// # Example
286 ///
287 /// ```rust
288 /// use nonce_auth::NonceError;
289 ///
290 /// let auth_error = NonceError::InvalidSignature;
291 /// let system_error = NonceError::from_storage_message("Connection timeout");
292 ///
293 /// assert!(auth_error.is_authentication_error());
294 /// assert!(!system_error.is_authentication_error());
295 /// ```
296 pub fn is_authentication_error(&self) -> bool {
297 matches!(
298 self,
299 NonceError::DuplicateNonce
300 | NonceError::InvalidSignature
301 | NonceError::TimestampOutOfWindow
302 )
303 }
304
305 /// Returns true if this error represents a request-side issue.
306 ///
307 /// Request errors indicate problems with the request that should
308 /// be fixed before retrying.
309 ///
310 /// # Example
311 ///
312 /// ```rust
313 /// use nonce_auth::NonceError;
314 ///
315 /// let request_error = NonceError::InvalidSignature;
316 /// let system_error = NonceError::from_storage_message("Connection failed");
317 ///
318 /// assert!(request_error.is_client_error());
319 /// assert!(!system_error.is_client_error());
320 /// ```
321 pub fn is_client_error(&self) -> bool {
322 matches!(
323 self,
324 NonceError::DuplicateNonce
325 | NonceError::InvalidSignature
326 | NonceError::TimestampOutOfWindow
327 )
328 }
329
330 /// Returns true if this error represents a system-side issue.
331 ///
332 /// System errors indicate problems with the system that are
333 /// not the request's fault and may be temporary.
334 ///
335 /// # Example
336 ///
337 /// ```rust
338 /// use nonce_auth::NonceError;
339 ///
340 /// let system_error = NonceError::from_storage_message("Connection failed");
341 /// let request_error = NonceError::InvalidSignature;
342 ///
343 /// assert!(system_error.is_server_error());
344 /// assert!(!request_error.is_server_error());
345 /// ```
346 pub fn is_server_error(&self) -> bool {
347 matches!(
348 self,
349 NonceError::StorageError(_) | NonceError::CryptoError(_)
350 )
351 }
352}
353
354// SQLite error conversion is now provided in examples/sqlite_storage.rs
355// since rusqlite is no longer a core dependency
356
357#[cfg(test)]
358mod tests {
359 use super::*;
360 use std::error::Error;
361
362 #[test]
363 fn test_error_types() {
364 // Test error display messages
365 assert_eq!(
366 NonceError::DuplicateNonce.to_string(),
367 "Nonce already exists"
368 );
369 assert_eq!(
370 NonceError::InvalidSignature.to_string(),
371 "Invalid signature"
372 );
373 assert_eq!(
374 NonceError::TimestampOutOfWindow.to_string(),
375 "Timestamp out of window"
376 );
377
378 let storage_error = NonceError::from_storage_message("test error");
379 assert_eq!(storage_error.to_string(), "Storage error: test error");
380
381 let crypto_error = NonceError::CryptoError("crypto test error".to_string());
382 assert_eq!(crypto_error.to_string(), "Crypto error: crypto test error");
383 }
384
385 #[test]
386 fn test_error_codes() {
387 assert_eq!(NonceError::DuplicateNonce.code(), "duplicate_nonce");
388 assert_eq!(NonceError::InvalidSignature.code(), "invalid_signature");
389 assert_eq!(
390 NonceError::TimestampOutOfWindow.code(),
391 "timestamp_out_of_window"
392 );
393 assert_eq!(
394 NonceError::from_storage_message("test").code(),
395 "storage_error"
396 );
397 assert_eq!(
398 NonceError::CryptoError("test".to_string()).code(),
399 "crypto_error"
400 );
401 }
402
403 #[test]
404 fn test_storage_error_from_error() {
405 use std::io;
406
407 let io_error = io::Error::new(io::ErrorKind::PermissionDenied, "Permission denied");
408 let nonce_error = NonceError::from_storage_error(io_error);
409
410 assert_eq!(nonce_error.code(), "storage_error");
411 assert!(nonce_error.source().is_some());
412 assert!(nonce_error.to_string().contains("Permission denied"));
413 }
414
415 #[test]
416 fn test_storage_error_from_message() {
417 let error = NonceError::from_storage_message("Connection timeout");
418
419 assert_eq!(error.code(), "storage_error");
420 assert!(error.to_string().contains("Connection timeout"));
421 }
422
423 #[test]
424 fn test_error_classification() {
425 // Authentication errors (client errors, not temporary)
426 let auth_errors = [
427 NonceError::DuplicateNonce,
428 NonceError::InvalidSignature,
429 NonceError::TimestampOutOfWindow,
430 ];
431 for error in &auth_errors {
432 assert!(error.is_authentication_error());
433 assert!(error.is_client_error());
434 assert!(!error.is_server_error());
435 assert!(!error.is_temporary());
436 }
437
438 // System errors (server errors, temporary)
439 let system_errors = [
440 NonceError::from_storage_message("test"),
441 NonceError::CryptoError("test".to_string()),
442 ];
443 for error in &system_errors {
444 assert!(!error.is_authentication_error());
445 assert!(!error.is_client_error());
446 assert!(error.is_server_error());
447 assert!(error.is_temporary());
448 }
449 }
450}