Skip to main content

keepass_ng/format/kdbx4/
parse.rs

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