diff --git a/dev-utility-tauri/Cargo.toml b/dev-utility-tauri/Cargo.toml index a351666..78360f2 100644 --- a/dev-utility-tauri/Cargo.toml +++ b/dev-utility-tauri/Cargo.toml @@ -29,6 +29,7 @@ serde = { workspace = true } serde_json = { workspace = true } dev-utility-core = { path = "../dev-utility" } thiserror = "2.0.12" +tauri-plugin-dialog = "2" [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] tauri-plugin-updater = "2" diff --git a/dev-utility-tauri/src/lib.rs b/dev-utility-tauri/src/lib.rs index 24d4a01..c696683 100644 --- a/dev-utility-tauri/src/lib.rs +++ b/dev-utility-tauri/src/lib.rs @@ -30,6 +30,7 @@ const UPDATE_ID: &str = "update"; #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() + .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_os::init()) .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_clipboard_manager::init()) @@ -119,7 +120,13 @@ pub fn run() { #[cfg(desktop)] updates::app_install_update, #[cfg(desktop)] - // dev_utility_core::hardware::list_hid_devices, + dev_utility_core::hardware::list_hid_devices, + #[cfg(desktop)] + dev_utility_core::hardware::fido2_get_device_info, + #[cfg(desktop)] + dev_utility_core::hardware::fido2_register, + #[cfg(desktop)] + dev_utility_core::hardware::fido2_authenticate, dev_utility_core::codec::decode_base64, dev_utility_core::codec::encode_base64, dev_utility_core::cryptography::generate_rsa_key, diff --git a/dev-utility/Cargo.toml b/dev-utility/Cargo.toml index 623b530..024211d 100644 --- a/dev-utility/Cargo.toml +++ b/dev-utility/Cargo.toml @@ -78,6 +78,10 @@ hmac = "0.12" urlencoding = "2.1" markup5ever_rcdom = "0.3.0" +# USB +hidapi = { version = "2.6.3", features = ["macos-shared-device"] } +ctap-hid-fido2 = "3.5.5" + [target.'cfg(not(target_arch = "wasm32"))'.dependencies] tauri = { workspace = true } jsonwebtoken = { version = "9.3.1" } diff --git a/dev-utility/src/core/hardware/fido2.rs b/dev-utility/src/core/hardware/fido2.rs new file mode 100644 index 0000000..6e5f6c2 --- /dev/null +++ b/dev-utility/src/core/hardware/fido2.rs @@ -0,0 +1,344 @@ +use ctap_hid_fido2::{ + fidokey::{GetAssertionArgsBuilder, MakeCredentialArgsBuilder}, + get_fidokey_devices, + public_key::{PublicKey, PublicKeyType}, + verifier::{self, AttestationVerifyResult}, + Cfg, FidoKeyHid, FidoKeyHidFactory, +}; +use serde::{Deserialize, Serialize}; +use std::{ + collections::HashMap, + sync::{Arc, Mutex}, +}; +use tauri::{ipc::Channel, AppHandle, Listener}; +use universal_function_macro::universal_function; + +use crate::error::UtilityError; + +#[derive(Clone)] +pub struct FidokeyDeviceInfo { + pub pid: u16, + pub vid: u16, + pub product_string: String, + pub info: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum Fido2PublicKeyType { + Unknown = 0, + Ecdsa256 = 1, + Ed25519 = 2, +} +impl From for Fido2PublicKeyType { + fn from(public_key_type: PublicKeyType) -> Self { + match public_key_type { + PublicKeyType::Unknown => Fido2PublicKeyType::Unknown, + PublicKeyType::Ecdsa256 => Fido2PublicKeyType::Ecdsa256, + PublicKeyType::Ed25519 => Fido2PublicKeyType::Ed25519, + } + } +} + +impl From for PublicKeyType { + fn from(fido2_public_key_type: Fido2PublicKeyType) -> Self { + match fido2_public_key_type { + Fido2PublicKeyType::Unknown => PublicKeyType::Unknown, + Fido2PublicKeyType::Ecdsa256 => PublicKeyType::Ecdsa256, + Fido2PublicKeyType::Ed25519 => PublicKeyType::Ed25519, + } + } +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Fido2PublicKey { + pub key_type: Fido2PublicKeyType, + pub pem: String, + pub der_hex: String, +} +impl From for Fido2PublicKey { + fn from(public_key: PublicKey) -> Self { + Fido2PublicKey { + key_type: Fido2PublicKeyType::from(public_key.key_type), + pem: public_key.pem, + der_hex: hex::encode(public_key.der), + } + } +} + +impl From for PublicKey { + fn from(fido2_public_key: Fido2PublicKey) -> Self { + PublicKey { + key_type: PublicKeyType::from(fido2_public_key.key_type), + pem: fido2_public_key.pem, + der: hex::decode(fido2_public_key.der_hex).unwrap(), + } + } +} + +/// Fido2 Credential Data +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Fido2Credential { + pub id: Vec, + pub public_key: Fido2PublicKey, +} +impl From for Fido2Credential { + fn from(attestation_verify_result: AttestationVerifyResult) -> Self { + Fido2Credential { + id: attestation_verify_result.credential_id, + public_key: attestation_verify_result.credential_public_key.into(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Fido2UserEntity { + pub user_id: Option, + pub user_name: Option, + pub user_display_name: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Fido2RegisterParams { + pub rpid: String, + pub user: Option, +} + +#[derive(Clone, Serialize)] +#[serde( + rename_all = "camelCase", + rename_all_fields = "camelCase", + tag = "event", + content = "data" +)] +pub enum Fido2RegisterEvent { + Fido2RegisterPinNeeded { challenge: String }, + Fido2RegisterTouchNeeded { challenge: String }, + Fido2RegisterFinished { credential: Fido2Credential }, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AlgorithmInfo { + pub r#type: String, + pub name: String, +} +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum UserVerificationMethod { + None, + Fingerprint, + Pin, + Biometric, + Voice, + FaceRecognition, + Other(String), +} +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Fido2DeviceInfo { + // CTAP 2.0 + pub aaguid: String, + pub capabilities: HashMap, + pub supported_versions: Vec, + pub available_extensions: Vec, + pub max_message_size_bytes: u32, + pub pin_auth_protocols: Vec, + + // CTAP 2.1 + pub max_credentials_per_list: u32, + pub max_credential_id_size: u32, + pub communication_methods: Vec, + pub supported_algorithms: Vec, + pub max_large_blob_size: u32, + pub requires_pin_change: bool, + pub minimum_pin_length: u32, + pub firmware_version_string: String, + pub max_credential_blob_size: u32, + pub max_rp_ids_for_min_pin: u32, + pub preferred_uv_attempts: u32, + pub user_verification_method: UserVerificationMethod, + pub remaining_resident_keys: u32, +} + +pub trait FidoKeyHidExt { + fn get_human_readable_info(&self) -> Result; +} +impl FidoKeyHidExt for FidoKeyHid { + fn get_human_readable_info(&self) -> Result { + let info = self.get_info()?; + Ok(Fido2DeviceInfo { + supported_versions: info.versions, + available_extensions: info.extensions, + aaguid: hex::encode(info.aaguid), + capabilities: info.options.into_iter().collect(), + max_message_size_bytes: info.max_msg_size as u32, + pin_auth_protocols: info.pin_uv_auth_protocols, + max_credentials_per_list: info.max_credential_count_in_list, + max_credential_id_size: info.max_credential_id_length, + communication_methods: info.transports, + supported_algorithms: { + let mut algorithms = Vec::new(); + let mut current_alg: Option = None; + let mut current_type: Option = None; + + for (key, value) in info.algorithms { + match key.as_str() { + "alg" => { + // If there was complete algorithm information before, save it first + if let (Some(alg), Some(typ)) = + (current_alg.take(), current_type.take()) + { + algorithms.push(AlgorithmInfo { + r#type: typ, + name: alg, + }); + } + current_alg = Some(value); + } + "type" => { + current_type = Some(value); + // If algorithm ID already exists, construct complete information immediately + if let Some(alg) = current_alg.take() { + algorithms.push(AlgorithmInfo { + r#type: current_type.take().unwrap(), + name: alg, + }); + } + } + _ => {} // ignore other keys + } + } + + // Handle the remaining algorithms + if let (Some(alg), Some(typ)) = (current_alg, current_type) { + algorithms.push(AlgorithmInfo { + r#type: typ, + name: alg, + }); + } + + algorithms + }, + max_large_blob_size: info.max_serialized_large_blob_array, + requires_pin_change: info.force_pin_change, + minimum_pin_length: info.min_pin_length, + firmware_version_string: format!( + "{}.{}.{}", + (info.firmware_version >> 16) & 0xFF, + (info.firmware_version >> 8) & 0xFF, + info.firmware_version & 0xFF + ), + max_credential_blob_size: info.max_cred_blob_length, + max_rp_ids_for_min_pin: info.max_rpids_for_set_min_pin_length, + preferred_uv_attempts: info.preferred_platform_uv_attempts, + user_verification_method: match info.uv_modality { + 1 => UserVerificationMethod::Fingerprint, + 2 => UserVerificationMethod::Pin, + 3 => UserVerificationMethod::Biometric, + 4 => UserVerificationMethod::Voice, + 5 => UserVerificationMethod::FaceRecognition, + 0 => UserVerificationMethod::None, + _ => UserVerificationMethod::Other(format!("Unknown({})", info.uv_modality)), + }, + remaining_resident_keys: info.remaining_discoverable_credentials, + }) + } +} + +#[universal_function(desktop_only)] +pub async fn fido2_get_device_info() -> Result { + let device = FidoKeyHidFactory::create(&Cfg::init()) + .map_err(|e| UtilityError::Fido2Error(e.to_string()))?; + let info = device + .get_human_readable_info() + .map_err(|e| UtilityError::Fido2Error(e.to_string()))?; + Ok(info) +} + +#[tauri::command] +pub async fn fido2_register( + app: AppHandle, + params: Fido2RegisterParams, + on_event: Channel, +) -> Result { + // create `challenge` + let challenge = verifier::create_challenge(); + + on_event + .send(Fido2RegisterEvent::Fido2RegisterPinNeeded { + challenge: challenge + .iter() + .map(|byte| format!("{:02x}", byte)) + .collect::(), + }) + .unwrap(); + + let pin = Arc::new(Mutex::new(String::new())); + let pin_clone = pin.clone(); + app.listen("fido2_register_pin_enter", move |event| { + *pin_clone.lock().unwrap() = event.payload().to_string(); + }); + while pin.lock().unwrap().is_empty() { + std::thread::sleep(std::time::Duration::from_millis(100)); + println!("waiting for pin"); + } + + println!("pin received"); + let pin = pin.lock().unwrap().clone(); + + // create `MakeCredentialArgs` + let make_credential_args = MakeCredentialArgsBuilder::new(¶ms.rpid, &challenge) + .pin(&pin) + .build(); + + // create `FidoKeyHid` + let device = FidoKeyHidFactory::create(&Cfg::init()).unwrap(); + + // get `Attestation` Object + let attestation = device + .make_credential_with_args(&make_credential_args) + .unwrap(); + println!("- Register Success"); + + // verify `Attestation` Object + let verify_result = verifier::verify_attestation(¶ms.rpid, &challenge, &attestation); + if !verify_result.is_success { + println!("- ! Verify Failed"); + return Err(UtilityError::Fido2Error("Verify Failed".to_string())); + } + + Ok(verify_result.into()) +} + +#[universal_function(desktop_only)] +pub fn fido2_authenticate( + rpid: &str, + pin: &str, + credential: Fido2Credential, +) -> Result<(), UtilityError> { + // create `challenge` + let challenge = verifier::create_challenge(); + + // create `GetAssertionArgs` + let get_assertion_args = GetAssertionArgsBuilder::new(rpid, &challenge) + .pin(&pin) + .credential_id(&credential.id) + .build(); + + // create `FidoKeyHid` + let device = FidoKeyHidFactory::create(&Cfg::init()).unwrap(); + + // get `Assertion` Object + let assertions = device.get_assertion_with_args(&get_assertion_args).unwrap(); + println!("- Authenticate Success"); + + // verify `Assertion` Object + let public_key: PublicKey = credential.public_key.clone().into(); + if !verifier::verify_assertion(rpid, &public_key, &challenge, &assertions[0]) { + println!("- ! Verify Assertion Failed"); + return Err(UtilityError::Fido2Error( + "Verify Assertion Failed".to_string(), + )); + } + + Ok(()) +} diff --git a/dev-utility/src/core/hardware/hid.rs b/dev-utility/src/core/hardware/hid.rs new file mode 100644 index 0000000..fb81a74 --- /dev/null +++ b/dev-utility/src/core/hardware/hid.rs @@ -0,0 +1,88 @@ +use std::collections::HashMap; + +use crate::error::UtilityError; +use hidapi::{DeviceInfo, HidApi}; +use serde::{Deserialize, Serialize}; +use universal_function_macro::universal_function; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] + +pub struct HidDeviceInfo { + pub vendor_id: u16, + pub product_id: u16, + pub manufacturer_string: Option, + pub product_string: Option, + pub serial_number: Option, + pub path: String, + pub interface_number: i32, + pub usage_page: u16, + pub usage: u16, +} + +impl From for HidDeviceInfo { + fn from(device_info: DeviceInfo) -> Self { + HidDeviceInfo { + vendor_id: device_info.vendor_id(), + product_id: device_info.product_id(), + manufacturer_string: device_info.manufacturer_string().map(|s| s.to_string()), + product_string: device_info.product_string().map(|s| s.to_string()), + serial_number: device_info.serial_number().map(|s| s.to_string()), + path: device_info.path().to_string_lossy().to_string(), + interface_number: device_info.interface_number(), + usage_page: device_info.usage_page(), + usage: device_info.usage(), + } + } +} + +#[derive(Debug)] +pub struct LogicalDevice { + pub vendor_id: u16, + pub product_id: u16, + pub device_path: String, + pub product_name: Option, + pub manufacturer_name: Option, + pub serial_number: Option, + pub interfaces: Vec, +} + +#[derive(Debug)] +pub struct HidInterface { + pub usage_page: u16, + pub usage: u16, + // pub capabilities: Vec, +} + +#[universal_function(desktop_only)] +pub fn list_hid_devices() -> Result, UtilityError> { + let api = HidApi::new().map_err(|e| UtilityError::ApiError(e.to_string()))?; + + let devices = api + .device_list() + .map(|device_info| HidDeviceInfo::from(device_info.clone())) + .collect(); + + Ok(devices) +} + +// pub fn get_logical_devices() -> Result, UtilityError> { +// let devices = list_hid_devices()?; + +// let mut logical_devices: HashMap = HashMap::new(); + +// for device in devices { +// let device_path = device.path; +// let logical_device = logical_devices.entry(device_path).or_insert(LogicalDevice { +// vendor_id: device.vendor_id, +// product_id: device.product_id, +// device_path: device.path, +// product_name: device.product_string, +// manufacturer_name: device.manufacturer_string, +// serial_number: device.serial_number, +// interfaces: Vec::new(), +// }); +// } + +// // Ok(logical_devices.values().cloned().collect()) +// } diff --git a/dev-utility/src/core/hardware/mod.rs b/dev-utility/src/core/hardware/mod.rs new file mode 100644 index 0000000..c4101c6 --- /dev/null +++ b/dev-utility/src/core/hardware/mod.rs @@ -0,0 +1,5 @@ +pub mod hid; +pub use hid::*; + +pub mod fido2; +pub use fido2::*; \ No newline at end of file diff --git a/dev-utility/src/core/mod.rs b/dev-utility/src/core/mod.rs index d05eb88..44ee03e 100644 --- a/dev-utility/src/core/mod.rs +++ b/dev-utility/src/core/mod.rs @@ -15,4 +15,5 @@ pub mod codec; pub mod cryptography; pub mod formatter; pub mod generator; -pub mod network; \ No newline at end of file +pub mod network; +pub mod hardware; \ No newline at end of file diff --git a/dev-utility/src/error.rs b/dev-utility/src/error.rs index 40ca19f..de4cfc4 100644 --- a/dev-utility/src/error.rs +++ b/dev-utility/src/error.rs @@ -23,6 +23,10 @@ pub enum UtilityError { ParseError(String), #[error("Invalid input: {0}")] InvalidInput(String), + #[error("API error: {0}")] + ApiError(String), + #[error("Fido2 error: {0}")] + Fido2Error(String), } impl serde::Serialize for UtilityError { diff --git a/package.json b/package.json index 5e65c52..59a6cff 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "web": "pnpm --filter @dev-utility/frontend dev:wasm" }, "dependencies": { + "@tauri-apps/plugin-dialog": "~2.3.0", "react": "^19.1.0", "react-dom": "^19.1.0" }, diff --git a/packages/frontend/src/components/tree-view.tsx b/packages/frontend/src/components/tree-view.tsx new file mode 100644 index 0000000..fcfcee3 --- /dev/null +++ b/packages/frontend/src/components/tree-view.tsx @@ -0,0 +1,495 @@ +import * as AccordionPrimitive from "@radix-ui/react-accordion"; +import { cva } from "class-variance-authority"; +import { ChevronRight } from "lucide-react"; +import React from "react"; +import { cn } from "@/lib/utils"; + +const treeVariants = cva( + "group hover:before:opacity-100 before:absolute before:rounded-lg before:left-0 px-2 before:w-full before:opacity-0 before:bg-accent/70 before:h-[2rem] before:-z-10", +); + +const selectedTreeVariants = cva( + "before:opacity-100 before:bg-accent/70 text-accent-foreground", +); + +const dragOverVariants = cva( + "before:opacity-100 before:bg-primary/20 text-primary-foreground", +); + +interface TreeDataItem { + id: string; + name: string; + icon?: any; + selectedIcon?: any; + openIcon?: any; + children?: TreeDataItem[]; + actions?: React.ReactNode; + onClick?: () => void; + draggable?: boolean; + droppable?: boolean; + disabled?: boolean; +} + +type TreeProps = React.HTMLAttributes & { + data: TreeDataItem[] | TreeDataItem; + initialSelectedItemId?: string; + onSelectChange?: (item: TreeDataItem | undefined) => void; + expandAll?: boolean; + defaultNodeIcon?: any; + defaultLeafIcon?: any; + onDocumentDrag?: (sourceItem: TreeDataItem, targetItem: TreeDataItem) => void; +}; + +const TreeView = React.forwardRef( + ( + { + data, + initialSelectedItemId, + onSelectChange, + expandAll, + defaultLeafIcon, + defaultNodeIcon, + className, + onDocumentDrag, + ...props + }, + ref, + ) => { + const [selectedItemId, setSelectedItemId] = React.useState< + string | undefined + >(initialSelectedItemId); + + const [draggedItem, setDraggedItem] = React.useState( + null, + ); + + const handleSelectChange = React.useCallback( + (item: TreeDataItem | undefined) => { + setSelectedItemId(item?.id); + if (onSelectChange) { + onSelectChange(item); + } + }, + [onSelectChange], + ); + + const handleDragStart = React.useCallback((item: TreeDataItem) => { + setDraggedItem(item); + }, []); + + const handleDrop = React.useCallback( + (targetItem: TreeDataItem) => { + if (draggedItem && onDocumentDrag && draggedItem.id !== targetItem.id) { + onDocumentDrag(draggedItem, targetItem); + } + setDraggedItem(null); + }, + [draggedItem, onDocumentDrag], + ); + + const expandedItemIds = React.useMemo(() => { + if (!initialSelectedItemId) { + return [] as string[]; + } + + const ids: string[] = []; + + function walkTreeItems( + items: TreeDataItem[] | TreeDataItem, + targetId: string, + ) { + if (items instanceof Array) { + for (let i = 0; i < items.length; i++) { + ids.push(items[i]!.id); + if (walkTreeItems(items[i]!, targetId) && !expandAll) { + return true; + } + if (!expandAll) ids.pop(); + } + } else if (!expandAll && items.id === targetId) { + return true; + } else if (items.children) { + return walkTreeItems(items.children, targetId); + } + } + + walkTreeItems(data, initialSelectedItemId); + return ids; + }, [data, expandAll, initialSelectedItemId]); + + return ( +
+ +
{ + handleDrop({ id: "", name: "parent_div" }); + }} + >
+
+ ); + }, +); +TreeView.displayName = "TreeView"; + +type TreeItemProps = TreeProps & { + selectedItemId?: string; + handleSelectChange: (item: TreeDataItem | undefined) => void; + expandedItemIds: string[]; + defaultNodeIcon?: any; + defaultLeafIcon?: any; + handleDragStart?: (item: TreeDataItem) => void; + handleDrop?: (item: TreeDataItem) => void; + draggedItem: TreeDataItem | null; +}; + +const TreeItem = React.forwardRef( + ( + { + className, + data, + selectedItemId, + handleSelectChange, + expandedItemIds, + defaultNodeIcon, + defaultLeafIcon, + handleDragStart, + handleDrop, + draggedItem, + ...props + }, + ref, + ) => { + if (!(data instanceof Array)) { + data = [data]; + } + return ( +
+
    + {data.map((item) => ( +
  • + {item.children ? ( + + ) : ( + + )} +
  • + ))} +
+
+ ); + }, +); +TreeItem.displayName = "TreeItem"; + +const TreeNode = ({ + item, + handleSelectChange, + expandedItemIds, + selectedItemId, + defaultNodeIcon, + defaultLeafIcon, + handleDragStart, + handleDrop, + draggedItem, +}: { + item: TreeDataItem; + handleSelectChange: (item: TreeDataItem | undefined) => void; + expandedItemIds: string[]; + selectedItemId?: string; + defaultNodeIcon?: any; + defaultLeafIcon?: any; + handleDragStart?: (item: TreeDataItem) => void; + handleDrop?: (item: TreeDataItem) => void; + draggedItem: TreeDataItem | null; +}) => { + const [value, setValue] = React.useState( + expandedItemIds.includes(item.id) ? [item.id] : [], + ); + const [isDragOver, setIsDragOver] = React.useState(false); + + const onDragStart = (e: React.DragEvent) => { + if (!item.draggable) { + e.preventDefault(); + return; + } + e.dataTransfer.setData("text/plain", item.id); + handleDragStart?.(item); + }; + + const onDragOver = (e: React.DragEvent) => { + if (item.droppable !== false && draggedItem && draggedItem.id !== item.id) { + e.preventDefault(); + setIsDragOver(true); + } + }; + + const onDragLeave = () => { + setIsDragOver(false); + }; + + const onDrop = (e: React.DragEvent) => { + e.preventDefault(); + setIsDragOver(false); + handleDrop?.(item); + }; + + return ( + setValue(s)} + > + + { + handleSelectChange(item); + item.onClick?.(); + }} + draggable={!!item.draggable} + onDragStart={onDragStart} + onDragOver={onDragOver} + onDragLeave={onDragLeave} + onDrop={onDrop} + > + + {item.name} + + {item.actions} + + + + + + + + ); +}; + +const TreeLeaf = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & { + item: TreeDataItem; + selectedItemId?: string; + handleSelectChange: (item: TreeDataItem | undefined) => void; + defaultLeafIcon?: any; + handleDragStart?: (item: TreeDataItem) => void; + handleDrop?: (item: TreeDataItem) => void; + draggedItem: TreeDataItem | null; + } +>( + ( + { + className, + item, + selectedItemId, + handleSelectChange, + defaultLeafIcon, + handleDragStart, + handleDrop, + draggedItem, + ...props + }, + ref, + ) => { + const [isDragOver, setIsDragOver] = React.useState(false); + + const onDragStart = (e: React.DragEvent) => { + if (!item.draggable || item.disabled) { + e.preventDefault(); + return; + } + e.dataTransfer.setData("text/plain", item.id); + handleDragStart?.(item); + }; + + const onDragOver = (e: React.DragEvent) => { + if ( + item.droppable !== false && + !item.disabled && + draggedItem && + draggedItem.id !== item.id + ) { + e.preventDefault(); + setIsDragOver(true); + } + }; + + const onDragLeave = () => { + setIsDragOver(false); + }; + + const onDrop = (e: React.DragEvent) => { + if (item.disabled) return; + e.preventDefault(); + setIsDragOver(false); + handleDrop?.(item); + }; + + return ( +
{ + if (item.disabled) return; + handleSelectChange(item); + item.onClick?.(); + }} + draggable={!!item.draggable && !item.disabled} + onDragStart={onDragStart} + onDragOver={onDragOver} + onDragLeave={onDragLeave} + onDrop={onDrop} + {...props} + > + + {item.name} + + {item.actions} + +
+ ); + }, +); +TreeLeaf.displayName = "TreeLeaf"; + +const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + svg]:rotate-90", + className, + )} + {...props} + > + + {children} + + +)); +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName; + +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +
{children}
+
+)); +AccordionContent.displayName = AccordionPrimitive.Content.displayName; + +const TreeIcon = ({ + item, + isOpen, + isSelected, + default: defaultIcon, +}: { + item: TreeDataItem; + isOpen?: boolean; + isSelected?: boolean; + default?: any; +}) => { + let Icon = defaultIcon; + if (isSelected && item.selectedIcon) { + Icon = item.selectedIcon; + } else if (isOpen && item.openIcon) { + Icon = item.openIcon; + } else if (item.icon) { + Icon = item.icon; + } + return Icon ? : <>; +}; + +const TreeActions = ({ + children, + isSelected, +}: { + children: React.ReactNode; + isSelected: boolean; +}) => { + return ( +
+ {children} +
+ ); +}; + +export { TreeView, type TreeDataItem }; diff --git a/packages/frontend/src/locales/en-US/messages.po b/packages/frontend/src/locales/en-US/messages.po index efe6021..154f574 100644 --- a/packages/frontend/src/locales/en-US/messages.po +++ b/packages/frontend/src/locales/en-US/messages.po @@ -424,3 +424,67 @@ msgstr "windows" #: src/utilities/cryptography/oath/totp.tsx:230 msgid "You must verify that the algorithm you want to use is supported by the application your clients might be using." msgstr "You must verify that the algorithm you want to use is supported by the application your clients might be using." + +#: src/utilities/meta.ts:233 +msgid "Hardware" +msgstr "Hardware" + +#: src/utilities/meta.ts:239 +msgid "HID Devices" +msgstr "HID Devices" + +#: src/utilities/hardware/hid.tsx:61 +msgid "Refresh" +msgstr "Refresh" + +#: src/utilities/hardware/hid.tsx:67 +msgid "Error: {0}" +msgstr "Error: {0}" + +#: src/utilities/hardware/hid.tsx:79 +msgid "No HID devices found" +msgstr "No HID devices found" + +#: src/utilities/hardware/hid.tsx:87 +msgid "Device Info" +msgstr "Device Info" + +#: src/utilities/hardware/hid.tsx:91 +msgid "VID" +msgstr "VID" + +#: src/utilities/hardware/hid.tsx:95 +msgid "PID" +msgstr "PID" + +#: src/utilities/hardware/hid.tsx:99 +msgid "Serial Number" +msgstr "Serial Number" + +#: src/utilities/hardware/hid.tsx:103 +msgid "Interface" +msgstr "Interface" + +#: src/utilities/hardware/hid.tsx:107 +msgid "Usage" +msgstr "Usage" + +#: src/utilities/hardware/hid.tsx:111 +msgid "Device Path" +msgstr "Device Path" + +#: src/utilities/hardware/hid.tsx:122 +msgid "Unknown Product" +msgstr "Unknown Product" + +#: src/utilities/hardware/hid.tsx:125 +msgid "Unknown Manufacturer" +msgstr "Unknown Manufacturer" + +#: src/utilities/hardware/hid.tsx:146 +msgid "None" +msgstr "None" + +#: src/utilities/hardware/hid.tsx:57 +msgid "{0} devices" +msgstr "{0} devices" diff --git a/packages/frontend/src/locales/zh-CN/messages.po b/packages/frontend/src/locales/zh-CN/messages.po index 0d9d3a5..33ea899 100644 --- a/packages/frontend/src/locales/zh-CN/messages.po +++ b/packages/frontend/src/locales/zh-CN/messages.po @@ -430,3 +430,67 @@ msgstr "" msgid "You must verify that the algorithm you want to use is supported by the application your clients might be using." msgstr "" +#: src/utilities/meta.ts:233 +msgid "Hardware" +msgstr "硬件" + +#: src/utilities/meta.ts:239 +msgid "HID Devices" +msgstr "HID 设备" + +#: src/utilities/hardware/hid.tsx:61 +msgid "Refresh" +msgstr "刷新" + +#: src/utilities/hardware/hid.tsx:67 +msgid "Error: {0}" +msgstr "错误:{0}" + +#: src/utilities/hardware/hid.tsx:79 +msgid "No HID devices found" +msgstr "未找到 HID 设备" + +#: src/utilities/hardware/hid.tsx:87 +msgid "Device Info" +msgstr "设备信息" + +#: src/utilities/hardware/hid.tsx:91 +msgid "VID" +msgstr "VID" + +#: src/utilities/hardware/hid.tsx:95 +msgid "PID" +msgstr "PID" + +#: src/utilities/hardware/hid.tsx:99 +msgid "Serial Number" +msgstr "序列号" + +#: src/utilities/hardware/hid.tsx:103 +msgid "Interface" +msgstr "接口" + +#: src/utilities/hardware/hid.tsx:107 +msgid "Usage" +msgstr "用途" + +#: src/utilities/hardware/hid.tsx:111 +msgid "Device Path" +msgstr "设备路径" + +#: src/utilities/hardware/hid.tsx:122 +msgid "Unknown Product" +msgstr "未知产品" + +#: src/utilities/hardware/hid.tsx:125 +msgid "Unknown Manufacturer" +msgstr "未知制造商" + +#: src/utilities/hardware/hid.tsx:146 +msgid "None" +msgstr "无" + +#: src/utilities/hardware/hid.tsx:57 +msgid "{0} devices" +msgstr "{0} 设备" + diff --git a/packages/frontend/src/utilities/cryptography/fido/fido2.tsx b/packages/frontend/src/utilities/hardware/fido2.tsx similarity index 73% rename from packages/frontend/src/utilities/cryptography/fido/fido2.tsx rename to packages/frontend/src/utilities/hardware/fido2.tsx index 8de6b4e..e1785f8 100644 --- a/packages/frontend/src/utilities/cryptography/fido/fido2.tsx +++ b/packages/frontend/src/utilities/hardware/fido2.tsx @@ -15,6 +15,7 @@ import { msg } from "@lingui/core/macro"; import { useLingui } from "@lingui/react/macro"; +import { Channel, invoke } from "@tauri-apps/api/core"; import { CheckCircle, Copy, @@ -31,7 +32,7 @@ import { SmartphoneIcon, Unlock, } from "lucide-react"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; @@ -55,6 +56,13 @@ import { Separator } from "@/components/ui/separator"; import { Switch } from "@/components/ui/switch"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Textarea } from "@/components/ui/textarea"; +import { useUtilityInvoke } from "../invoke"; +import { + type Fido2DeviceInfo, + Fido2SupportedAlgorithm, + InvokeFunction, +} from "../types"; +import { useFido2Register } from "./hook"; enum Fido2Tab { Authenticator = "authenticator", @@ -64,8 +72,17 @@ enum Fido2Tab { Settings = "settings", } -// FIDO2 Authenticator Simulator -function AuthenticatorSimulator() { +// FIDO2 Authenticator +function Authenticator() { + const { data, trigger } = useUtilityInvoke( + InvokeFunction.Fido2GetDeviceInfo, + { + onSuccess: (data) => { + console.log(data); + }, + }, + ); + const { t } = useLingui(); const [isAuthenticatorActive, setIsAuthenticatorActive] = useState(false); const [pin, setPin] = useState(""); @@ -98,7 +115,7 @@ function AuthenticatorSimulator() { }; return ( -
+
{/* Authenticator Status */} @@ -109,8 +126,18 @@ function AuthenticatorSimulator() { {t(msg`Manage your FIDO2 authenticator device`)} + - + {/*
- + */} {/* Device Information */} @@ -197,45 +224,209 @@ function AuthenticatorSimulator() { -
-
- {t(msg`AAGUID`)} -

- 550e8400-e29b-41d4-a716-446655440000 -

-
-
- - {t(msg`Protocol Version`)} - -

FIDO_2_0

-
-
- - {t(msg`User Verification`)} - - Supported -
-
- - {t(msg`Resident Keys`)} - - Supported -
-
+ {data ? ( + <> +
+
+ + {t(msg`AAGUID`)} + +

+ {data.aaguid || "N/A"} +

+
+
+ + {t(msg`Firmware Version`)} + +

+ {data.firmwareVersionString || "N/A"} +

+
+
+ + {t(msg`User Verification`)} + + + {data.capabilities?.clientPin + ? t(msg`Supported`) + : t(msg`Not Supported`)} + +
+
+ + {t(msg`Resident Keys`)} + + + {data.capabilities?.rk + ? t(msg`Supported`) + : t(msg`Not Supported`)} + +
+
+ + {t(msg`Min PIN Length`)} + +

+ {data.minimumPinLength || "N/A"} +

+
+
+ + {t(msg`Max Credentials`)} + +

+ {data.maxCredentialsPerList || "N/A"} +

+
+
- + + +
+ +
+ {data.supportedVersions?.map((version: string) => ( + + {version} + + )) || ( + N/A + )} +
+
-
- -
- ES256 - RS256 - PS256 +
+ +
+ {data.communicationMethods?.map((method: string) => ( + + {method.toUpperCase()} + + )) || ( + N/A + )} +
+
+ +
+ +
+ {data.supportedAlgorithms?.map((alg: any, index: number) => { + // Map algorithm names to readable format + const algName = + alg.name === "-7" + ? "ES256" + : alg.name === "-8" + ? "EdDSA" + : alg.name === "-257" + ? "RS256" + : alg.name === "-37" + ? "PS256" + : `ALG ${alg.name}`; + return ( + + {algName} + + ); + }) || ( + N/A + )} +
+
+ +
+ +
+ {data.availableExtensions?.map((ext: string) => ( + + {ext} + + )) || ( + N/A + )} +
+
+ +
+ +
+
+ + {t(msg`User Presence`)} + + + {data.capabilities?.up ? t(msg`Yes`) : t(msg`No`)} + +
+
+ + {t(msg`Platform`)} + + + {data.capabilities?.plat ? t(msg`Yes`) : t(msg`No`)} + +
+
+ + {t(msg`Credential Mgmt`)} + + + {data.capabilities?.credentialMgmtPreview + ? t(msg`Yes`) + : t(msg`No`)} + +
+
+ + {t(msg`PIN Required`)} + + + {data.requiresPinChange ? t(msg`Yes`) : t(msg`No`)} + +
+
+
+ + ) : ( +
+ +

{t(msg`No device information available`)}

+

+ {t(msg`Click refresh to load device data`)} +

-
+ )}
@@ -244,37 +435,23 @@ function AuthenticatorSimulator() { // FIDO2 Registration function Registration() { + const { data, trigger, onEvent } = useFido2Register(); const { t } = useLingui(); + const [relyingPartyId, setRelyingPartyId] = useState("example.com"); + const [userId, setUserId] = useState(""); const [userName, setUserName] = useState(""); const [userDisplayName, setUserDisplayName] = useState(""); + const [algorithm, setAlgorithm] = useState("ES256"); const [residentKey, setResidentKey] = useState(false); const [userVerification, setUserVerification] = useState("preferred"); const [attestation, setAttestation] = useState("none"); const handleRegistration = async () => { - // TODO: Implement FIDO2 registration - // await invoke(InvokeFunction.Fido2Registration, { - // relyingPartyId, - // userId, - // userName, - // userDisplayName, - // algorithm, - // residentKey, - // userVerification, - // attestation, - // }); - console.log("FIDO2 Registration:", { - relyingPartyId, - userId, - userName, - userDisplayName, - algorithm, - residentKey, - userVerification, - attestation, + trigger({ + rpid: relyingPartyId, }); }; @@ -292,46 +469,47 @@ function Registration() { -
- - setRelyingPartyId(e.target.value)} - /> -
- -
- - setUserId(e.target.value)} - /> -
- -
- - setUserName(e.target.value)} - /> +
+
+ + setRelyingPartyId(e.target.value)} + /> +
+
+
+ + setUserId(e.target.value)} + /> +
+
+ + setUserName(e.target.value)} + /> +
-
- - setUserDisplayName(e.target.value)} - /> +
+ + setUserDisplayName(e.target.value)} + /> +
-