Skip to main content

keepass_ng/format/
kdb.rs

1use crate::{
2    config::{CompressionConfig, DatabaseConfig, InnerCipherConfig, KdfConfig, OuterCipherConfig},
3    crypt::calculate_sha256,
4    db::*,
5    error::{DatabaseIntegrityError, DatabaseKeyError, DatabaseOpenError},
6    format::DatabaseVersion,
7    key::DatabaseKey,
8};
9
10use byteorder::{ByteOrder, LittleEndian};
11use cipher::Array;
12
13use std::{collections::HashMap, convert::TryInto, str};
14
15#[derive(Debug)]
16struct KDBHeader {
17    // https://gist.github.com/lgg/e6ccc6e212d18dd2ecd8a8c116fb1e45
18    pub flags: u32,
19    pub subversion: u32,
20    pub master_seed: Vec<u8>,   // 16 bytes
21    pub encryption_iv: Vec<u8>, // 16 bytes
22    pub num_groups: u32,
23    pub num_entries: u32,
24    pub contents_hash: Vec<u8>,  // 32 bytes
25    pub transform_seed: Vec<u8>, // 32 bytes
26    pub transform_rounds: u32,
27}
28
29const HEADER_SIZE: usize = 4 + 4 + 4 + 4 + 16 + 16 + 4 + 4 + 32 + 32 + 4; // first 4 bytes are the KeePass magic
30
31fn parse_header(data: &[u8]) -> Result<KDBHeader, DatabaseIntegrityError> {
32    if data.len() < HEADER_SIZE {
33        return Err(DatabaseIntegrityError::InvalidFixedHeader { size: data.len() });
34    }
35
36    Ok(KDBHeader {
37        flags: LittleEndian::read_u32(&data[8..]),
38        subversion: LittleEndian::read_u32(&data[12..]),
39        master_seed: data[16..32].to_vec(),
40        encryption_iv: data[32..48].to_vec(),
41        num_groups: LittleEndian::read_u32(&data[48..]),
42        num_entries: LittleEndian::read_u32(&data[52..]),
43        contents_hash: data[56..88].to_vec(),
44        transform_seed: data[88..120].to_vec(),
45        transform_rounds: LittleEndian::read_u32(&data[120..]),
46    })
47}
48
49fn from_utf8(data: &[u8]) -> String {
50    String::from_utf8_lossy(data).trim_end_matches('\0').to_owned()
51}
52
53fn ensure_length(field_type: u16, field_size: u32, expected_field_size: u32) -> Result<(), KdbOpenError> {
54    if field_size == expected_field_size {
55        Ok(())
56    } else {
57        Err(KdbOpenError::InvalidFieldLength {
58            field_type,
59            field_size,
60            expected_field_size,
61        })
62    }
63}
64
65fn entry_name(field_type: u16) -> &'static str {
66    match field_type {
67        0x0004 => "Title",
68        0x0005 => "URL",
69        0x0006 => "UserName",
70        0x0008 => "Additional",
71        0x000d => "BinaryDesc",
72        _ => {
73            panic!("Unsupported field type!");
74        }
75    }
76}
77
78// Collapse the tail of a deque of Groups up to the given level
79fn collapse_tail_groups(branch: &mut Vec<NodePtr>, level: usize, root: &NodePtr) -> Option<()> {
80    while level < branch.len() {
81        let leaf = branch.pop()?; // guaranteed to be at least 1 element since 0 <= level < branch.len()
82        let parent = match branch.last() {
83            Some(parent) => parent,
84            None => root,
85        };
86        let count = group_get_children(parent)?.len();
87        group_add_child(parent, leaf, count).ok()?;
88    }
89    Some(())
90}
91
92// A map from a GroupId to a path identifying (by name) a group in the group tree.
93type GidMap = HashMap<u32, Vec<String>>;
94
95fn parse_groups(root: &NodePtr, header_num_groups: u32, data: &mut &[u8]) -> Result<GidMap, KdbOpenError> {
96    // Loop over group TLVs
97    let mut gid_map: HashMap<u32, Vec<String>> = HashMap::new(); // the gid to group path map
98    let mut branch: Vec<NodePtr> = Vec::new(); // the current branch in the group tree
99    let mut group = rc_refcell_node(Group::new("")); // the current group (will be added as a leaf of the branch)
100    let mut level: Option<u16> = None; // the current group's level
101    let mut gid: Option<u32> = None; // the current group's id
102    let mut group_path: Vec<String> = Vec::new(); // the current group path
103    let mut num_groups = 0; // the total number of parsed groups
104    while num_groups < header_num_groups as usize {
105        // Read group TLV
106        let field_type = data.get(0..2).ok_or(KdbOpenError::UnexpectedEof).map(LittleEndian::read_u16)?;
107        let field_size = data.get(2..6).ok_or(KdbOpenError::UnexpectedEof).map(LittleEndian::read_u32)?;
108        let field_value = data.get(6..6 + field_size as usize).ok_or(KdbOpenError::UnexpectedEof)?;
109
110        match field_type {
111            0x0000 => {} // KeePass ignores this field type
112            0x0001 => {
113                // GroupId
114                ensure_length(field_type, field_size, 4)?;
115                gid = Some(LittleEndian::read_u32(field_value));
116            }
117            0x0002 => group.borrow_mut().set_title(Some(&from_utf8(field_value))), // GroupName
118            0x0003..=0x0006 => {
119                // Creation/LastMod/LastAccess/Expire
120                ensure_length(field_type, field_size, 5)?;
121            }
122            0x0007 | 0x0009 => {
123                // ImageId or Flags
124                ensure_length(field_type, field_size, 4)?;
125            }
126            0x0008 => {
127                // Level
128                ensure_length(field_type, field_size, 2)?;
129                level = Some(LittleEndian::read_u16(field_value));
130            }
131            0xffff => {
132                ensure_length(field_type, field_size, 0)?;
133
134                let level = level.ok_or(KdbOpenError::InvalidGroupLevel {
135                    current: None,
136                    expected: branch.len() as u16,
137                })? as usize;
138
139                // Update the current group tree branch (collapse previous sub-branch, initiate
140                // current sub-branch)
141                if level < branch.len() {
142                    group_path.truncate(level);
143                    collapse_tail_groups(&mut branch, level, root).ok_or(KdbOpenError::IncompleteGroup)?;
144                }
145                if level == branch.len() {
146                    group_path.push(group.borrow().get_title().unwrap_or("").to_string());
147                    branch.push(group);
148                } else {
149                    // Level is beyond the current depth, missing intermediate levels?
150                    #[allow(clippy::cast_possible_truncation)]
151                    return Err(KdbOpenError::InvalidGroupLevel {
152                        current: Some(level as u16),
153                        expected: branch.len() as u16,
154                    });
155                }
156
157                // Update the GroupId map and reset state for the next group
158                let group_id = gid.ok_or(KdbOpenError::InvalidGroupId(None))?;
159                gid_map.insert(group_id, group_path.clone());
160                group = rc_refcell_node(Group::new(""));
161                gid = None;
162                num_groups += 1;
163            }
164            _ => {
165                return Err(KdbOpenError::InvalidGroupFieldType(field_type));
166            }
167        }
168
169        *data = data.get(6 + field_size as usize..).ok_or(KdbOpenError::UnexpectedEof)?;
170    }
171    if gid.is_some() {
172        return Err(KdbOpenError::IncompleteGroup);
173    }
174    // Collapse last group tree branch into the root
175    collapse_tail_groups(&mut branch, 0, root).ok_or(KdbOpenError::IncompleteGroup)?;
176
177    Ok(gid_map)
178}
179
180fn parse_entries(root: &NodePtr, gid_map: &GidMap, header_num_entries: u32, data: &mut &[u8]) -> Result<(), KdbOpenError> {
181    // Loop over entry TLVs
182    let mut entry = Entry::default(); // the current entry
183    let mut gid: Option<u32> = None; // the current entry's group id
184    let mut num_entries = 0;
185    while num_entries < header_num_entries {
186        // Read entry TLV
187        let field_type = data.get(0..2).ok_or(KdbOpenError::UnexpectedEof).map(LittleEndian::read_u16)?;
188        let field_size = data.get(2..6).ok_or(KdbOpenError::UnexpectedEof).map(LittleEndian::read_u32)?;
189        let field_value = data.get(6..6 + field_size as usize).ok_or(KdbOpenError::UnexpectedEof)?;
190
191        match field_type {
192            0x0000 => {} // KeePass ignores this field type
193            0x0001 => {
194                // uuid
195                ensure_length(field_type, field_size, 16)?;
196            }
197            0x0002 => {
198                // GroupId
199                ensure_length(field_type, field_size, 4)?;
200                gid = Some(LittleEndian::read_u32(field_value));
201            }
202            0x0003 => {
203                // ImageId
204                ensure_length(field_type, field_size, 4)?;
205            }
206            0x0004 | 0x0005 | 0x0006 | 0x0008 | 0x000d => {
207                // Title/URL/UserName/Additional/BinaryDesc
208                entry.set_unprotected_field_pair(entry_name(field_type), Some(&from_utf8(field_value)));
209            }
210            0x0007 => {
211                // Password
212                entry.set_protected_field_pair("Password", Some(field_value));
213            }
214            0x0009..=0x000c => {
215                // Creation/LastMod/LastAccess/Expire
216                ensure_length(field_type, field_size, 5)?;
217            }
218            0x000e => {
219                // BinaryData
220                entry.set_binary_field_pair("BinaryData", Some(field_value));
221            }
222            0xffff => {
223                ensure_length(field_type, field_size, 0)?;
224
225                let group_id = gid.ok_or(KdbOpenError::EntryMissingGroupId)?;
226                let group_path: Vec<&str> = gid_map
227                    .get(&group_id)
228                    .ok_or(KdbOpenError::InvalidGroupId(Some(group_id)))?
229                    .iter()
230                    .map(std::string::String::as_str)
231                    .collect();
232
233                let group = Group::get(root, group_path.as_slice()).ok_or(KdbOpenError::IncompleteGroup)?;
234                with_node_mut::<Group, _, _>(&group, |group| {
235                    let count = group.get_children().len();
236                    group.add_child(rc_refcell_node(entry), count);
237                    Ok::<(), KdbOpenError>(())
238                })
239                .ok_or(KdbOpenError::IncompleteGroup)??;
240
241                entry = Entry::default();
242                gid = None;
243                num_entries += 1;
244            }
245            _ => {
246                return Err(KdbOpenError::InvalidEntryFieldType(field_type));
247            }
248        }
249
250        *data = data.get(6 + field_size as usize..).ok_or(KdbOpenError::UnexpectedEof)?;
251    }
252    if gid.is_some() {
253        return Err(KdbOpenError::IncompleteEntry);
254    }
255
256    Ok(())
257}
258
259fn parse_db(header: &KDBHeader, data: &[u8]) -> Result<NodePtr, KdbOpenError> {
260    let root = rc_refcell_node(Group::new("Root"));
261
262    let mut pos = data;
263
264    let gid_map = parse_groups(&root, header.num_groups, &mut pos)?;
265
266    parse_entries(&root, &gid_map, header.num_entries, &mut pos)?;
267
268    Ok(root)
269}
270
271pub(crate) fn parse_kdb(data: &[u8], db_key: &DatabaseKey) -> Result<Database, DatabaseOpenError> {
272    let header = parse_header(data)?;
273    #[allow(clippy::cast_possible_truncation)]
274    let version = DatabaseVersion::KDB(header.subversion as u16);
275
276    // Rest of file after header is payload
277    let payload_encrypted = &data[HEADER_SIZE..];
278
279    // derive master key from composite key, transform_seed, transform_rounds and master_seed
280    let key_elements = db_key.get_key_elements()?;
281    let key_elements: Vec<&[u8]> = key_elements.iter().map(|v| &v[..]).collect();
282    let composite_key = if key_elements.len() == 1 {
283        let key_element: [u8; 32] = key_elements[0].try_into().unwrap();
284        Array::from(key_element) // single pass of SHA256, already done before the call to parse()
285    } else {
286        calculate_sha256(&key_elements) // second pass of SHA256
287    };
288
289    // KDF is always AES
290    let kdf_config = KdfConfig::Aes {
291        rounds: u64::from(header.transform_rounds),
292    };
293
294    let transformed_key = kdf_config.get_kdf_seeded(&header.transform_seed).transform_key(&composite_key)?;
295
296    let master_key = calculate_sha256(&[&header.master_seed, transformed_key.as_slice()]);
297
298    let outer_cipher_config = if header.flags & 2 != 0 {
299        OuterCipherConfig::AES256
300    } else if header.flags & 8 != 0 {
301        OuterCipherConfig::Twofish
302    } else {
303        return Err(DatabaseIntegrityError::from(KdbOpenError::InvalidFixedCipherID(header.flags)).into());
304    };
305
306    // Decrypt payload
307    let payload_padded = outer_cipher_config
308        .get_cipher(master_key.as_slice(), header.encryption_iv.as_ref())?
309        .decrypt(payload_encrypted)?;
310    let padlen = payload_padded[payload_padded.len() - 1] as usize;
311    let payload = &payload_padded[..payload_padded.len() - padlen];
312
313    // Check if we decrypted correctly
314    let hash = calculate_sha256(&[payload]);
315    if header.contents_hash != hash.as_slice() {
316        return Err(DatabaseKeyError::IncorrectKey.into());
317    }
318
319    let root_group = parse_db(&header, payload).map_err(DatabaseIntegrityError::from)?;
320
321    let config = DatabaseConfig {
322        version,
323        outer_cipher_config,
324        compression_config: CompressionConfig::None,
325        inner_cipher_config: InnerCipherConfig::Plain,
326        kdf_config,
327        public_custom_data: Default::default(),
328    };
329
330    Ok(Database {
331        config,
332        root: root_group.into(),
333        deleted_objects: Default::default(),
334        meta: Meta::new(),
335    })
336}
337
338/// Errors that can occur when opening a KeePass 1 database
339#[derive(Debug, thiserror::Error)]
340pub enum KdbOpenError {
341    #[error("Unexpected end of file while reading KDB data")]
342    UnexpectedEof,
343
344    #[error("Field of type {field_type} has invalid length {field_size}, expected {expected_field_size}")]
345    InvalidFieldLength {
346        field_type: u16,
347        field_size: u32,
348        expected_field_size: u32,
349    },
350
351    #[error("Invalid group level: got {current:?}, expected {expected}")]
352    InvalidGroupLevel { current: Option<u16>, expected: u16 },
353
354    #[error("Invalid group ID: {0:?}")]
355    InvalidGroupId(Option<u32>),
356
357    #[error("Invalid group field type: {0}")]
358    InvalidGroupFieldType(u16),
359
360    #[error("Group was not terminated before end of file")]
361    IncompleteGroup,
362
363    #[error("Entry is missing group ID")]
364    EntryMissingGroupId,
365
366    #[error("Invalid entry field type: {0}")]
367    InvalidEntryFieldType(u16),
368
369    #[error("Entry was not terminated before end of file")]
370    IncompleteEntry,
371
372    #[error("Invalid fixed cipher ID: {0}")]
373    InvalidFixedCipherID(u32),
374}