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}