feat(rfid): Implement multi-protocol RFID system with 4 protocols

Implement comprehensive multi-protocol RFID system with deterministic
card_id-based generation, MIFARE key attacks, and protocol-specific UI.

## New Protocol System (4 protocols):
- EM4100 (low security) - Instant clone, already implemented
- MIFARE_Classic_Weak_Defaults (low) - Dictionary attack succeeds (95%)
- MIFARE_Classic_Custom_Keys (medium) - Requires Darkside attack (30s)
- MIFARE_DESFire (high) - UID only, forces physical theft

## Key Features:

### 1. Protocol Foundation
- Created rfid-protocols.js with protocol definitions
- Added protocol detection, capabilities, security levels
- Defined attack durations and common MIFARE keys

### 2. Deterministic Card Generation
- Updated rfid-data.js with card_id-based generation
- Same card_id always produces same hex/UID (deterministic)
- Simplified scenario format - no manual hex/UID needed
- getCardDisplayData() supports all protocols

### 3. MIFARE Attack System
- Created rfid-attacks.js with MIFAREAttackManager
- Dictionary Attack: Instant, 95% success on weak defaults
- Darkside Attack: 30 sec (10s on weak), cracks all keys
- Nested Attack: 10 sec, uses known key to crack rest
- Protocol-aware attack behavior

### 4. UI Enhancements
- Updated rfid-ui.js with protocol-specific displays
- showProtocolInfo() with color-coded security badges
- showAttackProgress() and updateAttackProgress()
- Protocol headers with icons and frequency info
- Updated showCardDataScreen() and showEmulationScreen()

### 5. Unlock System Integration
- Updated unlock-system.js for card_id matching
- Support multiple valid cards per door (array)
- Added acceptsUIDOnly flag for DESFire UID emulation
- Backward compatible with legacy key_id format

### 6. Minigame Integration
- Updated rfid-minigame.js with attack methods
- startKeyAttack() triggers dictionary/darkside/nested
- handleCardTap() and handleEmulate() use card_id arrays
- UID-only emulation validation for DESFire
- Attack manager cleanup on minigame exit

### 7. Styling
- Added CSS for protocol headers and security badges
- Color-coded security levels (red=low, teal=medium, green=high)
- Attack progress styling with smooth transitions
- Dimmed menu items for unlikely attack options

## Scenario Format Changes:

Before (manual technical data):
```json
{
  "type": "keycard",
  "rfid_hex": "01AB34CD56",
  "rfid_facility": 1,
  "key_id": "employee_badge"
}
```

After (simplified with card_id):
```json
{
  "type": "keycard",
  "card_id": "employee_badge",
  "rfid_protocol": "MIFARE_Classic_Weak_Defaults",
  "name": "Employee Badge"
}
```

Technical data (hex/UID) generated automatically from card_id.

## Door Configuration:

Multiple valid cards per door:
```json
{
  "lockType": "rfid",
  "requires": ["employee_badge", "contractor_badge", "master_card"],
  "acceptsUIDOnly": false
}
```

## Files Modified:
- js/minigames/rfid/rfid-protocols.js (NEW)
- js/minigames/rfid/rfid-attacks.js (NEW)
- js/minigames/rfid/rfid-data.js
- js/minigames/rfid/rfid-ui.js
- js/minigames/rfid/rfid-minigame.js
- js/systems/unlock-system.js
- css/rfid-minigame.css
- planning_notes/rfid_keycard/protocols_and_interactions/03_UPDATES_SUMMARY.md (NEW)

## Next Steps:
- Phase 5: Ink integration (syncCardProtocolsToInk)
- Test with scenarios for each protocol
- Add Ink variable documentation

Estimated implementation time: ~12 hours (Phases 1-4 complete)
This commit is contained in:
Z. Cliffe Schreuders
2025-11-15 23:48:15 +00:00
parent d697eef3ca
commit 7ecda9d39d
9 changed files with 2318 additions and 878 deletions

View File

@@ -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 {

View File

@@ -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<Object>} {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<Object>} {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;

View File

@@ -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'}` };
}
}

View File

@@ -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');

View File

@@ -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();
}

View File

@@ -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 = `
<div>HEX: ${this.dataManager.formatHex(card.rfid_hex)}</div>
<div>Facility: ${facility}</div>
<div>Card: ${cardNumber}</div>
`;
// 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 = `
<div class="protocol-header-top">
<span class="protocol-icon">${displayData.icon}</span>
<span class="protocol-name">${displayData.protocolName}</span>
</div>
<div class="protocol-meta">
<span>${displayData.frequency}</span>
<span class="security-badge security-${displayData.security}">
${displayData.security.toUpperCase()}
</span>
</div>
`;
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 = `<strong>${field.label}:</strong> ${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 = `
<div class="protocol-header-top">
<span class="protocol-icon">${displayData.icon}</span>
<span class="protocol-name">${displayData.protocolName}</span>
</div>
<div class="protocol-meta">
<span>${displayData.frequency}</span>
<span class="security-badge security-${displayData.security}">
${displayData.security.toUpperCase()}
</span>
</div>
`;
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 = `
<div>HEX: ${this.dataManager.formatHex(cardData.rfid_hex)}</div>
<div>Facility: ${facility}</div>
<div>Card: ${cardNumber}</div>
<div>Checksum: 0x${checksum.toString(16).toUpperCase().padStart(2, '0')}</div>
<div>DEZ 8: ${dez8}</div>
`;
displayData.fields.forEach(field => {
const fieldDiv = document.createElement('div');
fieldDiv.innerHTML = `<strong>${field.label}:</strong> ${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 = `<strong>Checksum:</strong> 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

View File

@@ -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) {

View File

@@ -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)