nonce_auth/nonce/client.rs
1use hmac::Mac;
2use std::time::{SystemTime, UNIX_EPOCH};
3
4use super::NonceError;
5use crate::AuthData;
6use crate::HmacSha256;
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 auth_data = client.create_auth_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 authentication data with custom signature algorithm.
100 ///
101 /// This method provides complete flexibility to create authentication 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(AuthData)` - 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 auth data with payload included in signature
124 /// let auth_data = client.create_auth_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_auth_data<F>(&self, signature_builder: F) -> Result<AuthData, NonceError>
131 where
132 F: FnOnce(&mut hmac::Hmac<sha2::Sha256>, &str, &str),
133 {
134 let timestamp = SystemTime::now()
135 .duration_since(UNIX_EPOCH)
136 .unwrap()
137 .as_secs();
138
139 let nonce = uuid::Uuid::new_v4().to_string();
140
141 let signature = self.generate_signature(|mac| {
142 signature_builder(mac, ×tamp.to_string(), &nonce);
143 })?;
144
145 Ok(AuthData {
146 timestamp,
147 nonce,
148 signature,
149 })
150 }
151
152 /// Generates an HMAC-SHA256 signature with custom data builder.
153 ///
154 /// This method provides maximum flexibility for signature generation by
155 /// allowing applications to define exactly what data should be included
156 /// in the signature through a closure.
157 ///
158 /// # Arguments
159 ///
160 /// * `data_builder` - A closure that adds data to the HMAC instance
161 ///
162 /// # Returns
163 ///
164 /// * `Ok(String)` - The hex-encoded HMAC signature
165 /// * `Err(NonceError)` - If there's an error in the cryptographic operations
166 ///
167 /// # Example
168 ///
169 /// ```rust
170 /// use nonce_auth::NonceClient;
171 /// use hmac::Mac;
172 ///
173 /// let client = NonceClient::new(b"shared_secret");
174 ///
175 /// // Generate signature with custom data
176 /// let signature = client.generate_signature(|mac| {
177 /// mac.update(b"timestamp");
178 /// mac.update(b"nonce");
179 /// mac.update(b"payload");
180 /// mac.update(b"method");
181 /// }).unwrap();
182 /// ```
183 pub fn generate_signature<F>(&self, data_builder: F) -> Result<String, NonceError>
184 where
185 F: FnOnce(&mut hmac::Hmac<sha2::Sha256>),
186 {
187 let mut mac = HmacSha256::new_from_slice(&self.secret)
188 .map_err(|e| NonceError::CryptoError(e.to_string()))?;
189
190 data_builder(&mut mac);
191
192 let result = mac.finalize();
193 let signature = hex::encode(result.into_bytes());
194 Ok(signature)
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201
202 const TEST_SECRET: &[u8] = b"test_secret_key_123";
203
204 #[test]
205 fn test_client_creation() {
206 let client = NonceClient::new(TEST_SECRET);
207 assert_eq!(client.secret, TEST_SECRET);
208 }
209
210 #[test]
211 fn test_create_auth_data_with_custom_signature() {
212 let client = NonceClient::new(TEST_SECRET);
213 let payload = "test payload";
214
215 let auth_data = client
216 .create_auth_data(|mac, timestamp, nonce| {
217 mac.update(timestamp.as_bytes());
218 mac.update(nonce.as_bytes());
219 mac.update(payload.as_bytes());
220 })
221 .unwrap();
222
223 assert!(auth_data.timestamp > 0);
224 assert!(!auth_data.nonce.is_empty());
225 assert!(!auth_data.signature.is_empty());
226 assert_eq!(auth_data.signature.len(), 64);
227
228 // Verify the signature includes the payload
229 let expected_signature = client
230 .generate_signature(|mac| {
231 mac.update(auth_data.timestamp.to_string().as_bytes());
232 mac.update(auth_data.nonce.as_bytes());
233 mac.update(payload.as_bytes());
234 })
235 .unwrap();
236 assert_eq!(auth_data.signature, expected_signature);
237 }
238
239 #[test]
240 fn test_multiple_auth_data_different_nonces() {
241 let client = NonceClient::new(TEST_SECRET);
242
243 let auth_data1 = client
244 .create_auth_data(|mac, timestamp, nonce| {
245 mac.update(timestamp.as_bytes());
246 mac.update(nonce.as_bytes());
247 })
248 .unwrap();
249
250 let auth_data2 = client
251 .create_auth_data(|mac, timestamp, nonce| {
252 mac.update(timestamp.as_bytes());
253 mac.update(nonce.as_bytes());
254 })
255 .unwrap();
256
257 assert_ne!(auth_data1.nonce, auth_data2.nonce);
258 assert_ne!(auth_data1.signature, auth_data2.signature);
259 }
260
261 #[test]
262 fn test_sign_with_builder() {
263 let client = NonceClient::new(TEST_SECRET);
264
265 let signature1 = client
266 .generate_signature(|mac| {
267 mac.update(b"data1");
268 mac.update(b"data2");
269 })
270 .unwrap();
271
272 let signature2 = client
273 .generate_signature(|mac| {
274 mac.update(b"data1");
275 mac.update(b"data3");
276 })
277 .unwrap();
278
279 assert_eq!(signature1.len(), 64);
280 assert_eq!(signature2.len(), 64);
281 assert_ne!(signature1, signature2);
282 }
283
284 #[test]
285 fn test_different_secrets_different_signatures() {
286 let client1 = NonceClient::new(b"secret1");
287 let client2 = NonceClient::new(b"secret2");
288
289 let sig1 = client1
290 .generate_signature(|mac| {
291 mac.update(b"data");
292 })
293 .unwrap();
294
295 let sig2 = client2
296 .generate_signature(|mac| {
297 mac.update(b"data");
298 })
299 .unwrap();
300
301 assert_ne!(sig1, sig2);
302 }
303
304 #[test]
305 fn test_empty_secret() {
306 let client = NonceClient::new(&[]);
307 let result = client.generate_signature(|mac| {
308 mac.update(b"data");
309 });
310 assert!(result.is_ok()); // Empty secret should still work
311 }
312}