1use std::collections::{HashMap, HashSet, LinkedList};
5use std::fmt::{Display, Formatter};
6
7use card_backend::SmartcardError;
8use iso7816_tlv::simple::Tlv;
9use regex::Regex;
10
11pub(crate) const AID_MGR: &[u8] = &[0xA0, 0x00, 0x00, 0x05, 0x27, 0x47, 0x11, 0x17];
12pub(crate) const AID_OTP: &[u8] = &[0xA0, 0x00, 0x00, 0x05, 0x27, 0x20, 0x01];
13
14#[derive(Debug, Clone, Copy, PartialEq)]
16pub struct Version {
17 major: u8,
18 minor: u8,
19 patch: u8,
20}
21
22impl Version {
23 pub fn major(&self) -> u8 {
24 self.major
25 }
26
27 pub fn minor(&self) -> u8 {
28 self.minor
29 }
30
31 pub fn patch(&self) -> u8 {
32 self.patch
33 }
34}
35
36impl Display for Version {
37 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
38 f.write_fmt(format_args!("{}.{}.{}", self.major, self.minor, self.patch))
39 }
40}
41
42const VERSION_STRING_PATTERN: &str = r"\b(?P<major>\d+).(?P<minor>\d).(?P<patch>\d)\b";
43
44impl TryFrom<String> for Version {
45 type Error = SmartcardError;
46
47 fn try_from(value: String) -> Result<Self, Self::Error> {
48 let re = Regex::new(VERSION_STRING_PATTERN).unwrap();
49
50 if let Some(caps) = re.captures(&value) {
51 Ok(Version {
52 major: caps[1].parse::<u8>().unwrap(),
53 minor: caps[2].parse::<u8>().unwrap(),
54 patch: caps[3].parse::<u8>().unwrap(),
55 })
56 } else {
57 Err(SmartcardError::Error(format!(
58 "Unexpected version string: '{}'",
59 value
60 )))
61 }
62 }
63}
64
65impl From<[u8; 3]> for Version {
66 fn from(value: [u8; 3]) -> Self {
67 Self {
68 major: value[0],
69 minor: value[1],
70 patch: value[2],
71 }
72 }
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
77pub enum Application {
78 Otp,
79 U2F,
80 Openpgp,
81 Piv,
82 Oath,
83 HsmAuth,
84 Fido2,
85}
86
87impl From<Application> for u16 {
88 fn from(value: Application) -> Self {
89 match value {
90 Application::Otp => 0x01,
91 Application::U2F => 0x02,
92 Application::Openpgp => 0x08,
93 Application::Piv => 0x10,
94 Application::Oath => 0x20,
95 Application::HsmAuth => 0x100,
96 Application::Fido2 => 0x200,
97 }
98 }
99}
100
101pub(crate) fn applications_to_bitmask(apps: HashSet<Application>) -> u16 {
102 let mut bitmask = 0;
103 apps.iter().for_each(|&a| bitmask |= u16::from(a));
104
105 bitmask
106}
107
108fn applications(value: Option<u16>) -> LinkedList<Application> {
109 let mut list = LinkedList::new();
110
111 if let Some(value) = value {
112 for a in [
113 Application::Otp,
114 Application::U2F,
115 Application::Openpgp,
116 Application::Piv,
117 Application::Oath,
118 Application::HsmAuth,
119 Application::Fido2,
120 ] {
121 if value & u16::from(a) != 0 {
122 list.push_back(a)
123 }
124 }
125 }
126
127 list
128}
129
130#[derive(Debug, Clone, Copy, PartialEq)]
132pub enum FormFactor {
133 Unknown,
134 UsbAKeychain,
135 UsbANano,
136 UsbCKeychain,
137 UsbCNano,
138 UsbCLightning,
139 UsbABio,
140 UsbCBio,
141}
142
143impl From<Option<u8>> for FormFactor {
144 fn from(value: Option<u8>) -> Self {
145 match value {
146 Some(0x01) => FormFactor::UsbAKeychain,
147 Some(0x02) => FormFactor::UsbANano,
148 Some(0x03) => FormFactor::UsbCKeychain,
149 Some(0x04) => FormFactor::UsbCNano,
150 Some(0x05) => FormFactor::UsbCLightning,
151 Some(0x06) => FormFactor::UsbABio,
152 Some(0x07) => FormFactor::UsbCBio,
153 _ => FormFactor::Unknown,
154 }
155 }
156}
157
158const TAG_USB_SUPPORTED: u8 = 0x01;
159const TAG_SERIAL: u8 = 0x02;
160pub(crate) const TAG_USB_ENABLED: u8 = 0x03;
161const TAG_FORM_FACTOR: u8 = 0x04;
162const TAG_VERSION: u8 = 0x05;
163const TAG_AUTO_EJECT_TIMEOUT: u8 = 0x06;
164const TAG_CHALRESP_TIMEOUT: u8 = 0x07;
165const TAG_DEVICE_FLAGS: u8 = 0x08;
166const TAG_CONFIG_LOCK: u8 = 0x0A;
168const TAG_NFC_SUPPORTED: u8 = 0x0D;
171pub(crate) const TAG_NFC_ENABLED: u8 = 0x0E;
172
173#[derive(Debug, PartialEq)]
175pub struct DeviceInfo {
176 usb_supported: LinkedList<Application>,
177 serial: Option<u32>,
178 usb_enabled: LinkedList<Application>,
179 form_factor: FormFactor,
180 version: Option<Version>,
181 auto_eject_timeout: Option<u16>,
182 chalresp_timeout: Option<u8>,
183 device_flags: Option<u8>,
184 config_lock: Option<u8>,
186 nfc_supported: LinkedList<Application>,
189 nfc_enabled: LinkedList<Application>,
190}
191
192impl DeviceInfo {
193 pub fn serial(&self) -> Option<u32> {
195 self.serial
196 }
197
198 pub fn version(&self) -> Option<Version> {
200 self.version
201 }
202
203 pub fn form_factor(&self) -> FormFactor {
205 self.form_factor
206 }
207
208 pub fn usb_supported(&self) -> HashSet<Application> {
210 self.usb_supported.iter().cloned().collect()
211 }
212
213 pub fn usb_enabled(&self) -> HashSet<Application> {
215 self.usb_enabled.iter().cloned().collect()
216 }
217
218 pub fn nfc_supported(&self) -> HashSet<Application> {
220 self.nfc_supported.iter().cloned().collect()
221 }
222
223 pub fn nfc_enabled(&self) -> HashSet<Application> {
225 self.nfc_enabled.iter().cloned().collect()
226 }
227}
228
229impl Display for DeviceInfo {
230 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
231 if let Some(serial) = self.serial {
232 f.write_fmt(format_args!("Serial {}\n", serial))?;
233 }
234 if let Some(version) = self.version {
235 f.write_fmt(format_args!("Version {}\n", version))?;
236 }
237 f.write_fmt(format_args!("Form factor: {:?}\n", self.form_factor))?;
238
239 f.write_fmt(format_args!("USB supported: {:?}\n", self.usb_supported))?;
240 f.write_fmt(format_args!("USB enabled: {:?}\n", self.usb_enabled))?;
241 f.write_fmt(format_args!("NFC supported: {:?}\n", self.nfc_supported))?;
242 f.write_fmt(format_args!("NFC enabled: {:?}\n", self.nfc_enabled))?;
243
244 if let Some(auto_eject_timeout) = self.auto_eject_timeout {
245 f.write_fmt(format_args!(
246 "Auto eject timeout: {:?}\n",
247 auto_eject_timeout
248 ))?;
249 }
250
251 if let Some(chalresp_timeout) = self.chalresp_timeout {
252 f.write_fmt(format_args!("ChalResp timeout: {:?}\n", chalresp_timeout))?;
253 }
254
255 if let Some(device_flags) = self.device_flags {
256 f.write_fmt(format_args!("Device flags: {:?}\n", device_flags))?;
257 }
258
259 if let Some(config_lock) = self.config_lock {
260 f.write_fmt(format_args!("Config lock: {:?}\n", config_lock))?;
261 }
262
263 Ok(())
268 }
269}
270
271impl TryFrom<&[u8]> for DeviceInfo {
272 type Error = SmartcardError;
273
274 fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
275 assert_eq!(value[0] as usize, value.len() - 1);
277
278 let mut map = HashMap::new();
279
280 let mut remaining: &[u8] = &value[1..];
281 while !remaining.is_empty() {
282 let (r, rest) = Tlv::parse(remaining);
283 remaining = rest;
284
285 let tag: u8 = r.as_ref().unwrap().tag().into();
286 let value = r.as_ref().unwrap().value().to_vec();
287
288 map.insert(tag, value);
289 }
290
291 fn to_applications(val: Option<&Vec<u8>>) -> LinkedList<Application> {
294 applications(val.map(|x| match x.len() {
295 1 => x[0] as u16,
296 2 => u16::from_be_bytes([x[0], x[1]]),
297 _ => unimplemented!(),
298 }))
299 }
300
301 Ok(DeviceInfo {
302 usb_supported: to_applications(map.get(&TAG_USB_SUPPORTED)),
303 serial: map
304 .get(&TAG_SERIAL)
305 .map(|x| u32::from_be_bytes(x[0..4].try_into().unwrap())),
306 usb_enabled: to_applications(map.get(&TAG_USB_ENABLED)),
307 form_factor: map.get(&TAG_FORM_FACTOR).map(|x| x[0]).into(),
308 version: map.get(&TAG_VERSION).map(|x| [x[0], x[1], x[2]].into()),
309 auto_eject_timeout: map
310 .get(&TAG_AUTO_EJECT_TIMEOUT)
311 .map(|x| u16::from_be_bytes(x[0..2].try_into().unwrap())),
312 chalresp_timeout: map.get(&TAG_CHALRESP_TIMEOUT).map(|x| x[0]),
313 device_flags: map.get(&TAG_DEVICE_FLAGS).map(|x| x[0]),
314 config_lock: map.get(&TAG_CONFIG_LOCK).map(|x| x[0]),
316 nfc_supported: to_applications(map.get(&TAG_NFC_SUPPORTED)),
319 nfc_enabled: to_applications(map.get(&TAG_NFC_ENABLED)),
320 })
321 }
322}
323
324#[cfg(test)]
325mod test {
326 use std::collections::LinkedList;
327
328 use hex_literal::hex;
329
330 use crate::yubikey::{DeviceInfo, FormFactor, Version};
331 use crate::Application;
332
333 #[test]
334 fn test_config() {
335 let config = hex!("2e0102023f0302023b020400f46eec04010105030502070602000007010f0801000d02023f0e02023b0a01000f0100");
336
337 let di: DeviceInfo = (&config[..]).try_into().unwrap();
338
339 let expected = DeviceInfo {
340 serial: Some(16019180),
341 version: Some(Version {
342 major: 5,
343 minor: 2,
344 patch: 7,
345 }),
346 form_factor: FormFactor::UsbAKeychain,
347 auto_eject_timeout: Some(0),
348 chalresp_timeout: Some(15),
349 device_flags: Some(0),
350 config_lock: Some(0),
351
352 usb_supported: LinkedList::from([
353 Application::Otp,
354 Application::U2F,
355 Application::Openpgp,
356 Application::Piv,
357 Application::Oath,
358 Application::Fido2,
359 ]),
360 usb_enabled: LinkedList::from([
361 Application::Otp,
362 Application::U2F,
363 Application::Openpgp,
364 Application::Piv,
365 Application::Oath,
366 Application::Fido2,
367 ]),
368
369 nfc_supported: LinkedList::from([
370 Application::Otp,
371 Application::U2F,
372 Application::Openpgp,
373 Application::Piv,
374 Application::Oath,
375 Application::Fido2,
376 ]),
377 nfc_enabled: LinkedList::from([
378 Application::Otp,
379 Application::U2F,
380 Application::Openpgp,
381 Application::Piv,
382 Application::Oath,
383 Application::Fido2,
384 ]),
385 };
386
387 assert_eq!(di, expected);
388 }
389}