Implement comprehensive NFC chip identification using platform-specific commands and heuristics: - NTAG/Ultralight detection via GET_VERSION command - DESFire EV1/EV2/EV3 and NTAG 424 DNA detection - ISO 15693 detection with memory-based SLIX/ICODE DNA differentiation - JavaCard/JCOP detection via CPLC data - MIFARE Classic detection via SAK values Use block count as authoritative source for NXP ISO 15693 chips since IC reference values overlap between SLIX and ICODE DNA families. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
388 lines
10 KiB
TypeScript
388 lines
10 KiB
TypeScript
/**
|
|
* MIFARE Detector
|
|
* Identifies MIFARE Classic 1K/4K/Mini based on SAK values
|
|
* Also provides stubs for DESFire and Plus detection (Phase 4)
|
|
*/
|
|
|
|
import {Platform} from 'react-native';
|
|
import {ChipType, CHIP_MEMORY_SIZES} from '../../types/detection';
|
|
|
|
/**
|
|
* SAK (Select Acknowledge) values for MIFARE chips
|
|
*
|
|
* SAK is returned during ISO 14443-3A anticollision and indicates card capabilities
|
|
*/
|
|
const MIFARE_SAK_VALUES = {
|
|
// MIFARE Classic 1K variants
|
|
CLASSIC_1K: 0x08,
|
|
CLASSIC_1K_SMARTMX: 0x28, // Classic 1K emulation on SmartMX
|
|
CLASSIC_1K_INFINEON: 0x88, // Infineon variant
|
|
|
|
// MIFARE Classic 4K variants
|
|
CLASSIC_4K: 0x18,
|
|
CLASSIC_4K_SMARTMX: 0x38, // Classic 4K emulation on SmartMX
|
|
CLASSIC_4K_INFINEON: 0x98, // Infineon variant
|
|
|
|
// MIFARE Classic 2K (rare)
|
|
CLASSIC_2K: 0x19,
|
|
|
|
// MIFARE Classic Mini
|
|
CLASSIC_MINI: 0x09,
|
|
|
|
// MIFARE Classic 1K with UID changeable (magic cards often)
|
|
CLASSIC_1K_UID_CHANGEABLE: 0x01,
|
|
|
|
// Cards with ISO 14443-4 support (bit 5 set)
|
|
// These might be DESFire, Plus, or SmartMX
|
|
ISO_DEP_CAPABLE: 0x20,
|
|
} as const;
|
|
|
|
// All SAK values that indicate MIFARE Classic
|
|
const ALL_CLASSIC_1K_SAKS: number[] = [
|
|
MIFARE_SAK_VALUES.CLASSIC_1K,
|
|
MIFARE_SAK_VALUES.CLASSIC_1K_SMARTMX,
|
|
MIFARE_SAK_VALUES.CLASSIC_1K_INFINEON,
|
|
MIFARE_SAK_VALUES.CLASSIC_1K_UID_CHANGEABLE,
|
|
];
|
|
|
|
const ALL_CLASSIC_4K_SAKS: number[] = [
|
|
MIFARE_SAK_VALUES.CLASSIC_4K,
|
|
MIFARE_SAK_VALUES.CLASSIC_4K_SMARTMX,
|
|
MIFARE_SAK_VALUES.CLASSIC_4K_INFINEON,
|
|
MIFARE_SAK_VALUES.CLASSIC_2K, // 2K treated as 4K variant
|
|
];
|
|
|
|
/**
|
|
* Result of MIFARE Classic detection
|
|
*/
|
|
export interface MifareClassicDetectionResult {
|
|
success: boolean;
|
|
chipType?: ChipType;
|
|
memorySize?: number;
|
|
sectorCount?: number;
|
|
blockCount?: number;
|
|
note?: string;
|
|
}
|
|
|
|
/**
|
|
* Detect MIFARE Classic variant from SAK value
|
|
*/
|
|
export function detectMifareClassic(sak: number): MifareClassicDetectionResult {
|
|
// Check for MIFARE Classic 1K variants
|
|
if (ALL_CLASSIC_1K_SAKS.includes(sak)) {
|
|
return {
|
|
success: true,
|
|
chipType: ChipType.MIFARE_CLASSIC_1K,
|
|
memorySize: CHIP_MEMORY_SIZES[ChipType.MIFARE_CLASSIC_1K],
|
|
sectorCount: 16,
|
|
blockCount: 64,
|
|
note:
|
|
Platform.OS === 'ios'
|
|
? 'Sector operations require Android'
|
|
: undefined,
|
|
};
|
|
}
|
|
|
|
// Check for MIFARE Classic 4K variants
|
|
if (ALL_CLASSIC_4K_SAKS.includes(sak)) {
|
|
return {
|
|
success: true,
|
|
chipType: ChipType.MIFARE_CLASSIC_4K,
|
|
memorySize: CHIP_MEMORY_SIZES[ChipType.MIFARE_CLASSIC_4K],
|
|
sectorCount: 40, // 32 small sectors + 8 large sectors
|
|
blockCount: 256,
|
|
note:
|
|
Platform.OS === 'ios'
|
|
? 'Sector operations require Android'
|
|
: undefined,
|
|
};
|
|
}
|
|
|
|
// Check for MIFARE Classic Mini (SAK 0x09)
|
|
if (sak === MIFARE_SAK_VALUES.CLASSIC_MINI) {
|
|
return {
|
|
success: true,
|
|
chipType: ChipType.MIFARE_CLASSIC_MINI,
|
|
memorySize: CHIP_MEMORY_SIZES[ChipType.MIFARE_CLASSIC_MINI],
|
|
sectorCount: 5,
|
|
blockCount: 20,
|
|
note:
|
|
Platform.OS === 'ios'
|
|
? 'Sector operations require Android'
|
|
: undefined,
|
|
};
|
|
}
|
|
|
|
return {
|
|
success: false,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Check if SAK indicates a MIFARE Classic chip
|
|
*/
|
|
export function isMifareClassicSak(sak: number): boolean {
|
|
return (
|
|
ALL_CLASSIC_1K_SAKS.includes(sak) ||
|
|
ALL_CLASSIC_4K_SAKS.includes(sak) ||
|
|
sak === MIFARE_SAK_VALUES.CLASSIC_MINI
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Check if SAK indicates ISO 14443-4 (ISO-DEP) capability
|
|
* This means the chip might be DESFire, Plus, or SmartMX
|
|
*/
|
|
export function hasIsoDepCapability(sak: number): boolean {
|
|
// Bit 5 (0x20) indicates ISO 14443-4 compliance
|
|
return (sak & 0x20) !== 0;
|
|
}
|
|
|
|
/**
|
|
* Get human-readable description of SAK value
|
|
*/
|
|
export function describeSak(sak: number): string {
|
|
if (sak === MIFARE_SAK_VALUES.CLASSIC_1K) {
|
|
return 'MIFARE Classic 1K';
|
|
}
|
|
if (sak === MIFARE_SAK_VALUES.CLASSIC_4K) {
|
|
return 'MIFARE Classic 4K';
|
|
}
|
|
if (sak === MIFARE_SAK_VALUES.CLASSIC_2K) {
|
|
return 'MIFARE Classic 2K';
|
|
}
|
|
if (sak === MIFARE_SAK_VALUES.CLASSIC_MINI) {
|
|
return 'MIFARE Classic Mini';
|
|
}
|
|
if (sak === 0x00) {
|
|
return 'Type 2 Tag (NTAG/Ultralight)';
|
|
}
|
|
if (hasIsoDepCapability(sak)) {
|
|
return 'ISO 14443-4 capable (DESFire/Plus/SmartMX)';
|
|
}
|
|
return `Unknown (SAK: 0x${sak.toString(16).padStart(2, '0')})`;
|
|
}
|
|
|
|
/**
|
|
* iOS MIFARE Classic limitation info
|
|
*/
|
|
export const IOS_MIFARE_CLASSIC_NOTE =
|
|
'iOS can detect MIFARE Classic but cannot perform sector-level operations. ' +
|
|
'For cloning or data extraction, an Android device is required.';
|
|
|
|
// ============================================================================
|
|
// SAK Swap Detection
|
|
// ============================================================================
|
|
|
|
/**
|
|
* SAK values that indicate potential SAK swap capability
|
|
*
|
|
* SAK swap refers to chips that can operate in multiple modes:
|
|
* - MIFARE Plus in SL1 emulates Classic but can switch to SL3
|
|
* - Some magic/clone cards have mutable SAK values
|
|
* - DESFire cards with MIFARE Classic emulation
|
|
*/
|
|
const SAK_SWAP_INDICATORS = {
|
|
// MIFARE Plus SL1 (emulating Classic 1K but can upgrade)
|
|
PLUS_SL1_2K: 0x08, // Same as Classic 1K but actually Plus
|
|
PLUS_SL1_4K: 0x18, // Same as Classic 4K but actually Plus
|
|
|
|
// MIFARE Plus SL2/SL3 (ISO-DEP mode)
|
|
PLUS_SL2_2K: 0x10,
|
|
PLUS_SL2_4K: 0x11,
|
|
PLUS_SL3_2K: 0x20,
|
|
PLUS_SL3_4K: 0x20,
|
|
|
|
// DESFire with MIFARE Application
|
|
DESFIRE_WITH_CLASSIC: 0x28, // DESFire + Classic emulation
|
|
|
|
// Known magic card indicators (Gen2/CUID often have unusual ATQA)
|
|
MAGIC_INDICATOR: 0x00,
|
|
} as const;
|
|
|
|
/**
|
|
* ATQA patterns that might indicate special cards
|
|
*/
|
|
const SUSPICIOUS_ATQA_PATTERNS = {
|
|
// Gen1a magic cards often have ATQA 0x0400
|
|
GEN1A_MAGIC: '04:00',
|
|
// Gen2/CUID cards
|
|
GEN2_MAGIC: '08:04',
|
|
// Standard Classic 1K
|
|
CLASSIC_1K: '00:04',
|
|
// Standard Classic 4K
|
|
CLASSIC_4K: '00:02',
|
|
};
|
|
|
|
/**
|
|
* SAK swap detection result
|
|
*/
|
|
export interface SakSwapDetection {
|
|
/** Whether SAK swap capability was detected */
|
|
hasSakSwap: boolean;
|
|
|
|
/** Type of SAK swap if detected */
|
|
swapType?:
|
|
| 'mifare_plus_sl1'
|
|
| 'desfire_with_classic'
|
|
| 'magic_card'
|
|
| 'unknown';
|
|
|
|
/** Confidence in the detection */
|
|
confidence: 'high' | 'medium' | 'low';
|
|
|
|
/** Human-readable description */
|
|
description: string;
|
|
|
|
/** Additional notes */
|
|
notes?: string[];
|
|
}
|
|
|
|
/**
|
|
* Detect if a tag might have SAK swap capability
|
|
*
|
|
* This checks for indicators that suggest the tag can operate in
|
|
* multiple modes or has been modified from factory defaults.
|
|
*/
|
|
export function detectSakSwap(
|
|
sak: number,
|
|
atqa?: string,
|
|
historicalBytes?: string,
|
|
): SakSwapDetection {
|
|
const notes: string[] = [];
|
|
|
|
// Check for MIFARE Plus in SL1 mode
|
|
// Plus in SL1 looks identical to Classic, but historical bytes may differ
|
|
if (
|
|
(sak === SAK_SWAP_INDICATORS.PLUS_SL1_2K ||
|
|
sak === SAK_SWAP_INDICATORS.PLUS_SL1_4K) &&
|
|
historicalBytes
|
|
) {
|
|
// MIFARE Plus typically has specific historical bytes patterns
|
|
if (
|
|
historicalBytes.includes('C1') ||
|
|
historicalBytes.includes('80:02')
|
|
) {
|
|
notes.push('Historical bytes suggest MIFARE Plus in SL1 mode');
|
|
return {
|
|
hasSakSwap: true,
|
|
swapType: 'mifare_plus_sl1',
|
|
confidence: 'medium',
|
|
description:
|
|
'MIFARE Plus in Security Level 1 (emulating Classic). Can be switched to SL2/SL3 with cryptographic authentication.',
|
|
notes,
|
|
};
|
|
}
|
|
}
|
|
|
|
// Check for DESFire with MIFARE Classic application
|
|
if (sak === SAK_SWAP_INDICATORS.DESFIRE_WITH_CLASSIC) {
|
|
return {
|
|
hasSakSwap: true,
|
|
swapType: 'desfire_with_classic',
|
|
confidence: 'high',
|
|
description:
|
|
'DESFire with MIFARE Classic emulation. Tag operates as both DESFire and Classic.',
|
|
notes: ['Full DESFire functionality available via ISO-DEP'],
|
|
};
|
|
}
|
|
|
|
// Check for Magic card indicators via ATQA
|
|
if (atqa) {
|
|
const cleanAtqa = atqa.toUpperCase();
|
|
|
|
// Gen1a magic cards have unusual ATQA patterns
|
|
if (
|
|
cleanAtqa === SUSPICIOUS_ATQA_PATTERNS.GEN1A_MAGIC &&
|
|
(sak === 0x08 || sak === 0x18)
|
|
) {
|
|
notes.push('ATQA pattern suggests Gen1a magic card');
|
|
return {
|
|
hasSakSwap: true,
|
|
swapType: 'magic_card',
|
|
confidence: 'medium',
|
|
description:
|
|
'Possible Gen1a magic card (UID-writable). SAK and UID can be modified with special commands.',
|
|
notes,
|
|
};
|
|
}
|
|
|
|
// Check for mismatched ATQA/SAK (common in clones)
|
|
const isClassic1kSak = sak === 0x08;
|
|
const isClassic4kSak = sak === 0x18;
|
|
const isClassic1kAtqa = cleanAtqa === SUSPICIOUS_ATQA_PATTERNS.CLASSIC_1K;
|
|
const isClassic4kAtqa = cleanAtqa === SUSPICIOUS_ATQA_PATTERNS.CLASSIC_4K;
|
|
|
|
if (
|
|
(isClassic1kSak && isClassic4kAtqa) ||
|
|
(isClassic4kSak && isClassic1kAtqa)
|
|
) {
|
|
notes.push('SAK/ATQA mismatch suggests modified or clone card');
|
|
return {
|
|
hasSakSwap: true,
|
|
swapType: 'magic_card',
|
|
confidence: 'low',
|
|
description:
|
|
'SAK and ATQA values are inconsistent. May be a magic/clone card with modified parameters.',
|
|
notes,
|
|
};
|
|
}
|
|
}
|
|
|
|
// Check for Plus SL2/SL3 modes
|
|
if (
|
|
sak === SAK_SWAP_INDICATORS.PLUS_SL2_2K ||
|
|
sak === SAK_SWAP_INDICATORS.PLUS_SL2_4K
|
|
) {
|
|
return {
|
|
hasSakSwap: true,
|
|
swapType: 'mifare_plus_sl1',
|
|
confidence: 'high',
|
|
description:
|
|
'MIFARE Plus in Security Level 2. Supports both Classic commands and AES authentication.',
|
|
notes: ['Can fall back to SL1 (Classic) mode in some configurations'],
|
|
};
|
|
}
|
|
|
|
if (
|
|
sak === SAK_SWAP_INDICATORS.PLUS_SL3_2K ||
|
|
sak === SAK_SWAP_INDICATORS.PLUS_SL3_4K
|
|
) {
|
|
// SL3 may look like generic ISO-DEP
|
|
if (historicalBytes?.includes('C1')) {
|
|
return {
|
|
hasSakSwap: true,
|
|
swapType: 'mifare_plus_sl1',
|
|
confidence: 'medium',
|
|
description:
|
|
'MIFARE Plus in Security Level 3 (AES-only mode). May have originated from SL1 configuration.',
|
|
notes: ['Cannot fall back to Classic mode once in SL3'],
|
|
};
|
|
}
|
|
}
|
|
|
|
// No SAK swap detected
|
|
return {
|
|
hasSakSwap: false,
|
|
confidence: 'high',
|
|
description: 'Standard tag with no SAK swap capability detected.',
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Check if tag might be a magic/clone card based on behavior
|
|
*/
|
|
export function mightBeMagicCard(sak: number, atqa?: string): boolean {
|
|
// Gen1a magic cards often have ATQA 0x0400
|
|
if (atqa === SUSPICIOUS_ATQA_PATTERNS.GEN1A_MAGIC) {
|
|
return true;
|
|
}
|
|
|
|
// SAK 0x00 with NfcA tech might be magic NTAG
|
|
if (sak === 0x00 && atqa === '00:44') {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|