keepass_ng/
config.rs

1//! Configuration options for how to compress and encrypt databases
2use hex_literal::hex;
3
4use std::convert::TryFrom;
5
6pub use crate::format::DatabaseVersion;
7
8#[cfg(feature = "save_kdbx4")]
9use crate::crypt::ciphers::Cipher;
10
11use crate::{
12    compression,
13    crypt::{ciphers, kdf},
14    error::{CompressionConfigError, CryptographyError, InnerCipherConfigError, KdfConfigError, OuterCipherConfigError},
15    format::KDBX4_CURRENT_MINOR_VERSION,
16    variant_dictionary::VariantDictionary,
17};
18
19const _CIPHERSUITE_AES128: [u8; 16] = hex!("61ab05a1946441c38d743a563df8dd35");
20const CIPHERSUITE_AES256: [u8; 16] = hex!("31c1f2e6bf714350be5805216afc5aff");
21const CIPHERSUITE_TWOFISH: [u8; 16] = hex!("ad68f29f576f4bb9a36ad47af965346c");
22const CIPHERSUITE_CHACHA20: [u8; 16] = hex!("d6038a2b8b6f4cb5a524339a31dbb59a");
23
24// Internal IDs for the ciphers
25const PLAIN: u32 = 0;
26const SALSA_20: u32 = 2;
27const CHA_CHA_20: u32 = 3;
28
29/// Configuration of how a database should be stored
30#[derive(Debug, Clone, PartialEq, Eq)]
31#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
32pub struct DatabaseConfig {
33    /// Version of the outer database file
34    pub version: DatabaseVersion,
35
36    /// What encryption to use for the outer encryption
37    pub outer_cipher_config: OuterCipherConfig,
38
39    /// What algorithm to use to compress the inner data
40    pub compression_config: CompressionConfig,
41
42    /// What encryption to use for protected fields inside the database
43    pub inner_cipher_config: InnerCipherConfig,
44
45    /// Settings for the Key Derivation Function (KDF)
46    pub kdf_config: KdfConfig,
47
48    /// Custom data of plugins/ports.
49    pub public_custom_data: Option<VariantDictionary>,
50}
51
52/// Sensible default configuration for new databases
53impl Default for DatabaseConfig {
54    fn default() -> Self {
55        Self {
56            version: DatabaseVersion::KDB4(KDBX4_CURRENT_MINOR_VERSION),
57            outer_cipher_config: OuterCipherConfig::AES256,
58            compression_config: CompressionConfig::GZip,
59            inner_cipher_config: InnerCipherConfig::ChaCha20,
60            kdf_config: KdfConfig::Argon2 {
61                iterations: 50,
62                memory: 1024 * 1024,
63                parallelism: 4,
64                version: argon2::Version::Version13,
65            },
66            public_custom_data: None,
67        }
68    }
69}
70
71/// Choices for outer encryption
72#[derive(Debug, Clone, PartialEq, Eq)]
73#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
74pub enum OuterCipherConfig {
75    AES256,
76    Twofish,
77    ChaCha20,
78}
79
80impl OuterCipherConfig {
81    pub(crate) fn get_cipher(&self, key: &[u8], iv: &[u8]) -> Result<Box<dyn ciphers::Cipher>, CryptographyError> {
82        match self {
83            OuterCipherConfig::AES256 => Ok(Box::new(ciphers::AES256Cipher::new(key, iv))),
84            OuterCipherConfig::Twofish => Ok(Box::new(ciphers::TwofishCipher::new(key, iv))),
85            OuterCipherConfig::ChaCha20 => Ok(Box::new(ciphers::ChaCha20Cipher::new_key_iv(key, iv)?)),
86        }
87    }
88
89    #[cfg(feature = "save_kdbx4")]
90    pub(crate) fn get_iv_size(&self) -> usize {
91        match self {
92            OuterCipherConfig::AES256 => ciphers::AES256Cipher::iv_size(),
93            OuterCipherConfig::Twofish => ciphers::TwofishCipher::iv_size(),
94            OuterCipherConfig::ChaCha20 => ciphers::ChaCha20Cipher::iv_size(),
95        }
96    }
97
98    #[cfg(feature = "save_kdbx4")]
99    pub(crate) fn dump(&self) -> [u8; 16] {
100        match self {
101            OuterCipherConfig::AES256 => CIPHERSUITE_AES256,
102            OuterCipherConfig::Twofish => CIPHERSUITE_TWOFISH,
103            OuterCipherConfig::ChaCha20 => CIPHERSUITE_CHACHA20,
104        }
105    }
106}
107
108impl TryFrom<&[u8]> for OuterCipherConfig {
109    type Error = OuterCipherConfigError;
110    fn try_from(v: &[u8]) -> Result<OuterCipherConfig, Self::Error> {
111        if v == CIPHERSUITE_AES256 {
112            Ok(OuterCipherConfig::AES256)
113        } else if v == CIPHERSUITE_TWOFISH {
114            Ok(OuterCipherConfig::Twofish)
115        } else if v == CIPHERSUITE_CHACHA20 {
116            Ok(OuterCipherConfig::ChaCha20)
117        } else {
118            Err(OuterCipherConfigError::InvalidOuterCipherID { cid: v.to_vec() })
119        }
120    }
121}
122
123/// Choices for encrypting protected values inside of databases
124#[derive(Debug, Clone, PartialEq, Eq)]
125#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
126pub enum InnerCipherConfig {
127    Plain,
128    Salsa20,
129    ChaCha20,
130}
131
132impl InnerCipherConfig {
133    pub(crate) fn get_cipher(&self, key: &[u8]) -> Box<dyn ciphers::Cipher> {
134        match self {
135            InnerCipherConfig::Plain => Box::new(ciphers::PlainCipher::new(key)),
136            InnerCipherConfig::Salsa20 => Box::new(ciphers::Salsa20Cipher::new(key)),
137            InnerCipherConfig::ChaCha20 => Box::new(ciphers::ChaCha20Cipher::new(key)),
138        }
139    }
140
141    #[cfg(feature = "save_kdbx4")]
142    pub(crate) fn dump(&self) -> u32 {
143        match self {
144            InnerCipherConfig::Plain => PLAIN,
145            InnerCipherConfig::Salsa20 => SALSA_20,
146            InnerCipherConfig::ChaCha20 => CHA_CHA_20,
147        }
148    }
149
150    #[cfg(feature = "save_kdbx4")]
151    pub(crate) fn get_key_size(&self) -> usize {
152        match self {
153            InnerCipherConfig::Plain => ciphers::PlainCipher::key_size(),
154            InnerCipherConfig::Salsa20 => ciphers::Salsa20Cipher::key_size(),
155            InnerCipherConfig::ChaCha20 => ciphers::ChaCha20Cipher::key_size(),
156        }
157    }
158}
159
160impl TryFrom<u32> for InnerCipherConfig {
161    type Error = InnerCipherConfigError;
162
163    fn try_from(v: u32) -> Result<InnerCipherConfig, Self::Error> {
164        match v {
165            PLAIN => Ok(InnerCipherConfig::Plain),
166            SALSA_20 => Ok(InnerCipherConfig::Salsa20),
167            CHA_CHA_20 => Ok(InnerCipherConfig::ChaCha20),
168            _ => Err(InnerCipherConfigError::InvalidInnerCipherID { cid: v }),
169        }
170    }
171}
172
173// Name of the KDF fields in the variant dictionaries.
174const KDF_ID: &str = "$UUID";
175// KDF fields used by Argon2.
176const KDF_MEMORY: &str = "M";
177const KDF_SALT: &str = "S";
178const KDF_ITERATIONS: &str = "I";
179const KDF_PARALLELISM: &str = "P";
180const KDF_VERSION: &str = "V";
181// KDF fields used by AES.
182const KDF_SEED: &str = "S";
183const KDF_ROUNDS: &str = "R";
184
185/// Choices for Key Derivation Functions (KDFs)
186#[derive(Debug, Clone, PartialEq, Eq)]
187#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
188pub enum KdfConfig {
189    /// Derive keys with repeated AES encryption
190    Aes { rounds: u64 },
191    /// Derive keys with Argon2d
192    Argon2 {
193        iterations: u64,
194        memory: u64,
195        parallelism: u32,
196
197        #[cfg_attr(feature = "serialization", serde(serialize_with = "serialize_argon2_version"))]
198        version: argon2::Version,
199    },
200    /// Derive keys with Argon2id
201    Argon2id {
202        iterations: u64,
203        memory: u64,
204        parallelism: u32,
205
206        #[cfg_attr(feature = "serialization", serde(serialize_with = "serialize_argon2_version"))]
207        version: argon2::Version,
208    },
209}
210
211#[cfg(feature = "serialization")]
212fn serialize_argon2_version<S: serde::Serializer>(
213    version: &argon2::Version,
214    serializer: S,
215) -> Result<<S as serde::Serializer>::Ok, <S as serde::Serializer>::Error> {
216    serializer.serialize_u32(version.as_u32())
217}
218
219impl KdfConfig {
220    #[cfg(feature = "save_kdbx4")]
221    fn seed_size(&self) -> usize {
222        match self {
223            KdfConfig::Aes { .. } => 32,
224            KdfConfig::Argon2 { .. } => 32,
225            KdfConfig::Argon2id { .. } => 32,
226        }
227    }
228
229    /// For writing out a database, generate a new KDF seed from the config and return the KDF
230    /// and the generated seed
231    #[cfg(feature = "save_kdbx4")]
232    pub(crate) fn get_kdf_and_seed(&self) -> Result<(Box<dyn kdf::Kdf>, Vec<u8>), getrandom::Error> {
233        let mut kdf_seed = vec![0; self.seed_size()];
234        getrandom::fill(&mut kdf_seed)?;
235
236        let kdf = self.get_kdf_seeded(&kdf_seed);
237
238        Ok((kdf, kdf_seed))
239    }
240
241    /// For reading a database, generate a KDF from the KDF config and a provided seed
242    pub(crate) fn get_kdf_seeded(&self, seed: &[u8]) -> Box<dyn kdf::Kdf> {
243        match self {
244            KdfConfig::Aes { rounds } => Box::new(kdf::AesKdf {
245                seed: seed.to_vec(),
246                rounds: *rounds,
247            }),
248            KdfConfig::Argon2 {
249                memory,
250                iterations,
251                parallelism,
252                version,
253            } => Box::new(kdf::Argon2Kdf {
254                memory: *memory,
255                salt: seed.to_vec(),
256                iterations: *iterations,
257                parallelism: *parallelism,
258                version: *version,
259                variant: argon2::Variant::Argon2d,
260            }),
261            KdfConfig::Argon2id {
262                memory,
263                iterations,
264                parallelism,
265                version,
266            } => Box::new(kdf::Argon2Kdf {
267                memory: *memory,
268                salt: seed.to_vec(),
269                iterations: *iterations,
270                parallelism: *parallelism,
271                version: *version,
272                variant: argon2::Variant::Argon2id,
273            }),
274        }
275    }
276
277    #[cfg(feature = "save_kdbx4")]
278    pub(crate) fn to_variant_dictionary(&self, seed: &[u8]) -> VariantDictionary {
279        let mut vd = VariantDictionary::new();
280
281        match self {
282            KdfConfig::Aes { rounds } => {
283                vd.set(KDF_ID, KDF_AES_KDBX4.to_vec());
284                vd.set(KDF_ROUNDS, *rounds);
285                vd.set(KDF_SEED, seed.to_vec());
286            }
287            KdfConfig::Argon2 {
288                memory,
289                iterations,
290                parallelism,
291                version,
292            } => {
293                vd.set(KDF_ID, KDF_ARGON2.to_vec());
294                vd.set(KDF_MEMORY, *memory);
295                vd.set(KDF_SALT, seed.to_vec());
296                vd.set(KDF_ITERATIONS, *iterations);
297                vd.set(KDF_PARALLELISM, *parallelism);
298                vd.set(KDF_VERSION, version.as_u32());
299            }
300            KdfConfig::Argon2id {
301                memory,
302                iterations,
303                parallelism,
304                version,
305            } => {
306                vd.set(KDF_ID, KDF_ARGON2ID.to_vec());
307                vd.set(KDF_MEMORY, *memory);
308                vd.set(KDF_SALT, seed.to_vec());
309                vd.set(KDF_ITERATIONS, *iterations);
310                vd.set(KDF_PARALLELISM, *parallelism);
311                vd.set(KDF_VERSION, version.as_u32());
312            }
313        }
314
315        vd
316    }
317}
318
319const KDF_AES_KDBX3: [u8; 16] = hex!("c9d9f39a628a4460bf740d08c18a4fea");
320const KDF_AES_KDBX4: [u8; 16] = hex!("7c02bb8279a74ac0927d114a00648238");
321const KDF_ARGON2: [u8; 16] = hex!("ef636ddf8c29444b91f7a9a403e30a0c");
322const KDF_ARGON2ID: [u8; 16] = hex!("9e298b1956db4773b23dfc3ec6f0a1e6");
323
324impl TryFrom<VariantDictionary> for (KdfConfig, Vec<u8>) {
325    type Error = KdfConfigError;
326
327    fn try_from(vd: VariantDictionary) -> Result<(KdfConfig, Vec<u8>), Self::Error> {
328        let uuid = vd.get::<Vec<u8>>(KDF_ID)?;
329
330        if uuid == &KDF_ARGON2ID {
331            let memory: u64 = *vd.get(KDF_MEMORY)?;
332            let salt: Vec<u8> = vd.get::<Vec<u8>>(KDF_SALT)?.clone();
333            let iterations: u64 = *vd.get(KDF_ITERATIONS)?;
334            let parallelism: u32 = *vd.get(KDF_PARALLELISM)?;
335            let version: u32 = *vd.get(KDF_VERSION)?;
336
337            let version = match version {
338                0x10 => argon2::Version::Version10,
339                0x13 => argon2::Version::Version13,
340                _ => return Err(KdfConfigError::InvalidKDFVersion { version }),
341            };
342
343            Ok((
344                KdfConfig::Argon2id {
345                    memory,
346                    iterations,
347                    parallelism,
348                    version,
349                },
350                salt,
351            ))
352        } else if uuid == &KDF_ARGON2 {
353            let memory: u64 = *vd.get(KDF_MEMORY)?;
354            let salt: Vec<u8> = vd.get::<Vec<u8>>(KDF_SALT)?.clone();
355            let iterations: u64 = *vd.get(KDF_ITERATIONS)?;
356            let parallelism: u32 = *vd.get(KDF_PARALLELISM)?;
357            let version: u32 = *vd.get(KDF_VERSION)?;
358
359            let version = match version {
360                0x10 => argon2::Version::Version10,
361                0x13 => argon2::Version::Version13,
362                _ => return Err(KdfConfigError::InvalidKDFVersion { version }),
363            };
364
365            Ok((
366                KdfConfig::Argon2 {
367                    memory,
368                    iterations,
369                    parallelism,
370                    version,
371                },
372                salt,
373            ))
374        } else if uuid == &KDF_AES_KDBX4 || uuid == &KDF_AES_KDBX3 {
375            let rounds: u64 = *vd.get(KDF_ROUNDS)?;
376            let seed: Vec<u8> = vd.get::<Vec<u8>>(KDF_SEED)?.clone();
377
378            Ok((KdfConfig::Aes { rounds }, seed))
379        } else {
380            Err(KdfConfigError::InvalidKDFUUID { uuid: uuid.clone() })
381        }
382    }
383}
384
385/// Choices of compression algorithm
386#[derive(Debug, Clone, PartialEq, Eq)]
387#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
388pub enum CompressionConfig {
389    None,
390    GZip,
391}
392
393impl CompressionConfig {
394    pub(crate) fn get_compression(&self) -> Box<dyn compression::Compression> {
395        match self {
396            CompressionConfig::None => Box::new(compression::NoCompression),
397            CompressionConfig::GZip => Box::new(compression::GZipCompression),
398        }
399    }
400
401    #[cfg(feature = "save_kdbx4")]
402    pub(crate) fn dump(&self) -> [u8; 4] {
403        match self {
404            CompressionConfig::None => [0, 0, 0, 0],
405            CompressionConfig::GZip => [1, 0, 0, 0],
406        }
407    }
408}
409
410impl TryFrom<u32> for CompressionConfig {
411    type Error = CompressionConfigError;
412
413    fn try_from(v: u32) -> Result<CompressionConfig, Self::Error> {
414        match v {
415            0 => Ok(CompressionConfig::None),
416            1 => Ok(CompressionConfig::GZip),
417            _ => Err(CompressionConfigError::InvalidCompressionSuite { cid: v }),
418        }
419    }
420}