mirror of
https://github.com/librekeys/picoforge.git
synced 2026-07-28 08:01:19 -07:00
Merge pull request #97 from librekeys/RS-Key-Support
Add RS-Keys Support and improve code documentation
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -29,4 +29,6 @@ src-svelte
|
||||
gpui-component
|
||||
packaging
|
||||
.cargo-packager
|
||||
llm-texts
|
||||
llm-texts
|
||||
opencode.jsonc
|
||||
AGENTS.md
|
||||
|
||||
Generated
+93
-20
@@ -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
@@ -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 = [] }
|
||||
|
||||
@@ -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.
|
||||
|
||||
+546
-166
File diff suppressed because it is too large
Load Diff
+346
-63
File diff suppressed because it is too large
Load Diff
+90
-2
@@ -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
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
+581
-21
File diff suppressed because it is too large
Load Diff
+553
-18
File diff suppressed because it is too large
Load Diff
+76
-1
@@ -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
@@ -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()
|
||||
|
||||
@@ -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
@@ -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
@@ -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
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
Reference in New Issue
Block a user