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}