encrypted_message/lib.rs
1//! ## Configuration
2//!
3//! First, you'll need to create a configuration type by implementing the [`Config`] trait.
4//! If your configuration type implements the [`Default`] trait, you can use the shorthand methods
5//! of the [`EncryptedMessage`] struct.
6//!
7//! The first key provided is considered the primary key, & is always used to encrypt new payloads.
8//! The following keys are used in the order provided when the primary key can't decrypt a payload. This allows you to rotate keys.
9//!
10//! ```
11//! use encrypted_message::{
12//! config::{Config, Secret, ExposeSecret as _},
13//! strategy::Randomized,
14//! };
15//!
16//! #[derive(Debug, Default)]
17//! struct EncryptionConfig;
18//! impl Config for EncryptionConfig {
19//! type Strategy = Randomized;
20//!
21//! fn keys(&self) -> Vec<Secret<[u8; 32]>> {
22//! std::env::var("ENCRYPTION_KEYS").unwrap()
23//! .split(", ")
24//! .map(|hex_key| {
25//! let hex_key = Secret::new(hex_key.to_string());
26//! let mut key = [0; 32];
27//! hex::decode_to_slice(hex_key.expose_secret(), &mut key).unwrap();
28//!
29//! key.into()
30//! })
31//! .collect()
32//! }
33//! }
34//! ```
35//!
36//! You can generate secure 32-byte keys using the `openssl` command-line tool:
37//! ```sh
38//! openssl rand -hex 32
39//! ```
40//!
41//! ## Encryption strategies
42//!
43//! Two encryption strategies are provided, [`Deterministic`](crate::strategy::Deterministic) & [`Randomized`](crate::strategy::Randomized).
44//!
45//! - [`Deterministic`](crate::strategy::Deterministic) encryption will always produce the same encrypted message for the same payload, allowing you to query encrypted data.
46//! - [`Randomized`](crate::strategy::Randomized) encryption will always produce a different encrypted message for the same payload. More secure than [`Deterministic`](crate::strategy::Deterministic), but impossible to query without decrypting all data.
47//!
48//! It's recommended to use different keys for each encryption strategy.
49//!
50//! ## Defining encrypted fields
51//!
52//! You can now define your encrypted fields using the [`EncryptedMessage`] struct.
53//! The first type parameter is the payload type, & the second is the configuration type.
54//!
55//! ```
56//! # use encrypted_message::{config::{Config, Secret}, strategy::Randomized};
57//! #
58//! # #[derive(Debug, Default)]
59//! # struct EncryptionConfig;
60//! # impl Config for EncryptionConfig {
61//! # type Strategy = Randomized;
62//! #
63//! # fn keys(&self) -> Vec<Secret<[u8; 32]>> {
64//! # vec![(*b"uuOxfpWgRgIEo3dIrdo0hnHJHF1hntvW").into()]
65//! # }
66//! # }
67//! #
68//! use encrypted_message::EncryptedMessage;
69//!
70//! struct User {
71//! diary: EncryptedMessage<String, EncryptionConfig>,
72//! }
73//! ```
74//!
75//! ## Encrypting & decrypting payloads
76//!
77//! If your [`Config`] implements the [`Default`] trait (like above), you can use the shorthand methods:
78//! ```
79//! # use encrypted_message::{
80//! # EncryptedMessage,
81//! # config::{Config, Secret},
82//! # strategy::Randomized,
83//! # };
84//! #
85//! # #[derive(Debug, Default)]
86//! # struct EncryptionConfig;
87//! # impl Config for EncryptionConfig {
88//! # type Strategy = Randomized;
89//! #
90//! # fn keys(&self) -> Vec<Secret<[u8; 32]>> {
91//! # vec![(*b"uuOxfpWgRgIEo3dIrdo0hnHJHF1hntvW").into()]
92//! # }
93//! # }
94//! #
95//! # struct User {
96//! # diary: EncryptedMessage<String, EncryptionConfig>,
97//! # }
98//! #
99//! // Encrypt a user's diary.
100//! let user = User {
101//! diary: EncryptedMessage::encrypt("Very personal stuff".to_string()).unwrap(),
102//! };
103//!
104//! // Decrypt the user's diary.
105//! let decrypted: String = user.diary.decrypt().unwrap();
106//! ```
107//!
108//! If your [`Config`] depends on external data:
109//! ```
110//! use encrypted_message::{
111//! EncryptedMessage,
112//! config::{Config, Secret, ExposeSecret as _},
113//! strategy::Randomized,
114//! };
115//! use pbkdf2::pbkdf2_hmac_array;
116//! use sha2::Sha256;
117//!
118//! #[derive(Debug)]
119//! struct UserEncryptionConfig {
120//! user_password: Secret<String>,
121//! salt: Secret<String>,
122//! }
123//!
124//! impl Config for UserEncryptionConfig {
125//! type Strategy = Randomized;
126//!
127//! fn keys(&self) -> Vec<Secret<[u8; 32]>> {
128//! let raw_key = self.user_password.expose_secret().as_bytes();
129//! let salt = self.salt.expose_secret().as_bytes();
130//! vec![pbkdf2_hmac_array::<Sha256, 32>(raw_key, salt, 2_u32.pow(16)).into()]
131//! }
132//! }
133//!
134//! struct User {
135//! diary: EncryptedMessage<String, UserEncryptionConfig>,
136//! }
137//!
138//! // Define the user's encryption configuration.
139//! let config = UserEncryptionConfig {
140//! user_password: "human-password-that-should-be-derived".to_string().into(),
141//! salt: "unique-salt".to_string().into(),
142//! };
143//!
144//! // Encrypt a user's diary.
145//! let user = User {
146//! diary: EncryptedMessage::encrypt_with_config("Very personal stuff".to_string(), &config).unwrap(),
147//! };
148//!
149//! // Decrypt the user's diary.
150//! let decrypted: String = user.diary.decrypt_with_config(&config).unwrap();
151//! ```
152
153pub mod strategy;
154use strategy::Strategy;
155
156pub mod error;
157pub use error::{EncryptionError, DecryptionError};
158
159mod integrations;
160
161pub mod config;
162use config::Config;
163
164mod utilities;
165use utilities::base64;
166
167#[cfg(test)]
168mod testing;
169
170use std::{fmt::Debug, marker::PhantomData};
171
172use serde::{Deserialize, Serialize, de::DeserializeOwned};
173use aes_gcm::{KeyInit as _, Aes256Gcm, AeadInPlace as _};
174use secrecy::ExposeSecret as _;
175
176/// Used to safely handle & transport encrypted data within your application.
177/// It contains an encrypted payload, along with a nonce & tag that are
178/// used in the encryption & decryption processes.
179#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
180#[cfg_attr(feature = "diesel", derive(diesel::AsExpression, diesel::FromSqlRow))]
181#[cfg_attr(feature = "diesel", diesel(sql_type = diesel::sql_types::Json))]
182#[cfg_attr(all(feature = "diesel", feature = "diesel-postgres"), diesel(sql_type = diesel::sql_types::Jsonb))]
183pub struct EncryptedMessage<P: Debug + DeserializeOwned + Serialize, C: Config> {
184 /// The base64-encoded & encrypted payload.
185 #[serde(rename = "p")]
186 payload: String,
187
188 /// The headers stored with the encrypted payload.
189 #[serde(rename = "h")]
190 headers: EncryptedMessageHeaders,
191
192 /// The payload type.
193 #[serde(skip)]
194 payload_type: PhantomData<P>,
195
196 /// The configuration for the encrypted message.
197 #[serde(skip)]
198 config: PhantomData<C>,
199}
200
201#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
202struct EncryptedMessageHeaders {
203 /// The base64-encoded nonce used to encrypt the payload.
204 #[serde(rename = "iv")]
205 nonce: String,
206
207 /// The base64-encoded auth tag used to verify the encrypted payload.
208 #[serde(rename = "at")]
209 tag: String,
210}
211
212impl<P: Debug + DeserializeOwned + Serialize, C: Config> EncryptedMessage<P, C> {
213 /// Creates an [`EncryptedMessage`] from a payload, using the AES-256-GCM encryption cipher.
214 ///
215 /// # Errors
216 ///
217 /// - Returns an [`EncryptionError::Serialization`] error if the payload cannot be serialized into a JSON string.
218 /// See [`serde_json::to_vec`] for more information.
219 pub fn encrypt_with_config(payload: P, config: &C) -> Result<Self, EncryptionError> {
220 let payload = serde_json::to_vec(&payload)?;
221
222 let key = config.primary_key();
223 let nonce = C::Strategy::generate_nonce_for(&payload, key.expose_secret());
224 let cipher = Aes256Gcm::new_from_slice(key.expose_secret()).unwrap();
225
226 let mut buffer = payload;
227 let tag = cipher.encrypt_in_place_detached(&nonce.into(), b"", &mut buffer).unwrap();
228
229 Ok(EncryptedMessage {
230 payload: base64::encode(buffer),
231 headers: EncryptedMessageHeaders {
232 nonce: base64::encode(nonce),
233 tag: base64::encode(tag),
234 },
235 payload_type: PhantomData,
236 config: PhantomData,
237 })
238 }
239
240 /// Decrypts the payload of the [`EncryptedMessage`], trying all available keys in order until it finds one that works.
241 ///
242 /// # Errors
243 ///
244 /// - Returns a [`DecryptionError::Base64Decoding`] error if the base64-decoding of the payload, nonce, or tag fails.
245 /// - Returns a [`DecryptionError::Decryption`] error if the payload cannot be decrypted with any of the available keys.
246 /// - Returns a [`DecryptionError::Deserialization`] error if the payload cannot be deserialized into the expected type.
247 /// See [`serde_json::from_slice`] for more information.
248 pub fn decrypt_with_config(&self, config: &C) -> Result<P, DecryptionError> {
249 let payload = base64::decode(&self.payload)?;
250 let nonce = base64::decode(&self.headers.nonce)?;
251 let tag = base64::decode(&self.headers.tag)?;
252
253 for key in config.keys() {
254 let cipher = Aes256Gcm::new_from_slice(key.expose_secret()).unwrap();
255
256 let mut buffer = payload.clone();
257 if cipher.decrypt_in_place_detached(nonce.as_slice().into(), b"", &mut buffer, tag.as_slice().into()).is_err() {
258 continue;
259 };
260
261 return Ok(serde_json::from_slice(&buffer)?);
262 }
263
264 Err(DecryptionError::Decryption)
265 }
266}
267
268impl<P: Debug + DeserializeOwned + Serialize, C: Config + Default> EncryptedMessage<P, C> {
269 /// This method is a shorthand for [`EncryptedMessage::encrypt_with_config`],
270 /// passing `&C::default()` as the configuration.
271 pub fn encrypt(payload: P) -> Result<Self, EncryptionError> {
272 Self::encrypt_with_config(payload, &C::default())
273 }
274
275 /// This method is a shorthand for [`EncryptedMessage::decrypt_with_config`],
276 /// passing `&C::default()` as the configuration.
277 pub fn decrypt(&self) -> Result<P, DecryptionError> {
278 self.decrypt_with_config(&C::default())
279 }
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285
286 use serde_json::json;
287
288 use crate::testing::{TestConfigDeterministic, TestConfigRandomized};
289
290 mod encrypt {
291 use super::*;
292
293 #[test]
294 fn deterministic() {
295 assert_eq!(
296 EncryptedMessage::<String, TestConfigDeterministic>::encrypt("rigo does pretty codes".to_string()).unwrap(),
297 EncryptedMessage {
298 payload: "K6FbTsR8lNt9osq7vfvpDl4gPOxaQUhH".to_string(),
299 headers: EncryptedMessageHeaders {
300 nonce: "1WOXnWc3iX5iA3wd".to_string(),
301 tag: "fdnw5HvNImSdBm0nTFiRFw==".to_string(),
302 },
303 payload_type: PhantomData,
304 config: PhantomData,
305 },
306 );
307 }
308
309 #[test]
310 fn randomized() {
311 let payload = "much secret much secure".to_string();
312
313 // Test that the encrypted messages never match, even when they contain the same payload.
314 assert_ne!(
315 EncryptedMessage::<String, TestConfigRandomized>::encrypt(payload.clone()).unwrap(),
316 EncryptedMessage::<String, TestConfigRandomized>::encrypt(payload).unwrap(),
317 );
318 }
319
320 #[test]
321 fn test_serialization_error() {
322 // A map with non-string keys can't be serialized into JSON.
323 let map = std::collections::HashMap::<[u8; 2], String>::from([([1, 2], "Hi".to_string())]);
324 assert!(matches!(EncryptedMessage::<_, TestConfigDeterministic>::encrypt(map).unwrap_err(), EncryptionError::Serialization(_)));
325 }
326 }
327
328 mod decrypt {
329 use super::*;
330
331 #[test]
332 fn decrypts_correctly() {
333 let payload = "hi :D".to_string();
334 let message = EncryptedMessage::<String, TestConfigDeterministic>::encrypt(payload.clone()).unwrap();
335 assert_eq!(message.decrypt().unwrap(), payload);
336 }
337
338 #[test]
339 fn test_base64_decoding_error() {
340 fn generate() -> EncryptedMessage<String, TestConfigDeterministic> {
341 EncryptedMessage::encrypt("hi :)".to_string()).unwrap()
342 }
343
344 // Test invalid payload.
345 let mut message = generate();
346 message.payload = "invalid".to_string();
347 assert!(matches!(message.decrypt().unwrap_err(), DecryptionError::Base64Decoding(_)));
348
349 // Test invalid nonce.
350 let mut message = generate();
351 message.headers.nonce = "invalid".to_string();
352 assert!(matches!(message.decrypt().unwrap_err(), DecryptionError::Base64Decoding(_)));
353
354 // Test invalid tag.
355 let mut message = generate();
356 message.headers.tag = "invalid".to_string();
357 assert!(matches!(message.decrypt().unwrap_err(), DecryptionError::Base64Decoding(_)));
358 }
359
360 #[test]
361 fn test_decryption_error() {
362 // Created using a random disposed key not used in other tests.
363 let message = EncryptedMessage {
364 payload: "2go7QdfuErm53fOI2jiNnHcPunwGWHpM".to_string(),
365 headers: EncryptedMessageHeaders {
366 nonce: "Exz8Fa9hKHEWvvmZ".to_string(),
367 tag: "r/AdKM4Dp0YAr/7dzAqujw==".to_string(),
368 },
369 payload_type: PhantomData::<String>,
370 config: PhantomData::<TestConfigDeterministic>,
371 };
372
373 assert!(matches!(message.decrypt().unwrap_err(), DecryptionError::Decryption));
374 }
375
376 #[test]
377 fn test_deserialization_error() {
378 let message = EncryptedMessage::<String, TestConfigDeterministic>::encrypt("hi :)".to_string()).unwrap();
379
380 // Change the payload type to an integer, even though the initial payload was serialized as a string.
381 let message = EncryptedMessage {
382 payload: message.payload,
383 headers: message.headers,
384 payload_type: PhantomData::<u8>,
385 config: message.config,
386 };
387
388 assert!(matches!(message.decrypt().unwrap_err(), DecryptionError::Deserialization(_)));
389 }
390 }
391
392 #[test]
393 fn allows_rotating_keys() {
394 // Created using TestConfig's second key.
395 let message = EncryptedMessage {
396 payload: "DT6PJ1ROSA==".to_string(),
397 headers: EncryptedMessageHeaders {
398 nonce: "nv6rH50Sn2Po320K".to_string(),
399 tag: "ZtAoub/4fB30QetW+O7oaA==".to_string(),
400 },
401 payload_type: PhantomData::<String>,
402 config: PhantomData::<TestConfigDeterministic>,
403 };
404
405 // Ensure that if encrypting the same value, it'll be different since it'll use the new primary key.
406 // Note that we're using the `Deterministic` encryption strategy, so the encrypted message would be the
407 // same if the key was the same.
408 let expected_payload = "hi :)".to_string();
409 assert_ne!(
410 EncryptedMessage::<String, TestConfigDeterministic>::encrypt(expected_payload.clone()).unwrap(),
411 message,
412 );
413
414 // Ensure that it can be decrypted even though the key is not primary anymore.
415 assert_eq!(message.decrypt().unwrap(), expected_payload);
416 }
417
418 #[test]
419 fn handles_empty_payload() {
420 let message = EncryptedMessage::<String, TestConfigDeterministic>::encrypt("".to_string()).unwrap();
421 assert_eq!(message.decrypt().unwrap(), "");
422 }
423
424 #[test]
425 fn handles_json_types() {
426 // Nullable values
427 let encrypted = EncryptedMessage::<Option<String>, TestConfigRandomized>::encrypt(None).unwrap();
428 assert_eq!(encrypted.decrypt().unwrap(), None);
429
430 let encrypted = EncryptedMessage::<Option<String>, TestConfigRandomized>::encrypt(Some("rigo is cool".to_string())).unwrap();
431 assert_eq!(encrypted.decrypt().unwrap(), Some("rigo is cool".to_string()));
432
433 // Boolean values
434 let encrypted = EncryptedMessage::<bool, TestConfigRandomized>::encrypt(true).unwrap();
435 assert_eq!(encrypted.decrypt().unwrap() as u8, 1);
436
437 // Integer values
438 let encrypted = EncryptedMessage::<u8, TestConfigRandomized>::encrypt(255).unwrap();
439 assert_eq!(encrypted.decrypt().unwrap(), 255);
440
441 // Float values
442 let encrypted = EncryptedMessage::<f64, TestConfigRandomized>::encrypt(0.12345).unwrap();
443 assert_eq!(encrypted.decrypt().unwrap(), 0.12345);
444
445 // String values
446 let encrypted = EncryptedMessage::<String, TestConfigRandomized>::encrypt("rigo is cool".to_string()).unwrap();
447 assert_eq!(encrypted.decrypt().unwrap(), "rigo is cool");
448
449 // Array values
450 let encrypted = EncryptedMessage::<Vec<u8>, TestConfigRandomized>::encrypt(vec![1, 2, 3]).unwrap();
451 assert_eq!(encrypted.decrypt().unwrap(), vec![1, 2, 3]);
452
453 // Object values
454 let encrypted = EncryptedMessage::<serde_json::Value, TestConfigRandomized>::encrypt(json!({ "a": 1, "b": "hello", "c": false })).unwrap();
455 assert_eq!(encrypted.decrypt().unwrap(), json!({ "a": 1, "b": "hello", "c": false }));
456 }
457
458 #[test]
459 fn to_and_from_json() {
460 let message = EncryptedMessage {
461 payload: "SBwByX5cxBSMgPlixDEf0pYEa6W41TIA".to_string(),
462 headers: EncryptedMessageHeaders {
463 nonce: "xg172uWMpjJqmWro".to_string(),
464 tag: "S88wdO9tf/381mZQ88kMNw==".to_string(),
465 },
466 payload_type: PhantomData::<String>,
467 config: PhantomData::<TestConfigRandomized>,
468 };
469
470 // To JSON.
471 let message_json = serde_json::to_value(&message).unwrap();
472 assert_eq!(
473 message_json,
474 json!({
475 "p": "SBwByX5cxBSMgPlixDEf0pYEa6W41TIA",
476 "h": {
477 "iv": "xg172uWMpjJqmWro",
478 "at": "S88wdO9tf/381mZQ88kMNw==",
479 },
480 }),
481 );
482
483 // From JSON.
484 assert_eq!(
485 serde_json::from_value::<EncryptedMessage::<_, _>>(message_json).unwrap(),
486 message,
487 );
488 }
489}