Merge pull request #97 from librekeys/RS-Key-Support

Add RS-Keys Support and improve code documentation
This commit is contained in:
Suyog Tandel
2026-06-23 23:26:52 +05:30
committed by GitHub
24 changed files with 3230 additions and 391 deletions
+40
View File
@@ -0,0 +1,40 @@
name: PicoForge CI
on:
push:
branches:
- '**'
pull_request:
branches:
- '**'
env:
CARGO_TERM_COLOR: always
jobs:
checks:
name: Code Quality Checks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Install Nix
uses: DeterminateSystems/nix-installer-action@v22
- name: Setup Nix cache
uses: DeterminateSystems/magic-nix-cache-action@v14
- name: Check formatting
run: nix develop --command cargo fmt --all -- --check
- name: Run cargo check
run: nix develop --command cargo check --all-targets
- name: Run clippy
run: nix develop --command cargo clippy --all-targets -- -D warnings
- name: Build project
run: nix develop --command cargo build --verbose
- name: Run tests
run: nix develop --command cargo test --verbose
+7
View File
@@ -4,9 +4,16 @@ on:
branches:
- release
workflow_dispatch:
workflow_run:
workflows: ["PicoForge CI"]
branches:
- release
types:
- completed
jobs:
build-and-release:
if: github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success'
permissions:
contents: write
issues: write
+2 -2
View File
@@ -19,10 +19,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Push to Wiki
uses: Andrew-Chen-Wang/github-wiki-action@v4
uses: Andrew-Chen-Wang/github-wiki-action@v5
with:
path: docs
token: ${{ secrets.WIKI_SYNC_PAT_TOKEN }}
+3 -1
View File
@@ -29,4 +29,6 @@ src-svelte
gpui-component
packaging
.cargo-packager
llm-texts
llm-texts
opencode.jsonc
AGENTS.md
Generated
+93 -20
View File
@@ -15,11 +15,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
dependencies = [
"cfg-if",
"cipher",
"cipher 0.4.4",
"cpufeatures 0.2.17",
"zeroize",
]
[[package]]
name = "aes"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1fc76eaeac4c9164506c466d4ffdd8ec9d0c5bf57ee97177c4d8eceb3a0e138"
dependencies = [
"cipher 0.5.2",
"cpubits",
"cpufeatures 0.3.0",
]
[[package]]
name = "ahash"
version = "0.8.12"
@@ -644,6 +655,15 @@ dependencies = [
"generic-array",
]
[[package]]
name = "block-padding"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "710f1dd022ef4e93f8a438b4ba958de7f64308434fa6a87104481645cc30068b"
dependencies = [
"hybrid-array",
]
[[package]]
name = "block2"
version = "0.6.2"
@@ -758,7 +778,16 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6"
dependencies = [
"cipher",
"cipher 0.4.4",
]
[[package]]
name = "cbc"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce2dc9ee5f88d11e0beb842c88b33c8a5cf0d1329c4b19494af42b07dbfe8896"
dependencies = [
"cipher 0.5.2",
]
[[package]]
@@ -851,11 +880,21 @@ version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
dependencies = [
"crypto-common",
"inout",
"crypto-common 0.1.7",
"inout 0.1.4",
"zeroize",
]
[[package]]
name = "cipher"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c"
dependencies = [
"crypto-common 0.2.2",
"inout 0.2.2",
]
[[package]]
name = "clang-sys"
version = "1.8.1"
@@ -1165,6 +1204,12 @@ dependencies = [
"unicode-segmentation",
]
[[package]]
name = "cpubits"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae"
[[package]]
name = "cpufeatures"
version = "0.2.17"
@@ -1243,6 +1288,15 @@ dependencies = [
"typenum",
]
[[package]]
name = "crypto-common"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
dependencies = [
"hybrid-array",
]
[[package]]
name = "ctor"
version = "0.4.3"
@@ -1319,7 +1373,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
"crypto-common 0.1.7",
"subtle",
]
@@ -2655,6 +2709,15 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424"
[[package]]
name = "hybrid-array"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da"
dependencies = [
"typenum",
]
[[package]]
name = "hyper"
version = "1.10.1"
@@ -2939,10 +3002,20 @@ version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
dependencies = [
"block-padding",
"block-padding 0.3.3",
"generic-array",
]
[[package]]
name = "inout"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7"
dependencies = [
"block-padding 0.4.2",
"hybrid-array",
]
[[package]]
name = "instant"
version = "0.1.13"
@@ -3414,9 +3487,9 @@ checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
[[package]]
name = "memmap2"
version = "0.9.10"
version = "0.9.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3"
checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0"
dependencies = [
"libc",
]
@@ -3915,14 +3988,14 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3299dd401feaf1d45afd8fd1c0586f10fcfb22f244bb9afa942cec73503b89d"
dependencies = [
"aes",
"aes 0.8.4",
"ashpd 0.12.3",
"async-fs",
"async-io",
"async-lock",
"blocking",
"cbc",
"cipher",
"cbc 0.1.2",
"cipher 0.4.4",
"digest",
"endi",
"futures-lite 2.6.1",
@@ -4133,14 +4206,14 @@ checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315"
[[package]]
name = "picoforge"
version = "0.5.0"
version = "0.6.0"
dependencies = [
"aes",
"aes 0.9.1",
"anyhow",
"base64",
"bitflags 2.13.0",
"byteorder",
"cbc",
"cbc 0.2.1",
"directories",
"gpui",
"gpui-component",
@@ -4415,9 +4488,9 @@ dependencies = [
[[package]]
name = "quinn"
version = "0.11.9"
version = "0.11.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8"
dependencies = [
"bytes",
"cfg_aliases",
@@ -4435,9 +4508,9 @@ dependencies = [
[[package]]
name = "quinn-proto"
version = "0.11.14"
version = "0.11.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"
checksum = "4fcb935c5bec503c2f0e306bdd3e58bb9029dcb14fa8d9ac76e3a5256ac0763e"
dependencies = [
"bytes",
"getrandom 0.3.4",
@@ -4948,9 +5021,9 @@ dependencies = [
[[package]]
name = "rustls"
version = "0.23.40"
version = "0.23.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b"
checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f"
dependencies = [
"once_cell",
"ring",
+11 -11
View File
@@ -15,19 +15,19 @@ log4rs = "1" # For logging to output (like s
directories = "6" # For Applcation config/data dir handling
# For device management backend:
pcsc = "2" # Standard Smart Card API (connect to the key)
hex = "0.4" # For parsing VID/PID strings
byteorder = "1.5" # Required for writing Big-Endian numbers (firmware requirement)
thiserror = "2" # Makes custom error handling much easier
anyhow = "1" # For easy error propagation
hidapi = "2.6" # For fido2 interface operations but non-standard commands
pcsc = "2" # Standard Smart Card API (connect to the key)
hex = "0.4" # For parsing VID/PID strings
byteorder = "1.5" # Required for writing Big-Endian numbers (firmware requirement)
thiserror = "2" # Makes custom error handling much easier
anyhow = "1" # For easy error propagation
hidapi = "2.6" # For fido2 interface operations but non-standard commands
serde_cbor_2 = "0.13"
rand = "0.10"
bitflags = "2.11"
base64 = "0.22" # For PEM encoding of DER certificates
ring = "0.17" # For signing fido2 messages with pin token
aes = "0.8"
cbc = "0.1"
bitflags = "2.13.0"
base64 = "0.22" # For PEM encoding of DER certificates
ring = "0.17" # For signing fido2 messages with pin token
aes = "0.9"
cbc = "0.2"
# For Application UI:
gpui = { version = "0.2.2", features = [] }
+3 -3
View File
@@ -23,9 +23,9 @@
> Check application [Installation Wiki](https://github.com/librekeys/picoforge/wiki/Installation) for installation guide of the PicoForge app on your system.
>
> **Supported Firmwares:**
> - **pico-fido**: v7.0, v7.2, v7.4, v7.6
> - **LibreKeys One**: v7.4.2
> - **RSKeys**: v0.2.8
> - **[pico-fido](https://github.com/polhenarejos/pico-fido)**: v7.0, v7.2, v7.4, v7.6
> - **[LibreKeys One](https://github.com/librekeys/pico-fido-firmwares/releases)**: v7.4.2
> - **[RSKeys](https://github.com/TheMaxMur/RS-Key)**: v0.2.8
>
> **Configuration Support:**
> - **pico-fido v7.0/v7.2** & **LibreKeys One v7.4.2**: Hardware configuration via FIDO mode is supported.
File diff suppressed because it is too large Load Diff
+346 -63
View File
File diff suppressed because it is too large Load Diff
+90 -2
View File
@@ -1,10 +1,66 @@
//! FIDO2 / CTAP2 protocol implementation for pico-fido and RS-Key firmware.
//!
//! ```text
//! fido/
//! ├── mod.rs — high-level FIDO2 operations (info, PIN, credentials, config)
//! ├── constants.rs — CTAP2 command codes, CBOR map keys, COSE algorithms, bitflags
//! └── hid.rs — USB HID transport (CTAPHID framing, channel init, CBOR exchange)
//! ```
//!
//! # Architecture
//!
//! Communication flows top-down:
//!
//! ```text
//! io::read_device_details()
//! │
//! ▼
//! fido::read_device_details() ← this file
//! │
//! ▼
//! HidTransport::open() ← hid.rs
//! │
//! ▼
//! USB HID (CTAPHID protocol)
//! ```
//!
//! [`constants`] is imported by both `mod.rs` and `hid.rs` and should be the
//! single source of truth for every CTAP2-defined byte value. If you need to
//! add a new command, sub-command, or CBOR key, put it there.
//!
//! [`hid`] owns the raw byte-level exchange: channel ID negotiation, packet
//! framing (init + continuation packets), PIN token acquisition, ECDH key
//! agreement, and CBOR serialization. It exposes [`HidTransport`] which the
//! rest of the module uses for all device I/O.
//!
//! [`mod.rs`] contains the public functions called from [`super::io`].
//! Each function opens an [`HidTransport`], performs the CTAP2 operation,
//! and parses the CBOR response into the structs defined in [`super::types`].
//!
//! # Vendor extensions
//!
//! Pico-fido firmware exposes vendor-specific CTAP commands (`0xC1`, `0xC2`)
//! for hardware configuration (VID/PID, LED, memory stats). These are handled
//! through [`HidTransport::send_vendor_config`] and the
//! [`VendorConfigCommand`] enum in constants. Legacy firmware (≤7.2) uses a
//! different physical-options encoding; see `firmware_supports_legacy_fido_hardware_config`.
//!
//! # Adding a new FIDO2 operation
//!
//! 1. Add any new command/sub-command enums to [`constants`].
//! 2. Implement the CBOR encoding and transport call in [`hid`] (if it
//! requires new framing or PIN token logic).
//! 3. Add the high-level function in this file, following the pattern:
//! open transport → build CBOR payload → send → parse response → return.
//! 4. Expose it through [`super::io`].
pub mod constants;
pub mod hid;
use crate::{
device::types::{
AppConfig, AppConfigInput, DeviceInfo, DeviceMethod, FidoDeviceInfo, FullDeviceStatus,
StoredCredential,
AppConfig, AppConfigInput, DeviceInfo, DeviceMethod, FidoDeviceInfo, FirmwareType,
FullDeviceStatus, PICOFIDO_AAGUID, RSKEY_AAGUID, StoredCredential,
},
error::PFError,
};
@@ -488,6 +544,26 @@ pub(crate) fn delete_credential(pin: String, credential_id_hex: String) -> Resul
Ok("Credential deleted successfully".into())
}
pub(crate) fn reset_device() -> Result<String, String> {
log::info!("Starting FIDO authenticatorReset...");
let transport =
HidTransport::open().map_err(|e| format!("Could not open HID transport: {}", e))?;
transport.reset().map_err(|e| {
let s = e.to_string();
if s.contains("0x30") {
return "Reset not allowed. The device must be unplugged and re-plugged within 10 seconds before sending the reset command.".to_string();
}
if s.contains("0x27") {
return "Reset declined. Touch was not confirmed on the device.".to_string();
}
format!("Reset failed: {}", s)
})?;
Ok("Device has been factory reset. All credentials and PIN have been erased.".to_string())
}
// Custom Fido functions ( works only with pico-fido firmware )
#[derive(Debug, Default, Clone, PartialEq, Eq)]
@@ -553,6 +629,14 @@ pub fn read_device_details() -> Result<FullDeviceStatus, PFError> {
.unwrap_or_else(|| "Unknown".to_string())
};
let firmware_type = if fido_info.aaguid == RSKEY_AAGUID {
FirmwareType::RSKey
} else if fido_info.aaguid == PICOFIDO_AAGUID {
FirmwareType::PicoFido
} else {
FirmwareType::Unknown
};
Ok(FullDeviceStatus {
info: DeviceInfo {
serial: management
@@ -566,6 +650,7 @@ pub fn read_device_details() -> Result<FullDeviceStatus, PFError> {
secure_boot: false,
secure_lock: false,
method: DeviceMethod::Fido,
firmware_type,
})
}
@@ -1041,6 +1126,9 @@ mod tests {
power_cycle_on_reset: None,
led_steady: None,
enable_secp256k1: None,
raw_curves_mask: None,
led_order: None,
enabled_usb_itf: None,
}
}
+62 -5
View File
@@ -1,8 +1,20 @@
//! Tauri Commands to interact with the pico-fido firmware via rescue and fido protocols.
//! Device I/O layer bridging rescue (pcsc) and FIDO2 protocols.
//!
//! High-level entry points for reading/writing device configuration,
//! managing credentials, and controlling LED/boot behavior.
//!
//! Functions are grouped by the protocol they use:
//! - Functions that use both rescue and FIDO (fallback/dispatch logic)
//! - Functions that communicate exclusively over the rescue (PC/SC) channel
//! - Functions that communicate exclusively over the FIDO2 channel
#![allow(unused)]
use crate::{device::fido, device::rescue, device::types::*, error::PFError};
// ── Shared: functions that use both rescue and FIDO ─────────────────────────
/// Read full device status. Tries rescue first, falls back to FIDO on failure.
pub fn read_device_details() -> Result<FullDeviceStatus, PFError> {
match rescue::read_device_details() {
Ok(status) => Ok(status),
@@ -13,6 +25,7 @@ pub fn read_device_details() -> Result<FullDeviceStatus, PFError> {
}
}
/// Write app config. Dispatches to rescue or FIDO based on `method`.
pub fn write_config(
config: AppConfigInput,
method: DeviceMethod,
@@ -25,14 +38,51 @@ pub fn write_config(
}
}
// ── Rescue protocol (PC/SC) ─────────────────────────────────────────────────
/// Lock or unlock secure boot via rescue.
pub fn enable_secure_boot(lock: bool) -> Result<String, PFError> {
rescue::enable_secure_boot(lock)
}
/// Reboot the device. Pass `true` to enter BOOTSEL mode.
pub fn reboot(to_bootsel: bool) -> Result<String, PFError> {
rescue::reboot_device(to_bootsel)
}
/// Read current LED status config via rescue.
pub fn read_led_config() -> Result<LedStatusConfig, PFError> {
rescue::read_led_config()
}
/// Write LED status (on/off, color, brightness, steady/blinking).
pub fn write_led_status(
status: u8,
color: u8,
brightness: u8,
steady: bool,
) -> Result<String, PFError> {
rescue::write_led_status(status, color, brightness, steady)
}
/// Read management app config via rescue.
pub fn read_management_config() -> Result<ManagementAppConfig, PFError> {
rescue::read_management_config()
}
/// Write management app enabled-mask via rescue.
pub fn write_management_config(enabled_mask: u16) -> Result<String, PFError> {
rescue::write_management_config(enabled_mask)
}
// ── FIDO2 protocol ──────────────────────────────────────────────────────────
/// Query basic FIDO device info (AAGUID, version, etc.).
pub(crate) fn get_fido_info() -> Result<FidoDeviceInfo, String> {
fido::get_fido_info()
}
/// Change the FIDO user PIN.
pub(crate) fn change_fido_pin(
current_pin: Option<String>,
new_pin: String,
@@ -40,6 +90,7 @@ pub(crate) fn change_fido_pin(
fido::change_fido_pin(current_pin, new_pin)
}
/// Set the minimum PIN length requirement.
pub(crate) fn set_min_pin_length(
current_pin: String,
min_pin_length: u8,
@@ -47,26 +98,32 @@ pub(crate) fn set_min_pin_length(
fido::set_min_pin_length(current_pin, min_pin_length)
}
pub fn reboot(to_bootsel: bool) -> Result<String, PFError> {
rescue::reboot_device(to_bootsel)
}
/// List stored credentials for the given PIN.
pub fn get_credentials(pin: String) -> Result<Vec<StoredCredential>, String> {
fido::get_credentials(pin)
}
/// Delete a single credential by its ID.
pub fn delete_credential(pin: String, credential_id: String) -> Result<String, String> {
fido::delete_credential(pin, credential_id)
}
/// Factory-reset the device, wiping all credentials and settings.
pub fn reset_device() -> Result<String, String> {
fido::reset_device()
}
/// Enable enterprise attestation for the device.
pub fn enable_enterprise_attestation(pin: String) -> Result<String, String> {
fido::enable_enterprise_attestation(pin)
}
/// Retrieve the enterprise attestation CSR.
pub fn get_enterprise_attestation_csr() -> Result<String, String> {
fido::get_enterprise_attestation_csr()
}
/// Upload a signed enterprise attestation certificate.
pub fn upload_enterprise_attestation_cert(
pin: String,
cert_path: String,
+41
View File
@@ -1,3 +1,44 @@
//! Device communication layer for pico-forge.
//!
//! ```text
//! device/
//! ├── mod.rs — module root, re-exports submodules
//! ├── io.rs — high-level entry points (both protocols)
//! ├── rescue.rs — rescue / PC/SC protocol implementation
//! ├── fido.rs — FIDO2 / CTAP2 protocol implementation
//! └── types.rs — shared structs, enums, and constants
//! ```
//!
//! # Overview
//!
//! The `device` module is the only place that talks to the hardware token.
//! Everything above it (UI state, gpui-component views) depends on the
//! public functions exported here; nothing below it should know about the
//! communication details.
//!
//! Two protocols are used:
//!
//! - **Rescue (PC/SC)** — low-level APDU channel for firmware-level
//! configuration: secure boot, LED status, USB applet management, and
//! device reboot. Implemented in [`rescue`].
//!
//! - **FIDO2 (CTAP2)** — standard authenticator protocol for credential
//! management, PIN operations, and enterprise attestation.
//! Implemented in [`fido`].
//!
//! [`io`] sits on top of both and exposes a single function per device
//! operation. Some functions dispatch to one protocol or the other based
//! on a [`types::DeviceMethod`] flag; others try rescue first and fall back
//! to FIDO on failure.
//!
//! # Adding a new device operation
//!
//! 1. Add any new structs/enums to [`types`].
//! 2. Implement the raw protocol call in [`rescue`] or [`fido`].
//! 3. Expose a high-level wrapper in [`io`] that picks the right protocol
//! and converts errors to the caller's expected type.
//! 4. Wire the wrapper into a gpui-component view or action handler.
pub mod fido;
pub mod io;
pub mod rescue;
File diff suppressed because it is too large Load Diff
+553 -18
View File
File diff suppressed because it is too large Load Diff
+76 -1
View File
@@ -1,11 +1,23 @@
//! Shared types for device communication.
//!
//! Organized into three groups:
//! - Application-level types: device info, config, and status used across both protocols
//! - Rescue (PC/SC) types: LED and USB applet configuration read/written over PC/SC
//! - FIDO2 types: credential and authenticator info from CTAP2
#![allow(unused)]
use serde::{Deserialize, Serialize};
use std::fmt;
// ── Application-level types ─────────────────────────────────────────────────
/// Internal application state holding device info for the current session.
struct PForgeState {
device_info: DeviceInfo,
}
/// Basic device identity and flash usage reported by the firmware.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct DeviceInfo {
@@ -15,6 +27,7 @@ pub struct DeviceInfo {
pub firmware_version: String,
}
/// Full device configuration (USB descriptors, LED, touch, crypto options).
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct AppConfig {
@@ -30,8 +43,15 @@ pub struct AppConfig {
pub power_cycle_on_reset: bool,
pub led_steady: bool,
pub enable_secp256k1: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub raw_curves_mask: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub led_order: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub enabled_usb_itf: Option<u8>,
}
/// Partial config update; `None` fields are left unchanged on the device.
#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct AppConfigInput {
@@ -46,8 +66,12 @@ pub struct AppConfigInput {
pub power_cycle_on_reset: Option<bool>,
pub led_steady: Option<bool>,
pub enable_secp256k1: Option<bool>,
pub raw_curves_mask: Option<u32>,
pub led_order: Option<u8>,
pub enabled_usb_itf: Option<u8>,
}
/// Aggregated snapshot of device info, config, and security state.
#[derive(Serialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct FullDeviceStatus {
@@ -56,8 +80,10 @@ pub struct FullDeviceStatus {
pub secure_boot: bool,
pub secure_lock: bool,
pub method: DeviceMethod,
pub firmware_type: FirmwareType,
}
/// Protocol channel used to communicate with the device.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub enum DeviceMethod {
#[serde(rename = "FIDO")]
@@ -65,8 +91,49 @@ pub enum DeviceMethod {
Rescue,
}
// Fido stuff:
/// Recognized firmware variants. Gates UI features, connection methods, and
/// compatibility checks throughout the application.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
pub enum FirmwareType {
PicoFido,
RSKey,
#[default]
Unknown,
}
impl fmt::Display for FirmwareType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::PicoFido => write!(f, "Pico-FIDO"),
Self::RSKey => write!(f, "RS-Key"),
Self::Unknown => write!(f, "Unknown"),
}
}
}
// ── Rescue (PC/SC) types ────────────────────────────────────────────────────
/// LED status configuration read from the Vendor/LED applet.
/// `statuses` is a fixed array of `(color, brightness)` pairs indexed by
/// device status: Idle, Processing, Touch, Boot.
#[derive(Serialize, Debug, Default, Clone, PartialEq)]
pub struct LedStatusConfig {
pub steady: bool,
pub statuses: [(u8, u8); 4],
}
/// USB application endpoint bitmasks from the Management applet.
/// `usb_supported` lists applets the firmware can run;
/// `usb_enabled` lists those active on next boot.
#[derive(Serialize, Debug, Default, Clone, PartialEq)]
pub struct ManagementAppConfig {
pub usb_supported: u16,
pub usb_enabled: u16,
}
// ── FIDO2 types ─────────────────────────────────────────────────────────────
/// Authenticator metadata from CTAP2 GetInfo.
#[derive(Serialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct FidoDeviceInfo {
@@ -91,6 +158,7 @@ pub struct FidoDeviceInfo {
pub max_cred_blob_length: Option<i128>,
}
/// A single FIDO2 credential stored on the device.
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct StoredCredential {
@@ -101,3 +169,10 @@ pub struct StoredCredential {
pub user_id: String,
pub credential_id: String,
}
// ── Constants ───────────────────────────────────────────────────────────────
/// AAGUID assigned to RS-Key hardware.
pub const RSKEY_AAGUID: &str = "2479C7BF6B3056839EC80E8171A918B7";
/// AAGUID assigned to Pico-Fido hardware.
pub const PICOFIDO_AAGUID: &str = "89FB94B706C936739B7E30526D968145";
+32 -10
View File
@@ -8,7 +8,7 @@ use gpui_component::{
};
type PinPromptCallback = std::rc::Rc<dyn Fn(String, WeakEntity<PinPromptContent>, &mut App)>;
type ConfirmCallback = std::rc::Rc<dyn Fn(WeakEntity<ConfirmContent>, &mut App)>;
type ConfirmCallback = std::rc::Rc<dyn Fn(WeakEntity<ConfirmContent>, &mut Window, &mut App)>;
type ChangePinCallback =
std::rc::Rc<dyn Fn(String, String, WeakEntity<ChangePinContent>, &mut App)>;
type SetPinCallback = std::rc::Rc<dyn Fn(String, WeakEntity<SetPinContent>, &mut App)>;
@@ -17,6 +17,9 @@ type SetPinCallback = std::rc::Rc<dyn Fn(String, WeakEntity<SetPinContent>, &mut
enum DialogPhase {
Input,
Loading,
/// Indicates the dialog is blocked on an asynchronous background task,
/// presenting a specific dynamic status message to guide the user (e.g. "Waiting for touch...").
LoadingWithMessage(String),
Success(String),
Error(String),
}
@@ -92,7 +95,7 @@ impl Render for PinPromptContent {
)
.into_any_element(),
DialogPhase::Loading => v_flex()
DialogPhase::Loading | DialogPhase::LoadingWithMessage(_) => v_flex()
.gap_4()
.child(self.description.clone())
.child(Input::new(&self.pin_input).disabled(true))
@@ -365,7 +368,7 @@ impl Render for ConfirmContent {
)
.into_any_element(),
DialogPhase::Loading => v_flex()
DialogPhase::Loading | DialogPhase::LoadingWithMessage(_) => v_flex()
.gap_4()
.child(self.message.clone())
.child(
@@ -414,11 +417,11 @@ impl Render for ConfirmContent {
Button::new("ok")
.with_variant(ok_variant)
.label(ok_label)
.on_click(move |_, _, cx| {
.on_click(move |_, window, cx| {
if let Some(h) = handle.upgrade() {
h.update(cx, |this, cx| this.set_loading(cx));
}
on_ok(handle.clone(), cx);
on_ok(handle.clone(), window, cx);
}),
),
)
@@ -447,11 +450,11 @@ impl Render for ConfirmContent {
Button::new("ok")
.with_variant(ok_variant)
.label(ok_label)
.on_click(move |_, _, cx| {
.on_click(move |_, window, cx| {
if let Some(h) = handle.upgrade() {
h.update(cx, |this, cx| this.set_loading(cx));
}
on_ok(handle.clone(), cx);
on_ok(handle.clone(), window, cx);
}),
),
)
@@ -468,7 +471,7 @@ pub fn open_confirm(
ok_variant: ButtonVariant,
window: &mut Window,
cx: &mut App,
on_ok: impl Fn(WeakEntity<ConfirmContent>, &mut App) + 'static,
on_ok: impl Fn(WeakEntity<ConfirmContent>, &mut Window, &mut App) + 'static,
) {
let title_str = SharedString::from(title.to_string());
let dialog_title = title_str.clone();
@@ -576,7 +579,7 @@ impl Render for ChangePinContent {
)
.into_any_element(),
DialogPhase::Loading => v_flex()
DialogPhase::Loading | DialogPhase::LoadingWithMessage(_) => v_flex()
.gap_4()
.child("Enter your current PIN and choose a new one.")
.child(
@@ -887,7 +890,7 @@ impl Render for SetPinContent {
)
.into_any_element(),
DialogPhase::Loading => v_flex()
DialogPhase::Loading | DialogPhase::LoadingWithMessage(_) => v_flex()
.gap_4()
.child("Choose a PIN for your pico-key.")
.child(
@@ -1100,6 +1103,13 @@ pub struct StatusContent {
}
impl StatusContent {
/// Transitions the dialog into a loading state while displaying a custom, dynamic status message.
/// Useful for multi-step background operations where user context needs to be updated.
pub fn set_loading(&mut self, msg: impl Into<String>, cx: &mut Context<Self>) {
self.phase = DialogPhase::LoadingWithMessage(msg.into());
cx.notify();
}
pub fn set_success(&mut self, msg: String, cx: &mut Context<Self>) {
self.phase = DialogPhase::Success(msg);
cx.notify();
@@ -1178,6 +1188,18 @@ impl Render for StatusContent {
.into_any_element()
}
DialogPhase::LoadingWithMessage(msg) => v_flex()
.gap_4()
.items_center()
.child(msg.clone())
.child(
Button::new("loading")
.primary()
.label("Applying...")
.loading(true),
)
.into_any_element(),
_ => v_flex()
.gap_4()
.items_center()
+25 -11
View File
@@ -219,18 +219,32 @@ impl<V: 'static> AppSidebar<V> {
.child("Device Status"),
)
.child({
let (text, color_bg, color_text) =
if let Some(status) = &state.status {
if status.method == DeviceMethod::Fido {
("Online - Fido", rgb(0xf59e0b), rgb(0xffffff))
} else {
("Online", rgb(0x16a34a), rgb(0xffffff))
}
} else if state.error.is_some() {
("Error", rgb(0xd97706), rgb(0xffffff))
let (text, color_bg, color_text) = if let Some(status) =
&state.status
{
let is_rskey = status.firmware_type
== crate::device::types::FirmwareType::RSKey;
let fw_label =
if is_rskey { "RS-Key" } else { "Pico-FIDO" };
if status.method == DeviceMethod::Fido {
(
format!("Online - FIDO ({})", fw_label),
rgb(0xf59e0b),
rgb(0xffffff),
)
} else {
("Offline", rgb(0xef4444), rgb(0xffffff))
};
(
format!("Online - {}", fw_label),
rgb(0x16a34a),
rgb(0xffffff),
)
}
} else if state.error.is_some() {
("Error".to_string(), rgb(0xd97706), rgb(0xffffff))
} else {
("Offline".to_string(), rgb(0xef4444), rgb(0xffffff))
};
div()
.px(px(6.))
+13 -1
View File
@@ -55,7 +55,7 @@ impl ApplicationRoot {
.map(|s| s.info.serial != status.info.serial)
.unwrap_or(true);
self.device.status = Some(status);
self.device.status = Some(status.clone());
self.device.error = None;
if device_changed {
@@ -72,6 +72,16 @@ impl ApplicationRoot {
}
}
if status.firmware_type == crate::device::types::FirmwareType::RSKey
&& status.method == crate::device::types::DeviceMethod::Rescue
{
self.device.led_status = io::read_led_config().ok();
self.device.management_apps = io::read_management_config().ok();
} else {
self.device.led_status = None;
self.device.management_apps = None;
}
if let Some(config_view) = &self.views.config
&& let Some(window) = window
{
@@ -85,6 +95,8 @@ impl ApplicationRoot {
self.device.status = None;
self.device.error = Some(format!("{}", e));
self.device.fido_info = None;
self.device.led_status = None;
self.device.management_apps = None;
}
}
self.device.loading = false;
+5 -1
View File
@@ -1,5 +1,5 @@
use crate::{
device::types::{FidoDeviceInfo, FullDeviceStatus},
device::types::{FidoDeviceInfo, FullDeviceStatus, LedStatusConfig, ManagementAppConfig},
ui::views::{config::ConfigView, passkeys::PasskeysView},
};
use gpui::{Entity, Pixels, SharedString, px};
@@ -17,6 +17,8 @@ pub enum ActiveView {
pub struct DeviceConnectionState {
pub status: Option<FullDeviceStatus>,
pub fido_info: Option<FidoDeviceInfo>,
pub led_status: Option<LedStatusConfig>,
pub management_apps: Option<ManagementAppConfig>,
pub error: Option<String>,
pub loading: bool,
}
@@ -26,6 +28,8 @@ impl DeviceConnectionState {
Self {
status: None,
fido_info: None,
led_status: None,
management_apps: None,
error: None,
loading: false,
}
+515 -49
View File
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More