Skip to main content

keepass/format/
kdbx3.rs

1use crate::{
2    config::{CompressionConfig, DatabaseConfig, InnerCipherConfig, KdfConfig, OuterCipherConfig},
3    crypt::{calculate_sha256, ciphers::Cipher},
4    db::{Database, DatabaseFormatError, DatabaseOpenError},
5    format::DatabaseVersion,
6    key::{DatabaseKey, DatabaseKeyError},
7};
8
9use byteorder::{ByteOrder, LittleEndian};
10use thiserror::Error;
11
12use std::convert::TryFrom;
13
14#[derive(Debug)]
15struct KDBX3Header {
16    // https://gist.github.com/msmuenchen/9318327
17    outer_cipher: OuterCipherConfig,
18    compression: CompressionConfig,
19    master_seed: Vec<u8>,
20
21    transform_seed: Vec<u8>,
22    kdf_config: KdfConfig,
23
24    outer_iv: Vec<u8>,
25    protected_stream_key: Vec<u8>,
26    stream_start: Vec<u8>,
27    inner_cipher: InnerCipherConfig,
28    body_start: usize,
29}
30
31fn parse_outer_header(data: &[u8]) -> Result<KDBX3Header, Kdbx3OuterHeaderError> {
32    let mut outer_cipher: Option<OuterCipherConfig> = None;
33    let mut compression: Option<CompressionConfig> = None;
34    let mut master_seed: Option<Vec<u8>> = None;
35    let mut transform_seed: Option<Vec<u8>> = None;
36    let mut transform_rounds: Option<u64> = None;
37    let mut outer_iv: Option<Vec<u8>> = None;
38    let mut protected_stream_key: Option<Vec<u8>> = None;
39    let mut stream_start: Option<Vec<u8>> = None;
40    let mut inner_cipher: Option<InnerCipherConfig> = None;
41
42    // skip over the version header
43    let mut pos = DatabaseVersion::get_version_header_size();
44
45    // parse header
46    loop {
47        // parse header blocks.
48        //
49        // every block is a triplet of (3 + entry_length) bytes with this structure:
50        //
51        // (
52        //   entry_type: u8,                        // a numeric entry type identifier
53        //   entry_length: u16,                     // length of the entry buffer
54        //   entry_buffer: [u8; entry_length]       // the entry buffer
55        // )
56
57        let entry_type = *data.get(pos).ok_or(Kdbx3OuterHeaderError::UnexpectedEof)?;
58
59        let entry_length = data
60            .get((pos + 1)..(pos + 3))
61            .ok_or(Kdbx3OuterHeaderError::UnexpectedEof)?;
62        let entry_length: usize = LittleEndian::read_u16(entry_length) as usize;
63
64        let entry_buffer = data
65            .get((pos + 3)..(pos + 3 + entry_length))
66            .ok_or(Kdbx3OuterHeaderError::UnexpectedEof)?;
67
68        pos += 3 + entry_length;
69
70        match entry_type {
71            // END - finished parsing header
72            0 => {
73                break;
74            }
75
76            // COMMENT
77            1 => {}
78
79            // CIPHERID - a UUID specifying which cipher suite
80            //            should be used to encrypt the payload
81            2 => {
82                outer_cipher = Some(OuterCipherConfig::try_from(entry_buffer)?);
83            }
84
85            // COMPRESSIONFLAGS - first byte determines compression of payload
86            3 => {
87                compression = Some(CompressionConfig::try_from(LittleEndian::read_u32(entry_buffer))?);
88            }
89
90            // MASTERSEED - Master seed for deriving the master key
91            4 => master_seed = Some(entry_buffer.to_vec()),
92
93            // TRANSFORMSEED - Seed used in deriving the transformed key
94            5 => transform_seed = Some(entry_buffer.to_vec()),
95
96            // TRANSFORMROUNDS - Number of rounds used in derivation of transformed key
97            6 => transform_rounds = Some(LittleEndian::read_u64(entry_buffer)),
98
99            // ENCRYPTIONIV - Initialization Vector for decrypting the payload
100            7 => outer_iv = Some(entry_buffer.to_vec()),
101
102            // PROTECTEDSTREAMKEY - Key for decrypting the inner protected values
103            8 => protected_stream_key = Some(entry_buffer.to_vec()),
104
105            // STREAMSTARTBYTES - First bytes of decrypted payload (to check correct decryption)
106            9 => stream_start = Some(entry_buffer.to_vec()),
107
108            // INNERRANDOMSTREAMID - specifies which cipher suite
109            //                       to use for decrypting the inner protected values
110            10 => {
111                inner_cipher = Some(InnerCipherConfig::try_from(LittleEndian::read_u32(entry_buffer))?);
112            }
113
114            _ => {
115                return Err(Kdbx3OuterHeaderError::InvalidOuterHeaderEntry(entry_type));
116            }
117        };
118    }
119
120    // at this point, the header needs to be fully defined - unwrap options and return errors if
121    // something is missing
122
123    fn get_or_err<T>(v: Option<T>, err: &'static str) -> Result<T, Kdbx3OuterHeaderError> {
124        v.ok_or(Kdbx3OuterHeaderError::IncompleteOuterHeader(err))
125    }
126
127    let outer_cipher = get_or_err(outer_cipher, "Outer Cipher ID")?;
128    let compression = get_or_err(compression, "Compression ID")?;
129    let master_seed = get_or_err(master_seed, "Master seed")?;
130    let transform_seed = get_or_err(transform_seed, "Transform seed")?;
131    let transform_rounds = get_or_err(transform_rounds, "Number of transformation rounds")?;
132    let outer_iv = get_or_err(outer_iv, "Outer cipher IV")?;
133    let protected_stream_key = get_or_err(protected_stream_key, "Protected stream key")?;
134    let stream_start = get_or_err(stream_start, "Stream start bytes")?;
135    let inner_cipher = get_or_err(inner_cipher, "Inner cipher ID")?;
136
137    // KDF type is always AES for KDBX3
138    let kdf_config = KdfConfig::Aes {
139        rounds: transform_rounds,
140    };
141
142    Ok(KDBX3Header {
143        outer_cipher,
144        compression,
145        master_seed,
146        transform_seed,
147        kdf_config,
148        outer_iv,
149        protected_stream_key,
150        stream_start,
151        inner_cipher,
152        body_start: pos,
153    })
154}
155
156/// Errors related to parsing the outer header of a KDBX3 database
157#[derive(Debug, Error)]
158#[non_exhaustive]
159pub enum Kdbx3OuterHeaderError {
160    /// Errors related to parsing the inner cipher configuration
161    #[error(transparent)]
162    InnerCipher(#[from] crate::config::InnerCipherConfigError),
163
164    /// Errors related to parsing the outer cipher configuration
165    #[error(transparent)]
166    OuterCipher(#[from] crate::config::OuterCipherConfigError),
167
168    /// Errors related to parsing the compression configuration
169    #[error(transparent)]
170    Compression(#[from] crate::config::CompressionConfigError),
171
172    /// An outer header entry has an invalid type identifier
173    #[error("Encountered invalid outer header entry with type {0}")]
174    InvalidOuterHeaderEntry(u8),
175
176    /// The outer header is missing a required entry
177    #[error("Outer header is missing {0}")]
178    IncompleteOuterHeader(&'static str),
179
180    /// Encountered an unexpected end of file while reading the outer header
181    #[error("Unexpected end of file while reading outer header")]
182    UnexpectedEof,
183}
184
185/// Open, decrypt and parse a KeePass database from a source and a password
186pub(crate) fn parse_kdbx3(data: &[u8], db_key: &DatabaseKey) -> Result<Database, DatabaseOpenError> {
187    let (config, mut inner_decryptor, xml) = decrypt_kdbx3(data, db_key)?;
188
189    // Parse XML data blocks
190    let mut db = crate::format::xml_db::parse_xml(&xml, &[], &mut *inner_decryptor)
191        .map_err(|e| DatabaseOpenError::Format(DatabaseFormatError::Kdbx3(Kdbx3OpenError::Xml(e))))?;
192
193    db.config = config;
194
195    Ok(db)
196}
197
198/// Open and decrypt a KeePass KDBX3 database from a source and a password
199#[allow(clippy::type_complexity)]
200pub(crate) fn decrypt_kdbx3(
201    data: &[u8],
202    db_key: &DatabaseKey,
203) -> Result<(DatabaseConfig, Box<dyn Cipher>, Vec<u8>), DatabaseOpenError> {
204    let version = DatabaseVersion::parse(data)?;
205    let header = parse_outer_header(data)
206        .map_err(|e| DatabaseOpenError::Format(DatabaseFormatError::Kdbx3(Kdbx3OpenError::OuterHeader(e))))?;
207
208    let inner_decryptor = header.inner_cipher.get_cipher(&header.protected_stream_key)?;
209
210    let config = DatabaseConfig {
211        version,
212        outer_cipher_config: header.outer_cipher,
213        compression_config: header.compression,
214        inner_cipher_config: header.inner_cipher,
215        kdf_config: header.kdf_config,
216        public_custom_data: Default::default(),
217    };
218
219    let mut pos = header.body_start;
220
221    // Turn enums into appropriate trait objects
222    let compression = config.compression_config.get_compression();
223
224    // Rest of file after header is payload
225    let payload_encrypted = data.get(pos..).ok_or(DatabaseOpenError::UnexpectedEof)?;
226
227    // derive master key from composite key, transform_seed, transform_rounds and master_seed
228    let key_elements = db_key.get_key_elements()?;
229    let key_elements: Vec<&[u8]> = key_elements.iter().map(|v| &v[..]).collect();
230    let composite_key = calculate_sha256(&key_elements);
231
232    // transform the key
233    let transformed_key = config
234        .kdf_config
235        .get_kdf_seeded(&header.transform_seed)
236        .transform_key(&composite_key)?;
237
238    let master_key = calculate_sha256(&[header.master_seed.as_ref(), &transformed_key]);
239
240    // Decrypt payload
241    let payload = config
242        .outer_cipher_config
243        .get_cipher(&master_key, header.outer_iv.as_ref())?
244        .decrypt(payload_encrypted)?;
245
246    // Check if we decrypted correctly
247    let payload_start = payload
248        .get(0..header.stream_start.len())
249        .ok_or(DatabaseOpenError::UnexpectedEof)?;
250    if payload_start != header.stream_start.as_slice() {
251        return Err(DatabaseKeyError::IncorrectKey.into());
252    }
253
254    let mut buf = Vec::new();
255
256    pos = 32;
257    let mut block_index = 0;
258    loop {
259        // Parse blocks in payload.
260        //
261        // Each block is a tuple of size (40 + block_size) with structure:
262        //
263        // (
264        //   block_id: u32,                                 // a numeric block ID (starts at 0)
265        //   block_hash: [u8, 32],                          // SHA256 of block_buffer_compressed
266        //   block_size: u32,                               // block_size size in bytes
267        //   block_buffer_compressed: [u8, block_size]      // Block data, possibly compressed
268        // )
269
270        // let block_id = LittleEndian::read_u32(&payload[pos..(pos + 4)]);
271        let block_hash = payload
272            .get((pos + 4)..(pos + 36))
273            .ok_or(DatabaseOpenError::UnexpectedEof)?;
274
275        let block_size = payload
276            .get((pos + 36)..(pos + 40))
277            .ok_or(DatabaseOpenError::UnexpectedEof)?;
278        let block_size = LittleEndian::read_u32(block_size) as usize;
279
280        // A block with size 0 means we have hit EOF
281        if block_size == 0 {
282            break;
283        }
284
285        let block_buffer_compressed = payload
286            .get((pos + 40)..(pos + 40 + block_size))
287            .ok_or(DatabaseOpenError::UnexpectedEof)?;
288
289        // Test block hash
290        let block_hash_check = calculate_sha256(&[block_buffer_compressed]);
291        if block_hash != block_hash_check.as_slice() {
292            return Err(DatabaseOpenError::Format(DatabaseFormatError::Kdbx3(
293                Kdbx3OpenError::BlockHashMismatch(block_index),
294            )));
295        }
296
297        // Decompress block_buffer_compressed
298        buf.append(&mut block_buffer_compressed.to_vec());
299
300        pos += 40 + block_size;
301        block_index += 1;
302    }
303
304    let xml = compression.decompress(&buf)?;
305
306    Ok((config, inner_decryptor, xml))
307}
308
309/// Errors related to opening a KDBX3 database
310#[derive(Debug, Error)]
311#[non_exhaustive]
312pub enum Kdbx3OpenError {
313    /// Errors related to parsing the outer header of the KDBX3 database
314    #[error(transparent)]
315    OuterHeader(#[from] Kdbx3OuterHeaderError),
316
317    /// A data block has a hash that does not match the expected hash, indicating possible corruption
318    #[error("block hash mismatch at block index {0}")]
319    BlockHashMismatch(usize),
320
321    /// Errors related to parsing the inner XML database of the KDBX3 file
322    #[error(transparent)]
323    Xml(#[from] crate::format::xml_db::ParseXmlError),
324}