Files
dt-nfc-identifier/src/services/detection/mifare.ts
michael fb7f836abd Add chip detection system for NTAG, DESFire, ISO 15693, and JavaCard
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>
2026-01-22 18:08:38 -08:00

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;
}