1use 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
86pub 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
157pub 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 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}