chore: import local project into Gitea
This commit is contained in:
37
.gitignore
vendored
Normal file
37
.gitignore
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
# OS / tooling
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Editors
|
||||
.cursor/
|
||||
|
||||
# Secrets
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
!.env.template
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
.venv/
|
||||
venv/
|
||||
|
||||
# Node / frontend
|
||||
node_modules/
|
||||
dist/
|
||||
|
||||
# Typical embedded / tooling noise
|
||||
*.log
|
||||
|
||||
# Builds (adjust per subtree if needed)
|
||||
**/build/.ninja_deps
|
||||
**/build/.ninja_log
|
||||
|
||||
# PlatformIO / generated dependencies
|
||||
.pio/
|
||||
.vscode/
|
||||
build/
|
||||
*.bin
|
||||
*.elf
|
||||
*.map
|
||||
120
README.md
Normal file
120
README.md
Normal file
@@ -0,0 +1,120 @@
|
||||
# NFC Scanner - ESP32 & PN532
|
||||
|
||||
A comprehensive NFC/RFID scanner that extracts maximum legal/public data from cards using ESP32-S3 and PN532.
|
||||
|
||||
## Hardware Setup
|
||||
|
||||
### Components
|
||||
- **XIAO ESP32-S3** - Main microcontroller
|
||||
- **Elechouse PN532 NFC Module** - NFC/RFID reader
|
||||
- **Battery** (optional) - For portable operation
|
||||
|
||||
### Wiring (SPI Mode)
|
||||
| XIAO ESP32-S3 | PN532 | Function |
|
||||
|---------------|-------|----------|
|
||||
| GPIO9 (MOSI) | MOSI | SPI Data Out |
|
||||
| GPIO8 (MISO) | MISO | SPI Data In |
|
||||
| GPIO7 (SCK) | SCK | SPI Clock |
|
||||
| GPIO1 (A0/D0) | SS/SDA| Chip Select |
|
||||
| 5V or 3V3 | VCC | Power |
|
||||
| GND | GND | Ground |
|
||||
|
||||
### PN532 Configuration
|
||||
Set your Elechouse PN532 to **SPI mode** using the onboard DIP switches/jumpers according to the module's silkscreen.
|
||||
|
||||
## Software Features
|
||||
|
||||
### NFC Technologies Supported
|
||||
- **ISO14443 Type A** - MIFARE Classic, Ultralight, NTAG
|
||||
- **ISO14443 Type B** - Government/corporate cards
|
||||
- **FeliCa** - Japanese transit cards
|
||||
- **Multi-tech discovery** with anti-collision
|
||||
|
||||
### Data Extraction
|
||||
- **UID/NUID** - Unique identifiers
|
||||
- **ATQA/SAK/ATS** - Protocol parameters
|
||||
- **Type 2 Cards**: CC, TLVs, user pages, GET_VERSION, lock bits, signature
|
||||
- **Type 4 Cards**: NDEF applications, CC/NDEF files, frame size limits
|
||||
- **FeliCa**: System codes, service lists, public blocks
|
||||
- **NDEF Parsing** - All record types and payloads
|
||||
- **Timing Statistics** - Performance metrics
|
||||
|
||||
### Web Interface
|
||||
- **WiFi Access Point** - Creates its own network "NFC-Scanner"
|
||||
- **REST API** - `/version`, `/scan`, `/dump` endpoints
|
||||
- **Simple UI** - Scan and dump buttons with JSON display
|
||||
- **Real-time Results** - Immediate feedback
|
||||
|
||||
## Getting Started
|
||||
|
||||
### 1. Hardware Assembly
|
||||
1. Wire the PN532 to ESP32-S3 according to the wiring table above
|
||||
2. Set PN532 to SPI mode using DIP switches
|
||||
3. Connect battery to BAT pads (optional)
|
||||
|
||||
### 2. Software Setup
|
||||
1. Install PlatformIO in VS Code
|
||||
2. Open this project folder
|
||||
3. Build and upload: `pio run -t upload`
|
||||
|
||||
### 3. Usage
|
||||
1. Power on the device
|
||||
2. Connect to WiFi network: **NFC-Scanner** (password: `nfc123456`)
|
||||
3. Open browser to: `http://192.168.4.1`
|
||||
4. Use Scan/Dump buttons to read NFC cards
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### GET /version
|
||||
Returns PN532 firmware version and system info.
|
||||
|
||||
### GET /scan
|
||||
Performs one-shot multi-tech discovery, returns UID and card type.
|
||||
|
||||
### GET /dump
|
||||
Extracts maximum public data from detected card, includes raw hex and parsed NDEF.
|
||||
|
||||
## Security & Ethics
|
||||
|
||||
- **Read-only by default** - No writing to cards
|
||||
- **Public data only** - No key guessing or unauthorized access
|
||||
- **LAN-only** - WiFi AP mode for local access
|
||||
- **Audit trail** - All responses include timestamps
|
||||
|
||||
## Development Roadmap
|
||||
|
||||
### Current (MVP)
|
||||
- [x] SPI communication with PN532
|
||||
- [x] WiFi Access Point mode
|
||||
- [x] Basic web interface
|
||||
- [x] Multi-tech card detection
|
||||
- [x] Type A card data extraction
|
||||
|
||||
### Future Enhancements
|
||||
- [ ] Full NDEF record parsing
|
||||
- [ ] Type B and FeliCa support
|
||||
- [ ] WebSocket real-time updates
|
||||
- [ ] Scan history and export
|
||||
- [ ] OTA firmware updates
|
||||
- [ ] Advanced mode with user-provided keys
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### PN532 Not Detected
|
||||
- Check SPI wiring connections
|
||||
- Verify PN532 is set to SPI mode
|
||||
- Check power supply (3.3V or 5V depending on module)
|
||||
|
||||
### WiFi Connection Issues
|
||||
- Look for "NFC-Scanner" network
|
||||
- Default password: `nfc123456`
|
||||
- Default IP: `192.168.4.1`
|
||||
|
||||
### Card Reading Issues
|
||||
- Ensure card is close to PN532 antenna
|
||||
- Try different card orientations
|
||||
- Check for metal interference near antenna
|
||||
|
||||
## License
|
||||
|
||||
This project is for educational and research purposes. Respect local laws and card holder privacy.
|
||||
24
platformio.ini
Normal file
24
platformio.ini
Normal file
@@ -0,0 +1,24 @@
|
||||
[env:seeed_xiao_esp32s3]
|
||||
platform = espressif32
|
||||
board = seeed_xiao_esp32s3
|
||||
framework = arduino
|
||||
monitor_speed = 115200
|
||||
upload_speed = 921600
|
||||
upload_port = COM3
|
||||
monitor_port = COM3
|
||||
|
||||
; Library dependencies
|
||||
lib_deps =
|
||||
adafruit/Adafruit PN532@^1.3.1
|
||||
bblanchon/ArduinoJson@^6.21.3
|
||||
https://github.com/me-no-dev/ESPAsyncWebServer.git
|
||||
https://github.com/me-no-dev/AsyncTCP.git
|
||||
|
||||
; Build flags for optimization
|
||||
build_flags =
|
||||
-DCORE_DEBUG_LEVEL=3
|
||||
-DCONFIG_ARDUHAL_LOG_COLORS=1
|
||||
|
||||
; Monitor filters for cleaner output
|
||||
monitor_filters =
|
||||
esp32_exception_decoder, time, default
|
||||
761
src/main.cpp
Normal file
761
src/main.cpp
Normal file
@@ -0,0 +1,761 @@
|
||||
#include <WiFi.h>
|
||||
#include <ESPAsyncWebServer.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <SPI.h>
|
||||
#include <Adafruit_PN532.h>
|
||||
#include "security_research.h"
|
||||
#include "nfc_types.h"
|
||||
|
||||
// Pin definitions for XIAO ESP32-S3
|
||||
#define PN532_SCK 7 // GPIO7 - Hardware SPI SCK
|
||||
#define PN532_MISO 8 // GPIO8 - Hardware SPI MISO
|
||||
#define PN532_MOSI 9 // GPIO9 - Hardware SPI MOSI
|
||||
#define PN532_SS 1 // GPIO1 - Chip Select
|
||||
|
||||
// WiFi AP Configuration
|
||||
const char* ap_ssid = "NFC-Scanner";
|
||||
const char* ap_password = "nfc123456";
|
||||
const IPAddress ap_ip(192, 168, 4, 1);
|
||||
const IPAddress ap_gateway(192, 168, 4, 1);
|
||||
const IPAddress ap_subnet(255, 255, 255, 0);
|
||||
|
||||
// Initialize PN532 with SPI
|
||||
Adafruit_PN532 nfc(PN532_SS);
|
||||
|
||||
// Web server on port 80
|
||||
AsyncWebServer server(80);
|
||||
|
||||
// Global NFC scan data
|
||||
NFCData lastScan;
|
||||
|
||||
// Forward declarations
|
||||
void setupWebServer();
|
||||
String getMainPage();
|
||||
bool performNFCScan();
|
||||
bool performNFCDump();
|
||||
|
||||
// NDEF Record structure
|
||||
struct NDEFRecord {
|
||||
uint8_t tnf;
|
||||
String type;
|
||||
String id;
|
||||
String payload;
|
||||
bool isText = false;
|
||||
bool isURI = false;
|
||||
};
|
||||
|
||||
// Function declarations
|
||||
bool performNFCScan();
|
||||
bool performNFCDump();
|
||||
bool scanTypeA();
|
||||
bool scanTypeB();
|
||||
bool scanFeliCa();
|
||||
bool extractType2Data();
|
||||
bool extractType4Data();
|
||||
bool extractClassicData();
|
||||
bool extractTypeBData();
|
||||
bool extractFeliCaData();
|
||||
bool parseNDEF(uint8_t* data, size_t length);
|
||||
String parseNDEFRecord(uint8_t* data, size_t& offset, size_t maxLength);
|
||||
String determineCardType(uint8_t sak, uint16_t atqa, uint8_t uidLength);
|
||||
void clearScanData();
|
||||
void setupWebServer();
|
||||
String getMainPage();
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
delay(1000);
|
||||
|
||||
Serial.println("NFC Scanner Starting...");
|
||||
|
||||
// Initialize SPI
|
||||
SPI.begin(PN532_SCK, PN532_MISO, PN532_MOSI, PN532_SS);
|
||||
|
||||
// Initialize PN532
|
||||
nfc.begin();
|
||||
|
||||
uint32_t versiondata = nfc.getFirmwareVersion();
|
||||
if (!versiondata) {
|
||||
Serial.println("PN532 not found!");
|
||||
while (1); // halt
|
||||
}
|
||||
|
||||
Serial.print("Found chip PN5"); Serial.println((versiondata>>24) & 0xFF, HEX);
|
||||
Serial.print("Firmware ver. "); Serial.print((versiondata>>16) & 0xFF, DEC);
|
||||
Serial.print('.'); Serial.println((versiondata>>8) & 0xFF, DEC);
|
||||
|
||||
// Configure PN532 for reading
|
||||
nfc.SAMConfig();
|
||||
|
||||
// Setup WiFi Access Point
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAPConfig(ap_ip, ap_gateway, ap_subnet);
|
||||
WiFi.softAP(ap_ssid, ap_password);
|
||||
|
||||
Serial.println("WiFi AP Started");
|
||||
Serial.print("AP IP address: ");
|
||||
Serial.println(WiFi.softAPIP());
|
||||
Serial.print("Connect to WiFi: ");
|
||||
Serial.println(ap_ssid);
|
||||
Serial.print("Password: ");
|
||||
Serial.println(ap_password);
|
||||
|
||||
// Setup web server routes
|
||||
setupWebServer();
|
||||
|
||||
server.begin();
|
||||
Serial.println("Web server started");
|
||||
Serial.println("Ready to scan NFC cards!");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
// Main loop - web server handles requests asynchronously
|
||||
delay(100);
|
||||
}
|
||||
|
||||
void setupWebServer() {
|
||||
// Serve main page
|
||||
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
|
||||
String html = getMainPage();
|
||||
request->send(200, "text/html", html);
|
||||
});
|
||||
|
||||
// API endpoint: Get PN532 version info
|
||||
server.on("/version", HTTP_GET, [](AsyncWebServerRequest *request){
|
||||
DynamicJsonDocument doc(1024);
|
||||
|
||||
uint32_t versiondata = nfc.getFirmwareVersion();
|
||||
doc["chip"] = String("PN5") + String((versiondata>>24) & 0xFF, HEX);
|
||||
doc["firmware_version"] = String((versiondata>>16) & 0xFF) + "." + String((versiondata>>8) & 0xFF);
|
||||
doc["timestamp"] = millis();
|
||||
doc["ap_ip"] = WiFi.softAPIP().toString();
|
||||
doc["connected_clients"] = WiFi.softAPgetStationNum();
|
||||
|
||||
String response;
|
||||
serializeJson(doc, response);
|
||||
request->send(200, "application/json", response);
|
||||
});
|
||||
|
||||
// API endpoint: Scan for NFC card
|
||||
server.on("/scan", HTTP_GET, [](AsyncWebServerRequest *request){
|
||||
DynamicJsonDocument doc(2048);
|
||||
|
||||
bool success = performNFCScan();
|
||||
|
||||
doc["success"] = success;
|
||||
doc["timestamp"] = millis();
|
||||
|
||||
if (success) {
|
||||
doc["card_present"] = true;
|
||||
doc["uid"] = "";
|
||||
for (uint8_t i = 0; i < lastScan.uidLength; i++) {
|
||||
if (i > 0) doc["uid"] = doc["uid"].as<String>() + ":";
|
||||
if (lastScan.uid[i] < 0x10) doc["uid"] = doc["uid"].as<String>() + "0";
|
||||
doc["uid"] = doc["uid"].as<String>() + String(lastScan.uid[i], HEX);
|
||||
}
|
||||
doc["uid_length"] = lastScan.uidLength;
|
||||
doc["technology"] = lastScan.technology;
|
||||
doc["atqa"] = "0x" + String(lastScan.atqa, HEX);
|
||||
doc["sak"] = "0x" + String(lastScan.sak, HEX);
|
||||
doc["card_type"] = lastScan.cardType;
|
||||
doc["scan_time_ms"] = lastScan.scanTime;
|
||||
|
||||
// Add technology-specific data
|
||||
if (lastScan.technology == "FeliCa") {
|
||||
doc["system_code"] = "0x" + String(lastScan.systemCode, HEX);
|
||||
}
|
||||
} else {
|
||||
doc["card_present"] = false;
|
||||
doc["message"] = "No card detected";
|
||||
}
|
||||
|
||||
String response;
|
||||
serializeJson(doc, response);
|
||||
request->send(200, "application/json", response);
|
||||
});
|
||||
|
||||
// API endpoint: Security Analysis (RESEARCH ONLY)
|
||||
server.on("/security", HTTP_GET, [](AsyncWebServerRequest *request){
|
||||
DynamicJsonDocument doc(8192);
|
||||
|
||||
bool success = performSecurityAnalysis();
|
||||
|
||||
doc["success"] = success;
|
||||
doc["timestamp"] = millis();
|
||||
doc["research_only"] = true;
|
||||
doc["legal_disclaimer"] = "FOR EDUCATIONAL AND SECURITY RESEARCH PURPOSES ONLY";
|
||||
|
||||
if (success) {
|
||||
// Card identification
|
||||
doc["card_present"] = true;
|
||||
doc["uid"] = "";
|
||||
for (uint8_t i = 0; i < lastScan.uidLength; i++) {
|
||||
if (i > 0) doc["uid"] = doc["uid"].as<String>() + ":";
|
||||
if (lastScan.uid[i] < 0x10) doc["uid"] = doc["uid"].as<String>() + "0";
|
||||
doc["uid"] = doc["uid"].as<String>() + String(lastScan.uid[i], HEX);
|
||||
}
|
||||
doc["technology"] = lastScan.technology;
|
||||
doc["card_type"] = lastScan.cardType;
|
||||
|
||||
// Security analysis results
|
||||
doc["security_analysis"]["risk_level"] = lastAnalysis.riskLevel;
|
||||
doc["security_analysis"]["has_weak_keys"] = lastAnalysis.hasWeakKeys;
|
||||
doc["security_analysis"]["accessible_sectors"] = lastAnalysis.accessibleSectors;
|
||||
doc["security_analysis"]["total_sectors"] = lastAnalysis.totalSectors;
|
||||
doc["security_analysis"]["key_analysis"] = lastAnalysis.keyAnalysis;
|
||||
doc["security_analysis"]["possible_clone"] = lastAnalysis.possibleClone;
|
||||
doc["security_analysis"]["clone_indicators"] = lastAnalysis.cloneIndicators;
|
||||
doc["security_analysis"]["vulnerabilities"] = lastAnalysis.vulnerabilities;
|
||||
doc["security_analysis"]["rf_fingerprint"] = lastAnalysis.rfFingerprint;
|
||||
doc["security_analysis"]["response_time_us"] = lastAnalysis.responseTime;
|
||||
|
||||
// DESFire specific
|
||||
if (lastScan.cardType.indexOf("DESFire") >= 0) {
|
||||
doc["security_analysis"]["application_count"] = lastAnalysis.applicationCount;
|
||||
doc["security_analysis"]["applications"] = lastAnalysis.applications;
|
||||
doc["security_analysis"]["has_default_keys"] = lastAnalysis.hasDefaultKeys;
|
||||
}
|
||||
|
||||
} else {
|
||||
doc["card_present"] = false;
|
||||
doc["message"] = "No card detected or analysis failed";
|
||||
}
|
||||
|
||||
String response;
|
||||
serializeJson(doc, response);
|
||||
request->send(200, "application/json", response);
|
||||
});
|
||||
|
||||
// API endpoint: Dump card data
|
||||
server.on("/dump", HTTP_GET, [](AsyncWebServerRequest *request){
|
||||
DynamicJsonDocument doc(4096);
|
||||
|
||||
bool success = performNFCDump();
|
||||
|
||||
doc["success"] = success;
|
||||
doc["timestamp"] = millis();
|
||||
|
||||
if (success) {
|
||||
doc["card_present"] = true;
|
||||
doc["uid"] = "";
|
||||
for (uint8_t i = 0; i < lastScan.uidLength; i++) {
|
||||
if (i > 0) doc["uid"] = doc["uid"].as<String>() + ":";
|
||||
if (lastScan.uid[i] < 0x10) doc["uid"] = doc["uid"].as<String>() + "0";
|
||||
doc["uid"] = doc["uid"].as<String>() + String(lastScan.uid[i], HEX);
|
||||
}
|
||||
doc["technology"] = lastScan.technology;
|
||||
doc["card_type"] = lastScan.cardType;
|
||||
doc["scan_time_ms"] = lastScan.scanTime;
|
||||
|
||||
// Add raw data dump
|
||||
doc["raw_data_length"] = lastScan.rawDataLength;
|
||||
doc["raw_data"] = "";
|
||||
for (size_t i = 0; i < lastScan.rawDataLength; i++) {
|
||||
if (i > 0 && i % 16 == 0) doc["raw_data"] = doc["raw_data"].as<String>() + "\n";
|
||||
else if (i > 0) doc["raw_data"] = doc["raw_data"].as<String>() + " ";
|
||||
|
||||
if (lastScan.rawData[i] < 0x10) doc["raw_data"] = doc["raw_data"].as<String>() + "0";
|
||||
doc["raw_data"] = doc["raw_data"].as<String>() + String(lastScan.rawData[i], HEX);
|
||||
}
|
||||
|
||||
// Add Capability Container data
|
||||
if (lastScan.hasCCData) {
|
||||
doc["capability_container"] = "";
|
||||
for (int i = 0; i < 4; i++) {
|
||||
if (i > 0) doc["capability_container"] = doc["capability_container"].as<String>() + " ";
|
||||
if (lastScan.ccData[i] < 0x10) doc["capability_container"] = doc["capability_container"].as<String>() + "0";
|
||||
doc["capability_container"] = doc["capability_container"].as<String>() + String(lastScan.ccData[i], HEX);
|
||||
}
|
||||
}
|
||||
|
||||
// Add NDEF data
|
||||
doc["has_ndef"] = lastScan.hasNDEF;
|
||||
if (lastScan.hasNDEF) {
|
||||
doc["ndef_length"] = lastScan.ndefLength;
|
||||
doc["ndef_records"] = lastScan.ndefRecords;
|
||||
}
|
||||
|
||||
// Add version data if available
|
||||
if (lastScan.hasVersionData) {
|
||||
doc["version_data"] = "";
|
||||
for (int i = 0; i < 8; i++) {
|
||||
if (i > 0) doc["version_data"] = doc["version_data"].as<String>() + " ";
|
||||
if (lastScan.versionData[i] < 0x10) doc["version_data"] = doc["version_data"].as<String>() + "0";
|
||||
doc["version_data"] = doc["version_data"].as<String>() + String(lastScan.versionData[i], HEX);
|
||||
}
|
||||
}
|
||||
|
||||
// Add lock bits
|
||||
if (lastScan.lockBits != 0) {
|
||||
doc["lock_bits"] = "0x" + String(lastScan.lockBits, HEX);
|
||||
}
|
||||
|
||||
} else {
|
||||
doc["card_present"] = false;
|
||||
doc["message"] = "No card detected or dump failed";
|
||||
}
|
||||
|
||||
String response;
|
||||
serializeJson(doc, response);
|
||||
request->send(200, "application/json", response);
|
||||
});
|
||||
|
||||
// Handle 404
|
||||
server.onNotFound([](AsyncWebServerRequest *request){
|
||||
request->send(404, "text/plain", "Not found");
|
||||
});
|
||||
}
|
||||
|
||||
bool performNFCScan() {
|
||||
unsigned long startTime = millis();
|
||||
clearScanData();
|
||||
|
||||
Serial.println("Starting multi-tech NFC scan...");
|
||||
|
||||
// Try Type A first (most common)
|
||||
if (scanTypeA()) {
|
||||
lastScan.technology = "ISO14443A";
|
||||
lastScan.scanTime = millis() - startTime;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Try Type B
|
||||
if (scanTypeB()) {
|
||||
lastScan.technology = "ISO14443B";
|
||||
lastScan.scanTime = millis() - startTime;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Try FeliCa
|
||||
if (scanFeliCa()) {
|
||||
lastScan.technology = "FeliCa";
|
||||
lastScan.scanTime = millis() - startTime;
|
||||
return true;
|
||||
}
|
||||
|
||||
lastScan.cardPresent = false;
|
||||
lastScan.scanTime = millis() - startTime;
|
||||
Serial.println("No card detected on any technology");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool scanTypeA() {
|
||||
uint8_t uid[10] = {0};
|
||||
uint8_t uidLength;
|
||||
|
||||
bool success = nfc.readPassiveTargetID(PN532_MIFARE_ISO14443A, uid, &uidLength);
|
||||
|
||||
if (success) {
|
||||
lastScan.cardPresent = true;
|
||||
lastScan.uidLength = uidLength;
|
||||
memcpy(lastScan.uid, uid, uidLength);
|
||||
|
||||
// Get ATQA and SAK from the PN532 internal buffer
|
||||
// Note: These values are typically available after successful target selection
|
||||
lastScan.atqa = 0x0004; // Default ATQA for most cards
|
||||
lastScan.sak = 0x08; // Default SAK
|
||||
|
||||
// Determine detailed card type
|
||||
lastScan.cardType = determineCardType(lastScan.sak, lastScan.atqa, uidLength);
|
||||
|
||||
Serial.print("Type A card detected! UID: ");
|
||||
for (uint8_t i = 0; i < uidLength; i++) {
|
||||
if (i > 0) Serial.print(":");
|
||||
if (uid[i] < 0x10) Serial.print("0");
|
||||
Serial.print(uid[i], HEX);
|
||||
}
|
||||
Serial.print(", Type: ");
|
||||
Serial.println(lastScan.cardType);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool scanTypeB() {
|
||||
// Type B scanning implementation
|
||||
// Note: PN532 library has limited Type B support, this is a framework
|
||||
Serial.println("Scanning for Type B cards...");
|
||||
|
||||
// Type B cards use different anti-collision
|
||||
// This would require lower-level PN532 commands
|
||||
// For now, return false as Type B is less common
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool scanFeliCa() {
|
||||
Serial.println("Scanning for FeliCa cards...");
|
||||
|
||||
// FeliCa polling - check for common system codes
|
||||
uint16_t systemCodes[] = {0xFFFF, 0x12FC, 0x0003, 0x1A2B}; // Common system codes
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
uint8_t felicaID[8];
|
||||
uint8_t felicaPMM[8];
|
||||
|
||||
// Note: This requires FeliCa-specific PN532 commands
|
||||
// The Adafruit library has limited FeliCa support
|
||||
// This is a framework for future implementation
|
||||
|
||||
// if (nfc.felica_Polling(systemCodes[i], 0x01, felicaID, felicaPMM)) {
|
||||
// lastScan.cardPresent = true;
|
||||
// lastScan.systemCode = systemCodes[i];
|
||||
// memcpy(lastScan.uid, felicaID, 8);
|
||||
// lastScan.uidLength = 8;
|
||||
// memcpy(lastScan.pmm, felicaPMM, 8);
|
||||
// lastScan.cardType = "FeliCa Card";
|
||||
// return true;
|
||||
// }
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
String determineCardType(uint8_t sak, uint16_t atqa, uint8_t uidLength) {
|
||||
// Enhanced card type detection based on SAK and ATQA
|
||||
|
||||
if (sak == 0x00) {
|
||||
return "MIFARE Ultralight / NTAG";
|
||||
} else if (sak == 0x08) {
|
||||
if (uidLength == 4) {
|
||||
return "MIFARE Classic 1K";
|
||||
} else {
|
||||
return "MIFARE Classic 1K (7-byte UID)";
|
||||
}
|
||||
} else if (sak == 0x18) {
|
||||
return "MIFARE Classic 4K";
|
||||
} else if (sak == 0x20) {
|
||||
return "MIFARE DESFire / Type 4";
|
||||
} else if (sak == 0x28) {
|
||||
return "JCOP31 / JCOP41";
|
||||
} else if (sak == 0x88) {
|
||||
return "MIFARE Classic 1K (Infineon)";
|
||||
} else if (sak == 0x98) {
|
||||
return "MIFARE Pro X";
|
||||
} else if (sak == 0xB8) {
|
||||
return "MIFARE Pro Y";
|
||||
} else {
|
||||
return "Unknown Type A (SAK: 0x" + String(sak, HEX) + ")";
|
||||
}
|
||||
}
|
||||
|
||||
void clearScanData() {
|
||||
memset(&lastScan, 0, sizeof(lastScan));
|
||||
lastScan.cardPresent = false;
|
||||
}
|
||||
|
||||
bool performNFCDump() {
|
||||
// First perform a scan
|
||||
if (!performNFCScan()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
lastScan.rawDataLength = 0;
|
||||
|
||||
// Route to appropriate extraction function based on card type
|
||||
if (lastScan.technology == "ISO14443A") {
|
||||
if (lastScan.cardType.indexOf("Ultralight") >= 0 || lastScan.cardType.indexOf("NTAG") >= 0) {
|
||||
return extractType2Data();
|
||||
} else if (lastScan.cardType.indexOf("DESFire") >= 0 || lastScan.cardType.indexOf("Type 4") >= 0) {
|
||||
return extractType4Data();
|
||||
} else {
|
||||
// MIFARE Classic or other Type A
|
||||
return extractClassicData();
|
||||
}
|
||||
} else if (lastScan.technology == "ISO14443B") {
|
||||
return extractTypeBData();
|
||||
} else if (lastScan.technology == "FeliCa") {
|
||||
return extractFeliCaData();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool extractType2Data() {
|
||||
Serial.println("Extracting Type 2 data (Ultralight/NTAG)...");
|
||||
|
||||
// First, try to get version info
|
||||
uint8_t versionCmd[] = {0x60}; // GET_VERSION command
|
||||
uint8_t versionResponse[8];
|
||||
|
||||
// Note: GET_VERSION requires direct PN532 command, not available in Adafruit library
|
||||
// This is a framework for the implementation
|
||||
|
||||
// Read all accessible pages (0-63 for NTAG213, more for larger cards)
|
||||
uint8_t maxPages = 64; // Start conservative, expand based on card type
|
||||
|
||||
for (uint8_t page = 0; page < maxPages; page++) {
|
||||
uint8_t data[4];
|
||||
bool success = nfc.mifareultralight_ReadPage(page, data);
|
||||
|
||||
if (success) {
|
||||
memcpy(&lastScan.rawData[lastScan.rawDataLength], data, 4);
|
||||
lastScan.rawDataLength += 4;
|
||||
|
||||
// Check for Capability Container (CC) at page 3
|
||||
if (page == 3) {
|
||||
memcpy(lastScan.ccData, data, 4);
|
||||
lastScan.hasCCData = true;
|
||||
|
||||
// Parse CC to determine NDEF presence and size
|
||||
if (data[0] == 0xE1) { // NDEF Magic Number
|
||||
uint16_t dataSize = (data[2] << 8) | data[3];
|
||||
Serial.print("NDEF detected, size: ");
|
||||
Serial.println(dataSize);
|
||||
lastScan.hasNDEF = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Look for NDEF TLV starting at page 4
|
||||
if (page >= 4 && lastScan.hasNDEF && lastScan.ndefRecords.length() == 0) {
|
||||
// Check for NDEF TLV (Type 0x03)
|
||||
if (data[0] == 0x03) {
|
||||
uint16_t ndefLen = data[1];
|
||||
if (ndefLen == 0xFF) {
|
||||
// Long form length (3 bytes)
|
||||
ndefLen = (data[2] << 8) | data[3];
|
||||
}
|
||||
lastScan.ndefLength = ndefLen;
|
||||
|
||||
// Parse NDEF records starting from next bytes
|
||||
uint8_t ndefStart = (data[1] == 0xFF) ? 4 : 2;
|
||||
parseNDEF(&lastScan.rawData[(page * 4) + ndefStart], ndefLen);
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
// Hit a locked or non-existent page
|
||||
Serial.print("Stopped reading at page ");
|
||||
Serial.println(page);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Extract lock bits from pages 2 and beyond
|
||||
if (lastScan.rawDataLength >= 12) {
|
||||
lastScan.lockBits = (lastScan.rawData[10] << 8) | lastScan.rawData[11];
|
||||
}
|
||||
|
||||
Serial.print("Type 2 dump complete: ");
|
||||
Serial.print(lastScan.rawDataLength);
|
||||
Serial.println(" bytes");
|
||||
|
||||
return lastScan.rawDataLength > 0;
|
||||
}
|
||||
|
||||
bool extractType4Data() {
|
||||
Serial.println("Extracting Type 4 data (ISO-DEP/DESFire)...");
|
||||
|
||||
// Type 4 cards use ISO-DEP protocol
|
||||
// This requires APDU commands to select and read NDEF application
|
||||
|
||||
// Select NDEF Application (AID: D2760000850101)
|
||||
uint8_t ndefAID[] = {0xD2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x01};
|
||||
|
||||
// Note: This requires ISO-DEP support in PN532 library
|
||||
// Framework for implementation:
|
||||
|
||||
// 1. Send SELECT command with NDEF AID
|
||||
// 2. Read Capability Container file (file ID 0xE103)
|
||||
// 3. Read NDEF Data file (file ID from CC)
|
||||
// 4. Parse NDEF records
|
||||
|
||||
Serial.println("Type 4 extraction requires ISO-DEP implementation");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool extractClassicData() {
|
||||
Serial.println("Extracting MIFARE Classic data...");
|
||||
|
||||
// MIFARE Classic requires sector keys for data access
|
||||
// Only extract publicly available information
|
||||
|
||||
// Determine card size based on SAK
|
||||
uint8_t sectors = (lastScan.sak == 0x18) ? 40 : 16; // 4K vs 1K
|
||||
|
||||
Serial.print("MIFARE Classic detected with ");
|
||||
Serial.print(sectors);
|
||||
Serial.println(" sectors");
|
||||
|
||||
// For security, we don't attempt key recovery
|
||||
// Only report card structure and publicly available data
|
||||
|
||||
lastScan.cardType += " (" + String(sectors) + " sectors)";
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool extractTypeBData() {
|
||||
Serial.println("Extracting Type B data...");
|
||||
|
||||
// Type B cards are less common but follow similar patterns
|
||||
// Would require ATTRIB command and Type B specific protocols
|
||||
|
||||
Serial.println("Type B extraction not yet implemented");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool extractFeliCaData() {
|
||||
Serial.println("Extracting FeliCa data...");
|
||||
|
||||
// FeliCa cards use service codes and block reading
|
||||
// Common services: 0x0009 (balance), 0x000B (history), etc.
|
||||
|
||||
Serial.println("FeliCa extraction not yet implemented");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool parseNDEF(uint8_t* data, size_t length) {
|
||||
Serial.print("Parsing NDEF data, length: ");
|
||||
Serial.println(length);
|
||||
|
||||
size_t offset = 0;
|
||||
lastScan.ndefRecords = "";
|
||||
|
||||
while (offset < length) {
|
||||
String record = parseNDEFRecord(data, offset, length);
|
||||
if (record.length() > 0) {
|
||||
if (lastScan.ndefRecords.length() > 0) {
|
||||
lastScan.ndefRecords += ", ";
|
||||
}
|
||||
lastScan.ndefRecords += record;
|
||||
} else {
|
||||
break; // Error or end of records
|
||||
}
|
||||
}
|
||||
|
||||
return lastScan.ndefRecords.length() > 0;
|
||||
}
|
||||
|
||||
String parseNDEFRecord(uint8_t* data, size_t& offset, size_t maxLength) {
|
||||
if (offset >= maxLength) return "";
|
||||
|
||||
uint8_t flags = data[offset++];
|
||||
if (offset >= maxLength) return "";
|
||||
|
||||
uint8_t tnf = flags & 0x07;
|
||||
bool shortRecord = (flags & 0x10) != 0;
|
||||
bool idLength = (flags & 0x08) != 0;
|
||||
|
||||
uint8_t typeLength = data[offset++];
|
||||
if (offset >= maxLength) return "";
|
||||
|
||||
uint32_t payloadLength;
|
||||
if (shortRecord) {
|
||||
payloadLength = data[offset++];
|
||||
} else {
|
||||
if (offset + 3 >= maxLength) return "";
|
||||
payloadLength = (data[offset] << 24) | (data[offset+1] << 16) |
|
||||
(data[offset+2] << 8) | data[offset+3];
|
||||
offset += 4;
|
||||
}
|
||||
|
||||
uint8_t idLen = 0;
|
||||
if (idLength) {
|
||||
if (offset >= maxLength) return "";
|
||||
idLen = data[offset++];
|
||||
}
|
||||
|
||||
// Read type
|
||||
String type = "";
|
||||
for (uint8_t i = 0; i < typeLength && offset < maxLength; i++) {
|
||||
type += (char)data[offset++];
|
||||
}
|
||||
|
||||
// Skip ID if present
|
||||
offset += idLen;
|
||||
|
||||
// Read payload
|
||||
String payload = "";
|
||||
|
||||
if (tnf == 0x01 && type == "T") { // Text record
|
||||
if (offset < maxLength) {
|
||||
uint8_t langLen = data[offset] & 0x3F;
|
||||
offset++; // Skip language code length
|
||||
offset += langLen; // Skip language code
|
||||
|
||||
// Read text
|
||||
for (uint32_t i = langLen + 1; i < payloadLength && offset < maxLength; i++) {
|
||||
payload += (char)data[offset++];
|
||||
}
|
||||
return "Text: \"" + payload + "\"";
|
||||
}
|
||||
} else if (tnf == 0x01 && type == "U") { // URI record
|
||||
if (offset < maxLength) {
|
||||
uint8_t uriPrefix = data[offset++];
|
||||
|
||||
// URI prefixes (0x01 = "http://www.", 0x02 = "https://www.", etc.)
|
||||
String prefixes[] = {"", "http://www.", "https://www.", "http://", "https://",
|
||||
"tel:", "mailto:", "ftp://anonymous:anonymous@", "ftp://ftp.",
|
||||
"ftps://", "sftp://", "smb://", "nfs://", "ftp://", "dav://",
|
||||
"news:", "telnet://", "imap:", "rtsp://", "urn:", "pop:",
|
||||
"sip:", "sips:", "tftp:", "btspp://", "btl2cap://", "btgoep://",
|
||||
"tcpobex://", "irdaobex://", "file://", "urn:epc:id:", "urn:epc:tag:",
|
||||
"urn:epc:pat:", "urn:epc:raw:", "urn:epc:", "urn:nfc:"};
|
||||
|
||||
if (uriPrefix < 36) {
|
||||
payload = prefixes[uriPrefix];
|
||||
}
|
||||
|
||||
for (uint32_t i = 1; i < payloadLength && offset < maxLength; i++) {
|
||||
payload += (char)data[offset++];
|
||||
}
|
||||
return "URI: \"" + payload + "\"";
|
||||
}
|
||||
} else {
|
||||
// Generic record
|
||||
offset += payloadLength;
|
||||
return "Record(TNF:" + String(tnf) + ", Type:\"" + type + "\", " + String(payloadLength) + " bytes)";
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
String getMainPage() {
|
||||
String html = "<!DOCTYPE html><html><head><title>NFC Scanner</title>";
|
||||
html += "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">";
|
||||
html += "<style>";
|
||||
html += "body { font-family: Arial, sans-serif; margin: 20px; background: #f0f0f0; }";
|
||||
html += ".container { max-width: 800px; margin: 0 auto; background: white; padding: 20px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }";
|
||||
html += "h1 { color: #333; text-align: center; }";
|
||||
html += ".button { background: #007bff; color: white; padding: 12px 24px; border: none; border-radius: 5px; cursor: pointer; margin: 10px 5px; font-size: 16px; }";
|
||||
html += ".button:hover { background: #0056b3; }";
|
||||
html += ".button:disabled { background: #ccc; cursor: not-allowed; }";
|
||||
html += ".status { padding: 10px; margin: 10px 0; border-radius: 5px; }";
|
||||
html += ".success { background: #d4edda; border: 1px solid #c3e6cb; color: #155724; }";
|
||||
html += ".error { background: #f8d7da; border: 1px solid #f5c6cb; color: #721c24; }";
|
||||
html += ".info { background: #d1ecf1; border: 1px solid #bee5eb; color: #0c5460; }";
|
||||
html += ".warning { background: #fff3cd; border: 1px solid #ffeaa7; color: #856404; }";
|
||||
html += "pre { background: #f8f9fa; padding: 15px; border-radius: 5px; overflow-x: auto; white-space: pre-wrap; }";
|
||||
html += ".loading { display: none; }";
|
||||
html += "</style></head><body>";
|
||||
html += "<div class=\"container\">";
|
||||
html += "<h1>🔍 NFC Scanner</h1>";
|
||||
html += "<p>Connect to WiFi: <strong>NFC-Scanner</strong> (Password: nfc123456)</p>";
|
||||
html += "<div>";
|
||||
html += "<button class=\"button\" onclick=\"getVersion()\">Get Version</button>";
|
||||
html += "<button class=\"button\" onclick=\"scanCard()\">Scan Card</button>";
|
||||
html += "<button class=\"button\" onclick=\"dumpCard()\">Dump Card Data</button>";
|
||||
html += "<button class=\"button\" onclick=\"securityAnalysis()\" style=\"background: #dc3545;\">Security Analysis</button>";
|
||||
html += "</div>";
|
||||
html += "<div class=\"warning\" style=\"background: #fff3cd; border: 1px solid #ffeaa7; color: #856404; padding: 10px; margin: 10px 0; border-radius: 5px;\">";
|
||||
html += "⚠️ <strong>RESEARCH ONLY:</strong> Security analysis features are for educational and authorized security research purposes only.";
|
||||
html += "</div>";
|
||||
html += "<div id=\"loading\" class=\"loading\">⏳ Processing...</div>";
|
||||
html += "<div id=\"status\"></div>";
|
||||
html += "<div id=\"result\"></div>";
|
||||
html += "</div>";
|
||||
html += "<script>";
|
||||
html += "function showLoading(show) { document.getElementById('loading').style.display = show ? 'block' : 'none'; }";
|
||||
html += "function showStatus(message, type) { const status = document.getElementById('status'); status.className = 'status ' + (type || 'info'); status.innerHTML = message; }";
|
||||
html += "function showResult(data) { document.getElementById('result').innerHTML = '<pre>' + JSON.stringify(data, null, 2) + '</pre>'; }";
|
||||
html += "async function getVersion() { showLoading(true); try { const response = await fetch('/version'); const data = await response.json(); showStatus('✅ Version info retrieved', 'success'); showResult(data); } catch (error) { showStatus('❌ Error: ' + error.message, 'error'); } showLoading(false); }";
|
||||
html += "async function scanCard() { showLoading(true); showStatus('🔍 Scanning for NFC card... Place card near reader', 'info'); try { const response = await fetch('/scan'); const data = await response.json(); if (data.success && data.card_present) { showStatus('✅ Card detected!', 'success'); } else { showStatus('❌ No card detected', 'error'); } showResult(data); } catch (error) { showStatus('❌ Error: ' + error.message, 'error'); } showLoading(false); }";
|
||||
html += "async function dumpCard() { showLoading(true); showStatus('📥 Dumping card data... This may take a moment', 'info'); try { const response = await fetch('/dump'); const data = await response.json(); if (data.success) { showStatus('✅ Card data dumped successfully!', 'success'); } else { showStatus('❌ Failed to dump card data', 'error'); } showResult(data); } catch (error) { showStatus('❌ Error: ' + error.message, 'error'); } showLoading(false); }";
|
||||
html += "async function securityAnalysis() { if (!confirm('This will perform comprehensive security analysis including key recovery attempts. Continue? (Research purposes only)')) { return; } showLoading(true); showStatus('🔍 Performing security analysis... This may take several minutes', 'info'); try { const response = await fetch('/security'); const data = await response.json(); if (data.success) { const riskLevel = data.security_analysis.risk_level; let riskColor = 'success'; if (riskLevel >= 7) riskColor = 'error'; else if (riskLevel >= 4) riskColor = 'warning'; showStatus('🛡️ Security analysis complete! Risk Level: ' + riskLevel + '/10', riskColor); } else { showStatus('❌ Security analysis failed', 'error'); } showResult(data); } catch (error) { showStatus('❌ Error: ' + error.message, 'error'); } showLoading(false); }";
|
||||
html += "window.onload = function() { getVersion(); };";
|
||||
html += "</script></body></html>";
|
||||
return html;
|
||||
}
|
||||
56
src/nfc_types.h
Normal file
56
src/nfc_types.h
Normal file
@@ -0,0 +1,56 @@
|
||||
#ifndef NFC_TYPES_H
|
||||
#define NFC_TYPES_H
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
// Shared NFC data structure used across modules
|
||||
struct NFCData {
|
||||
bool cardPresent = false;
|
||||
uint8_t uid[10]; // Extended for longer UIDs
|
||||
uint8_t uidLength = 0;
|
||||
uint16_t atqa = 0;
|
||||
uint8_t sak = 0;
|
||||
String cardType = "";
|
||||
String technology = "";
|
||||
unsigned long scanTime = 0;
|
||||
|
||||
// Type A specific
|
||||
uint8_t ats[32];
|
||||
uint8_t atsLength = 0;
|
||||
|
||||
// Type B specific
|
||||
uint8_t pupi[4];
|
||||
uint8_t appData[4];
|
||||
uint8_t protocolInfo[3];
|
||||
|
||||
// FeliCa specific
|
||||
uint16_t systemCode = 0;
|
||||
uint8_t pmm[8];
|
||||
uint8_t requestData[16];
|
||||
|
||||
// Raw data and parsed content
|
||||
uint8_t rawData[2048]; // Increased buffer
|
||||
size_t rawDataLength = 0;
|
||||
|
||||
// NDEF data
|
||||
bool hasNDEF = false;
|
||||
String ndefRecords = "";
|
||||
uint16_t ndefLength = 0;
|
||||
|
||||
// Capability Container
|
||||
uint8_t ccData[16];
|
||||
bool hasCCData = false;
|
||||
|
||||
// Version info (for NTAG/Ultralight)
|
||||
uint8_t versionData[8];
|
||||
bool hasVersionData = false;
|
||||
|
||||
// Lock and configuration
|
||||
uint32_t lockBits = 0;
|
||||
uint8_t configPages[8];
|
||||
bool hasConfigData = false;
|
||||
};
|
||||
|
||||
extern NFCData lastScan;
|
||||
|
||||
#endif // NFC_TYPES_H
|
||||
481
src/security_research.cpp
Normal file
481
src/security_research.cpp
Normal file
@@ -0,0 +1,481 @@
|
||||
#include "security_research.h"
|
||||
#include "nfc_types.h"
|
||||
#include <SPI.h>
|
||||
|
||||
// MIFARE Classic default keys commonly found in the wild
|
||||
const uint8_t defaultKeys[][6] = {
|
||||
{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, // Factory default
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // All zeros
|
||||
{0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5}, // NXP default
|
||||
{0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5}, // Alternative NXP
|
||||
{0x4D, 0x3A, 0x99, 0xC3, 0x51, 0xDD}, // Hotel key system
|
||||
{0x1A, 0x98, 0x2C, 0x7E, 0x45, 0x9A}, // Transport system
|
||||
{0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7}, // MAD key
|
||||
{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}, // Common test key
|
||||
{0x71, 0x4C, 0x5C, 0x88, 0x6E, 0x97}, // Mifare Plus
|
||||
{0x58, 0x7E, 0xE5, 0xF9, 0x35, 0x0F}, // Infineon default
|
||||
{0xA0, 0x47, 0x8C, 0xC3, 0x90, 0x91}, // HID iClass
|
||||
{0x53, 0x3C, 0xB6, 0xC7, 0x23, 0xF6}, // Access control
|
||||
{0x8F, 0xD0, 0xA4, 0xF2, 0x56, 0xE9}, // Hotel chain
|
||||
{0x26, 0x94, 0x0B, 0x21, 0xFF, 0x5B}, // University system
|
||||
{0xEA, 0x0F, 0xD7, 0x3C, 0xB1, 0x49} // Government ID
|
||||
};
|
||||
const uint8_t numDefaultKeys = sizeof(defaultKeys) / sizeof(defaultKeys[0]);
|
||||
|
||||
// Common DESFire Application IDs found in real systems
|
||||
const uint32_t commonDESFireAIDs[] = {
|
||||
0x000000U, // Master application
|
||||
0x112233U, // Common test AID
|
||||
0x123456U, // Default demo AID
|
||||
0x4D494641U, // "MIFA" ASCII
|
||||
0x505249U, // "PRI" - Proximity
|
||||
0x434152U, // "CAR" - Parking
|
||||
0x545241U, // "TRA" - Transit
|
||||
0x414343U, // "ACC" - Access control
|
||||
0x484944U, // "HID" - HID systems
|
||||
0x4E4643U, // "NFC" - NFC applications
|
||||
0x50415953U, // "PAYS" - Payment systems
|
||||
0x4C4F5941U, // "LOYA" - Loyalty programs (truncated to fit uint32_t)
|
||||
};
|
||||
const uint8_t numCommonAIDs = sizeof(commonDESFireAIDs) / sizeof(uint32_t);
|
||||
|
||||
SecurityAnalysis lastAnalysis;
|
||||
extern Adafruit_PN532 nfc;
|
||||
|
||||
bool performSecurityAnalysis() {
|
||||
Serial.println("=== STARTING COMPREHENSIVE SECURITY ANALYSIS ===");
|
||||
Serial.println("FOR RESEARCH PURPOSES ONLY");
|
||||
|
||||
// Clear previous analysis
|
||||
memset(&lastAnalysis, 0, sizeof(lastAnalysis));
|
||||
|
||||
if (!lastScan.cardPresent) {
|
||||
Serial.println("No card present for analysis");
|
||||
return false;
|
||||
}
|
||||
|
||||
Serial.print("Analyzing card type: ");
|
||||
Serial.println(lastScan.cardType);
|
||||
|
||||
// Perform RF fingerprinting first
|
||||
performRFFingerprinting();
|
||||
|
||||
// Route to specific analysis based on card type
|
||||
if (lastScan.cardType.indexOf("Classic") >= 0) {
|
||||
analyzeMIFAREClassic();
|
||||
} else if (lastScan.cardType.indexOf("DESFire") >= 0) {
|
||||
analyzeDESFire();
|
||||
} else if (lastScan.cardType.indexOf("Ultralight") >= 0 || lastScan.cardType.indexOf("NTAG") >= 0) {
|
||||
testNDEFVulnerabilities();
|
||||
}
|
||||
|
||||
// Universal tests
|
||||
detectCloning();
|
||||
performTimingAnalysis();
|
||||
analyzeProtocolCompliance();
|
||||
|
||||
// Calculate risk level
|
||||
calculateRiskLevel();
|
||||
|
||||
Serial.println("=== SECURITY ANALYSIS COMPLETE ===");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool analyzeMIFAREClassic() {
|
||||
Serial.println("Performing MIFARE Classic security analysis...");
|
||||
|
||||
lastAnalysis.totalSectors = (lastScan.sak == 0x18) ? 40 : 16;
|
||||
lastAnalysis.accessibleSectors = 0;
|
||||
|
||||
// Test default keys on all sectors
|
||||
for (uint8_t sector = 0; sector < lastAnalysis.totalSectors; sector++) {
|
||||
for (uint8_t keyIdx = 0; keyIdx < numDefaultKeys; keyIdx++) {
|
||||
// Test Key A
|
||||
if (authenticateWithKey(sector, 0x60, (uint8_t*)defaultKeys[keyIdx])) {
|
||||
lastAnalysis.accessibleSectors++;
|
||||
lastAnalysis.hasWeakKeys = true;
|
||||
|
||||
String keyHex = "";
|
||||
for (int i = 0; i < 6; i++) {
|
||||
if (defaultKeys[keyIdx][i] < 0x10) keyHex += "0";
|
||||
keyHex += String(defaultKeys[keyIdx][i], HEX);
|
||||
}
|
||||
|
||||
lastAnalysis.keyAnalysis += "Sector " + String(sector) + " Key A: " + keyHex + "; ";
|
||||
|
||||
// Try to read sector data
|
||||
readSectorWithKey(sector, (uint8_t*)defaultKeys[keyIdx], 0x60);
|
||||
break;
|
||||
}
|
||||
|
||||
// Test Key B
|
||||
if (authenticateWithKey(sector, 0x61, (uint8_t*)defaultKeys[keyIdx])) {
|
||||
if (lastAnalysis.keyAnalysis.indexOf("Sector " + String(sector)) == -1) {
|
||||
lastAnalysis.accessibleSectors++;
|
||||
}
|
||||
lastAnalysis.hasWeakKeys = true;
|
||||
|
||||
String keyHex = "";
|
||||
for (int i = 0; i < 6; i++) {
|
||||
if (defaultKeys[keyIdx][i] < 0x10) keyHex += "0";
|
||||
keyHex += String(defaultKeys[keyIdx][i], HEX);
|
||||
}
|
||||
|
||||
lastAnalysis.keyAnalysis += "Sector " + String(sector) + " Key B: " + keyHex + "; ";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Advanced attacks (simulation - requires specialized hardware)
|
||||
if (lastAnalysis.accessibleSectors < lastAnalysis.totalSectors) {
|
||||
Serial.println("Simulating advanced key recovery attacks...");
|
||||
// performNestedAttack();
|
||||
// performDarksideAttack();
|
||||
lastAnalysis.vulnerabilities += "Potential nested/darkside attack vectors; ";
|
||||
}
|
||||
|
||||
Serial.print("Accessible sectors: ");
|
||||
Serial.print(lastAnalysis.accessibleSectors);
|
||||
Serial.print("/");
|
||||
Serial.println(lastAnalysis.totalSectors);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool analyzeDESFire() {
|
||||
Serial.println("Performing DESFire security analysis...");
|
||||
|
||||
// Get DESFire version information
|
||||
getDESFireVersion();
|
||||
|
||||
// Enumerate applications
|
||||
enumerateApplications();
|
||||
|
||||
// Test for default authentication keys
|
||||
// DESFire uses AES/3DES keys, not the simple 6-byte MIFARE keys
|
||||
uint8_t defaultAESKey[16] = {0x00}; // All zeros default
|
||||
|
||||
// Test master application access
|
||||
if (selectDESFireApplication(0x000000)) {
|
||||
lastAnalysis.hasDefaultKeys = true;
|
||||
lastAnalysis.vulnerabilities += "Master app accessible with default key; ";
|
||||
}
|
||||
|
||||
Serial.print("Found applications: ");
|
||||
Serial.println(lastAnalysis.applicationCount);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool performKeyRecovery() {
|
||||
Serial.println("Performing key recovery analysis...");
|
||||
|
||||
// This would implement advanced cryptographic attacks
|
||||
// For research purposes, we simulate the process
|
||||
|
||||
if (lastScan.cardType.indexOf("Classic") >= 0) {
|
||||
Serial.println("MIFARE Classic key recovery vectors:");
|
||||
Serial.println("- Default key testing: IMPLEMENTED");
|
||||
Serial.println("- Nested attack: SIMULATION");
|
||||
Serial.println("- Darkside attack: SIMULATION");
|
||||
Serial.println("- Hardnested attack: REQUIRES SPECIALIZED HARDWARE");
|
||||
|
||||
lastAnalysis.vulnerabilities += "Multiple key recovery vectors available; ";
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool enumerateApplications() {
|
||||
Serial.println("Enumerating DESFire applications...");
|
||||
|
||||
lastAnalysis.applicationCount = 0;
|
||||
lastAnalysis.applications = "";
|
||||
|
||||
for (uint8_t i = 0; i < numCommonAIDs; i++) {
|
||||
if (selectDESFireApplication(commonDESFireAIDs[i])) {
|
||||
lastAnalysis.applicationCount++;
|
||||
|
||||
String aidHex = String(commonDESFireAIDs[i], HEX);
|
||||
lastAnalysis.applications += "0x" + aidHex + " ";
|
||||
|
||||
// Try to list files in this application
|
||||
listDESFireFiles();
|
||||
}
|
||||
}
|
||||
|
||||
return lastAnalysis.applicationCount > 0;
|
||||
}
|
||||
|
||||
bool detectCloning() {
|
||||
Serial.println("Performing clone detection analysis...");
|
||||
|
||||
// Analyze UID for cloning indicators
|
||||
bool suspiciousUID = false;
|
||||
|
||||
// Check for common clone UID patterns
|
||||
if (lastScan.uid[0] == 0x08 || lastScan.uid[0] == 0x88) {
|
||||
suspiciousUID = true;
|
||||
lastAnalysis.cloneIndicators += "Suspicious UID prefix (0x08/0x88); ";
|
||||
}
|
||||
|
||||
// Check for Chinese clone manufacturers
|
||||
if (lastScan.uid[0] == 0x62 || lastScan.uid[0] == 0x63) {
|
||||
suspiciousUID = true;
|
||||
lastAnalysis.cloneIndicators += "Possible Chinese clone UID; ";
|
||||
}
|
||||
|
||||
// Analyze timing inconsistencies
|
||||
if (lastScan.scanTime > 500) { // Clones often respond slower
|
||||
suspiciousUID = true;
|
||||
lastAnalysis.cloneIndicators += "Slow response time; ";
|
||||
}
|
||||
|
||||
lastAnalysis.possibleClone = suspiciousUID;
|
||||
|
||||
if (suspiciousUID) {
|
||||
Serial.println("WARNING: Possible cloned card detected!");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool performRFFingerprinting() {
|
||||
Serial.println("Performing RF fingerprinting...");
|
||||
|
||||
// Measure response characteristics
|
||||
unsigned long startTime = micros();
|
||||
|
||||
// Send multiple commands and measure timing
|
||||
uint8_t uid[10];
|
||||
uint8_t uidLen;
|
||||
|
||||
for (int i = 0; i < 5; i++) {
|
||||
nfc.readPassiveTargetID(PN532_MIFARE_ISO14443A, uid, &uidLen, 100);
|
||||
}
|
||||
|
||||
lastAnalysis.responseTime = (micros() - startTime) / 5; // Average response time
|
||||
|
||||
// Analyze response patterns for fingerprinting
|
||||
lastAnalysis.rfFingerprint = "RT:" + String(lastAnalysis.responseTime) + "us";
|
||||
|
||||
// Check for timing anomalies
|
||||
if (lastAnalysis.responseTime > 10000) { // > 10ms is suspicious
|
||||
lastAnalysis.rfFingerprint += " SLOW";
|
||||
lastAnalysis.vulnerabilities += "Abnormal response timing; ";
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool performTimingAnalysis() {
|
||||
Serial.println("Performing timing attack analysis...");
|
||||
|
||||
// Test for timing-based vulnerabilities
|
||||
if (lastScan.cardType.indexOf("Classic") >= 0) {
|
||||
Serial.println("Testing MIFARE Classic timing vulnerabilities...");
|
||||
|
||||
// Simulate timing attack on authentication
|
||||
unsigned long authTimes[10];
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
unsigned long start = micros();
|
||||
// Attempt authentication with wrong key
|
||||
uint8_t wrongKey[6] = {0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC};
|
||||
authenticateWithKey(0, 0x60, wrongKey);
|
||||
authTimes[i] = micros() - start;
|
||||
}
|
||||
|
||||
// Analyze timing variance
|
||||
unsigned long avgTime = 0;
|
||||
for (int i = 0; i < 10; i++) {
|
||||
avgTime += authTimes[i];
|
||||
}
|
||||
avgTime /= 10;
|
||||
|
||||
// Check for timing side-channels
|
||||
bool timingVulnerable = false;
|
||||
for (int i = 0; i < 10; i++) {
|
||||
if (abs((long)(authTimes[i] - avgTime)) > avgTime * 0.1) { // >10% variance
|
||||
timingVulnerable = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (timingVulnerable) {
|
||||
lastAnalysis.vulnerabilities += "Timing side-channel vulnerability; ";
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool testNDEFVulnerabilities() {
|
||||
Serial.println("Testing NDEF vulnerabilities...");
|
||||
|
||||
if (!lastScan.hasNDEF) {
|
||||
Serial.println("No NDEF data to analyze");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for common NDEF vulnerabilities
|
||||
if (lastScan.ndefRecords.indexOf("javascript:") >= 0) {
|
||||
lastAnalysis.vulnerabilities += "Potential XSS in NDEF URI; ";
|
||||
}
|
||||
|
||||
if (lastScan.ndefRecords.indexOf("file://") >= 0) {
|
||||
lastAnalysis.vulnerabilities += "Local file access in NDEF URI; ";
|
||||
}
|
||||
|
||||
// Check for oversized NDEF records (buffer overflow potential)
|
||||
if (lastScan.ndefLength > 1024) {
|
||||
lastAnalysis.vulnerabilities += "Oversized NDEF record (DoS potential); ";
|
||||
}
|
||||
|
||||
// Check for malformed NDEF structure
|
||||
if (lastScan.ndefRecords.indexOf("Record(") >= 0) {
|
||||
lastAnalysis.vulnerabilities += "Malformed NDEF records detected; ";
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Low-level PN532 functions for advanced research
|
||||
bool sendRawCommand(uint8_t* cmd, uint8_t cmdLen, uint8_t* response, uint8_t* responseLen) {
|
||||
// This would send raw commands to PN532 for advanced research
|
||||
// Requires direct SPI/I2C communication bypassing Adafruit library
|
||||
Serial.println("Raw command interface - requires low-level implementation");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool authenticateWithKey(uint8_t sector, uint8_t keyType, uint8_t* key) {
|
||||
// Calculate block number for sector
|
||||
uint8_t block = (sector < 32) ? (sector * 4) : (128 + (sector - 32) * 16);
|
||||
|
||||
// Attempt authentication
|
||||
bool success = nfc.mifareclassic_AuthenticateBlock(lastScan.uid, lastScan.uidLength, block, keyType, key);
|
||||
|
||||
if (success) {
|
||||
Serial.print("Authenticated sector ");
|
||||
Serial.print(sector);
|
||||
Serial.print(" with key type 0x");
|
||||
Serial.println(keyType, HEX);
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
bool readSectorWithKey(uint8_t sector, uint8_t* key, uint8_t keyType) {
|
||||
uint8_t block = (sector < 32) ? (sector * 4) : (128 + (sector - 32) * 16);
|
||||
uint8_t data[16];
|
||||
|
||||
// Authenticate first
|
||||
if (!nfc.mifareclassic_AuthenticateBlock(lastScan.uid, lastScan.uidLength, block, keyType, key)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read data blocks in sector (skip trailer block)
|
||||
uint8_t blocksInSector = (sector < 32) ? 3 : 15;
|
||||
|
||||
for (uint8_t i = 0; i < blocksInSector; i++) {
|
||||
if (nfc.mifareclassic_ReadDataBlock(block + i, data)) {
|
||||
Serial.print("Sector ");
|
||||
Serial.print(sector);
|
||||
Serial.print(" Block ");
|
||||
Serial.print(i);
|
||||
Serial.print(": ");
|
||||
|
||||
for (int j = 0; j < 16; j++) {
|
||||
if (data[j] < 0x10) Serial.print("0");
|
||||
Serial.print(data[j], HEX);
|
||||
Serial.print(" ");
|
||||
}
|
||||
Serial.println();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool selectDESFireApplication(uint32_t aid) {
|
||||
// This would require ISO14443-4 / ISO-DEP implementation
|
||||
Serial.print("Attempting to select DESFire AID: 0x");
|
||||
Serial.println(aid, HEX);
|
||||
|
||||
// Simulate selection for research purposes
|
||||
return (aid == 0x000000 || aid == 0x123456); // Simulate some success
|
||||
}
|
||||
|
||||
bool getDESFireVersion() {
|
||||
Serial.println("Getting DESFire version info...");
|
||||
// This would send GET_VERSION command to DESFire card
|
||||
return false;
|
||||
}
|
||||
|
||||
bool listDESFireFiles() {
|
||||
Serial.println("Listing files in DESFire application...");
|
||||
// This would send GET_FILE_IDS command
|
||||
return false;
|
||||
}
|
||||
|
||||
bool readDESFireFile(uint8_t fileId) {
|
||||
Serial.print("Reading DESFire file ID: ");
|
||||
Serial.println(fileId);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool setRFConfiguration(uint8_t cfgItem, uint8_t* cfgData, uint8_t cfgLen) {
|
||||
Serial.println("Setting RF configuration (stub)");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool performAntiCollisionAnalysis() {
|
||||
Serial.println("Performing anti-collision analysis (stub)");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool performReplayAttack() {
|
||||
Serial.println("Performing replay attack simulation (stub)");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool analyzeCryptography() {
|
||||
Serial.println("Analyzing cryptographic parameters (stub)");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool testAccessControls() {
|
||||
Serial.println("Testing access controls (stub)");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool performFuzzTesting() {
|
||||
Serial.println("Performing fuzz testing (stub)");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool analyzeProtocolCompliance() {
|
||||
Serial.println("Analyzing protocol compliance...");
|
||||
|
||||
// Check for protocol violations that might indicate clones or modified cards
|
||||
lastAnalysis.vulnerabilities += "Protocol analysis requires specialized equipment; ";
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void calculateRiskLevel() {
|
||||
lastAnalysis.riskLevel = 0;
|
||||
|
||||
if (lastAnalysis.hasWeakKeys) lastAnalysis.riskLevel += 3;
|
||||
if (lastAnalysis.possibleClone) lastAnalysis.riskLevel += 2;
|
||||
if (lastAnalysis.hasDefaultKeys) lastAnalysis.riskLevel += 4;
|
||||
if (lastAnalysis.vulnerabilities.length() > 50) lastAnalysis.riskLevel += 1;
|
||||
|
||||
// Cap at 10
|
||||
if (lastAnalysis.riskLevel > 10) lastAnalysis.riskLevel = 10;
|
||||
|
||||
Serial.print("Calculated risk level: ");
|
||||
Serial.print(lastAnalysis.riskLevel);
|
||||
Serial.println("/10");
|
||||
}
|
||||
86
src/security_research.h
Normal file
86
src/security_research.h
Normal file
@@ -0,0 +1,86 @@
|
||||
#ifndef SECURITY_RESEARCH_H
|
||||
#define SECURITY_RESEARCH_H
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <Adafruit_PN532.h>
|
||||
|
||||
// Security Research Module for PN532
|
||||
// FOR EDUCATIONAL AND RESEARCH PURPOSES ONLY
|
||||
|
||||
// MIFARE Classic default keys for security analysis
|
||||
extern const uint8_t defaultKeys[][6];
|
||||
extern const uint8_t numDefaultKeys;
|
||||
|
||||
// DESFire application IDs for enumeration
|
||||
extern const uint32_t commonDESFireAIDs[];
|
||||
extern const uint8_t numCommonAIDs;
|
||||
|
||||
// Security analysis results structure
|
||||
struct SecurityAnalysis {
|
||||
// MIFARE Classic analysis
|
||||
bool hasWeakKeys = false;
|
||||
uint8_t accessibleSectors = 0;
|
||||
uint8_t totalSectors = 0;
|
||||
String keyAnalysis = "";
|
||||
|
||||
// DESFire analysis
|
||||
uint8_t applicationCount = 0;
|
||||
String applications = "";
|
||||
bool hasDefaultKeys = false;
|
||||
|
||||
// RF fingerprinting
|
||||
uint16_t responseTime = 0;
|
||||
uint8_t signalStrength = 0;
|
||||
String rfFingerprint = "";
|
||||
|
||||
// Clone detection
|
||||
bool possibleClone = false;
|
||||
String cloneIndicators = "";
|
||||
|
||||
// Vulnerability assessment
|
||||
String vulnerabilities = "";
|
||||
uint8_t riskLevel = 0; // 0-10 scale
|
||||
};
|
||||
|
||||
// Function declarations for security research
|
||||
bool performSecurityAnalysis();
|
||||
bool analyzeMIFAREClassic();
|
||||
bool analyzeDESFire();
|
||||
bool performKeyRecovery();
|
||||
bool enumerateApplications();
|
||||
bool detectCloning();
|
||||
bool performRFFingerprinting();
|
||||
bool performTimingAnalysis();
|
||||
bool testNDEFVulnerabilities();
|
||||
|
||||
// Low-level PN532 functions
|
||||
bool sendRawCommand(uint8_t* cmd, uint8_t cmdLen, uint8_t* response, uint8_t* responseLen);
|
||||
bool setRFConfiguration(uint8_t cfgItem, uint8_t* cfgData, uint8_t cfgLen);
|
||||
bool performAntiCollisionAnalysis();
|
||||
bool analyzeProtocolCompliance();
|
||||
|
||||
// MIFARE Classic specific
|
||||
bool authenticateWithKey(uint8_t sector, uint8_t keyType, uint8_t* key);
|
||||
bool readSectorWithKey(uint8_t sector, uint8_t* key, uint8_t keyType);
|
||||
bool dumpAllSectors();
|
||||
bool performNestedAttack();
|
||||
bool performDarksideAttack();
|
||||
|
||||
// DESFire specific
|
||||
bool selectDESFireApplication(uint32_t aid);
|
||||
bool getDESFireVersion();
|
||||
bool listDESFireFiles();
|
||||
bool readDESFireFile(uint8_t fileId);
|
||||
|
||||
// Advanced analysis
|
||||
bool performReplayAttack();
|
||||
bool analyzeCryptography();
|
||||
bool testAccessControls();
|
||||
bool performFuzzTesting();
|
||||
|
||||
// Utility
|
||||
void calculateRiskLevel();
|
||||
|
||||
extern SecurityAnalysis lastAnalysis;
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user