From 91ef54f276781f961c912f437fdd130094c66a3e Mon Sep 17 00:00:00 2001 From: Token2 <6784409+token2@users.noreply.github.com> Date: Thu, 8 Jan 2026 19:42:53 +0100 Subject: [PATCH] Update gui1.py --- gui1.py | 344 ++++++++++++++++---------------------------------------- 1 file changed, 95 insertions(+), 249 deletions(-) diff --git a/gui1.py b/gui1.py index 001e183..9b9b3c8 100644 --- a/gui1.py +++ b/gui1.py @@ -1,26 +1,3 @@ -def execute_info_command_no_pin(device_digit): - """Execute info command without PIN - just show basic device info""" - tree.delete(*tree.get_children()) - - # Execute info command without PIN - info_command = [FIDO_COMMAND, "-info", "-device", device_digit] - try: - result = subprocess.run(info_command, capture_output=True, text=True) - if result.returncode == 0: - 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"Command execution failed: {e}\nOutput: {result.stderr}" - ) - - return True - - import os import re import subprocess @@ -44,7 +21,7 @@ def detect_terminal(): if shutil.which(term): return term, flag return None, None - + # Define the command to execute FIDO_COMMAND = "./fido2-manage.sh" @@ -54,114 +31,28 @@ TERM, TERM_FLAG = detect_terminal() if TERM is None: messagebox.showerror("Error", "No supported terminal emulator found. Please install xterm or gnome-terminal.") - # Command below for Windows # FIDO_COMMAND = 'fido2-manage-ui.exe' # Global variable to store the PIN PIN = None - # Function to get device list from fido2-manage-ui.exe def get_device_list(): try: - # Execute the command with '-list' argument and capture the output result = subprocess.run([FIDO_COMMAND, "-list"], capture_output=True, text=True) - # Split the output into lines and return as a list device_list = result.stdout.strip().split("\n") return device_list except Exception as e: - # Handle exceptions (e.g., file not found or command error) print(f"Error executing device list command: {e}") return [] - -# Function to set the PIN via dialog -def get_pin(): - global PIN - PIN = simpledialog.askstring( - "PIN Code", "Enter PIN code:", show="*" - ) - - -# Function to execute storage command -def execute_storage_command(device_digit): - global PIN - if PIN is None: - messagebox.showwarning("PIN Required", "PIN is required.") - return - - command = [FIDO_COMMAND, "-storage", "-pin", PIN, "-device", device_digit] - - 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}" - ) - - # Function to execute info command and append its output to the grid def execute_info_command(device_digit): global PIN - - if PIN is None: - messagebox.showwarning("PIN Required", "PIN is required.") - return False - tree.delete(*tree.get_children()) - - # First, try storage command - storage_command = [FIDO_COMMAND, "-storage", "-pin", PIN, "-device", device_digit] - try: - result = subprocess.run(storage_command, capture_output=True, text=True) - if result.stderr.find("FIDO_ERR_PIN_INVALID") != -1: - messagebox.showerror("Error", f"Invalid PIN provided") - return False + info_command = [FIDO_COMMAND, "-info", "-device", device_digit] - if result.stderr.find("FIDO_ERR_PIN_AUTH_BLOCKED") != -1: - messagebox.showerror( - "Error", f"Wrong PIN provided too many times. Reinsert the key" - ) - return False - - if result.stderr.find("FIDO_ERR_PIN_REQUIRED") != -1: - messagebox.showwarning( - "Warning", - "No PIN set for this key. You must set a PIN before managing passkeys." - ) - change_pin_button.config(text="Set PIN", state=tk.ACTIVE, command=set_pin) - return False - - if result.stderr.find("FIDO_ERR_INVALID_CBOR") != -1: - messagebox.showerror( - "Error", - f"This is an older key (probably FIDO2.0). No passkey management is possible with this key. Only basic information will be shown.", - ) - - # Check if the subprocess was executed successfully - if result.returncode == 0: - 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, storage_command) - except Exception as e: - messagebox.showerror( - "Error", f"Command execution failed: {e}\nOutput: {result.stderr}" - ) - - # Then, try info command - info_command = [FIDO_COMMAND, "-info", "-pin", PIN, "-device", device_digit] try: result = subprocess.run(info_command, capture_output=True, text=True) if result.returncode == 0: @@ -172,82 +63,86 @@ def execute_info_command(device_digit): else: raise subprocess.CalledProcessError(result.returncode, info_command) except Exception as e: - messagebox.showerror( - "Error", f"Command execution failed: {e}\nOutput: {result.stderr}" - ) - - return True + messagebox.showerror("Error", f"Command execution failed: {e}\nOutput: {result.stderr}") + storage_command = f"{FIDO_COMMAND} -storage -device {device_digit}" + + try: + child = pexpect.spawn(storage_command, encoding="utf-8", timeout=10) + + index = child.expect([ + r"Enter PIN for", + pexpect.EOF, + pexpect.TIMEOUT + ]) + + output = child.before + + if index == 0: + pin_button.config(text="Change PIN", state=tk.ACTIVE, command=change_pin) + + if index == 1: + messagebox.showwarning( + "Warning", + "No PIN is set for this key. You must set a PIN before managing passkeys." + ) + pin_button.config(text="Set PIN", state=tk.ACTIVE, command=set_pin) + + if index == 2: + if "FIDO_ERR_PIN_REQUIRED" in output: + pin_button.config(text="Set PIN", state=tk.ACTIVE, command=set_pin) + + if "FIDO_ERR_PIN_INVALID" in output: + messagebox.showerror("Error", f"Invalid PIN provided") + + if "FIDO_ERR_PIN_AUTH_BLOCKED" in output: + messagebox.showerror("Error", f"Wrong PIN provided too many times. Reinsert the key") + + if "FIDO_ERR_INVALID_CBOR" in output: + messagebox.showerror( + "Error", + f"This is an older key (probably FIDO2.0). No passkey management is possible with this key. Only basic information will be shown.", + ) + + messagebox.showerror("Unexpected Device Output", output) + return False + + except Exception as e: + messagebox.showerror("Error", f"Command execution failed: {e}\nOutput: {result.stderr}") + +# Function to set the PIN +def get_pin(): + global PIN + PIN = simpledialog.askstring("PIN Code", "Enter PIN code:", show="*") # Function to handle selection event def on_device_selected(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 if match: device_digit = match.group(1) - # Prompt for PIN - get_pin() - if PIN is not None: - if execute_info_command(device_digit): - check_passkeys_button_state() - check_changepin_button_state() + execute_info_command(device_digit) + passkeys_button.config(state=tk.NORMAL) # Always enable the Passkeys button else: messagebox.showinfo("Device Selected", "No digit found in the selected device") - -# Function to check if the "passkeys" button should be enabled -def check_passkeys_button_state(): - # Enable passkeys button if device is selected - if device_var.get(): - passkeys_button.config(state=tk.NORMAL) - else: - passkeys_button.config(state=tk.DISABLED) - - -# Function to check if the change PIN button should be enabled -def check_changepin_button_state(): - changepin_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: - changepin_button_state = tk.NORMAL - break - except ValueError: - pass - - change_pin_button.config(state=changepin_button_state) - - # Function to handle "passkeys" button click def on_passkeys_button_click(): global PIN - - # Get the selected device selected_device = device_var.get() match = re.search(r"\[(\d+)\]", selected_device) if not match: messagebox.showinfo("Device Selected", "No digit found in the selected device") return - + device_digit = match.group(1) - - # Prompt for PIN if not already set + if PIN is None: - get_pin() - - # Check again if PIN was entered - if PIN is None: - messagebox.showwarning("PIN Required", "PIN is required to manage passkeys.") - return - - # Execute the command to get resident keys + set_pin() + if PIN is None: + return + command = [ FIDO_COMMAND, "-residentKeys", @@ -259,17 +154,14 @@ def on_passkeys_button_click(): try: result = subprocess.run(command, capture_output=True, text=True) if result.returncode == 0: - # Parse the domains from the output domains = [] for line in result.stdout.splitlines(): match = re.search(r"= (.+)$", line) if match: domains.append(match.group(1)) - # Execute the command for each domain cumulated_output = [] for domain in domains: - domain_command = [ FIDO_COMMAND, "-residentKeys", @@ -293,7 +185,6 @@ def on_passkeys_button_click(): domain_result.returncode, domain_command ) - # Show the cumulated output in a new window cumulated_output_str = "\n\n".join(cumulated_output) show_output_in_new_window(cumulated_output_str, device_digit) else: @@ -303,9 +194,7 @@ def on_passkeys_button_click(): "Error", f"Command execution failed: {e}\nOutput: {result.stderr}" ) - def set_pin(): - """Set a new PIN on the device with full interactive control""" global PIN selected_device = device_var.get() match = re.search(r"\[(\d+)\]", selected_device) @@ -314,32 +203,30 @@ def set_pin(): device_digit = match.group(1) - # Ask for new PIN - while True: + while True: new_pin = simpledialog.askstring( "New PIN", "Enter your new PIN code:", show="*" ) - if new_pin is None: # User cancelled + if new_pin is None: + PIN = None return - + new_pin_confirmed = simpledialog.askstring( - "Confirm new PIN", "Confirm your new PIN code:", show="*" + "Confirm new PIN", "Enter your new PIN code:", show="*" ) - if new_pin_confirmed is None: # User cancelled + if new_pin_confirmed is None: + PIN = None return - + if new_pin == new_pin_confirmed: break else: messagebox.showerror("Error", "New PIN entries do not match!") - continue command = f"{FIDO_COMMAND} -setPIN -device {device_digit}" - # Enter new PIN in interactive shell try: child = pexpect.spawn(command, encoding="utf-8", timeout=20) - child.expect("Enter new PIN") child.sendline(new_pin) child.expect("Enter the same PIN again") @@ -350,35 +237,30 @@ def set_pin(): child.expect(pexpect.EOF) output = child.before.strip() - change_pin_button.config(text="Change PIN", state=tk.ACTIVE, command=change_pin) - if "FIDO_ERR_PIN_POLICY_VIOLATION" in output: match = re.search(r"minpinlen:\s*(\d+)", output) if match: min_pin_len = match.group(1) - else: - min_pin_len = "unknown" messagebox.showerror( "PIN not accepted.", f"The provided PIN does not fulfill the requirements of your device.\n" f"The PIN has to be at least {min_pin_len} long and must not be an easily guessable sequence, like e.g. 123456" ) - + PIN = None elif "error" in output.lower() or "FIDO_ERR" in output: messagebox.showerror("PIN Change Failed", output) + PIN = None else: messagebox.showinfo("Success", "PIN successfully set!") - except pexpect.exceptions.TIMEOUT: messagebox.showerror("Timeout", "The device did not respond in time.") + PIN = None except Exception as e: messagebox.showerror("Error", str(e)) - + PIN = None def change_pin(): - """Change PIN with interactive control and touch detection""" global PIN - if PIN is None: get_pin() @@ -388,52 +270,42 @@ def change_pin(): return device_digit = match.group(1) - while True: + old_pin = PIN + new_pin = simpledialog.askstring( "New PIN", "Enter your new PIN code:", show="*" ) - if new_pin is None: # User cancelled - return - new_pin_confirmed = simpledialog.askstring( - "Confirm new PIN", "Confirm your new PIN code:", show="*" + "Confirm new PIN", "Enter your new PIN code:", show="*" ) - if new_pin_confirmed is None: # User cancelled - return - if new_pin == new_pin_confirmed: break else: messagebox.showerror("Error", "New PIN entries do not match!") - continue command = f"{FIDO_COMMAND} -changePIN -device {device_digit}" try: child = pexpect.spawn(command, encoding="utf-8", timeout=20) - # --- Detect touch prompt --- i = child.expect([ - "Touch", - "Tap", - "Waiting for user", - "Enter current PIN", # sometimes no touch required + "Touch", + "Tap", + "Waiting for user", + "Enter current PIN", pexpect.EOF, pexpect.TIMEOUT - ], timeout=5) + ]) - if i in [0, 1, 2]: - # Prompt the user in the GUI + if i in [0, 1, 2]: messagebox.showinfo( "Touch Required", "Please touch your FIDO security key to continue." ) - # Now wait until the key is actually touched - child.expect("Enter current PIN", timeout=30) + child.expect("Enter current PIN") - # Now continue with PIN entry - child.sendline(PIN) + child.sendline(old_pin) child.expect("Enter new PIN") child.sendline(new_pin) @@ -443,7 +315,7 @@ def change_pin(): PIN = new_pin output = child.before.strip() - + idx = child.expect(["FIDO_ERR_PIN_POLICY_VIOLATION", pexpect.EOF], timeout=1) if idx == 0: command = f"{FIDO_COMMAND} -info -device {device_digit}" @@ -451,9 +323,6 @@ def change_pin(): info.expect(pexpect.EOF) info_text = info.before - print("info_text:\n", info_text) - - # extract minpinlen match = re.search(r"minpinlen:\s*(\d+)", info_text) if match: min_pin_len = match.group(1) @@ -466,10 +335,8 @@ def change_pin(): f"The PIN must be at least {min_pin_len} digits long and " f"must not be an easily guessable sequence (e.g. 123456)." ) - return - # If no violation detected, EOF happened normally child.expect(pexpect.EOF) output = child.before.strip() @@ -483,39 +350,29 @@ def change_pin(): except Exception as e: messagebox.showerror("Error", str(e)) - def refresh_combobox(): - """Refresh device list and reset all states""" - global PIN - device_combobox.set("") # Clear the selected value + device_combobox.set("") tree.delete(*tree.get_children()) passkeys_button.config(state=tk.DISABLED) - change_pin_button.config(state=tk.DISABLED) - PIN = None # Reset PIN on refresh + pin_button.config(state=tk.DISABLED) device_list = get_device_list() if not device_list: print("No devices found.") - device_combobox["values"] = device_list # Update the combobox values + device_combobox["values"] = device_list - -# Function to show the output in a new window def show_output_in_new_window(output, device_digit): - # Create a new window new_window = tk.Toplevel(root) new_window.geometry("800x650") new_window.title("Resident Keys / Passkeys") - # Create a Treeview widget for displaying output tree_new_window = ttk.Treeview( new_window, columns=("Domain", "Credential ID", "User"), show="headings" ) - # Set column 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 to the Treeview tree_scrollbar_y = ttk.Scrollbar( new_window, orient="vertical", command=tree_new_window.yview ) @@ -527,7 +384,6 @@ def show_output_in_new_window(output, device_digit): tree_scrollbar_x.pack(side="bottom", fill="x") tree_new_window.configure(xscrollcommand=tree_scrollbar_x.set) - # Parse the output and insert into the Treeview current_domain = "" for line in output.splitlines(): if line.startswith("Domain: "): @@ -540,11 +396,10 @@ def show_output_in_new_window(output, device_digit): "", tk.END, values=(current_domain, credential_id, user) ) - # Function to handle delete passkey button click def show_selected_value(): selected_item = tree_new_window.selection() if selected_item: - value = tree_new_window.item(selected_item, "values")[1] # Get Credential ID + value = tree_new_window.item(selected_item, "values")[1] new_window.destroy() command = [ FIDO_COMMAND, @@ -559,27 +414,21 @@ def show_output_in_new_window(output, device_digit): elif sys.platform.startswith("linux"): subprocess.Popen([TERM] + TERM_FLAG + command) - # Create the "Delete Passkey" 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(): messagebox.showinfo( "About", - "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 passkeys, and perform various operations on connected FIDO2.1 devices.\r\n\r\n" - "(c)TOKEN2 Sarl\r\nVersoix, Switzerland\r\n\r\n" - "Version 0.2 - Merged Edition", + "The FIDO2.1 Security Key Management Tool is a utility designed to manage and interact with FIDO2.1 security keys.\r\nIt 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", ) - # Create the main application window root = tk.Tk() -root.geometry("700x600") # Width x Height -root.title("FIDO2.1 Manager - Python version 0.2 - (c) Token2") +root.geometry("700x600") +root.title("FIDO2.1 Manager - Python version 0.1 - (c) Token2") # Create a frame for the first three elements top_frame = ttk.Frame(root) @@ -604,7 +453,6 @@ device_combobox.bind("<>", on_device_selected) refresh_button = tk.Button(top_frame, text="Refresh", command=refresh_combobox) refresh_button.pack(side=tk.LEFT, padx=10, pady=10) - # Create a Treeview widget for displaying output with scrollbars tree_frame = ttk.Frame(root) tree_frame.pack(expand=True, fill=tk.BOTH, padx=10, pady=10) @@ -621,7 +469,6 @@ 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") -# Set column headings tree.heading("Key", text="Key") tree.heading("Value", text="Value") tree.pack(expand=True, fill=tk.BOTH) @@ -632,16 +479,15 @@ passkeys_button = ttk.Button( ) passkeys_button.pack(side=tk.LEFT, padx=5, pady=10) -# Create the "Change PIN" button -change_pin_button = ttk.Button( - root, text="Change PIN", state=tk.DISABLED, command=change_pin +# Create the "Set PIN" button +pin_button = ttk.Button( + root, text="Set PIN", state=tk.DISABLED, command=set_pin ) -change_pin_button.pack(side=tk.LEFT, padx=5, pady=10) +pin_button.pack(side=tk.LEFT, padx=5, pady=10) # Create the "About" button about_button = ttk.Button(root, text="About", command=show_about_message) about_button.pack(side=tk.RIGHT, padx=5, pady=10) - # Run the Tkinter main loop root.mainloop()