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