feat: add support for RS-Keys firmware and add optito reset the device

This commit is contained in:
Suyog Tandel
2026-06-22 21:26:39 +05:30
parent 1c53fc8c40
commit 6b5d591e28
17 changed files with 1059 additions and 50 deletions
Generated
+1 -1
View File
@@ -4133,7 +4133,7 @@ checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315"
[[package]]
name = "picoforge"
version = "0.5.0"
version = "0.6.0"
dependencies = [
"aes",
"anyhow",
+3 -3
View File
@@ -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.
+17 -6
View File
@@ -164,13 +164,24 @@ impl HidTransport {
}
pub fn send_cbor(&self, cmd: u8, payload: &[u8]) -> Result<Vec<u8>, 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<Vec<u8>, 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<Vec<u8>, 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<Vec<u8>, PFError> {
let response_data = self.read_hid_response(cmd)?;
fn read_cbor_response(&self, cmd: u8, timeout_ms: i32) -> Result<Vec<u8>, 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<Vec<u8>, PFError> {
fn read_hid_response(&self, cmd: u8, timeout_ms: i32) -> Result<Vec<u8>, 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 {
+31 -1
View File
@@ -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<String, String> {
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<FullDeviceStatus, PFError> {
.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<FullDeviceStatus, PFError> {
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,
}
}
+20
View File
@@ -59,6 +59,26 @@ pub fn delete_credential(pin: String, credential_id: String) -> Result<String, S
fido::delete_credential(pin, credential_id)
}
pub fn reset_device() -> Result<String, String> {
fido::reset_device()
}
pub fn read_led_config() -> Result<LedStatusConfig, PFError> {
rescue::read_led_config()
}
pub fn write_led_status(status: u8, color: u8, brightness: u8, steady: bool) -> Result<String, PFError> {
rescue::write_led_status(status, color, brightness, steady)
}
pub fn read_management_config() -> Result<ManagementAppConfig, PFError> {
rescue::read_management_config()
}
pub fn write_management_config(enabled_mask: u16) -> Result<String, PFError> {
rescue::write_management_config(enabled_mask)
}
pub fn enable_enterprise_attestation(pin: String) -> Result<String, String> {
fido::enable_enterprise_attestation(pin)
}
+114 -2
View File
@@ -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<Self> {
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;
+269 -7
View File
@@ -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<u8>), PFError> {
fn connect_and_select() -> Result<(pcsc::Card, Vec<u8>, 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<u8>), 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<FullDeviceStatus, PFError> {
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<FullDeviceStatus, PFError> {
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<FullDeviceStatus, PFError> {
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<String, PFError> {
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<String, PFError> {
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<String, PFError> {
}
pub fn reboot_device(to_bootsel: bool) -> Result<String, PFError> {
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<String, PFError> {
/// UNSTABLE! (WIP)
pub fn enable_secure_boot(lock: bool) -> Result<String, PFError> {
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<String, PFError> {
Err(PFError::Device(format!("Secure Boot failed: {:02X?}", rx)))
}
}
// --- Vendor/LED Applet (RS-Key) ---
fn connect_and_select_aid(aid: &[u8]) -> Result<pcsc::Card, PFError> {
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<LedStatusConfig, PFError> {
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<String, PFError> {
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<ManagementAppConfig, PFError> {
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<String, PFError> {
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
)))
}
}
+48
View File
@@ -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<u8>,
}
#[derive(Deserialize, Debug, Clone)]
@@ -46,6 +49,7 @@ pub struct AppConfigInput {
pub power_cycle_on_reset: Option<bool>,
pub led_steady: Option<bool>,
pub enable_secp256k1: Option<bool>,
pub led_order: Option<u8>,
}
#[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)]
+32 -10
View File
@@ -8,7 +8,7 @@ use gpui_component::{
};
type PinPromptCallback = std::rc::Rc<dyn Fn(String, WeakEntity<PinPromptContent>, &mut App)>;
type ConfirmCallback = std::rc::Rc<dyn Fn(WeakEntity<ConfirmContent>, &mut App)>;
type ConfirmCallback = std::rc::Rc<dyn Fn(WeakEntity<ConfirmContent>, &mut Window, &mut App)>;
type ChangePinCallback =
std::rc::Rc<dyn Fn(String, String, WeakEntity<ChangePinContent>, &mut App)>;
type SetPinCallback = std::rc::Rc<dyn Fn(String, WeakEntity<SetPinContent>, &mut App)>;
@@ -17,6 +17,9 @@ type SetPinCallback = std::rc::Rc<dyn Fn(String, WeakEntity<SetPinContent>, &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<ConfirmContent>, &mut App) + 'static,
on_ok: impl Fn(WeakEntity<ConfirmContent>, &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<String>, cx: &mut Context<Self>) {
self.phase = DialogPhase::LoadingWithMessage(msg.into());
cx.notify();
}
pub fn set_success(&mut self, msg: String, cx: &mut Context<Self>) {
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()
+7 -4
View File
@@ -221,15 +221,18 @@ impl<V: 'static> AppSidebar<V> {
.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()
+11 -1
View File
@@ -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;
+5 -1
View File
@@ -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<FullDeviceStatus>,
pub fido_info: Option<FidoDeviceInfo>,
pub led_status: Option<LedStatusConfig>,
pub management_apps: Option<ManagementAppConfig>,
pub error: Option<String>,
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,
}
+327 -8
View File
@@ -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<Task<()>>,
}
@@ -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<Self>, 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<Self>) {
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<Self>, 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<Self>) {
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(
+6
View File
@@ -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),
+166 -6
View File
@@ -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<Self>) -> 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<Self>) {
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<Self>) {
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<Self>) -> 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();
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-alert-icon lucide-circle-alert"><circle cx="12" cy="12" r="10"/><line x1="12" x2="12" y1="8" y2="12"/><line x1="12" x2="12.01" y1="16" y2="16"/></svg>

After

Width:  |  Height:  |  Size: 359 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-trash-icon lucide-trash"><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>

After

Width:  |  Height:  |  Size: 354 B