mirror of
https://github.com/librekeys/picoforge.git
synced 2026-07-28 08:01:19 -07:00
Merge branch 'librekeys:main' into main
This commit is contained in:
+29
-31
@@ -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";
|
||||
|
||||
Generated
+1
@@ -2943,6 +2943,7 @@ dependencies = [
|
||||
"log4rs",
|
||||
"pcsc",
|
||||
"rand 0.9.2",
|
||||
"ring",
|
||||
"serde",
|
||||
"serde_cbor_2",
|
||||
"serde_json",
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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]
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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 />
|
||||
|
||||
Reference in New Issue
Block a user