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 `TURBOSQL_DB_PATH` environment variable:
87//!
88//! ```bash
89//! # Use a specific file
90//! export TURBOSQL_DB_PATH="/path/to/nonce_auth.db"
91//!
92//! # Use in-memory database (for testing)
93//! export TURBOSQL_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, 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("TURBOSQL_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("TURBOSQL_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("TURBOSQL_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("TURBOSQL_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("TURBOSQL_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}