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 serde::Deserialize;
5use zeroize::{Zeroize, ZeroizeOnDrop};
6
7use crate::crypt::calculate_sha256;
8
9pub type KeyElement = Vec<u8>;
10pub type KeyElements = Vec<KeyElement>;
11
12/// Errors related to the database key.
13#[derive(Debug, thiserror::Error)]
14pub enum DatabaseKeyError {
15    #[error("Incorrect key")]
16    IncorrectKey,
17    #[error(transparent)]
18    Cryptography(#[from] crate::crypt::CryptographyError),
19    #[error(transparent)]
20    Io(#[from] std::io::Error),
21    #[error(transparent)]
22    Xml(#[from] quick_xml::DeError),
23    #[error("Could not obtain a key from the keyfile")]
24    InvalidKeyFile,
25    #[cfg(feature = "challenge_response")]
26    #[error(transparent)]
27    ChallengeResponse(#[from] yubikey::ChallengeResponseKeyError),
28}
29
30#[cfg(feature = "challenge_response")]
31mod yubikey;
32
33#[cfg(feature = "challenge_response")]
34pub use yubikey::{ChallengeResponseKey, ChallengeResponseKeyError};
35
36fn parse_xml_keyfile(xml: &[u8]) -> Result<KeyElement, DatabaseKeyError> {
37    #[derive(Deserialize)]
38    struct KeyFileXml {
39        #[serde(rename = "Meta", default)]
40        meta: Option<KeyMetaXml>,
41        #[serde(rename = "Key", default)]
42        key: Option<KeyDataXml>,
43    }
44
45    #[derive(Deserialize)]
46    struct KeyMetaXml {
47        #[serde(rename = "Version", default)]
48        version: Option<String>,
49    }
50
51    #[derive(Deserialize)]
52    struct KeyDataXml {
53        #[serde(rename = "Data", default)]
54        data: Option<String>,
55    }
56
57    let parsed: KeyFileXml = quick_xml::de::from_reader(xml)?;
58    let key_version = parsed.meta.and_then(|meta| meta.version);
59    let key_value = parsed.key.and_then(|key| key.data).unwrap_or_default();
60
61    if key_value.is_empty() {
62        return Err(DatabaseKeyError::InvalidKeyFile);
63    }
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
92fn parse_keyfile(buffer: &[u8]) -> KeyElement {
93    // try to parse the buffer as XML, if successful, use that data instead of full file
94    if let Ok(v) = parse_xml_keyfile(buffer) {
95        v
96    } else if buffer.len() == 32 {
97        // legacy binary key format
98        buffer.to_vec()
99    } else {
100        calculate_sha256(&[buffer]).as_slice().to_vec()
101    }
102}
103
104/// A `KeePass` key, which might consist of a password and/or a keyfile
105#[derive(Debug, Clone, Default, PartialEq, Zeroize, ZeroizeOnDrop)]
106pub struct DatabaseKey {
107    password: Option<String>,
108    keyfile: Option<Vec<u8>>,
109    #[cfg(feature = "challenge_response")]
110    challenge_response_key: Option<ChallengeResponseKey>,
111    #[cfg(feature = "challenge_response")]
112    challenge_response_result: Option<KeyElement>,
113}
114
115impl DatabaseKey {
116    pub fn with_password(mut self, password: &str) -> Self {
117        self.password = Some(password.to_string());
118        self
119    }
120
121    #[cfg(feature = "utilities")]
122    pub fn with_password_from_prompt(mut self, prompt_message: &str) -> Result<Self, std::io::Error> {
123        self.password = Some(rpassword::prompt_password(prompt_message)?);
124        Ok(self)
125    }
126
127    #[cfg(all(feature = "challenge_response", feature = "utilities"))]
128    pub fn with_hmac_sha1_secret_from_prompt(mut self, prompt_message: &str) -> Result<Self, std::io::Error> {
129        self.challenge_response_key = Some(ChallengeResponseKey::LocalChallenge(rpassword::prompt_password(prompt_message)?));
130        Ok(self)
131    }
132
133    /// Creates a database key with a `keyfile`
134    ///
135    /// # Errors
136    ///
137    /// Fails if the `keyfile` cannot be read
138    pub fn with_keyfile(mut self, keyfile: &mut dyn Read) -> Result<Self, std::io::Error> {
139        let mut buf = Vec::new();
140        keyfile.read_to_end(&mut buf)?;
141
142        self.keyfile = Some(buf);
143
144        Ok(self)
145    }
146
147    #[cfg(feature = "challenge_response")]
148    pub fn with_challenge_response_key(mut self, challenge_response_key: ChallengeResponseKey) -> Self {
149        self.challenge_response_key = Some(challenge_response_key);
150        self
151    }
152
153    #[cfg(feature = "challenge_response")]
154    pub fn perform_challenge(mut self, kdf_seed: &[u8]) -> Result<Self, DatabaseKeyError> {
155        if let Some(challenge_response_key) = &self.challenge_response_key {
156            let response = challenge_response_key.perform_challenge(kdf_seed)?;
157            self.challenge_response_result = Some(response);
158        }
159
160        Ok(self)
161    }
162
163    pub fn new() -> Self {
164        DatabaseKey::default()
165    }
166
167    pub(crate) fn get_key_elements(&self) -> Result<KeyElements, DatabaseKeyError> {
168        let mut out = Vec::new();
169
170        if let Some(p) = &self.password {
171            out.push(calculate_sha256(&[p.as_bytes()]).to_vec());
172        }
173
174        if let Some(ref f) = self.keyfile {
175            out.push(parse_keyfile(f));
176        }
177
178        if out.is_empty() {
179            return Err(DatabaseKeyError::IncorrectKey);
180        }
181
182        #[cfg(feature = "challenge_response")]
183        if let Some(result) = &self.challenge_response_result {
184            out.push(calculate_sha256(&[result]).as_slice().to_vec());
185        } else if self.challenge_response_key.is_some() {
186            return Err(DatabaseKeyError::ChallengeResponse(ChallengeResponseKeyError::NotPerformed));
187        }
188
189        Ok(out)
190    }
191
192    /// Returns true if the database key is not associated with any key component.
193    pub fn is_empty(&self) -> bool {
194        if self.password.is_some() || self.keyfile.is_some() {
195            return false;
196        }
197        #[cfg(feature = "challenge_response")]
198        if self.challenge_response_key.is_some() {
199            return false;
200        }
201        true
202    }
203}
204
205#[cfg(test)]
206mod key_tests {
207
208    use crate::error::DatabaseKeyError;
209
210    use super::DatabaseKey;
211
212    #[test]
213    fn test_key() -> Result<(), DatabaseKeyError> {
214        let ke = DatabaseKey::new().with_password("asdf").get_key_elements()?;
215        assert_eq!(ke.len(), 1);
216
217        let ke = DatabaseKey::new()
218            .with_keyfile(&mut "bare-key-file".as_bytes())?
219            .get_key_elements()?;
220        assert_eq!(ke.len(), 1);
221
222        let ke = DatabaseKey::new()
223            .with_keyfile(&mut "0123456789ABCDEF0123456789ABCDEF".as_bytes())?
224            .get_key_elements()?;
225        assert_eq!(ke.len(), 1);
226
227        let ke = DatabaseKey::new()
228            .with_password("asdf")
229            .with_keyfile(&mut "bare-key-file".as_bytes())?
230            .get_key_elements()?;
231        assert_eq!(ke.len(), 2);
232
233        let ke = DatabaseKey::new()
234            .with_keyfile(&mut "<KeyFile><Key><Data>0!23456789ABCDEF0123456789ABCDEF</Data></Key></KeyFile>".as_bytes())?
235            .get_key_elements()?;
236        assert_eq!(ke.len(), 1);
237
238        let ke = DatabaseKey::new()
239            .with_keyfile(&mut "<KeyFile><Key><Data>NXyYiJMHg3ls+eBmjbAjWec9lcOToJiofbhNiFMTJMw=</Data></Key></KeyFile>".as_bytes())?
240            .get_key_elements()?;
241        assert_eq!(ke.len(), 1);
242
243        let xml_keyfile_v2 = r###"
244            <?xml version="1.0" encoding="utf-8"?>
245            <KeyFile>
246                <Meta>
247                    <Version>2.0</Version>
248                </Meta>
249                <Key>
250                    <Data Hash="A65F0C2D">
251                        36057B1C 35037FD9 62257893 C0A22403
252                        EE3F8FBB 504D9981 08B821CB 00D28F89
253                    </Data>
254                </Key>
255            </KeyFile>
256        "###;
257        let ke = DatabaseKey::new()
258            .with_keyfile(&mut xml_keyfile_v2.trim().as_bytes())?
259            .get_key_elements()?;
260        assert_eq!(ke.len(), 1);
261
262        // other XML files will just be hashed as a "bare" keyfile
263        let ke = DatabaseKey::new()
264            .with_keyfile(&mut "<Not><A><KeyFile></KeyFile></A></Not>".as_bytes())?
265            .get_key_elements()?;
266
267        assert_eq!(ke.len(), 1);
268
269        assert!(
270            DatabaseKey {
271                password: None,
272                keyfile: None,
273                #[cfg(feature = "challenge_response")]
274                challenge_response_key: None,
275                #[cfg(feature = "challenge_response")]
276                challenge_response_result: None,
277            }
278            .get_key_elements()
279            .is_err()
280        );
281
282        Ok(())
283    }
284}