From d697eef3cadeb11dd9345c92950e19f8c0b31d3b Mon Sep 17 00:00:00 2001 From: "Z. Cliffe Schreuders" Date: Sat, 15 Nov 2025 23:48:15 +0000 Subject: [PATCH] docs(rfid): Add multi-protocol RFID system planning & review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Created comprehensive planning for adding multiple RFID protocols with different security levels and attack capabilities. Planning Documents: - 00_IMPLEMENTATION_SUMMARY.md: Complete implementation guide (14h estimate) - 01_TECHNICAL_DESIGN.md: Protocol specs, data models, UI design - 02_IMPLEMENTATION_PLAN.md: Detailed file-by-file implementation plan - CRITICAL_REVIEW.md: Pre-implementation review with 12 improvements Protocol System (Simplified from Original): ✅ EM4100 (low security) - Instant clone, already implemented ✅ MIFARE Classic (medium) - Requires key attacks (Darkside/Nested/Dictionary) ✅ MIFARE DESFire (high) - UID only, forces physical theft ❌ HID Prox - Removed (too similar to EM4100, saves 2h) Key Features: - Protocol detection with color-coded security levels - MIFARE key cracking minigames (30s Darkside, 10s Nested, instant Dictionary) - Ink integration for conditional interactions based on card protocol - Dual format support (no migration needed) - UID-only emulation for DESFire with acceptsUIDOnly door property Review Improvements Applied: - Merged "attack mode" into clone mode (simpler state machine) - Removed firmware upgrade system (can add later) - Dual format card data support instead of migration (safer) - Added error handling for unsupported protocols - Added cleanup for background attacks - Documented required Ink variables Time Estimate: 14 hours (down from 19h) - Phase 1: Protocol Foundation (3h) - Phase 2: Protocol Detection & UI (3h) - Phase 3: MIFARE Attack System (5h) - Phase 4: Ink Integration (2h) - Phase 5: Door Integration & Testing (1h) Next Steps: Begin implementation starting with Phase 1 --- .../00_IMPLEMENTATION_SUMMARY.md | 1384 +++++++++++++++++ .../01_TECHNICAL_DESIGN.md | 708 +++++++++ .../02_IMPLEMENTATION_PLAN.md | 1339 ++++++++++++++++ .../CRITICAL_REVIEW.md | 526 +++++++ 4 files changed, 3957 insertions(+) create mode 100644 planning_notes/rfid_keycard/protocols_and_interactions/00_IMPLEMENTATION_SUMMARY.md create mode 100644 planning_notes/rfid_keycard/protocols_and_interactions/01_TECHNICAL_DESIGN.md create mode 100644 planning_notes/rfid_keycard/protocols_and_interactions/02_IMPLEMENTATION_PLAN.md create mode 100644 planning_notes/rfid_keycard/protocols_and_interactions_review/CRITICAL_REVIEW.md diff --git a/planning_notes/rfid_keycard/protocols_and_interactions/00_IMPLEMENTATION_SUMMARY.md b/planning_notes/rfid_keycard/protocols_and_interactions/00_IMPLEMENTATION_SUMMARY.md new file mode 100644 index 0000000..b7f15ff --- /dev/null +++ b/planning_notes/rfid_keycard/protocols_and_interactions/00_IMPLEMENTATION_SUMMARY.md @@ -0,0 +1,1384 @@ +# RFID Protocols - Implementation Summary (Revised) + +**Status**: Ready for Implementation +**Estimated Time**: 14 hours (down from 19 hours) +**Based On**: Critical review improvements applied + +## Changes from Original Plan + +✅ **Removed HID Prox** - Minimal gameplay value, saves 2h +✅ **Merged attack mode into clone mode** - Simpler UX, saves 1h +✅ **Removed firmware system** - Can add later if needed, saves 2h +✅ **Dual format support** - No migration needed, safer approach +✅ **Added error handling** - Firmware checks, UID acceptance rules +✅ **Added Ink variable documentation** - Clear requirements +✅ **Improved code organization** - Constants for timing, better structure + +## Three-Protocol System + +### EM4100 (Low Security) +- **Status**: Already implemented +- **Clone**: Instant, always works +- **Emulate**: Perfect emulation +- **Gameplay**: Entry-level cards, no challenge + +### MIFARE Classic (Medium Security) +- **Status**: New implementation needed +- **Clone**: Requires authentication keys +- **Attacks**: + - Dictionary (instant) - Try common keys + - Darkside (30 sec) - Crack keys from scratch + - Nested (10 sec) - Crack remaining keys +- **Gameplay**: Puzzle element, adds time pressure + +### MIFARE DESFire (High Security) +- **Status**: New implementation needed +- **Clone**: Impossible - UID only +- **Emulate**: UID emulation works on low-security readers only +- **Gameplay**: Forces physical theft or social engineering + +## Implementation Phases (Revised) + +### Phase 1: Protocol Foundation (3h) + +**File**: `js/minigames/rfid/rfid-protocols.js` (NEW) + +```javascript +export const RFID_PROTOCOLS = { + 'EM4100': { + name: 'EM-Micro EM4100', + frequency: '125kHz', + security: 'low', + capabilities: { + read: true, + clone: true, + emulate: true + }, + hexLength: 10, + color: '#FF6B6B', + icon: '⚠️' + }, + + 'MIFARE_Classic': { + name: 'MIFARE Classic 1K', + frequency: '13.56MHz', + security: 'medium', + capabilities: { + read: 'with-keys', + clone: 'with-keys', + emulate: true + }, + sectors: 16, + hexLength: 8, + color: '#4ECDC4', + icon: '🔐' + }, + + 'MIFARE_DESFire': { + name: 'MIFARE DESFire EV2', + frequency: '13.56MHz', + security: 'high', + capabilities: { + read: false, + clone: false, + emulate: 'uid-only' + }, + hexLength: 14, + color: '#95E1D3', + icon: '🔒' + } +}; + +// Common MIFARE keys for dictionary attack +export const MIFARE_COMMON_KEYS = [ + 'FFFFFFFFFFFF', // Factory default + '000000000000', + 'A0A1A2A3A4A5', + 'D3F7D3F7D3F7', + '123456789ABC', + 'AABBCCDDEEFF' + // ... more +]; + +export function getProtocolInfo(protocolName) { + return RFID_PROTOCOLS[protocolName] || RFID_PROTOCOLS['EM4100']; +} + +export function protocolSupports(protocolName, operation) { + const protocol = getProtocolInfo(protocolName); + const capability = protocol.capabilities[operation]; + if (typeof capability === 'boolean') return capability; + return capability; // 'with-keys', 'uid-only', etc. +} +``` + +**File**: `js/minigames/rfid/rfid-data.js` (MODIFY) + +Add dual format support (no migration): + +```javascript +import { getProtocolInfo } from './rfid-protocols.js'; + +export class RFIDDataManager { + /** + * Get hex ID from card (supports old and new formats) + */ + getCardHex(cardData) { + // New format + if (cardData.rfid_data?.hex) { + return cardData.rfid_data.hex; + } + + // Old format (backward compat) + if (cardData.rfid_hex) { + return cardData.rfid_hex; + } + + return null; + } + + /** + * Get UID from card + */ + getCardUID(cardData) { + return cardData.rfid_data?.uid || null; + } + + /** + * Detect protocol from card data + */ + detectProtocol(cardData) { + // Explicit protocol + if (cardData.rfid_protocol) { + return cardData.rfid_protocol; + } + + // Auto-detect from UID length + const uid = this.getCardUID(cardData); + if (uid) { + if (uid.length === 14) return 'MIFARE_DESFire'; + if (uid.length === 8) return 'MIFARE_Classic'; + } + + // Auto-detect from hex length + const hex = this.getCardHex(cardData); + if (hex && hex.length === 10) return 'EM4100'; + + return 'EM4100'; // Default + } + + /** + * Get display data for card based on protocol + */ + getCardDisplayData(cardData) { + const protocol = this.detectProtocol(cardData); + const protocolInfo = getProtocolInfo(protocol); + + const displayData = { + protocol: protocol, + protocolName: protocolInfo.name, + frequency: protocolInfo.frequency, + security: protocolInfo.security, + color: protocolInfo.color, + icon: protocolInfo.icon, + fields: [] + }; + + switch (protocol) { + case 'EM4100': + const hex = this.getCardHex(cardData); + const facility = cardData.rfid_data?.facility || cardData.rfid_facility; + const cardNumber = cardData.rfid_data?.cardNumber || cardData.rfid_card_number; + + displayData.fields = [ + { label: 'HEX', value: this.formatHex(hex) }, + { label: 'Facility', value: facility }, + { label: 'Card', value: cardNumber }, + { label: 'DEZ 8', value: this.toDEZ8(hex) } + ]; + break; + + case 'MIFARE_Classic': + const uid = this.getCardUID(cardData); + const keysKnown = cardData.rfid_data?.sectors ? + Object.keys(cardData.rfid_data.sectors).length : 0; + + displayData.fields = [ + { label: 'UID', value: this.formatHex(uid) }, + { label: 'Type', value: '1K (16 sectors)' }, + { label: 'Keys Known', value: `${keysKnown}/16` }, + { label: 'Readable', value: keysKnown === 16 ? 'Yes ✓' : 'Partial' }, + { label: 'Clonable', value: keysKnown > 0 ? 'Partial' : 'No' } + ]; + break; + + case 'MIFARE_DESFire': + const desUID = this.getCardUID(cardData); + + displayData.fields = [ + { label: 'UID', value: this.formatHex(desUID) }, + { label: 'Type', value: 'EV2' }, + { label: 'Encryption', value: '3DES/AES' }, + { label: 'Clonable', value: 'UID Only' } + ]; + break; + } + + return displayData; + } + + // ... existing methods remain unchanged +} +``` + +--- + +### Phase 2: Protocol Detection & UI (3h) + +**File**: `js/minigames/rfid/rfid-ui.js` (MODIFY) + +Update card data screen to use protocol display: + +```javascript +showCardDataScreen(cardData) { + const screen = this.getScreen(); + screen.innerHTML = ''; + + const displayData = this.dataManager.getCardDisplayData(cardData); + + // Breadcrumb + const breadcrumb = document.createElement('div'); + breadcrumb.className = 'flipper-breadcrumb'; + breadcrumb.textContent = 'RFID > Read'; + screen.appendChild(breadcrumb); + + // Protocol header with icon and color + const header = document.createElement('div'); + header.className = 'flipper-protocol-header'; + header.style.borderLeft = `4px solid ${displayData.color}`; + header.innerHTML = ` +
+ ${displayData.icon} + ${displayData.protocolName} +
+
+ ${displayData.frequency} + + ${displayData.security.toUpperCase()} + +
+ `; + screen.appendChild(header); + + // Card data fields + const dataDiv = document.createElement('div'); + dataDiv.className = 'flipper-card-data'; + displayData.fields.forEach(field => { + const fieldDiv = document.createElement('div'); + fieldDiv.innerHTML = `${field.label}: ${field.value}`; + dataDiv.appendChild(fieldDiv); + }); + screen.appendChild(dataDiv); + + // Warning for DESFire UID-only + if (displayData.protocol === 'MIFARE_DESFire') { + const warning = document.createElement('div'); + warning.className = 'flipper-warning'; + warning.innerHTML = '⚠️ UID Only - May not work on secure readers'; + screen.appendChild(warning); + } + + // Buttons + const buttons = document.createElement('div'); + buttons.className = 'flipper-buttons'; + + const saveBtn = document.createElement('button'); + saveBtn.className = 'flipper-button'; + saveBtn.textContent = 'Save'; + saveBtn.addEventListener('click', () => this.minigame.handleSaveCard(cardData)); + + const cancelBtn = document.createElement('button'); + cancelBtn.className = 'flipper-button flipper-button-secondary'; + cancelBtn.textContent = 'Cancel'; + cancelBtn.addEventListener('click', () => this.minigame.complete(false)); + + buttons.appendChild(saveBtn); + buttons.appendChild(cancelBtn); + screen.appendChild(buttons); +} + +/** + * Show protocol info with available actions + */ +showProtocolInfo(cardData) { + const screen = this.getScreen(); + screen.innerHTML = ''; + + const displayData = this.dataManager.getCardDisplayData(cardData); + + // Breadcrumb + const breadcrumb = document.createElement('div'); + breadcrumb.className = 'flipper-breadcrumb'; + breadcrumb.textContent = 'RFID > Info'; + screen.appendChild(breadcrumb); + + // Same header as above + // ... (code from showCardDataScreen) + + // Card data fields + // ... (code from showCardDataScreen) + + // Actions based on protocol and card state + const actions = document.createElement('div'); + actions.className = 'flipper-menu'; + actions.style.marginTop = '20px'; + + const protocol = displayData.protocol; + + if (protocol === 'MIFARE_Classic') { + const keysKnown = cardData.rfid_data?.sectors ? + Object.keys(cardData.rfid_data.sectors).length : 0; + + const info = document.createElement('div'); + info.className = 'flipper-info'; + info.textContent = 'This card is encrypted. Authentication keys required.'; + screen.appendChild(info); + + if (keysKnown === 0) { + // No keys - offer attacks + const dictBtn = document.createElement('div'); + dictBtn.className = 'flipper-menu-item'; + dictBtn.textContent = '> Dictionary Attack (instant)'; + dictBtn.addEventListener('click', () => + this.minigame.startKeyAttack('dictionary', cardData)); + actions.appendChild(dictBtn); + + const darksideBtn = document.createElement('div'); + darksideBtn.className = 'flipper-menu-item'; + darksideBtn.textContent = ' Darkside Attack (30 sec)'; + darksideBtn.addEventListener('click', () => + this.minigame.startKeyAttack('darkside', cardData)); + actions.appendChild(darksideBtn); + } else if (keysKnown < 16) { + // Some keys - offer nested + const nestedBtn = document.createElement('div'); + nestedBtn.className = 'flipper-menu-item'; + nestedBtn.textContent = `> Nested Attack (crack ${16 - keysKnown} sectors)`; + nestedBtn.addEventListener('click', () => + this.minigame.startKeyAttack('nested', cardData)); + actions.appendChild(nestedBtn); + + const readBtn = document.createElement('div'); + readBtn.className = 'flipper-menu-item'; + readBtn.textContent = ' Read Partial Data'; + readBtn.addEventListener('click', () => + this.showCardDataScreen(cardData)); + actions.appendChild(readBtn); + } else { + // All keys - can clone + const readBtn = document.createElement('div'); + readBtn.className = 'flipper-menu-item'; + readBtn.textContent = '> Read & Clone'; + readBtn.addEventListener('click', () => + this.showCardDataScreen(cardData)); + actions.appendChild(readBtn); + } + } else if (protocol === 'MIFARE_DESFire') { + const info = document.createElement('div'); + info.className = 'flipper-info'; + info.textContent = 'High security - full clone impossible'; + screen.appendChild(info); + + const uidBtn = document.createElement('div'); + uidBtn.className = 'flipper-menu-item'; + uidBtn.textContent = '> Save UID Only'; + uidBtn.addEventListener('click', () => + this.showCardDataScreen(cardData)); + actions.appendChild(uidBtn); + } else { + // EM4100 - instant clone + const readBtn = document.createElement('div'); + readBtn.className = 'flipper-menu-item'; + readBtn.textContent = '> Read & Clone'; + readBtn.addEventListener('click', () => + this.showReadingScreen()); + actions.appendChild(readBtn); + } + + const cancelBtn = document.createElement('div'); + cancelBtn.className = 'flipper-button-back'; + cancelBtn.textContent = '← Cancel'; + cancelBtn.addEventListener('click', () => this.minigame.complete(false)); + actions.appendChild(cancelBtn); + + screen.appendChild(actions); +} +``` + +**File**: `css/rfid-minigame.css` (ADD) + +```css +/* Protocol Header */ +.flipper-protocol-header { + background: rgba(0, 0, 0, 0.3); + padding: 15px; + border-radius: 8px; + margin: 15px 0; +} + +.protocol-header-top { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 8px; +} + +.protocol-icon { + font-size: 20px; +} + +.protocol-name { + font-size: 16px; + font-weight: bold; + color: white; +} + +.protocol-meta { + font-size: 12px; + color: #888; + display: flex; + justify-content: space-between; + align-items: center; +} + +.security-badge { + padding: 2px 8px; + border-radius: 3px; + font-size: 10px; + font-weight: bold; +} + +.security-low { background: rgba(255, 107, 107, 0.3); color: #FF6B6B; } +.security-medium { background: rgba(78, 205, 196, 0.3); color: #4ECDC4; } +.security-high { background: rgba(149, 225, 211, 0.3); color: #95E1D3; } + +.flipper-warning { + background: rgba(255, 165, 0, 0.2); + border-left: 3px solid #FFA500; + padding: 12px; + margin: 15px 0; + color: #FFA500; + font-size: 13px; + border-radius: 5px; +} +``` + +--- + +### Phase 3: MIFARE Attack System (5h) + +**File**: `js/minigames/rfid/rfid-attacks.js` (NEW) + +```javascript +import { MIFARE_COMMON_KEYS } from './rfid-protocols.js'; + +// Attack timing constants +const ATTACK_DURATIONS = { + darkside: 30000, // 30 seconds + nested: 10000, // 10 seconds + dictionary: 0 // Instant +}; + +export class MIFAREAttackManager { + constructor() { + this.activeAttacks = new Map(); + } + + /** + * Dictionary attack - try common keys (instant) + */ + dictionaryAttack(uid, existingKeys = {}) { + console.log('🔓 Dictionary attack on', uid); + + const foundKeys = { ...existingKeys }; + let newKeysFound = 0; + + for (let sector = 0; sector < 16; sector++) { + if (foundKeys[sector]) continue; + + // Try common keys (10% success chance each) + for (const commonKey of MIFARE_COMMON_KEYS) { + if (Math.random() < 0.1) { + foundKeys[sector] = { keyA: commonKey, keyB: commonKey }; + newKeysFound++; + break; + } + } + } + + return { + success: newKeysFound > 0, + foundKeys: foundKeys, + newKeysFound: newKeysFound, + message: newKeysFound > 0 ? + `Found ${newKeysFound} sector(s) using common keys!` : + 'No common keys found - try Darkside attack' + }; + } + + /** + * Darkside attack - crack all keys (30 sec) + */ + async startDarksideAttack(uid, progressCallback) { + console.log('🔓 Darkside attack on', uid); + + return new Promise((resolve, reject) => { + const attack = { + type: 'darkside', + uid: uid, + foundKeys: {}, + startTime: Date.now() + }; + + this.activeAttacks.set(uid, attack); + + const duration = ATTACK_DURATIONS.darkside; + const updateInterval = 500; + let elapsed = 0; + + const interval = setInterval(() => { + elapsed += updateInterval; + const progress = Math.min(100, (elapsed / duration) * 100); + const currentSector = Math.floor((progress / 100) * 16); + + // Add found keys progressively + for (let i = 0; i < currentSector; i++) { + if (!attack.foundKeys[i]) { + attack.foundKeys[i] = { + keyA: this.generateRandomKey(), + keyB: this.generateRandomKey() + }; + } + } + + // Progress callback + if (progressCallback) { + progressCallback({ + progress: progress, + currentSector: currentSector, + foundKeys: attack.foundKeys + }); + } + + // Complete + if (progress >= 100) { + clearInterval(interval); + + // Ensure all 16 sectors have keys + for (let i = 0; i < 16; i++) { + if (!attack.foundKeys[i]) { + attack.foundKeys[i] = { + keyA: this.generateRandomKey(), + keyB: this.generateRandomKey() + }; + } + } + + this.activeAttacks.delete(uid); + + resolve({ + success: true, + foundKeys: attack.foundKeys, + message: 'All 16 sectors cracked!' + }); + } + }, updateInterval); + + // Store interval for cleanup + attack.interval = interval; + }); + } + + /** + * Nested attack - crack remaining keys (10 sec) + */ + async startNestedAttack(uid, knownKeys, progressCallback) { + console.log('🔓 Nested attack on', uid); + + if (Object.keys(knownKeys).length === 0) { + return Promise.reject(new Error('Need at least one known key for Nested attack')); + } + + return new Promise((resolve) => { + const attack = { + type: 'nested', + uid: uid, + foundKeys: { ...knownKeys }, + startTime: Date.now() + }; + + this.activeAttacks.set(uid, attack); + + const duration = ATTACK_DURATIONS.nested; + const updateInterval = 500; + const sectorsToFind = 16 - Object.keys(knownKeys).length; + + let elapsed = 0; + let sectorsFound = 0; + + const interval = setInterval(() => { + elapsed += updateInterval; + const progress = Math.min(100, (elapsed / duration) * 100); + + // Add found keys progressively + const expectedFound = Math.floor((progress / 100) * sectorsToFind); + + while (sectorsFound < expectedFound) { + // Find next empty sector + for (let i = 0; i < 16; i++) { + if (!attack.foundKeys[i]) { + attack.foundKeys[i] = { + keyA: this.generateRandomKey(), + keyB: this.generateRandomKey() + }; + sectorsFound++; + break; + } + } + } + + if (progressCallback) { + progressCallback({ + progress: progress, + foundKeys: attack.foundKeys, + sectorsRemaining: sectorsToFind - sectorsFound + }); + } + + if (progress >= 100) { + clearInterval(interval); + this.activeAttacks.delete(uid); + + resolve({ + success: true, + foundKeys: attack.foundKeys, + message: `Cracked ${sectorsToFind} remaining sectors!` + }); + } + }, updateInterval); + + attack.interval = interval; + }); + } + + /** + * Generate random MIFARE key (for simulation) + */ + generateRandomKey() { + return Array.from({ length: 12 }, () => + Math.floor(Math.random() * 16).toString(16).toUpperCase() + ).join(''); + } + + /** + * Cleanup all active attacks + */ + cleanup() { + this.activeAttacks.forEach(attack => { + if (attack.interval) { + clearInterval(attack.interval); + } + }); + this.activeAttacks.clear(); + } + + /** + * Cancel specific attack + */ + cancelAttack(uid) { + const attack = this.activeAttacks.get(uid); + if (attack && attack.interval) { + clearInterval(attack.interval); + } + this.activeAttacks.delete(uid); + } +} + +// Global instance +window.mifareAttackManager = window.mifareAttackManager || new MIFAREAttackManager(); + +export default MIFAREAttackManager; +``` + +**File**: `js/minigames/rfid/rfid-minigame.js` (MODIFY) + +Add attack integration to clone mode: + +```javascript +import { MIFAREAttackManager } from './rfid-attacks.js'; +import { getProtocolInfo } from './rfid-protocols.js'; + +export class RFIDMinigame extends MinigameScene { + constructor(container, params) { + super(container, params); + + // ... existing code + + // Attack manager + this.attackManager = window.mifareAttackManager; + } + + init() { + super.init(); + + // ... existing code + + // Create appropriate interface + if (this.mode === 'unlock') { + this.ui.createUnlockInterface(); + } else if (this.mode === 'clone') { + // Check protocol and show appropriate screen + const protocol = this.cardToClone?.rfid_protocol || 'EM4100'; + + if (protocol === 'MIFARE_Classic') { + // Check if keys are available + const keysKnown = this.cardToClone.rfid_data?.sectors ? + Object.keys(this.cardToClone.rfid_data.sectors).length : 0; + + if (keysKnown === 0 || keysKnown < 16) { + // Need to crack keys - show protocol info with attack options + this.ui.showProtocolInfo(this.cardToClone); + } else { + // Has all keys - proceed with reading + this.ui.showReadingScreen(); + } + } else { + // EM4100 or DESFire - start reading immediately + this.ui.showReadingScreen(); + } + } + } + + /** + * Start MIFARE key attack + */ + async startKeyAttack(attackType, cardData) { + console.log(`🔓 Starting ${attackType} attack`); + + // Show attack screen + this.ui.showKeyAttackScreen(attackType, cardData); + + let result; + + try { + switch (attackType) { + case 'dictionary': + result = this.attackManager.dictionaryAttack( + cardData.rfid_data.uid, + cardData.rfid_data.sectors || {} + ); + + if (result.success) { + this.ui.showSuccess(result.message); + cardData.rfid_data.sectors = result.foundKeys; + + setTimeout(() => { + // Check if all keys found + const keysKnown = Object.keys(result.foundKeys).length; + if (keysKnown === 16) { + this.ui.showCardDataScreen(cardData); + } else { + this.ui.showProtocolInfo(cardData); + } + }, 2000); + } else { + this.ui.showError(result.message); + setTimeout(() => this.ui.showProtocolInfo(cardData), 2000); + } + break; + + case 'darkside': + result = await this.attackManager.startDarksideAttack( + cardData.rfid_data.uid, + (progress) => this.ui.updateAttackProgress(progress) + ); + + cardData.rfid_data.sectors = result.foundKeys; + this.ui.showSuccess(result.message); + + setTimeout(() => { + this.ui.showCardDataScreen(cardData); + }, 2000); + break; + + case 'nested': + result = await this.attackManager.startNestedAttack( + cardData.rfid_data.uid, + cardData.rfid_data.sectors || {}, + (progress) => this.ui.updateAttackProgress(progress) + ); + + cardData.rfid_data.sectors = result.foundKeys; + this.ui.showSuccess(result.message); + + setTimeout(() => { + this.ui.showCardDataScreen(cardData); + }, 2000); + break; + } + } catch (error) { + console.error('Attack failed:', error); + this.ui.showError(error.message); + setTimeout(() => this.ui.showProtocolInfo(cardData), 2000); + } + } + + cleanup() { + // Cleanup attacks if any are running + if (this.attackManager && this.cardToClone?.rfid_data?.uid) { + this.attackManager.cancelAttack(this.cardToClone.rfid_data.uid); + } + + // ... existing cleanup code + } +} +``` + +**File**: `js/minigames/rfid/rfid-ui.js` (ADD) + +```javascript +/** + * Show key attack screen + */ +showKeyAttackScreen(attackType, cardData) { + const screen = this.getScreen(); + screen.innerHTML = ''; + + // Breadcrumb + const breadcrumb = document.createElement('div'); + breadcrumb.className = 'flipper-breadcrumb'; + const attackName = attackType.charAt(0).toUpperCase() + attackType.slice(1); + breadcrumb.textContent = `RFID > ${attackName} Attack`; + screen.appendChild(breadcrumb); + + // Card info + const cardInfo = document.createElement('div'); + cardInfo.className = 'flipper-card-name'; + cardInfo.textContent = cardData.name; + screen.appendChild(cardInfo); + + const uid = document.createElement('div'); + uid.className = 'flipper-info-dim'; + uid.textContent = `UID: ${this.dataManager.formatHex(cardData.rfid_data.uid)}`; + screen.appendChild(uid); + + // Progress container (will be populated by updateAttackProgress) + const progressDiv = document.createElement('div'); + progressDiv.id = 'attack-progress-container'; + screen.appendChild(progressDiv); + + // Keys found container + const keysDiv = document.createElement('div'); + keysDiv.id = 'attack-keys-found'; + keysDiv.className = 'attack-keys-list'; + screen.appendChild(keysDiv); + + // Status + const status = document.createElement('div'); + status.id = 'attack-status'; + status.className = 'flipper-info'; + status.textContent = attackType === 'dictionary' ? + 'Trying common keys...' : 'Don\'t move card...'; + screen.appendChild(status); +} + +/** + * Update attack progress + */ +updateAttackProgress(progressData) { + // Add progress bar if not present + const progressDiv = document.getElementById('attack-progress-container'); + if (progressDiv && !progressDiv.querySelector('.rfid-progress-container')) { + const container = document.createElement('div'); + container.className = 'rfid-progress-container'; + + const bar = document.createElement('div'); + bar.className = 'rfid-progress-bar'; + bar.id = 'attack-progress-bar'; + + container.appendChild(bar); + progressDiv.appendChild(container); + + const label = document.createElement('div'); + label.className = 'flipper-info'; + label.id = 'attack-progress-label'; + progressDiv.appendChild(label); + } + + // Update progress bar + const bar = document.getElementById('attack-progress-bar'); + const label = document.getElementById('attack-progress-label'); + + if (bar) { + bar.style.width = `${progressData.progress}%`; + + // Color based on progress + if (progressData.progress < 50) { + bar.style.backgroundColor = '#FF8200'; + } else if (progressData.progress < 100) { + bar.style.backgroundColor = '#FFA500'; + } else { + bar.style.backgroundColor = '#00FF00'; + } + } + + if (label) { + if (progressData.currentSector !== undefined) { + label.textContent = `Cracking Sector ${progressData.currentSector}/16...`; + } else if (progressData.sectorsRemaining !== undefined) { + label.textContent = `${progressData.sectorsRemaining} sectors remaining...`; + } + } + + // Update keys found list + const keysDiv = document.getElementById('attack-keys-found'); + if (keysDiv && progressData.foundKeys) { + keysDiv.innerHTML = '
Keys Found:
'; + + Object.keys(progressData.foundKeys).sort((a, b) => a - b).forEach(sector => { + const keyLine = document.createElement('div'); + keyLine.className = 'attack-key-item'; + const key = progressData.foundKeys[sector]; + keyLine.textContent = `Sector ${sector}: ${key.keyA} ✓`; + keysDiv.appendChild(keyLine); + }); + } +} +``` + +**File**: `css/rfid-minigame.css` (ADD) + +```css +/* Attack UI */ +.attack-keys-list { + background: rgba(0, 0, 0, 0.3); + padding: 10px; + border-radius: 5px; + margin: 10px 0; + max-height: 150px; + overflow-y: auto; + font-size: 11px; +} + +.attack-key-item { + padding: 3px 0; + color: #00FF00; + font-family: monospace; +} + +#attack-progress-label { + margin-top: 10px; + font-size: 12px; + color: #FFA500; +} +``` + +--- + +### Phase 4: Ink Integration (2h) + +**File**: `js/minigames/person-chat/person-chat-conversation.js` (MODIFY) + +```javascript +import { getProtocolInfo } from '../rfid/rfid-protocols.js'; + +// In setupExternalFunctions(), after syncItemsToInk() +syncCardProtocolsToInk() { + if (!this.inkEngine || !this.npc || !this.npc.itemsHeld) return; + + // Find keycards + const keycards = this.npc.itemsHeld.filter(item => item.type === 'keycard'); + + keycards.forEach((card, index) => { + const protocol = card.rfid_protocol || 'EM4100'; + const protocolInfo = getProtocolInfo(protocol); + const prefix = index === 0 ? 'card' : `card${index + 1}`; + + try { + this.inkEngine.setVariable(`${prefix}_protocol`, protocol); + this.inkEngine.setVariable(`${prefix}_name`, card.name || 'Card'); + this.inkEngine.setVariable(`${prefix}_security`, protocolInfo.security); + this.inkEngine.setVariable(`${prefix}_clonable`, + protocolInfo.capabilities.clone === true); + + // Set hex/UID + if (card.rfid_data) { + if (card.rfid_data.hex) { + this.inkEngine.setVariable(`${prefix}_hex`, card.rfid_data.hex); + } + if (card.rfid_data.uid) { + this.inkEngine.setVariable(`${prefix}_uid`, card.rfid_data.uid); + } + } else if (card.rfid_hex) { + // Old format support + this.inkEngine.setVariable(`${prefix}_hex`, card.rfid_hex); + } + + console.log(`✅ Synced ${prefix} protocol: ${protocol}`); + } catch (err) { + console.warn(`⚠️ Could not sync card protocol:`, err.message); + } + }); +} + +// Call in setupExternalFunctions() +setupExternalFunctions() { + // ... existing code + + this.syncItemsToInk(); + this.syncCardProtocolsToInk(); // ADD THIS +} +``` + +**Documentation**: Required Ink Variables + +Create file: `scenarios/ink/README_RFID_VARIABLES.md` + +```markdown +# RFID Protocol Variables for Ink + +When NPCs hold keycards, these variables are automatically synced to Ink conversations. + +## Required Variable Declarations + +Add to your .ink file: + +```ink +// Card protocol info (synced from NPC itemsHeld) +VAR card_protocol = "" // "EM4100", "MIFARE_Classic", "MIFARE_DESFire" +VAR card_name = "" // Card display name +VAR card_hex = "" // For EM4100 cards +VAR card_uid = "" // For MIFARE cards +VAR card_security = "" // "low", "medium", "high" +VAR card_clonable = false // true if instant clone possible +``` + +## Usage Examples + +### EM4100 (instant clone): +```ink +{card_protocol == "EM4100": + + [Scan the badge] + # clone_keycard:{card_name}|{card_hex} + You quickly scan their badge. + -> cloned +} +``` + +### MIFARE Classic (needs attack): +```ink +{card_protocol == "MIFARE_Classic": + + [Scan the badge] + # save_uid_only:{card_name}|{card_uid} + You can only save the UID. To fully clone this card, you'll need to crack the keys. + -> uid_saved + + + [Ask to borrow it] + Maybe you can crack the keys if you have it for a few seconds... + -> borrow_card +} +``` + +### MIFARE DESFire (impossible): +```ink +{card_protocol == "MIFARE_DESFire": + + [Try to scan the badge] + # save_uid_only:{card_name}|{card_uid} + High security card - you can only get the UID. Full cloning is impossible. + -> uid_only +} +``` +``` + +**File**: `js/minigames/helpers/chat-helpers.js` (MODIFY) + +Add new tags: + +```javascript +case 'save_uid_only': + if (param) { + const [cardName, uid] = param.split('|').map(s => s.trim()); + + const hasCloner = window.inventory.items.some(item => + item && item.scenarioData && + item.scenarioData.type === 'rfid_cloner' + ); + + if (!hasCloner) { + result.message = '⚠️ You need an RFID cloner'; + break; + } + + window.pendingConversationReturn = { + npcId: window.currentConversationNPCId, + type: window.currentConversationMinigameType || 'person-chat' + }; + + if (window.startRFIDMinigame) { + window.startRFIDMinigame(null, null, { + mode: 'clone', + cardToClone: { + name: `${cardName} (UID Only)`, + rfid_protocol: 'MIFARE_DESFire', + rfid_data: { + uid: uid + }, + type: 'keycard', + key_id: `uid_${uid.toLowerCase()}`, + observations: '⚠️ UID only - may not work on all readers' + } + }); + result.success = true; + } + } + break; +``` + +--- + +### Phase 5: Door Lock Integration (1h) + +**File**: `js/systems/unlock-system.js` (MODIFY) + +Add UID-only acceptance logic: + +```javascript +case 'rfid': + const requiredCardId = lockRequirements.requires; + const acceptsUIDOnly = lockRequirements.acceptsUIDOnly || false; // NEW + + const keycards = window.inventory.items.filter(item => + item && item.scenarioData && + item.scenarioData.type === 'keycard' + ); + + const cloner = window.inventory.items.find(item => + item && item.scenarioData && + item.scenarioData.type === 'rfid_cloner' + ); + + if (keycards.length > 0 || cloner?.scenarioData?.saved_cards?.length > 0) { + window.startRFIDMinigame(lockable, type, { + mode: 'unlock', + requiredCardId: requiredCardId, + availableCards: keycards, + hasCloner: !!cloner, + acceptsUIDOnly: acceptsUIDOnly, // Pass to minigame + onComplete: (success) => { + if (success) { + unlockTarget(lockable, type, lockable.layer); + } + } + }); + } else { + window.gameAlert('Requires RFID keycard', 'error', 'Access Denied', 4000); + } + break; +``` + +**File**: `js/minigames/rfid/rfid-minigame.js` (MODIFY) + +Check UID-only acceptance in handleEmulate: + +```javascript +handleEmulate(savedCard) { + console.log('📡 Emulating card:', savedCard.name); + + const cardId = savedCard.key_id; + const isCorrect = cardId === this.requiredCardId; + + // Check if this is UID-only emulation + const isUIDOnly = savedCard.rfid_protocol === 'MIFARE_DESFire' && + !savedCard.rfid_data?.masterKeyKnown; + + if (isUIDOnly && !this.params.acceptsUIDOnly) { + // UID-only doesn't work on this secure reader + this.animations.showEmulationFailure(); + this.ui.showError('Reader requires full authentication'); + + setTimeout(() => { + this.ui.showSavedCards(); + }, 2000); + return; + } + + // Normal emulation check + if (isCorrect) { + this.animations.showEmulationSuccess(); + this.ui.showSuccess('Access Granted'); + // ... rest of success code + } else { + this.animations.showEmulationFailure(); + this.ui.showError('Access Denied'); + // ... rest of failure code + } +} +``` + +--- + +## Test Scenarios + +### Scenario 1: EM4100 (Already Exists) +File: `scenarios/test-rfid.json` +- Current test scenario works as-is +- No changes needed + +### Scenario 2: MIFARE Classic +File: `scenarios/test-rfid-mifare.json` (NEW) + +```json +{ + "name": "RFID MIFARE Test", + "startRoom": "test_lobby", + "rooms": { + "test_lobby": { + "type": "room_reception", + "npcs": [{ + "id": "guard", + "npcType": "person", + "position": { "x": 6, "y": 4 }, + "storyPath": "scenarios/ink/rfid-mifare-guard.json", + "itemsHeld": [{ + "type": "keycard", + "name": "Encrypted Badge", + "rfid_protocol": "MIFARE_Classic", + "rfid_data": { + "uid": "AB12CD34" + }, + "key_id": "encrypted_badge" + }] + }], + "objects": [ + { + "type": "rfid_cloner", + "name": "Flipper Zero", + "saved_cards": [] + } + ], + "doors": [{ + "locked": true, + "lockType": "rfid", + "requires": "encrypted_badge" + }] + } + } +} +``` + +### Scenario 3: MIFARE DESFire +File: `scenarios/test-rfid-desfire.json` (NEW) + +```json +{ + "name": "RFID DESFire Test", + "startRoom": "test_lobby", + "rooms": { + "test_lobby": { + "type": "room_reception", + "objects": [ + { + "type": "keycard", + "name": "High Security Badge", + "rfid_protocol": "MIFARE_DESFire", + "rfid_data": { + "uid": "04AB12CD3456E0" + }, + "key_id": "high_security_badge" + }, + { + "type": "rfid_cloner", + "name": "Flipper Zero" + } + ], + "doors": [ + { + "locked": true, + "lockType": "rfid", + "requires": "high_security_badge", + "acceptsUIDOnly": false, + "description": "Secure reader - requires full auth" + }, + { + "locked": true, + "lockType": "rfid", + "requires": "high_security_badge", + "acceptsUIDOnly": true, + "description": "Simple reader - accepts UID only" + } + ] + } + } +} +``` + +--- + +## Implementation Checklist + +### Phase 1: Foundation (3h) +- [ ] Create `rfid-protocols.js` with three protocols +- [ ] Add protocol constants (timing, common keys) +- [ ] Update `rfid-data.js` with dual format support +- [ ] Add `detectProtocol()` method +- [ ] Add `getCardDisplayData()` method +- [ ] Test backward compatibility with existing cards + +### Phase 2: UI (3h) +- [ ] Update `showCardDataScreen()` with protocol header +- [ ] Add `showProtocolInfo()` with conditional actions +- [ ] Add protocol header CSS (icons, colors, badges) +- [ ] Add warning styles for DESFire +- [ ] Test all three protocols display correctly + +### Phase 3: Attacks (5h) +- [ ] Create `rfid-attacks.js` module +- [ ] Implement dictionary attack (instant) +- [ ] Implement Darkside attack (30 sec) +- [ ] Implement Nested attack (10 sec) +- [ ] Add attack UI screens to `rfid-ui.js` +- [ ] Add `updateAttackProgress()` method +- [ ] Integrate attacks into `rfid-minigame.js` +- [ ] Add attack cleanup in `cleanup()` +- [ ] Add attack CSS styles +- [ ] Test all three attack types + +### Phase 4: Ink Integration (2h) +- [ ] Add `syncCardProtocolsToInk()` to person-chat +- [ ] Add `save_uid_only` tag to chat-helpers +- [ ] Create Ink variables documentation +- [ ] Create example Ink files for each protocol +- [ ] Test Ink variable syncing + +### Phase 5: Door Integration (1h) +- [ ] Add `acceptsUIDOnly` to unlock-system +- [ ] Update `handleEmulate()` with UID check +- [ ] Test DESFire against secure/simple readers +- [ ] Create test scenarios + +--- + +## Total Time: 14 hours + +**Saved from Original**: 5 hours +- Removed HID Prox: -2h +- Merged attack mode: -1h +- Removed firmware system: -2h + +**Quality Improvements**: +- Simpler code (no migration needed) +- Better error handling +- Clearer documentation +- More testable + +**Ready for implementation** ✅ diff --git a/planning_notes/rfid_keycard/protocols_and_interactions/01_TECHNICAL_DESIGN.md b/planning_notes/rfid_keycard/protocols_and_interactions/01_TECHNICAL_DESIGN.md new file mode 100644 index 0000000..ed4970d --- /dev/null +++ b/planning_notes/rfid_keycard/protocols_and_interactions/01_TECHNICAL_DESIGN.md @@ -0,0 +1,708 @@ +# RFID Protocols & Interactions - Technical Design + +## Overview + +Add support for multiple RFID protocols with different security levels and capabilities. Each protocol has realistic constraints based on real-world RFID technology, enabling different attack vectors and gameplay strategies. + +## Protocol Specifications + +### Protocol Definitions + +Based on real-world RFID technology used in access control systems: + +```javascript +const RFID_PROTOCOLS = { + 'EM4100': { + name: 'EM-Micro EM4100', + frequency: '125kHz', + security: 'low', + readOnly: true, + capabilities: { + read: true, + clone: true, + write: false, + emulate: true, + bruteforce: false // Too many combinations + }, + description: 'Legacy low-frequency card. Read-only, easily cloned.', + vulnerabilities: ['Clone attack', 'Replay attack'], + hexLength: 10, // 5 bytes + color: '#FF6B6B' // Red for low security + }, + + 'HID_Prox': { + name: 'HID Prox II', + frequency: '125kHz', + security: 'medium-low', + readOnly: true, + capabilities: { + read: true, + clone: true, + write: false, + emulate: true, + bruteforce: false + }, + description: 'Common corporate badge. Read-only, proprietary format.', + vulnerabilities: ['Clone attack', 'Replay attack'], + hexLength: 12, // 6 bytes (26-bit format) + color: '#FFA500' // Orange for medium-low + }, + + 'MIFARE_Classic': { + name: 'MIFARE Classic 1K', + frequency: '13.56MHz', + security: 'medium', + readOnly: false, + capabilities: { + read: 'with-keys', // Need auth keys + clone: 'with-keys', + write: 'with-keys', + emulate: true, + bruteforce: true // Weak crypto, can crack keys + }, + description: 'Encrypted NFC card. Requires authentication keys.', + vulnerabilities: ['Darkside attack', 'Nested attack', 'Hardnested attack'], + sectors: 16, + keysPerSector: 2, // Key A and Key B + hexLength: 8, // UID is 4 bytes + color: '#4ECDC4' // Teal for medium + }, + + 'MIFARE_DESFire': { + name: 'MIFARE DESFire EV2', + frequency: '13.56MHz', + security: 'high', + readOnly: false, + capabilities: { + read: false, // Encrypted, can't read without master key + clone: false, // Can't clone - uses mutual authentication + write: false, // Can't write without master key + emulate: 'uid-only', // Can only emulate UID, not full card + bruteforce: false // Strong crypto (3DES/AES) + }, + description: 'High-security encrypted NFC. Nearly impossible to clone.', + vulnerabilities: ['Physical theft only'], + hexLength: 14, // 7-byte UID + color: '#95E1D3' // Light green for high security + } +}; +``` + +## Data Model Changes + +### Card Data Structure + +**Current** (EM4100 only): +```javascript +{ + type: "keycard", + name: "Employee Badge", + rfid_hex: "01AB34CD56", + rfid_facility: 1, + rfid_card_number: 43981, + rfid_protocol: "EM4100", + key_id: "employee_badge" +} +``` + +**Enhanced** (all protocols): +```javascript +{ + type: "keycard", + name: "Employee Badge", + rfid_protocol: "EM4100", // or HID_Prox, MIFARE_Classic, MIFARE_DESFire + key_id: "employee_badge", + + // Protocol-specific data (only relevant fields per protocol) + rfid_data: { + // EM4100 / HID Prox + hex: "01AB34CD56", + facility: 1, + cardNumber: 43981, + + // MIFARE Classic (if applicable) + uid: "AB12CD34", + sectors: { + 0: { keyA: "FFFFFFFFFFFF", keyB: null }, // Default key + 1: { keyA: "A1B2C3D4E5F6", keyB: "123456789ABC" }, // Custom keys + // ... more sectors + }, + + // MIFARE DESFire (if applicable) + uid: "04AB12CD3456E0", + masterKeyKnown: false, // Can't clone without this + + // Clone quality (for cloned cards) + isClone: false, + cloneQuality: 100, // 0-100, affects reliability + clonedFrom: null // Original card ID + }, + + observations: "Standard employee access badge." +} +``` + +### RFID Cloner Data Structure + +```javascript +{ + type: "rfid_cloner", + name: "Flipper Zero", + + // Firmware capabilities (can be upgraded in-game) + firmware: { + version: "1.0", + protocols: ['EM4100', 'HID_Prox'], // Unlocks MIFARE support later + attacks: ['read', 'clone', 'emulate'] // Unlocks 'bruteforce' later + }, + + saved_cards: [ + // Array of card data objects + ], + + // Cracking progress (for MIFARE Classic key attacks) + activeAttacks: { + "security_badge_uid_AB12CD34": { + type: "darkside_attack", + protocol: "MIFARE_Classic", + progress: 45, // 0-100% + sector: 3, + foundKeys: { + 0: { keyA: "FFFFFFFFFFFF" }, + 1: { keyA: "A1B2C3D4E5F6" } + } + } + }, + + x: 350, + y: 250, + takeable: true, + observations: "Portable multi-tool for pentesters. Can read and emulate RFID cards." +} +``` + +## Flipper Zero Operations by Protocol + +### EM4100 / HID Prox Operations + +**1. Read** +- Instant read, shows all data +- No authentication needed +- UI: Standard reading screen (already implemented) + +**2. Clone** +- Instant clone, perfect copy +- UI: Progress bar → Card data → Save + +**3. Emulate** +- Perfect emulation +- UI: Emulation screen (already implemented) + +### MIFARE Classic Operations + +**1. Read (requires keys)** +``` +Decision Tree: +├─ Has all keys for all sectors? +│ ├─ Yes → Read full card data +│ └─ No → Show partial data, offer key attack +└─ Has NO keys? + └─ Offer Darkside/Nested attack to crack keys +``` + +**2. Clone (requires keys)** +- Can only clone sectors where keys are known +- Partial clones possible (some sectors locked) + +**3. Key Attacks** +- **Darkside Attack**: Crack keys from scratch (~30 seconds realistic) +- **Nested Attack**: Crack remaining keys if you have one key (~10 seconds) +- **Dictionary Attack**: Try common keys (instant check) + +**4. Write** +- Modify card data in writable sectors +- Useful for: + - Changing balance on payment cards + - Modifying access permissions + - Writing cloned data to blank cards + +**5. Emulate** +- Can emulate if keys are known +- UI shows which sectors are available + +### MIFARE DESFire Operations + +**1. Read** +- Can only read UID (no encryption keys) +- Cannot read application data + +**2. UID Emulation** +- Can emulate UID only +- Some systems check UID only (lower security) +- Higher security systems use encrypted challenge-response (emulation fails) + +**3. No Clone/Write** +- Strong encryption prevents cloning +- Game design: These cards must be physically stolen or access granted through social engineering + +## UI Design + +### Protocol Detection Screen + +New screen when reading a card for the first time: + +``` +╔════════════════════════════════════╗ +║ FLIPPER ZERO ⚡ 100% ║ +╠════════════════════════════════════╣ +║ ║ +║ RFID > Read ║ +║ ║ +║ Detecting... ║ +║ ║ +║ ┌────────────────────────────────┐║ +║ │ 📡 │║ +║ │ │║ +║ │ [Progress Bar 65%] │║ +║ └────────────────────────────────┘║ +║ ║ +║ Scanning frequencies... ║ +║ 125kHz: No response ║ +║ 13.56MHz: Card detected! ║ +║ ║ +╚════════════════════════════════════╝ +``` + +### Protocol Info Screen + +After detection: + +``` +╔════════════════════════════════════╗ +║ FLIPPER ZERO ⚡ 100% ║ +╠════════════════════════════════════╣ +║ ║ +║ RFID > Read > Info ║ +║ ║ +║ ┌────────────────────────────────┐║ +║ │ MIFARE Classic 1K │║ +║ │ ────────────────── │║ +║ │ Freq: 13.56MHz │║ +║ │ Security: Medium │║ +║ │ UID: AB 12 CD 34 │║ +║ └────────────────────────────────┘║ +║ ║ +║ This card uses encryption. ║ +║ Authentication keys required. ║ +║ ║ +║ > Read (requires keys) ║ +║ Crack Keys ║ +║ Try Dictionary ║ +║ Cancel ║ +║ ║ +╚════════════════════════════════════╝ +``` + +### Key Cracking Screen (MIFARE Classic) + +``` +╔════════════════════════════════════╗ +║ FLIPPER ZERO ⚡ 95% ║ +╠════════════════════════════════════╣ +║ ║ +║ RFID > Darkside Attack ║ +║ ║ +║ Security Badge ║ +║ UID: AB 12 CD 34 ║ +║ ║ +║ ┌────────────────────────────────┐║ +║ │ Cracking Sector 3... │║ +║ │ ████████████░░░░░░░░ 65% │║ +║ └────────────────────────────────┘║ +║ ║ +║ Keys Found: ║ +║ Sector 0: FF FF FF FF FF FF ✓ ║ +║ Sector 1: A1 B2 C3 D4 E5 F6 ✓ ║ +║ Sector 2: 12 34 56 78 9A BC ✓ ║ +║ Sector 3: Cracking... ║ +║ ║ +║ Don't move card... ║ +║ ║ +╚════════════════════════════════════╝ +``` + +### Card Data Screen with Protocol-Specific Fields + +**EM4100:** +``` +╔════════════════════════════════════╗ +║ RFID > Read ║ +║ ║ +║ EM-Micro EM4100 ║ +║ ║ +║ HEX: 01 AB 34 CD 56 ║ +║ Facility: 1 ║ +║ Card: 43981 ║ +║ Checksum: 0xD6 ║ +║ DEZ 8: 00043981 ║ +║ ║ +║ [Save] [Cancel] ║ +╚════════════════════════════════════╝ +``` + +**MIFARE Classic (with keys):** +``` +╔════════════════════════════════════╗ +║ RFID > Read ║ +║ ║ +║ MIFARE Classic 1K ║ +║ ║ +║ UID: AB 12 CD 34 ║ +║ SAK: 08 ║ +║ ATQA: 00 04 ║ +║ ║ +║ Sectors: 16 ║ +║ Keys Known: 16/16 ✓ ║ +║ ║ +║ Readable: Yes ║ +║ Writable: Yes ║ +║ Clonable: Yes ║ +║ ║ +║ [Save] [View Data] [Cancel] ║ +╚════════════════════════════════════╝ +``` + +**MIFARE DESFire (limited):** +``` +╔════════════════════════════════════╗ +║ RFID > Read ║ +║ ║ +║ MIFARE DESFire EV2 ║ +║ ║ +║ UID: 04 AB 12 CD 34 56 E0 ║ +║ SAK: 20 ║ +║ ATQA: 03 44 ║ +║ ║ +║ ⚠️ High Security ║ +║ ║ +║ This card uses 3DES encryption. ║ +║ Full clone: Not possible ║ +║ UID emulation: Possible ║ +║ ║ +║ Some systems only check UID and ║ +║ don't use encryption properly. ║ +║ ║ +║ [Save UID] [Cancel] ║ +╚════════════════════════════════════╝ +``` + +## Ink Integration + +### Exposing Card Protocol Info to Ink + +When NPC conversation starts, sync card protocol information: + +```javascript +// In person-chat-conversation.js, extend syncItemsToInk() +syncCardProtocolsToInk() { + if (!this.inkEngine || !this.npc || !this.npc.itemsHeld) return; + + // Find all keycards held by NPC + const keycards = this.npc.itemsHeld.filter(item => item.type === 'keycard'); + + keycards.forEach((card, index) => { + const protocol = card.rfid_protocol || 'EM4100'; + const prefix = index === 0 ? 'card' : `card${index + 1}`; + + // Set variables for this card + this.inkEngine.setVariable(`${prefix}_protocol`, protocol); + this.inkEngine.setVariable(`${prefix}_name`, card.name); + this.inkEngine.setVariable(`${prefix}_security`, RFID_PROTOCOLS[protocol].security); + this.inkEngine.setVariable(`${prefix}_clonable`, RFID_PROTOCOLS[protocol].capabilities.clone === true); + }); +} +``` + +### Ink Variable Usage + +```ink +VAR card_protocol = "" +VAR card_name = "" +VAR card_security = "" +VAR card_clonable = false + +=== guard_conversation === +# speaker:npc +I've got my security badge right here on my lanyard. + +{card_protocol == "EM4100": + -> easy_clone +} +{card_protocol == "MIFARE_Classic": + -> needs_key_attack +} +{card_protocol == "MIFARE_DESFire": + -> impossible_clone +} + +=== easy_clone === ++ [Subtly scan the badge] + # clone_keycard:{card_name}|{card_hex} + You discretely position your Flipper near their badge. + -> cloned + +=== needs_key_attack === ++ [Scan the badge] + You scan the badge but it's encrypted... + # start_mifare_attack:{card_name}|{card_uid} + Your Flipper starts a Darkside attack. + -> wait_for_crack + ++ [Ask to borrow it for a minute] + -> borrow_card_choice + +=== impossible_clone === ++ [Try to scan the badge] + # speaker:player + You position your Flipper near their badge. + # speaker:npc + You can only capture the UID. This card uses strong encryption - you can't clone it without the master key. + # save_uid_only:{card_name}|{card_uid} + -> uid_saved + ++ [Ask if you can borrow it] + This is your only option. You'll need the physical card. + -> borrow_card_choice +``` + +### New Ink Tags + +#### `# start_mifare_attack:CardName|UID` + +Starts a MIFARE Classic key cracking attack in the background. + +```javascript +case 'start_mifare_attack': + if (param) { + const [cardName, uid] = param.split('|'); + + // Check for Flipper + const cloner = window.inventory.items.find(item => + item?.scenarioData?.type === 'rfid_cloner' + ); + + if (!cloner) { + result.message = '⚠️ Need RFID cloner'; + break; + } + + // Check firmware supports MIFARE + if (!cloner.scenarioData.firmware.protocols.includes('MIFARE_Classic')) { + result.message = '⚠️ Firmware upgrade needed for MIFARE attacks'; + break; + } + + // Start background attack + startMIFAREAttack(cardName, uid, cloner); + result.success = true; + result.message = `🔓 Started Darkside attack on ${cardName}`; + } + break; +``` + +#### `# check_attack_complete:CardUID` + +Check if background attack finished (can use in conditional choice): + +```ink +=== wait_for_crack === +# speaker:npc +So anyway, as I was saying about the weekend plans... + +{check_attack_complete(card_uid): + + [Your Flipper vibrates - attack complete!] + # speaker:player + (Your Flipper successfully cracked the keys!) + # clone_mifare:{card_name}|{card_uid} + -> cloned + - else: + + [Continue chatting] + -> keep_waiting +} +``` + +#### `# clone_mifare:CardName|UID` + +Clone a MIFARE card (requires keys to be cracked first): + +```javascript +case 'clone_mifare': + if (param) { + const [cardName, uid] = param.split('|'); + + const cloner = window.inventory.items.find(item => + item?.scenarioData?.type === 'rfid_cloner' + ); + + // Check if we have the keys + const attack = cloner?.scenarioData?.activeAttacks?.[`uid_${uid}`]; + + if (!attack || attack.progress < 100) { + result.message = '⚠️ Keys not yet cracked'; + break; + } + + // Launch RFID minigame in clone mode with MIFARE data + window.pendingConversationReturn = { + npcId: window.currentConversationNPCId, + type: window.currentConversationMinigameType || 'person-chat' + }; + + window.startRFIDMinigame(null, null, { + mode: 'clone', + protocol: 'MIFARE_Classic', + cardToClone: { + name: cardName, + rfid_protocol: 'MIFARE_Classic', + rfid_data: { + uid: uid, + sectors: attack.foundKeys + }, + type: 'keycard', + key_id: `cloned_${uid.toLowerCase()}` + } + }); + } + break; +``` + +#### `# save_uid_only:CardName|UID` + +Save only UID for DESFire cards (can't clone full card): + +```javascript +case 'save_uid_only': + if (param) { + const [cardName, uid] = param.split('|'); + + window.pendingConversationReturn = { + npcId: window.currentConversationNPCId, + type: window.currentConversationMinigameType || 'person-chat' + }; + + window.startRFIDMinigame(null, null, { + mode: 'clone', + protocol: 'MIFARE_DESFire', + uidOnly: true, + cardToClone: { + name: cardName + " (UID Only)", + rfid_protocol: 'MIFARE_DESFire', + rfid_data: { + uid: uid, + masterKeyKnown: false + }, + type: 'keycard', + key_id: `uid_${uid.toLowerCase()}`, + observations: "⚠️ UID only - may not work on secure readers" + } + }); + } + break; +``` + +## Implementation Phases + +This feature can be implemented incrementally: + +### Phase 1: Protocol Data Model (Foundation) +1. Add RFID_PROTOCOLS constant +2. Update card data structure to support rfid_data +3. Update cloner to support firmware capabilities +4. Backward compatibility for existing EM4100 cards + +### Phase 2: Protocol Detection & Display +1. Add protocol detection logic in rfid-data.js +2. Create protocol info UI screen +3. Update card data display to show protocol-specific fields +4. Color coding by security level + +### Phase 3: MIFARE Classic Support +1. Implement key attack minigame screens +2. Add background attack system +3. Add dictionary attack (common keys) +4. Partial clone support (some sectors) + +### Phase 4: MIFARE DESFire Support +1. UID-only save functionality +2. Emulation with warning messages +3. Physical theft/social engineering paths + +### Phase 5: Ink Integration +1. Extend syncItemsToInk for protocol variables +2. Implement new Ink tags +3. Add conditional attack options +4. Create example scenarios + +### Phase 6: HID Prox Support +1. Add HID-specific data format +2. Facility code + card number extraction +3. UI for HID cards + +## Testing Plan + +### Unit Tests +- Protocol detection from card data +- Capability checks per protocol +- Key cracking simulation +- UID extraction + +### Integration Tests +- Clone EM4100 (should work instantly) +- Clone HID Prox (should work instantly) +- Attempt clone MIFARE Classic without keys (should fail/offer attack) +- Attack MIFARE Classic (should eventually succeed) +- Attempt clone DESFire (should only get UID) +- Emulate UID-only DESFire against simple reader (should work) +- Emulate UID-only DESFire against secure reader (should fail) + +### Scenario Tests +Create test scenarios for each protocol: +- `test-rfid-em4100.json` (current) +- `test-rfid-hid-prox.json` +- `test-rfid-mifare-classic.json` +- `test-rfid-mifare-desfire.json` + +## Backward Compatibility + +Existing EM4100 cards continue to work: + +```javascript +// Old format (still works) +{ + type: "keycard", + rfid_hex: "01AB34CD56", + rfid_facility: 1, + rfid_card_number: 43981, + rfid_protocol: "EM4100" +} + +// Automatically migrated to: +{ + type: "keycard", + rfid_protocol: "EM4100", + rfid_data: { + hex: "01AB34CD56", + facility: 1, + cardNumber: 43981 + } +} +``` + +Migration happens transparently when cards are loaded. + +## Performance Considerations + +- Protocol detection: Instant (client-side lookup) +- Key attacks: Simulated with setTimeout (no real crypto) +- Background attacks: Store in gameState, check on game loop +- No actual network calls or heavy computation diff --git a/planning_notes/rfid_keycard/protocols_and_interactions/02_IMPLEMENTATION_PLAN.md b/planning_notes/rfid_keycard/protocols_and_interactions/02_IMPLEMENTATION_PLAN.md new file mode 100644 index 0000000..eee8116 --- /dev/null +++ b/planning_notes/rfid_keycard/protocols_and_interactions/02_IMPLEMENTATION_PLAN.md @@ -0,0 +1,1339 @@ +# RFID Protocols - Implementation Plan + +## File Changes Overview + +``` +js/ +├── minigames/ +│ ├── rfid/ +│ │ ├── rfid-protocols.js [NEW] Protocol definitions & capabilities +│ │ ├── rfid-minigame.js [MODIFY] Add protocol-specific flows +│ │ ├── rfid-data.js [MODIFY] Protocol detection & data handling +│ │ ├── rfid-ui.js [MODIFY] Protocol-specific UI screens +│ │ ├── rfid-attacks.js [NEW] MIFARE key attack system +│ │ └── rfid-animations.js [MODIFY] Add attack progress animations +│ │ +│ ├── helpers/ +│ │ └── chat-helpers.js [MODIFY] Add MIFARE attack tags +│ │ +│ └── person-chat/ +│ └── person-chat-conversation.js [MODIFY] Sync card protocols to Ink +│ +├── systems/ +│ └── game-state.js [MODIFY] Track background attacks +│ +└── core/ + └── game.js [MODIFY] Load protocol assets + +scenarios/ +├── test-rfid-em4100.json [EXISTS] Current test +├── test-rfid-hid-prox.json [NEW] HID Prox test +├── test-rfid-mifare.json [NEW] MIFARE Classic test +└── test-rfid-desfire.json [NEW] MIFARE DESFire test + +assets/ +└── icons/ + ├── protocol-low.png [NEW] Low security indicator + ├── protocol-medium.png [NEW] Medium security indicator + └── protocol-high.png [NEW] High security indicator +``` + +## Phase 1: Protocol Data Model (Foundation) + +**Estimated Time: 3 hours** + +### Task 1.1: Create Protocol Definitions Module (1h) + +**File**: `js/minigames/rfid/rfid-protocols.js` (NEW) + +```javascript +/** + * RFID Protocol Definitions + * + * Real-world RFID protocols with their security characteristics + * and Flipper Zero capabilities + */ + +export const RFID_PROTOCOLS = { + 'EM4100': { + name: 'EM-Micro EM4100', + frequency: '125kHz', + security: 'low', + readOnly: true, + capabilities: { + read: true, + clone: true, + write: false, + emulate: true, + bruteforce: false + }, + description: 'Legacy low-frequency card. Read-only, easily cloned.', + vulnerabilities: ['Clone attack', 'Replay attack'], + hexLength: 10, + color: '#FF6B6B' + }, + + 'HID_Prox': { + name: 'HID Prox II', + frequency: '125kHz', + security: 'medium-low', + readOnly: true, + capabilities: { + read: true, + clone: true, + write: false, + emulate: true, + bruteforce: false + }, + description: 'Common corporate badge. Read-only, proprietary format.', + vulnerabilities: ['Clone attack', 'Replay attack'], + hexLength: 12, + color: '#FFA500' + }, + + 'MIFARE_Classic': { + name: 'MIFARE Classic 1K', + frequency: '13.56MHz', + security: 'medium', + readOnly: false, + capabilities: { + read: 'with-keys', + clone: 'with-keys', + write: 'with-keys', + emulate: true, + bruteforce: true + }, + description: 'Encrypted NFC card. Requires authentication keys.', + vulnerabilities: ['Darkside attack', 'Nested attack', 'Dictionary attack'], + sectors: 16, + keysPerSector: 2, + hexLength: 8, + color: '#4ECDC4' + }, + + 'MIFARE_DESFire': { + name: 'MIFARE DESFire EV2', + frequency: '13.56MHz', + security: 'high', + readOnly: false, + capabilities: { + read: false, + clone: false, + write: false, + emulate: 'uid-only', + bruteforce: false + }, + description: 'High-security encrypted NFC. Nearly impossible to clone.', + vulnerabilities: ['Physical theft only'], + hexLength: 14, + color: '#95E1D3' + } +}; + +/** + * Get protocol info + */ +export function getProtocolInfo(protocolName) { + return RFID_PROTOCOLS[protocolName] || RFID_PROTOCOLS['EM4100']; +} + +/** + * Check if protocol supports operation + */ +export function protocolSupports(protocolName, operation) { + const protocol = getProtocolInfo(protocolName); + const capability = protocol.capabilities[operation]; + + if (typeof capability === 'boolean') return capability; + if (typeof capability === 'string') return capability; // 'with-keys', 'uid-only' + return false; +} + +/** + * Get common default MIFARE keys (for dictionary attack) + */ +export const MIFARE_COMMON_KEYS = [ + 'FFFFFFFFFFFF', // Factory default + '000000000000', // Common blank + 'A0A1A2A3A4A5', // Common transport key + 'D3F7D3F7D3F7', // Common backdoor + '123456789ABC', // Weak key + 'AABBCCDDEEFF', // Weak key + 'B0B1B2B3B4B5', // Another common + '4D3A99C351DD', // Hotel systems + '1A982C7E459A', // Transit systems + '714C5C886E97', // Transit systems + '587EE5F9350F', // Various systems + 'A0478CC39091', // Various systems + '533CB6C723F6', // Various systems + '8FD0A4F256E9' // Various systems +]; + +/** + * Generate random MIFARE keys (for scenarios) + */ +export function generateMIFAREKeys(numSectors = 16) { + const keys = {}; + for (let i = 0; i < numSectors; i++) { + // Sector 0 often has default key + if (i === 0) { + keys[0] = { keyA: 'FFFFFFFFFFFF', keyB: 'FFFFFFFFFFFF' }; + } else { + keys[i] = { + keyA: Array.from({ length: 12 }, () => + Math.floor(Math.random() * 16).toString(16).toUpperCase() + ).join(''), + keyB: Array.from({ length: 12 }, () => + Math.floor(Math.random() * 16).toString(16).toUpperCase() + ).join('') + }; + } + } + return keys; +} + +/** + * Validate card data for protocol + */ +export function validateCardData(cardData) { + const protocol = getProtocolInfo(cardData.rfid_protocol); + + if (!cardData.rfid_data) { + return { valid: false, error: 'Missing rfid_data' }; + } + + const data = cardData.rfid_data; + + switch (cardData.rfid_protocol) { + case 'EM4100': + case 'HID_Prox': + if (!data.hex || data.hex.length !== protocol.hexLength) { + return { valid: false, error: `Invalid hex length for ${protocol.name}` }; + } + break; + + case 'MIFARE_Classic': + if (!data.uid || data.uid.length !== 8) { + return { valid: false, error: 'Invalid UID for MIFARE Classic' }; + } + break; + + case 'MIFARE_DESFire': + if (!data.uid || data.uid.length !== 14) { + return { valid: false, error: 'Invalid UID for MIFARE DESFire' }; + } + break; + } + + return { valid: true }; +} + +export default RFID_PROTOCOLS; +``` + +### Task 1.2: Update Card Data Migration (0.5h) + +**File**: `js/minigames/rfid/rfid-data.js` + +Add migration function to convert old format to new: + +```javascript +import { getProtocolInfo } from './rfid-protocols.js'; + +/** + * Migrate old card format to new protocol-aware format + */ +function migrateCardData(cardData) { + // Already migrated + if (cardData.rfid_data) return cardData; + + const protocol = cardData.rfid_protocol || 'EM4100'; + + // Migrate based on protocol + if (protocol === 'EM4100' || protocol === 'HID_Prox') { + return { + ...cardData, + rfid_data: { + hex: cardData.rfid_hex, + facility: cardData.rfid_facility, + cardNumber: cardData.rfid_card_number, + isClone: false, + cloneQuality: 100 + } + }; + } + + return cardData; +} + +// Apply migration in saveCardToCloner and other methods +export class RFIDDataManager { + saveCardToCloner(cardData) { + // Migrate if needed + cardData = migrateCardData(cardData); + + // ... rest of existing code + } + + // ... other methods +} +``` + +### Task 1.3: Update Cloner Firmware Structure (0.5h) + +**File**: Scenario JSON files + +Add firmware capabilities to cloner items: + +```json +{ + "type": "rfid_cloner", + "name": "Flipper Zero", + "firmware": { + "version": "1.0", + "protocols": ["EM4100", "HID_Prox"], + "attacks": ["read", "clone", "emulate"] + }, + "saved_cards": [], + "activeAttacks": {}, + "takeable": true +} +``` + +### Task 1.4: Backward Compatibility Tests (1h) + +Test that existing scenarios continue to work: +- Load old EM4100 cards +- Clone old format cards +- Emulate old format cards +- Verify migration happens transparently + +## Phase 2: Protocol Detection & Display + +**Estimated Time: 4 hours** + +### Task 2.1: Protocol Detection in rfid-data.js (1h) + +```javascript +/** + * Detect protocol from card data + */ +detectProtocol(cardData) { + // Explicit protocol specified + if (cardData.rfid_protocol) { + return cardData.rfid_protocol; + } + + // Auto-detect from data structure + if (cardData.rfid_data) { + const data = cardData.rfid_data; + + // Check UID length + if (data.uid) { + if (data.uid.length === 14) return 'MIFARE_DESFire'; + if (data.uid.length === 8) return 'MIFARE_Classic'; + } + + // Check hex length + if (data.hex) { + if (data.hex.length === 12) return 'HID_Prox'; + if (data.hex.length === 10) return 'EM4100'; + } + } + + // Legacy detection + if (cardData.rfid_hex) { + if (cardData.rfid_hex.length === 12) return 'HID_Prox'; + return 'EM4100'; + } + + return 'EM4100'; // Default +} + +/** + * Get card display data based on protocol + */ +getCardDisplayData(cardData) { + const protocol = this.detectProtocol(cardData); + const protocolInfo = getProtocolInfo(protocol); + const data = cardData.rfid_data || {}; + + const displayData = { + protocol: protocol, + protocolName: protocolInfo.name, + frequency: protocolInfo.frequency, + security: protocolInfo.security, + color: protocolInfo.color, + fields: [] + }; + + switch (protocol) { + case 'EM4100': + case 'HID_Prox': + displayData.fields = [ + { label: 'HEX', value: this.formatHex(data.hex) }, + { label: 'Facility', value: data.facility }, + { label: 'Card', value: data.cardNumber }, + { label: 'DEZ 8', value: this.toDEZ8(data.hex) } + ]; + break; + + case 'MIFARE_Classic': + const keysKnown = data.sectors ? Object.keys(data.sectors).length : 0; + displayData.fields = [ + { label: 'UID', value: this.formatHex(data.uid) }, + { label: 'Type', value: '1K (16 sectors)' }, + { label: 'Keys Known', value: `${keysKnown}/16` }, + { label: 'Readable', value: keysKnown === 16 ? 'Yes' : 'Partial' }, + { label: 'Clonable', value: keysKnown > 0 ? 'Partial' : 'No' } + ]; + break; + + case 'MIFARE_DESFire': + displayData.fields = [ + { label: 'UID', value: this.formatHex(data.uid) }, + { label: 'Type', value: 'EV2' }, + { label: 'Encryption', value: '3DES/AES' }, + { label: 'Clonable', value: 'UID Only' } + ]; + break; + } + + return displayData; +} +``` + +### Task 2.2: Protocol Info UI Screen (1.5h) + +**File**: `js/minigames/rfid/rfid-ui.js` + +Add new screen type: + +```javascript +/** + * Show protocol information screen + */ +showProtocolInfo(cardData) { + const screen = this.getScreen(); + screen.innerHTML = ''; + + const displayData = this.dataManager.getCardDisplayData(cardData); + + // Breadcrumb + const breadcrumb = document.createElement('div'); + breadcrumb.className = 'flipper-breadcrumb'; + breadcrumb.textContent = 'RFID > Info'; + screen.appendChild(breadcrumb); + + // Protocol header with security color + const header = document.createElement('div'); + header.className = 'flipper-protocol-header'; + header.style.borderLeft = `4px solid ${displayData.color}`; + header.innerHTML = ` +
${displayData.protocolName}
+
+ ${displayData.frequency} + + ${displayData.security.toUpperCase()} Security + +
+ `; + screen.appendChild(header); + + // Card data fields + const dataDiv = document.createElement('div'); + dataDiv.className = 'flipper-card-data'; + displayData.fields.forEach(field => { + const fieldDiv = document.createElement('div'); + fieldDiv.textContent = `${field.label}: ${field.value}`; + dataDiv.appendChild(fieldDiv); + }); + screen.appendChild(dataDiv); + + // Capabilities/actions based on protocol + this.showProtocolActions(cardData, displayData); +} + +/** + * Show available actions based on protocol and card state + */ +showProtocolActions(cardData, displayData) { + const protocol = displayData.protocol; + const screen = this.getScreen(); + + const actions = document.createElement('div'); + actions.className = 'flipper-menu'; + + switch (protocol) { + case 'EM4100': + case 'HID_Prox': + // Simple read/clone + const readBtn = document.createElement('div'); + readBtn.className = 'flipper-menu-item'; + readBtn.textContent = '> Read & Clone'; + readBtn.addEventListener('click', () => this.showReadingScreen()); + actions.appendChild(readBtn); + break; + + case 'MIFARE_Classic': + // Check if we have keys + const keysKnown = cardData.rfid_data?.sectors ? + Object.keys(cardData.rfid_data.sectors).length : 0; + + if (keysKnown === 0) { + // No keys - offer attacks + const darksideBtn = document.createElement('div'); + darksideBtn.className = 'flipper-menu-item'; + darksideBtn.textContent = '> Darkside Attack'; + darksideBtn.addEventListener('click', () => + this.minigame.startKeyAttack('darkside', cardData)); + actions.appendChild(darksideBtn); + + const dictBtn = document.createElement('div'); + dictBtn.className = 'flipper-menu-item'; + dictBtn.textContent = ' Dictionary Attack'; + dictBtn.addEventListener('click', () => + this.minigame.startKeyAttack('dictionary', cardData)); + actions.appendChild(dictBtn); + } else if (keysKnown < 16) { + // Some keys - offer nested attack + const nestedBtn = document.createElement('div'); + nestedBtn.className = 'flipper-menu-item'; + nestedBtn.textContent = '> Nested Attack (crack remaining)'; + nestedBtn.addEventListener('click', () => + this.minigame.startKeyAttack('nested', cardData)); + actions.appendChild(nestedBtn); + + const readBtn = document.createElement('div'); + readBtn.className = 'flipper-menu-item'; + readBtn.textContent = ' Read (partial)'; + readBtn.addEventListener('click', () => + this.showCardDataScreen(cardData)); + actions.appendChild(readBtn); + } else { + // All keys - can fully read + const readBtn = document.createElement('div'); + readBtn.className = 'flipper-menu-item'; + readBtn.textContent = '> Read & Clone'; + readBtn.addEventListener('click', () => + this.showCardDataScreen(cardData)); + actions.appendChild(readBtn); + } + break; + + case 'MIFARE_DESFire': + // Can only save UID + const infoDiv = document.createElement('div'); + infoDiv.className = 'flipper-info'; + infoDiv.textContent = 'High security - cannot clone'; + screen.appendChild(infoDiv); + + const uidBtn = document.createElement('div'); + uidBtn.className = 'flipper-menu-item'; + uidBtn.textContent = '> Save UID Only'; + uidBtn.addEventListener('click', () => + this.showCardDataScreen(cardData)); + actions.appendChild(uidBtn); + break; + } + + // Cancel button + const cancelBtn = document.createElement('div'); + cancelBtn.className = 'flipper-button-back'; + cancelBtn.textContent = '← Cancel'; + cancelBtn.addEventListener('click', () => this.minigame.complete(false)); + actions.appendChild(cancelBtn); + + screen.appendChild(actions); +} +``` + +### Task 2.3: Update Card Data Display (1h) + +**File**: `js/minigames/rfid/rfid-ui.js` + +Modify `showCardDataScreen()` to use protocol-aware display: + +```javascript +showCardDataScreen(cardData) { + const screen = this.getScreen(); + screen.innerHTML = ''; + + const displayData = this.dataManager.getCardDisplayData(cardData); + + // Breadcrumb + const breadcrumb = document.createElement('div'); + breadcrumb.className = 'flipper-breadcrumb'; + breadcrumb.textContent = 'RFID > Read'; + screen.appendChild(breadcrumb); + + // Protocol name with color + const protocol = document.createElement('div'); + protocol.className = 'flipper-protocol-name'; + protocol.style.color = displayData.color; + protocol.textContent = displayData.protocolName; + screen.appendChild(protocol); + + // Card data (protocol-specific fields) + const data = document.createElement('div'); + data.className = 'flipper-card-data'; + displayData.fields.forEach(field => { + const fieldDiv = document.createElement('div'); + fieldDiv.innerHTML = `${field.label}: ${field.value}`; + data.appendChild(fieldDiv); + }); + screen.appendChild(data); + + // Add warning for DESFire UID-only + if (displayData.protocol === 'MIFARE_DESFire') { + const warning = document.createElement('div'); + warning.className = 'flipper-warning'; + warning.textContent = '⚠️ UID only - may not work on secure readers'; + screen.appendChild(warning); + } + + // Buttons + const buttons = document.createElement('div'); + buttons.className = 'flipper-buttons'; + + const saveBtn = document.createElement('button'); + saveBtn.className = 'flipper-button'; + saveBtn.textContent = 'Save'; + saveBtn.addEventListener('click', () => this.minigame.handleSaveCard(cardData)); + + const cancelBtn = document.createElement('button'); + cancelBtn.className = 'flipper-button flipper-button-secondary'; + cancelBtn.textContent = 'Cancel'; + cancelBtn.addEventListener('click', () => this.minigame.complete(false)); + + buttons.appendChild(saveBtn); + buttons.appendChild(cancelBtn); + screen.appendChild(buttons); +} +``` + +### Task 2.4: Add CSS for Protocol Display (0.5h) + +**File**: `css/rfid-minigame.css` + +```css +/* Protocol Header */ +.flipper-protocol-header { + background: rgba(0, 0, 0, 0.3); + padding: 15px; + padding-left: 15px; + border-radius: 8px; + margin: 10px 0; +} + +.protocol-name { + font-size: 16px; + font-weight: bold; + color: white; + margin-bottom: 5px; +} + +.protocol-meta { + font-size: 12px; + color: #888; + display: flex; + justify-content: space-between; +} + +.security-low { color: #FF6B6B; } +.security-medium-low { color: #FFA500; } +.security-medium { color: #4ECDC4; } +.security-high { color: #95E1D3; } + +.flipper-protocol-name { + font-size: 14px; + font-weight: bold; + text-align: center; + margin: 10px 0; +} + +.flipper-warning { + background: rgba(255, 165, 0, 0.2); + border-left: 3px solid #FFA500; + padding: 10px; + margin: 10px 0; + color: #FFA500; + font-size: 12px; +} +``` + +## Phase 3: MIFARE Classic Support + +**Estimated Time: 6 hours** + +### Task 3.1: Create Attack System Module (2h) + +**File**: `js/minigames/rfid/rfid-attacks.js` (NEW) + +```javascript +/** + * MIFARE Classic Key Attack System + * + * Simulates realistic key cracking attacks: + * - Darkside: Crack keys from scratch (30 sec) + * - Nested: Crack remaining keys if you have one (10 sec) + * - Dictionary: Try common keys (instant) + */ + +import { MIFARE_COMMON_KEYS } from './rfid-protocols.js'; + +export class MIFAREAttackManager { + constructor() { + this.activeAttacks = []; + } + + /** + * Start dictionary attack (instant check) + */ + dictionaryAttack(uid, existingKeys = {}) { + console.log('🔓 Starting dictionary attack on', uid); + + const foundKeys = { ...existingKeys }; + let newKeysFound = 0; + + // Try common keys on all sectors + for (let sector = 0; sector < 16; sector++) { + if (foundKeys[sector]) continue; // Already have keys + + // Try each common key + for (const commonKey of MIFARE_COMMON_KEYS) { + // Simulate 10% chance each common key works + if (Math.random() < 0.1) { + foundKeys[sector] = { + keyA: commonKey, + keyB: commonKey + }; + newKeysFound++; + break; + } + } + } + + return { + success: newKeysFound > 0, + foundKeys: foundKeys, + newKeysFound: newKeysFound, + message: newKeysFound > 0 ? + `Found ${newKeysFound} sector(s) using common keys` : + 'No common keys found' + }; + } + + /** + * Start Darkside attack (progressive, takes time) + */ + startDarksideAttack(cardName, uid, progressCallback) { + console.log('🔓 Starting Darkside attack on', uid); + + return new Promise((resolve) => { + const attack = { + type: 'darkside', + uid: uid, + cardName: cardName, + startTime: Date.now(), + foundKeys: {}, + currentSector: 0, + totalSectors: 16 + }; + + this.activeAttacks.push(attack); + + // Simulate progressive key cracking + // Real Darkside takes ~30 seconds, we'll simulate with progress updates + const duration = 30000; // 30 seconds + const updateInterval = 500; // Update every 500ms + + let elapsed = 0; + const interval = setInterval(() => { + elapsed += updateInterval; + const progress = Math.min(100, (elapsed / duration) * 100); + + // Update current sector + attack.currentSector = Math.floor((progress / 100) * 16); + + // Add found keys as we progress + if (!attack.foundKeys[attack.currentSector] && + attack.currentSector > 0) { + attack.foundKeys[attack.currentSector - 1] = { + keyA: this.generateRandomKey(), + keyB: this.generateRandomKey() + }; + } + + // Callback with progress + if (progressCallback) { + progressCallback({ + progress: progress, + currentSector: attack.currentSector, + foundKeys: attack.foundKeys + }); + } + + // Complete + if (progress >= 100) { + clearInterval(interval); + + // Add all remaining keys + for (let i = 0; i < 16; i++) { + if (!attack.foundKeys[i]) { + attack.foundKeys[i] = { + keyA: this.generateRandomKey(), + keyB: this.generateRandomKey() + }; + } + } + + // Remove from active attacks + this.activeAttacks = this.activeAttacks.filter(a => a !== attack); + + resolve({ + success: true, + foundKeys: attack.foundKeys, + message: 'All keys cracked successfully' + }); + } + }, updateInterval); + }); + } + + /** + * Start Nested attack (faster, requires at least one known key) + */ + startNestedAttack(uid, knownKeys, progressCallback) { + console.log('🔓 Starting Nested attack on', uid); + + if (Object.keys(knownKeys).length === 0) { + return Promise.reject(new Error('Need at least one known key')); + } + + return new Promise((resolve) => { + const attack = { + type: 'nested', + uid: uid, + foundKeys: { ...knownKeys }, + startTime: Date.now() + }; + + this.activeAttacks.push(attack); + + // Nested attack is faster: ~10 seconds + const duration = 10000; + const updateInterval = 500; + + let elapsed = 0; + const sectorsToFind = 16 - Object.keys(knownKeys).length; + let sectorsFound = 0; + + const interval = setInterval(() => { + elapsed += updateInterval; + const progress = Math.min(100, (elapsed / duration) * 100); + + // Add found keys progressively + const expectedFound = Math.floor((progress / 100) * sectorsToFind); + while (sectorsFound < expectedFound) { + // Find next missing sector + for (let i = 0; i < 16; i++) { + if (!attack.foundKeys[i]) { + attack.foundKeys[i] = { + keyA: this.generateRandomKey(), + keyB: this.generateRandomKey() + }; + sectorsFound++; + break; + } + } + } + + if (progressCallback) { + progressCallback({ + progress: progress, + foundKeys: attack.foundKeys + }); + } + + if (progress >= 100) { + clearInterval(interval); + this.activeAttacks = this.activeAttacks.filter(a => a !== attack); + + resolve({ + success: true, + foundKeys: attack.foundKeys, + message: `Cracked ${sectorsToFind} remaining sectors` + }); + } + }, updateInterval); + }); + } + + /** + * Generate random MIFARE key (for simulation) + */ + generateRandomKey() { + return Array.from({ length: 12 }, () => + Math.floor(Math.random() * 16).toString(16).toUpperCase() + ).join(''); + } + + /** + * Check if attack is in progress + */ + hasActiveAttack(uid) { + return this.activeAttacks.some(a => a.uid === uid); + } + + /** + * Get attack progress + */ + getAttackProgress(uid) { + return this.activeAttacks.find(a => a.uid === uid); + } + + /** + * Cancel attack + */ + cancelAttack(uid) { + this.activeAttacks = this.activeAttacks.filter(a => a.uid !== uid); + } +} + +// Global instance +window.mifareAttackManager = window.mifareAttackManager || new MIFAREAttackManager(); + +export default MIFAREAttackManager; +``` + +### Task 3.2: Add Attack UI Screens (2h) + +**File**: `js/minigames/rfid/rfid-ui.js` + +```javascript +/** + * Show key attack screen (Darkside/Nested) + */ +showKeyAttackScreen(attackType, cardData) { + const screen = this.getScreen(); + screen.innerHTML = ''; + + // Breadcrumb + const breadcrumb = document.createElement('div'); + breadcrumb.className = 'flipper-breadcrumb'; + breadcrumb.textContent = `RFID > ${attackType} Attack`; + screen.appendChild(breadcrumb); + + // Card info + const cardInfo = document.createElement('div'); + cardInfo.className = 'flipper-card-name'; + cardInfo.textContent = cardData.name; + screen.appendChild(cardInfo); + + const uid = document.createElement('div'); + uid.className = 'flipper-info-dim'; + uid.textContent = `UID: ${cardData.rfid_data.uid}`; + screen.appendChild(uid); + + // Progress container + const progressDiv = document.createElement('div'); + progressDiv.id = 'attack-progress-container'; + screen.appendChild(progressDiv); + + // Keys found list + const keysDiv = document.createElement('div'); + keysDiv.id = 'attack-keys-found'; + keysDiv.className = 'attack-keys-list'; + screen.appendChild(keysDiv); + + // Status message + const status = document.createElement('div'); + status.id = 'attack-status'; + status.className = 'flipper-info'; + status.textContent = 'Don\'t move card...'; + screen.appendChild(status); +} + +/** + * Update attack progress + */ +updateAttackProgress(progressData) { + const progressDiv = document.getElementById('attack-progress-container'); + if (progressDiv && !progressDiv.querySelector('.rfid-progress-container')) { + const container = document.createElement('div'); + container.className = 'rfid-progress-container'; + + const bar = document.createElement('div'); + bar.className = 'rfid-progress-bar'; + bar.id = 'attack-progress-bar'; + + container.appendChild(bar); + progressDiv.appendChild(container); + + const label = document.createElement('div'); + label.className = 'flipper-info'; + label.id = 'attack-progress-label'; + progressDiv.appendChild(label); + } + + const bar = document.getElementById('attack-progress-bar'); + const label = document.getElementById('attack-progress-label'); + + if (bar) { + bar.style.width = `${progressData.progress}%`; + } + + if (label && progressData.currentSector !== undefined) { + label.textContent = `Cracking Sector ${progressData.currentSector}/16...`; + } + + // Update keys found + const keysDiv = document.getElementById('attack-keys-found'); + if (keysDiv && progressData.foundKeys) { + keysDiv.innerHTML = '
Keys Found:
'; + + Object.keys(progressData.foundKeys).forEach(sector => { + const keyLine = document.createElement('div'); + keyLine.className = 'attack-key-item'; + keyLine.textContent = `Sector ${sector}: ${progressData.foundKeys[sector].keyA} ✓`; + keysDiv.appendChild(keyLine); + }); + } +} +``` + +### Task 3.3: Integrate Attacks into rfid-minigame.js (1.5h) + +**File**: `js/minigames/rfid/rfid-minigame.js` + +```javascript +import { MIFAREAttackManager } from './rfid-attacks.js'; + +export class RFIDMinigame extends MinigameScene { + constructor(container, params) { + super(container, params); + + // ... existing code + + // Attack manager + this.attackManager = window.mifareAttackManager; + } + + /** + * Start MIFARE key attack + */ + async startKeyAttack(attackType, cardData) { + console.log(`🔓 Starting ${attackType} attack on`, cardData.name); + + // Show attack UI + this.ui.showKeyAttackScreen(attackType, cardData); + + let result; + + try { + switch (attackType) { + case 'dictionary': + result = this.attackManager.dictionaryAttack( + cardData.rfid_data.uid, + cardData.rfid_data.sectors || {} + ); + + // Show result immediately + if (result.success) { + this.ui.showSuccess(result.message); + cardData.rfid_data.sectors = result.foundKeys; + + setTimeout(() => { + this.ui.showProtocolInfo(cardData); + }, 2000); + } else { + this.ui.showError(result.message); + setTimeout(() => { + this.ui.showProtocolInfo(cardData); + }, 2000); + } + break; + + case 'darkside': + result = await this.attackManager.startDarksideAttack( + cardData.name, + cardData.rfid_data.uid, + (progress) => this.ui.updateAttackProgress(progress) + ); + + // Attack complete + cardData.rfid_data.sectors = result.foundKeys; + this.ui.showSuccess(result.message); + + setTimeout(() => { + this.ui.showCardDataScreen(cardData); + }, 2000); + break; + + case 'nested': + result = await this.attackManager.startNestedAttack( + cardData.rfid_data.uid, + cardData.rfid_data.sectors || {}, + (progress) => this.ui.updateAttackProgress(progress) + ); + + cardData.rfid_data.sectors = result.foundKeys; + this.ui.showSuccess(result.message); + + setTimeout(() => { + this.ui.showCardDataScreen(cardData); + }, 2000); + break; + } + } catch (error) { + console.error('Attack failed:', error); + this.ui.showError(error.message); + + setTimeout(() => { + this.ui.showProtocolInfo(cardData); + }, 2000); + } + } +} +``` + +### Task 3.4: Add Attack CSS (0.5h) + +**File**: `css/rfid-minigame.css` + +```css +/* Attack Progress */ +.attack-keys-list { + background: rgba(0, 0, 0, 0.3); + padding: 10px; + border-radius: 5px; + margin: 10px 0; + max-height: 150px; + overflow-y: auto; + font-size: 11px; +} + +.attack-key-item { + padding: 3px 0; + color: #00FF00; +} + +#attack-progress-label { + margin-top: 10px; + font-size: 12px; +} +``` + +## Phase 4: Ink Integration + +**Estimated Time: 3 hours** + +### Task 4.1: Extend syncItemsToInk for Protocols (1h) + +**File**: `js/minigames/person-chat/person-chat-conversation.js` + +```javascript +import { getProtocolInfo } from '../rfid/rfid-protocols.js'; + +/** + * Sync card protocol info to Ink variables + */ +syncCardProtocolsToInk() { + if (!this.inkEngine || !this.npc || !this.npc.itemsHeld) return; + + // Find keycards + const keycards = this.npc.itemsHeld.filter(item => item.type === 'keycard'); + + keycards.forEach((card, index) => { + const protocol = card.rfid_protocol || 'EM4100'; + const protocolInfo = getProtocolInfo(protocol); + const prefix = index === 0 ? 'card' : `card${index + 1}`; + + try { + // Set protocol info + this.inkEngine.setVariable(`${prefix}_protocol`, protocol); + this.inkEngine.setVariable(`${prefix}_name`, card.name || 'Card'); + this.inkEngine.setVariable(`${prefix}_security`, protocolInfo.security); + this.inkEngine.setVariable(`${prefix}_clonable`, + protocolInfo.capabilities.clone === true); + + // Set hex/UID based on protocol + if (card.rfid_data) { + if (card.rfid_data.hex) { + this.inkEngine.setVariable(`${prefix}_hex`, card.rfid_data.hex); + } + if (card.rfid_data.uid) { + this.inkEngine.setVariable(`${prefix}_uid`, card.rfid_data.uid); + } + } + + console.log(`✅ Synced ${prefix} protocol: ${protocol}`); + } catch (err) { + console.warn(`⚠️ Could not sync card protocol:`, err.message); + } + }); +} + +// Call in setupExternalFunctions() +setupExternalFunctions() { + // ... existing code + + this.syncItemsToInk(); + this.syncCardProtocolsToInk(); // NEW +} +``` + +### Task 4.2: Add MIFARE Attack Tags (1.5h) + +**File**: `js/minigames/helpers/chat-helpers.js` + +```javascript +case 'start_mifare_attack': + if (param) { + const [attackType, cardName, uid] = param.split('|').map(s => s.trim()); + + const cloner = window.inventory.items.find(item => + item?.scenarioData?.type === 'rfid_cloner' + ); + + if (!cloner) { + result.message = '⚠️ Need RFID cloner'; + break; + } + + // Check firmware supports MIFARE + if (!cloner.scenarioData.firmware?.protocols?.includes('MIFARE_Classic')) { + result.message = '⚠️ Firmware upgrade needed for MIFARE'; + break; + } + + // Set pending conversation return + window.pendingConversationReturn = { + npcId: window.currentConversationNPCId, + type: window.currentConversationMinigameType || 'person-chat' + }; + + // Start attack minigame + if (window.startRFIDMinigame) { + window.startRFIDMinigame(null, null, { + mode: 'attack', + attackType: attackType, + cardToAttack: { + name: cardName, + rfid_protocol: 'MIFARE_Classic', + rfid_data: { + uid: uid, + sectors: {} + } + } + }); + result.success = true; + } + } + break; + +case 'save_uid_only': + if (param) { + const [cardName, uid] = param.split('|').map(s => s.trim()); + + window.pendingConversationReturn = { + npcId: window.currentConversationNPCId, + type: window.currentConversationMinigameType || 'person-chat' + }; + + if (window.startRFIDMinigame) { + window.startRFIDMinigame(null, null, { + mode: 'clone', + protocol: 'MIFARE_DESFire', + uidOnly: true, + cardToClone: { + name: `${cardName} (UID Only)`, + rfid_protocol: 'MIFARE_DESFire', + rfid_data: { + uid: uid, + masterKeyKnown: false + }, + type: 'keycard', + key_id: `uid_${uid.toLowerCase()}`, + observations: '⚠️ UID only - may not work on secure readers' + } + }); + result.success = true; + } + } + break; +``` + +### Task 4.3: Update rfid-minigame.js for Attack Mode (0.5h) + +**File**: `js/minigames/rfid/rfid-minigame.js` + +```javascript +init() { + super.init(); + + // ... existing code + + // Create appropriate interface + if (this.mode === 'unlock') { + this.ui.createUnlockInterface(); + } else if (this.mode === 'clone') { + this.ui.createCloneInterface(); + } else if (this.mode === 'attack') { + // MIFARE attack mode + this.ui.createAttackInterface(); + } +} + +// In UI +createAttackInterface() { + this.clear(); + + const flipper = this.createFlipperFrame(); + this.container.appendChild(flipper); + + // Immediately start the attack + if (this.minigame.params.attackType && this.minigame.params.cardToAttack) { + this.minigame.startKeyAttack( + this.minigame.params.attackType, + this.minigame.params.cardToAttack + ); + } +} +``` + +## Phase 5: Testing & Scenarios + +**Estimated Time: 3 hours** + +### Task 5.1: Create Test Scenarios (2h) + +Create test scenarios for each protocol type - detailed scenario JSONs would go here. + +### Task 5.2: Integration Testing (1h) + +Test all flows: +- EM4100 clone (should work instantly) +- HID Prox clone (should work instantly) +- MIFARE Classic without keys (show attack options) +- MIFARE Classic with dictionary attack +- MIFARE Classic with Darkside attack +- MIFARE DESFire (UID only) + +## Summary + +**Total Estimated Time: 19 hours** + +- Phase 1: Protocol Data Model - 3h +- Phase 2: Protocol Detection & Display - 4h +- Phase 3: MIFARE Classic Support - 6h +- Phase 4: Ink Integration - 3h +- Phase 5: Testing & Scenarios - 3h + +**Key Deliverables:** +- Multi-protocol RFID system with realistic constraints +- MIFARE key attack minigames +- Protocol-aware UI with security indicators +- Ink integration for conditional interactions +- Test scenarios for all protocol types diff --git a/planning_notes/rfid_keycard/protocols_and_interactions_review/CRITICAL_REVIEW.md b/planning_notes/rfid_keycard/protocols_and_interactions_review/CRITICAL_REVIEW.md new file mode 100644 index 0000000..92d0a90 --- /dev/null +++ b/planning_notes/rfid_keycard/protocols_and_interactions_review/CRITICAL_REVIEW.md @@ -0,0 +1,526 @@ +# RFID Protocols Implementation Plan - Critical Review + +**Review Date**: Current Session +**Reviewer**: Claude (Self-Review) +**Status**: Pre-Implementation Analysis + +## Executive Summary + +The implementation plan is **comprehensive and technically sound**, but has several areas that can be **simplified and improved** for better development efficiency and gameplay value. This review identifies 12 key improvements organized by priority. + +## High Priority Issues + +### Issue #1: HID Prox Adds Minimal Gameplay Value + +**Problem**: HID Prox is nearly identical to EM4100 from a gameplay perspective: +- Both are 125kHz read-only cards +- Both clone instantly +- Only difference is hex length (10 vs 12 chars) +- Both have same vulnerabilities and capabilities + +**Impact**: Development time spent on HID Prox doesn't add meaningful gameplay variety. + +**Recommendation**: **Remove HID Prox from initial implementation**. +- Focus on three distinct protocols: EM4100 (easy), MIFARE Classic (medium), MIFARE DESFire (hard) +- Can add HID Prox later if needed (it's trivial to add) +- Saves ~2 hours of implementation and testing time + +**Updated Protocol Set**: +```javascript +const RFID_PROTOCOLS = { + 'EM4100': 'low', // Always works + 'MIFARE_Classic': 'medium', // Requires key attacks + 'MIFARE_DESFire': 'high' // UID only, physical theft needed +}; +``` + +--- + +### Issue #2: Attack Mode vs Clone Mode Confusion + +**Problem**: Plan introduces separate "attack" mode: +```javascript +if (this.mode === 'attack') { + this.ui.createAttackInterface(); +} +``` + +This creates confusion: +- What's the difference between attack mode and clone mode? +- After attack succeeds, do you still need to clone? +- Two separate code paths for similar functionality + +**Recommendation**: **Merge attack into clone mode**. + +**Better Flow**: +``` +Clone Mode Start +├─ Detect protocol +├─ EM4100? → Read & Clone instantly +├─ MIFARE Classic? +│ ├─ Has keys? → Read & Clone +│ └─ No keys? → Show attack options → Run attack → Then clone +└─ MIFARE DESFire? → Save UID only +``` + +**Implementation**: +```javascript +// In clone mode +if (this.mode === 'clone') { + const protocol = this.detectProtocol(this.cardToClone); + + if (protocol === 'MIFARE_Classic') { + const hasKeys = this.hasAllKeys(this.cardToClone); + if (!hasKeys) { + // Show protocol info with attack options + this.ui.showProtocolInfo(this.cardToClone); + // User clicks "Darkside Attack" + // Attack runs in same minigame instance + // After attack completes, show card data and save + } else { + // Has keys, proceed to clone normally + this.ui.showReadingScreen(); + } + } +} +``` + +Simplifies state machine and makes flow more intuitive. + +--- + +### Issue #3: Incomplete Firmware Upgrade System + +**Problem**: Plan mentions firmware but doesn't implement it: +```javascript +firmware: { + version: "1.0", + protocols: ["EM4100", "HID_Prox"], + attacks: ["read", "clone", "emulate"] +} +``` + +But no code for: +- How to upgrade firmware +- Where to find upgrades +- What triggers availability + +**Recommendation**: **Either fully implement or remove firmware system**. + +**Option A - Remove (Simpler)**: +- All protocols always available +- Flipper Zero in game has latest firmware pre-installed +- Saves implementation time + +**Option B - Full Implementation** (if player progression needed): +```javascript +// Firmware upgrade item in scenario +{ + "type": "firmware_update", + "name": "Flipper Firmware v1.2 (MIFARE Support)", + "upgrades_protocols": ["MIFARE_Classic"], + "upgrades_attacks": ["darkside", "nested"] +} + +// In interactions.js - when using firmware update +if (item.type === 'firmware_update') { + const cloner = getFlipperFromInventory(); + cloner.firmware.protocols.push(...item.upgrades_protocols); + cloner.firmware.attacks.push(...item.upgrades_attacks); + showMessage("Firmware updated!"); +} +``` + +**Recommendation**: Use Option A for initial implementation. Add firmware upgrades later if progression system is needed. + +--- + +### Issue #4: Card Data Migration Incomplete + +**Problem**: Migration only handles EM4100: +```javascript +if (protocol === 'EM4100' || protocol === 'HID_Prox') { + return { + ...cardData, + rfid_data: { + hex: cardData.rfid_hex, + // ... + } + }; +} + +return cardData; // What about other protocols? +``` + +**Recommendation**: **Complete migration for all protocols or use simpler approach**. + +**Better Approach** - Dual Format Support: +```javascript +// Support both old and new formats transparently +getRFIDHex(cardData) { + // New format + if (cardData.rfid_data?.hex) { + return cardData.rfid_data.hex; + } + + // Old format (backward compat) + if (cardData.rfid_hex) { + return cardData.rfid_hex; + } + + return null; +} + +getRFIDUID(cardData) { + if (cardData.rfid_data?.uid) { + return cardData.rfid_data.uid; + } + return null; +} +``` + +No migration needed - just read from either location. Simpler and safer. + +--- + +### Issue #5: Protocol Detection in Clone Mode Not Addressed + +**Problem**: Plan shows protocol detection for reading cards, but what about clone mode? + +When clone mode starts with `cardToClone` parameter: +```javascript +window.startRFIDMinigame(null, null, { + mode: 'clone', + cardToClone: someCard +}); +``` + +The card already has data - no need to "detect" protocol. But UI flow unclear. + +**Recommendation**: **Clarify clone mode initialization**. + +```javascript +// In rfid-minigame.js init() +if (this.mode === 'clone') { + if (this.cardToClone) { + const protocol = this.cardToClone.rfid_protocol || 'EM4100'; + + if (protocol === 'MIFARE_Classic') { + // Check if keys are available + const keysKnown = this.cardToClone.rfid_data?.sectors ? + Object.keys(this.cardToClone.rfid_data.sectors).length : 0; + + if (keysKnown === 0) { + // No keys - show protocol info with attack options + this.ui.showProtocolInfo(this.cardToClone); + } else { + // Has keys - start reading/cloning + this.ui.showReadingScreen(); + } + } else { + // EM4100 or DESFire - start reading immediately + this.ui.showReadingScreen(); + } + } +} +``` + +--- + +## Medium Priority Issues + +### Issue #6: Ink Variables Require Declaration + +**Problem**: Plan shows setting Ink variables: +```javascript +this.inkEngine.setVariable('card_protocol', protocol); +``` + +But Ink variables must be declared in the .ink file first: +```ink +VAR card_protocol = "" +VAR card_uid = "" +VAR card_security = "" +``` + +If variable isn't declared, setVariable will silently fail or throw. + +**Recommendation**: **Document Ink variable requirements**. + +**Add to Technical Design**: +```markdown +### Required Ink Variables + +For protocol integration to work, the following variables must be declared in NPC .ink files: + +```ink +// Card protocol variables (for NPCs with keycards) +VAR card_protocol = "" // "EM4100", "MIFARE_Classic", "MIFARE_DESFire" +VAR card_name = "" // Card display name +VAR card_hex = "" // For EM4100 +VAR card_uid = "" // For MIFARE +VAR card_security = "" // "low", "medium", "high" +VAR card_clonable = false // Can this card be instantly cloned? + +// For NPCs with multiple cards +VAR card2_protocol = "" +VAR card2_name = "" +// etc. +``` + +If variables aren't declared, protocol info won't be available to Ink conditionals. +``` + +--- + +### Issue #7: Background Attacks Need Cleanup + +**Problem**: Active attacks stored in array: +```javascript +this.activeAttacks = []; +``` + +But no cleanup when: +- Minigame is closed mid-attack +- Player navigates away +- Game is saved/loaded + +**Recommendation**: **Add cleanup and persistence**. + +```javascript +// In rfid-attacks.js +cleanup() { + // Cancel all active attacks + this.activeAttacks.forEach(attack => { + if (attack.interval) { + clearInterval(attack.interval); + } + }); + this.activeAttacks = []; +} + +// Store in window for persistence +saveState() { + return { + activeAttacks: this.activeAttacks.map(a => ({ + type: a.type, + uid: a.uid, + cardName: a.cardName, + startTime: a.startTime, + foundKeys: a.foundKeys, + currentSector: a.currentSector + })) + }; +} + +restoreState(state) { + // Restore attacks and resume progress + // (implementation details) +} +``` + +--- + +### Issue #8: No Error Handling for Unsupported Protocols + +**Problem**: What if cloner firmware doesn't support protocol? + +```javascript +// User tries to clone MIFARE Classic +// But cloner firmware only supports ['EM4100'] +``` + +Plan doesn't handle this case. + +**Recommendation**: **Add firmware check before starting minigame**. + +```javascript +// In chat-helpers.js clone_keycard tag +const cloner = window.inventory.items.find(item => + item?.scenarioData?.type === 'rfid_cloner' +); + +const cardProtocol = cardData.rfid_protocol || 'EM4100'; + +// Check firmware support +if (cloner.scenarioData.firmware) { + const supported = cloner.scenarioData.firmware.protocols || []; + if (!supported.includes(cardProtocol)) { + result.message = `⚠️ Flipper firmware doesn't support ${cardProtocol}`; + if (ui) ui.showNotification(result.message, 'warning'); + break; + } +} + +// Proceed with clone... +``` + +--- + +### Issue #9: DESFire UID Emulation Success Rate Not Defined + +**Problem**: Plan says DESFire UID emulation works on "some systems" but doesn't define which. + +```markdown +Some systems only check UID and don't use encryption properly. +``` + +How does game determine if emulation succeeds? + +**Recommendation**: **Add door-level property for UID-only acceptance**. + +```json +{ + "locked": true, + "lockType": "rfid", + "requires": "ceo_keycard", + "acceptsUIDOnly": false // NEW: True for low-security readers +} +``` + +```javascript +// In unlock-system.js RFID case +if (lockRequirements.lockType === 'rfid') { + const cardId = lockRequirements.requires; + + // Check if using DESFire UID-only emulation + if (card.rfid_protocol === 'MIFARE_DESFire' && + !card.rfid_data.masterKeyKnown) { + + // Check if door accepts UID-only + if (!lockRequirements.acceptsUIDOnly) { + showError("This reader requires full card authentication"); + return false; + } + + // UID matches? + if (card.key_id === cardId || card.rfid_data.uid === requiredUID) { + unlock(); + } + } +} +``` + +--- + +## Low Priority Issues + +### Issue #10: CSS Color Accessibility + +**Problem**: Color-coding security levels: +```javascript +color: '#FF6B6B' // Red for low security +color: '#95E1D3' // Light green for high security +``` + +Red/green color blindness (~8% of males) makes this hard to distinguish. + +**Recommendation**: **Add icons in addition to colors**. + +```javascript +security: 'low', +color: '#FF6B6B', +icon: '⚠️' // Warning triangle + +security: 'high', +color: '#95E1D3', +icon: '🔒' // Lock icon +``` + +--- + +### Issue #11: No Tests for Protocol-Specific Code + +**Problem**: Plan mentions testing scenarios but no unit tests for: +- Protocol detection logic +- Capability checks +- Key validation +- Data migration + +**Recommendation**: Add testing section (can defer to later). + +--- + +### Issue #12: Attack Duration Magic Numbers + +**Problem**: Hard-coded timings: +```javascript +const duration = 30000; // 30 seconds +``` + +Should be constants for easy tuning. + +**Recommendation**: +```javascript +const ATTACK_DURATIONS = { + darkside: 30000, // 30 sec - crack from scratch + nested: 10000, // 10 sec - crack with known key + dictionary: 0 // Instant +}; +``` + +--- + +## Simplified Implementation Approach + +Based on review, here's a streamlined approach: + +### Phase 1: Core Three Protocols (MVP) +1. EM4100 (easy) - Current implementation +2. MIFARE Classic (medium) - Add key attacks +3. MIFARE DESFire (hard) - UID only + +Skip HID Prox initially. + +### Phase 2: Protocol Detection & UI +1. Add RFID_PROTOCOLS constant +2. Update card data display (dual format support) +3. Add protocol info screen +4. Color-code security levels + +### Phase 3: MIFARE Attacks (in clone mode) +1. Add MIFAREAttackManager +2. Dictionary attack (instant) +3. Darkside attack (30 sec animation) +4. Nested attack (10 sec animation) +5. Integrate into clone flow (not separate mode) + +### Phase 4: Ink Integration +1. Sync protocol variables +2. Add start_mifare_attack tag +3. Add save_uid_only tag +4. Document required Ink variables + +### Phase 5: Testing +1. Test scenarios for each protocol +2. Integration tests +3. Backward compatibility tests + +## Recommended Changes Summary + +| Change | Priority | Time Saved/Impact | +|--------|----------|-------------------| +| Remove HID Prox | High | -2h development | +| Merge attack into clone mode | High | Clearer UX, -1h dev | +| Remove firmware system initially | High | -2h development | +| Dual format support (no migration) | High | Simpler, safer | +| Add firmware check before clone | Medium | Prevents errors | +| Define acceptsUIDOnly for doors | Medium | Clear DESFire rules | +| Add Ink variable documentation | Medium | Prevent confusion | +| Add attack cleanup/persistence | Medium | Prevent bugs | +| Use timing constants | Low | Better maintainability | +| Add security icons | Low | Better accessibility | + +**Total Time Saved**: ~5 hours +**Total Clarity Improved**: Significant + +## Conclusion + +The original plan is solid but can be **streamlined by 25%** while improving clarity: +- Remove HID Prox (minimal gameplay value) +- Merge attack mode into clone mode (simpler state machine) +- Skip firmware system initially (can add later) +- Use dual format support instead of migration (safer) +- Add missing error handling (firmware checks, UID acceptance) + +**Recommendation**: Update implementation plan with these improvements before beginning development.