hapi_iron_oxide/
lib.rs

1//! ## hapi-iron-oxide
2//! This module is made to be compatible with [brc-dd/iron-webcrypto](https://github.com/brc-dd/iron-webcrypto), which is the crate that backs [vvo/iron-session](https://github.com/vvo/iron-session). This allows APIs made in Rust be able to talk to Next.js.
3//!
4//! ### Installation
5//! ```bash
6//! cargo add hapi-iron-oxide
7//! ```
8//!
9//! ### Usage
10//! ```
11//! use hapi_iron_oxide::{seal, unseal};
12//!
13//! let password = "passwordpasswordpasswordpasswordpasswordpasswordpasswordpassword";
14//! let data = "Hello World Please";
15//!
16//! let sealed = seal::<32, 32, _>(data.to_string(), password, Default::default()).unwrap();
17//! let unsealed = unseal(sealed, password.clone(), Default::default());
18//!
19//! assert_eq!(unsealed.unwrap(), data.to_string());
20//! ```
21//!
22//! The options struct can be customized like This
23//! ```rust
24//! use hapi_iron_oxide::*;
25//! use hapi_iron_oxide::options::EncryptionOptions;
26//! use hapi_iron_oxide::algorithm::Algorithm;
27//!
28//! let password = "passwordpasswordpasswordpasswordpasswordpasswordpasswordpassword";
29//! let data = "Hello World Please";
30//!
31//! let options = SealOptionsBuilder::new().ttl(1000).finish();
32//!
33//! assert_eq!(
34//!     options,
35//!     SealOptions {
36//!         encryption: EncryptionOptions {
37//!             algorithm: Algorithm::Aes256Cbc,
38//!             iterations: 1,
39//!             minimum_password_length: 32,
40//!         },
41//!         integrity: EncryptionOptions {
42//!             algorithm: Algorithm::Sha256,
43//!             iterations: 1,
44//!             minimum_password_length: 32,
45//!         },
46//!         ttl: 1000,
47//!         timestamp_skew: 60,
48//!         local_offset: 0,
49//!     }
50//! );
51//!
52//! let sealed = seal::<32, 32, _>(data.to_string(), password, options).unwrap();
53//! assert!(!sealed.is_empty());
54//! ```
55//!
56//! ### Thank you
57//! Thank you to
58//! - [brc-dd/iron-webcrypto](https://github.com/brc-dd/iron-webcrypto)
59//! - [iron-auth/iron-crypto](https://github.com/iron-auth/iron-crypto)
60//! for the ideas and implementation details.
61
62use base64::Engine;
63use constant_time_eq::constant_time_eq;
64use constants::{IV_SIZE, MAC_PREFIX};
65use encryption::decrypt;
66use errors::HapiIronOxideError;
67use hmac_sign::{seal_hmac_with_password, unseal_hmac_with_password};
68use key::KeyOptions;
69use password::SpecificPasswordInit;
70use time::Duration;
71
72use crate::base64_engine::ENGINE;
73
74pub mod algorithm;
75pub mod base64_engine;
76pub mod constants;
77pub mod encryption;
78pub mod errors;
79pub mod hmac_sign;
80pub mod key;
81pub mod options;
82pub mod password;
83
84pub use options::{SealOptions, SealOptionsBuilder};
85
86/// Creates sealed string from given options.
87pub fn seal<const E: usize, const I: usize, U>(
88    data: String,
89    password: U,
90    options: SealOptions,
91) -> Result<String, HapiIronOxideError>
92where
93    U: SpecificPasswordInit,
94{
95    let normalized_password = password.normalize()?;
96
97    let encrypted = encryption::encrypt::<E>(
98        data,
99        normalized_password.clone(),
100        KeyOptions {
101            algorithm: options.encryption.algorithm,
102            iterations: options.encryption.iterations,
103            minimum_password_length: options.encryption.minimum_password_length,
104            salt: None,
105            iv: None,
106        },
107    )?;
108
109    let encrypted_base64 = ENGINE.encode(encrypted.encrypted);
110    let iv_base64 = ENGINE.encode(encrypted.key.iv);
111
112    let expiration: String = match options.ttl {
113        x if x == 0 => "".to_string(),
114        _ => {
115            let ttl =
116                time::OffsetDateTime::now_utc() + Duration::new(options.ttl.try_into().unwrap(), 0);
117            let ttl_millisecond = ttl.unix_timestamp_nanos() / 1_000_000;
118
119            ttl_millisecond.to_string()
120        }
121    };
122
123    let mac_base_string = format!(
124        "{}*{}*{}*{}*{}*{}",
125        &MAC_PREFIX,
126        normalized_password.id,
127        Clone::clone(&encrypted.key.salt).unwrap_or("".to_string()),
128        iv_base64,
129        encrypted_base64,
130        expiration
131    );
132
133    let mac_options = KeyOptions {
134        algorithm: options.integrity.algorithm,
135        iterations: options.integrity.iterations,
136        minimum_password_length: options.integrity.minimum_password_length,
137        salt: encrypted.key.salt,
138        iv: None,
139    };
140
141    let result = seal_hmac_with_password::<I>(
142        mac_base_string.clone(),
143        Clone::clone(&normalized_password.integrity),
144        mac_options,
145    )?;
146
147    let sealed = format!(
148        "{}*{}*{}",
149        mac_base_string,
150        result.salt.unwrap_or("".to_string()),
151        ENGINE.encode(result.digest)
152    );
153
154    Ok(sealed)
155}
156
157/// Unseal the sealed string back into the original string.
158pub fn unseal<U>(
159    sealed: String,
160    password: U,
161    options: SealOptions,
162) -> Result<String, HapiIronOxideError>
163where
164    U: SpecificPasswordInit,
165{
166    let now = time::OffsetDateTime::now_utc() + time::Duration::new(options.local_offset.into(), 0);
167
168    let mut parts = sealed.split("*");
169
170    let prefix = parts.next().ok_or(HapiIronOxideError::InvalidSeal)?;
171    let password_id = parts.next().ok_or(HapiIronOxideError::InvalidSeal)?;
172    let encryption_salt = parts.next().ok_or(HapiIronOxideError::InvalidSeal)?;
173    let encryption_iv = parts.next().ok_or(HapiIronOxideError::InvalidSeal)?;
174    let encryption_value = parts.next().ok_or(HapiIronOxideError::InvalidSeal)?;
175    let expiration = parts.next().ok_or(HapiIronOxideError::InvalidSeal)?;
176    let hmac_salt = parts.next().ok_or(HapiIronOxideError::InvalidSeal)?;
177    let hmac_digest = parts.next().ok_or(HapiIronOxideError::InvalidSeal)?;
178
179    let mac_base_string = format!(
180        "{}*{}*{}*{}*{}*{}",
181        prefix, password_id, encryption_salt, encryption_iv, encryption_value, expiration
182    );
183
184    if prefix != MAC_PREFIX {
185        return Err(HapiIronOxideError::InvalidSealPrefix);
186    }
187
188    if !expiration.is_empty() {
189        let expiration_number = i128::from_str_radix(expiration, 10)?;
190        let expiration_time =
191            time::OffsetDateTime::from_unix_timestamp_nanos(expiration_number * 1_000_000)?;
192
193        if expiration_time <= now - time::Duration::new(options.timestamp_skew.into(), 0) {
194            return Err(HapiIronOxideError::ExpiredSeal);
195        }
196    }
197
198    let normalized_password = password.normalize_unseal(Some(password_id))?;
199
200    let mac_options = KeyOptions {
201        algorithm: options.integrity.algorithm,
202        iterations: options.integrity.iterations,
203        minimum_password_length: options.integrity.minimum_password_length,
204        salt: Some(hmac_salt.to_string()),
205        iv: None,
206    };
207
208    let result = unseal_hmac_with_password(
209        mac_base_string,
210        Clone::clone(&normalized_password.integrity),
211        mac_options,
212    )?;
213
214    if !constant_time_eq(
215        result.digest.as_slice(),
216        ENGINE.decode(hmac_digest)?.as_slice(),
217    ) {
218        return Err(HapiIronOxideError::InvalidHmacValue);
219    }
220
221    let decrypted_value = ENGINE.decode(encryption_value)?;
222
223    let mut iv: [u8; IV_SIZE] = [0; IV_SIZE];
224    // any invalid length causes a panic
225    ENGINE.decode_slice_unchecked(encryption_iv, &mut iv)?;
226
227    let decrypt_options = KeyOptions {
228        algorithm: options.encryption.algorithm,
229        iterations: options.encryption.iterations,
230        minimum_password_length: options.encryption.minimum_password_length,
231        salt: Some(encryption_salt.to_string()),
232        iv: Some(iv),
233    };
234
235    let decrypted_vector = decrypt(decrypted_value, normalized_password, decrypt_options)?;
236
237    Ok(String::from_utf8(decrypted_vector).unwrap())
238}
239
240#[cfg(test)]
241mod tests {
242    use super::*;
243
244    const PASSWORD: &'static str =
245        "passwordpasswordpasswordpasswordpasswordpasswordpasswordpassword";
246    const DATA_STRING: &'static str = "{\"dis\":\"eh\"}";
247
248    #[test]
249    fn test_seal_unseal() {
250        let sealed =
251            seal::<32, 32, _>(DATA_STRING.to_string(), PASSWORD, Default::default()).unwrap();
252        let unsealed = unseal(sealed, PASSWORD, Default::default()).unwrap();
253
254        assert_eq!(unsealed, DATA_STRING.to_string());
255    }
256
257    #[test]
258    fn test_unseal_from_node() {
259        let s = "Fe26.2**79c5378388cbe4f2c71d3ea08b562f7b26bc13c029843549bb3c155e12dc86d7*KuWCSG7MB23J8sPKmUj6Hg*AdqzRL9iLYGku3uG903Pww**a268d5bb817dc86e60e413ed25ddf833962d249c806f72019420c2a0341751a3*uh1HrJMhK4x4WhAu8pnjpNabnaDzQbCzhK31YDVsGxQ";
260
261        let u = unseal(s.to_string(), PASSWORD, Default::default()).unwrap();
262
263        assert_eq!(u, DATA_STRING.to_string());
264    }
265
266    #[test]
267    fn test_seal_custom_salt_size() {
268        let sealed =
269            seal::<64, 128, _>(DATA_STRING.to_string(), PASSWORD, Default::default()).unwrap();
270
271        let unsealed = unseal(sealed, PASSWORD, Default::default()).unwrap();
272
273        assert_eq!(unsealed, DATA_STRING.to_string());
274    }
275
276    #[test]
277    #[should_panic]
278    fn test_seal_unseal_invalid_key_size_in_vec_u8() {
279        let p = b"passwordpasswordpasswordpasswordpasswordpasswordpasswordpassword";
280        let password_as_bytes = p.to_vec();
281
282        let sealed = seal::<32, 32, _>(
283            DATA_STRING.to_string(),
284            password_as_bytes.clone(),
285            Default::default(),
286        )
287        .unwrap();
288        let unsealed = unseal(sealed, password_as_bytes, Default::default()).unwrap();
289
290        assert_eq!(unsealed, DATA_STRING.to_string());
291    }
292}