From 4e8426c8bb644a9413bb481e73cbca66f5beaa3b Mon Sep 17 00:00:00 2001 From: "M. B. Scheuck" Date: Thu, 5 Jun 2025 21:46:29 +0200 Subject: [PATCH 1/2] Create `.gitignore` --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bdc9737 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +# Ignore build/ directory +build/ From 737e0c961ddb9024da15aa8249e9789644ee6a21 Mon Sep 17 00:00:00 2001 From: "M. B. Scheuck" Date: Thu, 5 Jun 2025 21:47:46 +0200 Subject: [PATCH 2/2] Add terminal support for other than `x-terminal-emulator` This gets the terminal from which `gui.py` is executed and saves it into the global `TERM` variable. Defaults to `x-terminal-emulator`. Also autoformats the imports and code via black. --- gui.py | 330 +++++++++++++++++++++++++++++++++++---------------------- 1 file changed, 201 insertions(+), 129 deletions(-) diff --git a/gui.py b/gui.py index 55fc18b..247d54b 100644 --- a/gui.py +++ b/gui.py @@ -1,106 +1,129 @@ +import os +import re import subprocess import sys import tkinter as tk -from tkinter import ttk, messagebox, simpledialog -import re +from tkinter import messagebox, simpledialog, ttk # Define the command to execute -FIDO_COMMAND = './fido2-manage.sh' -#Command below for Windows -#FIDO_COMMAND = 'fido2-manage-ui.exe' +FIDO_COMMAND = "./fido2-manage.sh" + +# 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 +# FIDO_COMMAND = 'fido2-manage-ui.exe' # Global variable to store the PIN -pin = None +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) + 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') - + 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 storage command and prepend its output to the grid def execute_storage_command(device_digit): - global pin - command = [FIDO_COMMAND, '-storage', '-pin', pin, '-device', device_digit] - + global PIN + command = [FIDO_COMMAND, "-storage", "-pin", PIN, "-device", device_digit] + try: result = subprocess.run(command, capture_output=True, text=True) # Check if the subprocess was executed successfully - #print (result) + # print (result) if result.returncode == 0: # Parse the output and insert into the treeview - for line in reversed(result.stdout.splitlines()): # Insert in reversed order to prepend + for line in reversed( + result.stdout.splitlines() + ): # Insert in reversed order to prepend if ": " in line: key, value = line.split(": ", 1) - tree.insert("", 0, values=(key, value)) # Insert at the top of the grid - + tree.insert( + "", 0, values=(key, value) + ) # Insert at the top 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}") + 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 + global PIN tree.delete(*tree.get_children()) - command = [FIDO_COMMAND, '-storage', '-pin', pin, '-device', device_digit] + command = [FIDO_COMMAND, "-storage", "-pin", PIN, "-device", device_digit] try: result = subprocess.run(command, capture_output=True, text=True) - - if result.stderr.find("FIDO_ERR_PIN_INVALID")!= -1: - #exit + + if result.stderr.find("FIDO_ERR_PIN_INVALID") != -1: + # exit messagebox.showerror("Error", f"Invalid PIN provided") return - -# Check FIDO_ERR_PIN_AUTH_BLOCKED - if result.stderr.find("FIDO_ERR_PIN_AUTH_BLOCKED")!= -1: - #exit - messagebox.showerror("Error", f"Wrong PIN provided to many times. Reinsert the key") + + # Check FIDO_ERR_PIN_AUTH_BLOCKED + if result.stderr.find("FIDO_ERR_PIN_AUTH_BLOCKED") != -1: + # exit + messagebox.showerror( + "Error", f"Wrong PIN provided to many times. Reinsert the key" + ) return -# Check FIDO_ERR_PIN_REQUIRED - if result.stderr.find("FIDO_ERR_PIN_REQUIRED")!= -1: - #exit - messagebox.showerror("Error", f"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") - command = [FIDO_COMMAND, '-setPIN', '-device', device_digit] - if sys.platform.startswith('win'): - subprocess.Popen(['start', 'cmd', '/c'] + command, shell=True) - elif sys.platform.startswith('linux'): - subprocess.Popen(['x-terminal-emulator', '-e'] + command) + # Check FIDO_ERR_PIN_REQUIRED + if result.stderr.find("FIDO_ERR_PIN_REQUIRED") != -1: + # exit + messagebox.showerror( + "Error", + f"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", + ) + command = [FIDO_COMMAND, "-setPIN", "-device", device_digit] + if sys.platform.startswith("win"): + subprocess.Popen(["start", "cmd", "/c"] + command, shell=True) + elif sys.platform.startswith("linux"): + subprocess.Popen([TERM, "-e"] + command) - return + return + # Check FIDO version + if result.stderr.find("FIDO_ERR_INVALID_CBOR") != -1: + # exit + 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 FIDO version - if result.stderr.find("FIDO_ERR_INVALID_CBOR")!= -1: - #exit - 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: # 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 + 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}") - command = [FIDO_COMMAND, '-info', '-pin', pin, '-device', device_digit] + messagebox.showerror( + "Error", f"Command execution failed: {e}\nOutput: {result.stderr}" + ) + command = [FIDO_COMMAND, "-info", "-pin", PIN, "-device", device_digit] try: result = subprocess.run(command, capture_output=True, text=True) # Check if the subprocess was executed successfully @@ -109,46 +132,52 @@ def execute_info_command(device_digit): 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 + 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}") + messagebox.showerror( + "Error", f"Command execution failed: {e}\nOutput: {result.stderr}" + ) # Function to set the PIN def set_pin(): - global pin - pin = simpledialog.askstring("PIN Code", "Enter PIN code (enter 0000 if no PIN is set/known):", show='*') - + global PIN + PIN = simpledialog.askstring( + "PIN Code", "Enter PIN code (enter 0000 if no PIN is set/known):", show="*" + ) # Function to handle selection event def on_device_selected(event): - global pin + 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 + match = re.search(r"\[(\d+)\]", selected_device) + PIN = None set_pin() - #print (pin) + # print (pin) if match: device_digit = match.group(1) - - if pin is not None: + + if PIN is not None: 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 for child in tree.get_children(): - values = tree.item(child, 'values') - if values and len(values) == 2 and values[0] == 'existing rk(s)': + 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: @@ -156,7 +185,7 @@ def check_passkeys_button_state(): break except ValueError: pass - + passkeys_button.config(state=passkeys_button_state) @@ -164,8 +193,8 @@ def check_passkeys_button_state(): def check_changepin_button_state(): 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] == 'remaining rk(s)': + 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: @@ -173,47 +202,65 @@ def check_changepin_button_state(): break except ValueError: pass - + change_pin_button.config(state=passkeys_button_state) - - - - # Function to handle "passkeys" button click def on_passkeys_button_click(): - global pin + global PIN # Get the selected device and PIN selected_device = device_var.get() - match = re.search(r'\[(\d+)\]', selected_device) + match = re.search(r"\[(\d+)\]", selected_device) if match: device_digit = match.group(1) - #pin = simpledialog.askstring("PIN Code", "Enter PIN code (enter 0000 if no PIN is set/known):", show='*') - if pin is not None: + # pin = simpledialog.askstring("PIN Code", "Enter PIN code (enter 0000 if no PIN is set/known):", show='*') + if PIN is not None: # Execute the command to get resident keys - command = [FIDO_COMMAND, '-residentKeys', '-pin', pin, '-device', device_digit] + command = [ + FIDO_COMMAND, + "-residentKeys", + "-pin", + PIN, + "-device", + device_digit, + ] 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) + 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', '-domain', domain, '-pin', pin, '-device', device_digit] - domain_result = subprocess.run(domain_command, capture_output=True, text=True) - + + domain_command = [ + FIDO_COMMAND, + "-residentKeys", + "-domain", + domain, + "-pin", + PIN, + "-device", + device_digit, + ] + domain_result = subprocess.run( + domain_command, capture_output=True, text=True + ) + if domain_result.returncode == 0: - cumulated_output.append(f"Domain: {domain}\n{domain_result.stdout}") + cumulated_output.append( + f"Domain: {domain}\n{domain_result.stdout}" + ) else: - raise subprocess.CalledProcessError(domain_result.returncode, domain_command) + raise subprocess.CalledProcessError( + domain_result.returncode, domain_command + ) # Show the cumulated output in a new window cumulated_output_str = "\n\n".join(cumulated_output) @@ -221,30 +268,29 @@ def on_passkeys_button_click(): else: raise subprocess.CalledProcessError(result.returncode, command) except Exception as e: - messagebox.showerror("Error", f"Command execution failed: {e}\nOutput: {result.stderr}") + 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(): - # Get the selected device and PIN + # Get the selected device and PIN selected_device = device_var.get() # Extract the digit inside the first pair of square brackets - match = re.search(r'\[(\d+)\]', selected_device) + match = re.search(r"\[(\d+)\]", selected_device) if match: device_digit = match.group(1) - command = [FIDO_COMMAND, '-changePIN', '-device', device_digit] - if sys.platform.startswith('win'): - subprocess.Popen(['start', 'cmd', '/c'] + command, shell=True) - elif sys.platform.startswith('linux'): - subprocess.Popen(['x-terminal-emulator', '-e'] + command) + command = [FIDO_COMMAND, "-changePIN", "-device", device_digit] + if sys.platform.startswith("win"): + subprocess.Popen(["start", "cmd", "/c"] + command, shell=True) + elif sys.platform.startswith("linux"): + subprocess.Popen([TERM, "-e"] + command) - - - - pass - - + + def refresh_combobox(): # Implement your refresh logic here # For example, you can update the values in the combobox @@ -253,14 +299,13 @@ def refresh_combobox(): tree.delete(*tree.get_children()) passkeys_button.config(state=tk.DISABLED) change_pin_button.config(state=tk.DISABLED) - device_list = get_device_list() # Assuming you have a function to get the device list + device_list = ( + get_device_list() + ) # Assuming you have a function to get the device list if not device_list: print("No devices found.") - device_combobox['values'] = device_list # Update the combobox values - - - - + device_combobox["values"] = device_list # Update the combobox values + # Function to show the output in a new window def show_output_in_new_window(output, device_digit): @@ -270,7 +315,9 @@ def show_output_in_new_window(output, device_digit): 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") + 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") @@ -278,10 +325,14 @@ def show_output_in_new_window(output, device_digit): 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) + 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 = 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) @@ -294,31 +345,44 @@ def show_output_in_new_window(output, device_digit): 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)) + tree_new_window.insert( + "", tk.END, values=(current_domain, credential_id, user) + ) # Function to handle show value 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 the Credential ID of the selected item new_window.destroy() - command = [FIDO_COMMAND, '-delete', '-device', device_digit, '-credential', value] - if sys.platform.startswith('win'): - subprocess.Popen(['start', 'cmd', '/c'] + command, shell=True) - elif sys.platform.startswith('linux'): - subprocess.Popen(['x-terminal-emulator', '-e'] + command) + command = [ + FIDO_COMMAND, + "-delete", + "-device", + device_digit, + "-credential", + value, + ] + if sys.platform.startswith("win"): + subprocess.Popen(["start", "cmd", "/c"] + command, shell=True) + elif sys.platform.startswith("linux"): + subprocess.Popen([TERM, "-e"] + command) # Create the "Show Value" button - show_value_button = tk.Button(new_window, text="delete passkey", command=show_selected_value) + 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\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") - + 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", + ) + # Create the main application window root = tk.Tk() @@ -338,7 +402,9 @@ 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, values=device_list, width=60) +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 @@ -351,7 +417,13 @@ 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 = 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") @@ -362,21 +434,21 @@ tree.heading("Value", text="Value") tree.pack(expand=True, fill=tk.BOTH) # Create the "passkeys" button -passkeys_button = ttk.Button(root, text="Passkeys", state=tk.DISABLED, command=on_passkeys_button_click) +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) # Create the "Change PIN" button -change_pin_button = ttk.Button(root, text="Change PIN", state=tk.DISABLED, command=change_pin) +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) - 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() -