From 895824a861e125fc5e956b689e12efe306fcaf18 Mon Sep 17 00:00:00 2001 From: Token2 <6784409+token2@users.noreply.github.com> Date: Thu, 8 Jan 2026 19:30:18 +0100 Subject: [PATCH] Update gui1.py --- gui1.py | 292 +++++++++++++++++++++++++++++++------------------------- 1 file changed, 161 insertions(+), 131 deletions(-) diff --git a/gui1.py b/gui1.py index 7ceeb7f..9e6f536 100644 --- a/gui1.py +++ b/gui1.py @@ -40,101 +40,19 @@ 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 execute info command and append its output to the grid -def execute_info_command(device_digit): - global PIN - - tree.delete(*tree.get_children()) - - info_command = [FIDO_COMMAND, "-info", "-device", device_digit] - - try: - result = subprocess.run(info_command, capture_output=True, text=True) - # Check if the subprocess was executed successfully - if result.returncode == 0: - # Parse the output and insert into the treeview - for line in result.stdout.splitlines(): - if ": " in line: - key, value = line.split(": ", 1) - tree.insert( - "", tk.END, values=(key, value) - ) # Append to the end of the grid - else: - raise subprocess.CalledProcessError(result.returncode, command) - except Exception as e: - 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 - ] - ) - - # catch stdout from cli - output = child.before - - if index == 0: - pin_button.config(text="Change PIN", state=tk.ACTIVE, command=change_pin) - - #if PIN is not set - 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 to 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.", - ) - - # Unexpected EOF - 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 +# Function to set the PIN via dialog def get_pin(): global PIN PIN = simpledialog.askstring( @@ -142,24 +60,122 @@ def get_pin(): ) +# 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 + + 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." + ) + 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: + 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 + + # 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) - - if (execute_info_command(device_digit) == "PIN set"): - check_passkeys_button_state() - check_changepin_button_state() - + # Prompt for PIN + get_pin() + if PIN is not None: + if execute_info_command(device_digit): + check_passkeys_button_state() + check_changepin_button_state() 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(): passkeys_button_state = tk.DISABLED @@ -177,27 +193,26 @@ def check_passkeys_button_state(): passkeys_button.config(state=passkeys_button_state) -# Function to check if the "passkeys" button should be enabled +# Function to check if the change PIN button should be enabled def check_changepin_button_state(): - passkeys_button_state = tk.DISABLED + 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: - passkeys_button_state = tk.NORMAL + changepin_button_state = tk.NORMAL break except ValueError: pass - pin_button.config(state=passkeys_button_state) + 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 and PIN selected_device = device_var.get() match = re.search(r"\[(\d+)\]", selected_device) @@ -271,7 +286,10 @@ def on_passkeys_button_click(): else: messagebox.showinfo("Device Selected", "No digit found in the selected device") + 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) if not match: @@ -281,14 +299,19 @@ def set_pin(): # Ask for new PIN while True: - 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", "Enter your new PIN code:", show="*" + "Confirm new PIN", "Confirm your new PIN code:", show="*" ) - if (new_pin == new_pin_confirmed): + 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!") @@ -310,16 +333,18 @@ def set_pin(): child.expect(pexpect.EOF) output = child.before.strip() - pin_button.config(text="Change PIN",state=tk.ACTIVE, command=change_pin) + 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 fullfill the requirements of you device.\n\ - The PIN has to be at least {min_pin_len} long and must not be a easy guessable sequence, like e.g. 123456" + 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" ) elif "error" in output.lower() or "FIDO_ERR" in output: @@ -334,7 +359,9 @@ def set_pin(): def change_pin(): + """Change PIN with interactive control and touch detection""" global PIN + if PIN is None: get_pin() @@ -344,16 +371,21 @@ 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", "Enter your new PIN code:", show="*" + "Confirm new PIN", "Confirm your new PIN code:", show="*" ) - if (new_pin == new_pin_confirmed): + 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!") @@ -372,7 +404,7 @@ def change_pin(): "Enter current PIN", # sometimes no touch required pexpect.EOF, pexpect.TIMEOUT - ]) + ], timeout=5) if i in [0, 1, 2]: # Prompt the user in the GUI @@ -381,10 +413,10 @@ def change_pin(): "Please touch your FIDO security key to continue." ) # Now wait until the key is actually touched - child.expect("Enter current PIN") + child.expect("Enter current PIN", timeout=30) # Now continue with PIN entry - child.sendline(old_pin) + child.sendline(PIN) child.expect("Enter new PIN") child.sendline(new_pin) @@ -394,16 +426,13 @@ def change_pin(): PIN = new_pin output = child.before.strip() - - testminlen = child.before idx = child.expect(["FIDO_ERR_PIN_POLICY_VIOLATION", pexpect.EOF], timeout=1) if idx == 0: command = f"{FIDO_COMMAND} -info -device {device_digit}" - # Run the command info = pexpect.spawn(command, encoding="utf-8") info.expect(pexpect.EOF) - info_text = info.before # <-- now this contains the full text + info_text = info.before print("info_text:\n", info_text) @@ -437,19 +466,16 @@ def change_pin(): except Exception as e: messagebox.showerror("Error", str(e)) + def refresh_combobox(): - # Implement your refresh logic here - # For example, you can update the values in the combobox - # based on some external data source or trigger a refresh action. + """Refresh device list and reset all states""" global PIN device_combobox.set("") # Clear the selected value tree.delete(*tree.get_children()) passkeys_button.config(state=tk.DISABLED) - pin_button.config(state=tk.DISABLED) + change_pin_button.config(state=tk.DISABLED) PIN = None # Reset PIN on refresh - device_list = ( - get_device_list() - ) # Assuming you have a function to get the device list + device_list = get_device_list() if not device_list: print("No devices found.") device_combobox["values"] = device_list # Update the combobox values @@ -497,13 +523,11 @@ def show_output_in_new_window(output, device_digit): "", tk.END, values=(current_domain, credential_id, user) ) - # Function to handle show value button click + # 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 the Credential ID of the selected item + value = tree_new_window.item(selected_item, "values")[1] # Get Credential ID new_window.destroy() command = [ FIDO_COMMAND, @@ -518,9 +542,9 @@ def show_output_in_new_window(output, device_digit): elif sys.platform.startswith("linux"): subprocess.Popen([TERM] + TERM_FLAG + command) - # Create the "Show Value" button + # Create the "Delete Passkey" button show_value_button = tk.Button( - new_window, text="delete passkey", command=show_selected_value + new_window, text="Delete Passkey", command=show_selected_value ) show_value_button.pack(pady=10) @@ -528,14 +552,17 @@ def show_output_in_new_window(output, device_digit): 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\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", + "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", ) # Create the main application window root = tk.Tk() root.geometry("700x600") # Width x Height -root.title("FIDO2.1 Manager - Python version 0.1 - (c) Token2") +root.title("FIDO2.1 Manager - Python version 0.2 - (c) Token2") # Create a frame for the first three elements top_frame = ttk.Frame(root) @@ -548,13 +575,14 @@ label.pack(side=tk.LEFT, padx=10, pady=10) # Create a ComboBox (dropdown) and populate it with device list device_list = get_device_list() if not device_list: - device_list = "No devices found." + device_list = ["No devices found."] device_var = tk.StringVar() 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) + # Create the refresh button refresh_button = tk.Button(top_frame, text="Refresh", command=refresh_combobox) refresh_button.pack(side=tk.LEFT, padx=10, pady=10) @@ -581,17 +609,19 @@ tree.heading("Key", text="Key") tree.heading("Value", text="Value") tree.pack(expand=True, fill=tk.BOTH) -# Create the "passkeys" button +# Create the "Passkeys" button 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) -pin_button = ttk.Button( - root, text="Set PIN", state=tk.DISABLED, command=set_pin +# Create the "Change PIN" button +change_pin_button = ttk.Button( + root, text="Change PIN", state=tk.DISABLED, command=change_pin ) -pin_button.pack(side=tk.LEFT, padx=5, pady=10) +change_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)