diff --git a/css/rfid-minigame.css b/css/rfid-minigame.css index c901b74..9b5ab0a 100644 --- a/css/rfid-minigame.css +++ b/css/rfid-minigame.css @@ -366,6 +366,87 @@ border-radius: 3px; } +/* Protocol-Specific Displays */ +.flipper-protocol-header { + background: rgba(255, 255, 255, 0.05); + border-radius: 8px; + padding: 12px; + margin-bottom: 15px; +} + +.protocol-header-top { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 8px; +} + +.protocol-icon { + font-size: 20px; +} + +.protocol-name { + font-size: 14px; + font-weight: bold; + color: white; +} + +.protocol-meta { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 12px; + color: #AAA; +} + +.security-badge { + padding: 3px 8px; + border-radius: 4px; + font-size: 11px; + font-weight: bold; +} + +.security-badge.security-low { + background: #FF6B6B; + color: white; +} + +.security-badge.security-medium { + background: #4ECDC4; + color: white; +} + +.security-badge.security-high { + background: #95E1D3; + color: #333; +} + +.flipper-menu-item-dim { + opacity: 0.5; +} + +.flipper-menu-item-dim:hover { + background: rgba(255, 255, 255, 0.03); + opacity: 0.7; +} + +/* Attack Progress */ +#attack-status { + font-size: 12px; + margin-top: 10px; + color: #FFA500; +} + +#attack-percentage { + font-size: 16px; + font-weight: bold; + color: white; +} + +#attack-progress-bar { + transition: width 0.5s ease, background-color 0.3s; +} + /* Responsive */ @media (max-width: 500px) { .flipper-zero-frame { diff --git a/js/minigames/rfid/rfid-attacks.js b/js/minigames/rfid/rfid-attacks.js new file mode 100644 index 0000000..d769099 --- /dev/null +++ b/js/minigames/rfid/rfid-attacks.js @@ -0,0 +1,330 @@ +/** + * MIFARE Attack Manager + * + * Handles MIFARE Classic key attacks: + * - Dictionary Attack: Try common keys (instant) + * - Darkside Attack: Crack keys from scratch (30 sec) + * - Nested Attack: Crack remaining keys when one is known (10 sec) + * + * @module rfid-attacks + */ + +import { MIFARE_COMMON_KEYS, ATTACK_DURATIONS } from './rfid-protocols.js'; + +export class MIFAREAttackManager { + constructor() { + this.activeAttacks = new Map(); + console.log('๐Ÿ”“ MIFAREAttackManager initialized'); + } + + /** + * Dictionary attack - protocol-aware success rates + * Tries common keys against all 16 sectors + * @param {string} uid - Card UID + * @param {Object} existingKeys - Already known keys {sector: {keyA, keyB}} + * @param {string} protocol - Protocol name (determines success rate) + * @returns {Object} {success, foundKeys, newKeysFound, message} + */ + dictionaryAttack(uid, existingKeys = {}, protocol) { + console.log(`๐Ÿ”“ Dictionary attack on ${uid} (${protocol})`); + + const foundKeys = { ...existingKeys }; + let newKeysFound = 0; + + // Success rate based on protocol + // Weak defaults: 95% (most sectors use factory default) + // Custom keys: 0% (no default keys) + const successRate = protocol === 'MIFARE_Classic_Weak_Defaults' ? 0.95 : 0.0; + + for (let sector = 0; sector < 16; sector++) { + if (foundKeys[sector]) continue; + + if (Math.random() < successRate) { + foundKeys[sector] = { + keyA: MIFARE_COMMON_KEYS[0], // FFFFFFFFFFFF (factory default) + keyB: MIFARE_COMMON_KEYS[0] + }; + newKeysFound++; + } + } + + return { + success: newKeysFound > 0, + foundKeys: foundKeys, + newKeysFound: newKeysFound, + message: this.getDictionaryMessage(newKeysFound, protocol) + }; + } + + /** + * Get message for dictionary attack result + * @param {number} found - Number of sectors found + * @param {string} protocol - Protocol name + * @returns {string} Message text + */ + getDictionaryMessage(found, protocol) { + if (found === 16) { + return '๐Ÿ”“ All sectors use factory defaults!'; + } else if (found > 0) { + return `๐Ÿ”“ Found ${found} sectors with default keys`; + } else if (protocol === 'MIFARE_Classic_Weak_Defaults') { + return 'โš ๏ธ Some sectors have custom keys - try Nested attack'; + } else { + return 'โš ๏ธ No default keys - use Darkside attack'; + } + } + + /** + * Darkside attack - crack all keys from scratch + * Exploits crypto weakness to brute force sector keys + * Duration varies based on protocol (weak defaults crack faster) + * @param {string} uid - Card UID + * @param {Function} progressCallback - Progress update callback + * @param {string} protocol - Protocol name + * @returns {Promise} {success, foundKeys, message} + */ + async startDarksideAttack(uid, progressCallback, protocol) { + console.log(`๐Ÿ”“ Darkside attack on ${uid}`); + + // Weak defaults crack faster (10 sec vs 30 sec) + const duration = protocol === 'MIFARE_Classic_Weak_Defaults' ? + ATTACK_DURATIONS.darksideWeak : ATTACK_DURATIONS.darkside; + + return new Promise((resolve) => { + const attack = { + type: 'darkside', + uid: uid, + protocol: protocol, + foundKeys: {}, + startTime: Date.now() + }; + + this.activeAttacks.set(uid, attack); + + const updateInterval = 500; // Update every 500ms + let elapsed = 0; + + const interval = setInterval(() => { + elapsed += updateInterval; + const progress = Math.min(100, (elapsed / duration) * 100); + const currentSector = Math.floor((progress / 100) * 16); + + // Add keys progressively + for (let i = 0; i < currentSector; i++) { + if (!attack.foundKeys[i]) { + attack.foundKeys[i] = { + keyA: this.generateRandomKey(), + keyB: this.generateRandomKey() + }; + } + } + + if (progressCallback) { + progressCallback({ + progress: progress, + currentSector: currentSector, + foundKeys: attack.foundKeys, + totalSectors: 16, + elapsed: elapsed, + duration: duration + }); + } + + if (progress >= 100) { + clearInterval(interval); + + // Ensure all 16 sectors are complete + 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); + + attack.interval = interval; + }); + } + + /** + * Nested attack - crack remaining keys when one is known + * Uses known key to exploit crypto and crack remaining sectors + * @param {string} uid - Card UID + * @param {Object} knownKeys - Already known keys + * @param {Function} progressCallback - Progress update callback + * @returns {Promise} {success, foundKeys, message} + */ + 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')); + } + + 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; // 10 seconds + 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); + + const expectedFound = Math.floor((progress / 100) * sectorsToFind); + + // Add keys progressively + while (sectorsFound < expectedFound) { + 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, + sectorsTotal: sectorsToFind, + elapsed: elapsed, + duration: duration + }); + } + + if (progress >= 100) { + clearInterval(interval); + + // Ensure all sectors are complete + 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: `๐Ÿ”“ Cracked ${sectorsToFind} remaining sectors!` + }); + } + }, updateInterval); + + attack.interval = interval; + }); + } + + /** + * Generate random MIFARE key (12 hex characters) + * @returns {string} 12-character hex key + */ + generateRandomKey() { + return Array.from({ length: 12 }, () => + Math.floor(Math.random() * 16).toString(16).toUpperCase() + ).join(''); + } + + /** + * Get attack in progress for given UID + * @param {string} uid - Card UID + * @returns {Object|null} Attack object or null + */ + getActiveAttack(uid) { + return this.activeAttacks.get(uid) || null; + } + + /** + * Cancel attack in progress + * @param {string} uid - Card UID + */ + cancelAttack(uid) { + const attack = this.activeAttacks.get(uid); + if (attack && attack.interval) { + clearInterval(attack.interval); + console.log(`โŒ Cancelled ${attack.type} attack on ${uid}`); + } + this.activeAttacks.delete(uid); + } + + /** + * Cancel all active attacks and clean up + */ + cleanup() { + console.log(`๐Ÿงน Cleaning up ${this.activeAttacks.size} active attacks`); + this.activeAttacks.forEach((attack, uid) => { + if (attack.interval) { + clearInterval(attack.interval); + } + }); + this.activeAttacks.clear(); + } + + /** + * Save state for persistence (for future implementation) + * @returns {Object} Serializable state + */ + saveState() { + return { + activeAttacks: Array.from(this.activeAttacks.entries()).map(([uid, attack]) => ({ + uid: uid, + type: attack.type, + protocol: attack.protocol, + startTime: attack.startTime, + foundKeys: attack.foundKeys + })) + }; + } + + /** + * Restore state from saved data (for future implementation) + * @param {Object} state - Saved state + */ + restoreState(state) { + if (!state || !state.activeAttacks) return; + + // Note: Full restoration would require restarting attack timers + // For now, just restore the found keys + state.activeAttacks.forEach(attackData => { + console.log(`โฎ๏ธ Restored attack state for ${attackData.uid}`); + // Could restart attacks here if needed + }); + } +} + +// Create global instance +window.mifareAttackManager = window.mifareAttackManager || new MIFAREAttackManager(); + +export default MIFAREAttackManager; diff --git a/js/minigames/rfid/rfid-data.js b/js/minigames/rfid/rfid-data.js index b375683..d3ee0d1 100644 --- a/js/minigames/rfid/rfid-data.js +++ b/js/minigames/rfid/rfid-data.js @@ -2,7 +2,8 @@ * RFID Data Manager * * Handles RFID card data management: - * - Card generation with EM4100 protocol + * - Card generation with deterministic card_id-based generation + * - Multi-protocol support (EM4100, MIFARE Classic, MIFARE DESFire) * - Hex ID validation * - Card save/load to cloner device * - Format conversions (hex, DEZ8, facility codes) @@ -10,6 +11,8 @@ * @module rfid-data */ +import { getProtocolInfo, detectProtocol, isMIFARE } from './rfid-protocols.js'; + // Maximum number of cards that can be saved to cloner const MAX_SAVED_CARDS = 50; @@ -30,6 +33,163 @@ export class RFIDDataManager { console.log('๐Ÿ” RFIDDataManager initialized'); } + /** + * Generate RFID technical data from card_id (deterministic) + * Same card_id always produces same hex/UID + * @param {string} cardId - Logical card identifier + * @param {string} protocol - RFID protocol name + * @returns {Object} Protocol-specific RFID data + */ + generateRFIDDataFromCardId(cardId, protocol) { + const seed = this.hashCardId(cardId); + const data = { cardId: cardId }; + + switch (protocol) { + case 'EM4100': + data.hex = this.generateHexFromSeed(seed, 10); + data.facility = (seed % 256); + data.cardNumber = (seed % 65536); + break; + + case 'MIFARE_Classic_Weak_Defaults': + case 'MIFARE_Classic_Custom_Keys': + data.uid = this.generateHexFromSeed(seed, 8); + data.sectors = {}; // Empty until cloned/cracked + break; + + case 'MIFARE_DESFire': + data.uid = this.generateHexFromSeed(seed, 14); + data.masterKeyKnown = false; + break; + + default: + // Default to EM4100 + data.hex = this.generateHexFromSeed(seed, 10); + data.facility = (seed % 256); + data.cardNumber = (seed % 65536); + } + + return data; + } + + /** + * Hash card_id to deterministic seed + * Uses simple string hashing algorithm + * @param {string} cardId - Card identifier string + * @returns {number} Positive integer seed + */ + hashCardId(cardId) { + let hash = 0; + for (let i = 0; i < cardId.length; i++) { + const char = cardId.charCodeAt(i); + hash = ((hash << 5) - hash) + char; + hash = hash & hash; // Convert to 32bit integer + } + return Math.abs(hash); + } + + /** + * Generate hex string from seed using Linear Congruential Generator + * Ensures deterministic output for same seed + * @param {number} seed - Integer seed value + * @param {number} length - Desired hex string length + * @returns {string} Hex string of specified length + */ + generateHexFromSeed(seed, length) { + let hex = ''; + let currentSeed = seed; + + for (let i = 0; i < length; i++) { + // Linear congruential generator (LCG) + // Parameters from glibc + currentSeed = (currentSeed * 1103515245 + 12345) & 0x7fffffff; + hex += (currentSeed % 16).toString(16).toUpperCase(); + } + + return hex; + } + + /** + * Get card display data for all protocols + * Supports both new (card_id) and legacy formats + * @param {Object} cardData - Card scenario data + * @returns {Object} Display data with protocol info and fields + */ + getCardDisplayData(cardData) { + const protocol = detectProtocol(cardData); + const protocolInfo = getProtocolInfo(protocol); + + // Ensure rfid_data exists (generate if using card_id) + if (!cardData.rfid_data && cardData.card_id) { + cardData.rfid_data = this.generateRFIDDataFromCardId( + cardData.card_id, + protocol + ); + } + + const displayData = { + protocol: protocol, + protocolName: protocolInfo.name, + frequency: protocolInfo.frequency, + security: protocolInfo.security, + color: protocolInfo.color, + icon: protocolInfo.icon, + description: protocolInfo.description, + fields: [] + }; + + switch (protocol) { + case 'EM4100': + // Support both new (rfid_data.hex) and legacy (rfid_hex) formats + const hex = cardData.rfid_data?.hex || cardData.rfid_hex; + const facility = cardData.rfid_data?.facility || cardData.rfid_facility || 0; + const cardNumber = cardData.rfid_data?.cardNumber || cardData.rfid_card_number || 0; + + 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_Weak_Defaults': + case 'MIFARE_Classic_Custom_Keys': + const uid = cardData.rfid_data?.uid; + 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 โœ“' : keysKnown > 0 ? 'Partial' : 'No' }, + { label: 'Clonable', value: keysKnown > 0 ? 'Yes โœ“' : 'No' } + ]; + + // Add security note + if (protocol === 'MIFARE_Classic_Weak_Defaults') { + displayData.securityNote = 'Uses factory default keys'; + } else { + displayData.securityNote = 'Uses custom encryption keys'; + } + break; + + case 'MIFARE_DESFire': + const desUID = cardData.rfid_data?.uid; + displayData.fields = [ + { label: 'UID', value: this.formatHex(desUID) }, + { label: 'Type', value: 'EV2' }, + { label: 'Encryption', value: '3DES/AES' }, + { label: 'Clonable', value: 'UID Only' } + ]; + displayData.securityNote = 'High security - full clone impossible'; + break; + } + + return displayData; + } + /** * Generate a random RFID card with EM4100 format * @returns {Object} Card data with hex, facility code, card number @@ -84,6 +244,7 @@ export class RFIDDataManager { /** * Save card to RFID cloner device + * Supports all protocols (EM4100, MIFARE Classic, MIFARE DESFire) * @param {Object} cardData - Card data to save * @returns {Object} {success: boolean, message: string} */ @@ -97,10 +258,20 @@ export class RFIDDataManager { return { success: false, message: 'RFID cloner not found in inventory' }; } - // Validate hex ID - const validation = this.validateHex(cardData.rfid_hex); - if (!validation.valid) { - return { success: false, message: validation.error }; + // Determine protocol and validate + const protocol = cardData.rfid_protocol || 'EM4100'; + + // For EM4100, validate hex ID (legacy support) + if (protocol === 'EM4100' && cardData.rfid_hex) { + const validation = this.validateHex(cardData.rfid_hex); + if (!validation.valid) { + return { success: false, message: validation.error }; + } + } + + // Ensure rfid_data exists for card_id-based cards + if (!cardData.rfid_data && cardData.card_id) { + cardData.rfid_data = this.generateRFIDDataFromCardId(cardData.card_id, protocol); } // Initialize saved_cards array if missing @@ -113,10 +284,21 @@ export class RFIDDataManager { return { success: false, message: `Cloner full (max ${MAX_SAVED_CARDS} cards)` }; } - // Check for duplicate hex ID - const existingIndex = cloner.scenarioData.saved_cards.findIndex(card => - card.rfid_hex === cardData.rfid_hex - ); + // Check for duplicate by card_id (preferred) or hex/UID + let existingIndex = -1; + if (cardData.card_id) { + existingIndex = cloner.scenarioData.saved_cards.findIndex(card => + card.card_id === cardData.card_id + ); + } else if (cardData.rfid_hex) { + existingIndex = cloner.scenarioData.saved_cards.findIndex(card => + card.rfid_hex === cardData.rfid_hex + ); + } else if (cardData.rfid_data?.uid) { + existingIndex = cloner.scenarioData.saved_cards.findIndex(card => + card.rfid_data?.uid === cardData.rfid_data.uid + ); + } if (existingIndex !== -1) { // Overwrite existing card with updated timestamp @@ -124,16 +306,16 @@ export class RFIDDataManager { ...cardData, timestamp: Date.now() }; - console.log(`๐Ÿ“ก Overwritten duplicate card: ${cardData.name}`); - return { success: true, message: `Updated: ${cardData.name}` }; + console.log(`๐Ÿ“ก Overwritten duplicate card: ${cardData.name || 'Card'}`); + return { success: true, message: `Updated: ${cardData.name || 'Card'}` }; } else { // Add new card cloner.scenarioData.saved_cards.push({ ...cardData, timestamp: Date.now() }); - console.log(`๐Ÿ“ก Saved new card: ${cardData.name}`); - return { success: true, message: `Saved: ${cardData.name}` }; + console.log(`๐Ÿ“ก Saved new card: ${cardData.name || 'Card'}`); + return { success: true, message: `Saved: ${cardData.name || 'Card'}` }; } } diff --git a/js/minigames/rfid/rfid-minigame.js b/js/minigames/rfid/rfid-minigame.js index 170e240..9f689c3 100644 --- a/js/minigames/rfid/rfid-minigame.js +++ b/js/minigames/rfid/rfid-minigame.js @@ -16,6 +16,8 @@ import { MinigameScene } from '../framework/base-minigame.js'; import { RFIDUIRenderer } from './rfid-ui.js'; import { RFIDDataManager } from './rfid-data.js'; import { RFIDAnimations } from './rfid-animations.js'; +import { MIFAREAttackManager } from './rfid-attacks.js'; +import { detectProtocol } from './rfid-protocols.js'; export class RFIDMinigame extends MinigameScene { constructor(container, params) { @@ -33,7 +35,8 @@ export class RFIDMinigame extends MinigameScene { // Parameters this.params = params; this.mode = params.mode || 'unlock'; // 'unlock' or 'clone' - this.requiredCardId = params.requiredCardId; // For unlock mode + this.requiredCardIds = params.requiredCardIds || (params.requiredCardId ? [params.requiredCardId] : []); // Array of valid card IDs + this.acceptsUIDOnly = params.acceptsUIDOnly || false; // For MIFARE DESFire UID-only emulation this.availableCards = params.availableCards || []; // For unlock mode this.hasCloner = params.hasCloner || false; // For unlock mode this.cardToClone = params.cardToClone; // For clone mode @@ -42,6 +45,7 @@ export class RFIDMinigame extends MinigameScene { this.ui = null; this.dataManager = null; this.animations = null; + this.attackManager = null; // State this.gameResult = null; @@ -60,6 +64,7 @@ export class RFIDMinigame extends MinigameScene { // Initialize components this.dataManager = new RFIDDataManager(); this.animations = new RFIDAnimations(this); + this.attackManager = new MIFAREAttackManager(); this.ui = new RFIDUIRenderer(this); // Create appropriate interface @@ -92,8 +97,9 @@ export class RFIDMinigame extends MinigameScene { handleCardTap(card) { console.log('๐Ÿ“ก Card tapped:', card.scenarioData?.name); - const cardId = card.scenarioData?.key_id || card.key_id; - const isCorrect = cardId === this.requiredCardId; + // Support both card_id (new) and key_id (legacy) + const cardId = card.scenarioData?.card_id || card.scenarioData?.key_id || card.key_id; + const isCorrect = this.requiredCardIds.includes(cardId); if (isCorrect) { this.animations.showTapSuccess(); @@ -114,13 +120,43 @@ export class RFIDMinigame extends MinigameScene { /** * Handle card emulation (unlock mode) + * Supports all protocols including UID-only emulation * @param {Object} savedCard - Saved card from cloner */ handleEmulate(savedCard) { console.log('๐Ÿ“ก Emulating card:', savedCard.name); - const cardId = savedCard.key_id; - const isCorrect = cardId === this.requiredCardId; + // Support both card_id (new) and key_id (legacy) + const cardId = savedCard.card_id || savedCard.key_id; + const isCorrect = this.requiredCardIds.includes(cardId); + + // Check if UID-only emulation (MIFARE DESFire without master key) + const protocol = savedCard.rfid_protocol || 'EM4100'; + const isUIDOnly = protocol === 'MIFARE_DESFire' && !savedCard.rfid_data?.masterKeyKnown; + + // If UID-only and door doesn't accept it, reject + if (isUIDOnly && !this.acceptsUIDOnly) { + this.animations.showEmulationFailure(); + this.ui.showError('Reader requires full authentication'); + + // Emit event + if (window.eventDispatcher) { + window.eventDispatcher.emit('card_emulated', { + cardName: savedCard.name, + cardId: cardId, + protocol: protocol, + uidOnly: true, + readerRejectsUIDOnly: true, + success: false, + timestamp: Date.now() + }); + } + + setTimeout(() => { + this.ui.showSavedCards(); + }, 2000); + return; + } if (isCorrect) { this.animations.showEmulationSuccess(); @@ -130,7 +166,9 @@ export class RFIDMinigame extends MinigameScene { if (window.eventDispatcher) { window.eventDispatcher.emit('card_emulated', { cardName: savedCard.name, - cardHex: savedCard.rfid_hex, + cardId: cardId, + protocol: protocol, + uidOnly: isUIDOnly, success: true, timestamp: Date.now() }); @@ -147,7 +185,8 @@ export class RFIDMinigame extends MinigameScene { if (window.eventDispatcher) { window.eventDispatcher.emit('card_emulated', { cardName: savedCard.name, - cardHex: savedCard.rfid_hex, + cardId: cardId, + protocol: protocol, success: false, timestamp: Date.now() }); @@ -214,6 +253,103 @@ export class RFIDMinigame extends MinigameScene { } } + /** + * Start MIFARE key attack + * @param {string} attackType - 'dictionary', 'darkside', or 'nested' + * @param {Object} cardData - Card to attack + */ + startKeyAttack(attackType, cardData) { + console.log(`๐Ÿ”“ Starting ${attackType} attack on card:`, cardData.name); + + const protocol = cardData.rfid_protocol || 'EM4100'; + const uid = cardData.rfid_data?.uid; + + if (!uid) { + console.error('No UID found for MIFARE attack'); + this.ui.showError('Invalid card data'); + return; + } + + if (attackType === 'dictionary') { + // Dictionary attack is instant + const existingKeys = cardData.rfid_data?.sectors || {}; + const result = this.attackManager.dictionaryAttack(uid, existingKeys, protocol); + + // Update card data with found keys + if (result.success) { + cardData.rfid_data.sectors = result.foundKeys; + this.ui.showSuccess(result.message); + + setTimeout(() => { + // Show updated protocol info + this.ui.showProtocolInfo(cardData); + }, 1500); + } else { + this.ui.showError(result.message); + + setTimeout(() => { + this.ui.showProtocolInfo(cardData); + }, 1500); + } + + } else if (attackType === 'darkside') { + // Show attack progress screen + this.ui.showAttackProgress({ + type: 'Darkside', + progress: 0, + currentSector: 0, + totalSectors: 16 + }); + + // Start attack + this.attackManager.startDarksideAttack(uid, (progressData) => { + this.ui.updateAttackProgress(progressData); + }, protocol).then((result) => { + // Update card data with found keys + cardData.rfid_data.sectors = result.foundKeys; + + this.ui.showSuccess(result.message); + + setTimeout(() => { + // Show card data - now fully readable + this.ui.showCardDataScreen(cardData); + }, 1500); + }).catch((error) => { + console.error('Darkside attack error:', error); + this.ui.showError('Attack failed'); + }); + + } else if (attackType === 'nested') { + // Show attack progress screen + const knownKeys = cardData.rfid_data?.sectors || {}; + const sectorsToFind = 16 - Object.keys(knownKeys).length; + + this.ui.showAttackProgress({ + type: 'Nested', + progress: 0, + sectorsRemaining: sectorsToFind + }); + + // Start attack + this.attackManager.startNestedAttack(uid, knownKeys, (progressData) => { + this.ui.updateAttackProgress(progressData); + }).then((result) => { + // Update card data with found keys + cardData.rfid_data.sectors = result.foundKeys; + + this.ui.showSuccess(result.message); + + setTimeout(() => { + // Show card data - now fully readable + this.ui.showCardDataScreen(cardData); + }, 1500); + }).catch((error) => { + console.error('Nested attack error:', error); + this.ui.showError(error.message || 'Attack failed'); + }); + } + } + complete(success) { // Check if we need to return to conversation if (window.pendingConversationReturn && window.returnToConversationAfterRFID) { @@ -233,6 +369,11 @@ export class RFIDMinigame extends MinigameScene { this.animations.cleanup(); } + // Cleanup attacks + if (this.attackManager) { + this.attackManager.cleanup(); + } + // Call parent cleanup super.cleanup(); console.log('๐Ÿงน RFIDMinigame cleanup complete'); diff --git a/js/minigames/rfid/rfid-protocols.js b/js/minigames/rfid/rfid-protocols.js new file mode 100644 index 0000000..86bb719 --- /dev/null +++ b/js/minigames/rfid/rfid-protocols.js @@ -0,0 +1,183 @@ +/** + * RFID Protocol Definitions + * + * Defines the four supported RFID protocols with their security characteristics + * and capabilities. Used throughout the RFID minigame system for protocol-specific + * behavior and UI rendering. + */ + +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: 'โš ๏ธ', + description: 'Legacy read-only card with no encryption' + }, + + 'MIFARE_Classic_Weak_Defaults': { + name: 'MIFARE Classic 1K (Default Keys)', + frequency: '13.56MHz', + security: 'low', + capabilities: { + read: true, // Dictionary attack works instantly + clone: true, + emulate: true + }, + attackTime: 'instant', + sectors: 16, + hexLength: 8, + color: '#FF6B6B', // Red like EM4100 - equally weak + icon: 'โš ๏ธ', + description: 'Encrypted card using factory default keys (FFFFFFFFFFFF)' + }, + + 'MIFARE_Classic_Custom_Keys': { + name: 'MIFARE Classic 1K (Custom Keys)', + frequency: '13.56MHz', + security: 'medium', + capabilities: { + read: 'with-keys', + clone: 'with-keys', + emulate: true + }, + attackTime: '30sec', + sectors: 16, + hexLength: 8, + color: '#4ECDC4', // Teal for medium security + icon: '๐Ÿ”', + description: 'Encrypted card with custom keys - requires attack to crack' + }, + + 'MIFARE_DESFire': { + name: 'MIFARE DESFire EV2', + frequency: '13.56MHz', + security: 'high', + capabilities: { + read: false, + clone: false, + emulate: 'uid-only' + }, + hexLength: 14, + color: '#95E1D3', + icon: '๐Ÿ”’', + description: 'High security with 3DES/AES encryption - UID only' + } +}; + +/** + * Common MIFARE keys used in dictionary attacks + * Ordered by likelihood (factory default first) + */ +export const MIFARE_COMMON_KEYS = [ + 'FFFFFFFFFFFF', // Factory default (most common) + '000000000000', + 'A0A1A2A3A4A5', + 'D3F7D3F7D3F7', + '123456789ABC', + 'AABBCCDDEEFF', + 'B0B1B2B3B4B5', + '4D3A99C351DD', + '1A982C7E459A', + 'AA1234567890', + 'A0478CC39091', + '533CB6C723F6', + '8FD0A4F256E9' +]; + +/** + * Attack duration constants (milliseconds) + */ +export const ATTACK_DURATIONS = { + darkside: 30000, // 30 seconds - crack from scratch + darksideWeak: 10000, // 10 seconds - crack weak crypto faster + nested: 10000, // 10 seconds - crack with known key + dictionary: 0 // Instant +}; + +/** + * Get protocol information by protocol name + * @param {string} protocol - Protocol name + * @returns {Object} Protocol info object + */ +export function getProtocolInfo(protocol) { + return RFID_PROTOCOLS[protocol] || RFID_PROTOCOLS['EM4100']; +} + +/** + * Detect protocol from card data + * Supports both new (rfid_protocol) and legacy formats + * @param {Object} cardData - Card scenario data + * @returns {string} Protocol name + */ +export function detectProtocol(cardData) { + // New format - explicit protocol + if (cardData.rfid_protocol) { + return cardData.rfid_protocol; + } + + // Legacy format - detect from structure + if (cardData.rfid_hex) { + return 'EM4100'; + } + + // Default + return 'EM4100'; +} + +/** + * Check if protocol supports instant cloning + * @param {string} protocol - Protocol name + * @returns {boolean} True if can clone instantly + */ +export function supportsInstantClone(protocol) { + return protocol === 'EM4100' || protocol === 'MIFARE_Classic_Weak_Defaults'; +} + +/** + * Check if protocol requires key attacks + * @param {string} protocol - Protocol name + * @returns {boolean} True if needs attack + */ +export function requiresKeyAttack(protocol) { + return protocol === 'MIFARE_Classic_Custom_Keys'; +} + +/** + * Check if protocol is UID-only + * @param {string} protocol - Protocol name + * @returns {boolean} True if only UID can be saved + */ +export function isUIDOnly(protocol) { + return protocol === 'MIFARE_DESFire'; +} + +/** + * Check if card is MIFARE variant + * @param {string} protocol - Protocol name + * @returns {boolean} True if MIFARE protocol + */ +export function isMIFARE(protocol) { + return protocol.startsWith('MIFARE_'); +} + +/** + * Get security level display text + * @param {string} security - Security level ('low', 'medium', 'high') + * @returns {string} Display text + */ +export function getSecurityDisplay(security) { + const displays = { + 'low': 'โš ๏ธ LOW', + 'medium': '๐Ÿ” MEDIUM', + 'high': '๐Ÿ”’ HIGH' + }; + return displays[security] || security.toUpperCase(); +} diff --git a/js/minigames/rfid/rfid-ui.js b/js/minigames/rfid/rfid-ui.js index 70202a7..4c1393b 100644 --- a/js/minigames/rfid/rfid-ui.js +++ b/js/minigames/rfid/rfid-ui.js @@ -8,10 +8,13 @@ * - Emulation screen * - Card reading screen (clone mode) * - Card data display + * - Protocol-specific displays for all supported protocols * * @module rfid-ui */ +import { getProtocolInfo, detectProtocol } from './rfid-protocols.js'; + export class RFIDUIRenderer { constructor(minigame) { this.minigame = minigame; @@ -243,13 +246,16 @@ export class RFIDUIRenderer { } /** - * Show emulation screen + * Show emulation screen (supports all protocols) * @param {Object} card - Card to emulate */ showEmulationScreen(card) { const screen = this.getScreen(); screen.innerHTML = ''; + // Get protocol-specific display data + const displayData = this.dataManager.getCardDisplayData(card); + // Breadcrumb const breadcrumb = document.createElement('div'); breadcrumb.className = 'flipper-breadcrumb'; @@ -262,34 +268,41 @@ export class RFIDUIRenderer { icon.textContent = '๐Ÿ“ก'; screen.appendChild(icon); - // Protocol - const protocol = document.createElement('div'); - protocol.className = 'flipper-info'; - protocol.textContent = 'EM-Micro EM4100'; - screen.appendChild(protocol); + // Protocol with color indicator + const protocolDiv = document.createElement('div'); + protocolDiv.className = 'flipper-info'; + protocolDiv.style.borderLeft = `4px solid ${displayData.color}`; + protocolDiv.style.paddingLeft = '8px'; + protocolDiv.innerHTML = `${displayData.icon} ${displayData.protocolName}`; + screen.appendChild(protocolDiv); // Card name const name = document.createElement('div'); name.className = 'flipper-card-name'; - name.textContent = card.name; + name.textContent = card.name || 'Card'; screen.appendChild(name); - // Card data - const { facility, cardNumber } = this.dataManager.hexToFacilityCard(card.rfid_hex); - + // Card data fields const data = document.createElement('div'); data.className = 'flipper-card-data'; - data.innerHTML = ` -
HEX: ${this.dataManager.formatHex(card.rfid_hex)}
-
Facility: ${facility}
-
Card: ${cardNumber}
- `; + + // Show first 3 fields (most relevant for emulation) + displayData.fields.slice(0, 3).forEach(field => { + const fieldDiv = document.createElement('div'); + fieldDiv.innerHTML = `${field.label}: ${field.value}`; + data.appendChild(fieldDiv); + }); + screen.appendChild(data); // Emulating message const emulating = document.createElement('div'); emulating.className = 'flipper-emulating'; - emulating.textContent = 'Emulating...'; + if (displayData.protocol === 'MIFARE_DESFire' && !card.rfid_data?.masterKeyKnown) { + emulating.textContent = 'Emulating UID only...'; + } else { + emulating.textContent = 'Emulating...'; + } screen.appendChild(emulating); // Trigger emulation after showing screen @@ -298,6 +311,160 @@ export class RFIDUIRenderer { }, 500); } + /** + * Show protocol information screen with attack options + * @param {Object} cardData - Card data to display protocol info for + */ + showProtocolInfo(cardData) { + const screen = this.getScreen(); + screen.innerHTML = ''; + + const displayData = this.dataManager.getCardDisplayData(cardData); + const protocol = displayData.protocol; + + // Breadcrumb + const breadcrumb = document.createElement('div'); + breadcrumb.className = 'flipper-breadcrumb'; + breadcrumb.textContent = 'RFID > Info'; + 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); + + // Security note + if (displayData.securityNote) { + const note = document.createElement('div'); + note.className = 'flipper-info'; + note.textContent = displayData.securityNote; + screen.appendChild(note); + } + + // 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); + + // Actions based on protocol + const actions = document.createElement('div'); + actions.className = 'flipper-menu'; + actions.style.marginTop = '20px'; + + if (protocol === 'MIFARE_Classic_Weak_Defaults') { + const keysKnown = cardData.rfid_data?.sectors ? + Object.keys(cardData.rfid_data.sectors).length : 0; + + if (keysKnown === 0) { + // Suggest dictionary first + 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); + } else if (keysKnown < 16) { + // Some keys found + const nestedBtn = document.createElement('div'); + nestedBtn.className = 'flipper-menu-item'; + nestedBtn.textContent = `> Nested Attack (${16 - keysKnown} sectors)`; + nestedBtn.addEventListener('click', () => + this.minigame.startKeyAttack('nested', cardData)); + actions.appendChild(nestedBtn); + } 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_Classic_Custom_Keys') { + const keysKnown = cardData.rfid_data?.sectors ? + Object.keys(cardData.rfid_data.sectors).length : 0; + + if (keysKnown === 0) { + // No keys - suggest Darkside + 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); + + // Dictionary unlikely but allow try + const dictBtn = document.createElement('div'); + dictBtn.className = 'flipper-menu-item flipper-menu-item-dim'; + dictBtn.textContent = ' Dictionary Attack (unlikely)'; + dictBtn.addEventListener('click', () => + this.minigame.startKeyAttack('dictionary', cardData)); + actions.appendChild(dictBtn); + } else if (keysKnown < 16) { + // Some keys - nested attack + const nestedBtn = document.createElement('div'); + nestedBtn.className = 'flipper-menu-item'; + nestedBtn.textContent = `> Nested Attack (~10 sec)`; + nestedBtn.addEventListener('click', () => + this.minigame.startKeyAttack('nested', cardData)); + actions.appendChild(nestedBtn); + } else { + // All keys + 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') { + // UID only + 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 + 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); + } + /** * Show card reading screen (clone mode) */ @@ -365,39 +532,68 @@ export class RFIDUIRenderer { } /** - * Show card data screen after reading + * Show card data screen after reading (supports all protocols) * @param {Object} cardData - Read card data */ showCardDataScreen(cardData) { const screen = this.getScreen(); screen.innerHTML = ''; + // Get protocol-specific display data + const displayData = this.dataManager.getCardDisplayData(cardData); + // Breadcrumb const breadcrumb = document.createElement('div'); breadcrumb.className = 'flipper-breadcrumb'; breadcrumb.textContent = 'RFID > Read'; screen.appendChild(breadcrumb); - // Protocol - const protocol = document.createElement('div'); - protocol.className = 'flipper-info'; - protocol.textContent = 'EM-Micro EM4100'; - screen.appendChild(protocol); + // Protocol header + const protocolHeader = document.createElement('div'); + protocolHeader.className = 'flipper-protocol-header'; + protocolHeader.style.borderLeft = `4px solid ${displayData.color}`; + protocolHeader.innerHTML = ` +
+ ${displayData.icon} + ${displayData.protocolName} +
+
+ ${displayData.frequency} + + ${displayData.security.toUpperCase()} + +
+ `; + screen.appendChild(protocolHeader); - // Card data - const { facility, cardNumber } = this.dataManager.hexToFacilityCard(cardData.rfid_hex); - const checksum = this.dataManager.calculateChecksum(cardData.rfid_hex); - const dez8 = this.dataManager.toDEZ8(cardData.rfid_hex); + // Security note (if applicable) + if (displayData.securityNote) { + const note = document.createElement('div'); + note.className = 'flipper-info'; + note.textContent = displayData.securityNote; + screen.appendChild(note); + } + // Card data fields const data = document.createElement('div'); data.className = 'flipper-card-data'; - data.innerHTML = ` -
HEX: ${this.dataManager.formatHex(cardData.rfid_hex)}
-
Facility: ${facility}
-
Card: ${cardNumber}
-
Checksum: 0x${checksum.toString(16).toUpperCase().padStart(2, '0')}
-
DEZ 8: ${dez8}
- `; + displayData.fields.forEach(field => { + const fieldDiv = document.createElement('div'); + fieldDiv.innerHTML = `${field.label}: ${field.value}`; + data.appendChild(fieldDiv); + }); + + // For EM4100, add checksum (legacy) + if (displayData.protocol === 'EM4100') { + const hex = cardData.rfid_data?.hex || cardData.rfid_hex; + if (hex) { + const checksum = this.dataManager.calculateChecksum(hex); + const checksumDiv = document.createElement('div'); + checksumDiv.innerHTML = `Checksum: 0x${checksum.toString(16).toUpperCase().padStart(2, '0')}`; + data.appendChild(checksumDiv); + } + } + screen.appendChild(data); // Buttons @@ -406,7 +602,7 @@ export class RFIDUIRenderer { const saveBtn = document.createElement('button'); saveBtn.className = 'flipper-button'; - saveBtn.textContent = 'Save'; + saveBtn.textContent = displayData.protocol === 'MIFARE_DESFire' ? 'Save UID' : 'Save'; saveBtn.addEventListener('click', () => this.minigame.handleSaveCard(cardData)); const cancelBtn = document.createElement('button'); @@ -419,6 +615,99 @@ export class RFIDUIRenderer { screen.appendChild(buttons); } + /** + * Show attack progress screen + * @param {Object} data - Attack data {type, progress, currentSector, etc.} + */ + showAttackProgress(data) { + const screen = this.getScreen(); + screen.innerHTML = ''; + + // Breadcrumb + const breadcrumb = document.createElement('div'); + breadcrumb.className = 'flipper-breadcrumb'; + breadcrumb.textContent = `RFID > ${data.type} Attack`; + screen.appendChild(breadcrumb); + + // Attack type + const type = document.createElement('div'); + type.className = 'flipper-info'; + type.textContent = `${data.type} Attack`; + type.style.fontSize = '18px'; + type.style.marginBottom = '10px'; + screen.appendChild(type); + + // Status + const status = document.createElement('div'); + status.className = 'flipper-info-dim'; + status.id = 'attack-status'; + if (data.currentSector !== undefined) { + status.textContent = `Sector ${data.currentSector}/${data.totalSectors || 16}`; + } else if (data.sectorsRemaining !== undefined) { + status.textContent = `${data.sectorsRemaining} sectors remaining`; + } else { + status.textContent = 'Working...'; + } + screen.appendChild(status); + + // Progress bar + const progressContainer = document.createElement('div'); + progressContainer.className = 'rfid-progress-container'; + progressContainer.style.marginTop = '20px'; + + const progressBar = document.createElement('div'); + progressBar.className = 'rfid-progress-bar'; + progressBar.id = 'attack-progress-bar'; + progressBar.style.width = `${data.progress || 0}%`; + + progressContainer.appendChild(progressBar); + screen.appendChild(progressContainer); + + // Percentage + const percentage = document.createElement('div'); + percentage.className = 'flipper-info'; + percentage.id = 'attack-percentage'; + percentage.textContent = `${Math.floor(data.progress || 0)}%`; + percentage.style.textAlign = 'center'; + percentage.style.marginTop = '10px'; + screen.appendChild(percentage); + } + + /** + * Update attack progress + * @param {Object} data - Progress data + */ + updateAttackProgress(data) { + const progressBar = document.getElementById('attack-progress-bar'); + const status = document.getElementById('attack-status'); + const percentage = document.getElementById('attack-percentage'); + + if (progressBar) { + progressBar.style.width = `${data.progress}%`; + + // Change color based on progress + if (data.progress < 50) { + progressBar.style.backgroundColor = '#FF8200'; + } else if (data.progress < 100) { + progressBar.style.backgroundColor = '#FFA500'; + } else { + progressBar.style.backgroundColor = '#00FF00'; + } + } + + if (status) { + if (data.currentSector !== undefined) { + status.textContent = `Sector ${data.currentSector}/${data.totalSectors || 16}`; + } else if (data.sectorsRemaining !== undefined) { + status.textContent = `${data.sectorsRemaining} sectors remaining`; + } + } + + if (percentage) { + percentage.textContent = `${Math.floor(data.progress)}%`; + } + } + /** * Show success message * @param {string} message - Success message diff --git a/js/systems/unlock-system.js b/js/systems/unlock-system.js index 3632534..00ba13b 100644 --- a/js/systems/unlock-system.js +++ b/js/systems/unlock-system.js @@ -304,8 +304,15 @@ export function handleUnlock(lockable, type) { case 'rfid': console.log('RFID LOCK UNLOCK ATTEMPT'); - const requiredCardId = lockRequirements.requires; - console.log('RFID CARD REQUIRED', requiredCardId); + + // Support both single card ID (legacy) and array of card IDs + const requiredCardIds = Array.isArray(lockRequirements.requires) ? + lockRequirements.requires : [lockRequirements.requires]; + + // Check if door accepts UID-only emulation (for DESFire cards) + const acceptsUIDOnly = lockRequirements.acceptsUIDOnly || false; + + console.log('RFID CARD REQUIRED', requiredCardIds, 'acceptsUIDOnly:', acceptsUIDOnly); // Check for keycards in inventory const keycards = window.inventory.items.filter(item => @@ -313,6 +320,11 @@ export function handleUnlock(lockable, type) { item.scenarioData.type === 'keycard' ); + // Check if any physical card matches + const hasValidCard = keycards.some(card => + requiredCardIds.includes(card.scenarioData.card_id || card.scenarioData.key_id) + ); + // Check for RFID cloner with saved cards const cloner = window.inventory.items.find(item => item && item.scenarioData && @@ -322,22 +334,28 @@ export function handleUnlock(lockable, type) { const hasCloner = !!cloner; const savedCards = cloner?.scenarioData?.saved_cards || []; - // Combine available cards - const availableCards = [...keycards]; + // Check if any saved card matches + const hasValidClone = savedCards.some(card => + requiredCardIds.includes(card.card_id || card.key_id) + ); console.log('RFID CHECK', { - requiredCardId, + requiredCardIds, + acceptsUIDOnly, hasCloner, keycardsCount: keycards.length, - savedCardsCount: savedCards.length + savedCardsCount: savedCards.length, + hasValidCard, + hasValidClone }); if (keycards.length > 0 || savedCards.length > 0) { // Start RFID minigame in unlock mode window.startRFIDMinigame(lockable, type, { mode: 'unlock', - requiredCardId: requiredCardId, - availableCards: availableCards, + requiredCardIds: requiredCardIds, // Pass array + acceptsUIDOnly: acceptsUIDOnly, + availableCards: keycards, hasCloner: hasCloner, onComplete: (success) => { if (success) { 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 index b7f15ff..5a8e7c8 100644 --- a/planning_notes/rfid_keycard/protocols_and_interactions/00_IMPLEMENTATION_SUMMARY.md +++ b/planning_notes/rfid_keycard/protocols_and_interactions/00_IMPLEMENTATION_SUMMARY.md @@ -1,41 +1,105 @@ # RFID Protocols - Implementation Summary (Revised) **Status**: Ready for Implementation -**Estimated Time**: 14 hours (down from 19 hours) -**Based On**: Critical review improvements applied +**Estimated Time**: 14 hours +**Last Updated**: After protocol split and card_id simplification ## Changes from Original Plan +โœ… **Split MIFARE Classic** - Now two protocols (weak defaults vs custom keys) +โœ… **Simplified card data** - Uses card_id like keys, generates technical data automatically โœ… **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 +โœ… **Added error handling** - Protocol checks, UID acceptance rules โœ… **Improved code organization** - Constants for timing, better structure -## Three-Protocol System +## Four-Protocol System ### EM4100 (Low Security) - **Status**: Already implemented - **Clone**: Instant, always works - **Emulate**: Perfect emulation +- **Tech**: 125kHz, read-only, no encryption - **Gameplay**: Entry-level cards, no challenge -### MIFARE Classic (Medium Security) +### MIFARE Classic - Weak Defaults (Low Security) - **Status**: New implementation needed -- **Clone**: Requires authentication keys +- **Clone**: Dictionary attack succeeds instantly (~95% success rate) +- **Emulate**: Perfect emulation once cloned +- **Tech**: 13.56MHz, encrypted but uses factory default keys (FFFFFFFFFFFF) +- **Gameplay**: Slightly more interesting than EM4100, but still trivial +- **Real-world**: Cheap hotels, old transit cards, poorly maintained systems + +### MIFARE Classic - Custom Keys (Medium Security) +- **Status**: New implementation needed +- **Clone**: Requires Darkside attack (~30 seconds) +- **Emulate**: Perfect emulation once cloned +- **Tech**: 13.56MHz, encrypted with custom keys - **Attacks**: - - Dictionary (instant) - Try common keys - - Darkside (30 sec) - Crack keys from scratch - - Nested (10 sec) - Crack remaining keys + - Dictionary (instant) - Fails (0% success for custom keys) + - Darkside (30 sec) - Cracks all 16 sectors + - Nested (10 sec) - If you have one key, crack the rest - **Gameplay**: Puzzle element, adds time pressure +- **Real-world**: Corporate badges, banks, government facilities ### MIFARE DESFire (High Security) - **Status**: New implementation needed - **Clone**: Impossible - UID only -- **Emulate**: UID emulation works on low-security readers only +- **Emulate**: UID emulation works only on `acceptsUIDOnly: true` readers +- **Tech**: 13.56MHz, strong encryption (3DES/AES) - **Gameplay**: Forces physical theft or social engineering +- **Real-world**: High-security government, military, modern banking + +## Card Data Simplification + +### Key Innovation: card_id Pattern + +Cards now use `card_id` (like keys use `key_id`), and technical RFID data is **generated deterministically**: + +**Scenario JSON (Simple):** +```json +{ + "type": "keycard", + "card_id": "employee_badge", + "rfid_protocol": "EM4100", + "name": "Employee Badge" +} +``` + +**Runtime (Auto-generated):** +```json +{ + "type": "keycard", + "card_id": "employee_badge", + "rfid_protocol": "EM4100", + "name": "Employee Badge", + "rfid_data": { + "cardId": "employee_badge", + "hex": "A1B2C3D4E5", // Generated from card_id seed + "facility": 161, + "cardNumber": 45926 + } +} +``` + +### Benefits: + +1. **No manual hex/UID specification** - Generated automatically +2. **Deterministic** - Same card_id always generates same technical data +3. **Multiple cards, same access** - Like keys, multiple cards can share card_id +4. **Cleaner scenarios** - Scenario designers don't need to understand RFID protocols + +### Door Configuration (Multiple Valid Cards): + +```json +{ + "locked": true, + "lockType": "rfid", + "requires": ["employee_badge", "contractor_badge", "security_badge"], + "acceptsUIDOnly": false +} +``` ## Implementation Phases (Revised) @@ -59,8 +123,24 @@ export const RFID_PROTOCOLS = { icon: 'โš ๏ธ' }, - 'MIFARE_Classic': { - name: 'MIFARE Classic 1K', + 'MIFARE_Classic_Weak_Defaults': { + name: 'MIFARE Classic 1K (Default Keys)', + frequency: '13.56MHz', + security: 'low', + capabilities: { + read: true, // Dictionary attack works + clone: true, + emulate: true + }, + attackTime: 'instant', + sectors: 16, + hexLength: 8, + color: '#FF6B6B', // Red like EM4100 - equally weak + icon: 'โš ๏ธ' + }, + + 'MIFARE_Classic_Custom_Keys': { + name: 'MIFARE Classic 1K (Custom Keys)', frequency: '13.56MHz', security: 'medium', capabilities: { @@ -68,9 +148,10 @@ export const RFID_PROTOCOLS = { clone: 'with-keys', emulate: true }, + attackTime: '30sec', sectors: 16, hexLength: 8, - color: '#4ECDC4', + color: '#4ECDC4', // Teal for medium icon: '๐Ÿ”' }, @@ -96,84 +177,102 @@ export const MIFARE_COMMON_KEYS = [ 'A0A1A2A3A4A5', 'D3F7D3F7D3F7', '123456789ABC', - 'AABBCCDDEEFF' - // ... more + 'AABBCCDDEEFF', + 'B0B1B2B3B4B5', + '4D3A99C351DD', + '1A982C7E459A' ]; -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. -} +// Attack timing constants +export const ATTACK_DURATIONS = { + darkside: 30000, // 30 seconds + nested: 10000, // 10 seconds + dictionary: 0 // Instant +}; ``` **File**: `js/minigames/rfid/rfid-data.js` (MODIFY) -Add dual format support (no migration): +Add deterministic generation: ```javascript -import { getProtocolInfo } from './rfid-protocols.js'; - export class RFIDDataManager { /** - * Get hex ID from card (supports old and new formats) + * Generate RFID technical data from card_id + * Same card_id always produces same hex/UID (deterministic) */ - getCardHex(cardData) { - // New format - if (cardData.rfid_data?.hex) { - return cardData.rfid_data.hex; + generateRFIDDataFromCardId(cardId, protocol) { + const seed = this.hashCardId(cardId); + + const data = { + cardId: cardId + }; + + switch (protocol) { + case 'EM4100': + data.hex = this.generateHexFromSeed(seed, 10); + data.facility = (seed % 256); + data.cardNumber = (seed % 65536); + break; + + case 'MIFARE_Classic_Weak_Defaults': + case 'MIFARE_Classic_Custom_Keys': + data.uid = this.generateHexFromSeed(seed, 8); + data.sectors = {}; // Empty until cloned/cracked + break; + + case 'MIFARE_DESFire': + data.uid = this.generateHexFromSeed(seed, 14); + data.masterKeyKnown = false; + break; } - // Old format (backward compat) - if (cardData.rfid_hex) { - return cardData.rfid_hex; - } - - return null; + return data; } /** - * Get UID from card + * Hash card_id to deterministic seed */ - getCardUID(cardData) { - return cardData.rfid_data?.uid || null; + hashCardId(cardId) { + let hash = 0; + for (let i = 0; i < cardId.length; i++) { + const char = cardId.charCodeAt(i); + hash = ((hash << 5) - hash) + char; + hash = hash & hash; + } + return Math.abs(hash); } /** - * Detect protocol from card data + * Generate hex string from seed using LCG */ - detectProtocol(cardData) { - // Explicit protocol - if (cardData.rfid_protocol) { - return cardData.rfid_protocol; + generateHexFromSeed(seed, length) { + let hex = ''; + let currentSeed = seed; + + for (let i = 0; i < length; i++) { + currentSeed = (currentSeed * 1103515245 + 12345) & 0x7fffffff; + hex += (currentSeed % 16).toString(16).toUpperCase(); } - // 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 + return hex; } /** - * Get display data for card based on protocol + * Get card display data (supports card_id or legacy formats) */ getCardDisplayData(cardData) { const protocol = this.detectProtocol(cardData); const protocolInfo = getProtocolInfo(protocol); + // Ensure rfid_data exists + if (!cardData.rfid_data && cardData.card_id) { + cardData.rfid_data = this.generateRFIDDataFromCardId( + cardData.card_id, + protocol + ); + } + const displayData = { protocol: protocol, protocolName: protocolInfo.name, @@ -186,20 +285,18 @@ export class RFIDDataManager { 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; - + const hex = cardData.rfid_data?.hex || cardData.rfid_hex; displayData.fields = [ { label: 'HEX', value: this.formatHex(hex) }, - { label: 'Facility', value: facility }, - { label: 'Card', value: cardNumber }, + { label: 'Facility', value: cardData.rfid_data?.facility || 0 }, + { label: 'Card', value: cardData.rfid_data?.cardNumber || 0 }, { label: 'DEZ 8', value: this.toDEZ8(hex) } ]; break; - case 'MIFARE_Classic': - const uid = this.getCardUID(cardData); + case 'MIFARE_Classic_Weak_Defaults': + case 'MIFARE_Classic_Custom_Keys': + const uid = cardData.rfid_data?.uid; const keysKnown = cardData.rfid_data?.sectors ? Object.keys(cardData.rfid_data.sectors).length : 0; @@ -210,24 +307,29 @@ export class RFIDDataManager { { label: 'Readable', value: keysKnown === 16 ? 'Yes โœ“' : 'Partial' }, { label: 'Clonable', value: keysKnown > 0 ? 'Partial' : 'No' } ]; + + // Add security note + if (protocol === 'MIFARE_Classic_Weak_Defaults') { + displayData.securityNote = 'Uses factory default keys'; + } else { + displayData.securityNote = 'Uses custom encryption keys'; + } break; case 'MIFARE_DESFire': - const desUID = this.getCardUID(cardData); - + const desUID = cardData.rfid_data?.uid; displayData.fields = [ { label: 'UID', value: this.formatHex(desUID) }, { label: 'Type', value: 'EV2' }, { label: 'Encryption', value: '3DES/AES' }, { label: 'Clonable', value: 'UID Only' } ]; + displayData.securityNote = 'High security - full clone impossible'; break; } return displayData; } - - // ... existing methods remain unchanged } ``` @@ -237,19 +339,20 @@ export class RFIDDataManager { **File**: `js/minigames/rfid/rfid-ui.js` (MODIFY) -Update card data screen to use protocol display: +Update to show protocol-specific information: ```javascript -showCardDataScreen(cardData) { +showProtocolInfo(cardData) { const screen = this.getScreen(); screen.innerHTML = ''; const displayData = this.dataManager.getCardDisplayData(cardData); + const protocol = displayData.protocol; // Breadcrumb const breadcrumb = document.createElement('div'); breadcrumb.className = 'flipper-breadcrumb'; - breadcrumb.textContent = 'RFID > Read'; + breadcrumb.textContent = 'RFID > Info'; screen.appendChild(breadcrumb); // Protocol header with icon and color @@ -270,6 +373,14 @@ showCardDataScreen(cardData) { `; screen.appendChild(header); + // Security note + if (displayData.securityNote) { + const note = document.createElement('div'); + note.className = 'flipper-info'; + note.textContent = displayData.securityNote; + screen.appendChild(note); + } + // Card data fields const dataDiv = document.createElement('div'); dataDiv.className = 'flipper-card-data'; @@ -280,100 +391,31 @@ showCardDataScreen(cardData) { }); 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 + // Actions based on protocol const actions = document.createElement('div'); actions.className = 'flipper-menu'; actions.style.marginTop = '20px'; - const protocol = displayData.protocol; - - if (protocol === 'MIFARE_Classic') { + if (protocol === 'MIFARE_Classic_Weak_Defaults') { 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 + // Suggest dictionary first 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 + // Some keys found const nestedBtn = document.createElement('div'); nestedBtn.className = 'flipper-menu-item'; - nestedBtn.textContent = `> Nested Attack (crack ${16 - keysKnown} sectors)`; + nestedBtn.textContent = `> Nested Attack (${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'); @@ -383,20 +425,56 @@ showProtocolInfo(cardData) { 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); + } else if (protocol === 'MIFARE_Classic_Custom_Keys') { + const keysKnown = cardData.rfid_data?.sectors ? + Object.keys(cardData.rfid_data.sectors).length : 0; + + if (keysKnown === 0) { + // No keys - suggest Darkside + 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); + + // Dictionary unlikely but allow try + const dictBtn = document.createElement('div'); + dictBtn.className = 'flipper-menu-item'; + dictBtn.textContent = ' Dictionary Attack (unlikely)'; + dictBtn.addEventListener('click', () => + this.minigame.startKeyAttack('dictionary', cardData)); + actions.appendChild(dictBtn); + } else if (keysKnown < 16) { + // Some keys - nested attack + const nestedBtn = document.createElement('div'); + nestedBtn.className = 'flipper-menu-item'; + nestedBtn.textContent = `> Nested Attack (~10 sec)`; + nestedBtn.addEventListener('click', () => + this.minigame.startKeyAttack('nested', cardData)); + actions.appendChild(nestedBtn); + } else { + // All keys + 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') { + // UID only 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 + // EM4100 - instant const readBtn = document.createElement('div'); readBtn.className = 'flipper-menu-item'; readBtn.textContent = '> Read & Clone'; @@ -415,64 +493,6 @@ showProtocolInfo(cardData) { } ``` -**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) @@ -480,14 +500,7 @@ showProtocolInfo(cardData) { **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 -}; +import { MIFARE_COMMON_KEYS, ATTACK_DURATIONS } from './rfid-protocols.js'; export class MIFAREAttackManager { constructor() { @@ -495,24 +508,26 @@ export class MIFAREAttackManager { } /** - * Dictionary attack - try common keys (instant) + * Dictionary attack - protocol-aware success rates */ - dictionaryAttack(uid, existingKeys = {}) { - console.log('๐Ÿ”“ Dictionary attack on', uid); + dictionaryAttack(uid, existingKeys = {}, protocol) { + console.log(`๐Ÿ”“ Dictionary attack on ${uid} (${protocol})`); const foundKeys = { ...existingKeys }; let newKeysFound = 0; + // Success rate based on protocol + const successRate = protocol === 'MIFARE_Classic_Weak_Defaults' ? 0.95 : 0.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; - } + if (Math.random() < successRate) { + foundKeys[sector] = { + keyA: MIFARE_COMMON_KEYS[0], // FFFFFFFFFFFF + keyB: MIFARE_COMMON_KEYS[0] + }; + newKeysFound++; } } @@ -520,19 +535,33 @@ export class MIFAREAttackManager { success: newKeysFound > 0, foundKeys: foundKeys, newKeysFound: newKeysFound, - message: newKeysFound > 0 ? - `Found ${newKeysFound} sector(s) using common keys!` : - 'No common keys found - try Darkside attack' + message: this.getDictionaryMessage(newKeysFound, protocol) }; } - /** - * Darkside attack - crack all keys (30 sec) - */ - async startDarksideAttack(uid, progressCallback) { - console.log('๐Ÿ”“ Darkside attack on', uid); + getDictionaryMessage(found, protocol) { + if (found === 16) { + return '๐Ÿ”“ All sectors use factory defaults!'; + } else if (found > 0) { + return `๐Ÿ”“ Found ${found} sectors with default keys`; + } else if (protocol === 'MIFARE_Classic_Weak_Defaults') { + return 'โš ๏ธ Some sectors have custom keys - try Nested attack'; + } else { + return 'โš ๏ธ No default keys - use Darkside attack'; + } + } - return new Promise((resolve, reject) => { + /** + * Darkside attack - crack all keys (30 sec or 10 sec for weak) + */ + async startDarksideAttack(uid, progressCallback, protocol) { + console.log(`๐Ÿ”“ Darkside attack on ${uid}`); + + // Weak defaults crack faster + const duration = protocol === 'MIFARE_Classic_Weak_Defaults' ? + 10000 : ATTACK_DURATIONS.darkside; + + return new Promise((resolve) => { const attack = { type: 'darkside', uid: uid, @@ -542,7 +571,6 @@ export class MIFAREAttackManager { this.activeAttacks.set(uid, attack); - const duration = ATTACK_DURATIONS.darkside; const updateInterval = 500; let elapsed = 0; @@ -551,7 +579,7 @@ export class MIFAREAttackManager { const progress = Math.min(100, (elapsed / duration) * 100); const currentSector = Math.floor((progress / 100) * 16); - // Add found keys progressively + // Add keys progressively for (let i = 0; i < currentSector; i++) { if (!attack.foundKeys[i]) { attack.foundKeys[i] = { @@ -561,7 +589,6 @@ export class MIFAREAttackManager { } } - // Progress callback if (progressCallback) { progressCallback({ progress: progress, @@ -570,11 +597,10 @@ export class MIFAREAttackManager { }); } - // Complete if (progress >= 100) { clearInterval(interval); - // Ensure all 16 sectors have keys + // Ensure all 16 sectors for (let i = 0; i < 16; i++) { if (!attack.foundKeys[i]) { attack.foundKeys[i] = { @@ -594,7 +620,6 @@ export class MIFAREAttackManager { } }, updateInterval); - // Store interval for cleanup attack.interval = interval; }); } @@ -603,10 +628,10 @@ export class MIFAREAttackManager { * Nested attack - crack remaining keys (10 sec) */ async startNestedAttack(uid, knownKeys, progressCallback) { - console.log('๐Ÿ”“ Nested attack on', uid); + 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 Promise.reject(new Error('Need at least one known key')); } return new Promise((resolve) => { @@ -630,11 +655,9 @@ export class MIFAREAttackManager { 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] = { @@ -671,30 +694,19 @@ export class MIFAREAttackManager { }); } - /** - * 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); - } + if (attack.interval) clearInterval(attack.interval); }); this.activeAttacks.clear(); } - /** - * Cancel specific attack - */ cancelAttack(uid) { const attack = this.activeAttacks.get(uid); if (attack && attack.interval) { @@ -704,476 +716,52 @@ export class MIFAREAttackManager { } } -// 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) +### Phase 4: Unlock System Integration (2h) **File**: `js/systems/unlock-system.js` (MODIFY) -Add UID-only acceptance logic: +Update to use card_id matching: ```javascript case 'rfid': - const requiredCardId = lockRequirements.requires; - const acceptsUIDOnly = lockRequirements.acceptsUIDOnly || false; // NEW + const requiredCardIds = Array.isArray(lockRequirements.requires) ? + lockRequirements.requires : [lockRequirements.requires]; + const acceptsUIDOnly = lockRequirements.acceptsUIDOnly || false; + // Check physical keycards const keycards = window.inventory.items.filter(item => item && item.scenarioData && item.scenarioData.type === 'keycard' ); + // Check if any physical card matches + const hasValidCard = keycards.some(card => + requiredCardIds.includes(card.scenarioData.card_id) + ); + + // Check cloner saved cards const cloner = window.inventory.items.find(item => item && item.scenarioData && item.scenarioData.type === 'rfid_cloner' ); + const hasValidClone = cloner?.scenarioData?.saved_cards?.some(card => + requiredCardIds.includes(card.card_id) + ); + if (keycards.length > 0 || cloner?.scenarioData?.saved_cards?.length > 0) { window.startRFIDMinigame(lockable, type, { mode: 'unlock', - requiredCardId: requiredCardId, + requiredCardIds: requiredCardIds, // Pass array availableCards: keycards, hasCloner: !!cloner, - acceptsUIDOnly: acceptsUIDOnly, // Pass to minigame + acceptsUIDOnly: acceptsUIDOnly, onComplete: (success) => { if (success) { unlockTarget(lockable, type, lockable.layer); @@ -1188,133 +776,289 @@ case 'rfid': **File**: `js/minigames/rfid/rfid-minigame.js` (MODIFY) -Check UID-only acceptance in handleEmulate: +Update unlock matching logic: ```javascript +handleCardTap(card) { + console.log('๐Ÿ“ก Card tapped:', card.scenarioData?.name); + + const cardId = card.scenarioData?.card_id; + const isCorrect = this.requiredCardIds.includes(cardId); + + if (isCorrect) { + this.animations.showTapSuccess(); + this.ui.showSuccess('Access Granted'); + setTimeout(() => this.complete(true), 1500); + } else { + this.animations.showTapFailure(); + this.ui.showError('Access Denied'); + setTimeout(() => this.ui.showTapInterface(), 1500); + } +} + handleEmulate(savedCard) { console.log('๐Ÿ“ก Emulating card:', savedCard.name); - const cardId = savedCard.key_id; - const isCorrect = cardId === this.requiredCardId; + const cardId = savedCard.card_id; + const isCorrect = this.requiredCardIds.includes(cardId); - // Check if this is UID-only emulation + // Check if 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); + setTimeout(() => this.ui.showSavedCards(), 2000); return; } - // Normal emulation check if (isCorrect) { this.animations.showEmulationSuccess(); this.ui.showSuccess('Access Granted'); - // ... rest of success code + setTimeout(() => this.complete(true), 2000); } else { this.animations.showEmulationFailure(); this.ui.showError('Access Denied'); - // ... rest of failure code + setTimeout(() => this.ui.showSavedCards(), 1500); } } ``` --- -## Test Scenarios +### Phase 5: Ink Integration (2h) -### Scenario 1: EM4100 (Already Exists) -File: `scenarios/test-rfid.json` -- Current test scenario works as-is -- No changes needed +**File**: `js/minigames/person-chat/person-chat-conversation.js` (MODIFY) -### Scenario 2: MIFARE Classic -File: `scenarios/test-rfid-mifare.json` (NEW) +```javascript +import { getProtocolInfo } from '../rfid/rfid-protocols.js'; -```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" - }] +syncCardProtocolsToInk() { + if (!this.inkEngine || !this.npc || !this.npc.itemsHeld) return; + + 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}`; + + // Ensure rfid_data exists + if (!card.rfid_data && card.card_id) { + card.rfid_data = window.rfidDataManager.generateRFIDDataFromCardId( + card.card_id, + protocol + ); } - } + + try { + this.inkEngine.setVariable(`${prefix}_protocol`, protocol); + this.inkEngine.setVariable(`${prefix}_name`, card.name || 'Card'); + this.inkEngine.setVariable(`${prefix}_card_id`, card.card_id); + this.inkEngine.setVariable(`${prefix}_security`, protocolInfo.security); + + // Simplified booleans + const isInstantClone = protocol === 'EM4100' || + protocol === 'MIFARE_Classic_Weak_Defaults'; + this.inkEngine.setVariable(`${prefix}_instant_clone`, isInstantClone); + + const needsAttack = protocol === 'MIFARE_Classic_Custom_Keys'; + this.inkEngine.setVariable(`${prefix}_needs_attack`, needsAttack); + + const isUIDOnly = protocol === 'MIFARE_DESFire'; + this.inkEngine.setVariable(`${prefix}_uid_only`, isUIDOnly); + + // Set UID or hex + if (card.rfid_data?.uid) { + this.inkEngine.setVariable(`${prefix}_uid`, card.rfid_data.uid); + } + if (card.rfid_data?.hex) { + this.inkEngine.setVariable(`${prefix}_hex`, card.rfid_data.hex); + } + + console.log(`โœ… Synced ${prefix}: ${protocol} (card_id: ${card.card_id})`); + } catch (err) { + console.warn(`โš ๏ธ Could not sync card protocol:`, err.message); + } + }); +} + +// Call in setupExternalFunctions() +setupExternalFunctions() { + // ... existing code + this.syncItemsToInk(); + this.syncCardProtocolsToInk(); // ADD } ``` -### Scenario 3: MIFARE DESFire -File: `scenarios/test-rfid-desfire.json` (NEW) +**Documentation**: Create `scenarios/ink/README_RFID_VARIABLES.md` + +```markdown +# RFID Protocol Variables for Ink + +## Required Variable Declarations + +```ink +// Card protocol info (auto-synced from NPC itemsHeld) +VAR card_protocol = "" // Protocol name +VAR card_name = "" // Display name +VAR card_card_id = "" // Logical card ID +VAR card_uid = "" // For MIFARE cards +VAR card_hex = "" // For EM4100 cards +VAR card_security = "" // "low", "medium", "high" +VAR card_instant_clone = false // true for EM4100 and weak MIFARE +VAR card_needs_attack = false // true for custom key MIFARE +VAR card_uid_only = false // true for DESFire +``` + +## Usage Examples + +### EM4100 (instant): +```ink +{card_instant_clone && card_protocol == "EM4100": + + [Scan badge] + # clone_keycard:{card_card_id} + -> cloned +} +``` + +### MIFARE Weak Defaults (instant dictionary): +```ink +{card_instant_clone && card_protocol == "MIFARE_Classic_Weak_Defaults": + + [Scan badge] + # clone_keycard:{card_card_id} + It uses default keys - dictionary attack succeeds instantly! + -> cloned +} +``` + +### MIFARE Custom Keys (needs attack): +```ink +{card_needs_attack: + + [Scan badge] + # save_uid_and_start_attack:{card_card_id}|{card_uid} + Custom keys detected. Starting Darkside attack... + -> wait_for_attack +} +``` + +### DESFire (UID only): +```ink +{card_uid_only: + + [Try to scan] + # save_uid_only:{card_card_id}|{card_uid} + High security card - you can only save the UID. + -> uid_saved +} +``` +``` + +--- + +## Example Scenarios + +### Scenario 1: Hotel (Weak MIFARE) ```json { - "name": "RFID DESFire Test", - "startRoom": "test_lobby", + "name": "Hotel Test", + "startRoom": "lobby", "rooms": { - "test_lobby": { + "lobby": { "type": "room_reception", "objects": [ { "type": "keycard", - "name": "High Security Badge", - "rfid_protocol": "MIFARE_DESFire", - "rfid_data": { - "uid": "04AB12CD3456E0" - }, - "key_id": "high_security_badge" + "card_id": "room_301", + "rfid_protocol": "MIFARE_Classic_Weak_Defaults", + "name": "Room 301 Keycard" + }, + { + "type": "keycard", + "card_id": "master_hotel", + "rfid_protocol": "MIFARE_Classic_Weak_Defaults", + "name": "Hotel Master Key" }, { "type": "rfid_cloner", "name": "Flipper Zero" } ], + "doors": [{ + "locked": true, + "lockType": "rfid", + "requires": ["room_301", "master_hotel"] + }] + } + } +} +``` + +### Scenario 2: Corporate (Custom Keys) + +```json +{ + "name": "Corporate Office", + "startRoom": "reception", + "rooms": { + "reception": { + "type": "room_reception", + "npcs": [{ + "id": "guard", + "itemsHeld": [{ + "type": "keycard", + "card_id": "security_access", + "rfid_protocol": "MIFARE_Classic_Custom_Keys", + "name": "Security Badge" + }] + }], + "objects": [{ + "type": "rfid_cloner", + "name": "Flipper Zero" + }], + "doors": [{ + "locked": true, + "lockType": "rfid", + "requires": "security_access", + "acceptsUIDOnly": false + }] + } + } +} +``` + +### Scenario 3: Bank (DESFire) + +```json +{ + "name": "Bank Vault", + "startRoom": "lobby", + "rooms": { + "lobby": { + "type": "room_office", + "objects": [ + { + "type": "keycard", + "card_id": "executive_access", + "rfid_protocol": "MIFARE_DESFire", + "name": "Executive Card" + } + ], "doors": [ { "locked": true, "lockType": "rfid", - "requires": "high_security_badge", + "requires": "executive_access", "acceptsUIDOnly": false, - "description": "Secure reader - requires full auth" + "description": "Vault door - requires full auth" }, { "locked": true, "lockType": "rfid", - "requires": "high_security_badge", + "requires": "executive_access", "acceptsUIDOnly": true, - "description": "Simple reader - accepts UID only" + "description": "Office door - UID check only" } ] } @@ -1327,58 +1071,64 @@ File: `scenarios/test-rfid-desfire.json` (NEW) ## 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 +- [ ] Create `rfid-protocols.js` with 4 protocols +- [ ] Add MIFARE_COMMON_KEYS constant +- [ ] Add ATTACK_DURATIONS constant +- [ ] Add `generateRFIDDataFromCardId()` to rfid-data.js +- [ ] Add `hashCardId()` helper +- [ ] Add `generateHexFromSeed()` helper +- [ ] Update `getCardDisplayData()` for 4 protocols +- [ ] Test deterministic generation ### 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 +- [ ] Update `showProtocolInfo()` for 4 protocols +- [ ] Add protocol-specific action menus +- [ ] Update `showCardDataScreen()` with security notes +- [ ] Add CSS for protocol headers +- [ ] Add CSS for security badges +- [ ] Test UI for all protocols ### 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 +- [ ] Create `rfid-attacks.js` +- [ ] Implement protocol-aware `dictionaryAttack()` +- [ ] Implement `startDarksideAttack()` with variable duration +- [ ] Implement `startNestedAttack()` +- [ ] Add attack UI screens +- [ ] Add `updateAttackProgress()` +- [ ] Integrate into `rfid-minigame.js` +- [ ] Add cleanup logic +- [ ] Test all 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 +### Phase 4: Unlock Integration (2h) +- [ ] Update unlock-system.js to use card_id arrays +- [ ] Update `handleCardTap()` for card_id matching +- [ ] Update `handleEmulate()` with UID-only check +- [ ] Add acceptsUIDOnly door property support +- [ ] Test multiple valid cards per door + +### Phase 5: Ink Integration (2h) +- [ ] Add `syncCardProtocolsToInk()` +- [ ] Add protocol-specific variables +- [ ] Create Ink variable documentation +- [ ] Add `save_uid_only` tag +- [ ] Create example .ink files - [ ] 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 +**Protocol Count**: 4 +- EM4100 (low, instant) +- MIFARE_Classic_Weak_Defaults (low, instant dictionary) +- MIFARE_Classic_Custom_Keys (medium, 30sec Darkside) +- MIFARE_DESFire (high, UID only) -**Quality Improvements**: -- Simpler code (no migration needed) -- Better error handling -- Clearer documentation -- More testable +**Key Features**: +- โœ… card_id pattern (like keys) +- โœ… Deterministic RFID data generation +- โœ… Multiple cards per door +- โœ… Protocol-aware attacks +- โœ… Ink integration with simple variables **Ready for implementation** โœ… diff --git a/planning_notes/rfid_keycard/protocols_and_interactions/03_UPDATES_SUMMARY.md b/planning_notes/rfid_keycard/protocols_and_interactions/03_UPDATES_SUMMARY.md new file mode 100644 index 0000000..98d3c9e --- /dev/null +++ b/planning_notes/rfid_keycard/protocols_and_interactions/03_UPDATES_SUMMARY.md @@ -0,0 +1,466 @@ +# RFID Protocols - Key Updates Summary + +**Date**: Latest Revision +**Status**: Supersedes portions of 01_TECHNICAL_DESIGN.md and 02_IMPLEMENTATION_PLAN.md + +This document summarizes the critical updates made after the initial planning review. + +## Major Changes + +### 1. Four Protocols Instead of Three + +**Original Plan**: 3 protocols (EM4100, MIFARE_Classic, MIFARE_DESFire) + +**Updated Plan**: 4 protocols by splitting MIFARE Classic: + +```javascript +'EM4100' // Low - instant clone +'MIFARE_Classic_Weak_Defaults' // Low - instant dictionary attack +'MIFARE_Classic_Custom_Keys' // Medium - 30sec Darkside attack +'MIFARE_DESFire' // High - UID only +``` + +**Rationale**: MIFARE Classic security depends entirely on configuration. A card with default keys (FFFFFFFFFFFF) is as weak as EM4100, while one with custom keys requires real effort to crack. + +### 2. Simplified Card Data Format + +**Original Plan**: Manual hex/UID specification in scenarios: +```json +{ + "type": "keycard", + "rfid_hex": "01AB34CD56", + "rfid_facility": 1, + "rfid_card_number": 43981, + "rfid_protocol": "EM4100", + "key_id": "employee_badge" +} +``` + +**Updated Plan**: card_id with automatic generation: +```json +{ + "type": "keycard", + "card_id": "employee_badge", + "rfid_protocol": "EM4100", + "name": "Employee Badge" +} +``` + +**Benefits**: +- Matches existing key system pattern +- No manual hex/UID needed - generated deterministically from card_id +- Multiple cards can share same card_id (like keys) +- Cleaner scenarios + +### 3. Protocol-Specific Attack Behavior + +**Dictionary Attack**: +- `MIFARE_Classic_Weak_Defaults`: 95% success rate (most sectors use FFFFFFFFFFFF) +- `MIFARE_Classic_Custom_Keys`: 0% success rate (no default keys) + +**Darkside Attack**: +- `MIFARE_Classic_Weak_Defaults`: 10 seconds (weak crypto) +- `MIFARE_Classic_Custom_Keys`: 30 seconds (normal) + +### 4. Door Lock Configuration + +**Original**: Single card requirement +```json +{ + "lockType": "rfid", + "requires": "employee_badge" +} +``` + +**Updated**: Multiple valid cards (like key system) +```json +{ + "lockType": "rfid", + "requires": ["employee_badge", "contractor_badge", "security_badge"], + "acceptsUIDOnly": false +} +``` + +## Implementation Updates + +### Protocol Definitions + +```javascript +// js/minigames/rfid/rfid-protocols.js + +export const RFID_PROTOCOLS = { + 'EM4100': { + name: 'EM-Micro EM4100', + security: 'low', + color: '#FF6B6B', + icon: 'โš ๏ธ' + }, + + 'MIFARE_Classic_Weak_Defaults': { + name: 'MIFARE Classic 1K (Default Keys)', + security: 'low', // Same as EM4100 + color: '#FF6B6B', // Same color - equally weak + icon: 'โš ๏ธ', + attackTime: 'instant' + }, + + 'MIFARE_Classic_Custom_Keys': { + name: 'MIFARE Classic 1K (Custom Keys)', + security: 'medium', + color: '#4ECDC4', + icon: '๐Ÿ”', + attackTime: '30sec' + }, + + 'MIFARE_DESFire': { + name: 'MIFARE DESFire EV2', + security: 'high', + color: '#95E1D3', + icon: '๐Ÿ”’' + } +}; +``` + +### Deterministic Data Generation + +```javascript +// js/minigames/rfid/rfid-data.js + +export class RFIDDataManager { + /** + * Generate RFID data from card_id (deterministic) + * Same card_id always produces same hex/UID + */ + generateRFIDDataFromCardId(cardId, protocol) { + const seed = this.hashCardId(cardId); + const data = { cardId: cardId }; + + switch (protocol) { + case 'EM4100': + data.hex = this.generateHexFromSeed(seed, 10); + data.facility = (seed % 256); + data.cardNumber = (seed % 65536); + break; + + case 'MIFARE_Classic_Weak_Defaults': + case 'MIFARE_Classic_Custom_Keys': + data.uid = this.generateHexFromSeed(seed, 8); + data.sectors = {}; + break; + + case 'MIFARE_DESFire': + data.uid = this.generateHexFromSeed(seed, 14); + data.masterKeyKnown = false; + break; + } + + return data; + } + + hashCardId(cardId) { + let hash = 0; + for (let i = 0; i < cardId.length; i++) { + const char = cardId.charCodeAt(i); + hash = ((hash << 5) - hash) + char; + hash = hash & hash; + } + return Math.abs(hash); + } + + generateHexFromSeed(seed, length) { + let hex = ''; + let currentSeed = seed; + + for (let i = 0; i < length; i++) { + // Linear congruential generator + currentSeed = (currentSeed * 1103515245 + 12345) & 0x7fffffff; + hex += (currentSeed % 16).toString(16).toUpperCase(); + } + + return hex; + } +} +``` + +### Protocol-Aware Attacks + +```javascript +// js/minigames/rfid/rfid-attacks.js + +export class MIFAREAttackManager { + dictionaryAttack(uid, existingKeys = {}, protocol) { + const foundKeys = { ...existingKeys }; + let newKeysFound = 0; + + // Success rate depends on protocol + const successRate = protocol === 'MIFARE_Classic_Weak_Defaults' ? 0.95 : 0.0; + + for (let sector = 0; sector < 16; sector++) { + if (foundKeys[sector]) continue; + + if (Math.random() < successRate) { + foundKeys[sector] = { + keyA: 'FFFFFFFFFFFF', // Factory default + keyB: 'FFFFFFFFFFFF' + }; + newKeysFound++; + } + } + + return { + success: newKeysFound > 0, + foundKeys: foundKeys, + message: this.getDictionaryMessage(newKeysFound, protocol) + }; + } + + async startDarksideAttack(uid, progressCallback, protocol) { + // Weak defaults crack faster (10 sec vs 30 sec) + const duration = protocol === 'MIFARE_Classic_Weak_Defaults' ? + 10000 : 30000; + + // ... attack implementation with variable duration + } +} +``` + +### Unlock System Changes + +```javascript +// js/systems/unlock-system.js + +case 'rfid': + // Support multiple valid cards + const requiredCardIds = Array.isArray(lockRequirements.requires) ? + lockRequirements.requires : [lockRequirements.requires]; + + const acceptsUIDOnly = lockRequirements.acceptsUIDOnly || false; + + // Check if any physical card matches + const hasValidCard = keycards.some(card => + requiredCardIds.includes(card.scenarioData.card_id) // Match by card_id + ); + + // Check cloner saved cards + const hasValidClone = cloner?.scenarioData?.saved_cards?.some(card => + requiredCardIds.includes(card.card_id) // Match by card_id + ); + + // Pass array of valid IDs to minigame + window.startRFIDMinigame(lockable, type, { + mode: 'unlock', + requiredCardIds: requiredCardIds, // Array + acceptsUIDOnly: acceptsUIDOnly + }); + break; +``` + +### Ink Variables + +```javascript +// js/minigames/person-chat/person-chat-conversation.js + +syncCardProtocolsToInk() { + 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}`; + + // Ensure rfid_data exists (generate if needed) + if (!card.rfid_data && card.card_id) { + card.rfid_data = window.rfidDataManager.generateRFIDDataFromCardId( + card.card_id, + protocol + ); + } + + // Set simplified boolean variables + const isInstantClone = protocol === 'EM4100' || + protocol === 'MIFARE_Classic_Weak_Defaults'; + this.inkEngine.setVariable(`${prefix}_instant_clone`, isInstantClone); + + const needsAttack = protocol === 'MIFARE_Classic_Custom_Keys'; + this.inkEngine.setVariable(`${prefix}_needs_attack`, needsAttack); + + const isUIDOnly = protocol === 'MIFARE_DESFire'; + this.inkEngine.setVariable(`${prefix}_uid_only`, isUIDOnly); + + this.inkEngine.setVariable(`${prefix}_protocol`, protocol); + this.inkEngine.setVariable(`${prefix}_card_id`, card.card_id); + this.inkEngine.setVariable(`${prefix}_security`, protocolInfo.security); + }); +} +``` + +## Scenario Examples + +### Hotel (Weak MIFARE) + +```json +{ + "objects": [ + { + "type": "keycard", + "card_id": "room_301", + "rfid_protocol": "MIFARE_Classic_Weak_Defaults", + "name": "Room 301 Keycard" + }, + { + "type": "keycard", + "card_id": "master_hotel", + "rfid_protocol": "MIFARE_Classic_Weak_Defaults", + "name": "Hotel Master Key" + } + ], + "doors": [{ + "locked": true, + "lockType": "rfid", + "requires": ["room_301", "master_hotel"] + }] +} +``` + +**Player experience**: Dictionary attack instantly finds all default keys โ†’ clone โ†’ use + +### Corporate (Custom Keys) + +```json +{ + "npcs": [{ + "id": "guard", + "itemsHeld": [{ + "type": "keycard", + "card_id": "security_access", + "rfid_protocol": "MIFARE_Classic_Custom_Keys", + "name": "Security Badge" + }] + }], + "doors": [{ + "locked": true, + "lockType": "rfid", + "requires": "security_access" + }] +} +``` + +**Player experience**: Clone from NPC โ†’ Dictionary fails โ†’ Darkside 30 sec โ†’ clone โ†’ use + +### Bank (DESFire) + +```json +{ + "objects": [{ + "type": "keycard", + "card_id": "executive_access", + "rfid_protocol": "MIFARE_DESFire", + "name": "Executive Card" + }], + "doors": [ + { + "locked": true, + "lockType": "rfid", + "requires": "executive_access", + "acceptsUIDOnly": false, + "description": "Vault - requires full auth" + }, + { + "locked": true, + "lockType": "rfid", + "requires": "executive_access", + "acceptsUIDOnly": true, + "description": "Office - accepts UID only" + } + ] +} +``` + +**Player experience**: Can only save UID โ†’ Works on poorly-configured readers โ†’ Doesn't work on secure vault + +## Ink Usage Examples + +### Required Variables + +```ink +VAR card_protocol = "" +VAR card_card_id = "" +VAR card_instant_clone = false +VAR card_needs_attack = false +VAR card_uid_only = false +``` + +### EM4100 + +```ink +{card_instant_clone && card_protocol == "EM4100": + + [Scan their badge] + # clone_keycard:{card_card_id} + You quickly scan their badge. + -> cloned +} +``` + +### MIFARE Weak Defaults + +```ink +{card_instant_clone && card_protocol == "MIFARE_Classic_Weak_Defaults": + + [Scan their badge] + # clone_keycard:{card_card_id} + Your Flipper finds all the default keys instantly! + -> cloned +} +``` + +### MIFARE Custom Keys + +```ink +{card_needs_attack: + + [Try to scan] + The card is encrypted with custom keys. + # save_uid_only:{card_card_id}|{card_uid} + You'll need to run a Darkside attack to clone it fully. + -> uid_saved +} +``` + +### MIFARE DESFire + +```ink +{card_uid_only: + + [Try to scan] + # save_uid_only:{card_card_id}|{card_uid} + High security encryption - you can only save the UID. + -> uid_only +} +``` + +## Key Takeaways + +1. **Four protocols** give meaningful gameplay progression: + - Instant (EM4100, weak MIFARE) + - Quick challenge (custom MIFARE with 30sec attack) + - Impossible/UID-only (DESFire) + +2. **card_id system** simplifies scenarios dramatically: + - No need to specify technical details + - Multiple cards can share access + - Deterministic generation prevents conflicts + +3. **Protocol awareness** makes attacks realistic: + - Dictionary succeeds on weak configs, fails on strong + - Darkside faster on weak keys + - DESFire can't be attacked at all + +4. **Door flexibility** matches key system: + - Multiple valid cards per door + - UID-only acceptance flag for poorly-configured readers + +## Next Steps + +Refer to `00_IMPLEMENTATION_SUMMARY.md` for complete implementation guide with all code examples and checklists. + +The original `01_TECHNICAL_DESIGN.md` and `02_IMPLEMENTATION_PLAN.md` are still valid for overall architecture and file organization, but use this document for: +- Protocol definitions (4 instead of 3) +- Card data format (card_id approach) +- Attack behavior (protocol-specific) +- Scenario structure (simplified JSON)