Skip to main content

keepass/key/
mod.rs

1use std::io::Read;
2
3use base64::{engine::general_purpose as base64_engine, Engine as _};
4use quick_xml::{encoding::EncodingError, events::Event, reader::Reader};
5use thiserror::Error;
6use zeroize::{Zeroize, ZeroizeOnDrop};
7
8use crate::crypt::calculate_sha256;
9
10pub type KeyElement = Vec<u8>;
11pub type KeyElements = Vec<KeyElement>;
12
13#[cfg(feature = "challenge_response")]
14mod yubikey;
15
16#[cfg(feature = "challenge_response")]
17pub use yubikey::{ChallengeResponseKey, ChallengeResponseKeyError};
18
19fn parse_xml_keyfile(xml: &[u8]) -> Result<KeyElement, ParseXmlKeyFileError> {
20    let mut tag_stack = Vec::new();
21
22    let mut key_version: Option<String> = None;
23    let mut key_value: Option<String> = None;
24
25    let mut reader = Reader::from_reader(xml);
26    let mut buf = Vec::new();
27
28    loop {
29        match reader.read_event_into(&mut buf)? {
30            Event::Eof => break,
31
32            Event::Start(e) => {
33                tag_stack.push(e.name().as_ref().to_string());
34            }
35
36            Event::End(_) => {
37                tag_stack.pop();
38            }
39
40            Event::Text(e) => {
41                let s = e.into_inner().into_owned();
42
43                if tag_stack == ["KeyFile", "Meta", "Version"] {
44                    key_version = Some(s);
45                    continue;
46                }
47
48                if tag_stack == ["KeyFile", "Key", "Data"] {
49                    key_value = Some(s);
50                    continue;
51                }
52            }
53
54            _ => (),
55        }
56    }
57
58    let key_value = key_value.ok_or(ParseXmlKeyFileError::EmptyKey)?;
59
60    let key_bytes = key_value.as_bytes().to_vec();
61
62    if key_version == Some("2.0".to_string()) {
63        // TODO we should also validate the integrity of a v2 keyfile using the hash value
64
65        let trimmed_key = key_value
66            .trim()
67            .replace(" ", "")
68            .replace("\n", "")
69            .replace("\t", "")
70            .replace("\r", "");
71
72        return if let Ok(key) = hex::decode(&trimmed_key) {
73            Ok(key)
74        } else {
75            Ok(key_bytes)
76        };
77    }
78
79    // Check if the key is base64-encoded. If yes, return decoded bytes
80    if let Ok(key) = base64_engine::STANDARD.decode(&key_bytes) {
81        Ok(key)
82    } else {
83        Ok(key_bytes)
84    }
85}
86
87/// Errors that can occur when parsing an XML keyfile
88#[derive(Debug, Error)]
89#[non_exhaustive]
90pub enum ParseXmlKeyFileError {
91    /// No key data element was found in the XML keyfile
92    #[error("The XML keyfile is missing a key data element")]
93    EmptyKey,
94
95    /// A tag in the XML keyfile contains text that cannot be decoded as UTF-8
96    #[error(transparent)]
97    Encoding(#[from] EncodingError),
98
99    /// An error occurred while reading the XML keyfile
100    #[error(transparent)]
101    Xml(#[from] quick_xml::Error),
102}
103
104fn parse_keyfile(buffer: &[u8]) -> Result<KeyElement, DatabaseKeyError> {
105    // try to parse the buffer as XML, if successful, use that data instead of full file
106    if let Ok(v) = parse_xml_keyfile(buffer) {
107        return Ok(v);
108    }
109
110    // legacy binary key format
111    if buffer.len() == 32 {
112        return Ok(buffer.to_vec());
113    }
114
115    // legacy hex key format
116    if buffer.len() == 64 {
117        if let Ok(key_bytes) = hex::decode(buffer) {
118            return Ok(key_bytes);
119        }
120    }
121
122    // interpret as a "bare" keyfile and hash the entire file contents
123    Ok(calculate_sha256(&[buffer]).as_slice().to_vec())
124}
125
126/// A KeePass key, which might consist of a password and/or a keyfile
127#[derive(Debug, Clone, Default, PartialEq, Zeroize, ZeroizeOnDrop)]
128pub struct DatabaseKey {
129    password: Option<String>,
130    keyfile: Option<Vec<u8>>,
131    #[cfg(feature = "challenge_response")]
132    challenge_response_key: Option<ChallengeResponseKey>,
133    #[cfg(feature = "challenge_response")]
134    challenge_response_result: Option<KeyElement>,
135}
136
137impl DatabaseKey {
138    /// Modify the database key to include a password
139    pub fn with_password(mut self, password: &str) -> Self {
140        self.password = Some(password.to_string());
141        self
142    }
143
144    /// Modify the database key to include a password, which is read from a prompt
145    #[cfg(feature = "utilities")]
146    pub fn with_password_from_prompt(mut self, prompt_message: &str) -> Result<Self, std::io::Error> {
147        self.password = Some(rpassword::prompt_password(prompt_message)?);
148        Ok(self)
149    }
150
151    /// Modify the database key to include a challenge-response key, where the secret is read from
152    /// a prompt
153    #[cfg(all(feature = "challenge_response", feature = "utilities"))]
154    pub fn with_hmac_sha1_secret_from_prompt(mut self, prompt_message: &str) -> Result<Self, std::io::Error> {
155        self.challenge_response_key = Some(ChallengeResponseKey::LocalChallenge(rpassword::prompt_password(
156            prompt_message,
157        )?));
158        Ok(self)
159    }
160
161    /// Modify the database key to include a keyfile
162    ///
163    /// The keyfile is only read as raw data but not parsed until the actual key elements are
164    /// requested, so errors with keyfile parsing will only be raised at that point, not when
165    /// calling this method.
166    pub fn with_keyfile(mut self, keyfile: &mut dyn Read) -> Result<Self, std::io::Error> {
167        let mut buf = Vec::new();
168        keyfile.read_to_end(&mut buf)?;
169
170        self.keyfile = Some(buf);
171
172        Ok(self)
173    }
174
175    /// Modify the database key to include a challenge-response key
176    #[cfg(feature = "challenge_response")]
177    pub fn with_challenge_response_key(mut self, challenge_response_key: ChallengeResponseKey) -> Self {
178        self.challenge_response_key = Some(challenge_response_key);
179        self
180    }
181
182    /// Perform the challenge-response operation for the database key, if a challenge-response key
183    /// is present.
184    #[cfg(feature = "challenge_response")]
185    pub fn perform_challenge(mut self, kdf_seed: &[u8]) -> Result<Self, DatabaseKeyError> {
186        if let Some(challenge_response_key) = &self.challenge_response_key {
187            let response = challenge_response_key.perform_challenge(kdf_seed)?;
188            self.challenge_response_result = Some(response);
189        }
190
191        Ok(self)
192    }
193
194    /// Create a new, empty database key
195    pub fn new() -> Self {
196        Default::default()
197    }
198
199    pub(crate) fn get_key_elements(&self) -> Result<KeyElements, DatabaseKeyError> {
200        let mut out = Vec::new();
201
202        if let Some(p) = &self.password {
203            out.push(calculate_sha256(&[p.as_bytes()]).to_vec());
204        }
205
206        if let Some(ref f) = self.keyfile {
207            out.push(parse_keyfile(f)?);
208        }
209
210        if out.is_empty() {
211            return Err(DatabaseKeyError::EmptyKey);
212        }
213
214        #[cfg(feature = "challenge_response")]
215        if let Some(result) = &self.challenge_response_result {
216            out.push(calculate_sha256(&[result]).as_slice().to_vec());
217        } else if self.challenge_response_key.is_some() {
218            return Err(DatabaseKeyError::ChallengeResponse(
219                crate::key::yubikey::ChallengeResponseKeyError::NotPerformed,
220            ));
221        }
222
223        Ok(out)
224    }
225
226    /// Returns true if the database key is not associated with any key component.
227    pub fn is_empty(&self) -> bool {
228        if self.password.is_some() || self.keyfile.is_some() {
229            return false;
230        }
231        #[cfg(feature = "challenge_response")]
232        if self.challenge_response_key.is_some() {
233            return false;
234        }
235        true
236    }
237}
238
239/// Errors that can occur when working with database keys
240#[derive(Debug, Error)]
241#[non_exhaustive]
242pub enum DatabaseKeyError {
243    /// The database key contains no components, i.e. no password, keyfile or challenge-response key
244    #[error("The key contains no components")]
245    EmptyKey,
246
247    /// The database key is incorrect
248    #[error("Incorrect key")]
249    IncorrectKey,
250
251    /// An I/O error occurred while reading the keyfile
252    #[error("I/O error reading keyfile: {0}")]
253    Io(#[from] std::io::Error),
254
255    /// An error occurred while parsing the XML keyfile
256    #[error("XML error reading keyfile: {0}")]
257    Xml(#[from] quick_xml::Error),
258
259    /// An error occurred while parsing the non-XML keyfile
260    #[error("Invalid keyfile format")]
261    InvalidKeyFile,
262
263    /// An error occurred during challenge-response authentication
264    #[cfg(feature = "challenge_response")]
265    #[error("Challenge-response key error: {0}")]
266    ChallengeResponse(#[from] crate::key::yubikey::ChallengeResponseKeyError),
267}
268
269#[cfg(test)]
270mod key_tests {
271
272    use super::{DatabaseKey, DatabaseKeyError};
273
274    #[test]
275    fn test_key() -> Result<(), DatabaseKeyError> {
276        let ke = DatabaseKey::new().with_password("asdf").get_key_elements()?;
277        assert_eq!(ke.len(), 1);
278
279        let ke = DatabaseKey::new()
280            .with_keyfile(&mut "bare-key-file".as_bytes())?
281            .get_key_elements()?;
282        assert_eq!(ke.len(), 1);
283
284        let ke = DatabaseKey::new()
285            .with_keyfile(&mut "0123456789ABCDEF0123456789ABCDEF".as_bytes())?
286            .get_key_elements()?;
287        assert_eq!(ke.len(), 1);
288
289        let ke = DatabaseKey::new()
290            .with_password("asdf")
291            .with_keyfile(&mut "bare-key-file".as_bytes())?
292            .get_key_elements()?;
293        assert_eq!(ke.len(), 2);
294
295        let ke = DatabaseKey::new()
296            .with_keyfile(
297                &mut "<KeyFile><Key><Data>0!23456789ABCDEF0123456789ABCDEF</Data></Key></KeyFile>".as_bytes(),
298            )?
299            .get_key_elements()?;
300        assert_eq!(ke.len(), 1);
301
302        let ke = DatabaseKey::new()
303            .with_keyfile(
304                &mut "<KeyFile><Key><Data>NXyYiJMHg3ls+eBmjbAjWec9lcOToJiofbhNiFMTJMw=</Data></Key></KeyFile>"
305                    .as_bytes(),
306            )?
307            .get_key_elements()?;
308        assert_eq!(ke.len(), 1);
309
310        let xml_keyfile_v2 = r###"
311            <?xml version="1.0" encoding="utf-8"?>
312            <KeyFile>
313                <Meta>
314                    <Version>2.0</Version>
315                </Meta>
316                <Key>
317                    <Data Hash="A65F0C2D">
318                        36057B1C 35037FD9 62257893 C0A22403
319                        EE3F8FBB 504D9981 08B821CB 00D28F89
320                    </Data>
321                </Key>
322            </KeyFile>
323        "###;
324        let ke = DatabaseKey::new()
325            .with_keyfile(&mut xml_keyfile_v2.trim().as_bytes())?
326            .get_key_elements()?;
327        assert_eq!(ke.len(), 1);
328
329        // other XML files will just be hashed as a "bare" keyfile
330        let ke = DatabaseKey::new()
331            .with_keyfile(&mut "<Not><A><KeyFile></KeyFile></A></Not>".as_bytes())?
332            .get_key_elements()?;
333
334        assert_eq!(ke.len(), 1);
335
336        assert!(DatabaseKey {
337            password: None,
338            keyfile: None,
339            #[cfg(feature = "challenge_response")]
340            challenge_response_key: None,
341            #[cfg(feature = "challenge_response")]
342            challenge_response_result: None,
343        }
344        .get_key_elements()
345        .is_err());
346
347        Ok(())
348    }
349}