keepass_ng/format/kdbx4/
parse.rs

1use std::convert::{TryFrom, TryInto};
2
3use byteorder::{ByteOrder, LittleEndian};
4
5use crate::{
6    config::{CompressionConfig, DatabaseConfig, InnerCipherConfig, KdfConfig, OuterCipherConfig},
7    crypt::{self, ciphers::Cipher},
8    db::{Database, HeaderAttachment, rc_refcell_node},
9    error::{DatabaseIntegrityError, DatabaseKeyError, DatabaseOpenError},
10    format::{
11        DatabaseVersion,
12        kdbx4::{
13            HEADER_COMMENT, HEADER_COMPRESSION_ID, HEADER_ENCRYPTION_IV, HEADER_END, HEADER_KDF_PARAMS, HEADER_MASTER_SEED,
14            HEADER_OUTER_ENCRYPTION_ID, HEADER_PUBLIC_CUSTOM_DATA, INNER_HEADER_BINARY_ATTACHMENTS, INNER_HEADER_END,
15            INNER_HEADER_RANDOM_STREAM_ID, INNER_HEADER_RANDOM_STREAM_KEY, KDBX4OuterHeader,
16        },
17    },
18    hmac_block_stream,
19    key::DatabaseKey,
20    variant_dictionary::VariantDictionary,
21};
22
23use super::KDBX4InnerHeader;
24
25impl From<&[u8]> for HeaderAttachment {
26    fn from(data: &[u8]) -> Self {
27        let flags = data[0];
28        let content = data[1..].to_vec();
29
30        HeaderAttachment { flags, content }
31    }
32}
33
34/// Open, decrypt and parse a `KeePass` database from a source and key elements
35pub(crate) fn parse_kdbx4(data: &[u8], db_key: &DatabaseKey) -> Result<Database, DatabaseOpenError> {
36    let (config, header_attachments, mut inner_decryptor, xml) = decrypt_kdbx4(data, db_key)?;
37
38    let database_content = crate::xml_db::parse::parse(&xml, &mut *inner_decryptor)?;
39
40    let db = Database {
41        config,
42        header_attachments,
43        root: rc_refcell_node(database_content.root.group).into(),
44        deleted_objects: database_content.root.deleted_objects,
45        meta: database_content.meta,
46    };
47
48    Ok(db)
49}
50
51/// Open and decrypt a `KeePass` KDBX4 database from a source and key elements
52#[allow(clippy::type_complexity)]
53pub(crate) fn decrypt_kdbx4(
54    data: &[u8],
55    db_key: &DatabaseKey,
56) -> Result<(DatabaseConfig, Vec<HeaderAttachment>, Box<dyn Cipher>, Vec<u8>), DatabaseOpenError> {
57    // parse header
58    let (outer_header, inner_header_start) = parse_outer_header(data)?;
59
60    // split file into segments:
61    //      header_data         - The outer header data
62    //      header_sha256       - A Sha256 hash of header_data (for verification of header integrity)
63    //      header_hmac         - A HMAC of the header_data (for verification of the key_elements)
64    //      hmac_block_stream   - A HMAC-verified block stream of encrypted and compressed blocks
65    let header_data = &data[0..inner_header_start];
66    let header_sha256 = &data[inner_header_start..(inner_header_start + 32)];
67    let header_hmac = &data[(inner_header_start + 32)..(inner_header_start + 64)];
68    let hmac_block_stream = &data[(inner_header_start + 64)..];
69
70    // verify header
71    if header_sha256 != crypt::calculate_sha256(&[header_data]).as_slice() {
72        return Err(DatabaseIntegrityError::HeaderHashMismatch.into());
73    }
74
75    #[cfg(feature = "challenge_response")]
76    let db_key = db_key.clone().perform_challenge(&outer_header.kdf_seed)?;
77
78    // derive master key from composite key, transform_seed, transform_rounds and master_seed
79    let key_elements = db_key.get_key_elements()?;
80    let key_elements: Vec<&[u8]> = key_elements.iter().map(|v| &v[..]).collect();
81    let composite_key = crypt::calculate_sha256(&key_elements);
82    let transformed_key = outer_header
83        .kdf_config
84        .get_kdf_seeded(&outer_header.kdf_seed)
85        .transform_key(&composite_key)?;
86    let t_k = transformed_key.as_slice();
87    let master_key = crypt::calculate_sha256(&[outer_header.master_seed.as_ref(), t_k]);
88
89    // verify credentials
90    let hmac_key = crypt::calculate_sha512(&[&outer_header.master_seed, t_k, &hmac_block_stream::HMAC_KEY_END]);
91    let header_hmac_key = hmac_block_stream::get_hmac_block_key(u64::MAX, &hmac_key);
92    if header_hmac != crypt::calculate_hmac(&[header_data], header_hmac_key.as_slice())?.as_slice() {
93        return Err(DatabaseKeyError::IncorrectKey.into());
94    }
95
96    // read encrypted payload from hmac-verified block stream
97    let payload_encrypted = hmac_block_stream::read_hmac_block_stream(hmac_block_stream, &hmac_key)?;
98
99    // Decrypt and decompress encrypted payload
100    let payload_compressed = outer_header
101        .outer_cipher_config
102        .get_cipher(master_key.as_slice(), &outer_header.outer_iv)?
103        .decrypt(&payload_encrypted)?;
104
105    let payload = outer_header.compression_config.get_compression().decompress(&payload_compressed)?;
106
107    // KDBX4 has inner header, too - parse it
108    let (header_attachments, inner_header, body_start) = parse_inner_header(&payload)?;
109
110    // after inner header is one XML document
111    let xml = &payload[body_start..];
112
113    // initialize the inner decryptor
114    let inner_decryptor = inner_header.inner_random_stream.get_cipher(&inner_header.inner_random_stream_key);
115
116    let config = DatabaseConfig {
117        version: outer_header.version,
118        outer_cipher_config: outer_header.outer_cipher_config,
119        compression_config: outer_header.compression_config,
120        inner_cipher_config: inner_header.inner_random_stream,
121        kdf_config: outer_header.kdf_config,
122        public_custom_data: outer_header.public_custom_data,
123    };
124
125    Ok((config, header_attachments, inner_decryptor, xml.to_vec()))
126}
127
128fn parse_outer_header(data: &[u8]) -> Result<(KDBX4OuterHeader, usize), DatabaseOpenError> {
129    let version = DatabaseVersion::parse(data)?;
130
131    // skip over the version header
132    let mut pos = DatabaseVersion::get_version_header_size();
133
134    let mut outer_cipher: Option<OuterCipherConfig> = None;
135    let mut compression_config: Option<CompressionConfig> = None;
136    let mut master_seed: Option<Vec<u8>> = None;
137    let mut outer_iv: Option<Vec<u8>> = None;
138    let mut kdf_config: Option<KdfConfig> = None;
139    let mut kdf_seed: Option<Vec<u8>> = None;
140    let mut public_custom_data: Option<VariantDictionary> = None;
141
142    // parse header
143    loop {
144        // parse header blocks.
145        //
146        // every block is a triplet of (3 + entry_length) bytes with this structure:
147        //
148        // (
149        //   entry_type: u8,                        // a numeric entry type identifier
150        //   entry_length: u32,                     // length of the entry buffer
151        //   entry_buffer: [u8; entry_length]       // the entry buffer
152        // )
153
154        let entry_type = data[pos];
155        let entry_length: usize = LittleEndian::read_u32(&data[pos + 1..(pos + 5)]) as usize;
156        let entry_buffer = &data[(pos + 5)..(pos + 5 + entry_length)];
157
158        pos += 5 + entry_length;
159
160        match entry_type {
161            HEADER_END => {
162                break;
163            }
164
165            HEADER_COMMENT => {}
166
167            HEADER_OUTER_ENCRYPTION_ID => {
168                outer_cipher = Some(OuterCipherConfig::try_from(entry_buffer)?);
169            }
170
171            HEADER_COMPRESSION_ID => {
172                compression_config = Some(CompressionConfig::try_from(LittleEndian::read_u32(entry_buffer))?);
173            }
174
175            HEADER_MASTER_SEED => master_seed = Some(entry_buffer.to_vec()),
176
177            HEADER_ENCRYPTION_IV => outer_iv = Some(entry_buffer.to_vec()),
178
179            HEADER_KDF_PARAMS => {
180                let vd = VariantDictionary::parse(entry_buffer)?;
181                let (kconf, kseed) = vd.try_into()?;
182                kdf_config = Some(kconf);
183                kdf_seed = Some(kseed);
184            }
185
186            HEADER_PUBLIC_CUSTOM_DATA => {
187                let vd = VariantDictionary::parse(entry_buffer)?;
188                public_custom_data = Some(vd)
189            }
190
191            _ => {
192                return Err(DatabaseIntegrityError::InvalidOuterHeaderEntry { entry_type }.into());
193            }
194        };
195    }
196
197    // at this point, the header needs to be fully defined - unwrap options and return errors if
198    // something is missing
199
200    fn get_or_err<T>(v: Option<T>, err: &str) -> Result<T, DatabaseIntegrityError> {
201        v.ok_or_else(|| DatabaseIntegrityError::IncompleteOuterHeader { missing_field: err.into() })
202    }
203
204    let outer_cipher_config = get_or_err(outer_cipher, "Outer Cipher ID")?;
205    let compression_config = get_or_err(compression_config, "Compression ID")?;
206    let master_seed = get_or_err(master_seed, "Master seed")?;
207    let outer_iv = get_or_err(outer_iv, "Outer IV")?;
208    let kdf_config = get_or_err(kdf_config, "Key Derivation Function Parameters")?;
209    let kdf_seed = get_or_err(kdf_seed, "Key Derivation Function Seed")?;
210
211    Ok((
212        KDBX4OuterHeader {
213            version,
214            outer_cipher_config,
215            compression_config,
216            master_seed,
217            outer_iv,
218            kdf_config,
219            kdf_seed,
220            public_custom_data,
221        },
222        pos,
223    ))
224}
225
226fn parse_inner_header(data: &[u8]) -> Result<(Vec<HeaderAttachment>, KDBX4InnerHeader, usize), DatabaseOpenError> {
227    let mut pos = 0;
228
229    let mut inner_random_stream = None;
230    let mut inner_random_stream_key = None;
231    let mut header_attachments = Vec::new();
232
233    loop {
234        let entry_type = data[pos];
235        let entry_length: usize = LittleEndian::read_u32(&data[pos + 1..(pos + 5)]) as usize;
236        let entry_buffer = &data[(pos + 5)..(pos + 5 + entry_length)];
237
238        pos += 5 + entry_length;
239
240        match entry_type {
241            INNER_HEADER_END => break,
242
243            INNER_HEADER_RANDOM_STREAM_ID => {
244                inner_random_stream = Some(InnerCipherConfig::try_from(LittleEndian::read_u32(entry_buffer))?);
245            }
246
247            INNER_HEADER_RANDOM_STREAM_KEY => inner_random_stream_key = Some(entry_buffer.to_vec()),
248
249            INNER_HEADER_BINARY_ATTACHMENTS => {
250                let header_attachment = HeaderAttachment::from(entry_buffer);
251                header_attachments.push(header_attachment);
252            }
253
254            _ => {
255                return Err(DatabaseIntegrityError::InvalidInnerHeaderEntry { entry_type }.into());
256            }
257        }
258    }
259
260    fn get_or_err<T>(v: Option<T>, err: &str) -> Result<T, DatabaseIntegrityError> {
261        v.ok_or_else(|| DatabaseIntegrityError::IncompleteInnerHeader { missing_field: err.into() })
262    }
263
264    let inner_random_stream = get_or_err(inner_random_stream, "Inner random stream")?;
265    let inner_random_stream_key = get_or_err(inner_random_stream_key, "Inner random stream key")?;
266
267    let inner_header = KDBX4InnerHeader {
268        inner_random_stream,
269        inner_random_stream_key,
270    };
271
272    Ok((header_attachments, inner_header, pos))
273}