mirror of
https://github.com/librekeys/picoforge.git
synced 2026-07-28 08:01:19 -07:00
Merge pull request #100 from librekeys/feat/rskey-0.3.1-support
feat: RSKey v0.3.x support and refactor hal module
This commit is contained in:
@@ -1,8 +1,13 @@
|
||||
//! Build script for embedding application resources.
|
||||
//!
|
||||
//! On Windows, embeds the application icon into the PE binary so that
|
||||
//! the `.exe` and taskbar show the correct icon. On Unix this is a no-op.
|
||||
|
||||
#[cfg(windows)]
|
||||
#[allow(clippy::single_component_path_imports)]
|
||||
use tauri_winres;
|
||||
|
||||
// Configures windows application resource.( fix for app icon and launching app as admin)
|
||||
/// Embed the application icon into the Windows PE binary.
|
||||
#[cfg(windows)]
|
||||
fn main() {
|
||||
let mut res = tauri_winres::WindowsResource::new();
|
||||
|
||||
+12
-1
@@ -1,12 +1,23 @@
|
||||
/// Custom error types for Pico Forge application.
|
||||
//! Application-wide error types.
|
||||
//!
|
||||
//! `PFError` is a single enum covering the four failure modes
|
||||
//! encountered during device discovery, communication, and I/O.
|
||||
//! Each variant carries enough context to render a user-facing message
|
||||
//! and to serialize through the UI layer.
|
||||
|
||||
/// Custom error types for PicoForge operations.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum PFError {
|
||||
/// No compatible FIDO device could be detected on any transport.
|
||||
#[error("No device found")]
|
||||
NoDevice,
|
||||
/// Wrapped error from the PC/SC smart card subsystem.
|
||||
#[error("PCSC Error: {0}")]
|
||||
Pcsc(#[from] pcsc::Error),
|
||||
/// An I/O or encoding/decoding failure (hex, CBOR, transport framing).
|
||||
#[error("IO/Hex Error: {0}")]
|
||||
Io(String),
|
||||
/// A device-level error returned by the firmware or transport layer.
|
||||
#[error("Device Error: {0}")]
|
||||
Device(String),
|
||||
}
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
//! COSE (CBOR Object Signing and Encryption) algorithm, curve, and key-parameter
|
||||
//! constants used in CTAP2 credential creation and authentication responses.
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
use std::fmt;
|
||||
|
||||
/// COSE algorithm identifiers as defined in the IANA COSE Algorithms registry.
|
||||
#[repr(i32)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum CoseAlgorithm {
|
||||
/// ECDSA w/ SHA-256 on P-256 (NIST P-256 / secp256r1).
|
||||
ES256 = -7,
|
||||
/// EdDSA (Edwards-curve Digital Signature Algorithm).
|
||||
EdDSA = -8,
|
||||
/// ECDSA w/ SHA-256 on P-256 (parallel-sphere variant).
|
||||
ESP256 = -9,
|
||||
/// Ed25519 signature algorithm (EdDSA on Curve25519).
|
||||
Ed25519 = -19,
|
||||
/// ECDH-ES key agreement w/ HKDF-256.
|
||||
EcdhEsHkdf256 = -25,
|
||||
/// ECDSA w/ SHA-384 on P-384.
|
||||
ES384 = -35,
|
||||
/// ECDSA w/ SHA-512 on P-521.
|
||||
ES512 = -36,
|
||||
/// ECDSA w/ SHA-256 on secp256k1 (Koblitz curve).
|
||||
ES256K = -47,
|
||||
/// ECDSA w/ SHA-384 on P-384 (parallel-sphere variant).
|
||||
ESP384 = -51,
|
||||
/// ECDSA w/ SHA-512 on P-521 (parallel-sphere variant).
|
||||
ESP512 = -52,
|
||||
/// Ed448 signature algorithm.
|
||||
Ed448 = -53,
|
||||
/// RSASSA-PKCS1-v1_5 w/ SHA-256.
|
||||
RS256 = -257,
|
||||
/// RSASSA-PKCS1-v1_5 w/ SHA-384.
|
||||
RS384 = -258,
|
||||
/// RSASSA-PKCS1-v1_5 w/ SHA-512.
|
||||
RS512 = -259,
|
||||
/// BLS (Boneh–Lynn–Shacham) signature w/ BLS12-381 (curve B).
|
||||
ESB256 = -265,
|
||||
/// BLS signature w/ BLS12-381 (curve B, larger subgroup).
|
||||
ESB384 = -267,
|
||||
/// BLS signature w/ BLS12-381 (curve B, full size).
|
||||
ESB512 = -268,
|
||||
/// ML-DSA-44 (CRYSTALS-Dilithium, NIST Level 2).
|
||||
MLDSA44 = -48,
|
||||
/// ML-DSA-65 (CRYSTALS-Dilithium, NIST Level 3).
|
||||
MLDSA65 = -49,
|
||||
/// ML-DSA-87 (CRYSTALS-Dilithium, NIST Level 5).
|
||||
MLDSA87 = -50,
|
||||
}
|
||||
|
||||
impl CoseAlgorithm {
|
||||
/// Decode a COSE algorithm identifier from an `i128` value as seen in
|
||||
/// CTAP2 `authenticatorGetInfo` or credential public-key data.
|
||||
pub fn from_i128(val: i128) -> Option<Self> {
|
||||
match val as i32 {
|
||||
-7 => Some(Self::ES256),
|
||||
-8 => Some(Self::EdDSA),
|
||||
-9 => Some(Self::ESP256),
|
||||
-19 => Some(Self::Ed25519),
|
||||
-25 => Some(Self::EcdhEsHkdf256),
|
||||
-35 => Some(Self::ES384),
|
||||
-36 => Some(Self::ES512),
|
||||
-47 => Some(Self::ES256K),
|
||||
-51 => Some(Self::ESP384),
|
||||
-52 => Some(Self::ESP512),
|
||||
-53 => Some(Self::Ed448),
|
||||
-257 => Some(Self::RS256),
|
||||
-258 => Some(Self::RS384),
|
||||
-259 => Some(Self::RS512),
|
||||
-265 => Some(Self::ESB256),
|
||||
-267 => Some(Self::ESB384),
|
||||
-268 => Some(Self::ESB512),
|
||||
-48 => Some(Self::MLDSA44),
|
||||
-49 => Some(Self::MLDSA65),
|
||||
-50 => Some(Self::MLDSA87),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for CoseAlgorithm {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::ES256 => write!(f, "ES256"),
|
||||
Self::EdDSA => write!(f, "EdDSA"),
|
||||
Self::ESP256 => write!(f, "ESP256"),
|
||||
Self::Ed25519 => write!(f, "Ed25519"),
|
||||
Self::EcdhEsHkdf256 => write!(f, "ECDH-ES-HKDF-256"),
|
||||
Self::ES384 => write!(f, "ES384"),
|
||||
Self::ES512 => write!(f, "ES512"),
|
||||
Self::ES256K => write!(f, "ES256K"),
|
||||
Self::ESP384 => write!(f, "ESP384"),
|
||||
Self::ESP512 => write!(f, "ESP512"),
|
||||
Self::Ed448 => write!(f, "Ed448"),
|
||||
Self::RS256 => write!(f, "RS256"),
|
||||
Self::RS384 => write!(f, "RS384"),
|
||||
Self::RS512 => write!(f, "RS512"),
|
||||
Self::ESB256 => write!(f, "ESB256"),
|
||||
Self::ESB384 => write!(f, "ESB384"),
|
||||
Self::ESB512 => write!(f, "ESB512"),
|
||||
Self::MLDSA44 => write!(f, "ML-DSA-44"),
|
||||
Self::MLDSA65 => write!(f, "ML-DSA-65"),
|
||||
Self::MLDSA87 => write!(f, "ML-DSA-87"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// COSE elliptic curve identifiers from the IANA COSE Elliptic Curves registry.
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum CoseCurve {
|
||||
/// NIST P-256 (secp256r1).
|
||||
P256 = 1,
|
||||
/// NIST P-384 (secp384r1).
|
||||
P384 = 2,
|
||||
/// NIST P-521 (secp521r1).
|
||||
P521 = 3,
|
||||
/// X25519 key-exchange curve.
|
||||
X25519 = 4,
|
||||
/// X448 key-exchange curve.
|
||||
X448 = 5,
|
||||
/// Ed25519 signing curve.
|
||||
Ed25519 = 6,
|
||||
/// Ed448 signing curve.
|
||||
Ed448 = 7,
|
||||
/// secp256k1 (Koblitz curve, used by ES256K).
|
||||
P256K1 = 8,
|
||||
/// Barreto–Naehrig BN256 curve (pairing-friendly).
|
||||
BP256R1 = 9,
|
||||
/// Barreto–Naehrig BN384 curve (pairing-friendly).
|
||||
BP384R1 = 10,
|
||||
/// Barreto–Naehrig BN512 curve (pairing-friendly).
|
||||
BP512R1 = 11,
|
||||
}
|
||||
|
||||
/// COSE key-parameter labels from RFC 8152 §7.1 / IANA.
|
||||
#[repr(i32)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum CoseKeyParam {
|
||||
/// Key type (kty).
|
||||
Kty = 1,
|
||||
/// Key identifier (kid).
|
||||
Kid = 2,
|
||||
/// Algorithm (alg).
|
||||
Alg = 3,
|
||||
/// Key operations (key_ops).
|
||||
KeyOps = 4,
|
||||
/// Base initialization vector (Base IV).
|
||||
BaseIV = 5,
|
||||
/// Curve / subgroup (crv).
|
||||
Crv = -1,
|
||||
/// X coordinate.
|
||||
X = -2,
|
||||
/// Y coordinate.
|
||||
Y = -3,
|
||||
/// Private key (d).
|
||||
D = -4,
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
//! Shared COSE algorithm/curve/key-parameter definitions and firmware-version parsing.
|
||||
|
||||
pub mod cose;
|
||||
pub mod version;
|
||||
|
||||
pub use version::FirmwareVersion;
|
||||
@@ -0,0 +1,197 @@
|
||||
//! Firmware version parsing and comparison helpers.
|
||||
//!
|
||||
//! Firmware version strings follow a `major.minor[.patch]` format.
|
||||
//! Two-part versions (e.g. `7.6`) are common; three-part versions
|
||||
//! appear on newer firmware releases. The methods on [`FirmwareVersion`]
|
||||
//! are used throughout the HAL to gate feature enablement based on
|
||||
//! known firmware compatibility boundaries.
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
use std::fmt;
|
||||
|
||||
/// Parsed firmware version supporting semantic comparison.
|
||||
///
|
||||
/// Each version is split on `.` — the first component becomes `major`,
|
||||
/// the second `minor`, and an optional third becomes `patch`. The raw
|
||||
/// string is preserved for display purposes.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct FirmwareVersion {
|
||||
pub major: u16,
|
||||
pub minor: u16,
|
||||
pub patch: u16,
|
||||
pub raw: String,
|
||||
}
|
||||
|
||||
impl FirmwareVersion {
|
||||
/// Parse a version string in `major.minor` or `major.minor.patch` format.
|
||||
///
|
||||
/// Returns `None` if the string has fewer than two components, or if
|
||||
/// any component is not a valid unsigned integer.
|
||||
pub fn parse(version: &str) -> Option<Self> {
|
||||
let parts: Vec<&str> = version.split('.').collect();
|
||||
let major = parts.first()?.parse().ok()?;
|
||||
let minor = parts.get(1)?.parse().ok()?;
|
||||
let patch = parts.get(2).and_then(|s| s.parse().ok()).unwrap_or(0);
|
||||
Some(Self {
|
||||
major,
|
||||
minor,
|
||||
patch,
|
||||
raw: version.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns `true` when `self >= (major, minor)`.
|
||||
///
|
||||
/// Only major and minor are compared; patch is ignored so that
|
||||
/// two-part version strings (e.g. `7.2`) compare correctly with
|
||||
/// three-part ones (e.g. `7.2.1`).
|
||||
pub fn is_at_least(&self, major: u16, minor: u16) -> bool {
|
||||
self.major > major || (self.major == major && self.minor >= minor)
|
||||
}
|
||||
|
||||
/// Returns `true` when `lo <= self <= hi`.
|
||||
///
|
||||
/// The upper bound is inclusive. Calls [`is_at_least`](Self::is_at_least)
|
||||
/// for the lower bound and performs a symmetric comparison for the upper.
|
||||
pub fn is_between(&self, lo_major: u16, lo_minor: u16, hi_major: u16, hi_minor: u16) -> bool {
|
||||
self.is_at_least(lo_major, lo_minor)
|
||||
&& (self.major < hi_major || (self.major == hi_major && self.minor <= hi_minor))
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for FirmwareVersion {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.raw)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FirmwareVersion {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
major: 0,
|
||||
minor: 0,
|
||||
patch: 0,
|
||||
raw: "0.0".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_two_part_version() {
|
||||
let v = FirmwareVersion::parse("7.6").unwrap();
|
||||
assert_eq!(v.major, 7);
|
||||
assert_eq!(v.minor, 6);
|
||||
assert_eq!(v.patch, 0);
|
||||
assert_eq!(v.raw, "7.6");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_three_part_version() {
|
||||
let v = FirmwareVersion::parse("5.7.4").unwrap();
|
||||
assert_eq!(v.major, 5);
|
||||
assert_eq!(v.minor, 7);
|
||||
assert_eq!(v.patch, 4);
|
||||
assert_eq!(v.raw, "5.7.4");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_single_part_fails() {
|
||||
assert!(FirmwareVersion::parse("7").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_non_numeric_fails() {
|
||||
assert!(FirmwareVersion::parse("a.b").is_none());
|
||||
assert!(FirmwareVersion::parse("7.x").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_empty_fails() {
|
||||
assert!(FirmwareVersion::parse("").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_at_least_exact_match() {
|
||||
let v = FirmwareVersion::parse("7.2").unwrap();
|
||||
assert!(v.is_at_least(7, 2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_at_least_above() {
|
||||
let v = FirmwareVersion::parse("7.6").unwrap();
|
||||
assert!(v.is_at_least(7, 2));
|
||||
assert!(v.is_at_least(6, 0));
|
||||
assert!(v.is_at_least(7, 6));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_at_least_below() {
|
||||
let v = FirmwareVersion::parse("7.0").unwrap();
|
||||
assert!(!v.is_at_least(7, 2));
|
||||
assert!(!v.is_at_least(8, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_between_inclusive_range() {
|
||||
let v = FirmwareVersion::parse("7.2").unwrap();
|
||||
assert!(v.is_between(6, 0, 8, 0));
|
||||
assert!(v.is_between(7, 0, 7, 2));
|
||||
assert!(v.is_between(7, 2, 7, 2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_between_outside_range() {
|
||||
let v = FirmwareVersion::parse("7.6").unwrap();
|
||||
assert!(!v.is_between(6, 0, 7, 2));
|
||||
assert!(!v.is_between(8, 0, 9, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_version() {
|
||||
let v = FirmwareVersion::default();
|
||||
assert_eq!(v.major, 0);
|
||||
assert_eq!(v.minor, 0);
|
||||
assert_eq!(v.patch, 0);
|
||||
assert_eq!(v.raw, "0.0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
let v = FirmwareVersion::parse("7.6.1").unwrap();
|
||||
assert_eq!(v.to_string(), "7.6.1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_with_patch_zero() {
|
||||
let v = FirmwareVersion::parse("7.6.0").unwrap();
|
||||
assert_eq!(v.major, 7);
|
||||
assert_eq!(v.minor, 6);
|
||||
assert_eq!(v.patch, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_legacy_fido_config_boundaries() {
|
||||
// <= 7.2 supports legacy FIDO hardware config
|
||||
assert!(FirmwareVersion::parse("7.2").unwrap().is_at_least(0, 0));
|
||||
assert!(
|
||||
!FirmwareVersion::parse("7.3")
|
||||
.unwrap()
|
||||
.is_between(0, 0, 7, 2)
|
||||
);
|
||||
assert!(
|
||||
FirmwareVersion::parse("7.2")
|
||||
.unwrap()
|
||||
.is_between(0, 0, 7, 2)
|
||||
);
|
||||
assert!(
|
||||
FirmwareVersion::parse("6.6")
|
||||
.unwrap()
|
||||
.is_between(0, 0, 7, 2)
|
||||
);
|
||||
}
|
||||
}
|
||||
+555
-165
File diff suppressed because it is too large
Load Diff
+733
-98
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,175 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
//! Firmware-type abstraction layer.
|
||||
//!
|
||||
//! This module provides the [`FirmwareTrait`] trait and two concrete
|
||||
//! implementations ([`PicoFidoFirmware`], [`RSKeyFirmware`]) that
|
||||
//! encapsulate per-firmware capability gating. The trait methods are
|
||||
//! checked throughout [`crate::hal::io`] to select the correct command
|
||||
//! path for each operation.
|
||||
//!
|
||||
//! ## Detection
|
||||
//!
|
||||
//! Firmware type is determined at transport-scan time via AAGUID lookup
|
||||
//! in [`AnyFirmware::detect_by_aaguid`]. The AAGUID constants live in
|
||||
//! [`crate::hal::types`]. LK-ONE shares pico-fido's AAGUID and is
|
||||
//! treated as a pico-fido variant.
|
||||
//!
|
||||
//! ## Trait methods
|
||||
//!
|
||||
//! | Method | What it gates |
|
||||
//! |---|---|
|
||||
//! | `supports_legacy_fido_hardware_config` | Whether the device accepts legacy `vendorPrototype` 0xFF commands for hardware config (pico-fido ≤ 7.2, or RS-Key which uses a separate `0x41` path). |
|
||||
//! | `supports_fido_config_write` | Whether `authenticatorConfig` + `vendorPrototype` writes can be used for config. |
|
||||
//! | `supports_rs_key_vendor_command` | Whether RS-Key-specific vendor commands (0x05 etc.) are available. |
|
||||
//! | `supports_rescue_channel` | Whether the PC/SC rescue channel is accessible. |
|
||||
|
||||
pub mod picofido;
|
||||
pub mod rskey;
|
||||
|
||||
pub use picofido::*;
|
||||
pub use rskey::*;
|
||||
|
||||
use crate::hal::common::FirmwareVersion;
|
||||
use crate::hal::types::*;
|
||||
|
||||
/// Dispatch enum wrapping a concrete firmware implementation.
|
||||
///
|
||||
/// Most callers interact through the [`FirmwareTrait`] methods rather
|
||||
/// than matching on variants directly.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum AnyFirmware {
|
||||
PicoFido(PicoFidoFirmware),
|
||||
RSKey(RSKeyFirmware),
|
||||
}
|
||||
|
||||
/// Capability gating per firmware variant.
|
||||
///
|
||||
/// Each method queries a static or version-derived capability flag.
|
||||
/// The concrete implementations in [`PicoFidoFirmware`] and
|
||||
/// [`RSKeyFirmware`] encode the known compatibility boundaries for
|
||||
/// each firmware.
|
||||
pub trait FirmwareTrait {
|
||||
fn firmware_type(&self) -> FirmwareType;
|
||||
fn version(&self) -> &FirmwareVersion;
|
||||
fn major_minor(&self) -> (u16, u16) {
|
||||
(self.version().major, self.version().minor)
|
||||
}
|
||||
fn version_str(&self) -> &str {
|
||||
&self.version().raw
|
||||
}
|
||||
|
||||
/// Whether the firmware accepts legacy FIDO hardware-config commands.
|
||||
fn supports_legacy_fido_hardware_config(&self) -> bool;
|
||||
/// Whether the firmware accepts FIDO config writes.
|
||||
fn supports_fido_config_write(&self) -> bool;
|
||||
/// Whether RS-Key-specific vendor commands are available.
|
||||
fn supports_rs_key_vendor_command(&self) -> bool;
|
||||
/// Whether the PC/SC rescue channel can be activated.
|
||||
fn supports_rescue_channel(&self) -> bool;
|
||||
}
|
||||
|
||||
impl AnyFirmware {
|
||||
/// Detect firmware type from the authenticator's AAGUID hex string.
|
||||
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
|
||||
|| aaguid == crate::hal::types::LKONE_AAGUID
|
||||
{
|
||||
FirmwareType::PicoFido
|
||||
} else {
|
||||
FirmwareType::Unknown
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct an `AnyFirmware` from a known firmware type and version string.
|
||||
///
|
||||
/// LkOne and Unknown are treated as pico-fido variants.
|
||||
pub fn new(fw_type: FirmwareType, version: &str) -> Self {
|
||||
let ver = FirmwareVersion::parse(version).unwrap_or_default();
|
||||
match fw_type {
|
||||
FirmwareType::PicoFido => Self::PicoFido(PicoFidoFirmware::new(ver)),
|
||||
FirmwareType::RSKey => Self::RSKey(RSKeyFirmware::new(ver)),
|
||||
FirmwareType::LkOne | FirmwareType::Unknown => {
|
||||
Self::PicoFido(PicoFidoFirmware::new(ver))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct an `AnyFirmware` with an explicit legacy-vendor flag for pico-fido.
|
||||
///
|
||||
/// The flag is only meaningful for `FirmwareType::PicoFido`; other types
|
||||
/// ignore it.
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Delegate to the inner firmware's version.
|
||||
pub fn version(&self) -> &FirmwareVersion {
|
||||
match self {
|
||||
Self::PicoFido(fw) => fw.version(),
|
||||
Self::RSKey(fw) => fw.version(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the concrete [`FirmwareType`] of the inner firmware.
|
||||
pub fn firmware_type(&self) -> FirmwareType {
|
||||
match self {
|
||||
Self::PicoFido(_) => FirmwareType::PicoFido,
|
||||
Self::RSKey(_) => FirmwareType::RSKey,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the inner firmware supports legacy FIDO hardware-config commands.
|
||||
pub fn supports_legacy_fido_hardware_config(&self) -> bool {
|
||||
match self {
|
||||
Self::PicoFido(fw) => fw.supports_legacy_fido_hardware_config(),
|
||||
Self::RSKey(fw) => fw.supports_legacy_fido_hardware_config(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the inner firmware supports FIDO config writes.
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the inner firmware supports the new-style (post-v7.2) FIDO hardware config path.
|
||||
///
|
||||
/// This is the logical negation of `supports_legacy_fido_hardware_config` for
|
||||
/// pico-fido, and always `false` for RS-Key.
|
||||
pub fn supports_new_fido_hardware_config(&self) -> bool {
|
||||
match self {
|
||||
Self::PicoFido(fw) => !fw.supports_legacy_fido_hardware_config(),
|
||||
Self::RSKey(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether RS-Key-specific vendor commands are available on the inner firmware.
|
||||
pub fn supports_rs_key_vendor_command(&self) -> bool {
|
||||
match self {
|
||||
Self::PicoFido(_) => false,
|
||||
Self::RSKey(fw) => fw.supports_rs_key_vendor_command(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the PC/SC rescue channel can be used with the inner firmware.
|
||||
pub fn supports_rescue_channel(&self) -> bool {
|
||||
match self {
|
||||
Self::PicoFido(_) => true,
|
||||
Self::RSKey(_) => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
//! pico-fido / pico-keys-sdk firmware implementation.
|
||||
//!
|
||||
//! The [`PicoFidoFirmware`] struct stores a parsed version and an optional
|
||||
//! legacy-vendor flag. The `supports_legacy_fido_hardware_config` method
|
||||
//! returns `true` when the firmware is ≤ 7.2 or when the legacy probe
|
||||
//! succeeded – gating the old `vendorPrototype` 0xFF command path.
|
||||
|
||||
use crate::hal::common::FirmwareVersion;
|
||||
use crate::hal::firmwares::FirmwareTrait;
|
||||
use crate::hal::types::FirmwareType;
|
||||
|
||||
/// Firmware implementation for pico-fido / pico-keys-sdk devices.
|
||||
///
|
||||
/// Version-gates the legacy vendor-prototype hardware config path:
|
||||
/// - Versions ≤ 7.2 (major < 7, or 7.x where x ≤ 2) support the legacy path.
|
||||
/// - Versions ≥ 7.3 require the rescue channel or the new-style config.
|
||||
/// - An explicit `has_legacy_vendor` probe result can override the version check.
|
||||
#[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 {
|
||||
/// Create a new pico-fido firmware state with no legacy vendor flag.
|
||||
pub fn new(version: FirmwareVersion) -> Self {
|
||||
Self {
|
||||
version,
|
||||
has_legacy_vendor: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_legacy_vendor(mut self, legacy: bool) -> Self {
|
||||
self.has_legacy_vendor = legacy;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl FirmwareTrait for PicoFidoFirmware {
|
||||
fn firmware_type(&self) -> FirmwareType {
|
||||
FirmwareType::PicoFido
|
||||
}
|
||||
|
||||
fn version(&self) -> &FirmwareVersion {
|
||||
&self.version
|
||||
}
|
||||
|
||||
fn supports_legacy_fido_hardware_config(&self) -> bool {
|
||||
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 {
|
||||
false
|
||||
}
|
||||
|
||||
fn supports_rescue_channel(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
//! RS-Key firmware implementation.
|
||||
//!
|
||||
//! RS-Key reports its SDK version (e.g. 5.7) via CTAP `GetInfo`, which
|
||||
//! does not directly map to the RS-Key release version. Because of this,
|
||||
//! capability gating uses static values and runtime probes rather than
|
||||
//! version checks. RS-Key supports both `legacy_fido_hardware_config`
|
||||
//! (via the `0x41` CONFIG_READ/CONFIG_WRITE path) and the rescue channel.
|
||||
|
||||
use crate::hal::common::FirmwareVersion;
|
||||
use crate::hal::firmwares::FirmwareTrait;
|
||||
use crate::hal::types::FirmwareType;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RSKeyFirmware {
|
||||
version: FirmwareVersion,
|
||||
}
|
||||
|
||||
impl RSKeyFirmware {
|
||||
pub fn new(version: FirmwareVersion) -> Self {
|
||||
Self { version }
|
||||
}
|
||||
}
|
||||
|
||||
impl FirmwareTrait for RSKeyFirmware {
|
||||
fn firmware_type(&self) -> FirmwareType {
|
||||
FirmwareType::RSKey
|
||||
}
|
||||
|
||||
fn version(&self) -> &FirmwareVersion {
|
||||
&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 {
|
||||
true
|
||||
}
|
||||
|
||||
fn supports_rescue_channel(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
+201
-59
@@ -1,31 +1,144 @@
|
||||
//! Device I/O layer bridging rescue (pcsc) and FIDO2 protocols.
|
||||
//! High-level device I/O dispatching across FIDO and Rescue 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
|
||||
//! Each public function here selects the appropriate protocol path based
|
||||
//! on the detected firmware type or an explicit [`DeviceMethod`] parameter.
|
||||
//! Some functions (e.g. `read_device_details`) try FIDO first and merge
|
||||
//! results from Rescue to produce a complete status snapshot.
|
||||
|
||||
#![allow(unused)]
|
||||
use crate::{
|
||||
error::PFError,
|
||||
hal::{fido, rescue, transport::DeviceHandle, types::*},
|
||||
};
|
||||
|
||||
use crate::{error::PFError, hal::fido, hal::rescue, hal::types::*};
|
||||
|
||||
// ── Shared: functions that use both rescue and FIDO ─────────────────────────
|
||||
|
||||
/// Read full device status. Tries rescue first, falls back to FIDO on failure.
|
||||
/// Read full device status by merging FIDO and Rescue data where available.
|
||||
///
|
||||
/// Tries the FIDO HID transport first, then falls back to the PC/SC
|
||||
/// rescue channel. When both succeed, fields from the more detailed
|
||||
/// source are used (e.g. serial/flash from Rescue, AAGUID from FIDO).
|
||||
pub fn read_device_details() -> Result<FullDeviceStatus, PFError> {
|
||||
match rescue::read_device_details() {
|
||||
Ok(status) => Ok(status),
|
||||
Err(e) => {
|
||||
log::warn!("Rescue method failed: {}. Falling back to FIDO...", e);
|
||||
fido::read_device_details()
|
||||
let mut fido_status: Option<FullDeviceStatus> = None;
|
||||
let mut rescue_status: Option<FullDeviceStatus> = None;
|
||||
let mut rescue_fw_type: Option<FirmwareType> = None;
|
||||
|
||||
// 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),
|
||||
}
|
||||
|
||||
// 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),
|
||||
}
|
||||
}
|
||||
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) {
|
||||
(Some(fido), Some(rescue)) => {
|
||||
log::info!("Merging FIDO and Rescue device details");
|
||||
Ok(FullDeviceStatus {
|
||||
info: DeviceInfo {
|
||||
serial: rescue.info.serial,
|
||||
flash_used: rescue.info.flash_used,
|
||||
flash_total: rescue.info.flash_total,
|
||||
firmware_version: fido.info.firmware_version,
|
||||
},
|
||||
config: AppConfig {
|
||||
vid: if !rescue.config.vid.is_empty() {
|
||||
rescue.config.vid
|
||||
} else {
|
||||
fido.config.vid
|
||||
},
|
||||
pid: if !rescue.config.pid.is_empty() {
|
||||
rescue.config.pid
|
||||
} else {
|
||||
fido.config.pid
|
||||
},
|
||||
led_gpio: rescue.config.led_gpio,
|
||||
led_brightness: rescue.config.led_brightness,
|
||||
led_dimmable: rescue.config.led_dimmable,
|
||||
power_cycle_on_reset: rescue.config.power_cycle_on_reset,
|
||||
led_steady: rescue.config.led_steady,
|
||||
enable_secp256k1: rescue.config.enable_secp256k1,
|
||||
led_driver: rescue.config.led_driver.or_else(|| {
|
||||
if fido.config.led_driver.is_some() {
|
||||
fido.config.led_driver
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}),
|
||||
product_name: rescue.config.product_name,
|
||||
touch_timeout: rescue.config.touch_timeout,
|
||||
raw_curves_mask: rescue.config.raw_curves_mask,
|
||||
led_order: rescue.config.led_order,
|
||||
enabled_usb_itf: rescue.config.enabled_usb_itf,
|
||||
led_num: rescue.config.led_num,
|
||||
},
|
||||
secure_boot: rescue.secure_boot,
|
||||
secure_lock: rescue.secure_lock,
|
||||
method: DeviceMethod::Fido,
|
||||
firmware_type: fido.firmware_type,
|
||||
})
|
||||
}
|
||||
(Some(fido), None) => {
|
||||
log::info!("Using FIDO-only device details");
|
||||
Ok(FullDeviceStatus {
|
||||
firmware_type: fido.firmware_type,
|
||||
..fido
|
||||
})
|
||||
}
|
||||
(None, Some(rescue)) => {
|
||||
log::info!("Using Rescue-only device details");
|
||||
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");
|
||||
Err(PFError::NoDevice)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Write app config. Dispatches to rescue or FIDO based on `method`.
|
||||
#[allow(dead_code)]
|
||||
/// Enable or lock secure boot on the device (Rescue-only operation).
|
||||
pub fn enable_secure_boot(lock: bool) -> Result<String, PFError> {
|
||||
rescue::enable_secure_boot(lock)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
/// Reboot the device (normal or BOOTSEL mode) via the Rescue channel.
|
||||
pub fn reboot(to_bootsel: bool) -> Result<String, PFError> {
|
||||
rescue::reboot_device(to_bootsel)
|
||||
}
|
||||
|
||||
/// Write device configuration, selecting FIDO or Rescue path by method.
|
||||
///
|
||||
/// The FIDO path requires a PIN; the Rescue path does not.
|
||||
pub fn write_config(
|
||||
config: AppConfigInput,
|
||||
method: DeviceMethod,
|
||||
@@ -38,51 +151,80 @@ 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)
|
||||
/// Read the LED status configuration via the specified transport method.
|
||||
pub fn read_led_config(method: DeviceMethod) -> Result<LedStatusConfig, PFError> {
|
||||
match method {
|
||||
DeviceMethod::Fido => {
|
||||
let transport = crate::hal::transport::fido::HidTransport::open()?;
|
||||
fido::read_rskey_led_config(&transport)
|
||||
}
|
||||
DeviceMethod::Rescue => rescue::read_led_config(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,
|
||||
/// Write LED status configuration (all four status slots) via the specified transport.
|
||||
pub fn write_led_config(
|
||||
method: DeviceMethod,
|
||||
config: LedStatusConfig,
|
||||
pin: Option<String>,
|
||||
) -> Result<String, PFError> {
|
||||
rescue::write_led_status(status, color, brightness, steady)
|
||||
match method {
|
||||
DeviceMethod::Fido => {
|
||||
let pin = pin.ok_or_else(|| {
|
||||
PFError::Device("PIN is required for FIDO LED config write".into())
|
||||
})?;
|
||||
let transport = crate::hal::transport::fido::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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Read management app config via rescue.
|
||||
pub fn read_management_config() -> Result<ManagementAppConfig, PFError> {
|
||||
rescue::read_management_config()
|
||||
/// Read USB interface configuration from the Management applet.
|
||||
pub fn read_management_config(method: DeviceMethod) -> Result<ManagementAppConfig, PFError> {
|
||||
match method {
|
||||
DeviceMethod::Fido => {
|
||||
let transport = crate::hal::transport::fido::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(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Write management app enabled-mask via rescue.
|
||||
pub fn write_management_config(enabled_mask: u16) -> Result<String, PFError> {
|
||||
rescue::write_management_config(enabled_mask)
|
||||
/// Write the USB interface enable mask via the specified transport.
|
||||
pub fn write_management_config(
|
||||
method: DeviceMethod,
|
||||
enabled_mask: u16,
|
||||
pin: Option<String>,
|
||||
) -> Result<String, PFError> {
|
||||
match method {
|
||||
DeviceMethod::Fido => {
|
||||
let pin = pin.ok_or_else(|| {
|
||||
PFError::Device("PIN is required for FIDO management config write".into())
|
||||
})?;
|
||||
let transport = crate::hal::transport::fido::HidTransport::open()?;
|
||||
fido::write_rskey_dev_config(&transport, enabled_mask, &pin)
|
||||
}
|
||||
DeviceMethod::Rescue => rescue::write_management_config(enabled_mask),
|
||||
}
|
||||
}
|
||||
|
||||
// ── FIDO2 protocol ──────────────────────────────────────────────────────────
|
||||
|
||||
/// Query basic FIDO device info (AAGUID, version, etc.).
|
||||
/// Retrieve the FIDO authenticator metadata (GetInfo) as [`FidoDeviceInfo`].
|
||||
pub(crate) fn get_fido_info() -> Result<FidoDeviceInfo, String> {
|
||||
fido::get_fido_info()
|
||||
}
|
||||
|
||||
/// Change the FIDO user PIN.
|
||||
/// Change the FIDO PIN from `current_pin` to `new_pin`.
|
||||
pub(crate) fn change_fido_pin(
|
||||
current_pin: Option<String>,
|
||||
new_pin: String,
|
||||
@@ -90,7 +232,7 @@ pub(crate) fn change_fido_pin(
|
||||
fido::change_fido_pin(current_pin, new_pin)
|
||||
}
|
||||
|
||||
/// Set the minimum PIN length requirement.
|
||||
/// Set a new minimum PIN length on the authenticator.
|
||||
pub(crate) fn set_min_pin_length(
|
||||
current_pin: String,
|
||||
min_pin_length: u8,
|
||||
@@ -98,32 +240,32 @@ pub(crate) fn set_min_pin_length(
|
||||
fido::set_min_pin_length(current_pin, min_pin_length)
|
||||
}
|
||||
|
||||
/// List stored credentials for the given PIN.
|
||||
/// Enumerate all credentials stored on the authenticator.
|
||||
pub fn get_credentials(pin: String) -> Result<Vec<StoredCredential>, String> {
|
||||
fido::get_credentials(pin)
|
||||
}
|
||||
|
||||
/// Delete a single credential by its ID.
|
||||
/// Delete a credential from the authenticator by credential 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.
|
||||
/// Perform a factory reset on the authenticator.
|
||||
pub fn reset_device() -> Result<String, String> {
|
||||
fido::reset_device()
|
||||
}
|
||||
|
||||
/// Enable enterprise attestation for the device.
|
||||
/// Enable enterprise attestation on the authenticator.
|
||||
pub fn enable_enterprise_attestation(pin: String) -> Result<String, String> {
|
||||
fido::enable_enterprise_attestation(pin)
|
||||
}
|
||||
|
||||
/// Retrieve the enterprise attestation CSR.
|
||||
/// Retrieve the enterprise attestation CSR from the authenticator.
|
||||
pub fn get_enterprise_attestation_csr() -> Result<String, String> {
|
||||
fido::get_enterprise_attestation_csr()
|
||||
}
|
||||
|
||||
/// Upload a signed enterprise attestation certificate.
|
||||
/// Upload an X.509 certificate for enterprise attestation.
|
||||
pub fn upload_enterprise_attestation_cert(
|
||||
pin: String,
|
||||
cert_path: String,
|
||||
|
||||
+32
-35
@@ -1,45 +1,42 @@
|
||||
//! Device communication layer for pico-forge.
|
||||
//! Hardware abstraction layer — all device communication lives here.
|
||||
//!
|
||||
//! ```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
|
||||
//! hal/
|
||||
//! ├── mod.rs — module root
|
||||
//! ├── io.rs — high-level entry points dispatching across protocols
|
||||
//! ├── types.rs — shared structs, enums, and constants
|
||||
//! ├── common/ — COSE algorithm/curve enums and firmware-version parsing
|
||||
//! │ ├── cose.rs
|
||||
//! │ └── version.rs
|
||||
//! ├── firmwares/ — per-firmware capability gating (PicoFido, RSKey)
|
||||
//! │ ├── picofido.rs
|
||||
//! │ └── rskey.rs
|
||||
//! ├── transport/ — physical transport abstractions (HID, PC/SC)
|
||||
//! │ ├── fido.rs — CTAPHID framing over USB HID
|
||||
//! │ └── pcsc.rs — ISO 7816-4 APDU over PC/SC
|
||||
//! ├── fido/ — FIDO2 / CTAP2 protocol implementation
|
||||
//! │ ├── constants.rs — CTAP2 command codes, CBOR keys, vendor commands
|
||||
//! │ └── ops.rs — FidoOperations trait, PIN/credential management
|
||||
//! └── rescue/ — Rescue applet protocol (PC/SC APDU)
|
||||
//! ├── constants.rs — ISO 7816-4 constants, PHY tags, vendor AIDs
|
||||
//! └── ops.rs — RescueOperations trait
|
||||
//! ```
|
||||
//!
|
||||
//! # Overview
|
||||
//! # Architecture
|
||||
//!
|
||||
//! 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.
|
||||
//! [`types`] defines the data types shared across all submodules.
|
||||
//! [`firmwares`] provides [`AnyFirmware`](crate::hal::firmwares::AnyFirmware) with per-firmware capability
|
||||
//! checks (e.g. legacy vs new vendor commands).
|
||||
//! [`transport`] discovers the device and returns a [`DeviceHandle`](crate::hal::transport::DeviceHandle)
|
||||
//! wrapping either a FIDO HID or Rescue PC/SC connection.
|
||||
//! [`fido`] and [`rescue`] implement the protocol-level operations.
|
||||
//! [`io`] sits on top and exposes one function per device operation,
|
||||
//! selecting the correct protocol path based on the detected firmware.
|
||||
|
||||
pub mod common;
|
||||
pub mod fido;
|
||||
pub mod firmwares;
|
||||
pub mod io;
|
||||
pub mod rescue;
|
||||
pub mod transport;
|
||||
pub mod types;
|
||||
|
||||
@@ -124,10 +124,13 @@ pub enum RescueInstruction {
|
||||
/// Data field contains the tag value to write.
|
||||
Write = 0x1C,
|
||||
|
||||
/// Lock or unlock device access.
|
||||
/// Lock or unlock device access (pico-fido only, not RS-Key).
|
||||
///
|
||||
/// P2 parameter determines lock state (0x00=Unlock, 0x01=Lock).
|
||||
/// When locked, PHY configuration commands are rejected.
|
||||
///
|
||||
/// **Note**: This instruction is only available on pico-fido firmware
|
||||
/// (RP2350/ESP32). RS-Key uses `OtpLock = 0x1B` instead.
|
||||
Secure = 0x1D,
|
||||
|
||||
/// Read hardware configuration from flash memory.
|
||||
@@ -241,7 +244,8 @@ pub const P2_UNUSED: u8 = 0x00;
|
||||
/// commands to access hardware configuration.
|
||||
///
|
||||
/// PHY configuration is shared between pico-fido and RS-Key, with RS-Key adding
|
||||
/// additional tags like `LedOrder` for RGB LED support.
|
||||
/// additional tags like `LedOrder` for RGB LED support and `LedNum` for
|
||||
/// multi-LED count.
|
||||
///
|
||||
/// References:
|
||||
/// - [pico-fido](https://github.com/polhenarejos/pico-fido) `src/fs/phy.h`
|
||||
@@ -308,6 +312,13 @@ pub enum PhyTag {
|
||||
/// Data format: `[ORDER]` (1 byte).
|
||||
/// RS-Key specific tag for configuring LED color channel order.
|
||||
LedOrder = 0x0D,
|
||||
|
||||
/// Number of LEDs on the device (RS-Key extension).
|
||||
///
|
||||
/// Data format: `[COUNT]` (1 byte).
|
||||
/// RS-Key specific tag specifying how many individual LEDs
|
||||
/// are present (e.g., 1 for single, 3 for RGB).
|
||||
LedNum = 0x0E,
|
||||
}
|
||||
|
||||
impl PhyTag {
|
||||
@@ -327,6 +338,7 @@ impl PhyTag {
|
||||
0x0B => Some(Self::EnabledUsbItf),
|
||||
0x0C => Some(Self::LedDriver),
|
||||
0x0D => Some(Self::LedOrder),
|
||||
0x0E => Some(Self::LedNum),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -344,6 +356,13 @@ impl PhyTag {
|
||||
/// - [RS-Key](https://github.com/TheMaxMur/RS-Key) `crates/rsk-rescue/src/phy.rs`
|
||||
bitflags::bitflags! {
|
||||
pub struct RescueOptions: u16 {
|
||||
/// Windows Compatible ID (WCID) support.
|
||||
///
|
||||
/// When set, the device advertises WCID descriptors for
|
||||
/// automatic driver installation on Windows without
|
||||
/// requiring a custom .inf file.
|
||||
const WCID = 0x01;
|
||||
|
||||
/// LED supports dimming (PWM control).
|
||||
///
|
||||
/// When set, the LED brightness can be adjusted. When clear,
|
||||
|
||||
+25
-919
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,131 @@
|
||||
//! Device discovery and transport abstraction.
|
||||
//!
|
||||
//! Two physical transports coexist:
|
||||
//!
|
||||
//! * **FIDO HID** ([`fido::HidTransport`]) — the primary CTAP2 / CTAPHID channel
|
||||
//! over USB HID. Used for normal operations (credential management, PIN,
|
||||
//! authentication). Supports both pico-fido and RS-Key firmwares.
|
||||
//! * **Rescue PC/SC** ([`pcsc::PcscTransport`]) — an ISO 7816-4 APDU channel over
|
||||
//! a PC/SC smart-card reader. Used when the device is in rescue/bootloader mode
|
||||
//! or when FIDO commands are blocked (e.g. firmware version ≥ 7.4 on pico-fido).
|
||||
//!
|
||||
//! The [`DeviceHandle::discover`] method tries FIDO HID first and falls back to
|
||||
//! PC/SC. This ensures normal operation prefers the faster HID path while still
|
||||
//! allowing rescue access when needed.
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use crate::error::PFError;
|
||||
use crate::hal::types::FirmwareType;
|
||||
|
||||
pub mod fido;
|
||||
use fido::HidTransport;
|
||||
|
||||
pub mod pcsc;
|
||||
use pcsc::PcscTransport;
|
||||
|
||||
/// A connected device handle over either the FIDO or rescue transport.
|
||||
pub enum DeviceHandle {
|
||||
/// Connected via CTAPHID (USB HID).
|
||||
Fido(HidTransport),
|
||||
/// Connected via PC/SC (ISO 7816-4 APDU, rescue/bootloader mode).
|
||||
Rescue(PcscTransport),
|
||||
}
|
||||
|
||||
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(t) => f.debug_tuple("Rescue").field(&t.firmware_type).finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Opaque device identity presented to consumers after discovery.
|
||||
///
|
||||
/// vid/pid are populated only for the FIDO HID path; the rescue path
|
||||
/// reports (0, 0) since PC/SC does not expose USB identifiers.
|
||||
#[derive(Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub struct DeviceIdentity {
|
||||
/// USB Vendor ID (0 for rescue/PC/SC).
|
||||
pub vid: u16,
|
||||
/// USB Product ID (0 for rescue/PC/SC).
|
||||
pub pid: u16,
|
||||
/// Human-readable product name.
|
||||
pub product_name: String,
|
||||
/// Detected firmware type (unknown for FIDO HID until GetInfo is called).
|
||||
pub firmware_type: FirmwareType,
|
||||
}
|
||||
|
||||
impl DeviceHandle {
|
||||
/// Return the firmware type for a rescue handle, or `Unknown` for FIDO.
|
||||
pub fn firmware_type(&self) -> FirmwareType {
|
||||
match self {
|
||||
Self::Fido(_) => FirmwareType::Unknown,
|
||||
Self::Rescue(t) => t.firmware_type.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract the inner FIDO transport, consuming the handle.
|
||||
#[allow(dead_code)]
|
||||
pub fn into_fido(self) -> Option<HidTransport> {
|
||||
match self {
|
||||
Self::Fido(t) => Some(t),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to discover a device via FIDO HID first, falling back to Rescue PC/SC.
|
||||
#[allow(dead_code)]
|
||||
pub fn discover() -> Result<(Self, DeviceIdentity), PFError> {
|
||||
match Self::try_fido() {
|
||||
Ok(Some((handle, identity))) => {
|
||||
log::info!("Device discovered via FIDO HID transport");
|
||||
return Ok((handle, identity));
|
||||
}
|
||||
Ok(None) => log::info!("No FIDO HID device found"),
|
||||
Err(e) => log::warn!("FIDO HID discovery error: {}", e),
|
||||
}
|
||||
|
||||
match Self::try_rescue() {
|
||||
Ok(Some((handle, identity))) => {
|
||||
log::info!("Device discovered via Rescue PC/SC transport");
|
||||
return Ok((handle, identity));
|
||||
}
|
||||
Ok(None) => log::info!("No Rescue PC/SC device found"),
|
||||
Err(e) => log::warn!("Rescue PC/SC discovery error: {}", e),
|
||||
}
|
||||
|
||||
Err(PFError::NoDevice)
|
||||
}
|
||||
|
||||
/// Try to connect via FIDO HID transport.
|
||||
pub fn try_fido() -> Result<Option<(Self, DeviceIdentity)>, PFError> {
|
||||
let transport = HidTransport::open()?;
|
||||
let identity = DeviceIdentity {
|
||||
vid: transport.vid,
|
||||
pid: transport.pid,
|
||||
product_name: transport.product_name.clone(),
|
||||
firmware_type: FirmwareType::Unknown,
|
||||
};
|
||||
Ok(Some((Self::Fido(transport), identity)))
|
||||
}
|
||||
|
||||
/// Try to connect via Rescue PC/SC transport.
|
||||
pub fn try_rescue() -> Result<Option<(Self, DeviceIdentity)>, PFError> {
|
||||
match PcscTransport::open() {
|
||||
Ok(transport) => {
|
||||
let identity = DeviceIdentity {
|
||||
vid: 0,
|
||||
pid: 0,
|
||||
product_name: "Rescue Device".into(),
|
||||
firmware_type: transport.firmware_type.clone(),
|
||||
};
|
||||
Ok(Some((Self::Rescue(transport), identity)))
|
||||
}
|
||||
Err(PFError::NoDevice) => Ok(None),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
//! PC/SC (Smart Card) transport for the rescue channel.
|
||||
//!
|
||||
//! Communicates with the device via ISO 7816-4 APDUs over a PC/SC
|
||||
//! compatible smart-card reader. The device exposes a rescue applet
|
||||
//! identified by [`RESCUE_AID`] when in rescue/bootloader mode.
|
||||
|
||||
use crate::error::PFError;
|
||||
use crate::hal::{rescue::constants::*, types::FirmwareType};
|
||||
use pcsc::{Context, Protocols, Scope, ShareMode};
|
||||
|
||||
/// PC/SC transport wrapping a connected ISO 7816-4 smart card.
|
||||
pub struct PcscTransport {
|
||||
/// The connected PC/SC card handle.
|
||||
pub card: pcsc::Card,
|
||||
/// Firmware type determined during the SELECT AID exchange.
|
||||
pub firmware_type: FirmwareType,
|
||||
/// Raw response bytes from the SELECT AID command.
|
||||
pub select_resp: Vec<u8>,
|
||||
}
|
||||
|
||||
impl PcscTransport {
|
||||
/// Open the rescue channel using the default Rescue AID.
|
||||
pub fn open() -> Result<Self, PFError> {
|
||||
Self::open_with_aid(RESCUE_AID)
|
||||
}
|
||||
|
||||
/// Open the rescue channel using a custom AID.
|
||||
///
|
||||
/// Scans for the first connected reader, sends the SELECT AID APDU,
|
||||
/// and determines the firmware type from the reader name or response data.
|
||||
pub fn open_with_aid(aid: &[u8]) -> Result<Self, PFError> {
|
||||
let ctx = Context::establish(Scope::User).map_err(|e| {
|
||||
log::error!("Failed to establish PCSC context: {}", e);
|
||||
PFError::Pcsc(e)
|
||||
})?;
|
||||
|
||||
let mut readers_buf = [0; 2048];
|
||||
let mut readers = ctx.list_readers(&mut readers_buf)?;
|
||||
|
||||
let reader = readers.next().ok_or_else(|| {
|
||||
log::info!("No Smart Card Reader found");
|
||||
PFError::NoDevice
|
||||
})?;
|
||||
|
||||
let reader_name = reader.to_string_lossy();
|
||||
let mut fw_type = if reader_name.contains("RS-Key") || reader_name.contains("RSK") {
|
||||
FirmwareType::RSKey
|
||||
} else {
|
||||
FirmwareType::Unknown
|
||||
};
|
||||
|
||||
let card = ctx.connect(reader, ShareMode::Shared, Protocols::ANY)?;
|
||||
|
||||
let mut apdu = vec![
|
||||
APDU_CLA_ISO,
|
||||
APDU_INS_SELECT,
|
||||
APDU_P1_SELECT_BY_DF_NAME,
|
||||
APDU_P2_RETURN_FCI,
|
||||
aid.len() as u8,
|
||||
];
|
||||
apdu.extend_from_slice(aid);
|
||||
|
||||
let mut rx_buf = [0; 256];
|
||||
let rx = card.transmit(&apdu, &mut rx_buf)?;
|
||||
|
||||
if !rx.ends_with(&[0x90, 0x00]) {
|
||||
log::error!("Rescue Applet not found on the device!");
|
||||
return Err(PFError::Device(
|
||||
"Rescue Applet not found on device. Is it in FIDO mode?".into(),
|
||||
));
|
||||
}
|
||||
|
||||
let data = rx.to_vec();
|
||||
|
||||
if fw_type == FirmwareType::Unknown {
|
||||
if data.len() >= 4 && data[2] >= 8 {
|
||||
fw_type = FirmwareType::RSKey;
|
||||
} else {
|
||||
fw_type = FirmwareType::PicoFido;
|
||||
}
|
||||
}
|
||||
|
||||
log::info!("Successfully connected to Rescue Applet");
|
||||
log::info!("Detected firmware type: {:?}", fw_type);
|
||||
|
||||
Ok(Self {
|
||||
card,
|
||||
firmware_type: fw_type,
|
||||
select_resp: data,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn transmit<'a>(&self, apdu: &[u8], rx_buf: &'a mut [u8]) -> Result<&'a [u8], PFError> {
|
||||
self.card.transmit(apdu, rx_buf).map_err(PFError::Pcsc)
|
||||
}
|
||||
}
|
||||
@@ -34,21 +34,31 @@ pub struct AppConfig {
|
||||
pub vid: String,
|
||||
pub pid: String,
|
||||
pub product_name: String,
|
||||
/// GPIO pin the status LED is connected to.
|
||||
pub led_gpio: u8,
|
||||
pub led_brightness: u8,
|
||||
/// Touch-button press timeout in seconds.
|
||||
pub touch_timeout: u8,
|
||||
/// LED driver type identifier (e.g. PWM direct vs external driver).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub led_driver: Option<u8>,
|
||||
pub led_dimmable: bool,
|
||||
pub power_cycle_on_reset: bool,
|
||||
/// When set, the LED stays on (not pulsed) for touch/processing states.
|
||||
pub led_steady: bool,
|
||||
pub enable_secp256k1: bool,
|
||||
/// Bitmask of raw (unwrapped) curve identifiers supported by the firmware.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub raw_curves_mask: Option<u32>,
|
||||
/// The order in which LED colours are sequenced during status transitions.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub led_order: Option<u8>,
|
||||
/// Bitmask of USB interface endpoints that are enabled.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub enabled_usb_itf: Option<u8>,
|
||||
/// Number of individual LEDs on the device.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub led_num: Option<u8>,
|
||||
}
|
||||
|
||||
/// Partial config update; `None` fields are left unchanged on the device.
|
||||
@@ -69,25 +79,34 @@ pub struct AppConfigInput {
|
||||
pub raw_curves_mask: Option<u32>,
|
||||
pub led_order: Option<u8>,
|
||||
pub enabled_usb_itf: Option<u8>,
|
||||
pub led_num: Option<u8>,
|
||||
}
|
||||
|
||||
/// Aggregated snapshot of device info, config, and security state.
|
||||
#[derive(Serialize, Debug, Clone, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FullDeviceStatus {
|
||||
/// Basic device identity and flash usage.
|
||||
pub info: DeviceInfo,
|
||||
/// Full device configuration (USB descriptors, LED, touch, crypto).
|
||||
pub config: AppConfig,
|
||||
/// Whether secure boot is enabled on the device.
|
||||
pub secure_boot: bool,
|
||||
/// Whether the device's secure configuration is locked (read-only until reset).
|
||||
pub secure_lock: bool,
|
||||
/// Protocol channel used for the last successful communication.
|
||||
pub method: DeviceMethod,
|
||||
/// Detected firmware variant.
|
||||
pub firmware_type: FirmwareType,
|
||||
}
|
||||
|
||||
/// Protocol channel used to communicate with the device.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
pub enum DeviceMethod {
|
||||
/// Communication over FIDO HID (CTAPHID / CTAP2).
|
||||
#[serde(rename = "FIDO")]
|
||||
Fido,
|
||||
/// Communication over PC/SC rescue channel (ISO 7816-4 APDU).
|
||||
Rescue,
|
||||
}
|
||||
|
||||
@@ -95,8 +114,13 @@ pub enum DeviceMethod {
|
||||
/// compatibility checks throughout the application.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
|
||||
pub enum FirmwareType {
|
||||
/// Pol Henarejos' pico-fido / pico-keys-sdk based firmware.
|
||||
PicoFido,
|
||||
/// TheMaxMur's RS-Key firmware (SDK 5.x+).
|
||||
RSKey,
|
||||
/// LibreKeys LK-ONE (pico-fido fork, same AAGUID as pico-fido).
|
||||
LkOne,
|
||||
/// Unrecognised or undetected firmware.
|
||||
#[default]
|
||||
Unknown,
|
||||
}
|
||||
@@ -106,6 +130,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"),
|
||||
}
|
||||
}
|
||||
@@ -118,7 +143,10 @@ impl fmt::Display for FirmwareType {
|
||||
/// device status: Idle, Processing, Touch, Boot.
|
||||
#[derive(Serialize, Debug, Default, Clone, PartialEq)]
|
||||
pub struct LedStatusConfig {
|
||||
/// Whether the LED stays on steady (true) or pulses (false).
|
||||
pub steady: bool,
|
||||
/// Fixed array of `(color, brightness)` pairs indexed by device status:
|
||||
/// Idle, Processing, Touch, Boot.
|
||||
pub statuses: [(u8, u8); 4],
|
||||
}
|
||||
|
||||
@@ -137,14 +165,20 @@ pub struct ManagementAppConfig {
|
||||
#[derive(Serialize, Debug, Clone, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FidoDeviceInfo {
|
||||
/// Supported CTAP versions reported by the authenticator.
|
||||
pub versions: Vec<String>,
|
||||
/// Supported CTAP extensions.
|
||||
pub extensions: Vec<String>,
|
||||
/// Authenticator Attestation GUID (hex-encoded, uppercase, no dashes).
|
||||
pub aaguid: String,
|
||||
/// Authenticator options map from `authenticatorGetInfo`.
|
||||
pub options: std::collections::HashMap<String, bool>,
|
||||
pub max_msg_size: i128,
|
||||
/// PIN/UV protocol versions supported.
|
||||
pub pin_protocols: Vec<u32>,
|
||||
pub remaining_discoverable_credentials: Option<i128>,
|
||||
pub min_pin_length: i128,
|
||||
/// Firmware version as reported by the authenticator (may differ from the HAL-parsed version).
|
||||
pub firmware_version: String,
|
||||
/// Supported vendor config commands (human-readable names), parsed from CTAP GetInfo.
|
||||
pub vendor_config_commands: Vec<String>,
|
||||
@@ -152,6 +186,7 @@ pub struct FidoDeviceInfo {
|
||||
pub certifications: std::collections::HashMap<String, bool>,
|
||||
pub max_credential_count_in_list: Option<i128>,
|
||||
pub max_credential_id_length: Option<i128>,
|
||||
/// List of supported COSE algorithm display names.
|
||||
pub algorithms: Vec<String>,
|
||||
pub max_serialized_large_blob_array: Option<i128>,
|
||||
pub force_pin_change: Option<bool>,
|
||||
@@ -176,3 +211,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;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user