hmac_serialiser/
lib.rs

1//! # HMAC Signer
2//!
3//! `hmac-serialiser` is a Rust library for generating and verifying HMAC signatures for secure data transmission.
4//!
5//! Regarding the cryptographic implementations, you can choose which implementations to use from via the `features` flag in the `Cargo.toml` file:
6//! - `rust_crypto` (default)
7//!   - the underlying [SHA1](https://crates.io/crates/sha1), [SHA2](https://crates.io/crates/sha2), [HMAC](https://crates.io/crates/hmac), and [HKDF](https://crates.io/crates/hkdf) implementations are by [RustCrypto](https://github.com/RustCrypto).
8//! - `ring`
9//!   - The underlying SHA1, SHA2, HMAC, and HKDF implementations are from the [ring](https://crates.io/crates/ring) crate.
10//!
11//! Additionally, the data serialisation and de-serialisation uses the [serde](https://crates.io/crates/serde) crate and
12//! the signed data is then encoded or decoded using the [base64](https://crates.io/crates/base64) crate.
13//!
14//! ## License
15//!
16//! This library is licensed under the MIT license.
17//!
18//! ## Features
19//!
20//! - Supports various encoding schemes for signatures.
21//! - Flexible HMAC signer logic for custom data types.
22//! - Provides a convenient interface for signing and verifying data.
23//!
24//! ## Example
25//!
26//! ```rust
27//! use hmac_serialiser::{Encoder, HmacSigner, KeyInfo, Payload, Algorithm};
28//! use serde::{Serialize, Deserialize};
29//!
30//! #[derive(Serialize, Deserialize, Debug)]
31//! struct UserData {
32//!     // Add your data fields here
33//!     username: String,
34//!     email: String,
35//! }
36//!
37//! impl Payload for UserData {
38//!     fn get_exp(&self) -> Option<chrono::DateTime<chrono::Utc>> {
39//!         // Add logic to retrieve expiration time if needed
40//!         None
41//!     }
42//! }
43//!
44//! fn main() {
45//!     // Define your secret key, salt, and optional info
46//!     let key_info = KeyInfo {
47//!         key: b"your_secret_key".to_vec(),
48//!         salt: b"your_salt".to_vec(),
49//!         info: vec![], // empty info
50//!     };
51//!
52//!     // Initialize the HMAC signer
53//!     let signer = HmacSigner::new(key_info, Algorithm::SHA256, Encoder::UrlSafeNoPadding);
54//!
55//!     // Serialize your data
56//!     let user_data = UserData {
57//!         username: "user123".to_string(),
58//!         email: "user123@example.com".to_string(),
59//!     };
60//!
61//!     // Sign the data (safe to use by clients)
62//!     let token = signer.sign(&user_data);
63//!     println!("Token: {}", token);
64//!     
65//!     // Verify the token given by the client
66//!     let verified_data: UserData = signer.unsign(&token)
67//!         .expect("Failed to verify token");
68//!     println!("Verified data: {:?}", verified_data);
69//! }
70//! ```
71//!
72//! ## Supported Encoders
73//!
74//! - `Standard`: Standard base64 encoding.
75//! - `UrlSafe`: URL-safe base64 encoding.
76//! - `StandardNoPadding`: Standard base64 encoding without padding.
77//! - `UrlSafeNoPadding`: URL-safe base64 encoding without padding. (Default)
78//!
79//! ## Supported HMAC Algorithms
80//!
81//! - `SHA1`
82//! - `SHA256` (Default)
83//! - `SHA384`
84//! - `SHA512`
85//!
86//! Note: Although SHA1 is cryptographically broken, HMAC-SHA1 is not used for integrity checks like file hash checks.
87//! Therefore, it is still considered secure to use HMAC-SHA1 to verify the authenticity of a given payload.
88//! However, it is still recommended to choose a stronger hash function like SHA256 or even SHA512.
89//!
90//! ## Traits
91//!
92//! - `Payload`: A trait for data structures that can be signed and verified.
93//!
94//! ## Errors
95//!
96//! Errors are represented by the `Error` enum, which includes:
97//!
98//! - `InvalidInput`: Invalid input payload.
99//! - `InvalidSignature`: Invalid signature provided.
100//! - `InvalidPayload`: Invalid payload structure when de-serialising valid payload
101//! - `InvalidToken`: Invalid token provided.
102//! - `HkdfExpandError`: Error during key expansion.
103//! - `HkdfFillError`: Error during key filling.
104//! - `TokenExpired`: Token has expired.
105//!
106//! ## Contributing
107//!
108//! Contributions are welcome! Feel free to open issues and pull requests on [GitHub](https://github.com/KJHJason/hmac-serialiser/tree/master/rust).
109//!
110//! ```
111
112pub mod algorithm;
113pub mod errors;
114pub mod hkdf;
115
116use base64::{engine::general_purpose, Engine as _};
117use serde::{Deserialize, Serialize};
118
119pub use algorithm::Algorithm;
120pub use errors::Error;
121
122#[cfg(not(feature = "ring"))]
123use hmac::Mac;
124
125#[cfg(feature = "ring")]
126use ring::hmac;
127
128pub const DELIM: char = '.';
129
130/// An enum for defining the encoding scheme for the payload and the signature.
131///
132/// Usually, you should use the encoder with no padding to shorten the token length by a few characters.
133///
134/// Whether to use URL-safe or Standard encoding depends on the application's requirements.
135///
136/// For example, if you are developing a password reset route
137/// in a web application like /password-reset?token=...., you would want
138/// to use the UrlSafe encoding so that the token can be safely used in the URL.
139#[derive(Default, Debug, Clone)]
140pub enum Encoder {
141    // Standard base64 encoding
142    Standard,
143
144    // URL-safe base64 encoding
145    UrlSafe,
146
147    // Standard base64 encoding without padding
148    StandardNoPadding,
149
150    #[default]
151    // URL-safe base64 encoding without padding
152    UrlSafeNoPadding,
153}
154
155impl Encoder {
156    #[inline]
157    fn get_encoder(&self) -> general_purpose::GeneralPurpose {
158        match self {
159            Encoder::Standard => general_purpose::STANDARD,
160            Encoder::UrlSafe => general_purpose::URL_SAFE,
161            Encoder::StandardNoPadding => general_purpose::STANDARD_NO_PAD,
162            Encoder::UrlSafeNoPadding => general_purpose::URL_SAFE_NO_PAD,
163        }
164    }
165}
166
167/// A trait for custom payload types that can be signed and verified.
168///
169/// This trait defines methods for retrieving expiration time and is used in conjunction with
170/// signing and verifying operations.
171///
172/// If your payload type does not require an expiration time, you can implement the trait as follows:
173/// ```rust
174/// use hmac_serialiser::Payload;
175/// use chrono::{DateTime, Utc};
176///
177/// struct CustomData {
178///    data: String,
179/// }
180///
181/// impl Payload for CustomData {
182///    fn get_exp(&self) -> Option<DateTime<Utc>> {
183///       None
184///   }
185/// }
186///```
187pub trait Payload {
188    fn get_exp(&self) -> Option<chrono::DateTime<chrono::Utc>>;
189}
190
191/// A struct that holds the key information required for key expansion.
192///
193/// The key expansion process is used to derive a new key from the main secret key. Its main purpose is to expand
194/// the key to the HMAC algorithm's block size to avoid padding which can reduce the effort required for a brute force attack.
195///
196/// The `KeyInfo` struct contains the main secret key, salt for key expansion, and optional application-specific info.
197/// - `key` field is the main secret key used for signing and verifying the payload.
198/// - `salt` field is used for key expansion.
199/// - `info` field is optional and can be used to provide application-specific information.
200///
201/// The `salt` and the `info` fields can help to prevent key reuse and provide additional security.
202#[derive(Debug, Clone)]
203pub struct KeyInfo {
204    // Main secret key
205    pub key: Vec<u8>,
206
207    // Salt for the key expansion (Optional)
208    pub salt: Vec<u8>,
209
210    // Application specific info (Optional)
211    pub info: Vec<u8>,
212}
213
214impl Default for KeyInfo {
215    fn default() -> Self {
216        Self {
217            key: vec![],
218            salt: vec![],
219            info: vec![],
220        }
221    }
222}
223
224/// A struct that holds the HMAC signer logic.
225///
226/// The `HmacSigner` struct is used for signing and verifying the payload using HMAC signatures.
227#[derive(Debug, Clone)]
228pub struct HmacSigner {
229    #[cfg(not(feature = "ring"))]
230    expanded_key: Vec<u8>,
231    #[cfg(not(feature = "ring"))]
232    algo: Algorithm,
233    #[cfg(feature = "ring")]
234    expanded_key: hmac::Key,
235
236    encoder: general_purpose::GeneralPurpose,
237}
238
239#[cfg(not(feature = "ring"))]
240macro_rules! get_hmac {
241    ($self:ident, $D:ty) => {
242        hmac::Hmac::<$D>::new_from_slice(&$self.expanded_key)
243            .expect("HMAC can take key of any size")
244    };
245}
246
247#[cfg(not(feature = "ring"))]
248macro_rules! hmac_sign {
249    ($self:ident, $payload:ident, $D:ty) => {{
250        let mut mac = get_hmac!($self, $D);
251        mac.update($payload);
252        mac.finalize().into_bytes().to_vec()
253    }};
254}
255
256#[cfg(not(feature = "ring"))]
257macro_rules! hmac_verify {
258    ($self:ident, $payload:ident, $signature:ident, $D:ty) => {{
259        let mut mac = get_hmac!($self, $D);
260        mac.update($payload);
261        mac.verify_slice($signature).is_ok()
262    }};
263}
264
265impl HmacSigner {
266    pub fn new(key_info: KeyInfo, algo: Algorithm, encoder: Encoder) -> Self {
267        if key_info.key.is_empty() {
268            panic!("Key cannot be empty"); // panic if key is empty as it is usually due to developer error
269        }
270
271        let expanded_key = hkdf::HkdfWrapper::new(algo.clone()).expand(
272            &key_info.key,
273            &key_info.salt,
274            &key_info.info,
275        );
276
277        #[cfg(feature = "ring")]
278        {
279            let expanded_key = hmac::Key::new(algo.to_hmac(), &expanded_key);
280            return Self {
281                expanded_key,
282                encoder: encoder.get_encoder(),
283            };
284        }
285        #[cfg(not(feature = "ring"))]
286        Self {
287            expanded_key,
288            algo,
289            encoder: encoder.get_encoder(),
290        }
291    }
292
293    #[inline]
294    #[cfg(not(feature = "ring"))]
295    fn sign_payload(&self, payload: &[u8]) -> Vec<u8> {
296        match self.algo {
297            Algorithm::SHA1 => hmac_sign!(self, payload, sha1::Sha1),
298            Algorithm::SHA256 => hmac_sign!(self, payload, sha2::Sha256),
299            Algorithm::SHA384 => hmac_sign!(self, payload, sha2::Sha384),
300            Algorithm::SHA512 => hmac_sign!(self, payload, sha2::Sha512),
301        }
302    }
303
304    #[inline]
305    #[cfg(not(feature = "ring"))]
306    fn verify(&self, payload: &[u8], signature: &[u8]) -> bool {
307        match self.algo {
308            Algorithm::SHA1 => hmac_verify!(self, payload, signature, sha1::Sha1),
309            Algorithm::SHA256 => hmac_verify!(self, payload, signature, sha2::Sha256),
310            Algorithm::SHA384 => hmac_verify!(self, payload, signature, sha2::Sha384),
311            Algorithm::SHA512 => hmac_verify!(self, payload, signature, sha2::Sha512),
312        }
313    }
314
315    #[inline]
316    #[cfg(feature = "ring")]
317    fn sign_payload(&self, payload: &[u8]) -> Vec<u8> {
318        hmac::sign(&self.expanded_key, payload).as_ref().to_vec()
319    }
320
321    #[inline]
322    #[cfg(feature = "ring")]
323    fn verify(&self, payload: &[u8], signature: &[u8]) -> bool {
324        hmac::verify(&self.expanded_key, payload, signature).is_ok()
325    }
326}
327
328impl HmacSigner {
329    /// Verifies the token and returns the deserialised payload.
330    ///
331    /// Before verifying the payload, the input token is split into two parts: the encoded payload and the signature.
332    /// If the token does not contain two parts, an `InvalidInput` error is returned.
333    ///
334    /// Afterwards, if the encoded payload is empty, an `InvalidToken` error is returned even if the signature is valid.
335    ///
336    /// The signature is then decoded using the provided encoder. If the decoding fails, an `InvalidSignature` error is returned.
337    ///
338    /// The encoded payload and the signature are then verified via HMAC. If the verification fails, an `InvalidToken` error is returned.
339    ///
340    /// If the encoded payload is valid, the payload is decoded and deserialised using serde.
341    /// If the payload's expiration time is not provided, the deserialized payload is returned.
342    /// 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.
343    ///
344    /// Sample Usage:
345    /// ```rust
346    /// use hmac_serialiser::{HmacSigner, KeyInfo, Encoder, algorithm::Algorithm, Error, Payload};
347    /// use serde::{Serialize, Deserialize};
348    ///
349    /// #[derive(Serialize, Deserialize, Debug)]
350    /// struct UserData {
351    ///     username: String,
352    /// }
353    /// impl Payload for UserData {
354    ///    fn get_exp(&self) -> Option<chrono::DateTime<chrono::Utc>> {
355    ///         None
356    ///     }
357    /// }
358    ///
359    /// let key_info = KeyInfo {
360    ///    key: b"your_secret_key".to_vec(),
361    ///    salt: b"your_salt".to_vec(),
362    ///    info: vec![], // empty info
363    /// };
364    ///
365    /// // Initialize the HMAC signer
366    /// let signer = HmacSigner::new(key_info, Algorithm::SHA256, Encoder::UrlSafe);
367    /// let result: Result<UserData, Error> = signer.unsign(&"token.signature");
368    /// // or
369    /// let result = signer.unsign::<UserData>(&"token.signature");
370    /// ```
371    pub fn unsign<T: for<'de> Deserialize<'de> + Payload>(&self, token: &str) -> Result<T, Error> {
372        let parts: Vec<&str> = token.split(DELIM).collect();
373        if parts.len() != 2 {
374            return Err(Error::InvalidInput(token.to_string()));
375        }
376
377        let encoded_payload = parts[0];
378        if encoded_payload.is_empty() {
379            return Err(Error::InvalidToken);
380        }
381
382        let signature = self
383            .encoder
384            .decode(parts[1])
385            .map_err(|_| Error::InvalidSignature)?;
386        let encoded_payload = parts[0].as_bytes();
387        if !self.verify(&encoded_payload, &signature) {
388            return Err(Error::InvalidToken);
389        }
390
391        // at this pt, the token is valid and hence we can safely unwrap
392        let decoded_payload = self
393            .encoder
394            .decode(encoded_payload)
395            .expect("payload should be valid base64");
396        let payload = String::from_utf8(decoded_payload).expect("payload should be valid utf-8");
397
398        // usually de-serialisation errors are
399        // caused when the developer was expecting the
400        // wrong payload type or has recently changed the payload type
401        let deserialised_payload: T =
402            serde_json::from_str(&payload).map_err(|_| Error::InvalidPayload)?;
403
404        if let Some(expiry) = deserialised_payload.get_exp() {
405            if expiry < chrono::Utc::now() {
406                return Err(Error::TokenExpired);
407            }
408        }
409        Ok(deserialised_payload)
410    }
411
412    /// Signs the payload and returns the token which can be sent to the client.
413    ///
414    /// Sample Usage:
415    /// ```rust
416    /// use hmac_serialiser::{HmacSigner, KeyInfo, Encoder, algorithm::Algorithm, Error, Payload};
417    /// use serde::{Serialize, Deserialize};
418    ///
419    /// #[derive(Serialize, Deserialize, Debug)]
420    /// struct UserData {
421    ///     username: String,
422    /// }
423    /// impl Payload for UserData {
424    ///    fn get_exp(&self) -> Option<chrono::DateTime<chrono::Utc>> {
425    ///         None
426    ///     }
427    /// }
428    ///
429    /// let key_info = KeyInfo {
430    ///    key: b"your_secret_key".to_vec(),
431    ///    salt: b"your_salt".to_vec(),
432    ///    info: b"auth-context".to_vec(),
433    /// };
434    ///
435    /// // Initialize the HMAC signer
436    /// let signer = HmacSigner::new(key_info, Algorithm::SHA256, Encoder::UrlSafe);
437    /// let user = UserData { username: "user123".to_string() };
438    /// let result: String = signer.sign(&user);
439    /// ```
440    pub fn sign<T: Serialize + Payload>(&self, payload: &T) -> String {
441        let token = serde_json::to_string(payload).unwrap();
442        let token = self.encoder.encode(token.as_bytes());
443        let signature = self.sign_payload(token.as_bytes());
444        let signature = self.encoder.encode(&signature);
445        format!("{}{}{}", token, DELIM, signature)
446    }
447}