hmac_serialiser_rs/lib.rs
1//! # HMAC Signer
2//!
3//! `hmac_serialiser_rs` is a Rust library for generating and verifying HMAC signatures for secure data transmission.
4//! It uses the `ring` crate for HMAC operations and `serde` for serialising and deserialising data.
5//! Moreover, it uses the `base64` crate for encoding and decoding data.
6//!
7//! ## License
8//!
9//! This library is licensed under the MIT license.
10//!
11//! ## Features
12//!
13//! - Supports various encoding schemes for signatures.
14//! - Flexible HMAC signer logic for custom data types.
15//! - Provides a convenient interface for signing and verifying data.
16//!
17//! ## Example
18//!
19//! ```rust
20//! use hmac_serialiser_rs::{Encoder, HmacSigner, KeyInfo, SignerLogic, Algorithm};
21//! use serde::{Serialize, Deserialize};
22//! use std::error::Error;
23//!
24//! #[derive(Serialize, Deserialize, Debug)]
25//! struct UserData {
26//! // Add your data fields here
27//! username: String,
28//! email: String,
29//! }
30//!
31//! impl hmac_serialiser_rs::Data for UserData {
32//! fn get_exp(&self) -> Option<chrono::DateTime<chrono::Utc>> {
33//! // Add logic to retrieve expiration time if needed
34//! None
35//! }
36//! }
37//!
38//! fn main() -> Result<(), Box<dyn Error>> {
39//! // Define your secret key, salt, and optional info
40//! let key_info = KeyInfo {
41//! key: b"your_secret_key".to_vec(),
42//! salt: b"your_salt".to_vec(),
43//! info: vec![], // empty info
44//! };
45//!
46//! // Initialize the HMAC signer
47//! let signer = HmacSigner::new(key_info, Algorithm::SHA256, Encoder::UrlSafe);
48//!
49//! // Serialize your data
50//! let user_data = UserData {
51//! username: "user123".to_string(),
52//! email: "user123@example.com".to_string(),
53//! };
54//! let token = signer.sign(&user_data);
55//! println!("Token: {}", token);
56//!
57//! // Verify the token
58//! let verified_data: UserData = signer.unsign(&token)?;
59//! println!("Verified data: {:?}", verified_data);
60//!
61//! Ok(())
62//! }
63//! ```
64//!
65//! ## Supported Encoders
66//!
67//! - `Standard`: Standard base64 encoding.
68//! - `UrlSafe`: URL-safe base64 encoding.
69//! - `StandardNoPadding`: Standard base64 encoding without padding.
70//! - `UrlSafeNoPadding`: URL-safe base64 encoding without padding.
71//!
72//! ## Supported HMAC Algorithms
73//!
74//! - `SHA1`
75//! - `SHA256`
76//! - `SHA384`
77//! - `SHA512`
78//!
79//! ## Traits
80//!
81//! - `Data`: A trait for data structures that can be signed and verified.
82//! - `SignerLogic`: A trait for defining signer logic.
83//!
84//! ## Errors
85//!
86//! Errors are represented by the `Error` enum, which includes:
87//!
88//! - `InvalidInput`: Invalid input data.
89//! - `InvalidSignature`: Invalid signature provided.
90//! - `InvalidToken`: Invalid token provided.
91//! - `HkdfExpandError`: Error during key expansion.
92//! - `HkdfFillError`: Error during key filling.
93//! - `TokenExpired`: Token has expired.
94//!
95//! ## Contributing
96//!
97//! Contributions are welcome! Feel free to open issues and pull requests on [GitHub](https://github.com/KJHJason/hmac-serialiser-rs).
98//!
99//! ```
100
101pub mod algorithm;
102pub mod errors;
103mod hkdf;
104
105use algorithm::{Algorithm, HkdfAlgorithm};
106use base64::{engine::general_purpose, Engine as _};
107use errors::Error;
108use ring::hmac;
109use serde::{Deserialize, Serialize};
110
111const DELIM: char = '.';
112
113/// An enum for defining the encoding scheme for the payload and the signature.
114#[derive(Debug, Clone)]
115pub enum Encoder {
116 // Standard base64 encoding
117 Standard,
118 // URL-safe base64 encoding
119 UrlSafe,
120 // Standard base64 encoding without padding
121 StandardNoPadding,
122 // URL-safe base64 encoding without padding
123 UrlSafeNoPadding,
124}
125
126impl Encoder {
127 #[inline]
128 fn get_encoder(&self) -> general_purpose::GeneralPurpose {
129 match self {
130 Encoder::Standard => general_purpose::STANDARD,
131 Encoder::UrlSafe => general_purpose::URL_SAFE,
132 Encoder::StandardNoPadding => general_purpose::STANDARD_NO_PAD,
133 Encoder::UrlSafeNoPadding => general_purpose::URL_SAFE_NO_PAD,
134 }
135 }
136}
137
138/// A trait for custom data types that can be signed and verified.
139///
140/// This trait defines methods for retrieving expiration time and is used in conjunction with
141/// signing and verifying operations.
142///
143/// If your data type does not require an expiration time, you can implement the trait as follows:
144/// ```rust
145/// use hmac_serialiser_rs::Data;
146/// use chrono::{DateTime, Utc};
147///
148/// struct CustomData {
149/// data: String,
150/// }
151///
152/// impl Data for CustomData {
153/// fn get_exp(&self) -> Option<DateTime<Utc>> {
154/// None
155/// }
156/// }
157///```
158pub trait Data {
159 fn get_exp(&self) -> Option<chrono::DateTime<chrono::Utc>>;
160}
161
162/// A struct that holds the key information required for key expansion.
163///
164/// The key expansion process is used to derive a new key from the main secret key. Its main purpose is to expand
165/// the key to the HMAC algorithm's block size to avoid padding which can reduce the effort required for a brute force attack.
166///
167/// The `KeyInfo` struct contains the main secret key, salt for key expansion, and optional application-specific info.
168/// - `key` field is the main secret key used for signing and verifying data.
169/// - `salt` field is used for key expansion.
170/// - `info` field is optional and can be used to provide application-specific information.
171///
172/// The `salt` and the `info` fields can help to prevent key reuse and provide additional security.
173#[derive(Debug, Clone)]
174pub struct KeyInfo {
175 // Main secret key
176 pub key: Vec<u8>,
177
178 // Salt for the key expansion (Optional)
179 pub salt: Vec<u8>,
180
181 // Application specific info (Optional)
182 pub info: Vec<u8>,
183}
184
185/// A struct that holds the HMAC signer logic.
186///
187/// The `HmacSigner` struct is used for signing and verifying data using HMAC signatures.
188#[derive(Debug, Clone)]
189pub struct HmacSigner {
190 key: hmac::Key,
191 encoder: general_purpose::GeneralPurpose,
192}
193
194impl HmacSigner {
195 pub fn new(key_info: KeyInfo, algo: Algorithm, encoder: Encoder) -> Self {
196 if key_info.key.is_empty() {
197 panic!("Key cannot be empty"); // panic if key is empty as it is usually due to developer error
198 }
199
200 let hkdf_algo = HkdfAlgorithm::from_hmac(&algo);
201 let hkdf = hkdf::HkdfWrapper::new(&key_info.salt, hkdf_algo);
202 let expanded_key = hkdf
203 .expand(&key_info.key, &key_info.info)
204 .expect("Failed to expand key");
205
206 Self {
207 key: hmac::Key::new(algo.to_hmac(), &expanded_key),
208 encoder: encoder.get_encoder(),
209 }
210 }
211 #[inline]
212 fn sign_data(&self, data: &[u8]) -> Vec<u8> {
213 hmac::sign(&self.key, data).as_ref().to_vec()
214 }
215 #[inline]
216 fn verify(&self, data: &[u8], signature: &[u8]) -> bool {
217 hmac::verify(&self.key, data, signature).is_ok()
218 }
219}
220
221/// A trait for defining the signer logic.
222pub trait SignerLogic {
223 fn unsign<T: for<'de> Deserialize<'de> + Data>(&self, token: &str) -> Result<T, Error>;
224 fn sign<T: Serialize + Data>(&self, data: &T) -> String;
225}
226
227impl SignerLogic for HmacSigner {
228 /// Verifies the token and returns the deserialised data.
229 ///
230 /// Before verifying the payload, the input token is split into two parts: the encoded payload and the signature.
231 /// If the token does not contain two parts, an `InvalidInput` error is returned.
232 ///
233 /// Afterwards, if the encoded payload is empty, an `InvalidToken` error is returned even if the signature is valid.
234 ///
235 /// The signature is then decoded using the provided encoder. If the decoding fails, an `InvalidSignature` error is returned.
236 ///
237 /// The encoded payload and the signature are then verified via HMAC. If the verification fails, an `InvalidToken` error is returned.
238 ///
239 /// If the encoded payload is valid, the payload is decoded and deserialised using serde.
240 /// If the payload's expiration time is not provided, the deserialized data is returned.
241 /// Otherwise, the expiration time is checked against the current time. If the expiration time is earlier than the current time, a `TokenExpired` error is returned.
242 ///
243 /// Sample Usage:
244 /// ```rust
245 /// use hmac_serialiser_rs::{HmacSigner, KeyInfo, Encoder, algorithm::Algorithm, errors::Error, SignerLogic, Data};
246 /// use serde::{Serialize, Deserialize};
247 ///
248 /// #[derive(Serialize, Deserialize, Debug)]
249 /// struct UserData {
250 /// username: String,
251 /// }
252 /// impl Data for UserData {
253 /// fn get_exp(&self) -> Option<chrono::DateTime<chrono::Utc>> {
254 /// None
255 /// }
256 /// }
257 ///
258 /// let key_info = KeyInfo {
259 /// key: b"your_secret_key".to_vec(),
260 /// salt: b"your_salt".to_vec(),
261 /// info: vec![], // empty info
262 /// };
263 ///
264 /// // Initialize the HMAC signer
265 /// let signer = HmacSigner::new(key_info, Algorithm::SHA256, Encoder::UrlSafe);
266 /// let result: Result<UserData, Error> = signer.unsign(&"token.signature");
267 /// // or
268 /// let result = signer.unsign::<UserData>(&"token.signature");
269 /// ```
270 fn unsign<T: for<'de> Deserialize<'de> + Data>(&self, token: &str) -> Result<T, Error> {
271 let parts: Vec<&str> = token.split(DELIM).collect();
272 if parts.len() != 2 {
273 return Err(Error::InvalidInput(token.to_string()));
274 }
275
276 let encoded_data = parts[0];
277 if encoded_data.is_empty() {
278 return Err(Error::InvalidToken);
279 }
280
281 let signature = match self.encoder.decode(parts[1]) {
282 Ok(signature) => signature,
283 Err(_) => return Err(Error::InvalidSignature),
284 };
285
286 let encoded_data = parts[0].as_bytes();
287 if !self.verify(&encoded_data, &signature) {
288 return Err(Error::InvalidToken);
289 }
290
291 // at this pt, the token is valid and hence we can safely unwrap
292 let decoded_data = self.encoder.decode(encoded_data).unwrap();
293 let data = String::from_utf8(decoded_data).unwrap();
294 let deserialised_data: T = serde_json::from_str(&data).unwrap();
295 if let Some(expiry) = deserialised_data.get_exp() {
296 if expiry < chrono::Utc::now() {
297 return Err(Error::TokenExpired);
298 }
299 }
300 Ok(deserialised_data)
301 }
302
303 /// Signs the data and returns the token which can be sent to the client.
304 ///
305 /// Sample Usage:
306 /// ```rust
307 /// use hmac_serialiser_rs::{HmacSigner, KeyInfo, Encoder, algorithm::Algorithm, errors::Error, SignerLogic, Data};
308 /// use serde::{Serialize, Deserialize};
309 ///
310 /// #[derive(Serialize, Deserialize, Debug)]
311 /// struct UserData {
312 /// username: String,
313 /// }
314 /// impl Data for UserData {
315 /// fn get_exp(&self) -> Option<chrono::DateTime<chrono::Utc>> {
316 /// None
317 /// }
318 /// }
319 ///
320 /// let key_info = KeyInfo {
321 /// key: b"your_secret_key".to_vec(),
322 /// salt: b"your_salt".to_vec(),
323 /// info: b"auth-context".to_vec(),
324 /// };
325 ///
326 /// // Initialize the HMAC signer
327 /// let signer = HmacSigner::new(key_info, Algorithm::SHA256, Encoder::UrlSafe);
328 /// let user = UserData { username: "user123".to_string() };
329 /// let result: String = signer.sign(&user);
330 /// ```
331 fn sign<T: Serialize + Data>(&self, data: &T) -> String {
332 let token = serde_json::to_string(data).unwrap();
333 let token = self.encoder.encode(token.as_bytes());
334 let signature = self.sign_data(token.as_bytes());
335 let signature = self.encoder.encode(&signature);
336 format!("{}{}{}", token, DELIM, signature)
337 }
338}
339
340#[cfg(test)]
341mod tests {
342 use super::*;
343 use chrono::{Duration, Utc};
344
345 #[derive(Serialize, Deserialize, Debug)]
346 struct TestClaim {
347 #[serde(with = "chrono::serde::ts_seconds")]
348 exp: chrono::DateTime<Utc>,
349 data: String,
350 }
351
352 impl Data for TestClaim {
353 fn get_exp(&self) -> Option<chrono::DateTime<Utc>> {
354 Some(self.exp)
355 }
356 }
357
358 fn setup(salt: Vec<u8>, info: Vec<u8>, algo: Algorithm, encoder: Encoder) -> HmacSigner {
359 let key_info = KeyInfo {
360 key: b"test_secret_key".to_vec(),
361 salt,
362 info,
363 };
364 HmacSigner::new(key_info, algo, encoder)
365 }
366
367 #[test]
368 fn test_sign_and_unsign_valid_token() {
369 let signer = setup(
370 vec![1, 2, 3],
371 vec![4, 5, 6],
372 Algorithm::SHA256,
373 Encoder::UrlSafe,
374 );
375 let claim = TestClaim {
376 exp: Utc::now() + Duration::hours(1),
377 data: "test_data".to_string(),
378 };
379
380 let token = signer.sign(&claim);
381 let verified_claim: TestClaim = signer.unsign(&token).unwrap();
382 println!("Token: {}", token);
383 println!("Verified claim: {:?}", verified_claim);
384 assert_eq!(verified_claim.data, claim.data);
385 }
386
387 #[test]
388 fn test_invalid_token() {
389 let data = "tttttttttttttttttttttttttttttttttttttttttt";
390 let expected_error = Error::InvalidInput(data.to_string());
391 let signer = setup(
392 vec![1, 2, 3],
393 vec![4, 5, 6],
394 Algorithm::SHA256,
395 Encoder::UrlSafe,
396 );
397 match signer.unsign::<TestClaim>(&data) {
398 Ok(_) => panic!("Expected error"),
399 Err(e) => assert_eq!(e, expected_error),
400 };
401 }
402
403 #[test]
404 fn test_invalid_token_with_valid_signature() {
405 let signer = setup(
406 vec![1, 2, 3],
407 vec![4, 5, 6],
408 Algorithm::SHA256,
409 Encoder::UrlSafe,
410 );
411 let claim = TestClaim {
412 exp: Utc::now() + Duration::hours(1),
413 data: "test_data".to_string(),
414 };
415
416 let token = signer.sign(&claim);
417 let valid_signature = token.split('.').collect::<Vec<&str>>()[1];
418 let invalid_token = format!("{}.{}", "bad_data", valid_signature);
419 println!("Invalid token: {}", invalid_token);
420 println!("Valid token: {}", token);
421
422 let result: Result<TestClaim, Error> = signer.unsign(&invalid_token);
423 assert!(matches!(result, Err(Error::InvalidToken)));
424 }
425
426 #[test]
427 fn test_unsign_expired_token() {
428 let signer = setup(
429 vec![1, 2, 3],
430 vec![4, 5, 6],
431 Algorithm::SHA256,
432 Encoder::UrlSafe,
433 );
434 let claim = TestClaim {
435 exp: Utc::now() - Duration::hours(1),
436 data: "test_data".to_string(),
437 };
438
439 let token = signer.sign(&claim);
440 let result: Result<TestClaim, Error> = signer.unsign(&token);
441
442 assert!(matches!(result, Err(Error::TokenExpired)));
443 }
444
445 #[test]
446 fn test_unsign_invalid_signature() {
447 let signer = setup(
448 vec![1, 2, 3],
449 vec![4, 5, 6],
450 Algorithm::SHA256,
451 Encoder::UrlSafe,
452 );
453 let claim = TestClaim {
454 exp: Utc::now() + Duration::hours(1),
455 data: "test_data".to_string(),
456 };
457
458 let token = signer.sign(&claim);
459 let mut invalid_token = token.clone();
460 invalid_token.push_str("invalid");
461
462 let result: Result<TestClaim, Error> = signer.unsign(&invalid_token);
463
464 assert!(matches!(result, Err(Error::InvalidSignature)));
465 }
466
467 #[test]
468 fn test_unsign_malformed_token() {
469 let signer = setup(
470 vec![1, 2, 3],
471 vec![4, 5, 6],
472 Algorithm::SHA256,
473 Encoder::UrlSafe,
474 );
475
476 let malformed_token = "malformed.token";
477
478 let result: Result<TestClaim, Error> = signer.unsign(malformed_token);
479
480 assert!(matches!(result, Err(Error::InvalidSignature)));
481 }
482
483 #[test]
484 fn test_unsign_invalid_base64_signature() {
485 let signer = setup(
486 vec![1, 2, 3],
487 vec![4, 5, 6],
488 Algorithm::SHA256,
489 Encoder::UrlSafe,
490 );
491 let claim = TestClaim {
492 exp: Utc::now() + Duration::hours(1),
493 data: "test_data".to_string(),
494 };
495
496 let token = signer.sign(&claim);
497 let parts: Vec<&str> = token.split(DELIM).collect();
498 let invalid_token = format!("{}.{}", parts[0], "invalid_base64");
499
500 let result: Result<TestClaim, Error> = signer.unsign(&invalid_token);
501
502 assert!(matches!(result, Err(Error::InvalidSignature)));
503 }
504}