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
8#[cfg(feature = "challenge_response")]
9use challenge_response::{
10    ChallengeResponse,
11    config::{Config, Mode, Slot},
12};
13
14use crate::{crypt::calculate_sha256, error::DatabaseKeyError};
15
16pub type KeyElement = Vec<u8>;
17pub type KeyElements = Vec<KeyElement>;
18
19#[cfg(feature = "challenge_response")]
20fn parse_yubikey_slot(slot_number: &str) -> Result<Slot, DatabaseKeyError> {
21    if let Some(slot) = Slot::from_str(slot_number) {
22        return Ok(slot);
23    }
24    Err(DatabaseKeyError::ChallengeResponseKeyError("Invalid slot number".to_string()))
25}
26
27fn parse_xml_keyfile(xml: &[u8]) -> Result<KeyElement, DatabaseKeyError> {
28    let parser = EventReader::new(xml);
29
30    let mut tag_stack = Vec::new();
31
32    let mut key_version: Option<String> = None;
33    let mut key_value: Option<String> = None;
34
35    for ev in parser {
36        match ev? {
37            XmlEvent::StartElement {
38                name: OwnedName { ref local_name, .. },
39                ..
40            } => {
41                tag_stack.push(local_name.clone());
42            }
43            XmlEvent::EndElement { .. } => {
44                tag_stack.pop();
45            }
46            XmlEvent::Characters(s) => {
47                if tag_stack == ["KeyFile", "Meta", "Version"] {
48                    key_version = Some(s);
49                    continue;
50                }
51
52                if tag_stack == ["KeyFile", "Key", "Data"] {
53                    key_value = Some(s);
54                    continue;
55                }
56            }
57            _ => {}
58        }
59    }
60
61    let key_value = match key_value {
62        Some(k) => k,
63        None => return Err(DatabaseKeyError::InvalidKeyFile),
64    };
65    let key_bytes = key_value.as_bytes().to_vec();
66
67    if key_version == Some("2.0".to_string()) {
68        let trimmed_key = key_value.trim().replace(" ", "").replace("\n", "").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    if let Ok(key) = base64_engine::STANDARD.decode(&key_bytes) {
81        Ok(key)
82    } else {
83        Ok(key_bytes)
84    }
85}
86
87fn parse_keyfile(buffer: &[u8]) -> KeyElement {
88    if let Ok(v) = parse_xml_keyfile(buffer) {
90        v
91    } else if buffer.len() == 32 {
92        buffer.to_vec()
94    } else {
95        calculate_sha256(&[buffer]).as_slice().to_vec()
96    }
97}
98
99#[cfg(feature = "challenge_response")]
100#[derive(Debug, Clone, PartialEq, Zeroize, ZeroizeOnDrop)]
101pub enum ChallengeResponseKey {
102    LocalChallenge(String),
103    YubikeyChallenge(Yubikey, String),
104}
105
106#[derive(Debug, Clone, PartialEq, Zeroize, ZeroizeOnDrop)]
107pub struct Yubikey {
108    pub serial_number: u32,
109    pub name: Option<String>,
110}
111
112#[cfg(feature = "challenge_response")]
113impl ChallengeResponseKey {
114    fn perform_challenge(&self, challenge: &[u8]) -> Result<KeyElement, DatabaseKeyError> {
115        match self {
116            ChallengeResponseKey::LocalChallenge(secret) => {
117                let secret_bytes = hex::decode(secret).map_err(|e| DatabaseKeyError::ChallengeResponseKeyError(e.to_string()))?;
118
119                let response = crate::crypt::calculate_hmac_sha1(&[challenge], &secret_bytes)?.to_vec();
120                Ok(response)
121            }
122            ChallengeResponseKey::YubikeyChallenge(yubikey, slot_number) => {
123                let mut challenge_response_client = ChallengeResponse::new()
124                    .map_err(|e| DatabaseKeyError::ChallengeResponseKeyError(format!("Could not search for yubikey: {}", e)))?;
125                let slot = parse_yubikey_slot(slot_number)?;
126
127                let yubikey_device = match challenge_response_client.find_device_from_serial(yubikey.serial_number) {
128                    Ok(d) => d,
129                    Err(_e) => return Err(DatabaseKeyError::ChallengeResponseKeyError("Yubikey not found".to_string())),
130                };
131
132                let mut config = Config::new_from(yubikey_device);
133                config = config.set_variable_size(true);
134                config = config.set_mode(Mode::Sha1);
135                config = config.set_slot(slot);
136
137                match challenge_response_client.challenge_response_hmac(challenge, config) {
138                    Ok(hmac_result) => Ok(hmac_result.to_vec()),
139                    Err(e) => Err(DatabaseKeyError::ChallengeResponseKeyError(format!(
140                        "Could not perform challenge response: {e}",
141                    ))),
142                }
143            }
144        }
145    }
146
147    pub fn get_available_yubikeys() -> Result<Vec<Yubikey>, DatabaseKeyError> {
148        let mut challenge_response_client = ChallengeResponse::new()
149            .map_err(|e| DatabaseKeyError::ChallengeResponseKeyError(format!("Could not search for yubikey: {e}")))?;
150        let mut response: Vec<Yubikey> = vec![];
151        let yubikeys = match challenge_response_client.find_all_devices() {
152            Ok(y) => y,
153            Err(e) => {
154                return Err(DatabaseKeyError::ChallengeResponseKeyError(format!(
155                    "Could not search for yubikeys: {e}",
156                )));
157            }
158        };
159        for yubikey in yubikeys {
160            let serial_number = match yubikey.serial {
161                Some(n) => n,
162                None => continue,
163            };
164            response.push(Yubikey {
165                serial_number,
166                name: yubikey.name,
167            });
168        }
169        Ok(response)
170    }
171
172    pub fn get_yubikey(serial_number: Option<u32>) -> Result<Yubikey, DatabaseKeyError> {
173        let all_yubikeys = ChallengeResponseKey::get_available_yubikeys()?;
174        if all_yubikeys.is_empty() {
175            return Err(DatabaseKeyError::ChallengeResponseKeyError(
176                "No yubikey connected to the system".to_string(),
177            ));
178        }
179
180        let serial_number = match serial_number {
181            Some(n) => n,
182            None => {
183                if all_yubikeys.len() != 1 {
184                    return Err(DatabaseKeyError::ChallengeResponseKeyError(
185                        "Multiple yubikeys are connected to the system. Please provide a serial number.".to_string(),
186                    ));
187                }
188                return Ok(all_yubikeys[0].clone());
189            }
190        };
191
192        for yubikey in all_yubikeys {
193            if yubikey.serial_number == serial_number {
194                return Ok(yubikey);
195            }
196        }
197        Err(DatabaseKeyError::ChallengeResponseKeyError(format!(
198            "Could not find yubikey with serial number {}",
199            serial_number
200        )))
201    }
202}
203
204#[derive(Debug, Clone, Default, PartialEq, Zeroize, ZeroizeOnDrop)]
206pub struct DatabaseKey {
207    password: Option<String>,
208    keyfile: Option<Vec<u8>>,
209    #[cfg(feature = "challenge_response")]
210    challenge_response_key: Option<ChallengeResponseKey>,
211    #[cfg(feature = "challenge_response")]
212    challenge_response_result: Option<KeyElement>,
213}
214
215impl DatabaseKey {
216    pub fn with_password(mut self, password: &str) -> Self {
217        self.password = Some(password.to_string());
218        self
219    }
220
221    #[cfg(feature = "utilities")]
222    pub fn with_password_from_prompt(mut self, prompt_message: &str) -> Result<Self, std::io::Error> {
223        self.password = Some(rpassword::prompt_password(prompt_message)?);
224        Ok(self)
225    }
226
227    #[cfg(all(feature = "challenge_response", feature = "utilities"))]
228    pub fn with_hmac_sha1_secret_from_prompt(mut self, prompt_message: &str) -> Result<Self, std::io::Error> {
229        self.challenge_response_key = Some(ChallengeResponseKey::LocalChallenge(rpassword::prompt_password(prompt_message)?));
230        Ok(self)
231    }
232
233    pub fn with_keyfile(mut self, keyfile: &mut dyn Read) -> Result<Self, std::io::Error> {
234        let mut buf = Vec::new();
235        keyfile.read_to_end(&mut buf)?;
236
237        self.keyfile = Some(buf);
238
239        Ok(self)
240    }
241
242    #[cfg(feature = "challenge_response")]
243    pub fn with_challenge_response_key(mut self, challenge_response_key: ChallengeResponseKey) -> Self {
244        self.challenge_response_key = Some(challenge_response_key);
245        self
246    }
247
248    #[cfg(feature = "challenge_response")]
249    pub fn perform_challenge(mut self, kdf_seed: &[u8]) -> Result<Self, DatabaseKeyError> {
250        if let Some(challenge_response_key) = &self.challenge_response_key {
251            let response = challenge_response_key.perform_challenge(kdf_seed)?;
252            self.challenge_response_result = Some(response);
253        }
254
255        Ok(self)
256    }
257
258    pub fn new() -> Self {
259        DatabaseKey::default()
260    }
261
262    pub(crate) fn get_key_elements(&self) -> Result<KeyElements, DatabaseKeyError> {
263        let mut out = Vec::new();
264
265        if let Some(p) = &self.password {
266            out.push(calculate_sha256(&[p.as_bytes()]).to_vec());
267        }
268
269        if let Some(ref f) = self.keyfile {
270            out.push(parse_keyfile(f));
271        }
272
273        if out.is_empty() {
274            return Err(DatabaseKeyError::IncorrectKey);
275        }
276
277        #[cfg(feature = "challenge_response")]
278        if let Some(result) = &self.challenge_response_result {
279            out.push(calculate_sha256(&[result]).as_slice().to_vec());
280        } else if self.challenge_response_key.is_some() {
281            return Err(DatabaseKeyError::ChallengeResponseKeyError(
282                "Challenge-response was not performed".to_string(),
283            ));
284        }
285
286        Ok(out)
287    }
288
289    pub fn is_empty(&self) -> bool {
291        if self.password.is_some() || self.keyfile.is_some() {
292            return false;
293        }
294        #[cfg(feature = "challenge_response")]
295        if self.challenge_response_key.is_some() {
296            return false;
297        }
298        true
299    }
300}
301
302#[cfg(test)]
303mod key_tests {
304
305    use crate::error::DatabaseKeyError;
306
307    use super::DatabaseKey;
308
309    #[test]
310    fn test_key() -> Result<(), DatabaseKeyError> {
311        let ke = DatabaseKey::new().with_password("asdf").get_key_elements()?;
312        assert_eq!(ke.len(), 1);
313
314        let ke = DatabaseKey::new()
315            .with_keyfile(&mut "bare-key-file".as_bytes())?
316            .get_key_elements()?;
317        assert_eq!(ke.len(), 1);
318
319        let ke = DatabaseKey::new()
320            .with_keyfile(&mut "0123456789ABCDEF0123456789ABCDEF".as_bytes())?
321            .get_key_elements()?;
322        assert_eq!(ke.len(), 1);
323
324        let ke = DatabaseKey::new()
325            .with_password("asdf")
326            .with_keyfile(&mut "bare-key-file".as_bytes())?
327            .get_key_elements()?;
328        assert_eq!(ke.len(), 2);
329
330        let ke = DatabaseKey::new()
331            .with_keyfile(&mut "<KeyFile><Key><Data>0!23456789ABCDEF0123456789ABCDEF</Data></Key></KeyFile>".as_bytes())?
332            .get_key_elements()?;
333        assert_eq!(ke.len(), 1);
334
335        let ke = DatabaseKey::new()
336            .with_keyfile(&mut "<KeyFile><Key><Data>NXyYiJMHg3ls+eBmjbAjWec9lcOToJiofbhNiFMTJMw=</Data></Key></KeyFile>".as_bytes())?
337            .get_key_elements()?;
338        assert_eq!(ke.len(), 1);
339
340        let xml_keyfile_v2 = r###"
341            <?xml version="1.0" encoding="utf-8"?>
342            <KeyFile>
343                <Meta>
344                    <Version>2.0</Version>
345                </Meta>
346                <Key>
347                    <Data Hash="A65F0C2D">
348                        36057B1C 35037FD9 62257893 C0A22403
349                        EE3F8FBB 504D9981 08B821CB 00D28F89
350                    </Data>
351                </Key>
352            </KeyFile>
353        "###;
354        let ke = DatabaseKey::new()
355            .with_keyfile(&mut xml_keyfile_v2.trim().as_bytes())?
356            .get_key_elements()?;
357        assert_eq!(ke.len(), 1);
358
359        let ke = DatabaseKey::new()
361            .with_keyfile(&mut "<Not><A><KeyFile></KeyFile></A></Not>".as_bytes())?
362            .get_key_elements()?;
363
364        assert_eq!(ke.len(), 1);
365
366        assert!(
367            DatabaseKey {
368                password: None,
369                keyfile: None,
370                #[cfg(feature = "challenge_response")]
371                challenge_response_key: None,
372                #[cfg(feature = "challenge_response")]
373                challenge_response_result: None,
374            }
375            .get_key_elements()
376            .is_err()
377        );
378
379        Ok(())
380    }
381}