Skip to content

Commit 612be47

Browse files
msirringhausiinuwa
andauthored
Implement "Select devices" flow
* Implement "Select devices" flow * Don't cache devices, but send them around --------- Co-authored-by: Isaiah Inuwa <isaiah.inuwa@gmail.com>
1 parent e8a20e1 commit 612be47

File tree

5 files changed

+94
-32
lines changed

5 files changed

+94
-32
lines changed

xyz-iinuwa-credential-manager-portal-gtk/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

xyz-iinuwa-credential-manager-portal-gtk/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ serde_json = "1.0.140"
2020
tracing = "0.1.41"
2121
tracing-subscriber = "0.3"
2222
zbus = "5.5.0"
23-
libwebauthn = { git = "https://github.com/linux-credentials/libwebauthn", rev = "dc23daed528f512f2bcb61fce9eb6b8ee74066e2" }
23+
libwebauthn = { git = "https://github.com/linux-credentials/libwebauthn", rev = "73970627b81ff775fbd80fb70694bef049ac2c85" }
2424
async-trait = "0.1.88"
2525
tokio = { version = "1", features = ["rt-multi-thread"] }
2626

xyz-iinuwa-credential-manager-portal-gtk/src/credential_service/mod.rs

Lines changed: 69 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1-
use std::sync::{Arc, Mutex, OnceLock};
1+
use std::{
2+
sync::{Arc, Mutex, OnceLock},
3+
time::Duration,
4+
};
25

36
use libwebauthn::{
47
self,
58
ops::webauthn::{GetAssertionResponse, MakeCredentialResponse},
6-
transport::Device as _,
9+
transport::{hid::HidDevice, Device as _},
710
webauthn::{Error as WebAuthnError, WebAuthn},
811
UxUpdate,
912
};
@@ -62,32 +65,67 @@ impl CredentialService {
6265

6366
pub(crate) async fn poll_device_discovery_usb(&mut self) -> Result<UsbState, String> {
6467
debug!("polling for USB status");
65-
let prev_usb_state = *self.usb_state.lock().await;
68+
let prev_usb_state = self.usb_state.lock().await.clone();
6669
let next_usb_state = match prev_usb_state {
6770
UsbState::Idle | UsbState::Waiting => {
68-
let devices = libwebauthn::transport::hid::list_devices().await.unwrap();
69-
if devices.is_empty() {
71+
let mut hid_devices = libwebauthn::transport::hid::list_devices().await.unwrap();
72+
if hid_devices.is_empty() {
7073
let state = UsbState::Waiting;
71-
*self.usb_state.lock().await = state;
74+
*self.usb_state.lock().await = state.clone();
7275
return Ok(state);
73-
}
74-
if devices.is_empty() {
75-
Ok(UsbState::Waiting)
76+
} else if hid_devices.len() == 1 {
77+
Ok(UsbState::Connected(hid_devices.swap_remove(0)))
7678
} else {
77-
Ok(UsbState::Connected)
79+
Ok(UsbState::SelectingDevice(hid_devices))
7880
}
7981
}
80-
UsbState::Connected => {
81-
// TODO: I'm not sure how we want to handle multiple usb devices
82-
// just take the first one found for now.
83-
// TODO: store this device reference, perhaps in the enum itself
82+
UsbState::SelectingDevice(hid_devices) => {
83+
let (blinking_tx, mut blinking_rx) =
84+
tokio::sync::mpsc::channel::<Option<HidDevice>>(hid_devices.len());
85+
let mut expected_answers = hid_devices.len();
86+
for mut device in hid_devices {
87+
let tx = blinking_tx.clone();
88+
tokio().spawn(async move {
89+
let (mut channel, _state_rx) = device.channel().await.unwrap();
90+
let res = channel
91+
.blink_and_wait_for_user_presence(Duration::from_secs(300))
92+
.await;
93+
drop(channel);
94+
match res {
95+
Ok(true) => {
96+
let _ = tx.send(Some(device)).await;
97+
}
98+
Ok(false) | Err(_) => {
99+
let _ = tx.send(None).await;
100+
}
101+
}
102+
});
103+
}
104+
let mut state = UsbState::Idle;
105+
while let Some(msg) = blinking_rx.recv().await {
106+
expected_answers -= 1;
107+
match msg {
108+
Some(device) => {
109+
state = UsbState::Connected(device);
110+
break;
111+
}
112+
None => {
113+
if expected_answers == 0 {
114+
break;
115+
} else {
116+
continue;
117+
}
118+
}
119+
}
120+
}
121+
Ok(state)
122+
}
123+
UsbState::Connected(mut device) => {
84124
let handler = self.usb_uv_handler.clone();
85125
let cred_request = self.cred_request.clone();
86126
let signal_tx = self.usb_uv_handler.signal_tx.clone();
87127
let pin_rx = self.usb_uv_handler.pin_rx.clone();
88128
tokio().spawn(async move {
89-
let mut devices = libwebauthn::transport::hid::list_devices().await.unwrap();
90-
let device = devices.first_mut().unwrap();
91129
let (mut channel, state_rx) = device.channel().await.unwrap();
92130
tokio().spawn(async move {
93131
handle_usb_updates(signal_tx, pin_rx, state_rx).await;
@@ -252,7 +290,7 @@ impl CredentialService {
252290
UsbState::Completed => Ok(prev_usb_state),
253291
}?;
254292

255-
*self.usb_state.lock().await = next_usb_state;
293+
*self.usb_state.lock().await = next_usb_state.clone();
256294
Ok(next_usb_state)
257295
}
258296

@@ -263,7 +301,7 @@ impl CredentialService {
263301
}
264302

265303
pub(crate) async fn validate_usb_device_pin(&mut self, pin: &str) -> Result<(), ()> {
266-
let current_state = *self.usb_state.lock().await;
304+
let current_state = self.usb_state.lock().await.clone();
267305
match current_state {
268306
UsbState::NeedsPin {
269307
attempts_left: Some(attempts_left),
@@ -281,7 +319,7 @@ impl CredentialService {
281319
}
282320
}
283321

284-
#[derive(Copy, Clone, Debug, Default, PartialEq)]
322+
#[derive(Clone, Debug, Default)]
285323
pub enum UsbState {
286324
/// Not polling for FIDO USB device.
287325
#[default]
@@ -291,13 +329,17 @@ pub enum UsbState {
291329
Waiting,
292330

293331
/// USB device connected, prompt user to tap
294-
Connected,
332+
Connected(HidDevice),
295333

296334
/// The device needs the PIN to be entered.
297-
NeedsPin { attempts_left: Option<u32> },
335+
NeedsPin {
336+
attempts_left: Option<u32>,
337+
},
298338

299339
/// The device needs on-device user verification.
300-
NeedsUserVerification { attempts_left: Option<u32> },
340+
NeedsUserVerification {
341+
attempts_left: Option<u32>,
342+
},
301343

302344
/// The device needs evidence of user presence (e.g. touch) to release the credential.
303345
NeedsUserPresence,
@@ -306,7 +348,11 @@ pub enum UsbState {
306348
Completed,
307349
// TODO: implement cancellation
308350
// This isn't actually sent from the server.
309-
// UserCancelled,
351+
//UserCancelled,
352+
353+
// When we encounter multiple devices, we let all of them blink and continue
354+
// with the one that was tapped.
355+
SelectingDevice(Vec<HidDevice>),
310356
}
311357

312358
#[derive(Clone, Debug)]

xyz-iinuwa-credential-manager-portal-gtk/src/view_model/gtk/mod.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,9 @@ impl ViewModel {
109109
ViewUpdate::SetCredentials(credentials) => {
110110
view_model.update_credentials(&credentials)
111111
}
112-
ViewUpdate::SelectDevice(device) => {
113-
view_model.select_device(&device)
112+
ViewUpdate::SelectingDevice => view_model.selecting_device(),
113+
ViewUpdate::WaitingForDevice(device) => {
114+
view_model.waiting_for_device(&device)
114115
}
115116
ViewUpdate::SelectCredential(cred_id) => {
116117
view_model.select_credential(cred_id)
@@ -243,7 +244,7 @@ impl ViewModel {
243244
self.set_credentials(credential_list);
244245
}
245246

246-
fn select_device(&self, device: &Device) {
247+
fn waiting_for_device(&self, device: &Device) {
247248
match device.transport {
248249
Transport::Usb => {
249250
self.set_prompt("Insert your security key.");
@@ -257,6 +258,10 @@ impl ViewModel {
257258
self.set_selected_credential("");
258259
}
259260

261+
fn selecting_device(&self) {
262+
self.set_prompt("Multiple devices found. Please select with which to proceed.");
263+
}
264+
260265
fn select_credential(&self, cred_id: String) {
261266
self.set_selected_credential(cred_id);
262267
}

xyz-iinuwa-credential-manager-portal-gtk/src/view_model/mod.rs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ impl ViewModel {
208208
}
209209

210210
self.tx_update
211-
.send(ViewUpdate::SelectDevice(device.clone()))
211+
.send(ViewUpdate::WaitingForDevice(device.clone()))
212212
.await
213213
.unwrap();
214214
}
@@ -282,7 +282,13 @@ impl ViewModel {
282282
self.credential_service.lock().await.complete_auth();
283283
self.tx_update.send(ViewUpdate::Completed).await.unwrap();
284284
}
285-
_ => {}
285+
UsbState::SelectingDevice => {
286+
self.tx_update
287+
.send(ViewUpdate::SelectingDevice)
288+
.await
289+
.unwrap();
290+
}
291+
UsbState::NotListening | UsbState::Waiting | UsbState::UserCancelled => {}
286292
}
287293
}
288294
};
@@ -302,12 +308,13 @@ pub enum ViewUpdate {
302308
SetTitle(String),
303309
SetDevices(Vec<Device>),
304310
SetCredentials(Vec<Credential>),
305-
SelectDevice(Device),
311+
WaitingForDevice(Device),
306312
SelectCredential(String),
307313
UsbNeedsPin { attempts_left: Option<u32> },
308314
UsbNeedsUserVerification { attempts_left: Option<u32> },
309315
UsbNeedsUserPresence,
310316
Completed,
317+
SelectingDevice,
311318
}
312319

313320
pub enum BackgroundEvent {
@@ -465,14 +472,18 @@ pub enum UsbState {
465472

466473
// This isn't actually sent from the server.
467474
UserCancelled,
475+
476+
/// Multiple devices found
477+
SelectingDevice,
468478
}
469479

470480
impl From<crate::credential_service::UsbState> for UsbState {
471481
fn from(val: crate::credential_service::UsbState) -> Self {
472482
match val {
473483
crate::credential_service::UsbState::Idle => UsbState::NotListening,
484+
crate::credential_service::UsbState::SelectingDevice(..) => UsbState::SelectingDevice,
474485
crate::credential_service::UsbState::Waiting => UsbState::Waiting,
475-
crate::credential_service::UsbState::Connected => UsbState::Connected,
486+
crate::credential_service::UsbState::Connected(..) => UsbState::Connected,
476487
crate::credential_service::UsbState::NeedsPin { attempts_left } => {
477488
UsbState::NeedsPin { attempts_left }
478489
}

0 commit comments

Comments
 (0)