feat(hal): introduce RS-Key(0.3.x) support and FirmwareTrait architecture

This commit significantly refactors the HAL layer to introduce proper support for the RS-Key firmware and abstraction across different hardware profiles
  (PicoFido, RSKey, LkOne).

    Key changes include:
    - **Firmware Abstraction**: Added `FirmwareTrait` and `AnyFirmware` in `src/hal/firmwares/mod.rs` with specific implementations for `PicoFidoFirmware` and
  `RSKeyFirmware` to decouple hardware-specific logic from the high-level `io.rs` layer.
    - **RS-Key FIDO Support**: Implemented RS-Key payload logic in `src/hal/fido/mod.rs` to support reading/writing LED configuration and DEV_CONF directly over
  the CTAPHID FIDO transport using TLV and custom command targets.
    - **Constants Cleanup**: Stripped out and consolidated 200+ lines of raw CTAP2 constants and vendor opcodes from `src/hal/fido/constants.rs` to clean up the
  module.
    - **UI & PIN Dialogs**: Updated `src/ui/screens/config/view_model.rs` to route `DeviceMethod` correctly when applying configurations. Added
  `StatusDialogHandle` integration to prompt the user for their FIDO PIN and instruct them to "Please touch your device if it flashes" when performing hardware
  config writes over FIDO.
    - **LkOne Type**: Added the `LkOne` AAGUID and firmware types to the hardware definition tree.
    - **Home View Check**: Fixed a UI issue in `home/view.rs` to conditionally display the LED config card based on whether the `FirmwareType` actually supports
  FIDO config modifications (like RS-Key).

    This lays the architectural foundation needed to handle FIDO configurations dynamically based on the discovered firmware type.
This commit is contained in:
Suyog Tandel
2026-07-08 21:14:55 +05:30
parent b293e294f5
commit c76b059ae6
16 changed files with 1172 additions and 417 deletions
+1
View File
@@ -1,4 +1,5 @@
#![allow(dead_code)]
use std::fmt;
#[repr(i32)]
+1 -1
View File
@@ -1,6 +1,6 @@
#![allow(dead_code)]
use std::fmt;
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FirmwareVersion {
pub major: u16,
+35 -176
View File
@@ -27,6 +27,8 @@
use std::fmt;
pub use crate::hal::common::cose::{CoseAlgorithm, CoseCurve, CoseKeyParam};
// ══════════════════════════════════════════════════════════════════════════════
// CTAP2 STANDARD — FIDO Alliance specification §8.1
// ══════════════════════════════════════════════════════════════════════════════
@@ -343,169 +345,6 @@ bitflags::bitflags! {
}
}
// ── COSE key types (RFC 8152) ───────────────────────────────────────────────
/// COSE algorithm identifiers (IANA COSE Algorithms registry).
///
/// Used in `pubKeyCredParams` to specify which signature algorithms
/// the platform supports. The authenticator picks the first match.
#[repr(i32)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CoseAlgorithm {
/// ECDSA with P-256 and SHA-256 (most common for WebAuthn).
ES256 = -7,
/// EdDSA with Ed25519.
EdDSA = -8,
/// ECDSA with P-256 (alternate ID, same as ES256).
ESP256 = -9,
/// EdDSA with Ed25519 (alternate ID).
Ed25519 = -19,
/// ECDH-ES with HKDF-256 key agreement.
EcdhEsHkdf256 = -25,
/// ECDSA with P-384 and SHA-384.
ES384 = -35,
/// ECDSA with P-521 and SHA-512.
ES512 = -36,
/// ECDSA with secp256k1 and SHA-256 (Bitcoin curve).
ES256K = -47,
/// ECDSA with P-384 (alternate ID).
ESP384 = -51,
/// ECDSA with P-521 (alternate ID).
ESP512 = -52,
/// EdDSA with Ed448.
Ed448 = -53,
/// RSASSA-PKCS1-v1_5 with SHA-256.
RS256 = -257,
/// RSASSA-PKCS1-v1_5 with SHA-384.
RS384 = -258,
/// RSASSA-PKCS1-v1_5 with SHA-512.
RS512 = -259,
/// ECDSA with brainpool256r1 and SHA-256.
ESB256 = -265,
/// ECDSA with brainpool384r1 and SHA-384.
ESB384 = -267,
/// ECDSA with brainpool512r1 and SHA-512.
ESB512 = -268,
/// ML-DSA-44 (FIPS 204, Level 2) — post-quantum signing.
///
/// RS-Key specific. Uses COSE key type AKP (7) instead of EC2/OKP.
MLDSA44 = -48,
/// ML-DSA-65 (FIPS 204, Level 3) — declared in getInfo but may be
/// unsupported for credential creation.
MLDSA65 = -49,
/// ML-DSA-87 (FIPS 204, Level 5) — declared in getInfo but may be
/// unsupported for credential creation.
MLDSA87 = -50,
}
impl CoseAlgorithm {
/// Convert a raw i128 (from CBOR) to a [`CoseAlgorithm`].
pub fn from_i128(val: i128) -> Option<Self> {
match val as i32 {
-7 => Some(Self::ES256),
-8 => Some(Self::EdDSA),
-9 => Some(Self::ESP256),
-19 => Some(Self::Ed25519),
-25 => Some(Self::EcdhEsHkdf256),
-35 => Some(Self::ES384),
-36 => Some(Self::ES512),
-47 => Some(Self::ES256K),
-51 => Some(Self::ESP384),
-52 => Some(Self::ESP512),
-53 => Some(Self::Ed448),
-257 => Some(Self::RS256),
-258 => Some(Self::RS384),
-259 => Some(Self::RS512),
-265 => Some(Self::ESB256),
-267 => Some(Self::ESB384),
-268 => Some(Self::ESB512),
-48 => Some(Self::MLDSA44),
-49 => Some(Self::MLDSA65),
-50 => Some(Self::MLDSA87),
_ => None,
}
}
}
impl fmt::Display for CoseAlgorithm {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::ES256 => write!(f, "ES256"),
Self::EdDSA => write!(f, "EdDSA"),
Self::ESP256 => write!(f, "ESP256"),
Self::Ed25519 => write!(f, "Ed25519"),
Self::EcdhEsHkdf256 => write!(f, "ECDH-ES-HKDF-256"),
Self::ES384 => write!(f, "ES384"),
Self::ES512 => write!(f, "ES512"),
Self::ES256K => write!(f, "ES256K"),
Self::ESP384 => write!(f, "ESP384"),
Self::ESP512 => write!(f, "ESP512"),
Self::Ed448 => write!(f, "Ed448"),
Self::RS256 => write!(f, "RS256"),
Self::RS384 => write!(f, "RS384"),
Self::RS512 => write!(f, "RS512"),
Self::ESB256 => write!(f, "ESB256"),
Self::ESB384 => write!(f, "ESB384"),
Self::ESB512 => write!(f, "ESB512"),
Self::MLDSA44 => write!(f, "ML-DSA-44"),
Self::MLDSA65 => write!(f, "ML-DSA-65"),
Self::MLDSA87 => write!(f, "ML-DSA-87"),
}
}
}
/// COSE elliptic curve identifiers (RFC 8152 §13.1.1).
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CoseCurve {
/// NIST P-256 (secp256r1, prime256v1).
P256 = 1,
/// NIST P-384 (secp384r1).
P384 = 2,
/// NIST P-521 (secp521r1).
P521 = 3,
/// X25519 for key agreement.
X25519 = 4,
/// X448 for key agreement.
X448 = 5,
/// Ed25519 for signing.
Ed25519 = 6,
/// Ed448 for signing.
Ed448 = 7,
/// secp256k1 (Bitcoin/Ethereum curve).
P256K1 = 8,
/// BrainpoolP256R1.
BP256R1 = 9,
/// BrainpoolP384R1.
BP384R1 = 10,
/// BrainpoolP512R1.
BP512R1 = 11,
}
/// COSE key parameter identifiers (RFC 8152 §7.1).
#[repr(i32)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CoseKeyParam {
/// Key type (OKP, EC2, RSA, etc.).
Kty = 1,
/// Key identifier.
Kid = 2,
/// Algorithm identifier.
Alg = 3,
/// Key operations (sign, verify, encrypt, etc.).
KeyOps = 4,
/// Base IV for symmetric operations.
BaseIV = 5,
/// Elliptic curve identifier.
Crv = -1,
/// X coordinate (EC2) or public key bytes (OKP).
X = -2,
/// Y coordinate (EC2).
Y = -3,
/// Private key (EC2 or OKP).
D = -4,
}
// ── CTAP2 errors (§8.2) ────────────────────────────────────────────────────
/// CTAP2 error codes (§8.2).
@@ -609,7 +448,7 @@ pub enum Ctap2Error {
/// - **All versions**: Backup(0x01), MSE(0x02), Unlock(0x03), EA(0x04)
/// - **≤ v7.2**: PhysicalOptions(0x05), Memory(0x06) — removed in later
/// releases. PicoForge keeps them for legacy device support.
/// - **Current**: AdminPin(0x08) added.
/// - **≥ v7.6**: AdminPin(0x08) added.
///
/// RS-Key uses a different vendor command scheme (CTAPHID 0x41 with
/// 64-bit sub-command IDs) — this enum does NOT apply to RS-Key.
@@ -632,6 +471,8 @@ pub enum VendorCommand {
///
/// **Legacy** (pico-fido ≤ v7.2 only). Removed in current firmware.
Memory = 0x06,
/// Admin PIN operations (added in pico-fido v7.6).
AdminPin = 0x08,
}
/// Pico-fido vendor config command IDs (64-bit).
@@ -869,11 +710,31 @@ pub const CTAP_VENDOR_CONFIG_CMD: u8 = 0xC2;
/// RS-Key CTAPHID vendor command (0x41).
///
/// Carries CBOR-encoded sub-commands for seed backup, attestation,
/// and audit operations. This is RS-Key specific and not part of pico-fido.
/// audit operations, and PicoForge hardware config.
/// This is RS-Key specific and not part of pico-fido.
///
/// See [RS-Key protocol §9](https://themaxmur.github.io/RS-Key/develop/) for details.
pub const RSKEY_CTAPHID_VENDOR_CMD: u8 = 0x41;
/// RS-Key CONFIG_READ sub-command ID (0x0D).
///
/// Reads device configuration over FIDO. Supports DEV_CONF (0x00),
/// PHY (0x01), and LED (0x02) targets. Ungated — no PIN needed.
pub const RSKEY_CONFIG_READ: u8 = 0x0D;
/// RS-Key CONFIG_WRITE sub-command ID (0x0C).
///
/// Writes device configuration over FIDO. Supports the same targets
/// as CONFIG_READ. Requires ACFG-gated PIN token.
pub const RSKEY_CONFIG_WRITE: u8 = 0x0C;
/// RS-Key config target: device configuration (VID, PID, serial, product name).
pub const RSKEY_CFG_TARGET_DEV_CONF: u8 = 0x00;
/// RS-Key config target: physical config (LED GPIO, brightness, options).
pub const RSKEY_CFG_TARGET_PHY: u8 = 0x01;
/// RS-Key config target: LED status config.
pub const RSKEY_CFG_TARGET_LED: u8 = 0x02;
// ══════════════════════════════════════════════════════════════════════════════
// SHARED PROTOCOL CONSTANTS
// ══════════════════════════════════════════════════════════════════════════════
@@ -1237,6 +1098,7 @@ mod tests {
// PhysicalOptions(0x05) and Memory(0x06) are legacy <=v7.2
assert_eq!(VendorCommand::PhysicalOptions as u8, 0x05);
assert_eq!(VendorCommand::Memory as u8, 0x06);
assert_eq!(VendorCommand::AdminPin as u8, 0x08);
}
// ── RS-Key vendor command ────────────────────────────────────────────────
@@ -1271,20 +1133,17 @@ mod tests {
}
// ── Vendor config command IDs ────────────────────────────────────────────
// Reference: pico-fido src/fido/ctap.h (for auth/enable/disable/EA/PIN)
// Reference: pico-fido src/fido/ctap.h (tagged releases v3.0v7.6)
// RS-Key protocol docs §11 (for physical config commands)
//
// NOTE: The auth encryption and PIN policy IDs in PicoForge do NOT match the
// current pico-fido ctap.h values. The ctap.h values are documented below
// for reference but the PicoForge values may target an older firmware version.
// PicoForge values match all tagged releases (v3.0v7.6). The `main`
// branch restructured these to a 0x000X... prefix (unreleased) — not
// relevant for current version targeting.
//
// Firmware ctap.h values:
// AuthEncryptionEnable: 0x00043f56b34285e2
// AuthEncryptionDisable: 0x0001a40f04a25ed9
// EnterpriseAttestationUpload: 0x0002a674c29a8dcf
// PinComplexityPolicy: 0x0007d70fe96c3897
//
// PicoForge values (verified against RS-Key protocol for physical ones):
// v3.0+: AuthEncryptionEnable(0x03e4...), AuthEncryptionDisable(0x1831...)
// v7.0+: EnterpriseAttestationUpload(0x66f2...), PinComplexityPolicy(0x6c07...)
// v7.0+: PhysicalOptions changed from 0x969f... (v6.06.4) to 0x269f... (v7.0+)
// main (unreleased): all changed to 0x000X... prefix, PHY options removed
#[test]
fn test_vendor_config_command_from_u64() {
+74
View File
@@ -1882,6 +1882,80 @@ impl HidTransport {
Ok(())
}
/// Read physical configuration from an RS-Key via CTAPHID 0x41 CONFIG_READ.
///
/// Sends `{1: 0x0D, 2: {1: target}}` CBOR payload to the RS-Key vendor
/// command handler inside a CTAPHID_CBOR message with the vendor sub-command
/// prefix. Returns raw TLV bytes for the requested target.
/// Ungated — no PIN needed.
///
/// Targets: `RSKEY_CFG_TARGET_DEV_CONF` (0x00), `RSKEY_CFG_TARGET_PHY` (0x01),
/// `RSKEY_CFG_TARGET_LED` (0x02).
pub fn rs_key_config_read(&self, target: u8) -> Result<Vec<u8>, PFError> {
let mut params = BTreeMap::new();
params.insert(Value::Integer(1), Value::Integer(RSKEY_CONFIG_READ as i128));
let mut target_map = BTreeMap::new();
target_map.insert(Value::Integer(1), Value::Integer(target as i128));
params.insert(Value::Integer(2), Value::Map(target_map));
let inner = to_vec(&Value::Map(params)).map_err(|e| PFError::Io(e.to_string()))?;
let mut full_payload = vec![RSKEY_CTAPHID_VENDOR_CMD];
full_payload.extend(inner);
self.send_cbor(CTAPHID_CBOR, &full_payload)
}
/// Write physical configuration to an RS-Key via CTAPHID 0x41 CONFIG_WRITE.
///
/// Sends `{1: 0x0C, 2: {1: target, 2: blob}, 3: protocol, 4: mac}` CBOR
/// to the RS-Key vendor command handler. Requires a PIN token obtained with
/// `AUTHENTICATOR_CONFIG` permission.
///
/// The MAC is computed as `HMAC-SHA256(pin_token, 0xFF*32 || 0x41 || 0x0C || cbor_params)[..16]`
/// per the RS-Key protocol spec.
pub fn rs_key_config_write(
&self,
pin_token: &[u8],
target: u8,
blob: &[u8],
) -> Result<(), PFError> {
let mut params_map = BTreeMap::new();
params_map.insert(Value::Integer(1), Value::Integer(target as i128));
params_map.insert(Value::Integer(2), Value::Bytes(blob.to_vec()));
let params = Value::Map(params_map);
let params_bytes = to_vec(&params).map_err(|e| PFError::Io(e.to_string()))?;
// MAC = HMAC-SHA256(pin_token, 0xFF*32 || vendor_cmd || sub_cmd || cbor_params)[..16]
let mac = {
let mut input = vec![0xFFu8; 32];
input.push(RSKEY_CTAPHID_VENDOR_CMD);
input.push(RSKEY_CONFIG_WRITE);
input.extend(&params_bytes);
let hmac_key = hmac::Key::new(hmac::HMAC_SHA256, pin_token);
hmac::sign(&hmac_key, &input).as_ref()[..16].to_vec()
};
let mut outer = BTreeMap::new();
outer.insert(
Value::Integer(1),
Value::Integer(RSKEY_CONFIG_WRITE as i128),
);
outer.insert(Value::Integer(2), params);
outer.insert(Value::Integer(3), Value::Integer(1)); // PIN protocol v1
outer.insert(Value::Integer(4), Value::Bytes(mac));
let inner = to_vec(&Value::Map(outer)).map_err(|e| PFError::Io(e.to_string()))?;
let mut full_payload = vec![RSKEY_CTAPHID_VENDOR_CMD];
full_payload.extend(inner);
// CONFIG_WRITE can involve flash erasure/write which takes
// several seconds on RP2040 — use a generous timeout.
const CONFIG_WRITE_TIMEOUT_MS: i32 = 30_000;
self.send_cbor_with_timeout(CTAPHID_CBOR, &full_payload, CONFIG_WRITE_TIMEOUT_MS)
.map(|_| ())
}
/// Sign a credential management command using HMAC-SHA-256.
///
/// Uses pico-fido's non-standard signing scheme: for sub-commands 0x01
+559 -74
View File
File diff suppressed because it is too large Load Diff
+28 -3
View File
@@ -6,7 +6,7 @@ pub use picofido::*;
pub use rskey::*;
use crate::hal::common::FirmwareVersion;
use crate::hal::types::FirmwareType;
use crate::hal::types::*;
#[derive(Debug, Clone)]
pub enum AnyFirmware {
@@ -25,6 +25,7 @@ pub trait FirmwareTrait {
}
fn supports_legacy_fido_hardware_config(&self) -> bool;
fn supports_fido_config_write(&self) -> bool;
fn supports_rs_key_vendor_command(&self) -> bool;
fn supports_rescue_channel(&self) -> bool;
}
@@ -33,7 +34,9 @@ impl AnyFirmware {
pub fn detect_by_aaguid(aaguid: &str) -> FirmwareType {
if aaguid == crate::hal::types::RSKEY_AAGUID {
FirmwareType::RSKey
} else if aaguid == crate::hal::types::PICOFIDO_AAGUID {
} else if aaguid == crate::hal::types::PICOFIDO_AAGUID
|| aaguid == crate::hal::types::LKONE_AAGUID
{
FirmwareType::PicoFido
} else {
FirmwareType::Unknown
@@ -45,7 +48,22 @@ impl AnyFirmware {
match fw_type {
FirmwareType::PicoFido => Self::PicoFido(PicoFidoFirmware::new(ver)),
FirmwareType::RSKey => Self::RSKey(RSKeyFirmware::new(ver)),
FirmwareType::Unknown => Self::PicoFido(PicoFidoFirmware::new(ver)),
FirmwareType::LkOne | FirmwareType::Unknown => {
Self::PicoFido(PicoFidoFirmware::new(ver))
}
}
}
pub fn new_with_legacy(fw_type: FirmwareType, version: &str, has_legacy_vendor: bool) -> Self {
let ver = FirmwareVersion::parse(version).unwrap_or_default();
match fw_type {
FirmwareType::PicoFido => {
Self::PicoFido(PicoFidoFirmware::new(ver).with_legacy_vendor(has_legacy_vendor))
}
FirmwareType::RSKey => Self::RSKey(RSKeyFirmware::new(ver)),
FirmwareType::LkOne | FirmwareType::Unknown => {
Self::PicoFido(PicoFidoFirmware::new(ver))
}
}
}
@@ -70,6 +88,13 @@ impl AnyFirmware {
}
}
pub fn supports_fido_config_write(&self) -> bool {
match self {
Self::PicoFido(fw) => fw.supports_fido_config_write(),
Self::RSKey(fw) => fw.supports_fido_config_write(),
}
}
pub fn supports_new_fido_hardware_config(&self) -> bool {
match self {
Self::PicoFido(fw) => !fw.supports_legacy_fido_hardware_config(),
+19 -2
View File
@@ -5,11 +5,22 @@ use crate::hal::types::FirmwareType;
#[derive(Debug, Clone)]
pub struct PicoFidoFirmware {
version: FirmwareVersion,
/// Whether the device responded positively to the legacy
/// VendorPrototype 0xFF probe (PicoForge CONFIG_PHY_* commands).
has_legacy_vendor: bool,
}
impl PicoFidoFirmware {
pub fn new(version: FirmwareVersion) -> Self {
Self { version }
Self {
version,
has_legacy_vendor: false,
}
}
pub fn with_legacy_vendor(mut self, legacy: bool) -> Self {
self.has_legacy_vendor = legacy;
self
}
}
@@ -23,7 +34,13 @@ impl FirmwareTrait for PicoFidoFirmware {
}
fn supports_legacy_fido_hardware_config(&self) -> bool {
self.version.major < 7 || (self.version.major == 7 && self.version.minor <= 2)
self.has_legacy_vendor
|| self.version.major < 7
|| (self.version.major == 7 && self.version.minor <= 2)
}
fn supports_fido_config_write(&self) -> bool {
self.has_legacy_vendor || self.version.major >= 7
}
fn supports_rs_key_vendor_command(&self) -> bool {
+15 -1
View File
@@ -22,12 +22,26 @@ impl FirmwareTrait for RSKeyFirmware {
&self.version
}
/// RS-Key reports firmware 5.x (< 7) per the SDK version scheme.
/// Per the protocol integration notes, this version range triggers
/// PicoForge's legacy hardware-config path (authenticatorConfig +
/// vendorPrototype) which RS-Key supports for writes, and for reads
/// it tries the 0x41 CONFIG_READ path instead.
fn supports_legacy_fido_hardware_config(&self) -> bool {
false
}
/// RS-Key supports FIDO config write via CTAPHID 0x41 CONFIG_WRITE
/// on v0.3.1+. The CTAP firmware version from GET_INFO reports the SDK
/// version (e.g., 5.7) which does not map to the RS-Key release version,
/// so we cannot version-gate here. Actual support is determined via a
/// runtime CONFIG_READ probe in write_rskey_config().
fn supports_fido_config_write(&self) -> bool {
true
}
fn supports_rs_key_vendor_command(&self) -> bool {
self.version.is_at_least(0, 1)
true
}
fn supports_rescue_channel(&self) -> bool {
+92 -25
View File
@@ -1,26 +1,40 @@
use crate::{
error::PFError,
hal::{fido, rescue, types::*},
hal::{fido, rescue, transport::DeviceHandle, types::*},
};
pub fn read_device_details() -> Result<FullDeviceStatus, PFError> {
let mut fido_status: Option<FullDeviceStatus> = None;
let mut rescue_status: Option<FullDeviceStatus> = None;
let mut rescue_fw_type: Option<FirmwareType> = None;
match fido::read_device_details() {
Ok(status) => {
log::info!("FIDO device details read successfully");
fido_status = Some(status);
}
Err(e) => log::warn!("FIDO read_device_details failed: {}", e),
// Discover via FIDO/HID transport
match DeviceHandle::try_fido() {
Ok(Some((_handle, _identity))) => match fido::read_device_details() {
Ok(status) => {
log::info!("FIDO device details read successfully");
fido_status = Some(status);
}
Err(e) => log::warn!("FIDO read_device_details failed: {}", e),
},
Ok(None) => log::info!("No FIDO HID device found"),
Err(e) => log::warn!("FIDO HID discovery error: {}", e),
}
match rescue::read_device_details() {
Ok(status) => {
log::info!("Rescue device details read successfully");
rescue_status = Some(status);
// Discover via Rescue/PC/SC transport
match DeviceHandle::try_rescue() {
Ok(Some((handle, _identity))) => {
rescue_fw_type = Some(handle.firmware_type());
match rescue::read_device_details() {
Ok(status) => {
log::info!("Rescue device details read successfully");
rescue_status = Some(status);
}
Err(e) => log::warn!("Rescue read_device_details failed: {}", e),
}
}
Err(e) => log::warn!("Rescue read_device_details failed: {}", e),
Ok(None) => log::info!("No Rescue PC/SC device found"),
Err(e) => log::warn!("Rescue PC/SC discovery error: {}", e),
}
match (fido_status, rescue_status) {
@@ -79,7 +93,17 @@ pub fn read_device_details() -> Result<FullDeviceStatus, PFError> {
}
(None, Some(rescue)) => {
log::info!("Using Rescue-only device details");
Ok(rescue)
let ft = rescue_fw_type.and_then(|ft| {
if rescue.firmware_type == FirmwareType::Unknown {
Some(ft)
} else {
None
}
});
Ok(FullDeviceStatus {
firmware_type: ft.unwrap_or(rescue.firmware_type),
..rescue
})
}
(None, None) => {
log::error!("Failed to read device details via both FIDO and Rescue");
@@ -110,25 +134,68 @@ pub fn write_config(
}
}
pub fn read_led_config() -> Result<LedStatusConfig, PFError> {
rescue::read_led_config()
pub fn read_led_config(method: DeviceMethod) -> Result<LedStatusConfig, PFError> {
match method {
DeviceMethod::Fido => {
let transport = crate::hal::fido::hid::HidTransport::open()?;
fido::read_rskey_led_config(&transport)
}
DeviceMethod::Rescue => rescue::read_led_config(),
}
}
pub fn write_led_status(
status: u8,
color: u8,
brightness: u8,
steady: bool,
pub fn write_led_config(
method: DeviceMethod,
config: LedStatusConfig,
pin: Option<String>,
) -> Result<String, PFError> {
rescue::write_led_status(status, color, brightness, steady)
match method {
DeviceMethod::Fido => {
let pin = pin.ok_or_else(|| {
PFError::Device("PIN is required for FIDO LED config write".into())
})?;
let transport = crate::hal::fido::hid::HidTransport::open()?;
fido::write_rskey_led_config(&transport, &config, &pin)
}
DeviceMethod::Rescue => {
for i in 0..4 {
let (color, brightness) = config.statuses[i];
rescue::write_led_status(i as u8, color, brightness, config.steady)?;
}
Ok("LED configuration applied successfully.".to_string())
}
}
}
pub fn read_management_config() -> Result<ManagementAppConfig, PFError> {
rescue::read_management_config()
pub fn read_management_config(method: DeviceMethod) -> Result<ManagementAppConfig, PFError> {
match method {
DeviceMethod::Fido => {
let transport = crate::hal::fido::hid::HidTransport::open()?;
let info = fido::read_rskey_management_info(&transport)?;
Ok(ManagementAppConfig {
usb_supported: info.usb_supported.unwrap_or(0),
usb_enabled: info.usb_enabled.unwrap_or(0),
})
}
DeviceMethod::Rescue => rescue::read_management_config(),
}
}
pub fn write_management_config(enabled_mask: u16) -> Result<String, PFError> {
rescue::write_management_config(enabled_mask)
pub fn write_management_config(
method: DeviceMethod,
enabled_mask: u16,
pin: Option<String>,
) -> Result<String, PFError> {
match method {
DeviceMethod::Fido => {
let pin = pin.ok_or_else(|| {
PFError::Device("PIN is required for FIDO management config write".into())
})?;
let transport = crate::hal::fido::hid::HidTransport::open()?;
fido::write_rskey_dev_config(&transport, enabled_mask, &pin)
}
DeviceMethod::Rescue => rescue::write_management_config(enabled_mask),
}
}
pub(crate) fn get_fido_info() -> Result<FidoDeviceInfo, String> {
+29 -6
View File
@@ -1,4 +1,3 @@
#![allow(dead_code)]
use std::fmt;
use crate::error::PFError;
@@ -7,19 +6,20 @@ use crate::hal::types::FirmwareType;
pub enum DeviceHandle {
Fido(HidTransport),
Rescue(pcsc::Card, FirmwareType),
Rescue(FirmwareType),
}
impl fmt::Debug for DeviceHandle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Fido(t) => f.debug_tuple("Fido").field(t).finish(),
Self::Rescue(_, ft) => f.debug_tuple("Rescue").field(ft).finish(),
Self::Rescue(ft) => f.debug_tuple("Rescue").field(ft).finish(),
}
}
}
#[derive(Debug)]
#[allow(dead_code)]
pub struct DeviceIdentity {
pub vid: u16,
pub pid: u16,
@@ -28,6 +28,24 @@ pub struct DeviceIdentity {
}
impl DeviceHandle {
pub fn firmware_type(&self) -> FirmwareType {
match self {
Self::Fido(_) => FirmwareType::Unknown,
Self::Rescue(ft) => ft.clone(),
}
}
/// Extract the inner FIDO transport, consuming the handle.
#[allow(dead_code)]
pub fn into_fido(self) -> Option<HidTransport> {
match self {
Self::Fido(t) => Some(t),
_ => None,
}
}
/// Try to discover a device via FIDO HID first, falling back to Rescue PC/SC.
#[allow(dead_code)]
pub fn discover() -> Result<(Self, DeviceIdentity), PFError> {
match Self::try_fido() {
Ok(Some((handle, identity))) => {
@@ -50,7 +68,8 @@ impl DeviceHandle {
Err(PFError::NoDevice)
}
fn try_fido() -> Result<Option<(Self, DeviceIdentity)>, PFError> {
/// Try to connect via FIDO HID transport.
pub fn try_fido() -> Result<Option<(Self, DeviceIdentity)>, PFError> {
let transport = HidTransport::open()?;
let identity = DeviceIdentity {
vid: transport.vid,
@@ -61,7 +80,8 @@ impl DeviceHandle {
Ok(Some((Self::Fido(transport), identity)))
}
fn try_rescue() -> Result<Option<(Self, DeviceIdentity)>, PFError> {
/// Try to connect via Rescue PC/SC transport.
pub fn try_rescue() -> Result<Option<(Self, DeviceIdentity)>, PFError> {
let ctx = pcsc::Context::establish(pcsc::Scope::User).map_err(PFError::Pcsc)?;
let mut readers_buf = [0; 2048];
let mut readers = ctx.list_readers(&mut readers_buf).map_err(PFError::Pcsc)?;
@@ -75,15 +95,18 @@ impl DeviceHandle {
} else {
FirmwareType::Unknown
};
// Connection opened just to verify the reader is responsive;
// actual rescue operations open their own PC/SC connections.
let card = ctx
.connect(reader, pcsc::ShareMode::Shared, pcsc::Protocols::ANY)
.map_err(PFError::Pcsc)?;
drop(card);
let identity = DeviceIdentity {
vid: 0,
pid: 0,
product_name: reader_name.to_string(),
firmware_type: fw_type.clone(),
};
Ok(Some((Self::Rescue(card, fw_type), identity)))
Ok(Some((Self::Rescue(fw_type), identity)))
}
}
+7
View File
@@ -100,6 +100,7 @@ pub enum DeviceMethod {
pub enum FirmwareType {
PicoFido,
RSKey,
LkOne,
#[default]
Unknown,
}
@@ -109,6 +110,7 @@ impl fmt::Display for FirmwareType {
match self {
Self::PicoFido => write!(f, "pico-fido"),
Self::RSKey => write!(f, "RS-Key"),
Self::LkOne => write!(f, "LK-ONE"),
Self::Unknown => write!(f, "Unknown"),
}
}
@@ -179,3 +181,8 @@ pub struct StoredCredential {
pub const RSKEY_AAGUID: &str = "2479C7BF6B3056839EC80E8171A918B7";
/// AAGUID assigned to Pico-Fido hardware.
pub const PICOFIDO_AAGUID: &str = "89FB94B706C936739B7E30526D968145";
/// AAGUID assigned to LibreKeys LK-ONE hardware (same as pico-fido fork).
pub const LKONE_AAGUID: &str = "89FB94B706C936739B7E30526D968145";
/// LibreKeys USB VID:PID allocated by OpenMoko.
pub const LKONE_VID: u16 = 0x1D50;
pub const LKONE_PID: u16 = 0x619B;
+28 -17
View File
@@ -36,6 +36,11 @@ pub struct PinPromptContent {
}
impl PinPromptContent {
pub fn set_loading_msg(&mut self, msg: impl Into<String>, cx: &mut Context<Self>) {
self.phase = DialogPhase::LoadingWithMessage(msg.into());
cx.notify();
}
fn set_loading(&mut self, cx: &mut Context<Self>) {
self.phase = DialogPhase::Loading;
cx.notify();
@@ -95,23 +100,29 @@ impl Render for PinPromptContent {
)
.into_any_element(),
DialogPhase::Loading | DialogPhase::LoadingWithMessage(_) => v_flex()
.gap_4()
.child(self.description.clone())
.child(Input::new(&self.pin_input).disabled(true))
.child(
h_flex()
.justify_end()
.gap_2()
.child(Button::new("cancel").label("Cancel").disabled(true))
.child(
Button::new("confirm")
.primary()
.label("Loading...")
.loading(true),
),
)
.into_any_element(),
DialogPhase::Loading | DialogPhase::LoadingWithMessage(_) => {
let text = match &self.phase {
DialogPhase::LoadingWithMessage(msg) => msg.clone(),
_ => self.description.to_string(),
};
v_flex()
.gap_4()
.child(text)
.child(Input::new(&self.pin_input).disabled(true))
.child(
h_flex()
.justify_end()
.gap_2()
.child(Button::new("cancel").label("Cancel").disabled(true))
.child(
Button::new("confirm")
.primary()
.label("Loading...")
.loading(true),
),
)
.into_any_element()
}
DialogPhase::Error(err_msg) => {
let pin_input = self.pin_input.clone();
+22 -20
View File
@@ -14,6 +14,7 @@
//! - **`apply_fresh_state()`** lets ViewModels push post-write HAL results
//! back into the repo so subscribers get the event.
use crate::hal::firmwares::AnyFirmware;
use crate::hal::io;
use crate::hal::types;
use gpui::*;
@@ -23,7 +24,8 @@ pub use crate::hal::rescue::constants::{
USB_CAP_U2F,
};
pub use types::{
AppConfigInput, DeviceMethod, FidoDeviceInfo, FirmwareType, FullDeviceStatus, StoredCredential,
AppConfigInput, DeviceMethod, FidoDeviceInfo, FirmwareType, FullDeviceStatus, LedStatusConfig,
StoredCredential,
};
// ── Events ──────────────────────────────────────────────────────────────────
@@ -70,18 +72,19 @@ impl DeviceRepo {
// ── HAL static methods (blocking — call from background executor) ──────
pub fn firmware_supports_legacy_fido_config(version: &str) -> bool {
crate::hal::fido::firmware_supports_legacy_fido_hardware_config(version)
pub fn firmware_supports_legacy_fido_config(
fw_type: &types::FirmwareType,
version: &str,
) -> bool {
AnyFirmware::new(fw_type.clone(), version).supports_legacy_fido_hardware_config()
}
pub fn read_device_state_blocking() -> Result<FreshDeviceState, crate::error::PFError> {
let status = io::read_device_details()?;
let (led_status, management_apps) = if status.firmware_type == types::FirmwareType::RSKey
&& status.method == types::DeviceMethod::Rescue
{
let (led_status, management_apps) = if status.firmware_type == types::FirmwareType::RSKey {
(
io::read_led_config().ok(),
io::read_management_config().ok(),
io::read_led_config(status.method.clone()).ok(),
io::read_management_config(status.method.clone()).ok(),
)
} else {
(None, None)
@@ -101,19 +104,20 @@ impl DeviceRepo {
io::write_config(config, method, pin)
}
pub fn write_led_status_blocking(
status_idx: u8,
color: u8,
brightness: u8,
steady: bool,
pub fn write_led_config_blocking(
method: DeviceMethod,
config: LedStatusConfig,
pin: Option<String>,
) -> Result<String, crate::error::PFError> {
io::write_led_status(status_idx, color, brightness, steady)
io::write_led_config(method, config, pin)
}
pub fn write_management_config_blocking(
method: DeviceMethod,
enabled_mask: u16,
pin: Option<String>,
) -> Result<String, crate::error::PFError> {
io::write_management_config(enabled_mask)
io::write_management_config(method, enabled_mask, pin)
}
pub fn get_fido_info_blocking() -> Result<types::FidoDeviceInfo, String> {
@@ -223,11 +227,9 @@ impl DeviceRepo {
}
}
if status.firmware_type == types::FirmwareType::RSKey
&& status.method == types::DeviceMethod::Rescue
{
self.led_status = io::read_led_config().ok();
self.management_apps = io::read_management_config().ok();
if status.firmware_type == types::FirmwareType::RSKey {
self.led_status = io::read_led_config(status.method.clone()).ok();
self.management_apps = io::read_management_config(status.method.clone()).ok();
} else {
self.led_status = None;
self.management_apps = None;
+17 -12
View File
@@ -578,39 +578,44 @@ impl Render for ConfigViewModel {
let device = self.device.read(cx);
let status = device.status.clone();
let is_fido = status.as_ref().map(|s| s.method.clone()) == Some(DeviceMethod::Fido);
let is_rskey = status.as_ref().map(|s| &s.firmware_type) == Some(&FirmwareType::RSKey);
let supports_legacy_fido_config = status
.as_ref()
.map(ConfigViewModel::status_supports_legacy_fido_config)
.unwrap_or(false);
let hardware_config_disabled = is_fido && !supports_legacy_fido_config;
let hardware_config_disabled = is_fido && !supports_legacy_fido_config && !is_rskey;
// RS-Key supports full config read/write over FIDO via CONFIG_READ/CONFIG_WRITE.
// Other firmwares (pico-fido) don't: product name, LED driver, curves, etc.
let fido_no_rskey = is_fido && !is_rskey;
let led_card = self
.render_led_card(cx, is_fido, hardware_config_disabled)
.render_led_card(cx, fido_no_rskey, hardware_config_disabled)
.into_any_element();
let options_card = self
.render_options_card(cx, is_fido, hardware_config_disabled)
.render_options_card(cx, fido_no_rskey, hardware_config_disabled)
.into_any_element();
let identity_card = self
.render_identity_card(cx.theme(), is_fido, hardware_config_disabled)
.render_identity_card(cx.theme(), fido_no_rskey, hardware_config_disabled)
.into_any_element();
let touch_card = self
.render_touch_card(cx.theme(), is_fido)
.render_touch_card(cx.theme(), fido_no_rskey)
.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(&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();
let rskey_usb_itf = self
.render_rskey_usb_itf_card(cx, is_fido)
.into_any_element();
// RS-Key cards always enabled in FIDO mode — they work via
// CONFIG_WRITE with PIN token.
let rskey_led = self.render_rskey_led_card(cx, false).into_any_element();
let rskey_apps = self.render_rskey_apps_card(cx, false).into_any_element();
let rskey_usb_itf = self.render_rskey_usb_itf_card(cx, false).into_any_element();
grid_children.push(rskey_led);
grid_children.push(rskey_apps);
grid_children.push(rskey_usb_itf);
+241 -78
View File
@@ -2,7 +2,7 @@ use crate::ui::app::AppModels;
use crate::ui::components::dialog::PinPromptContent;
use crate::ui::components::{dialog, dialog::StatusContent};
use crate::ui::models::device::{
AppConfigInput, DeviceEvent, DeviceMethod, DeviceRepo, FullDeviceStatus,
AppConfigInput, DeviceEvent, DeviceMethod, DeviceRepo, FullDeviceStatus, LedStatusConfig,
};
use gpui::*;
use gpui_component::input::InputState;
@@ -446,11 +446,33 @@ impl ConfigViewModel {
return;
}
let dialog = dialog_handle;
// Tell the user to look at their key!
cx.update(|cx| {
match &dialog {
StatusDialogHandle::Pin(dh) => {
let _ = dh.update(cx, |d, cx| {
d.set_loading_msg("Applying configuration... Please touch your device if it flashes.", cx);
});
}
StatusDialogHandle::Status(dh) => {
let _ = dh.update(cx, |d, cx| {
d.set_loading("Applying configuration... Please touch your device if it flashes.", cx);
});
}
}
}).ok();
let result = cx
.background_executor()
.spawn(async move { DeviceRepo::write_config_blocking(changes, method_clone, pin) })
.spawn(async move {
DeviceRepo::write_config_blocking(changes, method_clone, pin)
})
.await;
let dialog_handle = dialog;
let fresh_state = if result.is_ok() {
cx.background_executor()
.spawn(async move { DeviceRepo::read_device_state_blocking().ok() })
@@ -516,6 +538,8 @@ impl ConfigViewModel {
if method == DeviceMethod::Fido && err_msg.contains("0x3E") {
err_msg = "The device firmware does not support being configured in fido only communication mode. \nHave a look at the troubleshooting guide to fix this".to_string();
} else if method == DeviceMethod::Fido && err_msg.contains("0x27") {
err_msg = "Configuration denied (Status: 0x27). This usually means the operation timed out waiting for you to touch the device's button, or the PIN token was rejected.".to_string();
}
match &dialog_handle {
@@ -681,7 +705,8 @@ impl ConfigViewModel {
};
if method == DeviceMethod::Fido {
if Self::status_supports_legacy_fido_config(status) {
let is_rskey = status.firmware_type == crate::ui::models::device::FirmwareType::RSKey;
if Self::status_supports_legacy_fido_config(status) || is_rskey {
self.open_pin_dialog(changes, window, cx);
} else {
let handle =
@@ -708,7 +733,10 @@ impl ConfigViewModel {
pub(super) fn status_supports_legacy_fido_config(status: &FullDeviceStatus) -> bool {
status.method == DeviceMethod::Fido
&& DeviceRepo::firmware_supports_legacy_fido_config(&status.info.firmware_version)
&& DeviceRepo::firmware_supports_legacy_fido_config(
&status.firmware_type,
&status.info.firmware_version,
)
}
#[allow(dead_code)]
@@ -799,27 +827,194 @@ impl ConfigViewModel {
}
pub(super) 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;
let config = LedStatusConfig {
steady: self.led_status_steady,
statuses: [
(self.led_status_colors[0], self.led_status_brightness[0]),
(self.led_status_colors[1], self.led_status_brightness[1]),
(self.led_status_colors[2], self.led_status_brightness[2]),
(self.led_status_colors[3], self.led_status_brightness[3]),
],
};
let method = self
.device
.read(cx)
.status
.as_ref()
.map(|s| s.method.clone());
if method == Some(DeviceMethod::Fido) {
let view_handle = cx.entity().downgrade();
dialog::open_pin_prompt(
"Authentication Required",
"Enter your device PIN to update LED configuration.",
None,
"Confirm",
window,
cx,
move |pin, dialog_handle, cx| {
let _ = view_handle.update(cx, |this, cx| {
this.do_write_led_config(
config.clone(),
DeviceMethod::Fido,
Some(pin),
StatusDialogHandle::Pin(dialog_handle),
cx,
);
});
},
);
} else {
let handle = dialog::open_status_dialog("Applying LED Configuration...", window, cx);
self.do_write_led_config(
config,
DeviceMethod::Rescue,
None,
StatusDialogHandle::Status(handle),
cx,
);
}
}
fn do_write_led_config(
&mut self,
config: LedStatusConfig,
method: DeviceMethod,
pin: Option<String>,
dialog_handle: StatusDialogHandle,
cx: &mut Context<Self>,
) {
self.loading = true;
let handle = dialog::open_status_dialog("Applying LED Configuration...", window, cx);
cx.notify();
let entity = cx.entity().downgrade();
self._task = Some(cx.spawn(async move |_, cx| {
let result = cx
.background_executor()
.spawn(async move { DeviceRepo::write_led_config_blocking(method, config, pin) })
.await;
let fresh_state = if result.is_ok() {
cx.background_executor()
.spawn(async move { DeviceRepo::read_device_state_blocking().ok() })
.await
} else {
None
};
let _ = entity.update(cx, |this, cx| {
this.loading = false;
match result {
Ok(_) => {
if let Some(fs) = fresh_state {
this.device.update(cx, |repo, repo_cx| {
repo.apply_fresh_state(fs, repo_cx);
});
}
match &dialog_handle {
StatusDialogHandle::Pin(dh) => {
let _ = dh.update(cx, |d, cx| {
d.set_success(
"LED configuration applied successfully.".to_string(),
cx,
);
});
}
StatusDialogHandle::Status(dh) => {
let _ = dh.update(cx, |d, cx| {
d.set_success(
"LED configuration applied successfully.".to_string(),
cx,
);
});
}
}
}
Err(e) => match &dialog_handle {
StatusDialogHandle::Pin(dh) => {
let _ = dh.update(cx, |d, cx| {
d.set_error(format!("Failed to apply LED config: {}", e), cx);
});
}
StatusDialogHandle::Status(dh) => {
let _ = dh.update(cx, |d, cx| {
d.set_error(format!("Failed to apply LED config: {}", e), cx);
});
}
},
}
cx.notify();
});
}));
}
pub(super) fn apply_rskey_apps_settings(
&mut self,
window: &mut Window,
cx: &mut Context<Self>,
) {
let mask = self.usb_apps_enabled;
let method = self
.device
.read(cx)
.status
.as_ref()
.map(|s| s.method.clone());
if method == Some(DeviceMethod::Fido) {
let view_handle = cx.entity().downgrade();
dialog::open_pin_prompt(
"Authentication Required",
"Enter your device PIN to update USB application configuration.",
None,
"Confirm",
window,
cx,
move |pin, dialog_handle, cx| {
let _ = view_handle.update(cx, |this, cx| {
this.do_write_management_config(
mask,
DeviceMethod::Fido,
Some(pin),
StatusDialogHandle::Pin(dialog_handle),
cx,
);
});
},
);
} else {
let handle = dialog::open_status_dialog("Applying USB Applications...", window, cx);
self.do_write_management_config(
mask,
DeviceMethod::Rescue,
None,
StatusDialogHandle::Status(handle),
cx,
);
}
}
fn do_write_management_config(
&mut self,
mask: u16,
method: DeviceMethod,
pin: Option<String>,
dialog_handle: StatusDialogHandle,
cx: &mut Context<Self>,
) {
self.loading = true;
cx.notify();
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 {
DeviceRepo::write_led_status_blocking(
i as u8,
colors[i],
brightnesses[i],
steady,
)?;
}
Ok::<_, crate::error::PFError>(())
DeviceRepo::write_management_config_blocking(method, mask, pin)
})
.await;
@@ -840,70 +1035,38 @@ impl ConfigViewModel {
repo.apply_fresh_state(fs, repo_cx);
});
}
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();
});
}));
}
pub(super) 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 { DeviceRepo::write_management_config_blocking(mask) })
.await;
let fresh_state = if result.is_ok() {
cx.background_executor()
.spawn(async move { DeviceRepo::read_device_state_blocking().ok() })
.await
} else {
None
};
let _ = entity.update(cx, |this, cx| {
this.loading = false;
match result {
Ok(_) => {
if let Some(fs) = fresh_state {
this.device.update(cx, |repo, repo_cx| {
repo.apply_fresh_state(fs, repo_cx);
});
match &dialog_handle {
StatusDialogHandle::Pin(dh) => {
let _ = dh.update(cx, |d, cx| {
d.set_success(
"USB applications updated successfully. Please re-plug the device.".to_string(),
cx,
);
});
}
StatusDialogHandle::Status(dh) => {
let _ = dh.update(cx, |d, cx| {
d.set_success(
"USB applications updated successfully. Please re-plug the device.".to_string(),
cx,
);
});
}
}
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);
});
match &dialog_handle {
StatusDialogHandle::Pin(dh) => {
let _ = dh.update(cx, |d, cx| {
d.set_error(format!("Failed to apply USB applications: {}", e), cx);
});
}
StatusDialogHandle::Status(dh) => {
let _ = dh.update(cx, |d, cx| {
d.set_error(format!("Failed to apply USB applications: {}", e), cx);
});
}
}
}
}
cx.notify();
+4 -2
View File
@@ -1,5 +1,5 @@
use crate::ui::components::{card::Card, page_view::PageView, tag::Tag};
use crate::ui::models::device::{DeviceMethod, FidoDeviceInfo, FullDeviceStatus};
use crate::ui::models::device::{DeviceMethod, FidoDeviceInfo, FirmwareType, FullDeviceStatus};
use crate::ui::screens::home::view_model::HomeViewModel;
use gpui::prelude::FluentBuilder;
use gpui::*;
@@ -245,10 +245,12 @@ impl HomeViewModel {
fn render_led_config(status: &FullDeviceStatus, theme: &Theme) -> impl IntoElement {
let config = &status.config;
let has_fido_config =
status.firmware_type == FirmwareType::RSKey || status.method != DeviceMethod::Fido;
Card::new()
.title("LED Configuration")
.icon(Icon::default().path("icons/microchip.svg"))
.child(if status.method == DeviceMethod::Fido {
.child(if !has_fido_config {
v_flex()
.items_center()
.justify_center()