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        return v;
96    }
97
98    // legacy binary key format
99    if buffer.len() == 32 {
100        return buffer.to_vec();
101    }
102
103    // legacy hex key format
104    if buffer.len() == 64
105        && let Ok(key_bytes) = hex::decode(buffer)
106    {
107        return key_bytes;
108    }
109
110    // interpret as a "bare" keyfile and hash the entire file contents
111    calculate_sha256(&[buffer]).as_slice().to_vec()
112}
113
114/// A `KeePass` key, which might consist of a password and/or a keyfile
115#[derive(Debug, Clone, Default, PartialEq, Zeroize, ZeroizeOnDrop)]
116pub struct DatabaseKey {
117    password: Option<String>,
118    keyfile: Option<Vec<u8>>,
119    #[cfg(feature = "challenge_response")]
120    challenge_response_key: Option<ChallengeResponseKey>,
121    #[cfg(feature = "challenge_response")]
122    challenge_response_result: Option<KeyElement>,
123}
124
125impl DatabaseKey {
126    pub fn with_password(mut self, password: &str) -> Self {
127        self.password = Some(password.to_string());
128        self
129    }
130
131    #[cfg(feature = "utilities")]
132    pub fn with_password_from_prompt(mut self, prompt_message: &str) -> Result<Self, std::io::Error> {
133        self.password = Some(rpassword::prompt_password(prompt_message)?);
134        Ok(self)
135    }
136
137    #[cfg(all(feature = "challenge_response", feature = "utilities"))]
138    pub fn with_hmac_sha1_secret_from_prompt(mut self, prompt_message: &str) -> Result<Self, std::io::Error> {
139        self.challenge_response_key = Some(ChallengeResponseKey::LocalChallenge(rpassword::prompt_password(prompt_message)?));
140        Ok(self)
141    }
142
143    /// Creates a database key with a `keyfile`
144    ///
145    /// # Errors
146    ///
147    /// Fails if the `keyfile` cannot be read
148    pub fn with_keyfile(mut self, keyfile: &mut dyn Read) -> Result<Self, std::io::Error> {
149        let mut buf = Vec::new();
150        keyfile.read_to_end(&mut buf)?;
151
152        self.keyfile = Some(buf);
153
154        Ok(self)
155    }
156
157    #[cfg(feature = "challenge_response")]
158    pub fn with_challenge_response_key(mut self, challenge_response_key: ChallengeResponseKey) -> Self {
159        self.challenge_response_key = Some(challenge_response_key);
160        self
161    }
162
163    #[cfg(feature = "challenge_response")]
164    pub fn perform_challenge(mut self, kdf_seed: &[u8]) -> Result<Self, DatabaseKeyError> {
165        if let Some(challenge_response_key) = &self.challenge_response_key {
166            let response = challenge_response_key.perform_challenge(kdf_seed)?;
167            self.challenge_response_result = Some(response);
168        }
169
170        Ok(self)
171    }
172
173    pub fn new() -> Self {
174        DatabaseKey::default()
175    }
176
177    pub(crate) fn get_key_elements(&self) -> Result<KeyElements, DatabaseKeyError> {
178        let mut out = Vec::new();
179
180        if let Some(p) = &self.password {
181            out.push(calculate_sha256(&[p.as_bytes()]).to_vec());
182        }
183
184        if let Some(ref f) = self.keyfile {
185            out.push(parse_keyfile(f));
186        }
187
188        if out.is_empty() {
189            return Err(DatabaseKeyError::IncorrectKey);
190        }
191
192        #[cfg(feature = "challenge_response")]
193        if let Some(result) = &self.challenge_response_result {
194            out.push(calculate_sha256(&[result]).as_slice().to_vec());
195        } else if self.challenge_response_key.is_some() {
196            return Err(DatabaseKeyError::ChallengeResponse(ChallengeResponseKeyError::NotPerformed));
197        }
198
199        Ok(out)
200    }
201
202    /// Returns true if the database key is not associated with any key component.
203    pub fn is_empty(&self) -> bool {
204        if self.password.is_some() || self.keyfile.is_some() {
205            return false;
206        }
207        #[cfg(feature = "challenge_response")]
208        if self.challenge_response_key.is_some() {
209            return false;
210        }
211        true
212    }
213}
214
215#[cfg(test)]
216mod key_tests {
217
218    use crate::error::DatabaseKeyError;
219
220    use super::DatabaseKey;
221
222    #[test]
223    fn test_key() -> Result<(), DatabaseKeyError> {
224        let ke = DatabaseKey::new().with_password("asdf").get_key_elements()?;
225        assert_eq!(ke.len(), 1);
226
227        let ke = DatabaseKey::new()
228            .with_keyfile(&mut "bare-key-file".as_bytes())?
229            .get_key_elements()?;
230        assert_eq!(ke.len(), 1);
231
232        let ke = DatabaseKey::new()
233            .with_keyfile(&mut "0123456789ABCDEF0123456789ABCDEF".as_bytes())?
234            .get_key_elements()?;
235        assert_eq!(ke.len(), 1);
236
237        let ke = DatabaseKey::new()
238            .with_password("asdf")
239            .with_keyfile(&mut "bare-key-file".as_bytes())?
240            .get_key_elements()?;
241        assert_eq!(ke.len(), 2);
242
243        let ke = DatabaseKey::new()
244            .with_keyfile(&mut "<KeyFile><Key><Data>0!23456789ABCDEF0123456789ABCDEF</Data></Key></KeyFile>".as_bytes())?
245            .get_key_elements()?;
246        assert_eq!(ke.len(), 1);
247
248        let ke = DatabaseKey::new()
249            .with_keyfile(&mut "<KeyFile><Key><Data>NXyYiJMHg3ls+eBmjbAjWec9lcOToJiofbhNiFMTJMw=</Data></Key></KeyFile>".as_bytes())?
250            .get_key_elements()?;
251        assert_eq!(ke.len(), 1);
252
253        let xml_keyfile_v2 = r###"
254            <?xml version="1.0" encoding="utf-8"?>
255            <KeyFile>
256                <Meta>
257                    <Version>2.0</Version>
258                </Meta>
259                <Key>
260                    <Data Hash="A65F0C2D">
261                        36057B1C 35037FD9 62257893 C0A22403
262                        EE3F8FBB 504D9981 08B821CB 00D28F89
263                    </Data>
264                </Key>
265            </KeyFile>
266        "###;
267        let ke = DatabaseKey::new()
268            .with_keyfile(&mut xml_keyfile_v2.trim().as_bytes())?
269            .get_key_elements()?;
270        assert_eq!(ke.len(), 1);
271
272        // other XML files will just be hashed as a "bare" keyfile
273        let ke = DatabaseKey::new()
274            .with_keyfile(&mut "<Not><A><KeyFile></KeyFile></A></Not>".as_bytes())?
275            .get_key_elements()?;
276
277        assert_eq!(ke.len(), 1);
278
279        assert!(
280            DatabaseKey {
281                password: None,
282                keyfile: None,
283                #[cfg(feature = "challenge_response")]
284                challenge_response_key: None,
285                #[cfg(feature = "challenge_response")]
286                challenge_response_result: None,
287            }
288            .get_key_elements()
289            .is_err()
290        );
291
292        Ok(())
293    }
294}