Skip to main content

keepass_ng/key/
yubikey.rs

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