Merge pull request #20 from MBSck/feature/distro-terminal-support

Add support for more linux distros for `gui.py`
This commit is contained in:
Token2
2025-06-06 19:07:01 +02:00
committed by GitHub
2 changed files with 203 additions and 129 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
# Ignore build/ directory
build/

258
gui.py
View File

@@ -1,24 +1,31 @@
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'
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
@@ -28,10 +35,11 @@ def get_device_list():
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)
@@ -39,21 +47,28 @@ def execute_storage_command(device_digit):
# 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)
@@ -65,29 +80,33 @@ def execute_info_command(device_digit):
# 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")
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)
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
# 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.")
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:
@@ -95,12 +114,16 @@ 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}")
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)
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:
@@ -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:
@@ -177,29 +206,32 @@ def check_changepin_button_state():
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:
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))
@@ -207,13 +239,28 @@ def on_passkeys_button_click():
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,26 +268,25 @@ 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
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
@@ -253,13 +299,12 @@ 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
@@ -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,30 +345,43 @@ 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
@@ -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("<<ComboboxSelected>>", 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()