webauthn_authenticator_rs/bluetooth/
mod.rs1use std::{
50 collections::{HashMap, HashSet},
51 ops::RangeInclusive,
52 pin::Pin,
53 time::{Duration, Instant},
54};
55
56#[cfg(doc)]
57use crate::stubs::*;
58
59use async_trait::async_trait;
60use btleplug::{
61 api::{
62 bleuuid::uuid_from_u16, Central, CentralEvent, Characteristic, Manager as _,
63 Peripheral as _, ScanFilter, WriteType,
64 },
65 platform::{Manager, Peripheral, PeripheralId},
66};
67use futures::{executor::block_on, stream::BoxStream, Stream, StreamExt};
68use tokio::{sync::mpsc, task::spawn};
69use tokio_stream::wrappers::ReceiverStream;
70use uuid::{uuid, Uuid};
71use webauthn_rs_proto::AuthenticatorTransport;
72
73use crate::{
74 error::WebauthnCError,
75 transport::{
76 types::{
77 CBORResponse, KeepAliveStatus, Response, U2FError, BTLE_CANCEL, BTLE_KEEPALIVE,
78 U2FHID_ERROR, U2FHID_MSG, U2FHID_PING,
79 },
80 Token, TokenEvent, Transport, TYPE_INIT,
81 },
82 ui::UiCallback,
83};
84
85use self::framing::{BtleFrame, BtleFrameIterator};
86
87mod framing;
88
89const FIDO_GATT_SERVICE: Uuid = uuid_from_u16(0xfffd);
96
97const FIDO_CONTROL_POINT: Uuid = uuid!("F1D0FFF1-DEAA-ECEE-B42F-C9BA7ED623BB");
101
102const FIDO_STATUS: Uuid = uuid!("F1D0FFF2-DEAA-ECEE-B42F-C9BA7ED623BB");
107
108const FIDO_CONTROL_POINT_LENGTH: Uuid = uuid!("F1D0FFF3-DEAA-ECEE-B42F-C9BA7ED623BB");
113
114const FIDO_SERVICE_REVISION_BITFIELD: Uuid = uuid!("F1D0FFF4-DEAA-ECEE-B42F-C9BA7ED623BB");
122
123const VALID_MTU_RANGE: RangeInclusive<usize> = 20..=512;
125
126const SERVICE_REVISION_CTAP2: u8 = 0x20;
129
130pub struct BluetoothDeviceWatcher {
131 stream: ReceiverStream<TokenEvent<BluetoothToken>>,
133}
134
135impl BluetoothDeviceWatcher {
136 async fn new(
137 transport: &BluetoothTransport,
138 debounce: Duration,
139 ) -> Result<BluetoothDeviceWatcher, WebauthnCError> {
140 let (tx, rx) = mpsc::channel(16);
141 let stream = ReceiverStream::from(rx);
142
143 let adapters = transport.manager.adapters().await?;
144 let adapter = adapters
145 .into_iter()
146 .next()
147 .ok_or(WebauthnCError::NoBluetoothAdapter)?;
148
149 let filter = ScanFilter {
150 services: vec![FIDO_GATT_SERVICE],
151 };
152 adapter.start_scan(filter).await?;
153
154 let mut events = adapter.events().await?;
155 if let Err(e) = tx.send(TokenEvent::EnumerationComplete).await {
156 error!("could not send Bluetooth EnumerationComplete: {e:?}");
157 return Err(WebauthnCError::Internal);
158 }
159
160 spawn(async move {
161 let mut recents: HashMap<PeripheralId, Instant> = HashMap::new();
166 let mut connected = HashSet::new();
167 while let Some(event) = events.next().await {
168 if tx.is_closed() {
169 break;
170 }
171 match event {
172 CentralEvent::DeviceConnected(id) => {
173 trace!("device connected: {id:?}");
174 let peripheral = match adapter.peripheral(&id).await {
175 Ok(p) => p,
176 Err(e) => {
177 error!("could not get info for BTLE peripheral {id:?}: {e:?}");
178 continue;
179 }
180 };
181
182 let properties = match peripheral.properties().await {
183 Ok(Some(p)) => p,
184 Ok(None) => {
185 error!(
186 "no properties available for BTLE peripheral {id:?}, ignoring"
187 );
188 continue;
189 }
190 Err(e) => {
191 error!(
192 "could not get properties for BTLE peripheral {id:?}: {e:?}"
193 );
194 continue;
195 }
196 };
197
198 if !properties.services.contains(&FIDO_GATT_SERVICE) {
199 trace!("BTLE peripheral {id:?} is not a FIDO token, skipping");
200 continue;
201 }
202
203 connected.insert(id);
204 if tx
205 .send(TokenEvent::Added(BluetoothToken::new(peripheral)))
206 .await
207 .is_err()
208 {
209 break;
211 }
212 }
213
214 CentralEvent::DeviceDisconnected(id) => {
215 trace!("device disconnected: {id:?}");
216 if !connected.remove(&id) {
217 continue;
219 }
220
221 if tx.send(TokenEvent::Removed(id)).await.is_err() {
222 break;
224 }
225 }
226
227 CentralEvent::DeviceDiscovered(id) => {
228 trace!("device discovered: {id:?}");
229 if let Some(last_seen) = recents.get(&id) {
230 if last_seen.elapsed() < debounce {
231 trace!("ignoring recently-seen device: {id:?}");
232 recents.insert(id, Instant::now());
233 continue;
234 }
235
236 recents.remove(&id);
237 }
238
239 let peripheral = match adapter.peripheral(&id).await {
240 Ok(p) => p,
241 Err(e) => {
242 error!("could not get info for BTLE peripheral {id:?}: {e:?}");
243 continue;
244 }
245 };
246
247 let properties = match peripheral.properties().await {
248 Ok(Some(p)) => p,
249 Ok(None) => {
250 error!(
251 "no properties available for BTLE peripheral {id:?}, ignoring"
252 );
253 continue;
254 }
255 Err(e) => {
256 error!(
257 "could not get properties for BTLE peripheral {id:?}: {e:?}"
258 );
259 continue;
260 }
261 };
262
263 trace!("services: {:?}", properties.services);
264 if !properties.services.is_empty()
266 && !properties.services.contains(&FIDO_GATT_SERVICE)
267 {
268 trace!("BTLE peripheral {id:?} is not a FIDO token, skipping");
269 continue;
270 }
271
272 trace!("device name: {:?}", properties.local_name);
273 recents.insert(id, Instant::now());
274 if let Err(e) = peripheral.connect().await {
275 error!("could not connect: {e:?}");
276 }
277 }
278
279 CentralEvent::ServicesAdvertisement { id, services } => {
280 trace!("services advertisement: {id:?} {services:?}");
283 if !services.contains(&FIDO_GATT_SERVICE) {
284 trace!("BTLE peripheral {id:?} is not a FIDO token, skipping");
285 continue;
286 }
287
288 let peripheral = match adapter.peripheral(&id).await {
289 Ok(p) => p,
290 Err(e) => {
291 error!("could not get info for BTLE peripheral {id:?}: {e:?}");
292 continue;
293 }
294 };
295
296 if peripheral.is_connected().await? {
297 trace!("ignoring connected peripheral: {id:?}");
298 continue;
299 }
300
301 if let Some(last_seen) = recents.get(&id) {
302 if last_seen.elapsed() < debounce {
303 trace!("ignoring recently-seen device: {id:?}");
304 recents.insert(id, Instant::now());
305 continue;
306 }
307
308 recents.remove(&id);
309 }
310
311 let properties = match peripheral.properties().await {
312 Ok(Some(p)) => p,
313 Ok(None) => {
314 error!(
315 "no properties available for BTLE peripheral {id:?}, ignoring"
316 );
317 continue;
318 }
319 Err(e) => {
320 error!(
321 "could not get properties for BTLE peripheral {id:?}: {e:?}"
322 );
323 continue;
324 }
325 };
326
327 trace!("device name: {:?}", properties.local_name);
328 recents.insert(id, Instant::now());
329 if let Err(e) = peripheral.connect().await {
330 error!("could not connect: {e:?}");
331 }
332 }
333
334 _ => (),
335 }
336 }
337
338 adapter.stop_scan().await
339 });
340
341 Ok(Self { stream })
342 }
343}
344
345impl Stream for BluetoothDeviceWatcher {
346 type Item = TokenEvent<BluetoothToken>;
347
348 fn poll_next(
349 self: Pin<&mut Self>,
350 cx: &mut std::task::Context<'_>,
351 ) -> std::task::Poll<Option<Self::Item>> {
352 ReceiverStream::poll_next(Pin::new(&mut Pin::get_mut(self).stream), cx)
353 }
354}
355
356#[derive(Debug)]
357pub struct BluetoothTransport {
358 manager: Manager,
359}
360
361impl BluetoothTransport {
362 pub async fn new() -> Result<Self, WebauthnCError> {
364 Ok(Self {
365 manager: Manager::new().await?,
366 })
367 }
368}
369
370#[async_trait]
371impl<'b> Transport<'b> for BluetoothTransport {
372 type Token = BluetoothToken;
373
374 async fn tokens(&self) -> Result<Vec<Self::Token>, WebauthnCError> {
386 warn!("tokens() is not supported for Bluetooth devices, use watch()");
387 Ok(vec![])
388 }
389
390 async fn watch(&self) -> Result<BoxStream<TokenEvent<Self::Token>>, WebauthnCError> {
398 trace!("Scanning for BTLE tokens");
399 let stream = BluetoothDeviceWatcher::new(self, Duration::from_secs(10)).await?;
400 Ok(Box::pin(stream))
401 }
402}
403
404#[derive(Debug)]
405pub struct BluetoothToken {
406 device: Peripheral,
407 mtu: usize,
408 control_point: Option<Characteristic>,
409}
410
411impl BluetoothToken {
412 fn new(device: Peripheral) -> Self {
413 BluetoothToken {
414 device,
415 mtu: 0,
416 control_point: None,
417 }
418 }
419
420 #[inline]
424 fn checked_mtu(&self) -> Result<usize, WebauthnCError> {
425 if !VALID_MTU_RANGE.contains(&self.mtu) {
426 Err(WebauthnCError::UnexpectedState)
427 } else {
428 Ok(self.mtu)
429 }
430 }
431
432 async fn send_one(&self, frame: BtleFrame) -> Result<(), WebauthnCError> {
434 let d = frame.as_vec(self.checked_mtu()?)?;
435 trace!(">>> {}", hex::encode(&d));
436 self.device
437 .write(
438 self.control_point
439 .as_ref()
440 .ok_or(WebauthnCError::UnexpectedState)?,
441 &d,
442 WriteType::WithResponse,
443 )
444 .await?;
445 Ok(())
446 }
447
448 async fn send(&self, frame: &BtleFrame) -> Result<(), WebauthnCError> {
451 for f in BtleFrameIterator::new(frame, self.checked_mtu()?)? {
452 self.send_one(f).await?;
453 }
454 Ok(())
455 }
456}
457
458#[async_trait]
459impl Token for BluetoothToken {
460 type Id = PeripheralId;
461
462 async fn transmit_raw<U>(&mut self, cmd: &[u8], ui: &U) -> Result<Vec<u8>, WebauthnCError>
463 where
464 U: UiCallback,
465 {
466 let mut stream = self.device.notifications().await?;
470
471 let cmd = BtleFrame {
473 cmd: U2FHID_MSG,
474 len: cmd.len() as u16,
475 data: cmd.to_vec(),
476 };
477 self.send(&cmd).await?;
478
479 let resp = loop {
481 let mut t = 0usize;
482 let mut s = 0usize;
483 let mut c = Vec::new();
484
485 while let Some(data) = stream.next().await {
486 trace!("<<< {}", hex::encode(&data.value));
487 if data.uuid != FIDO_STATUS {
488 trace!("Ignoring notification for unknown UUID: {:?}", data.uuid);
489 continue;
490 }
491
492 let frame = BtleFrame::try_from(data.value.as_slice())?;
493 if frame.cmd >= TYPE_INIT {
494 if t == 0 {
495 t = usize::from(frame.len);
497 } else {
498 error!("Unexpected initial frame");
499 return Err(WebauthnCError::Unknown);
500 }
501 } else if t == 0 {
502 error!("Unexpected continuation frame");
503 return Err(WebauthnCError::Unknown);
504 }
505
506 s += frame.data.len();
507 c.push(frame);
508
509 if s >= t {
510 break;
512 }
513 }
514
515 if s < t {
516 error!("Stream stopped before getting complete message");
517 return Err(WebauthnCError::Unknown);
518 }
519
520 let f: BtleFrame = c.iter().sum();
521 trace!("recv done: {f:?}");
522 let resp = Response::try_from(&f)?;
523 trace!("Response: {resp:?}");
524
525 if let Response::KeepAlive(r) = resp {
526 trace!("waiting for {:?}", r);
527 match r {
528 KeepAliveStatus::UserPresenceNeeded => ui.request_touch(),
529 KeepAliveStatus::Processing => ui.processing(),
530 _ => (),
531 }
532 } else {
535 break resp;
536 }
537 };
538
539 match resp {
541 Response::Cbor(c) => {
542 if c.status.is_ok() {
543 Ok(c.data)
544 } else {
545 let e = WebauthnCError::Ctap(c.status);
546 error!("Ctap error: {:?}", e);
547 Err(e)
548 }
549 }
550 e => {
551 error!("Unhandled response type: {:?}", e);
552 Err(WebauthnCError::Cbor)
553 }
554 }
555 }
556
557 async fn init(&mut self) -> Result<(), WebauthnCError> {
558 if !self.device.is_connected().await? {
559 self.device.connect().await?;
560 }
561
562 self.device.discover_services().await?;
563 let service = self
564 .device
565 .services()
566 .into_iter()
567 .find(|s| s.uuid == FIDO_GATT_SERVICE)
568 .ok_or(WebauthnCError::NotSupported)?;
569
570 if let Some(c) = service
575 .characteristics
576 .iter()
577 .find(|c| c.uuid == FIDO_SERVICE_REVISION_BITFIELD)
578 {
579 trace!("Selecting protocol version");
580 if let Some(b) = self.device.read(c).await?.first() {
581 trace!("Service revision bitfield: {b:#08b}");
582 if b & SERVICE_REVISION_CTAP2 == 0 {
583 error!("Device does not support CTAP2, not supported!");
584 return Err(WebauthnCError::NotSupported);
585 }
586
587 trace!("Requesting CTAP2");
588 self.device
589 .write(c, &[SERVICE_REVISION_CTAP2], WriteType::WithResponse)
590 .await?;
591 trace!("Done");
592 } else {
593 error!("Could not read protocol version");
594 return Err(WebauthnCError::MissingRequiredField);
595 }
596 } else {
597 error!("Device does not support CTAP2, not supported!");
598 return Err(WebauthnCError::NotSupported);
599 }
600
601 if let Some(c) = service
603 .characteristics
604 .iter()
605 .find(|c| c.uuid == FIDO_CONTROL_POINT_LENGTH)
606 {
607 let b = self.device.read(c).await?;
608 if b.len() < 2 {
609 return Err(WebauthnCError::MessageTooShort);
610 }
611 self.mtu = u16::from_be_bytes(
612 b[0..2]
613 .try_into()
614 .map_err(|_| WebauthnCError::MessageTooShort)?,
615 ) as usize;
616 trace!("Control point length: {}", self.mtu);
617 if self.mtu < 20 || self.mtu > 512 {
618 error!("Control point length must be between 20 and 512 bytes");
619 return Err(WebauthnCError::NotSupported);
620 }
621 } else {
622 error!("No control point length specified!");
623 return Err(WebauthnCError::MissingRequiredField);
624 }
625
626 if let Some(c) = service
629 .characteristics
630 .iter()
631 .find(|c| c.uuid == FIDO_STATUS)
632 {
633 self.device.subscribe(c).await?;
634 } else {
635 error!("No status attribute, cannot get responses to commands!");
636 return Err(WebauthnCError::MissingRequiredField);
637 }
638
639 if let Some(c) = service
641 .characteristics
642 .iter()
643 .find(|c| c.uuid == FIDO_CONTROL_POINT)
644 {
645 self.control_point = Some(c.to_owned());
646 } else {
647 error!("No control point attribute, cannot send commands!");
648 return Err(WebauthnCError::MissingRequiredField);
649 }
650
651 Ok(())
652 }
653
654 async fn close(&mut self) -> Result<(), WebauthnCError> {
655 if self.device.is_connected().await.unwrap_or_default() {
656 self.device.disconnect().await?;
657 }
658 Ok(())
659 }
660
661 fn get_transport(&self) -> AuthenticatorTransport {
662 AuthenticatorTransport::Ble
663 }
664
665 async fn cancel(&mut self) -> Result<(), WebauthnCError> {
666 self.send_one(BtleFrame {
667 cmd: BTLE_CANCEL,
668 len: 0,
669 data: vec![],
670 })
671 .await
672 }
673}
674
675impl Drop for BluetoothToken {
676 fn drop(&mut self) {
677 trace!("dropping");
678 block_on(self.close()).ok();
679 }
680}
681
682impl TryFrom<&BtleFrame> for Response {
686 type Error = WebauthnCError;
687
688 fn try_from(f: &BtleFrame) -> Result<Response, WebauthnCError> {
689 if !f.complete() {
690 error!("cannot parse incomplete frame");
691 return Err(WebauthnCError::UnexpectedState);
692 }
693
694 let b = &f.data[..];
695 Ok(match f.cmd {
696 U2FHID_PING => Response::Ping(b.to_vec()),
697 BTLE_KEEPALIVE => Response::KeepAlive(KeepAliveStatus::from(b)),
698 U2FHID_MSG => CBORResponse::try_from(b).map(Response::Cbor)?,
699 U2FHID_ERROR => Response::Error(U2FError::from(b)),
700 _ => {
701 error!("unknown BTLE command: 0x{:02x}", f.cmd,);
702 Response::Unknown
703 }
704 })
705 }
706}