From 2409ec362543d2b41946db1af37c1fef7a76d3a6 Mon Sep 17 00:00:00 2001 From: michael Date: Mon, 26 Jan 2026 14:06:49 -0800 Subject: [PATCH] Fixed MFC 1k/4k issue and Ultralight detection --- src/data/chipInfo.ts | 34 +++++++++++++ src/data/products.ts | 78 +++++++++++++++--------------- src/screens/ResultScreen.tsx | 9 +++- src/screens/ScanScreen.tsx | 24 +++++---- src/services/detection/detector.ts | 46 ++++++++++++++++-- src/services/matching/index.ts | 1 + src/services/matching/matcher.ts | 27 ++++++++++- src/services/nfc/NFCManager.ts | 58 +++++++++++++++++++++- src/services/nfc/commands.ts | 3 +- src/types/nfc.ts | 14 ++++++ 10 files changed, 237 insertions(+), 57 deletions(-) diff --git a/src/data/chipInfo.ts b/src/data/chipInfo.ts index 6b5130c..2073a64 100644 --- a/src/data/chipInfo.ts +++ b/src/data/chipInfo.ts @@ -79,6 +79,40 @@ export const CHIP_INFO: Partial> = { memoryNote: '1904 bytes user memory', }, + // MIFARE Ultralight family + [ChipType.ULTRALIGHT]: { + description: + 'Original MIFARE Ultralight - a simple, low-cost NFC tag with minimal memory.', + commonUses: ['Single-use tickets', 'Event passes', 'Disposable tags'], + capabilities: ['NFC Forum Type 2', 'OTP area', 'Lock bits'], + securityLevel: 'low', + memoryNote: '48 bytes user memory', + }, + [ChipType.ULTRALIGHT_C]: { + description: + 'Ultralight with 3DES authentication for basic security applications.', + commonUses: ['Limited-use tickets', 'Access tokens', 'Loyalty cards'], + capabilities: ['NFC Forum Type 2', '3DES authentication', 'Counter'], + securityLevel: 'medium', + memoryNote: '144 bytes user memory', + }, + [ChipType.ULTRALIGHT_EV1]: { + description: + 'Enhanced Ultralight with password protection and originality check.', + commonUses: ['Event tickets', 'Brand protection', 'Gaming tokens'], + capabilities: ['Password protection', 'Originality signature', 'Counter'], + securityLevel: 'low', + memoryNote: '48 or 128 bytes user memory', + }, + [ChipType.ULTRALIGHT_AES]: { + description: + 'Ultralight with AES-128 authentication for secure applications.', + commonUses: ['Secure tickets', 'Brand protection', 'Limited-use passes'], + capabilities: ['AES-128 authentication', 'Counter', 'Originality signature'], + securityLevel: 'high', + memoryNote: '540 bytes user memory', + }, + // MIFARE Classic [ChipType.MIFARE_CLASSIC_1K]: { description: diff --git a/src/data/products.ts b/src/data/products.ts index 2945a61..781ff81 100644 --- a/src/data/products.ts +++ b/src/data/products.ts @@ -64,6 +64,18 @@ const SLIX_COMPATIBLE: ChipType[] = [ ChipType.SLIX_L, ]; +/** + * MIFARE Ultralight compatible chips (for Gen4 magic implants) + * Note: Ultralight AES cannot be cloned if AES protection is active + */ +const ULTRALIGHT_COMPATIBLE: ChipType[] = [ + ChipType.ULTRALIGHT, + ChipType.ULTRALIGHT_C, + ChipType.ULTRALIGHT_EV1, + ChipType.ULTRALIGHT_NANO, + ChipType.ULTRALIGHT_AES, +]; + export const PRODUCTS: Product[] = [ // ========================================================================== // X-Series (Glass Capsule) Implants - NFC @@ -136,8 +148,9 @@ export const PRODUCTS: Product[] = [ 'Dual-frequency', 'NTAG216 NFC chip', '888 bytes user memory', + 'URL/contact sharing', 'T5577 125kHz chip', - 'LF access control', + 'Access control (LF)', ], url: 'https://dangerousthings.com/product/next/', canReceiveClone: true, @@ -158,7 +171,7 @@ export const PRODUCTS: Product[] = [ 'URL/contact sharing', 'Green, Blue, and White LEDs available (HF)', 'T5577 125kHz chip', - 'LF access control', + 'Access control (LF)', ], url: 'https://dangerousthings.com/product/next-v2/', canReceiveClone: true, @@ -175,14 +188,14 @@ export const PRODUCTS: Product[] = [ compatibleChips: [...MIFARE_CLASSIC_COMPATIBLE], features: [ 'Magic MIFARE Classic chip', - 'Changeable UID', + 'Changeable UID (4-byte)', + 'Gen1a and gen2 magic options', 'T5577 125kHz chip', - 'Clone access cards', + 'Access control (LF)', ], url: 'https://dangerousthings.com/product/xmagic/', canReceiveClone: true, exactMatch: false, - notes: 'Can clone MIFARE Classic cards (requires Android + keys)', }, // ========================================================================== @@ -200,7 +213,8 @@ export const PRODUCTS: Product[] = [ 'DESFire EV3 chip', '8KB memory', 'Latest security features', - 'Transaction MAC', + 'Access control (HF)', + 'URL/contact sharing', ], url: 'https://dangerousthings.com/product/xdf3/', canReceiveClone: false, @@ -221,14 +235,12 @@ export const PRODUCTS: Product[] = [ compatibleChips: [...MIFARE_CLASSIC_COMPATIBLE], features: [ 'Magic MIFARE Classic 1K', - 'Changeable UID', - 'Clone access cards', - 'Gen1a compatible', + 'Gen1a and gen2 magic options', + 'Changeable UID (4-byte)', ], url: 'https://dangerousthings.com/product/xm1/', canReceiveClone: true, exactMatch: false, - notes: 'Can clone MIFARE Classic 1K cards (requires Android + keys)', }, // ========================================================================== @@ -243,6 +255,7 @@ export const PRODUCTS: Product[] = [ categories: [ProductCategory.SECURE], compatibleChips: [ChipType.NTAG424_DNA, ChipType.NTAG424_DNA_TT, ChipType.ICODE_DNA], features: [ + 'VivoKey Ecosystem', 'Spark Actions', 'Verify API', ], @@ -265,9 +278,11 @@ export const PRODUCTS: Product[] = [ compatibleChips: [...NTAG21X_COMPATIBLE, ...NTAG_I2C_COMPATIBLE], features: [ 'NTAG216 NFC chip', + '888 bytes user memory', + 'URL/contact sharing', 'T5577 125kHz chip', - 'Enhanced read range', 'Bioresin encapsulation', + 'Enhanced read range', ], url: 'https://dangerousthings.com/product/dnext/', canReceiveClone: true, @@ -281,17 +296,20 @@ export const PRODUCTS: Product[] = [ 'Bioresin dual-frequency implant with Ultimate Gen4 magic MIFARE and T5577 for maximum compatibility.', formFactor: FormFactor.BIORESIN, categories: [ProductCategory.DUAL_FREQUENCY, ProductCategory.ACCESS], - compatibleChips: [...MIFARE_CLASSIC_COMPATIBLE], + compatibleChips: [...MIFARE_CLASSIC_COMPATIBLE, ...ULTRALIGHT_COMPATIBLE], features: [ 'Ultimate Gen4 magic chip', + 'Changeable UID', + 'Access control (HF)', 'T5577 125kHz chip', 'Changeable UID', 'Enhanced cloning capability', + 'Access control (LF)', ], url: 'https://dangerousthings.com/product/dug4t/', canReceiveClone: true, exactMatch: false, - notes: 'Can clone MIFARE Classic cards (requires Android + keys)', + notes: 'Supports 1K and 4K cloning. Also includes T5577 for LF.', }, // ========================================================================== @@ -308,7 +326,7 @@ export const PRODUCTS: Product[] = [ features: [ 'NTAG216 chip', '888 bytes user memory', - 'Flexible form factor', + 'URL/contact sharing', 'Extended read range', ], url: 'https://dangerousthings.com/product/flexnt/', @@ -331,7 +349,7 @@ export const PRODUCTS: Product[] = [ 'DESFire EV2 chip', '8KB memory', 'AES-128 encryption', - 'Flexible form factor', + 'URL/contact sharing', ], url: 'https://dangerousthings.com/product/flexdf2/', canReceiveClone: false, @@ -343,24 +361,6 @@ export const PRODUCTS: Product[] = [ // ========================================================================== // Flex Implants - Magic/Cloning // ========================================================================== - { - id: 'flexm1', - name: 'flexM1', - description: 'Flexible magic MIFARE Classic 1K implant for cloning access cards.', - formFactor: FormFactor.FLEX, - categories: [ProductCategory.ACCESS], - compatibleChips: [...MIFARE_CLASSIC_COMPATIBLE], - features: [ - 'Magic MIFARE Classic 1K', - 'Changeable UID', - 'Flexible form factor', - 'Extended read range', - ], - url: 'https://dangerousthings.com/product/flexm1/', - canReceiveClone: true, - exactMatch: false, - notes: 'Can clone MIFARE Classic 1K cards (requires Android + keys)', - }, { id: 'flexm1-v2', name: 'flexM1 v2', @@ -377,7 +377,6 @@ export const PRODUCTS: Product[] = [ url: 'https://dangerousthings.com/product/flexm1-v2/', canReceiveClone: true, exactMatch: false, - notes: 'Can clone MIFARE Classic 1K cards (requires Android + keys)', }, // ========================================================================== @@ -394,9 +393,9 @@ export const PRODUCTS: Product[] = [ features: [ 'JCOP4 secure element', 'Fidesmo platform', - 'Payment capable', 'FIDO2/WebAuthn', 'OTP support', + 'URL/contact sharing', ], url: 'https://dangerousthings.com/product/apex-flex/', canReceiveClone: false, @@ -416,6 +415,9 @@ export const PRODUCTS: Product[] = [ 'Full JavaCard access', 'GlobalPlatformPro compatible', 'Custom applet support', + 'FIDO2/WebAuthn', + 'OTP support', + 'URL/contact sharing', ], url: 'https://dangerousthings.com/product/flexsecure/', canReceiveClone: false, @@ -436,7 +438,6 @@ export const PRODUCTS: Product[] = [ features: [ 'HID iCLASS SE compatible', 'Enterprise access control', - 'Flexible form factor', ], url: 'https://dangerousthings.com/product/flexclass/', canReceiveClone: false, @@ -449,17 +450,16 @@ export const PRODUCTS: Product[] = [ description: 'Flexible Ultimate Gen4 magic MIFARE implant for cloning access cards.', formFactor: FormFactor.FLEX, categories: [ProductCategory.ACCESS], - compatibleChips: [...MIFARE_CLASSIC_COMPATIBLE], + compatibleChips: [...MIFARE_CLASSIC_COMPATIBLE, ...ULTRALIGHT_COMPATIBLE], features: [ 'Ultimate Gen4 magic chip', 'Changeable UID', - 'Flexible form factor', 'Enhanced cloning capability', ], url: 'https://dangerousthings.com/product/flexug4/', canReceiveClone: true, exactMatch: false, - notes: 'Can clone MIFARE Classic cards (requires Android + keys)', + notes: 'Supports 1K and 4K cloning.', }, ]; diff --git a/src/screens/ResultScreen.tsx b/src/screens/ResultScreen.tsx index 0d06efa..19e5d99 100644 --- a/src/screens/ResultScreen.tsx +++ b/src/screens/ResultScreen.tsx @@ -3,7 +3,7 @@ import { StyleSheet, View, ScrollView, Linking, TouchableOpacity } from 'react-n import { Button, Text, Surface, Divider, Chip } from 'react-native-paper'; import type { ResultScreenProps } from '../types/navigation'; import { DTColors } from '../theme'; -import { matchChipToProducts, getMatchSummary, getDesfireEvMismatchWarning } from '../services/matching'; +import { matchChipToProducts, getMatchSummary, getDesfireEvMismatchWarning, getMifareClassicCapacityWarning } from '../services/matching'; import { Product } from '../types/products'; import { getChipInfo, getChipFamilyInfo, getSecurityLevelDescription } from '../data/chipInfo'; import { getChipFamily } from '../types/detection'; @@ -296,6 +296,13 @@ export function ResultScreen({ route, navigation }: ResultScreenProps) { )} + {/* MIFARE Classic 4K → 1K capacity warning */} + {getMifareClassicCapacityWarning(transponder.type, product) && ( + + ⚠️ {getMifareClassicCapacityWarning(transponder.type, product)} + + )} + {product.features.slice(0, 3).map((feature, idx) => ( diff --git a/src/screens/ScanScreen.tsx b/src/screens/ScanScreen.tsx index 1910095..b0f88b7 100644 --- a/src/screens/ScanScreen.tsx +++ b/src/screens/ScanScreen.tsx @@ -1,13 +1,13 @@ -import React, {useCallback, useEffect} from 'react'; -import {StyleSheet, View, Platform} from 'react-native'; -import {Button, Text} from 'react-native-paper'; -import type {ScanScreenProps} from '../types/navigation'; -import {DTColors} from '../theme'; -import {useScan} from '../hooks'; -import {getScanInstructions} from '../services/nfc'; -import {ScanAnimation} from '../components'; +import React, { useCallback, useEffect } from 'react'; +import { StyleSheet, View, Platform } from 'react-native'; +import { Button, Text } from 'react-native-paper'; +import type { ScanScreenProps } from '../types/navigation'; +import { DTColors } from '../theme'; +import { useScan } from '../hooks'; +import { getScanInstructions } from '../services/nfc'; +import { ScanAnimation } from '../components'; -export function ScanScreen({navigation}: ScanScreenProps) { +export function ScanScreen({ navigation }: ScanScreenProps) { const { state, tag, @@ -194,6 +194,12 @@ export function ScanScreen({navigation}: ScanScreenProps) { {getScanInstructions()} + + Not Scanning? + + + Phones can only read high frequency (13.56 MHz) -- low and ultra high frequency transponders cannot be scanned. + {scanProgress && scanProgress.current > 1 && ( {scanProgress.step} diff --git a/src/services/detection/detector.ts b/src/services/detection/detector.ts index b250a6a..eb3068b 100644 --- a/src/services/detection/detector.ts +++ b/src/services/detection/detector.ts @@ -146,12 +146,29 @@ export async function detectChip( // If we have MifareClassic tech type and NO IsoDep, it's definitely Classic if (hasMifareClassicTech && !hasIsoDepTech) { - // Determine 1K vs 4K based on SAK or UID length + // Determine 1K vs 4K vs Mini + // Priority: mifareClassic.size (most reliable on Android) > SAK > UID length let chipType = ChipType.MIFARE_CLASSIC_1K; // Default to 1K let memorySize = 1024; - // SAK 0x18 or 0x38 indicates 4K - if (sak === 0x18 || sak === 0x38 || sak === 0x98) { + // First, check mifareClassic object from Android (most reliable) + if (rawData.mifareClassic?.size) { + const size = rawData.mifareClassic.size; + console.log('[Detector] Using mifareClassic.size:', size); + if (size >= 4096) { + chipType = ChipType.MIFARE_CLASSIC_4K; + memorySize = 4096; + } else if (size >= 1024) { + chipType = ChipType.MIFARE_CLASSIC_1K; + memorySize = 1024; + } else if (size >= 320) { + chipType = ChipType.MIFARE_CLASSIC_MINI; + memorySize = 320; + } + } + // Fallback to SAK-based detection + // SAK 0x18, 0x38, or 0x98 indicates 4K + else if (sak === 0x18 || sak === 0x38 || sak === 0x98) { chipType = ChipType.MIFARE_CLASSIC_4K; memorySize = 4096; } @@ -233,8 +250,27 @@ export async function detectChip( }), }; } - // If GET_VERSION failed but tag looks like Type 2, mark as unknown NTAG - if (sak === 0x00 && techTypes.some(t => t.includes('NfcA'))) { + // If GET_VERSION failed, check for MIFARE Ultralight + // Original Ultralight doesn't support GET_VERSION (only EV1+ does) + // SAK can be 0x00 or undefined for Ultralight + if ((sak === 0x00 || sak === undefined) && techTypes.some(t => t.includes('NfcA'))) { + // Check for MifareUltralight tech type (Android provides this) + const hasMifareUltralightTech = techTypes.some(t => + t.includes('MifareUltralight'), + ); + + if (hasMifareUltralightTech) { + console.log('[Detector] MifareUltralight tech detected, identifying as original Ultralight'); + return { + success: true, + transponder: createTransponder(ChipType.ULTRALIGHT, rawData, { + memorySize: 48, // Original Ultralight has 48 bytes user memory + confidence: 'medium', + }), + }; + } + + // No MifareUltralight tech - fall back to unknown NTAG console.log('[Detector] NTAG detection failed, falling back to NTAG_UNKNOWN'); return { success: true, diff --git a/src/services/matching/index.ts b/src/services/matching/index.ts index a7b44c7..8fecd03 100644 --- a/src/services/matching/index.ts +++ b/src/services/matching/index.ts @@ -11,4 +11,5 @@ export { canCloneToProduct, getDesfireEvLevel, getDesfireEvMismatchWarning, + getMifareClassicCapacityWarning, } from './matcher'; diff --git a/src/services/matching/matcher.ts b/src/services/matching/matcher.ts index c4af529..bf5c8e7 100644 --- a/src/services/matching/matcher.ts +++ b/src/services/matching/matcher.ts @@ -35,7 +35,6 @@ export function matchChipToProducts(chip: Transponder): MatchResult { if (product.canReceiveClone && cloneability?.cloneable) { if ((product.name.startsWith("xMagic") || product.name.startsWith("xM1") || product.name.startsWith("flexM1")) && chip.rawData.uid.replaceAll(":", "").length / 2 !== 4) { // This skips things without a 4-byte UID - } else { cloneTargets.push(product); } @@ -177,6 +176,32 @@ export function getDesfireEvLevel(chipType: ChipType): DesfireEvLevel | null { } } +/** + * IDs of products that only support MIFARE Classic 1K (not 4K) + */ +const MIFARE_1K_ONLY_PRODUCTS = new Set(['xmagic', 'xm1', 'flexm1-v2']); + +/** + * Check if a scanned 4K card is being matched to a 1K-only implant + * Returns warning message if capacity mismatch, null if no issue + */ +export function getMifareClassicCapacityWarning( + chipType: ChipType, + product: Product, +): string | null { + // Only applies to MIFARE Classic 4K cards + if (chipType !== ChipType.MIFARE_CLASSIC_4K) { + return null; + } + + // Only warn for 1K-only implants + if (!MIFARE_1K_ONLY_PRODUCTS.has(product.id)) { + return null; + } + + return 'This implant has 1K memory only - might not have capacity to clone your 4K card.'; +} + /** * Check if there's a DESFire EV mismatch between chip and product * Returns warning message if mismatch, null if no issue diff --git a/src/services/nfc/NFCManager.ts b/src/services/nfc/NFCManager.ts index b9a7f78..4f0ea2b 100644 --- a/src/services/nfc/NFCManager.ts +++ b/src/services/nfc/NFCManager.ts @@ -12,6 +12,7 @@ import type { ScanErrorType, NfcTechType, NdefRecord, + MifareClassicInfo, } from '../../types/nfc'; /** @@ -116,6 +117,56 @@ function parseAts(tag: TagEvent): {ats?: string; historicalBytes?: string} { return {ats, historicalBytes}; } +/** + * Parse MIFARE Classic info from TagEvent (Android only) + * + * Uses NDEF maxSize to determine card capacity: + * - 4K: maxSize ~3356 bytes (NDEF capacity) + * - 1K: maxSize ~716 bytes (NDEF capacity) + * - Mini: maxSize ~80 bytes (NDEF capacity) + */ +function parseMifareClassic(tag: TagEvent): MifareClassicInfo | undefined { + const techTypes = tag.techTypes || []; + const isMifareClassic = techTypes.some(t => t.includes('MifareClassic')); + + if (!isMifareClassic) { + return undefined; + } + + // Use NDEF maxSize to determine card type + // MIFARE Classic 4K has ~3356 bytes NDEF capacity + // MIFARE Classic 1K has ~716 bytes NDEF capacity + const maxSize = (tag as any).maxSize; + + if (typeof maxSize === 'number' && maxSize > 0) { + let size: number; + let sectorCount: number; + let blockCount: number; + + if (maxSize >= 2000) { + // 4K card (NDEF maxSize ~3356) + size = 4096; + sectorCount = 40; + blockCount = 256; + } else if (maxSize >= 500) { + // 1K card (NDEF maxSize ~716) + size = 1024; + sectorCount = 16; + blockCount = 64; + } else { + // Mini card (NDEF maxSize ~80) + size = 320; + sectorCount = 5; + blockCount = 20; + } + + console.log('[NFCManager] MIFARE Classic from maxSize:', {maxSize, size, sectorCount, blockCount}); + return {size, sectorCount, blockCount}; + } + + return undefined; +} + /** * Parse NDEF records from TagEvent */ @@ -167,6 +218,7 @@ function tagEventToRawData(tag: TagEvent): RawTagData { const atqa = parseAtqa(tag); const {ats, historicalBytes} = parseAts(tag); const ndefRecords = parseNdefRecords(tag); + const mifareClassic = parseMifareClassic(tag); const isoDep = (tag as any).isoDep; const iso7816 = (tag as any).iso7816; @@ -204,6 +256,7 @@ function tagEventToRawData(tag: TagEvent): RawTagData { historicalBytes, maxTransceiveLength, ndefRecords, + mifareClassic, }; } @@ -366,7 +419,10 @@ class NFCManagerService { }; } - return {tag: tagEventToRawData(tag)}; + // Convert to RawTagData + const rawData = tagEventToRawData(tag); + + return {tag: rawData}; } catch (error) { return {error: categorizeError(error)}; } finally { diff --git a/src/services/nfc/commands.ts b/src/services/nfc/commands.ts index 0fdb070..2060b06 100644 --- a/src/services/nfc/commands.ts +++ b/src/services/nfc/commands.ts @@ -207,7 +207,8 @@ export async function transceiveNfcA(command: number[]): Promise { const response = await NfcManager.nfcAHandler.transceive(command); return Array.from(response); } catch (error) { - console.error('[commands] NfcA transceive failed:', error); + // Use debug level - some failures are expected (e.g., GET_VERSION on original Ultralight) + console.debug('[commands] NfcA transceive failed:', error); throw error; } } diff --git a/src/types/nfc.ts b/src/types/nfc.ts index e3843bc..4c6f8ac 100644 --- a/src/types/nfc.ts +++ b/src/types/nfc.ts @@ -32,6 +32,18 @@ export interface NdefRecord { payload: number[]; } +/** + * MIFARE Classic specific info from Android + */ +export interface MifareClassicInfo { + /** Total memory size in bytes */ + size: number; + /** Number of sectors */ + sectorCount: number; + /** Total number of blocks */ + blockCount: number; +} + /** * Raw tag data from NFC scan */ @@ -52,6 +64,8 @@ export interface RawTagData { maxTransceiveLength?: number; /** Cached NDEF records (if tag supports NDEF) */ ndefRecords?: NdefRecord[]; + /** MIFARE Classic info (Android only) */ + mifareClassic?: MifareClassicInfo; } /**