Update gui1.py

This commit is contained in:
Token2
2026-01-08 19:30:18 +01:00
committed by GitHub
parent 2913f5c20a
commit 895824a861

292
gui1.py
View File

@@ -40,101 +40,19 @@ PIN = None
# Function to get device list from fido2-manage-ui.exe # Function to get device list from fido2-manage-ui.exe
def get_device_list(): def get_device_list():
try: try:
# Execute the command with '-list' argument and capture the output # 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 # 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 return device_list
except Exception as e: except Exception as e:
# Handle exceptions (e.g., file not found or command error) # Handle exceptions (e.g., file not found or command error)
print(f"Error executing device list command: {e}") print(f"Error executing device list command: {e}")
return [] return []
# Function to execute info command and append its output to the grid # Function to set the PIN via dialog
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
def get_pin(): def get_pin():
global PIN global PIN
PIN = simpledialog.askstring( 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 # Function to handle selection event
def on_device_selected(event): def on_device_selected(event):
global PIN
selected_device = device_var.get() selected_device = device_var.get()
# Extract the digit inside the first pair of square brackets # Extract the digit inside the first pair of square brackets
match = re.search(r"\[(\d+)\]", selected_device) match = re.search(r"\[(\d+)\]", selected_device)
PIN = None
if match: if match:
device_digit = match.group(1) device_digit = match.group(1)
# Prompt for PIN
if (execute_info_command(device_digit) == "PIN set"): get_pin()
check_passkeys_button_state() if PIN is not None:
check_changepin_button_state() if execute_info_command(device_digit):
check_passkeys_button_state()
check_changepin_button_state()
else: else:
messagebox.showinfo("Device Selected", "No digit found in the selected device") messagebox.showinfo("Device Selected", "No digit found in the selected device")
# Function to check if the "passkeys" button should be enabled # Function to check if the "passkeys" button should be enabled
def check_passkeys_button_state(): def check_passkeys_button_state():
passkeys_button_state = tk.DISABLED passkeys_button_state = tk.DISABLED
@@ -177,27 +193,26 @@ def check_passkeys_button_state():
passkeys_button.config(state=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(): def check_changepin_button_state():
passkeys_button_state = tk.DISABLED changepin_button_state = tk.DISABLED
for child in tree.get_children(): for child in tree.get_children():
values = tree.item(child, "values") values = tree.item(child, "values")
if values and len(values) == 2 and values[0] == "remaining rk(s)": if values and len(values) == 2 and values[0] == "remaining rk(s)":
try: try:
rk_count = int(values[1]) rk_count = int(values[1])
if rk_count > 0: if rk_count > 0:
passkeys_button_state = tk.NORMAL changepin_button_state = tk.NORMAL
break break
except ValueError: except ValueError:
pass pass
pin_button.config(state=passkeys_button_state) change_pin_button.config(state=changepin_button_state)
# Function to handle "passkeys" button click # Function to handle "passkeys" button click
def on_passkeys_button_click(): def on_passkeys_button_click():
global PIN global PIN
# Get the selected device and PIN # Get the selected device and PIN
selected_device = device_var.get() selected_device = device_var.get()
match = re.search(r"\[(\d+)\]", selected_device) match = re.search(r"\[(\d+)\]", selected_device)
@@ -271,7 +286,10 @@ def on_passkeys_button_click():
else: else:
messagebox.showinfo("Device Selected", "No digit found in the selected device") messagebox.showinfo("Device Selected", "No digit found in the selected device")
def set_pin(): def set_pin():
"""Set a new PIN on the device with full interactive control"""
global PIN
selected_device = device_var.get() selected_device = device_var.get()
match = re.search(r"\[(\d+)\]", selected_device) match = re.search(r"\[(\d+)\]", selected_device)
if not match: if not match:
@@ -281,14 +299,19 @@ def set_pin():
# Ask for new PIN # Ask for new PIN
while True: while True:
new_pin = simpledialog.askstring( new_pin = simpledialog.askstring(
"New PIN", "Enter your new PIN code:", show="*" "New PIN", "Enter your new PIN code:", show="*"
) )
if new_pin is None: # User cancelled
return
new_pin_confirmed = simpledialog.askstring( 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 break
else: else:
messagebox.showerror("Error", "New PIN entries do not match!") messagebox.showerror("Error", "New PIN entries do not match!")
@@ -310,16 +333,18 @@ def set_pin():
child.expect(pexpect.EOF) child.expect(pexpect.EOF)
output = child.before.strip() 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: if "FIDO_ERR_PIN_POLICY_VIOLATION" in output:
match = re.search(r"minpinlen:\s*(\d+)", output) match = re.search(r"minpinlen:\s*(\d+)", output)
if match: if match:
min_pin_len = match.group(1) min_pin_len = match.group(1)
else:
min_pin_len = "unknown"
messagebox.showerror( messagebox.showerror(
"PIN not accepted.", "PIN not accepted.",
f"The provided PIN does not fullfill the requirements of you device.\n\ f"The provided PIN does not fulfill the requirements of your 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 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: elif "error" in output.lower() or "FIDO_ERR" in output:
@@ -334,7 +359,9 @@ def set_pin():
def change_pin(): def change_pin():
"""Change PIN with interactive control and touch detection"""
global PIN global PIN
if PIN is None: if PIN is None:
get_pin() get_pin()
@@ -344,16 +371,21 @@ def change_pin():
return return
device_digit = match.group(1) device_digit = match.group(1)
while True:
old_pin = PIN
while True:
new_pin = simpledialog.askstring( new_pin = simpledialog.askstring(
"New PIN", "Enter your new PIN code:", show="*" "New PIN", "Enter your new PIN code:", show="*"
) )
if new_pin is None: # User cancelled
return
new_pin_confirmed = simpledialog.askstring( 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 break
else: else:
messagebox.showerror("Error", "New PIN entries do not match!") messagebox.showerror("Error", "New PIN entries do not match!")
@@ -372,7 +404,7 @@ def change_pin():
"Enter current PIN", # sometimes no touch required "Enter current PIN", # sometimes no touch required
pexpect.EOF, pexpect.EOF,
pexpect.TIMEOUT pexpect.TIMEOUT
]) ], timeout=5)
if i in [0, 1, 2]: if i in [0, 1, 2]:
# Prompt the user in the GUI # Prompt the user in the GUI
@@ -381,10 +413,10 @@ def change_pin():
"Please touch your FIDO security key to continue." "Please touch your FIDO security key to continue."
) )
# Now wait until the key is actually touched # 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 # Now continue with PIN entry
child.sendline(old_pin) child.sendline(PIN)
child.expect("Enter new PIN") child.expect("Enter new PIN")
child.sendline(new_pin) child.sendline(new_pin)
@@ -395,15 +427,12 @@ def change_pin():
output = child.before.strip() output = child.before.strip()
testminlen = child.before
idx = child.expect(["FIDO_ERR_PIN_POLICY_VIOLATION", pexpect.EOF], timeout=1) idx = child.expect(["FIDO_ERR_PIN_POLICY_VIOLATION", pexpect.EOF], timeout=1)
if idx == 0: if idx == 0:
command = f"{FIDO_COMMAND} -info -device {device_digit}" command = f"{FIDO_COMMAND} -info -device {device_digit}"
# Run the command
info = pexpect.spawn(command, encoding="utf-8") info = pexpect.spawn(command, encoding="utf-8")
info.expect(pexpect.EOF) info.expect(pexpect.EOF)
info_text = info.before # <-- now this contains the full text info_text = info.before
print("info_text:\n", info_text) print("info_text:\n", info_text)
@@ -437,19 +466,16 @@ def change_pin():
except Exception as e: except Exception as e:
messagebox.showerror("Error", str(e)) messagebox.showerror("Error", str(e))
def refresh_combobox(): def refresh_combobox():
# Implement your refresh logic here """Refresh device list and reset all states"""
# For example, you can update the values in the combobox
# based on some external data source or trigger a refresh action.
global PIN global PIN
device_combobox.set("") # Clear the selected value device_combobox.set("") # Clear the selected value
tree.delete(*tree.get_children()) tree.delete(*tree.get_children())
passkeys_button.config(state=tk.DISABLED) 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 PIN = None # Reset PIN on refresh
device_list = ( device_list = get_device_list()
get_device_list()
) # Assuming you have a function to get the device list
if not device_list: if not device_list:
print("No devices found.") print("No devices found.")
device_combobox["values"] = device_list # Update the combobox values 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) "", 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(): def show_selected_value():
selected_item = tree_new_window.selection() selected_item = tree_new_window.selection()
if selected_item: if selected_item:
value = tree_new_window.item(selected_item, "values")[ value = tree_new_window.item(selected_item, "values")[1] # Get Credential ID
1
] # Get the Credential ID of the selected item
new_window.destroy() new_window.destroy()
command = [ command = [
FIDO_COMMAND, FIDO_COMMAND,
@@ -518,9 +542,9 @@ def show_output_in_new_window(output, device_digit):
elif sys.platform.startswith("linux"): elif sys.platform.startswith("linux"):
subprocess.Popen([TERM] + TERM_FLAG + command) subprocess.Popen([TERM] + TERM_FLAG + command)
# Create the "Show Value" button # Create the "Delete Passkey" button
show_value_button = tk.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) show_value_button.pack(pady=10)
@@ -528,14 +552,17 @@ def show_output_in_new_window(output, device_digit):
def show_about_message(): def show_about_message():
messagebox.showinfo( messagebox.showinfo(
"About", "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 # Create the main application window
root = tk.Tk() root = tk.Tk()
root.geometry("700x600") # Width x Height 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 # Create a frame for the first three elements
top_frame = ttk.Frame(root) 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 # Create a ComboBox (dropdown) and populate it with device list
device_list = get_device_list() device_list = get_device_list()
if not device_list: if not device_list:
device_list = "No devices found." device_list = ["No devices found."]
device_var = tk.StringVar() device_var = tk.StringVar()
device_combobox = ttk.Combobox( device_combobox = ttk.Combobox(
top_frame, textvariable=device_var, values=device_list, width=60 top_frame, textvariable=device_var, values=device_list, width=60
) )
device_combobox.pack(side=tk.LEFT, padx=10, pady=10) device_combobox.pack(side=tk.LEFT, padx=10, pady=10)
device_combobox.bind("<<ComboboxSelected>>", on_device_selected) device_combobox.bind("<<ComboboxSelected>>", on_device_selected)
# Create the refresh button # Create the refresh button
refresh_button = tk.Button(top_frame, text="Refresh", command=refresh_combobox) refresh_button = tk.Button(top_frame, text="Refresh", command=refresh_combobox)
refresh_button.pack(side=tk.LEFT, padx=10, pady=10) 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.heading("Value", text="Value")
tree.pack(expand=True, fill=tk.BOTH) tree.pack(expand=True, fill=tk.BOTH)
# Create the "passkeys" button # Create the "Passkeys" button
passkeys_button = ttk.Button( passkeys_button = ttk.Button(
root, text="Passkeys", state=tk.DISABLED, command=on_passkeys_button_click root, text="Passkeys", state=tk.DISABLED, command=on_passkeys_button_click
) )
passkeys_button.pack(side=tk.LEFT, padx=5, pady=10) passkeys_button.pack(side=tk.LEFT, padx=5, pady=10)
pin_button = ttk.Button( # Create the "Change PIN" button
root, text="Set PIN", state=tk.DISABLED, command=set_pin 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 = ttk.Button(root, text="About", command=show_about_message)
about_button.pack(side=tk.RIGHT, padx=5, pady=10) about_button.pack(side=tk.RIGHT, padx=5, pady=10)