nonce_auth/
lib.rs

1//! # Nonce Auth
2//!
3//! A Rust library for secure nonce-based authentication that prevents replay attacks.
4//!
5//! This library provides a complete solution for implementing nonce-based authentication
6//! in client-server applications. It uses HMAC-SHA256 signatures and SQLite for persistent
7//! nonce storage to ensure that each request can only be used once.
8//!
9//! ## Features
10//!
11//! - **HMAC-SHA256 Signing**: Cryptographic signing of requests using shared secrets
12//! - **Replay Attack Prevention**: Each nonce can only be used once
13//! - **Time Window Validation**: Requests outside the time window are rejected
14//! - **Context Isolation**: Nonces can be scoped to different business contexts
15//! - **SQLite Persistence**: Automatic nonce storage and cleanup
16//! - **Async Support**: Fully asynchronous API design
17//! - **Client-Server Separation**: Clean separation of client and server responsibilities
18//!
19//! ## Quick Start
20//!
21//! ### Basic Usage
22//!
23//! ```rust
24//! use nonce_auth::{NonceClient, NonceServer};
25//! use std::time::Duration;
26//! use hmac::Mac;
27//!
28//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
29//! // Initialize the database
30//! NonceServer::init().await?;
31//!
32//! // Create client and server with shared secret
33//! let secret = b"shared_secret_key";
34//! let client = NonceClient::new(secret);
35//! let server = NonceServer::new(secret, None, None);
36//!
37//! // Client generates authentication data with custom signature
38//! let protection_data = client.create_protection_data(|mac, timestamp, nonce| {
39//!     mac.update(timestamp.as_bytes());
40//!     mac.update(nonce.as_bytes());
41//! })?;
42//!
43//! // Server verifies the authentication data with matching signature algorithm
44//! match server.verify_protection_data(&protection_data, None, |mac| {
45//!     mac.update(protection_data.timestamp.to_string().as_bytes());
46//!     mac.update(protection_data.nonce.as_bytes());
47//! }).await {
48//!     Ok(()) => println!("Authentication verified successfully"),
49//!     Err(e) => println!("Verification failed: {e}"),
50//! }
51//! # Ok(())
52//! # }
53//! ```
54//!
55//! ### With Context Isolation
56//!
57//! ```rust
58//! use nonce_auth::{NonceClient, NonceServer};
59//! use hmac::Mac;
60//!
61//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
62//! # NonceServer::init().await?;
63//! let client = NonceClient::new(b"secret");
64//! let server = NonceServer::new(b"secret", None, None);
65//!
66//! let protection_data = client.create_protection_data(|mac, timestamp, nonce| {
67//!     mac.update(timestamp.as_bytes());
68//!     mac.update(nonce.as_bytes());
69//! })?;
70//!
71//! // Same nonce can be used in different contexts
72//! server.verify_protection_data(&protection_data, Some("api_v1"), |mac| {
73//!     mac.update(protection_data.timestamp.to_string().as_bytes());
74//!     mac.update(protection_data.nonce.as_bytes());
75//! }).await?;
76//! server.verify_protection_data(&protection_data, Some("api_v2"), |mac| {
77//!     mac.update(protection_data.timestamp.to_string().as_bytes());
78//!     mac.update(protection_data.nonce.as_bytes());
79//! }).await?;
80//! # Ok(())
81//! # }
82//! ```
83//!
84//! ## Database Configuration
85//!
86//! The SQLite database location can be configured using the `NONCE_AUTH_DB_PATH` environment variable:
87//!
88//! ```bash
89//! # Use a specific file
90//! export NONCE_AUTH_DB_PATH="/path/to/nonce_auth.db"
91//!
92//! # Use in-memory database (for testing)
93//! export NONCE_AUTH_DB_PATH=":memory:"
94//! ```
95//!
96//! If not set, it defaults to `nonce_auth.db` in the current directory.
97//!
98//! ## Architecture
99//!
100//! The library is designed with clear separation between client and server responsibilities:
101//!
102//! - **[`NonceClient`]**: Lightweight client for generating signed requests
103//! - **[`NonceServer`]**: Server-side verification and nonce management
104//! - **[`ProtectionData`]**: The data structure exchanged between client and server
105//! - **[`NonceError`]**: Comprehensive error handling for all failure modes
106
107use hmac::Hmac;
108use serde::{Deserialize, Serialize};
109use sha2::Sha256;
110
111pub mod nonce;
112
113// Re-export commonly used types
114pub use nonce::{NonceClient, NonceConfig, NonceError, NonceServer};
115
116/// Internal type alias for HMAC-SHA256 operations.
117type HmacSha256 = Hmac<Sha256>;
118
119/// Authentication data for nonce-based request verification.
120///
121/// This structure contains the cryptographic authentication information
122/// that is embedded within or sent alongside application requests. It is
123/// specifically designed for nonce-based authentication and replay attack
124/// prevention, not as a complete request structure.
125///
126/// # Purpose
127///
128/// `ProtectionData` represents only the authentication portion of a request:
129/// - It does not contain application payload or business logic data
130/// - It focuses solely on cryptographic verification and replay prevention
131/// - It can be embedded in larger request structures or sent as headers
132///
133/// # Fields
134///
135/// - `timestamp`: Unix timestamp (seconds since epoch) when the auth data was created
136/// - `nonce`: A unique identifier (typically UUID) that prevents request reuse
137/// - `signature`: HMAC-SHA256 signature that can include various data fields
138///
139/// # Serialization
140///
141/// This struct implements `Serialize` and `Deserialize` for easy JSON/binary
142/// serialization when sending authentication data over the network.
143///
144/// # Example
145///
146/// ```rust
147/// use nonce_auth::{NonceClient, ProtectionData};
148/// use hmac::Mac;
149///
150/// let client = NonceClient::new(b"secret");
151/// let protection_data: ProtectionData = client.create_protection_data(|mac, timestamp, nonce| {
152///     mac.update(timestamp.as_bytes());
153///     mac.update(nonce.as_bytes());
154/// }).unwrap();
155///
156/// // Embed in a larger request structure
157/// #[derive(serde::Serialize)]
158/// struct ApiRequest {
159///     payload: String,
160///     auth: ProtectionData,
161/// }
162///
163/// let request = ApiRequest {
164///     payload: "application data".to_string(),
165///     auth: protection_data,
166/// };
167/// ```
168///
169/// # Security Notes
170///
171/// - The timestamp prevents very old authentication attempts from being replayed
172/// - The nonce ensures each authentication attempt is unique and can only be used once
173/// - The signature proves the authentication data hasn't been tampered with
174/// - The signature algorithm is flexible and can include additional request data
175#[derive(Debug, Clone, Serialize, Deserialize)]
176pub struct ProtectionData {
177    /// Unix timestamp (seconds since epoch) when this authentication data was created.
178    ///
179    /// Used by the server to validate that the authentication attempt is within the
180    /// acceptable time window and not too old.
181    pub timestamp: u64,
182
183    /// A unique nonce value, typically a UUID string.
184    ///
185    /// This value must be unique and is used to prevent the same
186    /// authentication data from being processed multiple times.
187    pub nonce: String,
188
189    /// HMAC-SHA256 signature that can include various data fields.
190    ///
191    /// The signature algorithm is flexible and can be customized to include
192    /// timestamp, nonce, payload, HTTP method, path, or any other relevant data.
193    /// This proves that the authentication data was created by someone who knows
194    /// the shared secret and that the included data hasn't been tampered with.
195    pub signature: String,
196}
197
198#[cfg(test)]
199mod tests {
200    use crate::nonce::{NonceClient, NonceError, NonceServer};
201    use hmac::Mac;
202    use std::time::{Duration, SystemTime, UNIX_EPOCH};
203
204    const TEST_SECRET: &[u8] = b"test_secret_key_123";
205
206    #[tokio::test]
207    async fn test_client_server_separation() {
208        // Initialize database
209        unsafe {
210            std::env::set_var("NONCE_AUTH_DB_PATH", ":memory:");
211        }
212        NonceServer::init().await.unwrap();
213
214        let client = NonceClient::new(TEST_SECRET);
215        let server = NonceServer::new(
216            TEST_SECRET,
217            Some(Duration::from_secs(300)), // 5 min TTL
218            Some(Duration::from_secs(300)), // 5 min time window
219        );
220
221        // Client creates protection data with custom signature
222        let protection_data = client
223            .create_protection_data(|mac, timestamp, nonce| {
224                mac.update(timestamp.as_bytes());
225                mac.update(nonce.as_bytes());
226            })
227            .unwrap();
228
229        // Server verifies the protection data with matching signature algorithm
230        assert!(
231            server
232                .verify_protection_data(&protection_data, None, |mac| {
233                    mac.update(protection_data.timestamp.to_string().as_bytes());
234                    mac.update(protection_data.nonce.as_bytes());
235                })
236                .await
237                .is_ok()
238        );
239
240        // Test duplicate protection data detection
241        assert!(matches!(
242            server
243                .verify_protection_data(&protection_data, None, |mac| {
244                    mac.update(protection_data.timestamp.to_string().as_bytes());
245                    mac.update(protection_data.nonce.as_bytes());
246                })
247                .await,
248            Err(NonceError::DuplicateNonce)
249        ));
250
251        // Test invalid signature
252        let mut bad_protection_data = client
253            .create_protection_data(|mac, timestamp, nonce| {
254                mac.update(timestamp.as_bytes());
255                mac.update(nonce.as_bytes());
256            })
257            .unwrap();
258        bad_protection_data.signature = "invalid_signature".to_string();
259
260        assert!(matches!(
261            server
262                .verify_protection_data(&bad_protection_data, None, |mac| {
263                    mac.update(bad_protection_data.timestamp.to_string().as_bytes());
264                    mac.update(bad_protection_data.nonce.as_bytes());
265                })
266                .await,
267            Err(NonceError::InvalidSignature)
268        ));
269    }
270
271    #[tokio::test]
272    async fn test_context_isolation() {
273        unsafe {
274            std::env::set_var("NONCE_AUTH_DB_PATH", ":memory:");
275        }
276        NonceServer::init().await.unwrap();
277
278        let client = NonceClient::new(TEST_SECRET);
279        let server = NonceServer::new(TEST_SECRET, None, None);
280
281        // Create one protection data to test context isolation
282        let protection_data = client
283            .create_protection_data(|mac, timestamp, nonce| {
284                mac.update(timestamp.as_bytes());
285                mac.update(nonce.as_bytes());
286            })
287            .unwrap();
288
289        // Same nonce can be used in different contexts
290        assert!(
291            server
292                .verify_protection_data(&protection_data, Some("context1"), |mac| {
293                    mac.update(protection_data.timestamp.to_string().as_bytes());
294                    mac.update(protection_data.nonce.as_bytes());
295                })
296                .await
297                .is_ok()
298        );
299        assert!(
300            server
301                .verify_protection_data(&protection_data, Some("context2"), |mac| {
302                    mac.update(protection_data.timestamp.to_string().as_bytes());
303                    mac.update(protection_data.nonce.as_bytes());
304                })
305                .await
306                .is_ok()
307        );
308        assert!(
309            server
310                .verify_protection_data(&protection_data, Some("context3"), |mac| {
311                    mac.update(protection_data.timestamp.to_string().as_bytes());
312                    mac.update(protection_data.nonce.as_bytes());
313                })
314                .await
315                .is_ok()
316        );
317
318        // But cannot be reused in the same context
319        let protection_data_copy = protection_data.clone();
320        assert!(matches!(
321            server
322                .verify_protection_data(&protection_data_copy, Some("context1"), |mac| {
323                    mac.update(protection_data_copy.timestamp.to_string().as_bytes());
324                    mac.update(protection_data_copy.nonce.as_bytes());
325                })
326                .await,
327            Err(NonceError::DuplicateNonce)
328        ));
329
330        // Test with no context (NULL context)
331        assert!(
332            server
333                .verify_protection_data(&protection_data, None, |mac| {
334                    mac.update(protection_data.timestamp.to_string().as_bytes());
335                    mac.update(protection_data.nonce.as_bytes());
336                })
337                .await
338                .is_ok()
339        );
340
341        // Cannot reuse with no context
342        let protection_data_copy2 = protection_data.clone();
343        assert!(matches!(
344            server
345                .verify_protection_data(&protection_data_copy2, None, |mac| {
346                    mac.update(protection_data_copy2.timestamp.to_string().as_bytes());
347                    mac.update(protection_data_copy2.nonce.as_bytes());
348                })
349                .await,
350            Err(NonceError::DuplicateNonce)
351        ));
352    }
353
354    #[tokio::test]
355    async fn test_timestamp_validation() {
356        unsafe {
357            std::env::set_var("NONCE_AUTH_DB_PATH", ":memory:");
358        }
359        NonceServer::init().await.unwrap();
360
361        let client = NonceClient::new(TEST_SECRET);
362        let server = NonceServer::new(
363            TEST_SECRET,
364            Some(Duration::from_secs(300)),
365            Some(Duration::from_secs(60)), // 1 minute window
366        );
367
368        // Create protection data with old timestamp
369        let old_timestamp = SystemTime::now()
370            .duration_since(UNIX_EPOCH)
371            .unwrap()
372            .as_secs()
373            .saturating_sub(3600); // 1 hour ago
374
375        let nonce = uuid::Uuid::new_v4().to_string();
376
377        // Create signature with old timestamp
378        let signature = client
379            .generate_signature(|mac| {
380                mac.update(old_timestamp.to_string().as_bytes());
381                mac.update(nonce.as_bytes());
382            })
383            .unwrap();
384
385        let old_protection_data = crate::ProtectionData {
386            timestamp: old_timestamp,
387            nonce,
388            signature,
389        };
390
391        assert!(matches!(
392            server
393                .verify_protection_data(&old_protection_data, None, |mac| {
394                    mac.update(old_protection_data.timestamp.to_string().as_bytes());
395                    mac.update(old_protection_data.nonce.as_bytes());
396                })
397                .await,
398            Err(NonceError::TimestampOutOfWindow)
399        ));
400    }
401
402    #[tokio::test]
403    async fn test_server_default_values() {
404        let server = NonceServer::new(TEST_SECRET, None, None);
405
406        // Test default values
407        assert_eq!(server.ttl(), Duration::from_secs(300)); // 5 minutes
408        assert_eq!(server.time_window(), Duration::from_secs(60)); // 1 minute
409    }
410
411    #[tokio::test]
412    async fn test_server_custom_values() {
413        let custom_ttl = Duration::from_secs(600);
414        let custom_window = Duration::from_secs(120);
415
416        let server = NonceServer::new(TEST_SECRET, Some(custom_ttl), Some(custom_window));
417
418        assert_eq!(server.ttl(), custom_ttl);
419        assert_eq!(server.time_window(), custom_window);
420    }
421
422    #[tokio::test]
423    async fn test_protection_data_expiration() {
424        unsafe {
425            std::env::set_var("NONCE_AUTH_DB_PATH", ":memory:");
426        }
427        NonceServer::init().await.unwrap();
428
429        let client = NonceClient::new(TEST_SECRET);
430        let server = NonceServer::new(
431            TEST_SECRET,
432            Some(Duration::from_millis(100)), // Very short TTL
433            Some(Duration::from_secs(300)),
434        );
435
436        let protection_data = client
437            .create_protection_data(|mac, timestamp, nonce| {
438                mac.update(timestamp.as_bytes());
439                mac.update(nonce.as_bytes());
440            })
441            .unwrap();
442
443        // First verification should succeed
444        assert!(
445            server
446                .verify_protection_data(&protection_data, None, |mac| {
447                    mac.update(protection_data.timestamp.to_string().as_bytes());
448                    mac.update(protection_data.nonce.as_bytes());
449                })
450                .await
451                .is_ok()
452        );
453
454        // Second verification with same nonce should fail (already consumed)
455        let duplicate_protection_data = protection_data.clone();
456        assert!(matches!(
457            server
458                .verify_protection_data(&duplicate_protection_data, None, |mac| {
459                    mac.update(duplicate_protection_data.timestamp.to_string().as_bytes());
460                    mac.update(duplicate_protection_data.nonce.as_bytes());
461                })
462                .await,
463            Err(NonceError::DuplicateNonce)
464        ));
465    }
466
467    #[tokio::test]
468    async fn test_cleanup_expired() {
469        unsafe {
470            std::env::set_var("NONCE_AUTH_DB_PATH", ":memory:");
471        }
472        NonceServer::init().await.unwrap();
473
474        // Test cleanup function
475        let result = NonceServer::cleanup_expired_nonces(Duration::from_secs(300)).await;
476        assert!(result.is_ok());
477    }
478
479    #[tokio::test]
480    async fn test_signature_verification() {
481        let client = NonceClient::new(TEST_SECRET);
482
483        // Test direct signature creation and verification
484        let timestamp = "1234567890";
485        let nonce = "test-nonce";
486
487        let signature = client
488            .generate_signature(|mac| {
489                mac.update(timestamp.as_bytes());
490                mac.update(nonce.as_bytes());
491            })
492            .unwrap();
493        assert!(!signature.is_empty());
494
495        // Test with different secret should produce different signature
496        let client2 = NonceClient::new(b"different_secret");
497        let signature2 = client2
498            .generate_signature(|mac| {
499                mac.update(timestamp.as_bytes());
500                mac.update(nonce.as_bytes());
501            })
502            .unwrap();
503        assert_ne!(signature, signature2);
504    }
505
506    #[tokio::test]
507    async fn test_serialization() {
508        let client = NonceClient::new(TEST_SECRET);
509        let protection_data = client
510            .create_protection_data(|mac, timestamp, nonce| {
511                mac.update(timestamp.as_bytes());
512                mac.update(nonce.as_bytes());
513            })
514            .unwrap();
515
516        // Test JSON serialization
517        let json = serde_json::to_string(&protection_data).unwrap();
518        assert!(!json.is_empty());
519
520        // Test deserialization
521        let deserialized: crate::ProtectionData = serde_json::from_str(&json).unwrap();
522        assert_eq!(protection_data.timestamp, deserialized.timestamp);
523        assert_eq!(protection_data.nonce, deserialized.nonce);
524        assert_eq!(protection_data.signature, deserialized.signature);
525    }
526}