From df73943f4649011473be0f97edb43c584b4ae075 Mon Sep 17 00:00:00 2001 From: Vaishakh S Nair <36193436+vaishakhsnair@users.noreply.github.com> Date: Tue, 24 Mar 2026 19:09:57 +0530 Subject: [PATCH] added fingerprint supported version --- src/device/fido/hid.rs | 150 +++++++++++++++++++++++++++--- src/ui/components/button.rs | 13 ++- src/ui/views/security.rs | 179 +++++++++++++++++++----------------- 3 files changed, 243 insertions(+), 99 deletions(-) diff --git a/src/device/fido/hid.rs b/src/device/fido/hid.rs index c34513c..dcaf4b7 100644 --- a/src/device/fido/hid.rs +++ b/src/device/fido/hid.rs @@ -177,6 +177,8 @@ impl HidTransport { } fn write_cbor_request(&self, cmd: u8, payload: &[u8]) -> Result<(), PFError> { + self.drain_pending_packets(); + log::debug!( "Sending CBOR Command: 0x{:02X}, Payload Size: {} bytes", cmd, @@ -242,6 +244,30 @@ impl HidTransport { Ok(()) } + fn drain_pending_packets(&self) { + let mut drain_buf = [0u8; HID_REPORT_SIZE]; + let mut drained = 0usize; + + loop { + match self.device.read_timeout(&mut drain_buf[..], HID_READ_TIMEOUT_MS) { + Ok(0) => break, + Ok(n) => { + drained += 1; + log::warn!( + "Drained stale HID packet before request ({} bytes): {:02X?}", + n, + &drain_buf[..n] + ); + } + Err(_) => break, + } + } + + if drained > 0 { + log::warn!("Drained {} stale HID packet(s) before sending request", drained); + } + } + fn read_cbor_response(&self, cmd: u8) -> Result, PFError> { log::debug!("Waiting for response..."); @@ -263,15 +289,28 @@ impl HidTransport { )); } - if let Err(e) = self + buf.fill(0); + let bytes_read = match self .device .read_timeout(&mut buf[..], HID_RESP_READ_TIMEOUT_MS) { - log::error!("Timeout reading response packet: {}", e); - return Err(PFError::Io(format!( - "Timeout reading response packet: {}", - e - ))); + Ok(n) => n, + Err(e) => { + log::error!("Timeout reading response packet: {}", e); + return Err(PFError::Io(format!( + "Timeout reading response packet: {}", + e + ))); + } + }; + + if bytes_read < 7 { + log::warn!( + "Ignoring short HID response packet ({} bytes): {:02X?}", + bytes_read, + &buf[..bytes_read] + ); + continue; } // Check CID mismatch @@ -324,15 +363,28 @@ impl HidTransport { // 2. Read Continuation Packets while read_len < expected_len { - if let Err(e) = self + buf.fill(0); + let bytes_read = match self .device .read_timeout(&mut buf[..], HID_CONT_READ_TIMEOUT_MS) { - log::error!("Timeout reading continuation packet: {}", e); - return Err(PFError::Io(format!( - "Timeout reading continuation packet: {}", - e - ))); + Ok(n) => n, + Err(e) => { + log::error!("Timeout reading continuation packet: {}", e); + return Err(PFError::Io(format!( + "Timeout reading continuation packet: {}", + e + ))); + } + }; + + if bytes_read < 5 { + log::warn!( + "Ignoring short HID continuation packet ({} bytes): {:02X?}", + bytes_read, + &buf[..bytes_read] + ); + continue; } if u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]) != self.cid { @@ -1772,7 +1824,17 @@ impl HidTransport { payload.extend(to_vec(&Value::Map(map)).map_err(|e| PFError::Io(e.to_string()))?); let resp = self.send_cbor(CTAPHID_CBOR, &payload)?; - from_slice(&resp).map_err(|e| PFError::Io(e.to_string())) + if resp.is_empty() { + return match sub_cmd { + BioEnrollmentSubCommand::SetFriendlyName + | BioEnrollmentSubCommand::RemoveEnrollment + | BioEnrollmentSubCommand::CancelCurrentEnrollment => { + Ok(Value::Map(BTreeMap::new())) + } + _ => self.parse_bio_cbor_response(&resp, sub_cmd), + }; + } + self.parse_bio_cbor_response(&resp, sub_cmd) } fn sign_bio_enrollment_command( @@ -1832,6 +1894,60 @@ impl HidTransport { }) } + fn parse_bio_cbor_response( + &self, + resp: &[u8], + sub_cmd: BioEnrollmentSubCommand, + ) -> Result { + let mut attempts: Vec<&[u8]> = Vec::new(); + attempts.push(resp); + + let trimmed = trim_trailing_zeroes(resp); + if trimmed.len() != resp.len() { + attempts.push(trimmed); + } + + if let Some(stripped) = resp.strip_prefix(&[0x00]) { + attempts.push(stripped); + let stripped_trimmed = trim_trailing_zeroes(stripped); + if stripped_trimmed.len() != stripped.len() { + attempts.push(stripped_trimmed); + } + } + + let mut last_err = None; + for candidate in attempts { + if candidate.is_empty() { + continue; + } + match from_slice(candidate) { + Ok(value) => { + if candidate.as_ptr() != resp.as_ptr() || candidate.len() != resp.len() { + log::warn!( + "BioEnrollment {:?} response required relaxed CBOR parsing. raw={}, parsed={}", + sub_cmd, + hex::encode_upper(resp), + hex::encode_upper(candidate) + ); + } + return Ok(value); + } + Err(err) => last_err = Some(err), + } + } + + log::error!( + "Failed to parse BioEnrollment {:?} response as CBOR. raw={}", + sub_cmd, + hex::encode_upper(resp) + ); + Err(PFError::Io( + last_err + .map(|err| err.to_string()) + .unwrap_or_else(|| "Empty BioEnrollment response".to_string()), + )) + } + fn sign_credential_mgmt_command( &self, pin_token: &[u8], @@ -1864,6 +1980,14 @@ impl HidTransport { } } +fn trim_trailing_zeroes(bytes: &[u8]) -> &[u8] { + let mut end = bytes.len(); + while end > 0 && bytes[end - 1] == 0x00 { + end -= 1; + } + &bytes[..end] +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/ui/components/button.rs b/src/ui/components/button.rs index 6672629..49b9429 100644 --- a/src/ui/components/button.rs +++ b/src/ui/components/button.rs @@ -159,6 +159,7 @@ impl RenderOnce for PFButton { /// A stateless Icon + Text button wrapper #[derive(IntoElement)] pub struct PFIconButton { + id: SharedString, icon: Icon, text: SharedString, on_click: ClickHandler, @@ -174,9 +175,11 @@ pub struct PFIconButton { impl PFIconButton { pub fn new(icon: impl Into, text: impl Into) -> Self { + let text = text.into(); Self { + id: text.clone(), icon: icon.into(), - text: text.into(), + text, on_click: None, bg_color_start: rgb(0x1b1b1d), bg_color_hover: rgb(0x232325), @@ -189,6 +192,11 @@ impl PFIconButton { } } + pub fn id(mut self, id: impl Into) -> Self { + self.id = id.into(); + self + } + pub fn on_click( mut self, handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, @@ -232,10 +240,11 @@ impl PFIconButton { impl RenderOnce for PFIconButton { fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { + let id = self.id; let text = self.text; let icon = self.icon; - let mut btn = Button::new("pf-icon-btn") + let mut btn = Button::new(id) .custom( ButtonCustomVariant::new(cx) .color(self.bg_color_start.into()) diff --git a/src/ui/views/security.rs b/src/ui/views/security.rs index e55edfc..c7e2951 100644 --- a/src/ui/views/security.rs +++ b/src/ui/views/security.rs @@ -12,9 +12,7 @@ use crate::ui::rootview::ApplicationRoot; use crate::ui::types::DeviceConnectionState; use gpui::*; use gpui_component::button::ButtonVariant; -use gpui_component::{ - ActiveTheme, Icon, StyledExt, Theme, WindowExt, badge::Badge, h_flex, v_flex, -}; +use gpui_component::{ActiveTheme, Icon, StyledExt, Theme, WindowExt, h_flex, v_flex}; pub struct SecurityView { root: WeakEntity, @@ -414,21 +412,6 @@ impl SecurityView { .unwrap_or(false); let muted_fg = cx.theme().muted_foreground; - let header_right = if unlocked { - Badge::new() - .child( - h_flex() - .gap_1() - .items_center() - .child(Icon::default().path("icons/lock-open.svg").size_3p5()) - .child("Unlocked"), - ) - .color(gpui::green()) - .into_any_element() - } else { - Tag::new("PIN required").into_any_element() - }; - let sensor_chips = if let Some(sensor) = status.as_ref().and_then(|s| s.sensor.clone()) { h_flex() .gap_2() @@ -462,8 +445,23 @@ impl SecurityView { .title("Fingerprints") .description("Enumerate, enroll, rename, and delete fingerprints stored on the key.") .icon(Icon::default().path("icons/shield.svg")) - .header_right(header_right) - .child(v_flex().gap_4().child(sensor_chips).child(body)) + .child( + v_flex() + .gap_4() + .child( + h_flex() + .justify_between() + .items_center() + .gap_3() + .child(sensor_chips) + .child(if unlocked { + Tag::new("Unlocked").active(true).into_any_element() + } else { + Tag::new("Locked").into_any_element() + }), + ) + .child(body), + ) } fn render_locked_state(&self, cx: &mut Context) -> impl IntoElement { @@ -531,6 +529,7 @@ impl SecurityView { ) .child( PFIconButton::new(Icon::default().path("icons/lock-open.svg"), "Unlock Fingerprints") + .id("security-unlock-fingerprints") .on_click(listener) .with_colors(rgb(0xe4e4e7), rgb(0xd0d0d3), rgb(0xe4e4e7)) .with_text_color(rgb(0x18181b)) @@ -592,6 +591,20 @@ impl SecurityView { v_flex() .gap_6() + .child( + div() + .p_3() + .rounded_lg() + .border_1() + .border_color(border) + .bg(muted) + .child( + div() + .text_sm() + .text_color(muted_fg) + .child("Enrollment requires two fingerprint captures on the sensor."), + ), + ) .child( h_flex() .justify_between() @@ -615,6 +628,7 @@ impl SecurityView { Icon::default().path("icons/plus.svg").size_3p5(), "Enroll", ) + .id("security-enroll-fingerprint") .small() .loading(self.loading) .on_click(enroll_listener), @@ -624,6 +638,7 @@ impl SecurityView { Icon::default().path("icons/lock.svg").size_3p5(), "Lock", ) + .id("security-lock-fingerprints") .small() .disabled(self.loading) .on_click(lock_listener), @@ -642,86 +657,82 @@ impl SecurityView { let muted_fg = cx.theme().muted_foreground; let rename_template = template.clone(); let delete_template = template.clone(); + let template_id = template.template_id.clone(); + let template_name = template + .friendly_name + .clone() + .unwrap_or_else(|| "Unnamed fingerprint".to_string()); div() .w_full() .border_1() .border_color(border) .rounded_xl() - .p_4() + .px_4() + .py_3() .child( - v_flex() + h_flex() + .justify_between() + .items_center() .gap_4() .child( h_flex() - .justify_between() - .items_start() + .items_center() + .gap_3() .child( - v_flex() - .gap_1() - .child( - div() - .font_weight(FontWeight::SEMIBOLD) - .child( - template - .friendly_name - .clone() - .unwrap_or_else(|| "Unnamed fingerprint".to_string()), - ), - ) - .child( - div() - .text_sm() - .text_color(muted_fg) - .font_family("Mono") - .child(template.template_id.clone()), - ), + div() + .font_weight(FontWeight::SEMIBOLD) + .child(template_name), ) .child( - h_flex() - .gap_2() - .child( - PFButton::new("Rename") - .small() - .disabled(self.loading) - .with_colors( - rgb(0x222225), - rgb(0x2a2a2d), - rgb(0x333336), - ) - .on_click(cx.listener(move |this, _, window, cx| { - this.open_rename_dialog( - rename_template.clone(), - window, - cx, - ); - })), - ) - .child( - PFButton::new("Delete") - .small() - .disabled(self.loading) - .with_colors( - rgb(0x7f1d1d), - rgb(0x991b1b), - rgb(0xb91c1c), - ) - .with_text_color(rgb(0xfef2f2)) - .on_click(cx.listener(move |this, _, window, cx| { - this.open_delete_dialog( - delete_template.clone(), - window, - cx, - ); - })), - ), + div() + .text_sm() + .text_color(muted_fg) + .font_family("Mono") + .child(template.template_id.clone()), ), ) .child( - div() - .text_sm() - .text_color(muted_fg) - .child("Rename or delete this template. Enrollment requires two captures on the sensor."), + h_flex() + .gap_2() + .items_center() + .child( + PFButton::new("Rename") + .id(format!("rename-{}", template_id)) + .small() + .disabled(self.loading) + .with_colors( + rgb(0x222225), + rgb(0x2a2a2d), + rgb(0x333336), + ) + .on_click(cx.listener(move |this, _, window, cx| { + this.open_rename_dialog( + rename_template.clone(), + window, + cx, + ); + })), + ) + .child( + PFButton::new("Delete") + .id(format!("delete-{}", template.template_id)) + .small() + .disabled(self.loading) + .with_colors( + rgb(0x7f1d1d), + rgb(0x991b1b), + rgb(0xb91c1c), + ) + .with_text_color(rgb(0xfef2f2)) + .on_click(cx.listener(move |this, _, window, cx| { + this.open_delete_dialog( + delete_template.clone(), + window, + cx, + ); + })), + ), ), ) }