Skip to main content

keepass_ng/key/
yubikey.rs

1use challenge_response::{
2    ChallengeResponse,
3    config::{Config, Mode, Slot},
4};
5use zeroize::{Zeroize, ZeroizeOnDrop};
6
7use crate::{error::DatabaseKeyError, key::KeyElement};
8
9fn parse_yubikey_slot(slot_number: &str) -> Result<Slot, DatabaseKeyError> {
10    if let Some(slot) = Slot::from_str(slot_number) {
11        return Ok(slot);
12    }
13    Err(DatabaseKeyError::ChallengeResponseKeyError("Invalid slot number".to_string()))
14}
15
16#[derive(Debug, Clone, PartialEq, Zeroize, ZeroizeOnDrop)]
17pub enum ChallengeResponseKey {
18    LocalChallenge(String),
19    YubikeyChallenge(Yubikey, String),
20}
21
22#[derive(Debug, Clone, PartialEq, Zeroize, ZeroizeOnDrop)]
23pub struct Yubikey {
24    pub serial_number: u32,
25    pub name: Option<String>,
26}
27
28impl ChallengeResponseKey {
29    pub(crate) fn perform_challenge(&self, challenge: &[u8]) -> Result<KeyElement, DatabaseKeyError> {
30        match self {
31            ChallengeResponseKey::LocalChallenge(secret) => {
32                let secret_bytes = hex::decode(secret).map_err(|e| DatabaseKeyError::ChallengeResponseKeyError(e.to_string()))?;
33
34                let response = crate::crypt::calculate_hmac_sha1(&[challenge], &secret_bytes)?.to_vec();
35                Ok(response)
36            }
37            ChallengeResponseKey::YubikeyChallenge(yubikey, slot_number) => {
38                let mut challenge_response_client = ChallengeResponse::new()
39                    .map_err(|e| DatabaseKeyError::ChallengeResponseKeyError(format!("Could not search for yubikey: {}", e)))?;
40                let slot = parse_yubikey_slot(slot_number)?;
41
42                let yubikey_device = match challenge_response_client.find_device_from_serial(yubikey.serial_number) {
43                    Ok(d) => d,
44                    Err(_e) => return Err(DatabaseKeyError::ChallengeResponseKeyError("Yubikey not found".to_string())),
45                };
46
47                let mut config = Config::new_from(yubikey_device);
48                config = config.set_variable_size(true);
49                config = config.set_mode(Mode::Sha1);
50                config = config.set_slot(slot);
51
52                match challenge_response_client.challenge_response_hmac(challenge, config) {
53                    Ok(hmac_result) => Ok(hmac_result.to_vec()),
54                    Err(e) => Err(DatabaseKeyError::ChallengeResponseKeyError(format!(
55                        "Could not perform challenge response: {}",
56                        e
57                    ))),
58                }
59            }
60        }
61    }
62
63    pub fn get_available_yubikeys() -> Result<Vec<Yubikey>, DatabaseKeyError> {
64        let mut challenge_response_client = ChallengeResponse::new()
65            .map_err(|e| DatabaseKeyError::ChallengeResponseKeyError(format!("Could not search for yubikey: {}", e)))?;
66        let mut response: Vec<Yubikey> = vec![];
67        let yubikeys = match challenge_response_client.find_all_devices() {
68            Ok(y) => y,
69            Err(e) => {
70                return Err(DatabaseKeyError::ChallengeResponseKeyError(format!(
71                    "Could not search for yubikeys: {}",
72                    e
73                )));
74            }
75        };
76        for yubikey in yubikeys {
77            let serial_number = match yubikey.serial {
78                Some(n) => n,
79                None => continue,
80            };
81            response.push(Yubikey {
82                serial_number,
83                name: yubikey.name,
84            });
85        }
86        Ok(response)
87    }
88
89    pub fn get_yubikey(serial_number: Option<u32>) -> Result<Yubikey, DatabaseKeyError> {
90        let all_yubikeys = ChallengeResponseKey::get_available_yubikeys()?;
91        if all_yubikeys.is_empty() {
92            return Err(DatabaseKeyError::ChallengeResponseKeyError(
93                "No yubikey connected to the system".to_string(),
94            ));
95        }
96
97        let serial_number = match serial_number {
98            Some(n) => n,
99            None => {
100                if all_yubikeys.len() != 1 {
101                    return Err(DatabaseKeyError::ChallengeResponseKeyError(
102                        "Multiple yubikeys are connected to the system. Please provide a serial number.".to_string(),
103                    ));
104                }
105                return Ok(all_yubikeys[0].clone());
106            }
107        };
108
109        for yubikey in all_yubikeys {
110            if yubikey.serial_number == serial_number {
111                return Ok(yubikey);
112            }
113        }
114        Err(DatabaseKeyError::ChallengeResponseKeyError(format!(
115            "Could not find yubikey with serial number {}",
116            serial_number
117        )))
118    }
119}