nonce_auth/nonce/credential_builder.rs
1use crate::NonceCredential;
2use crate::nonce::error::NonceError;
3use crate::nonce::time_utils::current_timestamp;
4use base64::Engine;
5use hmac::{Hmac, Mac};
6use sha2::Sha256;
7
8type HmacSha256 = Hmac<Sha256>;
9
10/// A function that generates unique nonce values.
11pub type NonceGeneratorFn = Box<dyn Fn() -> String + Send + Sync>;
12
13/// A function that provides timestamps.
14pub type TimeProviderFn = Box<dyn Fn() -> Result<u64, NonceError> + Send + Sync>;
15
16/// Builder for creating cryptographic credentials.
17///
18/// `CredentialBuilder` provides a fluent interface for configuring and creating
19/// `NonceCredential` instances. It supports custom nonce generation, time providers,
20/// and various signing methods.
21///
22/// # Example: Basic Usage
23///
24/// ```rust
25/// use nonce_auth::CredentialBuilder;
26///
27/// let credential = CredentialBuilder::new(b"my_secret")
28/// .sign(b"payload")?;
29/// # Ok::<(), nonce_auth::NonceError>(())
30/// ```
31///
32/// # Example: Custom Configuration
33///
34/// ```rust
35/// use nonce_auth::CredentialBuilder;
36/// use std::time::{SystemTime, UNIX_EPOCH};
37///
38/// # fn example() -> Result<(), nonce_auth::NonceError> {
39/// let credential = CredentialBuilder::new(b"my_secret")
40/// .with_nonce_generator(|| format!("custom-{}", uuid::Uuid::new_v4()))
41/// .with_time_provider(|| {
42/// SystemTime::now()
43/// .duration_since(UNIX_EPOCH)
44/// .map(|d| d.as_secs())
45/// .map_err(|e| nonce_auth::NonceError::CryptoError(format!("Time error: {}", e)))
46/// })
47/// .sign(b"payload")?;
48/// # Ok(())
49/// # }
50/// ```
51pub struct CredentialBuilder {
52 secret: Vec<u8>,
53 nonce_generator: NonceGeneratorFn,
54 time_provider: TimeProviderFn,
55}
56
57impl CredentialBuilder {
58 /// Creates a new `CredentialBuilder` with the provided secret.
59 ///
60 /// The secret is required for all signing operations. Other settings
61 /// can be configured using the chainable `with_*` methods.
62 ///
63 /// # Arguments
64 ///
65 /// * `secret` - The shared secret key for HMAC operations
66 ///
67 /// # Example
68 ///
69 /// ```rust
70 /// use nonce_auth::CredentialBuilder;
71 ///
72 /// // Simple usage
73 /// let credential1 = CredentialBuilder::new(b"key")
74 /// .sign(b"data")?;
75 ///
76 /// // With additional configuration in any order
77 /// let credential2 = CredentialBuilder::new(b"key")
78 /// .with_time_provider(|| Ok(1234567890))
79 /// .with_nonce_generator(|| "custom".to_string())
80 /// .sign(b"data")?;
81 /// # Ok::<(), nonce_auth::NonceError>(())
82 /// ```
83 pub fn new(secret: &[u8]) -> Self {
84 Self {
85 secret: secret.to_vec(),
86 nonce_generator: Box::new(|| uuid::Uuid::new_v4().to_string()),
87 time_provider: Box::new(|| Ok(current_timestamp()? as u64)),
88 }
89 }
90
91 /// Sets a custom nonce generator function.
92 ///
93 /// The nonce generator should produce unique values for each call.
94 /// The default generator uses UUID v4.
95 ///
96 /// # Arguments
97 ///
98 /// * `generator` - A function that returns unique nonce strings
99 ///
100 /// # Example
101 ///
102 /// ```rust
103 /// use nonce_auth::CredentialBuilder;
104 /// use std::sync::atomic::{AtomicU64, Ordering};
105 /// use std::sync::Arc;
106 ///
107 /// let counter = Arc::new(AtomicU64::new(0));
108 /// let counter_clone = counter.clone();
109 ///
110 /// let credential = CredentialBuilder::new(b"key")
111 /// .with_nonce_generator(move || {
112 /// let id = counter_clone.fetch_add(1, Ordering::SeqCst);
113 /// format!("nonce-{:010}", id)
114 /// })
115 /// .sign(b"data")?;
116 /// # Ok::<(), nonce_auth::NonceError>(())
117 /// ```
118 pub fn with_nonce_generator<F>(mut self, generator: F) -> Self
119 where
120 F: Fn() -> String + Send + Sync + 'static,
121 {
122 self.nonce_generator = Box::new(generator);
123 self
124 }
125
126 /// Sets a custom time provider function.
127 ///
128 /// The time provider should return the current Unix timestamp.
129 /// The default provider uses system time.
130 ///
131 /// # Arguments
132 ///
133 /// * `provider` - A function that returns the current timestamp
134 ///
135 /// # Example
136 ///
137 /// ```rust
138 /// use nonce_auth::CredentialBuilder;
139 ///
140 /// let credential = CredentialBuilder::new(b"key")
141 /// .with_time_provider(|| {
142 /// // Custom time source (e.g., NTP-synchronized)
143 /// Ok(1234567890)
144 /// })
145 /// .sign(b"data")?;
146 /// # Ok::<(), nonce_auth::NonceError>(())
147 /// ```
148 pub fn with_time_provider<F>(mut self, provider: F) -> Self
149 where
150 F: Fn() -> Result<u64, NonceError> + Send + Sync + 'static,
151 {
152 self.time_provider = Box::new(provider);
153 self
154 }
155
156 /// Signs a payload and creates a `NonceCredential`.
157 ///
158 /// This method generates a nonce, gets the current timestamp, and creates
159 /// an HMAC signature over the timestamp, nonce, and payload.
160 ///
161 /// # Arguments
162 ///
163 /// * `payload` - The data to be signed
164 ///
165 /// # Returns
166 ///
167 /// A `NonceCredential` containing the timestamp, nonce, and signature.
168 ///
169 /// # Errors
170 ///
171 /// Returns an error if:
172 /// - Time provider fails
173 /// - Signature generation fails
174 ///
175 /// # Example
176 ///
177 /// ```rust
178 /// use nonce_auth::CredentialBuilder;
179 ///
180 /// let credential = CredentialBuilder::new(b"secret")
181 /// .sign(b"important_data")?;
182 /// # Ok::<(), nonce_auth::NonceError>(())
183 /// ```
184 pub fn sign(self, payload: &[u8]) -> Result<NonceCredential, NonceError> {
185 let timestamp = (self.time_provider)()?;
186 let nonce = (self.nonce_generator)();
187
188 let signature = self.create_signature(&self.secret, timestamp, &nonce, payload)?;
189
190 Ok(NonceCredential {
191 timestamp,
192 nonce,
193 signature,
194 })
195 }
196
197 /// Signs multiple data components as a structured payload.
198 ///
199 /// This method concatenates all components in order and signs them as a single payload.
200 /// The order of components is significant for verification.
201 ///
202 /// # Arguments
203 ///
204 /// * `components` - Array of data components to sign in order
205 ///
206 /// # Returns
207 ///
208 /// A `NonceCredential` containing the timestamp, nonce, and signature.
209 ///
210 /// # Example
211 ///
212 /// ```rust
213 /// use nonce_auth::CredentialBuilder;
214 ///
215 /// let credential = CredentialBuilder::new(b"secret")
216 /// .sign_structured(&[b"user123", b"action", b"data"])?;
217 /// # Ok::<(), nonce_auth::NonceError>(())
218 /// ```
219 pub fn sign_structured(self, components: &[&[u8]]) -> Result<NonceCredential, NonceError> {
220 let timestamp = (self.time_provider)()?;
221 let nonce = (self.nonce_generator)();
222
223 let signature =
224 self.create_structured_signature(&self.secret, timestamp, &nonce, components)?;
225
226 Ok(NonceCredential {
227 timestamp,
228 nonce,
229 signature,
230 })
231 }
232
233 /// Signs using a custom MAC construction function.
234 ///
235 /// This method provides maximum flexibility by allowing custom MAC construction.
236 /// The provided function receives a MAC instance and the generated timestamp and nonce.
237 ///
238 /// # Arguments
239 ///
240 /// * `mac_fn` - Function that constructs the MAC using timestamp, nonce, and custom data
241 ///
242 /// # Returns
243 ///
244 /// A `NonceCredential` containing the timestamp, nonce, and signature.
245 ///
246 /// # Example
247 ///
248 /// ```rust
249 /// use nonce_auth::CredentialBuilder;
250 /// use hmac::Mac;
251 ///
252 /// # fn example() -> Result<(), nonce_auth::NonceError> {
253 /// let credential = CredentialBuilder::new(b"secret")
254 /// .sign_with(|mac, timestamp, nonce| {
255 /// mac.update(b"prefix:");
256 /// mac.update(timestamp.as_bytes());
257 /// mac.update(b":nonce:");
258 /// mac.update(nonce.as_bytes());
259 /// mac.update(b":custom_data");
260 /// })?;
261 /// # Ok(())
262 /// # }
263 /// ```
264 pub fn sign_with<F>(self, mac_fn: F) -> Result<NonceCredential, NonceError>
265 where
266 F: FnOnce(&mut HmacSha256, &str, &str),
267 {
268 let timestamp = (self.time_provider)()?;
269 let nonce = (self.nonce_generator)();
270
271 let signature = self.create_custom_signature(&self.secret, timestamp, &nonce, mac_fn)?;
272
273 Ok(NonceCredential {
274 timestamp,
275 nonce,
276 signature,
277 })
278 }
279
280 /// Creates a standard HMAC signature for timestamp, nonce, and payload.
281 fn create_signature(
282 &self,
283 secret: &[u8],
284 timestamp: u64,
285 nonce: &str,
286 payload: &[u8],
287 ) -> Result<String, NonceError> {
288 let mut mac = HmacSha256::new_from_slice(secret)
289 .map_err(|e| NonceError::CryptoError(format!("Invalid secret key: {e}")))?;
290
291 mac.update(timestamp.to_string().as_bytes());
292 mac.update(nonce.as_bytes());
293 mac.update(payload);
294
295 let result = mac.finalize();
296 Ok(base64::engine::general_purpose::STANDARD.encode(result.into_bytes()))
297 }
298
299 /// Creates a structured signature for multiple data components.
300 fn create_structured_signature(
301 &self,
302 secret: &[u8],
303 timestamp: u64,
304 nonce: &str,
305 components: &[&[u8]],
306 ) -> Result<String, NonceError> {
307 let mut mac = HmacSha256::new_from_slice(secret)
308 .map_err(|e| NonceError::CryptoError(format!("Invalid secret key: {e}")))?;
309
310 mac.update(timestamp.to_string().as_bytes());
311 mac.update(nonce.as_bytes());
312 for component in components {
313 mac.update(component);
314 }
315
316 let result = mac.finalize();
317 Ok(base64::engine::general_purpose::STANDARD.encode(result.into_bytes()))
318 }
319
320 /// Creates a custom signature using a user-provided MAC construction function.
321 fn create_custom_signature<F>(
322 &self,
323 secret: &[u8],
324 timestamp: u64,
325 nonce: &str,
326 mac_fn: F,
327 ) -> Result<String, NonceError>
328 where
329 F: FnOnce(&mut HmacSha256, &str, &str),
330 {
331 let mut mac = HmacSha256::new_from_slice(secret)
332 .map_err(|e| NonceError::CryptoError(format!("Invalid secret key: {e}")))?;
333
334 mac_fn(&mut mac, ×tamp.to_string(), nonce);
335
336 let result = mac.finalize();
337 Ok(base64::engine::general_purpose::STANDARD.encode(result.into_bytes()))
338 }
339}
340
341#[cfg(test)]
342mod tests {
343 use super::*;
344 use std::sync::Arc;
345 use std::sync::atomic::{AtomicU64, Ordering};
346
347 #[test]
348 fn test_credential_builder_new() {
349 let builder = CredentialBuilder::new(b"test_secret");
350 assert_eq!(builder.secret, b"test_secret".to_vec());
351 }
352
353 #[test]
354 fn test_basic_signing() {
355 let credential = CredentialBuilder::new(b"secret").sign(b"payload").unwrap();
356
357 assert!(!credential.nonce.is_empty());
358 assert!(credential.timestamp > 0);
359 assert!(!credential.signature.is_empty());
360 }
361
362 #[test]
363 fn test_structured_signing() {
364 let credential = CredentialBuilder::new(b"secret")
365 .sign_structured(&[b"part1", b"part2", b"part3"])
366 .unwrap();
367
368 assert!(!credential.nonce.is_empty());
369 assert!(credential.timestamp > 0);
370 assert!(!credential.signature.is_empty());
371 }
372
373 #[test]
374 fn test_custom_nonce_generator() {
375 let counter = Arc::new(AtomicU64::new(0));
376 let counter_clone = counter.clone();
377
378 let credential = CredentialBuilder::new(b"secret")
379 .with_nonce_generator(move || {
380 let id = counter_clone.fetch_add(1, Ordering::SeqCst);
381 format!("custom-{id:010}")
382 })
383 .sign(b"payload")
384 .unwrap();
385
386 assert_eq!(credential.nonce, "custom-0000000000");
387 }
388
389 #[test]
390 fn test_custom_time_provider() {
391 let fixed_time = 1234567890u64;
392 let credential = CredentialBuilder::new(b"secret")
393 .with_time_provider(move || Ok(fixed_time))
394 .sign(b"payload")
395 .unwrap();
396
397 assert_eq!(credential.timestamp, fixed_time);
398 }
399
400 #[test]
401 fn test_time_provider_error() {
402 let result = CredentialBuilder::new(b"secret")
403 .with_time_provider(|| Err(NonceError::CryptoError("Time error".to_string())))
404 .sign(b"payload");
405
406 assert!(matches!(result, Err(NonceError::CryptoError(_))));
407 }
408
409 #[test]
410 fn test_sign_with_custom_mac() {
411 let credential = CredentialBuilder::new(b"secret")
412 .sign_with(|mac, timestamp, nonce| {
413 mac.update(b"prefix:");
414 mac.update(timestamp.as_bytes());
415 mac.update(b":nonce:");
416 mac.update(nonce.as_bytes());
417 mac.update(b":custom");
418 })
419 .unwrap();
420
421 assert!(!credential.nonce.is_empty());
422 assert!(credential.timestamp > 0);
423 assert!(!credential.signature.is_empty());
424 }
425
426 #[test]
427 fn test_multiple_credentials_different_nonces() {
428 let builder = || CredentialBuilder::new(b"secret");
429
430 let cred1 = builder().sign(b"payload").unwrap();
431 let cred2 = builder().sign(b"payload").unwrap();
432
433 // Different nonces should be generated
434 assert_ne!(cred1.nonce, cred2.nonce);
435
436 // But signatures should be different due to different nonces
437 assert_ne!(cred1.signature, cred2.signature);
438 }
439
440 #[test]
441 fn test_structured_vs_regular_signing() {
442 let secret = b"secret";
443
444 // Sign components individually
445 let mut combined = Vec::new();
446 combined.extend_from_slice(b"part1");
447 combined.extend_from_slice(b"part2");
448
449 let cred1 = CredentialBuilder::new(secret)
450 .with_nonce_generator(|| "fixed_nonce".to_string())
451 .with_time_provider(|| Ok(1234567890))
452 .sign(&combined)
453 .unwrap();
454
455 // Sign as structured components
456 let cred2 = CredentialBuilder::new(secret)
457 .with_nonce_generator(|| "fixed_nonce".to_string())
458 .with_time_provider(|| Ok(1234567890))
459 .sign_structured(&[b"part1", b"part2"])
460 .unwrap();
461
462 // Should produce the same result
463 assert_eq!(cred1.signature, cred2.signature);
464 }
465
466 #[test]
467 fn test_builder_method_chaining() {
468 let secret = b"test_secret";
469 let payload = b"test_payload";
470
471 // Test different building orders produce same result
472 let cred1 = CredentialBuilder::new(secret)
473 .with_nonce_generator(|| "custom".to_string())
474 .with_time_provider(|| Ok(1234567890))
475 .sign(payload)
476 .unwrap();
477
478 let cred2 = CredentialBuilder::new(secret)
479 .with_time_provider(|| Ok(1234567890))
480 .with_nonce_generator(|| "custom".to_string())
481 .sign(payload)
482 .unwrap();
483
484 assert_eq!(cred1.nonce, "custom");
485 assert_eq!(cred1.timestamp, 1234567890);
486 assert_eq!(cred1.signature, cred2.signature);
487 }
488}