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}