1use 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
59impl 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 #[serde(with = "argon2_params_helper")]
77 params: Params,
78
79 #[serde(with = "argon2_algorithm_helper")]
81 algorithm: Algorithm,
82
83 #[serde(with = "argon2_version_helper")]
85 version: Version,
86
87 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 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 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 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 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 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 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 pub kdf_info: KdfInfo,
345
346 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}