Update gui.py

This commit is contained in:
Token2
2026-01-08 19:51:12 +01:00
committed by GitHub
parent f7d4dad7cb
commit ffb284ca99

357
gui.py
View File

@@ -9,8 +9,8 @@ import pexpect
def detect_terminal(): def detect_terminal():
candidates = [ candidates = [
("gnome-terminal", ["--"]), # modern Ubuntu defaults ("gnome-terminal", ["--"]),
("x-terminal-emulator", ["-e"]),# Debian/Ubuntu wrapper ("x-terminal-emulator", ["-e"]),
("xterm", ["-e"]), ("xterm", ["-e"]),
("konsole", ["-e"]), ("konsole", ["-e"]),
("lxterminal", ["-e"]), ("lxterminal", ["-e"]),
@@ -22,85 +22,52 @@ def detect_terminal():
return term, flag return term, flag
return None, None return None, None
# 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, TERM_FLAG = detect_terminal() TERM, TERM_FLAG = detect_terminal()
if TERM is None: if TERM is None:
messagebox.showerror("Error", "No supported terminal emulator found. Please install xterm or gnome-terminal.") messagebox.showerror("Error", "No supported terminal emulator found. Please install xterm or gnome-terminal.")
sys.exit(1)
# 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(): def get_device_list():
try: 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 return device_list
except Exception as e: except Exception as e:
# 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
def execute_info_command(device_digit): def execute_info_command(device_digit):
global PIN global PIN
tree.delete(*tree.get_children()) tree.delete(*tree.get_children())
info_command = [FIDO_COMMAND, "-info", "-device", device_digit] info_command = [FIDO_COMMAND, "-info", "-device", device_digit]
try: try:
result = subprocess.run(info_command, capture_output=True, text=True) result = subprocess.run(info_command, capture_output=True, text=True)
# Check if the subprocess was executed successfully
if result.returncode == 0: if result.returncode == 0:
# Parse the output and insert into the treeview
for line in result.stdout.splitlines(): for line in result.stdout.splitlines():
if ": " in line: if ": " in line:
key, value = line.split(": ", 1) key, value = line.split(": ", 1)
tree.insert( tree.insert("", tk.END, values=(key, value))
"", tk.END, values=(key, value)
) # Append to the end of the grid
else: else:
raise subprocess.CalledProcessError(result.returncode, command) raise subprocess.CalledProcessError(result.returncode, info_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}"
)
storage_command = f"{FIDO_COMMAND} -storage -device {device_digit}" storage_command = f"{FIDO_COMMAND} -storage -device {device_digit}"
try: try:
child = pexpect.spawn(storage_command, encoding="utf-8", timeout=10) child = pexpect.spawn(storage_command, encoding="utf-8", timeout=10)
index = child.expect([r"Enter PIN for", pexpect.EOF, pexpect.TIMEOUT])
index = child.expect([
r"Enter PIN for",
pexpect.EOF,
pexpect.TIMEOUT
]
)
# catch stdout from cli
output = child.before output = child.before
if index == 0: if index == 0:
pin_button.config(text="Change PIN", state=tk.ACTIVE, command=change_pin) pin_button.config(text="Change PIN", state=tk.ACTIVE, command=change_pin)
#if PIN is not set
if index == 1: if index == 1:
messagebox.showwarning( messagebox.showwarning(
"Warning", "Warning",
@@ -113,156 +80,107 @@ def execute_info_command(device_digit):
pin_button.config(text="Set PIN", state=tk.ACTIVE, command=set_pin) pin_button.config(text="Set PIN", state=tk.ACTIVE, command=set_pin)
if "FIDO_ERR_PIN_INVALID" in output: if "FIDO_ERR_PIN_INVALID" in output:
messagebox.showerror("Error", f"Invalid PIN provided") messagebox.showerror("Error", "Invalid PIN provided")
if "FIDO_ERR_PIN_AUTH_BLOCKED" in output: if "FIDO_ERR_PIN_AUTH_BLOCKED" in output:
messagebox.showerror("Error", f"Wrong PIN provided to many times. Reinsert the key") messagebox.showerror("Error", "Wrong PIN provided too many times. Reinsert the key")
if "FIDO_ERR_INVALID_CBOR" in output: if "FIDO_ERR_INVALID_CBOR" in output:
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.", "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) messagebox.showerror("Unexpected Device Output", output)
return False return False
except Exception as e:
messagebox.showerror("Error", f"Command execution failed: {e}\nOutput: {result.stderr}")
def on_device_selected(event):
selected_device = device_var.get()
match = re.search(r"\[(\d+)\]", selected_device)
if match:
device_digit = match.group(1)
execute_info_command(device_digit)
passkeys_button.config(state=tk.NORMAL)
else:
messagebox.showinfo("Device Selected", "No digit found in the selected device")
def get_pin():
global PIN
PIN = simpledialog.askstring("PIN Code", "Enter your PIN code:", show="*")
return PIN
def on_passkeys_button_click():
global PIN
selected_device = device_var.get()
match = re.search(r"\[(\d+)\]", selected_device)
if not match:
messagebox.showinfo("Device Selected", "No digit found in the selected device")
return
device_digit = match.group(1)
if PIN is None:
get_pin()
if PIN is None:
return
command = [
FIDO_COMMAND,
"-residentKeys",
"-pin",
PIN,
"-device",
device_digit,
]
try:
result = subprocess.run(command, capture_output=True, text=True)
if result.returncode == 0:
domains = []
for line in result.stdout.splitlines():
match = re.search(r"= (.+)$", line)
if match:
domains.append(match.group(1))
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
)
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: 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}"
) )
# 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):
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)
if (execute_info_command(device_digit) == "PIN set"):
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
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 set_pin(): def set_pin():
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:
@@ -270,27 +188,30 @@ def set_pin():
device_digit = match.group(1) device_digit = match.group(1)
# 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:
PIN = None
return
new_pin_confirmed = simpledialog.askstring( new_pin_confirmed = simpledialog.askstring(
"Confirm new PIN", "Enter your new PIN code:", show="*" "Confirm new PIN", "Enter your new PIN code:", show="*"
) )
if (new_pin == new_pin_confirmed): if new_pin_confirmed is None:
PIN = None
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!")
continue
command = f"{FIDO_COMMAND} -setPIN -device {device_digit}" command = f"{FIDO_COMMAND} -setPIN -device {device_digit}"
# Enter new PIN in interactive shell
try: try:
child = pexpect.spawn(command, encoding="utf-8", timeout=20) child = pexpect.spawn(command, encoding="utf-8", timeout=20)
child.expect("Enter new PIN") child.expect("Enter new PIN")
child.sendline(new_pin) child.sendline(new_pin)
child.expect("Enter the same PIN again") child.expect("Enter the same PIN again")
@@ -301,28 +222,27 @@ 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)
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)
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"
) )
PIN = None
elif "error" in output.lower() or "FIDO_ERR" in output: elif "error" in output.lower() or "FIDO_ERR" in output:
messagebox.showerror("PIN Change Failed", output) messagebox.showerror("PIN Change Failed", output)
PIN = None
else: else:
messagebox.showinfo("Success", "PIN successfully set!") messagebox.showinfo("Success", "PIN successfully set!")
except pexpect.exceptions.TIMEOUT: except pexpect.exceptions.TIMEOUT:
messagebox.showerror("Timeout", "The device did not respond in time.") messagebox.showerror("Timeout", "The device did not respond in time.")
PIN = None
except Exception as e: except Exception as e:
messagebox.showerror("Error", str(e)) messagebox.showerror("Error", str(e))
PIN = None
def change_pin(): def change_pin():
global PIN global PIN
@@ -344,37 +264,32 @@ def change_pin():
new_pin_confirmed = simpledialog.askstring( new_pin_confirmed = simpledialog.askstring(
"Confirm new PIN", "Enter your new PIN code:", show="*" "Confirm new PIN", "Enter your new PIN code:", show="*"
) )
if (new_pin == new_pin_confirmed): 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!")
continue
command = f"{FIDO_COMMAND} -changePIN -device {device_digit}" command = f"{FIDO_COMMAND} -changePIN -device {device_digit}"
try: try:
child = pexpect.spawn(command, encoding="utf-8", timeout=20) child = pexpect.spawn(command, encoding="utf-8", timeout=20)
# --- Detect touch prompt ---
i = child.expect([ i = child.expect([
"Touch", "Touch",
"Tap", "Tap",
"Waiting for user", "Waiting for user",
"Enter current PIN", # sometimes no touch required "Enter current PIN",
pexpect.EOF, pexpect.EOF,
pexpect.TIMEOUT pexpect.TIMEOUT
]) ])
if i in [0, 1, 2]: if i in [0, 1, 2]:
# Prompt the user in the GUI
messagebox.showinfo( messagebox.showinfo(
"Touch Required", "Touch Required",
"Please touch your FIDO security key to continue." "Please touch your FIDO security key to continue."
) )
# Now wait until the key is actually touched
child.expect("Enter current PIN") child.expect("Enter current PIN")
# Now continue with PIN entry
child.sendline(old_pin) child.sendline(old_pin)
child.expect("Enter new PIN") child.expect("Enter new PIN")
@@ -386,19 +301,13 @@ 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)
# extract minpinlen
match = re.search(r"minpinlen:\s*(\d+)", info_text) match = re.search(r"minpinlen:\s*(\d+)", info_text)
if match: if match:
min_pin_len = match.group(1) min_pin_len = match.group(1)
@@ -411,10 +320,8 @@ def change_pin():
f"The PIN must be at least {min_pin_len} digits long and " f"The PIN must be at least {min_pin_len} digits long and "
f"must not be an easily guessable sequence (e.g. 123456)." f"must not be an easily guessable sequence (e.g. 123456)."
) )
return return
# If no violation detected, EOF happened normally
child.expect(pexpect.EOF) child.expect(pexpect.EOF)
output = child.before.strip() output = child.before.strip()
@@ -429,39 +336,28 @@ def change_pin():
messagebox.showerror("Error", str(e)) messagebox.showerror("Error", str(e))
def refresh_combobox(): def refresh_combobox():
# Implement your refresh logic here device_combobox.set("")
# 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()) tree.delete(*tree.get_children())
passkeys_button.config(state=tk.DISABLED) passkeys_button.config(state=tk.DISABLED)
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
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
# Function to show the output in a new window
def show_output_in_new_window(output, device_digit): def show_output_in_new_window(output, device_digit):
# Create a new window
new_window = tk.Toplevel(root) new_window = tk.Toplevel(root)
new_window.geometry("800x650") new_window.geometry("800x650")
new_window.title("Resident Keys / Passkeys") new_window.title("Resident Keys / Passkeys")
# Create a Treeview widget for displaying output
tree_new_window = ttk.Treeview( tree_new_window = ttk.Treeview(
new_window, columns=("Domain", "Credential ID", "User"), show="headings" new_window, columns=("Domain", "Credential ID", "User"), show="headings"
) )
# Set column headings
tree_new_window.heading("Domain", text="Domain") tree_new_window.heading("Domain", text="Domain")
tree_new_window.heading("Credential ID", text="Credential ID") tree_new_window.heading("Credential ID", text="Credential ID")
tree_new_window.heading("User", text="User") tree_new_window.heading("User", text="User")
tree_new_window.pack(expand=True, fill=tk.BOTH, padx=10, pady=10) tree_new_window.pack(expand=True, fill=tk.BOTH, padx=10, pady=10)
# Add scrollbars to the Treeview
tree_scrollbar_y = ttk.Scrollbar( tree_scrollbar_y = ttk.Scrollbar(
new_window, orient="vertical", command=tree_new_window.yview new_window, orient="vertical", command=tree_new_window.yview
) )
@@ -473,7 +369,6 @@ def show_output_in_new_window(output, device_digit):
tree_scrollbar_x.pack(side="bottom", fill="x") tree_scrollbar_x.pack(side="bottom", fill="x")
tree_new_window.configure(xscrollcommand=tree_scrollbar_x.set) tree_new_window.configure(xscrollcommand=tree_scrollbar_x.set)
# Parse the output and insert into the Treeview
current_domain = "" current_domain = ""
for line in output.splitlines(): for line in output.splitlines():
if line.startswith("Domain: "): if line.startswith("Domain: "):
@@ -486,13 +381,10 @@ 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
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]
1
] # Get the Credential ID of the selected item
new_window.destroy() new_window.destroy()
command = [ command = [
FIDO_COMMAND, FIDO_COMMAND,
@@ -507,49 +399,40 @@ 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
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)
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\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 = tk.Tk()
root.geometry("700x600") # Width x Height root.geometry("700x600")
root.title("FIDO2.1 Manager - Python version 0.1 - (c) Token2") 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 = ttk.Frame(root)
top_frame.pack(side=tk.TOP, fill=tk.X) top_frame.pack(side=tk.TOP, fill=tk.X)
# Create a label for the dropdown
label = tk.Label(top_frame, text="Select Device:") label = tk.Label(top_frame, text="Select Device:")
label.pack(side=tk.LEFT, padx=10, pady=10) label.pack(side=tk.LEFT, padx=10, pady=10)
# 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
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)
# Create a Treeview widget for displaying output with scrollbars
tree_frame = ttk.Frame(root) tree_frame = ttk.Frame(root)
tree_frame.pack(expand=True, fill=tk.BOTH, padx=10, pady=10) tree_frame.pack(expand=True, fill=tk.BOTH, padx=10, pady=10)
tree_scrollbar_y = ttk.Scrollbar(tree_frame, orient="vertical") tree_scrollbar_y = ttk.Scrollbar(tree_frame, orient="vertical")
@@ -565,12 +448,10 @@ tree_scrollbar_y.config(command=tree.yview)
tree_scrollbar_x.config(command=tree.xview) tree_scrollbar_x.config(command=tree.xview)
tree_scrollbar_y.pack(side="right", fill="y") tree_scrollbar_y.pack(side="right", fill="y")
tree_scrollbar_x.pack(side="bottom", fill="x") tree_scrollbar_x.pack(side="bottom", fill="x")
# Set column headings
tree.heading("Key", text="Key") 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
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
) )
@@ -584,6 +465,4 @@ 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)
# Run the Tkinter main loop
root.mainloop() root.mainloop()