snapper_box/crypto/
types.rs

1//! Intermediate types used for encryption and decryption <span style="color:red">**HAZMAT**</span>
2//!
3//! # <span style="color:red">**DANGER**</span>
4//!
5//! This module deals in low level cryptographic details. It is advisable to not deal with this module
6//! directly, and instead use a higher level API.
7
8use std::{borrow::Cow, io::Cursor};
9
10use chacha20::{
11    cipher::{NewCipher, StreamCipher},
12    XChaCha20,
13};
14use redacted::RedactedBytes;
15use serde::{de::DeserializeOwned, Deserialize, Serialize};
16use snafu::{ensure, ResultExt};
17use zeroize::Zeroize;
18use zstd::stream::encode_all;
19
20use crate::{
21    crypto::key::{Key, Nonce},
22    error::{BackendError, BadHMAC, Compression, Decompression},
23};
24
25/// An unencrypted blob of plaintext.
26///
27/// This type exists to facilitate marshaling data to a serialized, encrypted, representation
28#[derive(Hash, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Zeroize)]
29#[zeroize(drop)]
30pub struct ClearText {
31    /// The cleartext payload
32    pub(crate) payload: Vec<u8>,
33}
34
35impl ClearText {
36    /// Creates a new `Cleartext` from a serializeable object
37    ///
38    /// # Errors
39    ///
40    /// Will return an error if serialization fails
41    pub fn new<T>(item: &T) -> Result<Self, BackendError>
42    where
43        T: Serialize,
44    {
45        // Attempt to serialize the item
46        match serde_cbor::to_vec(item) {
47            Ok(payload) => Ok(ClearText { payload }),
48            Err(_) => {
49                // Do not preserve the underlying serde error, as this may secrets into the logs
50                Err(BackendError::ItemSerialization)
51            }
52        }
53    }
54
55    /// Attempts to create an encrypted version of this `Cleartext` using the provided key. `ZStd`
56    /// compression will be applied to the plain text before encryption with the provided level, if the
57    /// compression option is set with a `Some` value.
58    ///
59    /// # <span style="color:red">**DANGER**</span>
60    ///
61    /// Compression can be incredibly dangerous when combined with encryption, _do not_ set the compression
62    /// flag unless you know what you are doing, and you are 100% sure that compression related attacks do
63    /// not fall into your threat model.
64    ///
65    ///
66    /// # Errors
67    ///
68    /// Will return an error if encryption fails.
69    pub fn encrypt<K>(
70        self,
71        key: &K,
72        compression: Option<i32>,
73    ) -> Result<CipherText<'static>, BackendError>
74    where
75        K: Key,
76    {
77        // Compress the payload, if requested
78        let mut payload = if let Some(level) = compression {
79            let input = Cursor::new(&self.payload);
80            encode_all(input, level).context(Compression)?
81        } else {
82            self.payload.clone()
83        };
84        // Perform the encryption
85        let nonce = Nonce::random();
86        let mut chacha = XChaCha20::new(key.encryption_key(), nonce.nonce());
87        chacha.apply_keystream(&mut payload[..]);
88        // Generate the hmac tag
89        let hmac: [u8; 32] = blake3::keyed_hash(key.hmac_key(), &payload[..]).into();
90        Ok(CipherText {
91            compressed: compression.is_some(),
92            nonce,
93            hmac: hmac.into(),
94            payload: payload.into(),
95        })
96    }
97
98    /// Converts this `Cleartext` back into its original type.
99    ///
100    /// # Errors
101    ///
102    /// Will return `Err(Error::ItemDeserialization)` if the serialization. Be warned, since this
103    /// intentionally erases the underlying `serde` error, it can be difficult to tell if this is due to
104    /// data corruption, or simply calling this method with the wrong type argument.
105    pub fn deserialize<T>(&self) -> Result<T, BackendError>
106    where
107        T: DeserializeOwned,
108    {
109        match serde_cbor::from_slice(&self.payload) {
110            Ok(x) => Ok(x),
111            Err(_) => Err(BackendError::ItemDeserialization),
112        }
113    }
114}
115
116/// An encrypted plaintext, with associated data
117///
118/// This structure contains the payload, encrypted with `XChaCha20`, the nonce that was used to encrypt
119/// it, as well as the HMAC of the encrypted payload. It also includes a flag indicating whether or not
120/// to treat the plaintext as compressed
121#[derive(Debug, Hash, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
122pub struct CipherText<'a> {
123    /// Whether or not the payload is compressed
124    pub(crate) compressed: bool,
125    /// The nonce used for the encryption
126    pub(crate) nonce: Nonce,
127    /// The HMAC tag
128    pub(crate) hmac: RedactedBytes<32>,
129    /// The encrypted payload
130    #[serde(serialize_with = "serde_bytes::serialize")]
131    pub(crate) payload: Cow<'a, [u8]>,
132}
133
134impl CipherText<'_> {
135    /// Attempts to decrypt the `Ciphertext` with the given key, turning it into a `Cleartext`. This will
136    /// also decompress the payload, if it was compressed.
137    ///
138    /// # Errors
139    ///
140    /// Will return:
141    ///   * `Error::BadHMAC` - If the hmac tag fails to validate (decryption failure)
142    ///   * `Error::Decompression` - If compressed data fails to decompress
143    pub fn decrypt<K>(&self, key: &K) -> Result<ClearText, BackendError>
144    where
145        K: Key,
146    {
147        // Verify the mac
148        let hmac = blake3::keyed_hash(key.hmac_key(), &self.payload[..]);
149        ensure!(hmac.eq(&*self.hmac), BadHMAC);
150        // Copy the bytes into a local zeroizing buffer, and decrypt
151        let mut payload = self.payload.to_vec();
152        let mut chacha = XChaCha20::new(key.encryption_key(), self.nonce.nonce());
153        chacha.apply_keystream(&mut payload[..]);
154        // Uncompress the payload, if needed, otherwise, return as is
155        if self.compressed {
156            let input = Cursor::new(&payload[..]);
157            let output = zstd::decode_all(input).context(Decompression)?;
158            // Zeroize compressed payload
159            payload.zeroize();
160            Ok(ClearText { payload: output })
161        } else {
162            Ok(ClearText { payload })
163        }
164    }
165
166    /// Returns true if this `CipherText` is compressed
167    pub fn compressed(&self) -> bool {
168        self.compressed
169    }
170}
171
172/// Unit tests
173#[cfg(test)]
174mod tests {
175    use super::*;
176    use crate::crypto::key::RootKey;
177    /// Tests for the cleartext/ciphertext pair of types
178    mod text {
179        use super::*;
180        /// Test round trip without compression
181        #[test]
182        fn round_trip() {
183            let key = RootKey::random();
184            let item = "The quick brown fox jumps over the lazy dog";
185            let cleartext = ClearText::new(&item).expect("Failed to make cleartext");
186            let ciphertext = cleartext.encrypt(&key, None).expect("Failed to encrypt");
187            let decrypted = ciphertext.decrypt(&key).expect("Failed to decrypt");
188            let decrypted_item: String = decrypted.deserialize().expect("Failed to deserialize");
189            assert_eq!(decrypted_item, item);
190        }
191        /// Test round trip with compression
192        #[test]
193        fn round_trip_compression() {
194            let key = RootKey::random();
195            let item = "The quick brown fox jumps over the lazy dog";
196            let cleartext = ClearText::new(&item).expect("Failed to make cleartext");
197            let ciphertext = cleartext.encrypt(&key, Some(0)).expect("Failed to encrypt");
198            let decrypted = ciphertext.decrypt(&key).expect("Failed to decrypt");
199            let decrypted_item: String = decrypted.deserialize().expect("Failed to deserialize");
200            assert_eq!(decrypted_item, item);
201        }
202        /// Make sure repeated invokations are non-equal
203        #[test]
204        fn repeated_invokations() {
205            let key = RootKey::random();
206            let item = "The quick brown fox jumps over the lazy dog";
207            let cleartext = ClearText::new(&item).expect("Failed to make cleartext");
208            let ciphertext_1 = cleartext
209                .clone()
210                .encrypt(&key, None)
211                .expect("Failed to encrypt");
212            let ciphertext_2 = cleartext.encrypt(&key, None).expect("Failed to encrypt");
213            assert_ne!(ciphertext_1.nonce, ciphertext_2.nonce);
214            assert_ne!(ciphertext_1.payload, ciphertext_2.payload);
215        }
216        /// Make sure corrupted data doesn't decrypt
217        #[test]
218        fn corruption() {
219            let key = RootKey::random();
220            let item = "The quick brown fox jumps over the lazy dog";
221            let cleartext = ClearText::new(&item).expect("Failed to make cleartext");
222            let mut ciphertext = cleartext.encrypt(&key, Some(0)).expect("Failed to encrypt");
223            // Corrupt the first byte of the payload
224            ciphertext.payload.to_mut()[0] = ciphertext.payload[0].wrapping_add(1_u8);
225            let decrypted = ciphertext.decrypt(&key);
226            match decrypted {
227                Ok(_) => panic!("Somehow decrypted corrupted data"),
228                Err(e) => assert!(matches!(e, BackendError::BadHMAC)),
229            }
230        }
231        /// Make sure data can't be decrypted with the wrong key
232        #[test]
233        fn wrong_key() {
234            let key = RootKey::random();
235            let wrong_key = RootKey::random();
236            let item = "The quick brown fox jumps over the lazy dog";
237            let cleartext = ClearText::new(&item).expect("Failed to make cleartext");
238            let ciphertext = cleartext.encrypt(&key, Some(0)).expect("Failed to encrypt");
239            let decrypted = ciphertext.decrypt(&wrong_key);
240            match decrypted {
241                Ok(_) => panic!("Somehow decrypted corrupted data"),
242                Err(e) => assert!(matches!(e, BackendError::BadHMAC)),
243            }
244        }
245    }
246}