mirror of
https://github.com/token2/fido2-manage.git
synced 2026-04-09 10:45:39 +00:00
for DMG files for MacOS distribution
helper scripts to produce DMG files for MacOS distribution
This commit is contained in:
128
build_macos.sh
Normal file
128
build_macos.sh
Normal file
@@ -0,0 +1,128 @@
|
||||
#!/bin/bash
|
||||
set -eo pipefail
|
||||
|
||||
# --- Configuration ---
|
||||
APP_NAME="fido2-manage"
|
||||
CLI_EXECUTABLE_NAME="fido2-token2"
|
||||
FINAL_APP_NAME="${APP_NAME}.app"
|
||||
DMG_NAME="${APP_NAME}.dmg"
|
||||
VOL_NAME="FIDO2 Manager"
|
||||
BUILD_DIR="build"
|
||||
DIST_DIR="dist"
|
||||
STAGING_DIR="${BUILD_DIR}/staging" # A clean directory for our portable binaries
|
||||
|
||||
# --- Helper Functions ---
|
||||
info() {
|
||||
echo "[INFO] $1"
|
||||
}
|
||||
|
||||
fatal() {
|
||||
echo "[FATAL] $1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
check_command() {
|
||||
if ! command -v "$1" &> /dev/null; then
|
||||
fatal "'$1' is not installed. Please install it first for the build environment."
|
||||
fi
|
||||
}
|
||||
|
||||
# --- Build Steps ---
|
||||
|
||||
# 1. Prerequisite Checks for the Build Machine
|
||||
info "Checking build machine prerequisites..."
|
||||
check_command "cmake"
|
||||
check_command "hdiutil"
|
||||
check_command "otool"
|
||||
check_command "install_name_tool"
|
||||
if ! command -v "brew" &> /dev/null; then
|
||||
fatal "Homebrew is not installed. It is required to fetch build dependencies."
|
||||
fi
|
||||
dependencies=("pkg-config" "openssl@3" "libcbor" "zlib" "python-tk")
|
||||
info "Checking Homebrew dependencies..."
|
||||
for dep in "${dependencies[@]}"; do
|
||||
if ! brew list "$dep" &>/dev/null; then
|
||||
info "Dependency '$dep' not found. Installing with Homebrew..."
|
||||
brew install "$dep" || fatal "Failed to install '$dep'."
|
||||
fi
|
||||
done
|
||||
PYTHON_EXEC="/opt/homebrew/bin/python3"
|
||||
|
||||
# 2. Setup Python environment and install PyInstaller
|
||||
info "Setting up Python virtual environment and installing PyInstaller..."
|
||||
if [ -d ".venv" ]; then rm -rf ".venv"; fi
|
||||
"$PYTHON_EXEC" -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install --upgrade pip
|
||||
# Let pip choose the latest compatible version of PyInstaller
|
||||
pip install pyinstaller
|
||||
deactivate
|
||||
|
||||
# 3. Build the C++ binary
|
||||
info "Building the C++ binary: ${CLI_EXECUTABLE_NAME}..."
|
||||
# Clean up old build artifacts to ensure a fresh build
|
||||
if [ -d "$BUILD_DIR" ]; then rm -rf "$BUILD_DIR"; fi
|
||||
if [ -d "$DIST_DIR" ]; then rm -rf "$DIST_DIR"; fi
|
||||
if [ -f "${APP_NAME}.spec" ]; then rm -f "${APP_NAME}.spec"; fi
|
||||
|
||||
mkdir -p "$STAGING_DIR"
|
||||
cmake -S . -B "$BUILD_DIR" -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES="arm64"
|
||||
cmake --build "$BUILD_DIR" --config Release
|
||||
CLI_BINARY_PATH="${BUILD_DIR}/tools/${CLI_EXECUTABLE_NAME}"
|
||||
if [ ! -f "$CLI_BINARY_PATH" ]; then
|
||||
fatal "Build failed. C++ Executable not found."
|
||||
fi
|
||||
|
||||
# 4. Make the C++ binary and its dependencies portable
|
||||
info "Making the C++ binary and its libraries portable..."
|
||||
if [ ! -f "./bundle_libs.sh" ]; then
|
||||
fatal "./bundle_libs.sh not found. Please create it first."
|
||||
fi
|
||||
chmod +x ./bundle_libs.sh
|
||||
# The bundle_libs script copies the binary and its dylib dependencies into the staging folder
|
||||
# and corrects their internal linkage paths.
|
||||
./bundle_libs.sh "$STAGING_DIR" "$CLI_BINARY_PATH"
|
||||
|
||||
# 4.1 Fix library version compatibility issues
|
||||
info "Fixing library version compatibility..."
|
||||
cd "$STAGING_DIR"
|
||||
# Create symlink for libcbor version compatibility
|
||||
if [ -f "libcbor.0.12.dylib" ] && [ ! -f "libcbor.0.11.dylib" ]; then
|
||||
ln -sf libcbor.0.12.dylib libcbor.0.11.dylib
|
||||
fi
|
||||
cd ..
|
||||
|
||||
# 4.2 Fix library linking (this must be done on macOS)
|
||||
info "Fixing library linking..."
|
||||
if [ ! -f "./fix_macos_linking.sh" ]; then
|
||||
fatal "./fix_macos_linking.sh not found. Please run this script to create it."
|
||||
fi
|
||||
chmod +x ./fix_macos_linking.sh
|
||||
./fix_macos_linking.sh
|
||||
|
||||
info "Portable binary and libraries are in ${STAGING_DIR}. Verifying linkage..."
|
||||
# This check is for you to confirm it worked. All paths should start with @rpath or be system paths.
|
||||
otool -L "${STAGING_DIR}/${CLI_EXECUTABLE_NAME}"
|
||||
|
||||
# 5. Create the standalone macOS app using PyInstaller
|
||||
info "Creating the standalone .app bundle with PyInstaller..."
|
||||
source .venv/bin/activate
|
||||
# Bundle the Python script and include the entire staging directory as data.
|
||||
# PyInstaller will place the contents of the staging directory in the root of the app bundle.
|
||||
pyinstaller --name "$APP_NAME" \
|
||||
--windowed \
|
||||
--noconsole \
|
||||
--add-data "${STAGING_DIR}/*:." \
|
||||
gui-mac.py
|
||||
deactivate
|
||||
|
||||
# 6. Package the final .app into a .dmg
|
||||
info "Packaging into ${DMG_NAME}..."
|
||||
APP_BUNDLE_PATH="${DIST_DIR}/${FINAL_APP_NAME}"
|
||||
FINAL_DMG_PATH="${DIST_DIR}/${DMG_NAME}"
|
||||
|
||||
if [ -f "$FINAL_DMG_PATH" ]; then rm -f "$FINAL_DMG_PATH"; fi
|
||||
|
||||
hdiutil create -fs HFS+ -srcfolder "$APP_BUNDLE_PATH" -volname "$VOL_NAME" "$FINAL_DMG_PATH"
|
||||
|
||||
info "Process complete! The final distributable file is: ${FINAL_DMG_PATH}"
|
||||
131
bundle_libs.sh
Normal file
131
bundle_libs.sh
Normal file
@@ -0,0 +1,131 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Standalone script to bundle Homebrew libraries with a macOS binary
|
||||
# This makes the binary portable and self-contained
|
||||
#
|
||||
set -e
|
||||
|
||||
# Configuration
|
||||
TARGET_DIR="$1"
|
||||
BINARY_PATH="$2"
|
||||
|
||||
if [[ ! -d "$TARGET_DIR" ]] || [[ ! -f "$BINARY_PATH" ]]; then
|
||||
echo "Usage: $0 <target_directory> <path_to_binary>"
|
||||
echo "Example: $0 ./staging ./build/tools/fido2-token2"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Copy the main binary into the target directory
|
||||
BINARY_NAME=$(basename "$BINARY_PATH")
|
||||
cp "$BINARY_PATH" "$TARGET_DIR/"
|
||||
PORTABLE_BINARY_PATH="${TARGET_DIR}/${BINARY_NAME}"
|
||||
|
||||
echo "--- Making ${BINARY_NAME} portable ---"
|
||||
echo "Target directory: $TARGET_DIR"
|
||||
|
||||
# Keep track of processed libraries to avoid infinite loops
|
||||
processed_libs=":"
|
||||
|
||||
# Function to process a single binary (executable or library)
|
||||
process_binary() {
|
||||
local file_to_fix="$1"
|
||||
local depth="${2:-0}"
|
||||
local indent=$(printf "%*s" $((depth * 2)) "")
|
||||
|
||||
echo "${indent}Processing: $(basename "$file_to_fix")"
|
||||
|
||||
# Get the list of Homebrew library dependencies, removing trailing colons from otool output
|
||||
local deps=$(otool -L "$file_to_fix" 2>/dev/null | grep '/opt/homebrew/' | awk '{print $1}' | tr -d ':' | grep -v "^$file_to_fix$" || true)
|
||||
|
||||
if [[ -z "$deps" ]]; then
|
||||
echo "${indent} No Homebrew dependencies found"
|
||||
return
|
||||
fi
|
||||
|
||||
while IFS= read -r lib; do
|
||||
if [[ -z "$lib" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
local lib_name=$(basename "$lib")
|
||||
local new_lib_path="${TARGET_DIR}/${lib_name}"
|
||||
|
||||
# Skip if already processed
|
||||
if echo "$processed_libs" | grep -q ":${lib_name}:"; then
|
||||
echo "${indent} -> ${lib_name} (already processed, updating reference)"
|
||||
install_name_tool -change "$lib" "@executable_path/${lib_name}" "$file_to_fix" 2>/dev/null || {
|
||||
echo "${indent} Warning: Could not update reference to ${lib_name}"
|
||||
}
|
||||
continue
|
||||
fi
|
||||
|
||||
# Mark as processed
|
||||
processed_libs="${processed_libs}${lib_name}:"
|
||||
|
||||
# Copy the library if it doesn't exist
|
||||
if [[ ! -f "$new_lib_path" ]]; then
|
||||
echo "${indent} -> Copying ${lib_name}"
|
||||
if ! cp "$lib" "$TARGET_DIR/"; then
|
||||
echo "${indent} Error: Could not copy ${lib}"
|
||||
continue
|
||||
fi
|
||||
chmod 755 "$new_lib_path"
|
||||
fi
|
||||
|
||||
# Update the dependency path in the current binary
|
||||
echo "${indent} -> Updating reference to ${lib_name}"
|
||||
install_name_tool -change "$lib" "@executable_path/${lib_name}" "$file_to_fix" 2>/dev/null || {
|
||||
echo "${indent} Warning: Could not update reference to ${lib_name}"
|
||||
}
|
||||
|
||||
# Set the library's own ID to be relative (only for libraries, not the main executable)
|
||||
if [[ "$new_lib_path" != "$PORTABLE_BINARY_PATH" ]]; then
|
||||
install_name_tool -id "@executable_path/${lib_name}" "$new_lib_path" 2>/dev/null || {
|
||||
echo "${indent} Warning: Could not set ID for ${lib_name}"
|
||||
}
|
||||
fi
|
||||
|
||||
# Recursively process this library's dependencies
|
||||
if [[ -f "$new_lib_path" ]] && [[ $depth -lt 5 ]]; then
|
||||
process_binary "$new_lib_path" $((depth + 1))
|
||||
fi
|
||||
|
||||
done <<< "$deps"
|
||||
}
|
||||
|
||||
# Start processing with the main executable
|
||||
echo "Starting dependency analysis..."
|
||||
process_binary "$PORTABLE_BINARY_PATH"
|
||||
|
||||
echo ""
|
||||
echo "--- Bundling Summary ---"
|
||||
echo "Bundled libraries:"
|
||||
ls -la "$TARGET_DIR"
|
||||
|
||||
echo ""
|
||||
echo "--- Final Verification ---"
|
||||
echo "Main executable dependencies:"
|
||||
otool -L "$PORTABLE_BINARY_PATH"
|
||||
|
||||
echo ""
|
||||
echo "--- Checking for remaining external dependencies ---"
|
||||
remaining_deps=$(otool -L "$PORTABLE_BINARY_PATH" | grep -E '/opt/homebrew/|/usr/local/' | grep -v '@executable_path' || true)
|
||||
if [[ -n "$remaining_deps" ]]; then
|
||||
echo "WARNING: Some external dependencies remain:"
|
||||
echo "$remaining_deps"
|
||||
else
|
||||
echo "SUCCESS: All external dependencies have been bundled!"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "--- Library ID Verification ---"
|
||||
for lib in "$TARGET_DIR"/*.dylib; do
|
||||
if [[ -f "$lib" ]]; then
|
||||
lib_name=$(basename "$lib")
|
||||
lib_id=$(otool -D "$lib" | tail -n1)
|
||||
echo "${lib_name}: ${lib_id}"
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "Bundling complete for ${BINARY_NAME}"
|
||||
103
fix_macos_linking.sh
Normal file
103
fix_macos_linking.sh
Normal file
@@ -0,0 +1,103 @@
|
||||
#!/bin/bash
|
||||
# Script to fix macOS library linking for fido2-token2 binary
|
||||
# This script should be run on macOS with proper tools
|
||||
|
||||
set -e
|
||||
|
||||
STAGING_DIR="build/staging"
|
||||
BINARY_NAME="fido2-token2"
|
||||
BINARY_PATH="${STAGING_DIR}/${BINARY_NAME}"
|
||||
|
||||
# Also check alternative paths
|
||||
if [[ ! -f "$BINARY_PATH" ]]; then
|
||||
STAGING_DIR="staging"
|
||||
BINARY_PATH="${STAGING_DIR}/${BINARY_NAME}"
|
||||
fi
|
||||
|
||||
echo "=== Fixing macOS Library Linking ==="
|
||||
echo "Target binary: $BINARY_PATH"
|
||||
|
||||
# Check if we're on macOS
|
||||
if [[ "$OSTYPE" != "darwin"* ]]; then
|
||||
echo "ERROR: This script must be run on macOS"
|
||||
echo "Please run this script on your macOS build machine"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if required tools are available
|
||||
for tool in otool install_name_tool; do
|
||||
if ! command -v "$tool" &> /dev/null; then
|
||||
echo "ERROR: $tool is required but not found"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Check if binary exists
|
||||
if [[ ! -f "$BINARY_PATH" ]]; then
|
||||
echo "ERROR: Binary not found at $BINARY_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Current library dependencies:"
|
||||
otool -L "$BINARY_PATH"
|
||||
|
||||
echo ""
|
||||
echo "=== Fixing Library References ==="
|
||||
|
||||
# Fix libcbor reference
|
||||
echo "Fixing libcbor reference..."
|
||||
install_name_tool -change "/opt/homebrew/opt/libcbor/lib/libcbor.0.11.dylib" "@executable_path/libcbor.0.11.dylib" "$BINARY_PATH"
|
||||
install_name_tool -change "/opt/homebrew/Cellar/libcbor/0.12.0/lib/libcbor.0.11.dylib" "@executable_path/libcbor.0.11.dylib" "$BINARY_PATH"
|
||||
|
||||
# Fix OpenSSL reference
|
||||
echo "Fixing OpenSSL reference..."
|
||||
install_name_tool -change "/opt/homebrew/opt/openssl@3/lib/libcrypto.3.dylib" "@executable_path/libcrypto.3.dylib" "$BINARY_PATH"
|
||||
|
||||
# Fix libfido2 @rpath reference (from local build)
|
||||
echo "Fixing libfido2 @rpath reference..."
|
||||
install_name_tool -change "@rpath/libfido2.1.dylib" "@executable_path/libfido2.1.dylib" "$BINARY_PATH"
|
||||
|
||||
# Fix any other homebrew references
|
||||
echo "Checking for remaining homebrew references..."
|
||||
homebrew_deps=$(otool -L "$BINARY_PATH" | grep -E '/opt/homebrew/|/usr/local/' | awk '{print $1}' || true)
|
||||
|
||||
if [[ -n "$homebrew_deps" ]]; then
|
||||
echo "Found additional homebrew dependencies to fix:"
|
||||
while IFS= read -r dep; do
|
||||
if [[ -n "$dep" ]]; then
|
||||
lib_name=$(basename "$dep")
|
||||
echo " Fixing: $dep -> @executable_path/$lib_name"
|
||||
install_name_tool -change "$dep" "@executable_path/$lib_name" "$BINARY_PATH"
|
||||
fi
|
||||
done <<< "$homebrew_deps"
|
||||
fi
|
||||
|
||||
# Fix library IDs for the bundled libraries
|
||||
echo ""
|
||||
echo "=== Fixing Library IDs ==="
|
||||
for lib in "${STAGING_DIR}"/*.dylib; do
|
||||
if [[ -f "$lib" ]]; then
|
||||
lib_name=$(basename "$lib")
|
||||
echo "Setting ID for $lib_name"
|
||||
install_name_tool -id "@executable_path/$lib_name" "$lib"
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "=== Final Verification ==="
|
||||
echo "Updated library dependencies:"
|
||||
otool -L "$BINARY_PATH"
|
||||
|
||||
echo ""
|
||||
echo "Checking for remaining external dependencies..."
|
||||
remaining_deps=$(otool -L "$BINARY_PATH" | grep -E '/opt/homebrew/|/usr/local/' | grep -v '@executable_path' || true)
|
||||
if [[ -n "$remaining_deps" ]]; then
|
||||
echo "WARNING: Some external dependencies remain:"
|
||||
echo "$remaining_deps"
|
||||
exit 1
|
||||
else
|
||||
echo "SUCCESS: All external dependencies have been fixed!"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Library linking fix complete!"
|
||||
344
gui-mac.py
Normal file
344
gui-mac.py
Normal file
@@ -0,0 +1,344 @@
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tkinter as tk
|
||||
from tkinter import messagebox, simpledialog, ttk
|
||||
|
||||
# --- Path Resolution for pyinstaller Bundle ---
|
||||
def get_fido_command_path():
|
||||
"""
|
||||
Determines the absolute path to the fido2-token2 binary, whether running
|
||||
as a script or as a bundled pyinstaller app. Enhanced for self-contained builds.
|
||||
"""
|
||||
if getattr(sys, 'frozen', False):
|
||||
# We are running in a PyInstaller bundle
|
||||
if hasattr(sys, '_MEIPASS'):
|
||||
# PyInstaller bundle with temporary directory
|
||||
base_path = sys._MEIPASS
|
||||
else:
|
||||
# PyInstaller one-file bundle
|
||||
base_path = os.path.dirname(sys.executable)
|
||||
|
||||
# Print debug info for troubleshooting
|
||||
print(f"[DEBUG] Running in bundle mode, base_path: {base_path}")
|
||||
print(f"[DEBUG] Contents of base_path: {os.listdir(base_path) if os.path.exists(base_path) else 'NOT FOUND'}")
|
||||
|
||||
# Primary locations to check for the fido2-token2 binary
|
||||
binary_locations = [
|
||||
os.path.join(base_path, "fido2-token2"),
|
||||
os.path.join(base_path, "Contents", "MacOS", "fido2-token2"),
|
||||
os.path.join(base_path, "Contents", "Frameworks", "fido2-token2"),
|
||||
os.path.join(base_path, "MacOS", "fido2-token2"),
|
||||
os.path.join(base_path, "Frameworks", "fido2-token2"),
|
||||
os.path.join(os.path.dirname(base_path), "fido2-token2"),
|
||||
os.path.join(os.path.dirname(base_path), "Frameworks", "fido2-token2"),
|
||||
os.path.join(os.path.dirname(base_path), "MacOS", "fido2-token2")
|
||||
]
|
||||
|
||||
for binary_path in binary_locations:
|
||||
print(f"[DEBUG] Checking for binary: {binary_path}")
|
||||
if os.path.exists(binary_path):
|
||||
print(f"[DEBUG] Found fido2-token2 binary at: {binary_path}")
|
||||
return binary_path
|
||||
|
||||
# Final fallback - return first location even if not found
|
||||
print(f"[DEBUG] Binary not found, returning default: {binary_locations[0]}")
|
||||
return binary_locations[0]
|
||||
else:
|
||||
# We are running in a normal Python environment for development.
|
||||
# Look for fido2-token2 binary in common development locations
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
dev_locations = [
|
||||
os.path.join(script_dir, "build", "tools", "fido2-token2"),
|
||||
os.path.join(script_dir, "tools", "fido2-token2"),
|
||||
os.path.join(script_dir, "fido2-token2"),
|
||||
"./build/tools/fido2-token2",
|
||||
"./tools/fido2-token2",
|
||||
"./fido2-token2",
|
||||
"fido2-token2" # System PATH lookup
|
||||
]
|
||||
|
||||
for binary_path in dev_locations:
|
||||
if os.path.exists(binary_path):
|
||||
print(f"[DEBUG] Found development binary at: {binary_path}")
|
||||
return binary_path
|
||||
|
||||
# Fallback to system-wide installation
|
||||
print(f"[DEBUG] Using system PATH lookup for fido2-token2")
|
||||
return "fido2-token2"
|
||||
|
||||
print("FIDO2 Manager GUI starting...")
|
||||
FIDO_COMMAND = get_fido_command_path()
|
||||
print(f"FIDO2 Command Path: {FIDO_COMMAND}")
|
||||
print(f"Script exists: {os.path.exists(FIDO_COMMAND)}")
|
||||
|
||||
def get_device_number_from_string(device_string):
|
||||
"""
|
||||
Extract device number from device string format "Device [1] : SoloKeys"
|
||||
"""
|
||||
import re
|
||||
match = re.search(r'Device \[(\d+)\]', device_string)
|
||||
if match:
|
||||
return match.group(1)
|
||||
return "1" # Default to device 1
|
||||
# --- End of Path Resolution ---
|
||||
|
||||
# Global variable to store the PIN for the current session
|
||||
PIN = None
|
||||
|
||||
# --- Core Functions ---
|
||||
|
||||
def get_device_list():
|
||||
"""Gets the list of connected FIDO devices by calling the C binary directly."""
|
||||
try:
|
||||
if not os.path.exists(FIDO_COMMAND):
|
||||
messagebox.showerror("Dependency Error", f"Required tool not found at: {FIDO_COMMAND}")
|
||||
return []
|
||||
|
||||
# Call fido2-token2 -L directly to list devices
|
||||
result = subprocess.run([FIDO_COMMAND, "-L"], capture_output=True, text=True, check=True)
|
||||
|
||||
# Parse the output to format like the shell script
|
||||
device_list = []
|
||||
device_count = 1
|
||||
|
||||
for line in result.stdout.strip().split("\n"):
|
||||
if line.strip():
|
||||
# Extract device info (look for parentheses with device type)
|
||||
import re
|
||||
match = re.search(r'\(([^)]+)\)', line)
|
||||
if match:
|
||||
device_type = match.group(1)
|
||||
device_list.append(f"Device [{device_count}] : {device_type}")
|
||||
device_count += 1
|
||||
else:
|
||||
# Fallback if no parentheses found
|
||||
device_list.append(f"Device [{device_count}] : {line.strip()}")
|
||||
device_count += 1
|
||||
|
||||
return device_list
|
||||
except Exception as e:
|
||||
error_message = f"Error getting device list: {e}\n\nDetails:\n{getattr(e, 'stderr', '')}"
|
||||
messagebox.showerror("Execution Error", error_message)
|
||||
return []
|
||||
|
||||
def get_device_path_by_number(device_number):
|
||||
"""Get the actual device path string for a given device number."""
|
||||
try:
|
||||
# Get device list from fido2-token2 -L
|
||||
result = subprocess.run([FIDO_COMMAND, "-L"], capture_output=True, text=True, check=True)
|
||||
lines = [line.strip() for line in result.stdout.split('\n') if line.strip()]
|
||||
|
||||
# Convert device_number to index (1-based to 0-based)
|
||||
device_index = int(device_number) - 1
|
||||
|
||||
if 0 <= device_index < len(lines):
|
||||
device_line = lines[device_index]
|
||||
# Extract device path (everything before the first colon and space)
|
||||
# Handle both "path: info" and "path:info" formats
|
||||
if ':' in device_line:
|
||||
# Split on ':' and take first part, but handle "pcsc://slot0:" specially
|
||||
if device_line.startswith('pcsc://'):
|
||||
return "pcsc://slot0"
|
||||
else:
|
||||
# For other formats like "/dev/hidraw0: ..."
|
||||
parts = device_line.split(':', 1)
|
||||
return parts[0].strip()
|
||||
else:
|
||||
# If no colon, return the whole line
|
||||
return device_line
|
||||
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Failed to get device path: {e}")
|
||||
return None
|
||||
|
||||
def execute_info_command(device_string):
|
||||
"""Fetches and displays device info in the treeview."""
|
||||
tree.delete(*tree.get_children())
|
||||
# Extract device number from device_string
|
||||
device_number = get_device_number_from_string(device_string)
|
||||
|
||||
# Get the actual device path for this device number
|
||||
device_path = get_device_path_by_number(device_number)
|
||||
if not device_path:
|
||||
messagebox.showerror("Error", f"Could not find device path for device {device_number}")
|
||||
return
|
||||
|
||||
print(f"[DEBUG] Using device path: {device_path}")
|
||||
|
||||
# Build PIN arguments for fido2-token2 (-w pin)
|
||||
pin_args = ["-w", PIN] if PIN else []
|
||||
|
||||
# Commands to run with fido2-token2 directly
|
||||
commands_to_run = {
|
||||
"info": [FIDO_COMMAND, "-I"] + pin_args + [device_path],
|
||||
"storage": [FIDO_COMMAND, "-I", "-c"] + pin_args + [device_path],
|
||||
}
|
||||
|
||||
for key, command in commands_to_run.items():
|
||||
try:
|
||||
print(f"[DEBUG] Running command: {' '.join(command)}")
|
||||
result = subprocess.run(command, capture_output=True, text=True)
|
||||
|
||||
if result.returncode != 0:
|
||||
print(f"[DEBUG] Command {key} failed with code {result.returncode}")
|
||||
print(f"[DEBUG] stderr: {result.stderr}")
|
||||
|
||||
if "FIDO_ERR_PIN_INVALID" in result.stderr:
|
||||
messagebox.showerror("Error", "Invalid PIN provided.")
|
||||
return # Stop processing on bad PIN
|
||||
if "FIDO_ERR_PIN_REQUIRED" in result.stderr:
|
||||
messagebox.showerror("Error", "A PIN is required for this operation.")
|
||||
return # Stop processing
|
||||
continue # Try next command even if one fails for other reasons
|
||||
|
||||
# Parse output and add to tree
|
||||
for line in result.stdout.splitlines():
|
||||
if ": " in line:
|
||||
k, v = line.split(": ", 1)
|
||||
tree.insert("", tk.END, values=(k.strip(), v.strip()))
|
||||
elif line.strip():
|
||||
# For lines without colon, add as single value
|
||||
tree.insert("", tk.END, values=(line.strip(), ""))
|
||||
|
||||
except Exception as e:
|
||||
messagebox.showerror("Error", f"Command '{key}' failed: {e}\nOutput: {getattr(e, 'stderr', '')}")
|
||||
|
||||
check_passkeys_button_state()
|
||||
check_changepin_button_state()
|
||||
|
||||
# --- UI Event Handlers ---
|
||||
|
||||
def set_pin_and_get_info(device_string):
|
||||
"""Prompts for PIN and then fetches device info."""
|
||||
global PIN
|
||||
PIN = simpledialog.askstring("PIN Code", "Enter PIN code:", show="*")
|
||||
if PIN is not None: # Proceed even if PIN is empty, but not if cancelled
|
||||
execute_info_command(device_string)
|
||||
|
||||
def on_device_selected(event):
|
||||
"""Handles the device selection from the dropdown."""
|
||||
selected_text = device_var.get()
|
||||
# Robust regex to find device paths on Linux, macOS, or Windows
|
||||
match = re.search(r"(/dev/\w+)|(pcsc://\S+)|(windows://\S+)|(ioreg://\S+)", selected_text)
|
||||
if match:
|
||||
device_string = match.group(0).split(':')[0] # Get the path part before any colon
|
||||
set_pin_and_get_info(device_string)
|
||||
else:
|
||||
messagebox.showerror("Device Error", "Could not parse device path from selection.")
|
||||
|
||||
def refresh_combobox():
|
||||
"""Refreshes the device list in the dropdown."""
|
||||
device_list = get_device_list()
|
||||
if not device_list:
|
||||
device_combobox['values'] = ["No FIDO devices found."]
|
||||
else:
|
||||
device_combobox['values'] = device_list
|
||||
device_combobox.set("")
|
||||
tree.delete(*tree.get_children())
|
||||
passkeys_button.config(state=tk.DISABLED)
|
||||
change_pin_button.config(state=tk.DISABLED)
|
||||
|
||||
def show_about_message():
|
||||
"""Displays the about dialog."""
|
||||
messagebox.showinfo(
|
||||
"About",
|
||||
"FIDO2 Manager\n\n"
|
||||
"A utility to manage and interact with FIDO2 security keys.\n\n"
|
||||
"(c) TOKEN2 Sàrl, Yubico AB"
|
||||
)
|
||||
|
||||
def check_passkeys_button_state():
|
||||
"""Enables 'Passkeys' button if resident keys exist."""
|
||||
state = tk.DISABLED
|
||||
for child in tree.get_children():
|
||||
values = tree.item(child, "values")
|
||||
if values and len(values) > 1 and values[0] == "existing rk(s)":
|
||||
try:
|
||||
if int(values[1]) > 0:
|
||||
state = tk.NORMAL
|
||||
break
|
||||
except (ValueError, IndexError):
|
||||
pass
|
||||
passkeys_button.config(state=state)
|
||||
|
||||
def check_changepin_button_state():
|
||||
"""Enables 'Change PIN' button if PIN is set."""
|
||||
state = tk.DISABLED
|
||||
for child in tree.get_children():
|
||||
values = tree.item(child, "values")
|
||||
if values and len(values) > 1 and values[0] == "pin retries":
|
||||
state = tk.NORMAL
|
||||
break
|
||||
change_pin_button.config(state=state)
|
||||
|
||||
# Placeholder functions for buttons
|
||||
def on_passkeys_button_click():
|
||||
messagebox.showinfo("Not Implemented", "Passkey management functionality is not yet implemented.")
|
||||
|
||||
def change_pin():
|
||||
messagebox.showinfo("Not Implemented", "Change PIN functionality is not yet implemented.")
|
||||
|
||||
# --- GUI Layout ---
|
||||
|
||||
root = tk.Tk()
|
||||
root.title("FIDO2 Manager")
|
||||
root.geometry("800x600")
|
||||
root.minsize(700, 400)
|
||||
|
||||
# Main container
|
||||
main_frame = ttk.Frame(root, padding="10")
|
||||
main_frame.pack(expand=True, fill="both")
|
||||
|
||||
# --- Top Frame: Device Selection & Refresh ---
|
||||
top_frame = ttk.Frame(main_frame)
|
||||
top_frame.pack(side="top", fill="x", pady=(0, 10))
|
||||
|
||||
ttk.Label(top_frame, text="Select Device:").pack(side="left")
|
||||
|
||||
device_var = tk.StringVar()
|
||||
device_combobox = ttk.Combobox(top_frame, textvariable=device_var, state="readonly")
|
||||
device_combobox.pack(side="left", expand=True, fill="x", padx=5)
|
||||
device_combobox.bind("<<ComboboxSelected>>", on_device_selected)
|
||||
|
||||
refresh_button = ttk.Button(top_frame, text="Refresh", command=refresh_combobox)
|
||||
refresh_button.pack(side="left")
|
||||
|
||||
# --- Center Frame: Information Treeview ---
|
||||
tree_frame = ttk.Frame(main_frame)
|
||||
tree_frame.pack(expand=True, fill="both")
|
||||
|
||||
tree = ttk.Treeview(tree_frame, columns=("Key", "Value"), show="headings")
|
||||
tree.heading("Key", text="Key")
|
||||
tree.heading("Value", text="Value")
|
||||
tree.column("Key", width=200, stretch=tk.NO, anchor="w")
|
||||
tree.column("Value", width=550, anchor="w")
|
||||
|
||||
# Scrollbars
|
||||
vsb = ttk.Scrollbar(tree_frame, orient="vertical", command=tree.yview)
|
||||
hsb = ttk.Scrollbar(tree_frame, orient="horizontal", command=tree.xview)
|
||||
tree.configure(yscrollcommand=vsb.set, xscrollcommand=hsb.set)
|
||||
|
||||
vsb.pack(side="right", fill="y")
|
||||
hsb.pack(side="bottom", fill="x")
|
||||
tree.pack(side="left", expand=True, fill="both")
|
||||
|
||||
# --- Bottom Frame: Action Buttons ---
|
||||
bottom_frame = ttk.Frame(main_frame)
|
||||
bottom_frame.pack(side="bottom", fill="x", pady=(10, 0))
|
||||
|
||||
passkeys_button = ttk.Button(bottom_frame, text="Passkeys", state=tk.DISABLED, command=on_passkeys_button_click)
|
||||
passkeys_button.pack(side="left")
|
||||
|
||||
change_pin_button = ttk.Button(bottom_frame, text="Change PIN", state=tk.DISABLED, command=change_pin)
|
||||
change_pin_button.pack(side="left", padx=5)
|
||||
|
||||
about_button = ttk.Button(bottom_frame, text="About", command=show_about_message)
|
||||
about_button.pack(side="right")
|
||||
|
||||
# --- Initial Load ---
|
||||
refresh_combobox()
|
||||
root.mainloop()
|
||||
Reference in New Issue
Block a user