🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
353 lines
12 KiB
Python
353 lines
12 KiB
Python
"""Proxmark3 worker for executing commands."""
|
|
import asyncio
|
|
from dataclasses import dataclass
|
|
from typing import Optional
|
|
from pathlib import Path
|
|
import sys
|
|
import os
|
|
|
|
from .. import config
|
|
|
|
|
|
@dataclass
|
|
class PM3CommandResult:
|
|
"""Result from a PM3 command execution."""
|
|
success: bool
|
|
output: str
|
|
error: Optional[str] = None
|
|
|
|
|
|
class PM3Worker:
|
|
"""Worker for executing Proxmark3 commands using the built-in pm3 module."""
|
|
|
|
def __init__(self, device_path: str = None):
|
|
"""Initialize the PM3 worker.
|
|
|
|
Args:
|
|
device_path: Path to PM3 device (default: from config)
|
|
"""
|
|
self.device_path = device_path or config.PM3_DEVICE
|
|
self._device = None
|
|
self._connected = False
|
|
self._lock = asyncio.Lock()
|
|
self._pm3_module = None
|
|
|
|
async def _import_pm3(self):
|
|
"""Import the pm3 module (lazy loading)."""
|
|
if self._pm3_module is None:
|
|
try:
|
|
# Setup Python path for pm3 module
|
|
# The pm3 module is in proxmark3/client/pyscripts
|
|
# The _pm3.so is in proxmark3/client/experimental_lib/example_py
|
|
|
|
# Try multiple possible locations for pyscripts
|
|
# The pm3 module is in proxmark3/client/pyscripts
|
|
possible_pyscripts = [
|
|
os.path.expanduser("~/.pm3/proxmark3/client/pyscripts"), # Pi install location
|
|
"/home/dt/.pm3/proxmark3/client/pyscripts", # Pi install (explicit path)
|
|
os.path.expanduser("~/.pm3/client/pyscripts"), # Alternative structure
|
|
"/usr/local/share/proxmark3/pyscripts", # System install
|
|
"/usr/share/proxmark3/pyscripts", # System install alt
|
|
os.path.join(os.path.dirname(__file__), "../../../.pm3-test/proxmark3/client/pyscripts"), # Dev build
|
|
]
|
|
|
|
pm3_pyscripts = None
|
|
for path in possible_pyscripts:
|
|
pm3_py = os.path.join(path, "pm3.py")
|
|
if os.path.exists(pm3_py):
|
|
pm3_pyscripts = path
|
|
break
|
|
|
|
if not pm3_pyscripts:
|
|
raise ImportError(f"Could not find pm3.py in any of: {possible_pyscripts}")
|
|
|
|
# experimental_lib is at the same level as pyscripts (client/experimental_lib)
|
|
pm3_lib = os.path.join(os.path.dirname(pm3_pyscripts), "experimental_lib/example_py")
|
|
|
|
# Add to Python path if not already there
|
|
if pm3_pyscripts not in sys.path:
|
|
sys.path.insert(0, pm3_pyscripts)
|
|
if pm3_lib not in sys.path:
|
|
sys.path.insert(0, pm3_lib)
|
|
|
|
# Import pm3 module
|
|
import pm3
|
|
self._pm3_module = pm3
|
|
except ImportError as e:
|
|
raise ImportError(
|
|
"pm3 module not found. Ensure Proxmark3 client is installed with Python support. "
|
|
f"Error: {e}"
|
|
)
|
|
return self._pm3_module
|
|
|
|
async def _connect_internal(self):
|
|
"""Internal connect without locking (caller must hold lock)."""
|
|
if self._connected:
|
|
return
|
|
|
|
try:
|
|
pm3 = await self._import_pm3()
|
|
|
|
# Run blocking pm3.pm3() constructor in executor
|
|
loop = asyncio.get_event_loop()
|
|
self._device = await loop.run_in_executor(
|
|
None,
|
|
pm3.pm3,
|
|
self.device_path
|
|
)
|
|
self._connected = True
|
|
except Exception as e:
|
|
raise ConnectionError(f"Failed to connect to PM3 at {self.device_path}: {e}")
|
|
|
|
async def connect(self):
|
|
"""Connect to the Proxmark3 device."""
|
|
async with self._lock:
|
|
await self._connect_internal()
|
|
|
|
async def disconnect(self):
|
|
"""Disconnect from the Proxmark3 device."""
|
|
async with self._lock:
|
|
if self._device:
|
|
try:
|
|
# Close the device connection
|
|
# Note: pm3 module may not have explicit close, connection closes when object is deleted
|
|
self._device = None
|
|
self._connected = False
|
|
except Exception as e:
|
|
print(f"Error disconnecting from PM3: {e}")
|
|
|
|
async def is_connected(self) -> bool:
|
|
"""Check if connected to PM3 device."""
|
|
if not self._connected:
|
|
# Try to check if device exists
|
|
return Path(self.device_path).exists()
|
|
return self._connected
|
|
|
|
async def execute_command(self, command: str) -> PM3CommandResult:
|
|
"""Execute a Proxmark3 command.
|
|
|
|
Args:
|
|
command: PM3 command to execute (e.g., "hw version")
|
|
|
|
Returns:
|
|
PM3CommandResult with success status, output, and optional error
|
|
"""
|
|
async with self._lock:
|
|
try:
|
|
# Ensure we're connected (use internal method since we hold the lock)
|
|
if not self._connected:
|
|
await self._connect_internal()
|
|
|
|
if not self._device:
|
|
return PM3CommandResult(
|
|
success=False,
|
|
output="",
|
|
error="Not connected to PM3 device"
|
|
)
|
|
|
|
# Execute command in executor (blocking call)
|
|
loop = asyncio.get_event_loop()
|
|
|
|
try:
|
|
# Helper function to run command and get output in same executor call
|
|
def run_command():
|
|
try:
|
|
self._device.console(command)
|
|
output = self._device.grabbed_output
|
|
return output
|
|
except Exception as e:
|
|
raise Exception(f"Error in run_command: {e}, device type: {type(self._device)}")
|
|
|
|
# Execute command and get output
|
|
output = await loop.run_in_executor(None, run_command)
|
|
|
|
return PM3CommandResult(
|
|
success=True,
|
|
output=str(output) if output else "",
|
|
error=None
|
|
)
|
|
except Exception as cmd_error:
|
|
return PM3CommandResult(
|
|
success=False,
|
|
output="",
|
|
error=f"Command execution failed: {cmd_error}"
|
|
)
|
|
|
|
except Exception as e:
|
|
return PM3CommandResult(
|
|
success=False,
|
|
output="",
|
|
error=str(e)
|
|
)
|
|
|
|
|
|
# Mock PM3 Worker for testing without actual hardware
|
|
class MockPM3Worker(PM3Worker):
|
|
"""Mock PM3 worker for testing without hardware."""
|
|
|
|
def __init__(self, device_path: str = None):
|
|
super().__init__(device_path)
|
|
self._mock_responses = {
|
|
"hw version": "Proxmark3 RFID instrument\n client: RRG/Iceman/master/v4.14831",
|
|
"hw status": "Device: PM3 GENERIC\nBootrom: master/v4.14831\nOS: master/v4.14831",
|
|
"hw tune": "Measuring antenna characteristics, please wait...\n# LF antenna: 50.00 V @ 125.00 kHz",
|
|
"hw led": "[+] LED control command executed",
|
|
}
|
|
|
|
async def connect(self):
|
|
"""Mock connect."""
|
|
self._connected = True
|
|
|
|
async def disconnect(self):
|
|
"""Mock disconnect."""
|
|
self._connected = False
|
|
|
|
async def is_connected(self) -> bool:
|
|
"""Mock connection check."""
|
|
return True
|
|
|
|
async def execute_command(self, command: str) -> PM3CommandResult:
|
|
"""Mock command execution."""
|
|
await asyncio.sleep(0.1) # Simulate command delay
|
|
|
|
# Return mock response if available
|
|
for cmd_prefix, response in self._mock_responses.items():
|
|
if command.startswith(cmd_prefix):
|
|
return PM3CommandResult(
|
|
success=True,
|
|
output=response,
|
|
error=None
|
|
)
|
|
|
|
# Default response for unknown commands
|
|
return PM3CommandResult(
|
|
success=True,
|
|
output=f"Mock response for: {command}",
|
|
error=None
|
|
)
|
|
|
|
|
|
class SubprocessPM3Worker(PM3Worker):
|
|
"""PM3 worker using subprocess to call pm3 CLI directly.
|
|
|
|
This is a fallback for when the Python SWIG bindings aren't working.
|
|
It executes pm3 CLI commands via subprocess which is more robust.
|
|
"""
|
|
|
|
def __init__(self, device_path: str = None):
|
|
super().__init__(device_path)
|
|
self._pm3_path = None
|
|
self._find_pm3_executable()
|
|
|
|
def _find_pm3_executable(self):
|
|
"""Find the pm3 executable."""
|
|
possible_paths = [
|
|
"/usr/local/bin/pm3",
|
|
os.path.expanduser("~/.pm3/proxmark3/client/proxmark3"),
|
|
"/home/dt/.pm3/proxmark3/client/proxmark3",
|
|
"/usr/bin/pm3",
|
|
]
|
|
|
|
for path in possible_paths:
|
|
if os.path.exists(path) and os.access(path, os.X_OK):
|
|
self._pm3_path = path
|
|
break
|
|
|
|
if not self._pm3_path:
|
|
# Try to find in PATH
|
|
import shutil
|
|
pm3_in_path = shutil.which("pm3") or shutil.which("proxmark3")
|
|
if pm3_in_path:
|
|
self._pm3_path = pm3_in_path
|
|
|
|
async def connect(self):
|
|
"""Check that pm3 executable exists and device is available."""
|
|
async with self._lock:
|
|
if self._connected:
|
|
return
|
|
|
|
if not self._pm3_path:
|
|
raise ConnectionError("pm3 executable not found")
|
|
|
|
if not Path(self.device_path).exists():
|
|
raise ConnectionError(f"PM3 device not found at {self.device_path}")
|
|
|
|
self._connected = True
|
|
|
|
async def disconnect(self):
|
|
"""Mark as disconnected."""
|
|
async with self._lock:
|
|
self._connected = False
|
|
|
|
async def is_connected(self) -> bool:
|
|
"""Check if device exists."""
|
|
return Path(self.device_path).exists() if self.device_path else False
|
|
|
|
async def execute_command(self, command: str) -> PM3CommandResult:
|
|
"""Execute a PM3 command via subprocess.
|
|
|
|
Args:
|
|
command: PM3 command to execute (e.g., "hw version")
|
|
|
|
Returns:
|
|
PM3CommandResult with success status, output, and optional error
|
|
"""
|
|
async with self._lock:
|
|
try:
|
|
if not self._pm3_path:
|
|
return PM3CommandResult(
|
|
success=False,
|
|
output="",
|
|
error="pm3 executable not found"
|
|
)
|
|
|
|
# Build command: pm3 -p /dev/ttyACM0 -c "command"
|
|
cmd = [
|
|
self._pm3_path,
|
|
"-p", self.device_path,
|
|
"-c", command
|
|
]
|
|
|
|
# Run subprocess
|
|
process = await asyncio.create_subprocess_exec(
|
|
*cmd,
|
|
stdout=asyncio.subprocess.PIPE,
|
|
stderr=asyncio.subprocess.PIPE
|
|
)
|
|
|
|
try:
|
|
stdout, stderr = await asyncio.wait_for(
|
|
process.communicate(),
|
|
timeout=30.0 # 30 second timeout
|
|
)
|
|
except asyncio.TimeoutError:
|
|
process.kill()
|
|
return PM3CommandResult(
|
|
success=False,
|
|
output="",
|
|
error="Command timed out after 30 seconds"
|
|
)
|
|
|
|
output = stdout.decode("utf-8", errors="replace")
|
|
error_output = stderr.decode("utf-8", errors="replace")
|
|
|
|
if process.returncode == 0:
|
|
return PM3CommandResult(
|
|
success=True,
|
|
output=output,
|
|
error=None
|
|
)
|
|
else:
|
|
return PM3CommandResult(
|
|
success=False,
|
|
output=output,
|
|
error=error_output or f"Command failed with exit code {process.returncode}"
|
|
)
|
|
|
|
except Exception as e:
|
|
return PM3CommandResult(
|
|
success=False,
|
|
output="",
|
|
error=str(e)
|
|
)
|