Skip to main content

keepass/
config.rs

1//! Configuration options for how to compress and encrypt databases
2use hex_literal::hex;
3use thiserror::Error;
4
5use std::convert::TryFrom;
6
7pub use crate::format::DatabaseVersion;
8
9#[cfg(feature = "save_kdbx4")]
10use crate::crypt::ciphers::Cipher;
11use crate::{
12    compression,
13    crypt::{
14        ciphers::{self},
15        kdf, CryptographyError,
16    },
17    format::{variant_dictionary::VariantDictionaryError, KDBX4_CURRENT_MINOR_VERSION},
18};
19
20pub use crate::format::variant_dictionary::{VariantDictionary, VariantDictionaryValue};
21
22const _CIPHERSUITE_AES128: [u8; 16] = hex!("61ab05a1946441c38d743a563df8dd35");
23const CIPHERSUITE_AES256: [u8; 16] = hex!("31c1f2e6bf714350be5805216afc5aff");
24const CIPHERSUITE_TWOFISH: [u8; 16] = hex!("ad68f29f576f4bb9a36ad47af965346c");
25const CIPHERSUITE_CHACHA20: [u8; 16] = hex!("d6038a2b8b6f4cb5a524339a31dbb59a");
26
27// Internal IDs for the ciphers
28const PLAIN: u32 = 0;
29const SALSA_20: u32 = 2;
30const CHA_CHA_20: u32 = 3;
31
32/// Configuration of how a database should be stored
33#[derive(Debug, Clone, PartialEq, Eq)]
34#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
35#[non_exhaustive]
36pub struct DatabaseConfig {
37    /// Version of the outer database file
38    pub version: DatabaseVersion,
39
40    /// What encryption to use for the outer encryption
41    pub outer_cipher_config: OuterCipherConfig,
42
43    /// What algorithm to use to compress the inner data
44    pub compression_config: CompressionConfig,
45
46    /// What encryption to use for protected fields inside the database
47    pub inner_cipher_config: InnerCipherConfig,
48
49    /// Settings for the Key Derivation Function (KDF)
50    pub kdf_config: KdfConfig,
51
52    /// Custom data of plugins/ports.
53    pub public_custom_data: Option<VariantDictionary>,
54}
55
56/// Sensible default configuration for new databases
57impl Default for DatabaseConfig {
58    fn default() -> Self {
59        Self {
60            version: DatabaseVersion::KDB4(KDBX4_CURRENT_MINOR_VERSION),
61            outer_cipher_config: OuterCipherConfig::AES256,
62            compression_config: CompressionConfig::GZip,
63            inner_cipher_config: InnerCipherConfig::ChaCha20,
64            kdf_config: KdfConfig::Argon2 {
65                iterations: 50,
66                memory: 1024 * 1024,
67                parallelism: 4,
68                version: argon2::Version::Version13,
69            },
70            public_custom_data: None,
71        }
72    }
73}
74
75/// Choices for outer encryption
76#[derive(Debug, Clone, PartialEq, Eq)]
77#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
78#[non_exhaustive]
79pub enum OuterCipherConfig {
80    /// Encrypt the database with AES-256 in CBC mode.
81    AES256,
82
83    /// Encrypt the database with Twofish in CBC mode.
84    Twofish,
85
86    /// Encrypt the database with ChaCha20.
87    ChaCha20,
88}
89
90impl OuterCipherConfig {
91    pub(crate) fn get_cipher(
92        &self,
93        key: &[u8],
94        iv: &[u8],
95    ) -> Result<Box<dyn ciphers::Cipher>, CryptographyError> {
96        match self {
97            OuterCipherConfig::AES256 => Ok(Box::new(ciphers::AES256Cipher::new(key, iv)?)),
98            OuterCipherConfig::Twofish => Ok(Box::new(ciphers::TwofishCipher::new(key, iv)?)),
99            OuterCipherConfig::ChaCha20 => Ok(Box::new(ciphers::ChaCha20Cipher::new_key_iv(key, iv)?)),
100        }
101    }
102
103    #[cfg(feature = "save_kdbx4")]
104    pub(crate) fn get_iv_size(&self) -> usize {
105        match self {
106            OuterCipherConfig::AES256 => ciphers::AES256Cipher::iv_size(),
107            OuterCipherConfig::Twofish => ciphers::TwofishCipher::iv_size(),
108            OuterCipherConfig::ChaCha20 => ciphers::ChaCha20Cipher::iv_size(),
109        }
110    }
111
112    #[cfg(feature = "save_kdbx4")]
113    pub(crate) fn dump(&self) -> [u8; 16] {
114        match self {
115            OuterCipherConfig::AES256 => CIPHERSUITE_AES256,
116            OuterCipherConfig::Twofish => CIPHERSUITE_TWOFISH,
117            OuterCipherConfig::ChaCha20 => CIPHERSUITE_CHACHA20,
118        }
119    }
120}
121
122impl TryFrom<&[u8]> for OuterCipherConfig {
123    type Error = OuterCipherConfigError;
124    fn try_from(v: &[u8]) -> Result<OuterCipherConfig, Self::Error> {
125        if v == CIPHERSUITE_AES256 {
126            Ok(OuterCipherConfig::AES256)
127        } else if v == CIPHERSUITE_TWOFISH {
128            Ok(OuterCipherConfig::Twofish)
129        } else if v == CIPHERSUITE_CHACHA20 {
130            Ok(OuterCipherConfig::ChaCha20)
131        } else {
132            Err(OuterCipherConfigError::InvalidOuterCipherID { cid: v.to_vec() })
133        }
134    }
135}
136
137/// Errors with the configuration of the outer encryption
138#[derive(Debug, Error)]
139#[non_exhaustive]
140pub enum OuterCipherConfigError {
141    /// Errors with cryptographic operations
142    #[error(transparent)]
143    Cryptography(#[from] CryptographyError),
144
145    /// The identifier for the outer cipher specified in the database is invalid
146    #[error("Invalid outer cipher ID: {:?}", cid)]
147    InvalidOuterCipherID {
148        /// The invalid cipher ID that was encountered, as a byte vector
149        cid: Vec<u8>,
150    },
151}
152
153/// Choices for encrypting protected values inside of databases
154#[derive(Debug, Clone, PartialEq, Eq)]
155#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
156#[non_exhaustive]
157pub enum InnerCipherConfig {
158    /// Don't encrypt proected values
159    Plain,
160
161    /// Encrypt protected values with Salsa20
162    Salsa20,
163
164    /// Encrypt protected values with ChaCha20
165    ChaCha20,
166}
167
168impl InnerCipherConfig {
169    pub(crate) fn get_cipher(
170        &self,
171        protected_stream_key: &[u8],
172    ) -> Result<Box<dyn ciphers::Cipher>, CryptographyError> {
173        match self {
174            InnerCipherConfig::Plain => Ok(Box::new(ciphers::PlainCipher::new(protected_stream_key)?)),
175            InnerCipherConfig::Salsa20 => Ok(Box::new(ciphers::Salsa20Cipher::new(protected_stream_key)?)),
176            InnerCipherConfig::ChaCha20 => Ok(Box::new(ciphers::ChaCha20Cipher::new(protected_stream_key)?)),
177        }
178    }
179
180    #[cfg(feature = "save_kdbx4")]
181    pub(crate) fn dump(&self) -> u32 {
182        match self {
183            InnerCipherConfig::Plain => PLAIN,
184            InnerCipherConfig::Salsa20 => SALSA_20,
185            InnerCipherConfig::ChaCha20 => CHA_CHA_20,
186        }
187    }
188
189    #[cfg(feature = "save_kdbx4")]
190    pub(crate) fn get_key_size(&self) -> usize {
191        match self {
192            InnerCipherConfig::Plain => ciphers::PlainCipher::key_size(),
193            InnerCipherConfig::Salsa20 => ciphers::Salsa20Cipher::key_size(),
194            InnerCipherConfig::ChaCha20 => ciphers::ChaCha20Cipher::key_size(),
195        }
196    }
197}
198
199impl TryFrom<u32> for InnerCipherConfig {
200    type Error = InnerCipherConfigError;
201
202    fn try_from(v: u32) -> Result<InnerCipherConfig, Self::Error> {
203        match v {
204            PLAIN => Ok(InnerCipherConfig::Plain),
205            SALSA_20 => Ok(InnerCipherConfig::Salsa20),
206            CHA_CHA_20 => Ok(InnerCipherConfig::ChaCha20),
207            _ => Err(InnerCipherConfigError::InvalidInnerCipherID { cid: v }),
208        }
209    }
210}
211
212/// Errors with the configuration of the inner encryption
213#[derive(Debug, Error)]
214#[non_exhaustive]
215pub enum InnerCipherConfigError {
216    /// A cryptographic error occurred while configuring the inner cipher
217    #[error(transparent)]
218    Cryptography(#[from] CryptographyError),
219
220    /// The identifier for the inner cipher specified in the database is invalid
221    #[error("Invalid inner cipher ID: {}", cid)]
222    InvalidInnerCipherID {
223        /// The invalid cipher ID that was encountered
224        cid: u32,
225    },
226}
227
228// Name of the KDF fields in the variant dictionaries.
229const KDF_ID: &str = "$UUID";
230// KDF fields used by Argon2.
231const KDF_MEMORY: &str = "M";
232const KDF_SALT: &str = "S";
233const KDF_ITERATIONS: &str = "I";
234const KDF_PARALLELISM: &str = "P";
235const KDF_VERSION: &str = "V";
236// KDF fields used by AES.
237const KDF_SEED: &str = "S";
238const KDF_ROUNDS: &str = "R";
239
240/// Choices for Key Derivation Functions (KDFs)
241#[derive(Debug, Clone, PartialEq, Eq)]
242#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
243#[non_exhaustive]
244pub enum KdfConfig {
245    /// Derive keys with repeated AES encryption
246    Aes {
247        /// The number of rounds of AES encryption to perform when deriving keys
248        rounds: u64,
249    },
250    /// Derive keys with Argon2d
251    Argon2 {
252        /// The number of iterations to perform when deriving keys
253        iterations: u64,
254
255        /// The amount of memory (in bytes) to use when deriving keys
256        ///
257        /// KDBX stores this value in bytes, while the underlying Argon2 implementation
258        /// expects KiB and converts internally when deriving keys.
259        memory: u64,
260
261        /// The degree of parallelism to use when deriving keys
262        parallelism: u32,
263
264        /// The version of the Argon2 algorithm to use when deriving keys
265        #[cfg_attr(feature = "serialization", serde(serialize_with = "serialize_argon2_version"))]
266        version: argon2::Version,
267    },
268
269    /// Derive keys with Argon2id
270    Argon2id {
271        /// The number of iterations to perform when deriving keys
272        iterations: u64,
273
274        /// The amount of memory (in bytes) to use when deriving keys
275        ///
276        /// KDBX stores this value in bytes, while the underlying Argon2 implementation
277        /// expects KiB and converts internally when deriving keys.
278        memory: u64,
279
280        /// The degree of parallelism to use when deriving keys
281        parallelism: u32,
282
283        /// The version of the Argon2 algorithm to use when deriving keys
284        #[cfg_attr(feature = "serialization", serde(serialize_with = "serialize_argon2_version"))]
285        version: argon2::Version,
286    },
287}
288
289#[cfg(feature = "serialization")]
290fn serialize_argon2_version<S: serde::Serializer>(
291    version: &argon2::Version,
292    serializer: S,
293) -> Result<<S as serde::Serializer>::Ok, <S as serde::Serializer>::Error> {
294    serializer.serialize_u32(version.as_u32())
295}
296
297impl KdfConfig {
298    #[cfg(feature = "save_kdbx4")]
299    fn seed_size(&self) -> usize {
300        match self {
301            KdfConfig::Aes { .. } => 32,
302            KdfConfig::Argon2 { .. } => 32,
303            KdfConfig::Argon2id { .. } => 32,
304        }
305    }
306
307    /// For writing out a database, generate a new KDF seed from the config and return the KDF
308    /// and the generated seed
309    #[cfg(feature = "save_kdbx4")]
310    pub(crate) fn get_kdf_and_seed(&self) -> Result<(Box<dyn kdf::Kdf>, Vec<u8>), getrandom::Error> {
311        let mut kdf_seed = vec![0; self.seed_size()];
312        getrandom::fill(&mut kdf_seed)?;
313
314        let kdf = self.get_kdf_seeded(&kdf_seed);
315
316        Ok((kdf, kdf_seed))
317    }
318
319    /// For reading a database, generate a KDF from the KDF config and a provided seed
320    pub(crate) fn get_kdf_seeded(&self, seed: &[u8]) -> Box<dyn kdf::Kdf> {
321        match self {
322            KdfConfig::Aes { rounds } => Box::new(kdf::AesKdf {
323                seed: seed.to_vec(),
324                rounds: *rounds,
325            }),
326            KdfConfig::Argon2 {
327                memory,
328                iterations,
329                parallelism,
330                version,
331            } => Box::new(kdf::Argon2Kdf {
332                memory: *memory,
333                salt: seed.to_vec(),
334                iterations: *iterations,
335                parallelism: *parallelism,
336                version: *version,
337                variant: argon2::Variant::Argon2d,
338            }),
339            KdfConfig::Argon2id {
340                memory,
341                iterations,
342                parallelism,
343                version,
344            } => Box::new(kdf::Argon2Kdf {
345                memory: *memory,
346                salt: seed.to_vec(),
347                iterations: *iterations,
348                parallelism: *parallelism,
349                version: *version,
350                variant: argon2::Variant::Argon2id,
351            }),
352        }
353    }
354
355    #[cfg(feature = "save_kdbx4")]
356    pub(crate) fn to_variant_dictionary(&self, seed: &[u8]) -> VariantDictionary {
357        let mut vd = VariantDictionary::new();
358
359        match self {
360            KdfConfig::Aes { rounds } => {
361                // always use the KDBX3 AES KDF UUID for compatibility with other libraries
362                vd.set(KDF_ID, KDF_AES_KDBX3.to_vec());
363                vd.set(KDF_ROUNDS, *rounds);
364                vd.set(KDF_SEED, seed.to_vec());
365            }
366            KdfConfig::Argon2 {
367                memory,
368                iterations,
369                parallelism,
370                version,
371            } => {
372                vd.set(KDF_ID, KDF_ARGON2.to_vec());
373                vd.set(KDF_MEMORY, *memory);
374                vd.set(KDF_SALT, seed.to_vec());
375                vd.set(KDF_ITERATIONS, *iterations);
376                vd.set(KDF_PARALLELISM, *parallelism);
377                vd.set(KDF_VERSION, version.as_u32());
378            }
379            KdfConfig::Argon2id {
380                memory,
381                iterations,
382                parallelism,
383                version,
384            } => {
385                vd.set(KDF_ID, KDF_ARGON2ID.to_vec());
386                vd.set(KDF_MEMORY, *memory);
387                vd.set(KDF_SALT, seed.to_vec());
388                vd.set(KDF_ITERATIONS, *iterations);
389                vd.set(KDF_PARALLELISM, *parallelism);
390                vd.set(KDF_VERSION, version.as_u32());
391            }
392        }
393
394        vd
395    }
396}
397
398const KDF_AES_KDBX3: [u8; 16] = hex!("c9d9f39a628a4460bf740d08c18a4fea");
399const KDF_AES_KDBX4: [u8; 16] = hex!("7c02bb8279a74ac0927d114a00648238");
400const KDF_ARGON2: [u8; 16] = hex!("ef636ddf8c29444b91f7a9a403e30a0c");
401const KDF_ARGON2ID: [u8; 16] = hex!("9e298b1956db4773b23dfc3ec6f0a1e6");
402
403impl TryFrom<VariantDictionary> for (KdfConfig, Vec<u8>) {
404    type Error = KdfConfigError;
405
406    fn try_from(vd: VariantDictionary) -> Result<(KdfConfig, Vec<u8>), Self::Error> {
407        let uuid = vd.get_typed::<Vec<u8>>(KDF_ID)?;
408
409        if uuid == &KDF_ARGON2ID {
410            let memory: u64 = *vd.get_typed(KDF_MEMORY)?;
411            let salt: Vec<u8> = vd.get_typed::<Vec<u8>>(KDF_SALT)?.clone();
412            let iterations: u64 = *vd.get_typed(KDF_ITERATIONS)?;
413            let parallelism: u32 = *vd.get_typed(KDF_PARALLELISM)?;
414            let version: u32 = *vd.get_typed(KDF_VERSION)?;
415
416            let version = match version {
417                0x10 => argon2::Version::Version10,
418                0x13 => argon2::Version::Version13,
419                _ => return Err(KdfConfigError::InvalidKDFVersion { version }),
420            };
421
422            Ok((
423                KdfConfig::Argon2id {
424                    memory,
425                    iterations,
426                    parallelism,
427                    version,
428                },
429                salt,
430            ))
431        } else if uuid == &KDF_ARGON2 {
432            let memory: u64 = *vd.get_typed(KDF_MEMORY)?;
433            let salt: Vec<u8> = vd.get_typed::<Vec<u8>>(KDF_SALT)?.clone();
434            let iterations: u64 = *vd.get_typed(KDF_ITERATIONS)?;
435            let parallelism: u32 = *vd.get_typed(KDF_PARALLELISM)?;
436            let version: u32 = *vd.get_typed(KDF_VERSION)?;
437
438            let version = match version {
439                0x10 => argon2::Version::Version10,
440                0x13 => argon2::Version::Version13,
441                _ => return Err(KdfConfigError::InvalidKDFVersion { version }),
442            };
443
444            Ok((
445                KdfConfig::Argon2 {
446                    memory,
447                    iterations,
448                    parallelism,
449                    version,
450                },
451                salt,
452            ))
453        } else if uuid == &KDF_AES_KDBX4 || uuid == &KDF_AES_KDBX3 {
454            let rounds: u64 = *vd.get_typed(KDF_ROUNDS)?;
455            let seed: Vec<u8> = vd.get_typed::<Vec<u8>>(KDF_SEED)?.clone();
456
457            Ok((KdfConfig::Aes { rounds }, seed))
458        } else {
459            Err(KdfConfigError::InvalidKDFUUID { uuid: uuid.clone() })
460        }
461    }
462}
463
464/// Errors with the configuration of the Key Derivation Function
465#[derive(Debug, Error)]
466#[non_exhaustive]
467pub enum KdfConfigError {
468    /// An invalid KDF version was specified in the database
469    #[error("Invalid KDF version: {}", version)]
470    InvalidKDFVersion {
471        /// The invalid KDF version that was encountered
472        version: u32,
473    },
474
475    /// An invalid KDF UUID was specified in the database
476    #[error("Invalid KDF UUID: {:?}", uuid)]
477    InvalidKDFUUID {
478        /// The invalid KDF UUID that was encountered, as a byte vector
479        uuid: Vec<u8>,
480    },
481
482    /// Errors parsing KDF parameters from the variant dictionary
483    #[error(transparent)]
484    VariantDictionary(#[from] VariantDictionaryError),
485}
486
487/// Choices of compression algorithm
488#[derive(Debug, Clone, PartialEq, Eq)]
489#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
490#[non_exhaustive]
491pub enum CompressionConfig {
492    /// Don't compress the inner data
493    None,
494
495    /// Compress the inner data with GZip
496    GZip,
497}
498
499impl CompressionConfig {
500    pub(crate) fn get_compression(&self) -> Box<dyn compression::Compression> {
501        match self {
502            CompressionConfig::None => Box::new(compression::NoCompression),
503            CompressionConfig::GZip => Box::new(compression::GZipCompression),
504        }
505    }
506
507    #[cfg(feature = "save_kdbx4")]
508    pub(crate) fn dump(&self) -> [u8; 4] {
509        match self {
510            CompressionConfig::None => [0, 0, 0, 0],
511            CompressionConfig::GZip => [1, 0, 0, 0],
512        }
513    }
514}
515
516impl TryFrom<u32> for CompressionConfig {
517    type Error = CompressionConfigError;
518
519    fn try_from(v: u32) -> Result<CompressionConfig, Self::Error> {
520        match v {
521            0 => Ok(CompressionConfig::None),
522            1 => Ok(CompressionConfig::GZip),
523            _ => Err(CompressionConfigError::InvalidCompressionSuite { cid: v }),
524        }
525    }
526}
527
528/// Errors with the configuration of the compression algorithm
529#[derive(Debug, Error)]
530#[non_exhaustive]
531pub enum CompressionConfigError {
532    /// The identifier for the compression algorithm specified in the database is invalid
533    #[error("Invalid compression algorithm: {}", cid)]
534    InvalidCompressionSuite {
535        /// The invalid compression suite ID that was encountered
536        cid: u32,
537    },
538}