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