chore: organise backend code for fido support

This commit is contained in:
Suyog Tandel
2026-01-10 22:14:43 +05:30
parent 458710579a
commit 5e4ce6bbcf
23 changed files with 2925 additions and 1555 deletions
+18 -6
View File
@@ -15,6 +15,10 @@
> [!IMPORTANT]
> PicoForge is an independent, community-developed tool and is not affiliated with or endorsed by the official [pico-fido](https://github.com/polhenarejos/pico-fido) project.
> This software does not share any code with the official closed-source pico-fido application.
>
> For some reason, when running the app on windows, it needs to be ran as administrator or it cannot detect the device, this will be fixed in future. Till then run the app as admin.
>
> For linux users if RPM or DEB does not work or not supported on the distro, it is better to compile the app from source and use it, AppImages does not work correctly as of now.
## About
@@ -106,13 +110,17 @@ The compiled binaries will be available in:
#### Linux
Install PC/SC dependencies:
- AppImages do not work, either use DEBs or RPMs, or build from source.
- Flatpaks are planned for future `BETA` versions of the apps.
- Install PC/SC dependencies:
```bash
sudo apt install libpcsclite-dev pcscd
```
Start the PC/SC daemon:
- Start the PC/SC daemon:
```bash
sudo systemctl start pcscd
@@ -121,11 +129,15 @@ sudo systemctl enable pcscd
#### macOS
No additional setup required. PC/SC framework is built-in.
- App has not been tested by me on macos and I cannot debug for it ( as I do not have a mac ), if someone is ready to help me, just open an issue or message me on the discord server.
- No additional setup required. PC/SC framework is built-in.
#### Windows
Ensure Smart Card service is running:
- Run the app as `Administrator` or it will not work correctly or fail to detect the device.
- Ensure Smart Card service is running:
```powershell
Get-Service SCardSvr | Start-Service
@@ -194,7 +206,7 @@ picoforge/
## Contributing
Contributions are welcome! Please follow these steps:
Contributions are welcome (REALLY NEEDED, PLEASE HELP ME)! Please follow these steps:
1. Fork the repository
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
@@ -255,4 +267,4 @@ See [LICENSE](LICENSE) for full details.
Copyright © 2026 Suyog Tandel
</div>
</div>
+52
View File
@@ -0,0 +1,52 @@
{
"$schema": "https://biomejs.dev/schemas/2.3.11/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"files": {
"ignoreUnknown": false,
"includes": [
"src/**",
"!src/app.html",
"!src/app.css",
"!src/lib/components/ui/**",
"!src/lib/utils.ts",
"!src/lib/hooks/is-mobile.svelte.ts"
]
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 140
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
},
"javascript": {
"formatter": {
"lineWidth": 120,
"quoteStyle": "double",
"semicolons": "always",
"trailingCommas": "es5"
}
},
"html": {
"formatter": {
"lineWidth": 140
}
},
"assist": {
"enabled": true,
"actions": {
"source": {
"organizeImports": "on"
}
}
}
}
Generated
+56
View File
@@ -1,6 +1,7 @@
{
"version": "5",
"specifiers": {
"npm:@biomejs/biome@^2.3.11": "2.3.11",
"npm:@internationalized/date@^3.10.1": "3.10.1",
"npm:@lucide/svelte@0.562": "0.562.0_svelte@5.46.1__acorn@8.15.0",
"npm:@sveltejs/adapter-static@^3.0.6": "3.0.10_@sveltejs+kit@2.49.3__@sveltejs+vite-plugin-svelte@5.1.1___svelte@5.46.1____acorn@8.15.0___vite@6.4.1____picomatch@4.0.3__svelte@5.46.1___acorn@8.15.0__typescript@5.9.3__vite@6.4.1___picomatch@4.0.3__acorn@8.15.0_@sveltejs+vite-plugin-svelte@5.1.1__svelte@5.46.1___acorn@8.15.0__vite@6.4.1___picomatch@4.0.3_svelte@5.46.1__acorn@8.15.0_typescript@5.9.3_vite@6.4.1__picomatch@4.0.3",
@@ -23,6 +24,60 @@
"npm:vite@^6.0.3": "6.4.1_picomatch@4.0.3"
},
"npm": {
"@biomejs/biome@2.3.11": {
"integrity": "sha512-/zt+6qazBWguPG6+eWmiELqO+9jRsMZ/DBU3lfuU2ngtIQYzymocHhKiZRyrbra4aCOoyTg/BmY+6WH5mv9xmQ==",
"optionalDependencies": [
"@biomejs/cli-darwin-arm64",
"@biomejs/cli-darwin-x64",
"@biomejs/cli-linux-arm64",
"@biomejs/cli-linux-arm64-musl",
"@biomejs/cli-linux-x64",
"@biomejs/cli-linux-x64-musl",
"@biomejs/cli-win32-arm64",
"@biomejs/cli-win32-x64"
],
"bin": true
},
"@biomejs/cli-darwin-arm64@2.3.11": {
"integrity": "sha512-/uXXkBcPKVQY7rc9Ys2CrlirBJYbpESEDme7RKiBD6MmqR2w3j0+ZZXRIL2xiaNPsIMMNhP1YnA+jRRxoOAFrA==",
"os": ["darwin"],
"cpu": ["arm64"]
},
"@biomejs/cli-darwin-x64@2.3.11": {
"integrity": "sha512-fh7nnvbweDPm2xEmFjfmq7zSUiox88plgdHF9OIW4i99WnXrAC3o2P3ag9judoUMv8FCSUnlwJCM1B64nO5Fbg==",
"os": ["darwin"],
"cpu": ["x64"]
},
"@biomejs/cli-linux-arm64-musl@2.3.11": {
"integrity": "sha512-XPSQ+XIPZMLaZ6zveQdwNjbX+QdROEd1zPgMwD47zvHV+tCGB88VH+aynyGxAHdzL+Tm/+DtKST5SECs4iwCLg==",
"os": ["linux"],
"cpu": ["arm64"]
},
"@biomejs/cli-linux-arm64@2.3.11": {
"integrity": "sha512-l4xkGa9E7Uc0/05qU2lMYfN1H+fzzkHgaJoy98wO+b/7Gl78srbCRRgwYSW+BTLixTBrM6Ede5NSBwt7rd/i6g==",
"os": ["linux"],
"cpu": ["arm64"]
},
"@biomejs/cli-linux-x64-musl@2.3.11": {
"integrity": "sha512-vU7a8wLs5C9yJ4CB8a44r12aXYb8yYgBn+WeyzbMjaCMklzCv1oXr8x+VEyWodgJt9bDmhiaW/I0RHbn7rsNmw==",
"os": ["linux"],
"cpu": ["x64"]
},
"@biomejs/cli-linux-x64@2.3.11": {
"integrity": "sha512-/1s9V/H3cSe0r0Mv/Z8JryF5x9ywRxywomqZVLHAoa/uN0eY7F8gEngWKNS5vbbN/BsfpCG5yeBT5ENh50Frxg==",
"os": ["linux"],
"cpu": ["x64"]
},
"@biomejs/cli-win32-arm64@2.3.11": {
"integrity": "sha512-PZQ6ElCOnkYapSsysiTy0+fYX+agXPlWugh6+eQ6uPKI3vKAqNp6TnMhoM3oY2NltSB89hz59o8xIfOdyhi9Iw==",
"os": ["win32"],
"cpu": ["arm64"]
},
"@biomejs/cli-win32-x64@2.3.11": {
"integrity": "sha512-43VrG813EW+b5+YbDbz31uUsheX+qFKCpXeY9kfdAx+ww3naKxeVkTD9zLIWxUPfJquANMHrmW3wbe/037G0Qg==",
"os": ["win32"],
"cpu": ["x64"]
},
"@esbuild/aix-ppc64@0.25.12": {
"integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
"os": ["aix"],
@@ -1036,6 +1091,7 @@
"workspace": {
"packageJson": {
"dependencies": [
"npm:@biomejs/biome@^2.3.11",
"npm:@internationalized/date@^3.10.1",
"npm:@lucide/svelte@0.562",
"npm:@sveltejs/adapter-static@^3.0.6",
+5 -2
View File
@@ -10,8 +10,10 @@
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"tauri": "tauri",
"fmt": "deno fmt",
"fmt:check": "deno fmt --check"
"fmt": "biome format --write",
"fmt:check": "biome format",
"lint": "biome lint",
"lint:fix": "biome lint --write"
},
"license": "MIT",
"dependencies": {
@@ -23,6 +25,7 @@
"tailwindcss": "^4.1.18"
},
"devDependencies": {
"@biomejs/biome": "^2.3.11",
"@internationalized/date": "^3.10.1",
"@sveltejs/adapter-static": "^3.0.6",
"@sveltejs/kit": "^2.9.0",
+14
View File
@@ -2952,14 +2952,18 @@ name = "picoforge"
version = "0.1.1"
dependencies = [
"anyhow",
"bitflags 2.10.0",
"byteorder",
"ctap-hid-fido2",
"directories",
"hex",
"hidapi",
"log",
"log4rs",
"pcsc",
"rand 0.9.2",
"serde",
"serde_cbor_2",
"serde_json",
"tauri",
"tauri-build",
@@ -3562,6 +3566,16 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_cbor_2"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34aec2709de9078e077090abd848e967abab63c9fb3fdb5d4799ad359d8d482c"
dependencies = [
"half",
"serde",
]
[[package]]
name = "serde_core"
version = "1.0.228"
+4
View File
@@ -31,6 +31,10 @@ byteorder = "1.5" # Required for writing Big-Endian numbers (firmware requi
thiserror = "2" # Makes custom error handling much easier
anyhow = "1" # For easy error propagation
ctap-hid-fido2 = "3.5" # For fido2 interface operations
hidapi = "2.6" # For fido2 interface operations but non-standard commands
serde_cbor_2 = "0.13"
rand = "0.9"
bitflags = "2.10"
log = "0.4" # Logging facade
log4rs = "1" # For logging to output (like stdout)
+4
View File
@@ -0,0 +1,4 @@
edition = "2024"
hard_tabs = true
tab_spaces = 4
max_width = 100
-73
View File
@@ -1,73 +0,0 @@
use ctap_hid_fido2::{Cfg, FidoKeyHidFactory};
use crate::types::FidoDeviceInfo;
use std::collections::HashMap;
// use ctap_hid_fido2::fidokey::FidoKeyHid;
#[tauri::command]
pub(crate) fn get_fido_info() -> Result<FidoDeviceInfo, String> {
let cfg = Cfg::init();
let device = FidoKeyHidFactory::create(&cfg)
.map_err(|_| "Could not connect to FIDO device. Is it plugged in?".to_string())?;
let info = device
.get_info()
.map_err(|e| format!("Error reading device info: {:?}", e))?;
let options_map: HashMap<String, bool> = info.options.into_iter().collect();
Ok(FidoDeviceInfo {
versions: info.versions,
extensions: info.extensions,
aaguid: hex::encode_upper(info.aaguid),
options: options_map,
max_msg_size: info.max_msg_size,
pin_protocols: info.pin_uv_auth_protocols,
min_pin_length: info.min_pin_length,
firmware_version: format!("0x{:X}", info.firmware_version),
})
}
#[tauri::command]
pub(crate) fn change_fido_pin(
current_pin: Option<String>,
new_pin: String,
) -> Result<String, String> {
let cfg = Cfg::init();
let device = FidoKeyHidFactory::create(&cfg)
.map_err(|e| format!("Failed to connect to FIDO device: {:?}", e))?;
match current_pin {
Some(old) => {
device
.change_pin(&old, &new_pin)
.map_err(|e| format!("Failed to change PIN: {:?}", e))?;
Ok("PIN Changed Successfully".into())
}
None => {
device
.set_new_pin(&new_pin)
.map_err(|e| format!("Failed to set PIN: {:?}", e))?;
Ok("PIN Set Successfully".into())
}
}
}
#[tauri::command]
pub(crate) fn set_min_pin_length(
current_pin: String,
min_pin_length: u8,
) -> Result<String, String> {
let cfg = Cfg::init();
let device = FidoKeyHidFactory::create(&cfg)
.map_err(|e| format!("Failed to connect to FIDO device: {:?}", e))?;
device
.set_min_pin_length(min_pin_length, Some(&current_pin))
.map_err(|e| format!("Failed to set minimum PIN length: {:?}", e))?;
Ok(format!(
"Minimum PIN length successfully set to {}",
min_pin_length
))
}
+385
View File
@@ -0,0 +1,385 @@
//! Constants, enums, bitflags and data structures for FIDO2 protocol for pico-fido firmware.
use std::fmt;
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CtapCommand {
MakeCredential = 0x01,
GetAssertion = 0x02,
GetInfo = 0x04,
ClientPin = 0x06,
Reset = 0x07,
GetNextAssertion = 0x08,
CredentialMgmt = 0x0A,
Selection = 0x0B,
LargeBlobs = 0x0C,
Config = 0x0D,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum U2fCommand {
Register = 0x01,
Authenticate = 0x02,
Version = 0x03,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VendorCommand {
Backup = 0x01,
ManageSecurityEnvironment = 0x02,
Unlock = 0x03,
EnterpriseAttestation = 0x04,
PhysicalOptions = 0x05,
Memory = 0x06,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AuthenticateControl {
EnforceUserPresence = 0x03,
CheckOnly = 0x07,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ClientPinSubCommand {
GetPinRetries = 0x01,
GetKeyAgreement = 0x02,
SetPin = 0x03,
ChangePin = 0x04,
GetPinToken = 0x05,
GetPinUvAuthTokenUsingUvWithPermissions = 0x06,
GetUvRetries = 0x07,
GetPinUvAuthTokenUsingPinWithPermissions = 0x08,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MakeCredentialParam {
ClientDataHash = 0x01,
Rp = 0x02,
User = 0x03,
PubKeyCredParams = 0x04,
ExcludeList = 0x05,
Extensions = 0x06,
Options = 0x07,
PinUvAuthParam = 0x08,
PinUvAuthProtocol = 0x09,
EnterpriseAttestation = 0x0A,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GetAssertionParam {
RpId = 0x01,
ClientDataHash = 0x02,
AllowList = 0x03,
Extensions = 0x04,
Options = 0x05,
PinUvAuthParam = 0x06,
PinUvAuthProtocol = 0x07,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ClientPinParam {
PinUvAuthProtocol = 0x01,
SubCommand = 0x02,
KeyAgreement = 0x03,
PinUvAuthParam = 0x04,
NewPinEnc = 0x05,
PinHashEnc = 0x06,
Permissions = 0x09,
PermissionsRpId = 0x0A,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConfigParam {
SubCommand = 0x01,
SubCommandParams = 0x02,
PinUvAuthProtocol = 0x03,
PinUvAuthParam = 0x04,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConfigSubCommand {
EnableEnterpriseAttestation = 0x01,
ToggleAlwaysUv = 0x02,
SetMinPinLength = 0x03,
VendorPrototype = 0xFF,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VendorParam {
VendorCommand = 0x01,
VendorSubParams = 0x02,
PinUvAuthProtocol = 0x03,
PinUvAuthParam = 0x04,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VendorSubParam {
VendorParam = 0x01,
CoseKey = 0x02,
VendorParamInt = 0x03,
VendorParamText = 0x04,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConfigSubCommandParam {
NewMinPinLength = 0x01,
MinPinLengthRPIDs = 0x02,
ForceChangePin = 0x03,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VendorConfigCommand {
AuthEncryptionEnable,
AuthEncryptionDisable,
EnterpriseAttestationUpload,
PinComplexityPolicy,
PhysicalVidPid,
PhysicalLedBrightness,
PhysicalLedGpio,
PhysicalOptions,
}
impl VendorConfigCommand {
pub fn to_u64(&self) -> u64 {
match self {
Self::AuthEncryptionEnable => 0x03e43f56b34285e2,
Self::AuthEncryptionDisable => 0x1831a40f04a25ed9,
Self::EnterpriseAttestationUpload => 0x66f2a674c29a8dcf,
Self::PinComplexityPolicy => 0x6c07d70fe96c3897,
Self::PhysicalVidPid => 0x6fcb19b0cbe3acfa,
Self::PhysicalLedBrightness => 0x76a85945985d02fd,
Self::PhysicalLedGpio => 0x7b392a394de9f948,
Self::PhysicalOptions => 0x269f3b09eceb805f,
}
}
pub fn from_u64(val: u64) -> Option<Self> {
match val {
0x03e43f56b34285e2 => Some(Self::AuthEncryptionEnable),
0x1831a40f04a25ed9 => Some(Self::AuthEncryptionDisable),
0x66f2a674c29a8dcf => Some(Self::EnterpriseAttestationUpload),
0x6c07d70fe96c3897 => Some(Self::PinComplexityPolicy),
0x6fcb19b0cbe3acfa => Some(Self::PhysicalVidPid),
0x76a85945985d02fd => Some(Self::PhysicalLedBrightness),
0x7b392a394de9f948 => Some(Self::PhysicalLedGpio),
0x269f3b09eceb805f => Some(Self::PhysicalOptions),
_ => None,
}
}
}
impl fmt::Display for VendorConfigCommand {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::AuthEncryptionEnable => write!(f, "AuthEncryptionEnable"),
Self::AuthEncryptionDisable => write!(f, "AuthEncryptionDisable"),
Self::EnterpriseAttestationUpload => write!(f, "EnterpriseAttestationUpload"),
Self::PinComplexityPolicy => write!(f, "PinComplexityPolicy"),
Self::PhysicalVidPid => write!(f, "PhysicalVidPid"),
Self::PhysicalLedBrightness => write!(f, "PhysicalLedBrightness"),
Self::PhysicalLedGpio => write!(f, "PhysicalLedGpio"),
Self::PhysicalOptions => write!(f, "PhysicalOptions"),
}
}
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BackupSubCommand {
GetEncryptedBackup = 0x01,
RestoreEncryptedBackup = 0x02,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MseSubCommand {
KeyAgreement = 0x01,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EnterpriseAttestationSubCommand {
GenerateCsr = 0x01,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PhysicalOptionsSubCommand {
GetOptions = 0x01,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MemorySubCommand {
GetStats = 0x01,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MemoryResponseKey {
FreeSpace = 0x01,
UsedSpace = 0x02,
TotalSpace = 0x03,
NumFiles = 0x04,
FlashSize = 0x05,
}
bitflags::bitflags! {
pub struct PinUvAuthTokenPermissions: u8 {
const MAKE_CREDENTIAL = 0x01;
const GET_ASSERTION = 0x02;
const CREDENTIAL_MANAGEMENT = 0x04;
const BIO_ENROLLMENT = 0x08;
const LARGE_BLOB_WRITE = 0x10;
const AUTHENTICATOR_CONFIG = 0x20;
const PER_CREDENTIAL_MGMT_READONLY = 0x40;
}
}
bitflags::bitflags! {
pub struct AuthenticatorFlags: u8 {
const USER_PRESENT = 0x01;
const USER_VERIFIED = 0x04;
const ATTESTED_CREDENTIAL_DATA = 0x40;
const EXTENSION_DATA = 0x80;
}
}
bitflags::bitflags! {
pub struct AuthenticatorOptions: u8 {
const ENTERPRISE_ATTESTATION = 0x01;
const USER_VERIFICATION = 0x02;
}
}
#[repr(i32)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CoseAlgorithm {
ES256 = -7,
EdDSA = -8,
ESP256 = -9,
Ed25519 = -19,
ECDH_ES_HKDF_256 = -25,
ES384 = -35,
ES512 = -36,
ES256K = -47,
ESP384 = -51,
ESP512 = -52,
Ed448 = -53,
RS256 = -257,
RS384 = -258,
RS512 = -259,
ESB256 = -265,
ESB384 = -267,
ESB512 = -268,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CoseCurve {
P256 = 1,
P384 = 2,
P521 = 3,
X25519 = 4,
X448 = 5,
Ed25519 = 6,
Ed448 = 7,
P256K1 = 8,
BP256R1 = 9,
BP384R1 = 10,
BP512R1 = 11,
}
#[repr(i32)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CoseKeyParam {
Kty = 1,
Kid = 2,
Alg = 3,
KeyOps = 4,
BaseIV = 5,
Crv = -1,
X = -2,
Y = -3,
D = -4,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Ctap2Error {
Success = 0x00,
CborUnexpectedType = 0x11,
InvalidCbor = 0x12,
MissingParameter = 0x14,
LimitExceeded = 0x15,
FpDatabaseFull = 0x17,
LargeBlobStorageFull = 0x18,
CredentialExcluded = 0x19,
Processing = 0x21,
InvalidCredential = 0x22,
UserActionPending = 0x23,
OperationPending = 0x24,
NoOperations = 0x25,
UnsupportedAlgorithm = 0x26,
OperationDenied = 0x27,
KeyStoreFull = 0x28,
UnsupportedOption = 0x2B,
InvalidOption = 0x2C,
KeepaliveCancel = 0x2D,
NoCredentials = 0x2E,
UserActionTimeout = 0x2F,
NotAllowed = 0x30,
PinInvalid = 0x31,
PinBlocked = 0x32,
PinAuthInvalid = 0x33,
PinAuthBlocked = 0x34,
PinNotSet = 0x35,
PuatRequired = 0x36,
PinPolicyViolation = 0x37,
RequestTooLarge = 0x39,
ActionTimeout = 0x3A,
UpRequired = 0x3B,
UvBlocked = 0x3C,
IntegrityFailure = 0x3D,
InvalidSubcommand = 0x3E,
UvInvalid = 0x3F,
UnauthorizedPermission = 0x40,
}
pub const CTAP_VENDOR_CBOR_CMD: u8 = 0xC1;
pub const CTAP_VENDOR_CONFIG_CMD: u8 = 0xC2;
pub const CTAP_APPID_SIZE: usize = 32;
pub const CTAP_CHAL_SIZE: usize = 32;
pub const CTAP_EC_KEY_SIZE: usize = 32;
pub const CTAP_EC_POINT_SIZE: usize = 65;
pub const CTAP_MAX_KH_SIZE: usize = 128;
pub const KEY_HANDLE_LEN: usize = 64;
pub const CTAP_MAX_EC_SIG_SIZE: usize = 72;
pub const CTAP_CTR_SIZE: usize = 4;
pub const MAX_PIN_RETRIES: u8 = 8;
pub const MAX_CREDENTIAL_COUNT_IN_LIST: usize = 16;
pub const MAX_CRED_ID_LENGTH: usize = 1024;
pub const MAX_RESIDENT_CREDENTIALS: usize = 256;
pub const MAX_CREDBLOB_LENGTH: usize = 128;
pub const MAX_MSG_SIZE: usize = 1024;
pub const MAX_FRAGMENT_LENGTH: usize = MAX_MSG_SIZE - 64;
pub const MAX_LARGE_BLOB_SIZE: usize = 2048;
pub const AAGUID: [u8; 16] = [
0x89, 0xFB, 0x94, 0xB7, 0x06, 0xC9, 0x36, 0x73, 0x9B, 0x7E, 0x30, 0x52, 0x6D, 0x96, 0x81, 0x45,
];
+71
View File
@@ -0,0 +1,71 @@
pub mod constants;
use crate::types::FidoDeviceInfo;
use ctap_hid_fido2::{Cfg, FidoKeyHidFactory};
use std::collections::HashMap;
pub(crate) fn get_fido_info() -> Result<FidoDeviceInfo, String> {
let cfg = Cfg::init();
let device = FidoKeyHidFactory::create(&cfg)
.map_err(|_| "Could not connect to FIDO device. Is it plugged in?".to_string())?;
let info = device
.get_info()
.map_err(|e| format!("Error reading device info: {:?}", e))?;
let options_map: HashMap<String, bool> = info.options.into_iter().collect();
Ok(FidoDeviceInfo {
versions: info.versions,
extensions: info.extensions,
aaguid: hex::encode_upper(info.aaguid),
options: options_map,
max_msg_size: info.max_msg_size,
pin_protocols: info.pin_uv_auth_protocols,
min_pin_length: info.min_pin_length,
firmware_version: format!("0x{:X}", info.firmware_version),
})
}
pub(crate) fn change_fido_pin(
current_pin: Option<String>,
new_pin: String,
) -> Result<String, String> {
let cfg = Cfg::init();
let device = FidoKeyHidFactory::create(&cfg)
.map_err(|e| format!("Failed to connect to FIDO device: {:?}", e))?;
match current_pin {
Some(old) => {
device
.change_pin(&old, &new_pin)
.map_err(|e| format!("Failed to change PIN: {:?}", e))?;
Ok("PIN Changed Successfully".into())
}
None => {
device
.set_new_pin(&new_pin)
.map_err(|e| format!("Failed to set PIN: {:?}", e))?;
Ok("PIN Set Successfully".into())
}
}
}
pub(crate) fn set_min_pin_length(
current_pin: String,
min_pin_length: u8,
) -> Result<String, String> {
let cfg = Cfg::init();
let device = FidoKeyHidFactory::create(&cfg)
.map_err(|e| format!("Failed to connect to FIDO device: {:?}", e))?;
device
.set_min_pin_length(min_pin_length, Some(&current_pin))
.map_err(|e| format!("Failed to set minimum PIN length: {:?}", e))?;
Ok(format!(
"Minimum PIN length successfully set to {}",
min_pin_length
))
}
+43
View File
@@ -0,0 +1,43 @@
//! Tauri Commands to interact with the pico-fido firmware via rescue and fido protocols.
use crate::{fido, rescue, types::*};
#[tauri::command]
pub fn read_device_details() -> Result<FullDeviceStatus, AppError> {
rescue::read_device_details()
}
#[tauri::command]
pub fn write_config(config: AppConfigInput) -> Result<String, AppError> {
rescue::write_config(config)
}
#[tauri::command]
pub fn enable_secure_boot(lock: bool) -> Result<String, AppError> {
rescue::enable_secure_boot(lock)
}
#[tauri::command]
pub(crate) fn get_fido_info() -> Result<FidoDeviceInfo, String> {
fido::get_fido_info()
}
#[tauri::command]
pub(crate) fn change_fido_pin(
current_pin: Option<String>,
new_pin: String,
) -> Result<String, String> {
fido::change_fido_pin(current_pin, new_pin)
}
#[tauri::command]
pub(crate) fn set_min_pin_length(
current_pin: String,
min_pin_length: u8,
) -> Result<String, String> {
fido::set_min_pin_length(current_pin, min_pin_length)
}
#[tauri::command]
pub fn reboot(to_bootsel: bool) -> Result<String, AppError> {
rescue::reboot_device(to_bootsel)
}
+18 -17
View File
@@ -1,27 +1,28 @@
// use tauri::State;
mod types;
mod fido;
mod io;
mod logging;
mod rescue;
mod types;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
logging::logger_init();
log::info!("Initialisng PicoForge...");
logging::logger_init();
log::info!("Initialisng PicoForge...");
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![
rescue::read_device_details,
rescue::get_device_info,
rescue::write_config,
fido::get_fido_info,
fido::change_fido_pin,
fido::set_min_pin_length,
rescue::enable_secure_boot
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![
io::read_device_details,
io::write_config,
io::get_fido_info,
io::change_fido_pin,
io::set_min_pin_length,
io::enable_secure_boot,
io::reboot
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
+1 -1
View File
@@ -76,4 +76,4 @@ pub fn logger_init() {
.unwrap();
log4rs::init_config(config).unwrap();
}
}
+1 -1
View File
@@ -2,5 +2,5 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
picoforge_lib::run()
picoforge_lib::run()
}
-362
View File
@@ -1,362 +0,0 @@
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
use pcsc::{Context, Protocols, Scope, ShareMode};
use std::io::Cursor;
use log;
use crate::types::*;
// --- Helper Functions ---
/// Connects to the first available reader and selects the Rescue Applet
fn connect_and_select() -> Result<(pcsc::Card, Vec<u8>), AppError> {
let ctx = Context::establish(Scope::User)?;
let mut readers_buf = [0; 2048];
let mut readers = ctx.list_readers(&mut readers_buf)?;
// Use the first reader found
let reader = readers
.next()
.ok_or_else(|| {
log::error!("No Smart Card Reader found");
AppError::Device("No Smart Card Reader found.".into())
})?;
let card = ctx.connect(reader, ShareMode::Shared, Protocols::ANY)?;
// Select Applet APDU: 00 A4 04 04 [Len] [AID]
let mut apdu = vec![0x00, 0xA4, 0x04, 0x04, RESCUE_AID.len() as u8];
apdu.extend_from_slice(RESCUE_AID);
let mut rx_buf = [0; 256];
let rx = card.transmit(&apdu, &mut rx_buf)?;
// Check Success (0x90 0x00)
if !rx.ends_with(&[0x90, 0x00]) {
log::error!("Rescue Applet not found on the device!");
return Err(AppError::Device(
// There is no such mode as fido, i tink the rescue applet stays active and at the same time fido mode works?
// Need to study this more.
"Rescue Applet not found on device. Is it in FIDO mode?".into(),
));
}
log::info!("Successfully connected to Rescue Applet");
Ok((card, rx.to_vec()))
}
// --- Tauri Commands ---
#[tauri::command]
pub fn read_device_details() -> Result<FullDeviceStatus, AppError> {
log::info!("Reading full device details");
let (card, select_resp) = connect_and_select()?;
// 1. Parse Basic Info (Same as your get_device_info)
if select_resp.len() < 14 {
return Err(AppError::Device("Invalid select response".into()));
}
let version_major = select_resp[2];
let version_minor = select_resp[3];
let serial_str = hex::encode_upper(&select_resp[4..12]);
// 2. Read Flash Info (APDU: 80 1E 02 00 00)
let mut rx_buf = [0; 256];
let rx_flash = card.transmit(&[0x80, INS_READ, 0x02, 0x00, 0x00], &mut rx_buf)?;
if !rx_flash.ends_with(&[0x90, 0x00]) {
return Err(AppError::Device("Failed to read flash".into()));
}
let mut rdr = Cursor::new(&rx_flash[..rx_flash.len() - 2]);
let _free = rdr.read_u32::<BigEndian>().unwrap_or(0);
let used = rdr.read_u32::<BigEndian>().unwrap_or(0);
let total = rdr.read_u32::<BigEndian>().unwrap_or(0);
// 3. Read Secure Boot Status (APDU: 80 1E 03 00 00) -> [Enabled(1), Locked(1), Key(1)...]
let rx_secure = card.transmit(&[0x80, INS_READ, 0x03, 0x00, 0x00], &mut rx_buf)?;
let (sb_enabled, sb_locked) = if rx_secure.ends_with(&[0x90, 0x00]) && rx_secure.len() >= 4 {
(rx_secure[0] != 0, rx_secure[1] != 0)
} else {
(false, false)
};
// 4. Read PHY Config (APDU: 80 1E 01 01 00) -> TLV Data
let rx_phy = card.transmit(&[0x80, INS_READ, 0x01, 0x01, 0x00], &mut rx_buf)?;
if !rx_phy.ends_with(&[0x90, 0x00]) {
return Err(AppError::Device("Failed to read config".into()));
}
// Parse TLV
let mut config = AppConfig::default();
let data = &rx_phy[..rx_phy.len() - 2];
let mut i = 0;
while i < data.len() {
if i + 2 > data.len() {
break;
}
let tag = data[i];
let len = data[i + 1] as usize;
i += 2;
if i + len > data.len() {
break;
}
let val = &data[i..i + len];
match tag {
TAG_VIDPID => {
if val.len() == 4 {
let vid = u16::from_be_bytes([val[0], val[1]]);
let pid = u16::from_be_bytes([val[2], val[3]]);
config.vid = format!("{:04X}", vid);
config.pid = format!("{:04X}", pid);
}
}
TAG_LED_GPIO => {
if !val.is_empty() {
config.led_gpio = val[0];
}
}
TAG_LED_BRIGHTNESS => {
if !val.is_empty() {
config.led_brightness = val[0];
}
}
TAG_UP_BTN => {
if !val.is_empty() {
config.touch_timeout = val[0];
}
}
TAG_USB_PRODUCT => {
// Remove null terminator if present
let s = std::str::from_utf8(val)
.unwrap_or("")
.trim_matches(char::from(0));
config.product_name = s.to_string();
}
TAG_OPTS => {
if val.len() >= 2 {
let opts = u16::from_be_bytes([val[0], val[1]]);
config.led_dimmable = (opts & OPT_LED_DIMMABLE) != 0;
config.power_cycle_on_reset = (opts & OPT_DISABLE_POWER_RESET) == 0;
config.led_steady = (opts & OPT_LED_STEADY) != 0;
}
}
TAG_CURVES => {
if val.len() >= 4 {
let curves = u32::from_be_bytes([val[0], val[1], val[2], val[3]]);
config.enable_secp256k1 = (curves & CURVE_SECP256K1) != 0;
}
}
TAG_LED_DRIVER => {
if !val.is_empty() {
config.led_driver = Some(val[0]);
}
}
_ => {}
}
i += len;
}
log::info!("Successfully read device details - Serial: {}, Firmware: {}.{}", serial_str, version_major, version_minor);
Ok(FullDeviceStatus {
info: DeviceInfo {
serial: serial_str,
flash_used: used / 1024,
flash_total: total / 1024,
firmware_version: format!("{}.{}", version_major, version_minor),
},
config,
secure_boot: sb_enabled,
secure_lock: sb_locked,
})
}
#[tauri::command]
pub fn get_device_info() -> Result<DeviceInfo, AppError> {
let (card, select_resp) = connect_and_select()?;
// 1. Parse Version & Serial from Select Response (see src/rescue.c)
// Response: [MCU, PROD, VER_MAJ, VER_MIN, SERIAL(8 bytes)..., 90, 00]
if select_resp.len() < 14 {
return Err(AppError::Device("Invalid response from device".into()));
}
let version_major = select_resp[2];
let version_minor = select_resp[3];
let serial_bytes = &select_resp[4..12];
let serial_str = hex::encode_upper(serial_bytes);
// 2. Read Flash Info
// APDU: 80 1E 02 00 00 (Read Flash Info)
let apdu_read = [0x80, INS_READ, 0x02, 0x00, 0x00];
let mut rx_buf = [0; 256];
let rx = card.transmit(&apdu_read, &mut rx_buf)?;
if !rx.ends_with(&[0x90, 0x00]) {
return Err(AppError::Device("Failed to read flash info".into()));
}
// Response: [Free(4), Used(4), Total(4), Files(4), Size(4), 90, 00]
// We need 'Used' (index 4) and 'Total' (index 8)
// Data is Big Endian
let mut rdr = Cursor::new(&rx[..rx.len() - 2]);
let _free = rdr.read_u32::<BigEndian>().unwrap_or(0);
let used = rdr.read_u32::<BigEndian>().unwrap_or(0);
let total = rdr.read_u32::<BigEndian>().unwrap_or(0);
Ok(DeviceInfo {
serial: serial_str,
flash_used: used / 1024, // Convert to KB
flash_total: total / 1024,
firmware_version: format!("{}.{}", version_major, version_minor),
})
}
#[tauri::command]
pub fn write_config(config: AppConfigInput) -> Result<String, AppError> {
log::info!("Writing configuration to device");
log::debug!("Config input: {:?}", config);
// 1. Construct TLV Blob
let mut tlv = Vec::new();
// VID:PID (Tag 0x00)
if let (Some(vid_str), Some(pid_str)) = (&config.vid, &config.pid) {
let vid =
u16::from_str_radix(vid_str, 16).map_err(|_| AppError::Io("Invalid VID".into()))?;
let pid =
u16::from_str_radix(pid_str, 16).map_err(|_| AppError::Io("Invalid PID".into()))?;
tlv.push(TAG_VIDPID);
tlv.push(0x04);
tlv.write_u16::<BigEndian>(vid).unwrap();
tlv.write_u16::<BigEndian>(pid).unwrap();
}
// LED GPIO (Tag 0x04)
if let Some(val) = config.led_gpio {
tlv.push(TAG_LED_GPIO);
tlv.push(0x01);
tlv.push(val);
}
// LED Brightness (Tag 0x05)
if let Some(val) = config.led_brightness {
tlv.push(TAG_LED_BRIGHTNESS);
tlv.push(0x01);
tlv.push(val);
}
// Touch Timeout (Tag 0x08)
if let Some(val) = config.touch_timeout {
tlv.push(TAG_UP_BTN);
tlv.push(0x01);
tlv.push(val);
}
// Options (Tag 0x06)
if let (Some(dim), Some(cycle), Some(steady)) = (
config.led_dimmable,
config.power_cycle_on_reset,
config.led_steady,
) {
let mut opts: u16 = 0;
if dim {
opts |= OPT_LED_DIMMABLE;
}
if !cycle {
opts |= OPT_DISABLE_POWER_RESET;
}
if steady {
opts |= OPT_LED_STEADY;
}
tlv.push(TAG_OPTS);
tlv.push(0x02);
tlv.write_u16::<BigEndian>(opts).unwrap();
}
// Curves (Tag 0x0A)
if let Some(enabled) = config.enable_secp256k1 {
let mut curves: u32 = 0;
if enabled {
curves |= CURVE_SECP256K1;
}
tlv.push(TAG_CURVES);
tlv.push(0x04);
tlv.write_u32::<BigEndian>(curves).unwrap();
}
// LED Driver (Tag 0x0C)
if let Some(val) = config.led_driver {
tlv.push(TAG_LED_DRIVER);
tlv.push(0x01);
tlv.push(val);
}
// Product Name (Tag 0x09)
if let Some(name) = config.product_name {
if !name.is_empty() {
let name_bytes = name.as_bytes();
let len = name_bytes.len() + 1;
if len > 32 {
return Err(AppError::Io("Product name too long".into()));
}
tlv.push(TAG_USB_PRODUCT);
tlv.push(len as u8);
tlv.extend_from_slice(name_bytes);
tlv.push(0x00);
}
}
// 2. Connect and Send
if tlv.is_empty() {
log::warn!("No configuration changes to apply");
return Ok("No changes to apply".into());
}
log::debug!("TLV payload size: {} bytes", tlv.len());
let (card, _) = connect_and_select()?;
// APDU: 80 1C 01 00 [Lc] [Data]
let mut apdu = vec![0x80, INS_WRITE, 0x01, 0x00, tlv.len() as u8];
apdu.extend_from_slice(&tlv);
let mut rx_buf = [0; 256];
let rx = card.transmit(&apdu, &mut rx_buf)?;
if rx.ends_with(&[0x90, 0x00]) {
log::info!("Configuration applied successfully");
Ok("Configuration Applied Successfully".into())
} else {
log::error!("Configuration write failed: {:02X?}", rx);
Err(AppError::Device(format!("Write failed: {:02X?}", rx)))
}
}
/// UNSTABLE! (WIP)
#[tauri::command]
pub fn enable_secure_boot(lock: bool) -> Result<String, AppError> {
let (card, _) = connect_and_select()?;
// APDU: 80 1D [KeyIndex] [LockBool] 00
// KeyIndex = 0 (Default), LockBool = 1 if true
let lock_byte = if lock { 0x01 } else { 0x00 };
let apdu = [0x80, INS_SECURE, 0x00, lock_byte, 0x00];
let mut rx_buf = [0; 256];
let rx = card.transmit(&apdu, &mut rx_buf)?;
if rx.ends_with(&[0x90, 0x00]) {
Ok("Secure Boot Enabled".into())
} else {
Err(AppError::Device(format!("Secure Boot failed: {:02X?}", rx)))
}
}
+130
View File
@@ -0,0 +1,130 @@
//! Constants, enums, bitflags and data structures for Rescue Application for pico-fido firmware.
// use serde::{Deserialize, Serialize};
// use std::fmt;
// --- 1. ISO 7816-4 Standard Constants ---
/// Class Byte (CLA)
pub const APDU_CLA_ISO: u8 = 0x00; // Standard ISO commands
pub const APDU_CLA_PROPRIETARY: u8 = 0x80; // Custom/Rescue commands
/// Instruction (INS) for Selection
pub const APDU_INS_SELECT: u8 = 0xA4;
/// Selection Parameters (P1, P2)
pub const APDU_P1_SELECT_BY_DF_NAME: u8 = 0x04;
pub const APDU_P2_RETURN_FCI: u8 = 0x04; // Return File Control Info
/// Status Words (SW1 SW2)
pub const SW_SUCCESS: [u8; 2] = [0x90, 0x00];
// --- 2. Rescue Applet Constants ---
// The Rescue Application ID (AID) from src/rescue.c
pub const RESCUE_AID: &[u8] = &[0xA0, 0x58, 0x3F, 0xC1, 0x9B, 0x7E, 0x4F, 0x21];
// APDU Instructions
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RescueInstruction {
KeyDevSign = 0x10,
Write = 0x1C,
Secure = 0x1D,
Read = 0x1E,
Reboot = 0x1F,
}
/// P1 Parameters for RescueInstruction::Read (0x1E)
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReadParam {
PhyConfig = 0x01,
FlashInfo = 0x02,
SecureBootStatus = 0x03,
}
/// P1 Parameters for WRITE (0x1C)
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WriteParam {
PhyConfig = 0x01,
}
/// P1 Parameters for RescueInstruction::KeyDevSign (0x10)
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SignParam {
SignData = 0x01,
GetPublicKey = 0x02,
UploadCert = 0x03,
}
/// P1 Parameters for RescueInstruction::Reboot (0x1F)
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RebootParam {
Normal = 0x00,
Bootsel = 0x01,
}
/// P2 Parameters for SECURE (0x1D)
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SecureLockParam {
#[default]
Unlock = 0x00,
Lock = 0x01,
}
/// Default P2 value when not used
pub const P2_UNUSED: u8 = 0x00;
// --- 3. PHY Configuration Tags & Flags ---
// PHY Tags from src/fs/phy.h
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PhyTag {
VidPid = 0x00,
LedGpio = 0x04,
LedBrightness = 0x05,
Opts = 0x06,
PresenceTimeout = 0x08, // Previously TAG_UP_BTN
UsbProduct = 0x09,
Curves = 0x0A,
LedDriver = 0x0C,
}
impl PhyTag {
/// Helper to convert raw u8 from device back to Enum
pub fn from_u8(val: u8) -> Option<Self> {
match val {
0x00 => Some(Self::VidPid),
0x04 => Some(Self::LedGpio),
0x05 => Some(Self::LedBrightness),
0x06 => Some(Self::Opts),
0x08 => Some(Self::PresenceTimeout),
0x09 => Some(Self::UsbProduct),
0x0A => Some(Self::Curves),
0x0C => Some(Self::LedDriver),
_ => None,
}
}
}
bitflags::bitflags! {
/// Configuration options for TAG_OPTS (Tag 0x06)
pub struct RescueOptions: u16 {
const LED_DIMMABLE = 0x02;
const DISABLE_POWER_RESET = 0x04;
const LED_STEADY = 0x08;
}
}
bitflags::bitflags! {
/// Enabled curves for TAG_CURVES (Tag 0x0A)
pub struct RescueCurves: u32 {
const SECP256K1 = 0x08;
}
}
+399
View File
@@ -0,0 +1,399 @@
pub mod constants;
use crate::{rescue::constants::*, types::*};
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
use log;
use pcsc::{Context, Protocols, Scope, ShareMode};
use std::io::Cursor;
/// Connects to the first available reader and selects the Rescue Applet
fn connect_and_select() -> Result<(pcsc::Card, Vec<u8>), AppError> {
let ctx = Context::establish(Scope::User)?;
let mut readers_buf = [0; 2048];
let mut readers = ctx.list_readers(&mut readers_buf)?;
// Use the first reader found
let reader = readers.next().ok_or_else(|| {
log::error!("No Smart Card Reader found");
AppError::Device("No Smart Card Reader found.".into())
})?;
let card = ctx.connect(reader, ShareMode::Shared, Protocols::ANY)?;
// Select Applet APDU: 00 A4 04 04 [Len] [AID]
let mut apdu = vec![
APDU_CLA_ISO,
APDU_INS_SELECT,
APDU_P1_SELECT_BY_DF_NAME,
APDU_P2_RETURN_FCI,
RESCUE_AID.len() as u8,
];
apdu.extend_from_slice(RESCUE_AID);
let mut rx_buf = [0; 256];
let rx = card.transmit(&apdu, &mut rx_buf)?;
// Check Success (0x90 0x00)
if !rx.ends_with(&[0x90, 0x00]) {
log::error!("Rescue Applet not found on the device!");
return Err(AppError::Device(
// There is no such mode as fido, i tink the rescue applet stays active and at the same time fido mode works?
// Need to study this more.
"Rescue Applet not found on device. Is it in FIDO mode?".into(),
));
}
log::info!("Successfully connected to Rescue Applet");
Ok((card, rx.to_vec()))
}
pub fn read_device_details() -> Result<FullDeviceStatus, AppError> {
log::info!("Reading full device details");
let (card, select_resp) = connect_and_select()?;
if select_resp.len() < 14 {
return Err(AppError::Device("Invalid select response".into()));
}
let version_major = select_resp[2];
let version_minor = select_resp[3];
let serial_str = hex::encode_upper(&select_resp[4..12]);
// 2. Read Flash Info
let mut rx_buf = [0; 256];
let rx_flash = card.transmit(
&[
APDU_CLA_PROPRIETARY,
RescueInstruction::Read as u8,
ReadParam::FlashInfo as u8,
P2_UNUSED,
0x00, // Le (Expected Length, 0 = Max)
],
&mut rx_buf,
)?;
if !rx_flash.ends_with(&SW_SUCCESS) {
return Err(AppError::Device("Failed to read flash".into()));
}
let mut rdr = Cursor::new(&rx_flash[..rx_flash.len() - 2]);
let _free = rdr.read_u32::<BigEndian>().unwrap_or(0);
let used = rdr.read_u32::<BigEndian>().unwrap_or(0);
let total = rdr.read_u32::<BigEndian>().unwrap_or(0);
// NOTE: captured but currently unused variables
let _nfiles = rdr.read_u32::<BigEndian>().unwrap_or(0);
let _chip_size = rdr.read_u32::<BigEndian>().unwrap_or(0);
// --- Read Secure Boot Status ---
let rx_secure = card.transmit(
&[
APDU_CLA_PROPRIETARY,
RescueInstruction::Read as u8,
ReadParam::SecureBootStatus as u8,
P2_UNUSED,
0x00,
],
&mut rx_buf,
)?;
let (sb_enabled, sb_locked) = if rx_secure.ends_with(&[0x90, 0x00]) && rx_secure.len() >= 4 {
(rx_secure[0] != 0, rx_secure[1] != 0)
} else {
(false, false)
};
// --- Read PHY Config ---
let rx_phy = card.transmit(
&[
APDU_CLA_PROPRIETARY,
RescueInstruction::Read as u8,
ReadParam::PhyConfig as u8,
0x01, // Note: Firmware uses P2=1 here sometimes, leaving as 1 to be safe
0x00,
],
&mut rx_buf,
)?;
if !rx_phy.ends_with(&[0x90, 0x00]) {
return Err(AppError::Device("Failed to read config".into()));
}
// Parse TLV
let mut config = AppConfig::default();
let data = &rx_phy[..rx_phy.len() - 2];
let mut i = 0;
while i < data.len() {
if i + 2 > data.len() {
break;
}
let tag_byte = data[i];
let len = data[i + 1] as usize;
i += 2;
if i + len > data.len() {
break;
}
let val = &data[i..i + len];
if let Some(tag) = PhyTag::from_u8(tag_byte) {
match tag {
PhyTag::VidPid => {
if val.len() == 4 {
let vid = u16::from_be_bytes([val[0], val[1]]);
let pid = u16::from_be_bytes([val[2], val[3]]);
config.vid = format!("{:04X}", vid);
config.pid = format!("{:04X}", pid);
}
}
PhyTag::LedGpio => {
if !val.is_empty() {
config.led_gpio = val[0];
}
}
PhyTag::LedBrightness => {
if !val.is_empty() {
config.led_brightness = val[0];
}
}
PhyTag::PresenceTimeout => {
if !val.is_empty() {
config.touch_timeout = val[0];
}
}
PhyTag::UsbProduct => {
// Remove null terminator if present
let s = std::str::from_utf8(val)
.unwrap_or("")
.trim_matches(char::from(0));
config.product_name = s.to_string();
}
PhyTag::Opts => {
if val.len() >= 2 {
let opts_val = u16::from_be_bytes([val[0], val[1]]);
let opts = RescueOptions::from_bits_truncate(opts_val);
config.led_dimmable = opts.contains(RescueOptions::LED_DIMMABLE);
config.power_cycle_on_reset =
!opts.contains(RescueOptions::DISABLE_POWER_RESET);
config.led_steady = opts.contains(RescueOptions::LED_STEADY);
}
}
PhyTag::Curves => {
if val.len() >= 4 {
let curves_val = u32::from_be_bytes([val[0], val[1], val[2], val[3]]);
let curves = RescueCurves::from_bits_truncate(curves_val);
config.enable_secp256k1 = curves.contains(RescueCurves::SECP256K1);
}
}
PhyTag::LedDriver => {
if !val.is_empty() {
config.led_driver = Some(val[0]);
}
}
}
}
i += len;
}
log::info!(
"Successfully read device details - Serial: {}, Firmware: {}.{}",
serial_str,
version_major,
version_minor
);
Ok(FullDeviceStatus {
info: DeviceInfo {
serial: serial_str,
flash_used: used / 1024,
flash_total: total / 1024,
firmware_version: format!("{}.{}", version_major, version_minor),
},
config,
secure_boot: sb_enabled,
secure_lock: sb_locked,
})
}
pub fn write_config(config: AppConfigInput) -> Result<String, AppError> {
log::info!("Writing configuration to device");
log::debug!("Config input: {:?}", config);
// 1. Construct TLV Blob
let mut tlv = Vec::new();
// VID:PID (Tag 0x00)
if let (Some(vid_str), Some(pid_str)) = (&config.vid, &config.pid) {
let vid =
u16::from_str_radix(vid_str, 16).map_err(|_| AppError::Io("Invalid VID".into()))?;
let pid =
u16::from_str_radix(pid_str, 16).map_err(|_| AppError::Io("Invalid PID".into()))?;
tlv.push(PhyTag::VidPid as u8);
tlv.push(0x04);
tlv.write_u16::<BigEndian>(vid).unwrap();
tlv.write_u16::<BigEndian>(pid).unwrap();
}
// LED GPIO (Tag 0x04)
if let Some(val) = config.led_gpio {
tlv.push(PhyTag::LedGpio as u8);
tlv.push(0x01);
tlv.push(val);
}
// LED Brightness (Tag 0x05)
if let Some(val) = config.led_brightness {
tlv.push(PhyTag::LedBrightness as u8);
tlv.push(0x01);
tlv.push(val);
}
// Touch Timeout (Tag 0x08)
if let Some(val) = config.touch_timeout {
tlv.push(PhyTag::PresenceTimeout as u8);
tlv.push(0x01);
tlv.push(val);
}
// Options
if let (Some(dim), Some(cycle), Some(steady)) = (
config.led_dimmable,
config.power_cycle_on_reset,
config.led_steady,
) {
let mut opts = RescueOptions::empty();
if dim {
opts.insert(RescueOptions::LED_DIMMABLE);
}
if !cycle {
opts.insert(RescueOptions::DISABLE_POWER_RESET);
}
if steady {
opts.insert(RescueOptions::LED_STEADY);
}
tlv.push(PhyTag::Opts as u8);
tlv.push(0x02);
tlv.write_u16::<BigEndian>(opts.bits()).unwrap();
}
// Curves
if let Some(enabled) = config.enable_secp256k1 {
let mut curves = RescueCurves::empty();
if enabled {
curves.insert(RescueCurves::SECP256K1);
}
tlv.push(PhyTag::Curves as u8);
tlv.push(0x04);
tlv.write_u32::<BigEndian>(curves.bits()).unwrap();
}
// LED Driver (Tag 0x0C)
if let Some(val) = config.led_driver {
tlv.push(PhyTag::LedDriver as u8);
tlv.push(0x01);
tlv.push(val);
}
// Product Name (Tag 0x09)
if let Some(name) = config.product_name {
if !name.is_empty() {
let name_bytes = name.as_bytes();
let len = name_bytes.len() + 1;
if len > 32 {
return Err(AppError::Io("Product name too long".into()));
}
tlv.push(PhyTag::UsbProduct as u8);
tlv.push(len as u8);
tlv.extend_from_slice(name_bytes);
tlv.push(0x00); // Null terminator
}
}
// 2. Connect and Send
if tlv.is_empty() {
log::warn!("No configuration changes to apply");
return Ok("No changes to apply".into());
}
log::debug!("TLV payload size: {} bytes", tlv.len());
let (card, _) = connect_and_select()?;
// APDU: 80 1C 01 00 [Lc] [Data]
let mut apdu = vec![
APDU_CLA_PROPRIETARY,
RescueInstruction::Write as u8,
WriteParam::PhyConfig as u8,
P2_UNUSED,
tlv.len() as u8, // Lc
];
apdu.extend_from_slice(&tlv);
let mut rx_buf = [0; 256];
let rx = card.transmit(&apdu, &mut rx_buf)?;
if rx.ends_with(&[0x90, 0x00]) {
log::info!("Configuration applied successfully");
Ok("Configuration Applied Successfully".into())
} else {
log::error!("Configuration write failed: {:02X?}", rx);
Err(AppError::Device(format!("Write failed: {:02X?}", rx)))
}
}
pub fn reboot_device(to_bootsel: bool) -> Result<String, AppError> {
let (card, _) = connect_and_select()?;
let param = if to_bootsel {
RebootParam::Bootsel
} else {
RebootParam::Normal
};
let apdu = [
APDU_CLA_PROPRIETARY,
RescueInstruction::Reboot as u8,
param as u8,
P2_UNUSED,
0x00,
];
let mut rx_buf = [0; 256];
let rx = card.transmit(&apdu, &mut rx_buf)?;
if rx.ends_with(&SW_SUCCESS) {
Ok("Reboot command sent".into())
} else {
Err(AppError::Device(format!("Reboot failed: {:02X?}", rx)))
}
}
/// UNSTABLE! (WIP)
pub fn enable_secure_boot(lock: bool) -> Result<String, AppError> {
let (card, _) = connect_and_select()?;
// APDU: 80 1D [KeyIndex] [LockBool] 00
// KeyIndex = 0 (Default), LockBool = 1 if true
let lock_byte = if lock { 0x01 } else { 0x00 };
let apdu = [
APDU_CLA_PROPRIETARY,
RescueInstruction::Secure as u8,
0x00, // Boot Key Index (0 = Default)
lock_byte as u8,
0x00,
];
let mut rx_buf = [0; 256];
let rx = card.transmit(&apdu, &mut rx_buf)?;
if rx.ends_with(&[0x90, 0x00]) {
Ok("Secure Boot Enabled".into())
} else {
Err(AppError::Device(format!("Secure Boot failed: {:02X?}", rx)))
}
}
+54 -83
View File
@@ -1,117 +1,88 @@
use serde::{Deserialize, Serialize};
// --- Constants ---
// The Rescue Application ID (AID) from src/rescue.c
pub const RESCUE_AID: &[u8] = &[0xA0, 0x58, 0x3F, 0xC1, 0x9B, 0x7E, 0x4F, 0x21];
// APDU Instructions
pub const INS_WRITE: u8 = 0x1C;
pub const INS_SECURE: u8 = 0x1D;
pub const INS_READ: u8 = 0x1E;
// PHY Tags from src/fs/phy.h
pub const TAG_VIDPID: u8 = 0x00;
pub const TAG_LED_GPIO: u8 = 0x04;
pub const TAG_LED_BRIGHTNESS: u8 = 0x05;
pub const TAG_OPTS: u8 = 0x06;
pub const TAG_UP_BTN: u8 = 0x08; // Presence Button Timeout
pub const TAG_USB_PRODUCT: u8 = 0x09;
pub const TAG_CURVES: u8 = 0x0A;
pub const TAG_LED_DRIVER: u8 = 0x0C;
// Bitmasks for TAG_OPTS
pub const OPT_LED_DIMMABLE: u16 = 0x02;
pub const OPT_DISABLE_POWER_RESET: u16 = 0x04;
pub const OPT_LED_STEADY: u16 = 0x08;
// Bitmasks for TAG_CURVES
pub const CURVE_SECP256K1: u32 = 0x08;
// --- Data Structures ---
#[derive(Serialize)]
pub struct DeviceInfo {
pub serial: String,
pub flash_used: u32,
pub flash_total: u32,
pub firmware_version: String,
pub serial: String,
pub flash_used: u32,
pub flash_total: u32,
pub firmware_version: String,
}
#[derive(Serialize, Deserialize, Debug, Default)]
pub struct AppConfig {
pub vid: String,
pub pid: String,
pub product_name: String,
pub led_gpio: u8,
pub led_brightness: u8,
pub touch_timeout: u8,
#[serde(skip_serializing_if = "Option::is_none")]
pub led_driver: Option<u8>,
// New Options
pub led_dimmable: bool,
pub power_cycle_on_reset: bool,
pub led_steady: bool,
pub enable_secp256k1: bool,
pub vid: String,
pub pid: String,
pub product_name: String,
pub led_gpio: u8,
pub led_brightness: u8,
pub touch_timeout: u8,
#[serde(skip_serializing_if = "Option::is_none")]
pub led_driver: Option<u8>,
// New Options
pub led_dimmable: bool,
pub power_cycle_on_reset: bool,
pub led_steady: bool,
pub enable_secp256k1: bool,
}
#[derive(Deserialize, Debug)]
pub struct AppConfigInput {
pub vid: Option<String>,
pub pid: Option<String>,
pub product_name: Option<String>,
pub led_gpio: Option<u8>,
pub led_brightness: Option<u8>,
pub touch_timeout: Option<u8>,
pub led_driver: Option<u8>,
pub led_dimmable: Option<bool>,
pub power_cycle_on_reset: Option<bool>,
pub led_steady: Option<bool>,
pub enable_secp256k1: Option<bool>,
pub vid: Option<String>,
pub pid: Option<String>,
pub product_name: Option<String>,
pub led_gpio: Option<u8>,
pub led_brightness: Option<u8>,
pub touch_timeout: Option<u8>,
pub led_driver: Option<u8>,
pub led_dimmable: Option<bool>,
pub power_cycle_on_reset: Option<bool>,
pub led_steady: Option<bool>,
pub enable_secp256k1: Option<bool>,
}
#[derive(Serialize)]
pub struct FullDeviceStatus {
pub info: DeviceInfo,
pub config: AppConfig,
pub secure_boot: bool,
pub secure_lock: bool,
pub info: DeviceInfo,
pub config: AppConfig,
pub secure_boot: bool,
pub secure_lock: bool,
}
// Fido stuff:
#[derive(Serialize)]
pub struct FidoDeviceInfo {
pub versions: Vec<String>,
pub extensions: Vec<String>,
pub aaguid: String,
pub options: std::collections::HashMap<String, bool>,
pub max_msg_size: i32,
pub pin_protocols: Vec<u32>,
// pub remaining_disc_creds: u32,
pub min_pin_length: u32,
pub firmware_version: String,
pub versions: Vec<String>,
pub extensions: Vec<String>,
pub aaguid: String,
pub options: std::collections::HashMap<String, bool>,
pub max_msg_size: i32,
pub pin_protocols: Vec<u32>,
// pub remaining_disc_creds: u32,
pub min_pin_length: u32,
pub firmware_version: String,
}
// Error stuff:
#[derive(Debug, thiserror::Error)]
pub enum AppError {
#[error("PCSC Error: {0}")]
Pcsc(#[from] pcsc::Error),
#[error("IO/Hex Error: {0}")]
Io(String),
#[error("Device Error: {0}")]
Device(String),
#[error("PCSC Error: {0}")]
Pcsc(#[from] pcsc::Error),
#[error("IO/Hex Error: {0}")]
Io(String),
#[error("Device Error: {0}")]
Device(String),
}
// Allow error to be serialized to string for Tauri
impl serde::Serialize for AppError {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
+3 -5
View File
@@ -75,10 +75,7 @@ class DeviceManager {
};
if (!this.connected) {
logger.add(
`Device Connected! Serial: ${this.info.serial}, FW: v${this.info.firmwareVersion}`,
"success",
);
logger.add(`Device Connected! Serial: ${this.info.serial}, FW: v${this.info.firmwareVersion}`, "success");
}
this.connected = true;
} catch (err) {
@@ -128,7 +125,8 @@ class DeviceManager {
logger.add(`Queuing change: Timeout -> ${this.config.touchTimeout}`, "info");
}
const optionsChanged = this.config.ledDimmable !== this.#originalConfig.ledDimmable ||
const optionsChanged =
this.config.ledDimmable !== this.#originalConfig.ledDimmable ||
this.config.powerCycleOnReset !== this.#originalConfig.powerCycleOnReset ||
this.config.ledSteady !== this.#originalConfig.ledSteady;
+2 -2
View File
@@ -1,6 +1,6 @@
<script>
let { children } = $props();
import "../app.css";
let { children } = $props();
import "../app.css";
</script>
{@render children()}

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