mirror of
https://github.com/librekeys/picoforge.git
synced 2026-07-28 08:01:19 -07:00
added fingerprint supported version
This commit is contained in:
+137
-13
@@ -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::*;
|
||||
|
||||
@@ -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
@@ -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,
|
||||
);
|
||||
})),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user