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, &timestamp.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}