mirror of
https://github.com/librekeys/picoforge.git
synced 2026-07-28 08:01:19 -07:00
feat: add support for RS-Keys firmware and add optito reset the device
This commit is contained in:
Generated
+1
-1
@@ -4133,7 +4133,7 @@ checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315"
|
||||
|
||||
[[package]]
|
||||
name = "picoforge"
|
||||
version = "0.5.0"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"anyhow",
|
||||
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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(
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
|
||||
@@ -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 |
@@ -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 |
Reference in New Issue
Block a user