Skip to content
Merged
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
2 changes: 1 addition & 1 deletion xyz-iinuwa-credential-manager-portal-gtk/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion xyz-iinuwa-credential-manager-portal-gtk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ serde_json = "1.0.140"
tracing = "0.1.41"
tracing-subscriber = "0.3"
zbus = "5.5.0"
libwebauthn = { git = "https://github.com/linux-credentials/libwebauthn", rev = "dc23daed528f512f2bcb61fce9eb6b8ee74066e2" }
libwebauthn = { git = "https://github.com/linux-credentials/libwebauthn", rev = "73970627b81ff775fbd80fb70694bef049ac2c85" }
async-trait = "0.1.88"
tokio = { version = "1", features = ["rt-multi-thread"] }

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
use std::sync::{Arc, Mutex, OnceLock};
use std::{
sync::{Arc, Mutex, OnceLock},
time::Duration,
};

use libwebauthn::{
self,
ops::webauthn::{GetAssertionResponse, MakeCredentialResponse},
transport::Device as _,
transport::{hid::HidDevice, Device as _},
webauthn::{Error as WebAuthnError, WebAuthn},
UxUpdate,
};
Expand Down Expand Up @@ -62,32 +65,67 @@ impl CredentialService {

pub(crate) async fn poll_device_discovery_usb(&mut self) -> Result<UsbState, String> {
debug!("polling for USB status");
let prev_usb_state = *self.usb_state.lock().await;
let prev_usb_state = self.usb_state.lock().await.clone();
let next_usb_state = match prev_usb_state {
UsbState::Idle | UsbState::Waiting => {
let devices = libwebauthn::transport::hid::list_devices().await.unwrap();
if devices.is_empty() {
let mut hid_devices = libwebauthn::transport::hid::list_devices().await.unwrap();
if hid_devices.is_empty() {
let state = UsbState::Waiting;
*self.usb_state.lock().await = state;
*self.usb_state.lock().await = state.clone();
return Ok(state);
}
if devices.is_empty() {
Ok(UsbState::Waiting)
} else if hid_devices.len() == 1 {
Ok(UsbState::Connected(hid_devices.swap_remove(0)))
} else {
Ok(UsbState::Connected)
Ok(UsbState::SelectingDevice(hid_devices))
}
}
UsbState::Connected => {
// TODO: I'm not sure how we want to handle multiple usb devices
// just take the first one found for now.
// TODO: store this device reference, perhaps in the enum itself
UsbState::SelectingDevice(hid_devices) => {
let (blinking_tx, mut blinking_rx) =
tokio::sync::mpsc::channel::<Option<HidDevice>>(hid_devices.len());
let mut expected_answers = hid_devices.len();
for mut device in hid_devices {
let tx = blinking_tx.clone();
tokio().spawn(async move {
let (mut channel, _state_rx) = device.channel().await.unwrap();
let res = channel
.blink_and_wait_for_user_presence(Duration::from_secs(300))
.await;
drop(channel);
match res {
Ok(true) => {
let _ = tx.send(Some(device)).await;
}
Ok(false) | Err(_) => {
let _ = tx.send(None).await;
}
}
});
}
let mut state = UsbState::Idle;
while let Some(msg) = blinking_rx.recv().await {
expected_answers -= 1;
match msg {
Some(device) => {
state = UsbState::Connected(device);
break;
}
None => {
if expected_answers == 0 {
break;
} else {
continue;
}
}
}
}
Ok(state)
}
UsbState::Connected(mut device) => {
let handler = self.usb_uv_handler.clone();
let cred_request = self.cred_request.clone();
let signal_tx = self.usb_uv_handler.signal_tx.clone();
let pin_rx = self.usb_uv_handler.pin_rx.clone();
tokio().spawn(async move {
let mut devices = libwebauthn::transport::hid::list_devices().await.unwrap();
let device = devices.first_mut().unwrap();
let (mut channel, state_rx) = device.channel().await.unwrap();
tokio().spawn(async move {
handle_usb_updates(signal_tx, pin_rx, state_rx).await;
Expand Down Expand Up @@ -252,7 +290,7 @@ impl CredentialService {
UsbState::Completed => Ok(prev_usb_state),
}?;

*self.usb_state.lock().await = next_usb_state;
*self.usb_state.lock().await = next_usb_state.clone();
Ok(next_usb_state)
}

Expand All @@ -263,7 +301,7 @@ impl CredentialService {
}

pub(crate) async fn validate_usb_device_pin(&mut self, pin: &str) -> Result<(), ()> {
let current_state = *self.usb_state.lock().await;
let current_state = self.usb_state.lock().await.clone();
match current_state {
UsbState::NeedsPin {
attempts_left: Some(attempts_left),
Expand All @@ -281,7 +319,7 @@ impl CredentialService {
}
}

#[derive(Copy, Clone, Debug, Default, PartialEq)]
#[derive(Clone, Debug, Default)]
pub enum UsbState {
/// Not polling for FIDO USB device.
#[default]
Expand All @@ -291,13 +329,17 @@ pub enum UsbState {
Waiting,

/// USB device connected, prompt user to tap
Connected,
Connected(HidDevice),

/// The device needs the PIN to be entered.
NeedsPin { attempts_left: Option<u32> },
NeedsPin {
attempts_left: Option<u32>,
},

/// The device needs on-device user verification.
NeedsUserVerification { attempts_left: Option<u32> },
NeedsUserVerification {
attempts_left: Option<u32>,
},

/// The device needs evidence of user presence (e.g. touch) to release the credential.
NeedsUserPresence,
Expand All @@ -306,7 +348,11 @@ pub enum UsbState {
Completed,
// TODO: implement cancellation
// This isn't actually sent from the server.
// UserCancelled,
//UserCancelled,

// When we encounter multiple devices, we let all of them blink and continue
// with the one that was tapped.
SelectingDevice(Vec<HidDevice>),
}

#[derive(Clone, Debug)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,9 @@ impl ViewModel {
ViewUpdate::SetCredentials(credentials) => {
view_model.update_credentials(&credentials)
}
ViewUpdate::SelectDevice(device) => {
view_model.select_device(&device)
ViewUpdate::SelectingDevice => view_model.selecting_device(),
ViewUpdate::WaitingForDevice(device) => {
view_model.waiting_for_device(&device)
}
ViewUpdate::SelectCredential(cred_id) => {
view_model.select_credential(cred_id)
Expand Down Expand Up @@ -243,7 +244,7 @@ impl ViewModel {
self.set_credentials(credential_list);
}

fn select_device(&self, device: &Device) {
fn waiting_for_device(&self, device: &Device) {
match device.transport {
Transport::Usb => {
self.set_prompt("Insert your security key.");
Expand All @@ -257,6 +258,10 @@ impl ViewModel {
self.set_selected_credential("");
}

fn selecting_device(&self) {
self.set_prompt("Multiple devices found. Please select with which to proceed.");
}

fn select_credential(&self, cred_id: String) {
self.set_selected_credential(cred_id);
}
Expand Down
19 changes: 15 additions & 4 deletions xyz-iinuwa-credential-manager-portal-gtk/src/view_model/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ impl ViewModel {
}

self.tx_update
.send(ViewUpdate::SelectDevice(device.clone()))
.send(ViewUpdate::WaitingForDevice(device.clone()))
.await
.unwrap();
}
Expand Down Expand Up @@ -282,7 +282,13 @@ impl ViewModel {
self.credential_service.lock().await.complete_auth();
self.tx_update.send(ViewUpdate::Completed).await.unwrap();
}
_ => {}
UsbState::SelectingDevice => {
self.tx_update
.send(ViewUpdate::SelectingDevice)
.await
.unwrap();
}
UsbState::NotListening | UsbState::Waiting | UsbState::UserCancelled => {}
}
}
};
Expand All @@ -302,12 +308,13 @@ pub enum ViewUpdate {
SetTitle(String),
SetDevices(Vec<Device>),
SetCredentials(Vec<Credential>),
SelectDevice(Device),
WaitingForDevice(Device),
SelectCredential(String),
UsbNeedsPin { attempts_left: Option<u32> },
UsbNeedsUserVerification { attempts_left: Option<u32> },
UsbNeedsUserPresence,
Completed,
SelectingDevice,
}

pub enum BackgroundEvent {
Expand Down Expand Up @@ -465,14 +472,18 @@ pub enum UsbState {

// This isn't actually sent from the server.
UserCancelled,

/// Multiple devices found
SelectingDevice,
}

impl From<crate::credential_service::UsbState> for UsbState {
fn from(val: crate::credential_service::UsbState) -> Self {
match val {
crate::credential_service::UsbState::Idle => UsbState::NotListening,
crate::credential_service::UsbState::SelectingDevice(..) => UsbState::SelectingDevice,
crate::credential_service::UsbState::Waiting => UsbState::Waiting,
crate::credential_service::UsbState::Connected => UsbState::Connected,
crate::credential_service::UsbState::Connected(..) => UsbState::Connected,
crate::credential_service::UsbState::NeedsPin { attempts_left } => {
UsbState::NeedsPin { attempts_left }
}
Expand Down