Skip to main content

keepass_ng/format/
kdbx3.rs

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