squash commits that implement #37 (pico-openpgp support). See branch pico-openpgp for detailed commits.

This commit is contained in:
Fabrice Bellamy
2026-01-24 14:15:05 +05:30
committed by Suyog Tandel
parent bdba205be6
commit ee528979e4
12 changed files with 620 additions and 327 deletions
+6 -2
View File
@@ -382,8 +382,12 @@ pub fn write_config(config: AppConfigInput, pin: Option<String>) -> Result<Strin
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())
log::error!(
"A security PIN is required to be set to change the configuration in fido mode"
);
PFError::Device(
"A security PIN is required to be set to change the configuration in fido mode".into(),
)
})?;
// 1. Obtain PIN token using the library handle
@@ -0,0 +1,22 @@
<script lang="ts">
import * as Alert from "$lib/components/ui/alert";
import { TriangleAlert } from "@lucide/svelte";
import { device } from "$lib/device/manager.svelte";
interface Props {
message?: string;
}
let { message = "This device does implement this feature." }: Props =
$props();
</script>
<Alert.Root variant={device.error ? "destructive" : "default"}>
<TriangleAlert class="h-4 w-4" />
<Alert.Title
>{device.error ? "Connection Error" : "Feature not supported"}</Alert.Title
>
<Alert.Description>
{device.error || message}
</Alert.Description>
</Alert.Root>
@@ -0,0 +1,81 @@
<script lang="ts">
import { Button } from "$lib/components/ui/button";
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
import * as Dialog from "$lib/components/ui/dialog";
import * as Alert from "$lib/components/ui/alert";
import { Loader2, Shield, Unlock } from "@lucide/svelte";
import { device } from "$lib/device/manager.svelte";
let { open = $bindable(false), pin = $bindable("") } = $props();
let error = $state("");
let loading = $state(false);
async function handleUnlock() {
if (!pin) {
error = "Please enter your PIN";
return;
}
loading = true;
const res = await device.getCredentials(pin);
if (res.success) {
open = false;
// Reset state for next time
// pin = ""; // Do not reset pin here, as it might be needed by the parent
error = "";
} else {
error =
typeof res.msg === "string" ? res.msg : "Failed to verify PIN";
}
loading = false;
}
</script>
<Dialog.Root bind:open>
<Dialog.Content class="sm:max-w-md">
<Dialog.Header>
<Dialog.Title class="flex items-center gap-2">
<Shield class="h-5 w-5 text-primary" /> Authenticate to View Passkeys
</Dialog.Title>
<Dialog.Description
>Enter your FIDO2 PIN to access and manage stored credentials.</Dialog.Description
>
</Dialog.Header>
<div class="space-y-4 py-4">
<div class="space-y-2">
<Label for="auth-pin">Device PIN</Label>
<Input
id="auth-pin"
type="password"
placeholder="Enter your PIN..."
bind:value={pin}
disabled={loading}
onkeydown={(e) => e.key === "Enter" && handleUnlock()}
autofocus
/>
</div>
{#if error}
<Alert.Root
variant="destructive"
class="animate-in fade-in slide-in-from-top-1"
>
<Alert.Description>{error}</Alert.Description>
</Alert.Root>
{/if}
</div>
<Dialog.Footer>
<Button class="w-full" onclick={handleUnlock} disabled={loading}>
{#if loading}
<Loader2 class="mr-2 h-4 w-4 animate-spin" />
Verifying...
{:else}
<Unlock class="mr-2 h-4 w-4" /> Unlock Storage
{/if}
</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>
@@ -0,0 +1,73 @@
<script lang="ts">
import * as AlertDialog from "$lib/components/ui/alert-dialog";
import { Button } from "$lib/components/ui/button";
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
import { Loader2 } from "@lucide/svelte";
import type { StoredCredential } from "$lib/device/types.svelte";
interface Props {
open: boolean;
credential: StoredCredential | null;
loading: boolean;
onDelete: () => void;
}
let { open = $bindable(), credential, loading, onDelete }: Props = $props();
let deleteConfirmationInput = $state("");
$effect(() => {
if (open) {
deleteConfirmationInput = "";
}
});
</script>
<AlertDialog.Root bind:open>
<AlertDialog.Content>
<AlertDialog.Header>
<AlertDialog.Title>Are you sure?</AlertDialog.Title>
<AlertDialog.Description>
This action cannot be undone. This will permanently delete the
passkey for
<span class="font-semibold text-foreground"
>{credential?.rpId}</span
>.
</AlertDialog.Description>
</AlertDialog.Header>
<div class="py-4 space-y-3">
<Label for="confirm-delete">
Type <span
class="font-mono text-xs bg-muted px-1 py-0.5 rounded"
>{credential?.rpId}</span
> to confirm
</Label>
<Input
id="confirm-delete"
bind:value={deleteConfirmationInput}
placeholder={credential?.rpId}
autocomplete="off"
class="font-mono"
/>
</div>
<AlertDialog.Footer>
<AlertDialog.Cancel disabled={loading}>Cancel</AlertDialog.Cancel>
<Button
variant="destructive"
onclick={onDelete}
disabled={deleteConfirmationInput !== credential?.rpId ||
loading}
>
{#if loading}
<Loader2 class="mr-2 h-4 w-4 animate-spin" />
Deleting...
{:else}
Delete Passkey
{/if}
</Button>
</AlertDialog.Footer>
</AlertDialog.Content>
</AlertDialog.Root>
+3 -3
View File
@@ -3,9 +3,9 @@ import type { DeviceConfig } from "$lib/device/types.svelte.ts";
export const VENDORS = [
{ value: "custom", label: "Custom (Manual Entry)", vid: "", pid: "" },
{ value: "generic", label: "Generic (FEFF:FCFD)", vid: "FEFF", pid: "FCFD" },
{ value: "pico-hsm", label: "Pico Keys HSM (2E8A:0x10FD)", vid: "2E8A", pid: "0x10FD" },
{ value: "pico-fido", label: "Pico Keys Fido (2E8A:0x10FE)", vid: "2E8A", pid: "0x10FE" },
{ value: "pico-openpgp", label: "Pico Keys OpenPGP (2E8A:0x10FF)", vid: "2E8A", pid: "0x10FF" },
{ value: "pico-hsm", label: "Pico Keys HSM (2E8A:10FD)", vid: "2E8A", pid: "10FD" },
{ value: "pico-fido", label: "Pico Keys Fido (2E8A:10FE)", vid: "2E8A", pid: "10FE" },
{ value: "pico-openpgp", label: "Pico Keys OpenPGP (2E8A:10FF)", vid: "2E8A", pid: "10FF" },
{ value: "pico", label: "Pico (2E8A:0003)", vid: "2E8A", pid: "0003" },
{ value: "solokeys", label: "SoloKeys (0483:A2CA)", vid: "0483", pid: "A2CA" },
{ value: "nitrohsm", label: "NitroHSM (20A0:4230)", vid: "20A0", pid: "4230" },
+11 -3
View File
@@ -38,6 +38,10 @@ class DeviceManager {
return match ? match.value : "custom";
}
get hasFido() {
return !!this.fidoInfo;
}
async refresh() {
this.loading = true;
this.error = null;
@@ -63,9 +67,13 @@ class DeviceManager {
this.method = status.method;
const fido = await invoke<FidoInfo>("get_fido_info");
this.fidoInfo = fido;
try {
const fido = await invoke<FidoInfo>("get_fido_info");
this.fidoInfo = fido;
} catch (e) {
logger.add("FIDO info could not be read (device might not support FIDO features)", "warning");
this.fidoInfo = null;
}
if (!this.connected) {
logger.add(`Device Connected! Serial: ${this.info.serial}, FW: v${this.info.firmwareVersion}`, "success");
+24 -8
View File
@@ -12,37 +12,53 @@
}
</script>
<div class="space-y-6">
<div class="space-y-4">
<div>
<h1 class="text-3xl font-bold tracking-tight">About</h1>
</div>
<Card.Root>
<Card.Content class="pt-6">
<div class="flex flex-col items-center justify-center space-y-4 text-center py-8">
<div
class="flex flex-col items-center justify-center space-y-4 text-center py-8"
>
<!-- <div class="bg-primary text-primary p-6 rounded-2xl"> -->
<!-- <img src="/build-configure-symbolic.svg" alt="PicoForge Logo" class="h-12 w-12" /> -->
<!-- </div> -->
<img src="/in.suyogtandel.picoforge.svg" alt="PicoForge Logo" class="h-64 w-64 shadow-sm" />
<img
src="/in.suyogtandel.picoforge.svg"
alt="PicoForge Logo"
class="h-64 w-64 shadow-sm"
/>
<h2 class="text-2xl font-bold">PicoForge</h2>
<Badge variant="secondary" class="px-4 py-1">v0.3.0</Badge>
<p class="text-muted-foreground max-w-md">
An open source commissioning tool for Pico FIDO security keys. Developed with Rust, Tauri, and Svelte.
An open source commissioning tool for Pico FIDO security keys.
Developed with Rust, Tauri, and Svelte.
</p>
<div class="text-sm text-muted-foreground space-y-1 pt-4 border-t w-full max-w-xs">
<div
class="text-sm text-muted-foreground space-y-1 pt-4 border-t w-full max-w-xs"
>
<div class="flex justify-between">
<span>Code By:</span> <span class="font-medium text-foreground">Suyog Tandel</span>
<span>Code By:</span>
<span class="font-medium text-foreground">Suyog Tandel</span>
</div>
<div class="flex justify-between items-center pt-2 mt-2">
<span class="flex items-center gap-1">Copyright:</span>
<span class="font-medium text-foreground">© 2026 Suyog Tandel</span>
<span class="font-medium text-foreground">© 2026 Suyog Tandel</span
>
</div>
</div>
<div class="flex gap-4 pt-6">
<Button variant="outline" size="sm" class="gap-2" onclick={openGithub}>
<Button
variant="outline"
size="sm"
class="gap-2"
onclick={openGithub}
>
<Github class="h-4 w-4" />
GitHub
</Button>
+61 -49
View File
@@ -12,55 +12,32 @@
import { device } from "$lib/device/manager.svelte";
import { LED_DRIVERS, VENDORS } from "$lib/device/constants.svelte";
import { Microchip, RefreshCw, Save, Settings, Tag, TriangleAlert, X, Key } from "@lucide/svelte";
import {
Microchip,
RefreshCw,
Save,
Settings,
Tag,
TriangleAlert,
X,
} from "@lucide/svelte";
import { configViewState as state } from "$lib/state/configState.svelte";
import NoDeviceStatus from "$lib/components/device/NoDeviceStatus.svelte";
</script>
<div class="space-y-6">
<div class="space-y-4">
<div>
<h1 class="text-3xl font-bold tracking-tight">Configuration</h1>
<p class="text-muted-foreground">Customize device settings and behavior.</p>
</div>
{#if !device.connected}
<NoDeviceStatus message="Connect your device to access configuration options." />
<NoDeviceStatus
message="Connect your device to access configuration options."
/>
{:else}
<div class="grid gap-6 lg:grid-cols-2">
<Card.Root class="lg:col-span-2">
<Card.Header>
<Card.Title class="flex items-center gap-2">
<Key class="h-5 w-5" />
PIN Management
</Card.Title>
<Card.Description>Configure FIDO2 PIN security</Card.Description>
</Card.Header>
<Card.Content class="space-y-4">
<div class="flex items-center justify-between p-4 border rounded-lg">
<div class="space-y-1">
<p class="font-medium">Current PIN Status</p>
<p class="text-sm text-muted-foreground">
{device.fidoInfo?.options?.clientPin ? "PIN is set" : "No PIN configured"}
</p>
</div>
<Button variant="outline" onclick={() => state.openPinDialog()}>
{device.fidoInfo?.options?.clientPin ? "Change PIN" : "Set PIN"}
</Button>
</div>
<div class="flex items-center justify-between p-4 border rounded-lg">
<div class="space-y-1">
<p class="font-medium">Minimum PIN Length</p>
<p class="text-sm text-muted-foreground">
Current: {device.fidoInfo?.minPinLength || 4} characters
</p>
</div>
<Button variant="outline" disabled={!device.fidoInfo?.options?.clientPin} onclick={() => state.openMinPinDialog()}>Update Minimum Length</Button>
</div>
</Card.Content>
</Card.Root>
<Card.Root>
<Card.Header>
<Card.Title class="flex items-center gap-2">
@@ -79,7 +56,8 @@
disabled={!device.connected}
>
<Select.Trigger class="w-full">
{VENDORS.find((v) => v.value === device.selectedVendor)?.label ?? "Select a vendor"}
{VENDORS.find((v) => v.value === device.selectedVendor)
?.label ?? "Select a vendor"}
</Select.Trigger>
<Select.Content>
{#each VENDORS as vendor}
@@ -99,7 +77,8 @@
bind:value={device.config.vid}
maxlength={4}
placeholder="CAFE"
disabled={!device.connected || device.selectedVendor !== "custom"}
disabled={!device.connected ||
device.selectedVendor !== "custom"}
class="font-mono"
/>
</div>
@@ -110,7 +89,8 @@
bind:value={device.config.pid}
maxlength={4}
placeholder="4242"
disabled={!device.connected || device.selectedVendor !== "custom"}
disabled={!device.connected ||
device.selectedVendor !== "custom"}
class="font-mono"
/>
</div>
@@ -120,7 +100,12 @@
<div class="space-y-2">
<Label for="product">Product Name</Label>
<Input id="product" bind:value={device.config.productName} placeholder="My Key" disabled={!device.connected} />
<Input
id="product"
bind:value={device.config.productName}
placeholder="My Key"
disabled={!device.connected}
/>
</div>
</Card.Content>
</Card.Root>
@@ -136,14 +121,25 @@
<Card.Content class="space-y-4">
<div class="space-y-2">
<Label for="led-gpio">LED GPIO Pin</Label>
<Input id="led-gpio" type="number" bind:value={device.config.ledGpio} min="0" max="29" />
<Input
id="led-gpio"
type="number"
bind:value={device.config.ledGpio}
min="0"
max="29"
/>
</div>
<div class="space-y-2">
<Label for="led-driver">LED Driver</Label>
<Select.Root type="single" bind:value={device.config.ledDriver} disabled={!device.connected}>
<Select.Root
type="single"
bind:value={device.config.ledDriver}
disabled={!device.connected}
>
<Select.Trigger class="w-full">
{LED_DRIVERS.find((d) => d.value === device.config.ledDriver)?.label ?? "Select driver"}
{LED_DRIVERS.find((d) => d.value === device.config.ledDriver)
?.label ?? "Select driver"}
</Select.Trigger>
<Select.Content>
{#each LED_DRIVERS as driver}
@@ -168,14 +164,18 @@
disabled={!device.connected}
class="flex-1"
/>
<span class="text-xs text-muted-foreground min-w-[4ch]">Level {device.config.ledBrightness}</span>
<span class="text-xs text-muted-foreground min-w-[4ch]"
>Level {device.config.ledBrightness}</span
>
</div>
</div>
<div class="flex items-center justify-between space-x-2">
<div class="space-y-0.5">
<Label>LED Dimmable</Label>
<p class="text-sm text-muted-foreground">Allow brightness adjustment</p>
<p class="text-sm text-muted-foreground">
Allow brightness adjustment
</p>
</div>
<Switch bind:checked={device.config.ledDimmable} />
</div>
@@ -183,7 +183,9 @@
<div class="flex items-center justify-between space-x-2">
<div class="space-y-0.5">
<Label>LED Steady Mode</Label>
<p class="text-sm text-muted-foreground">Keep LED on constantly</p>
<p class="text-sm text-muted-foreground">
Keep LED on constantly
</p>
</div>
<Switch bind:checked={device.config.ledSteady} />
</div>
@@ -201,7 +203,13 @@
<Card.Content class="space-y-4">
<div class="space-y-2">
<Label for="touch-timeout">Touch Timeout (seconds)</Label>
<Input id="touch-timeout" type="number" bind:value={device.config.touchTimeout} min="1" max="255" />
<Input
id="touch-timeout"
type="number"
bind:value={device.config.touchTimeout}
min="1"
max="255"
/>
</div>
</Card.Content>
</Card.Root>
@@ -218,7 +226,9 @@
<div class="flex items-center justify-between space-x-2">
<div class="space-y-0.5">
<Label>Power Cycle on Reset</Label>
<p class="text-sm text-muted-foreground">Restart device on reset</p>
<p class="text-sm text-muted-foreground">
Restart device on reset
</p>
</div>
<Switch bind:checked={device.config.powerCycleOnReset} />
</div>
@@ -226,7 +236,9 @@
<div class="flex items-center justify-between space-x-2">
<div class="space-y-0.5">
<Label>Enable Secp256k1</Label>
<p class="text-sm text-muted-foreground">Does not work on Android!</p>
<p class="text-sm text-muted-foreground">
Does not work on Android!
</p>
</div>
<Switch bind:checked={device.config.enableSecp256k1} />
</div>
+1 -1
View File
@@ -19,7 +19,7 @@
import NoDeviceStatus from "$lib/components/device/NoDeviceStatus.svelte";
</script>
<div class="space-y-6">
<div class="space-y-4">
<div>
<h1 class="text-3xl font-bold tracking-tight">Device Overview</h1>
<p class="text-muted-foreground">
+17 -6
View File
@@ -8,24 +8,35 @@
import { Terminal } from "@lucide/svelte";
</script>
<div class="space-y-6 h-full flex flex-col">
<div class="space-y-4 h-full flex flex-col">
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold tracking-tight">System Logs</h1>
<p class="text-muted-foreground">Real-time device communication and application events.</p>
<p class="text-muted-foreground">
Real-time device communication and application events.
</p>
</div>
<Button variant="outline" size="sm" onclick={() => (logger.logs = [])}>Clear Logs</Button>
<Button variant="outline" size="sm" onclick={() => (logger.logs = [])}
>Clear Logs</Button
>
</div>
<Card.Root class="flex-1 flex flex-col min-h-[500px] bg-black border-zinc-800">
<Card.Root
class="flex-1 flex flex-col min-h-[500px] bg-black border-zinc-800"
>
<Card.Content class="p-0 flex-1 flex flex-col">
{#if logger.logs.length === 0}
<div class="flex-1 flex flex-col items-center justify-center text-zinc-500">
<div
class="flex-1 flex flex-col items-center justify-center text-zinc-500"
>
<Terminal class="h-12 w-12 mb-4 opacity-50" />
<p>No events recorded yet.</p>
</div>
{:else}
<ScrollArea class="h-[500px] p-4 font-mono text-sm" orientation="vertical">
<ScrollArea
class="h-[500px] p-4 font-mono text-sm"
orientation="vertical"
>
<div class="space-y-1">
{#each logger.logs as log}
<div class="flex gap-3">
File diff suppressed because it is too large Load Diff
+37 -12
View File
@@ -12,32 +12,43 @@
import { Lock, TriangleAlert } from "@lucide/svelte";
async function lockDevice() {
logger.add("Action Blocked: Secure Boot toggle attempted but feature is disabled.", "warning");
logger.add(
"Action Blocked: Secure Boot toggle attempted but feature is disabled.",
"warning",
);
return;
}
</script>
<div class="space-y-6">
<div class="space-y-4">
<div>
<h1 class="text-3xl font-bold tracking-tight text-destructive">Secure Boot</h1>
<p class="text-muted-foreground">Permanently lock this device to the current firmware vendor.</p>
<h1 class="text-3xl font-bold tracking-tight text-destructive">
Secure Boot
</h1>
<p class="text-muted-foreground">
Permanently lock this device to the current firmware vendor.
</p>
</div>
<Alert.Root variant="destructive">
<TriangleAlert class="h-4 w-4" />
<Alert.Title>Feature Unstable</Alert.Title>
<Alert.Description>This feature is currently under work and disabled for safety.</Alert.Description>
<Alert.Description
>This feature is currently under work and disabled for safety.</Alert.Description
>
</Alert.Root>
<Card.Root class="border-destructive/30 opacity-60">
<Card.Header>
<Card.Title>Lock Settings</Card.Title>
</Card.Header>
<Card.Content class="space-y-6 pointer-events-none">
<Card.Content class="space-y-4 pointer-events-none">
<div class="flex items-center justify-between space-x-2">
<div class="flex flex-col space-y-1">
<Label for="secure-boot">Enable Secure Boot</Label>
<p class="font-normal text-xs text-muted-foreground">Verifies firmware signature on startup</p>
<p class="font-normal text-xs text-muted-foreground">
Verifies firmware signature on startup
</p>
</div>
<Switch
id="secure-boot"
@@ -50,16 +61,30 @@
<div class="flex items-center justify-between space-x-2">
<div class="flex flex-col space-y-1">
<Label for="secure-lock">Secure Lock</Label>
<p class="font-normal text-xs text-muted-foreground">Prevents reading key material via debug ports</p>
<p class="font-normal text-xs text-muted-foreground">
Prevents reading key material via debug ports
</p>
</div>
<Switch id="secure-lock" bind:checked={device.security.secureLock} disabled={true} />
<Switch
id="secure-lock"
bind:checked={device.security.secureLock}
disabled={true}
/>
</div>
<Separator />
<div class="flex items-center space-x-2 bg-destructive/10 p-4 rounded-md border border-destructive/20">
<Switch id="confirm" bind:checked={device.security.confirmed} disabled={true} />
<Label for="confirm" class="text-destructive font-medium">I understand the risks of bricking my device.</Label>
<div
class="flex items-center space-x-2 bg-destructive/10 p-4 rounded-md border border-destructive/20"
>
<Switch
id="confirm"
bind:checked={device.security.confirmed}
disabled={true}
/>
<Label for="confirm" class="text-destructive font-medium"
>I understand the risks of bricking my device.</Label
>
</div>
</Card.Content>
<Card.Footer class="border-t bg-muted/20 px-6 py-4 flex justify-end">