ykoath_protocol/
lib.rs

1//! <https://developers.yubico.com/OATH/YKOATH_Protocol.html>
2
3pub mod calculate;
4pub mod calculate_all;
5mod error;
6mod escape_ascii;
7pub mod list;
8pub mod select;
9
10pub use error::Error;
11use pcsc::{Card, Context, Protocols, Scope, ShareMode, MAX_BUFFER_SIZE};
12use std::ffi::CString;
13use std::fmt;
14
15pub struct YubiKey {
16    name: CString,
17    card: Card,
18}
19
20impl fmt::Debug for YubiKey {
21    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
22        f.debug_tuple("YubiKey").field(&self.name).finish()
23    }
24}
25
26impl YubiKey {
27    #[tracing::instrument(err, ret, skip_all)]
28    pub fn connect(buf: &mut Vec<u8>) -> Result<Self, Error> {
29        let context = Context::establish(Scope::User)?;
30        Self::connect_with(&context, buf)
31    }
32
33    #[tracing::instrument(err, ret, skip_all)]
34    pub fn connect_with(context: &Context, buf: &mut Vec<u8>) -> Result<Self, Error> {
35        // https://github.com/Yubico/yubikey-manager/blob/4.0.9/ykman/pcsc/__init__.py#L46
36        const NAME: &[u8] = b"yubico yubikey";
37
38        buf.resize(context.list_readers_len()?, 0);
39        let name = context
40            .list_readers(buf)?
41            .find(|name| {
42                // https://github.com/Yubico/yubikey-manager/blob/4.0.9/ykman/pcsc/__init__.py#L165
43                name.to_bytes()
44                    .to_ascii_lowercase()
45                    .windows(NAME.len())
46                    .any(|window| window == NAME)
47            })
48            .ok_or(Error::NoDevice)?;
49        tracing::debug!(?name);
50        Ok(Self {
51            name: name.to_owned(),
52            card: context.connect(name, ShareMode::Shared, Protocols::ANY)?,
53        })
54    }
55
56    #[tracing::instrument(err, level = "debug", ret)]
57    fn transmit<'a>(&self, buf: &'a mut Vec<u8>) -> Result<&'a [u8], Error> {
58        // https://github.com/tokio-rs/tracing/issues/2796
59        #[allow(clippy::redundant_locals)]
60        let buf = buf;
61        if buf.len() >= 5 {
62            // Lc
63            buf[4] = (buf.len() - 5).try_into()?;
64        }
65        let mid = buf.len();
66        loop {
67            let len = buf.len();
68            buf.resize(len + MAX_BUFFER_SIZE, 0);
69            let (occupied, vacant) = buf.split_at_mut(len);
70            let send = if mid == len {
71                &occupied[..mid]
72            } else {
73                // SEND REMAINING INSTRUCTION
74                &[0x00, 0xa5, 0x00, 0x00]
75            };
76            tracing::debug!(?send);
77            let receive = self.card.transmit(send, vacant)?;
78            tracing::debug!(?receive);
79            let len = len + receive.len();
80            buf.truncate(len);
81            let code = u16::from_le_bytes([
82                buf.pop().ok_or(Error::InsufficientData)?,
83                buf.pop().ok_or(Error::InsufficientData)?,
84            ]);
85            match code {
86                0x9000 => {
87                    break Ok(&buf[mid..]);
88                }
89                0x6100..=0x61ff => Ok(()),
90                0x6a84 => Err(Error::NoSpace),
91                0x6984 => Err(Error::NoSuchObject),
92                0x6982 => Err(Error::AuthRequired),
93                0x6a80 => Err(Error::WrongSyntax),
94                0x6581 => Err(Error::GenericError),
95                _ => Err(Error::UnknownCode(code)),
96            }?;
97        }
98    }
99
100    fn push(buf: &mut Vec<u8>, tag: u8, data: &[u8]) -> Result<(), Error> {
101        buf.push(tag);
102        buf.push(data.len().try_into()?);
103        buf.extend_from_slice(data);
104        Ok(())
105    }
106
107    fn pop<'a>(buf: &mut &'a [u8], tags: &[u8]) -> Result<(u8, &'a [u8]), Error> {
108        let tag = *buf.first().ok_or(Error::InsufficientData)?;
109        if tags.contains(&tag) {
110            let len = *buf.get(1).ok_or(Error::InsufficientData)? as usize;
111            let data = buf.get(2..2 + len).ok_or(Error::InsufficientData)?;
112            *buf = &buf[2 + len..];
113            Ok((tag, data))
114        } else {
115            Err(Error::UnexpectedValue(tag))
116        }
117    }
118}
119
120#[derive(Clone, Copy, Debug)]
121pub enum Algorithm {
122    HmacSha1,
123    HmacSha256,
124    HmacSha512,
125}
126
127impl TryFrom<u8> for Algorithm {
128    type Error = Error;
129
130    fn try_from(value: u8) -> Result<Self, Self::Error> {
131        match value {
132            0x01 => Ok(Self::HmacSha1),
133            0x02 => Ok(Self::HmacSha256),
134            0x03 => Ok(Self::HmacSha512),
135            _ => Err(Error::UnexpectedValue(value)),
136        }
137    }
138}
139
140#[derive(Clone, Copy, Debug)]
141pub enum Type {
142    Hotp,
143    Totp,
144}
145
146impl TryFrom<u8> for Type {
147    type Error = Error;
148
149    fn try_from(value: u8) -> Result<Self, Self::Error> {
150        match value {
151            0x10 => Ok(Self::Hotp),
152            0x20 => Ok(Self::Totp),
153            _ => Err(Error::UnexpectedValue(value)),
154        }
155    }
156}