webauthn_authenticator_rs/usb/
mod.rs

1//! [USBTransport] communicates with a FIDO token over USB HID.
2//!
3//! This module should work on most platforms with USB support, provided that
4//! the user has permissions.
5//!
6//! ## Windows support
7//!
8//! Windows' WebAuthn API (on Windows 10 build 1903 and later) blocks
9//! non-Administrator access to **all** USB HID FIDO tokens, making them
10//! invisible to normal USB HID APIs.
11//!
12//! Use [Win10][crate::win10::Win10] (available with the `win10` feature) on
13//! Windows instead.
14mod framing;
15mod responses;
16#[cfg(any(all(doc, not(doctest)), feature = "vendor-solokey"))]
17mod solokey;
18#[cfg(any(all(doc, not(doctest)), feature = "vendor-yubikey"))]
19mod yubikey;
20
21use fido_hid_rs::{
22    HidReportBytes, HidSendReportBytes, USBDevice, USBDeviceImpl, USBDeviceInfo, USBDeviceInfoImpl,
23    USBDeviceManager, USBDeviceManagerImpl, WatchEvent,
24};
25
26use crate::error::WebauthnCError;
27use crate::transport::types::{KeepAliveStatus, Response, U2FHID_CANCEL, U2FHID_CBOR, U2FHID_INIT};
28use crate::transport::*;
29use crate::ui::UiCallback;
30use crate::usb::framing::*;
31use async_trait::async_trait;
32use futures::stream::BoxStream;
33use futures::StreamExt as _;
34
35#[cfg(doc)]
36use crate::stubs::*;
37
38use openssl::rand::rand_bytes;
39use std::fmt;
40use std::time::Duration;
41use webauthn_rs_proto::AuthenticatorTransport;
42
43pub(crate) use self::responses::InitResponse;
44
45// u2f_hid.h
46const CID_BROADCAST: u32 = 0xffffffff;
47
48pub struct USBTransport {
49    manager: USBDeviceManagerImpl,
50}
51
52pub struct USBToken {
53    device: USBDeviceImpl,
54    cid: u32,
55    supports_ctap1: bool,
56    supports_ctap2: bool,
57    initialised: bool,
58}
59
60impl fmt::Debug for USBTransport {
61    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62        f.debug_struct("USBTransport").finish()
63    }
64}
65
66impl fmt::Debug for USBToken {
67    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68        f.debug_struct("USBToken")
69            .field("cid", &self.cid)
70            .field("supports_ctap1", &self.supports_ctap1)
71            .field("supports_ctap2", &self.supports_ctap2)
72            .field("initialised", &self.initialised)
73            .finish()
74    }
75}
76
77impl USBTransport {
78    pub async fn new() -> Result<Self, WebauthnCError> {
79        Ok(Self {
80            manager: USBDeviceManager::new().await?,
81        })
82    }
83}
84
85#[async_trait]
86impl<'b> Transport<'b> for USBTransport {
87    type Token = USBToken;
88
89    async fn watch(&self) -> Result<BoxStream<TokenEvent<Self::Token>>, WebauthnCError> {
90        let ret = self.manager.watch_devices().await?;
91
92        Ok(Box::pin(ret.filter_map(|event| async move {
93            trace!("USB event: {event:?}");
94            match event {
95                WatchEvent::Added(d) => {
96                    if let Ok(dev) = d.open().await {
97                        let mut token = USBToken::new(dev);
98                        if let Ok(()) = token.init().await {
99                            Some(TokenEvent::Added(token))
100                        } else {
101                            None
102                        }
103                    } else {
104                        None
105                    }
106                }
107                WatchEvent::Removed(i) => Some(TokenEvent::Removed(i)),
108                WatchEvent::EnumerationComplete => Some(TokenEvent::EnumerationComplete),
109            }
110        })))
111    }
112
113    /// Gets a list of attached USB HID FIDO tokens.
114    ///
115    /// Any un-openable devices will be silently ignored.
116    ///
117    /// ## Platform-specific issues
118    ///
119    /// ### Linux
120    ///
121    /// systemd (udev) v252 and later [automatically tag USB HID FIDO tokens][1]
122    /// and set permissions based on the `f1d0` usage page, which should work
123    /// with any FIDO-compliant token.
124    ///
125    /// Previously, most distributions used a fixed list of device IDs, which
126    /// can be a problem for new or esoteric tokens.
127    ///
128    /// [1]: https://github.com/systemd/systemd/issues/11996
129    ///
130    /// ### Windows
131    ///
132    /// On Windows 10 build 1903 or later, this will not return any devices
133    /// unless the program is run as Administrator.
134    async fn tokens(&self) -> Result<Vec<Self::Token>, WebauthnCError> {
135        Ok(futures::stream::iter(self.manager.get_devices().await?)
136            .filter_map(|d| async move {
137                if let Ok(dev) = d.open().await {
138                    Some(USBToken::new(dev))
139                } else {
140                    None
141                }
142            })
143            .collect()
144            .await)
145    }
146}
147
148impl USBToken {
149    fn new(device: USBDeviceImpl) -> Self {
150        USBToken {
151            device, // : Mutex::new(device),
152            cid: 0,
153            supports_ctap1: false,
154            supports_ctap2: false,
155            initialised: false,
156        }
157    }
158
159    /// Sends a single [U2FHIDFrame] to the device, without fragmentation.
160    async fn send_one(&mut self, frame: &U2FHIDFrame) -> Result<(), WebauthnCError> {
161        let d: HidSendReportBytes = frame.into();
162        trace!(">>> {}", hex::encode(d));
163        // let guard = self.device.lock()?;
164        self.device.write(d).await?;
165        Ok(())
166    }
167
168    /// Sends a [U2FHIDFrame] to the device, fragmenting the message to fit
169    /// within the USB HID MTU.
170    async fn send(&mut self, frame: &U2FHIDFrame) -> Result<(), WebauthnCError> {
171        for f in U2FHIDFrameIterator::new(frame)? {
172            self.send_one(&f).await?;
173        }
174        Ok(())
175    }
176
177    /// Receives a single [U2FHIDFrame] from the device, without fragmentation.
178    async fn recv_one(&mut self) -> Result<U2FHIDFrame, WebauthnCError> {
179        let ret: HidReportBytes = async {
180            // let guard = self.device.lock()?;
181            let ret = self.device.read().await?;
182            Ok::<HidReportBytes, WebauthnCError>(ret)
183        }
184        .await?;
185
186        trace!("<<< {}", hex::encode(ret));
187        U2FHIDFrame::try_from(&ret)
188    }
189
190    /// Recives a [Response] from the device, handling fragmented [U2FHIDFrame]
191    /// responses if needed.
192    async fn recv(&mut self) -> Result<Response, WebauthnCError> {
193        // Recieve first chunk
194        let mut f = self.recv_one().await?;
195        let mut s: usize = f.data.len();
196        let t = usize::from(f.len);
197
198        // Get more chunks, if needed
199        while s < t {
200            let n = self.recv_one().await?;
201            s += n.data.len();
202            f += n;
203        }
204        Response::try_from(&f)
205    }
206}
207
208#[async_trait]
209impl Token for USBToken {
210    // TODO: platform code
211    type Id = <USBDeviceInfoImpl as USBDeviceInfo>::Id;
212
213    async fn transmit_raw<U>(&mut self, cmd: &[u8], ui: &U) -> Result<Vec<u8>, WebauthnCError>
214    where
215        U: UiCallback,
216    {
217        if !self.initialised {
218            error!("attempted to transmit to uninitialised token");
219            return Err(WebauthnCError::Internal);
220        }
221
222        let cmd = U2FHIDFrame {
223            cid: self.cid,
224            cmd: U2FHID_CBOR,
225            len: cmd.len() as u16,
226            data: cmd.to_vec(),
227        };
228        self.send(&cmd).await?;
229
230        // Get a response, checking for keep-alive
231        let resp = loop {
232            let resp = self.recv().await?;
233
234            if let Response::KeepAlive(r) = resp {
235                trace!("waiting for {:?}", r);
236                match r {
237                    KeepAliveStatus::UserPresenceNeeded => ui.request_touch(),
238                    KeepAliveStatus::Processing => ui.processing(),
239                    _ => (),
240                }
241                // TODO: maybe time out at some point
242                tokio::time::sleep(Duration::from_millis(100)).await;
243            } else {
244                break resp;
245            }
246        };
247
248        // Get a response
249        match resp {
250            Response::Cbor(c) => {
251                if c.status.is_ok() {
252                    Ok(c.data)
253                } else {
254                    let e = WebauthnCError::Ctap(c.status);
255                    error!("Ctap error: {:?}", e);
256                    Err(e)
257                }
258            }
259            e => {
260                error!("Unhandled response type: {:?}", e);
261                Err(WebauthnCError::Cbor)
262            }
263        }
264    }
265
266    async fn init(&mut self) -> Result<(), WebauthnCError> {
267        if self.initialised {
268            warn!("attempted to init an already-initialised token");
269            return Ok(());
270        }
271
272        // Setup a channel to communicate with the device (CTAPHID_INIT).
273        let mut nonce: [u8; 8] = [0; 8];
274        rand_bytes(&mut nonce)?;
275
276        self.send(&U2FHIDFrame {
277            cid: CID_BROADCAST,
278            cmd: U2FHID_INIT,
279            len: nonce.len() as u16,
280            data: nonce.to_vec(),
281        })
282        .await?;
283
284        match self.recv().await? {
285            Response::Init(i) => {
286                trace!(?i);
287                assert_eq!(&nonce, &i.nonce[..]);
288                self.cid = i.cid;
289                self.supports_ctap1 = i.supports_ctap1();
290                self.supports_ctap2 = i.supports_ctap2();
291
292                if self.supports_ctap2 {
293                    self.initialised = true;
294                    Ok(())
295                } else {
296                    error!("token does not support CTAP 2");
297                    Err(WebauthnCError::NotSupported)
298                }
299            }
300            e => {
301                error!("Unhandled response type: {:?}", e);
302                Err(WebauthnCError::Internal)
303            }
304        }
305    }
306
307    async fn close(&mut self) -> Result<(), WebauthnCError> {
308        Ok(())
309    }
310
311    fn get_transport(&self) -> AuthenticatorTransport {
312        AuthenticatorTransport::Usb
313    }
314
315    async fn cancel(&mut self) -> Result<(), WebauthnCError> {
316        if !self.initialised {
317            error!("attempted to cancel uninitialised token");
318            return Err(WebauthnCError::Internal);
319        }
320
321        let cmd = U2FHIDFrame {
322            cid: self.cid,
323            cmd: U2FHID_CANCEL,
324            len: 0,
325            data: vec![],
326        };
327        self.send_one(&cmd).await
328    }
329}