nonce_auth/
lib.rs

1//! # Nonce Auth
2//!
3//! A Rust library for secure nonce-based authentication with pluggable storage backends.
4//!
5//! This library provides a complete solution for implementing nonce-based authentication
6//! in client-server applications. It uses HMAC-SHA256 signatures and pluggable storage
7//! backends for persistent 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//! - **Pluggable Storage**: Flexible storage backends (memory, database, Redis, etc.)
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, storage::MemoryStorage};
25//! use std::time::Duration;
26//! use std::sync::Arc;
27//! use hmac::Mac;
28//!
29//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
30//! // Create a storage backend
31//! let storage = Arc::new(MemoryStorage::new());
32//!
33//! // Create client and server with shared secret
34//! let secret = b"shared_secret_key";
35//! let client = NonceClient::new(secret);
36//! let server = NonceServer::new(secret, storage, None, None);
37//!
38//! // Initialize the storage backend
39//! server.init().await?;
40//!
41//! // Client generates authentication data with custom signature
42//! let protection_data = client.create_protection_data(|mac, timestamp, nonce| {
43//!     mac.update(timestamp.as_bytes());
44//!     mac.update(nonce.as_bytes());
45//! })?;
46//!
47//! // Server verifies the authentication data with matching signature algorithm
48//! match server.verify_protection_data(&protection_data, None, |mac| {
49//!     mac.update(protection_data.timestamp.to_string().as_bytes());
50//!     mac.update(protection_data.nonce.as_bytes());
51//! }).await {
52//!     Ok(()) => println!("Authentication verified successfully"),
53//!     Err(e) => println!("Verification failed: {e}"),
54//! }
55//! # Ok(())
56//! # }
57//! ```
58//!
59//! ### With Context Isolation
60//!
61//! ```rust
62//! use nonce_auth::{NonceClient, NonceServer, storage::MemoryStorage};
63//! use std::sync::Arc;
64//! use hmac::Mac;
65//!
66//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
67//! let storage = Arc::new(MemoryStorage::new());
68//! let client = NonceClient::new(b"secret");
69//! let server = NonceServer::new(b"secret", storage, None, None);
70//!
71//! let protection_data = client.create_protection_data(|mac, timestamp, nonce| {
72//!     mac.update(timestamp.as_bytes());
73//!     mac.update(nonce.as_bytes());
74//! })?;
75//!
76//! // Same nonce can be used in different contexts
77//! server.verify_protection_data(&protection_data, Some("api_v1"), |mac| {
78//!     mac.update(protection_data.timestamp.to_string().as_bytes());
79//!     mac.update(protection_data.nonce.as_bytes());
80//! }).await?;
81//! server.verify_protection_data(&protection_data, Some("api_v2"), |mac| {
82//!     mac.update(protection_data.timestamp.to_string().as_bytes());
83//!     mac.update(protection_data.nonce.as_bytes());
84//! }).await?;
85//! # Ok(())
86//! # }
87//! ```
88//!
89//! ## Storage Backends
90//!
91//! The library supports pluggable storage backends through the `NonceStorage` trait:
92//!
93//! ### In-Memory Storage (for testing/development)
94//!
95//! ```rust
96//! use nonce_auth::storage::MemoryStorage;
97//! use std::sync::Arc;
98//!
99//! let storage = Arc::new(MemoryStorage::new());
100//! ```
101//!
102//! ### Custom Storage Implementation
103//!
104//! ```rust
105//! use nonce_auth::storage::{NonceStorage, NonceEntry, StorageStats};
106//! use nonce_auth::NonceError;
107//! use async_trait::async_trait;
108//! use std::time::Duration;
109//!
110//! pub struct CustomStorage {
111//!     // Your storage implementation
112//! }
113//!
114//! #[async_trait]
115//! impl NonceStorage for CustomStorage {
116//!     async fn get(&self, nonce: &str, context: Option<&str>) -> Result<Option<NonceEntry>, NonceError> {
117//!         // Implementation
118//!         todo!()
119//!     }
120//!
121//!     async fn set(&self, nonce: &str, context: Option<&str>, ttl: Duration) -> Result<(), NonceError> {
122//!         // Implementation
123//!         todo!()
124//!     }
125//!
126//!     async fn exists(&self, nonce: &str, context: Option<&str>) -> Result<bool, NonceError> {
127//!         // Implementation
128//!         todo!()
129//!     }
130//!
131//!     async fn cleanup_expired(&self, cutoff_time: i64) -> Result<usize, NonceError> {
132//!         // Implementation
133//!         todo!()
134//!     }
135//!
136//!     async fn get_stats(&self) -> Result<StorageStats, NonceError> {
137//!         // Implementation
138//!         todo!()
139//!     }
140//! }
141//! ```
142//!
143//! ## Architecture
144//!
145//! The library is designed with clear separation between client and server responsibilities:
146//!
147//! - **[`NonceClient`]**: Lightweight client for generating signed requests
148//! - **[`NonceServer`]**: Server-side verification and nonce management with pluggable storage
149//! - **[`NonceStorage`]**: Abstract storage trait for implementing custom backends
150//! - **[`ProtectionData`]**: The data structure exchanged between client and server
151//! - **[`NonceError`]**: Comprehensive error handling for all failure modes
152
153use hmac::Hmac;
154use serde::{Deserialize, Serialize};
155use sha2::Sha256;
156
157pub mod nonce;
158pub mod storage {
159    //! Storage backend abstractions and implementations.
160    //!
161    //! This module provides the storage abstraction layer for nonce persistence.
162    //! It includes the `NonceStorage` trait and basic implementations.
163
164    pub use crate::nonce::storage::*;
165}
166
167// Re-export commonly used types
168pub use nonce::{NonceClient, NonceConfig, NonceError, NonceServer};
169
170/// Internal type alias for HMAC-SHA256 operations.
171type HmacSha256 = Hmac<Sha256>;
172
173/// Authentication data for nonce-based request verification.
174///
175/// This structure contains the cryptographic authentication information
176/// that is embedded within or sent alongside application requests. It is
177/// specifically designed for nonce-based authentication and replay attack
178/// prevention, not as a complete request structure.
179///
180/// # Purpose
181///
182/// `ProtectionData` represents only the authentication portion of a request:
183/// - It does not contain application payload or business logic data
184/// - It focuses solely on cryptographic verification and replay prevention
185/// - It can be embedded in larger request structures or sent as headers
186///
187/// # Fields
188///
189/// - `timestamp`: Unix timestamp (seconds since epoch) when the auth data was created
190/// - `nonce`: A unique identifier (typically UUID) that prevents request reuse
191/// - `signature`: HMAC-SHA256 signature that can include various data fields
192///
193/// # Serialization
194///
195/// This struct implements `Serialize` and `Deserialize` for easy JSON/binary
196/// serialization when sending authentication data over the network.
197///
198/// # Example
199///
200/// ```rust
201/// use nonce_auth::{NonceClient, ProtectionData};
202/// use hmac::Mac;
203///
204/// let client = NonceClient::new(b"secret");
205/// let protection_data: ProtectionData = client.create_protection_data(|mac, timestamp, nonce| {
206///     mac.update(timestamp.as_bytes());
207///     mac.update(nonce.as_bytes());
208/// }).unwrap();
209///
210/// // Embed in a larger request structure
211/// #[derive(serde::Serialize)]
212/// struct ApiRequest {
213///     payload: String,
214///     auth: ProtectionData,
215/// }
216///
217/// let request = ApiRequest {
218///     payload: "application data".to_string(),
219///     auth: protection_data,
220/// };
221/// ```
222///
223/// # Security Notes
224///
225/// - The timestamp prevents very old authentication attempts from being replayed
226/// - The nonce ensures each authentication attempt is unique and can only be used once
227/// - The signature proves the authentication data hasn't been tampered with
228/// - The signature algorithm is flexible and can include additional request data
229#[derive(Debug, Clone, Serialize, Deserialize)]
230pub struct ProtectionData {
231    /// Unix timestamp (seconds since epoch) when this authentication data was created.
232    ///
233    /// Used by the server to validate that the authentication attempt is within the
234    /// acceptable time window and not too old.
235    pub timestamp: u64,
236
237    /// A unique nonce value, typically a UUID string.
238    ///
239    /// This value must be unique and is used to prevent the same
240    /// authentication data from being processed multiple times.
241    pub nonce: String,
242
243    /// HMAC-SHA256 signature that can include various data fields.
244    ///
245    /// The signature algorithm is flexible and can be customized to include
246    /// timestamp, nonce, payload, HTTP method, path, or any other relevant data.
247    /// This proves that the authentication data was created by someone who knows
248    /// the shared secret and that the included data hasn't been tampered with.
249    pub signature: String,
250}
251
252#[cfg(test)]
253mod tests {
254    use crate::nonce::storage::MemoryStorage;
255    use crate::nonce::{NonceClient, NonceError, NonceServer};
256    use hmac::Mac;
257    use std::sync::Arc;
258
259    #[tokio::test]
260    async fn test_client_server_separation() {
261        let storage = Arc::new(MemoryStorage::new());
262        let client = NonceClient::new(b"test_secret");
263        let server = NonceServer::new(b"test_secret", storage, None, None);
264
265        server.init().await.unwrap();
266
267        // Client creates protection data
268        let protection_data = client
269            .create_protection_data(|mac, timestamp, nonce| {
270                mac.update(timestamp.as_bytes());
271                mac.update(nonce.as_bytes());
272            })
273            .unwrap();
274
275        // Server verifies protection data
276        let result = server
277            .verify_protection_data(&protection_data, None, |mac| {
278                mac.update(protection_data.timestamp.to_string().as_bytes());
279                mac.update(protection_data.nonce.as_bytes());
280            })
281            .await;
282
283        assert!(result.is_ok());
284
285        // Same nonce should be rejected
286        let result = server
287            .verify_protection_data(&protection_data, None, |mac| {
288                mac.update(protection_data.timestamp.to_string().as_bytes());
289                mac.update(protection_data.nonce.as_bytes());
290            })
291            .await;
292
293        assert!(matches!(result, Err(NonceError::DuplicateNonce)));
294    }
295
296    #[tokio::test]
297    async fn test_context_isolation() {
298        let storage = Arc::new(MemoryStorage::new());
299        let client = NonceClient::new(b"test_secret");
300        let server = NonceServer::new(b"test_secret", storage, None, None);
301
302        server.init().await.unwrap();
303
304        let protection_data = client
305            .create_protection_data(|mac, timestamp, nonce| {
306                mac.update(timestamp.as_bytes());
307                mac.update(nonce.as_bytes());
308            })
309            .unwrap();
310
311        // Same nonce should work in different contexts
312        server
313            .verify_protection_data(&protection_data, Some("context1"), |mac| {
314                mac.update(protection_data.timestamp.to_string().as_bytes());
315                mac.update(protection_data.nonce.as_bytes());
316            })
317            .await
318            .unwrap();
319
320        server
321            .verify_protection_data(&protection_data, Some("context2"), |mac| {
322                mac.update(protection_data.timestamp.to_string().as_bytes());
323                mac.update(protection_data.nonce.as_bytes());
324            })
325            .await
326            .unwrap();
327
328        // But should fail if used twice in same context
329        let result = server
330            .verify_protection_data(&protection_data, Some("context1"), |mac| {
331                mac.update(protection_data.timestamp.to_string().as_bytes());
332                mac.update(protection_data.nonce.as_bytes());
333            })
334            .await;
335
336        assert!(matches!(result, Err(NonceError::DuplicateNonce)));
337    }
338
339    #[tokio::test]
340    async fn test_timestamp_validation() {
341        let storage = Arc::new(MemoryStorage::new());
342        let client = NonceClient::new(b"test_secret");
343        let server = NonceServer::new(
344            b"test_secret",
345            storage,
346            None,
347            Some(std::time::Duration::from_secs(1)),
348        );
349
350        server.init().await.unwrap();
351
352        // Create protection data with old timestamp
353        let mut protection_data = client
354            .create_protection_data(|mac, timestamp, nonce| {
355                mac.update(timestamp.as_bytes());
356                mac.update(nonce.as_bytes());
357            })
358            .unwrap();
359
360        // Simulate old timestamp (2 seconds ago, but time window is 1 second)
361        protection_data.timestamp = std::time::SystemTime::now()
362            .duration_since(std::time::UNIX_EPOCH)
363            .unwrap()
364            .as_secs()
365            - 2;
366
367        let result = server
368            .verify_protection_data(&protection_data, None, |mac| {
369                mac.update(protection_data.timestamp.to_string().as_bytes());
370                mac.update(protection_data.nonce.as_bytes());
371            })
372            .await;
373
374        assert!(matches!(result, Err(NonceError::TimestampOutOfWindow)));
375    }
376
377    #[tokio::test]
378    async fn test_signature_verification() {
379        let storage = Arc::new(MemoryStorage::new());
380        let client = NonceClient::new(b"test_secret");
381        let server = NonceServer::new(b"different_secret", storage, None, None);
382
383        server.init().await.unwrap();
384
385        let protection_data = client
386            .create_protection_data(|mac, timestamp, nonce| {
387                mac.update(timestamp.as_bytes());
388                mac.update(nonce.as_bytes());
389            })
390            .unwrap();
391
392        let result = server
393            .verify_protection_data(&protection_data, None, |mac| {
394                mac.update(protection_data.timestamp.to_string().as_bytes());
395                mac.update(protection_data.nonce.as_bytes());
396            })
397            .await;
398
399        assert!(matches!(result, Err(NonceError::InvalidSignature)));
400    }
401
402    #[tokio::test]
403    async fn test_serialization() {
404        let client = NonceClient::new(b"test_secret");
405        let protection_data = client
406            .create_protection_data(|mac, timestamp, nonce| {
407                mac.update(timestamp.as_bytes());
408                mac.update(nonce.as_bytes());
409            })
410            .unwrap();
411
412        // Test JSON serialization
413        let json = serde_json::to_string(&protection_data).unwrap();
414        let deserialized: super::ProtectionData = serde_json::from_str(&json).unwrap();
415
416        assert_eq!(protection_data.timestamp, deserialized.timestamp);
417        assert_eq!(protection_data.nonce, deserialized.nonce);
418        assert_eq!(protection_data.signature, deserialized.signature);
419    }
420}