added fingerprint supported version

This commit is contained in:
Vaishakh S Nair
2026-03-24 19:09:57 +05:30
parent d613c276d0
commit df73943f46
3 changed files with 243 additions and 99 deletions
+137 -13
View File
@@ -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<Vec<u8>, 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<Value, PFError> {
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::*;
+11 -2
View File
@@ -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<Icon>, text: impl Into<SharedString>) -> 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<SharedString>) -> 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())
+95 -84
View File
@@ -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<ApplicationRoot>,
@@ -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<Self>) -> 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,
);
})),
),
),
)
}