Files
fido2-manage/gui.py
2025-08-17 10:01:28 +02:00

474 lines
17 KiB
Python

import os
import re
import subprocess
import sys
import tkinter as tk
import shutil
from tkinter import messagebox, simpledialog, ttk
def detect_terminal():
candidates = [
("gnome-terminal", ["--"]), # modern Ubuntu defaults
("x-terminal-emulator", ["-e"]),# Debian/Ubuntu wrapper
("xterm", ["-e"]),
("konsole", ["-e"]),
("lxterminal", ["-e"]),
("tilix", ["-e"]),
("mate-terminal", ["-e"]),
]
for term, flag in candidates:
if shutil.which(term):
return term, flag
return None, None
# Define the command to execute
FIDO_COMMAND = "./fido2-manage.sh"
# Checks the terminal emulator from which "gui.py" is executed
# and sets it for the subprocess commands
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 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]
try:
result = subprocess.run(command, capture_output=True, text=True)
# Check if the subprocess was executed successfully
# 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
if ": " in line:
key, value = line.split(": ", 1)
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}"
)
# 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())
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
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"
)
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([TERM] + TERM_FLAG + 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.",
)
# 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}"
)
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
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}"
)
# 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="*"
)
# 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
set_pin()
# print (pin)
if match:
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.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)":
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)
# Function to check if the "passkeys" button should be enabled
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)":
try:
rk_count = int(values[1])
if rk_count > 0:
passkeys_button_state = tk.NORMAL
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
# Get the selected device and PIN
selected_device = device_var.get()
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:
# Execute the command to get resident keys
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)
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
)
if domain_result.returncode == 0:
cumulated_output.append(
f"Domain: {domain}\n{domain_result.stdout}"
)
else:
raise subprocess.CalledProcessError(
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:
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():
# 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)
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([TERM] + TERM_FLAG + command)
pass
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.
device_combobox.set("") # Clear the selected value
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
if not device_list:
print("No devices found.")
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):
# 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
)
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 the output and insert into the 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)
)
# 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
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([TERM] + TERM_FLAG + command)
# Create the "Show Value" 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\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.1 - (c) Token2")
# Create a frame for the first three elements
top_frame = ttk.Frame(root)
top_frame.pack(side=tk.TOP, fill=tk.X)
# Create a label for the dropdown
label = tk.Label(top_frame, text="Select Device:")
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_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("<<ComboboxSelected>>", 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)
# 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)
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")
# Set column headings
tree.heading("Key", text="Key")
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.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.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()