Skip to main content

keepass/format/
kdb.rs

1use crate::{
2    config::{CompressionConfig, DatabaseConfig, InnerCipherConfig, KdfConfig, OuterCipherConfig},
3    crypt::calculate_sha256,
4    db::{fields, Database, DatabaseFormatError, DatabaseOpenError, GroupId, Value},
5    format::DatabaseVersion,
6    key::{DatabaseKey, DatabaseKeyError},
7};
8
9use byteorder::{ByteOrder, LittleEndian};
10use hybrid_array::Array as GenericArray;
11use thiserror::Error;
12
13use std::{
14    collections::HashMap,
15    convert::{TryFrom, TryInto},
16};
17
18#[derive(Debug)]
19struct KDBHeader {
20    // https://gist.github.com/lgg/e6ccc6e212d18dd2ecd8a8c116fb1e45
21    pub flags: u32,
22    pub subversion: u32,
23    pub master_seed: Vec<u8>,   // 16 bytes
24    pub encryption_iv: Vec<u8>, // 16 bytes
25    pub num_groups: u32,
26    pub num_entries: u32,
27    pub contents_hash: Vec<u8>,  // 32 bytes
28    pub transform_seed: Vec<u8>, // 32 bytes
29    pub transform_rounds: u32,
30}
31
32const HEADER_SIZE: usize = 4 + 4 + 4 + 4 + 16 + 16 + 4 + 4 + 32 + 32 + 4; // first 4 bytes are the KeePass magic
33
34impl TryFrom<&[u8]> for KDBHeader {
35    type Error = DatabaseOpenError;
36
37    #[allow(clippy::indexing_slicing)] // data length is checked
38    fn try_from(data: &[u8]) -> Result<Self, Self::Error> {
39        if data.len() < HEADER_SIZE {
40            return Err(DatabaseOpenError::UnexpectedEof);
41        }
42
43        Ok(KDBHeader {
44            flags: LittleEndian::read_u32(&data[8..]),
45            subversion: LittleEndian::read_u32(&data[12..]),
46            master_seed: data[16..32].to_vec(),
47            encryption_iv: data[32..48].to_vec(),
48            num_groups: LittleEndian::read_u32(&data[48..]),
49            num_entries: LittleEndian::read_u32(&data[52..]),
50            contents_hash: data[56..88].to_vec(),
51            transform_seed: data[88..120].to_vec(),
52            transform_rounds: LittleEndian::read_u32(&data[120..]),
53        })
54    }
55}
56
57fn from_utf8(data: &[u8]) -> String {
58    String::from_utf8_lossy(data).trim_end_matches('\0').to_owned()
59}
60
61fn expected_group_field_size(ftype: u16) -> Option<u32> {
62    match ftype {
63        0x0001 => Some(4), // GroupId
64        0x0002 => None,    // GroupName (variable length)
65        0x0003 => Some(5), // CreationTime
66        0x0004 => Some(5), // LastModTime
67        0x0005 => Some(5), // LastAccessTime
68        0x0006 => Some(5), // ExpireTime
69        0x0007 => Some(4), // ImageId
70        0x0008 => Some(2), // Level
71        0x0009 => Some(4), // Flags
72        0xffff => Some(0), // End of group
73        _ => None,         // Unknown field type
74    }
75}
76
77fn parse_groups(
78    db: &mut Database,
79    header_num_groups: u32,
80    data: &mut &[u8],
81) -> Result<HashMap<u32, GroupId>, DatabaseOpenError> {
82    let mut gid_map: HashMap<u32, GroupId> = HashMap::new();
83    gid_map.insert(0, db.root);
84
85    // current branch of the group tree being parsed
86    let mut branch: Vec<GroupId> = Vec::new();
87    branch.push(db.root);
88
89    // state variables for the current group being parsed
90    let mut parsing_name: Option<String> = None;
91    let mut parsing_level: Option<u16> = None;
92    let mut parsing_gid: Option<u32> = None;
93
94    // the total number of parsed groups
95    let mut num_groups = 0;
96    while num_groups < header_num_groups as usize {
97        // Read group TLV
98        let field_type = data
99            .get(0..2)
100            .map(LittleEndian::read_u16)
101            .ok_or(DatabaseOpenError::UnexpectedEof)?;
102
103        let field_size = data
104            .get(2..6)
105            .map(LittleEndian::read_u32)
106            .ok_or(DatabaseOpenError::UnexpectedEof)?;
107
108        let field_value = data
109            .get(6..6 + field_size as usize)
110            .ok_or(DatabaseOpenError::UnexpectedEof)?;
111
112        if let Some(expected_field_size) = expected_group_field_size(field_type) {
113            if expected_field_size != field_size {
114                return Err(DatabaseOpenError::Format(DatabaseFormatError::Kdb(
115                    KdbOpenError::InvalidFieldLength {
116                        field_type,
117                        field_size,
118                        expected_field_size,
119                    },
120                )));
121            }
122        }
123
124        match field_type {
125            0x0000 => {} // KeePass ignores this field type
126
127            // GroupId
128            0x0001 => parsing_gid = Some(LittleEndian::read_u32(field_value)),
129
130            // GroupName
131            0x0002 => parsing_name = Some(from_utf8(field_value)),
132
133            // Creation/LastMod/LastAccess/Expire times
134            0x0003..=0x0006 => {}
135
136            // ImageId
137            0x0007 => {}
138
139            // Level
140            0x0008 => parsing_level = Some(LittleEndian::read_u16(field_value)),
141
142            // Flags
143            0x0009 => {}
144
145            // End of group
146            0xffff => {
147                let group_id = parsing_gid.ok_or(DatabaseOpenError::Format(DatabaseFormatError::Kdb(
148                    KdbOpenError::InvalidGroupId(None),
149                )))?;
150
151                let level = parsing_level.ok_or(DatabaseOpenError::Format(DatabaseFormatError::Kdb(
152                    KdbOpenError::InvalidGroupLevel {
153                        current: None,
154                        expected: branch.len() as u16,
155                    },
156                )))? as usize;
157
158                let name = parsing_name.clone().unwrap_or_else(|| String::from(""));
159
160                let parent_id: GroupId = if level <= branch.len() {
161                    branch.truncate(level);
162                    *branch.last().unwrap_or(&db.root().id())
163                } else {
164                    // Level is beyond the current depth, missing intermediate levels?
165                    return Err(DatabaseOpenError::Format(DatabaseFormatError::Kdb(
166                        KdbOpenError::InvalidGroupLevel {
167                            current: Some(level as u16),
168                            expected: branch.len() as u16,
169                        },
170                    )));
171                };
172
173                #[allow(clippy::expect_used)] // parent_id is guaranteed to exist
174                let mut parent = db.group_mut(parent_id).expect("parent group must exist");
175
176                let mut group = parent.add_group();
177                group.name = name;
178
179                parsing_gid = None;
180                parsing_name = None;
181                parsing_level = None;
182
183                gid_map.insert(group_id, group.id());
184
185                branch.push(group.id());
186
187                num_groups += 1;
188            }
189            _ => {
190                return Err(DatabaseOpenError::Format(DatabaseFormatError::Kdb(
191                    KdbOpenError::InvalidGroupFieldType(field_type),
192                )));
193            }
194        }
195
196        *data = data
197            .get(6 + field_size as usize..)
198            .ok_or(DatabaseOpenError::UnexpectedEof)?;
199    }
200
201    if parsing_gid.is_some() {
202        return Err(DatabaseOpenError::Format(DatabaseFormatError::Kdb(
203            KdbOpenError::IncompleteGroup,
204        )));
205    }
206
207    Ok(gid_map)
208}
209
210fn expected_entry_field_size(ftype: u16) -> Option<u32> {
211    match ftype {
212        0x0000 => None,     // KeePass ignores this field type
213        0x0001 => Some(16), // uuid
214        0x0002 => Some(4),  // GroupId
215        0x0003 => Some(4),  // ImageId
216        0x0004 => None,     // Title (variable length)
217        0x0005 => None,     // URL (variable length)
218        0x0006 => None,     // UserName (variable length)
219        0x0007 => None,     // Password (variable length)
220        0x0008 => None,     // Additional (variable length)
221        0x0009 => Some(5),  // CreationTime
222        0x000a => Some(5),  // LastModTime
223        0x000b => Some(5),  // LastAccessTime
224        0x000c => Some(5),  // ExpireTime
225        0x000d => None,     // BinaryDesc (variable length)
226        0x000e => None,     // BinaryData (variable length)
227        0xffff => Some(0),  // End of entry
228        _ => None,          // Unknown field type
229    }
230}
231
232fn parse_entries(
233    db: &mut Database,
234    gid_map: HashMap<u32, GroupId>,
235    header_num_entries: u32,
236    data: &mut &[u8],
237) -> Result<(), DatabaseOpenError> {
238    let mut parsing_gid: Option<u32> = None;
239    let mut parsing_fields: HashMap<String, Value<String>> = HashMap::new();
240
241    let mut parsing_binary_desc: Option<String> = None;
242    let mut parsing_binary_data: Option<Vec<u8>> = None;
243
244    let mut entry_attachments: HashMap<String, Vec<u8>> = HashMap::new();
245
246    let mut num_entries = 0;
247    while num_entries < header_num_entries {
248        let field_type = data.get(0..2).ok_or(DatabaseOpenError::UnexpectedEof)?;
249        let field_type = LittleEndian::read_u16(field_type);
250
251        let field_size = data.get(2..6).ok_or(DatabaseOpenError::UnexpectedEof)?;
252        let field_size = LittleEndian::read_u32(field_size);
253
254        let field_value = data
255            .get(6..6 + field_size as usize)
256            .ok_or(DatabaseOpenError::UnexpectedEof)?;
257
258        if let Some(expected_field_size) = expected_entry_field_size(field_type) {
259            if expected_field_size != field_size {
260                return Err(DatabaseOpenError::Format(DatabaseFormatError::Kdb(
261                    KdbOpenError::InvalidFieldLength {
262                        field_type,
263                        field_size,
264                        expected_field_size,
265                    },
266                )));
267            }
268        }
269
270        match field_type {
271            // ignored by KeePass
272            0x0000 => {} // KeePass ignores this field type
273
274            // UUID
275            0x0001 => {}
276
277            // GroupId
278            0x0002 => parsing_gid = Some(LittleEndian::read_u32(field_value)),
279
280            // ImageId
281            0x0003 => {}
282
283            // Title
284            0x0004 => {
285                parsing_fields.insert(
286                    String::from(fields::TITLE),
287                    Value::unprotected(from_utf8(field_value)),
288                );
289            }
290
291            // URL
292            0x0005 => {
293                parsing_fields.insert(
294                    String::from(fields::URL),
295                    Value::unprotected(from_utf8(field_value)),
296                );
297            }
298
299            // UserName
300            0x0006 => {
301                parsing_fields.insert(
302                    String::from(fields::USERNAME),
303                    Value::unprotected(from_utf8(field_value)),
304                );
305            }
306
307            // Password
308            0x0007 => {
309                parsing_fields.insert(
310                    String::from(fields::PASSWORD),
311                    Value::protected(from_utf8(field_value)),
312                );
313            }
314
315            // Additional
316            0x0008 => {
317                parsing_fields.insert(
318                    String::from(fields::NOTES),
319                    Value::unprotected(from_utf8(field_value)),
320                );
321            }
322
323            // Creation/LastMod/LastAccess/Expire times
324            0x0009..=0x000c => {}
325
326            // BinaryDesc
327            0x000d => {
328                if let Some(ref data) = parsing_binary_data {
329                    entry_attachments.insert(from_utf8(field_value), data.clone());
330                    parsing_binary_desc = None;
331                } else {
332                    parsing_binary_desc = Some(from_utf8(field_value));
333                }
334            }
335
336            // BinaryData
337            0x000e => {
338                if let Some(ref desc) = parsing_binary_desc {
339                    entry_attachments.insert(desc.clone(), field_value.to_vec());
340                    parsing_binary_data = None;
341                } else {
342                    parsing_binary_data = Some(field_value.to_vec());
343                }
344            }
345
346            0xffff => {
347                let gid = parsing_gid.ok_or(DatabaseOpenError::Format(DatabaseFormatError::Kdb(
348                    KdbOpenError::InvalidGroupId(None),
349                )))?;
350                let group_id =
351                    *gid_map
352                        .get(&gid)
353                        .ok_or(DatabaseOpenError::Format(DatabaseFormatError::Kdb(
354                            KdbOpenError::InvalidGroupId(Some(gid)),
355                        )))?;
356
357                #[allow(clippy::expect_used)] // group_id was checked before
358                let mut group = db.group_mut(group_id).expect("group must exist");
359
360                let mut entry = group.add_entry();
361                entry.fields = parsing_fields.clone();
362
363                for (desc, data) in entry_attachments.drain() {
364                    entry.add_attachment(desc, Value::protected(data));
365                }
366
367                parsing_fields.clear();
368
369                parsing_gid = None;
370                num_entries += 1;
371            }
372
373            _ => {
374                return Err(DatabaseOpenError::Format(DatabaseFormatError::Kdb(
375                    KdbOpenError::InvalidEntryFieldType(field_type),
376                )));
377            }
378        }
379
380        *data = data
381            .get(6 + field_size as usize..)
382            .ok_or(DatabaseOpenError::UnexpectedEof)?;
383    }
384
385    if parsing_gid.is_some() {
386        return Err(DatabaseOpenError::Format(DatabaseFormatError::Kdb(
387            KdbOpenError::IncompleteEntry,
388        )));
389    }
390
391    Ok(())
392}
393
394pub(crate) fn parse_kdb(data: &[u8], db_key: &DatabaseKey) -> Result<Database, DatabaseOpenError> {
395    let header = KDBHeader::try_from(data)?;
396    let version = DatabaseVersion::KDB(header.subversion as u16);
397
398    // Rest of file after header is payload
399    let payload_encrypted = data.get(HEADER_SIZE..).ok_or(DatabaseOpenError::UnexpectedEof)?;
400
401    // derive master key from composite key, transform_seed, transform_rounds and master_seed
402    let key_elements = db_key.get_key_elements()?;
403    let key_elements: Vec<&[u8]> = key_elements.iter().map(|v| &v[..]).collect();
404    let composite_key = if key_elements.len() == 1 {
405        #[allow(clippy::indexing_slicing, clippy::expect_used)] // key_elements is guaranteed to be 1 byte
406        let key_element: [u8; 32] = key_elements[0]
407            .try_into()
408            .expect("initializing from single element should always succeed");
409        GenericArray::from(key_element) // single pass of SHA256, already done before the call to parse()
410    } else {
411        calculate_sha256(&key_elements) // second pass of SHA256
412    };
413
414    // KDF is always AES
415    let kdf_config = KdfConfig::Aes {
416        rounds: u64::from(header.transform_rounds),
417    };
418
419    let transformed_key = kdf_config
420        .get_kdf_seeded(&header.transform_seed)
421        .transform_key(&composite_key)?;
422
423    let master_key = calculate_sha256(&[&header.master_seed, &transformed_key]);
424
425    let outer_cipher_config = if header.flags & 2 != 0 {
426        OuterCipherConfig::AES256
427    } else if header.flags & 8 != 0 {
428        OuterCipherConfig::Twofish
429    } else {
430        return Err(DatabaseOpenError::Format(DatabaseFormatError::Kdb(
431            KdbOpenError::InvalidFixedCipherID(header.flags),
432        )));
433    };
434
435    // Decrypt payload
436    #[allow(clippy::expect_used)] // master key is fixed-length, should never fail
437    let payload_padded = outer_cipher_config
438        .get_cipher(&master_key, header.encryption_iv.as_ref())
439        .expect("Database key correctly derived")
440        .decrypt(payload_encrypted)?;
441
442    let padlen = payload_padded
443        .last()
444        .copied()
445        .ok_or(DatabaseOpenError::UnexpectedEof)? as usize;
446    let payload = payload_padded
447        .get(..payload_padded.len() - padlen)
448        .ok_or(DatabaseOpenError::UnexpectedEof)?;
449
450    // Check if we decrypted correctly
451    let hash = calculate_sha256(&[payload]);
452    if header.contents_hash != hash.as_slice() {
453        return Err(DatabaseOpenError::Key(DatabaseKeyError::IncorrectKey));
454    }
455
456    let config = DatabaseConfig {
457        version,
458        outer_cipher_config,
459        compression_config: CompressionConfig::None,
460        inner_cipher_config: InnerCipherConfig::Plain,
461        kdf_config,
462        public_custom_data: Default::default(),
463    };
464
465    let mut db = Database::with_data(config, GroupId::new());
466    db.root_mut().name = String::from("Root");
467
468    let mut pos = payload;
469
470    let gid_map = parse_groups(&mut db, header.num_groups, &mut pos)?;
471    parse_entries(&mut db, gid_map, header.num_entries, &mut pos)?;
472
473    Ok(db)
474}
475
476/// Errors that can occur when opening a KeePass 1 database
477#[derive(Debug, Error)]
478#[non_exhaustive]
479pub enum KdbOpenError {
480    /// A field has an invalid length (either too short or too long) compared to the expected
481    /// length for its type
482    #[error("Field of type {field_type} has invalid length {field_size}, expected {expected_field_size}")]
483    InvalidFieldLength {
484        /// The type of the field that has an invalid length
485        field_type: u16,
486
487        /// The actual length of the field as read from the database
488        field_size: u32,
489
490        /// The expected length of the field based on its type, if it is a known fixed-length type
491        expected_field_size: u32,
492    },
493
494    /// A group has an invalid level that does not match the expected level based on the current
495    /// depth of the group tree being parsed
496    #[error("Invalid group level: got {current:?}, expected {expected}")]
497    InvalidGroupLevel {
498        /// The actual level of the group as read from the database, or `None` if the level field
499        /// was missing
500        current: Option<u16>,
501
502        /// The expected level of the group based on the current depth of the group tree being
503        /// parsed
504        expected: u16,
505    },
506
507    /// Encountered the end of a group definition without finding a valid group ID field, or found
508    /// a group ID that does not match any previously defined group
509    #[error("Invalid group ID: {0:?}")]
510    InvalidGroupId(Option<u32>),
511
512    /// Encountered a field type in a group definition that is not recognized as a valid group
513    /// field type
514    #[error("Invalid group field type: {0}")]
515    InvalidGroupFieldType(u16),
516
517    /// Encountered the end of the file before the current group definition was properly terminated
518    /// with an end-of-group field
519    #[error("Group was not terminated before end of file")]
520    IncompleteGroup,
521
522    /// Encountered an entry definition that is missing a valid group ID field
523    #[error("Entry is missing group ID")]
524    EntryMissingGroupId,
525
526    /// Encountered an invalid entry field type
527    #[error("Invalid entry field type: {0}")]
528    InvalidEntryFieldType(u16),
529
530    /// Encountered the end of the file before the current entry definition was properly terminated
531    #[error("Entry was not terminated before end of file")]
532    IncompleteEntry,
533
534    /// The database is encrypted with a cipher that is not recognized or supported
535    #[error("Invalid fixed cipher ID: {0}")]
536    InvalidFixedCipherID(u32),
537}