306 lines
13 KiB
Python
306 lines
13 KiB
Python
"""
|
|
Smart Card Command GUI
|
|
|
|
Graphical interface for sending APDU commands to smart cards and escape commands to readers.
|
|
|
|
REQUIRES: pyscard, tkinter
|
|
For detailed requirements and installation, please refer to the README.md.
|
|
WARNING: Escape commands are hardware-specific and may vary by reader model.
|
|
"""
|
|
|
|
import platform
|
|
import sys
|
|
|
|
# Import API functions
|
|
from API import list_readers, parse_command_string, connect_to_reader, send_command, disconnect_reader
|
|
|
|
try:
|
|
from smartcard.util import toHexString
|
|
from smartcard.scard import SCardReleaseContext
|
|
except ImportError:
|
|
print("smartcard imports failed - pyscard may not be properly installed")
|
|
sys.exit(1)
|
|
|
|
try:
|
|
import tkinter as tk
|
|
from tkinter import ttk, messagebox
|
|
GUI_AVAILABLE = True
|
|
except ImportError:
|
|
GUI_AVAILABLE = False
|
|
if __name__ == "__main__":
|
|
error_msg = "tkinter library not found!\n\n"
|
|
system = platform.system()
|
|
if system == "Darwin":
|
|
error_msg += "For macOS, tkinter should be included with Python.\n"
|
|
error_msg += "If missing, install Python from python.org or use:\n"
|
|
error_msg += " brew install python-tk\n"
|
|
elif system == "Linux":
|
|
error_msg += "For Ubuntu/Linux, install tkinter using:\n"
|
|
error_msg += " sudo apt-get install python3-tk\n"
|
|
else:
|
|
error_msg += "For Windows, tkinter should be included with Python.\n"
|
|
error_msg += "If missing, reinstall Python and ensure 'tcl/tk and IDLE' is selected.\n"
|
|
|
|
print(error_msg)
|
|
if sys.stdout.isatty():
|
|
input("Press Enter to exit...")
|
|
sys.exit(1)
|
|
|
|
|
|
if GUI_AVAILABLE:
|
|
class SmartCardCommandUI:
|
|
def __init__(self, root):
|
|
self.root = root
|
|
self.root.title("Send Command")
|
|
|
|
try:
|
|
self.root.iconbitmap("ACS_Small.ico")
|
|
except Exception:
|
|
pass
|
|
|
|
self.platform = platform.system()
|
|
self.is_macos = self.platform == "Darwin"
|
|
self.is_linux = self.platform == "Linux"
|
|
|
|
if self.is_linux:
|
|
self.root.geometry("900x700")
|
|
self.root.minsize(850, 600)
|
|
elif self.is_macos:
|
|
self.root.geometry("850x650")
|
|
self.root.minsize(750, 550)
|
|
else:
|
|
self.root.geometry("850x650")
|
|
self.root.minsize(750, 550)
|
|
|
|
|
|
self.check_platform_requirements()
|
|
self.create_widgets()
|
|
self.refresh_readers()
|
|
|
|
def create_widgets(self):
|
|
# Reader selection frame
|
|
reader_frame = ttk.LabelFrame(self.root, text="Smart Card Reader", padding=10)
|
|
reader_frame.pack(fill="x", padx=10, pady=5)
|
|
|
|
reader_inner = ttk.Frame(reader_frame)
|
|
reader_inner.pack(fill="x")
|
|
|
|
ttk.Label(reader_inner, text="Select Reader:").pack(side="left", padx=5)
|
|
self.reader_var = tk.StringVar()
|
|
self.reader_combo = ttk.Combobox(reader_inner, textvariable=self.reader_var, state="readonly")
|
|
self.reader_combo.pack(side="left", fill="x", expand=True, padx=5)
|
|
|
|
self.refresh_button = ttk.Button(reader_inner, text="Refresh", command=self.refresh_readers)
|
|
self.refresh_button.pack(side="left", padx=5)
|
|
|
|
# Command frame (contains both APDU and Escape commands)
|
|
command_frame = ttk.LabelFrame(self.root, text="Commands", padding=10)
|
|
command_frame.pack(fill="x", padx=10, pady=5)
|
|
|
|
command_frame.columnconfigure(1, weight=1)
|
|
command_frame.columnconfigure(2, weight=0)
|
|
|
|
# APDU Command section
|
|
ttk.Label(command_frame, text="Enter APDU (hex):").grid(row=0, column=0, padx=5, sticky="w")
|
|
|
|
entry_width = 40 if self.is_linux else 35
|
|
self.apdu_entry = ttk.Entry(command_frame, width=entry_width)
|
|
self.apdu_entry.grid(row=0, column=1, padx=5, sticky="ew")
|
|
self.apdu_entry.insert(0, "FF CA 00 00 00")
|
|
|
|
self.send_apdu_button = ttk.Button(command_frame, text="Send APDU", command=self.send_apdu)
|
|
self.send_apdu_button.grid(row=0, column=2, padx=5, sticky="e")
|
|
|
|
# Escape Command section (under APDU)
|
|
ttk.Label(command_frame, text="Enter Escape Command (hex):").grid(row=1, column=0, padx=5, pady=(10,0), sticky="w")
|
|
|
|
self.escape_entry = ttk.Entry(command_frame, width=entry_width)
|
|
self.escape_entry.grid(row=1, column=1, padx=5, pady=(10,0), sticky="ew")
|
|
self.escape_entry.insert(0, "E0 00 00 18 00")
|
|
|
|
self.send_escape_button = ttk.Button(command_frame, text="Send Escape Command", command=self.send_escape_command)
|
|
self.send_escape_button.grid(row=1, column=2, padx=5, pady=(10,0), sticky="e")
|
|
|
|
# Response frame
|
|
response_frame = ttk.LabelFrame(self.root, text="Response", padding=10)
|
|
response_frame.pack(fill="both", expand=True, padx=10, pady=5)
|
|
|
|
text_height = 14 if self.is_linux else 10
|
|
self.response_text = tk.Text(response_frame, height=text_height, wrap=tk.WORD)
|
|
self.response_text.pack(fill="both", expand=True)
|
|
|
|
def check_platform_requirements(self):
|
|
if self.is_linux:
|
|
try:
|
|
import subprocess
|
|
result = subprocess.run(['systemctl', 'is-active', 'pcscd'],
|
|
capture_output=True, text=True, timeout=2)
|
|
if result.returncode != 0:
|
|
messagebox.showwarning(
|
|
"PCSC Service",
|
|
"pcscd service may not be running. Please ensure it is active.\n\n"
|
|
"On Ubuntu/Linux, you might need to run:\n"
|
|
" sudo systemctl start pcscd\n"
|
|
" sudo systemctl enable pcscd\n"
|
|
"If not installed:\n"
|
|
" sudo apt-get install pcscd libpcsclite1"
|
|
)
|
|
except (FileNotFoundError, subprocess.TimeoutExpired, Exception):
|
|
pass
|
|
|
|
def refresh_readers(self):
|
|
reader_list = list_readers()
|
|
self.reader_combo['values'] = [str(reader) for reader in reader_list]
|
|
if len(reader_list) > 0:
|
|
self.reader_combo.current(0)
|
|
else:
|
|
messagebox.showwarning("No Readers Found", "No smart card readers found! Please check connections and drivers.")
|
|
|
|
|
|
def send_apdu(self):
|
|
# Check if reader is selected
|
|
if not self.reader_var.get():
|
|
messagebox.showerror("Error", "Please select a reader first!")
|
|
return
|
|
|
|
command_str = self.apdu_entry.get().strip()
|
|
if not command_str:
|
|
error_msg = "Please enter an APDU command."
|
|
self.update_response_text(error_msg + "\n")
|
|
return
|
|
|
|
# Get selected reader
|
|
reader_list = list_readers()
|
|
selected_reader = None
|
|
for reader in reader_list:
|
|
if str(reader) == self.reader_var.get():
|
|
selected_reader = reader
|
|
break
|
|
|
|
if not selected_reader:
|
|
messagebox.showerror("Error", "Selected reader not found!")
|
|
return
|
|
|
|
connection = None
|
|
try:
|
|
# Connect to reader in APDU mode
|
|
connection, error_msg, _ = connect_to_reader(selected_reader)
|
|
if not connection:
|
|
help_msg = error_msg or "Connection failed. Please check reader and card."
|
|
messagebox.showerror("Error", help_msg)
|
|
return
|
|
|
|
# Send APDU command
|
|
success, response, sw1, sw2, error_msg = send_command(
|
|
connection,
|
|
command_str,
|
|
mode='apdu'
|
|
)
|
|
|
|
if success:
|
|
self.update_response_text(f"Sent APDU: {toHexString(parse_command_string(command_str))}\n")
|
|
self.update_response_text(f"Response Data: {toHexString(response)}\n")
|
|
self.update_response_text(f"Status Words: {hex(sw1)[2:].zfill(2).upper()} {hex(sw2)[2:].zfill(2).upper()}\n")
|
|
self.update_response_text("-" * 80 + "\n")
|
|
else:
|
|
self.update_response_text(f"Failed to send APDU: {error_msg}\n")
|
|
messagebox.showerror("Error", f"Failed to send APDU: {error_msg}")
|
|
|
|
except ValueError as e:
|
|
error_msg = f"Invalid APDU format: {str(e)}"
|
|
self.update_response_text(error_msg + "\n")
|
|
messagebox.showerror("Error", error_msg)
|
|
except Exception as e:
|
|
error_msg = f"Error sending APDU: {str(e)}"
|
|
self.update_response_text(error_msg + "\n")
|
|
messagebox.showerror("Error", error_msg)
|
|
finally:
|
|
# Always disconnect
|
|
disconnect_reader(connection)
|
|
|
|
def send_escape_command(self):
|
|
# Check if reader is selected
|
|
if not self.reader_var.get():
|
|
messagebox.showerror("Error", "Please select a reader first!")
|
|
return
|
|
|
|
command_str = self.escape_entry.get().strip()
|
|
if not command_str:
|
|
error_msg = "Please enter an escape command."
|
|
self.update_response_text(error_msg + "\n")
|
|
return
|
|
|
|
# Get selected reader
|
|
reader_list = list_readers()
|
|
selected_reader = None
|
|
for reader in reader_list:
|
|
if str(reader) == self.reader_var.get():
|
|
selected_reader = reader
|
|
break
|
|
|
|
if not selected_reader:
|
|
messagebox.showerror("Error", "Selected reader not found!")
|
|
return
|
|
|
|
hcard = None
|
|
hcontext = None
|
|
try:
|
|
# Connect to reader in escape mode (direct connection)
|
|
hcard, error_msg, hcontext = connect_to_reader(selected_reader, mode='direct')
|
|
if not hcard:
|
|
help_msg = error_msg or "Connection failed. Please check reader."
|
|
messagebox.showerror("Error", help_msg)
|
|
return
|
|
|
|
# Send escape command
|
|
command_bytes = parse_command_string(command_str)
|
|
self.update_response_text(f"Sending escape command: {toHexString(command_bytes)}\n")
|
|
success, response, error_msg = send_command(hcard, command_bytes, mode='escape')
|
|
|
|
if success:
|
|
self.update_response_text("Escape command sent successfully\n")
|
|
if response and len(response) > 0:
|
|
response_str = toHexString(response[:50]) if len(response) > 50 else toHexString(response)
|
|
self.update_response_text(f"Response ({len(response)} bytes): {response_str}\n")
|
|
else:
|
|
self.update_response_text("Response: (empty)\n")
|
|
self.update_response_text("-" * 80 + "\n")
|
|
else:
|
|
self.update_response_text(f"Failed to send escape command: {error_msg}\n")
|
|
messagebox.showerror("Error", f"Failed to send escape command: {error_msg}")
|
|
|
|
except ValueError as e:
|
|
error_msg = f"Invalid escape command format: {str(e)}"
|
|
self.update_response_text(error_msg + "\n")
|
|
messagebox.showerror("Error", error_msg)
|
|
except Exception as e:
|
|
error_msg = f"Error sending escape command: {str(e)}"
|
|
self.update_response_text(error_msg + "\n")
|
|
messagebox.showerror("Error", error_msg)
|
|
finally:
|
|
disconnect_reader(hcard, hcontext)
|
|
|
|
def update_response_text(self, message):
|
|
self.response_text.insert(tk.END, message)
|
|
self.response_text.see(tk.END)
|
|
|
|
def cleanup(self):
|
|
# No persistent connections to clean up since we connect/disconnect in each operation
|
|
pass
|
|
|
|
|
|
if __name__ == "__main__":
|
|
if GUI_AVAILABLE:
|
|
root = tk.Tk()
|
|
app = SmartCardCommandUI(root)
|
|
|
|
def on_closing():
|
|
app.cleanup()
|
|
root.destroy()
|
|
|
|
root.protocol("WM_DELETE_WINDOW", on_closing)
|
|
root.mainloop()
|
|
else:
|
|
print("GUI not available. Use the API functions programmatically:")
|
|
print(" # Import from API.py for programmatic usage")
|
|
print(" # from API import list_readers, connect_to_reader, send_command, disconnect_card") |