Merge branch 'librekeys:main' into main

This commit is contained in:
Lab-8916100448256
2026-01-21 19:23:32 +01:00
committed by GitHub
15 changed files with 468 additions and 89 deletions
+29 -31
View File
@@ -5,18 +5,16 @@
fetchFromGitHub,
makeDesktopItem,
cargo-tauri,
pkg-config,
cargo-tauri,
wrapGAppsHook3,
copyDesktopItems,
atkmm,
eudev,
gdk-pixbuf,
glib,
gtk3,
libgudev,
libsoup_3,
pango,
openssl,
pcsclite,
udev,
webkitgtk_4_1,
}:
rustPlatform.buildRustPackage (finalAttrs: {
@@ -32,13 +30,32 @@ rustPlatform.buildRustPackage (finalAttrs: {
};
cargoRoot = "src-tauri";
buildAndTestSubdir = finalAttrs.cargoRoot;
cargoHash = "sha256-nLf8v4MIt2zAeA9YMVaoI3s/yut5/Jy2fGM3Sx33EJc=";
npmDist = buildNpmPackage {
name = "${finalAttrs.pname}-${finalAttrs.version}-dist";
postPatch = ''
sed -i src-tauri/tauri.conf.json -e '/beforeBuildCommand/d'
'';
nativeBuildInputs = [
pkg-config
cargo-tauri.hook
wrapGAppsHook3
copyDesktopItems
];
buildInputs = [
glib
gtk3
openssl
pcsclite
udev
webkitgtk_4_1
];
frontendDist = buildNpmPackage {
name = "${finalAttrs.pname}-${finalAttrs.version}-frontend-dist";
inherit (finalAttrs) src;
npmDepsHash = "sha256-7DLooiGLzk3JRsKAftOxSf7HAgHBXCJDaAFp2p/pryc=";
@@ -54,28 +71,9 @@ rustPlatform.buildRustPackage (finalAttrs: {
};
preBuild = ''
sed -i '/beforeBuildCommand/d' src-tauri/tauri.conf.json
cp -r ${finalAttrs.npmDist} build
cp -r ${finalAttrs.frontendDist} build
'';
nativeBuildInputs = [
cargo-tauri.hook
pkg-config
];
buildInputs = [
atkmm
eudev
gdk-pixbuf
glib
gtk3
libgudev
libsoup_3
pango
pcsclite
webkitgtk_4_1
];
postInstall = ''
install -Dm644 ${finalAttrs.src}/static/in.suyogtandel.picoforge.svg $out/share/icons/hicolor/scalable/apps/picoforge.svg
'';
@@ -97,7 +95,7 @@ rustPlatform.buildRustPackage (finalAttrs: {
meta = {
changelog = "https://github.com/librekeys/picoforge/releases/tag/v${finalAttrs.version}";
description = "An open source commissioning tool for Pico FIDO security keys.";
description = "An open source commissioning tool for Pico FIDO security keys";
homepage = "https://github.com/librekeys/picoforge";
license = lib.licenses.agpl3Only;
mainProgram = "picoforge";
+1
View File
@@ -2943,6 +2943,7 @@ dependencies = [
"log4rs",
"pcsc",
"rand 0.9.2",
"ring",
"serde",
"serde_cbor_2",
"serde_json",
+1
View File
@@ -35,6 +35,7 @@ hidapi = "2.6" # For fido2 interface operations but non-standard command
serde_cbor_2 = "0.13"
rand = "0.9"
bitflags = "2.10"
ring = "0.17" # For signing fido2 messages with pin token
log = "0.4" # Logging facade
log4rs = "1" # For logging to output (like stdout)
+106 -8
View File
@@ -2,8 +2,13 @@
use anyhow::{Result, anyhow};
use rand::Rng;
use serde_cbor_2::{Value, to_vec};
use std::collections::BTreeMap;
use std::time::Duration;
use crate::error::PFError;
use crate::fido::constants::*;
// HID Transport Constants
const HID_REPORT_SIZE: usize = 64;
const HID_USAGE_PAGE_FIDO: u16 = 0xF1D0;
@@ -35,7 +40,7 @@ impl HidTransport {
.find(|d| d.usage_page() == HID_USAGE_PAGE_FIDO)
.ok_or_else(|| {
log::warn!("No FIDO device found with Usage Page 0xF1D0.");
anyhow!("No FIDO device found. Is it plugged in?")
PFError::NoDevice
})?;
log::debug!(
@@ -74,6 +79,17 @@ impl HidTransport {
fn init_channel(device: &hidapi::HidDevice) -> Result<u32> {
log::debug!("Initializing CTAPHID channel...");
// --- Drain Step ---
// Read and discard any pending packets to avoid using a stale response for CID negotiation.
let mut drain_buf = [0u8; HID_REPORT_SIZE];
while let Ok(n) = device.read_timeout(&mut drain_buf[..], 10) {
if n == 0 {
break;
}
log::trace!("Drained stale HID packet: {:02X?}", &drain_buf[0..16]);
}
let mut nonce = [0u8; 8];
rand::rng().fill(&mut nonce);
@@ -86,7 +102,7 @@ impl HidTransport {
report[8..16].copy_from_slice(&nonce);
log::trace!("Sending CTAPHID_INIT broadcast with nonce: {:02X?}", nonce);
device.write(&report).map_err(|e| {
device.write(&report[..]).map_err(|e| {
log::error!("Failed to write INIT packet: {}", e);
e
})?;
@@ -95,11 +111,11 @@ impl HidTransport {
let start = std::time::Instant::now();
while start.elapsed() < Duration::from_secs(1) {
let mut buf = [0u8; HID_REPORT_SIZE];
if device.read_timeout(&mut buf, 100).is_ok() {
if device.read_timeout(&mut buf[..], 100).is_ok() {
// Check if response matches our broadcast and nonce
if buf[0..4] == CTAPHID_CID_BROADCAST.to_be_bytes()
&& buf[4] == CTAPHID_INIT
&& &buf[7..15] == &nonce
&& buf[7..15] == nonce
{
// New CID is at bytes 16..20
let new_cid = u32::from_be_bytes([buf[15], buf[16], buf[17], buf[18]]);
@@ -136,7 +152,7 @@ impl HidTransport {
sent += to_copy;
// log::trace!("Writing Init Packet (Sent: {}/{})", sent, total_len);
if let Err(e) = self.device.write(&report) {
if let Err(e) = self.device.write(&report[..]) {
log::error!("Failed to write initial HID packet: {}", e);
return Err(e.into());
}
@@ -153,7 +169,7 @@ impl HidTransport {
sent += to_copy;
// log::trace!("Writing Cont Packet Seq {} (Sent: {}/{})", sequence - 1, sent, total_len);
if let Err(e) = self.device.write(&report) {
if let Err(e) = self.device.write(&report[..]) {
log::error!(
"Failed to write continuation HID packet (Seq {}): {}",
sequence - 1,
@@ -176,7 +192,7 @@ impl HidTransport {
let mut buf = [0u8; HID_REPORT_SIZE];
loop {
if let Err(e) = self.device.read_timeout(&mut buf, 2000) {
if let Err(e) = self.device.read_timeout(&mut buf[..], 2000) {
log::error!("Timeout reading response packet: {}", e);
return Err(e.into());
}
@@ -227,7 +243,7 @@ impl HidTransport {
// 2. Read Continuation Packets
while read_len < expected_len {
if let Err(e) = self.device.read_timeout(&mut buf, 500) {
if let Err(e) = self.device.read_timeout(&mut buf[..], 500) {
log::error!("Timeout reading continuation packet: {}", e);
return Err(e.into());
}
@@ -274,4 +290,86 @@ impl HidTransport {
// Return payload without status byte
Ok(response_data[1..].to_vec())
}
pub fn send_vendor_config(
&self,
pin_token: &[u8],
vendor_cmd: VendorConfigCommand,
param: Value,
) -> Result<(), PFError> {
log::debug!("Sending vendor config command: {}...", vendor_cmd);
// Build subCommandParams (Key 0x02)
// This map contains:
// 0x01: vendorCommandId (u64)
// 0x02/0x03/0x04: param
let mut sub_params_inner = BTreeMap::new();
sub_params_inner.insert(
Value::Integer(0x01),
Value::Integer(vendor_cmd.to_u64() as i128),
);
match param {
Value::Bytes(_) => {
sub_params_inner.insert(Value::Integer(0x02), param.clone());
}
Value::Integer(_) => {
sub_params_inner.insert(Value::Integer(0x03), param.clone());
}
Value::Text(_) => {
sub_params_inner.insert(Value::Integer(0x04), param.clone());
}
_ => return Err(PFError::Io("Unsupported parameter type".into())),
}
let sub_params = Value::Map(sub_params_inner);
let sub_params_bytes = to_vec(&sub_params).map_err(|e| PFError::Io(e.to_string()))?;
// Build HMAC message for signing
// According to FIDO 2.1: authenticate(pinUvAuthToken, 32×0xff || 0x0d || uint8(subCommand) || subCommandParams)
let mut message = vec![0xff; 32];
message.push(CtapCommand::Config as u8);
message.push(ConfigSubCommand::VendorPrototype as u8);
message.extend(&sub_params_bytes);
// Sign using provided PIN token
use ring::hmac;
let hmac_key = hmac::Key::new(hmac::HMAC_SHA256, pin_token);
let sig = hmac::sign(&hmac_key, &message);
let pin_auth = sig.as_ref()[0..16].to_vec();
// Build full authenticatorConfig map
let mut config_map = BTreeMap::new();
config_map.insert(
Value::Integer(ConfigParam::SubCommand as i128),
Value::Integer(ConfigSubCommand::VendorPrototype as i128),
);
config_map.insert(
Value::Integer(ConfigParam::SubCommandParams as i128),
sub_params,
);
config_map.insert(
Value::Integer(ConfigParam::PinUvAuthProtocol as i128),
Value::Integer(1),
);
config_map.insert(
Value::Integer(ConfigParam::PinUvAuthParam as i128),
Value::Bytes(pin_auth),
);
let config_payload_cbor =
to_vec(&Value::Map(config_map)).map_err(|e| PFError::Io(e.to_string()))?;
// Encapsulate for CTAP
let mut payload = vec![CtapCommand::Config as u8];
payload.extend(config_payload_cbor);
// Send via HID
self.send_cbor(CTAPHID_CBOR, &payload).map_err(|e| {
log::error!("Failed to send FIDO config: {}", e);
PFError::Device(format!("FIDO config failed: {}", e))
})?;
Ok(())
}
}
+129 -4
View File
@@ -5,13 +5,19 @@ pub mod hid;
use crate::{
error::PFError,
types::{AppConfig, DeviceInfo, FidoDeviceInfo, FullDeviceStatus, StoredCredential},
types::{
AppConfig, AppConfigInput, DeviceInfo, FidoDeviceInfo, FullDeviceStatus, StoredCredential,
},
};
use constants::*;
use ctap_hid_fido2::{
Cfg, FidoKeyHidFactory, public_key_credential_descriptor::PublicKeyCredentialDescriptor,
Cfg, FidoKeyHidFactory,
fidokey::make_credential::{MakeCredentialArgs, MakeCredentialArgsBuilder},
public_key_credential_descriptor::PublicKeyCredentialDescriptor,
public_key_credential_user_entity::PublicKeyCredentialUserEntity,
};
use hid::*;
use rand::Rng;
use serde_cbor_2::{Value, from_slice, to_vec};
use std::collections::{BTreeMap, HashMap};
@@ -149,8 +155,12 @@ pub fn read_device_details() -> Result<FullDeviceStatus, PFError> {
log::info!("Starting FIDO device details read...");
let transport = HidTransport::open().map_err(|e| {
log::error!("Failed to open HID transport: {}", e);
PFError::Device(e.to_string())
if let Some(PFError::NoDevice) = e.downcast_ref::<PFError>() {
PFError::NoDevice
} else {
log::error!("Failed to open HID transport: {}", e);
PFError::Device(e.to_string())
}
})?;
// --- 1. Get Info ---
@@ -332,3 +342,118 @@ pub fn read_device_details() -> Result<FullDeviceStatus, PFError> {
method: "FIDO".to_string(),
})
}
pub fn write_config(config: AppConfigInput, pin: Option<String>) -> Result<String, PFError> {
log::info!("Starting FIDO write_config...");
let pin_val = pin.as_deref().ok_or_else(|| {
log::error!("PIN is required for configuration");
PFError::Device("PIN is required for configuration".into())
})?;
// 1. Obtain PIN token using the library handle
let pin_token = {
let cfg = Cfg::init();
let device = FidoKeyHidFactory::create(&cfg)
.map_err(|e| PFError::Device(format!("Could not connect to FIDO device: {:?}", e)))?;
use ctap_hid_fido2::fidokey::pin::Permission;
// Try to obtain a token with AuthenticatorConfiguration permission (CTAP 2.1)
match device
.get_pinuv_auth_token_with_permission(pin_val, Permission::AuthenticatorConfiguration)
{
Ok(token) => {
log::debug!("Successfully obtained PIN token with ACFG permission.");
token.key
}
Err(e) => {
log::warn!(
"Failed to get PIN token with ACFG permission (Error: {:?}). Falling back to standard token.",
e
);
// Fallback to standard PIN token (Subcommand 0x05)
let token = device.get_pin_token(pin_val).map_err(|e2| {
log::error!("Failed to obtain even a standard PIN token: {:?}", e2);
PFError::Device(format!("PIN token acquisition failed: {:?}", e2))
})?;
log::debug!("Successfully obtained standard PIN token (fallback).");
token.key
}
}
// Library handle 'device' is dropped here, closing the HID session.
};
// 2. Open custom HidTransport and send vendor commands using the token
let transport = HidTransport::open().map_err(|e| {
log::error!("Failed to open HID transport: {}", e);
PFError::Device(format!("Could not open HID transport: {}", e))
})?;
// VID/PID config
if let (Some(vid_str), Some(pid_str)) = (&config.vid, &config.pid) {
let vid = u16::from_str_radix(vid_str, 16).map_err(|e| PFError::Io(e.to_string()))?;
let pid = u16::from_str_radix(pid_str, 16).map_err(|e| PFError::Io(e.to_string()))?;
let vidpid = ((vid as u32) << 16) | (pid as u32);
transport.send_vendor_config(
&pin_token,
VendorConfigCommand::PhysicalVidPid,
Value::Integer(vidpid as i128),
)?;
}
// LED GPIO config
if let Some(gpio) = config.led_gpio {
transport.send_vendor_config(
&pin_token,
VendorConfigCommand::PhysicalLedGpio,
Value::Integer(gpio as i128),
)?;
}
// LED brightness config
if let Some(brightness) = config.led_brightness {
transport.send_vendor_config(
&pin_token,
VendorConfigCommand::PhysicalLedBrightness,
Value::Integer(brightness as i128),
)?;
}
// Options config
let mut opts = 0u16;
if config.led_dimmable.unwrap_or(false) {
opts |= 0x02; // PHY_OPT_DIMM
}
if !config.power_cycle_on_reset.unwrap_or(true) {
opts |= 0x04; // PHY_OPT_DISABLE_POWER_RESET
}
if config.led_steady.unwrap_or(false) {
opts |= 0x08; // PHY_OPT_LED_STEADY
}
// Touch_timeout config
if let Some(timeout) = config.touch_timeout {
// In the firmware's phy_data, touch_timeout is often part of opts or separate.
// Looking at the previous code, it was separate (0x08).
// However, in vendor configuration, we usually send parameters individually.
transport
.send_vendor_config(
&pin_token,
VendorConfigCommand::PhysicalOptions, // Assuming there's a command for it or it's in opts
Value::Integer(timeout as i128), // Wait, let's check VendorConfigCommand again
)
.ok(); // If it fails, maybe it's not supported as a standalone vendor cmd
}
transport.send_vendor_config(
&pin_token,
VendorConfigCommand::PhysicalOptions,
Value::Integer(opts as i128),
)?;
// ToDo : Product name configuration is not implemented in pico-fido firmware (cbor_config.c)?
Ok(
"Configuration updated successfully! Unplug and re-plug the device to apply VID/PID changes."
.to_string(),
)
}
+10 -2
View File
@@ -13,8 +13,16 @@ pub fn read_device_details() -> Result<FullDeviceStatus, PFError> {
}
#[tauri::command]
pub fn write_config(config: AppConfigInput) -> Result<String, PFError> {
rescue::write_config(config)
pub fn write_config(
config: AppConfigInput,
method: String,
pin: Option<String>,
) -> Result<String, PFError> {
if method == "FIDO" {
fido::write_config(config, pin)
} else {
rescue::write_config(config)
}
}
#[tauri::command]
+1 -1
View File
@@ -32,7 +32,7 @@ pub struct AppConfig {
pub enable_secp256k1: bool,
}
#[derive(Deserialize, Debug)]
#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct AppConfigInput {
pub vid: Option<String>,
@@ -0,0 +1,51 @@
<script lang="ts">
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
import * as AlertDialog from "$lib/components/ui/alert-dialog";
import { configViewState as configState } from "$lib/state/configState.svelte";
</script>
<AlertDialog.Root bind:open={configState.authPinDialogOpen}>
<AlertDialog.Content
onOpenAutoFocus={(e) => {
e.preventDefault();
document.getElementById("auth-pin")?.focus();
}}
>
<AlertDialog.Header>
<AlertDialog.Title>Authentication Required</AlertDialog.Title>
<AlertDialog.Description>
Please enter your FIDO2 PIN to authorize the configuration
change.
</AlertDialog.Description>
</AlertDialog.Header>
<div class="space-y-4 py-4">
<div class="space-y-2">
<Label for="auth-pin">FIDO2 PIN</Label>
<Input
id="auth-pin"
type="password"
bind:value={configState.authPin}
placeholder="Enter your PIN"
onkeydown={(e) =>
e.key === "Enter" && configState.confirmAuthPinSave()}
/>
</div>
{#if configState.authPinError}
<p class="text-sm text-destructive">
{configState.authPinError}
</p>
{/if}
</div>
<AlertDialog.Footer>
<AlertDialog.Cancel
onclick={() => (configState.authPinDialogOpen = false)}
>Cancel</AlertDialog.Cancel
>
<AlertDialog.Action onclick={() => configState.confirmAuthPinSave()}
>Confirm</AlertDialog.Action
>
</AlertDialog.Footer>
</AlertDialog.Content>
</AlertDialog.Root>
+49 -14
View File
@@ -5,41 +5,76 @@
import { Slider } from "$lib/components/ui/slider/index.js";
import * as AlertDialog from "$lib/components/ui/alert-dialog";
import { configViewState as state } from "$lib/state/configState.svelte";
import { configViewState as configState } from "$lib/state/configState.svelte";
</script>
<AlertDialog.Root bind:open={state.minPinDialogOpen}>
<AlertDialog.Content>
<AlertDialog.Root bind:open={configState.minPinDialogOpen}>
<AlertDialog.Content
onOpenAutoFocus={(e) => {
e.preventDefault();
document.getElementById("min-pin-current")?.focus();
}}
>
<AlertDialog.Header>
<AlertDialog.Title>Update Minimum PIN Length</AlertDialog.Title>
<AlertDialog.Description>
Set the minimum allowed PIN length (4-63 characters) and enter a new PIN that meets this requirement.
Set the minimum allowed PIN length (4-63 characters) and enter a new PIN
that meets this requirement.
</AlertDialog.Description>
</AlertDialog.Header>
<div class="space-y-4 py-4">
<div class="space-y-2">
<Label for="min-pin-length">Minimum PIN Length ({state.minPinLength})</Label>
<Slider type="single" bind:value={state.minPinLength} min={4} max={63} step={1} />
<Label for="min-pin-length"
>Minimum PIN Length ({configState.minPinLength})</Label
>
<Slider
type="single"
bind:value={configState.minPinLength}
min={4}
max={63}
step={1}
/>
</div>
<div class="space-y-2">
<Label for="min-pin-current">Current PIN</Label>
<Input id="min-pin-current" type="password" bind:value={state.minPinCurrentPin} placeholder="Enter current PIN" />
<Input
id="min-pin-current"
type="password"
bind:value={configState.minPinCurrentPin}
placeholder="Enter current PIN"
/>
</div>
<div class="space-y-2">
<Label for="min-pin-new">New PIN (min {state.minPinLength} chars)</Label>
<Input id="min-pin-new" type="password" bind:value={state.minPinNewPin} placeholder="Enter new PIN" />
<Label for="min-pin-new"
>New PIN (min {configState.minPinLength} chars)</Label
>
<Input
id="min-pin-new"
type="password"
bind:value={configState.minPinNewPin}
placeholder="Enter new PIN"
/>
</div>
<div class="space-y-2">
<Label for="min-pin-confirm">Confirm New PIN</Label>
<Input id="min-pin-confirm" type="password" bind:value={state.minPinConfirmPin} placeholder="Confirm new PIN" />
<Input
id="min-pin-confirm"
type="password"
bind:value={configState.minPinConfirmPin}
placeholder="Confirm new PIN"
/>
</div>
{#if state.minPinError}
<p class="text-sm text-destructive">{state.minPinError}</p>
{#if configState.minPinError}
<p class="text-sm text-destructive">{configState.minPinError}</p>
{/if}
</div>
<AlertDialog.Footer>
<AlertDialog.Cancel onclick={() => (state.minPinDialogOpen = false)}>Cancel</AlertDialog.Cancel>
<AlertDialog.Action onclick={state.handleMinPinChange}>Update</AlertDialog.Action>
<AlertDialog.Cancel onclick={() => (configState.minPinDialogOpen = false)}
>Cancel</AlertDialog.Cancel
>
<AlertDialog.Action onclick={configState.handleMinPinChange}
>Update</AlertDialog.Action
>
</AlertDialog.Footer>
</AlertDialog.Content>
</AlertDialog.Root>
+45 -14
View File
@@ -4,41 +4,72 @@
import { Label } from "$lib/components/ui/label";
import * as AlertDialog from "$lib/components/ui/alert-dialog";
import { configViewState as state } from "$lib/state/configState.svelte";
import { configViewState as configState } from "$lib/state/configState.svelte";
</script>
<AlertDialog.Root bind:open={state.setPinDialogOpen}>
<AlertDialog.Content>
<AlertDialog.Root bind:open={configState.setPinDialogOpen}>
<AlertDialog.Content
onOpenAutoFocus={(e) => {
e.preventDefault();
const id = configState.isSettingPin ? "new-pin" : "current-pin";
document.getElementById(id)?.focus();
}}
>
<AlertDialog.Header>
<AlertDialog.Title>{state.isSettingPin ? "Set PIN" : "Change PIN"}</AlertDialog.Title>
<AlertDialog.Title
>{configState.isSettingPin
? "Set PIN"
: "Change PIN"}</AlertDialog.Title
>
<AlertDialog.Description>
{state.isSettingPin
? "Set a PIN for your FIDO2 device. Minimum length is " + (state.minPinLength || 4) + " characters."
{configState.isSettingPin
? "Set a PIN for your FIDO2 device. Minimum length is " +
(configState.minPinLength || 4) +
" characters."
: "Enter your current PIN and choose a new one."}
</AlertDialog.Description>
</AlertDialog.Header>
<div class="space-y-4 py-4">
{#if !state.isSettingPin}
{#if !configState.isSettingPin}
<div class="space-y-2">
<Label for="current-pin">Current PIN</Label>
<Input id="current-pin" type="password" bind:value={state.currentPin} placeholder="Enter current PIN" />
<Input
id="current-pin"
type="password"
bind:value={configState.currentPin}
placeholder="Enter current PIN"
/>
</div>
{/if}
<div class="space-y-2">
<Label for="new-pin">New PIN</Label>
<Input id="new-pin" type="password" bind:value={state.newPin} placeholder="Enter new PIN" />
<Input
id="new-pin"
type="password"
bind:value={configState.newPin}
placeholder="Enter new PIN"
/>
</div>
<div class="space-y-2">
<Label for="confirm-pin">Confirm New PIN</Label>
<Input id="confirm-pin" type="password" bind:value={state.confirmPin} placeholder="Confirm new PIN" />
<Input
id="confirm-pin"
type="password"
bind:value={configState.confirmPin}
placeholder="Confirm new PIN"
/>
</div>
{#if state.pinError}
<p class="text-sm text-destructive">{state.pinError}</p>
{#if configState.pinError}
<p class="text-sm text-destructive">{configState.pinError}</p>
{/if}
</div>
<AlertDialog.Footer>
<AlertDialog.Cancel onclick={() => (state.setPinDialogOpen = false)}>Cancel</AlertDialog.Cancel>
<AlertDialog.Action onclick={() => state.handlePinChange()}>Confirm</AlertDialog.Action>
<AlertDialog.Cancel onclick={() => (configState.setPinDialogOpen = false)}
>Cancel</AlertDialog.Cancel
>
<AlertDialog.Action onclick={() => configState.handlePinChange()}
>Confirm</AlertDialog.Action
>
</AlertDialog.Footer>
</AlertDialog.Content>
</AlertDialog.Root>
+6 -4
View File
@@ -95,7 +95,7 @@ class DeviceManager {
}
}
async save() {
async save(pin: string | null = null) {
if (!this.connected || !this.#originalConfig) return { success: false, msg: "Device not connected" };
this.loading = true;
@@ -158,15 +158,17 @@ class DeviceManager {
return { success: false, msg: "No changes detected." };
} else {
logger.add("Sending configuration to device...", "info");
const response = await invoke("write_config", { config: rustConfig });
const response = await invoke("write_config", { config: rustConfig, method: this.method, pin });
logger.add(`Device Response: ${response}`, "success");
await this.refresh();
return { success: true, msg: "Configuration Applied Successfully!" };
}
} catch (err: any) {
logger.add(`Write Failed: ${err}`, "error");
return { success: false, msg: `Error: ${err}` };
console.error("Write failed:", err);
const msg = typeof err === "string" ? err : err.message || JSON.stringify(err);
logger.add(`Write Failed: ${msg}`, "error");
return { success: false, msg: `Error: ${msg}` };
} finally {
this.loading = false;
}
+14 -6
View File
@@ -120,11 +120,19 @@
>Device Status</span
>
{#if device.connected}
<Badge
variant="default"
class="bg-green-600 hover:bg-green-600 text-[10px] px-1.5 h-5"
>Online</Badge
>
{#if device.method === "FIDO"}
<Badge
variant="default"
class="bg-amber-500 hover:bg-amber-500 text-[10px] px-1.5 h-5"
>Online - Fido</Badge
>
{:else}
<Badge
variant="default"
class="bg-green-600 hover:bg-green-600 text-[10px] px-1.5 h-5"
>Online</Badge
>
{/if}
{:else if device.error}
<Badge
variant="destructive"
@@ -165,7 +173,7 @@
<RefreshCw class="h-4 w-4 {device.loading ? 'animate-spin' : ''}" />
</Button>
<div
class={`h-2 w-2 rounded-full ${device.connected ? "bg-green-500" : device.error ? "bg-amber-500" : "bg-red-500"}`}
class={`h-2 w-2 rounded-full ${device.connected ? (device.method === "FIDO" ? "bg-amber-500" : "bg-green-500") : device.error ? "bg-amber-500" : "bg-red-500"}`}
></div>
</div>
</Sidebar.Footer>
+21
View File
@@ -15,17 +15,38 @@ class configState {
minPinLength = $state(4);
minPinError = $state("");
authPinDialogOpen = $state(false);
authPin = $state("");
authPinError = $state("");
dialogOpen = $state(false);
dialogTitle = $state("");
dialogMessage = $state("");
async handleSave() {
if (device.method === "FIDO" && device.fidoInfo?.options?.clientPin) {
this.authPin = "";
this.authPinError = "";
this.authPinDialogOpen = true;
return;
}
const result = await device.save();
if (result) {
this.showStatusDialog(result.success ? "Success" : "Write Failed", result.msg);
}
}
async confirmAuthPinSave() {
const result = await device.save(this.authPin);
if (result.success) {
this.authPinDialogOpen = false;
this.showStatusDialog("Success", result.msg);
} else {
this.authPinError = result.msg as string;
}
}
showStatusDialog(title: string, message: string) {
this.dialogTitle = title;
this.dialogMessage = message;
-4
View File
@@ -60,10 +60,6 @@
<p class="text-muted-foreground">Product Name</p>
<p class="font-medium truncate">{device.config.productName}</p>
</div>
<div class="space-y-1">
<p class="text-muted-foreground">Connection Method</p>
<Badge variant="outline" class="font-mono">{device.method}</Badge>
</div>
</div>
<Separator />
+5 -1
View File
@@ -16,6 +16,7 @@
import SetPinDialog from "$lib/components/dialogs/setPinDialog.svelte";
import MinPinDialog from "$lib/components/dialogs/minPinDialog.svelte";
import AuthPinDialog from "$lib/components/dialogs/authPinDialog.svelte";
import MessageDialog from "$lib/components/dialogs/messageDialog.svelte";
type View = "home" | "passkeys" | "config" | "security" | "logs" | "about";
@@ -35,7 +36,9 @@
$effect(() => {
logger.logs.length;
tick().then(() => {
const viewport = document.querySelector("[data-radix-scroll-area-viewport]");
const viewport = document.querySelector(
"[data-radix-scroll-area-viewport]",
);
if (viewport) {
viewport.scrollTop = viewport.scrollHeight;
}
@@ -62,3 +65,4 @@
<MessageDialog />
<SetPinDialog />
<MinPinDialog />
<AuthPinDialog />