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}