Skip to main content

devolutions_crypto/derive_encrypt/
mod.rs

1mod kdf_encrypted_data_v1;
2
3use std::borrow::Borrow;
4use std::convert::TryFrom;
5
6use crate::ciphertext;
7use crate::enums::KdfEncryptedDataSubtype;
8use crate::key_derivation::DerivationParameters;
9use crate::{
10    CiphertextVersion, DataType, Error, Header, HeaderType, KdfEncryptedDataVersion, Result,
11};
12
13use kdf_encrypted_data_v1::KdfEncryptedDataV1;
14
15#[cfg(feature = "fuzz")]
16use arbitrary::Arbitrary;
17
18/// A blob that stores key derivation parameters alongside a symmetric ciphertext,
19/// allowing decryption with only the original password.
20///
21/// The serialized format contains the [`DerivationParameters`](crate::key_derivation::DerivationParameters)
22/// used to derive the key and the resulting [`Ciphertext`](crate::ciphertext::Ciphertext),
23/// so no external state is required to decrypt.
24#[derive(Clone, Debug)]
25#[cfg_attr(feature = "fuzz", derive(Arbitrary))]
26pub struct KdfEncryptedData {
27    pub(crate) header: Header<KdfEncryptedData>,
28    payload: KdfEncryptedDataPayload,
29}
30
31impl HeaderType for KdfEncryptedData {
32    type Version = KdfEncryptedDataVersion;
33    type Subtype = KdfEncryptedDataSubtype;
34
35    fn data_type() -> DataType {
36        DataType::KdfEncryptedData
37    }
38}
39
40#[derive(Clone, Debug)]
41#[cfg_attr(feature = "fuzz", derive(Arbitrary))]
42enum KdfEncryptedDataPayload {
43    V1(KdfEncryptedDataV1),
44}
45
46/// Encrypts `data` with a key derived from `password` and returns a self-contained blob.
47///
48/// Equivalent to calling [`encrypt_with_password_and_aad`] with an empty AAD.
49///
50/// # Arguments
51///  * `data` - The plaintext data to encrypt.
52///  * `password` - The password from which the encryption key is derived.
53///  * `derivation_parameters` - Pre-built key derivation parameters.
54///  * `ciphertext_version` - Cipher to use. `CiphertextVersion::Latest` is recommended.
55/// # Returns
56/// Returns a [`KdfEncryptedData`] blob containing the key derivation parameters and the ciphertext.
57/// # Example
58/// ```rust
59/// use devolutions_crypto::derive_encrypt::encrypt_with_password;
60/// use devolutions_crypto::key_derivation::Argon2;
61/// use devolutions_crypto::CiphertextVersion;
62///
63/// let params = Argon2::new().parameters();
64/// let blob = encrypt_with_password(b"secret", b"password", params, CiphertextVersion::Latest).unwrap();
65/// let plaintext = blob.decrypt_with_password(b"password").unwrap();
66/// assert_eq!(plaintext, b"secret");
67/// ```
68pub fn encrypt_with_password(
69    data: &[u8],
70    password: &[u8],
71    derivation_parameters: DerivationParameters,
72    ciphertext_version: CiphertextVersion,
73) -> Result<KdfEncryptedData> {
74    encrypt_with_password_and_aad(
75        data,
76        password,
77        [].as_slice(),
78        derivation_parameters,
79        ciphertext_version,
80    )
81}
82
83/// Encrypts `data` with a key derived from `password` and authenticates `aad`.
84///
85/// # Arguments
86///  * `data` - The plaintext data to encrypt.
87///  * `password` - The password from which the encryption key is derived.
88///  * `aad` - Additional Authenticated Data bound to the ciphertext; must be provided unchanged on decryption.
89///  * `derivation_parameters` - Pre-built key derivation parameters (includes the salt). Use
90///    [`Argon2::new().derive(password)`](crate::key_derivation::Argon2::derive) to generate them.
91///  * `ciphertext_version` - Cipher to use. `CiphertextVersion::Latest` is recommended.
92/// # Returns
93/// Returns a [`KdfEncryptedData`] blob containing the key derivation parameters and the ciphertext.
94pub fn encrypt_with_password_and_aad(
95    data: &[u8],
96    password: &[u8],
97    aad: &[u8],
98    derivation_parameters: DerivationParameters,
99    ciphertext_version: CiphertextVersion,
100) -> Result<KdfEncryptedData> {
101    let mut header: Header<KdfEncryptedData> = Header::default();
102    header.version = KdfEncryptedDataVersion::V1;
103
104    let secret_key = derivation_parameters.derive(password)?;
105    let ciphertext =
106        ciphertext::encrypt_with_secret_key_and_aad(data, &secret_key, aad, ciphertext_version)?;
107
108    Ok(KdfEncryptedData {
109        header,
110        payload: KdfEncryptedDataPayload::V1(KdfEncryptedDataV1 {
111            derivation_parameters,
112            ciphertext,
113        }),
114    })
115}
116
117impl KdfEncryptedData {
118    /// Decrypts this blob using `password`.
119    ///
120    /// Equivalent to calling [`decrypt_with_password_and_aad`](Self::decrypt_with_password_and_aad) with an empty AAD.
121    ///
122    /// # Arguments
123    ///  * `password` - The password used during encryption.
124    /// # Returns
125    /// Returns the decrypted plaintext, or an error if the password is wrong or the blob is invalid.
126    pub fn decrypt_with_password(&self, password: &[u8]) -> Result<Vec<u8>> {
127        self.decrypt_with_password_and_aad(password, [].as_slice())
128    }
129
130    /// Decrypts this blob using `password`, verifying `aad`.
131    ///
132    /// # Arguments
133    ///  * `password` - The password used during encryption.
134    ///  * `aad` - The same Additional Authenticated Data that was provided during encryption.
135    /// # Returns
136    /// Returns the decrypted plaintext, or an error if the password is wrong, the AAD does not match,
137    /// or the blob is invalid.
138    pub fn decrypt_with_password_and_aad(&self, password: &[u8], aad: &[u8]) -> Result<Vec<u8>> {
139        match &self.payload {
140            KdfEncryptedDataPayload::V1(payload) => {
141                let secret_key = payload.derivation_parameters.derive(password)?;
142                payload
143                    .ciphertext
144                    .decrypt_with_secret_key_and_aad(&secret_key, aad)
145            }
146        }
147    }
148}
149
150impl From<KdfEncryptedData> for Vec<u8> {
151    fn from(data: KdfEncryptedData) -> Self {
152        let mut header: Self = data.header.borrow().into();
153        let mut payload: Self = data.payload.into();
154        header.append(&mut payload);
155        header
156    }
157}
158
159impl From<KdfEncryptedDataPayload> for Vec<u8> {
160    fn from(data: KdfEncryptedDataPayload) -> Self {
161        match data {
162            KdfEncryptedDataPayload::V1(v1) => v1.into(),
163        }
164    }
165}
166
167impl TryFrom<&[u8]> for KdfEncryptedData {
168    type Error = Error;
169
170    fn try_from(data: &[u8]) -> Result<Self> {
171        if data.len() < Header::len() {
172            return Err(Error::InvalidLength);
173        }
174
175        let header = Header::try_from(&data[0..Header::len()])?;
176
177        let payload = match header.version {
178            KdfEncryptedDataVersion::V1 => {
179                KdfEncryptedDataPayload::V1(KdfEncryptedDataV1::try_from(&data[Header::len()..])?)
180            }
181            // `Latest` (discriminant 0) is a dispatch sentinel for the encrypt path only;
182            // it is never written to the wire. Blobs on disk always carry a concrete version.
183            KdfEncryptedDataVersion::Latest => return Err(Error::UnknownVersion),
184        };
185
186        Ok(Self { header, payload })
187    }
188}
189
190#[cfg(test)]
191mod tests {
192    use crate::key_derivation::Argon2;
193    use crate::utils::validate_header;
194    use crate::Pbkdf2;
195
196    use super::*;
197
198    #[test]
199    fn derive_encrypt_roundtrip_latest() {
200        let data = b"derive encrypt payload";
201        let password = b"a very strong password";
202        let aad = b"public data";
203
204        let params = Argon2::new().parameters();
205        let wrapped =
206            encrypt_with_password_and_aad(data, password, aad, params, CiphertextVersion::Latest)
207                .unwrap();
208
209        let wrapped_raw: Vec<u8> = wrapped.into();
210        let wrapped = KdfEncryptedData::try_from(wrapped_raw.as_slice()).unwrap();
211        let decrypted = wrapped
212            .decrypt_with_password_and_aad(password, aad)
213            .unwrap();
214
215        assert_eq!(decrypted, data);
216    }
217
218    #[test]
219    fn derive_encrypt_pbkdf2() {
220        let data = b"derive encrypt payload";
221        let password = b"a very strong password";
222
223        let params = Pbkdf2::with_params(10).parameters().unwrap();
224        let wrapped =
225            encrypt_with_password(data, password, params, CiphertextVersion::Latest).unwrap();
226
227        assert!(wrapped.decrypt_with_password(b"wrong password").is_err());
228    }
229
230    #[test]
231    fn derive_encrypt_wrong_password_fails() {
232        let data = b"derive encrypt payload";
233        let password = b"a very strong password";
234
235        let params = Argon2::new().parameters();
236        let wrapped =
237            encrypt_with_password(data, password, params, CiphertextVersion::Latest).unwrap();
238
239        assert!(wrapped.decrypt_with_password(b"wrong password").is_err());
240    }
241
242    #[test]
243    fn derive_encrypt_wrong_aad_fails() {
244        let data = b"derive encrypt payload";
245        let password = b"a very strong password";
246
247        let params = Argon2::new().parameters();
248        let wrapped = encrypt_with_password_and_aad(
249            data,
250            password,
251            b"the right aad",
252            params,
253            CiphertextVersion::Latest,
254        )
255        .unwrap();
256
257        assert!(wrapped
258            .decrypt_with_password_and_aad(password, b"wrong aad")
259            .is_err());
260    }
261
262    #[test]
263    fn validate_header_accepts_derive_encrypt() {
264        let password = b"a very strong password";
265        let params = Argon2::new().parameters();
266        let wrapped = encrypt_with_password(
267            b"derive encrypt payload",
268            password,
269            params,
270            CiphertextVersion::Latest,
271        )
272        .unwrap();
273
274        let wrapped_raw: Vec<u8> = wrapped.into();
275        assert!(validate_header(
276            wrapped_raw.as_slice(),
277            DataType::KdfEncryptedData
278        ));
279        assert!(!validate_header(
280            wrapped_raw.as_slice(),
281            DataType::Ciphertext
282        ));
283    }
284}