nym_store_cipher/
lib.rs

1// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
2// SPDX-License-Identifier: Apache-2.0
3
4use aes_gcm::aead::{Aead, Nonce};
5use aes_gcm::{AeadCore, AeadInPlace, KeyInit};
6use rand::{thread_rng, CryptoRng, Fill, RngCore};
7use serde::{Deserialize, Serialize};
8use serde_helpers::{argon2_algorithm_helper, argon2_params_helper, argon2_version_helper};
9use thiserror::Error;
10use zeroize::{Zeroize, ZeroizeOnDrop};
11
12pub use aes_gcm::Aes256Gcm;
13pub use aes_gcm::{Key, KeySizeUser};
14pub use argon2::{Algorithm, Argon2, Params, Version};
15pub use generic_array::typenum::Unsigned;
16
17mod serde_helpers;
18
19pub const CURRENT_VERSION: u8 = 1;
20pub const ARGON2_SALT_SIZE: usize = 16;
21pub const AES256GCM_NONCE_SIZE: usize = 12;
22
23const VERIFICATION_PHRASE: &[u8] = &[0u8; 32];
24
25#[derive(Debug, Error)]
26pub enum Error {
27    #[error("Unsupported cipher")]
28    UnsupportedCipher,
29
30    #[error("failed to encrypt/decrypt provided data: {cause}")]
31    AesFailure { cause: aes_gcm::Error },
32
33    #[error("failed to expand the passphrase: {cause}")]
34    Argon2Failure { cause: argon2::Error },
35
36    #[cfg(feature = "json")]
37    #[error("failed to serialize/deserialize JSON: {source}")]
38    SerdeJsonFailure {
39        #[from]
40        source: serde_json::Error,
41    },
42
43    #[error("failed to generate random bytes: {source}")]
44    RandomError {
45        #[from]
46        source: rand::Error,
47    },
48
49    #[error("the received ciphertext was encrypted with different store version ({received}). The current version is {CURRENT_VERSION}")]
50    VersionMismatch { received: u8 },
51
52    #[error("the decoded verification phrase did not match the expected value")]
53    VerificationPhraseMismatch,
54
55    #[error("could not import the store - the provided passphrase was invalid")]
56    InvalidImportPassphrase,
57}
58
59// it's weird that this couldn't be auto-derived with a `#[from]`...
60impl From<aes_gcm::Error> for Error {
61    fn from(cause: aes_gcm::Error) -> Self {
62        Error::AesFailure { cause }
63    }
64}
65
66impl From<argon2::Error> for Error {
67    fn from(cause: argon2::Error) -> Self {
68        Error::Argon2Failure { cause }
69    }
70}
71
72#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
73pub enum KdfInfo {
74    Argon2 {
75        /// The Argon2 parameters that were used when deriving the store key.
76        #[serde(with = "argon2_params_helper")]
77        params: Params,
78
79        /// The specific Argon2 algorithm variant used when deriving the store key.
80        #[serde(with = "argon2_algorithm_helper")]
81        algorithm: Algorithm,
82
83        /// The specific version of the Argon2 algorithm used when deriving the store key.
84        #[serde(with = "argon2_version_helper")]
85        version: Version,
86
87        /// The salt that was used when the passphrase was expanded into a store key.
88        kdf_salt: [u8; ARGON2_SALT_SIZE],
89    },
90}
91
92impl KdfInfo {
93    pub fn expand_key<C>(&self, passphrase: &[u8]) -> Result<Key<C>, Error>
94    where
95        C: KeySizeUser,
96    {
97        match self {
98            KdfInfo::Argon2 {
99                params,
100                algorithm,
101                version,
102                kdf_salt,
103            } => argon2_derive_cipher_key::<C>(
104                passphrase,
105                kdf_salt,
106                &[],
107                params.clone(),
108                *algorithm,
109                *version,
110            ),
111        }
112    }
113
114    pub fn new_with_default_settings() -> Result<Self, Error> {
115        let kdf_salt = Self::random_salt()?;
116        Ok(KdfInfo::Argon2 {
117            params: Default::default(),
118            algorithm: Default::default(),
119            version: Default::default(),
120            kdf_salt,
121        })
122    }
123
124    pub fn random_salt() -> Result<[u8; ARGON2_SALT_SIZE], Error> {
125        let mut rng = thread_rng();
126        Self::random_salt_with_rng(&mut rng)
127    }
128
129    pub fn random_salt_with_rng<R: RngCore + CryptoRng>(
130        rng: &mut R,
131    ) -> Result<[u8; ARGON2_SALT_SIZE], Error> {
132        let mut salt = [0u8; ARGON2_SALT_SIZE];
133        salt.try_fill(rng)?;
134        Ok(salt)
135    }
136}
137
138#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
139pub enum CiphertextInfo {
140    Aes256Gcm {
141        /// The nonce that was used to encrypt the ciphertext.
142        nonce: [u8; AES256GCM_NONCE_SIZE],
143        ciphertext: Vec<u8>,
144    },
145}
146
147impl CiphertextInfo {
148    pub fn nonce<C>(&self) -> &Nonce<C>
149    where
150        C: AeadCore,
151    {
152        match self {
153            CiphertextInfo::Aes256Gcm { nonce, .. } => Nonce::<C>::from_slice(nonce),
154        }
155    }
156
157    pub fn ciphertext(&self) -> &[u8] {
158        match self {
159            CiphertextInfo::Aes256Gcm { ciphertext, .. } => ciphertext,
160        }
161    }
162}
163
164#[derive(Zeroize, ZeroizeOnDrop)]
165pub struct StoreCipher<C = Aes256Gcm>
166where
167    C: KeySizeUser,
168{
169    key: Key<C>,
170
171    #[zeroize(skip)]
172    kdf_info: KdfInfo,
173}
174
175impl StoreCipher<Aes256Gcm> {
176    pub fn import_aes256gcm(
177        passphrase: &[u8],
178        exported: ExportedStoreCipher,
179    ) -> Result<Self, Error> {
180        // that's a terrible interface, but we can refactor it later
181        if !matches!(exported.ciphertext_info, CiphertextInfo::Aes256Gcm { .. }) {
182            return Err(Error::UnsupportedCipher);
183        }
184
185        let mut key = exported.kdf_info.expand_key::<Aes256Gcm>(passphrase)?;
186
187        // check if correct key was derived
188        let Ok(plaintext) = Aes256Gcm::new(&key).decrypt(
189            exported.ciphertext_info.nonce::<Aes256Gcm>(),
190            exported.ciphertext_info.ciphertext(),
191        ) else {
192            key.zeroize();
193            return Err(Error::InvalidImportPassphrase);
194        };
195
196        // if we successfully decrypted aes256gcm ciphertext, it's almost certainly correct
197        // otherwise the tag wouldn't match, but let's do the sanity sake
198        if plaintext != VERIFICATION_PHRASE {
199            key.zeroize();
200            return Err(Error::VerificationPhraseMismatch);
201        }
202
203        Ok(StoreCipher {
204            key,
205            kdf_info: exported.kdf_info,
206        })
207    }
208
209    pub fn export_aes256gcm(&self) -> Result<ExportedStoreCipher, Error> {
210        let verification_ciphertext = self.encrypt_data_ref(VERIFICATION_PHRASE)?;
211
212        Ok(ExportedStoreCipher {
213            kdf_info: self.kdf_info.clone(),
214            ciphertext_info: CiphertextInfo::Aes256Gcm {
215                // the unwrap is fine, otherwise it implies we've been using incorrect nonces all along!
216                nonce: verification_ciphertext.nonce.try_into().unwrap(),
217                ciphertext: verification_ciphertext.ciphertext,
218            },
219        })
220    }
221
222    pub fn new_aes256gcm(passphrase: &[u8], kdf_info: KdfInfo) -> Result<Self, Error> {
223        Self::new(passphrase, kdf_info)
224    }
225}
226
227impl<C: KeySizeUser + KeyInit> StoreCipher<C>
228where
229    C: KeySizeUser + KeyInit,
230{
231    pub fn new(passphrase: &[u8], kdf_info: KdfInfo) -> Result<Self, Error> {
232        let key = kdf_info.expand_key::<C>(passphrase)?;
233        Ok(StoreCipher { key, kdf_info })
234    }
235
236    pub fn new_with_default_kdf(passphrase: &[u8]) -> Result<Self, Error> {
237        let kdf_info = KdfInfo::new_with_default_settings()?;
238        Self::new(passphrase, kdf_info)
239    }
240
241    #[cfg(feature = "json")]
242    pub fn encrypt_json_value<T: Serialize>(&self, data: &T) -> Result<EncryptedData, Error>
243    where
244        C: AeadInPlace,
245    {
246        let raw = serde_json::to_vec(data)?;
247        self.encrypt_data(raw)
248    }
249
250    // Unless you know what you're doing, use `Self::encrypt_data` instead.
251    // As the caller of this method needs to make sure to correctly dispose of the original plaintext.
252    pub fn encrypt_data_ref(&self, data: &[u8]) -> Result<EncryptedData, Error>
253    where
254        C: Aead,
255    {
256        let nonce = Self::random_nonce()?;
257
258        let cipher = C::new(&self.key);
259        let ciphertext = cipher.encrypt(&nonce, data)?;
260
261        Ok(EncryptedData {
262            version: CURRENT_VERSION,
263            ciphertext,
264            nonce: nonce.to_vec(),
265        })
266    }
267
268    pub fn encrypt_data(&self, mut data: Vec<u8>) -> Result<EncryptedData, Error>
269    where
270        C: AeadInPlace,
271    {
272        let nonce = Self::random_nonce()?;
273
274        let cipher = C::new(&self.key);
275        cipher.encrypt_in_place(&nonce, &[], &mut data)?;
276
277        Ok(EncryptedData {
278            version: CURRENT_VERSION,
279            ciphertext: data,
280            nonce: nonce.to_vec(),
281        })
282    }
283
284    #[cfg(feature = "json")]
285    pub fn decrypt_json_value<T: serde::de::DeserializeOwned>(
286        &self,
287        data: EncryptedData,
288    ) -> Result<T, Error>
289    where
290        C: AeadInPlace,
291    {
292        let plaintext = zeroize::Zeroizing::new(self.decrypt_data(data)?);
293        let value = serde_json::from_slice(&plaintext)?;
294        Ok(value)
295    }
296
297    pub fn decrypt_data_unchecked(&self, data: EncryptedData) -> Result<Vec<u8>, Error>
298    where
299        C: Aead,
300    {
301        let cipher = C::new(&self.key);
302        let plaintext = cipher.decrypt(
303            Nonce::<C>::from_slice(&data.nonce),
304            data.ciphertext.as_ref(),
305        )?;
306        Ok(plaintext)
307    }
308
309    pub fn decrypt_data(&self, data: EncryptedData) -> Result<Vec<u8>, Error>
310    where
311        C: Aead,
312    {
313        if data.version != CURRENT_VERSION {
314            return Err(Error::VersionMismatch {
315                received: data.version,
316            });
317        }
318
319        self.decrypt_data_unchecked(data)
320    }
321
322    pub fn random_nonce() -> Result<Nonce<C>, Error>
323    where
324        C: AeadCore,
325    {
326        let mut rng = thread_rng();
327        Self::random_nonce_with_rng(&mut rng)
328    }
329
330    pub fn random_nonce_with_rng<R: RngCore + CryptoRng>(rng: &mut R) -> Result<Nonce<C>, Error>
331    where
332        C: AeadCore,
333    {
334        let mut nonce = Nonce::<C>::default();
335        nonce.try_fill(rng)?;
336        Ok(nonce)
337    }
338}
339
340#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
341pub struct ExportedStoreCipher {
342    /// Info about the key derivation method that was used to expand the
343    /// passphrase into an encryption key.
344    pub kdf_info: KdfInfo,
345
346    /// The ciphertext of known plaintext and additional data that is needed to
347    /// verify correct key derivation and cipher choice.
348    pub ciphertext_info: CiphertextInfo,
349}
350
351#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
352pub struct EncryptedData {
353    pub version: u8,
354    pub ciphertext: Vec<u8>,
355    pub nonce: Vec<u8>,
356}
357
358pub fn argon2_derive_cipher_key<C>(
359    passphrase: &[u8],
360    salt: &[u8],
361    pepper: &[u8],
362    params: Params,
363    algorithm: Algorithm,
364    version: Version,
365) -> Result<Key<C>, Error>
366where
367    C: KeySizeUser,
368{
369    let argon2 = if pepper.is_empty() {
370        Argon2::new(algorithm, version, params)
371    } else {
372        Argon2::new_with_secret(pepper, algorithm, version, params)?
373    };
374
375    let mut key = Key::<C>::default();
376    argon2.hash_password_into(passphrase, salt, &mut key)?;
377
378    Ok(key)
379}