1use std::io::Read;
2
3use base64::{engine::general_purpose as base64_engine, Engine as _};
4use quick_xml::{encoding::EncodingError, events::Event, reader::Reader, XmlVersion};
5use thiserror::Error;
6use zeroize::{Zeroize, ZeroizeOnDrop};
7
8use crate::crypt::calculate_sha256;
9
10pub type KeyElement = Vec<u8>;
11pub type KeyElements = Vec<KeyElement>;
12
13#[cfg(feature = "challenge_response")]
14mod yubikey;
15
16#[cfg(feature = "challenge_response")]
17pub use yubikey::{ChallengeResponseKey, ChallengeResponseKeyError};
18
19fn parse_xml_keyfile(xml: &[u8]) -> Result<KeyElement, ParseXmlKeyFileError> {
20 let mut tag_stack = Vec::new();
21
22 let mut key_version: Option<String> = None;
23 let mut key_value: Option<String> = None;
24
25 let mut reader = Reader::from_reader(xml);
26 let mut buf = Vec::new();
27 let mut xml_version = XmlVersion::Implicit1_0;
28
29 loop {
30 match reader.read_event_into(&mut buf)? {
31 Event::Eof => break,
32
33 Event::Decl(e) => {
34 xml_version = e.xml_version()?;
35 }
36
37 Event::Start(e) => {
38 tag_stack.push(e.name().as_ref().to_string());
39 }
40
41 Event::End(_) => {
42 tag_stack.pop();
43 }
44
45 Event::Text(e) => {
46 let s = e.xml_content(xml_version).into_owned();
47
48 if tag_stack == ["KeyFile", "Meta", "Version"] {
49 key_version = Some(s);
50 continue;
51 }
52
53 if tag_stack == ["KeyFile", "Key", "Data"] {
54 key_value = Some(s);
55 continue;
56 }
57 }
58
59 _ => (),
60 }
61 }
62
63 let key_value = key_value.ok_or(ParseXmlKeyFileError::EmptyKey)?;
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
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 if let Ok(key) = base64_engine::STANDARD.decode(&key_bytes) {
86 Ok(key)
87 } else {
88 Ok(key_bytes)
89 }
90}
91
92#[derive(Debug, Error)]
94#[non_exhaustive]
95pub enum ParseXmlKeyFileError {
96 #[error("The XML keyfile is missing a key data element")]
98 EmptyKey,
99
100 #[error(transparent)]
102 Encoding(#[from] EncodingError),
103
104 #[error(transparent)]
106 Xml(#[from] quick_xml::Error),
107}
108
109fn parse_keyfile(buffer: &[u8]) -> Result<KeyElement, DatabaseKeyError> {
110 if let Ok(v) = parse_xml_keyfile(buffer) {
112 return Ok(v);
113 }
114
115 if buffer.len() == 32 {
117 return Ok(buffer.to_vec());
118 }
119
120 if buffer.len() == 64 {
122 if let Ok(key_bytes) = hex::decode(buffer) {
123 return Ok(key_bytes);
124 }
125 }
126
127 Ok(calculate_sha256(&[buffer]).as_slice().to_vec())
129}
130
131#[derive(Debug, Clone, Default, PartialEq, Zeroize, ZeroizeOnDrop)]
133pub struct DatabaseKey {
134 password: Option<String>,
135 keyfile: Option<Vec<u8>>,
136 #[cfg(feature = "challenge_response")]
137 challenge_response_key: Option<ChallengeResponseKey>,
138 #[cfg(feature = "challenge_response")]
139 challenge_response_result: Option<KeyElement>,
140}
141
142impl DatabaseKey {
143 pub fn with_password(mut self, password: &str) -> Self {
145 self.password = Some(password.to_string());
146 self
147 }
148
149 #[cfg(feature = "utilities")]
151 pub fn with_password_from_prompt(mut self, prompt_message: &str) -> Result<Self, std::io::Error> {
152 self.password = Some(rpassword::prompt_password(prompt_message)?);
153 Ok(self)
154 }
155
156 #[cfg(all(feature = "challenge_response", feature = "utilities"))]
159 pub fn with_hmac_sha1_secret_from_prompt(mut self, prompt_message: &str) -> Result<Self, std::io::Error> {
160 self.challenge_response_key = Some(ChallengeResponseKey::LocalChallenge(rpassword::prompt_password(
161 prompt_message,
162 )?));
163 Ok(self)
164 }
165
166 pub fn with_keyfile(mut self, keyfile: &mut dyn Read) -> Result<Self, std::io::Error> {
172 let mut buf = Vec::new();
173 keyfile.read_to_end(&mut buf)?;
174
175 self.keyfile = Some(buf);
176
177 Ok(self)
178 }
179
180 #[cfg(feature = "challenge_response")]
182 pub fn with_challenge_response_key(mut self, challenge_response_key: ChallengeResponseKey) -> Self {
183 self.challenge_response_key = Some(challenge_response_key);
184 self
185 }
186
187 #[cfg(feature = "challenge_response")]
190 pub fn perform_challenge(mut self, kdf_seed: &[u8]) -> Result<Self, DatabaseKeyError> {
191 if let Some(challenge_response_key) = &self.challenge_response_key {
192 let response = challenge_response_key.perform_challenge(kdf_seed)?;
193 self.challenge_response_result = Some(response);
194 }
195
196 Ok(self)
197 }
198
199 pub fn new() -> Self {
201 Default::default()
202 }
203
204 pub(crate) fn get_key_elements(&self) -> Result<KeyElements, DatabaseKeyError> {
205 let mut out = Vec::new();
206
207 if let Some(p) = &self.password {
208 out.push(calculate_sha256(&[p.as_bytes()]).to_vec());
209 }
210
211 if let Some(ref f) = self.keyfile {
212 out.push(parse_keyfile(f)?);
213 }
214
215 if out.is_empty() {
216 return Err(DatabaseKeyError::EmptyKey);
217 }
218
219 #[cfg(feature = "challenge_response")]
220 if let Some(result) = &self.challenge_response_result {
221 out.push(calculate_sha256(&[result]).as_slice().to_vec());
222 } else if self.challenge_response_key.is_some() {
223 return Err(DatabaseKeyError::ChallengeResponse(
224 crate::key::yubikey::ChallengeResponseKeyError::NotPerformed,
225 ));
226 }
227
228 Ok(out)
229 }
230
231 pub fn is_empty(&self) -> bool {
233 if self.password.is_some() || self.keyfile.is_some() {
234 return false;
235 }
236 #[cfg(feature = "challenge_response")]
237 if self.challenge_response_key.is_some() {
238 return false;
239 }
240 true
241 }
242}
243
244#[derive(Debug, Error)]
246#[non_exhaustive]
247pub enum DatabaseKeyError {
248 #[error("The key contains no components")]
250 EmptyKey,
251
252 #[error("Incorrect key")]
254 IncorrectKey,
255
256 #[error("I/O error reading keyfile: {0}")]
258 Io(#[from] std::io::Error),
259
260 #[error("XML error reading keyfile: {0}")]
262 Xml(#[from] quick_xml::Error),
263
264 #[error("Invalid keyfile format")]
266 InvalidKeyFile,
267
268 #[cfg(feature = "challenge_response")]
270 #[error("Challenge-response key error: {0}")]
271 ChallengeResponse(#[from] crate::key::yubikey::ChallengeResponseKeyError),
272}
273
274#[cfg(test)]
275mod key_tests {
276
277 use super::{DatabaseKey, DatabaseKeyError};
278
279 #[test]
280 fn test_key() -> Result<(), DatabaseKeyError> {
281 let ke = DatabaseKey::new().with_password("asdf").get_key_elements()?;
282 assert_eq!(ke.len(), 1);
283
284 let ke = DatabaseKey::new()
285 .with_keyfile(&mut "bare-key-file".as_bytes())?
286 .get_key_elements()?;
287 assert_eq!(ke.len(), 1);
288
289 let ke = DatabaseKey::new()
290 .with_keyfile(&mut "0123456789ABCDEF0123456789ABCDEF".as_bytes())?
291 .get_key_elements()?;
292 assert_eq!(ke.len(), 1);
293
294 let ke = DatabaseKey::new()
295 .with_password("asdf")
296 .with_keyfile(&mut "bare-key-file".as_bytes())?
297 .get_key_elements()?;
298 assert_eq!(ke.len(), 2);
299
300 let ke = DatabaseKey::new()
301 .with_keyfile(
302 &mut "<KeyFile><Key><Data>0!23456789ABCDEF0123456789ABCDEF</Data></Key></KeyFile>".as_bytes(),
303 )?
304 .get_key_elements()?;
305 assert_eq!(ke.len(), 1);
306
307 let ke = DatabaseKey::new()
308 .with_keyfile(
309 &mut "<KeyFile><Key><Data>NXyYiJMHg3ls+eBmjbAjWec9lcOToJiofbhNiFMTJMw=</Data></Key></KeyFile>"
310 .as_bytes(),
311 )?
312 .get_key_elements()?;
313 assert_eq!(ke.len(), 1);
314
315 let xml_keyfile_v2 = r###"
316 <?xml version="1.0" encoding="utf-8"?>
317 <KeyFile>
318 <Meta>
319 <Version>2.0</Version>
320 </Meta>
321 <Key>
322 <Data Hash="A65F0C2D">
323 36057B1C 35037FD9 62257893 C0A22403
324 EE3F8FBB 504D9981 08B821CB 00D28F89
325 </Data>
326 </Key>
327 </KeyFile>
328 "###;
329 let ke = DatabaseKey::new()
330 .with_keyfile(&mut xml_keyfile_v2.trim().as_bytes())?
331 .get_key_elements()?;
332 assert_eq!(ke.len(), 1);
333
334 let ke = DatabaseKey::new()
336 .with_keyfile(&mut "<Not><A><KeyFile></KeyFile></A></Not>".as_bytes())?
337 .get_key_elements()?;
338
339 assert_eq!(ke.len(), 1);
340
341 assert!(DatabaseKey {
342 password: None,
343 keyfile: None,
344 #[cfg(feature = "challenge_response")]
345 challenge_response_key: None,
346 #[cfg(feature = "challenge_response")]
347 challenge_response_result: None,
348 }
349 .get_key_elements()
350 .is_err());
351
352 Ok(())
353 }
354}