nonce_auth/nonce/
client.rs

1use hmac::Mac;
2use std::marker::PhantomData;
3use std::time::{SystemTime, UNIX_EPOCH};
4
5use super::NonceError;
6use crate::{HmacSha256, NonceCredential};
7
8/// A function type for generating nonces.
9pub type NonceGenerator = Box<dyn Fn() -> String + Send + Sync>;
10
11/// A function type for providing timestamps.
12pub type TimeProvider = Box<dyn Fn() -> Result<u64, NonceError> + Send + Sync>;
13
14/// A client for generating cryptographically signed `NonceCredential`s.
15///
16/// This client is stateless and responsible for creating credentials that can be
17/// verified by a `NonceServer`.
18///
19/// # Example
20///
21/// ```rust
22/// use nonce_auth::NonceClient;
23/// use hmac::Mac; // Trait for `mac.update()`
24///
25/// let client = NonceClient::new(b"my_secret");
26/// let payload = b"some_data_to_protect";
27///
28/// // Standard usage:
29/// let credential = client.credential_builder().sign(payload).unwrap();
30///
31/// // Advanced usage:
32/// let custom_credential = client.credential_builder().sign_with(|mac, ts, nonce| {
33///     mac.update(ts.as_bytes());
34///     mac.update(nonce.as_bytes());
35///     mac.update(payload);
36/// }).unwrap();
37/// ```
38pub struct NonceClient {
39    secret: Vec<u8>,
40    nonce_generator: NonceGenerator,
41    time_provider: TimeProvider,
42}
43
44impl NonceClient {
45    /// Creates a new `NonceClient` with the specified shared secret and default generators.
46    ///
47    /// Uses UUID v4 for nonce generation and system time for timestamps.
48    /// For more customization options, use `NonceClient::builder()`.
49    pub fn new(secret: &[u8]) -> Self {
50        Self {
51            secret: secret.to_vec(),
52            nonce_generator: Box::new(|| uuid::Uuid::new_v4().to_string()),
53            time_provider: Box::new(|| {
54                SystemTime::now()
55                    .duration_since(UNIX_EPOCH)
56                    .map_err(|e| NonceError::CryptoError(format!("System clock error: {e}")))
57                    .map(|d| d.as_secs())
58            }),
59        }
60    }
61
62    /// Creates a new `NonceClientBuilder` for advanced configuration.
63    pub fn builder() -> NonceClientBuilder {
64        NonceClientBuilder::new()
65    }
66
67    /// Returns a builder to construct and sign a `NonceCredential`.
68    ///
69    /// This is the recommended entry point for creating credentials.
70    pub fn credential_builder(&self) -> NonceCredentialBuilder<'_> {
71        NonceCredentialBuilder::new(self)
72    }
73
74    /// Low-level function to generate a signature. Used internally by the builder.
75    fn generate_signature<F>(&self, data_builder: F) -> Result<String, NonceError>
76    where
77        F: FnOnce(&mut hmac::Hmac<sha2::Sha256>),
78    {
79        let mut mac = HmacSha256::new_from_slice(&self.secret)
80            .map_err(|e| NonceError::CryptoError(e.to_string()))?;
81        data_builder(&mut mac);
82        let result = mac.finalize();
83        Ok(hex::encode(result.into_bytes()))
84    }
85}
86
87/// A builder for creating a `NonceCredential`.
88///
89/// This builder provides a safe and ergonomic API for signing data.
90pub struct NonceCredentialBuilder<'a> {
91    client: &'a NonceClient,
92    _phantom: PhantomData<()>,
93}
94
95impl<'a> NonceCredentialBuilder<'a> {
96    fn new(client: &'a NonceClient) -> Self {
97        Self {
98            client,
99            _phantom: PhantomData,
100        }
101    }
102
103    /// Signs the standard components (`timestamp`, `nonce`) plus a payload.
104    ///
105    /// This is the recommended method for most use cases, as it ensures the payload
106    /// is always included in the signature.
107    ///
108    /// # Arguments
109    ///
110    /// * `payload`: The business data to include in the signature.
111    pub fn sign(self, payload: &[u8]) -> Result<NonceCredential, NonceError> {
112        self.sign_with(|mac, timestamp, nonce| {
113            mac.update(timestamp.as_bytes());
114            mac.update(nonce.as_bytes());
115            mac.update(payload);
116        })
117    }
118
119    /// Signs the credential using custom logic defined in a closure for advanced use cases.
120    ///
121    /// # Warning
122    ///
123    /// You are responsible for including all relevant data in the signature
124    /// within the closure. Forgetting to include the payload can lead to security vulnerabilities.
125    ///
126    /// # Arguments
127    ///
128    /// * `signature_builder`: A closure that defines the exact data to be signed.
129    pub fn sign_with<F>(self, signature_builder: F) -> Result<NonceCredential, NonceError>
130    where
131        F: FnOnce(&mut hmac::Hmac<sha2::Sha256>, &str, &str),
132    {
133        let timestamp = (self.client.time_provider)()?;
134        let nonce = (self.client.nonce_generator)();
135
136        let signature = self.client.generate_signature(|mac| {
137            signature_builder(mac, &timestamp.to_string(), &nonce);
138        })?;
139
140        Ok(NonceCredential {
141            timestamp,
142            nonce,
143            signature,
144        })
145    }
146}
147
148/// A builder for creating customized `NonceClient` instances.
149///
150/// This builder allows you to customize nonce generation, timestamp providers,
151/// and other aspects of the client behavior.
152///
153/// # Example
154///
155/// ```rust
156/// use nonce_auth::NonceClient;
157/// use std::time::{SystemTime, UNIX_EPOCH};
158///
159/// // Create a client with custom nonce generator
160/// let client = NonceClient::builder()
161///     .with_secret(b"my_secret")
162///     .with_nonce_generator(|| format!("custom-{}", rand::random::<u32>()))
163///     .with_time_provider(|| {
164///         SystemTime::now()
165///             .duration_since(UNIX_EPOCH)
166///             .map(|d| d.as_secs())
167///             .map_err(|e| nonce_auth::NonceError::CryptoError(format!("Time error: {}", e)))
168///     })
169///     .build();
170/// ```
171pub struct NonceClientBuilder {
172    secret: Option<Vec<u8>>,
173    nonce_generator: Option<NonceGenerator>,
174    time_provider: Option<TimeProvider>,
175}
176
177impl NonceClientBuilder {
178    /// Creates a new `NonceClientBuilder` with default settings.
179    pub fn new() -> Self {
180        Self {
181            secret: None,
182            nonce_generator: None,
183            time_provider: None,
184        }
185    }
186
187    /// Sets the shared secret for cryptographic operations.
188    pub fn with_secret(mut self, secret: &[u8]) -> Self {
189        self.secret = Some(secret.to_vec());
190        self
191    }
192
193    /// Sets a custom nonce generator function.
194    ///
195    /// The function should return a unique string each time it's called.
196    /// The default uses UUID v4.
197    pub fn with_nonce_generator<F>(mut self, generator: F) -> Self
198    where
199        F: Fn() -> String + Send + Sync + 'static,
200    {
201        self.nonce_generator = Some(Box::new(generator));
202        self
203    }
204
205    /// Sets a custom time provider function for generating timestamps.
206    ///
207    /// The function should return the current time as seconds since UNIX epoch.
208    /// The default uses `SystemTime::now()`.
209    pub fn with_time_provider<F>(mut self, provider: F) -> Self
210    where
211        F: Fn() -> Result<u64, NonceError> + Send + Sync + 'static,
212    {
213        self.time_provider = Some(Box::new(provider));
214        self
215    }
216
217    /// Builds the `NonceClient` with the configured options.
218    ///
219    /// # Panics
220    ///
221    /// Panics if no secret was provided via `with_secret()`.
222    pub fn build(self) -> NonceClient {
223        let secret = self
224            .secret
225            .expect("Secret is required. Use with_secret() to provide one.");
226
227        let nonce_generator = self
228            .nonce_generator
229            .unwrap_or_else(|| Box::new(|| uuid::Uuid::new_v4().to_string()));
230
231        let time_provider = self.time_provider.unwrap_or_else(|| {
232            Box::new(|| {
233                SystemTime::now()
234                    .duration_since(UNIX_EPOCH)
235                    .map_err(|e| NonceError::CryptoError(format!("System clock error: {e}")))
236                    .map(|d| d.as_secs())
237            })
238        });
239
240        NonceClient {
241            secret,
242            nonce_generator,
243            time_provider,
244        }
245    }
246}
247
248impl Default for NonceClientBuilder {
249    fn default() -> Self {
250        Self::new()
251    }
252}
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257
258    const TEST_SECRET: &[u8] = b"test_secret_key_123";
259
260    #[test]
261    fn test_client_creation() {
262        let client = NonceClient::new(TEST_SECRET);
263        assert_eq!(client.secret, TEST_SECRET);
264    }
265
266    #[test]
267    fn test_builder_sign_standard() {
268        let client = NonceClient::new(TEST_SECRET);
269        let payload = b"test payload";
270
271        let credential = client.credential_builder().sign(payload).unwrap();
272
273        assert!(credential.timestamp > 0);
274        assert!(!credential.nonce.is_empty());
275        assert!(!credential.signature.is_empty());
276        assert_eq!(credential.signature.len(), 64);
277
278        // Verify the signature matches the expected components
279        let expected_signature = client
280            .generate_signature(|mac| {
281                mac.update(credential.timestamp.to_string().as_bytes());
282                mac.update(credential.nonce.as_bytes());
283                mac.update(payload);
284            })
285            .unwrap();
286        assert_eq!(credential.signature, expected_signature);
287    }
288
289    #[test]
290    fn test_builder_sign_with_custom_logic() {
291        let client = NonceClient::new(TEST_SECRET);
292        let payload = "test payload";
293        let extra = "extra_context";
294
295        let credential = client
296            .credential_builder()
297            .sign_with(|mac, timestamp, nonce| {
298                mac.update(timestamp.as_bytes());
299                mac.update(nonce.as_bytes());
300                mac.update(payload.as_bytes());
301                mac.update(extra.as_bytes());
302            })
303            .unwrap();
304
305        // Verify the signature includes all custom parts
306        let expected_signature = client
307            .generate_signature(|mac| {
308                mac.update(credential.timestamp.to_string().as_bytes());
309                mac.update(credential.nonce.as_bytes());
310                mac.update(payload.as_bytes());
311                mac.update(extra.as_bytes());
312            })
313            .unwrap();
314        assert_eq!(credential.signature, expected_signature);
315    }
316
317    #[test]
318    fn test_multiple_credentials_different_nonces() {
319        let client = NonceClient::new(TEST_SECRET);
320
321        let credential1 = client.credential_builder().sign(b"payload1").unwrap();
322        let credential2 = client.credential_builder().sign(b"payload2").unwrap();
323
324        assert_ne!(credential1.nonce, credential2.nonce);
325        assert_ne!(credential1.signature, credential2.signature);
326    }
327
328    #[test]
329    fn test_builder_with_secret() {
330        let client = NonceClient::builder().with_secret(TEST_SECRET).build();
331
332        assert_eq!(client.secret, TEST_SECRET);
333    }
334
335    #[test]
336    fn test_builder_with_custom_nonce_generator() {
337        let counter = std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0));
338        let counter_clone = counter.clone();
339
340        let client = NonceClient::builder()
341            .with_secret(TEST_SECRET)
342            .with_nonce_generator(move || {
343                let val = counter_clone.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
344                format!("custom-nonce-{val}")
345            })
346            .build();
347
348        let credential1 = client.credential_builder().sign(b"test").unwrap();
349        let credential2 = client.credential_builder().sign(b"test").unwrap();
350
351        assert_eq!(credential1.nonce, "custom-nonce-0");
352        assert_eq!(credential2.nonce, "custom-nonce-1");
353    }
354
355    #[test]
356    fn test_builder_with_custom_time_provider() {
357        let fixed_time = 1234567890u64;
358
359        let client = NonceClient::builder()
360            .with_secret(TEST_SECRET)
361            .with_time_provider(move || Ok(fixed_time))
362            .build();
363
364        let credential = client.credential_builder().sign(b"test").unwrap();
365        assert_eq!(credential.timestamp, fixed_time);
366    }
367
368    #[test]
369    fn test_builder_with_all_custom_options() {
370        let client = NonceClient::builder()
371            .with_secret(TEST_SECRET)
372            .with_nonce_generator(|| "fixed-nonce".to_string())
373            .with_time_provider(|| Ok(9999999999))
374            .build();
375
376        let credential = client.credential_builder().sign(b"test payload").unwrap();
377
378        assert_eq!(credential.nonce, "fixed-nonce");
379        assert_eq!(credential.timestamp, 9999999999);
380
381        // Verify signature is computed correctly with fixed values
382        let expected_signature = client
383            .generate_signature(|mac| {
384                mac.update(b"9999999999");
385                mac.update(b"fixed-nonce");
386                mac.update(b"test payload");
387            })
388            .unwrap();
389        assert_eq!(credential.signature, expected_signature);
390    }
391
392    #[test]
393    #[should_panic(expected = "Secret is required")]
394    fn test_builder_without_secret_panics() {
395        NonceClient::builder().build();
396    }
397
398    #[test]
399    fn test_builder_default() {
400        let builder1 = NonceClientBuilder::new();
401        let builder2 = NonceClientBuilder::default();
402
403        // Both should have the same initial state (all None)
404        assert!(builder1.secret.is_none());
405        assert!(builder1.nonce_generator.is_none());
406        assert!(builder1.time_provider.is_none());
407
408        assert!(builder2.secret.is_none());
409        assert!(builder2.nonce_generator.is_none());
410        assert!(builder2.time_provider.is_none());
411    }
412}