Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# [unreleased]

## Improvements

- daemon: Build the list of devices offered to the user from libwebauthn's `available_transports()`, the same source discovery uses (#32)

# 0.4.0 [2026-11-05]

## Improvements
Expand Down
81 changes: 64 additions & 17 deletions credentialsd/src/credential_service/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -314,23 +314,7 @@ impl<H: HybridHandler + Send, N: NfcHandler + Send, U: UsbHandler + Send> Manage
async fn get_available_public_key_devices(&self) -> Result<Vec<Device>, ()> {
// We create the list new for each call, in case someone plugs in
// an NFC-reader in the middle of an auth-flow
let mut devices = vec![Device {
id: String::from("0"),
transport: Transport::Usb,
}];
if libwebauthn::transport::hybrid::is_available().await {
devices.push(Device {
id: String::from("1"),
transport: Transport::HybridQr,
});
}
if libwebauthn::transport::nfc::is_nfc_available() {
devices.push(Device {
id: String::from("2"),
transport: Transport::Nfc,
});
}
Ok(devices)
Ok(devices_for_transports(&available_transports().await))
}

async fn start_discovery(
Expand Down Expand Up @@ -618,6 +602,26 @@ impl From<GetAssertionResponse> for AuthenticatorResponse {
}
}

/// The devices to offer the user for the given available transports, with a
/// stable ID per transport.
///
/// libwebauthn reports Bluetooth availability as both `Ble` and `Hybrid`; only
/// hybrid (QR code) flows are offered for now.
fn devices_for_transports(available: &[libwebauthn::Transport]) -> Vec<Device> {
[
(libwebauthn::Transport::Usb, "0", Transport::Usb),
(libwebauthn::Transport::Hybrid, "1", Transport::HybridQr),
(libwebauthn::Transport::Nfc, "2", Transport::Nfc),
]
.into_iter()
.filter(|(available_transport, _, _)| available.contains(available_transport))
.map(|(_, id, transport)| Device {
id: String::from(id),
transport,
})
.collect()
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -735,6 +739,49 @@ mod tests {
CredentialRequest::CreatePublicKeyCredentialRequest(make_credentials_request)
}

#[test]
fn devices_for_transports_follow_available_transports() {
let ids_and_transports =
|available: &[libwebauthn::Transport]| -> Vec<(String, Transport)> {
devices_for_transports(available)
.into_iter()
.map(|d| (d.id, d.transport))
.collect()
};

assert_eq!(
ids_and_transports(&[libwebauthn::Transport::Usb]),
vec![(String::from("0"), Transport::Usb)]
);
assert_eq!(
ids_and_transports(&[
libwebauthn::Transport::Usb,
libwebauthn::Transport::Ble,
libwebauthn::Transport::Hybrid,
libwebauthn::Transport::Nfc,
]),
vec![
(String::from("0"), Transport::Usb),
(String::from("1"), Transport::HybridQr),
(String::from("2"), Transport::Nfc),
]
);
// IDs stay the same when a transport in between is unavailable.
assert_eq!(
ids_and_transports(&[libwebauthn::Transport::Nfc, libwebauthn::Transport::Usb]),
vec![
(String::from("0"), Transport::Usb),
(String::from("2"), Transport::Nfc),
]
);
}

#[test]
fn devices_for_transports_does_not_offer_ble_alone() {
assert!(devices_for_transports(&[libwebauthn::Transport::Ble]).is_empty());
assert!(devices_for_transports(&[]).is_empty());
}

#[tokio::test]
async fn test_init_request_returns_token_and_id() {
let service = CredentialService::new(MockHybridHandler, MockNfcHandler, MockUsbHandler);
Expand Down