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