Skip to main content

keepass_ng/key/
mod.rs

1use std::io::Read;
2
3use base64::{Engine as _, engine::general_purpose as base64_engine};
4use xml::name::OwnedName;
5use xml::reader::{EventReader, XmlEvent};
6use zeroize::{Zeroize, ZeroizeOnDrop};
7
8use crate::{crypt::calculate_sha256, error::DatabaseKeyError};
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;
18
19fn parse_xml_keyfile(xml: &[u8]) -> Result<KeyElement, DatabaseKeyError> {
20    let parser = EventReader::new(xml);
21
22    let mut tag_stack = Vec::new();
23
24    let mut key_version: Option<String> = None;
25    let mut key_value = String::new();
26
27    for ev in parser {
28        match ev? {
29            XmlEvent::StartElement {
30                name: OwnedName { ref local_name, .. },
31                ..
32            } => {
33                tag_stack.push(local_name.clone());
34            }
35            XmlEvent::EndElement { .. } => {
36                tag_stack.pop();
37            }
38            XmlEvent::Characters(s) => {
39                if tag_stack == ["KeyFile", "Meta", "Version"] {
40                    key_version = Some(s);
41                    continue;
42                }
43
44                if tag_stack == ["KeyFile", "Key", "Data"] {
45                    key_value.push_str(&s);
46                    continue;
47                }
48            }
49            _ => {}
50        }
51    }
52
53    if key_value.is_empty() {
54        return Err(DatabaseKeyError::InvalidKeyFile);
55    }
56
57    let key_bytes = key_value.as_bytes().to_vec();
58
59    if key_version == Some("2.0".to_string()) {
60        // TODO we should also validate the integrity of a v2 keyfile using the hash value
61
62        let trimmed_key = key_value
63            .trim()
64            .replace(" ", "")
65            .replace("\n", "")
66            .replace("\t", "")
67            .replace("\r", "");
68
69        return if let Ok(key) = hex::decode(&trimmed_key) {
70            Ok(key)
71        } else {
72            Ok(key_bytes)
73        };
74    }
75
76    // Check if the key is base64-encoded. If yes, return decoded bytes
77    if let Ok(key) = base64_engine::STANDARD.decode(&key_bytes) {
78        Ok(key)
79    } else {
80        Ok(key_bytes)
81    }
82}
83
84fn parse_keyfile(buffer: &[u8]) -> KeyElement {
85    // try to parse the buffer as XML, if successful, use that data instead of full file
86    if let Ok(v) = parse_xml_keyfile(buffer) {
87        v
88    } else if buffer.len() == 32 {
89        // legacy binary key format
90        buffer.to_vec()
91    } else {
92        calculate_sha256(&[buffer]).as_slice().to_vec()
93    }
94}
95
96/// A `KeePass` key, which might consist of a password and/or a keyfile
97#[derive(Debug, Clone, Default, PartialEq, Zeroize, ZeroizeOnDrop)]
98pub struct DatabaseKey {
99    password: Option<String>,
100    keyfile: Option<Vec<u8>>,
101    #[cfg(feature = "challenge_response")]
102    challenge_response_key: Option<ChallengeResponseKey>,
103    #[cfg(feature = "challenge_response")]
104    challenge_response_result: Option<KeyElement>,
105}
106
107impl DatabaseKey {
108    pub fn with_password(mut self, password: &str) -> Self {
109        self.password = Some(password.to_string());
110        self
111    }
112
113    #[cfg(feature = "utilities")]
114    pub fn with_password_from_prompt(mut self, prompt_message: &str) -> Result<Self, std::io::Error> {
115        self.password = Some(rpassword::prompt_password(prompt_message)?);
116        Ok(self)
117    }
118
119    #[cfg(all(feature = "challenge_response", feature = "utilities"))]
120    pub fn with_hmac_sha1_secret_from_prompt(mut self, prompt_message: &str) -> Result<Self, std::io::Error> {
121        self.challenge_response_key = Some(ChallengeResponseKey::LocalChallenge(rpassword::prompt_password(prompt_message)?));
122        Ok(self)
123    }
124
125    /// Creates a database key with a `keyfile`
126    ///
127    /// # Errors
128    ///
129    /// Fails if the `keyfile` cannot be read
130    pub fn with_keyfile(mut self, keyfile: &mut dyn Read) -> Result<Self, std::io::Error> {
131        let mut buf = Vec::new();
132        keyfile.read_to_end(&mut buf)?;
133
134        self.keyfile = Some(buf);
135
136        Ok(self)
137    }
138
139    #[cfg(feature = "challenge_response")]
140    pub fn with_challenge_response_key(mut self, challenge_response_key: ChallengeResponseKey) -> Self {
141        self.challenge_response_key = Some(challenge_response_key);
142        self
143    }
144
145    #[cfg(feature = "challenge_response")]
146    pub fn perform_challenge(mut self, kdf_seed: &[u8]) -> Result<Self, DatabaseKeyError> {
147        if let Some(challenge_response_key) = &self.challenge_response_key {
148            let response = challenge_response_key.perform_challenge(kdf_seed)?;
149            self.challenge_response_result = Some(response);
150        }
151
152        Ok(self)
153    }
154
155    pub fn new() -> Self {
156        DatabaseKey::default()
157    }
158
159    pub(crate) fn get_key_elements(&self) -> Result<KeyElements, DatabaseKeyError> {
160        let mut out = Vec::new();
161
162        if let Some(p) = &self.password {
163            out.push(calculate_sha256(&[p.as_bytes()]).to_vec());
164        }
165
166        if let Some(ref f) = self.keyfile {
167            out.push(parse_keyfile(f));
168        }
169
170        if out.is_empty() {
171            return Err(DatabaseKeyError::IncorrectKey);
172        }
173
174        #[cfg(feature = "challenge_response")]
175        if let Some(result) = &self.challenge_response_result {
176            out.push(calculate_sha256(&[result]).as_slice().to_vec());
177        } else if self.challenge_response_key.is_some() {
178            return Err(DatabaseKeyError::ChallengeResponseKeyError(
179                "Challenge-response was not performed".to_string(),
180            ));
181        }
182
183        Ok(out)
184    }
185
186    /// Returns true if the database key is not associated with any key component.
187    pub fn is_empty(&self) -> bool {
188        if self.password.is_some() || self.keyfile.is_some() {
189            return false;
190        }
191        #[cfg(feature = "challenge_response")]
192        if self.challenge_response_key.is_some() {
193            return false;
194        }
195        true
196    }
197}
198
199#[cfg(test)]
200mod key_tests {
201
202    use crate::error::DatabaseKeyError;
203
204    use super::DatabaseKey;
205
206    #[test]
207    fn test_key() -> Result<(), DatabaseKeyError> {
208        let ke = DatabaseKey::new().with_password("asdf").get_key_elements()?;
209        assert_eq!(ke.len(), 1);
210
211        let ke = DatabaseKey::new()
212            .with_keyfile(&mut "bare-key-file".as_bytes())?
213            .get_key_elements()?;
214        assert_eq!(ke.len(), 1);
215
216        let ke = DatabaseKey::new()
217            .with_keyfile(&mut "0123456789ABCDEF0123456789ABCDEF".as_bytes())?
218            .get_key_elements()?;
219        assert_eq!(ke.len(), 1);
220
221        let ke = DatabaseKey::new()
222            .with_password("asdf")
223            .with_keyfile(&mut "bare-key-file".as_bytes())?
224            .get_key_elements()?;
225        assert_eq!(ke.len(), 2);
226
227        let ke = DatabaseKey::new()
228            .with_keyfile(&mut "<KeyFile><Key><Data>0!23456789ABCDEF0123456789ABCDEF</Data></Key></KeyFile>".as_bytes())?
229            .get_key_elements()?;
230        assert_eq!(ke.len(), 1);
231
232        let ke = DatabaseKey::new()
233            .with_keyfile(&mut "<KeyFile><Key><Data>NXyYiJMHg3ls+eBmjbAjWec9lcOToJiofbhNiFMTJMw=</Data></Key></KeyFile>".as_bytes())?
234            .get_key_elements()?;
235        assert_eq!(ke.len(), 1);
236
237        let xml_keyfile_v2 = r###"
238            <?xml version="1.0" encoding="utf-8"?>
239            <KeyFile>
240                <Meta>
241                    <Version>2.0</Version>
242                </Meta>
243                <Key>
244                    <Data Hash="A65F0C2D">
245                        36057B1C 35037FD9 62257893 C0A22403
246                        EE3F8FBB 504D9981 08B821CB 00D28F89
247                    </Data>
248                </Key>
249            </KeyFile>
250        "###;
251        let ke = DatabaseKey::new()
252            .with_keyfile(&mut xml_keyfile_v2.trim().as_bytes())?
253            .get_key_elements()?;
254        assert_eq!(ke.len(), 1);
255
256        // other XML files will just be hashed as a "bare" keyfile
257        let ke = DatabaseKey::new()
258            .with_keyfile(&mut "<Not><A><KeyFile></KeyFile></A></Not>".as_bytes())?
259            .get_key_elements()?;
260
261        assert_eq!(ke.len(), 1);
262
263        assert!(
264            DatabaseKey {
265                password: None,
266                keyfile: None,
267                #[cfg(feature = "challenge_response")]
268                challenge_response_key: None,
269                #[cfg(feature = "challenge_response")]
270                challenge_response_result: None,
271            }
272            .get_key_elements()
273            .is_err()
274        );
275
276        Ok(())
277    }
278}