nonce_auth/nonce/client.rs
1use hmac::Mac;
2use std::time::{SystemTime, UNIX_EPOCH};
3
4use super::NonceError;
5use crate::HmacSha256;
6use crate::ProtectionData;
7
8/// Client-side nonce manager for generating signed requests.
9///
10/// The `NonceClient` is responsible for creating cryptographically signed
11/// requests that can be verified by a `NonceServer`. It provides a lightweight,
12/// stateless interface for generating nonces and signatures without requiring
13/// any database or persistent storage.
14///
15/// # Security Features
16///
17/// - **HMAC-SHA256 Signing**: Uses industry-standard HMAC with SHA256 for signatures
18/// - **UUID Nonces**: Generates cryptographically random UUIDs for nonces
19/// - **Timestamp Inclusion**: Includes current timestamp to prevent old request replay
20/// - **Stateless Design**: No local state or storage required
21/// - **Fully Customizable**: All signature algorithms are defined by the application
22///
23/// # Usage Pattern
24///
25/// The typical usage pattern is:
26/// 1. Create a client with a shared secret
27/// 2. Generate signed requests with custom signature algorithms
28/// 3. Send the signed request to the server for verification
29///
30/// # Example
31///
32/// ```rust
33/// use nonce_auth::NonceClient;
34/// use hmac::Mac;
35///
36/// // Create a client with a shared secret
37/// let client = NonceClient::new(b"my_shared_secret");
38///
39/// // Generate a signed request with custom signature
40/// let protection_data = client.create_protection_data(|mac, timestamp, nonce| {
41/// mac.update(timestamp.as_bytes());
42/// mac.update(nonce.as_bytes());
43/// mac.update(b"custom_payload");
44/// }).unwrap();
45/// ```
46///
47/// # Thread Safety
48///
49/// `NonceClient` is thread-safe and can be shared across multiple threads
50/// or used concurrently to generate multiple signed requests.
51pub struct NonceClient {
52 /// The secret key used for HMAC signature generation.
53 /// This should be the same secret used by the corresponding `NonceServer`.
54 secret: Vec<u8>,
55}
56
57impl NonceClient {
58 /// Creates a new `NonceClient` with the specified secret key.
59 ///
60 /// The secret key should be shared between the client and server
61 /// and kept confidential. It's used to generate HMAC signatures
62 /// that prove the authenticity of requests.
63 ///
64 /// # Arguments
65 ///
66 /// * `secret` - The secret key for HMAC signature generation.
67 /// This should match the secret used by the server.
68 ///
69 /// # Returns
70 ///
71 /// A new `NonceClient` instance ready to generate signed requests.
72 ///
73 /// # Security Considerations
74 ///
75 /// - Use a cryptographically strong secret key (at least 32 bytes recommended)
76 /// - Keep the secret key confidential and secure
77 /// - Use the same secret key on both client and server
78 /// - Consider rotating secret keys periodically
79 ///
80 /// # Example
81 ///
82 /// ```rust
83 /// use nonce_auth::NonceClient;
84 ///
85 /// // Create with a strong secret key
86 /// let secret = b"my_very_secure_secret_key_32_bytes";
87 /// let client = NonceClient::new(secret);
88 ///
89 /// // Or use a dynamically generated secret
90 /// let dynamic_secret = "generated_secret_from_key_exchange".as_bytes();
91 /// let client = NonceClient::new(dynamic_secret);
92 /// ```
93 pub fn new(secret: &[u8]) -> Self {
94 Self {
95 secret: secret.to_vec(),
96 }
97 }
98
99 /// Generates protection data with custom signature algorithm.
100 ///
101 /// This method provides complete flexibility to create protection data with
102 /// any signature algorithm. The signature algorithm is defined by the closure
103 /// which receives the MAC instance, timestamp, and nonce.
104 ///
105 /// # Arguments
106 ///
107 /// * `signature_builder` - A closure that defines how to build the signature data
108 ///
109 /// # Returns
110 ///
111 /// * `Ok(ProtectionData)` - Authentication data with custom signature
112 /// * `Err(NonceError)` - If there's an error in the cryptographic operations
113 ///
114 /// # Example
115 ///
116 /// ```rust
117 /// use nonce_auth::NonceClient;
118 /// use hmac::Mac;
119 ///
120 /// let client = NonceClient::new(b"shared_secret");
121 /// let payload = "request body";
122 ///
123 /// // Create protection data with payload included in signature
124 /// let protection_data = client.create_protection_data(|mac, timestamp, nonce| {
125 /// mac.update(timestamp.as_bytes());
126 /// mac.update(nonce.as_bytes());
127 /// mac.update(payload.as_bytes());
128 /// }).unwrap();
129 /// ```
130 pub fn create_protection_data<F>(
131 &self,
132 signature_builder: F,
133 ) -> Result<ProtectionData, NonceError>
134 where
135 F: FnOnce(&mut hmac::Hmac<sha2::Sha256>, &str, &str),
136 {
137 let timestamp = SystemTime::now()
138 .duration_since(UNIX_EPOCH)
139 .unwrap()
140 .as_secs();
141
142 let nonce = uuid::Uuid::new_v4().to_string();
143
144 let signature = self.generate_signature(|mac| {
145 signature_builder(mac, ×tamp.to_string(), &nonce);
146 })?;
147
148 Ok(ProtectionData {
149 timestamp,
150 nonce,
151 signature,
152 })
153 }
154
155 /// Generates an HMAC-SHA256 signature with custom data builder.
156 ///
157 /// This method provides maximum flexibility for signature generation by
158 /// allowing applications to define exactly what data should be included
159 /// in the signature through a closure.
160 ///
161 /// # Arguments
162 ///
163 /// * `data_builder` - A closure that adds data to the HMAC instance
164 ///
165 /// # Returns
166 ///
167 /// * `Ok(String)` - The hex-encoded HMAC signature
168 /// * `Err(NonceError)` - If there's an error in the cryptographic operations
169 ///
170 /// # Example
171 ///
172 /// ```rust
173 /// use nonce_auth::NonceClient;
174 /// use hmac::Mac;
175 ///
176 /// let client = NonceClient::new(b"shared_secret");
177 ///
178 /// // Generate signature with custom data
179 /// let signature = client.generate_signature(|mac| {
180 /// mac.update(b"timestamp");
181 /// mac.update(b"nonce");
182 /// mac.update(b"payload");
183 /// mac.update(b"method");
184 /// }).unwrap();
185 /// ```
186 pub fn generate_signature<F>(&self, data_builder: F) -> Result<String, NonceError>
187 where
188 F: FnOnce(&mut hmac::Hmac<sha2::Sha256>),
189 {
190 let mut mac = HmacSha256::new_from_slice(&self.secret)
191 .map_err(|e| NonceError::CryptoError(e.to_string()))?;
192
193 data_builder(&mut mac);
194
195 let result = mac.finalize();
196 let signature = hex::encode(result.into_bytes());
197 Ok(signature)
198 }
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204
205 const TEST_SECRET: &[u8] = b"test_secret_key_123";
206
207 #[test]
208 fn test_client_creation() {
209 let client = NonceClient::new(TEST_SECRET);
210 assert_eq!(client.secret, TEST_SECRET);
211 }
212
213 #[test]
214 fn test_create_protection_data_with_custom_signature() {
215 let client = NonceClient::new(TEST_SECRET);
216 let payload = "test payload";
217
218 let protection_data = client
219 .create_protection_data(|mac, timestamp, nonce| {
220 mac.update(timestamp.as_bytes());
221 mac.update(nonce.as_bytes());
222 mac.update(payload.as_bytes());
223 })
224 .unwrap();
225
226 assert!(protection_data.timestamp > 0);
227 assert!(!protection_data.nonce.is_empty());
228 assert!(!protection_data.signature.is_empty());
229 assert_eq!(protection_data.signature.len(), 64);
230
231 // Verify the signature includes the payload
232 let expected_signature = client
233 .generate_signature(|mac| {
234 mac.update(protection_data.timestamp.to_string().as_bytes());
235 mac.update(protection_data.nonce.as_bytes());
236 mac.update(payload.as_bytes());
237 })
238 .unwrap();
239 assert_eq!(protection_data.signature, expected_signature);
240 }
241
242 #[test]
243 fn test_multiple_protection_data_different_nonces() {
244 let client = NonceClient::new(TEST_SECRET);
245
246 let protection_data1 = client
247 .create_protection_data(|mac, timestamp, nonce| {
248 mac.update(timestamp.as_bytes());
249 mac.update(nonce.as_bytes());
250 })
251 .unwrap();
252
253 let protection_data2 = client
254 .create_protection_data(|mac, timestamp, nonce| {
255 mac.update(timestamp.as_bytes());
256 mac.update(nonce.as_bytes());
257 })
258 .unwrap();
259
260 assert_ne!(protection_data1.nonce, protection_data2.nonce);
261 assert_ne!(protection_data1.signature, protection_data2.signature);
262 }
263}