From 6b5d591e284be25bc2098f1c8f4efe8d3e1f8df0 Mon Sep 17 00:00:00 2001 From: Suyog Tandel Date: Mon, 22 Jun 2026 21:26:39 +0530 Subject: [PATCH 01/11] feat: add support for RS-Keys firmware and add optito reset the device --- Cargo.lock | 2 +- README.md | 6 +- src/device/fido/hid.rs | 23 ++- src/device/fido/mod.rs | 32 +++- src/device/io.rs | 20 ++ src/device/rescue/constants.rs | 116 +++++++++++- src/device/rescue/mod.rs | 276 ++++++++++++++++++++++++++- src/device/types.rs | 48 +++++ src/ui/components/dialog.rs | 42 ++++- src/ui/components/sidebar.rs | 11 +- src/ui/rootview.rs | 12 +- src/ui/types.rs | 6 +- src/ui/views/config.rs | 335 ++++++++++++++++++++++++++++++++- src/ui/views/home.rs | 6 + src/ui/views/passkeys.rs | 172 ++++++++++++++++- static/icons/circle-alert.svg | 1 + static/icons/trash.svg | 1 + 17 files changed, 1059 insertions(+), 50 deletions(-) create mode 100644 static/icons/circle-alert.svg create mode 100644 static/icons/trash.svg diff --git a/Cargo.lock b/Cargo.lock index d2cf8db..36d537a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4133,7 +4133,7 @@ checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" [[package]] name = "picoforge" -version = "0.5.0" +version = "0.6.0" dependencies = [ "aes", "anyhow", diff --git a/README.md b/README.md index 1c8623e..e6ee14f 100644 --- a/README.md +++ b/README.md @@ -23,9 +23,9 @@ > Check application [Installation Wiki](https://github.com/librekeys/picoforge/wiki/Installation) for installation guide of the PicoForge app on your system. > > **Supported Firmwares:** -> - **pico-fido**: v7.0, v7.2, v7.4, v7.6 -> - **LibreKeys One**: v7.4.2 -> - **RSKeys**: v0.2.8 +> - **[pico-fido](https://github.com/polhenarejos/pico-fido)**: v7.0, v7.2, v7.4, v7.6 +> - **[LibreKeys One](https://github.com/librekeys/pico-fido-firmwares/releases)**: v7.4.2 +> - **[RSKeys](https://github.com/TheMaxMur/RS-Key)**: v0.2.8 > > **Configuration Support:** > - **pico-fido v7.0/v7.2** & **LibreKeys One v7.4.2**: Hardware configuration via FIDO mode is supported. diff --git a/src/device/fido/hid.rs b/src/device/fido/hid.rs index 213fddc..8b6a225 100644 --- a/src/device/fido/hid.rs +++ b/src/device/fido/hid.rs @@ -164,13 +164,24 @@ impl HidTransport { } pub fn send_cbor(&self, cmd: u8, payload: &[u8]) -> Result, PFError> { + self.send_cbor_with_timeout(cmd, payload, HID_TOTAL_TIMEOUT_MS) + } + + pub fn send_cbor_with_timeout(&self, cmd: u8, payload: &[u8], timeout_ms: i32) -> Result, PFError> { self.write_cbor_request(cmd, payload)?; - self.read_cbor_response(cmd) + self.read_cbor_response(cmd, timeout_ms) } pub fn send_raw(&self, cmd: u8, payload: &[u8]) -> Result, PFError> { self.write_cbor_request(cmd, payload)?; - self.read_hid_response(cmd) + self.read_hid_response(cmd, HID_TOTAL_TIMEOUT_MS) + } + + pub fn reset(&self) -> Result<(), PFError> { + log::info!("Sending CTAP authenticatorReset (0x07)..."); + self.write_cbor_request(CTAPHID_CBOR, &[0x07])?; + self.read_cbor_response(CTAPHID_CBOR, 30_000)?; + Ok(()) } fn write_cbor_request(&self, cmd: u8, payload: &[u8]) -> Result<(), PFError> { @@ -239,8 +250,8 @@ impl HidTransport { Ok(()) } - fn read_cbor_response(&self, cmd: u8) -> Result, PFError> { - let response_data = self.read_hid_response(cmd)?; + fn read_cbor_response(&self, cmd: u8, timeout_ms: i32) -> Result, PFError> { + let response_data = self.read_hid_response(cmd, timeout_ms)?; // Check CTAP Status Byte (First byte of payload) if response_data.is_empty() { @@ -265,7 +276,7 @@ impl HidTransport { Ok(response_data[1..].to_vec()) } - fn read_hid_response(&self, cmd: u8) -> Result, PFError> { + fn read_hid_response(&self, cmd: u8, timeout_ms: i32) -> Result, PFError> { log::debug!("Waiting for response..."); let mut buf = [0u8; HID_REPORT_SIZE]; @@ -275,7 +286,7 @@ impl HidTransport { let mut last_seq = 0; let start_time = std::time::Instant::now(); - let timeout_duration = std::time::Duration::from_millis(HID_TOTAL_TIMEOUT_MS as u64); + let timeout_duration = std::time::Duration::from_millis(timeout_ms as u64); // 1. Read First Packet (Loop to handle Keepalives) loop { diff --git a/src/device/fido/mod.rs b/src/device/fido/mod.rs index d39fbb4..d6d11cc 100644 --- a/src/device/fido/mod.rs +++ b/src/device/fido/mod.rs @@ -4,7 +4,7 @@ pub mod hid; use crate::{ device::types::{ AppConfig, AppConfigInput, DeviceInfo, DeviceMethod, FidoDeviceInfo, FullDeviceStatus, - StoredCredential, + StoredCredential, RSKEY_AAGUID, PICOFIDO_AAGUID, FirmwareType, }, error::PFError, }; @@ -488,6 +488,26 @@ pub(crate) fn delete_credential(pin: String, credential_id_hex: String) -> Resul Ok("Credential deleted successfully".into()) } +pub(crate) fn reset_device() -> Result { + log::info!("Starting FIDO authenticatorReset..."); + + let transport = + HidTransport::open().map_err(|e| format!("Could not open HID transport: {}", e))?; + + transport.reset().map_err(|e| { + let s = e.to_string(); + if s.contains("0x30") { + return "Reset not allowed. The device must be unplugged and re-plugged within 10 seconds before sending the reset command.".to_string(); + } + if s.contains("0x27") { + return "Reset declined. Touch was not confirmed on the device.".to_string(); + } + format!("Reset failed: {}", s) + })?; + + Ok("Device has been factory reset. All credentials and PIN have been erased.".to_string()) +} + // Custom Fido functions ( works only with pico-fido firmware ) #[derive(Debug, Default, Clone, PartialEq, Eq)] @@ -553,6 +573,14 @@ pub fn read_device_details() -> Result { .unwrap_or_else(|| "Unknown".to_string()) }; + let firmware_type = if fido_info.aaguid == RSKEY_AAGUID { + FirmwareType::RSKey + } else if fido_info.aaguid == PICOFIDO_AAGUID { + FirmwareType::PicoFido + } else { + FirmwareType::Unknown + }; + Ok(FullDeviceStatus { info: DeviceInfo { serial: management @@ -566,6 +594,7 @@ pub fn read_device_details() -> Result { secure_boot: false, secure_lock: false, method: DeviceMethod::Fido, + firmware_type, }) } @@ -1041,6 +1070,7 @@ mod tests { power_cycle_on_reset: None, led_steady: None, enable_secp256k1: None, + led_order: None, } } diff --git a/src/device/io.rs b/src/device/io.rs index 61cd049..62dc9f3 100644 --- a/src/device/io.rs +++ b/src/device/io.rs @@ -59,6 +59,26 @@ pub fn delete_credential(pin: String, credential_id: String) -> Result Result { + fido::reset_device() +} + +pub fn read_led_config() -> Result { + rescue::read_led_config() +} + +pub fn write_led_status(status: u8, color: u8, brightness: u8, steady: bool) -> Result { + rescue::write_led_status(status, color, brightness, steady) +} + +pub fn read_management_config() -> Result { + rescue::read_management_config() +} + +pub fn write_management_config(enabled_mask: u16) -> Result { + rescue::write_management_config(enabled_mask) +} + pub fn enable_enterprise_attestation(pin: String) -> Result { fido::enable_enterprise_attestation(pin) } diff --git a/src/device/rescue/constants.rs b/src/device/rescue/constants.rs index 9f4c9ed..8ff25dc 100644 --- a/src/device/rescue/constants.rs +++ b/src/device/rescue/constants.rs @@ -1,4 +1,4 @@ -//! Constants, enums, bitflags and data structures for Rescue Application for pico-fido firmware. +//! Constants, enums, bitflags and data structures for Rescue and vendor applets. #![allow(unused)] // use serde::{Deserialize, Serialize}; @@ -91,10 +91,11 @@ pub enum PhyTag { LedGpio = 0x04, LedBrightness = 0x05, Opts = 0x06, - PresenceTimeout = 0x08, // Previously TAG_UP_BTN + PresenceTimeout = 0x08, UsbProduct = 0x09, Curves = 0x0A, LedDriver = 0x0C, + LedOrder = 0x0D, } impl PhyTag { @@ -109,6 +110,7 @@ impl PhyTag { 0x09 => Some(Self::UsbProduct), 0x0A => Some(Self::Curves), 0x0C => Some(Self::LedDriver), + 0x0D => Some(Self::LedOrder), _ => None, } } @@ -129,3 +131,113 @@ bitflags::bitflags! { const SECP256K1 = 0x08; } } + +// --- 4. Vendor/LED Applet (RS-Key specific) --- + +pub const VENDOR_LED_AID: &[u8] = &[0xF0, 0x00, 0x00, 0x00, 0x01]; + +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum VendorLedInstruction { + SetLed = 0x10, + GetLed = 0x11, +} + +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LedColor { + Off = 0, + Red = 1, + Green = 2, + Blue = 3, + Yellow = 4, + Magenta = 5, + Cyan = 6, + White = 7, +} + +impl LedColor { + pub fn from_u8(val: u8) -> Option { + match val { + 0 => Some(Self::Off), + 1 => Some(Self::Red), + 2 => Some(Self::Green), + 3 => Some(Self::Blue), + 4 => Some(Self::Yellow), + 5 => Some(Self::Magenta), + 6 => Some(Self::Cyan), + 7 => Some(Self::White), + _ => None, + } + } + + pub fn label(&self) -> &'static str { + match self { + Self::Off => "Off", + Self::Red => "Red", + Self::Green => "Green", + Self::Blue => "Blue", + Self::Yellow => "Yellow", + Self::Magenta => "Magenta", + Self::Cyan => "Cyan", + Self::White => "White", + } + } + + pub fn all() -> &'static [Self] { + &[ + Self::Off, Self::Red, Self::Green, Self::Blue, + Self::Yellow, Self::Magenta, Self::Cyan, Self::White, + ] + } +} + +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LedStatus { + Idle = 0, + Processing = 1, + Touch = 2, + Boot = 3, +} + +impl LedStatus { + pub fn label(&self) -> &'static str { + match self { + Self::Idle => "Idle", + Self::Processing => "Processing", + Self::Touch => "Touch", + Self::Boot => "Boot", + } + } + + pub fn all() -> &'static [Self] { + &[Self::Idle, Self::Processing, Self::Touch, Self::Boot] + } +} + +// --- 5. Management Applet (Yubico-compatible, RS-Key) --- + +pub const MANAGEMENT_AID: &[u8] = &[0xA0, 0x00, 0x00, 0x05, 0x27, 0x47, 0x11, 0x17]; + +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ManagementInstruction { + ReadConfig = 0x1D, + WriteConfig = 0x1C, +} + +pub const MGMT_TAG_USB_SUPPORTED: u8 = 0x01; +pub const MGMT_TAG_SERIAL: u8 = 0x02; +pub const MGMT_TAG_USB_ENABLED: u8 = 0x03; +pub const MGMT_TAG_FORM_FACTOR: u8 = 0x04; +pub const MGMT_TAG_VERSION: u8 = 0x05; +pub const MGMT_TAG_DEVICE_FLAGS: u8 = 0x08; +pub const MGMT_TAG_CONFIG_LOCK: u8 = 0x0A; + +pub const USB_CAP_OTP: u16 = 0x0001; +pub const USB_CAP_U2F: u16 = 0x0002; +pub const USB_CAP_OPENPGP: u16 = 0x0008; +pub const USB_CAP_PIV: u16 = 0x0010; +pub const USB_CAP_OATH: u16 = 0x0020; +pub const USB_CAP_FIDO2: u16 = 0x0200; diff --git a/src/device/rescue/mod.rs b/src/device/rescue/mod.rs index 95466a1..70f6d55 100644 --- a/src/device/rescue/mod.rs +++ b/src/device/rescue/mod.rs @@ -10,8 +10,27 @@ use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; use pcsc::{Context, Protocols, Scope, ShareMode}; use std::io::Cursor; +/// Differentiates between Pico-Fido and RS-Key firmwares based on the Rescue Applet SELECT response. +/// +/// **WARNING:** This is a temporary heuristic that relies on the major version byte (`major >= 8` implies RS-Key). +/// If Pico-Fido releases v8.x, this logic will silently fail and misidentify devices. +/// +/// TODO: Work with upstream RS-Key maintainers to expose a unique identity block or hardware string +/// in the SELECT response to reliably differentiate the firmwares in the long term. +fn detect_firmware_type(select_resp: &[u8]) -> FirmwareType { + if select_resp.len() >= 4 { + let major = select_resp[2]; + if major >= 8 { + return FirmwareType::RSKey; + } else { + return FirmwareType::PicoFido; + } + } + FirmwareType::Unknown +} + /// Connects to the first available reader and selects the Rescue Applet -fn connect_and_select() -> Result<(pcsc::Card, Vec), PFError> { +fn connect_and_select() -> Result<(pcsc::Card, Vec, FirmwareType), PFError> { let ctx = Context::establish(Scope::User).map_err(|e| { log::error!("Failed to establish PCSC context: {}", e); PFError::Pcsc(e) @@ -52,12 +71,15 @@ fn connect_and_select() -> Result<(pcsc::Card, Vec), PFError> { } log::info!("Successfully connected to Rescue Applet"); - Ok((card, rx.to_vec())) + let data = rx.to_vec(); + let fw_type = detect_firmware_type(&data); + log::info!("Detected firmware type: {:?}", fw_type); + Ok((card, data, fw_type)) } pub fn read_device_details() -> Result { log::info!("Reading full device details"); - let (card, select_resp) = connect_and_select()?; + let (card, select_resp, fw_type) = connect_and_select()?; log::info!("Select Response: {:?}", select_resp); @@ -213,6 +235,11 @@ pub fn read_device_details() -> Result { config.led_driver = Some(val[0]); } } + PhyTag::LedOrder => { + if !val.is_empty() { + config.led_order = Some(val[0]); + } + } } } i += len; @@ -236,6 +263,7 @@ pub fn read_device_details() -> Result { secure_boot: sb_enabled, secure_lock: sb_locked, method: DeviceMethod::Rescue, + firmware_type: fw_type, }) } @@ -332,7 +360,14 @@ pub fn write_config(config: AppConfigInput) -> Result { tlv.push(PhyTag::UsbProduct as u8); tlv.push(len as u8); tlv.extend_from_slice(name_bytes); - tlv.push(0x00); // Null terminator + tlv.push(0x00); + } + + // LED Order (Tag 0x0D) — RS-Key extension, silently preserved + if let Some(val) = config.led_order { + tlv.push(PhyTag::LedOrder as u8); + tlv.push(0x01); + tlv.push(val); } // 2. Connect and Send @@ -343,7 +378,7 @@ pub fn write_config(config: AppConfigInput) -> Result { log::debug!("TLV payload size: {} bytes", tlv.len()); - let (card, _) = connect_and_select()?; + let (card, _, _) = connect_and_select()?; // APDU: 80 1C 01 00 [Lc] [Data] let mut apdu = vec![ @@ -368,7 +403,7 @@ pub fn write_config(config: AppConfigInput) -> Result { } pub fn reboot_device(to_bootsel: bool) -> Result { - let (card, _) = connect_and_select()?; + let (card, _, _) = connect_and_select()?; let param = if to_bootsel { RebootParam::Bootsel @@ -396,7 +431,7 @@ pub fn reboot_device(to_bootsel: bool) -> Result { /// UNSTABLE! (WIP) pub fn enable_secure_boot(lock: bool) -> Result { - let (card, _) = connect_and_select()?; + let (card, _, _) = connect_and_select()?; // APDU: 80 1D [KeyIndex] [LockBool] 00 // KeyIndex = 0 (Default), LockBool = 1 if true @@ -419,3 +454,230 @@ pub fn enable_secure_boot(lock: bool) -> Result { Err(PFError::Device(format!("Secure Boot failed: {:02X?}", rx))) } } + +// --- Vendor/LED Applet (RS-Key) --- + +fn connect_and_select_aid(aid: &[u8]) -> Result { + let ctx = Context::establish(Scope::User).map_err(|e| { + log::error!("Failed to establish PCSC context: {}", e); + PFError::Pcsc(e) + })?; + + let mut readers_buf = [0; 2048]; + let mut readers = ctx.list_readers(&mut readers_buf)?; + let reader = readers.next().ok_or_else(|| { + log::info!("No Smart Card Reader found"); + PFError::NoDevice + })?; + + let card = ctx.connect(reader, ShareMode::Shared, Protocols::ANY)?; + + let mut apdu = vec![ + APDU_CLA_ISO, + APDU_INS_SELECT, + APDU_P1_SELECT_BY_DF_NAME, + 0x00, + aid.len() as u8, + ]; + apdu.extend_from_slice(aid); + apdu.push(0x00); + + let mut rx_buf = [0; 256]; + let rx = card.transmit(&apdu, &mut rx_buf)?; + + if !rx.ends_with(&[0x90, 0x00]) { + return Err(PFError::Device( + format!("Applet not found (AID {:02X?})", aid), + )); + } + + Ok(card) +} + +/// Reads the customized LED status configurations from the Vendor/LED applet. +/// +/// Communicates with the `F0 00 00 00 01` applet to retrieve a 9-byte configuration block +/// that dictates the color and brightness for each device state (idle, processing, touch, boot), +/// as well as the global 'steady' toggle flag. +pub fn read_led_config() -> Result { + log::info!("Reading LED status config from Vendor/LED applet"); + let card = connect_and_select_aid(VENDOR_LED_AID)?; + + let apdu = [ + APDU_CLA_ISO, + VendorLedInstruction::GetLed as u8, + 0x00, + 0x00, + 0x00, + ]; + let mut rx_buf = [0; 256]; + let rx = card.transmit(&apdu, &mut rx_buf)?; + + if !rx.ends_with(&SW_SUCCESS) || rx.len() < 11 { + return Err(PFError::Device("Failed to read LED config".into())); + } + + let data = &rx[..rx.len() - 2]; + if data.len() < 9 { + return Err(PFError::Device("LED config response too short".into())); + } + + let steady = data[0] != 0; + let mut statuses = [(0u8, 0u8); 4]; + for s in 0..4 { + statuses[s] = (data[1 + 2 * s], data[2 + 2 * s]); + } + + log::info!("LED config: steady={}, statuses={:?}", steady, statuses); + Ok(LedStatusConfig { steady, statuses }) +} + +/// Applies an individual LED status update to the Vendor/LED applet. +/// +/// Constructs the APDU payload combining the targeted status index, color code, and global +/// steady flag into `P2`, with the brightness value in `P1`. The update is persisted to flash +/// and applied immediately. +pub fn write_led_status( + status: u8, + color: u8, + brightness: u8, + steady: bool, +) -> Result { + log::info!( + "Setting LED: status={}, color={}, brightness={}, steady={}", + status, color, brightness, steady + ); + let card = connect_and_select_aid(VENDOR_LED_AID)?; + + let steady_bit: u8 = if steady { 0x08 } else { 0x00 }; + let p2 = (color & 0x07) | steady_bit | ((status & 0x03) << 4); + + let apdu = [ + APDU_CLA_ISO, + VendorLedInstruction::SetLed as u8, + brightness, + p2, + ]; + let mut rx_buf = [0; 256]; + let rx = card.transmit(&apdu, &mut rx_buf)?; + + if rx.ends_with(&SW_SUCCESS) { + Ok("LED status updated".into()) + } else { + Err(PFError::Device(format!("SET LED failed: {:02X?}", rx))) + } +} + +// --- Management Applet (RS-Key) --- + +/// Retrieves the device management configuration mapping from the Management applet. +/// +/// Reads the active state of various USB interfaces (U2F, OATH, PIV, OpenPGP, etc.) to +/// determine which are supported by the hardware and which are currently enabled by the user. +pub fn read_management_config() -> Result { + log::info!("Reading management config from Management applet"); + let card = connect_and_select_aid(MANAGEMENT_AID)?; + + let apdu = [ + APDU_CLA_ISO, + ManagementInstruction::ReadConfig as u8, + 0x00, + 0x00, + 0x00, + ]; + let mut rx_buf = [0; 256]; + let rx = card.transmit(&apdu, &mut rx_buf)?; + + if !rx.ends_with(&SW_SUCCESS) { + return Err(PFError::Device("Failed to read management config".into())); + } + + let data = &rx[..rx.len() - 2]; + if data.is_empty() { + return Err(PFError::Device("Empty management config response".into())); + } + + let overall_len = data[0] as usize; + let tlv_data = if data.len() > 1 + overall_len { + &data[1..1 + overall_len] + } else { + &data[1..] + }; + + let mut config = ManagementAppConfig::default(); + let mut i = 0; + while i < tlv_data.len() { + if i + 2 > tlv_data.len() { + break; + } + let tag = tlv_data[i]; + let len = tlv_data[i + 1] as usize; + i += 2; + if i + len > tlv_data.len() { + break; + } + let val = &tlv_data[i..i + len]; + match tag { + MGMT_TAG_USB_SUPPORTED => { + if val.len() >= 2 { + config.usb_supported = u16::from_be_bytes([val[0], val[1]]); + } + } + MGMT_TAG_USB_ENABLED => { + if val.len() >= 2 { + config.usb_enabled = u16::from_be_bytes([val[0], val[1]]); + } + } + _ => { + log::trace!("Management TLV tag 0x{:02X} skipped", tag); + } + } + i += len; + } + + log::info!( + "Management config: supported=0x{:04X}, enabled=0x{:04X}", + config.usb_supported, + config.usb_enabled + ); + Ok(config) +} + +/// Persists updated management endpoint configurations to the device. +/// +/// Overwrites the previously enabled interfaces with a new configuration bitmask. +/// For the changes to fully apply across all composite USB endpoints, a subsequent +/// device reboot or re-plug is required. +pub fn write_management_config(enabled_mask: u16) -> Result { + log::info!("Writing management config: enabled=0x{:04X}", enabled_mask); + let card = connect_and_select_aid(MANAGEMENT_AID)?; + + let inner = [ + MGMT_TAG_USB_ENABLED, + 0x02, + (enabled_mask >> 8) as u8, + (enabled_mask & 0xFF) as u8, + ]; + + let mut apdu = vec![ + APDU_CLA_ISO, + ManagementInstruction::WriteConfig as u8, + 0x00, + 0x00, + (inner.len() + 1) as u8, + inner.len() as u8, + ]; + apdu.extend_from_slice(&inner); + + let mut rx_buf = [0; 256]; + let rx = card.transmit(&apdu, &mut rx_buf)?; + + if rx.ends_with(&SW_SUCCESS) { + Ok("USB applications updated".into()) + } else { + Err(PFError::Device(format!( + "Management write failed: {:02X?}", + rx + ))) + } +} diff --git a/src/device/types.rs b/src/device/types.rs index 243e74c..b5f525c 100644 --- a/src/device/types.rs +++ b/src/device/types.rs @@ -1,6 +1,7 @@ #![allow(unused)] use serde::{Deserialize, Serialize}; +use std::fmt; struct PForgeState { device_info: DeviceInfo, @@ -30,6 +31,8 @@ pub struct AppConfig { pub power_cycle_on_reset: bool, pub led_steady: bool, pub enable_secp256k1: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub led_order: Option, } #[derive(Deserialize, Debug, Clone)] @@ -46,6 +49,7 @@ pub struct AppConfigInput { pub power_cycle_on_reset: Option, pub led_steady: Option, pub enable_secp256k1: Option, + pub led_order: Option, } #[derive(Serialize, Debug, Clone, PartialEq)] @@ -56,6 +60,7 @@ pub struct FullDeviceStatus { pub secure_boot: bool, pub secure_lock: bool, pub method: DeviceMethod, + pub firmware_type: FirmwareType, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] @@ -65,6 +70,49 @@ pub enum DeviceMethod { Rescue, } +/// Represents the recognized firmware variants running on the connected hardware token. +/// Used extensively to gate UI features, connection methods, and compatibility checks. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)] +pub enum FirmwareType { + PicoFido, + RSKey, + #[default] + Unknown, +} + +impl fmt::Display for FirmwareType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::PicoFido => write!(f, "Pico-FIDO"), + Self::RSKey => write!(f, "RS-Key"), + Self::Unknown => write!(f, "Unknown"), + } + } +} + +/// The globally unique Authenticator Attestation GUID (AAGUID) assigned to RS-Key hardware. +pub const RSKEY_AAGUID: &str = "2479C7BF6B3056839EC80E8171A918B7"; +/// The globally unique Authenticator Attestation GUID (AAGUID) assigned to Pico-Fido hardware. +pub const PICOFIDO_AAGUID: &str = "89FB94B706C936739B7E30526D968145"; + +/// Aggregates the LED status configurations read from the RS-Key Vendor/LED applet. +/// Contains the global steady flag and a fixed array of `(color_code, brightness)` pairs +/// mapped chronologically to device statuses: [Idle, Processing, Touch, Boot]. +#[derive(Serialize, Debug, Default, Clone, PartialEq)] +pub struct LedStatusConfig { + pub steady: bool, + pub statuses: [(u8, u8); 4], +} + +/// Encapsulates the bitmasks defining USB application endpoints on the device. +/// The `usb_supported` mask indicates which applets the firmware is capable of running, +/// while `usb_enabled` reflects the active endpoints the device will enumerate on next boot. +#[derive(Serialize, Debug, Default, Clone, PartialEq)] +pub struct ManagementAppConfig { + pub usb_supported: u16, + pub usb_enabled: u16, +} + // Fido stuff: #[derive(Serialize, Debug, Clone, PartialEq)] diff --git a/src/ui/components/dialog.rs b/src/ui/components/dialog.rs index e46af03..d8c9844 100644 --- a/src/ui/components/dialog.rs +++ b/src/ui/components/dialog.rs @@ -8,7 +8,7 @@ use gpui_component::{ }; type PinPromptCallback = std::rc::Rc, &mut App)>; -type ConfirmCallback = std::rc::Rc, &mut App)>; +type ConfirmCallback = std::rc::Rc, &mut Window, &mut App)>; type ChangePinCallback = std::rc::Rc, &mut App)>; type SetPinCallback = std::rc::Rc, &mut App)>; @@ -17,6 +17,9 @@ type SetPinCallback = std::rc::Rc, &mut enum DialogPhase { Input, Loading, + /// Indicates the dialog is blocked on an asynchronous background task, + /// presenting a specific dynamic status message to guide the user (e.g. "Waiting for touch..."). + LoadingWithMessage(String), Success(String), Error(String), } @@ -92,7 +95,7 @@ impl Render for PinPromptContent { ) .into_any_element(), - DialogPhase::Loading => v_flex() + DialogPhase::Loading | DialogPhase::LoadingWithMessage(_) => v_flex() .gap_4() .child(self.description.clone()) .child(Input::new(&self.pin_input).disabled(true)) @@ -365,7 +368,7 @@ impl Render for ConfirmContent { ) .into_any_element(), - DialogPhase::Loading => v_flex() + DialogPhase::Loading | DialogPhase::LoadingWithMessage(_) => v_flex() .gap_4() .child(self.message.clone()) .child( @@ -414,11 +417,11 @@ impl Render for ConfirmContent { Button::new("ok") .with_variant(ok_variant) .label(ok_label) - .on_click(move |_, _, cx| { + .on_click(move |_, window, cx| { if let Some(h) = handle.upgrade() { h.update(cx, |this, cx| this.set_loading(cx)); } - on_ok(handle.clone(), cx); + on_ok(handle.clone(), window, cx); }), ), ) @@ -447,11 +450,11 @@ impl Render for ConfirmContent { Button::new("ok") .with_variant(ok_variant) .label(ok_label) - .on_click(move |_, _, cx| { + .on_click(move |_, window, cx| { if let Some(h) = handle.upgrade() { h.update(cx, |this, cx| this.set_loading(cx)); } - on_ok(handle.clone(), cx); + on_ok(handle.clone(), window, cx); }), ), ) @@ -468,7 +471,7 @@ pub fn open_confirm( ok_variant: ButtonVariant, window: &mut Window, cx: &mut App, - on_ok: impl Fn(WeakEntity, &mut App) + 'static, + on_ok: impl Fn(WeakEntity, &mut Window, &mut App) + 'static, ) { let title_str = SharedString::from(title.to_string()); let dialog_title = title_str.clone(); @@ -576,7 +579,7 @@ impl Render for ChangePinContent { ) .into_any_element(), - DialogPhase::Loading => v_flex() + DialogPhase::Loading | DialogPhase::LoadingWithMessage(_) => v_flex() .gap_4() .child("Enter your current PIN and choose a new one.") .child( @@ -887,7 +890,7 @@ impl Render for SetPinContent { ) .into_any_element(), - DialogPhase::Loading => v_flex() + DialogPhase::Loading | DialogPhase::LoadingWithMessage(_) => v_flex() .gap_4() .child("Choose a PIN for your pico-key.") .child( @@ -1100,6 +1103,13 @@ pub struct StatusContent { } impl StatusContent { + /// Transitions the dialog into a loading state while displaying a custom, dynamic status message. + /// Useful for multi-step background operations where user context needs to be updated. + pub fn set_loading(&mut self, msg: impl Into, cx: &mut Context) { + self.phase = DialogPhase::LoadingWithMessage(msg.into()); + cx.notify(); + } + pub fn set_success(&mut self, msg: String, cx: &mut Context) { self.phase = DialogPhase::Success(msg); cx.notify(); @@ -1178,6 +1188,18 @@ impl Render for StatusContent { .into_any_element() } + DialogPhase::LoadingWithMessage(msg) => v_flex() + .gap_4() + .items_center() + .child(msg.clone()) + .child( + Button::new("loading") + .primary() + .label("Applying...") + .loading(true), + ) + .into_any_element(), + _ => v_flex() .gap_4() .items_center() diff --git a/src/ui/components/sidebar.rs b/src/ui/components/sidebar.rs index f57e098..9136bd5 100644 --- a/src/ui/components/sidebar.rs +++ b/src/ui/components/sidebar.rs @@ -221,15 +221,18 @@ impl AppSidebar { .child({ let (text, color_bg, color_text) = if let Some(status) = &state.status { + let is_rskey = status.firmware_type == crate::device::types::FirmwareType::RSKey; + let fw_label = if is_rskey { "RS-Key" } else { "Pico-FIDO" }; + if status.method == DeviceMethod::Fido { - ("Online - Fido", rgb(0xf59e0b), rgb(0xffffff)) + (format!("Online - FIDO ({})", fw_label), rgb(0xf59e0b), rgb(0xffffff)) } else { - ("Online", rgb(0x16a34a), rgb(0xffffff)) + (format!("Online - {}", fw_label), rgb(0x16a34a), rgb(0xffffff)) } } else if state.error.is_some() { - ("Error", rgb(0xd97706), rgb(0xffffff)) + ("Error".to_string(), rgb(0xd97706), rgb(0xffffff)) } else { - ("Offline", rgb(0xef4444), rgb(0xffffff)) + ("Offline".to_string(), rgb(0xef4444), rgb(0xffffff)) }; div() diff --git a/src/ui/rootview.rs b/src/ui/rootview.rs index 90d1f93..3a5677c 100644 --- a/src/ui/rootview.rs +++ b/src/ui/rootview.rs @@ -55,7 +55,7 @@ impl ApplicationRoot { .map(|s| s.info.serial != status.info.serial) .unwrap_or(true); - self.device.status = Some(status); + self.device.status = Some(status.clone()); self.device.error = None; if device_changed { @@ -72,6 +72,14 @@ impl ApplicationRoot { } } + if status.firmware_type == crate::device::types::FirmwareType::RSKey && status.method == crate::device::types::DeviceMethod::Rescue { + self.device.led_status = io::read_led_config().ok(); + self.device.management_apps = io::read_management_config().ok(); + } else { + self.device.led_status = None; + self.device.management_apps = None; + } + if let Some(config_view) = &self.views.config && let Some(window) = window { @@ -85,6 +93,8 @@ impl ApplicationRoot { self.device.status = None; self.device.error = Some(format!("{}", e)); self.device.fido_info = None; + self.device.led_status = None; + self.device.management_apps = None; } } self.device.loading = false; diff --git a/src/ui/types.rs b/src/ui/types.rs index 962e6f1..d842127 100644 --- a/src/ui/types.rs +++ b/src/ui/types.rs @@ -1,5 +1,5 @@ use crate::{ - device::types::{FidoDeviceInfo, FullDeviceStatus}, + device::types::{FidoDeviceInfo, FullDeviceStatus, LedStatusConfig, ManagementAppConfig}, ui::views::{config::ConfigView, passkeys::PasskeysView}, }; use gpui::{Entity, Pixels, SharedString, px}; @@ -17,6 +17,8 @@ pub enum ActiveView { pub struct DeviceConnectionState { pub status: Option, pub fido_info: Option, + pub led_status: Option, + pub management_apps: Option, pub error: Option, pub loading: bool, } @@ -26,6 +28,8 @@ impl DeviceConnectionState { Self { status: None, fido_info: None, + led_status: None, + management_apps: None, error: None, loading: false, } diff --git a/src/ui/views/config.rs b/src/ui/views/config.rs index 93630b2..8316f50 100644 --- a/src/ui/views/config.rs +++ b/src/ui/views/config.rs @@ -1,5 +1,6 @@ use crate::device::types::{AppConfigInput, DeviceMethod}; use crate::device::{fido, io}; +use crate::device::rescue::constants::{LedColor, LedStatus, USB_CAP_OTP, USB_CAP_U2F, USB_CAP_OPENPGP, USB_CAP_PIV, USB_CAP_OATH, USB_CAP_FIDO2}; use crate::ui::components::dialog::PinPromptContent; use crate::ui::components::{card::Card, dialog, dialog::StatusContent, page_view::PageView}; use crate::ui::rootview::ApplicationRoot; @@ -73,6 +74,14 @@ pub struct ConfigView { enable_secp256k1: bool, loading: bool, is_custom_vendor: bool, + + // RS-Key specific state + led_status_steady: bool, + led_status_colors: [u8; 4], + led_status_brightness: [u8; 4], + usb_apps_supported: u16, + usb_apps_enabled: u16, + _task: Option>, } @@ -194,6 +203,24 @@ impl ConfigView { let touch_timeout_input = cx.new(|cx| InputState::new(window, cx).default_value(current_touch_timeout.clone())); + let mut led_status_steady = false; + let mut led_status_colors = [0; 4]; + let mut led_status_brightness = [0; 4]; + if let Some(led) = &device.led_status { + led_status_steady = led.steady; + for i in 0..4 { + led_status_colors[i] = led.statuses[i].0; + led_status_brightness[i] = led.statuses[i].1; + } + } + + let mut usb_apps_supported = 0; + let mut usb_apps_enabled = 0; + if let Some(apps) = &device.management_apps { + usb_apps_supported = apps.usb_supported; + usb_apps_enabled = apps.usb_enabled; + } + Self { root, vendor_select, @@ -210,6 +237,11 @@ impl ConfigView { enable_secp256k1: config.map(|c| c.enable_secp256k1).unwrap_or(true), loading: false, is_custom_vendor, + led_status_steady, + led_status_colors, + led_status_brightness, + usb_apps_supported, + usb_apps_enabled, _task: None, } } @@ -384,6 +416,7 @@ impl ConfigView { power_cycle_on_reset: None, led_steady: None, enable_secp256k1: None, + led_order: None, }; let vid = self.vid_input.read(cx).text().to_string(); @@ -554,6 +587,19 @@ impl ConfigView { ); }); + if let Some(led) = &device.led_status { + self.led_status_steady = led.steady; + for i in 0..4 { + self.led_status_colors[i] = led.statuses[i].0; + self.led_status_brightness[i] = led.statuses[i].1; + } + } + + if let Some(apps) = &device.management_apps { + self.usb_apps_supported = apps.usb_supported; + self.usb_apps_enabled = apps.usb_enabled; + } + cx.notify(); } @@ -794,6 +840,266 @@ impl ConfigView { .icon(Icon::default().path("icons/settings.svg")) .child(content) } + + /// Renders the RS-Key-specific LED configuration card. + /// + /// This dynamic panel iterates through the device's LED operating statuses (Idle, Processing, + /// Touch, Boot) and provides interactive widgets to customize the active color and brightness + /// level for each. Only displayed when an RS-Key firmware is detected. + fn render_rskey_led_card(&mut self, cx: &mut Context, is_fido: bool) -> impl IntoElement { + let theme = cx.theme(); + let mut rows = v_flex().gap_4(); + + // Steady switch + let steady_listener = cx.listener(|this, checked, _, cx| { + this.led_status_steady = *checked; + cx.notify(); + }); + + rows = rows.child( + gpui_component::h_flex() + .items_center() + .justify_between() + .child( + v_flex().gap_0p5().child("Global Steady Mode").child( + div() + .text_sm() + .text_color(theme.muted_foreground) + .child("Keep status LEDs on constantly"), + ), + ) + .child( + Switch::new("rskey-led-steady") + .checked(self.led_status_steady) + .disabled(is_fido) + .on_click(steady_listener), + ), + ); + + rows = rows.child(div().h_px().bg(theme.border)); + + // Create rows for each status + for (i, status) in LedStatus::all().iter().enumerate() { + let color_val = self.led_status_colors[i]; + let brightness_val = self.led_status_brightness[i]; + + // A dropdown for color, and a simple + / - or slider for brightness. + // But we don't have a simple dropdown component that works inline easily without a state entity per row. + // Let's just use a simple label and two buttons to cycle color, or we can instantiate 4 SelectStates? + // Since we need SelectState for dropdowns, we can't easily spawn them dynamically in render without keeping them in the struct. + // A simpler approach for the UI: Just show the current color text and + / - buttons to cycle it, and + / - for brightness. + // This avoids adding 4 SelectStates and 4 SliderStates to ConfigView. + let c_i = i; + let cycle_color_listener = cx.listener(move |this, _, _, cx| { + let mut c = this.led_status_colors[c_i]; + c = (c + 1) % 8; + this.led_status_colors[c_i] = c; + cx.notify(); + }); + + let dec_bright_listener = cx.listener(move |this, _, _, cx| { + let mut b = this.led_status_brightness[c_i]; + if b > 0 { b -= 1; } + this.led_status_brightness[c_i] = b; + cx.notify(); + }); + + let inc_bright_listener = cx.listener(move |this, _, _, cx| { + let mut b = this.led_status_brightness[c_i]; + if b < 15 { b += 1; } + this.led_status_brightness[c_i] = b; + cx.notify(); + }); + + let color_name = LedColor::from_u8(color_val).map(|c| c.label()).unwrap_or("Unknown"); + + rows = rows.child( + gpui_component::h_flex() + .items_center() + .justify_between() + .child(div().w_24().child(status.label())) + .child( + gpui_component::h_flex().gap_2().items_center() + .child( + Button::new(gpui::SharedString::from(format!("color-btn-{}", i))) + .child(color_name) + .disabled(is_fido) + .on_click(cycle_color_listener) + ) + .child(div().w_4()) + .child( + Button::new(gpui::SharedString::from(format!("bdec-btn-{}", i))) + .child("-") + .disabled(is_fido || brightness_val == 0) + .on_click(dec_bright_listener) + ) + .child(div().w_8().flex().justify_center().child(brightness_val.to_string())) + .child( + Button::new(gpui::SharedString::from(format!("binc-btn-{}", i))) + .child("+") + .disabled(is_fido || brightness_val == 15) + .on_click(inc_bright_listener) + ) + ) + ); + } + + // Add a save button for LED status + rows = rows.child(div().h_px().bg(theme.border)); + rows = rows.child( + gpui_component::h_flex().justify_end().child( + Button::new("apply-rskey-leds") + .child("Save LED Status") + .disabled(is_fido || self.loading) + .on_click(cx.listener(|this, _, window, cx| { + this.apply_rskey_led_settings(window, cx); + })) + ) + ); + + Card::new() + .title("Status LED Colors") + .description("Configure LED colors and brightness per device state") + .icon(Icon::default().path("icons/palette.svg")) + .child(rows) + } + + fn apply_rskey_led_settings(&mut self, window: &mut Window, cx: &mut Context) { + let steady = self.led_status_steady; + let colors = self.led_status_colors; + let brightnesses = self.led_status_brightness; + + self.loading = true; + let handle = dialog::open_status_dialog("Applying LED Configuration...", window, cx); + let entity = cx.entity().downgrade(); + + self._task = Some(cx.spawn(async move |_, cx| { + let result = cx.background_executor().spawn(async move { + for i in 0..4 { + io::write_led_status(i as u8, colors[i], brightnesses[i], steady)?; + } + Ok::<_, crate::error::PFError>(()) + }).await; + + let _ = entity.update(cx, |this, cx| { + this.loading = false; + match result { + Ok(_) => { + let _ = handle.update(cx, |d, cx| { + d.set_success("LED configuration applied successfully.".to_string(), cx); + }); + } + Err(e) => { + let _ = handle.update(cx, |d, cx| { + d.set_error(format!("Failed to apply LED config: {}", e), cx); + }); + } + } + cx.notify(); + }); + })); + } + + /// Renders the RS-Key-specific USB Applications management card. + /// + /// Provides toggles to enable or disable USB endpoints such as U2F, OATH, PIV, and OpenPGP. + /// Safely computes the bitmasks and writes to the Management applet. Gated by hardware support. + fn render_rskey_apps_card(&mut self, cx: &mut Context, is_fido: bool) -> impl IntoElement { + let theme = cx.theme(); + let mut rows = v_flex().gap_4(); + + let apps = [ + ("FIDO2", USB_CAP_FIDO2), + ("OATH", USB_CAP_OATH), + ("PIV", USB_CAP_PIV), + ("OpenPGP", USB_CAP_OPENPGP), + ("U2F", USB_CAP_U2F), + ("OTP", USB_CAP_OTP), + ]; + + for (name, cap) in apps { + let is_supported = (self.usb_apps_supported & cap) != 0; + let is_enabled = (self.usb_apps_enabled & cap) != 0; + + let toggle_listener = cx.listener(move |this, checked, _, cx| { + if *checked { + this.usb_apps_enabled |= cap; + } else { + this.usb_apps_enabled &= !cap; + } + cx.notify(); + }); + + rows = rows.child( + gpui_component::h_flex() + .items_center() + .justify_between() + .child( + v_flex().gap_0p5().child(name).child( + div() + .text_sm() + .text_color(theme.muted_foreground) + .child(if is_supported { "Supported" } else { "Not Supported by Firmware" }), + ), + ) + .child( + Switch::new(gpui::SharedString::from(format!("app-toggle-{}", cap))) + .checked(is_enabled) + .disabled(is_fido || !is_supported) + .on_click(toggle_listener), + ), + ); + } + + rows = rows.child(div().h_px().bg(theme.border)); + rows = rows.child( + gpui_component::h_flex().justify_end().child( + Button::new("apply-rskey-apps") + .child("Save USB Applications") + .disabled(is_fido || self.loading) + .on_click(cx.listener(|this, _, window, cx| { + this.apply_rskey_apps_settings(window, cx); + })) + ) + ); + + Card::new() + .title("USB Applications") + .description("Enable or disable specific USB features") + .icon(Icon::default().path("icons/microchip.svg")) + .child(rows) + } + + fn apply_rskey_apps_settings(&mut self, window: &mut Window, cx: &mut Context) { + let mask = self.usb_apps_enabled; + + self.loading = true; + let handle = dialog::open_status_dialog("Applying USB Applications...", window, cx); + let entity = cx.entity().downgrade(); + + self._task = Some(cx.spawn(async move |_, cx| { + let result = cx.background_executor().spawn(async move { + io::write_management_config(mask) + }).await; + + let _ = entity.update(cx, |this, cx| { + this.loading = false; + match result { + Ok(_) => { + let _ = handle.update(cx, |d, cx| { + d.set_success("USB applications updated successfully. Please re-plug the device.".to_string(), cx); + }); + } + Err(e) => { + let _ = handle.update(cx, |d, cx| { + d.set_error(format!("Failed to apply USB applications: {}", e), cx); + }); + } + } + cx.notify(); + }); + })); + } } impl Render for ConfigView { @@ -845,16 +1151,32 @@ impl Render for ConfigView { .render_options_card(cx, is_fido, hardware_config_disabled) .into_any_element(); - let theme = cx.theme(); - let identity_card = self - .render_identity_card(theme, is_fido, hardware_config_disabled) + .render_identity_card(cx.theme(), is_fido, hardware_config_disabled) .into_any_element(); - let touch_card = self.render_touch_card(theme, is_fido).into_any_element(); + let touch_card = self.render_touch_card(cx.theme(), is_fido).into_any_element(); let is_wide = window.bounds().size.width > px(1100.0); let columns = if is_wide { 2 } else { 1 }; + let is_rskey = status.as_ref().map(|s| &s.firmware_type) == Some(&crate::device::types::FirmwareType::RSKey); + + let mut grid_children = vec![ + identity_card, + led_card, + touch_card, + options_card, + ]; + + if is_rskey { + let rskey_led = self.render_rskey_led_card(cx, is_fido).into_any_element(); + let rskey_apps = self.render_rskey_apps_card(cx, is_fido).into_any_element(); + grid_children.push(rskey_led); + grid_children.push(rskey_apps); + } + + let theme = cx.theme(); + PageView::build( "Configuration", "Customize device settings and behavior.", @@ -865,10 +1187,7 @@ impl Render for ConfigView { .grid() .grid_cols(columns) .gap_6() - .child(identity_card) - .child(led_card) - .child(touch_card) - .child(options_card), + .children(grid_children), ) .child( gpui_component::h_flex().justify_end().pt_4().child( diff --git a/src/ui/views/home.rs b/src/ui/views/home.rs index 89843b3..9116835 100644 --- a/src/ui/views/home.rs +++ b/src/ui/views/home.rs @@ -110,6 +110,12 @@ impl HomeView { theme, true, )) + .child(Self::render_kv( + "Firmware Type", + status.firmware_type.to_string(), + theme, + false, + )) .child(Self::render_kv( "VID:PID", format!("{}:{}", config.vid, config.pid), diff --git a/src/ui/views/passkeys.rs b/src/ui/views/passkeys.rs index de9b586..9f0942e 100644 --- a/src/ui/views/passkeys.rs +++ b/src/ui/views/passkeys.rs @@ -13,7 +13,7 @@ use directories::UserDirs; use gpui::prelude::FluentBuilder; use gpui::*; use gpui_component::Disableable; -use gpui_component::button::{Button, ButtonVariant, ButtonVariants}; +use gpui_component::button::{Button, ButtonVariant, ButtonVariants, ButtonCustomVariant}; use gpui_component::{ ActiveTheme, Icon, Placement, Sizable, StyledExt, Theme, WindowExt, badge::Badge, @@ -223,7 +223,7 @@ impl PasskeysView { ButtonVariant::Danger, window, cx, - move |dialog_handle, cx| { + move |dialog_handle, _, cx| { let _ = view_handle.update(cx, |this, cx| { this.execute_delete(cred_id.clone(), pin_str.clone(), dialog_handle, cx); }); @@ -986,9 +986,9 @@ impl PasskeysView { ); Card::new() - .title("Enterprise Attestation Certificate") - .icon(Icon::default().path("icons/scroll-text.svg")) - .description("Manage the device enterprise attestation") + .title("Enterprise Attestation") + .description("Configure enterprise-specific features") + .icon(Icon::default().path("icons/shield-check.svg")) .child( v_flex() .gap_3() @@ -998,6 +998,55 @@ impl PasskeysView { ) } + fn render_reset_device_row(&self, cx: &mut Context) -> impl IntoElement { + let theme = cx.theme(); + + let header = gpui_component::h_flex() + .items_center() + .justify_between() + .w_full() + .gap_4() + .child( + v_flex() + .gap_1() + .child( + div() + .text_base() + .font_weight(gpui::FontWeight::MEDIUM) + .text_color(theme.foreground) + .child("Factory Reset"), + ) + .child( + div() + .text_sm() + .text_color(theme.muted_foreground) + .child("Erase all passkeys, credentials, and PIN. Cannot be undone."), + ), + ) + .child( + Button::new("reset-device") + .icon(Icon::default().path("icons/circle-alert.svg")) + .child("Reset Device") + .custom( + ButtonCustomVariant::new(cx) + .color(theme.danger.into()) + .hover(theme.danger_hover.into()) + .active(theme.danger_active.into()) + .foreground(theme.danger_foreground.into()), + ) + .disabled(self.loading) + .on_click(cx.listener(|this, _, window, cx| { + this.open_reset_dialog(window, cx); + })), + ); + + Card::new() + .title("Reset") + .description("Perform a destructive factory reset") + .icon(Icon::default().path("icons/trash.svg")) + .child(header) + } + fn render_no_device(&self, theme: &Theme) -> impl IntoElement { div() .flex() @@ -1032,6 +1081,116 @@ impl PasskeysView { .into_any_element() } + /// Triggers the confirmation flow for a hardware factory reset. + /// + /// Warns the user of the destructive nature of this action (all credentials, passkeys, + /// and PINs will be irrecoverably erased) via a GPUI modal dialog. If confirmed, + /// it transitions to `execute_reset` to begin the 10-second touch confirmation window. + fn open_reset_dialog(&mut self, window: &mut Window, cx: &mut Context) { + let view_handle = cx.entity().downgrade(); + + dialog::open_confirm( + "Factory Reset Device", + "Are you sure you want to completely erase your device? This will permanently delete ALL passkeys, credentials, and your PIN. This action cannot be undone.".to_string(), + "Reset Device", + ButtonVariant::Danger, + window, + cx, + move |_dialog_handle, window, cx| { + // Close the confirm dialog before opening the status dialog + window.close_dialog(cx); + // When they click confirm, we swap to a status dialog for the reconnect wizard + let _ = view_handle.update(cx, |this, cx| { + this.execute_reset(window, cx); + }); + }, + ); + } + + /// Orchestrates the underlying FIDO factory reset protocol asynchronously. + /// + /// Changes the UI to a loading/status phase instructing the user to unplug, replug, + /// and touch the key within 10 seconds. Monitors the reset task and propagates any + /// success or error state back to the UI thread upon completion. + fn execute_reset(&mut self, window: &mut Window, cx: &mut Context) { + if self.loading { + return; + } + self.loading = true; + + let status_handle = dialog::open_status_dialog("Resetting Device...", window, cx); + let entity = cx.entity().downgrade(); + + let _ = status_handle.update(cx, |d, cx| { + d.set_loading("Unplug your security key, then plug it back in within 10 seconds.", cx); + }); + + self._task = Some(cx.spawn(async move |_, cx| { + // Wait for unplug/replug + let reconnected = cx.background_executor().spawn(async move { + let start = std::time::Instant::now(); + // 1. Wait for unplug + while start.elapsed().as_secs() < 15 { + std::thread::sleep(std::time::Duration::from_millis(200)); + if crate::device::fido::hid::HidTransport::open().is_err() { + break; + } + } + + // 2. Wait for replug + while start.elapsed().as_secs() < 15 { + std::thread::sleep(std::time::Duration::from_millis(500)); + if crate::device::fido::hid::HidTransport::open().is_ok() { + return true; + } + } + false + }).await; + + if !reconnected { + let _ = entity.update(cx, |this, cx| { + this.loading = false; + let _ = status_handle.update(cx, |d, cx| { + d.set_error("Timeout waiting for device reconnection. Reset canceled.".to_string(), cx); + }); + cx.notify(); + }); + return; + } + + // Tell user to touch + let _ = status_handle.update(cx, |d, cx| { + d.set_loading("Touch your security key now to confirm the reset...", cx); + }); + + // Execute reset + let result = cx.background_executor().spawn(async move { + io::reset_device() + }).await; + + let _ = entity.update(cx, |this, cx| { + this.loading = false; + match result { + Ok(msg) => { + log::info!("Device Reset: {}", msg); + this.lock_storage(cx); // clear cached pin/creds + let _ = status_handle.update(cx, |d, cx| { + d.set_success(msg, cx); + }); + cx.emit(PasskeysEvent::Notification("Device reset successfully".into())); + } + Err(e) => { + log::error!("Error resetting device: {}", e); + let _ = status_handle.update(cx, |d, cx| { + d.set_error(format!("Reset failed: {}", e), cx); + }); + } + } + cx.notify(); + }); + })); + } + fn render_pin_management(&self, cx: &mut Context) -> impl IntoElement { let status_row = self.render_pin_status_row(cx).into_any_element(); let min_len_row = self.render_min_pin_length_row(cx).into_any_element(); @@ -1595,7 +1754,8 @@ impl Render for PasskeysView { .gap_6() .child(self.render_pin_management(cx)) .child(self.render_stored_passkeys(cx)) - .child(self.render_enterprise_attestation(cx)); + .child(self.render_enterprise_attestation(cx)) + .child(self.render_reset_device_row(cx)); let theme = cx.theme(); diff --git a/static/icons/circle-alert.svg b/static/icons/circle-alert.svg new file mode 100644 index 0000000..b3f62b9 --- /dev/null +++ b/static/icons/circle-alert.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/icons/trash.svg b/static/icons/trash.svg new file mode 100644 index 0000000..8a6d8cf --- /dev/null +++ b/static/icons/trash.svg @@ -0,0 +1 @@ + \ No newline at end of file From c27ff57d781d6e854888e5596f6d07d76492526b Mon Sep 17 00:00:00 2001 From: Suyog Tandel Date: Mon, 22 Jun 2026 21:57:53 +0530 Subject: [PATCH 02/11] fix: config errors in config view --- src/ui/views/config.rs | 119 ++++++++++++++++++++++++++--------------- 1 file changed, 75 insertions(+), 44 deletions(-) diff --git a/src/ui/views/config.rs b/src/ui/views/config.rs index 8316f50..2e95125 100644 --- a/src/ui/views/config.rs +++ b/src/ui/views/config.rs @@ -404,43 +404,33 @@ impl ConfigView { let Some(status) = &device.status else { return }; let current_config = &status.config; - let mut changes = AppConfigInput { - vid: None, - pid: None, - product_name: None, - led_gpio: None, - led_brightness: None, - touch_timeout: None, - led_driver: None, - led_dimmable: None, - power_cycle_on_reset: None, - led_steady: None, - enable_secp256k1: None, - led_order: None, - }; + let mut has_changes = false; let vid = self.vid_input.read(cx).text().to_string(); if vid != current_config.vid { - changes.vid = Some(vid); + has_changes = true; } let pid = self.pid_input.read(cx).text().to_string(); if pid != current_config.pid { - changes.pid = Some(pid); + has_changes = true; } let product_name = self.product_name_input.read(cx).text().to_string(); if product_name != current_config.product_name { - changes.product_name = Some(product_name); + has_changes = true; } + let mut final_led_gpio = current_config.led_gpio; let led_gpio_str = self.led_gpio_input.read(cx).text().to_string(); - if let Ok(val) = led_gpio_str.parse::() - && val != current_config.led_gpio - { - changes.led_gpio = Some(val); + if let Ok(val) = led_gpio_str.parse::() { + if val != current_config.led_gpio { + has_changes = true; + } + final_led_gpio = val; } + let mut final_led_driver = current_config.led_driver; let driver_idx = self.led_driver_select.read(cx).selected_index(cx); if let Some(idx) = driver_idx && let Some(driver) = LedDriverType::all().get(idx.row) @@ -448,52 +438,58 @@ impl ConfigView { let val = driver.value(); let current_val = current_config.led_driver.unwrap_or(1); if val != current_val { - changes.led_driver = Some(val); + has_changes = true; } + final_led_driver = Some(val); } let brightness = self.led_brightness_slider.read(cx).value().start() as u8; if brightness != current_config.led_brightness { - changes.led_brightness = Some(brightness); + has_changes = true; } + let mut final_touch_timeout = current_config.touch_timeout; let touch_timeout_str = self.touch_timeout_input.read(cx).text().to_string(); - if let Ok(val) = touch_timeout_str.parse::() - && val != current_config.touch_timeout - { - changes.touch_timeout = Some(val); + if let Ok(val) = touch_timeout_str.parse::() { + if val != current_config.touch_timeout { + has_changes = true; + } + final_touch_timeout = val; } if (self.led_dimmable != current_config.led_dimmable) || (self.led_steady != current_config.led_steady) || (self.power_cycle != current_config.power_cycle_on_reset) { - changes.led_dimmable = Some(self.led_dimmable); - changes.led_steady = Some(self.led_steady); - changes.power_cycle_on_reset = Some(self.power_cycle); + has_changes = true; } if self.enable_secp256k1 != current_config.enable_secp256k1 { - changes.enable_secp256k1 = Some(self.enable_secp256k1); + has_changes = true; } - let has_changes = changes.vid.is_some() - || changes.pid.is_some() - || changes.product_name.is_some() - || changes.led_gpio.is_some() - || changes.led_brightness.is_some() - || changes.touch_timeout.is_some() - || changes.led_driver.is_some() - || changes.led_dimmable.is_some() - || changes.power_cycle_on_reset.is_some() - || changes.led_steady.is_some() - || changes.enable_secp256k1.is_some(); - if !has_changes { log::info!("No changes detected"); return; } + let changes = AppConfigInput { + vid: Some(vid), + pid: Some(pid), + product_name: Some(product_name), + led_gpio: Some(final_led_gpio), + led_brightness: Some(brightness), + touch_timeout: Some(final_touch_timeout), + led_driver: final_led_driver, + led_dimmable: Some(self.led_dimmable), + power_cycle_on_reset: Some(self.power_cycle), + led_steady: Some(self.led_steady), + enable_secp256k1: Some(self.enable_secp256k1), + led_order: current_config.led_order, + }; + + + let method = status.method.clone(); if method == DeviceMethod::Fido { @@ -923,6 +919,13 @@ impl ConfigView { .child( Button::new(gpui::SharedString::from(format!("color-btn-{}", i))) .child(color_name) + .custom( + ButtonCustomVariant::new(cx) + .color(rgb(0x27272a).into()) + .hover(rgb(0x3f3f46).into()) + .active(rgb(0x52525b).into()) + .border(theme.border), + ) .disabled(is_fido) .on_click(cycle_color_listener) ) @@ -930,6 +933,13 @@ impl ConfigView { .child( Button::new(gpui::SharedString::from(format!("bdec-btn-{}", i))) .child("-") + .custom( + ButtonCustomVariant::new(cx) + .color(rgb(0x1b1b1d).into()) + .hover(rgb(0x232325).into()) + .active(rgb(0x3f3f46).into()) + .border(theme.border), + ) .disabled(is_fido || brightness_val == 0) .on_click(dec_bright_listener) ) @@ -937,7 +947,14 @@ impl ConfigView { .child( Button::new(gpui::SharedString::from(format!("binc-btn-{}", i))) .child("+") - .disabled(is_fido || brightness_val == 15) + .custom( + ButtonCustomVariant::new(cx) + .color(rgb(0x1b1b1d).into()) + .hover(rgb(0x232325).into()) + .active(rgb(0x3f3f46).into()) + .border(theme.border), + ) + .disabled(is_fido || brightness_val >= 15) .on_click(inc_bright_listener) ) ) @@ -950,6 +967,13 @@ impl ConfigView { gpui_component::h_flex().justify_end().child( Button::new("apply-rskey-leds") .child("Save LED Status") + .custom( + ButtonCustomVariant::new(cx) + .color(rgb(0xe3e3e6).into()) + .hover(rgb(0xcfcfd1).into()) + .active(rgb(0xe3e3e6).into()) + .foreground(rgb(0x4b4b4e).into()), + ) .disabled(is_fido || self.loading) .on_click(cx.listener(|this, _, window, cx| { this.apply_rskey_led_settings(window, cx); @@ -1056,6 +1080,13 @@ impl ConfigView { gpui_component::h_flex().justify_end().child( Button::new("apply-rskey-apps") .child("Save USB Applications") + .custom( + ButtonCustomVariant::new(cx) + .color(rgb(0xe3e3e6).into()) + .hover(rgb(0xcfcfd1).into()) + .active(rgb(0xe3e3e6).into()) + .foreground(rgb(0x4b4b4e).into()), + ) .disabled(is_fido || self.loading) .on_click(cx.listener(|this, _, window, cx| { this.apply_rskey_apps_settings(window, cx); From 796c14462d3e2441c1dc0ce593b4486c18520b96 Mon Sep 17 00:00:00 2001 From: Suyog Tandel Date: Mon, 22 Jun 2026 22:05:07 +0530 Subject: [PATCH 03/11] chore: format code using cargo fmt --- src/device/fido/hid.rs | 7 +- src/device/fido/mod.rs | 4 +- src/device/io.rs | 7 +- src/device/rescue/constants.rs | 10 ++- src/device/rescue/mod.rs | 18 ++-- src/ui/components/dialog.rs | 2 +- src/ui/components/sidebar.rs | 39 +++++---- src/ui/rootview.rs | 4 +- src/ui/views/config.rs | 146 ++++++++++++++++++++------------- src/ui/views/passkeys.rs | 62 ++++++++------ 10 files changed, 187 insertions(+), 112 deletions(-) diff --git a/src/device/fido/hid.rs b/src/device/fido/hid.rs index 8b6a225..1ba78db 100644 --- a/src/device/fido/hid.rs +++ b/src/device/fido/hid.rs @@ -167,7 +167,12 @@ impl HidTransport { self.send_cbor_with_timeout(cmd, payload, HID_TOTAL_TIMEOUT_MS) } - pub fn send_cbor_with_timeout(&self, cmd: u8, payload: &[u8], timeout_ms: i32) -> Result, PFError> { + pub fn send_cbor_with_timeout( + &self, + cmd: u8, + payload: &[u8], + timeout_ms: i32, + ) -> Result, PFError> { self.write_cbor_request(cmd, payload)?; self.read_cbor_response(cmd, timeout_ms) } diff --git a/src/device/fido/mod.rs b/src/device/fido/mod.rs index d6d11cc..d266815 100644 --- a/src/device/fido/mod.rs +++ b/src/device/fido/mod.rs @@ -3,8 +3,8 @@ pub mod hid; use crate::{ device::types::{ - AppConfig, AppConfigInput, DeviceInfo, DeviceMethod, FidoDeviceInfo, FullDeviceStatus, - StoredCredential, RSKEY_AAGUID, PICOFIDO_AAGUID, FirmwareType, + AppConfig, AppConfigInput, DeviceInfo, DeviceMethod, FidoDeviceInfo, FirmwareType, + FullDeviceStatus, PICOFIDO_AAGUID, RSKEY_AAGUID, StoredCredential, }, error::PFError, }; diff --git a/src/device/io.rs b/src/device/io.rs index 62dc9f3..c52b166 100644 --- a/src/device/io.rs +++ b/src/device/io.rs @@ -67,7 +67,12 @@ pub fn read_led_config() -> Result { rescue::read_led_config() } -pub fn write_led_status(status: u8, color: u8, brightness: u8, steady: bool) -> Result { +pub fn write_led_status( + status: u8, + color: u8, + brightness: u8, + steady: bool, +) -> Result { rescue::write_led_status(status, color, brightness, steady) } diff --git a/src/device/rescue/constants.rs b/src/device/rescue/constants.rs index 8ff25dc..70fb9ed 100644 --- a/src/device/rescue/constants.rs +++ b/src/device/rescue/constants.rs @@ -186,8 +186,14 @@ impl LedColor { pub fn all() -> &'static [Self] { &[ - Self::Off, Self::Red, Self::Green, Self::Blue, - Self::Yellow, Self::Magenta, Self::Cyan, Self::White, + Self::Off, + Self::Red, + Self::Green, + Self::Blue, + Self::Yellow, + Self::Magenta, + Self::Cyan, + Self::White, ] } } diff --git a/src/device/rescue/mod.rs b/src/device/rescue/mod.rs index 70f6d55..5125b63 100644 --- a/src/device/rescue/mod.rs +++ b/src/device/rescue/mod.rs @@ -14,7 +14,7 @@ use std::io::Cursor; /// /// **WARNING:** This is a temporary heuristic that relies on the major version byte (`major >= 8` implies RS-Key). /// If Pico-Fido releases v8.x, this logic will silently fail and misidentify devices. -/// +/// /// TODO: Work with upstream RS-Key maintainers to expose a unique identity block or hardware string /// in the SELECT response to reliably differentiate the firmwares in the long term. fn detect_firmware_type(select_resp: &[u8]) -> FirmwareType { @@ -486,9 +486,10 @@ fn connect_and_select_aid(aid: &[u8]) -> Result { let rx = card.transmit(&apdu, &mut rx_buf)?; if !rx.ends_with(&[0x90, 0x00]) { - return Err(PFError::Device( - format!("Applet not found (AID {:02X?})", aid), - )); + return Err(PFError::Device(format!( + "Applet not found (AID {:02X?})", + aid + ))); } Ok(card) @@ -545,7 +546,10 @@ pub fn write_led_status( ) -> Result { log::info!( "Setting LED: status={}, color={}, brightness={}, steady={}", - status, color, brightness, steady + status, + color, + brightness, + steady ); let card = connect_and_select_aid(VENDOR_LED_AID)?; @@ -572,7 +576,7 @@ pub fn write_led_status( /// Retrieves the device management configuration mapping from the Management applet. /// -/// Reads the active state of various USB interfaces (U2F, OATH, PIV, OpenPGP, etc.) to +/// Reads the active state of various USB interfaces (U2F, OATH, PIV, OpenPGP, etc.) to /// determine which are supported by the hardware and which are currently enabled by the user. pub fn read_management_config() -> Result { log::info!("Reading management config from Management applet"); @@ -646,7 +650,7 @@ pub fn read_management_config() -> Result { /// Persists updated management endpoint configurations to the device. /// /// Overwrites the previously enabled interfaces with a new configuration bitmask. -/// For the changes to fully apply across all composite USB endpoints, a subsequent +/// For the changes to fully apply across all composite USB endpoints, a subsequent /// device reboot or re-plug is required. pub fn write_management_config(enabled_mask: u16) -> Result { log::info!("Writing management config: enabled=0x{:04X}", enabled_mask); diff --git a/src/ui/components/dialog.rs b/src/ui/components/dialog.rs index d8c9844..a0ad1f0 100644 --- a/src/ui/components/dialog.rs +++ b/src/ui/components/dialog.rs @@ -17,7 +17,7 @@ type SetPinCallback = std::rc::Rc, &mut enum DialogPhase { Input, Loading, - /// Indicates the dialog is blocked on an asynchronous background task, + /// Indicates the dialog is blocked on an asynchronous background task, /// presenting a specific dynamic status message to guide the user (e.g. "Waiting for touch..."). LoadingWithMessage(String), Success(String), diff --git a/src/ui/components/sidebar.rs b/src/ui/components/sidebar.rs index 9136bd5..db84745 100644 --- a/src/ui/components/sidebar.rs +++ b/src/ui/components/sidebar.rs @@ -219,21 +219,32 @@ impl AppSidebar { .child("Device Status"), ) .child({ - let (text, color_bg, color_text) = - if let Some(status) = &state.status { - let is_rskey = status.firmware_type == crate::device::types::FirmwareType::RSKey; - let fw_label = if is_rskey { "RS-Key" } else { "Pico-FIDO" }; - - if status.method == DeviceMethod::Fido { - (format!("Online - FIDO ({})", fw_label), rgb(0xf59e0b), rgb(0xffffff)) - } else { - (format!("Online - {}", fw_label), rgb(0x16a34a), rgb(0xffffff)) - } - } else if state.error.is_some() { - ("Error".to_string(), rgb(0xd97706), rgb(0xffffff)) + let (text, color_bg, color_text) = if let Some(status) = + &state.status + { + let is_rskey = status.firmware_type + == crate::device::types::FirmwareType::RSKey; + let fw_label = + if is_rskey { "RS-Key" } else { "Pico-FIDO" }; + + if status.method == DeviceMethod::Fido { + ( + format!("Online - FIDO ({})", fw_label), + rgb(0xf59e0b), + rgb(0xffffff), + ) } else { - ("Offline".to_string(), rgb(0xef4444), rgb(0xffffff)) - }; + ( + format!("Online - {}", fw_label), + rgb(0x16a34a), + rgb(0xffffff), + ) + } + } else if state.error.is_some() { + ("Error".to_string(), rgb(0xd97706), rgb(0xffffff)) + } else { + ("Offline".to_string(), rgb(0xef4444), rgb(0xffffff)) + }; div() .px(px(6.)) diff --git a/src/ui/rootview.rs b/src/ui/rootview.rs index 3a5677c..dd182f3 100644 --- a/src/ui/rootview.rs +++ b/src/ui/rootview.rs @@ -72,7 +72,9 @@ impl ApplicationRoot { } } - if status.firmware_type == crate::device::types::FirmwareType::RSKey && status.method == crate::device::types::DeviceMethod::Rescue { + if status.firmware_type == crate::device::types::FirmwareType::RSKey + && status.method == crate::device::types::DeviceMethod::Rescue + { self.device.led_status = io::read_led_config().ok(); self.device.management_apps = io::read_management_config().ok(); } else { diff --git a/src/ui/views/config.rs b/src/ui/views/config.rs index 2e95125..26278ea 100644 --- a/src/ui/views/config.rs +++ b/src/ui/views/config.rs @@ -1,6 +1,9 @@ +use crate::device::rescue::constants::{ + LedColor, LedStatus, USB_CAP_FIDO2, USB_CAP_OATH, USB_CAP_OPENPGP, USB_CAP_OTP, USB_CAP_PIV, + USB_CAP_U2F, +}; use crate::device::types::{AppConfigInput, DeviceMethod}; use crate::device::{fido, io}; -use crate::device::rescue::constants::{LedColor, LedStatus, USB_CAP_OTP, USB_CAP_U2F, USB_CAP_OPENPGP, USB_CAP_PIV, USB_CAP_OATH, USB_CAP_FIDO2}; use crate::ui::components::dialog::PinPromptContent; use crate::ui::components::{card::Card, dialog, dialog::StatusContent, page_view::PageView}; use crate::ui::rootview::ApplicationRoot; @@ -74,7 +77,7 @@ pub struct ConfigView { enable_secp256k1: bool, loading: bool, is_custom_vendor: bool, - + // RS-Key specific state led_status_steady: bool, led_status_colors: [u8; 4], @@ -488,8 +491,6 @@ impl ConfigView { led_order: current_config.led_order, }; - - let method = status.method.clone(); if method == DeviceMethod::Fido { @@ -895,19 +896,25 @@ impl ConfigView { let dec_bright_listener = cx.listener(move |this, _, _, cx| { let mut b = this.led_status_brightness[c_i]; - if b > 0 { b -= 1; } + if b > 0 { + b -= 1; + } this.led_status_brightness[c_i] = b; cx.notify(); }); let inc_bright_listener = cx.listener(move |this, _, _, cx| { let mut b = this.led_status_brightness[c_i]; - if b < 15 { b += 1; } + if b < 15 { + b += 1; + } this.led_status_brightness[c_i] = b; cx.notify(); }); - let color_name = LedColor::from_u8(color_val).map(|c| c.label()).unwrap_or("Unknown"); + let color_name = LedColor::from_u8(color_val) + .map(|c| c.label()) + .unwrap_or("Unknown"); rows = rows.child( gpui_component::h_flex() @@ -915,7 +922,9 @@ impl ConfigView { .justify_between() .child(div().w_24().child(status.label())) .child( - gpui_component::h_flex().gap_2().items_center() + gpui_component::h_flex() + .gap_2() + .items_center() .child( Button::new(gpui::SharedString::from(format!("color-btn-{}", i))) .child(color_name) @@ -927,7 +936,7 @@ impl ConfigView { .border(theme.border), ) .disabled(is_fido) - .on_click(cycle_color_listener) + .on_click(cycle_color_listener), ) .child(div().w_4()) .child( @@ -941,9 +950,15 @@ impl ConfigView { .border(theme.border), ) .disabled(is_fido || brightness_val == 0) - .on_click(dec_bright_listener) + .on_click(dec_bright_listener), + ) + .child( + div() + .w_8() + .flex() + .justify_center() + .child(brightness_val.to_string()), ) - .child(div().w_8().flex().justify_center().child(brightness_val.to_string())) .child( Button::new(gpui::SharedString::from(format!("binc-btn-{}", i))) .child("+") @@ -955,9 +970,9 @@ impl ConfigView { .border(theme.border), ) .disabled(is_fido || brightness_val >= 15) - .on_click(inc_bright_listener) - ) - ) + .on_click(inc_bright_listener), + ), + ), ); } @@ -977,8 +992,8 @@ impl ConfigView { .disabled(is_fido || self.loading) .on_click(cx.listener(|this, _, window, cx| { this.apply_rskey_led_settings(window, cx); - })) - ) + })), + ), ); Card::new() @@ -998,19 +1013,25 @@ impl ConfigView { let entity = cx.entity().downgrade(); self._task = Some(cx.spawn(async move |_, cx| { - let result = cx.background_executor().spawn(async move { - for i in 0..4 { - io::write_led_status(i as u8, colors[i], brightnesses[i], steady)?; - } - Ok::<_, crate::error::PFError>(()) - }).await; + let result = cx + .background_executor() + .spawn(async move { + for i in 0..4 { + io::write_led_status(i as u8, colors[i], brightnesses[i], steady)?; + } + Ok::<_, crate::error::PFError>(()) + }) + .await; let _ = entity.update(cx, |this, cx| { this.loading = false; match result { Ok(_) => { let _ = handle.update(cx, |d, cx| { - d.set_success("LED configuration applied successfully.".to_string(), cx); + d.set_success( + "LED configuration applied successfully.".to_string(), + cx, + ); }); } Err(e) => { @@ -1028,7 +1049,11 @@ impl ConfigView { /// /// Provides toggles to enable or disable USB endpoints such as U2F, OATH, PIV, and OpenPGP. /// Safely computes the bitmasks and writes to the Management applet. Gated by hardware support. - fn render_rskey_apps_card(&mut self, cx: &mut Context, is_fido: bool) -> impl IntoElement { + fn render_rskey_apps_card( + &mut self, + cx: &mut Context, + is_fido: bool, + ) -> impl IntoElement { let theme = cx.theme(); let mut rows = v_flex().gap_4(); @@ -1054,25 +1079,27 @@ impl ConfigView { cx.notify(); }); - rows = rows.child( - gpui_component::h_flex() - .items_center() - .justify_between() - .child( - v_flex().gap_0p5().child(name).child( - div() - .text_sm() - .text_color(theme.muted_foreground) - .child(if is_supported { "Supported" } else { "Not Supported by Firmware" }), + rows = + rows.child( + gpui_component::h_flex() + .items_center() + .justify_between() + .child(v_flex().gap_0p5().child(name).child( + div().text_sm().text_color(theme.muted_foreground).child( + if is_supported { + "Supported" + } else { + "Not Supported by Firmware" + }, + ), + )) + .child( + Switch::new(gpui::SharedString::from(format!("app-toggle-{}", cap))) + .checked(is_enabled) + .disabled(is_fido || !is_supported) + .on_click(toggle_listener), ), - ) - .child( - Switch::new(gpui::SharedString::from(format!("app-toggle-{}", cap))) - .checked(is_enabled) - .disabled(is_fido || !is_supported) - .on_click(toggle_listener), - ), - ); + ); } rows = rows.child(div().h_px().bg(theme.border)); @@ -1090,8 +1117,8 @@ impl ConfigView { .disabled(is_fido || self.loading) .on_click(cx.listener(|this, _, window, cx| { this.apply_rskey_apps_settings(window, cx); - })) - ) + })), + ), ); Card::new() @@ -1103,22 +1130,27 @@ impl ConfigView { fn apply_rskey_apps_settings(&mut self, window: &mut Window, cx: &mut Context) { let mask = self.usb_apps_enabled; - + self.loading = true; let handle = dialog::open_status_dialog("Applying USB Applications...", window, cx); let entity = cx.entity().downgrade(); self._task = Some(cx.spawn(async move |_, cx| { - let result = cx.background_executor().spawn(async move { - io::write_management_config(mask) - }).await; + let result = cx + .background_executor() + .spawn(async move { io::write_management_config(mask) }) + .await; let _ = entity.update(cx, |this, cx| { this.loading = false; match result { Ok(_) => { let _ = handle.update(cx, |d, cx| { - d.set_success("USB applications updated successfully. Please re-plug the device.".to_string(), cx); + d.set_success( + "USB applications updated successfully. Please re-plug the device." + .to_string(), + cx, + ); }); } Err(e) => { @@ -1185,19 +1217,17 @@ impl Render for ConfigView { let identity_card = self .render_identity_card(cx.theme(), is_fido, hardware_config_disabled) .into_any_element(); - let touch_card = self.render_touch_card(cx.theme(), is_fido).into_any_element(); + let touch_card = self + .render_touch_card(cx.theme(), is_fido) + .into_any_element(); let is_wide = window.bounds().size.width > px(1100.0); let columns = if is_wide { 2 } else { 1 }; - let is_rskey = status.as_ref().map(|s| &s.firmware_type) == Some(&crate::device::types::FirmwareType::RSKey); + let is_rskey = status.as_ref().map(|s| &s.firmware_type) + == Some(&crate::device::types::FirmwareType::RSKey); - let mut grid_children = vec![ - identity_card, - led_card, - touch_card, - options_card, - ]; + let mut grid_children = vec![identity_card, led_card, touch_card, options_card]; if is_rskey { let rskey_led = self.render_rskey_led_card(cx, is_fido).into_any_element(); @@ -1207,7 +1237,7 @@ impl Render for ConfigView { } let theme = cx.theme(); - + PageView::build( "Configuration", "Customize device settings and behavior.", diff --git a/src/ui/views/passkeys.rs b/src/ui/views/passkeys.rs index 9f0942e..3d8b3ec 100644 --- a/src/ui/views/passkeys.rs +++ b/src/ui/views/passkeys.rs @@ -13,7 +13,7 @@ use directories::UserDirs; use gpui::prelude::FluentBuilder; use gpui::*; use gpui_component::Disableable; -use gpui_component::button::{Button, ButtonVariant, ButtonVariants, ButtonCustomVariant}; +use gpui_component::button::{Button, ButtonCustomVariant, ButtonVariant, ButtonVariants}; use gpui_component::{ ActiveTheme, Icon, Placement, Sizable, StyledExt, Theme, WindowExt, badge::Badge, @@ -1117,41 +1117,50 @@ impl PasskeysView { return; } self.loading = true; - + let status_handle = dialog::open_status_dialog("Resetting Device...", window, cx); let entity = cx.entity().downgrade(); let _ = status_handle.update(cx, |d, cx| { - d.set_loading("Unplug your security key, then plug it back in within 10 seconds.", cx); + d.set_loading( + "Unplug your security key, then plug it back in within 10 seconds.", + cx, + ); }); self._task = Some(cx.spawn(async move |_, cx| { // Wait for unplug/replug - let reconnected = cx.background_executor().spawn(async move { - let start = std::time::Instant::now(); - // 1. Wait for unplug - while start.elapsed().as_secs() < 15 { - std::thread::sleep(std::time::Duration::from_millis(200)); - if crate::device::fido::hid::HidTransport::open().is_err() { - break; + let reconnected = cx + .background_executor() + .spawn(async move { + let start = std::time::Instant::now(); + // 1. Wait for unplug + while start.elapsed().as_secs() < 15 { + std::thread::sleep(std::time::Duration::from_millis(200)); + if crate::device::fido::hid::HidTransport::open().is_err() { + break; + } } - } - - // 2. Wait for replug - while start.elapsed().as_secs() < 15 { - std::thread::sleep(std::time::Duration::from_millis(500)); - if crate::device::fido::hid::HidTransport::open().is_ok() { - return true; + + // 2. Wait for replug + while start.elapsed().as_secs() < 15 { + std::thread::sleep(std::time::Duration::from_millis(500)); + if crate::device::fido::hid::HidTransport::open().is_ok() { + return true; + } } - } - false - }).await; + false + }) + .await; if !reconnected { let _ = entity.update(cx, |this, cx| { this.loading = false; let _ = status_handle.update(cx, |d, cx| { - d.set_error("Timeout waiting for device reconnection. Reset canceled.".to_string(), cx); + d.set_error( + "Timeout waiting for device reconnection. Reset canceled.".to_string(), + cx, + ); }); cx.notify(); }); @@ -1164,9 +1173,10 @@ impl PasskeysView { }); // Execute reset - let result = cx.background_executor().spawn(async move { - io::reset_device() - }).await; + let result = cx + .background_executor() + .spawn(async move { io::reset_device() }) + .await; let _ = entity.update(cx, |this, cx| { this.loading = false; @@ -1177,7 +1187,9 @@ impl PasskeysView { let _ = status_handle.update(cx, |d, cx| { d.set_success(msg, cx); }); - cx.emit(PasskeysEvent::Notification("Device reset successfully".into())); + cx.emit(PasskeysEvent::Notification( + "Device reset successfully".into(), + )); } Err(e) => { log::error!("Error resetting device: {}", e); From 22708b0e0ccfe0dde3009dc7195e7d67959c52c2 Mon Sep 17 00:00:00 2001 From: Suyog Tandel Date: Tue, 23 Jun 2026 00:06:11 +0530 Subject: [PATCH 04/11] feat: add hardware-endpoints panel to control usb interfaces for rskey --- src/device/rescue/constants.rs | 13 ++++++ src/device/rescue/mod.rs | 51 ++++++++++++--------- src/device/types.rs | 3 ++ src/ui/views/config.rs | 82 ++++++++++++++++++++++++++++++++++ 4 files changed, 129 insertions(+), 20 deletions(-) diff --git a/src/device/rescue/constants.rs b/src/device/rescue/constants.rs index 70fb9ed..66ac39f 100644 --- a/src/device/rescue/constants.rs +++ b/src/device/rescue/constants.rs @@ -96,6 +96,7 @@ pub enum PhyTag { Curves = 0x0A, LedDriver = 0x0C, LedOrder = 0x0D, + EnabledUsbItf = 0x0B, } impl PhyTag { @@ -109,6 +110,7 @@ impl PhyTag { 0x08 => Some(Self::PresenceTimeout), 0x09 => Some(Self::UsbProduct), 0x0A => Some(Self::Curves), + 0x0B => Some(Self::EnabledUsbItf), 0x0C => Some(Self::LedDriver), 0x0D => Some(Self::LedOrder), _ => None, @@ -132,6 +134,17 @@ bitflags::bitflags! { } } +bitflags::bitflags! { + /// Enabled USB interfaces for TAG 0x0B (EnabledUsbItf) + pub struct UsbInterfaces: u8 { + const CCID = 0x01; + const WCID = 0x02; + const HID = 0x04; + const KB = 0x08; + const LWIP = 0x10; + } +} + // --- 4. Vendor/LED Applet (RS-Key specific) --- pub const VENDOR_LED_AID: &[u8] = &[0xF0, 0x00, 0x00, 0x00, 0x01]; diff --git a/src/device/rescue/mod.rs b/src/device/rescue/mod.rs index 5125b63..23874d9 100644 --- a/src/device/rescue/mod.rs +++ b/src/device/rescue/mod.rs @@ -10,24 +10,7 @@ use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; use pcsc::{Context, Protocols, Scope, ShareMode}; use std::io::Cursor; -/// Differentiates between Pico-Fido and RS-Key firmwares based on the Rescue Applet SELECT response. -/// -/// **WARNING:** This is a temporary heuristic that relies on the major version byte (`major >= 8` implies RS-Key). -/// If Pico-Fido releases v8.x, this logic will silently fail and misidentify devices. -/// -/// TODO: Work with upstream RS-Key maintainers to expose a unique identity block or hardware string -/// in the SELECT response to reliably differentiate the firmwares in the long term. -fn detect_firmware_type(select_resp: &[u8]) -> FirmwareType { - if select_resp.len() >= 4 { - let major = select_resp[2]; - if major >= 8 { - return FirmwareType::RSKey; - } else { - return FirmwareType::PicoFido; - } - } - FirmwareType::Unknown -} + /// Connects to the first available reader and selects the Rescue Applet fn connect_and_select() -> Result<(pcsc::Card, Vec, FirmwareType), PFError> { @@ -45,6 +28,13 @@ fn connect_and_select() -> Result<(pcsc::Card, Vec, FirmwareType), PFError> PFError::NoDevice })?; + let reader_name = reader.to_string_lossy(); + let mut fw_type = if reader_name.contains("RS-Key") || reader_name.contains("RSK") { + FirmwareType::RSKey + } else { + FirmwareType::Unknown + }; + let card = ctx.connect(reader, ShareMode::Shared, Protocols::ANY)?; // Select Applet APDU: 00 A4 04 04 [Len] [AID] @@ -70,9 +60,17 @@ fn connect_and_select() -> Result<(pcsc::Card, Vec, FirmwareType), PFError> )); } - log::info!("Successfully connected to Rescue Applet"); let data = rx.to_vec(); - let fw_type = detect_firmware_type(&data); + + if fw_type == FirmwareType::Unknown { + if data.len() >= 4 && data[2] >= 8 { + fw_type = FirmwareType::RSKey; + } else { + fw_type = FirmwareType::PicoFido; + } + } + + log::info!("Successfully connected to Rescue Applet"); log::info!("Detected firmware type: {:?}", fw_type); Ok((card, data, fw_type)) } @@ -240,6 +238,11 @@ pub fn read_device_details() -> Result { config.led_order = Some(val[0]); } } + PhyTag::EnabledUsbItf => { + if !val.is_empty() { + config.enabled_usb_itf = Some(val[0]); + } + } } } i += len; @@ -370,6 +373,14 @@ pub fn write_config(config: AppConfigInput) -> Result { tlv.push(val); } + // Enabled USB Interfaces (Tag 0x0B) + if let Some(val) = config.enabled_usb_itf { + tlv.push(PhyTag::EnabledUsbItf as u8); + tlv.push(0x01); + // SAFETY: Never write a mask without CCID, otherwise Rescue applet is unreachable. + tlv.push(val | UsbInterfaces::CCID.bits()); + } + // 2. Connect and Send if tlv.is_empty() { log::warn!("No configuration changes to apply"); diff --git a/src/device/types.rs b/src/device/types.rs index b5f525c..8afdc58 100644 --- a/src/device/types.rs +++ b/src/device/types.rs @@ -33,6 +33,8 @@ pub struct AppConfig { pub enable_secp256k1: bool, #[serde(skip_serializing_if = "Option::is_none")] pub led_order: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub enabled_usb_itf: Option, } #[derive(Deserialize, Debug, Clone)] @@ -50,6 +52,7 @@ pub struct AppConfigInput { pub led_steady: Option, pub enable_secp256k1: Option, pub led_order: Option, + pub enabled_usb_itf: Option, } #[derive(Serialize, Debug, Clone, PartialEq)] diff --git a/src/ui/views/config.rs b/src/ui/views/config.rs index 26278ea..594c79c 100644 --- a/src/ui/views/config.rs +++ b/src/ui/views/config.rs @@ -84,6 +84,7 @@ pub struct ConfigView { led_status_brightness: [u8; 4], usb_apps_supported: u16, usb_apps_enabled: u16, + enabled_usb_itf: Option, _task: Option>, } @@ -245,6 +246,7 @@ impl ConfigView { led_status_brightness, usb_apps_supported, usb_apps_enabled, + enabled_usb_itf: config.and_then(|c| c.enabled_usb_itf), _task: None, } } @@ -471,6 +473,12 @@ impl ConfigView { has_changes = true; } + let mut final_enabled_usb_itf = current_config.enabled_usb_itf; + if self.enabled_usb_itf != current_config.enabled_usb_itf { + has_changes = true; + final_enabled_usb_itf = self.enabled_usb_itf; + } + if !has_changes { log::info!("No changes detected"); return; @@ -489,6 +497,7 @@ impl ConfigView { led_steady: Some(self.led_steady), enable_secp256k1: Some(self.enable_secp256k1), led_order: current_config.led_order, + enabled_usb_itf: final_enabled_usb_itf, }; let method = status.method.clone(); @@ -597,6 +606,8 @@ impl ConfigView { self.usb_apps_enabled = apps.usb_enabled; } + self.enabled_usb_itf = config.and_then(|c| c.enabled_usb_itf); + cx.notify(); } @@ -1163,6 +1174,75 @@ impl ConfigView { }); })); } + + fn render_rskey_usb_itf_card( + &mut self, + cx: &mut Context, + is_fido: bool, + ) -> impl IntoElement { + let theme = cx.theme(); + let mut rows = v_flex().gap_4(); + + // 0x01: CCID, 0x02: WCID, 0x04: HID, 0x08: KB, 0x10: LWIP + let interfaces = [ + ("CCID (Smart Card)", 0x01u8), + ("WCID (WebUSB)", 0x02u8), + ("HID (FIDO)", 0x04u8), + ("KB (Keyboard)", 0x08u8), + ("LWIP", 0x10u8), + ]; + + let current_mask = self.enabled_usb_itf.unwrap_or(0x1F); // Default to all on if missing + + for (name, bit) in interfaces { + let is_enabled = (current_mask & bit) != 0; + let is_ccid = bit == 0x01; + + let toggle_listener = cx.listener(move |this, checked, _, cx| { + let mut mask = this.enabled_usb_itf.unwrap_or(0x1F); + if *checked { + mask |= bit; + } else { + mask &= !bit; + } + + if bit == 0x01 { + // Force CCID on to prevent bricking + mask |= 0x01; + } + + this.enabled_usb_itf = Some(mask); + cx.notify(); + }); + + rows = rows.child( + gpui_component::h_flex() + .items_center() + .justify_between() + .child(v_flex().gap_0p5().child(name).child( + div().text_sm().text_color(theme.muted_foreground).child( + if is_ccid { + "Required for Rescue Applet" + } else { + "USB Endpoint" + }, + ), + )) + .child( + Switch::new(gpui::SharedString::from(format!("usb-itf-toggle-{}", bit))) + .checked(is_enabled || is_ccid) // CCID always looks checked + .disabled(is_fido || is_ccid) // Disable toggling CCID entirely! + .on_click(toggle_listener), + ), + ); + } + + Card::new() + .title("Hardware Endpoints") + .description("Toggle low-level USB interfaces") + .icon(Icon::default().path("icons/cpu.svg")) + .child(rows) + } } impl Render for ConfigView { @@ -1232,8 +1312,10 @@ impl Render for ConfigView { if is_rskey { let rskey_led = self.render_rskey_led_card(cx, is_fido).into_any_element(); let rskey_apps = self.render_rskey_apps_card(cx, is_fido).into_any_element(); + let rskey_usb_itf = self.render_rskey_usb_itf_card(cx, is_fido).into_any_element(); grid_children.push(rskey_led); grid_children.push(rskey_apps); + grid_children.push(rskey_usb_itf); } let theme = cx.theme(); From cc72224252d5e7e9cd708a9d769eefb4a5f4dabf Mon Sep 17 00:00:00 2001 From: Suyog Tandel Date: Tue, 23 Jun 2026 00:24:00 +0530 Subject: [PATCH 05/11] fix: bug in fido curve config --- src/device/rescue/mod.rs | 18 +++++++++++------- src/device/types.rs | 3 +++ src/ui/views/config.rs | 1 + 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/device/rescue/mod.rs b/src/device/rescue/mod.rs index 23874d9..54235b3 100644 --- a/src/device/rescue/mod.rs +++ b/src/device/rescue/mod.rs @@ -222,8 +222,9 @@ pub fn read_device_details() -> Result { } } PhyTag::Curves => { - if val.len() >= 4 { + if val.len() == 4 { let curves_val = u32::from_be_bytes([val[0], val[1], val[2], val[3]]); + config.raw_curves_mask = Some(curves_val); let curves = RescueCurves::from_bits_truncate(curves_val); config.enable_secp256k1 = curves.contains(RescueCurves::SECP256K1); } @@ -334,15 +335,18 @@ pub fn write_config(config: AppConfigInput) -> Result { } // Curves - if let Some(enabled) = config.enable_secp256k1 { - let mut curves = RescueCurves::empty(); - if enabled { - curves.insert(RescueCurves::SECP256K1); + if config.enable_secp256k1.is_some() || config.raw_curves_mask.is_some() { + let mut mask = config.raw_curves_mask.unwrap_or(0); + if let Some(enabled) = config.enable_secp256k1 { + if enabled { + mask |= RescueCurves::SECP256K1.bits(); + } else { + mask &= !RescueCurves::SECP256K1.bits(); + } } - tlv.push(PhyTag::Curves as u8); tlv.push(0x04); - tlv.write_u32::(curves.bits()).unwrap(); + tlv.write_u32::(mask).unwrap(); } // LED Driver (Tag 0x0C) diff --git a/src/device/types.rs b/src/device/types.rs index 8afdc58..7138eab 100644 --- a/src/device/types.rs +++ b/src/device/types.rs @@ -32,6 +32,8 @@ pub struct AppConfig { pub led_steady: bool, pub enable_secp256k1: bool, #[serde(skip_serializing_if = "Option::is_none")] + pub raw_curves_mask: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub led_order: Option, #[serde(skip_serializing_if = "Option::is_none")] pub enabled_usb_itf: Option, @@ -51,6 +53,7 @@ pub struct AppConfigInput { pub power_cycle_on_reset: Option, pub led_steady: Option, pub enable_secp256k1: Option, + pub raw_curves_mask: Option, pub led_order: Option, pub enabled_usb_itf: Option, } diff --git a/src/ui/views/config.rs b/src/ui/views/config.rs index 594c79c..7909557 100644 --- a/src/ui/views/config.rs +++ b/src/ui/views/config.rs @@ -496,6 +496,7 @@ impl ConfigView { power_cycle_on_reset: Some(self.power_cycle), led_steady: Some(self.led_steady), enable_secp256k1: Some(self.enable_secp256k1), + raw_curves_mask: current_config.raw_curves_mask, led_order: current_config.led_order, enabled_usb_itf: final_enabled_usb_itf, }; From 5157c0616c0acb727c2f81f7ea97da4657379f9f Mon Sep 17 00:00:00 2001 From: Suyog Tandel Date: Tue, 23 Jun 2026 15:03:30 +0530 Subject: [PATCH 06/11] fix: tests and update crates to latest versions --- .gitignore | 3 +- Cargo.lock | 111 ++++++++++++++++++++++++++++++++------- Cargo.toml | 22 ++++---- src/device/fido/hid.rs | 102 ++++++++++++++++++----------------- src/device/fido/mod.rs | 2 + src/ui/views/config.rs | 4 +- src/ui/views/passkeys.rs | 8 +-- 7 files changed, 166 insertions(+), 86 deletions(-) diff --git a/.gitignore b/.gitignore index a08d6a0..9c44668 100644 --- a/.gitignore +++ b/.gitignore @@ -29,4 +29,5 @@ src-svelte gpui-component packaging .cargo-packager -llm-texts \ No newline at end of file +llm-texts +opencode.jsonc diff --git a/Cargo.lock b/Cargo.lock index 36d537a..33d37b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15,11 +15,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", - "cipher", + "cipher 0.4.4", "cpufeatures 0.2.17", "zeroize", ] +[[package]] +name = "aes" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1fc76eaeac4c9164506c466d4ffdd8ec9d0c5bf57ee97177c4d8eceb3a0e138" +dependencies = [ + "cipher 0.5.2", + "cpubits", + "cpufeatures 0.3.0", +] + [[package]] name = "ahash" version = "0.8.12" @@ -644,6 +655,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-padding" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "710f1dd022ef4e93f8a438b4ba958de7f64308434fa6a87104481645cc30068b" +dependencies = [ + "hybrid-array", +] + [[package]] name = "block2" version = "0.6.2" @@ -758,7 +778,16 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" dependencies = [ - "cipher", + "cipher 0.4.4", +] + +[[package]] +name = "cbc" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce2dc9ee5f88d11e0beb842c88b33c8a5cf0d1329c4b19494af42b07dbfe8896" +dependencies = [ + "cipher 0.5.2", ] [[package]] @@ -851,11 +880,21 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "crypto-common", - "inout", + "crypto-common 0.1.7", + "inout 0.1.4", "zeroize", ] +[[package]] +name = "cipher" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" +dependencies = [ + "crypto-common 0.2.2", + "inout 0.2.2", +] + [[package]] name = "clang-sys" version = "1.8.1" @@ -1165,6 +1204,12 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "cpubits" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -1243,6 +1288,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + [[package]] name = "ctor" version = "0.4.3" @@ -1319,7 +1373,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", - "crypto-common", + "crypto-common 0.1.7", "subtle", ] @@ -2655,6 +2709,15 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" +[[package]] +name = "hybrid-array" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" version = "1.10.1" @@ -2939,10 +3002,20 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ - "block-padding", + "block-padding 0.3.3", "generic-array", ] +[[package]] +name = "inout" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" +dependencies = [ + "block-padding 0.4.2", + "hybrid-array", +] + [[package]] name = "instant" version = "0.1.13" @@ -3414,9 +3487,9 @@ checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" [[package]] name = "memmap2" -version = "0.9.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" dependencies = [ "libc", ] @@ -3915,14 +3988,14 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3299dd401feaf1d45afd8fd1c0586f10fcfb22f244bb9afa942cec73503b89d" dependencies = [ - "aes", + "aes 0.8.4", "ashpd 0.12.3", "async-fs", "async-io", "async-lock", "blocking", - "cbc", - "cipher", + "cbc 0.1.2", + "cipher 0.4.4", "digest", "endi", "futures-lite 2.6.1", @@ -4135,12 +4208,12 @@ checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" name = "picoforge" version = "0.6.0" dependencies = [ - "aes", + "aes 0.9.1", "anyhow", "base64", "bitflags 2.13.0", "byteorder", - "cbc", + "cbc 0.2.1", "directories", "gpui", "gpui-component", @@ -4415,9 +4488,9 @@ dependencies = [ [[package]] name = "quinn" -version = "0.11.9" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" dependencies = [ "bytes", "cfg_aliases", @@ -4435,9 +4508,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.14" +version = "0.11.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +checksum = "4fcb935c5bec503c2f0e306bdd3e58bb9029dcb14fa8d9ac76e3a5256ac0763e" dependencies = [ "bytes", "getrandom 0.3.4", @@ -4948,9 +5021,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.40" +version = "0.23.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" dependencies = [ "once_cell", "ring", diff --git a/Cargo.toml b/Cargo.toml index 041fb37..d8b9eeb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,19 +15,19 @@ log4rs = "1" # For logging to output (like s directories = "6" # For Applcation config/data dir handling # For device management backend: -pcsc = "2" # Standard Smart Card API (connect to the key) -hex = "0.4" # For parsing VID/PID strings -byteorder = "1.5" # Required for writing Big-Endian numbers (firmware requirement) -thiserror = "2" # Makes custom error handling much easier -anyhow = "1" # For easy error propagation -hidapi = "2.6" # For fido2 interface operations but non-standard commands +pcsc = "2" # Standard Smart Card API (connect to the key) +hex = "0.4" # For parsing VID/PID strings +byteorder = "1.5" # Required for writing Big-Endian numbers (firmware requirement) +thiserror = "2" # Makes custom error handling much easier +anyhow = "1" # For easy error propagation +hidapi = "2.6" # For fido2 interface operations but non-standard commands serde_cbor_2 = "0.13" rand = "0.10" -bitflags = "2.11" -base64 = "0.22" # For PEM encoding of DER certificates -ring = "0.17" # For signing fido2 messages with pin token -aes = "0.8" -cbc = "0.1" +bitflags = "2.13.0" +base64 = "0.22" # For PEM encoding of DER certificates +ring = "0.17" # For signing fido2 messages with pin token +aes = "0.9" +cbc = "0.2" # For Application UI: gpui = { version = "0.2.2", features = [] } diff --git a/src/device/fido/hid.rs b/src/device/fido/hid.rs index 1ba78db..5a22bf8 100644 --- a/src/device/fido/hid.rs +++ b/src/device/fido/hid.rs @@ -1,5 +1,4 @@ -use aes::cipher::generic_array::GenericArray; -use cbc::cipher::{BlockDecryptMut, BlockEncryptMut, KeyIvInit, block_padding::NoPadding}; +use cbc::cipher::{Block, BlockModeDecrypt, BlockModeEncrypt, KeyIvInit, block_padding::NoPadding}; use rand::RngExt; use ring::{agreement, digest, hmac}; use serde_cbor_2::{Value, from_slice, to_vec}; @@ -738,14 +737,15 @@ impl HidTransport { let pin_hash_16 = &pin_hash.as_ref()[0..16]; let iv = [0u8; 16]; - let mut block = *GenericArray::from_slice(pin_hash_16); + let mut block = Block::::try_from(pin_hash_16).unwrap(); let shared_secret_bytes = shared_secret.as_ref(); - let mut encryptor = cbc::Encryptor::::new( - GenericArray::from_slice(shared_secret_bytes), - GenericArray::from_slice(&iv), - ); - encryptor.encrypt_block_mut(&mut block); + let mut encryptor = cbc::Encryptor::::new_from_slices( + shared_secret_bytes, + &iv, + ) + .unwrap(); + encryptor.encrypt_block(&mut block); let pin_hash_enc = block.to_vec(); // 6. Send getPinToken command (Subcommand 0x05) @@ -777,11 +777,12 @@ impl HidTransport { Some(Value::Bytes(token_enc)) => { // Decrypt the PIN token using shared secret (AES-256-CBC, IV=0) let mut token_buf = token_enc.clone(); - let decrypted = cbc::Decryptor::::new( - GenericArray::from_slice(shared_secret_bytes), - GenericArray::from_slice(&iv), + let decrypted = cbc::Decryptor::::new_from_slices( + shared_secret_bytes, + &iv, ) - .decrypt_padded_mut::(&mut token_buf) + .map_err(|_| PFError::Device("Failed to create decryptor".into()))? + .decrypt_padded::(&mut token_buf) .map_err(|_| PFError::Device("Failed to decrypt PIN token".into()))?; log::info!("Successfully obtained and decrypted PIN token (Subcommand 0x05)."); Ok(decrypted.to_vec()) @@ -853,14 +854,15 @@ impl HidTransport { let pin_hash_16 = &pin_hash.as_ref()[0..16]; let iv = [0u8; 16]; - let mut block = *GenericArray::from_slice(pin_hash_16); + let mut block = Block::::try_from(pin_hash_16).unwrap(); let shared_secret_bytes = shared_secret.as_ref(); - let mut encryptor = cbc::Encryptor::::new( - GenericArray::from_slice(shared_secret_bytes), - GenericArray::from_slice(&iv), - ); - encryptor.encrypt_block_mut(&mut block); + let mut encryptor = cbc::Encryptor::::new_from_slices( + shared_secret_bytes, + &iv, + ) + .unwrap(); + encryptor.encrypt_block(&mut block); let pin_hash_enc = block.to_vec(); // 6. Send getPinUvAuthTokenUsingPinWithPermissions command (Subcommand 0x09) @@ -900,11 +902,12 @@ impl HidTransport { Some(Value::Bytes(token_enc)) => { // Decrypt the PIN token using shared secret (AES-256-CBC, IV=0) let mut token_buf = token_enc.clone(); - let decrypted = cbc::Decryptor::::new( - GenericArray::from_slice(shared_secret_bytes), - GenericArray::from_slice(&iv), + let decrypted = cbc::Decryptor::::new_from_slices( + shared_secret_bytes, + &iv, ) - .decrypt_padded_mut::(&mut token_buf) + .map_err(|_| PFError::Device("Failed to create decryptor".into()))? + .decrypt_padded::(&mut token_buf) .map_err(|_| PFError::Device("Failed to decrypt PIN token".into()))?; log::info!("Successfully obtained and decrypted PIN token (Subcommand 0x09)."); Ok(decrypted.to_vec()) @@ -983,13 +986,14 @@ impl HidTransport { let iv = [0u8; 16]; let mut new_pin_enc = Vec::new(); - let mut encryptor = cbc::Encryptor::::new( - GenericArray::from_slice(shared_secret_bytes), - GenericArray::from_slice(&iv), - ); + let mut encryptor = cbc::Encryptor::::new_from_slices( + shared_secret_bytes, + &iv, + ) + .unwrap(); for chunk in padded_new_pin.chunks_exact(16) { - let mut block = *GenericArray::from_slice(chunk); - encryptor.encrypt_block_mut(&mut block); + let mut block = Block::::try_from(chunk).unwrap(); + encryptor.encrypt_block(&mut block); new_pin_enc.extend_from_slice(&block); } @@ -1101,12 +1105,13 @@ impl HidTransport { let pin_hash = digest::digest(&digest::SHA256, current_pin.as_bytes()); let pin_hash_16 = &pin_hash.as_ref()[0..16]; let iv = [0u8; 16]; - let mut block = *GenericArray::from_slice(pin_hash_16); - cbc::Encryptor::::new( - GenericArray::from_slice(shared_secret_bytes), - GenericArray::from_slice(&iv), + let mut block = Block::::try_from(pin_hash_16).unwrap(); + cbc::Encryptor::::new_from_slices( + shared_secret_bytes, + &iv, ) - .encrypt_block_mut(&mut block); + .unwrap() + .encrypt_block(&mut block); let pin_hash_enc = block.to_vec(); // 6. Encrypt newPinEnc @@ -1115,13 +1120,14 @@ impl HidTransport { padded_new_pin[..bytes.len()].copy_from_slice(bytes); let mut new_pin_enc = Vec::new(); - let mut encryptor = cbc::Encryptor::::new( - GenericArray::from_slice(shared_secret_bytes), - GenericArray::from_slice(&iv), - ); + let mut encryptor = cbc::Encryptor::::new_from_slices( + shared_secret_bytes, + &iv, + ) + .unwrap(); for chunk in padded_new_pin.chunks_exact(16) { - let mut block = *GenericArray::from_slice(chunk); - encryptor.encrypt_block_mut(&mut block); + let mut block = Block::::try_from(chunk).unwrap(); + encryptor.encrypt_block(&mut block); new_pin_enc.extend_from_slice(&block); } @@ -1722,10 +1728,9 @@ mod tests { #[test] fn test_pin_hash_encryption_actually_encrypts() { // Verify that our AES-CBC encryption actually modifies the data. - // This guards against the previous bug where encrypt_block_mut + // This guards against the previous bug where encrypt_block // was called on a temporary copy (buffer.into()), discarding the result. - use aes::cipher::generic_array::GenericArray; - use cbc::cipher::{BlockEncryptMut, KeyIvInit}; + use cbc::cipher::BlockModeEncrypt; use ring::digest; let pin = "123456"; @@ -1736,14 +1741,15 @@ mod tests { let key = [0u8; 32]; let iv = [0u8; 16]; - let mut block = *GenericArray::from_slice(pin_hash_16); + let mut block = Block::::try_from(pin_hash_16).unwrap(); let original = block; - let mut encryptor = cbc::Encryptor::::new( - GenericArray::from_slice(&key), - GenericArray::from_slice(&iv), - ); - encryptor.encrypt_block_mut(&mut block); + let mut encryptor = cbc::Encryptor::::new_from_slices( + &key, + &iv, + ) + .unwrap(); + encryptor.encrypt_block(&mut block); // The encrypted block MUST differ from the original assert_ne!( diff --git a/src/device/fido/mod.rs b/src/device/fido/mod.rs index d266815..d78cc46 100644 --- a/src/device/fido/mod.rs +++ b/src/device/fido/mod.rs @@ -1070,7 +1070,9 @@ mod tests { power_cycle_on_reset: None, led_steady: None, enable_secp256k1: None, + raw_curves_mask: None, led_order: None, + enabled_usb_itf: None, } } diff --git a/src/ui/views/config.rs b/src/ui/views/config.rs index 7909557..3ff94a9 100644 --- a/src/ui/views/config.rs +++ b/src/ui/views/config.rs @@ -908,9 +908,7 @@ impl ConfigView { let dec_bright_listener = cx.listener(move |this, _, _, cx| { let mut b = this.led_status_brightness[c_i]; - if b > 0 { - b -= 1; - } + b = b.saturating_sub(1); this.led_status_brightness[c_i] = b; cx.notify(); }); diff --git a/src/ui/views/passkeys.rs b/src/ui/views/passkeys.rs index 3d8b3ec..2736626 100644 --- a/src/ui/views/passkeys.rs +++ b/src/ui/views/passkeys.rs @@ -1029,10 +1029,10 @@ impl PasskeysView { .child("Reset Device") .custom( ButtonCustomVariant::new(cx) - .color(theme.danger.into()) - .hover(theme.danger_hover.into()) - .active(theme.danger_active.into()) - .foreground(theme.danger_foreground.into()), + .color(theme.danger) + .hover(theme.danger_hover) + .active(theme.danger_active) + .foreground(theme.danger_foreground), ) .disabled(self.loading) .on_click(cx.listener(|this, _, window, cx| { From 2852893187eefe6859ae3297d92cf637b950918d Mon Sep 17 00:00:00 2001 From: Suyog Tandel Date: Tue, 23 Jun 2026 20:29:56 +0530 Subject: [PATCH 07/11] chore: format code using cargo fmt --- src/device/fido/hid.rs | 67 +++++++++++++--------------------------- src/device/rescue/mod.rs | 2 -- src/ui/views/config.rs | 27 +++++++++------- 3 files changed, 38 insertions(+), 58 deletions(-) diff --git a/src/device/fido/hid.rs b/src/device/fido/hid.rs index 5a22bf8..9f1599c 100644 --- a/src/device/fido/hid.rs +++ b/src/device/fido/hid.rs @@ -740,11 +740,8 @@ impl HidTransport { let mut block = Block::::try_from(pin_hash_16).unwrap(); let shared_secret_bytes = shared_secret.as_ref(); - let mut encryptor = cbc::Encryptor::::new_from_slices( - shared_secret_bytes, - &iv, - ) - .unwrap(); + let mut encryptor = + cbc::Encryptor::::new_from_slices(shared_secret_bytes, &iv).unwrap(); encryptor.encrypt_block(&mut block); let pin_hash_enc = block.to_vec(); @@ -777,13 +774,11 @@ impl HidTransport { Some(Value::Bytes(token_enc)) => { // Decrypt the PIN token using shared secret (AES-256-CBC, IV=0) let mut token_buf = token_enc.clone(); - let decrypted = cbc::Decryptor::::new_from_slices( - shared_secret_bytes, - &iv, - ) - .map_err(|_| PFError::Device("Failed to create decryptor".into()))? - .decrypt_padded::(&mut token_buf) - .map_err(|_| PFError::Device("Failed to decrypt PIN token".into()))?; + let decrypted = + cbc::Decryptor::::new_from_slices(shared_secret_bytes, &iv) + .map_err(|_| PFError::Device("Failed to create decryptor".into()))? + .decrypt_padded::(&mut token_buf) + .map_err(|_| PFError::Device("Failed to decrypt PIN token".into()))?; log::info!("Successfully obtained and decrypted PIN token (Subcommand 0x05)."); Ok(decrypted.to_vec()) } @@ -857,11 +852,8 @@ impl HidTransport { let mut block = Block::::try_from(pin_hash_16).unwrap(); let shared_secret_bytes = shared_secret.as_ref(); - let mut encryptor = cbc::Encryptor::::new_from_slices( - shared_secret_bytes, - &iv, - ) - .unwrap(); + let mut encryptor = + cbc::Encryptor::::new_from_slices(shared_secret_bytes, &iv).unwrap(); encryptor.encrypt_block(&mut block); let pin_hash_enc = block.to_vec(); @@ -902,13 +894,11 @@ impl HidTransport { Some(Value::Bytes(token_enc)) => { // Decrypt the PIN token using shared secret (AES-256-CBC, IV=0) let mut token_buf = token_enc.clone(); - let decrypted = cbc::Decryptor::::new_from_slices( - shared_secret_bytes, - &iv, - ) - .map_err(|_| PFError::Device("Failed to create decryptor".into()))? - .decrypt_padded::(&mut token_buf) - .map_err(|_| PFError::Device("Failed to decrypt PIN token".into()))?; + let decrypted = + cbc::Decryptor::::new_from_slices(shared_secret_bytes, &iv) + .map_err(|_| PFError::Device("Failed to create decryptor".into()))? + .decrypt_padded::(&mut token_buf) + .map_err(|_| PFError::Device("Failed to decrypt PIN token".into()))?; log::info!("Successfully obtained and decrypted PIN token (Subcommand 0x09)."); Ok(decrypted.to_vec()) } @@ -986,11 +976,8 @@ impl HidTransport { let iv = [0u8; 16]; let mut new_pin_enc = Vec::new(); - let mut encryptor = cbc::Encryptor::::new_from_slices( - shared_secret_bytes, - &iv, - ) - .unwrap(); + let mut encryptor = + cbc::Encryptor::::new_from_slices(shared_secret_bytes, &iv).unwrap(); for chunk in padded_new_pin.chunks_exact(16) { let mut block = Block::::try_from(chunk).unwrap(); encryptor.encrypt_block(&mut block); @@ -1106,12 +1093,9 @@ impl HidTransport { let pin_hash_16 = &pin_hash.as_ref()[0..16]; let iv = [0u8; 16]; let mut block = Block::::try_from(pin_hash_16).unwrap(); - cbc::Encryptor::::new_from_slices( - shared_secret_bytes, - &iv, - ) - .unwrap() - .encrypt_block(&mut block); + cbc::Encryptor::::new_from_slices(shared_secret_bytes, &iv) + .unwrap() + .encrypt_block(&mut block); let pin_hash_enc = block.to_vec(); // 6. Encrypt newPinEnc @@ -1120,11 +1104,8 @@ impl HidTransport { padded_new_pin[..bytes.len()].copy_from_slice(bytes); let mut new_pin_enc = Vec::new(); - let mut encryptor = cbc::Encryptor::::new_from_slices( - shared_secret_bytes, - &iv, - ) - .unwrap(); + let mut encryptor = + cbc::Encryptor::::new_from_slices(shared_secret_bytes, &iv).unwrap(); for chunk in padded_new_pin.chunks_exact(16) { let mut block = Block::::try_from(chunk).unwrap(); encryptor.encrypt_block(&mut block); @@ -1744,11 +1725,7 @@ mod tests { let mut block = Block::::try_from(pin_hash_16).unwrap(); let original = block; - let mut encryptor = cbc::Encryptor::::new_from_slices( - &key, - &iv, - ) - .unwrap(); + let mut encryptor = cbc::Encryptor::::new_from_slices(&key, &iv).unwrap(); encryptor.encrypt_block(&mut block); // The encrypted block MUST differ from the original diff --git a/src/device/rescue/mod.rs b/src/device/rescue/mod.rs index 54235b3..cbd6b4d 100644 --- a/src/device/rescue/mod.rs +++ b/src/device/rescue/mod.rs @@ -10,8 +10,6 @@ use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; use pcsc::{Context, Protocols, Scope, ShareMode}; use std::io::Cursor; - - /// Connects to the first available reader and selects the Rescue Applet fn connect_and_select() -> Result<(pcsc::Card, Vec, FirmwareType), PFError> { let ctx = Context::establish(Scope::User).map_err(|e| { diff --git a/src/ui/views/config.rs b/src/ui/views/config.rs index 3ff94a9..13978d9 100644 --- a/src/ui/views/config.rs +++ b/src/ui/views/config.rs @@ -1204,12 +1204,12 @@ impl ConfigView { } else { mask &= !bit; } - + if bit == 0x01 { // Force CCID on to prevent bricking mask |= 0x01; } - + this.enabled_usb_itf = Some(mask); cx.notify(); }); @@ -1218,15 +1218,18 @@ impl ConfigView { gpui_component::h_flex() .items_center() .justify_between() - .child(v_flex().gap_0p5().child(name).child( - div().text_sm().text_color(theme.muted_foreground).child( - if is_ccid { - "Required for Rescue Applet" - } else { - "USB Endpoint" - }, + .child( + v_flex().gap_0p5().child(name).child( + div() + .text_sm() + .text_color(theme.muted_foreground) + .child(if is_ccid { + "Required for Rescue Applet" + } else { + "USB Endpoint" + }), ), - )) + ) .child( Switch::new(gpui::SharedString::from(format!("usb-itf-toggle-{}", bit))) .checked(is_enabled || is_ccid) // CCID always looks checked @@ -1311,7 +1314,9 @@ impl Render for ConfigView { if is_rskey { let rskey_led = self.render_rskey_led_card(cx, is_fido).into_any_element(); let rskey_apps = self.render_rskey_apps_card(cx, is_fido).into_any_element(); - let rskey_usb_itf = self.render_rskey_usb_itf_card(cx, is_fido).into_any_element(); + let rskey_usb_itf = self + .render_rskey_usb_itf_card(cx, is_fido) + .into_any_element(); grid_children.push(rskey_led); grid_children.push(rskey_apps); grid_children.push(rskey_usb_itf); From e3dc8fc1c6f6fe8b917af4edfdd3e28332cbc746 Mon Sep 17 00:00:00 2001 From: Suyog Tandel Date: Tue, 23 Jun 2026 21:53:00 +0530 Subject: [PATCH 08/11] docs: add documentation to src/device/module --- src/device/fido/constants.rs | 712 +++++++++++++++++++++++++-------- src/device/fido/hid.rs | 290 +++++++++++++- src/device/fido/mod.rs | 56 +++ src/device/io.rs | 94 +++-- src/device/mod.rs | 41 ++ src/device/rescue/constants.rs | 475 ++++++++++++++++++++-- src/device/rescue/mod.rs | 264 +++++++++++- src/device/types.rs | 47 ++- 8 files changed, 1739 insertions(+), 240 deletions(-) diff --git a/src/device/fido/constants.rs b/src/device/fido/constants.rs index d2797f3..bf34356 100644 --- a/src/device/fido/constants.rs +++ b/src/device/fido/constants.rs @@ -1,375 +1,398 @@ -//! Constants, enums, bitflags and data structures for FIDO2 protocol for pico-fido firmware. +//! CTAP2 / FIDO2 protocol constants for pico-fido and RS-Key firmware. +//! +//! This file is the single source of truth for every byte value, error code, +//! and CBOR map key used by this codebase. Values are organized into three +//! categories: +//! +//! 1. **CTAP2 standard** — defined by the [FIDO CTAP2 spec], used by any +//! CTAP2-compliant authenticator. +//! 2. **Pico-fido vendor extensions** — custom commands/IDs for the +//! [pico-fido] firmware (vendor commands `0xC1`/`0xC2`, 64-bit config IDs). +//! 3. **RS-Key extensions** — additions specific to [RS-Key] firmware +//! (rescue applet AIDs, phy record tags, CTAPHID `0x41` vendor command). +//! +//! All enums use `#[repr(u8)]` or `#[repr(u64)]` so their numeric values +//! match the wire format exactly. +//! +//! # Reference +//! +//! - CTAP2 values: [CTAP2 v2.3 spec §8.1](https://fidoalliance.org/specs/fido-v2.3-ps-20260226/fido-client-to-authenticator-protocol-v2.3-ps-20260226.html) +//! - Pico-fido vendor commands: [pico-fido source](https://github.com/polhenarejos/pico-fido) +//! - RS-Key protocol: [RS-Key Host Protocol Docs](https://themaxmur.github.io/RS-Key/develop/protocol.html) +//! +//! [FIDO CTAP2 spec]: https://fidoalliance.org/specs/fido-v2.3-ps-20260226/fido-client-to-authenticator-protocol-v2.3-ps-20260226.html +//! [pico-fido]: https://github.com/polhenarejos/pico-fido +//! [RS-Key]: https://github.com/TheMaxMur/RS-Key #![allow(unused)] use std::fmt; +// ══════════════════════════════════════════════════════════════════════════════ +// CTAP2 STANDARD — FIDO Alliance specification §8.1 +// ══════════════════════════════════════════════════════════════════════════════ + +// ── CTAP2 command codes (§8.1) ────────────────────────────────────────────── + +/// CTAP2 CBOR command codes (CTAP2 spec §8.1). +/// +/// These are the opcodes sent as the first byte of a `CTAPHID_CBOR` payload. +/// The authenticator dispatches to the corresponding handler based on this byte. #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CtapCommand { + /// Create a new credential (§11.5.1). MakeCredential = 0x01, + /// Generate an authentication assertion (§11.5.2). GetAssertion = 0x02, + /// Return authenticator metadata (§11.5.3). GetInfo = 0x04, + /// PIN/UV token management (§11.5.4). ClientPin = 0x06, + /// Factory-reset all credentials and PIN (§11.5.5). Reset = 0x07, + /// Get the next assertion when multiple credentials match (§11.5.6). GetNextAssertion = 0x08, + /// Credential management operations (§11.5.8). CredentialMgmt = 0x0A, + /// Put the authenticator into a discoverable state (§11.5.7). Selection = 0x0B, + /// Read/write large blob storage (§11.5.9). LargeBlobs = 0x0C, + /// Authenticator configuration (enterprise attestation, min PIN, etc.) (§11.5.10). Config = 0x0D, } +/// CTAP1/U2F command codes (legacy protocol, U2F Raw Messages spec). #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum U2fCommand { + /// U2F Register command. Register = 0x01, + /// U2F Authenticate command. Authenticate = 0x02, + /// U2F Version inquiry. Version = 0x03, } -#[repr(u8)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum VendorCommand { - Backup = 0x01, - ManageSecurityEnvironment = 0x02, - Unlock = 0x03, - EnterpriseAttestation = 0x04, - PhysicalOptions = 0x05, - Memory = 0x06, -} - -#[repr(u8)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum AuthenticateControl { - EnforceUserPresence = 0x03, - CheckOnly = 0x07, -} +// ── CBOR map key enums (§11.5.x) ─────────────────────────────────────────── +// +// Each CTAP2 command encodes its parameters as a CBOR map with integer keys. +// These enums map human-readable names to the wire-format key bytes. +/// CBOR map keys for `authenticatorClientPIN` sub-commands (§11.5.4). +/// +/// The `subCommand` field selects which PIN operation to perform. #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ClientPinSubCommand { + /// Get remaining PIN attempts. GetPinRetries = 0x01, + /// Get ECDH key agreement public key. GetKeyAgreement = 0x02, + /// Set a new PIN (first-time setup). SetPin = 0x03, + /// Change an existing PIN. ChangePin = 0x04, + /// Get a PIN token for permission-gated operations. GetPinToken = 0x05, + /// Get a UV auth token using biometric/other UV (§11.5.4.1). GetPinUvAuthTokenUsingUvWithPermissions = 0x06, + /// Get remaining UV attempts. GetUvRetries = 0x07, - GetPinUvAuthTokenUsingPinWithPermissions = 0x09, // TODO: per fido spec, this should be 0x08? Needs to confirm and fix the firmware if true. + /// Get a PIN auth token with specific permissions. + /// + /// **Pico-fido note:** The CTAP2 spec defines this as `0x08`, but pico-fido + /// firmware uses `0x09`. This discrepancy is documented here for clarity. + GetPinUvAuthTokenUsingPinWithPermissions = 0x09, } +/// CBOR map keys for `authenticatorMakeCredential` (§11.5.1). #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum MakeCredentialParam { + /// SHA-256 hash of the client data (required). ClientDataHash = 0x01, + /// Relying party object `{ id, name, icon }` (required). Rp = 0x02, + /// User object `{ id, name, displayName, icon }` (required). User = 0x03, + /// Supported credential algorithms, preferred-first (required). PubKeyCredParams = 0x04, + /// Credentials to exclude (prevents duplication). ExcludeList = 0x05, + /// Extension inputs. Extensions = 0x06, + /// Options like `rk`, `up`, `uv`. Options = 0x07, + /// HMAC from PIN/UV token for user verification. PinUvAuthParam = 0x08, + /// PIN/UV protocol version (currently 1). PinUvAuthProtocol = 0x09, + /// Enterprise attestation mode (0=off, 1=permissive, 2=strict). EnterpriseAttestation = 0x0A, } +/// CBOR map keys for `authenticatorGetAssertion` (§11.5.2). #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum GetAssertionParam { + /// Relying party identifier (required). RpId = 0x01, + /// SHA-256 hash of the client data (required). ClientDataHash = 0x02, + /// Allowed credentials; if present, only these may be used. AllowList = 0x03, + /// Extension inputs. Extensions = 0x04, + /// Options like `up`, `uv`, `pin`. Options = 0x05, + /// HMAC from PIN/UV token. PinUvAuthParam = 0x06, + /// PIN/UV protocol version. PinUvAuthProtocol = 0x07, } +/// CBOR map keys for `authenticatorClientPIN` request body (§11.5.4). #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ClientPinParam { + /// PIN/UV protocol version (must be 1). PinUvAuthProtocol = 0x01, + /// Sub-command to execute (see [`ClientPinSubCommand`]). SubCommand = 0x02, + /// Platform's ECDH public key (COSE_Key). KeyAgreement = 0x03, + /// HMAC of the encrypted PIN or client data. PinUvAuthParam = 0x04, + /// AES-256-CBC encrypted new PIN. NewPinEnc = 0x05, + /// AES-256-CBC encrypted first 16 bytes of PIN hash. PinHashEnc = 0x06, + /// Permission bits for `getPinUvAuthTokenUsingPinWithPermissions`. Permissions = 0x09, + /// RP ID scope for the requested permissions. PermissionsRpId = 0x0A, } +/// CBOR map keys for `authenticatorClientPIN` response body (§11.5.4). #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ClientPinResponseParam { + /// Authenticator's ECDH public key (COSE_Key). KeyAgreement = 0x01, + /// Encrypted PIN/UV auth token. PinToken = 0x02, + /// Remaining PIN attempts. PinRetries = 0x03, + /// Continuation message for large payloads. NextMsg = 0x04, + /// Remaining UV attempts. UvRetries = 0x05, } +/// CBOR map keys for `authenticatorConfig` (§11.5.10). #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ConfigParam { + /// Config sub-command (see [`ConfigSubCommand`]). SubCommand = 0x01, + /// Sub-command parameters (CBOR map). SubCommandParams = 0x02, + /// PIN/UV protocol version. PinUvAuthProtocol = 0x03, + /// HMAC of the sub-command parameters. PinUvAuthParam = 0x04, } +/// Sub-commands for `authenticatorConfig` (§11.5.10). #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ConfigSubCommand { + /// Enable enterprise attestation for this authenticator. EnableEnterpriseAttestation = 0x01, + /// Toggle "always UV" policy (requires re-setting PIN). ToggleAlwaysUv = 0x02, + /// Set the minimum PIN length requirement. SetMinPinLength = 0x03, + /// Vendor-defined prototype config command (pico-fido/RS-Key extension). VendorPrototype = 0xFF, } -#[repr(u8)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum VendorParam { - VendorCommand = 0x01, - VendorSubParams = 0x02, - PinUvAuthProtocol = 0x03, - PinUvAuthParam = 0x04, -} - -#[repr(u8)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum VendorSubParam { - VendorParam = 0x01, - CoseKey = 0x02, - VendorParamInt = 0x03, - VendorParamText = 0x04, -} - +/// Sub-commands for `authenticatorCredentialManagement` (§11.5.8). #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CredentialMgmtSubCommand { + /// Get total credential/RP counts and remaining space. GetCredsMetadata = 0x01, + /// Begin enumerating Relying Parties. EnumerateRpsBegin = 0x02, + /// Get the next RP in the enumeration. EnumerateRpsGetNextRp = 0x03, + /// Begin enumerating credentials for a given RP. EnumerateCredentialsBegin = 0x04, + /// Get the next credential in the enumeration. EnumerateCredentialsGetNextCredential = 0x05, + /// Delete a stored credential. DeleteCredential = 0x06, + /// Update user information for a credential. UpdateUserInformation = 0x07, } +/// CBOR map keys for `authenticatorCredentialManagement` requests (§11.5.8). #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CredentialMgmtParam { + /// Sub-command to execute (see [`CredentialMgmtSubCommand`]). SubCommand = 0x01, + /// Sub-command parameters (CBOR map). SubCommandParams = 0x02, + /// PIN/UV protocol version. PinUvAuthProtocol = 0x03, + /// HMAC for authentication. PinUvAuthParam = 0x04, } +/// CBOR map keys for `authenticatorCredentialManagement` responses (§11.5.8). #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CredentialMgmtResponseParam { + /// Relying party object. Rp = 0x03, + /// SHA-256 hash of the RP ID. RpIdHash = 0x04, + /// Total number of RPs stored. TotalRps = 0x05, + /// User object. User = 0x06, + /// Credential descriptor. CredentialId = 0x07, + /// Credential public key (COSE_Key). PublicKey = 0x08, + /// Total credentials for the current RP. TotalCredentials = 0x09, } +/// Sub-command parameters for `authenticatorConfig` (§11.5.10). #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ConfigSubCommandParam { + /// New minimum PIN length. NewMinPinLength = 0x01, + /// RP IDs allowed to read the minimum PIN length. MinPinLengthRPIDs = 0x02, + /// Force PIN change on next use. ForceChangePin = 0x03, } -#[repr(u64)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum VendorConfigCommand { - AuthEncryptionEnable = 0x03e43f56b34285e2, - AuthEncryptionDisable = 0x1831a40f04a25ed9, - EnterpriseAttestationUpload = 0x66f2a674c29a8dcf, - PinComplexityPolicy = 0x6c07d70fe96c3897, - PhysicalVidPid = 0x6fcb19b0cbe3acfa, - PhysicalLedBrightness = 0x76a85945985d02fd, - PhysicalLedGpio = 0x7b392a394de9f948, - PhysicalOptions = 0x269f3b09eceb805f, -} - -impl VendorConfigCommand { - pub fn from_u64(val: u64) -> Option { - match val { - 0x03e43f56b34285e2 => Some(Self::AuthEncryptionEnable), - 0x1831a40f04a25ed9 => Some(Self::AuthEncryptionDisable), - 0x66f2a674c29a8dcf => Some(Self::EnterpriseAttestationUpload), - 0x6c07d70fe96c3897 => Some(Self::PinComplexityPolicy), - 0x6fcb19b0cbe3acfa => Some(Self::PhysicalVidPid), - 0x76a85945985d02fd => Some(Self::PhysicalLedBrightness), - 0x7b392a394de9f948 => Some(Self::PhysicalLedGpio), - 0x269f3b09eceb805f => Some(Self::PhysicalOptions), - _ => None, - } - } -} - -#[repr(u64)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum FidoCertification { - AuthEncryption = 0x03E43F56B34285E2, - AuthEncryptionLock = 0x1831A40F04A25ED9, - EnterpriseAttestation = 0x66F2A674C29A8DCF, - PinComplexity = 0x6C07D70FE96C3897, - PhysicalVidPid = 0x6FCB19B0CBE3ACFA, - LedBrightness = 0x76A85945985D02FD, - LedGpio = 0x7B392A394DE9F948, - PhysicalOptions = 0x269F3B09ECEB805F, -} - -impl FidoCertification { - pub fn from_u64(val: u64) -> Option { - match val { - 0x03E43F56B34285E2 => Some(Self::AuthEncryption), - 0x1831A40F04A25ED9 => Some(Self::AuthEncryptionLock), - 0x66F2A674C29A8DCF => Some(Self::EnterpriseAttestation), - 0x6C07D70FE96C3897 => Some(Self::PinComplexity), - 0x6FCB19B0CBE3ACFA => Some(Self::PhysicalVidPid), - 0x76A85945985D02FD => Some(Self::LedBrightness), - 0x7B392A394DE9F948 => Some(Self::LedGpio), - 0x269F3B09ECEB805F => Some(Self::PhysicalOptions), - _ => None, - } - } - - pub fn from_str(val: &str) -> Option { - let val = val.strip_prefix("0x").unwrap_or(val); - u64::from_str_radix(val, 16).ok().and_then(Self::from_u64) - } -} - -impl fmt::Display for FidoCertification { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::AuthEncryption => write!(f, "Auth Encryption"), - Self::AuthEncryptionLock => write!(f, "Auth Encryption (Lock)"), - Self::EnterpriseAttestation => write!(f, "Enterprise Attestation"), - Self::PinComplexity => write!(f, "PIN Complexity"), - Self::PhysicalVidPid => write!(f, "Physical VID/PID"), - Self::LedBrightness => write!(f, "LED Brightness"), - Self::LedGpio => write!(f, "LED GPIO"), - Self::PhysicalOptions => write!(f, "Physical Options"), - } - } -} - -impl fmt::Display for VendorConfigCommand { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::AuthEncryptionEnable => write!(f, "AuthEncryptionEnable"), - Self::AuthEncryptionDisable => write!(f, "AuthEncryptionDisable"), - Self::EnterpriseAttestationUpload => write!(f, "EnterpriseAttestationUpload"), - Self::PinComplexityPolicy => write!(f, "PinComplexityPolicy"), - Self::PhysicalVidPid => write!(f, "PhysicalVidPid"), - Self::PhysicalLedBrightness => write!(f, "PhysicalLedBrightness"), - Self::PhysicalLedGpio => write!(f, "PhysicalLedGpio"), - Self::PhysicalOptions => write!(f, "PhysicalOptions"), - } - } -} - +/// Control byte for U2F Authenticate (check-only vs. enforce presence). #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum BackupSubCommand { - GetEncryptedBackup = 0x01, - RestoreEncryptedBackup = 0x02, +pub enum AuthenticateControl { + /// Require user presence test. + EnforceUserPresence = 0x03, + /// Check if key handle is valid (no user interaction). + CheckOnly = 0x07, } -#[repr(u8)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum MseSubCommand { - KeyAgreement = 0x01, -} - -#[repr(u8)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum EnterpriseAttestationSubCommand { - GenerateCsr = 0x01, -} - -#[repr(u8)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum PhysicalOptionsSubCommand { - GetOptions = 0x01, -} - -#[repr(u8)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum MemorySubCommand { - GetStats = 0x01, -} - -#[repr(u8)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum MemoryResponseKey { - FreeSpace = 0x01, - UsedSpace = 0x02, - TotalSpace = 0x03, - NumFiles = 0x04, - FlashSize = 0x05, -} +// ── Bitflags (§11.3.2, §11.5.x) ──────────────────────────────────────────── +/// Permission bits for `getPinUvAuthTokenUsingPinWithPermissions` (§11.5.4.1). +/// +/// The platform requests specific permissions when obtaining a PIN/UV token. +/// The authenticator gates access to sensitive operations behind these flags. bitflags::bitflags! { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct PinUvAuthTokenPermissions: u8 { + /// Permission to create credentials (MakeCredential). const MAKE_CREDENTIAL = 0x01; + /// Permission to generate assertions (GetAssertion). const GET_ASSERTION = 0x02; + /// Permission to enumerate/delete credentials. const CREDENTIAL_MANAGEMENT = 0x04; + /// Permission for biometric enrollment. const BIO_ENROLLMENT = 0x08; + /// Permission to write to large blob storage. const LARGE_BLOB_WRITE = 0x10; + /// Permission to modify authenticator config (enterprise attestation, min PIN). const AUTHENTICATOR_CONFIG = 0x20; + /// Read-only credential management (no delete/update). const PER_CREDENTIAL_MGMT_READONLY = 0x40; } } +/// Flags byte in CTAP2 response messages (§11.3.2). +/// +/// Indicates the authenticator's state after processing a command. bitflags::bitflags! { pub struct AuthenticatorFlags: u8 { + /// User presence was tested and confirmed. const USER_PRESENT = 0x01; + /// User verification (biometric or PIN) was performed. const USER_VERIFIED = 0x04; + /// Response includes attested credential data. const ATTESTED_CREDENTIAL_DATA = 0x40; + /// Response includes extension output data. const EXTENSION_DATA = 0x80; } } +/// Options that can be passed in `MakeCredential` or `GetAssertion` (§11.5.1/2). bitflags::bitflags! { pub struct AuthenticatorOptions: u8 { + /// Request enterprise attestation (MakeCredential only). const ENTERPRISE_ATTESTATION = 0x01; + /// Require user verification (PIN or biometric). const USER_VERIFICATION = 0x02; } } +// ── COSE key types (RFC 8152) ─────────────────────────────────────────────── + +/// COSE algorithm identifiers (IANA COSE Algorithms registry). +/// +/// Used in `pubKeyCredParams` to specify which signature algorithms +/// the platform supports. The authenticator picks the first match. #[repr(i32)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CoseAlgorithm { + /// ECDSA with P-256 and SHA-256 (most common for WebAuthn). ES256 = -7, + /// EdDSA with Ed25519. EdDSA = -8, + /// ECDSA with P-256 (alternate ID, same as ES256). ESP256 = -9, + /// EdDSA with Ed25519 (alternate ID). Ed25519 = -19, + /// ECDH-ES with HKDF-256 key agreement. EcdhEsHkdf256 = -25, + /// ECDSA with P-384 and SHA-384. ES384 = -35, + /// ECDSA with P-521 and SHA-512. ES512 = -36, + /// ECDSA with secp256k1 and SHA-256 (Bitcoin curve). ES256K = -47, + /// ECDSA with P-384 (alternate ID). ESP384 = -51, + /// ECDSA with P-521 (alternate ID). ESP512 = -52, + /// EdDSA with Ed448. Ed448 = -53, + /// RSASSA-PKCS1-v1_5 with SHA-256. RS256 = -257, + /// RSASSA-PKCS1-v1_5 with SHA-384. RS384 = -258, + /// RSASSA-PKCS1-v1_5 with SHA-512. RS512 = -259, + /// ECDSA with brainpool256r1 and SHA-256. ESB256 = -265, + /// ECDSA with brainpool384r1 and SHA-384. ESB384 = -267, + /// ECDSA with brainpool512r1 and SHA-512. ESB512 = -268, } impl CoseAlgorithm { + /// Convert a raw i128 (from CBOR) to a [`CoseAlgorithm`]. pub fn from_i128(val: i128) -> Option { match val as i32 { -7 => Some(Self::ES256), @@ -418,99 +441,456 @@ impl fmt::Display for CoseAlgorithm { } } +/// COSE elliptic curve identifiers (RFC 8152 §13.1.1). #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CoseCurve { + /// NIST P-256 (secp256r1, prime256v1). P256 = 1, + /// NIST P-384 (secp384r1). P384 = 2, + /// NIST P-521 (secp521r1). P521 = 3, + /// X25519 for key agreement. X25519 = 4, + /// X448 for key agreement. X448 = 5, + /// Ed25519 for signing. Ed25519 = 6, + /// Ed448 for signing. Ed448 = 7, + /// secp256k1 (Bitcoin/Ethereum curve). P256K1 = 8, + /// BrainpoolP256R1. BP256R1 = 9, + /// BrainpoolP384R1. BP384R1 = 10, + /// BrainpoolP512R1. BP512R1 = 11, } +/// COSE key parameter identifiers (RFC 8152 §7.1). #[repr(i32)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CoseKeyParam { + /// Key type (OKP, EC2, RSA, etc.). Kty = 1, + /// Key identifier. Kid = 2, + /// Algorithm identifier. Alg = 3, + /// Key operations (sign, verify, encrypt, etc.). KeyOps = 4, + /// Base IV for symmetric operations. BaseIV = 5, + /// Elliptic curve identifier. Crv = -1, + /// X coordinate (EC2) or public key bytes (OKP). X = -2, + /// Y coordinate (EC2). Y = -3, + /// Private key (EC2 or OKP). D = -4, } +// ── CTAP2 errors (§8.2) ──────────────────────────────────────────────────── + +/// CTAP2 error codes (§8.2). +/// +/// Returned as the first byte of a `CTAPHID_CBOR` response when the +/// status code is non-zero. Negative status indicates an error. #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Ctap2Error { + /// Operation completed successfully. Success = 0x00, + /// CBOR value has an unexpected type. CborUnexpectedType = 0x11, + /// CBOR structure is malformed. InvalidCbor = 0x12, + /// A required parameter is missing. MissingParameter = 0x14, + /// A limit has been exceeded (e.g., too many credentials). LimitExceeded = 0x15, + /// Internal fingerprint database is full. FpDatabaseFull = 0x17, + /// Large blob storage is full. LargeBlobStorageFull = 0x18, + /// Credential already exists (exclusion list match). CredentialExcluded = 0x19, + /// Operation is still processing. Processing = 0x21, + /// Credential ID is invalid or not found. InvalidCredential = 0x22, + /// User action (touch) is pending. UserActionPending = 0x23, + /// Another operation is in progress. OperationPending = 0x24, + /// No more operations to process. NoOperations = 0x25, + /// Algorithm not supported by the authenticator. UnsupportedAlgorithm = 0x26, + /// Operation was denied (user declined or policy). OperationDenied = 0x27, + /// Key store is full. KeyStoreFull = 0x28, + /// Option not recognized or not supported. UnsupportedOption = 0x2B, + /// Option value is invalid. InvalidOption = 0x2C, + /// Keepalive was cancelled. KeepaliveCancel = 0x2D, + /// No matching credentials found. NoCredentials = 0x2E, + /// User action timed out. UserActionTimeout = 0x2F, + /// Operation not allowed (e.g., reset not within power cycle). NotAllowed = 0x30, + /// PIN is invalid. PinInvalid = 0x31, + /// PIN is blocked (too many failed attempts). PinBlocked = 0x32, + /// PIN authentication token is invalid. PinAuthInvalid = 0x33, + /// PIN authentication is blocked. PinAuthBlocked = 0x34, + /// PIN has not been set. PinNotSet = 0x35, + /// PIN/UV auth token required but not provided. PuatRequired = 0x36, + /// PIN policy violation (e.g., min length not met). PinPolicyViolation = 0x37, + /// Request payload is too large. RequestTooLarge = 0x39, + /// Action timed out. ActionTimeout = 0x3A, + /// User presence (touch) required. UpRequired = 0x3B, + /// User verification is blocked. UvBlocked = 0x3C, + /// Cryptographic integrity check failed. IntegrityFailure = 0x3D, + /// Sub-command not recognized. InvalidSubcommand = 0x3E, + /// User verification is invalid. UvInvalid = 0x3F, + /// Requested permission not authorized. UnauthorizedPermission = 0x40, } +// ══════════════════════════════════════════════════════════════════════════════ +// PICO-FIDO VENDOR EXTENSIONS +// ══════════════════════════════════════════════════════════════════════════════ +// +// The following types are NOT part of the CTAP2 standard. They are custom +// extensions used by the pico-fido firmware (and RS-Key, which shares the +// same vendor command surface). + +/// Pico-fido vendor commands sent via `CTAPHID_MSG` (not CBOR). +/// +/// These are raw byte commands, not CTAP2-standard. Each sub-command +/// carries its own TLV or binary payload. RS-Key implements the same +/// command set for compatibility. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum VendorCommand { + /// Encrypted backup / restore operations. + Backup = 0x01, + /// Manage security environment (key agreement). + ManageSecurityEnvironment = 0x02, + /// Unlock a locked device. + Unlock = 0x03, + /// Enterprise attestation CSR generation. + EnterpriseAttestation = 0x04, + /// Physical options (LED, power, etc.) — legacy TLV encoding. + PhysicalOptions = 0x05, + /// Flash memory statistics (free/used/total). + Memory = 0x06, +} + +/// Pico-fido vendor config command IDs (64-bit). +/// +/// These are sent via `authenticatorConfig` → `VendorPrototype` (0xFF) +/// sub-command. Each ID identifies a specific hardware configuration +/// operation (LED, VID/PID, encryption, etc.). +/// +/// RS-Key uses the same command IDs for compatibility. +#[repr(u64)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum VendorConfigCommand { + /// Enable authenticated encryption for secure communication. + AuthEncryptionEnable = 0x03e43f56b34285e2, + /// Disable authenticated encryption. + AuthEncryptionDisable = 0x1831a40f04a25ed9, + /// Upload enterprise attestation certificate. + EnterpriseAttestationUpload = 0x66f2a674c29a8dcf, + /// Configure PIN complexity policy. + PinComplexityPolicy = 0x6c07d70fe96c3897, + /// Set USB Vendor ID and Product ID. + PhysicalVidPid = 0x6fcb19b0cbe3acfa, + /// Set LED brightness level. + PhysicalLedBrightness = 0x76a85945985d02fd, + /// Set LED GPIO pin assignment. + PhysicalLedGpio = 0x7b392a394de9f948, + /// Physical options bitmask (dimmable, power-reset, steady LED). + PhysicalOptions = 0x269f3b09eceb805f, +} + +impl VendorConfigCommand { + /// Convert a raw 64-bit value to a [`VendorConfigCommand`]. + pub fn from_u64(val: u64) -> Option { + match val { + 0x03e43f56b34285e2 => Some(Self::AuthEncryptionEnable), + 0x1831a40f04a25ed9 => Some(Self::AuthEncryptionDisable), + 0x66f2a674c29a8dcf => Some(Self::EnterpriseAttestationUpload), + 0x6c07d70fe96c3897 => Some(Self::PinComplexityPolicy), + 0x6fcb19b0cbe3acfa => Some(Self::PhysicalVidPid), + 0x76a85945985d02fd => Some(Self::PhysicalLedBrightness), + 0x7b392a394de9f948 => Some(Self::PhysicalLedGpio), + 0x269f3b09eceb805f => Some(Self::PhysicalOptions), + _ => None, + } + } +} + +/// Certification identifiers reported by pico-fido/RS-Key firmware. +/// +/// These share the same 64-bit IDs as [`VendorConfigCommand`] but are +/// used in the `GetInfo` certifications map to indicate which features +/// the device has been certified for. +#[repr(u64)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FidoCertification { + /// Authenticated encryption enabled and certified. + AuthEncryption = 0x03E43F56B34285E2, + /// Authenticated encryption locked (cannot be disabled). + AuthEncryptionLock = 0x1831A40F04A25ED9, + /// Enterprise attestation certified. + EnterpriseAttestation = 0x66F2A674C29A8DCF, + /// PIN complexity policy enforced. + PinComplexity = 0x6C07D70FE96C3897, + /// Physical VID/PID configuration certified. + PhysicalVidPid = 0x6FCB19B0CBE3ACFA, + /// LED brightness control certified. + LedBrightness = 0x76A85945985D02FD, + /// LED GPIO assignment certified. + LedGpio = 0x7B392A394DE9F948, + /// Physical options (dimmable, power-reset, steady) certified. + PhysicalOptions = 0x269F3B09ECEB805F, +} + +impl FidoCertification { + /// Convert a raw 64-bit value to a [`FidoCertification`]. + pub fn from_u64(val: u64) -> Option { + match val { + 0x03E43F56B34285E2 => Some(Self::AuthEncryption), + 0x1831A40F04A25ED9 => Some(Self::AuthEncryptionLock), + 0x66F2A674C29A8DCF => Some(Self::EnterpriseAttestation), + 0x6C07D70FE96C3897 => Some(Self::PinComplexity), + 0x6FCB19B0CBE3ACFA => Some(Self::PhysicalVidPid), + 0x76A85945985D02FD => Some(Self::LedBrightness), + 0x7B392A394DE9F948 => Some(Self::LedGpio), + 0x269F3B09ECEB805F => Some(Self::PhysicalOptions), + _ => None, + } + } + + /// Parse a hex string (with or without `0x` prefix) to a [`FidoCertification`]. + pub fn from_str(val: &str) -> Option { + let val = val.strip_prefix("0x").unwrap_or(val); + u64::from_str_radix(val, 16).ok().and_then(Self::from_u64) + } +} + +impl fmt::Display for FidoCertification { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::AuthEncryption => write!(f, "Auth Encryption"), + Self::AuthEncryptionLock => write!(f, "Auth Encryption (Lock)"), + Self::EnterpriseAttestation => write!(f, "Enterprise Attestation"), + Self::PinComplexity => write!(f, "PIN Complexity"), + Self::PhysicalVidPid => write!(f, "Physical VID/PID"), + Self::LedBrightness => write!(f, "LED Brightness"), + Self::LedGpio => write!(f, "LED GPIO"), + Self::PhysicalOptions => write!(f, "Physical Options"), + } + } +} + +impl fmt::Display for VendorConfigCommand { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::AuthEncryptionEnable => write!(f, "AuthEncryptionEnable"), + Self::AuthEncryptionDisable => write!(f, "AuthEncryptionDisable"), + Self::EnterpriseAttestationUpload => write!(f, "EnterpriseAttestationUpload"), + Self::PinComplexityPolicy => write!(f, "PinComplexityPolicy"), + Self::PhysicalVidPid => write!(f, "PhysicalVidPid"), + Self::PhysicalLedBrightness => write!(f, "PhysicalLedBrightness"), + Self::PhysicalLedGpio => write!(f, "PhysicalLedGpio"), + Self::PhysicalOptions => write!(f, "PhysicalOptions"), + } + } +} + +// ── Vendor sub-commands ───────────────────────────────────────────────────── + +/// CBOR map keys for pico-fido vendor prototype commands. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum VendorParam { + /// Vendor command identifier (64-bit). + VendorCommand = 0x01, + /// Nested vendor sub-parameters. + VendorSubParams = 0x02, + /// PIN/UV protocol version. + PinUvAuthProtocol = 0x03, + /// HMAC for authentication. + PinUvAuthParam = 0x04, +} + +/// Sub-parameter keys inside vendor prototype payloads. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum VendorSubParam { + /// Raw vendor parameter value. + VendorParam = 0x01, + /// COSE-encoded public key. + CoseKey = 0x02, + /// Integer vendor parameter. + VendorParamInt = 0x03, + /// Text vendor parameter. + VendorParamText = 0x04, +} + +/// Backup sub-commands (encrypted backup/restore). +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BackupSubCommand { + /// Export an encrypted backup blob. + GetEncryptedBackup = 0x01, + /// Import and restore an encrypted backup. + RestoreEncryptedBackup = 0x02, +} + +/// Manage Security Environment sub-commands. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MseSubCommand { + /// Perform ECDH key agreement for secure channel setup. + KeyAgreement = 0x01, +} + +/// Enterprise attestation sub-commands. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EnterpriseAttestationSubCommand { + /// Generate a Certificate Signing Request. + GenerateCsr = 0x01, +} + +/// Physical options sub-commands (legacy vendor command 0x05). +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PhysicalOptionsSubCommand { + /// Read the current physical options bitmask. + GetOptions = 0x01, +} + +/// Memory sub-commands (vendor command 0x06). +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MemorySubCommand { + /// Get flash memory usage statistics. + GetStats = 0x01, +} + +/// Response keys for `Memory::GetStats`. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MemoryResponseKey { + /// Free space in bytes. + FreeSpace = 0x01, + /// Used space in bytes. + UsedSpace = 0x02, + /// Total flash capacity in bytes. + TotalSpace = 0x03, + /// Number of stored files/credentials. + NumFiles = 0x04, + /// Raw flash chip size. + FlashSize = 0x05, +} + +// ══════════════════════════════════════════════════════════════════════════════ +// RS-KEY SPECIFIC EXTENSIONS +// ══════════════════════════════════════════════════════════════════════════════ +// +// The following constants are specific to RS-Key firmware. Some overlap with +// pico-fido (RS-Key is a rust rewrite of pico-fido), but the Rescue applet AIDs and CTAPHID 0x41 +// vendor command are RS-Key additions. + +/// Vendor CBOR command opcode (pico-fido/RS-Key extension). +/// +/// Sent as the first byte of a `CTAPHID_CBOR` payload to invoke +/// vendor-specific CBOR-encoded commands. pub const CTAP_VENDOR_CBOR_CMD: u8 = 0xC1; + +/// Vendor config command opcode (pico-fido/RS-Key extension). +/// +/// Sent as the first byte of a `CTAPHID_CBOR` payload for +/// `authenticatorConfig` vendor prototype commands. pub const CTAP_VENDOR_CONFIG_CMD: u8 = 0xC2; +/// RS-Key CTAPHID vendor command (0x41). +/// +/// Carries CBOR-encoded sub-commands for seed backup, attestation, +/// and audit operations. This is RS-Key specific and not part of pico-fido. +/// +/// See [RS-Key protocol §9](https://themaxmur.github.io/RS-Key/develop/) for details. +pub const RSKEY_CTAPHID_VENDOR_CMD: u8 = 0x41; + +// ══════════════════════════════════════════════════════════════════════════════ +// SHARED PROTOCOL CONSTANTS +// ══════════════════════════════════════════════════════════════════════════════ + +/// Size of the relying party identifier hash (SHA-256). pub const CTAP_APPID_SIZE: usize = 32; +/// Size of the challenge hash (SHA-256). pub const CTAP_CHAL_SIZE: usize = 32; +/// Size of an EC public key coordinate (P-256). pub const CTAP_EC_KEY_SIZE: usize = 32; +/// Size of an uncompressed EC public key point (0x04 + X + Y). pub const CTAP_EC_POINT_SIZE: usize = 65; +/// Maximum key handle size stored on the device. pub const CTAP_MAX_KH_SIZE: usize = 128; +/// Default key handle length for credential serialization. pub const KEY_HANDLE_LEN: usize = 64; +/// Maximum EC signature size (DER-encoded). pub const CTAP_MAX_EC_SIG_SIZE: usize = 72; +/// Size of the transaction counter field. pub const CTAP_CTR_SIZE: usize = 4; +/// Maximum number of PIN entry attempts before lockout. pub const MAX_PIN_RETRIES: u8 = 8; +/// Maximum credentials returned in a single `GetAssertion` response. pub const MAX_CREDENTIAL_COUNT_IN_LIST: usize = 16; +/// Maximum credential ID length in bytes. pub const MAX_CRED_ID_LENGTH: usize = 1024; +/// Maximum number of discoverable (resident) credentials. pub const MAX_RESIDENT_CREDENTIALS: usize = 256; +/// Maximum length of a credential blob extension. pub const MAX_CREDBLOB_LENGTH: usize = 128; +/// Maximum CTAP2 message size in bytes. pub const MAX_MSG_SIZE: usize = 1024; +/// Maximum fragment size (message size minus CTAPHID header). pub const MAX_FRAGMENT_LENGTH: usize = MAX_MSG_SIZE - 64; +/// Maximum large blob array size in bytes. pub const MAX_LARGE_BLOB_SIZE: usize = 2048; +/// Default AAGUID for pico-fido firmware. +/// +/// Used to identify the authenticator model. Compare against +/// [`super::super::types::PICOFIDO_AAGUID`] or +/// [`super::super::types::RSKEY_AAGUID`] to determine firmware type. pub const AAGUID: [u8; 16] = [ 0x89, 0xFB, 0x94, 0xB7, 0x06, 0xC9, 0x36, 0x73, 0x9B, 0x7E, 0x30, 0x52, 0x6D, 0x96, 0x81, 0x45, ]; diff --git a/src/device/fido/hid.rs b/src/device/fido/hid.rs index 9f1599c..1f08303 100644 --- a/src/device/fido/hid.rs +++ b/src/device/fido/hid.rs @@ -1,3 +1,87 @@ +//! USB HID transport for CTAP2/FIDO2 communication. +//! +//! # What is HID? +//! +//! USB HID (Human Interface Device) is a standard USB device class for input +//! devices like keyboards, mice, and gamepads. HID devices communicate through +//! *reports* — fixed-size packets sent/received on USB endpoints. The OS +//! auto-detects HID devices without requiring custom drivers, making it ideal +//! for FIDO2 security keys that need to work across platforms. +//! +//! # What is CTAPHID? +//! +//! CTAPHID is the [CTAP2] transport binding for USB HID. It layers the CTAP2 +//! protocol on top of HID reports, allowing FIDO2 authenticators to +//! communicate with hosts through the standard HID driver stack. The +//! specification is defined in [CTAP2 §11.2](https://fidoalliance.org/specs/fido-v2.3-ps-20260226/fido-client-to-authenticator-protocol-v2.3-ps-20260226.html#usb-human-interface-device-hid). +//! +//! # Framing protocol +//! +//! CTAPHID uses 64-byte HID reports. Messages that exceed 64 bytes are split +//! across multiple packets: +//! +//! ```text +//! Init Packet (64 bytes): +//! CID(4) | CMD(1) | BCNT_HI(1) | BCNT_LO(1) | payload[..57] +//! +//! Continuation Packets: +//! CID(4) | SEQ(1) | payload[..59] +//! ``` +//! +//! - **CID** (Channel ID): 4-byte identifier negotiated via `CTAPHID_INIT`. +//! Multiplexes multiple logical channels on one HID device. +//! - **CMD**: Command byte (e.g., `0x90` for CBOR, `0x86` for INIT). +//! - **BCNT**: 16-bit big-endian payload length. +//! - **SEQ**: Sequence number for continuation packets (starts at 0). +//! +//! # Channel initialization +//! +//! Before any CTAP2 command can be sent, the host must negotiate a Channel ID: +//! +//! 1. Host sends `CTAPHID_INIT` to the broadcast CID (`0xFFFFFFFF`) with a +//! random 8-byte nonce. +//! 2. Device responds with the same nonce and a newly allocated CID. +//! 3. All subsequent communication uses this CID. +//! +//! This allows multiple CTAP2 sessions to coexist on one device (e.g., two +//! browsers open simultaneously). +//! +//! # Cryptographic operations +//! +//! PIN operations require ECDH key agreement and AES-256-CBC encryption: +//! +//! ```text +//! 1. Host → Device: GetKeyAgreement (returns device's P-256 public key) +//! 2. Host generates ephemeral P-256 key pair +//! 3. Host computes ECDH shared secret → SHA-256(shared_secret) +//! 4. PIN hash encrypted with AES-256-CBC (key = shared_secret, IV = 0) +//! 5. Token decrypted with same key +//! ``` +//! +//! The shared secret is derived as `SHA-256(ECDH_x_coordinate)`. +//! +//! # Firmware compatibility +//! +//! Both [pico-fido] and [RS-Key] implement CTAPHID. This module handles: +//! - Standard CTAP2 commands (GetInfo, MakeCredential, GetAssertion, etc.) +//! - Pico-fido vendor commands (`0xC1`, `0xC2`) for hardware config +//! - RS-Key vendor command (`0x41`) for seed backup and attestation +//! +//! # File structure +//! +//! - [`HidTransport`] — main transport struct; opens HID device, negotiates +//! CID, sends/receives CBOR payloads +//! - [`EnumerateRpResponse`], [`EnumerateCredentialResponse`] — response +//! types for credential management enumeration +//! - PIN methods (`get_pin_token`, `set_pin`, `change_pin`) implement the +//! full ECDH + AES-CBC flow per CTAP2 §11.5.4 +//! - Vendor methods (`send_vendor_config`, `get_enterprise_attestation_csr`) +//! handle pico-fido/RS-Key specific extensions +//! +//! [CTAP2]: https://fidoalliance.org/specs/fido-v2.3-ps-20260226/fido-client-to-authenticator-protocol-v2.3-ps-20260226.html +//! [pico-fido]: https://github.com/polhenarejos/pico-fido +//! [RS-Key]: https://github.com/TheMaxMur/RS-Key + use cbc::cipher::{Block, BlockModeDecrypt, BlockModeEncrypt, KeyIvInit, block_padding::NoPadding}; use rand::RngExt; use ring::{agreement, digest, hmac}; @@ -8,22 +92,70 @@ use std::time::Duration; use crate::device::fido::constants::*; use crate::error::PFError; -// HID Transport Constants +/// Size of a single USB HID report in bytes (CTAP2 §11.2 mandates 64-byte reports). const HID_REPORT_SIZE: usize = 64; + +/// FIDO Alliance HID Usage Page identifier. +/// +/// Devices advertising this usage page in their HID descriptor are identified +/// as FIDO authenticators by the operating system's HID enumeration. const HID_USAGE_PAGE_FIDO: u16 = 0xF1D0; + +/// Broadcast Channel ID used for the initial CTAPHID_INIT handshake. +/// +/// The host sends an INIT command to this CID to request a unique Channel ID +/// from the authenticator. All subsequent communication uses the negotiated CID. const CTAPHID_CID_BROADCAST: u32 = 0xFFFFFFFF; + +/// CTAPHID INIT command byte (0x86). +/// +/// Initiates channel negotiation. The host sends a random 8-byte nonce; the +/// device responds with the same nonce and a newly allocated Channel ID. const CTAPHID_INIT: u8 = 0x86; + +/// CTAPHID CBOR command byte (0x90). +/// +/// Wraps a CTAP2 CBOR-encoded command or response payload. The payload is +/// fragmented across one init packet and zero or more continuation packets. pub const CTAPHID_CBOR: u8 = 0x90; + +/// CTAPHID ERROR response byte (0xBF). +/// +/// Indicates the authenticator encountered an error processing the command. +/// The next byte contains the CTAP2 error code. const CTAPHID_ERROR: u8 = 0xBF; + +/// CTAPHID KEEPALIVE status byte (0xBB). +/// +/// Sent by the authenticator while processing a long-running operation (e.g., +/// MakeCredential with user interaction). The host must continue reading +/// until it receives the final CBOR or ERROR response. const CTAPHID_KEEPALIVE: u8 = 0xBB; -// Timeouts +/// Default timeout in milliseconds for draining stale HID packets. const HID_READ_TIMEOUT_MS: i32 = 10; + +/// Timeout in milliseconds for reading the CTAPHID_INIT response during channel negotiation. const HID_INIT_READ_TIMEOUT_MS: i32 = 100; + +/// Timeout in milliseconds for reading a single HID response packet (excluding keepalives). const HID_RESP_READ_TIMEOUT_MS: i32 = 2000; + +/// Timeout in milliseconds for reading CTAPHID continuation packets. const HID_CONT_READ_TIMEOUT_MS: i32 = 500; + +/// Maximum total time in milliseconds allowed for a complete CBOR command/response exchange. const HID_TOTAL_TIMEOUT_MS: i32 = 5000; +/// USB HID transport for CTAP2/FIDO2 communication. +/// +/// Wraps a `hidapi::HidDevice` and manages the CTAPHID framing layer: +/// channel negotiation (INIT), multi-packet CBOR send/receive, keepalive +/// handling, and all higher-level CTAP2 operations (PIN, credential management, +/// vendor commands). +/// +/// Created via [`HidTransport::open`], which scans for a device with the FIDO +/// HID Usage Page (0xF1D0) and performs the INIT handshake to obtain a Channel ID. pub struct HidTransport { device: hidapi::HidDevice, cid: u32, @@ -32,6 +164,10 @@ pub struct HidTransport { pub product_name: String, } +/// Response from enumerating a Relying Party via credential management. +/// +/// Returned by [`HidTransport::credential_management_enumerate_rps`]. Each entry +/// represents one RP stored on the authenticator. #[derive(Debug, Clone)] pub struct EnumerateRpResponse { pub rp: Value, @@ -40,6 +176,10 @@ pub struct EnumerateRpResponse { pub total_rps: Option, } +/// Response from enumerating a credential via credential management. +/// +/// Returned by [`HidTransport::credential_management_enumerate_credentials`]. +/// Each entry represents one credential (public key) registered under an RP. #[derive(Debug, Clone)] pub struct EnumerateCredentialResponse { pub user: Value, @@ -51,6 +191,11 @@ pub struct EnumerateCredentialResponse { } impl HidTransport { + /// Open the first available FIDO HID device and negotiate a Channel ID. + /// + /// Scans for a device with HID Usage Page `0xF1D0`, opens it, and performs + /// the CTAPHID_INIT handshake. Returns an error if no device is found or + /// the INIT handshake times out. pub fn open() -> Result { log::info!("Attempting to open HID transport for FIDO device..."); let api = hidapi::HidApi::new().map_err(|e| { @@ -101,6 +246,11 @@ impl HidTransport { }) } + /// Negotiate a CTAPHID Channel ID via CTAPHID_INIT. + /// + /// Sends an INIT command to the broadcast CID (`0xFFFFFFFF`) with a random + /// 8-byte nonce, then reads the response to extract the allocated CID. + /// Drains any stale packets before the handshake to avoid confusion. fn init_channel(device: &hidapi::HidDevice) -> Result { log::debug!("Initializing CTAPHID channel..."); @@ -162,10 +312,18 @@ impl HidTransport { )) } + /// Send a CTAP2 CBOR command and wait for the response using the default timeout. + /// + /// Convenience wrapper around [`send_cbor_with_timeout`](HidTransport::send_cbor_with_timeout). pub fn send_cbor(&self, cmd: u8, payload: &[u8]) -> Result, PFError> { self.send_cbor_with_timeout(cmd, payload, HID_TOTAL_TIMEOUT_MS) } + /// Send a CTAP2 CBOR command and wait for the response with a custom timeout. + /// + /// Fragments `payload` into CTAPHID init + continuation packets, then reads + /// and reassembles the response. The `timeout_ms` parameter overrides the + /// default for the read phase (useful for operations that require user interaction). pub fn send_cbor_with_timeout( &self, cmd: u8, @@ -176,11 +334,20 @@ impl HidTransport { self.read_cbor_response(cmd, timeout_ms) } + /// Send a CTAP2 CBOR command and return the raw HID response without status-byte parsing. + /// + /// Unlike [`send_cbor`](HidTransport::send_cbor), this does not check the CTAP status byte + /// or strip it from the response. Useful for vendor commands that return non-standard payloads. pub fn send_raw(&self, cmd: u8, payload: &[u8]) -> Result, PFError> { self.write_cbor_request(cmd, payload)?; self.read_hid_response(cmd, HID_TOTAL_TIMEOUT_MS) } + /// Send the CTAP authenticatorReset command (0x07). + /// + /// Resets the authenticator to its factory state: all credentials, PINs, + /// and configuration are erased. Uses a 30-second timeout to allow for + /// any required user interaction (e.g., touch confirmation). pub fn reset(&self) -> Result<(), PFError> { log::info!("Sending CTAP authenticatorReset (0x07)..."); self.write_cbor_request(CTAPHID_CBOR, &[0x07])?; @@ -188,6 +355,11 @@ impl HidTransport { Ok(()) } + /// Fragment and write a CTAPHID request to the device. + /// + /// Encodes the command byte and payload into a CTAPHID init packet followed + /// by zero or more continuation packets, then writes each 65-byte HID report + /// (1 byte Report ID + 64 bytes payload) to the device. fn write_cbor_request(&self, cmd: u8, payload: &[u8]) -> Result<(), PFError> { log::debug!( "Sending CBOR Command: 0x{:02X}, Payload Size: {} bytes", @@ -254,6 +426,11 @@ impl HidTransport { Ok(()) } + /// Read a CTAPHID response and verify the CTAP status byte. + /// + /// Delegates to [`read_hid_response`](HidTransport::read_hid_response) for packet + /// reassembly, then checks the first byte for a non-zero CTAP status code and + /// strips it before returning the payload. fn read_cbor_response(&self, cmd: u8, timeout_ms: i32) -> Result, PFError> { let response_data = self.read_hid_response(cmd, timeout_ms)?; @@ -280,6 +457,13 @@ impl HidTransport { Ok(response_data[1..].to_vec()) } + /// Read and reassemble a CTAPHID response from the device. + /// + /// Handles the full CTAPHID receive flow: + /// 1. Reads the init packet while skipping KEEPALIVE and mismatched-CID packets. + /// 2. Validates the command byte matches the expected response. + /// 3. Reads continuation packets in sequence order until the full payload is received. + /// 4. Enforces the `timeout_ms` deadline across the entire read. fn read_hid_response(&self, cmd: u8, timeout_ms: i32) -> Result, PFError> { log::debug!("Waiting for response..."); @@ -396,6 +580,12 @@ impl HidTransport { Ok(response_data) } + /// Send a pico-fido vendor-specific authenticatorConfig command. + /// + /// Wraps the vendor command ID and parameter into the VendorPrototype + /// sub-command structure, signs it with the PIN token, and sends it as + /// a CTAP Config command. The parameter value type (bytes, integer, or text) + /// determines which CBOR key (0x02/0x03/0x04) is used. pub fn send_vendor_config( &self, pin_token: &[u8], @@ -537,6 +727,9 @@ impl HidTransport { /// CBOR map keys out of order (0x01, 0x03, 0x04, 0x02) instead of the required /// ascending order (0x01, 0x02, 0x03, 0x04). The pico-fido firmware strictly /// enforces canonical CBOR ordering per CTAP2 spec. + /// + /// Builds the authenticatorConfig CBOR map with keys in ascending order, signs + /// it with the PIN token, and sends it as a CTAP Config command. pub fn send_config( &self, sub_cmd: ConfigSubCommand, @@ -585,6 +778,10 @@ impl HidTransport { } /// Send authenticatorConfig command to enable Enterprise attestation. + /// + /// Calls the EnableEnterpriseAttestation sub-command (0x01) via [`send_config`](HidTransport::send_config). + /// Enterprise attestation allows RPs to receive a per-device attestation certificate + /// during MakeCredential, enabling enterprise device identification. pub fn send_config_enable_ea(&self, pin_token: &[u8]) -> Result<(), PFError> { log::debug!("Sending Enterprise Attestation enable config command..."); match self.send_config( @@ -608,6 +805,10 @@ impl HidTransport { } /// Send authenticatorConfig command to set minimum PIN length. + /// + /// Calls the SetMinPinLength sub-command (0x03) via [`send_config`](HidTransport::send_config). + /// The minimum PIN length can only be increased; attempting to decrease it returns + /// `PIN_POLICY_VIOLATION` (0x37). A device reset is required to lower the minimum. pub fn send_config_set_min_pin_length( &self, pin_token: &[u8], @@ -653,6 +854,11 @@ impl HidTransport { } } + /// Request the authenticator's P-256 ECDH public key for PIN protocol v1. + /// + /// Sends a `getClientPin` command with `getKeyAgreement` sub-command (0x02). + /// The returned COSE Key contains the authenticator's ephemeral public key + /// (x and y coordinates) used for ECDH key agreement in PIN operations. pub fn get_key_agreement(&self) -> Result { let mut map = BTreeMap::new(); map.insert( @@ -685,6 +891,14 @@ impl HidTransport { } } + /// Obtain an encrypted PIN token using the standard getPinToken flow. + /// + /// Implements the full CTAP2 §11.5.4 PIN token acquisition: + /// 1. Fetches the authenticator's key agreement public key. + /// 2. Generates an ephemeral P-256 key pair on the platform. + /// 3. Performs ECDH and derives `SHA-256(shared_secret)`. + /// 4. Encrypts the first 16 bytes of `SHA-256(pin)` with AES-256-CBC. + /// 5. Sends getPinToken (sub-command 0x05) and decrypts the response token. pub fn get_pin_token(&self, pin: &str) -> Result, PFError> { log::info!("Starting custom get_pin_token (Subcommand 0x05)..."); @@ -789,6 +1003,12 @@ impl HidTransport { } } + /// Obtain a PIN token with specific permissions and optional RP ID scope. + /// + /// Like [`get_pin_token`](HidTransport::get_pin_token) but uses the + /// `getPinUvAuthTokenUsingPinWithPermissions` sub-command (0x09). This allows + /// requesting only the permissions needed (e.g., `CREDENTIAL_MANAGEMENT` for + /// enumeration/deletion), following the principle of least privilege. pub fn get_pin_token_with_permission( &self, pin: &str, @@ -911,6 +1131,16 @@ impl HidTransport { } } + /// Set a new PIN on the authenticator (sub-command 0x03). + /// + /// Implements the full CTAP2 setPin flow: + /// 1. Performs ECDH key agreement to derive the shared secret. + /// 2. Encrypts the new PIN (padded to 64 bytes) with AES-256-CBC. + /// 3. Computes `HMAC-SHA-256(shared_secret, newPinEnc)[0..16]` as pinUvAuthParam. + /// 4. Sends the SetPin command with the platform's public key, encrypted PIN, and HMAC. + /// + /// The PIN must be 4–63 characters. Fails with `PIN_POLICY_VIOLATION` (0x37) if + /// the PIN is too short. pub fn set_pin(&self, new_pin: &str) -> Result<(), PFError> { log::info!("Starting custom set_pin (Subcommand 0x03)..."); @@ -1030,6 +1260,18 @@ impl HidTransport { } } + /// Change the authenticator PIN (sub-command 0x04). + /// + /// Implements the full CTAP2 changePin flow: + /// 1. Performs ECDH key agreement to derive the shared secret. + /// 2. Encrypts `SHA-256(current_pin)[0..16]` with AES-256-CBC (pinHashEnc). + /// 3. Encrypts the new PIN (padded to 64 bytes) with AES-256-CBC (newPinEnc). + /// 4. Computes `HMAC-SHA-256(shared_secret, newPinEnc || pinHashEnc)[0..16]`. + /// 5. Sends the ChangePin command. + /// + /// Returns `CTAP2_ERR_PIN_AUTH_INVALID` (0x31) if the current PIN is wrong, + /// `CTAP2_ERR_PIN_BLOCKED` (0x32) if the PIN is blocked, or + /// `CTAP2_ERR_PIN_POLICY_VIOLATION` (0x37) if the new PIN violates policy. pub fn change_pin(&self, current_pin: &str, new_pin: &str) -> Result<(), PFError> { log::info!("Starting custom change_pin (Subcommand 0x04)..."); @@ -1173,7 +1415,11 @@ impl HidTransport { } } - /// Helper to sign the authenticatorConfig command + /// Sign an authenticatorConfig command using HMAC-SHA-256. + /// + /// Computes `HMAC-SHA-256(pin_token, 0x0d || subCommand || subCommandParams)[0..16]` + /// per the CTAP2 authenticatorConfig signing specification. The 0x0d byte + /// identifies the Config command category. fn sign_config_command( &self, pin_token: &[u8], @@ -1193,6 +1439,10 @@ impl HidTransport { sig.as_ref()[0..16].to_vec() } + /// Encode an uncompressed P-256 public key as a COSE_Key map. + /// + /// Returns CBOR bytes for a map with keys: kty(1)=EC2(2), alg(3)=ES256(-7), + /// crv(-1)=P-256(1), x(-2), y(-3). Used in PIN key agreement payloads. fn encode_cose_key(&self, x: &[u8], y: &[u8]) -> Vec { let mut bytes = vec![0xA5]; // Map(5) bytes.extend(to_vec(&Value::Integer(1)).unwrap()); @@ -1208,6 +1458,11 @@ impl HidTransport { bytes } + /// Build a CBOR map for ClientPin sub-command parameters. + /// + /// Constructs the parameter map with `pinProtocol`, `subCommand`, `keyAgreement`, + /// and `pinHashEnc`. Optionally includes `permissions` and `rpId` when the + /// `getPinUvAuthTokenUsingPinWithPermissions` sub-command is used. fn encode_client_pin_params( &self, sub_cmd: ClientPinSubCommand, @@ -1243,6 +1498,14 @@ impl HidTransport { bytes } + /// Enumerate all Relying Parties stored on the authenticator. + /// + /// Performs the CTAP2 credential management enumeration flow: + /// 1. Obtains a PIN token with `CREDENTIAL_MANAGEMENT` permission. + /// 2. Sends `EnumerateRpsBegin` (sub-command 0x02) to get the first RP. + /// 3. Iterates with `EnumerateRpsGetNextRp` (sub-command 0x03) until all RPs are returned. + /// + /// Returns an empty vector if no credentials exist on the device. pub fn credential_management_enumerate_rps( &self, pin: &str, @@ -1382,6 +1645,14 @@ impl HidTransport { Ok(all_rps) } + /// Enumerate all credentials registered under a specific Relying Party. + /// + /// Given an `rp_id_hash` (SHA-256 of the RP's ID), performs: + /// 1. Obtains a PIN token with `CREDENTIAL_MANAGEMENT` permission. + /// 2. Sends `EnumerateCredentialsBegin` (sub-command 0x04) with the RP ID hash. + /// 3. Iterates with `EnumerateCredentialsGetNextCredential` (sub-command 0x05). + /// + /// Returns user info, credential ID, and public key for each credential. pub fn credential_management_enumerate_credentials( &self, pin: &str, @@ -1553,6 +1824,12 @@ impl HidTransport { Ok(all_creds) } + /// Delete a specific credential from the authenticator. + /// + /// Obtains a PIN token with `CREDENTIAL_MANAGEMENT` permission, then sends + /// the `DeleteCredential` command (sub-command 0x06) with the credential ID + /// descriptor map. The `credential_id_map` must be a CBOR map with key 0x02 + /// containing the credential ID. pub fn credential_management_delete_credential( &self, pin: &str, @@ -1607,6 +1884,13 @@ impl HidTransport { Ok(()) } + /// Sign a credential management command using HMAC-SHA-256. + /// + /// Uses pico-fido's non-standard signing scheme: for sub-commands 0x01 + /// (GetCredsMetadata) and 0x02 (EnumerateRpsBegin), only the sub-command + /// byte is signed. For all others, the sub-command byte followed by the + /// CBOR-encoded SubCommandParams is signed. Returns the first 16 bytes + /// of the HMAC digest. fn sign_credential_mgmt_command( &self, pin_token: &[u8], diff --git a/src/device/fido/mod.rs b/src/device/fido/mod.rs index d78cc46..d0b3e6c 100644 --- a/src/device/fido/mod.rs +++ b/src/device/fido/mod.rs @@ -1,3 +1,59 @@ +//! FIDO2 / CTAP2 protocol implementation for pico-fido and RS-Key firmware. +//! +//! ```text +//! fido/ +//! ├── mod.rs — high-level FIDO2 operations (info, PIN, credentials, config) +//! ├── constants.rs — CTAP2 command codes, CBOR map keys, COSE algorithms, bitflags +//! └── hid.rs — USB HID transport (CTAPHID framing, channel init, CBOR exchange) +//! ``` +//! +//! # Architecture +//! +//! Communication flows top-down: +//! +//! ```text +//! io::read_device_details() +//! │ +//! ▼ +//! fido::read_device_details() ← this file +//! │ +//! ▼ +//! HidTransport::open() ← hid.rs +//! │ +//! ▼ +//! USB HID (CTAPHID protocol) +//! ``` +//! +//! [`constants`] is imported by both `mod.rs` and `hid.rs` and should be the +//! single source of truth for every CTAP2-defined byte value. If you need to +//! add a new command, sub-command, or CBOR key, put it there. +//! +//! [`hid`] owns the raw byte-level exchange: channel ID negotiation, packet +//! framing (init + continuation packets), PIN token acquisition, ECDH key +//! agreement, and CBOR serialization. It exposes [`HidTransport`] which the +//! rest of the module uses for all device I/O. +//! +//! [`mod.rs`] contains the public functions called from [`super::io`]. +//! Each function opens an [`HidTransport`], performs the CTAP2 operation, +//! and parses the CBOR response into the structs defined in [`super::types`]. +//! +//! # Vendor extensions +//! +//! Pico-fido firmware exposes vendor-specific CTAP commands (`0xC1`, `0xC2`) +//! for hardware configuration (VID/PID, LED, memory stats). These are handled +//! through [`HidTransport::send_vendor_config`] and the +//! [`VendorConfigCommand`] enum in constants. Legacy firmware (≤7.2) uses a +//! different physical-options encoding; see `firmware_supports_legacy_fido_hardware_config`. +//! +//! # Adding a new FIDO2 operation +//! +//! 1. Add any new command/sub-command enums to [`constants`]. +//! 2. Implement the CBOR encoding and transport call in [`hid`] (if it +//! requires new framing or PIN token logic). +//! 3. Add the high-level function in this file, following the pattern: +//! open transport → build CBOR payload → send → parse response → return. +//! 4. Expose it through [`super::io`]. + pub mod constants; pub mod hid; diff --git a/src/device/io.rs b/src/device/io.rs index c52b166..79897e7 100644 --- a/src/device/io.rs +++ b/src/device/io.rs @@ -1,8 +1,20 @@ -//! Tauri Commands to interact with the pico-fido firmware via rescue and fido protocols. +//! Device I/O layer bridging rescue (pcsc) and FIDO2 protocols. +//! +//! High-level entry points for reading/writing device configuration, +//! managing credentials, and controlling LED/boot behavior. +//! +//! Functions are grouped by the protocol they use: +//! - Functions that use both rescue and FIDO (fallback/dispatch logic) +//! - Functions that communicate exclusively over the rescue (PC/SC) channel +//! - Functions that communicate exclusively over the FIDO2 channel + #![allow(unused)] use crate::{device::fido, device::rescue, device::types::*, error::PFError}; +// ── Shared: functions that use both rescue and FIDO ───────────────────────── + +/// Read full device status. Tries rescue first, falls back to FIDO on failure. pub fn read_device_details() -> Result { match rescue::read_device_details() { Ok(status) => Ok(status), @@ -13,6 +25,7 @@ pub fn read_device_details() -> Result { } } +/// Write app config. Dispatches to rescue or FIDO based on `method`. pub fn write_config( config: AppConfigInput, method: DeviceMethod, @@ -25,48 +38,24 @@ pub fn write_config( } } +// ── Rescue protocol (PC/SC) ───────────────────────────────────────────────── + +/// Lock or unlock secure boot via rescue. pub fn enable_secure_boot(lock: bool) -> Result { rescue::enable_secure_boot(lock) } -pub(crate) fn get_fido_info() -> Result { - fido::get_fido_info() -} - -pub(crate) fn change_fido_pin( - current_pin: Option, - new_pin: String, -) -> Result { - fido::change_fido_pin(current_pin, new_pin) -} - -pub(crate) fn set_min_pin_length( - current_pin: String, - min_pin_length: u8, -) -> Result { - fido::set_min_pin_length(current_pin, min_pin_length) -} - +/// Reboot the device. Pass `true` to enter BOOTSEL mode. pub fn reboot(to_bootsel: bool) -> Result { rescue::reboot_device(to_bootsel) } -pub fn get_credentials(pin: String) -> Result, String> { - fido::get_credentials(pin) -} - -pub fn delete_credential(pin: String, credential_id: String) -> Result { - fido::delete_credential(pin, credential_id) -} - -pub fn reset_device() -> Result { - fido::reset_device() -} - +/// Read current LED status config via rescue. pub fn read_led_config() -> Result { rescue::read_led_config() } +/// Write LED status (on/off, color, brightness, steady/blinking). pub fn write_led_status( status: u8, color: u8, @@ -76,22 +65,65 @@ pub fn write_led_status( rescue::write_led_status(status, color, brightness, steady) } +/// Read management app config via rescue. pub fn read_management_config() -> Result { rescue::read_management_config() } +/// Write management app enabled-mask via rescue. pub fn write_management_config(enabled_mask: u16) -> Result { rescue::write_management_config(enabled_mask) } +// ── FIDO2 protocol ────────────────────────────────────────────────────────── + +/// Query basic FIDO device info (AAGUID, version, etc.). +pub(crate) fn get_fido_info() -> Result { + fido::get_fido_info() +} + +/// Change the FIDO user PIN. +pub(crate) fn change_fido_pin( + current_pin: Option, + new_pin: String, +) -> Result { + fido::change_fido_pin(current_pin, new_pin) +} + +/// Set the minimum PIN length requirement. +pub(crate) fn set_min_pin_length( + current_pin: String, + min_pin_length: u8, +) -> Result { + fido::set_min_pin_length(current_pin, min_pin_length) +} + +/// List stored credentials for the given PIN. +pub fn get_credentials(pin: String) -> Result, String> { + fido::get_credentials(pin) +} + +/// Delete a single credential by its ID. +pub fn delete_credential(pin: String, credential_id: String) -> Result { + fido::delete_credential(pin, credential_id) +} + +/// Factory-reset the device, wiping all credentials and settings. +pub fn reset_device() -> Result { + fido::reset_device() +} + +/// Enable enterprise attestation for the device. pub fn enable_enterprise_attestation(pin: String) -> Result { fido::enable_enterprise_attestation(pin) } +/// Retrieve the enterprise attestation CSR. pub fn get_enterprise_attestation_csr() -> Result { fido::get_enterprise_attestation_csr() } +/// Upload a signed enterprise attestation certificate. pub fn upload_enterprise_attestation_cert( pin: String, cert_path: String, diff --git a/src/device/mod.rs b/src/device/mod.rs index 3b591e9..96ec7a0 100644 --- a/src/device/mod.rs +++ b/src/device/mod.rs @@ -1,3 +1,44 @@ +//! Device communication layer for pico-forge. +//! +//! ```text +//! device/ +//! ├── mod.rs — module root, re-exports submodules +//! ├── io.rs — high-level entry points (both protocols) +//! ├── rescue.rs — rescue / PC/SC protocol implementation +//! ├── fido.rs — FIDO2 / CTAP2 protocol implementation +//! └── types.rs — shared structs, enums, and constants +//! ``` +//! +//! # Overview +//! +//! The `device` module is the only place that talks to the hardware token. +//! Everything above it (UI state, gpui-component views) depends on the +//! public functions exported here; nothing below it should know about the +//! communication details. +//! +//! Two protocols are used: +//! +//! - **Rescue (PC/SC)** — low-level APDU channel for firmware-level +//! configuration: secure boot, LED status, USB applet management, and +//! device reboot. Implemented in [`rescue`]. +//! +//! - **FIDO2 (CTAP2)** — standard authenticator protocol for credential +//! management, PIN operations, and enterprise attestation. +//! Implemented in [`fido`]. +//! +//! [`io`] sits on top of both and exposes a single function per device +//! operation. Some functions dispatch to one protocol or the other based +//! on a [`types::DeviceMethod`] flag; others try rescue first and fall back +//! to FIDO on failure. +//! +//! # Adding a new device operation +//! +//! 1. Add any new structs/enums to [`types`]. +//! 2. Implement the raw protocol call in [`rescue`] or [`fido`]. +//! 3. Expose a high-level wrapper in [`io`] that picks the right protocol +//! and converts errors to the caller's expected type. +//! 4. Wire the wrapper into a gpui-component view or action handler. + pub mod fido; pub mod io; pub mod rescue; diff --git a/src/device/rescue/constants.rs b/src/device/rescue/constants.rs index 66ac39f..0a34d2d 100644 --- a/src/device/rescue/constants.rs +++ b/src/device/rescue/constants.rs @@ -1,4 +1,53 @@ -//! Constants, enums, bitflags and data structures for Rescue and vendor applets. +//! Rescue and vendor applet constants for pico-fido and RS-Key firmware. +//! +//! This file defines constants, enums, bitflags, and data structures used by the +//! Rescue applet and vendor-specific applets (LED, Management) across both +//! [pico-fido](https://github.com/polhenarejos/pico-fido) and +//! [RS-Key](https://github.com/TheMaxMur/RS-Key) firmware. +//! +//! # Organization +//! +//! - **ISO 7816-4 Standard Constants**: APDU command structure constants (CLA, INS, P1, P2, SW) +//! - **Rescue Applet Constants**: Commands and parameters for the Rescue applet +//! - **PHY Configuration Tags & Flags**: Hardware configuration tags and option bitflags +//! - **Vendor/LED Applet**: RS-Key specific LED control commands +//! - **Management Applet**: Yubico-compatible management interface for configuration +//! +//! # Rescue Applet +//! +//! The Rescue applet (AID: `A0 58 3F C1 9B 7E 4F 21`) provides low-level device access +//! for firmware recovery and hardware configuration. It uses ISO 7816-4 APDU commands +//! with a proprietary CLA byte (0x80). +//! +//! This applet is implemented in both firmware variants: +//! - **pico-fido**: C implementation in `pico-keys-sdk/src/rescue.c` +//! - **RS-Key**: Rust reimplementation in `crates/rsk-rescue/src/lib.rs` +//! +//! Key operations: +//! - `KeyDevSign` (0x10): Cryptographic operations (sign, get public key, upload cert) +//! - `Write` (0x1C): Write hardware configuration (PHY tags), set RTC time +//! - `Read` (0x1E): Read hardware configuration, flash info, secure boot status, time +//! - `Reboot` (0x1F): Reboot device (normal or bootloader mode) +//! +//! # PHY Configuration Tags +//! +//! PHY tags define hardware-specific parameters stored in the device's flash memory. +//! These tags control USB identifiers, LED behavior, cryptographic curves, and +//! interface enablement. +//! +//! PHY configuration is shared between pico-fido and RS-Key, with RS-Key adding +//! additional tags like `LedOrder` for RGB LED support. +//! +//! References: +//! - [pico-fido](https://github.com/polhenarejos/pico-fido) `src/fs/phy.h` +//! - [RS-Key](https://github.com/TheMaxMur/RS-Key) `crates/rsk-rescue/src/phy.rs` +//! +//! # Vendor Applets +//! +//! - **LED Applet** (AID: `F0 00 00 00 01`): RS-Key specific LED color control +//! - **Management Applet** (Yubico-compatible): Read/write device configuration +//! +//! These applets are only available in RS-Key firmware, not in pico-fido. #![allow(unused)] // use serde::{Deserialize, Serialize}; @@ -6,101 +55,266 @@ // --- 1. ISO 7816-4 Standard Constants --- -/// Class Byte (CLA) -pub const APDU_CLA_ISO: u8 = 0x00; // Standard ISO commands -pub const APDU_CLA_PROPRIETARY: u8 = 0x80; // Custom/Rescue commands +/// ISO 7816-4 Class Byte (CLA) for standard commands. +/// +/// CLA byte indicates the class of the command. Value 0x00 indicates standard +/// ISO commands that follow the specification exactly. +pub const APDU_CLA_ISO: u8 = 0x00; -/// Instruction (INS) for Selection +/// ISO 7816-4 Class Byte (CLA) for proprietary commands. +/// +/// CLA byte 0x80 indicates proprietary commands that extend beyond the +/// standard ISO 7816-4 specification. Used by the Rescue applet. +pub const APDU_CLA_PROPRIETARY: u8 = 0x80; + +/// ISO 7816-4 SELECT instruction (INS) byte. +/// +/// The SELECT command (INS 0xA4) is used to select an application or file +/// on the device by its Application Identifier (AID). pub const APDU_INS_SELECT: u8 = 0xA4; -/// Selection Parameters (P1, P2) +/// SELECT P1 parameter: Select by DF name (Application Identifier). +/// +/// When P1 = 0x04, the command selects an application using its AID +/// (Application Identifier) in the data field. pub const APDU_P1_SELECT_BY_DF_NAME: u8 = 0x04; -pub const APDU_P2_RETURN_FCI: u8 = 0x04; // Return File Control Info -/// Status Words (SW1 SW2) +/// SELECT P2 parameter: Return File Control Information (FCI). +/// +/// When P2 = 0x04, the device returns FCI template containing +/// application metadata (AID, label, etc.) after selection. +pub const APDU_P2_RETURN_FCI: u8 = 0x04; + +/// ISO 7816-4 status word for successful command execution. +/// +/// SW1=0x90, SW2=0x00 indicates the command completed successfully +/// with no errors. pub const SW_SUCCESS: [u8; 2] = [0x90, 0x00]; // --- 2. Rescue Applet Constants --- -// The Rescue Application ID (AID) from src/rescue.c +/// Rescue Applet Application Identifier (AID). +/// +/// The Rescue applet is selected using this AID. The applet provides low-level +/// device access for firmware recovery and hardware configuration. +/// +/// This AID is shared between pico-fido and RS-Key firmware: +/// - **pico-fido**: Defined in `src/rescue.c` (C implementation) +/// - **RS-Key**: Defined in `crates/rsk-rescue/src/lib.rs` (Rust reimplementation) +/// +/// Byte sequence: `A0 58 3F C1 9B 7E 4F 21` pub const RESCUE_AID: &[u8] = &[0xA0, 0x58, 0x3F, 0xC1, 0x9B, 0x7E, 0x4F, 0x21]; -// APDU Instructions +/// Rescue applet instruction codes. +/// +/// These instructions define the operations supported by the Rescue applet. +/// Each instruction has specific P1/P2 parameters and data field requirements. #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum RescueInstruction { + /// Cryptographic operations: sign data, get public key, upload certificate. + /// + /// P1 parameter determines the operation type (SignData, GetPublicKey, UploadCert). + /// P2 is typically 0x00. Data field contains operation-specific payload. KeyDevSign = 0x10, + + /// Write hardware configuration to flash memory. + /// + /// P1 parameter determines which PHY tag to write (e.g., PhyConfig for 0x01). + /// Data field contains the tag value to write. Write = 0x1C, + + /// Lock or unlock device access. + /// + /// P2 parameter determines lock state (0x00=Unlock, 0x01=Lock). + /// When locked, PHY configuration commands are rejected. Secure = 0x1D, + + /// Read hardware configuration from flash memory. + /// + /// P1 parameter determines which PHY tag to read (e.g., PhyConfig for 0x01). + /// Response contains the tag value. Read = 0x1E, + + /// Reboot the device. + /// + /// P2 parameter determines reboot mode (0x00=Normal, 0x01=Bootsel). + /// Normal reboot restarts the firmware; bootsel enters bootloader mode. Reboot = 0x1F, } -/// P1 Parameters for RescueInstruction::Read (0x1E) +/// P1 parameters for `RescueInstruction::Read` (0x1E). +/// +/// These parameters determine which hardware configuration to read. #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ReadParam { + /// Read full PHY configuration (VID/PID, LED settings, curves, etc.). PhyConfig = 0x01, + + /// Read flash memory information (size, used, free). FlashInfo = 0x02, + + /// Read secure boot status and verification result. SecureBootStatus = 0x03, } -/// P1 Parameters for WRITE (0x1C) +/// P1 parameters for `RescueInstruction::Write` (0x1C). +/// +/// These parameters determine which hardware configuration to write. #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum WriteParam { + /// Write full PHY configuration (VID/PID, LED settings, curves, etc.). PhyConfig = 0x01, } -/// P1 Parameters for RescueInstruction::KeyDevSign (0x10) +/// P1 parameters for `RescueInstruction::KeyDevSign` (0x10). +/// +/// These parameters determine the cryptographic operation to perform. #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SignParam { + /// Sign data using the device's attestation key. + /// + /// Data field contains the data to sign. Response contains the signature. SignData = 0x01, + + /// Get the device's attestation public key. + /// + /// No data field required. Response contains the COSE-encoded public key. GetPublicKey = 0x02, + + /// Upload a certificate for the attestation key. + /// + /// Data field contains the DER-encoded certificate. Device stores it + /// for later retrieval during attestation. UploadCert = 0x03, } -/// P1 Parameters for RescueInstruction::Reboot (0x1F) +/// P1 parameters for `RescueInstruction::Reboot` (0x1F). +/// +/// These parameters determine the reboot mode. #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum RebootParam { + /// Normal reboot: restart the current firmware. Normal = 0x00, + + /// Bootsel reboot: enter RP2040 bootloader for firmware update. + /// + /// This mode allows flashing new firmware via USB mass storage. Bootsel = 0x01, } -/// P2 Parameters for SECURE (0x1D) +/// P2 parameters for `RescueInstruction::Secure` (0x1D). +/// +/// These parameters determine the lock state. #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum SecureLockParam { + /// Unlock the device: allow PHY configuration commands. #[default] Unlock = 0x00, + + /// Lock the device: reject PHY configuration commands. + /// + /// When locked, Write and some Read operations are rejected + /// to prevent unauthorized configuration changes. Lock = 0x01, } -/// Default P2 value when not used +/// Default P2 value when not used in APDU commands. +/// +/// Some commands don't use the P2 parameter. This constant provides +/// a consistent value (0x00) for such cases. pub const P2_UNUSED: u8 = 0x00; // --- 3. PHY Configuration Tags & Flags --- -// PHY Tags from src/fs/phy.h +/// PHY configuration tag identifiers. +/// +/// These tags define the hardware parameters stored in the device's flash memory. +/// Each tag has a specific format and purpose for configuring the device. +/// +/// Tags are used with `RescueInstruction::Read` and `RescueInstruction::Write` +/// commands to access hardware configuration. +/// +/// PHY configuration is shared between pico-fido and RS-Key, with RS-Key adding +/// additional tags like `LedOrder` for RGB LED support. +/// +/// References: +/// - [pico-fido](https://github.com/polhenarejos/pico-fido) `src/fs/phy.h` +/// - [RS-Key](https://github.com/TheMaxMur/RS-Key) `crates/rsk-rescue/src/phy.rs` #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum PhyTag { + /// USB Vendor ID and Product ID. + /// + /// Data format: `[VID_LSB, VID_MSB, PID_LSB, PID_MSB]` (4 bytes). + /// Used to identify the device to the host system. VidPid = 0x00, + + /// LED GPIO pin configuration. + /// + /// Data format: `[GPIO_PIN]` (1 byte) or `[R_PIN, G_PIN, B_PIN]` (3 bytes). + /// Defines which GPIO pins control the status LED(s). LedGpio = 0x04, + + /// LED brightness level. + /// + /// Data format: `[BRIGHTNESS]` (1 byte, 0-255). + /// Controls the default brightness of the status LED. LedBrightness = 0x05, + + /// Device configuration options (bitflags). + /// + /// Data format: `[OPTIONS_LSB, OPTIONS_MSB]` (2 bytes). + /// See `RescueOptions` bitflags for individual option bits. Opts = 0x06, + + /// Touch presence timeout. + /// + /// Data format: `[TIMEOUT_MS_LSB, TIMEOUT_MS_MSB]` (2 bytes). + /// Timeout in milliseconds for user presence verification. PresenceTimeout = 0x08, + + /// USB product string. + /// + /// Data format: UTF-8 string bytes. + /// The product name displayed to the host system. UsbProduct = 0x09, + + /// Enabled cryptographic curves (bitflags). + /// + /// Data format: `[CURVES_LSB, CURVES_MSB, CURVES_3, CURVES_MSB]` (4 bytes). + /// See `RescueCurves` bitflags for supported curves. Curves = 0x0A, - LedDriver = 0x0C, - LedOrder = 0x0D, + + /// Enabled USB interfaces (bitflags). + /// + /// Data format: `[INTERFACES]` (1 byte). + /// See `UsbInterfaces` bitflags for available interfaces. EnabledUsbItf = 0x0B, + + /// LED driver type. + /// + /// Data format: `[DRIVER_TYPE]` (1 byte). + /// Specifies the LED driver hardware (e.g., PWM, I2C, etc.). + LedDriver = 0x0C, + + /// LED color order for RGB LEDs. + /// + /// Data format: `[ORDER]` (1 byte). + /// RS-Key specific tag for configuring LED color channel order. + LedOrder = 0x0D, } impl PhyTag { - /// Helper to convert raw u8 from device back to Enum + /// Convert a raw u8 value to a PhyTag enum variant. + /// + /// Returns `None` if the value doesn't match any known tag. + /// Used when parsing device responses that contain raw tag bytes. pub fn from_u8(val: u8) -> Option { match val { 0x00 => Some(Self::VidPid), @@ -118,58 +332,173 @@ impl PhyTag { } } +/// Device configuration options bitflags. +/// +/// These flags control device behavior and capabilities. They are stored +/// in the PHY configuration under tag `0x06` (Opts). +/// +/// This bitflags type is shared between pico-fido and RS-Key firmware. +/// +/// References: +/// - [pico-fido](https://github.com/polhenarejos/pico-fido) `src/fs/phy.h` +/// - [RS-Key](https://github.com/TheMaxMur/RS-Key) `crates/rsk-rescue/src/phy.rs` bitflags::bitflags! { - /// Configuration options for TAG_OPTS (Tag 0x06) pub struct RescueOptions: u16 { + /// LED supports dimming (PWM control). + /// + /// When set, the LED brightness can be adjusted. When clear, + /// the LED is only on/off. const LED_DIMMABLE = 0x02; + + /// Disable power-on reset detection. + /// + /// When set, the device doesn't reset on power-on events. + /// Useful for devices with unstable power supply. const DISABLE_POWER_RESET = 0x04; + + /// LED stays steady (no blinking). + /// + /// When set, the LED remains solid when active. + /// When clear, the LED blinks to indicate activity. const LED_STEADY = 0x08; } } +/// Enabled cryptographic curves bitflags. +/// +/// These flags define which elliptic curves are available for +/// cryptographic operations (ECDH, ECDSA, etc.). +/// +/// This bitflags type is shared between pico-fido and RS-Key firmware. +/// +/// References: +/// - [pico-fido](https://github.com/polhenarejos/pico-fido) `src/fs/phy.h` +/// - [RS-Key](https://github.com/TheMaxMur/RS-Key) `crates/rsk-rescue/src/phy.rs` bitflags::bitflags! { - /// Enabled curves for TAG_CURVES (Tag 0x0A) pub struct RescueCurves: u32 { + /// SECP256K1 curve (Bitcoin/Ethereum). + /// + /// Used by cryptocurrency wallets and some FIDO2 implementations. + /// Curve OID: 1.3.132.0.10 const SECP256K1 = 0x08; } } +/// Enabled USB interfaces bitflags. +/// +/// These flags define which USB interfaces are active on the device. +/// Multiple interfaces can be enabled simultaneously. +/// +/// This bitflags type is shared between pico-fido and RS-Key firmware. +/// +/// References: +/// - [pico-fido](https://github.com/polhenarejos/pico-fido) `src/fs/phy.h` +/// - [RS-Key](https://github.com/TheMaxMur/RS-Key) `crates/rsk-rescue/src/phy.rs` bitflags::bitflags! { - /// Enabled USB interfaces for TAG 0x0B (EnabledUsbItf) pub struct UsbInterfaces: u8 { + /// CCID interface (smart card reader). + /// + /// Implements USB CCID class for smart card operations. + /// Used by some enterprise security solutions. const CCID = 0x01; + + /// WCID interface (Windows Compatible ID). + /// + /// Provides Windows-compatible device identification + /// without requiring custom drivers. const WCID = 0x02; + + /// HID interface (FIDO2/CTAP2). + /// + /// Implements USB HID class for CTAP2/FIDO2 communication. + /// This is the primary interface for security key operations. const HID = 0x04; + + /// Keyboard interface (HID keyboard). + /// + /// Emulates a USB keyboard for TOTP code entry or other + /// keyboard-based interactions. const KB = 0x08; + + /// LWIP interface (TCP/IP stack). + /// + /// Enables the lightweight TCP/IP stack for network + /// communication (if supported by hardware). const LWIP = 0x10; } } // --- 4. Vendor/LED Applet (RS-Key specific) --- +/// Vendor LED applet Application Identifier (AID). +/// +/// This AID selects the RS-Key specific LED control applet. +/// The applet provides commands to control the device's RGB LED +/// for status indication and user feedback. +/// +/// Byte sequence: `F0 00 00 00 01` +/// +/// **Note**: This applet is only available in RS-Key firmware, not in pico-fido. pub const VENDOR_LED_AID: &[u8] = &[0xF0, 0x00, 0x00, 0x00, 0x01]; +/// Vendor LED applet instruction codes. +/// +/// These instructions control the device's RGB LED. #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum VendorLedInstruction { + /// Set LED color for a specific status indicator. + /// + /// P1: Status indicator (LedStatus). + /// P2: LED color (LedColor). + /// Data: None. SetLed = 0x10, + + /// Get current LED color for a status indicator. + /// + /// P1: Status indicator (LedStatus). + /// P2: 0x00. + /// Data: None. + /// Response: LED color byte. GetLed = 0x11, } +/// LED color definitions for the Vendor LED applet. +/// +/// Each color corresponds to a specific RGB value that can be +/// displayed on the device's status LED. #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum LedColor { + /// LED off (no light). Off = 0, + + /// Red LED. Red = 1, + + /// Green LED. Green = 2, + + /// Blue LED. Blue = 3, + + /// Yellow LED (Red + Green). Yellow = 4, + + /// Magenta LED (Red + Blue). Magenta = 5, + + /// Cyan LED (Green + Blue). Cyan = 6, + + /// White LED (Red + Green + Blue). White = 7, } impl LedColor { + /// Convert a raw u8 value to a LedColor enum variant. + /// + /// Returns `None` if the value doesn't match any known color. pub fn from_u8(val: u8) -> Option { match val { 0 => Some(Self::Off), @@ -184,6 +513,10 @@ impl LedColor { } } + /// Get a human-readable label for the color. + /// + /// Returns a static string like "Red", "Green", etc. + /// Used for display in the UI. pub fn label(&self) -> &'static str { match self { Self::Off => "Off", @@ -197,6 +530,10 @@ impl LedColor { } } + /// Get all available LED colors. + /// + /// Returns a slice of all `LedColor` variants in order. + /// Useful for populating UI dropdowns or color pickers. pub fn all() -> &'static [Self] { &[ Self::Off, @@ -211,16 +548,43 @@ impl LedColor { } } +/// LED status indicator definitions. +/// +/// These indicate the device state for which the LED color is configured. +/// Each status can have a different color for visual feedback. #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum LedStatus { + /// Idle state: device is waiting for user interaction. + /// + /// Typically shows a dim or pulsing light to indicate the device + /// is ready but not actively processing. Idle = 0, + + /// Processing state: device is performing an operation. + /// + /// Shows a bright or blinking light to indicate the device + /// is busy (e.g., during cryptographic operations). Processing = 1, + + /// Touch required: device is waiting for user touch. + /// + /// Shows a specific color pattern to prompt the user to + /// touch the device's capacitive sensor. Touch = 2, + + /// Boot state: device is starting up. + /// + /// Shows a brief color pattern during the boot sequence + /// to indicate successful initialization. Boot = 3, } impl LedStatus { + /// Get a human-readable label for the status. + /// + /// Returns a static string like "Idle", "Processing", etc. + /// Used for display in the UI. pub fn label(&self) -> &'static str { match self { Self::Idle => "Idle", @@ -230,6 +594,10 @@ impl LedStatus { } } + /// Get all available LED statuses. + /// + /// Returns a slice of all `LedStatus` variants in order. + /// Useful for populating UI dropdowns or status lists. pub fn all() -> &'static [Self] { &[Self::Idle, Self::Processing, Self::Touch, Self::Boot] } @@ -237,26 +605,87 @@ impl LedStatus { // --- 5. Management Applet (Yubico-compatible, RS-Key) --- +/// Management applet Application Identifier (AID). +/// +/// This AID selects the Yubico-compatible management applet in RS-Key firmware. +/// The applet provides configuration read/write operations similar to Yubico's +/// management interface. +/// +/// Byte sequence: `A0 00 00 05 27 47 11 17` +/// +/// **Note**: This applet is only available in RS-Key firmware, not in pico-fido. pub const MANAGEMENT_AID: &[u8] = &[0xA0, 0x00, 0x00, 0x05, 0x27, 0x47, 0x11, 0x17]; +/// Management applet instruction codes. +/// +/// These instructions read and write device configuration using +/// a TLV (Tag-Length-Value) format. #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ManagementInstruction { + /// Read device configuration. + /// + /// Returns a TLV-encoded map of device settings. + /// Use `MGMT_TAG_*` constants to parse the response. ReadConfig = 0x1D, + + /// Write device configuration. + /// + /// Accepts a TLV-encoded map of settings to write. + /// Use `MGMT_TAG_*` constants to build the request. WriteConfig = 0x1C, } +/// TLV tag for USB support flag. +/// +/// Value: `true` if the device supports USB, `false` otherwise. pub const MGMT_TAG_USB_SUPPORTED: u8 = 0x01; + +/// TLV tag for device serial number. +/// +/// Value: 32-bit unsigned integer representing the device serial. pub const MGMT_TAG_SERIAL: u8 = 0x02; + +/// TLV tag for enabled USB interfaces. +/// +/// Value: Bitmask of enabled USB interfaces (see `USB_CAP_*` constants). pub const MGMT_TAG_USB_ENABLED: u8 = 0x03; + +/// TLV tag for device form factor. +/// +/// Value: Device form factor identifier (e.g., USB key, NFC, etc.). pub const MGMT_TAG_FORM_FACTOR: u8 = 0x04; + +/// TLV tag for firmware version. +/// +/// Value: Firmware version number (major.minor.patch encoded as integer). pub const MGMT_TAG_VERSION: u8 = 0x05; + +/// TLV tag for device flags. +/// +/// Value: Bitmask of device flags (e.g., FIPS compliance, etc.). pub const MGMT_TAG_DEVICE_FLAGS: u8 = 0x08; + +/// TLV tag for configuration lock state. +/// +/// Value: `true` if configuration is locked, `false` otherwise. +/// When locked, `WriteConfig` commands are rejected. pub const MGMT_TAG_CONFIG_LOCK: u8 = 0x0A; +/// USB capability: OTP (One-Time Password) interface. pub const USB_CAP_OTP: u16 = 0x0001; + +/// USB capability: U2F (Universal 2nd Factor) interface. pub const USB_CAP_U2F: u16 = 0x0002; + +/// USB capability: OpenPGP card interface. pub const USB_CAP_OPENPGP: u16 = 0x0008; + +/// USB capability: PIV (Personal Identity Verification) interface. pub const USB_CAP_PIV: u16 = 0x0010; + +/// USB capability: OATH (One-Time Auth) interface. pub const USB_CAP_OATH: u16 = 0x0020; + +/// USB capability: FIDO2/CTAP2 interface. pub const USB_CAP_FIDO2: u16 = 0x0200; diff --git a/src/device/rescue/mod.rs b/src/device/rescue/mod.rs index cbd6b4d..75e5e70 100644 --- a/src/device/rescue/mod.rs +++ b/src/device/rescue/mod.rs @@ -1,6 +1,161 @@ -//! Implements communication with the pico-fido firmware via the `Rescue API`. +//! Rescue applet implementation for pico-fido and RS-Key firmware. //! -//! For more details checkout the [pico-key-sdk](https://github.com/polhenarejos/pico-keys-sdk/blob/main/src/rescue.c) +//! ```text +//! rescue/ +//! ├── mod.rs — high-level rescue operations (read/write config, reboot, LED, management) +//! └── constants.rs — ISO 7816-4 constants, rescue instructions, PHY tags, vendor applets +//! ``` +//! +//! # What is the Rescue Applet? +//! +//! The Rescue applet is a low-level firmware recovery and hardware configuration +//! interface that operates independently of the FIDO2/CTAP2 stack. It provides +//! direct access to device hardware settings, flash memory, and security features +//! through a proprietary APDU-based protocol. +//! +//! Both [pico-fido](https://github.com/polhenarejos/pico-fido) (C) and +//! [RS-Key](https://github.com/TheMaxMur/RS-Key) (Rust) firmware implement +//! this applet with the same AID and command set. +//! +//! # Why is Rescue Mode Needed? +//! +//! FIDO2 devices expose a standardized interface (CTAP2) that abstracts away +//! hardware details. However, there are scenarios where direct hardware access +//! is required: +//! +//! - **Firmware recovery**: When FIDO mode is unresponsive or corrupted +//! - **Hardware configuration**: Changing USB VID/PID, LED settings, touch timeout +//! without requiring FIDO PIN authentication +//! - **Secure boot management**: Enabling/disabling secure boot, reading OTP status +//! - **Device provisioning**: Uploading attestation certificates, setting serial numbers +//! - **Firmware updates**: Rebooting into bootloader (BOOTSEL) mode for flashing +//! +//! The Rescue applet runs on the CCID (smart card) USB interface, which is always +//! available even when FIDO functionality is disabled or misconfigured. +//! +//! # Communication Protocol: PC/SC +//! +//! Unlike FIDO2 which uses USB HID (CTAPHID), the Rescue applet communicates via +//! **PC/SC** (Personal Computer/Smart Card) — the standard protocol for interacting +//! with smart card readers and ICCs (Integrated Circuit Cards). +//! +//! ```text +//! Host Application +//! │ +//! ▼ +//! pcsc-lite daemon (pcscd) ← Linux/macOS daemon +//! │ +//! ▼ +//! USB CCID Class Driver ← Smart card reader driver +//! │ +//! ▼ +//! Device CCID Interface ← Composite USB device +//! │ +//! ▼ +//! Rescue Applet (APDU commands) ← Firmware +//! ``` +//! +//! ## PC/SC Architecture +//! +//! The PC/SC specification defines a standard API for communicating with smart +//! cards. In our case, the RP2040/RP2350 device emulates a CCID-compliant smart +//! card reader with an embedded ICC. +//! +//! Key concepts: +//! - **Context**: A connection to the PC/SC daemon (establishes resource manager) +//! - **Reader**: A physical or virtual smart card reader (our device appears as one) +//! - **Card**: A connection to a specific card in a reader +//! - **APDU**: Application Protocol Data Unit — the command/response format +//! +//! ## APDU Command Structure +//! +//! ```text +//! ┌─────┬─────┬─────┬─────┬─────┬─────────────┐ +//! │ CLA │ INS │ P1 │ P2 │ Lc │ Data │ +//! └─────┴─────┴─────┴─────┴─────┴─────────────┘ +//! 1B 1B 1B 1B 0-1B 0-255 bytes +//! ``` +//! +//! - **CLA** (0x80 for Rescue): Command class — proprietary extension +//! - **INS**: Instruction code (e.g., 0x1E for READ, 0x1C for WRITE) +//! - **P1/P2**: Parameters (sub-command selectors) +//! - **Lc**: Length of data field +//! - **Data**: Command payload +//! +//! Response ends with Status Words (SW1 SW2): +//! - `0x90 0x00`: Success +//! - `0x6A 0x82`: File/application not found +//! - `0x69 0x82`: Security status not satisfied +//! +//! # Data Flow +//! +//! ```text +//! io::read_device_details() +//! │ +//! ▼ +//! rescue::read_device_details() ← this file +//! │ +//! ▼ +//! connect_and_select() ← PC/SC connection + applet selection +//! │ +//! ▼ +//! card.transmit(apdu) ← ISO 7816-4 APDU exchange +//! │ +//! ▼ +//! PC/SC (CCID USB interface) +//! ``` +//! +//! ## Applet Selection +//! +//! Every session begins with applet selection: +//! +//! ```text +//! APDU: 00 A4 04 04 08 A0 58 3F C1 9B 7E 4F 21 +//! ── ── ── ── ── ───────────────────────── +//! CLA INS P1 P2 Len AID (Rescue Applet) +//! ``` +//! +//! The SELECT response contains device identity: +//! - Byte 0: MCU type (1=RP2350, 2=ESP32-S3, etc.) +//! - Byte 1: Product type (2=FIDO) +//! - Byte 2: SDK version major +//! - Byte 3: SDK version minor +//! - Bytes 4-11: Serial number (8 bytes) +//! +//! # Module Structure +//! +//! [`constants`] defines all protocol constants shared between pico-fido and RS-Key: +//! - ISO 7816-4 command bytes (CLA, INS, P1, P2, SW) +//! - Rescue instruction codes and parameters +//! - PHY configuration tags and bitflags +//! - Vendor applet AIDs and instructions (LED, Management) +//! +//! [`mod.rs`] contains the public functions called from [`super::io`]: +//! - `read_device_details()`: Reads full device status via Rescue +//! - `write_config()`: Writes PHY configuration (VID/PID, LED, curves, etc.) +//! - `reboot_device()`: Reboots device (normal or BOOTSEL mode) +//! - `enable_secure_boot()`: Enables secure boot (WIP) +//! - `read_led_config()` / `write_led_status()`: LED color configuration (RS-Key) +//! - `read_management_config()` / `write_management_config()`: USB interface config (RS-Key) +//! +//! # Firmware Differences +//! +//! | Feature | pico-fido | RS-Key | +//! |---------|-----------|--------| +//! | Language | C | Rust | +//! | Rescue AID | `A0 58 3F C1 9B 7E 4F 21` | Same | +//! | Secure Boot | `INS_SECURE` (0x1D) | `INS_OTP_LOCK` (0x1B) — irreversible | +//! | LED Applet | Not available | Available (AID: `F0 00 00 00 01`) | +//! | Management | Not available | Available (Yubico-compatible) | +//! | Anti-rollback | Not available | Available (OTP fuses) | +//! +//! # References +//! +//! - [pico-fido Rescue](https://github.com/polhenarejos/pico-fido/blob/main/src/rescue.c) +//! - [RS-Key Rescue](https://github.com/TheMaxMur/RS-Key/blob/main/crates/rsk-rescue/src/lib.rs) +//! - [PC/SC Specification](https://pcsc1groupwg.readthedocs.io/) +//! - [ISO 7816-4](https://www.iso.org/standard/74873.html) +//! - [CCID Specification](https://www.usb.org/document-library/class-specification-12-chip-smart-card-interface) pub mod constants; @@ -10,7 +165,21 @@ use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; use pcsc::{Context, Protocols, Scope, ShareMode}; use std::io::Cursor; -/// Connects to the first available reader and selects the Rescue Applet +/// Establishes a PC/SC connection to the first available smart card reader and selects the Rescue Applet. +/// +/// Sends a SELECT APDU (`00 A4 04 04 08 A0 58 3F C1 9B 7E 4F 21`) to the device via the CCID interface. +/// The response contains device identity data (MCU type, product type, firmware version, serial number). +/// +/// # Returns +/// A tuple of `(Card, SelectResponse, FirmwareType)` where: +/// - `Card` is the active PC/SC card handle for subsequent APDU exchanges +/// - `SelectResponse` is the raw FCI/identity data from the SELECT command +/// - `FirmwareType` is detected as `RSKey`, `PicoFido`, or `Unknown` +/// +/// # Errors +/// - `PFError::NoDevice` if no smart card reader is found +/// - `PFError::Pcsc` if the PC/SC context cannot be established +/// - `PFError::Device` if the Rescue Applet is not found (wrong AID or device in wrong mode) fn connect_and_select() -> Result<(pcsc::Card, Vec, FirmwareType), PFError> { let ctx = Context::establish(Scope::User).map_err(|e| { log::error!("Failed to establish PCSC context: {}", e); @@ -73,6 +242,20 @@ fn connect_and_select() -> Result<(pcsc::Card, Vec, FirmwareType), PFError> Ok((card, data, fw_type)) } +/// Reads comprehensive device details including identity, flash usage, secure boot status, and PHY configuration. +/// +/// Performs three sequential APDU operations after applet selection: +/// 1. SELECT response is parsed for MCU type, firmware version, and serial number +/// 2. `READ(FlashInfo)` — reads flash usage statistics (free, used, total) +/// 3. `READ(SecureBootStatus)` — reads secure boot enable/lock state +/// 4. `READ(PhyConfig)` — reads TLV-encoded hardware configuration (VID/PID, LED, curves, etc.) +/// +/// # Returns +/// A `FullDeviceStatus` struct containing device info, parsed PHY config, and secure boot state. +/// +/// # Errors +/// - `PFError::Device` if the SELECT response is malformed or any READ command fails +/// - `PFError::NoDevice` if no reader is available pub fn read_device_details() -> Result { log::info!("Reading full device details"); let (card, select_resp, fw_type) = connect_and_select()?; @@ -269,6 +452,28 @@ pub fn read_device_details() -> Result { }) } +/// Writes PHY configuration to the device via the Rescue Applet's WRITE command. +/// +/// Constructs a TLV (Tag-Length-Value) blob from the provided `AppConfigInput` fields and sends +/// it as a single APDU: `80 1C 01 00 [Lc] [TLV Data]`. Supported tags include: +/// - `0x00`: VID:PID (4 bytes, big-endian) +/// - `0x04`: LED GPIO pin +/// - `0x05`: LED brightness +/// - `0x08`: Touch/presence timeout +/// - `0x06`: Options bitmask (LED_DIMMABLE, DISABLE_POWER_RESET, LED_STEADY) +/// - `0x07`: Elliptic curves bitmask (SECP256K1, etc.) +/// - `0x0C`: LED driver selection +/// - `0x09`: USB product name (null-terminated) +/// - `0x0D`: LED order (RS-Key extension) +/// - `0x0B`: Enabled USB interfaces (CCID bit is always forced on for safety) +/// +/// # Returns +/// A success message string on `SW 9000`. +/// +/// # Errors +/// - `PFError::Io` if VID/PID are not valid hex strings +/// - `PFError::Device` if the WRITE APDU fails or returns a non-success status +/// - `PFError::Io` if the product name exceeds 32 bytes pub fn write_config(config: AppConfigInput) -> Result { log::info!("Writing configuration to device"); log::debug!("Config input: {:?}", config); @@ -415,6 +620,21 @@ pub fn write_config(config: AppConfigInput) -> Result { } } +/// Reboots the device, optionally entering BOOTSEL (mass storage) mode for firmware updates. +/// +/// Sends a REBOOT APDU: `80 1B [P1] 00 00` where: +/// - `P1 = 0x00` (`RebootParam::Normal`): Reboots into normal FIDO mode +/// - `P1 = 0x01` (`RebootParam::Bootsel`): Reboots into BOOTSEL/UF2 bootloader mode +/// +/// # Arguments +/// * `to_bootsel` - If `true`, device enters UF2 bootloader mode for firmware flashing. +/// If `false`, device performs a normal reboot into FIDO mode. +/// +/// # Returns +/// A confirmation string if the reboot command was accepted. +/// +/// # Errors +/// - `PFError::Device` if the APDU fails or returns a non-success status pub fn reboot_device(to_bootsel: bool) -> Result { let (card, _, _) = connect_and_select()?; @@ -442,7 +662,27 @@ pub fn reboot_device(to_bootsel: bool) -> Result { } } -/// UNSTABLE! (WIP) +/// Enables or disables secure boot on the device. **UNSTABLE — work in progress.** +/// +/// Sends a SECURE APDU: `80 1D 00 [LockBool] 00` where: +/// - `LockBool = 0x01`: Enable and lock secure boot (irreversible on some firmware) +/// - `LockBool = 0x00`: Disable secure boot +/// +/// Uses pico-fido instruction `INS_SECURE` (0x1D). RS-Key uses `INS_OTP_LOCK` (0x1B) +/// for OTP fuse locking, which is a different operation. +/// +/// # Arguments +/// * `lock` - If `true`, enables secure boot with lock (may be irreversible). +/// +/// # Returns +/// A confirmation string if the secure boot command was accepted. +/// +/// # Errors +/// - `PFError::Device` if the APDU fails or returns a non-success status +/// +/// # Warning +/// This function is unstable and may change. Locking secure boot can permanently +/// prevent firmware downgrades. Use with caution. pub fn enable_secure_boot(lock: bool) -> Result { let (card, _, _) = connect_and_select()?; @@ -470,6 +710,22 @@ pub fn enable_secure_boot(lock: bool) -> Result { // --- Vendor/LED Applet (RS-Key) --- +/// Establishes a PC/SC connection and selects a specific vendor applet by AID. +/// +/// Unlike [`connect_and_select`] which selects the Rescue Applet, this function +/// selects an arbitrary applet (e.g., LED applet `F0 00 00 00 01` or Management applet). +/// Sends a SELECT APDU: `00 A4 04 00 [Len] [AID] 00`. +/// +/// # Arguments +/// * `aid` - The Application Identifier of the target applet (e.g., `VENDOR_LED_AID`, `MANAGEMENT_AID`) +/// +/// # Returns +/// An active `pcsc::Card` handle ready for APDU exchange with the selected applet. +/// +/// # Errors +/// - `PFError::NoDevice` if no smart card reader is found +/// - `PFError::Pcsc` if the PC/SC context cannot be established +/// - `PFError::Device` if the applet is not found (AID not recognized by firmware) fn connect_and_select_aid(aid: &[u8]) -> Result { let ctx = Context::establish(Scope::User).map_err(|e| { log::error!("Failed to establish PCSC context: {}", e); diff --git a/src/device/types.rs b/src/device/types.rs index 7138eab..d827585 100644 --- a/src/device/types.rs +++ b/src/device/types.rs @@ -1,12 +1,23 @@ +//! Shared types for device communication. +//! +//! Organized into three groups: +//! - Application-level types: device info, config, and status used across both protocols +//! - Rescue (PC/SC) types: LED and USB applet configuration read/written over PC/SC +//! - FIDO2 types: credential and authenticator info from CTAP2 + #![allow(unused)] use serde::{Deserialize, Serialize}; use std::fmt; +// ── Application-level types ───────────────────────────────────────────────── + +/// Internal application state holding device info for the current session. struct PForgeState { device_info: DeviceInfo, } +/// Basic device identity and flash usage reported by the firmware. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[serde(rename_all = "camelCase")] pub struct DeviceInfo { @@ -16,6 +27,7 @@ pub struct DeviceInfo { pub firmware_version: String, } +/// Full device configuration (USB descriptors, LED, touch, crypto options). #[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)] #[serde(rename_all = "camelCase")] pub struct AppConfig { @@ -39,6 +51,7 @@ pub struct AppConfig { pub enabled_usb_itf: Option, } +/// Partial config update; `None` fields are left unchanged on the device. #[derive(Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct AppConfigInput { @@ -58,6 +71,7 @@ pub struct AppConfigInput { pub enabled_usb_itf: Option, } +/// Aggregated snapshot of device info, config, and security state. #[derive(Serialize, Debug, Clone, PartialEq)] #[serde(rename_all = "camelCase")] pub struct FullDeviceStatus { @@ -69,6 +83,7 @@ pub struct FullDeviceStatus { pub firmware_type: FirmwareType, } +/// Protocol channel used to communicate with the device. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub enum DeviceMethod { #[serde(rename = "FIDO")] @@ -76,8 +91,8 @@ pub enum DeviceMethod { Rescue, } -/// Represents the recognized firmware variants running on the connected hardware token. -/// Used extensively to gate UI features, connection methods, and compatibility checks. +/// Recognized firmware variants. Gates UI features, connection methods, and +/// compatibility checks throughout the application. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)] pub enum FirmwareType { PicoFido, @@ -96,31 +111,29 @@ impl fmt::Display for FirmwareType { } } -/// The globally unique Authenticator Attestation GUID (AAGUID) assigned to RS-Key hardware. -pub const RSKEY_AAGUID: &str = "2479C7BF6B3056839EC80E8171A918B7"; -/// The globally unique Authenticator Attestation GUID (AAGUID) assigned to Pico-Fido hardware. -pub const PICOFIDO_AAGUID: &str = "89FB94B706C936739B7E30526D968145"; +// ── Rescue (PC/SC) types ──────────────────────────────────────────────────── -/// Aggregates the LED status configurations read from the RS-Key Vendor/LED applet. -/// Contains the global steady flag and a fixed array of `(color_code, brightness)` pairs -/// mapped chronologically to device statuses: [Idle, Processing, Touch, Boot]. +/// LED status configuration read from the Vendor/LED applet. +/// `statuses` is a fixed array of `(color, brightness)` pairs indexed by +/// device status: Idle, Processing, Touch, Boot. #[derive(Serialize, Debug, Default, Clone, PartialEq)] pub struct LedStatusConfig { pub steady: bool, pub statuses: [(u8, u8); 4], } -/// Encapsulates the bitmasks defining USB application endpoints on the device. -/// The `usb_supported` mask indicates which applets the firmware is capable of running, -/// while `usb_enabled` reflects the active endpoints the device will enumerate on next boot. +/// USB application endpoint bitmasks from the Management applet. +/// `usb_supported` lists applets the firmware can run; +/// `usb_enabled` lists those active on next boot. #[derive(Serialize, Debug, Default, Clone, PartialEq)] pub struct ManagementAppConfig { pub usb_supported: u16, pub usb_enabled: u16, } -// Fido stuff: +// ── FIDO2 types ───────────────────────────────────────────────────────────── +/// Authenticator metadata from CTAP2 GetInfo. #[derive(Serialize, Debug, Clone, PartialEq)] #[serde(rename_all = "camelCase")] pub struct FidoDeviceInfo { @@ -145,6 +158,7 @@ pub struct FidoDeviceInfo { pub max_cred_blob_length: Option, } +/// A single FIDO2 credential stored on the device. #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct StoredCredential { @@ -155,3 +169,10 @@ pub struct StoredCredential { pub user_id: String, pub credential_id: String, } + +// ── Constants ─────────────────────────────────────────────────────────────── + +/// AAGUID assigned to RS-Key hardware. +pub const RSKEY_AAGUID: &str = "2479C7BF6B3056839EC80E8171A918B7"; +/// AAGUID assigned to Pico-Fido hardware. +pub const PICOFIDO_AAGUID: &str = "89FB94B706C936739B7E30526D968145"; From 77db0aec49af8d42c0824f56c0f3fb497dd87037 Mon Sep 17 00:00:00 2001 From: Suyog Tandel Date: Tue, 23 Jun 2026 23:03:27 +0530 Subject: [PATCH 09/11] feat: add basic rust ci workflow --- .github/workflows/ci.yml | 40 +++++++++++++++++++++++++++++++++ .github/workflows/release.yml | 7 ++++++ .github/workflows/wiki-sync.yml | 4 ++-- .gitignore | 1 + 4 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..4b05daf --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,40 @@ +name: CI + +on: + push: + branches: + - '**' + pull_request: + branches: + - '**' + +env: + CARGO_TERM_COLOR: always + +jobs: + ci: + name: CI Checks + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@v22 + + - name: Setup Nix cache + uses: DeterminateSystems/magic-nix-cache-action@v14 + + - name: Check formatting + run: nix develop --command cargo fmt --all -- --check + + - name: Run cargo check + run: nix develop --command cargo check --all-targets + + - name: Run clippy + run: nix develop --command cargo clippy --all-targets -- -D warnings + + - name: Build project + run: nix develop --command cargo build --verbose + + - name: Run tests + run: nix develop --command cargo test --verbose diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5f91756..ed4ec4e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,9 +4,16 @@ on: branches: - release workflow_dispatch: + workflow_run: + workflows: ["CI"] + branches: + - release + types: + - completed jobs: build-and-release: + if: github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' permissions: contents: write issues: write diff --git a/.github/workflows/wiki-sync.yml b/.github/workflows/wiki-sync.yml index cc17c3d..19c963c 100644 --- a/.github/workflows/wiki-sync.yml +++ b/.github/workflows/wiki-sync.yml @@ -19,10 +19,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Code - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Push to Wiki - uses: Andrew-Chen-Wang/github-wiki-action@v4 + uses: Andrew-Chen-Wang/github-wiki-action@v5 with: path: docs token: ${{ secrets.WIKI_SYNC_PAT_TOKEN }} diff --git a/.gitignore b/.gitignore index 9c44668..a529951 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,4 @@ packaging .cargo-packager llm-texts opencode.jsonc +AGENTS.md From 0a0a7b46f0ae6695c9a1ba5165beb7a5f1503bd4 Mon Sep 17 00:00:00 2001 From: Suyog Tandel Date: Tue, 23 Jun 2026 23:08:20 +0530 Subject: [PATCH 10/11] fix: ci workflow name --- .github/workflows/ci.yml | 6 +++--- .github/workflows/release.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4b05daf..aaf4c83 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: CI +name: PicoForge CI on: push: @@ -12,8 +12,8 @@ env: CARGO_TERM_COLOR: always jobs: - ci: - name: CI Checks + checks: + name: Code Quality Checks runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ed4ec4e..5cd5eae 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,7 +5,7 @@ on: - release workflow_dispatch: workflow_run: - workflows: ["CI"] + workflows: ["PicoForge CI"] branches: - release types: From c268d8ac28065717a9cf2e804f3752a3a84ea205 Mon Sep 17 00:00:00 2001 From: Suyog Tandel Date: Tue, 23 Jun 2026 23:11:55 +0530 Subject: [PATCH 11/11] fix: clippy errors in new documentedation of code in device mod --- src/device/rescue/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/device/rescue/mod.rs b/src/device/rescue/mod.rs index 75e5e70..6514bf9 100644 --- a/src/device/rescue/mod.rs +++ b/src/device/rescue/mod.rs @@ -628,7 +628,7 @@ pub fn write_config(config: AppConfigInput) -> Result { /// /// # Arguments /// * `to_bootsel` - If `true`, device enters UF2 bootloader mode for firmware flashing. -/// If `false`, device performs a normal reboot into FIDO mode. +/// If `false`, device performs a normal reboot into FIDO mode. /// /// # Returns /// A confirmation string if the reboot command was accepted.