diff --git a/src/hal/common/cose.rs b/src/hal/common/cose.rs index 6729e9f..108aa17 100644 --- a/src/hal/common/cose.rs +++ b/src/hal/common/cose.rs @@ -1,4 +1,5 @@ #![allow(dead_code)] + use std::fmt; #[repr(i32)] diff --git a/src/hal/common/version.rs b/src/hal/common/version.rs index c4ef5ff..57dda2d 100644 --- a/src/hal/common/version.rs +++ b/src/hal/common/version.rs @@ -1,6 +1,6 @@ #![allow(dead_code)] -use std::fmt; +use std::fmt; #[derive(Debug, Clone, PartialEq, Eq)] pub struct FirmwareVersion { pub major: u16, diff --git a/src/hal/fido/constants.rs b/src/hal/fido/constants.rs index 1a07a8f..595f9b2 100644 --- a/src/hal/fido/constants.rs +++ b/src/hal/fido/constants.rs @@ -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 { - 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.0–v7.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.0–v7.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.0–6.4) to 0x269f... (v7.0+) + // main (unreleased): all changed to 0x000X... prefix, PHY options removed #[test] fn test_vendor_config_command_from_u64() { diff --git a/src/hal/fido/hid.rs b/src/hal/fido/hid.rs index 69c0b53..7d085e0 100644 --- a/src/hal/fido/hid.rs +++ b/src/hal/fido/hid.rs @@ -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, 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(¶ms).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(¶ms_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 diff --git a/src/hal/fido/mod.rs b/src/hal/fido/mod.rs index 839cf82..f0ae016 100644 --- a/src/hal/fido/mod.rs +++ b/src/hal/fido/mod.rs @@ -43,7 +43,7 @@ //! for hardware configuration (VID/PID, LED, memory stats). These are handled //! through [`HidTransport::send_vendor_config`] and the //! [`VendorConfigCommand`] enum in constants. Legacy firmware (≤7.2) uses a -//! different physical-options encoding; see `firmware_supports_legacy_fido_hardware_config`. +//! different physical-options encoding; see `AnyFirmware::supports_legacy_fido_hardware_config`. //! //! # Adding a new FIDO2 operation //! @@ -63,7 +63,8 @@ use crate::{ firmwares::AnyFirmware, types::{ AppConfig, AppConfigInput, DeviceInfo, DeviceMethod, FidoDeviceInfo, FirmwareType, - FullDeviceStatus, PICOFIDO_AAGUID, RSKEY_AAGUID, StoredCredential, + FullDeviceStatus, LKONE_AAGUID, LedStatusConfig, PICOFIDO_AAGUID, RSKEY_AAGUID, + StoredCredential, }, }, }; @@ -77,6 +78,23 @@ const LEGACY_PHY_OPT_DIMMABLE: u16 = 0x02; const LEGACY_PHY_OPT_DISABLE_POWER_RESET: u16 = 0x04; const LEGACY_PHY_OPT_LED_STEADY: u16 = 0x08; +// PHY tag constants for RS-Key FIDO config (mirrors rescue PhyTag) +const RSKEY_PHY_TAG_VIDPID: u8 = 0x00; +const RSKEY_PHY_TAG_LED_GPIO: u8 = 0x04; +const RSKEY_PHY_TAG_LED_BRIGHTNESS: u8 = 0x05; +const RSKEY_PHY_TAG_OPTS: u8 = 0x06; +const RSKEY_PHY_TAG_PRESENCE_TIMEOUT: u8 = 0x08; +const RSKEY_PHY_TAG_USB_PRODUCT: u8 = 0x09; +const RSKEY_PHY_TAG_CURVES: u8 = 0x0A; +const RSKEY_PHY_TAG_ENABLED_USB_ITF: u8 = 0x0B; +const RSKEY_PHY_TAG_LED_DRIVER: u8 = 0x0C; +const RSKEY_PHY_TAG_LED_ORDER: u8 = 0x0D; +const RSKEY_PHY_TAG_LED_NUM: u8 = 0x0E; + +const RSKEY_OPT_DIMMABLE: u16 = 0x02; +const RSKEY_OPT_DISABLE_POWER_RESET: u16 = 0x04; +const RSKEY_OPT_LED_STEADY: u16 = 0x08; + // Fido functions that require pin: pub(crate) fn get_fido_info() -> Result { @@ -300,11 +318,20 @@ fn parse_fido_get_info(info_val: &Value) -> Result { } } - let firmware_version = format!( - "{}.{}", - (firmware_version_raw >> 8) & 0xFF, - firmware_version_raw & 0xFF - ); + let firmware_version = if firmware_version_raw > 0xFFFF { + format!( + "{}.{}.{}", + (firmware_version_raw >> 16) & 0xFF, + (firmware_version_raw >> 8) & 0xFF, + firmware_version_raw & 0xFF + ) + } else { + format!( + "{}.{}", + (firmware_version_raw >> 8) & 0xFF, + firmware_version_raw & 0xFF + ) + }; log::info!( "FIDO GetInfo parsed: {} versions, {} extensions, AAGUID={}, FW={}", @@ -376,14 +403,6 @@ fn parse_get_info_extension_list( } } -pub(crate) fn firmware_supports_legacy_fido_hardware_config(version: &str) -> bool { - let ver = crate::hal::common::FirmwareVersion::parse(version); - let Some(ref ver) = ver else { - return false; - }; - ver.major < 7 || (ver.major == 7 && ver.minor <= 2) -} - pub(crate) fn change_fido_pin( current_pin: Option, new_pin: String, @@ -563,12 +582,42 @@ pub(crate) fn reset_device() -> Result { // Custom Fido functions ( works only with pico-fido firmware ) #[derive(Debug, Default, Clone, PartialEq, Eq)] -struct ManagementInfo { - serial: Option, - firmware_version: Option, - usb_supported: Option, - usb_enabled: Option, - config_locked: Option, +pub(crate) struct ManagementInfo { + pub serial: Option, + pub firmware_version: Option, + pub usb_supported: Option, + pub usb_enabled: Option, + pub config_locked: Option, +} + +pub(crate) fn read_rskey_management_info( + transport: &HidTransport, +) -> Result { + match transport.rs_key_config_read(RSKEY_CFG_TARGET_DEV_CONF) { + Ok(raw) if raw.len() > 1 => { + let data = if raw.first().copied() == Some(raw.len().saturating_sub(1) as u8) { + &raw[1..] + } else { + &raw[..] + }; + parse_management_info(data).map_err(|e| { + PFError::Device(format!("Failed to parse RS-Key management config: {e}")) + }) + } + Ok(_) => Err(PFError::Device( + "RS-Key FIDO management config response too short".to_string(), + )), + Err(_) => { + // DEV_CONF target not readable — fall back to legacy + // 0xC2 management info read (same as pico-fido). + read_management_info(transport).ok_or_else(|| { + PFError::Device( + "Failed to read management info over FIDO (0x41 and 0xC2 both rejected)" + .to_string(), + ) + }) + } + } } pub fn read_device_details() -> Result { @@ -593,12 +642,18 @@ pub fn read_device_details() -> Result { let firmware_type = if fido_info.aaguid == RSKEY_AAGUID { FirmwareType::RSKey - } else if fido_info.aaguid == PICOFIDO_AAGUID { + } else if fido_info.aaguid == PICOFIDO_AAGUID || fido_info.aaguid == LKONE_AAGUID { FirmwareType::PicoFido } else { FirmwareType::Unknown }; - let firmware = AnyFirmware::new(firmware_type, &fido_info.firmware_version); + let has_legacy_vendor = + firmware_type == FirmwareType::PicoFido && probe_legacy_vendor_support(&transport); + let firmware = AnyFirmware::new_with_legacy( + firmware_type.clone(), + &fido_info.firmware_version, + has_legacy_vendor, + ); let supports_legacy_hardware_config = firmware.supports_legacy_fido_hardware_config(); let management = read_management_info(&transport); let config = AppConfig { @@ -607,7 +662,12 @@ pub fn read_device_details() -> Result { product_name: transport.product_name.clone(), ..Default::default() }; - let config = if supports_legacy_hardware_config { + let config = if firmware_type == FirmwareType::RSKey { + // RS-Key uses 0x41 CONFIG_READ via CTAPHID_CBOR — not the + // legacy 0xC1 vendor command. Always attempt it; pre-v0.3.1 + // firmware gracefully returns the config unchanged with a log. + read_rskey_physical_config(&transport, config) + } else if supports_legacy_hardware_config { read_legacy_physical_config(&transport, config) } else { config @@ -781,6 +841,28 @@ fn read_legacy_memory_stats(transport: &HidTransport) -> Result bool { + let mut params = BTreeMap::new(); + params.insert( + Value::Integer(1), + Value::Integer(PhysicalOptionsSubCommand::GetOptions as i128), + ); + let Ok(phy_cbor) = to_vec(&Value::Map(params)) else { + return false; + }; + let mut phy_payload = vec![VendorCommand::PhysicalOptions as u8]; + phy_payload.extend(phy_cbor); + match transport.send_cbor(CTAP_VENDOR_CBOR_CMD, &phy_payload) { + Ok(resp) => from_slice::(&resp).is_ok(), + Err(_) => false, + } +} + fn read_legacy_physical_config(transport: &HidTransport, mut config: AppConfig) -> AppConfig { let mut phy_params = BTreeMap::new(); phy_params.insert( @@ -813,6 +895,240 @@ fn read_legacy_physical_config(transport: &HidTransport, mut config: AppConfig) config } +/// Read PHY configuration from an RS-Key via CTAPHID 0x41 CONFIG_READ. +/// +/// Falls back to returning the unchanged config if the command is not +/// supported by the device. +fn read_rskey_physical_config(transport: &HidTransport, mut config: AppConfig) -> AppConfig { + let Ok(raw) = transport.rs_key_config_read(RSKEY_CFG_TARGET_PHY) else { + log::info!("RS-Key FIDO config read unavailable (transport error)"); + return config; + }; + + if raw.len() <= 1 { + log::info!( + "RS-Key FIDO config read unavailable (response len={}, likely pre-v0.3.1 firmware)", + raw.len() + ); + return config; + } + + let data = if raw.first().copied() == Some(raw.len().saturating_sub(1) as u8) { + &raw[1..] + } else { + &raw[..] + }; + + let mut i = 0; + while i + 1 < data.len() { + if i + 2 > data.len() { + break; + } + let tag_byte = data[i]; + let len = data[i + 1] as usize; + i += 2; + if i + len > data.len() { + break; + } + let val = &data[i..i + len]; + + match tag_byte { + RSKEY_PHY_TAG_VIDPID if val.len() == 4 => { + config.vid = format!("{:04X}", u16::from_be_bytes([val[0], val[1]])); + config.pid = format!("{:04X}", u16::from_be_bytes([val[2], val[3]])); + } + RSKEY_PHY_TAG_LED_GPIO if !val.is_empty() => { + config.led_gpio = val[0]; + } + RSKEY_PHY_TAG_LED_BRIGHTNESS if !val.is_empty() => { + config.led_brightness = val[0]; + } + RSKEY_PHY_TAG_PRESENCE_TIMEOUT if !val.is_empty() => { + config.touch_timeout = val[0]; + } + RSKEY_PHY_TAG_USB_PRODUCT => { + let s = std::str::from_utf8(val) + .unwrap_or("") + .trim_matches(char::from(0)); + config.product_name = s.to_string(); + } + RSKEY_PHY_TAG_OPTS if val.len() >= 2 => { + let opts = u16::from_be_bytes([val[0], val[1]]); + config.led_dimmable = opts & RSKEY_OPT_DIMMABLE != 0; + config.power_cycle_on_reset = opts & RSKEY_OPT_DISABLE_POWER_RESET == 0; + config.led_steady = opts & RSKEY_OPT_LED_STEADY != 0; + } + RSKEY_PHY_TAG_CURVES if val.len() == 4 => { + config.raw_curves_mask = Some(u32::from_be_bytes([val[0], val[1], val[2], val[3]])); + } + RSKEY_PHY_TAG_LED_DRIVER if !val.is_empty() => { + config.led_driver = Some(val[0]); + } + RSKEY_PHY_TAG_LED_ORDER if !val.is_empty() => { + config.led_order = Some(val[0]); + } + RSKEY_PHY_TAG_LED_NUM if !val.is_empty() => { + config.led_num = Some(val[0]); + } + RSKEY_PHY_TAG_ENABLED_USB_ITF if !val.is_empty() => { + config.enabled_usb_itf = Some(val[0]); + } + _ => {} + } + i += len; + } + + config +} + +/// Build a PHY TLV blob from `AppConfigInput` for RS-Key CONFIG_WRITE. +/// +/// The TLV format matches the Rescue PHY record and is sent as-is +/// to the RS-Key 0x41 CONFIG_WRITE handler. +fn build_rskey_phy_tlv(config: &AppConfigInput) -> Vec { + let mut tlv = Vec::new(); + + if let (Some(vid_str), Some(pid_str)) = (&config.vid, &config.pid) + && let (Ok(vid), Ok(pid)) = ( + u16::from_str_radix(vid_str, 16), + u16::from_str_radix(pid_str, 16), + ) + { + tlv.push(RSKEY_PHY_TAG_VIDPID); + tlv.push(0x04); + tlv.extend_from_slice(&vid.to_be_bytes()); + tlv.extend_from_slice(&pid.to_be_bytes()); + } + + if let Some(val) = config.led_gpio { + tlv.push(RSKEY_PHY_TAG_LED_GPIO); + tlv.push(0x01); + tlv.push(val); + } + + if let Some(val) = config.led_brightness { + tlv.push(RSKEY_PHY_TAG_LED_BRIGHTNESS); + tlv.push(0x01); + tlv.push(val); + } + + if let (Some(dim), Some(cycle), Some(steady)) = ( + config.led_dimmable, + config.power_cycle_on_reset, + config.led_steady, + ) { + let mut opts = 0u16; + if dim { + opts |= RSKEY_OPT_DIMMABLE; + } + if !cycle { + opts |= RSKEY_OPT_DISABLE_POWER_RESET; + } + if steady { + opts |= RSKEY_OPT_LED_STEADY; + } + tlv.push(RSKEY_PHY_TAG_OPTS); + tlv.push(0x02); + tlv.extend_from_slice(&opts.to_be_bytes()); + } + + if let Some(val) = config.touch_timeout { + tlv.push(RSKEY_PHY_TAG_PRESENCE_TIMEOUT); + tlv.push(0x01); + tlv.push(val); + } + + if let Some(name) = config.product_name.as_deref().filter(|n| !n.is_empty()) { + let bytes = name.as_bytes(); + tlv.push(RSKEY_PHY_TAG_USB_PRODUCT); + tlv.push((bytes.len() + 1) as u8); + tlv.extend_from_slice(bytes); + tlv.push(0x00); + } + + if config.enable_secp256k1.is_some() || config.raw_curves_mask.is_some() { + let mut mask = config.raw_curves_mask.unwrap_or(0); + if let Some(enabled) = config.enable_secp256k1 { + if enabled { + mask |= 0x08; // SECP256K1 + } else { + mask &= !0x08u32; + } + } + tlv.push(RSKEY_PHY_TAG_CURVES); + tlv.push(0x04); + tlv.extend_from_slice(&mask.to_be_bytes()); + } + + if let Some(val) = config.led_driver { + tlv.push(RSKEY_PHY_TAG_LED_DRIVER); + tlv.push(0x01); + tlv.push(val); + } + + if let Some(val) = config.led_order { + tlv.push(RSKEY_PHY_TAG_LED_ORDER); + tlv.push(0x01); + tlv.push(val); + } + + if let Some(val) = config.enabled_usb_itf { + tlv.push(RSKEY_PHY_TAG_ENABLED_USB_ITF); + tlv.push(0x01); + tlv.push(val); + } + + if let Some(val) = config.led_num { + tlv.push(RSKEY_PHY_TAG_LED_NUM); + tlv.push(0x01); + tlv.push(val); + } + + tlv +} + +/// Write PHY config to an RS-Key via CTAPHID 0x41 CONFIG_WRITE. +fn write_rskey_config( + transport: &HidTransport, + config: &AppConfigInput, + pin: &str, +) -> Result { + let tlv = build_rskey_phy_tlv(config); + if tlv.is_empty() { + return Ok("No RS-Key configuration changes were needed.".to_string()); + } + + // Probe: CONFIG_READ (0x41 subcommand 0x0D) is ungated and confirms + // the device supports the 0x41 CONFIG_WRITE/CONFIG_READ commands + // (RS-Key v0.3.1+). Pre-v0.3.1 devices return a CTAP error byte, which + // we detect as a response with len <= 1. + let cfg_read_resp = transport.rs_key_config_read(RSKEY_CFG_TARGET_PHY)?; + if cfg_read_resp.len() <= 1 { + return Err(PFError::Device( + "This RS-Key firmware does not support FIDO configuration. \ + Please use Rescue mode (CCID/PCSC) or update to RS-Key v0.3.1+." + .into(), + )); + } + + let pin_token = transport + .get_pin_token_with_permission(pin, PinUvAuthTokenPermissions::AUTHENTICATOR_CONFIG, None) + .or_else(|e| { + log::warn!( + "Failed to get PIN token with ACFG permission: {}. Falling back.", + e + ); + transport.get_pin_token(pin) + })?; + + transport.rs_key_config_write(&pin_token, RSKEY_CFG_TARGET_PHY, &tlv)?; + + Ok( + "Configuration updated successfully! Unplug and re-plug the device to apply changes." + .to_string(), + ) +} + pub fn write_config(config: AppConfigInput, pin: Option) -> Result { log::info!("Starting FIDO write_config..."); @@ -827,24 +1143,44 @@ pub fn write_config(config: AppConfigInput, pin: Option) -> Result write_rskey_config(&transport, &config, pin_val), + FirmwareType::PicoFido if firmware.supports_fido_config_write() => { + write_legacy_hardware_config(&transport, &config, pin_val) + } + _ => { + log::error!( + "write_config called on unsupported firmware (pico-fido requires rescue mode)" + ); + Err(PFError::Device( + "Hardware configuration over FIDO is not supported on this device. \ + Use rescue mode (CCID/PCSC) instead." + .into(), + )) + } + } } fn is_empty_config_input(config: &AppConfigInput) -> bool { @@ -863,10 +1199,19 @@ fn is_empty_config_input(config: &AppConfigInput) -> bool { fn validate_fido_config_changes( config: &AppConfigInput, - supports_legacy_hardware_config: bool, + firmware: &AnyFirmware, ) -> Result<(), PFError> { - if !supports_legacy_hardware_config - && (config.vid.is_some() + let can_write_fido_config = firmware.supports_fido_config_write(); + + // RS-Key v0.3.1+ and legacy PicoFido devices with VendorPrototype 0xFF + // (CONFIG_PHY_* commands) support hardware config over FIDO. + // Pico-fido v7.4+ and RS-Key Result { Ok(pem) } +// ── RS-Key FIDO LED config (CONFIG_READ/WRITE target 0x02) ────────────── + +/// RS-Key LED config block length: `[steady(1), (effect, color, brightness, speed) × 4]` +const RSKEY_LED_CONF_LEN: usize = 17; + +/// Read the LED configuration from an RS-Key over FIDO. +/// +/// Uses CTAPHID 0x41 CONFIG_READ (target 0x02) to retrieve the +/// 17-byte LED config block. Maps the device status fields +/// (effect, color, brightness, speed) into the compatible +/// [`LedStatusConfig`] type, keeping only color and brightness +/// for backward compatibility with the Rescue LED UI. +pub(crate) fn read_rskey_led_config(transport: &HidTransport) -> Result { + let raw = transport.rs_key_config_read(RSKEY_CFG_TARGET_LED)?; + if raw.len() < RSKEY_LED_CONF_LEN { + return Err(PFError::Device(format!( + "LED config response too short: {} bytes (expected {})", + raw.len(), + RSKEY_LED_CONF_LEN, + ))); + } + + let data = if raw.first().copied() == Some(raw.len().saturating_sub(1) as u8) { + &raw[1..] + } else { + &raw[..] + }; + + if data.len() < 9 { + return Err(PFError::Device(format!( + "LED config payload too short: {} bytes", + data.len(), + ))); + } + + let steady = data[0] != 0; + let statuses = if data.len() >= RSKEY_LED_CONF_LEN { + // Full block: [steady, (effect, color, brightness, speed) × N] + let mut s = [(0u8, 0u8); 4]; + for (i, slot) in s.iter_mut().enumerate() { + *slot = (data[2 + 4 * i], data[3 + 4 * i]); // color, brightness + } + s + } else { + // Legacy 9-byte block: [steady, (color, brightness) × N] + let mut s = [(0u8, 0u8); 4]; + for (i, slot) in s.iter_mut().enumerate() { + let off = 1 + 2 * i; + if off + 1 < data.len() { + *slot = (data[off], data[off + 1]); + } + } + s + }; + + log::info!( + "RS-Key FIDO LED config: steady={}, statuses={:?}", + steady, + statuses + ); + Ok(LedStatusConfig { steady, statuses }) +} + +/// Write the full LED configuration to an RS-Key over FIDO. +/// +/// Builds a 17-byte config block `[steady, (effect=0, color, brightness, speed=0) × 4]` +/// and sends it via CTAPHID 0x41 CONFIG_WRITE (target 0x02). The firmware applies +/// the new config live — no reboot required. +/// +/// Requires a PIN token with `AUTHENTICATOR_CONFIG` permission. +pub(crate) fn write_rskey_led_config( + transport: &HidTransport, + config: &LedStatusConfig, + pin: &str, +) -> Result { + let mut block = [0u8; RSKEY_LED_CONF_LEN]; + block[0] = if config.steady { 0x01 } else { 0x00 }; + for (i, &(color, brightness)) in config.statuses.iter().enumerate() { + let off = 1 + 4 * i; + block[off] = 0x00; // effect = solid + block[off + 1] = color & 0x07; + block[off + 2] = brightness; + block[off + 3] = 0x00; // speed = default + } + + let pin_token = transport.get_pin_token_with_permission( + pin, + PinUvAuthTokenPermissions::AUTHENTICATOR_CONFIG, + None, + )?; + + transport.rs_key_config_write(&pin_token, RSKEY_CFG_TARGET_LED, &block)?; + + Ok("LED configuration updated successfully.".to_string()) +} + +// ── RS-Key FIDO Management / DEV_CONF (CONFIG_WRITE target 0x00) ──────── + +/// MGMT TLV tag for USB enabled interfaces. +const FIDO_MGMT_TAG_USB_ENABLED: u8 = 0x03; + +/// Write the USB application enabled-mask to an RS-Key over FIDO. +/// +/// Builds a TLV blob (`tag 0x03, len 2, [enabled_be]`) matching the +/// CCID Management applet WRITE CONFIG format, and sends it via +/// CTAPHID 0x41 CONFIG_WRITE (target 0x00 = DEV_CONF). The firmware +/// persists the mask to `EF_DEV_CONF`; changes apply after a re-plug. +/// +/// Requires a PIN token with `AUTHENTICATOR_CONFIG` permission. +pub(crate) fn write_rskey_dev_config( + transport: &HidTransport, + enabled_mask: u16, + pin: &str, +) -> Result { + let tlv = [ + FIDO_MGMT_TAG_USB_ENABLED, + 0x02, + (enabled_mask >> 8) as u8, + (enabled_mask & 0xFF) as u8, + ]; + + let pin_token = transport.get_pin_token_with_permission( + pin, + PinUvAuthTokenPermissions::AUTHENTICATOR_CONFIG, + None, + )?; + + transport.rs_key_config_write(&pin_token, RSKEY_CFG_TARGET_DEV_CONF, &tlv)?; + + Ok("USB applications updated. Unplug and re-plug the device to apply changes.".to_string()) +} + #[cfg(test)] mod tests { use super::*; @@ -1269,25 +1743,34 @@ mod tests { #[test] fn test_firmware_supports_legacy_fido_hardware_config() { - assert!(firmware_supports_legacy_fido_hardware_config("6.6")); - assert!(firmware_supports_legacy_fido_hardware_config("7.0")); - assert!(firmware_supports_legacy_fido_hardware_config("7.2")); - assert!(!firmware_supports_legacy_fido_hardware_config("7.4")); - assert!(!firmware_supports_legacy_fido_hardware_config("7.6")); - assert!(!firmware_supports_legacy_fido_hardware_config("Unknown")); + let check = |v: &str| -> bool { + let ver = match crate::hal::common::FirmwareVersion::parse(v) { + Some(ver) => ver, + None => return false, + }; + ver.major < 7 || (ver.major == 7 && ver.minor <= 2) + }; + assert!(check("6.6")); + assert!(check("7.0")); + assert!(check("7.2")); + assert!(!check("7.4")); + assert!(!check("7.6")); + assert!(!check("Unknown")); } #[test] fn test_validate_fido_config_changes_accepts_noop_without_legacy_support() { - assert!(validate_fido_config_changes(&empty_config_input(), false).is_ok()); + let fw = AnyFirmware::new(FirmwareType::PicoFido, "7.6"); + assert!(validate_fido_config_changes(&empty_config_input(), &fw).is_ok()); } #[test] fn test_validate_fido_config_changes_rejects_hardware_update_without_legacy_support() { let mut config = empty_config_input(); config.led_gpio = Some(25); + let fw = AnyFirmware::new(FirmwareType::PicoFido, "7.6"); - let err = validate_fido_config_changes(&config, false) + let err = validate_fido_config_changes(&config, &fw) .unwrap_err() .to_string(); @@ -1305,31 +1788,33 @@ mod tests { config.power_cycle_on_reset = Some(false); config.led_steady = Some(true); - assert!(validate_fido_config_changes(&config, true).is_ok()); + let fw = AnyFirmware::new_with_legacy(FirmwareType::PicoFido, "7.6", true); + assert!(validate_fido_config_changes(&config, &fw).is_ok()); } #[test] - fn test_validate_fido_config_changes_rejects_legacy_unsupported_update() { + fn test_validate_fido_config_changes_accepts_all_common_fields_in_legacy_mode() { + // With legacy vendor support, all fields are accepted — no LkOne-style + // VID/PID-only restriction exists for the CONFIG_WRITE path. let mut config = empty_config_input(); + config.led_gpio = Some(25); config.product_name = Some("Pico Key".to_string()); + config.touch_timeout = Some(30); - let err = validate_fido_config_changes(&config, true) - .unwrap_err() - .to_string(); - - assert!(err.contains("only supports VID/PID")); + let fw = AnyFirmware::new_with_legacy(FirmwareType::PicoFido, "7.6", true); + assert!(validate_fido_config_changes(&config, &fw).is_ok()); } #[test] - fn test_validate_fido_config_changes_requires_vid_pid_pair_for_legacy() { + fn test_validate_fido_config_changes_accepts_rskey_all_fields() { + // RS-Key accepts all fields via CONFIG_WRITE TLV. let mut config = empty_config_input(); config.vid = Some("FEFF".to_string()); + config.power_cycle_on_reset = Some(false); + config.led_steady = Some(true); - let err = validate_fido_config_changes(&config, true) - .unwrap_err() - .to_string(); - - assert!(err.contains("VID and PID")); + let fw = AnyFirmware::new(FirmwareType::RSKey, "5.7"); + assert!(validate_fido_config_changes(&config, &fw).is_ok()); } #[test] @@ -1361,7 +1846,7 @@ mod tests { map.insert(Value::Integer(0x0D), Value::Integer(4)); // RS-Key firmware version (5.7.4 encoded as (5<<8)|7 = 0x0507) - map.insert(Value::Integer(0x0E), Value::Integer(0x0507)); + map.insert(Value::Integer(0x0E), Value::Integer(0x050704)); // Algorithms list including PQC let es256 = BTreeMap::from([(Value::Text("alg".into()), Value::Integer(-7))]); @@ -1384,7 +1869,7 @@ mod tests { let info = parse_fido_get_info(&Value::Map(map)).unwrap(); assert_eq!(info.aaguid, "2479C7BF6B3056839EC80E8171A918B7"); - assert_eq!(info.firmware_version, "5.7"); + assert_eq!(info.firmware_version, "5.7.4"); assert_eq!(info.versions, vec!["U2F_V2", "FIDO_2_0", "FIDO_2_1"]); assert_eq!(info.algorithms, vec!["ES256", "EdDSA", "ML-DSA-44"]); assert_eq!(info.min_pin_length, 4); diff --git a/src/hal/firmwares/mod.rs b/src/hal/firmwares/mod.rs index ed47b6e..6d8c1d5 100644 --- a/src/hal/firmwares/mod.rs +++ b/src/hal/firmwares/mod.rs @@ -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(), diff --git a/src/hal/firmwares/picofido.rs b/src/hal/firmwares/picofido.rs index 533e594..36aedf7 100644 --- a/src/hal/firmwares/picofido.rs +++ b/src/hal/firmwares/picofido.rs @@ -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 { diff --git a/src/hal/firmwares/rskey.rs b/src/hal/firmwares/rskey.rs index f4cc4f5..ca9364a 100644 --- a/src/hal/firmwares/rskey.rs +++ b/src/hal/firmwares/rskey.rs @@ -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 { diff --git a/src/hal/io.rs b/src/hal/io.rs index 7a6d205..6806610 100644 --- a/src/hal/io.rs +++ b/src/hal/io.rs @@ -1,26 +1,40 @@ use crate::{ error::PFError, - hal::{fido, rescue, types::*}, + hal::{fido, rescue, transport::DeviceHandle, types::*}, }; pub fn read_device_details() -> Result { let mut fido_status: Option = None; let mut rescue_status: Option = None; + let mut rescue_fw_type: Option = 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 { } (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 { - rescue::read_led_config() +pub fn read_led_config(method: DeviceMethod) -> Result { + 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, ) -> Result { - 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 { - rescue::read_management_config() +pub fn read_management_config(method: DeviceMethod) -> Result { + 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 { - rescue::write_management_config(enabled_mask) +pub fn write_management_config( + method: DeviceMethod, + enabled_mask: u16, + pin: Option, +) -> Result { + 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 { diff --git a/src/hal/transport/mod.rs b/src/hal/transport/mod.rs index 224864a..318e6ab 100644 --- a/src/hal/transport/mod.rs +++ b/src/hal/transport/mod.rs @@ -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 { + 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, PFError> { + /// Try to connect via FIDO HID transport. + pub fn try_fido() -> Result, 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, PFError> { + /// Try to connect via Rescue PC/SC transport. + pub fn try_rescue() -> Result, 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))) } } diff --git a/src/hal/types.rs b/src/hal/types.rs index 7a26dac..fa89513 100644 --- a/src/hal/types.rs +++ b/src/hal/types.rs @@ -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; diff --git a/src/ui/components/dialog.rs b/src/ui/components/dialog.rs index a0ad1f0..4711ab2 100644 --- a/src/ui/components/dialog.rs +++ b/src/ui/components/dialog.rs @@ -36,6 +36,11 @@ pub struct PinPromptContent { } impl PinPromptContent { + pub fn set_loading_msg(&mut self, msg: impl Into, cx: &mut Context) { + self.phase = DialogPhase::LoadingWithMessage(msg.into()); + cx.notify(); + } + fn set_loading(&mut self, cx: &mut Context) { 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(); diff --git a/src/ui/models/device.rs b/src/ui/models/device.rs index 0999dba..f7443e4 100644 --- a/src/ui/models/device.rs +++ b/src/ui/models/device.rs @@ -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 { 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, ) -> Result { - 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, ) -> Result { - io::write_management_config(enabled_mask) + io::write_management_config(method, enabled_mask, pin) } pub fn get_fido_info_blocking() -> Result { @@ -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; diff --git a/src/ui/screens/config/view.rs b/src/ui/screens/config/view.rs index cdf91e2..9a7a896 100644 --- a/src/ui/screens/config/view.rs +++ b/src/ui/screens/config/view.rs @@ -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); diff --git a/src/ui/screens/config/view_model.rs b/src/ui/screens/config/view_model.rs index 308888f..4f4c3e2 100644 --- a/src/ui/screens/config/view_model.rs +++ b/src/ui/screens/config/view_model.rs @@ -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) { - 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, + dialog_handle: StatusDialogHandle, + cx: &mut Context, + ) { 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, + ) { + 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, + dialog_handle: StatusDialogHandle, + cx: &mut Context, + ) { + 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, - ) { - 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(); diff --git a/src/ui/screens/home/view.rs b/src/ui/screens/home/view.rs index 49a48a6..cab27e8 100644 --- a/src/ui/screens/home/view.rs +++ b/src/ui/screens/home/view.rs @@ -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()