Skip to content

Commit b017fd5

Browse files
Hybrid transport: Switch to event-driven discovery (#98)
## Changes * Switches from a periodic polling approach, to an event-driven discovery. It uses btleplug's event system to wait for a BLE advertisement. This is guaranteed to occur after the QR code is generated, making this safe. * Fixes iOS discovery limitation, by removing UUID filter for BlueZ device discovery. This seems to be a limitation in BlueZ (it [wouldn't be the first](deviceplug/btleplug#289)), that I could not find a workaround for. It should have no significant downside, however, as discovered devices that don't include the relevant service data are ignored (we do not attempt connecting to them). ## Testing Tested on an iPhone, and Android 15. Example `webauthn_cable` succeeds consistently.
1 parent e36387a commit b017fd5

File tree

3 files changed

+114
-85
lines changed

3 files changed

+114
-85
lines changed

libwebauthn/src/transport/ble/btleplug/manager.rs

Lines changed: 70 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ use std::collections::HashMap;
22

33
use btleplug::api::bleuuid::uuid_from_u16;
44
use btleplug::api::{
5-
Central as _, Manager as _, Peripheral as _, PeripheralProperties, ScanFilter,
5+
Central as _, CentralEvent, Manager as _, Peripheral as _, PeripheralProperties, ScanFilter,
66
};
7-
use btleplug::platform::{Adapter, Manager, Peripheral};
8-
use tracing::{debug, info, instrument, Level};
7+
use btleplug::platform::{Adapter, Manager, Peripheral, PeripheralId};
8+
use futures::{Stream, StreamExt};
9+
use tracing::{debug, info, instrument, trace, warn, Level};
910
use uuid::Uuid;
1011

1112
use super::device::FidoEndpoints;
@@ -50,17 +51,64 @@ impl SupportedRevisions {
5051
}
5152
}
5253

54+
async fn on_peripheral_service_data(
55+
adapter: &Adapter,
56+
id: &PeripheralId,
57+
uuids: &[Uuid],
58+
service_data: HashMap<Uuid, Vec<u8>>,
59+
) -> Option<(Peripheral, Vec<u8>)> {
60+
for uuid in uuids {
61+
if let Some(service_data) = service_data.get(uuid) {
62+
trace!(?id, ?service_data, "Found service data");
63+
let Ok(peripheral) = adapter.peripheral(id).await else {
64+
warn!(?id, "Could not get peripheral");
65+
return None;
66+
};
67+
68+
debug!({ ?id, ?service_data }, "Found service data for peripheral");
69+
return Some((peripheral, service_data.to_owned()));
70+
}
71+
}
72+
73+
trace!(
74+
{ ?id, ?service_data },
75+
"Ignoring periperal as it doesn't have service data for desired UUID"
76+
);
77+
None
78+
}
79+
5380
#[instrument(level = Level::DEBUG, skip_all)]
54-
pub async fn start_discovery(uuids: &[Uuid]) -> Result<(), Error> {
81+
/// Starts a discovery for devices advertising service data on any of the provided UUIDs
82+
pub async fn start_discovery_for_service_data(
83+
uuids: &[Uuid],
84+
) -> Result<impl Stream<Item = (Peripheral, Vec<u8>)> + use<'_>, Error> {
5585
let adapter = get_adapter().await?;
56-
let scan_filter = ScanFilter {
57-
services: uuids.to_vec(),
58-
};
86+
let scan_filter = ScanFilter::default();
87+
88+
let events = adapter.events().await.or(Err(Error::Unavailable))?;
5989

6090
adapter
6191
.start_scan(scan_filter)
6292
.await
63-
.or(Err(Error::ConnectionFailed))
93+
.or(Err(Error::ConnectionFailed))?;
94+
95+
let stream = events.filter_map({
96+
move |event| {
97+
let adapter = adapter.clone();
98+
let uuids = uuids.to_vec();
99+
async move {
100+
// trace!(?event);
101+
match event {
102+
CentralEvent::ServiceDataAdvertisement { id, service_data } => {
103+
on_peripheral_service_data(&adapter, &id, &uuids, service_data).await
104+
}
105+
_ => None,
106+
}
107+
}
108+
}
109+
});
110+
111+
Ok(stream)
64112
}
65113

66114
/// TODO(#86): Support multiple adapters.
@@ -84,6 +132,7 @@ async fn discover_properties(
84132
.properties()
85133
.await
86134
.or(Err(Error::ConnectionFailed))?;
135+
trace!({ ?peripheral, ?properties });
87136
if let Some(properties) = properties {
88137
result.push((peripheral, properties));
89138
}
@@ -117,38 +166,20 @@ pub async fn list_fido_devices() -> Result<Vec<FidoDevice>, Error> {
117166
Ok(with_properties)
118167
}
119168

120-
#[instrument(level = Level::DEBUG, skip_all)]
121-
pub async fn list_devices_with_service_data(
122-
service_uuid: Uuid,
123-
) -> Result<HashMap<FidoDevice, Vec<u8>>, Error> {
124-
let adapter = get_adapter().await?;
125-
let peripherals = adapter
126-
.peripherals()
169+
pub async fn get_device(peripheral: Peripheral) -> Result<Option<FidoDevice>, Error> {
170+
let Some(properties) = peripheral
171+
.properties()
127172
.await
128-
.or(Err(Error::ConnectionFailed))?;
129-
for peripheral in &peripherals {
130-
// TODO: parallelize this
131-
peripheral
132-
.discover_services()
133-
.await
134-
.or(Err(Error::ConnectionFailed))?;
135-
}
136-
let with_properties = discover_properties(peripherals).await?;
137-
Ok(with_properties
138-
.into_iter()
139-
.filter_map(
140-
|(peripheral, properties)| match properties.service_data.get(&service_uuid) {
141-
Some(service_data) => {
142-
let device = FidoDevice {
143-
peripheral,
144-
properties: properties.to_owned(),
145-
};
146-
Some((device, service_data.to_owned()))
147-
}
148-
None => None,
149-
},
150-
)
151-
.collect())
173+
.or(Err(Error::ConnectionFailed))?
174+
else {
175+
return Ok(None);
176+
};
177+
178+
let device = FidoDevice {
179+
peripheral,
180+
properties,
181+
};
182+
Ok(Some(device))
152183
}
153184

154185
pub async fn supported_fido_revisions(

libwebauthn/src/transport/ble/btleplug/mod.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,5 @@ pub use connection::Connection;
88
pub use device::FidoDevice;
99
pub use error::Error;
1010
pub use manager::{
11-
connect, list_devices_with_service_data, list_fido_devices, start_discovery,
12-
supported_fido_revisions,
11+
connect, list_fido_devices, start_discovery_for_service_data, supported_fido_revisions,
1312
};

libwebauthn/src/transport/cable/qr_code_device.rs

Lines changed: 43 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
use std::fmt::{Debug, Display};
2-
use std::time::{Duration, SystemTime};
2+
use std::pin::pin;
3+
use std::time::SystemTime;
34

45
use async_trait::async_trait;
6+
use futures::StreamExt;
57
use p256::elliptic_curve::sec1::ToEncodedPoint;
68
use p256::{NonZeroScalar, SecretKey};
79
use rand::rngs::OsRng;
@@ -10,8 +12,7 @@ use serde::Serialize;
1012
use serde_bytes::ByteArray;
1113
use serde_indexed::SerializeIndexed;
1214
use tokio::sync::mpsc;
13-
use tokio::time::sleep;
14-
use tracing::{debug, error, trace};
15+
use tracing::{debug, error, trace, warn};
1516
use uuid::Uuid;
1617

1718
use super::known_devices::CableKnownDeviceInfoStore;
@@ -28,8 +29,6 @@ use crate::UxUpdate;
2829
const CABLE_UUID_FIDO: &str = "0000fff9-0000-1000-8000-00805f9b34fb";
2930
const CABLE_UUID_GOOGLE: &str = "0000fde2-0000-1000-8000-00805f9b34fb";
3031

31-
const ADVERTISEMENT_WAIT_LOOP_MS: u64 = 2000;
32-
3332
#[derive(Debug, Clone, Copy)]
3433
pub enum QrCodeOperationHint {
3534
GetAssertionRequest,
@@ -185,50 +184,50 @@ impl CableQrCodeDevice<'_> {
185184
}
186185

187186
async fn await_advertisement(&self) -> Result<(FidoDevice, DecryptedAdvert), Error> {
188-
btleplug::manager::start_discovery(&[
187+
let uuids = &[
189188
Uuid::parse_str(CABLE_UUID_FIDO).unwrap(),
190-
Uuid::parse_str(CABLE_UUID_GOOGLE).unwrap(),
191-
])
192-
.await
193-
.or(Err(Error::Transport(TransportError::TransportUnavailable)))?;
194-
195-
loop {
196-
let devices_service_data = btleplug::manager::list_devices_with_service_data(
197-
Uuid::parse_str(CABLE_UUID_FIDO).unwrap(),
198-
)
189+
Uuid::parse_str(CABLE_UUID_GOOGLE).unwrap(), // Deprecated, but may still be in use.
190+
];
191+
let stream = btleplug::manager::start_discovery_for_service_data(uuids)
199192
.await
200193
.or(Err(Error::Transport(TransportError::TransportUnavailable)))?;
201-
debug!({ ?devices_service_data }, "Found devices with service data");
202-
203-
let device = devices_service_data
204-
.into_iter()
205-
.map(|(device, data)| {
206-
let eid_key = derive(&self.qr_code.qr_secret, None, KeyPurpose::EIDKey);
207-
trace!(?device, ?data, ?eid_key);
208-
let decrypted = trial_decrypt_advert(&eid_key, &data);
209-
trace!(?decrypted);
210-
(device, decrypted)
211-
})
212-
.find(|(_, decrypted)| decrypted.is_some())
213-
.map(|(device, decrypted)| {
214-
let decrypted = decrypted.unwrap();
215-
let advert = DecryptedAdvert::from(decrypted.as_slice());
216-
(device, advert)
217-
});
218-
219-
if let Some((device, decrypted)) = device {
220-
debug!(
221-
?device,
222-
?decrypted,
223-
"Successfully decrypted advertisement from device"
224-
);
225-
226-
return Ok((device, decrypted));
227-
}
228194

229-
debug!("No devices found with matching advertisement, waiting for new advertisement");
230-
sleep(Duration::from_millis(ADVERTISEMENT_WAIT_LOOP_MS as u64)).await;
195+
let mut stream = pin!(stream);
196+
while let Some((peripheral, data)) = stream.as_mut().next().await {
197+
debug!({ ?peripheral, ?data }, "Found device with service data");
198+
199+
let Some(device) = btleplug::manager::get_device(peripheral.clone())
200+
.await
201+
.or(Err(Error::Transport(TransportError::TransportUnavailable)))?
202+
else {
203+
warn!(
204+
?peripheral,
205+
"Unable to fetch peripheral properties, ignoring"
206+
);
207+
continue;
208+
};
209+
210+
let eid_key: Vec<u8> = derive(&self.qr_code.qr_secret, None, KeyPurpose::EIDKey);
211+
trace!(?device, ?data, ?eid_key);
212+
213+
let Some(decrypted) = trial_decrypt_advert(&eid_key, &data) else {
214+
warn!(?device, "Trial decrypt failed, ignoring");
215+
continue;
216+
};
217+
trace!(?decrypted);
218+
219+
let advert = DecryptedAdvert::from(decrypted.as_slice());
220+
debug!(
221+
?device,
222+
?decrypted,
223+
"Successfully decrypted advertisement from device"
224+
);
225+
226+
return Ok((device, advert));
231227
}
228+
229+
warn!("BLE advertisement discovery stream terminated");
230+
Err(Error::Transport(TransportError::TransportUnavailable))
232231
}
233232
}
234233

0 commit comments

Comments
 (0)