From 92a37a3b335aaa7781b8b072fbcca48610639bf9 Mon Sep 17 00:00:00 2001 From: Token2 Date: Tue, 8 Jul 2025 08:59:02 +0200 Subject: [PATCH] Update gui-mac.py --- gui-mac.py | 880 ++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 637 insertions(+), 243 deletions(-) diff --git a/gui-mac.py b/gui-mac.py index 9fd751f..05ab889 100644 --- a/gui-mac.py +++ b/gui-mac.py @@ -2,15 +2,20 @@ import os import re import subprocess import sys +import tempfile import tkinter as tk from tkinter import messagebox, simpledialog, ttk -# --- Path Resolution for pyinstaller Bundle --- -def get_fido_command_path(): + + +# --- Path Resolution for fido2-token2 Binary --- +def get_fido2_binary_path(): """ Determines the absolute path to the fido2-token2 binary, whether running - as a script or as a bundled pyinstaller app. Enhanced for self-contained builds. + as a script or as a bundled pyinstaller app. """ + binary_name = "fido2-token2" + if getattr(sys, 'frozen', False): # We are running in a PyInstaller bundle if hasattr(sys, '_MEIPASS'): @@ -20,325 +25,714 @@ def get_fido_command_path(): # PyInstaller one-file bundle base_path = os.path.dirname(sys.executable) - # Print debug info for troubleshooting - print(f"[DEBUG] Running in bundle mode, base_path: {base_path}") - print(f"[DEBUG] Contents of base_path: {os.listdir(base_path) if os.path.exists(base_path) else 'NOT FOUND'}") + # Try direct path first + binary_path = os.path.join(base_path, binary_name) + if os.path.exists(binary_path): + return binary_path - # Primary locations to check for the fido2-token2 binary - binary_locations = [ - os.path.join(base_path, "fido2-token2"), - os.path.join(base_path, "Contents", "MacOS", "fido2-token2"), - os.path.join(base_path, "Contents", "Frameworks", "fido2-token2"), - os.path.join(base_path, "MacOS", "fido2-token2"), - os.path.join(base_path, "Frameworks", "fido2-token2"), - os.path.join(os.path.dirname(base_path), "fido2-token2"), - os.path.join(os.path.dirname(base_path), "Frameworks", "fido2-token2"), - os.path.join(os.path.dirname(base_path), "MacOS", "fido2-token2") - ] + # Try alternative locations in the bundle + for alt_path in [ + os.path.join(base_path, "Contents", "MacOS", binary_name), + os.path.join(base_path, "Contents", "Frameworks", binary_name), + os.path.join(base_path, "MacOS", binary_name), + os.path.join(base_path, "Frameworks", binary_name), + os.path.join(os.path.dirname(base_path), binary_name), + os.path.join(os.path.dirname(base_path), "MacOS", binary_name), + os.path.join(os.path.dirname(base_path), "Frameworks", binary_name) + ]: + if os.path.exists(alt_path): + return alt_path - for binary_path in binary_locations: - print(f"[DEBUG] Checking for binary: {binary_path}") - if os.path.exists(binary_path): - print(f"[DEBUG] Found fido2-token2 binary at: {binary_path}") - return binary_path - - # Final fallback - return first location even if not found - print(f"[DEBUG] Binary not found, returning default: {binary_locations[0]}") - return binary_locations[0] + return binary_path # Return the expected path even if not found else: # We are running in a normal Python environment for development. - # Look for fido2-token2 binary in common development locations script_dir = os.path.dirname(os.path.abspath(__file__)) - - dev_locations = [ - os.path.join(script_dir, "build", "tools", "fido2-token2"), - os.path.join(script_dir, "tools", "fido2-token2"), - os.path.join(script_dir, "fido2-token2"), - "./build/tools/fido2-token2", - "./tools/fido2-token2", - "./fido2-token2", - "fido2-token2" # System PATH lookup + + # Try various development locations + dev_paths = [ + os.path.join(script_dir, "build", "staging", binary_name), + os.path.join(script_dir, "staging", binary_name), + os.path.join(script_dir, "build", "tools", binary_name), + os.path.join(script_dir, "tools", binary_name), + os.path.join(script_dir, binary_name) ] - - for binary_path in dev_locations: - if os.path.exists(binary_path): - print(f"[DEBUG] Found development binary at: {binary_path}") - return binary_path - # Fallback to system-wide installation - print(f"[DEBUG] Using system PATH lookup for fido2-token2") - return "fido2-token2" + for path in dev_paths: + if os.path.exists(path): + return path -print("FIDO2 Manager GUI starting...") -FIDO_COMMAND = get_fido_command_path() -print(f"FIDO2 Command Path: {FIDO_COMMAND}") -print(f"Script exists: {os.path.exists(FIDO_COMMAND)}") + # Fallback to PATH lookup + print(binary_name) + return binary_name -def get_device_number_from_string(device_string): - """ - Extract device number from device string format "Device [1] : SoloKeys" - """ - import re - match = re.search(r'Device \[(\d+)\]', device_string) - if match: - return match.group(1) - return "1" # Default to device 1 -# --- End of Path Resolution --- +# Define the command to execute directly +FIDO2_TOKEN_CMD = get_fido2_binary_path() -# Global variable to store the PIN for the current session +# Checks the terminal emulator from which "gui.py" is executed +# and sets it for the subprocess commands +TERM = os.environ.get("TERM", "x-terminal-emulator") + +# Command below for Windows +# FIDO2_TOKEN_CMD = 'fido2-token2.exe' + +# Global variables PIN = None +device_strings = [] # Store actual device strings for fido2-token2 -# --- Core Functions --- def get_device_list(): - """Gets the list of connected FIDO devices by calling the C binary directly.""" + """Get device list directly from fido2-token2""" + global device_strings try: - if not os.path.exists(FIDO_COMMAND): - messagebox.showerror("Dependency Error", f"Required tool not found at: {FIDO_COMMAND}") - return [] - - # Call fido2-token2 -L directly to list devices - result = subprocess.run([FIDO_COMMAND, "-L"], capture_output=True, text=True, check=True) + # Execute fido2-token2 -L to list devices + result = subprocess.run([FIDO2_TOKEN_CMD, "-L"], capture_output=True, text=True) + + if result.returncode != 0: + print(f"Error executing {FIDO2_TOKEN_CMD} -L: {result.stderr}") + return [] - # Parse the output to format like the shell script device_list = [] + device_strings = [] device_count = 1 - for line in result.stdout.strip().split("\n"): - if line.strip(): - # Extract device info (look for parentheses with device type) - import re - match = re.search(r'\(([^)]+)\)', line) - if match: - device_type = match.group(1) - device_list.append(f"Device [{device_count}] : {device_type}") - device_count += 1 + for line in result.stdout.strip().split('\n'): + if line.strip() and ':' in line: + # Handle pcsc devices specially + if 'pcsc://slot0:' in line: + device_string = "pcsc://slot0" + device_description = "pcsc://slot0" else: - # Fallback if no parentheses found - device_list.append(f"Device [{device_count}] : {line.strip()}") - device_count += 1 + # Extract device string - everything before the first space or tab + parts = line.split() + if parts: + device_string = parts[0] + # For ioreg devices, ensure we don't have trailing colon + if device_string.endswith(':'): + device_string = device_string[:-1] + else: + # Fallback: split by colon and reconstruct + colon_parts = line.split(':') + if len(colon_parts) >= 2: + device_string = ':'.join(colon_parts[:2]) + else: + device_string = line.strip() + + # Extract device description for display + match = re.search(r'\(([^)]+)\)', line) + if match: + device_description = match.group(1) + else: + device_description = device_string + + device_strings.append(device_string) + device_list.append(f"Device [{device_count}] : {device_description}") + device_count += 1 return device_list + except Exception as e: - error_message = f"Error getting device list: {e}\n\nDetails:\n{getattr(e, 'stderr', '')}" - messagebox.showerror("Execution Error", error_message) + print(f"Error executing device list command: {e}") return [] -def get_device_path_by_number(device_number): - """Get the actual device path string for a given device number.""" + +def get_device_string(device_digit): + """Get the actual device string for fido2-token2 command""" try: - # Get device list from fido2-token2 -L - result = subprocess.run([FIDO_COMMAND, "-L"], capture_output=True, text=True, check=True) - lines = [line.strip() for line in result.stdout.split('\n') if line.strip()] - - # Convert device_number to index (1-based to 0-based) - device_index = int(device_number) - 1 - - if 0 <= device_index < len(lines): - device_line = lines[device_index] - # Extract device path (everything before the first colon and space) - # Handle both "path: info" and "path:info" formats - if ':' in device_line: - # Split on ':' and take first part, but handle "pcsc://slot0:" specially - if device_line.startswith('pcsc://'): - return "pcsc://slot0" - else: - # For other formats like "/dev/hidraw0: ..." - parts = device_line.split(':', 1) - return parts[0].strip() - else: - # If no colon, return the whole line - return device_line - + device_index = int(device_digit) - 1 + if 0 <= device_index < len(device_strings): + return device_strings[device_index] return None - except Exception as e: - print(f"[ERROR] Failed to get device path: {e}") + except (ValueError, IndexError): return None -def execute_info_command(device_string): - """Fetches and displays device info in the treeview.""" - tree.delete(*tree.get_children()) - # Extract device number from device_string - device_number = get_device_number_from_string(device_string) - - # Get the actual device path for this device number - device_path = get_device_path_by_number(device_number) - if not device_path: - messagebox.showerror("Error", f"Could not find device path for device {device_number}") + +def execute_storage_command(device_digit): + """Execute storage command directly with fido2-token2""" + global PIN + device_string = get_device_string(device_digit) + if not device_string: + messagebox.showerror("Error", "Invalid device selection") return - print(f"[DEBUG] Using device path: {device_path}") + # Build command with PIN option if provided + command = [FIDO2_TOKEN_CMD, "-I", "-c"] + if PIN and PIN != "0000": + command.extend(["-w", PIN]) + command.append(device_string) - # Build PIN arguments for fido2-token2 (-w pin) - pin_args = ["-w", PIN] if PIN else [] + try: + result = subprocess.run(command, capture_output=True, text=True) + + if result.returncode == 0: + # Parse the output and insert into the treeview + for line in reversed(result.stdout.splitlines()): + if ": " in line: + key, value = line.split(": ", 1) + tree.insert("", 0, values=(key, value)) + else: + raise subprocess.CalledProcessError(result.returncode, command) + except Exception as e: + messagebox.showerror("Error", f"Command execution failed: {e}\nOutput: {result.stderr}") - # Commands to run with fido2-token2 directly - commands_to_run = { - "info": [FIDO_COMMAND, "-I"] + pin_args + [device_path], - "storage": [FIDO_COMMAND, "-I", "-c"] + pin_args + [device_path], - } - for key, command in commands_to_run.items(): - try: - print(f"[DEBUG] Running command: {' '.join(command)}") - result = subprocess.run(command, capture_output=True, text=True) - - if result.returncode != 0: - print(f"[DEBUG] Command {key} failed with code {result.returncode}") - print(f"[DEBUG] stderr: {result.stderr}") +def execute_info_command(device_digit): + """Execute info command directly with fido2-token2""" + global PIN + tree.delete(*tree.get_children()) + + device_string = get_device_string(device_digit) + if not device_string: + messagebox.showerror("Error", "Invalid device selection") + return + + # First execute storage command + storage_command = [FIDO2_TOKEN_CMD, "-I", "-c"] + if PIN and PIN != "0000": + storage_command.extend(["-w", PIN]) + storage_command.append(device_string) + + try: + result = subprocess.run(storage_command, capture_output=True, text=True) + + # Check for specific FIDO errors + if "FIDO_ERR_PIN_INVALID" in result.stderr: + messagebox.showerror("Error", "Invalid PIN provided") + return + if "FIDO_ERR_INVALID_ARGUMENT" in result.stderr: + messagebox.showerror("Error", "Invalid PIN provided") + return - if "FIDO_ERR_PIN_INVALID" in result.stderr: - messagebox.showerror("Error", "Invalid PIN provided.") - return # Stop processing on bad PIN - if "FIDO_ERR_PIN_REQUIRED" in result.stderr: - messagebox.showerror("Error", "A PIN is required for this operation.") - return # Stop processing - continue # Try next command even if one fails for other reasons - # Parse output and add to tree + if "FIDO_ERR_PIN_AUTH_BLOCKED" in result.stderr: + messagebox.showerror("Error", "Wrong PIN provided too many times. Reinsert the key") + return + + if "FIDO_ERR_PIN_REQUIRED" in result.stderr: + messagebox.showerror("Error", + "No PIN set for this key. Passkeys can be managed only with a PIN set. " + "You will be prompted to create a PIN on the next window") + set_pin_command = [FIDO2_TOKEN_CMD, "-S", device_string] + cmd_str = " ".join(set_pin_command) + + # macOS: use AppleScript to run in Terminal + apple_script = f''' + tell application "Terminal" + activate + do script "{cmd_str}" + end tell + ''' + subprocess.Popen(["osascript", "-e", apple_script]) + return + + if "FIDO_ERR_INVALID_CBOR" in result.stderr: + messagebox.showerror("Error", + "This is an older key (probably FIDO2.0). No passkey management is possible " + "with this key. Only basic information will be shown.") + + if "FIDO_ERR_INTERNAL" in result.stderr: + messagebox.showerror("Error", + "Internal error communicating with the device. Please try unplugging and " + "replugging the device, then refresh the device list.") + return + + if result.returncode == 0: + # Parse storage output for line in result.stdout.splitlines(): if ": " in line: - k, v = line.split(": ", 1) - tree.insert("", tk.END, values=(k.strip(), v.strip())) - elif line.strip(): - # For lines without colon, add as single value - tree.insert("", tk.END, values=(line.strip(), "")) - - except Exception as e: - messagebox.showerror("Error", f"Command '{key}' failed: {e}\nOutput: {getattr(e, 'stderr', '')}") + key, value = line.split(": ", 1) + tree.insert("", tk.END, values=(key, value)) + else: + raise subprocess.CalledProcessError(result.returncode, storage_command) + + except Exception as e: + messagebox.showerror("Error", f"Storage command execution failed: {e}\nOutput: {result.stderr}") + return + + # Now execute info command + info_command = [FIDO2_TOKEN_CMD, "-I"] + if PIN and PIN != "0000": + info_command.extend(["-w", PIN]) + info_command.append(device_string) + + try: + result = subprocess.run(info_command, capture_output=True, text=True) + + if result.returncode == 0: + # Parse info output + for line in result.stdout.splitlines(): + if ": " in line: + key, value = line.split(": ", 1) + tree.insert("", tk.END, values=(key, value)) + else: + raise subprocess.CalledProcessError(result.returncode, info_command) + + except Exception as e: + messagebox.showerror("Error", f"Info command execution failed: {e}\nOutput: {result.stderr}") - check_passkeys_button_state() - check_changepin_button_state() -# --- UI Event Handlers --- - -def set_pin_and_get_info(device_string): - """Prompts for PIN and then fetches device info.""" +def set_pin(): + """Set the PIN global variable""" global PIN - PIN = simpledialog.askstring("PIN Code", "Enter PIN code:", show="*") - if PIN is not None: # Proceed even if PIN is empty, but not if cancelled - execute_info_command(device_string) + PIN = simpledialog.askstring( + "PIN Code", "Enter PIN code (enter 0000 if no PIN is set/known):", show="*" + ) + def on_device_selected(event): - """Handles the device selection from the dropdown.""" - selected_text = device_var.get() - # Robust regex to find device paths on Linux, macOS, or Windows - match = re.search(r"(/dev/\w+)|(pcsc://\S+)|(windows://\S+)|(ioreg://\S+)", selected_text) + """Handle device selection event""" + global PIN + selected_device = device_var.get() + # Extract the digit inside the first pair of square brackets + match = re.search(r"\[(\d+)\]", selected_device) + PIN = None + + set_pin() + if match: - device_string = match.group(0).split(':')[0] # Get the path part before any colon - set_pin_and_get_info(device_string) + device_digit = match.group(1) + if PIN is not None: + execute_info_command(device_digit) + check_passkeys_button_state() + check_changepin_button_state() else: - messagebox.showerror("Device Error", "Could not parse device path from selection.") + messagebox.showinfo("Device Selected", "No digit found in the selected device") + + +def check_passkeys_button_state(): + """Check if the passkeys button should be enabled""" + passkeys_button_state = tk.DISABLED + for child in tree.get_children(): + values = tree.item(child, "values") + if values and len(values) == 2 and values[0] == "existing rk(s)": + try: + rk_count = int(values[1]) + if rk_count > 0: + passkeys_button_state = tk.NORMAL + break + except ValueError: + pass + + passkeys_button.config(state=passkeys_button_state) + + +def check_changepin_button_state(): + """Check if the change PIN button should be enabled""" + change_pin_button_state = tk.DISABLED + for child in tree.get_children(): + values = tree.item(child, "values") + if values and len(values) == 2 and values[0] == "remaining rk(s)": + try: + rk_count = int(values[1]) + if rk_count > 0: + change_pin_button_state = tk.NORMAL + break + except ValueError: + pass + + change_pin_button.config(state=change_pin_button_state) + + +def on_passkeys_button_click(): + """Handle passkeys button click""" + global PIN + selected_device = device_var.get() + match = re.search(r"\[(\d+)\]", selected_device) + + if match: + device_digit = match.group(1) + device_string = get_device_string(device_digit) + + if not device_string: + messagebox.showerror("Error", "Invalid device selection") + return + + if PIN is not None: + # Execute command to get resident keys (list domains) + command = [FIDO2_TOKEN_CMD, "-L", "-r"] + if PIN and PIN != "0000": + command.extend(["-w", PIN]) + command.append(device_string) + + try: + result = subprocess.run(command, capture_output=True, text=True) + if result.returncode == 0: + # Parse domains from output + domains = [] + for line in result.stdout.splitlines(): + match = re.search(r"= (.+)$", line) + if match: + domains.append(match.group(1)) + + # Execute command for each domain + cumulated_output = [] + for domain in domains: + domain_command = [FIDO2_TOKEN_CMD, "-L", "-k", domain] + if PIN and PIN != "0000": + domain_command.extend(["-w", PIN]) + domain_command.append(device_string) + + domain_result = subprocess.run(domain_command, capture_output=True, text=True) + + if domain_result.returncode == 0: + # Process the output line by line + processed_lines = [] + for line in domain_result.stdout.splitlines(): + if line.strip(): + parts = line.split() + if len(parts) >= 3: + key_id = parts[0] + credential_id = parts[1] + user_field = " ".join(parts[2:4]) if len(parts) > 3 else parts[2] + email_field = " ".join(parts[4:6]) if len(parts) > 5 else "" + + if user_field == "(null)": + user_field = "" + + # Determine if user_field is an email + if "@" in user_field: + email = user_field + user = "" + else: + user = user_field + email = email_field + + processed_lines.append(f"Credential ID: {credential_id}, User: {user} {email}") + + cumulated_output.append(f"Domain: {domain}\n" + "\n".join(processed_lines)) + else: + raise subprocess.CalledProcessError(domain_result.returncode, domain_command) + + # Show cumulated output in new window + cumulated_output_str = "\n\n".join(cumulated_output) + show_output_in_new_window(cumulated_output_str, device_digit) + else: + raise subprocess.CalledProcessError(result.returncode, command) + + except Exception as e: + messagebox.showerror("Error", f"Command execution failed: {e}\nOutput: {result.stderr}") + else: + messagebox.showinfo("Device Selected", "No digit found in the selected device") + + +def change_pin(): + """Change PIN using direct fido2-token2 command""" + selected_device = device_var.get() + match = re.search(r"\[(\d+)\]", selected_device) + + if match: + device_digit = match.group(1) + device_string = get_device_string(device_digit) + + if not device_string: + messagebox.showerror("Error", "Invalid device selection") + return + + command = [FIDO2_TOKEN_CMD, "-C", device_string] + cmd_str = " ".join(command) + + # macOS: use AppleScript to run in Terminal + apple_script = f''' + tell application "Terminal" + activate + do script "{cmd_str}" + end tell + ''' + subprocess.Popen(["osascript", "-e", apple_script]) + + + def refresh_combobox(): - """Refreshes the device list in the dropdown.""" - device_list = get_device_list() - if not device_list: - device_combobox['values'] = ["No FIDO devices found."] - else: - device_combobox['values'] = device_list + """Refresh the device combobox""" device_combobox.set("") tree.delete(*tree.get_children()) passkeys_button.config(state=tk.DISABLED) change_pin_button.config(state=tk.DISABLED) + + device_list = get_device_list() + if not device_list: + print("No devices found.") + device_combobox["values"] = device_list + + +def show_output_in_new_window(output, device_digit): + """Show output in a new window for passkey management""" + new_window = tk.Toplevel(root) + new_window.geometry("800x650") + new_window.title("Resident Keys / Passkeys") + + # Create Treeview for displaying output + tree_new_window = ttk.Treeview( + new_window, columns=("Domain", "Credential ID", "User"), show="headings" + ) + tree_new_window.heading("Domain", text="Domain") + tree_new_window.heading("Credential ID", text="Credential ID") + tree_new_window.heading("User", text="User") + tree_new_window.pack(expand=True, fill=tk.BOTH, padx=10, pady=10) + + # Add scrollbars + tree_scrollbar_y = ttk.Scrollbar(new_window, orient="vertical", command=tree_new_window.yview) + tree_scrollbar_y.pack(side="right", fill="y") + tree_new_window.configure(yscrollcommand=tree_scrollbar_y.set) + + tree_scrollbar_x = ttk.Scrollbar(new_window, orient="horizontal", command=tree_new_window.xview) + tree_scrollbar_x.pack(side="bottom", fill="x") + tree_new_window.configure(xscrollcommand=tree_scrollbar_x.set) + + # Parse output and insert into Treeview + current_domain = "" + for line in output.splitlines(): + if line.startswith("Domain: "): + current_domain = line.split("Domain: ")[1].strip() + elif "Credential ID: " in line and "User: " in line: + credential_id = line.split("Credential ID: ")[1].split(",")[0].strip() + user = line.split("User: ")[1].strip() + user = re.sub(re.escape(credential_id), "", user).strip() + tree_new_window.insert("", tk.END, values=(current_domain, credential_id, user)) + + def show_selected_value(): + """Delete selected passkey""" + selected_item = tree_new_window.selection() + if selected_item: + credential_id = tree_new_window.item(selected_item, "values")[1] + device_string = get_device_string(device_digit) + + if not device_string: + messagebox.showerror("Error", "Invalid device selection") + return + + new_window.destroy() + command = [FIDO2_TOKEN_CMD, "-D", "-i", credential_id, device_string] + cmd_str = " ".join(command) + # macOS: use AppleScript to run in Terminal + apple_script = f''' + tell application "Terminal" + activate + do script "{cmd_str}" + end tell + ''' + subprocess.Popen(["osascript", "-e", apple_script]) + + + # Create delete button + show_value_button = tk.Button(new_window, text="delete passkey", command=show_selected_value) + show_value_button.pack(pady=10) + def show_about_message(): - """Displays the about dialog.""" + """Show about dialog""" messagebox.showinfo( "About", - "FIDO2 Manager\n\n" - "A utility to manage and interact with FIDO2 security keys.\n\n" - "(c) TOKEN2 Sàrl, Yubico AB" + "The FIDO2.1 Security Key Management Tool is a utility designed to manage and interact with FIDO2.1 security keys.\r\n" + "It provides functionalities to view information, manage relying parties, and perform various operations on connected FIDO2.1 devices.\r\n\r\n" + "(c)TOKEN2 Sarl\r\nVersoix, Switzerland" ) -def check_passkeys_button_state(): - """Enables 'Passkeys' button if resident keys exist.""" - state = tk.DISABLED - for child in tree.get_children(): - values = tree.item(child, "values") - if values and len(values) > 1 and values[0] == "existing rk(s)": - try: - if int(values[1]) > 0: - state = tk.NORMAL - break - except (ValueError, IndexError): - pass - passkeys_button.config(state=state) -def check_changepin_button_state(): - """Enables 'Change PIN' button if PIN is set.""" - state = tk.DISABLED - for child in tree.get_children(): - values = tree.item(child, "values") - if values and len(values) > 1 and values[0] == "pin retries": - state = tk.NORMAL - break - change_pin_button.config(state=state) +def factory_reset(): + # Step 1: Confirm factory reset + confirm_reset = messagebox.askyesno( + "Confirm Factory Reset", + "Are you sure you want to factory reset?" + ) + if not confirm_reset: + return -# Placeholder functions for buttons -def on_passkeys_button_click(): - messagebox.showinfo("Not Implemented", "Passkey management functionality is not yet implemented.") + # Step 2: Ask to unplug and replug the key + replug_key = messagebox.askyesno( + "Replug Key", + "Please unplug the key, plug it back in, then click Yes to continue. Important - the reset command can be completed only within 10 seconds after plugging the key" + ) + if not replug_key: + return -def change_pin(): - messagebox.showinfo("Not Implemented", "Change PIN functionality is not yet implemented.") + # Step 3: Tell user to touch key when it starts blinking + messagebox.showinfo( + "Touch Key", + "When the key starts blinking, touch the sensor to complete reset." + ) -# --- GUI Layout --- + try: + # Run -L to list the device + list_command = [FIDO2_TOKEN_CMD, "-L"] + result = subprocess.run(list_command, capture_output=True, text=True, check=True) + output = result.stdout.strip() + # Extract string before first colon (e.g., ioreg://4302783856) + match = re.search(r"(ioreg://\d+):", output) + if not match: + messagebox.showerror("Error", "No valid device found in output") + return + + device_string = match.group(1) + + # Run -R with extracted device string + reset_command = [FIDO2_TOKEN_CMD, "-R", device_string] + subprocess.run(reset_command, check=True) + + messagebox.showinfo("Success", "Factory reset command sent successfully.") + + except subprocess.CalledProcessError as e: + messagebox.showerror("Error", f"Command failed:\n{e.stderr}") + except Exception as e: + messagebox.showerror("Error", f"Failed to reset: {e}") + +def terminal_path(): + bash_script = """#!/bin/bash +# CLI installer for FIDO2 Manager + +set -e + +APP_PATH="/Applications/fido2-manage.app" +CLI_SCRIPT="$APP_PATH/Contents/MacOS/fido2-manage-mac.sh" +SYMLINK_TARGET="/usr/local/bin/fido2-manage" + +echo "=== FIDO2 Manager CLI Installer ===" +echo "" + +if [[ ! -f "$CLI_SCRIPT" ]]; then + echo "❌ FIDO2 Manager app not found at: $APP_PATH" + echo "Please install the app first by dragging it to Applications folder." + exit 1 +fi + +echo "✅ Found FIDO2 Manager app" + +if [[ -L "$SYMLINK_TARGET" ]]; then + existing_target=$(readlink "$SYMLINK_TARGET") + if [[ "$existing_target" == "$CLI_SCRIPT" ]]; then + echo "✅ CLI already installed - fido2-manage command is available" + echo "" + echo "Test it: fido2-manage -help" + exit 0 + else + echo "⚠️ Existing fido2-manage command found pointing to: $existing_target" + echo "Do you want to replace it? (y/N)" + read -r response + if [[ ! "$response" =~ ^[Yy]$ ]]; then + echo "Installation cancelled." + exit 1 + fi + sudo rm -f "$SYMLINK_TARGET" + fi +elif [[ -f "$SYMLINK_TARGET" ]]; then + echo "⚠️ Existing fido2-manage file found (not a symlink)" + echo "Do you want to replace it? (y/N)" + read -r response + if [[ ! "$response" =~ ^[Yy]$ ]]; then + echo "Installation cancelled." + exit 1 + fi + sudo rm -f "$SYMLINK_TARGET" +fi + +echo "Creating symlink: $SYMLINK_TARGET -> $CLI_SCRIPT" +if sudo ln -sf "$CLI_SCRIPT" "$SYMLINK_TARGET"; then + echo "✅ CLI installed successfully!" + echo "" + echo "You can now use: fido2-manage -help" + echo "Examples:" + echo " fido2-manage -list" + echo " fido2-manage -info -device 1" + echo " fido2-manage -storage -device 1" + echo "" +else + echo "❌ Failed to create symlink. You may need administrator privileges." + exit 1 +fi + +echo "" +read -p "Press Enter to close this window..." +""" + + # Save script to a temp file + with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.sh') as f: + f.write(bash_script) + script_path = f.name + + os.chmod(script_path, 0o755) + + # Escape the path for AppleScript + escaped_path = script_path.replace(" ", "\\ ") + + # AppleScript to run in Terminal + applescript = f''' + tell application "Terminal" + activate + do script "{escaped_path}" + end tell + ''' + + subprocess.run(["osascript", "-e", applescript]) + + + +# Create main application window root = tk.Tk() -root.title("FIDO2 Manager") root.geometry("800x600") -root.minsize(700, 400) +root.title("FIDO2.1 Manager - Python GUI 0.2 - (c) Token2 ") -# Main container -main_frame = ttk.Frame(root, padding="10") -main_frame.pack(expand=True, fill="both") +# Create top frame for controls +top_frame = ttk.Frame(root) +top_frame.pack(side=tk.TOP, fill=tk.X) -# --- Top Frame: Device Selection & Refresh --- -top_frame = ttk.Frame(main_frame) -top_frame.pack(side="top", fill="x", pady=(0, 10)) +# Create label for dropdown +label = tk.Label(top_frame, text="Select Device:") +label.pack(side=tk.LEFT, padx=10, pady=10) -ttk.Label(top_frame, text="Select Device:").pack(side="left") +# Create ComboBox and populate with device list +device_list = get_device_list() +if not device_list: + device_list = ["No devices found."] device_var = tk.StringVar() -device_combobox = ttk.Combobox(top_frame, textvariable=device_var, state="readonly") -device_combobox.pack(side="left", expand=True, fill="x", padx=5) +device_combobox = ttk.Combobox(top_frame, textvariable=device_var, values=device_list, width=60) +device_combobox.pack(side=tk.LEFT, padx=10, pady=10) device_combobox.bind("<>", on_device_selected) -refresh_button = ttk.Button(top_frame, text="Refresh", command=refresh_combobox) -refresh_button.pack(side="left") +# Create refresh button +refresh_button = tk.Button(top_frame, text="Refresh", command=refresh_combobox) +refresh_button.pack(side=tk.LEFT, padx=10, pady=10) -# --- Center Frame: Information Treeview --- -tree_frame = ttk.Frame(main_frame) -tree_frame.pack(expand=True, fill="both") +# Create Treeview for displaying output +tree_frame = ttk.Frame(root) +tree_frame.pack(expand=True, fill=tk.BOTH, padx=10, pady=10) + +tree_scrollbar_y = ttk.Scrollbar(tree_frame, orient="vertical") +tree_scrollbar_x = ttk.Scrollbar(tree_frame, orient="horizontal") + +tree = ttk.Treeview( + tree_frame, + columns=("Key", "Value"), + show="headings", + yscrollcommand=tree_scrollbar_y.set, + xscrollcommand=tree_scrollbar_x.set, +) + +tree_scrollbar_y.config(command=tree.yview) +tree_scrollbar_x.config(command=tree.xview) +tree_scrollbar_y.pack(side="right", fill="y") +tree_scrollbar_x.pack(side="bottom", fill="x") -tree = ttk.Treeview(tree_frame, columns=("Key", "Value"), show="headings") tree.heading("Key", text="Key") tree.heading("Value", text="Value") -tree.column("Key", width=200, stretch=tk.NO, anchor="w") -tree.column("Value", width=550, anchor="w") +tree.pack(expand=True, fill=tk.BOTH) -# Scrollbars -vsb = ttk.Scrollbar(tree_frame, orient="vertical", command=tree.yview) -hsb = ttk.Scrollbar(tree_frame, orient="horizontal", command=tree.xview) -tree.configure(yscrollcommand=vsb.set, xscrollcommand=hsb.set) +# Create buttons +passkeys_button = ttk.Button(root, text="Passkeys", state=tk.DISABLED, command=on_passkeys_button_click) +passkeys_button.pack(side=tk.LEFT, padx=5, pady=10) -vsb.pack(side="right", fill="y") -hsb.pack(side="bottom", fill="x") -tree.pack(side="left", expand=True, fill="both") +change_pin_button = ttk.Button(root, text="Change PIN", state=tk.DISABLED, command=change_pin) +change_pin_button.pack(side=tk.LEFT, padx=5, pady=10) -# --- Bottom Frame: Action Buttons --- -bottom_frame = ttk.Frame(main_frame) -bottom_frame.pack(side="bottom", fill="x", pady=(10, 0)) +#factory_reset_button = tk.Button(root, text="Factory Reset", command=factory_reset ) +#factory_reset_button.pack(side=tk.LEFT, padx=10, pady=10) -passkeys_button = ttk.Button(bottom_frame, text="Passkeys", state=tk.DISABLED, command=on_passkeys_button_click) -passkeys_button.pack(side="left") +terminal_path = tk.Button(root, text="fido2-manage CLI", command=terminal_path ) +terminal_path.pack(side=tk.LEFT, padx=15, pady=10) -change_pin_button = ttk.Button(bottom_frame, text="Change PIN", state=tk.DISABLED, command=change_pin) -change_pin_button.pack(side="left", padx=5) +about_button = ttk.Button(root, text="About", command=show_about_message) +about_button.pack(side=tk.RIGHT, padx=5, pady=10) -about_button = ttk.Button(bottom_frame, text="About", command=show_about_message) -about_button.pack(side="right") - -# --- Initial Load --- -refresh_combobox() +# Run the main loop root.mainloop()