Skip to main content

keepass/format/kdbx4/
parse.rs

1//! Parse the kdbx4 format
2
3use std::convert::{TryFrom, TryInto};
4
5use byteorder::{ByteOrder, LittleEndian};
6use thiserror::Error;
7
8use crate::{
9    config::{CompressionConfig, DatabaseConfig, InnerCipherConfig, KdfConfig, OuterCipherConfig},
10    crypt::{self, ciphers::Cipher},
11    db::{Database, DatabaseFormatError, DatabaseOpenError, Value},
12    format::{
13        hmac_block_stream,
14        kdbx4::{
15            KDBX4OuterHeader, HEADER_COMMENT, HEADER_COMPRESSION_ID, HEADER_ENCRYPTION_IV, HEADER_END,
16            HEADER_KDF_PARAMS, HEADER_MASTER_SEED, HEADER_OUTER_ENCRYPTION_ID, HEADER_PUBLIC_CUSTOM_DATA,
17            INNER_HEADER_BINARY_ATTACHMENTS, INNER_HEADER_END, INNER_HEADER_RANDOM_STREAM_ID,
18            INNER_HEADER_RANDOM_STREAM_KEY,
19        },
20        variant_dictionary::VariantDictionary,
21        DatabaseVersion,
22    },
23    key::{DatabaseKey, DatabaseKeyError},
24};
25
26use super::KDBX4InnerHeader;
27
28/// Open, decrypt and parse a KeePass database from a source and key elements
29pub(crate) fn parse_kdbx4(data: &[u8], db_key: &DatabaseKey) -> Result<Database, DatabaseOpenError> {
30    let (config, header_attachments, mut inner_decryptor, xml) = decrypt_kdbx4(data, db_key)?;
31
32    let mut db = crate::format::xml_db::parse_xml(&xml, &header_attachments, &mut *inner_decryptor)
33        .map_err(|e| DatabaseOpenError::Format(DatabaseFormatError::Kdbx4(Kdbx4OpenError::Xml(e))))?;
34
35    db.config = config;
36
37    Ok(db)
38}
39
40/// Open and decrypt a KeePass KDBX4 database from a source and key elements
41#[allow(clippy::type_complexity)]
42pub(crate) fn decrypt_kdbx4(
43    data: &[u8],
44    db_key: &DatabaseKey,
45) -> Result<(DatabaseConfig, Vec<Value<Vec<u8>>>, Box<dyn Cipher>, Vec<u8>), DatabaseOpenError> {
46    let version = DatabaseVersion::parse(data)?;
47
48    // parse header
49    let (outer_header, inner_header_start) = parse_outer_header(data)
50        .map_err(|e| DatabaseOpenError::Format(DatabaseFormatError::Kdbx4(Kdbx4OpenError::OuterHeader(e))))?;
51
52    // split file into segments:
53    //      header_data         - The outer header data
54    //      header_sha256       - A Sha256 hash of header_data (for verification of header integrity)
55    //      header_hmac         - A HMAC of the header_data (for verification of the key_elements)
56    //      hmac_block_stream   - A HMAC-verified block stream of encrypted and compressed blocks
57    #[allow(clippy::indexing_slicing)] // inner_header_start is provided by parse_outer_header
58    let header_data = &data[0..inner_header_start];
59
60    let header_sha256 = data
61        .get(inner_header_start..(inner_header_start + 32))
62        .ok_or(DatabaseOpenError::UnexpectedEof)?;
63
64    let header_hmac = data
65        .get((inner_header_start + 32)..(inner_header_start + 64))
66        .ok_or(DatabaseOpenError::UnexpectedEof)?;
67
68    let hmac_block_stream = data
69        .get((inner_header_start + 64)..)
70        .ok_or(DatabaseOpenError::UnexpectedEof)?;
71
72    // verify header
73    if header_sha256 != crypt::calculate_sha256(&[header_data]).as_slice() {
74        return Err(DatabaseOpenError::Format(DatabaseFormatError::Kdbx4(
75            Kdbx4OpenError::HeaderHashMismatch,
76        )));
77    }
78
79    #[cfg(feature = "challenge_response")]
80    let db_key = db_key.clone().perform_challenge(&outer_header.kdf_seed)?;
81
82    // derive master key from composite key, transform_seed, transform_rounds and master_seed
83    let key_elements = db_key.get_key_elements()?;
84    let key_elements: Vec<&[u8]> = key_elements.iter().map(|v| &v[..]).collect();
85    let composite_key = crypt::calculate_sha256(&key_elements);
86    let transformed_key = outer_header
87        .kdf_config
88        .get_kdf_seeded(&outer_header.kdf_seed)
89        .transform_key(&composite_key)?;
90    let master_key = crypt::calculate_sha256(&[outer_header.master_seed.as_ref(), &transformed_key]);
91
92    // verify credentials
93    let hmac_key = crypt::calculate_sha512(&[
94        &outer_header.master_seed,
95        &transformed_key,
96        &hmac_block_stream::HMAC_KEY_END,
97    ]);
98    let header_hmac_key = hmac_block_stream::get_hmac_block_key(u64::MAX, &hmac_key);
99
100    #[allow(clippy::expect_used)] // HMAC block key is always correctly sized, so this can't fail
101    if header_hmac
102        != crypt::calculate_hmac(&[header_data], &header_hmac_key)
103            .expect("HMAC block key always correctly sized")
104            .as_slice()
105    {
106        return Err(DatabaseKeyError::IncorrectKey.into());
107    }
108
109    // read encrypted payload from hmac-verified block stream
110    let payload_encrypted = hmac_block_stream::read_hmac_block_stream(hmac_block_stream, &hmac_key)
111        .map_err(|e| DatabaseOpenError::Format(DatabaseFormatError::Kdbx4(Kdbx4OpenError::BlockStream(e))))?;
112
113    // Decrypt and decompress encrypted payload
114    let payload_compressed = outer_header
115        .outer_cipher_config
116        .get_cipher(&master_key, &outer_header.outer_iv)?
117        .decrypt(&payload_encrypted)?;
118
119    let payload = outer_header
120        .compression_config
121        .get_compression()
122        .decompress(&payload_compressed)?;
123
124    // KDBX4 has inner header, too - parse it
125    let (header_attachments, inner_header, body_start) = parse_inner_header(&payload)
126        .map_err(|e| DatabaseOpenError::Format(DatabaseFormatError::Kdbx4(Kdbx4OpenError::InnerHeader(e))))?;
127
128    // after inner header is one XML document
129    let xml = payload
130        .get(body_start..)
131        .ok_or(DatabaseOpenError::UnexpectedEof)?;
132
133    // initialize the inner decryptor
134    let inner_decryptor = inner_header
135        .inner_random_stream
136        .get_cipher(&inner_header.inner_random_stream_key)?;
137
138    let config = DatabaseConfig {
139        version,
140        outer_cipher_config: outer_header.outer_cipher_config,
141        compression_config: outer_header.compression_config,
142        inner_cipher_config: inner_header.inner_random_stream,
143        kdf_config: outer_header.kdf_config,
144        public_custom_data: outer_header.public_custom_data,
145    };
146
147    Ok((config, header_attachments, inner_decryptor, xml.to_vec()))
148}
149
150fn parse_outer_header(data: &[u8]) -> Result<(KDBX4OuterHeader, usize), Kdbx4OuterHeaderError> {
151    // skip over the version header
152    let mut pos = DatabaseVersion::get_version_header_size();
153
154    let mut outer_cipher: Option<OuterCipherConfig> = None;
155    let mut compression_config: Option<CompressionConfig> = None;
156    let mut master_seed: Option<Vec<u8>> = None;
157    let mut outer_iv: Option<Vec<u8>> = None;
158    let mut kdf_config: Option<KdfConfig> = None;
159    let mut kdf_seed: Option<Vec<u8>> = None;
160    let mut public_custom_data: Option<VariantDictionary> = None;
161
162    // parse header
163    loop {
164        // parse header blocks.
165        //
166        // every block is a triplet of (3 + entry_length) bytes with this structure:
167        //
168        // (
169        //   entry_type: u8,                        // a numeric entry type identifier
170        //   entry_length: u32,                     // length of the entry buffer
171        //   entry_buffer: [u8; entry_length]       // the entry buffer
172        // )
173
174        let entry_type = data.get(pos).ok_or(Kdbx4OuterHeaderError::UnexpectedEof)?;
175        let entry_length = data
176            .get(pos + 1..(pos + 5))
177            .ok_or(Kdbx4OuterHeaderError::UnexpectedEof)?;
178
179        let entry_length: usize = LittleEndian::read_u32(entry_length) as usize;
180
181        let entry_buffer = data
182            .get((pos + 5)..(pos + 5 + entry_length))
183            .ok_or(Kdbx4OuterHeaderError::UnexpectedEof)?;
184
185        pos += 5 + entry_length;
186
187        match *entry_type {
188            HEADER_END => {
189                break;
190            }
191
192            HEADER_COMMENT => {}
193
194            HEADER_OUTER_ENCRYPTION_ID => {
195                outer_cipher = Some(OuterCipherConfig::try_from(entry_buffer)?);
196            }
197
198            HEADER_COMPRESSION_ID => {
199                let id = entry_buffer
200                    .get(0..4)
201                    .ok_or(Kdbx4OuterHeaderError::UnexpectedEof)?;
202                compression_config = Some(CompressionConfig::try_from(LittleEndian::read_u32(id))?);
203            }
204
205            HEADER_MASTER_SEED => master_seed = Some(entry_buffer.to_vec()),
206
207            HEADER_ENCRYPTION_IV => outer_iv = Some(entry_buffer.to_vec()),
208
209            HEADER_KDF_PARAMS => {
210                let vd =
211                    VariantDictionary::parse(entry_buffer).map_err(Kdbx4OuterHeaderError::ParseKdfConfig)?;
212
213                let (kconf, kseed) = vd.try_into()?;
214                kdf_config = Some(kconf);
215                kdf_seed = Some(kseed)
216            }
217
218            HEADER_PUBLIC_CUSTOM_DATA => {
219                let vd =
220                    VariantDictionary::parse(entry_buffer).map_err(Kdbx4OuterHeaderError::ParseCustomData)?;
221
222                public_custom_data = Some(vd)
223            }
224
225            _ => return Err(Kdbx4OuterHeaderError::InvalidEntry(*entry_type)),
226        };
227    }
228
229    // at this point, the header needs to be fully defined - unwrap options and return errors if
230    // something is missing
231
232    fn get_or_err<T>(v: Option<T>, err: &'static str) -> Result<T, Kdbx4OuterHeaderError> {
233        v.ok_or(Kdbx4OuterHeaderError::Incomplete(err))
234    }
235
236    let outer_cipher_config = get_or_err(outer_cipher, "Outer Cipher ID")?;
237    let compression_config = get_or_err(compression_config, "Compression ID")?;
238    let master_seed = get_or_err(master_seed, "Master seed")?;
239    let outer_iv = get_or_err(outer_iv, "Outer IV")?;
240    let kdf_config = get_or_err(kdf_config, "Key Derivation Function Parameters")?;
241    let kdf_seed = get_or_err(kdf_seed, "Key Derivation Function Seed")?;
242
243    Ok((
244        KDBX4OuterHeader {
245            outer_cipher_config,
246            compression_config,
247            master_seed,
248            outer_iv,
249            kdf_config,
250            kdf_seed,
251            public_custom_data,
252        },
253        pos,
254    ))
255}
256
257/// Errors that can occur while parsing the KDBX4 outer header
258#[derive(Debug, Error)]
259#[non_exhaustive]
260pub enum Kdbx4OuterHeaderError {
261    /// The file ended unexpectedly while parsing the outer header
262    #[error("Unexpected end of file while parsing outer header")]
263    UnexpectedEof,
264
265    /// Errors related to the outer cipher configuration
266    #[error(transparent)]
267    OuterCipherConfig(#[from] crate::config::OuterCipherConfigError),
268
269    /// Errors related to the compression configuration
270    #[error(transparent)]
271    CompressionConfig(#[from] crate::config::CompressionConfigError),
272
273    /// Errors related to parsing the key derivation function configuration
274    #[error("error parsing KDF config: {0}")]
275    ParseKdfConfig(#[source] crate::format::variant_dictionary::VariantDictionaryError),
276
277    /// Errors related to parsing the public custom data
278    #[error("error parsing public custom data: {0}")]
279    ParseCustomData(#[source] crate::format::variant_dictionary::VariantDictionaryError),
280
281    /// Errors related to the key derivation function configuration
282    #[error(transparent)]
283    KdfConfig(#[from] crate::config::KdfConfigError),
284
285    /// The outer header contains an entry with an unrecognized or invalid entry type identifier
286    #[error("Invalid outer header entry: {0}")]
287    InvalidEntry(u8),
288
289    /// The outer header is missing a required field
290    #[error("Outer header incomplete - missing {0}")]
291    Incomplete(&'static str),
292}
293
294#[allow(clippy::type_complexity)]
295fn parse_inner_header(
296    data: &[u8],
297) -> Result<(Vec<Value<Vec<u8>>>, KDBX4InnerHeader, usize), Kdbx4InnerHeaderError> {
298    let mut pos = 0;
299
300    let mut inner_random_stream = None;
301    let mut inner_random_stream_key = None;
302    let mut header_attachments = Vec::new();
303
304    loop {
305        let entry_type = *data.get(pos).ok_or(Kdbx4InnerHeaderError::UnexpectedEof)?;
306
307        let entry_length = data
308            .get(pos + 1..(pos + 5))
309            .ok_or(Kdbx4InnerHeaderError::UnexpectedEof)?;
310        let entry_length: usize = LittleEndian::read_u32(entry_length) as usize;
311
312        let entry_buffer = data
313            .get((pos + 5)..(pos + 5 + entry_length))
314            .ok_or(Kdbx4InnerHeaderError::UnexpectedEof)?;
315
316        pos += 5 + entry_length;
317
318        match entry_type {
319            INNER_HEADER_END => break,
320
321            INNER_HEADER_RANDOM_STREAM_ID => {
322                let id = entry_buffer
323                    .get(0..4)
324                    .ok_or(Kdbx4InnerHeaderError::UnexpectedEof)?;
325                inner_random_stream = Some(InnerCipherConfig::try_from(LittleEndian::read_u32(id))?);
326            }
327
328            INNER_HEADER_RANDOM_STREAM_KEY => inner_random_stream_key = Some(entry_buffer.to_vec()),
329
330            #[allow(clippy::indexing_slicing)] // we check entry buffer length at the beginning of the block
331            INNER_HEADER_BINARY_ATTACHMENTS => {
332                if entry_buffer.is_empty() {
333                    return Err(Kdbx4InnerHeaderError::UnexpectedEof);
334                }
335
336                let flags = entry_buffer[0];
337                let data = entry_buffer[1..].to_vec();
338
339                let protected = flags & 0x01 != 0;
340
341                let data = if protected {
342                    Value::protected(data)
343                } else {
344                    Value::unprotected(data)
345                };
346
347                header_attachments.push(data);
348            }
349
350            _ => {
351                return Err(Kdbx4InnerHeaderError::InvalidEntry(entry_type));
352            }
353        }
354    }
355
356    fn get_or_err<T>(v: Option<T>, err: &'static str) -> Result<T, Kdbx4InnerHeaderError> {
357        v.ok_or(Kdbx4InnerHeaderError::Incomplete(err))
358    }
359
360    let inner_random_stream = get_or_err(inner_random_stream, "Inner random stream")?;
361    let inner_random_stream_key = get_or_err(inner_random_stream_key, "Inner random stream key")?;
362
363    let inner_header = KDBX4InnerHeader {
364        inner_random_stream,
365        inner_random_stream_key,
366    };
367
368    Ok((header_attachments, inner_header, pos))
369}
370
371/// Errors that can occur while parsing the KDBX4 inner header
372#[derive(Debug, Error)]
373#[non_exhaustive]
374pub enum Kdbx4InnerHeaderError {
375    /// Errors related to the inner cipher configuration
376    #[error(transparent)]
377    InnerCipherConfig(#[from] crate::config::InnerCipherConfigError),
378
379    /// Encountered an invalid header entry
380    #[error("Invalid inner header entry: {0}")]
381    InvalidEntry(u8),
382
383    /// The inner header is missing a required field
384    #[error("Inner header incomplete - missing {0}")]
385    Incomplete(&'static str),
386
387    /// The file ended unexpectedly while parsing the inner header
388    #[error("Unexpected end of file while parsing inner header")]
389    UnexpectedEof,
390}
391
392/// Errors that can occur while parsing a KDBX4 database
393#[derive(Debug, Error)]
394#[non_exhaustive]
395pub enum Kdbx4OpenError {
396    /// Errors related to XML parsing of the inner database
397    #[error(transparent)]
398    Xml(#[from] crate::format::xml_db::ParseXmlError),
399
400    /// Errors related to parsing the outer header of the KDBX4 file
401    #[error(transparent)]
402    OuterHeader(#[from] Kdbx4OuterHeaderError),
403
404    /// Errors related to parsing the inner header of the KDBX4 file
405    #[error(transparent)]
406    InnerHeader(#[from] Kdbx4InnerHeaderError),
407
408    /// The SHA256 hash of the outer header did not match the expected value, indicating that the
409    /// header may be corrupted or tampered with.
410    #[error("Header hash mismatch - the header may be corrupted")]
411    HeaderHashMismatch,
412
413    /// Errors related to the HMAC-verified block stream of the KDBX4 file
414    #[error(transparent)]
415    BlockStream(#[from] hmac_block_stream::BlockStreamError),
416}