Merge pull request #34 from janbaum/patch-gui

Enhance python GUI
This commit is contained in:
Token2
2025-12-15 11:13:26 +01:00
committed by GitHub

312
gui.py
View File

@@ -5,6 +5,7 @@ import sys
import tkinter as tk import tkinter as tk
import shutil import shutil
from tkinter import messagebox, simpledialog, ttk from tkinter import messagebox, simpledialog, ttk
import pexpect
def detect_terminal(): def detect_terminal():
candidates = [ candidates = [
@@ -55,26 +56,25 @@ def get_device_list():
return [] return []
# Function to execute storage command and prepend its output to the grid # Function to execute info command and append its output to the grid
def execute_storage_command(device_digit): def execute_info_command(device_digit):
global PIN global PIN
command = [FIDO_COMMAND, "-storage", "-pin", PIN, "-device", device_digit]
tree.delete(*tree.get_children())
info_command = [FIDO_COMMAND, "-info", "-device", device_digit]
try: try:
result = subprocess.run(command, capture_output=True, text=True) result = subprocess.run(info_command, capture_output=True, text=True)
# Check if the subprocess was executed successfully # Check if the subprocess was executed successfully
# print (result)
if result.returncode == 0: if result.returncode == 0:
# Parse the output and insert into the treeview # Parse the output and insert into the treeview
for line in reversed( for line in result.stdout.splitlines():
result.stdout.splitlines()
): # Insert in reversed order to prepend
if ": " in line: if ": " in line:
key, value = line.split(": ", 1) key, value = line.split(": ", 1)
tree.insert( tree.insert(
"", 0, values=(key, value) "", tk.END, values=(key, value)
) # Insert at the top of the grid ) # Append to the end of the grid
else: else:
raise subprocess.CalledProcessError(result.returncode, command) raise subprocess.CalledProcessError(result.returncode, command)
except Exception as e: except Exception as e:
@@ -82,80 +82,52 @@ def execute_storage_command(device_digit):
"Error", f"Command execution failed: {e}\nOutput: {result.stderr}" "Error", f"Command execution failed: {e}\nOutput: {result.stderr}"
) )
storage_command = f"{FIDO_COMMAND} -storage -device {device_digit}"
# 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: try:
result = subprocess.run(command, capture_output=True, text=True) child = pexpect.spawn(storage_command, encoding="utf-8", timeout=10)
if result.stderr.find("FIDO_ERR_PIN_INVALID") != -1: index = child.expect([
# exit 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") messagebox.showerror("Error", f"Invalid PIN provided")
return
# Check FIDO_ERR_PIN_AUTH_BLOCKED if "FIDO_ERR_PIN_AUTH_BLOCKED" in output:
if result.stderr.find("FIDO_ERR_PIN_AUTH_BLOCKED") != -1: messagebox.showerror("Error", f"Wrong PIN provided to many times. Reinsert the key")
# exit
messagebox.showerror(
"Error", f"Wrong PIN provided to many times. Reinsert the key"
)
return
# Check FIDO_ERR_PIN_REQUIRED if "FIDO_ERR_INVALID_CBOR" in output:
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( messagebox.showerror(
"Error", "Error",
f"This is an older key (probably FIDO2.0). No passkey management is possible with this key. Only basic information will be shown.", 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 # Unexpected EOF
if result.returncode == 0: messagebox.showerror("Unexpected Device Output", output)
# Parse the output and insert into the treeview return False
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: except Exception as e:
messagebox.showerror( messagebox.showerror(
"Error", f"Command execution failed: {e}\nOutput: {result.stderr}" "Error", f"Command execution failed: {e}\nOutput: {result.stderr}"
@@ -163,34 +135,31 @@ def execute_info_command(device_digit):
# Function to set the PIN # Function to set the PIN
def set_pin(): def get_pin():
global PIN global PIN
PIN = simpledialog.askstring( PIN = simpledialog.askstring(
"PIN Code", "Enter PIN code (enter 0000 if no PIN is set/known):", show="*" "PIN Code", "Enter PIN code:", show="*"
) )
# 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
set_pin()
# print (pin)
if match: if match:
device_digit = match.group(1) device_digit = match.group(1)
if PIN is not None: if (execute_info_command(device_digit) == "PIN set"):
execute_info_command(device_digit)
check_passkeys_button_state() check_passkeys_button_state()
check_changepin_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
@@ -222,7 +191,7 @@ def check_changepin_button_state():
except ValueError: except ValueError:
pass pass
change_pin_button.config(state=passkeys_button_state) pin_button.config(state=passkeys_button_state)
# Function to handle "passkeys" button click # Function to handle "passkeys" button click
@@ -293,22 +262,171 @@ 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():
selected_device = device_var.get()
match = re.search(r"\[(\d+)\]", selected_device)
if not match:
return
device_digit = match.group(1)
# Ask for new PIN
while True:
new_pin = simpledialog.askstring(
"New PIN", "Enter your new PIN code:", show="*"
)
new_pin_confirmed = simpledialog.askstring(
"Confirm new PIN", "Enter your new PIN code:", show="*"
)
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")
child.sendline(new_pin_confirmed)
PIN = new_pin
child.expect(pexpect.EOF)
output = child.before.strip()
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)
messagebox.showerror(
"PIN not accepted.",
f"The provided PIN does not fullfill the requirements of you 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"
)
elif "error" in output.lower() or "FIDO_ERR" in output:
messagebox.showerror("PIN Change Failed", output)
else:
messagebox.showinfo("Success", "PIN successfully set!")
except pexpect.exceptions.TIMEOUT:
messagebox.showerror("Timeout", "The device did not respond in time.")
except Exception as e:
messagebox.showerror("Error", str(e))
def change_pin(): def change_pin():
# Get the selected device and PIN global PIN
if PIN is None:
get_pin()
selected_device = device_var.get() 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: if not match:
return
device_digit = match.group(1) device_digit = match.group(1)
command = [FIDO_COMMAND, "-changePIN", "-device", device_digit] while True:
if sys.platform.startswith("win"): old_pin = PIN
subprocess.Popen(["start", "cmd", "/c"] + command, shell=True)
elif sys.platform.startswith("linux"):
subprocess.Popen([TERM] + TERM_FLAG + command)
pass new_pin = simpledialog.askstring(
"New PIN", "Enter your new PIN code:", show="*"
)
new_pin_confirmed = simpledialog.askstring(
"Confirm new PIN", "Enter your new PIN code:", show="*"
)
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
pexpect.EOF,
pexpect.TIMEOUT
])
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")
# Now continue with PIN entry
child.sendline(old_pin)
child.expect("Enter new PIN")
child.sendline(new_pin)
child.expect("Enter the same PIN again")
child.sendline(new_pin_confirmed)
PIN = new_pin
output = child.before.strip()
testminlen = child.before
idx = child.expect(["FIDO_ERR_PIN_POLICY_VIOLATION", pexpect.EOF], timeout=1)
if idx == 0:
command = f"{FIDO_COMMAND} -info -device {device_digit}"
# Run the command
info = pexpect.spawn(command, encoding="utf-8")
info.expect(pexpect.EOF)
info_text = info.before # <-- now this contains the full text
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)
else:
min_pin_len = "?"
messagebox.showerror(
"PIN not accepted",
f"The provided PIN violates the device policy.\n"
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()
if "error" in output.lower() or "FIDO_ERR" in output:
messagebox.showerror("PIN Change Failed", output)
else:
messagebox.showinfo("Success", "PIN successfully changed!")
except pexpect.exceptions.TIMEOUT:
messagebox.showerror("Timeout", "The device did not respond in time.")
except Exception as e:
messagebox.showerror("Error", str(e))
def refresh_combobox(): def refresh_combobox():
# Implement your refresh logic here # Implement your refresh logic here
@@ -317,7 +435,7 @@ def refresh_combobox():
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)
change_pin_button.config(state=tk.DISABLED) pin_button.config(state=tk.DISABLED)
device_list = ( device_list = (
get_device_list() get_device_list()
) # Assuming you have a function to get the device list ) # Assuming you have a function to get the device list
@@ -458,12 +576,10 @@ passkeys_button = ttk.Button(
) )
passkeys_button.pack(side=tk.LEFT, padx=5, pady=10) passkeys_button.pack(side=tk.LEFT, padx=5, pady=10)
# Create the "Change PIN" button pin_button = ttk.Button(
change_pin_button = ttk.Button( root, text="Set PIN", state=tk.DISABLED, command=set_pin
root, text="Change PIN", state=tk.DISABLED, command=change_pin
) )
change_pin_button.pack(side=tk.LEFT, padx=5, pady=10) pin_button.pack(side=tk.LEFT, padx=5, pady=10)
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)