"""AUTARCH RFID/NFC Tools Proxmark3 integration, badge cloning, NFC read/write, MIFARE operations, and card analysis for physical access security testing. """ DESCRIPTION = "RFID/NFC badge cloning & analysis" AUTHOR = "darkHal" VERSION = "1.0" CATEGORY = "analyze" import os import re import json import time import shutil import subprocess from pathlib import Path from datetime import datetime, timezone from typing import Dict, List, Optional, Any try: from core.paths import find_tool, get_data_dir except ImportError: def find_tool(name): return shutil.which(name) def get_data_dir(): return str(Path(__file__).parent.parent / 'data') # ── Card Types ─────────────────────────────────────────────────────────────── CARD_TYPES = { 'em410x': {'name': 'EM410x', 'frequency': '125 kHz', 'category': 'LF'}, 'hid_prox': {'name': 'HID ProxCard', 'frequency': '125 kHz', 'category': 'LF'}, 't5577': {'name': 'T5577', 'frequency': '125 kHz', 'category': 'LF', 'writable': True}, 'mifare_classic_1k': {'name': 'MIFARE Classic 1K', 'frequency': '13.56 MHz', 'category': 'HF'}, 'mifare_classic_4k': {'name': 'MIFARE Classic 4K', 'frequency': '13.56 MHz', 'category': 'HF'}, 'mifare_ultralight': {'name': 'MIFARE Ultralight', 'frequency': '13.56 MHz', 'category': 'HF'}, 'mifare_desfire': {'name': 'MIFARE DESFire', 'frequency': '13.56 MHz', 'category': 'HF'}, 'ntag213': {'name': 'NTAG213', 'frequency': '13.56 MHz', 'category': 'HF', 'nfc': True}, 'ntag215': {'name': 'NTAG215', 'frequency': '13.56 MHz', 'category': 'HF', 'nfc': True}, 'ntag216': {'name': 'NTAG216', 'frequency': '13.56 MHz', 'category': 'HF', 'nfc': True}, 'iclass': {'name': 'iCLASS', 'frequency': '13.56 MHz', 'category': 'HF'}, 'iso14443a': {'name': 'ISO 14443A', 'frequency': '13.56 MHz', 'category': 'HF'}, 'iso15693': {'name': 'ISO 15693', 'frequency': '13.56 MHz', 'category': 'HF'}, 'legic': {'name': 'LEGIC', 'frequency': '13.56 MHz', 'category': 'HF'}, } MIFARE_DEFAULT_KEYS = [ 'FFFFFFFFFFFF', 'A0A1A2A3A4A5', 'D3F7D3F7D3F7', '000000000000', 'B0B1B2B3B4B5', '4D3A99C351DD', '1A982C7E459A', 'AABBCCDDEEFF', '714C5C886E97', '587EE5F9350F', 'A0478CC39091', '533CB6C723F6', ] # ── RFID Manager ───────────────────────────────────────────────────────────── class RFIDManager: """RFID/NFC tool management via Proxmark3 and nfc-tools.""" def __init__(self): self.data_dir = os.path.join(get_data_dir(), 'rfid') os.makedirs(self.data_dir, exist_ok=True) self.dumps_dir = os.path.join(self.data_dir, 'dumps') os.makedirs(self.dumps_dir, exist_ok=True) # Tool discovery self.pm3_client = find_tool('pm3') or find_tool('proxmark3') or shutil.which('pm3') or shutil.which('proxmark3') self.nfc_list = shutil.which('nfc-list') self.nfc_poll = shutil.which('nfc-poll') self.nfc_mfclassic = shutil.which('nfc-mfclassic') self.cards: List[Dict] = [] self.last_read: Optional[Dict] = None def get_tools_status(self) -> Dict: """Check available tools.""" return { 'proxmark3': self.pm3_client is not None, 'nfc-list': self.nfc_list is not None, 'nfc-mfclassic': self.nfc_mfclassic is not None, 'card_types': len(CARD_TYPES), 'saved_cards': len(self.cards) } # ── Proxmark3 Commands ─────────────────────────────────────────────── def _pm3_cmd(self, command: str, timeout: int = 15) -> Dict: """Execute Proxmark3 command.""" if not self.pm3_client: return {'ok': False, 'error': 'Proxmark3 client not found'} try: result = subprocess.run( [self.pm3_client, '-c', command], capture_output=True, text=True, timeout=timeout ) return { 'ok': result.returncode == 0, 'stdout': result.stdout, 'stderr': result.stderr } except subprocess.TimeoutExpired: return {'ok': False, 'error': f'Command timed out: {command}'} except Exception as e: return {'ok': False, 'error': str(e)} # ── Low Frequency (125 kHz) ────────────────────────────────────────── def lf_search(self) -> Dict: """Search for LF (125 kHz) cards.""" result = self._pm3_cmd('lf search') if not result['ok']: return result output = result['stdout'] card = {'frequency': '125 kHz', 'category': 'LF'} # Parse EM410x em_match = re.search(r'EM\s*410x.*?ID[:\s]*([A-Fa-f0-9]+)', output, re.I) if em_match: card['type'] = 'em410x' card['id'] = em_match.group(1) card['name'] = 'EM410x' # Parse HID hid_match = re.search(r'HID.*?Card.*?([A-Fa-f0-9]+)', output, re.I) if hid_match: card['type'] = 'hid_prox' card['id'] = hid_match.group(1) card['name'] = 'HID ProxCard' if 'id' in card: card['raw_output'] = output self.last_read = card return {'ok': True, 'card': card} return {'ok': False, 'error': 'No LF card found', 'raw': output} def lf_read_em410x(self) -> Dict: """Read EM410x card.""" result = self._pm3_cmd('lf em 410x reader') if not result['ok']: return result match = re.search(r'EM\s*410x\s+ID[:\s]*([A-Fa-f0-9]+)', result['stdout'], re.I) if match: card = { 'type': 'em410x', 'id': match.group(1), 'name': 'EM410x', 'frequency': '125 kHz' } self.last_read = card return {'ok': True, 'card': card} return {'ok': False, 'error': 'Could not read EM410x', 'raw': result['stdout']} def lf_clone_em410x(self, card_id: str) -> Dict: """Clone EM410x ID to T5577 card.""" result = self._pm3_cmd(f'lf em 410x clone --id {card_id}') return { 'ok': 'written' in result.get('stdout', '').lower() or result['ok'], 'message': f'Cloned EM410x ID {card_id}' if result['ok'] else result.get('error', ''), 'raw': result.get('stdout', '') } def lf_sim_em410x(self, card_id: str) -> Dict: """Simulate EM410x card.""" result = self._pm3_cmd(f'lf em 410x sim --id {card_id}', timeout=30) return { 'ok': result['ok'], 'message': f'Simulating EM410x ID {card_id}', 'raw': result.get('stdout', '') } # ── High Frequency (13.56 MHz) ─────────────────────────────────────── def hf_search(self) -> Dict: """Search for HF (13.56 MHz) cards.""" result = self._pm3_cmd('hf search') if not result['ok']: return result output = result['stdout'] card = {'frequency': '13.56 MHz', 'category': 'HF'} # Parse UID uid_match = re.search(r'UID[:\s]*([A-Fa-f0-9\s]+)', output, re.I) if uid_match: card['uid'] = uid_match.group(1).replace(' ', '').strip() # Parse ATQA/SAK atqa_match = re.search(r'ATQA[:\s]*([A-Fa-f0-9\s]+)', output, re.I) if atqa_match: card['atqa'] = atqa_match.group(1).strip() sak_match = re.search(r'SAK[:\s]*([A-Fa-f0-9]+)', output, re.I) if sak_match: card['sak'] = sak_match.group(1).strip() # Detect type if 'mifare classic 1k' in output.lower(): card['type'] = 'mifare_classic_1k' card['name'] = 'MIFARE Classic 1K' elif 'mifare classic 4k' in output.lower(): card['type'] = 'mifare_classic_4k' card['name'] = 'MIFARE Classic 4K' elif 'ultralight' in output.lower() or 'ntag' in output.lower(): card['type'] = 'mifare_ultralight' card['name'] = 'MIFARE Ultralight/NTAG' elif 'desfire' in output.lower(): card['type'] = 'mifare_desfire' card['name'] = 'MIFARE DESFire' elif 'iso14443' in output.lower(): card['type'] = 'iso14443a' card['name'] = 'ISO 14443A' if 'uid' in card: card['raw_output'] = output self.last_read = card return {'ok': True, 'card': card} return {'ok': False, 'error': 'No HF card found', 'raw': output} def hf_dump_mifare(self, keys_file: str = None) -> Dict: """Dump MIFARE Classic card data.""" cmd = 'hf mf autopwn' if keys_file: cmd += f' -f {keys_file}' result = self._pm3_cmd(cmd, timeout=120) if not result['ok']: return result output = result['stdout'] # Look for dump file dump_match = re.search(r'saved.*?(\S+\.bin)', output, re.I) if dump_match: dump_file = dump_match.group(1) # Copy to our dumps directory dest = os.path.join(self.dumps_dir, Path(dump_file).name) if os.path.exists(dump_file): shutil.copy2(dump_file, dest) return { 'ok': True, 'dump_file': dest, 'message': 'MIFARE dump complete', 'raw': output } # Check for found keys keys = re.findall(r'key\s*[AB][:\s]*([A-Fa-f0-9]{12})', output, re.I) if keys: return { 'ok': True, 'keys_found': list(set(keys)), 'message': f'Found {len(set(keys))} keys', 'raw': output } return {'ok': False, 'error': 'Dump failed', 'raw': output} def hf_clone_mifare(self, dump_file: str) -> Dict: """Write MIFARE dump to blank card.""" result = self._pm3_cmd(f'hf mf restore -f {dump_file}', timeout=60) return { 'ok': 'restored' in result.get('stdout', '').lower() or result['ok'], 'message': 'Card cloned' if result['ok'] else 'Clone failed', 'raw': result.get('stdout', '') } # ── NFC Operations (via libnfc) ────────────────────────────────────── def nfc_scan(self) -> Dict: """Scan for NFC tags using libnfc.""" if not self.nfc_list: return {'ok': False, 'error': 'nfc-list not found (install libnfc)'} try: result = subprocess.run( [self.nfc_list], capture_output=True, text=True, timeout=10 ) tags = [] for line in result.stdout.splitlines(): uid_match = re.search(r'UID.*?:\s*([A-Fa-f0-9\s:]+)', line, re.I) if uid_match: tags.append({ 'uid': uid_match.group(1).replace(' ', '').replace(':', ''), 'raw': line.strip() }) return {'ok': True, 'tags': tags, 'count': len(tags)} except Exception as e: return {'ok': False, 'error': str(e)} # ── Card Database ──────────────────────────────────────────────────── def save_card(self, card: Dict, name: str = None) -> Dict: """Save card data to database.""" card['saved_at'] = datetime.now(timezone.utc).isoformat() card['display_name'] = name or card.get('name', 'Unknown Card') # Remove raw output to save space card.pop('raw_output', None) self.cards.append(card) self._save_cards() return {'ok': True, 'count': len(self.cards)} def get_saved_cards(self) -> List[Dict]: """List saved cards.""" return self.cards def delete_card(self, index: int) -> Dict: """Delete saved card by index.""" if 0 <= index < len(self.cards): self.cards.pop(index) self._save_cards() return {'ok': True} return {'ok': False, 'error': 'Invalid index'} def _save_cards(self): cards_file = os.path.join(self.data_dir, 'cards.json') with open(cards_file, 'w') as f: json.dump(self.cards, f, indent=2) def _load_cards(self): cards_file = os.path.join(self.data_dir, 'cards.json') if os.path.exists(cards_file): try: with open(cards_file) as f: self.cards = json.load(f) except Exception: pass def list_dumps(self) -> List[Dict]: """List saved card dumps.""" dumps = [] for f in Path(self.dumps_dir).iterdir(): if f.is_file(): dumps.append({ 'name': f.name, 'path': str(f), 'size': f.stat().st_size, 'modified': datetime.fromtimestamp(f.stat().st_mtime, timezone.utc).isoformat() }) return dumps def get_default_keys(self) -> List[str]: """Return common MIFARE default keys.""" return MIFARE_DEFAULT_KEYS def get_card_types(self) -> Dict: """Return supported card type info.""" return CARD_TYPES # ── Singleton ──────────────────────────────────────────────────────────────── _instance = None def get_rfid_manager() -> RFIDManager: global _instance if _instance is None: _instance = RFIDManager() _instance._load_cards() return _instance # ── CLI Interface ──────────────────────────────────────────────────────────── def run(): """CLI entry point for RFID/NFC module.""" mgr = get_rfid_manager() while True: tools = mgr.get_tools_status() print(f"\n{'='*60}") print(f" RFID / NFC Tools") print(f"{'='*60}") print(f" Proxmark3: {'OK' if tools['proxmark3'] else 'NOT FOUND'}") print(f" libnfc: {'OK' if tools['nfc-list'] else 'NOT FOUND'}") print(f" Saved cards: {tools['saved_cards']}") print() print(" 1 — LF Search (125 kHz)") print(" 2 — HF Search (13.56 MHz)") print(" 3 — Read EM410x") print(" 4 — Clone EM410x to T5577") print(" 5 — Dump MIFARE Classic") print(" 6 — Clone MIFARE from Dump") print(" 7 — NFC Scan (libnfc)") print(" 8 — Saved Cards") print(" 9 — Card Dumps") print(" 0 — Back") print() choice = input(" > ").strip() if choice == '0': break elif choice == '1': result = mgr.lf_search() if result['ok']: c = result['card'] print(f" Found: {c.get('name', '?')} ID: {c.get('id', '?')}") else: print(f" {result.get('error', 'No card found')}") elif choice == '2': result = mgr.hf_search() if result['ok']: c = result['card'] print(f" Found: {c.get('name', '?')} UID: {c.get('uid', '?')}") else: print(f" {result.get('error', 'No card found')}") elif choice == '3': result = mgr.lf_read_em410x() if result['ok']: print(f" EM410x ID: {result['card']['id']}") save = input(" Save card? (y/n): ").strip() if save.lower() == 'y': mgr.save_card(result['card']) else: print(f" {result['error']}") elif choice == '4': card_id = input(" EM410x ID to clone: ").strip() if card_id: result = mgr.lf_clone_em410x(card_id) print(f" {result.get('message', result.get('error'))}") elif choice == '5': result = mgr.hf_dump_mifare() if result['ok']: print(f" {result['message']}") if 'keys_found' in result: for k in result['keys_found']: print(f" Key: {k}") else: print(f" {result['error']}") elif choice == '6': dump = input(" Dump file path: ").strip() if dump: result = mgr.hf_clone_mifare(dump) print(f" {result['message']}") elif choice == '7': result = mgr.nfc_scan() if result['ok']: print(f" Found {result['count']} tags:") for t in result['tags']: print(f" UID: {t['uid']}") else: print(f" {result['error']}") elif choice == '8': cards = mgr.get_saved_cards() for i, c in enumerate(cards): print(f" [{i}] {c.get('display_name', '?')} " f"{c.get('type', '?')} ID={c.get('id', c.get('uid', '?'))}") elif choice == '9': for d in mgr.list_dumps(): print(f" {d['name']} ({d['size']} bytes)")