mirror of
https://github.com/cliffe/BreakEscape.git
synced 2026-02-20 13:50:46 +00:00
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:
@@ -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 {
|
||||
|
||||
330
js/minigames/rfid/rfid-attacks.js
Normal file
330
js/minigames/rfid/rfid-attacks.js
Normal 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;
|
||||
@@ -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'}` };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
183
js/minigames/rfid/rfid-protocols.js
Normal file
183
js/minigames/rfid/rfid-protocols.js
Normal 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();
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
Reference in New Issue
Block a user