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}