Update gui1.py

This commit is contained in:
Token2
2026-01-08 19:42:53 +01:00
committed by GitHub
parent aecdc96b93
commit 91ef54f276

310
gui1.py
View File

@@ -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
@@ -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)
info_command = [FIDO_COMMAND, "-info", "-device", device_digit]
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."
)
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,64 +63,73 @@ 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}")
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"Command execution failed: {e}\nOutput: {result.stderr}"
"Error",
f"This is an older key (probably FIDO2.0). No passkey management is possible with this key. Only basic information will be shown.",
)
return True
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:
@@ -238,16 +138,11 @@ def on_passkeys_button_click():
device_digit = match.group(1)
# Prompt for PIN if not already set
if PIN is None:
get_pin()
# Check again if PIN was entered
set_pin()
if PIN is None:
messagebox.showwarning("PIN Required", "PIN is required to manage passkeys.")
return
# Execute the command to get resident keys
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:
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
"Enter current PIN",
pexpect.EOF,
pexpect.TIMEOUT
], timeout=5)
])
if i in [0, 1, 2]:
# Prompt the user in the GUI
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)
@@ -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("<<ComboboxSelected>>", 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()