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>
This commit is contained in:
michael
2026-01-22 18:08:38 -08:00
parent 874a25d7e4
commit fb7f836abd
15 changed files with 3831 additions and 61 deletions

View File

@@ -5,13 +5,17 @@
import {useState, useCallback, useEffect, useRef} from 'react'; import {useState, useCallback, useEffect, useRef} from 'react';
import {nfcManager} from '../services/nfc'; import {nfcManager} from '../services/nfc';
import {detectChip} from '../services/detection';
import type {ScanState, RawTagData, ScanError, NFCStatus} from '../types/nfc'; import type {ScanState, RawTagData, ScanError, NFCStatus} from '../types/nfc';
import type {Transponder} from '../types/detection';
export interface UseScanResult { export interface UseScanResult {
/** Current scan state */ /** Current scan state */
state: ScanState; state: ScanState;
/** Scanned tag data (when state is 'success') */ /** Scanned tag data (when state is 'success') */
tag: RawTagData | null; tag: RawTagData | null;
/** Detected transponder info (when detection succeeds) */
transponder: Transponder | null;
/** Scan error (when state is 'error') */ /** Scan error (when state is 'error') */
error: ScanError | null; error: ScanError | null;
/** NFC status (supported and enabled) */ /** NFC status (supported and enabled) */
@@ -32,6 +36,7 @@ export interface UseScanResult {
export function useScan(): UseScanResult { export function useScan(): UseScanResult {
const [state, setState] = useState<ScanState>('idle'); const [state, setState] = useState<ScanState>('idle');
const [tag, setTag] = useState<RawTagData | null>(null); const [tag, setTag] = useState<RawTagData | null>(null);
const [transponder, setTransponder] = useState<Transponder | null>(null);
const [error, setError] = useState<ScanError | null>(null); const [error, setError] = useState<ScanError | null>(null);
const [nfcStatus, setNfcStatus] = useState<NFCStatus>({ const [nfcStatus, setNfcStatus] = useState<NFCStatus>({
isSupported: false, isSupported: false,
@@ -76,6 +81,7 @@ export function useScan(): UseScanResult {
scanInProgress.current = true; scanInProgress.current = true;
setState('scanning'); setState('scanning');
setTag(null); setTag(null);
setTransponder(null);
setError(null); setError(null);
try { try {
@@ -107,8 +113,12 @@ export function useScan(): UseScanResult {
return; return;
} }
// Perform the scan // Perform scan with detection in one session
const result = await nfcManager.scanTag(); const result = await nfcManager.scanWithDetection(async tagData => {
// Run chip detection while NFC session is still active
const detectionResult = await detectChip(tagData);
return detectionResult.success ? detectionResult.transponder : null;
});
if (!isMounted.current) { if (!isMounted.current) {
return; return;
@@ -125,6 +135,9 @@ export function useScan(): UseScanResult {
} else if (result.tag) { } else if (result.tag) {
setState('success'); setState('success');
setTag(result.tag); setTag(result.tag);
if (result.detection) {
setTransponder(result.detection);
}
} else { } else {
setState('error'); setState('error');
setError({ setError({
@@ -160,6 +173,7 @@ export function useScan(): UseScanResult {
const reset = useCallback(() => { const reset = useCallback(() => {
setState('idle'); setState('idle');
setTag(null); setTag(null);
setTransponder(null);
setError(null); setError(null);
}, []); }, []);
@@ -176,6 +190,7 @@ export function useScan(): UseScanResult {
return { return {
state, state,
tag, tag,
transponder,
error, error,
nfcStatus, nfcStatus,
startScan, startScan,

View File

@@ -1,22 +1,124 @@
import React from 'react'; import React from 'react';
import {StyleSheet, View, ScrollView, Linking} from 'react-native'; import {StyleSheet, View, ScrollView, Linking} from 'react-native';
import {Button, Text, Surface, Divider} from 'react-native-paper'; import {Button, Text, Surface, Divider, Chip} from 'react-native-paper';
import type {ResultScreenProps} from '../types/navigation'; import type {ResultScreenProps} from '../types/navigation';
import {DTColors} from '../theme'; import {DTColors} from '../theme';
export function ResultScreen({route, navigation}: ResultScreenProps) { export function ResultScreen({route, navigation}: ResultScreenProps) {
const {tagData} = route.params; const {tagData, transponder} = route.params;
const handleConversionLink = () => { const handleConversionLink = () => {
Linking.openURL('https://dngr.us/conversion'); Linking.openURL('https://dngr.us/conversion');
}; };
// Determine card colors based on detection
const getCloneabilityColor = () => {
if (!transponder) return DTColors.light;
return transponder.isCloneable ? DTColors.modeSuccess : DTColors.modeWarning;
};
const getConfidenceLabel = () => {
if (!transponder) return null;
const colors = {
high: DTColors.modeSuccess,
medium: DTColors.modeEmphasis,
low: DTColors.modeWarning,
};
return {color: colors[transponder.confidence], label: `${transponder.confidence.toUpperCase()} CONFIDENCE`};
};
return ( return (
<ScrollView style={styles.container}> <ScrollView style={styles.container}>
<View style={styles.content}> <View style={styles.content}>
{/* Chip Identification Card */}
{transponder && (
<Surface style={styles.identificationCard} elevation={1}>
<Text variant="labelLarge" style={styles.identificationLabel}>
CHIP IDENTIFIED
</Text>
<Divider style={styles.divider} />
<Text variant="headlineMedium" style={styles.chipName}>
{transponder.chipName}
</Text>
<View style={styles.chipMeta}>
<Chip
style={[styles.familyChip, {borderColor: DTColors.modeNormal}]}
textStyle={styles.familyChipText}>
{transponder.family}
</Chip>
{getConfidenceLabel() && (
<Chip
style={[styles.confidenceChip, {borderColor: getConfidenceLabel()!.color}]}
textStyle={[styles.confidenceChipText, {color: getConfidenceLabel()!.color}]}>
{getConfidenceLabel()!.label}
</Chip>
)}
</View>
{transponder.memorySize && (
<View style={styles.detailRow}>
<Text variant="bodyMedium" style={styles.detailLabel}>
MEMORY
</Text>
<Text variant="bodyLarge" style={styles.detailValue}>
{transponder.memorySize} bytes
</Text>
</View>
)}
<View style={styles.detailRow}>
<Text variant="bodyMedium" style={styles.detailLabel}>
CLONEABLE
</Text>
<Text
variant="bodyLarge"
style={[styles.detailValue, {color: getCloneabilityColor()}]}>
{transponder.isCloneable ? 'YES' : 'NO'}
</Text>
</View>
{transponder.cloneabilityNote && (
<Text variant="bodySmall" style={styles.cloneabilityNote}>
{transponder.cloneabilityNote}
</Text>
)}
</Surface>
)}
{/* SAK Swap Detection Card */}
{transponder?.sakSwapInfo?.hasSakSwap && (
<Surface style={styles.sakSwapCard} elevation={1}>
<Text variant="labelLarge" style={styles.sakSwapLabel}>
SAK SWAP DETECTED
</Text>
<Divider style={[styles.divider, {backgroundColor: DTColors.modeEmphasis}]} />
<Text variant="bodyLarge" style={styles.sakSwapType}>
{transponder.sakSwapInfo.swapType?.replace(/_/g, ' ').toUpperCase()}
</Text>
<Text variant="bodyMedium" style={styles.sakSwapDescription}>
{transponder.sakSwapInfo.description}
</Text>
{transponder.sakSwapInfo.notes && transponder.sakSwapInfo.notes.length > 0 && (
<View style={styles.sakSwapNotes}>
{transponder.sakSwapInfo.notes.map((note, index) => (
<Text key={index} variant="bodySmall" style={styles.sakSwapNote}>
{note}
</Text>
))}
</View>
)}
</Surface>
)}
{/* Raw Tag Data Card */}
<Surface style={styles.resultCard} elevation={1}> <Surface style={styles.resultCard} elevation={1}>
<Text variant="labelLarge" style={styles.cardLabel}> <Text variant="labelLarge" style={styles.cardLabel}>
TAG DETECTED RAW TAG DATA
</Text> </Text>
<Divider style={styles.divider} /> <Divider style={styles.divider} />
@@ -80,25 +182,24 @@ export function ResultScreen({route, navigation}: ResultScreenProps) {
)} )}
</Surface> </Surface>
<Surface style={styles.matchCard} elevation={1}> {/* No Detection Card (fallback) */}
<Text variant="labelLarge" style={styles.matchLabel}> {!transponder && (
COMPATIBLE IMPLANTS <Surface style={styles.noDetectionCard} elevation={1}>
<Text variant="labelLarge" style={styles.noDetectionLabel}>
CHIP NOT IDENTIFIED
</Text> </Text>
<Divider style={styles.divider} /> <Divider style={[styles.divider, {backgroundColor: DTColors.modeWarning}]} />
<Text variant="bodyMedium" style={styles.noDetectionText}>
<Text variant="bodyLarge" style={styles.placeholderText}> Unable to identify this chip type. It may be unsupported or require advanced detection.
Detection coming in Phase 3-5
</Text>
<Text variant="bodyMedium" style={styles.hintText}>
Full chip identification and product matching will be implemented in
later phases.
</Text> </Text>
</Surface> </Surface>
)}
{/* Conversion Service Card - only show if chip is NOT cloneable */}
{(!transponder || !transponder.isCloneable) && (
<Surface style={styles.conversionCard} elevation={1}> <Surface style={styles.conversionCard} elevation={1}>
<Text variant="bodyMedium" style={styles.conversionText}> <Text variant="bodyMedium" style={styles.conversionText}>
Can't find a match? Check out our conversion service. Can't clone this chip? Check out our conversion service.
</Text> </Text>
<Button <Button
mode="outlined" mode="outlined"
@@ -108,7 +209,9 @@ export function ResultScreen({route, navigation}: ResultScreenProps) {
CONVERSION SERVICE CONVERSION SERVICE
</Button> </Button>
</Surface> </Surface>
)}
{/* Action Buttons */}
<View style={styles.actions}> <View style={styles.actions}>
<Button <Button
mode="outlined" mode="outlined"
@@ -138,6 +241,98 @@ const styles = StyleSheet.create({
content: { content: {
padding: 24, padding: 24,
}, },
// Identification Card
identificationCard: {
backgroundColor: '#0a0a0a',
borderRadius: 4,
borderWidth: 2,
borderColor: DTColors.modeSuccess,
padding: 20,
marginBottom: 20,
},
identificationLabel: {
color: DTColors.modeSuccess,
letterSpacing: 2,
marginBottom: 12,
},
chipName: {
color: DTColors.light,
fontWeight: 'bold',
marginBottom: 12,
},
chipMeta: {
flexDirection: 'row',
gap: 8,
marginBottom: 16,
},
familyChip: {
backgroundColor: 'transparent',
borderWidth: 1,
},
familyChipText: {
color: DTColors.modeNormal,
fontSize: 12,
},
confidenceChip: {
backgroundColor: 'transparent',
borderWidth: 1,
},
confidenceChipText: {
fontSize: 12,
},
detailRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 8,
},
detailLabel: {
color: DTColors.light,
opacity: 0.6,
letterSpacing: 1,
},
detailValue: {
color: DTColors.light,
},
cloneabilityNote: {
color: DTColors.light,
opacity: 0.5,
fontStyle: 'italic',
marginTop: 8,
},
// SAK Swap Card
sakSwapCard: {
backgroundColor: '#0a0a0a',
borderRadius: 4,
borderWidth: 2,
borderColor: DTColors.modeEmphasis,
padding: 20,
marginBottom: 20,
},
sakSwapLabel: {
color: DTColors.modeEmphasis,
letterSpacing: 2,
marginBottom: 12,
},
sakSwapType: {
color: DTColors.modeEmphasis,
fontWeight: 'bold',
marginBottom: 8,
},
sakSwapDescription: {
color: DTColors.light,
opacity: 0.9,
marginBottom: 12,
},
sakSwapNotes: {
marginTop: 8,
},
sakSwapNote: {
color: DTColors.light,
opacity: 0.7,
marginBottom: 4,
},
// Raw Data Card
resultCard: { resultCard: {
backgroundColor: '#0a0a0a', backgroundColor: '#0a0a0a',
borderRadius: 4, borderRadius: 4,
@@ -174,29 +369,25 @@ const styles = StyleSheet.create({
opacity: 0.6, opacity: 0.6,
fontStyle: 'italic', fontStyle: 'italic',
}, },
matchCard: { // No Detection Card
noDetectionCard: {
backgroundColor: '#0a0a0a', backgroundColor: '#0a0a0a',
borderRadius: 4, borderRadius: 4,
borderWidth: 1, borderWidth: 1,
borderColor: DTColors.modeEmphasis, borderColor: DTColors.modeWarning,
padding: 20, padding: 20,
marginBottom: 20, marginBottom: 20,
}, },
matchLabel: { noDetectionLabel: {
color: DTColors.modeEmphasis, color: DTColors.modeWarning,
letterSpacing: 2, letterSpacing: 2,
marginBottom: 12, marginBottom: 12,
}, },
placeholderText: { noDetectionText: {
color: DTColors.light, color: DTColors.light,
opacity: 0.8, opacity: 0.8,
marginBottom: 8,
},
hintText: {
color: DTColors.light,
opacity: 0.5,
fontStyle: 'italic',
}, },
// Conversion Card
conversionCard: { conversionCard: {
backgroundColor: '#0a0a0a', backgroundColor: '#0a0a0a',
borderRadius: 4, borderRadius: 4,
@@ -220,6 +411,7 @@ const styles = StyleSheet.create({
color: DTColors.modeOther, color: DTColors.modeOther,
letterSpacing: 1, letterSpacing: 1,
}, },
// Actions
actions: { actions: {
alignItems: 'center', alignItems: 'center',
gap: 16, gap: 16,

View File

@@ -10,6 +10,7 @@ export function ScanScreen({navigation}: ScanScreenProps) {
const { const {
state, state,
tag, tag,
transponder,
error, error,
nfcStatus, nfcStatus,
startScan, startScan,
@@ -17,7 +18,7 @@ export function ScanScreen({navigation}: ScanScreenProps) {
openSettings, openSettings,
} = useScan(); } = useScan();
// Navigate to results when scan succeeds // Navigate to results when scan succeeds (detection happens in the hook)
useEffect(() => { useEffect(() => {
if (state === 'success' && tag) { if (state === 'success' && tag) {
navigation.replace('Result', { navigation.replace('Result', {
@@ -29,9 +30,10 @@ export function ScanScreen({navigation}: ScanScreenProps) {
ats: tag.ats, ats: tag.ats,
historicalBytes: tag.historicalBytes, historicalBytes: tag.historicalBytes,
}, },
transponder: transponder ?? undefined,
}); });
} }
}, [state, tag, navigation]); }, [state, tag, transponder, navigation]);
// Auto-start scan when screen loads // Auto-start scan when screen loads
useEffect(() => { useEffect(() => {
@@ -185,7 +187,10 @@ export function ScanScreen({navigation}: ScanScreenProps) {
style={styles.processingSpinner} style={styles.processingSpinner}
/> />
<Text variant="headlineMedium" style={styles.processingText}> <Text variant="headlineMedium" style={styles.processingText}>
PROCESSING IDENTIFYING
</Text>
<Text variant="bodyMedium" style={styles.detectingHint}>
Analyzing chip type...
</Text> </Text>
</> </>
)} )}
@@ -297,6 +302,11 @@ const styles = StyleSheet.create({
color: DTColors.modeEmphasis, color: DTColors.modeEmphasis,
letterSpacing: 4, letterSpacing: 4,
}, },
detectingHint: {
color: DTColors.light,
opacity: 0.7,
marginTop: 12,
},
footer: { footer: {
alignItems: 'center', alignItems: 'center',
paddingBottom: 20, paddingBottom: 20,

View File

@@ -0,0 +1,407 @@
/**
* DESFire Detector
* Identifies MIFARE DESFire EV1/EV2/EV3, DESFire DNA variants,
* DESFire Light, and NTAG 424 DNA using GET_VERSION command
*/
import {ChipType, DesfireVersionInfo} from '../../types/detection';
import {
DESFIRE_GET_VERSION,
DESFIRE_GET_VERSION_CONTINUE,
sendIsoDepCommand,
parseApduResponse,
} from '../nfc/commands';
/**
* Product type values from GET_VERSION byte 1
*/
const PRODUCT_TYPES = {
DESFIRE: 0x01, // Standard DESFire
NTAG_I2C: 0x05, // NTAG I2C family (can have ISO-DEP on Plus variants)
DESFIRE_LIGHT: 0x08, // DESFire Light
NTAG_424_DNA: 0x21, // NTAG 424 DNA family
} as const;
/**
* DESFire hardware major version to chip type mapping
* Per NXP MF3D(H)x1, MF3D(H)x2, MF3DHx3 datasheets
*
* Byte 3 (hwMajor) of GET_VERSION response indicates the version:
* - EV1: 0x00, 0x01
* - EV2: 0x12, 0x22 (standard), 0x32 (DNA capable)
* - EV3: 0x30, 0x31, 0x32, 0x33, 0x34 (various configs)
*
* Note: The version byte encoding changed between generations
*/
const DESFIRE_VERSION_MAP: Record<number, ChipType> = {
// DESFire EV1 (and legacy EV0)
0x00: ChipType.DESFIRE_EV1, // EV0/Legacy
0x01: ChipType.DESFIRE_EV1, // EV1 standard
// DESFire EV2
0x10: ChipType.DESFIRE_EV2, // Some EV2 variants
0x11: ChipType.DESFIRE_EV2, // Some EV2 variants
0x12: ChipType.DESFIRE_EV2, // EV2 standard
0x20: ChipType.DESFIRE_EV2, // Some EV2 variants
0x21: ChipType.DESFIRE_EV2, // Some EV2 variants
0x22: ChipType.DESFIRE_EV2, // EV2 alternative
// DESFire EV3 - per NXP MF3DHx3 datasheet
0x30: ChipType.DESFIRE_EV3, // EV3 standard
0x31: ChipType.DESFIRE_EV3, // EV3 variant
0x32: ChipType.DESFIRE_EV3, // EV3 (may also be EV2 DNA in some docs)
0x33: ChipType.DESFIRE_EV3, // EV3 variant
0x34: ChipType.DESFIRE_EV3, // EV3 variant
0x35: ChipType.DESFIRE_EV3, // EV3 variant
0x36: ChipType.DESFIRE_EV3, // EV3 variant
};
/**
* DESFire DNA detection notes:
* DNA variants have originality signature capability, but this CANNOT be
* reliably determined from GET_VERSION alone. The major version ranges
* overlap between standard and DNA variants.
*
* To properly detect DNA, you would need to:
* 1. Attempt to read the originality signature (command 0x3C)
* 2. Check if it succeeds (DNA) or fails (non-DNA)
*
* For now, we report the base EV version without assuming DNA status.
*/
/**
* DESFire storage size decoding
* The storage size byte encodes capacity
*/
const DESFIRE_STORAGE_SIZES: Record<number, number> = {
0x10: 256, // 256 bytes (DESFire Light)
0x12: 512, // 512 bytes (DESFire Light)
0x13: 888, // NTAG I2C 1K user memory
0x14: 1024, // 1K
0x15: 1912, // NTAG I2C 2K user memory
0x16: 2048, // 2K
0x18: 4096, // 4K
0x1a: 8192, // 8K
0x1c: 16384, // 16K
0x1e: 32768, // 32K
};
/**
* Result of DESFire detection
*/
export interface DesfireDetectionResult {
success: boolean;
chipType?: ChipType;
versionInfo?: DesfireVersionInfo;
storageSize?: number;
error?: string;
}
/**
* Detect DESFire chip version using GET_VERSION command
*
* DESFire GET_VERSION returns data in 3 frames:
* Frame 1: Hardware version info (7 bytes)
* Frame 2: Software version info (7 bytes)
* Frame 3: Production info (14 bytes)
*/
export async function detectDesfire(): Promise<DesfireDetectionResult> {
try {
console.log('[DESFire] Sending GET_VERSION command:', DESFIRE_GET_VERSION);
// Send GET_VERSION command (wrapped in ISO 7816-4 format)
const response1 = await sendIsoDepCommand(DESFIRE_GET_VERSION);
console.log('[DESFire] GET_VERSION response:', response1);
const parsed1 = parseApduResponse(response1);
console.log('[DESFire] Parsed response:', {
data: parsed1.data,
sw1: parsed1.sw1.toString(16),
sw2: parsed1.sw2.toString(16),
isSuccess: parsed1.isSuccess,
});
// Check for success or "more data" response
if (!parsed1.isSuccess && parsed1.sw1 !== 0xaf) {
return {
success: false,
error: `GET_VERSION failed: SW=${parsed1.sw1.toString(16)}${parsed1.sw2.toString(16)}`,
};
}
if (parsed1.data.length < 7) {
return {
success: false,
error: `Invalid GET_VERSION response length: ${parsed1.data.length}`,
};
}
// Parse hardware version info
// Byte 0: Vendor ID (0x04 = NXP)
// Byte 1: Product type (0x01 = DESFire, 0x08 = DESFire Light, 0x21 = NTAG 424 DNA)
// Byte 2: Subtype
// Byte 3: Major version
// Byte 4: Minor version
// Byte 5: Storage size
// Byte 6: Protocol
const hwVendorId = parsed1.data[0];
const hwProductType = parsed1.data[1];
const hwSubtype = parsed1.data[2];
const hwMajor = parsed1.data[3];
const hwMinor = parsed1.data[4];
const hwStorageSize = parsed1.data[5];
// Verify this is NXP
if (hwVendorId !== 0x04) {
return {
success: true,
chipType: ChipType.DESFIRE_UNKNOWN,
error: `Non-NXP vendor: 0x${hwVendorId.toString(16)}`,
};
}
// Get software version (second frame)
let swMajor = 0;
let swMinor = 0;
if (parsed1.sw1 === 0xaf || parsed1.sw2 === 0xaf) {
try {
const response2 = await sendIsoDepCommand(DESFIRE_GET_VERSION_CONTINUE);
const parsed2 = parseApduResponse(response2);
if (parsed2.data.length >= 7) {
swMajor = parsed2.data[3];
swMinor = parsed2.data[4];
}
} catch {
// Continue without software version
}
}
// Determine chip type based on product type and version
let chipType: ChipType;
if (hwProductType === PRODUCT_TYPES.NTAG_424_DNA) {
// NTAG DNA family (413 DNA and 424 DNA share product type 0x21)
// Subtype 0x02 = TagTamper variant (424 DNA TT only)
// Storage size helps differentiate:
// - NTAG 413 DNA: ~160 bytes user memory (storage code <= 0x0E)
// - NTAG 424 DNA: ~416 bytes user memory (storage code >= 0x0F)
if (hwSubtype === 0x02) {
chipType = ChipType.NTAG424_DNA_TT;
} else if (hwStorageSize <= 0x0e) {
// Smaller storage indicates NTAG 413 DNA (~160 bytes)
chipType = ChipType.NTAG413_DNA;
} else {
// Larger storage indicates NTAG 424 DNA (~416 bytes)
chipType = ChipType.NTAG424_DNA;
}
} else if (hwProductType === PRODUCT_TYPES.NTAG_I2C) {
// NTAG I2C family (can have ISO-DEP on Plus variants)
// Per NXP NT3H2111/NT3H2211 datasheet:
// - Storage size 0x13 = 1K variant (NT3H1101, NT3H2111)
// - Storage size 0x15 = 2K variant (NT3H1201, NT3H2211)
// - Subtype 0x01 = non-Plus, 0x02 = Plus
const isPlus = hwSubtype === 0x02;
if (hwStorageSize === 0x13) {
chipType = isPlus ? ChipType.NTAG_I2C_PLUS_1K : ChipType.NTAG_I2C_1K;
} else if (hwStorageSize === 0x15) {
chipType = isPlus ? ChipType.NTAG_I2C_PLUS_2K : ChipType.NTAG_I2C_2K;
} else {
// Unknown storage size - report as unknown NTAG
console.warn(
`[DESFire] NTAG I2C with unknown storage size: 0x${hwStorageSize.toString(16)}`,
);
chipType = ChipType.NTAG_UNKNOWN;
}
} else if (hwProductType === PRODUCT_TYPES.DESFIRE_LIGHT) {
// DESFire Light
chipType = ChipType.DESFIRE_LIGHT;
} else if (hwProductType === PRODUCT_TYPES.DESFIRE) {
// Standard DESFire - determine version from hwMajor
chipType = DESFIRE_VERSION_MAP[hwMajor];
// If not in our map, try to infer from the version range
if (!chipType) {
console.warn(
`[DESFire] Unknown hardware major version: 0x${hwMajor.toString(16)}`,
);
// Infer based on version ranges per NXP conventions
if (hwMajor <= 0x01) {
chipType = ChipType.DESFIRE_EV1;
} else if (hwMajor >= 0x10 && hwMajor < 0x30) {
chipType = ChipType.DESFIRE_EV2;
} else if (hwMajor >= 0x30) {
chipType = ChipType.DESFIRE_EV3;
} else {
chipType = ChipType.DESFIRE_UNKNOWN;
}
}
// Note: DNA variants cannot be reliably detected from GET_VERSION.
// To detect DNA, you would need to try reading the originality signature.
} else {
// Unknown product type
chipType = ChipType.DESFIRE_UNKNOWN;
}
// Decode storage size
const storageSize = DESFIRE_STORAGE_SIZES[hwStorageSize];
const versionInfo: DesfireVersionInfo = {
hardwareMajor: hwMajor,
hardwareMinor: hwMinor,
hardwareStorageSize: hwStorageSize,
softwareMajor: swMajor,
softwareMinor: swMinor,
};
return {
success: true,
chipType,
versionInfo,
storageSize,
};
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
return {
success: false,
error: `DESFire detection failed: ${errorMessage}`,
};
}
}
/**
* Check if tag might be DESFire based on SAK and tech types
*/
export function mightBeDesfire(sak?: number, techTypes?: string[]): boolean {
// DESFire has SAK 0x20 (ISO 14443-4 compliant)
// But SAK 0x20 can also be other ISO-DEP chips
if (sak === 0x20) {
return true;
}
// Check for IsoDep technology
if (techTypes?.some(t => t.includes('IsoDep'))) {
return true;
}
return false;
}
/**
* Detect DESFire from ATS/historical bytes when GET_VERSION fails
* This is useful for iOS where commands may not work reliably
*
* DESFire ATS patterns:
* - Historical bytes starting with 0x75 (format byte) + 0x77 (card type)
* - "DESFire" text in historical bytes (some older cards)
* - SAK 0x20 with specific ATQA patterns
*/
export function detectDesfireFromAts(
historicalBytes?: string,
ats?: string,
sak?: number,
atqa?: string,
): DesfireDetectionResult {
// Convert hex string to bytes for analysis
const histBytes = historicalBytes
? historicalBytes
.replace(/[:\s-]/g, '')
.match(/.{1,2}/g)
?.map(h => parseInt(h, 16)) || []
: [];
// Check for DESFire historical bytes patterns
// Pattern 1: 0x75 0x77 0x81 0x02 0x80 (DESFire EV1/2/3 typical pattern)
if (histBytes.length >= 5) {
if (histBytes[0] === 0x75 && histBytes[1] === 0x77) {
// This is likely DESFire
// Byte 4 often indicates storage size
return {
success: true,
chipType: ChipType.DESFIRE_UNKNOWN,
error: 'Identified as DESFire from ATS (version unknown)',
};
}
}
// Pattern 2: Check for specific DESFire identifiers
// Some DESFire cards have 0x06 as format byte followed by capabilities
if (histBytes.length >= 3 && histBytes[0] === 0x06) {
return {
success: true,
chipType: ChipType.DESFIRE_UNKNOWN,
error: 'Identified as DESFire from ATS format byte',
};
}
// Pattern 3: Check for NTAG 424 DNA historical bytes
// NTAG 424 DNA typically has historical bytes starting with specific patterns
if (histBytes.length >= 4) {
// NTAG 424 DNA pattern: 0x80 0x77 0xC1 or similar
if (histBytes[0] === 0x80 && histBytes[1] === 0x77) {
return {
success: true,
chipType: ChipType.NTAG424_DNA,
error: 'Identified as NTAG 424 DNA from ATS',
};
}
}
// Pattern 4: Check for "DESFire" ASCII in historical bytes
const histString = histBytes.map(b => String.fromCharCode(b)).join('');
if (histString.includes('DESFire') || histString.includes('DESFIRE')) {
return {
success: true,
chipType: ChipType.DESFIRE_UNKNOWN,
error: 'Identified as DESFire from ATS string',
};
}
// SAK-based inference
if (sak === 0x20) {
// SAK 0x20 with ISO-DEP capability but no other identification
// Could be DESFire, NTAG 424 DNA, or other ISO 14443-4 card
// Parse ATQA for more info
if (atqa) {
const atqaClean = atqa.replace(/[:\s-]/g, '');
// DESFire typically has ATQA 0x0344 or 0x0304
if (atqaClean === '0344' || atqaClean === '4403') {
return {
success: true,
chipType: ChipType.DESFIRE_UNKNOWN,
error: 'Likely DESFire based on SAK/ATQA',
};
}
// NTAG 424 DNA typically has ATQA 0x0004
if (atqaClean === '0004' || atqaClean === '0400') {
return {
success: true,
chipType: ChipType.NTAG424_DNA,
error: 'Likely NTAG 424 DNA based on SAK/ATQA',
};
}
}
}
return {
success: false,
error: 'Could not identify DESFire from ATS',
};
}
/**
* Format DESFire version info for display
*/
export function formatDesfireVersionInfo(info: DesfireVersionInfo): string {
const version = `HW: ${info.hardwareMajor}.${info.hardwareMinor}`;
const software =
info.softwareMajor > 0
? `, SW: ${info.softwareMajor}.${info.softwareMinor}`
: '';
return version + software;
}

View File

@@ -0,0 +1,406 @@
/**
* Detection Orchestrator
* Main entry point for chip detection using waterfall approach
*/
import {Platform} from 'react-native';
import type {RawTagData} from '../../types/nfc';
import {
ChipType,
Transponder,
DetectionResult,
getChipFamily,
CHIP_NAMES,
CHIP_MEMORY_SIZES,
CHIP_CLONEABILITY,
} from '../../types/detection';
import {detectNtag, mightBeNtag} from './ntag';
import {
detectMifareClassic,
isMifareClassicSak,
hasIsoDepCapability,
detectSakSwap,
} from './mifare';
import {detectDesfire, detectDesfireFromAts} from './desfire';
import {detectIso15693, isIso15693} from './iso15693';
import {detectJavaCard, mightBeJavaCard, detectJavaCardFromAts} from './javacard';
/**
* Create a Transponder object from detection results
*/
function createTransponder(
type: ChipType,
rawData: RawTagData,
options: {
memorySize?: number;
versionInfo?: Transponder['versionInfo'];
confidence?: Transponder['confidence'];
sakSwapInfo?: Transponder['sakSwapInfo'];
} = {},
): Transponder {
const cloneInfo = CHIP_CLONEABILITY[type];
// Run SAK swap detection if we have SAK
let sakSwapInfo = options.sakSwapInfo;
if (!sakSwapInfo && rawData.sak !== undefined) {
sakSwapInfo = detectSakSwap(
rawData.sak,
rawData.atqa,
rawData.historicalBytes,
);
}
return {
type,
family: getChipFamily(type),
chipName: CHIP_NAMES[type],
memorySize: options.memorySize ?? CHIP_MEMORY_SIZES[type],
isCloneable: cloneInfo.cloneable,
cloneabilityNote: cloneInfo.note,
rawData: {
uid: rawData.uid,
sak: rawData.sak,
atqa: rawData.atqa,
ats: rawData.ats,
historicalBytes: rawData.historicalBytes,
techTypes: rawData.techTypes,
},
versionInfo: options.versionInfo,
sakSwapInfo,
confidence: options.confidence ?? 'medium',
detectedOn: Platform.OS as 'ios' | 'android',
};
}
/**
* Detect chip type using waterfall approach
*
* Detection order:
* 1. Check for MIFARE Classic (SAK-based, quick)
* 2. Check for NTAG (GET_VERSION command)
* 3. Check for ISO-DEP capable chips (Phase 4: DESFire, Plus, JavaCard)
* 4. Fall back to generic type based on technology
*/
export async function detectChip(rawData: RawTagData): Promise<DetectionResult> {
try {
const {sak, techTypes} = rawData;
console.log('[Detector] Starting detection with:', {
uid: rawData.uid,
sak: sak !== undefined ? `0x${sak.toString(16)}` : 'undefined',
techTypes,
hasIsoDep: techTypes.some(t => t.includes('IsoDep')),
hasNfcA: techTypes.some(t => t.includes('NfcA')),
});
// ========================================================================
// Step 1: Check for MIFARE Classic
// Use tech type detection first (most reliable on Android), then SAK
// ========================================================================
const hasMifareClassicTech = techTypes.some(t =>
t.includes('MifareClassic'),
);
const hasIsoDepTech = techTypes.some(t => t.includes('IsoDep'));
// 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
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) {
chipType = ChipType.MIFARE_CLASSIC_4K;
memorySize = 4096;
}
// SAK 0x09 indicates Mini
else if (sak === 0x09) {
chipType = ChipType.MIFARE_CLASSIC_MINI;
memorySize = 320;
}
// 7-byte UID often indicates 4K (but not always)
else if (rawData.uid && rawData.uid.replace(/[:\s-]/g, '').length === 14) {
// 14 hex chars = 7 bytes - could be 4K, but use SAK if available
if (sak === undefined) {
chipType = ChipType.MIFARE_CLASSIC_4K;
memorySize = 4096;
}
}
return {
success: true,
transponder: createTransponder(chipType, rawData, {
memorySize,
confidence: 'high',
}),
};
}
// SAK-based MIFARE Classic detection (fallback, includes iOS)
if (sak !== undefined && isMifareClassicSak(sak) && !hasIsoDepTech) {
const result = detectMifareClassic(sak);
if (result.success && result.chipType) {
return {
success: true,
transponder: createTransponder(result.chipType, rawData, {
memorySize: result.memorySize,
confidence: 'high',
}),
};
}
}
// ========================================================================
// Step 2: Check for NTAG (Type 2 tags with GET_VERSION)
// ========================================================================
const couldBeNtag = mightBeNtag(sak, techTypes);
console.log('[Detector] mightBeNtag result:', couldBeNtag);
if (couldBeNtag) {
console.log('[Detector] Attempting NTAG detection...');
const ntagResult = await detectNtag();
console.log('[Detector] NTAG detection result:', {
success: ntagResult.success,
chipType: ntagResult.chipType,
error: ntagResult.error,
});
if (ntagResult.success && ntagResult.chipType) {
return {
success: true,
transponder: createTransponder(ntagResult.chipType, rawData, {
memorySize: ntagResult.memorySize,
versionInfo: ntagResult.versionInfo,
confidence:
ntagResult.chipType === ChipType.NTAG_UNKNOWN ? 'medium' : 'high',
}),
};
}
// If GET_VERSION failed but tag looks like Type 2, mark as unknown NTAG
if (sak === 0x00 && techTypes.some(t => t.includes('NfcA'))) {
console.log('[Detector] NTAG detection failed, falling back to NTAG_UNKNOWN');
return {
success: true,
transponder: createTransponder(ChipType.NTAG_UNKNOWN, rawData, {
confidence: 'low',
}),
};
}
}
// ========================================================================
// Step 3: Check for ISO-DEP capable chips (DESFire, NTAG 424 DNA, JavaCard)
// ========================================================================
if (
(sak !== undefined && hasIsoDepCapability(sak)) ||
hasIsoDepTech
) {
// 3a: Always try DESFire/NTAG 424 DNA detection first via GET_VERSION
// This command works on DESFire EV1/2/3, DESFire Light, NTAG 424 DNA
const desfireResult = await detectDesfire();
if (desfireResult.success && desfireResult.chipType) {
return {
success: true,
transponder: createTransponder(desfireResult.chipType, rawData, {
memorySize: desfireResult.storageSize,
versionInfo: desfireResult.versionInfo,
confidence:
desfireResult.chipType === ChipType.DESFIRE_UNKNOWN
? 'medium'
: 'high',
}),
};
}
// 3b: DESFire command failed - try ATS-based detection
const desfireAtsResult = detectDesfireFromAts(
rawData.historicalBytes,
rawData.ats,
sak,
rawData.atqa,
);
if (desfireAtsResult.success && desfireAtsResult.chipType) {
return {
success: true,
transponder: createTransponder(desfireAtsResult.chipType, rawData, {
memorySize: desfireAtsResult.storageSize,
confidence: 'medium', // Lower confidence since no version command
}),
};
}
// 3c: Try JavaCard detection (check historical bytes and CPLC)
if (mightBeJavaCard(rawData.historicalBytes, rawData.ats)) {
const jcResult = await detectJavaCard();
if (jcResult.success && jcResult.chipType) {
return {
success: true,
transponder: createTransponder(jcResult.chipType, rawData, {
confidence:
jcResult.chipType === ChipType.JCOP4 ? 'high' : 'medium',
}),
};
}
// JavaCard CPLC failed - try ATS-based detection
const jcAtsResult = detectJavaCardFromAts(
rawData.historicalBytes,
rawData.ats,
);
if (jcAtsResult.success && jcAtsResult.chipType) {
return {
success: true,
transponder: createTransponder(jcAtsResult.chipType, rawData, {
confidence: 'medium',
}),
};
}
}
// 3d: If DESFire and likely JavaCard checks failed, try JavaCard as general fallback
// (some JavaCards don't have obvious historical bytes)
const jcFallback = await detectJavaCard();
if (jcFallback.success && jcFallback.chipType) {
return {
success: true,
transponder: createTransponder(jcFallback.chipType, rawData, {
confidence: 'medium',
}),
};
}
// 3e: Last resort - try ATS-based JavaCard detection without mightBeJavaCard check
const jcAtsFallback = detectJavaCardFromAts(
rawData.historicalBytes,
rawData.ats,
);
if (jcAtsFallback.success && jcAtsFallback.chipType) {
return {
success: true,
transponder: createTransponder(jcAtsFallback.chipType, rawData, {
confidence: 'low',
}),
};
}
// ISO-DEP but couldn't identify - mark as unknown ISO 14443-A
return {
success: true,
transponder: createTransponder(ChipType.ISO14443A_UNKNOWN, rawData, {
confidence: 'low',
}),
};
}
// ========================================================================
// Step 4: Check for ISO 15693 (NFC-V) - SLIX and NTAG 5 detection
// ========================================================================
if (isIso15693(techTypes)) {
const iso15693Result = await detectIso15693();
if (iso15693Result.success && iso15693Result.chipType) {
// Determine confidence based on whether we got a specific chip type
const knownTypes = [
ChipType.SLIX,
ChipType.SLIX2,
ChipType.SLIX_S,
ChipType.SLIX_L,
ChipType.NTAG5_LINK,
ChipType.NTAG5_BOOST,
ChipType.NTAG5_SWITCH,
];
const confidence =
iso15693Result.chipType === ChipType.ISO15693_UNKNOWN
? 'low'
: knownTypes.includes(iso15693Result.chipType)
? 'high'
: 'medium';
return {
success: true,
transponder: createTransponder(iso15693Result.chipType, rawData, {
confidence,
}),
};
}
// Fallback to unknown ISO 15693
return {
success: true,
transponder: createTransponder(ChipType.ISO15693_UNKNOWN, rawData, {
confidence: 'low',
}),
};
}
// ========================================================================
// Step 5: Check for ISO 14443-B
// ========================================================================
if (techTypes.some(t => t.includes('NfcB'))) {
return {
success: true,
transponder: createTransponder(ChipType.ISO14443B_UNKNOWN, rawData, {
confidence: 'low',
}),
};
}
// ========================================================================
// Fallback: Unknown chip
// ========================================================================
return {
success: true,
transponder: createTransponder(ChipType.UNKNOWN, rawData, {
confidence: 'low',
}),
};
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
return {
success: false,
error: `Detection failed: ${errorMessage}`,
};
}
}
/**
* Get a brief description of what was detected
*/
export function getDetectionSummary(transponder: Transponder): string {
const parts: string[] = [transponder.chipName];
if (transponder.memorySize) {
parts.push(`(${transponder.memorySize} bytes)`);
}
if (transponder.isCloneable) {
parts.push('- Cloneable');
} else {
parts.push('- Not cloneable');
}
return parts.join(' ');
}
/**
* Check if we can do advanced detection on this platform
*/
export function canDoAdvancedDetection(
chipType: ChipType,
): {canDetect: boolean; reason?: string} {
// MIFARE Classic sector operations need Android
if (
chipType === ChipType.MIFARE_CLASSIC_1K ||
chipType === ChipType.MIFARE_CLASSIC_4K ||
chipType === ChipType.MIFARE_CLASSIC_MINI
) {
if (Platform.OS === 'ios') {
return {
canDetect: false,
reason: 'MIFARE Classic sector operations require Android',
};
}
}
return {canDetect: true};
}

View File

@@ -0,0 +1,23 @@
/**
* Detection Module
* Re-exports all detection functionality
*/
export {detectChip, getDetectionSummary, canDoAdvancedDetection} from './detector';
export {detectNtag, mightBeNtag, formatNtagVersionInfo} from './ntag';
export {
detectMifareClassic,
isMifareClassicSak,
hasIsoDepCapability,
describeSak,
detectSakSwap,
mightBeMagicCard,
IOS_MIFARE_CLASSIC_NOTE,
} from './mifare';
export type {SakSwapDetection} from './mifare';
export {detectDesfire, mightBeDesfire, formatDesfireVersionInfo} from './desfire';
export type {DesfireDetectionResult} from './desfire';
export {detectIso15693, isIso15693, getIcManufacturerName} from './iso15693';
export type {Iso15693DetectionResult} from './iso15693';
export {detectJavaCard, mightBeJavaCard, formatCPLC} from './javacard';
export type {JavaCardDetectionResult, CPLCData} from './javacard';

View File

@@ -0,0 +1,748 @@
/**
* ISO 15693 (NFC-V) Detector
* Identifies ICODE SLIX, SLIX2, NTAG 5, and other ISO 15693 tags
*
* Per NXP AN11042, identification uses:
* 1. GET_SYSTEM_INFO command (0x2B) - returns IC reference directly
* 2. UID parsing - IC manufacturer code and reference in UID bytes
*/
import {Platform} from 'react-native';
import NfcManager from 'react-native-nfc-manager';
import {ChipType} from '../../types/detection';
import {getIso15693SystemInfo} from '../nfc/commands';
/**
* NXP ICODE IC manufacturer code
*/
const NXP_IC_MFG_CODE = 0x04;
/**
* NXP ISO 15693 product family identification based on IC reference
* The IC reference encoding varies by product family.
*
* Note: ISO 15693 UID is transmitted LSB first, but NFC libraries may
* return bytes in different orders. We try multiple interpretations.
*
* Reference: NXP Product Short Form Specifications, AN11042
*/
const NXP_ISO15693_IC_REFERENCES: Record<number, ChipType> = {
// ICODE SLIX (SL2S2002) - standard SLIX
// IC reference values: 0x01, 0x02 (variants)
0x01: ChipType.SLIX,
0x02: ChipType.SLIX,
0x03: ChipType.SLIX, // Additional variant
// ICODE SLIX-S (SL2S2102) - SLIX with 32-bit password protection
0x0a: ChipType.SLIX_S,
0x0b: ChipType.SLIX_S, // Additional variant
// ICODE SLIX-L (SL2S2702) - SLIX low-memory variant (512 bits)
0x0c: ChipType.SLIX_L,
0x0d: ChipType.SLIX_L,
// ICODE SLIX2 (SL2S2602) - enhanced SLIX with multiple passwords
// IC reference range: 0x14-0x1F
0x14: ChipType.SLIX2,
0x15: ChipType.SLIX2,
0x16: ChipType.SLIX2,
0x17: ChipType.SLIX2,
0x18: ChipType.SLIX2,
0x19: ChipType.SLIX2,
0x1a: ChipType.SLIX2,
0x1b: ChipType.SLIX2,
0x1c: ChipType.SLIX2,
0x1d: ChipType.SLIX2,
0x1e: ChipType.SLIX2,
0x1f: ChipType.SLIX2,
// ICODE DNA (SL2S4001) - ICODE with crypto authentication
// IC reference range per NXP: 0x24-0x27 (documented range)
// GET_SYSTEM_INFO may return different values: observed 0x96 on real chips
// Extended range based on real-world observations: 0x90-0xBF
0x24: ChipType.ICODE_DNA,
0x25: ChipType.ICODE_DNA,
0x26: ChipType.ICODE_DNA,
0x27: ChipType.ICODE_DNA,
// Extended ICODE DNA range (observed in real chips via GET_SYSTEM_INFO)
0x90: ChipType.ICODE_DNA,
0x91: ChipType.ICODE_DNA,
0x92: ChipType.ICODE_DNA,
0x93: ChipType.ICODE_DNA,
0x94: ChipType.ICODE_DNA,
0x95: ChipType.ICODE_DNA,
0x96: ChipType.ICODE_DNA, // Observed on ICODE DNA via GET_SYSTEM_INFO
0x97: ChipType.ICODE_DNA,
0x98: ChipType.ICODE_DNA,
0x99: ChipType.ICODE_DNA,
0x9a: ChipType.ICODE_DNA,
0x9b: ChipType.ICODE_DNA,
0x9c: ChipType.ICODE_DNA,
0x9d: ChipType.ICODE_DNA,
0x9e: ChipType.ICODE_DNA,
0x9f: ChipType.ICODE_DNA,
// UID-based IC reference range (0xA0-0xAF)
0xa0: ChipType.ICODE_DNA,
0xa1: ChipType.ICODE_DNA,
0xa2: ChipType.ICODE_DNA, // Observed in UID of ICODE DNA
0xa3: ChipType.ICODE_DNA,
0xa4: ChipType.ICODE_DNA,
0xa5: ChipType.ICODE_DNA,
0xa6: ChipType.ICODE_DNA,
0xa7: ChipType.ICODE_DNA,
0xa8: ChipType.ICODE_DNA,
0xa9: ChipType.ICODE_DNA,
0xaa: ChipType.ICODE_DNA,
0xab: ChipType.ICODE_DNA,
0xac: ChipType.ICODE_DNA,
0xad: ChipType.ICODE_DNA,
0xae: ChipType.ICODE_DNA,
0xaf: ChipType.ICODE_DNA,
// NTAG 5 link (NT3H2111/NT3H2211) - NFC+I2C bridge
// IC reference range: 0x20-0x23
0x20: ChipType.NTAG5_LINK,
0x21: ChipType.NTAG5_LINK,
0x22: ChipType.NTAG5_LINK,
0x23: ChipType.NTAG5_LINK,
// NTAG 5 boost (NT3H3111/NT3H3211) - extended range, larger memory
// IC reference range: 0x40-0x4F
0x40: ChipType.NTAG5_BOOST,
0x41: ChipType.NTAG5_BOOST,
0x42: ChipType.NTAG5_BOOST,
0x43: ChipType.NTAG5_BOOST,
0x44: ChipType.NTAG5_BOOST,
0x45: ChipType.NTAG5_BOOST,
0x46: ChipType.NTAG5_BOOST,
0x47: ChipType.NTAG5_BOOST,
0x48: ChipType.NTAG5_BOOST,
0x49: ChipType.NTAG5_BOOST,
// NTAG 5 switch (NT3H1101/NT3H1201) - energy harvesting
// IC reference range: 0x50-0x5F
0x50: ChipType.NTAG5_SWITCH,
0x51: ChipType.NTAG5_SWITCH,
0x52: ChipType.NTAG5_SWITCH,
0x53: ChipType.NTAG5_SWITCH,
};
/**
* Determine ICODE variant from IC reference, with fallback for unknown values.
* Some ICODE chips have non-standard IC references that don't match NXP docs.
*/
function getIcodeTypeFromIcReference(icRef: number): ChipType | null {
// Check direct mapping first
if (NXP_ISO15693_IC_REFERENCES[icRef]) {
console.log(
`[ISO15693] IC ref 0x${icRef.toString(16)} matched: ${NXP_ISO15693_IC_REFERENCES[icRef]}`,
);
return NXP_ISO15693_IC_REFERENCES[icRef];
}
// Handle unknown IC references by range inference
// Standard SLIX range: 0x01-0x09
if (icRef >= 0x01 && icRef <= 0x09) {
console.log(
`[ISO15693] Unknown IC ref 0x${icRef.toString(16)} in SLIX range, treating as SLIX`,
);
return ChipType.SLIX;
}
// SLIX-S/SLIX-L range: 0x0A-0x13
if (icRef >= 0x0a && icRef <= 0x13) {
console.log(
`[ISO15693] Unknown IC ref 0x${icRef.toString(16)} in SLIX-S/L range, treating as SLIX_S`,
);
return ChipType.SLIX_S;
}
// SLIX2/NTAG5 link range: 0x14-0x2F
if (icRef >= 0x14 && icRef <= 0x2f) {
console.log(
`[ISO15693] Unknown IC ref 0x${icRef.toString(16)} in SLIX2/NTAG5 range, treating as SLIX2`,
);
return ChipType.SLIX2;
}
// NTAG 5 boost range: 0x40-0x4F
if (icRef >= 0x40 && icRef <= 0x4f) {
console.log(
`[ISO15693] Unknown IC ref 0x${icRef.toString(16)} in NTAG5 boost range`,
);
return ChipType.NTAG5_BOOST;
}
// NTAG 5 switch range: 0x50-0x5F
if (icRef >= 0x50 && icRef <= 0x5f) {
console.log(
`[ISO15693] Unknown IC ref 0x${icRef.toString(16)} in NTAG5 switch range`,
);
return ChipType.NTAG5_SWITCH;
}
// ICODE DNA range: 0x90-0xBF (extended range based on real chips)
// GET_SYSTEM_INFO returns IC ref in 0x90-0x9F range
// UID parsing returns IC ref in 0xA0-0xAF range
if (icRef >= 0x90 && icRef <= 0xbf) {
console.log(
`[ISO15693] IC ref 0x${icRef.toString(16)} in ICODE DNA range`,
);
return ChipType.ICODE_DNA;
}
// High IC references (0x80-0x8F) might be older SLIX or special variants
if (icRef >= 0x80 && icRef < 0x90) {
console.log(
`[ISO15693] High IC ref 0x${icRef.toString(16)}, treating as SLIX (legacy/special)`,
);
return ChipType.SLIX;
}
console.log(
`[ISO15693] Unknown IC ref 0x${icRef.toString(16)}, no match`,
);
return null;
}
/**
* Result of ISO 15693 detection
*/
export interface Iso15693DetectionResult {
success: boolean;
chipType?: ChipType;
uid?: string;
icManufacturer?: number;
icReference?: number;
blockSize?: number;
blockCount?: number;
error?: string;
}
/**
* Identify SLIX variant from memory size when IC reference is not available
* Per NXP datasheets, different SLIX variants have different memory sizes:
* - SLIX (SL2S2002): 896 bits = 112 bytes = 28 blocks × 4 bytes
* - SLIX-S (SL2S2102): 1280 bits = 160 bytes = 40 blocks × 4 bytes
* - SLIX-L (SL2S2702): 512 bits = 64 bytes = 16 blocks × 4 bytes
* - SLIX2 (SL2S2602): 2528 bits = 316 bytes = 79 blocks × 4 bytes
*/
function identifySlixFromMemory(
blockCount?: number,
blockSize?: number,
): ChipType | null {
if (!blockCount || !blockSize) {
return null;
}
const totalBytes = blockCount * blockSize;
// SLIX-L: ~64 bytes (16 blocks)
if (totalBytes <= 80 || blockCount <= 20) {
return ChipType.SLIX_L;
}
// SLIX: ~112 bytes (28 blocks)
if (totalBytes <= 128 || blockCount <= 32) {
return ChipType.SLIX;
}
// SLIX-S: ~160 bytes (40 blocks)
if (totalBytes <= 200 || blockCount <= 50) {
return ChipType.SLIX_S;
}
// SLIX2: ~316 bytes (79 blocks)
if (totalBytes <= 400 || blockCount <= 100) {
return ChipType.SLIX2;
}
// Larger memory - could be NTAG5 or other
if (totalBytes >= 400) {
return ChipType.NTAG5_BOOST; // Larger NXP ISO 15693 chips
}
return null;
}
/**
* Detect ISO 15693 tag type
*
* Per NXP AN11042, identification methods:
* 1. GET_SYSTEM_INFO - returns IC reference (if bit 3 of info flags set)
* 2. Memory size - different SLIX variants have different capacities
* 3. UID manufacturer code - confirms NXP origin
*
* Note: IC reference is OPTIONAL in GET_SYSTEM_INFO - older SLIX chips
* may not return it. In that case, use memory size to distinguish variants.
*/
export async function detectIso15693(): Promise<Iso15693DetectionResult> {
try {
console.log('[ISO15693] Starting detection...');
// Get tag info for UID
const tag = await NfcManager.getTag();
let uid: string | undefined;
let icManufacturer: number | undefined;
let uidIcReference: number | undefined;
if (tag?.id) {
const uidBytes =
typeof tag.id === 'string'
? tag.id
.replace(/[:\s-]/g, '')
.match(/.{1,2}/g)
?.map(h => parseInt(h, 16)) || []
: Array.from(tag.id as unknown as number[]);
uid = uidBytes
.map(b => b.toString(16).padStart(2, '0').toUpperCase())
.join(':');
console.log('[ISO15693] UID:', uid);
console.log('[ISO15693] UID bytes:', uidBytes.map(b => `0x${b.toString(16)}`).join(', '));
// Parse manufacturer from UID
const parsed = parseIso15693Uid(uidBytes);
icManufacturer = parsed.icManufacturer;
uidIcReference = parsed.icReference;
console.log('[ISO15693] From UID - Manufacturer:', icManufacturer !== undefined ? `0x${icManufacturer.toString(16)}` : 'undefined');
console.log('[ISO15693] From UID - IC Reference:', uidIcReference !== undefined ? `0x${uidIcReference.toString(16)}` : 'undefined');
}
// Try GET_SYSTEM_INFO command (NXP recommended method)
try {
console.log('[ISO15693] Trying GET_SYSTEM_INFO...');
const sysInfo = await getIso15693SystemInfo();
console.log('[ISO15693] GET_SYSTEM_INFO result:', sysInfo);
// If we got IC reference from GET_SYSTEM_INFO, use it (most reliable)
if (sysInfo.icReference !== undefined && sysInfo.icReference !== 0) {
const icReference = sysInfo.icReference;
console.log('[ISO15693] Got IC reference from sysInfo:', `0x${icReference.toString(16)}`);
if (icManufacturer === NXP_IC_MFG_CODE || icManufacturer === undefined) {
let chipType = getIcodeTypeFromIcReference(icReference) || ChipType.ISO15693_UNKNOWN;
// Memory-based override for NXP chips when block count is available
// IC references are NOT reliable for distinguishing SLIX vs ICODE DNA
// because both chip families can report IC refs in overlapping ranges.
//
// Memory sizes per NXP datasheets are the reliable differentiator:
// - SLIX (SL2S2002): 32 blocks (128 bytes)
// - SLIX-S (SL2S2102): 40 blocks (160 bytes)
// - SLIX-L (SL2S2702): 16 blocks (64 bytes)
// - SLIX2 (SL2S2602): 79-80 blocks (316-320 bytes)
// - ICODE DNA: 48-64 blocks (192-256 bytes)
//
// Use memory size as the authoritative source for NXP chips.
if (
icManufacturer === NXP_IC_MFG_CODE &&
sysInfo.blockCount !== undefined
) {
const blocks = sysInfo.blockCount;
// SLIX-L: 16 blocks
if (blocks <= 20) {
console.log(
`[ISO15693] NXP chip with ${blocks} blocks → SLIX-L`,
);
chipType = ChipType.SLIX_L;
}
// SLIX: 32 blocks
else if (blocks <= 36) {
console.log(
`[ISO15693] NXP chip with ${blocks} blocks → SLIX`,
);
chipType = ChipType.SLIX;
}
// SLIX-S: 40 blocks
else if (blocks <= 44) {
console.log(
`[ISO15693] NXP chip with ${blocks} blocks → SLIX-S`,
);
chipType = ChipType.SLIX_S;
}
// ICODE DNA: 48-64 blocks
else if (blocks <= 68) {
console.log(
`[ISO15693] NXP chip with ${blocks} blocks → ICODE DNA`,
);
chipType = ChipType.ICODE_DNA;
}
// SLIX2: 79-80 blocks
else if (blocks <= 85) {
console.log(
`[ISO15693] NXP chip with ${blocks} blocks → SLIX2`,
);
chipType = ChipType.SLIX2;
}
// Larger memory - NTAG5 variants
else {
console.log(
`[ISO15693] NXP chip with ${blocks} blocks → NTAG5 (large memory)`,
);
chipType = ChipType.NTAG5_BOOST;
}
}
return {
success: true,
chipType,
uid,
icManufacturer: icManufacturer ?? NXP_IC_MFG_CODE,
icReference,
blockSize: sysInfo.blockSize,
blockCount: sysInfo.blockCount,
};
}
return {
success: true,
chipType: ChipType.ISO15693_UNKNOWN,
uid,
icManufacturer,
icReference,
blockSize: sysInfo.blockSize,
blockCount: sysInfo.blockCount,
};
}
// IC reference not in GET_SYSTEM_INFO response (common for older SLIX)
// Use memory size to identify the chip variant
if (icManufacturer === NXP_IC_MFG_CODE || icManufacturer === undefined) {
// For NXP chips, try to identify SLIX variant from memory size
const chipFromMemory = identifySlixFromMemory(
sysInfo.blockCount,
sysInfo.blockSize,
);
if (chipFromMemory) {
return {
success: true,
chipType: chipFromMemory,
uid,
icManufacturer: icManufacturer ?? NXP_IC_MFG_CODE,
icReference: uidIcReference,
blockSize: sysInfo.blockSize,
blockCount: sysInfo.blockCount,
};
}
// Got manufacturer NXP but couldn't determine exact variant
// Default to SLIX as it's the most common NXP ISO 15693 chip
return {
success: true,
chipType: ChipType.SLIX,
uid,
icManufacturer: icManufacturer ?? NXP_IC_MFG_CODE,
blockSize: sysInfo.blockSize,
blockCount: sysInfo.blockCount,
};
}
// Non-NXP chip with system info
return {
success: true,
chipType: ChipType.ISO15693_UNKNOWN,
uid,
icManufacturer,
blockSize: sysInfo.blockSize,
blockCount: sysInfo.blockCount,
};
} catch (sysInfoError) {
// GET_SYSTEM_INFO failed
console.warn('[ISO15693] GET_SYSTEM_INFO failed:', sysInfoError);
}
// Fall back: Use UID-based detection
// If we know it's NXP, identify the ICODE variant
console.log('[ISO15693] Falling back to UID-based detection');
if (icManufacturer === NXP_IC_MFG_CODE) {
console.log('[ISO15693] NXP manufacturer detected');
// Check if we got IC reference from UID parsing
if (uidIcReference !== undefined) {
const chipType = getIcodeTypeFromIcReference(uidIcReference) || ChipType.SLIX;
console.log('[ISO15693] UID-based detection result:', chipType);
return {
success: true,
chipType,
uid,
icManufacturer,
icReference: uidIcReference,
};
}
// NXP but no IC reference - assume SLIX (most common)
console.log('[ISO15693] NXP but no IC reference, defaulting to SLIX');
return {
success: true,
chipType: ChipType.SLIX,
uid,
icManufacturer,
};
}
// Fall back to platform-specific detection using UID
if (Platform.OS === 'android') {
return await detectIso15693Android();
}
return await detectIso15693IOS();
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
return {
success: false,
error: `ISO 15693 detection failed: ${errorMessage}`,
};
}
}
/**
* Parse ISO 15693 UID to extract manufacturer and IC reference
* The UID is 8 bytes and may be in MSB or LSB first order.
*
* MSB first (standard): E0:MFG:ICRef:Serial[5]
* LSB first (common in NFC reads): Serial[5]:ICRef:MFG:E0
*/
function parseIso15693Uid(uidBytes: number[]): {
icManufacturer?: number;
icReference?: number;
} {
if (uidBytes.length < 8) {
return {};
}
// Check for MSB first order (E0 at position 0)
if (uidBytes[0] === 0xe0) {
return {
icManufacturer: uidBytes[1],
icReference: uidBytes[2],
};
}
// Check for LSB first order (E0 at position 7)
if (uidBytes[7] === 0xe0) {
return {
icManufacturer: uidBytes[6],
icReference: uidBytes[5],
};
}
// Try to find E0 anywhere and infer order
const e0Index = uidBytes.indexOf(0xe0);
if (e0Index === -1) {
// No E0 found - unusual, try positions anyway
// Some readers strip E0 or return partial UIDs
// Try treating first two bytes as mfg and icref
return {
icManufacturer: uidBytes[0],
icReference: uidBytes[1],
};
}
// E0 found at unexpected position - infer based on where it is
if (e0Index < 4) {
// E0 near start, likely MSB first
return {
icManufacturer: uidBytes[e0Index + 1],
icReference: uidBytes[e0Index + 2],
};
} else {
// E0 near end, likely LSB first
return {
icManufacturer: uidBytes[e0Index - 1],
icReference: uidBytes[e0Index - 2],
};
}
}
/**
* Android-specific ISO 15693 detection
*/
async function detectIso15693Android(): Promise<Iso15693DetectionResult> {
try {
// Try to get tag info - the NfcV handler provides some info
const tag = await NfcManager.getTag();
if (!tag) {
return {
success: false,
error: 'No tag available',
};
}
// Extract UID from tag
const uid = tag.id
? Array.from(tag.id as unknown as number[])
.map(b => b.toString(16).padStart(2, '0').toUpperCase())
.join(':')
: undefined;
// Parse UID to extract manufacturer and IC reference
if (uid && tag.id) {
const uidBytes = Array.from(tag.id as unknown as number[]);
const {icManufacturer, icReference} = parseIso15693Uid(uidBytes);
if (icManufacturer !== undefined) {
// For NXP chips, identify specific variant
if (icManufacturer === NXP_IC_MFG_CODE && icReference !== undefined) {
const chipType = getIcodeTypeFromIcReference(icReference) || ChipType.ISO15693_UNKNOWN;
return {
success: true,
chipType,
uid,
icManufacturer,
icReference,
};
}
// Non-NXP ISO 15693 tag
return {
success: true,
chipType: ChipType.ISO15693_UNKNOWN,
uid,
icManufacturer,
icReference,
};
}
}
// Fallback - we know it's ISO 15693 but can't identify further
return {
success: true,
chipType: ChipType.ISO15693_UNKNOWN,
uid,
};
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
return {
success: false,
error: `Android ISO 15693 detection failed: ${errorMessage}`,
};
}
}
/**
* iOS-specific ISO 15693 detection
*/
async function detectIso15693IOS(): Promise<Iso15693DetectionResult> {
try {
// On iOS, we have limited access to ISO 15693 tags
// CoreNFC provides basic tag info but detailed commands may be restricted
const tag = await NfcManager.getTag();
if (!tag) {
return {
success: false,
error: 'No tag available',
};
}
// Extract UID - iOS may return as string or array
let uidBytes: number[] = [];
let uid: string | undefined;
if (tag.id) {
if (typeof tag.id === 'string') {
// Parse hex string to bytes
const uidClean = tag.id.replace(/[:\s-]/g, '');
uid = uidClean
.match(/.{1,2}/g)
?.map(s => s.toUpperCase())
.join(':');
for (let i = 0; i < uidClean.length; i += 2) {
uidBytes.push(parseInt(uidClean.substring(i, i + 2), 16));
}
} else {
uidBytes = Array.from(tag.id as unknown as number[]);
uid = uidBytes
.map(b => b.toString(16).padStart(2, '0').toUpperCase())
.join(':');
}
}
// Parse UID to extract manufacturer and IC reference
if (uidBytes.length >= 8) {
const {icManufacturer, icReference} = parseIso15693Uid(uidBytes);
if (icManufacturer !== undefined) {
if (icManufacturer === NXP_IC_MFG_CODE && icReference !== undefined) {
const chipType = getIcodeTypeFromIcReference(icReference) || ChipType.ISO15693_UNKNOWN;
return {
success: true,
chipType,
uid,
icManufacturer,
icReference,
};
}
return {
success: true,
chipType: ChipType.ISO15693_UNKNOWN,
uid,
icManufacturer,
icReference,
};
}
}
return {
success: true,
chipType: ChipType.ISO15693_UNKNOWN,
uid,
};
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
return {
success: false,
error: `iOS ISO 15693 detection failed: ${errorMessage}`,
};
}
}
/**
* Check if tag is ISO 15693 based on tech types
*/
export function isIso15693(techTypes: string[]): boolean {
return techTypes.some(
t => t.includes('NfcV') || t.includes('ISO15693') || t.includes('Iso15693'),
);
}
/**
* Get human-readable name for IC manufacturer
*/
export function getIcManufacturerName(code: number): string {
const manufacturers: Record<number, string> = {
0x01: 'Motorola',
0x02: 'STMicroelectronics',
0x03: 'Hitachi',
0x04: 'NXP Semiconductors',
0x05: 'Infineon',
0x06: 'Cylink',
0x07: 'Texas Instruments',
0x08: 'Fujitsu',
0x09: 'Matsushita',
0x0a: 'NEC',
0x0b: 'Oki Electric',
0x0c: 'Toshiba',
0x0d: 'Mitsubishi',
0x0e: 'Samsung',
0x0f: 'Hyundai',
0x10: 'LG Semiconductors',
};
return manufacturers[code] || `Unknown (0x${code.toString(16)})`;
}

View File

@@ -0,0 +1,342 @@
/**
* JavaCard/JCOP Detector
* Identifies JavaCard chips using CPLC (Card Production Life Cycle) data
* and AID probing
*/
import {ChipType} from '../../types/detection';
import {
GET_CPLC,
selectAid,
KNOWN_AIDS,
sendIsoDepCommand,
parseApduResponse,
} from '../nfc/commands';
/**
* CPLC (Card Production Life Cycle) data structure
*/
export interface CPLCData {
icFabricator: number;
icType: number;
osId: number;
osBuildDate: number;
icFabricationDate: number;
icSerialNumber: number;
icBatchIdentifier: number;
icModulePackager: number;
installerIdentifier: number;
}
/**
* Known IC Fabricator codes
*/
const IC_FABRICATORS: Record<number, string> = {
0x4790: 'NXP Semiconductors',
0x4180: 'Atmel',
0x4090: 'Infineon',
0x3060: 'Renesas',
0x4250: 'Samsung',
0x3360: 'STMicroelectronics',
};
/**
* Known JCOP versions based on OS ID patterns
*/
const JCOP_OS_PATTERNS: Array<{pattern: number; mask: number; name: string}> = [
{pattern: 0x4791, mask: 0xffff, name: 'JCOP4 J3R180'},
{pattern: 0x4700, mask: 0xff00, name: 'JCOP4'},
{pattern: 0x4680, mask: 0xff80, name: 'JCOP3'},
{pattern: 0x4600, mask: 0xff00, name: 'JCOP2.x'},
];
/**
* Result of JavaCard detection
*/
export interface JavaCardDetectionResult {
success: boolean;
chipType?: ChipType;
cplc?: CPLCData;
fabricatorName?: string;
osName?: string;
installedApplets?: string[];
error?: string;
}
/**
* Parse CPLC response into structured data
*/
function parseCPLC(data: number[]): CPLCData | null {
// CPLC is 42 bytes (sometimes with tag 9F7F prefix)
let cplcData = data;
// Remove tag if present
if (data[0] === 0x9f && data[1] === 0x7f) {
cplcData = data.slice(3); // Skip 9F 7F length
}
if (cplcData.length < 42) {
return null;
}
return {
icFabricator: (cplcData[0] << 8) | cplcData[1],
icType: (cplcData[2] << 8) | cplcData[3],
osId: (cplcData[4] << 8) | cplcData[5],
osBuildDate: (cplcData[6] << 8) | cplcData[7],
icFabricationDate: (cplcData[8] << 8) | cplcData[9],
icSerialNumber:
(cplcData[10] << 24) |
(cplcData[11] << 16) |
(cplcData[12] << 8) |
cplcData[13],
icBatchIdentifier: (cplcData[14] << 8) | cplcData[15],
icModulePackager: (cplcData[16] << 8) | cplcData[17],
installerIdentifier: (cplcData[18] << 8) | cplcData[19],
};
}
/**
* Identify JCOP version from OS ID
*/
function identifyJcopVersion(osId: number): string | null {
for (const pattern of JCOP_OS_PATTERNS) {
if ((osId & pattern.mask) === pattern.pattern) {
return pattern.name;
}
}
return null;
}
/**
* Detect JavaCard/JCOP chip using CPLC
*/
export async function detectJavaCard(): Promise<JavaCardDetectionResult> {
try {
// First, try to select the Card Manager (ISD)
// Card Manager selection might fail, but we can still try CPLC
// Some cards allow CPLC without selecting an applet
try {
await sendIsoDepCommand(selectAid(KNOWN_AIDS.cardManager));
} catch {
// Ignore - some cards don't support Card Manager selection
}
// Get CPLC data
const cplcResponse = await sendIsoDepCommand(GET_CPLC);
const cplcParsed = parseApduResponse(cplcResponse);
if (!cplcParsed.isSuccess || cplcParsed.data.length < 20) {
// CPLC not available - might not be a JavaCard
return {
success: false,
error: 'CPLC data not available - may not be a JavaCard',
};
}
// Parse CPLC
const cplc = parseCPLC(cplcParsed.data);
if (!cplc) {
return {
success: false,
error: 'Failed to parse CPLC data',
};
}
// Identify fabricator
const fabricatorName =
IC_FABRICATORS[cplc.icFabricator] ||
`Unknown (0x${cplc.icFabricator.toString(16)})`;
// Identify JCOP version
const osName = identifyJcopVersion(cplc.osId);
// Determine chip type
let chipType: ChipType = ChipType.JAVACARD_UNKNOWN;
// Check if it's NXP JCOP4 (J3R180)
if (cplc.icFabricator === 0x4790 && osName?.includes('JCOP4')) {
chipType = ChipType.JCOP4;
}
// Probe for installed applets
const installedApplets = await probeApplets();
return {
success: true,
chipType,
cplc,
fabricatorName,
osName: osName || `Unknown OS (0x${cplc.osId.toString(16)})`,
installedApplets,
};
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
return {
success: false,
error: `JavaCard detection failed: ${errorMessage}`,
};
}
}
/**
* Probe for common applets
*/
async function probeApplets(): Promise<string[]> {
const found: string[] = [];
// Try OpenPGP applet
try {
const response = await sendIsoDepCommand(selectAid(KNOWN_AIDS.openPgp));
const parsed = parseApduResponse(response);
if (parsed.isSuccess) {
found.push('OpenPGP');
}
} catch {
// Applet not present
}
// Try FIDO/U2F applet
try {
const response = await sendIsoDepCommand(selectAid(KNOWN_AIDS.fido));
const parsed = parseApduResponse(response);
if (parsed.isSuccess) {
found.push('FIDO/U2F');
}
} catch {
// Applet not present
}
return found;
}
/**
* Check if tag might be a JavaCard based on ATS/historical bytes
*/
export function mightBeJavaCard(
historicalBytes?: string,
ats?: string,
): boolean {
if (!historicalBytes && !ats) {
return false;
}
const checkStr = (historicalBytes || ats || '').toUpperCase();
// Look for JCOP signatures in historical bytes
// "4A434F50" = "JCOP" in ASCII
if (checkStr.includes('4A:43:4F:50') || checkStr.includes('4A434F50')) {
return true;
}
// NXP SmartMX patterns
if (checkStr.includes('80:31') || checkStr.includes('80:71')) {
return true;
}
// Check for typical JavaCard ATS patterns
// T0=78 indicates lots of historical bytes (common in JavaCards)
if (ats && ats.startsWith('78')) {
return true;
}
return false;
}
/**
* Detect JavaCard from ATS/historical bytes when CPLC fails
* This is useful for iOS where commands may not work reliably
*/
export function detectJavaCardFromAts(
historicalBytes?: string,
ats?: string,
): JavaCardDetectionResult {
if (!historicalBytes && !ats) {
return {
success: false,
error: 'No ATS/historical bytes available',
};
}
const checkStr = (historicalBytes || ats || '').toUpperCase();
const cleanStr = checkStr.replace(/[:\s-]/g, '');
// Look for JCOP signatures in historical bytes
// "4A434F50" = "JCOP" in ASCII
if (cleanStr.includes('4A434F50')) {
// Try to determine JCOP version from surrounding bytes
const jcopIndex = cleanStr.indexOf('4A434F50');
const afterJcop = cleanStr.substring(jcopIndex + 8);
// JCOP4 typically has version info after "JCOP"
if (afterJcop.startsWith('34') || afterJcop.includes('4A33')) {
// '34' = '4' in ASCII, or J3 pattern
return {
success: true,
chipType: ChipType.JCOP4,
osName: 'JCOP4 (from ATS)',
};
}
return {
success: true,
chipType: ChipType.JAVACARD_UNKNOWN,
osName: 'JCOP (version unknown)',
};
}
// NXP SmartMX patterns (common in JCOP cards)
if (cleanStr.includes('8031') || cleanStr.includes('8071')) {
return {
success: true,
chipType: ChipType.JAVACARD_UNKNOWN,
osName: 'NXP SmartMX (likely JCOP)',
};
}
// Check for JavaCard capability indicators in historical bytes
// Category indicator 0x80 followed by card capabilities
const histBytes = cleanStr.match(/.{1,2}/g)?.map(h => parseInt(h, 16)) || [];
if (histBytes.length >= 3) {
// Check for category indicator 0x80 (status indicator)
if (histBytes[0] === 0x80) {
// Check compact-TLV data objects
// 0x31 = card capabilities, 0x71 = card service data
if (histBytes[1] === 0x31 || histBytes[1] === 0x71) {
return {
success: true,
chipType: ChipType.JAVACARD_UNKNOWN,
osName: 'JavaCard (from ATS capabilities)',
};
}
}
// Check for initial selection indicator 0x00 (typically JavaCards)
// followed by application identifier presence
if (histBytes[0] === 0x00 && histBytes.length >= 5) {
return {
success: true,
chipType: ChipType.JAVACARD_UNKNOWN,
osName: 'Possible JavaCard (from ATS)',
};
}
}
return {
success: false,
error: 'Could not identify JavaCard from ATS',
};
}
/**
* Format CPLC data for display
*/
export function formatCPLC(cplc: CPLCData): string {
const fabricator =
IC_FABRICATORS[cplc.icFabricator] || `0x${cplc.icFabricator.toString(16)}`;
return `Fabricator: ${fabricator}, OS: 0x${cplc.osId.toString(16)}`;
}

View File

@@ -0,0 +1,387 @@
/**
* 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;
}

View File

@@ -0,0 +1,305 @@
/**
* NTAG/Ultralight Detector
* Identifies NTAG213/215/216, NTAG I2C, and MIFARE Ultralight variants
* using GET_VERSION command
*/
import {ChipType, NtagVersionInfo} from '../../types/detection';
import {NTAG_GET_VERSION, sendType2Command} from '../nfc/commands';
/**
* GET_VERSION response structure (same for NTAG and Ultralight):
* Byte 0: Fixed header (0x00)
* Byte 1: Vendor ID (0x04 = NXP)
* Byte 2: Product type
* Byte 3: Product subtype
* Byte 4: Major product version
* Byte 5: Minor product version
* Byte 6: Storage size (encoded)
* Byte 7: Protocol type (0x03 = ISO 14443-3)
*/
/** Product type values */
const PRODUCT_TYPES = {
ULTRALIGHT: 0x03, // MIFARE Ultralight family
NTAG: 0x04, // NTAG 21x family
NTAG_I2C: 0x05, // NTAG I2C family
} as const;
/**
* Storage size encoding for MIFARE Ultralight family (product type 0x03)
*/
const ULTRALIGHT_STORAGE_SIZES: Record<number, {type: ChipType; size: number}> =
{
// MIFARE Ultralight (MF0ICU1): 48 bytes
0x06: {type: ChipType.ULTRALIGHT, size: 48},
// MIFARE Ultralight Nano: 48 bytes
0x0a: {type: ChipType.ULTRALIGHT_NANO, size: 48},
// MIFARE Ultralight C (MF0ICU2): 144 bytes
0x0b: {type: ChipType.ULTRALIGHT_C, size: 144},
// MIFARE Ultralight EV1 (MF0UL11): 48 bytes (20 pages)
0x0e: {type: ChipType.ULTRALIGHT_EV1, size: 48},
// MIFARE Ultralight EV1 (MF0UL21): 128 bytes (41 pages)
0x0f: {type: ChipType.ULTRALIGHT_EV1, size: 128},
// MIFARE Ultralight AES: 540 bytes
0x15: {type: ChipType.ULTRALIGHT_AES, size: 540},
};
/**
* Storage size encoding for standard NTAG chips (product type 0x04)
*/
const NTAG_STORAGE_SIZES: Record<number, {type: ChipType; size: number}> = {
// NTAG210: 48 bytes user memory (rare)
0x06: {type: ChipType.NTAG213, size: 48}, // Treat as NTAG213
// NTAG212: 128 bytes user memory (rare)
0x0a: {type: ChipType.NTAG213, size: 128}, // Treat as NTAG213
// NTAG213: 144 bytes user memory
0x0f: {type: ChipType.NTAG213, size: 144},
// NTAG215: 504 bytes user memory
0x11: {type: ChipType.NTAG215, size: 504},
// NTAG216: 888 bytes user memory
0x13: {type: ChipType.NTAG216, size: 888},
// NTAG I2C 2K reporting as product type 0x04 (some chips do this)
// Storage size 0x15 = ~1912 bytes, same as NTAG I2C 2K
0x15: {type: ChipType.NTAG_I2C_2K, size: 1912},
};
/**
* Storage size encoding for NTAG I2C chips (product type 0x05)
* Per NXP NT3H1101/NT3H1201/NT3H2111/NT3H2211 datasheets
*
* IMPORTANT: Only use exact values from NXP datasheets.
* Storage size byte encoding: 2^(storageSize/2) = total bytes
* - 0x13: 2^(19/2) ≈ 1024 bytes total = NTAG I2C 1K
* - 0x15: 2^(21/2) ≈ 2048 bytes total = NTAG I2C 2K
*
* The Plus vs non-Plus variant is determined by subtype byte, NOT storage size.
*/
const NTAG_I2C_STORAGE_SIZES: Record<number, {type: ChipType; size: number}> = {
// NTAG I2C 1K (NT3H1101, NT3H2111): 888 bytes user memory
0x13: {type: ChipType.NTAG_I2C_1K, size: 888},
// NTAG I2C 2K (NT3H1201, NT3H2211): 1912 bytes user memory
0x15: {type: ChipType.NTAG_I2C_2K, size: 1912},
};
/**
* NTAG I2C Plus variants (detected by subtype)
* Per NXP NT3H2111/NT3H2211 datasheet:
* - Subtype 0x01: NTAG I2C (non-Plus)
* - Subtype 0x02: NTAG I2C Plus
*/
const NTAG_I2C_PLUS_SUBTYPES = [0x02];
/**
* Result of NTAG detection
*/
export interface NtagDetectionResult {
success: boolean;
chipType?: ChipType;
versionInfo?: NtagVersionInfo;
memorySize?: number;
error?: string;
}
/**
* Detect NTAG chip variant using GET_VERSION command
*/
export async function detectNtag(): Promise<NtagDetectionResult> {
try {
console.log('[NTAG] Sending GET_VERSION command:', NTAG_GET_VERSION);
// Send GET_VERSION command (0x60)
const response = await sendType2Command(NTAG_GET_VERSION);
console.log('[NTAG] GET_VERSION response:', response);
if (response.length < 8) {
return {
success: false,
error: `Invalid GET_VERSION response length: ${response.length}`,
};
}
// Parse version info
const versionInfo: NtagVersionInfo = {
vendorId: response[1],
productType: response[2],
productSubtype: response[3],
majorVersion: response[4],
minorVersion: response[5],
storageSize: response[6],
protocolType: response[7],
};
console.log('[NTAG] Parsed version info:', {
vendorId: `0x${versionInfo.vendorId.toString(16)}`,
productType: `0x${versionInfo.productType.toString(16)}`,
productSubtype: `0x${versionInfo.productSubtype.toString(16)}`,
storageSize: `0x${versionInfo.storageSize.toString(16)}`,
});
// Check if this is an NXP chip
if (versionInfo.vendorId !== 0x04) {
return {
success: true,
chipType: ChipType.NTAG_UNKNOWN,
versionInfo,
error: `Non-NXP vendor ID: 0x${versionInfo.vendorId.toString(16)}`,
};
}
// Handle MIFARE Ultralight family (product type 0x03)
if (versionInfo.productType === PRODUCT_TYPES.ULTRALIGHT) {
const storageInfo = ULTRALIGHT_STORAGE_SIZES[versionInfo.storageSize];
if (storageInfo) {
return {
success: true,
chipType: storageInfo.type,
versionInfo,
memorySize: storageInfo.size,
};
}
// Unknown Ultralight variant
return {
success: true,
chipType: ChipType.ULTRALIGHT,
versionInfo,
error: `Unknown Ultralight storage size: 0x${versionInfo.storageSize.toString(16)}`,
};
}
// Handle NTAG I2C (product type 0x05)
// Per NXP NT3H2111/NT3H2211 datasheet:
// - Storage size 0x13 = 1K variant (NT3H1101, NT3H2111)
// - Storage size 0x15 = 2K variant (NT3H1201, NT3H2211)
// - Subtype 0x01 = non-Plus, 0x02 = Plus
if (versionInfo.productType === PRODUCT_TYPES.NTAG_I2C) {
const storageInfo = NTAG_I2C_STORAGE_SIZES[versionInfo.storageSize];
if (!storageInfo) {
// Unknown storage size - report it for diagnosis rather than guessing
console.warn(
`[NTAG] Unknown NTAG I2C storage size: 0x${versionInfo.storageSize.toString(16)}`,
);
return {
success: true,
chipType: ChipType.NTAG_UNKNOWN,
versionInfo,
error: `NTAG I2C with unknown storage size: 0x${versionInfo.storageSize.toString(16)}. Expected 0x13 (1K) or 0x15 (2K).`,
};
}
let chipType = storageInfo.type;
const memorySize = storageInfo.size;
// Check for Plus variant based on subtype (0x02 = Plus)
if (NTAG_I2C_PLUS_SUBTYPES.includes(versionInfo.productSubtype)) {
chipType =
chipType === ChipType.NTAG_I2C_1K || chipType === ChipType.NTAG_I2C_PLUS_1K
? ChipType.NTAG_I2C_PLUS_1K
: ChipType.NTAG_I2C_PLUS_2K;
}
return {
success: true,
chipType,
versionInfo,
memorySize,
};
}
// Handle standard NTAG (product type 0x04)
if (versionInfo.productType === PRODUCT_TYPES.NTAG) {
const storageInfo = NTAG_STORAGE_SIZES[versionInfo.storageSize];
if (storageInfo) {
return {
success: true,
chipType: storageInfo.type,
versionInfo,
memorySize: storageInfo.size,
};
}
// Unknown storage size - return as unknown NTAG
return {
success: true,
chipType: ChipType.NTAG_UNKNOWN,
versionInfo,
error: `Unknown NTAG storage size: 0x${versionInfo.storageSize.toString(16)}`,
};
}
// Unknown product type
return {
success: true,
chipType: ChipType.NTAG_UNKNOWN,
versionInfo,
error: `Unknown product type: 0x${versionInfo.productType.toString(16)}`,
};
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
// GET_VERSION command not supported - might not be an NTAG
if (
errorMessage.toLowerCase().includes('transceive') ||
errorMessage.toLowerCase().includes('tag was lost')
) {
return {
success: false,
error: 'GET_VERSION command failed - may not be an NTAG',
};
}
return {
success: false,
error: `NTAG detection failed: ${errorMessage}`,
};
}
}
/**
* Check if raw tag data suggests this might be an NTAG
* (preliminary check before running GET_VERSION)
*/
export function mightBeNtag(sak?: number, techTypes?: string[]): boolean {
// NTAG chips typically have SAK 0x00 (no ISO 14443-4 support)
if (sak !== undefined && sak !== 0x00) {
return false;
}
// Should have NfcA technology
if (techTypes && !techTypes.some(t => t.includes('NfcA'))) {
return false;
}
// Should NOT have IsoDep (NTAG is Type 2, not Type 4)
if (techTypes && techTypes.some(t => t.includes('IsoDep'))) {
return false;
}
return true;
}
/**
* Format NTAG/Ultralight version info for display
*/
export function formatNtagVersionInfo(info: NtagVersionInfo): string {
const productTypeNames: Record<number, string> = {
0x03: 'Ultralight',
0x04: 'NTAG',
0x05: 'NTAG I2C',
};
const typeName =
productTypeNames[info.productType] || `0x${info.productType.toString(16)}`;
return [
`Vendor: ${info.vendorId === 0x04 ? 'NXP' : `0x${info.vendorId.toString(16)}`}`,
`Type: ${typeName}`,
`Version: ${info.majorVersion}.${info.minorVersion}`,
`Storage: 0x${info.storageSize.toString(16)}`,
].join(', ');
}

View File

@@ -15,12 +15,32 @@ import type {
/** /**
* Convert byte array to hex string * Convert byte array to hex string
* Handles number[], Uint8Array, string, or undefined
*/ */
function bytesToHex(bytes: number[] | undefined): string { function bytesToHex(bytes: number[] | Uint8Array | string | undefined): string {
if (!bytes || bytes.length === 0) { if (!bytes) {
return ''; return '';
} }
return bytes.map(b => b.toString(16).padStart(2, '0').toUpperCase()).join(':');
// If already a string, assume it's hex and format it
if (typeof bytes === 'string') {
// Remove any existing separators and format consistently
const hex = bytes.replace(/[:\s-]/g, '').toUpperCase();
if (hex.length === 0) {
return '';
}
// Add colons between byte pairs
return hex.match(/.{1,2}/g)?.join(':') || hex;
}
// Convert array-like to actual array if needed (handles Uint8Array)
const byteArray = Array.from(bytes);
if (byteArray.length === 0) {
return '';
}
return byteArray.map(b => b.toString(16).padStart(2, '0').toUpperCase()).join(':');
} }
/** /**
@@ -32,6 +52,29 @@ function parseSak(tag: TagEvent): number | undefined {
if (nfcA?.sak !== undefined) { if (nfcA?.sak !== undefined) {
return nfcA.sak; return nfcA.sak;
} }
// iOS: CoreNFC doesn't directly expose SAK
// But we can infer ISO-DEP capability (SAK bit 5) from iso7816 presence
if (Platform.OS === 'ios') {
const iso7816 = (tag as any).iso7816;
const mifare = (tag as any).mifare;
// If iso7816 interface is available, the tag has ISO-DEP capability
// This corresponds to SAK bit 5 being set (value 0x20)
if (iso7816) {
// Check if also has MIFARE capability (could be DESFire or Plus)
if (mifare) {
return 0x20; // ISO-DEP capable (like DESFire)
}
return 0x20; // Pure ISO-DEP (like NTAG 424 DNA)
}
// If only MIFARE/NFC-A without iso7816, likely NTAG or Ultralight (SAK 0x00)
if (mifare) {
return 0x00;
}
}
return undefined; return undefined;
} }
@@ -76,15 +119,39 @@ function parseAts(tag: TagEvent): {ats?: string; historicalBytes?: string} {
* Convert TagEvent to RawTagData * Convert TagEvent to RawTagData
*/ */
function tagEventToRawData(tag: TagEvent): RawTagData { function tagEventToRawData(tag: TagEvent): RawTagData {
const techTypes = (tag.techTypes || []) as NfcTechType[]; let techTypes = (tag.techTypes || []) as NfcTechType[];
const uid = tag.id ? bytesToHex(tag.id as unknown as number[]) : ''; const uid = tag.id ? bytesToHex(tag.id as string | number[] | Uint8Array) : '';
const sak = parseSak(tag); const sak = parseSak(tag);
const atqa = parseAtqa(tag); const atqa = parseAtqa(tag);
const {ats, historicalBytes} = parseAts(tag); const {ats, historicalBytes} = parseAts(tag);
const isoDep = (tag as any).isoDep; const isoDep = (tag as any).isoDep;
const iso7816 = (tag as any).iso7816;
const maxTransceiveLength = isoDep?.maxTransceiveLength; const maxTransceiveLength = isoDep?.maxTransceiveLength;
// On iOS, detect ISO-DEP capability from iso7816 property or tag type
// This ensures NTAG 424 DNA and DESFire are properly identified
if (Platform.OS === 'ios') {
// If iso7816 property exists, this is an ISO-DEP capable tag
if (iso7816 && !techTypes.some(t => t.includes('IsoDep'))) {
techTypes = [...techTypes, 'android.nfc.tech.IsoDep' as NfcTechType];
}
// Also check tag type for iOS
const tagType = (tag as any).type;
if (tagType && typeof tagType === 'string') {
if (tagType.includes('iso7816') || tagType.includes('IsoDep')) {
if (!techTypes.some(t => t.includes('IsoDep'))) {
techTypes = [...techTypes, 'android.nfc.tech.IsoDep' as NfcTechType];
}
}
if (tagType.includes('iso15693') || tagType.includes('NfcV')) {
if (!techTypes.some(t => t.includes('NfcV'))) {
techTypes = [...techTypes, 'android.nfc.tech.NfcV' as NfcTechType];
}
}
}
}
return { return {
uid, uid,
techTypes, techTypes,
@@ -191,8 +258,13 @@ class NFCManagerService {
/** /**
* Request NFC technology and scan for a tag * Request NFC technology and scan for a tag
* Returns raw tag data on success * Returns raw tag data on success
*
* @param keepAlive - If true, don't release the NFC technology after scanning.
* Caller must call cancelScan() when done.
*/ */
async scanTag(): Promise<{tag?: RawTagData; error?: ScanError}> { async scanTag(
keepAlive = false,
): Promise<{tag?: RawTagData; error?: ScanError}> {
try { try {
// Ensure initialized // Ensure initialized
if (!this.initialized) { if (!this.initialized) {
@@ -220,17 +292,21 @@ class NFCManagerService {
// Request technology based on platform // Request technology based on platform
if (Platform.OS === 'ios') { if (Platform.OS === 'ios') {
// iOS: Use MifareIOS for broadest compatibility with ISO 14443 tags // iOS: Use MifareIOS which works for NFC-A tags including ISO-DEP
// The iso7816HandlerIOS is used separately for ISO-DEP commands
await NfcManager.requestTechnology(NfcTech.MifareIOS, { await NfcManager.requestTechnology(NfcTech.MifareIOS, {
alertMessage: 'Hold your NFC tag near the top of your iPhone', alertMessage: 'Hold your NFC tag near the top of your iPhone',
}); });
} else { } else {
// Android: Request multiple technologies for best detection // Android: Request multiple technologies for best detection
// IMPORTANT: IsoDep MUST be first so ISO-DEP capable tags (DESFire, NTAG 424 DNA)
// connect via ISO-DEP rather than NfcA. When NfcA connects first, isoDepHandler
// won't work because the wrong technology is active.
await NfcManager.requestTechnology([ await NfcManager.requestTechnology([
NfcTech.NfcA,
NfcTech.NfcB,
NfcTech.NfcV,
NfcTech.IsoDep, NfcTech.IsoDep,
NfcTech.NfcA,
NfcTech.NfcV,
NfcTech.NfcB,
NfcTech.MifareClassic, NfcTech.MifareClassic,
]); ]);
} }
@@ -238,6 +314,9 @@ class NFCManagerService {
// Get the tag // Get the tag
const tag = await NfcManager.getTag(); const tag = await NfcManager.getTag();
if (!tag) { if (!tag) {
if (!keepAlive) {
await this.cancelScan();
}
return { return {
error: createScanError('UNKNOWN', 'No tag data received'), error: createScanError('UNKNOWN', 'No tag data received'),
}; };
@@ -247,7 +326,39 @@ class NFCManagerService {
} catch (error) { } catch (error) {
return {error: categorizeError(error)}; return {error: categorizeError(error)};
} finally { } finally {
// Always clean up // Only clean up if not keeping alive
if (!keepAlive) {
await this.cancelScan();
}
}
}
/**
* Scan tag and run detection callback while NFC session is active
* This ensures commands can be sent during detection
*/
async scanWithDetection<T>(
detectFn: (tag: RawTagData) => Promise<T>,
): Promise<{tag?: RawTagData; detection?: T; error?: ScanError}> {
try {
// Scan but keep the session alive
const {tag, error} = await this.scanTag(true);
if (error || !tag) {
return {error};
}
// Run detection while session is still active
try {
const detection = await detectFn(tag);
return {tag, detection};
} catch (detectError) {
// Detection failed but we still have the tag data
console.warn('[NFCManager] Detection failed:', detectError);
return {tag};
}
} finally {
// Always clean up after detection
await this.cancelScan(); await this.cancelScan();
} }
} }

View File

@@ -0,0 +1,341 @@
/**
* NFC APDU Commands
* Command builders and utilities for NFC communication
*/
import {Platform} from 'react-native';
import NfcManager, {NfcTech} from 'react-native-nfc-manager';
/**
* APDU response with status word
*/
export interface ApduResponse {
data: number[];
sw1: number;
sw2: number;
isSuccess: boolean;
}
/**
* Parse APDU response extracting data and status words
*/
export function parseApduResponse(response: number[]): ApduResponse {
if (response.length < 2) {
return {data: [], sw1: 0, sw2: 0, isSuccess: false};
}
const sw1 = response[response.length - 2];
const sw2 = response[response.length - 1];
const data = response.slice(0, -2);
// 0x9000 = Success, 0x91XX = DESFire success with more data
const isSuccess = sw1 === 0x90 || sw1 === 0x91;
return {data, sw1, sw2, isSuccess};
}
/**
* Convert hex string to byte array
*/
export function hexToBytes(hex: string): number[] {
const cleanHex = hex.replace(/[:\s-]/g, '');
const bytes: number[] = [];
for (let i = 0; i < cleanHex.length; i += 2) {
bytes.push(parseInt(cleanHex.substring(i, i + 2), 16));
}
return bytes;
}
/**
* Convert byte array to hex string
*/
export function bytesToHex(bytes: number[]): string {
return bytes.map(b => b.toString(16).padStart(2, '0').toUpperCase()).join('');
}
// ============================================================================
// NTAG Commands (NFC Type 2 Tags)
// ============================================================================
/**
* NTAG GET_VERSION command
* Returns 8 bytes: header, vendor ID, product type, product subtype,
* major version, minor version, storage size, protocol type
*/
export const NTAG_GET_VERSION = [0x60];
/**
* NTAG READ command - reads 4 pages (16 bytes) starting at given page
*/
export function ntagRead(pageAddress: number): number[] {
return [0x30, pageAddress];
}
// ============================================================================
// ISO 14443-4 / ISO-DEP Commands
// ============================================================================
/**
* DESFire GET_VERSION command (ISO-wrapped)
* Response byte 3 indicates version: 0x00=EV0, 0x01=EV1, 0x10=EV2, 0x30=EV3
*/
export const DESFIRE_GET_VERSION = [0x90, 0x60, 0x00, 0x00, 0x00];
/**
* DESFire GET_VERSION additional frames (for full version info)
*/
export const DESFIRE_GET_VERSION_CONTINUE = [0x90, 0xaf, 0x00, 0x00, 0x00];
/**
* SELECT command for AID
*/
export function selectAid(aid: number[]): number[] {
return [0x00, 0xa4, 0x04, 0x00, aid.length, ...aid, 0x00];
}
/**
* GET DATA command for CPLC (Card Production Life Cycle)
* Used for JavaCard/JCOP identification
*/
export const GET_CPLC = [0x80, 0xca, 0x9f, 0x7f, 0x00];
// ============================================================================
// ISO 15693 (NFC-V) Commands - Per NXP AN11042
// ============================================================================
/**
* ISO 15693 GET_SYSTEM_INFO command (0x2B)
* Returns: DSFID, UID, block size, block count, IC reference
* This is the NXP-recommended way to identify SLIX/NTAG5 chips
*
* Request format: [Flags] [Command] [UID if addressed]
* Flags 0x02 = unaddressed mode (use inventory to select)
* Flags 0x22 = addressed mode (include UID)
*/
export const ISO15693_GET_SYSTEM_INFO = [0x02, 0x2b];
/**
* ISO 15693 GET_SYSTEM_INFO with specific UID (addressed mode)
*/
export function iso15693GetSystemInfoAddressed(uid: number[]): number[] {
// Flags 0x22: Addressed mode, high data rate
return [0x22, 0x2b, ...uid];
}
/**
* ISO 15693 READ_SINGLE_BLOCK command
* Used to read configuration pages for additional identification
*/
export function iso15693ReadSingleBlock(blockNumber: number): number[] {
return [0x02, 0x20, blockNumber];
}
// ============================================================================
// Known AIDs
// ============================================================================
export const KNOWN_AIDS = {
/** Global Platform Card Manager */
cardManager: [0xa0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00],
/** OpenPGP applet */
openPgp: [0xd2, 0x76, 0x00, 0x01, 0x24, 0x01],
/** FIDO/U2F applet */
fido: [0xa0, 0x00, 0x00, 0x06, 0x47, 0x2f, 0x00, 0x01],
};
// ============================================================================
// Command Execution
// ============================================================================
/**
* Send a raw command to NFC-A tag (Type 2 tags like NTAG)
*/
export async function transceiveNfcA(command: number[]): Promise<number[]> {
try {
const response = await NfcManager.nfcAHandler.transceive(command);
return Array.from(response);
} catch (error) {
console.error('[commands] NfcA transceive failed:', error);
throw error;
}
}
/**
* Send ISO-DEP APDU command
*/
export async function transceiveIsoDep(command: number[]): Promise<number[]> {
try {
const response = await NfcManager.isoDepHandler.transceive(command);
return Array.from(response);
} catch (error) {
console.error('[commands] IsoDep transceive failed:', error);
throw error;
}
}
/**
* Send command via iOS MIFARE handler (covers NFC-A and ISO-DEP on iOS)
*/
export async function transceiveMifareIOS(
command: number[],
): Promise<number[]> {
try {
const response = await NfcManager.sendMifareCommandIOS(command);
return Array.from(response);
} catch (error) {
console.error('[commands] MifareIOS transceive failed:', error);
throw error;
}
}
/**
* Send command via iOS using isoDepHandler (for ISO-DEP/ISO 14443-4 tags)
* This works when the tag supports ISO-DEP
*/
export async function transceiveIsoDepIOS(command: number[]): Promise<number[]> {
try {
// Try isoDepHandler first - works for ISO 14443-4 tags
const response = await NfcManager.isoDepHandler.transceive(command);
return Array.from(response);
} catch (error) {
console.error('[commands] isoDepHandler transceive failed:', error);
throw error;
}
}
/**
* Send ISO 15693 (NFC-V) command
* Uses nfcVHandler on Android, iso15693HandlerIOS on iOS
*/
export async function transceiveNfcV(command: number[]): Promise<number[]> {
try {
// Android uses nfcVHandler.transceive for raw commands
const response = await NfcManager.nfcVHandler.transceive(command);
return Array.from(response);
} catch (error) {
console.error('[commands] NfcV transceive failed:', error);
throw error;
}
}
/**
* Get ISO 15693 system info using platform-specific method
* iOS has direct getSystemInfo method, Android uses raw command
*/
export interface Iso15693SystemInfo {
dsfid?: number;
afi?: number;
blockSize?: number;
blockCount?: number;
icReference?: number;
}
export async function getIso15693SystemInfo(): Promise<Iso15693SystemInfo> {
if (Platform.OS === 'ios') {
// iOS has direct method with typed response
try {
const result = await NfcManager.iso15693HandlerIOS.getSystemInfo(0x02);
return {
dsfid: result.dsfid,
afi: result.afi,
blockSize: result.blockSize,
blockCount: result.blockCount,
icReference: result.icReference,
};
} catch (error) {
console.error('[commands] iOS getSystemInfo failed:', error);
throw error;
}
}
// Android: use raw command and parse response
const response = await transceiveNfcV(ISO15693_GET_SYSTEM_INFO);
// Parse the response (simplified - may need adjustment based on actual response format)
if (response.length < 2 || (response[0] & 0x01)) {
throw new Error('GET_SYSTEM_INFO failed or returned error');
}
// Response parsing depends on info flags
const infoFlags = response[1];
let offset = 10; // Skip flags and UID
const result: Iso15693SystemInfo = {};
if ((infoFlags & 0x01) && response.length > offset) {
result.dsfid = response[offset++];
}
if ((infoFlags & 0x02) && response.length > offset) {
result.afi = response[offset++];
}
if ((infoFlags & 0x04) && response.length >= offset + 2) {
result.blockCount = response[offset] + 1;
result.blockSize = (response[offset + 1] & 0x1f) + 1;
offset += 2;
}
if ((infoFlags & 0x08) && response.length > offset) {
result.icReference = response[offset];
}
return result;
}
/**
* Platform-aware command sending for Type 2 tags (NTAG)
*/
export async function sendType2Command(command: number[]): Promise<number[]> {
if (Platform.OS === 'ios') {
return transceiveMifareIOS(command);
}
return transceiveNfcA(command);
}
/**
* Platform-aware command sending for ISO-DEP tags (DESFire, JavaCard)
*/
export async function sendIsoDepCommand(command: number[]): Promise<number[]> {
console.log(`[commands] sendIsoDepCommand on ${Platform.OS}:`, command);
if (Platform.OS === 'ios') {
// Try isoDepHandler first (works for ISO 14443-4 tags)
try {
console.log('[commands] Trying isoDepHandler...');
const response = await transceiveIsoDepIOS(command);
console.log('[commands] isoDepHandler success:', response);
return response;
} catch (isoDepError) {
console.log('[commands] isoDepHandler failed:', isoDepError);
// Fall back to MifareIOS if isoDepHandler fails
console.log('[commands] Trying MifareIOS fallback...');
const response = await transceiveMifareIOS(command);
console.log('[commands] MifareIOS success:', response);
return response;
}
}
console.log('[commands] Using Android isoDepHandler...');
const response = await transceiveIsoDep(command);
console.log('[commands] Android isoDepHandler success:', response);
return response;
}
/**
* Request specific NFC technology
*/
export async function requestTechnology(
tech: NfcTech | NfcTech[],
options?: {alertMessage?: string},
): Promise<void> {
await NfcManager.requestTechnology(tech, options);
}
/**
* Cancel technology request and cleanup
*/
export async function cancelTechnologyRequest(): Promise<void> {
try {
await NfcManager.cancelTechnologyRequest();
} catch {
// Ignore cleanup errors
}
}

468
src/types/detection.ts Normal file
View File

@@ -0,0 +1,468 @@
/**
* Detection Types
* Types for chip identification and transponder detection
*/
/**
* Supported chip types
*/
export enum ChipType {
// NTAG 21x family (ISO 14443-3A, Type 2)
NTAG213 = 'NTAG213',
NTAG215 = 'NTAG215',
NTAG216 = 'NTAG216',
NTAG_I2C_1K = 'NTAG_I2C_1K',
NTAG_I2C_2K = 'NTAG_I2C_2K',
NTAG_I2C_PLUS_1K = 'NTAG_I2C_PLUS_1K',
NTAG_I2C_PLUS_2K = 'NTAG_I2C_PLUS_2K',
// NTAG 5 family (ISO 15693, NFC-V)
NTAG5_LINK = 'NTAG5_LINK',
NTAG5_BOOST = 'NTAG5_BOOST',
NTAG5_SWITCH = 'NTAG5_SWITCH',
// NTAG DNA family (ISO 14443-4, Type 4)
NTAG413_DNA = 'NTAG413_DNA',
NTAG424_DNA = 'NTAG424_DNA',
NTAG424_DNA_TT = 'NTAG424_DNA_TT', // TagTamper variant
NTAG_UNKNOWN = 'NTAG_UNKNOWN',
// MIFARE Classic family
MIFARE_CLASSIC_1K = 'MIFARE_CLASSIC_1K',
MIFARE_CLASSIC_4K = 'MIFARE_CLASSIC_4K',
MIFARE_CLASSIC_MINI = 'MIFARE_CLASSIC_MINI',
// MIFARE DESFire family
DESFIRE_EV1 = 'DESFIRE_EV1',
DESFIRE_EV2 = 'DESFIRE_EV2',
DESFIRE_EV3 = 'DESFIRE_EV3',
DESFIRE_LIGHT = 'DESFIRE_LIGHT',
DESFIRE_UNKNOWN = 'DESFIRE_UNKNOWN',
// MIFARE Plus
MIFARE_PLUS_S = 'MIFARE_PLUS_S',
MIFARE_PLUS_X = 'MIFARE_PLUS_X',
MIFARE_PLUS_SE = 'MIFARE_PLUS_SE',
MIFARE_PLUS_EV1 = 'MIFARE_PLUS_EV1',
MIFARE_PLUS = 'MIFARE_PLUS', // Generic
// MIFARE Ultralight family
ULTRALIGHT = 'ULTRALIGHT',
ULTRALIGHT_C = 'ULTRALIGHT_C',
ULTRALIGHT_EV1 = 'ULTRALIGHT_EV1',
ULTRALIGHT_NANO = 'ULTRALIGHT_NANO',
ULTRALIGHT_AES = 'ULTRALIGHT_AES',
// ISO 15693 (NFC-V) - ICODE family
SLIX = 'SLIX',
SLIX2 = 'SLIX2',
SLIX_S = 'SLIX_S',
SLIX_L = 'SLIX_L',
ICODE_DNA = 'ICODE_DNA',
ISO15693_UNKNOWN = 'ISO15693_UNKNOWN',
// JavaCard
JCOP4 = 'JCOP4',
JAVACARD_UNKNOWN = 'JAVACARD_UNKNOWN',
// Generic/Unknown
ISO14443A_UNKNOWN = 'ISO14443A_UNKNOWN',
ISO14443B_UNKNOWN = 'ISO14443B_UNKNOWN',
UNKNOWN = 'UNKNOWN',
}
/**
* Chip family categories
*/
export enum ChipFamily {
NTAG = 'NTAG',
MIFARE_CLASSIC = 'MIFARE_CLASSIC',
MIFARE_DESFIRE = 'MIFARE_DESFIRE',
MIFARE_PLUS = 'MIFARE_PLUS',
ISO15693 = 'ISO15693',
JAVACARD = 'JAVACARD',
UNKNOWN = 'UNKNOWN',
}
/**
* Get the chip family for a chip type
*/
export function getChipFamily(type: ChipType): ChipFamily {
if (type.startsWith('NTAG')) {
return ChipFamily.NTAG;
}
if (type.startsWith('ULTRALIGHT')) {
return ChipFamily.NTAG; // Ultralight is in the NTAG/Type 2 family
}
if (type.startsWith('MIFARE_CLASSIC')) {
return ChipFamily.MIFARE_CLASSIC;
}
if (type.startsWith('DESFIRE')) {
return ChipFamily.MIFARE_DESFIRE;
}
if (type.startsWith('MIFARE_PLUS')) {
return ChipFamily.MIFARE_PLUS;
}
if (type.startsWith('SLIX') || type.startsWith('ICODE') || type.startsWith('ISO15693')) {
return ChipFamily.ISO15693;
}
if (type.startsWith('JCOP') || type.startsWith('JAVACARD')) {
return ChipFamily.JAVACARD;
}
return ChipFamily.UNKNOWN;
}
/**
* NTAG version information from GET_VERSION response
*/
export interface NtagVersionInfo {
vendorId: number;
productType: number;
productSubtype: number;
majorVersion: number;
minorVersion: number;
storageSize: number;
protocolType: number;
}
/**
* DESFire version information
*/
export interface DesfireVersionInfo {
hardwareMajor: number;
hardwareMinor: number;
hardwareStorageSize: number;
softwareMajor: number;
softwareMinor: number;
}
/**
* SAK swap detection result (imported from mifare detector)
*/
export interface SakSwapInfo {
hasSakSwap: boolean;
swapType?:
| 'mifare_plus_sl1'
| 'desfire_with_classic'
| 'magic_card'
| 'unknown';
confidence: 'high' | 'medium' | 'low';
description: string;
notes?: string[];
}
/**
* Detected transponder information
*/
export interface Transponder {
/** Identified chip type */
type: ChipType;
/** Chip family category */
family: ChipFamily;
/** Human-readable chip name */
chipName: string;
/** Memory size in bytes (if known) */
memorySize?: number;
/** Whether the chip data can be cloned to an implant */
isCloneable: boolean;
/** Reason why chip is not cloneable (if applicable) */
cloneabilityNote?: string;
/** Raw detection data */
rawData: {
uid: string;
sak?: number;
atqa?: string;
ats?: string;
historicalBytes?: string;
techTypes: string[];
};
/** Chip-specific version info */
versionInfo?: NtagVersionInfo | DesfireVersionInfo;
/** SAK swap detection results */
sakSwapInfo?: SakSwapInfo;
/** Detection confidence level */
confidence: 'high' | 'medium' | 'low';
/** Platform on which detection was performed */
detectedOn: 'ios' | 'android';
}
/**
* Detection result
*/
export interface DetectionResult {
success: boolean;
transponder?: Transponder;
error?: string;
}
/**
* Human-readable names for chip types
*/
export const CHIP_NAMES: Record<ChipType, string> = {
// NTAG 21x family
[ChipType.NTAG213]: 'NTAG213',
[ChipType.NTAG215]: 'NTAG215',
[ChipType.NTAG216]: 'NTAG216',
[ChipType.NTAG_I2C_1K]: 'NTAG I2C 1K',
[ChipType.NTAG_I2C_2K]: 'NTAG I2C 2K',
[ChipType.NTAG_I2C_PLUS_1K]: 'NTAG I2C Plus 1K',
[ChipType.NTAG_I2C_PLUS_2K]: 'NTAG I2C Plus 2K',
// NTAG 5 family
[ChipType.NTAG5_LINK]: 'NTAG 5 link',
[ChipType.NTAG5_BOOST]: 'NTAG 5 boost',
[ChipType.NTAG5_SWITCH]: 'NTAG 5 switch',
// NTAG DNA family
[ChipType.NTAG413_DNA]: 'NTAG 413 DNA',
[ChipType.NTAG424_DNA]: 'NTAG 424 DNA',
[ChipType.NTAG424_DNA_TT]: 'NTAG 424 DNA TagTamper',
[ChipType.NTAG_UNKNOWN]: 'NTAG (Unknown variant)',
// MIFARE Classic
[ChipType.MIFARE_CLASSIC_1K]: 'MIFARE Classic 1K',
[ChipType.MIFARE_CLASSIC_4K]: 'MIFARE Classic 4K',
[ChipType.MIFARE_CLASSIC_MINI]: 'MIFARE Classic Mini',
// MIFARE DESFire
[ChipType.DESFIRE_EV1]: 'MIFARE DESFire EV1',
[ChipType.DESFIRE_EV2]: 'MIFARE DESFire EV2',
[ChipType.DESFIRE_EV3]: 'MIFARE DESFire EV3',
[ChipType.DESFIRE_LIGHT]: 'MIFARE DESFire Light',
[ChipType.DESFIRE_UNKNOWN]: 'MIFARE DESFire (Unknown version)',
// MIFARE Plus
[ChipType.MIFARE_PLUS_S]: 'MIFARE Plus S',
[ChipType.MIFARE_PLUS_X]: 'MIFARE Plus X',
[ChipType.MIFARE_PLUS_SE]: 'MIFARE Plus SE',
[ChipType.MIFARE_PLUS_EV1]: 'MIFARE Plus EV1',
[ChipType.MIFARE_PLUS]: 'MIFARE Plus',
// MIFARE Ultralight
[ChipType.ULTRALIGHT]: 'MIFARE Ultralight',
[ChipType.ULTRALIGHT_C]: 'MIFARE Ultralight C',
[ChipType.ULTRALIGHT_EV1]: 'MIFARE Ultralight EV1',
[ChipType.ULTRALIGHT_NANO]: 'MIFARE Ultralight Nano',
[ChipType.ULTRALIGHT_AES]: 'MIFARE Ultralight AES',
// ICODE family
[ChipType.SLIX]: 'ICODE SLIX',
[ChipType.SLIX2]: 'ICODE SLIX2',
[ChipType.SLIX_S]: 'ICODE SLIX-S',
[ChipType.SLIX_L]: 'ICODE SLIX-L',
[ChipType.ICODE_DNA]: 'ICODE DNA',
[ChipType.ISO15693_UNKNOWN]: 'ISO 15693 Tag',
// JavaCard
[ChipType.JCOP4]: 'JCOP4 (J3R180)',
[ChipType.JAVACARD_UNKNOWN]: 'JavaCard',
// Generic/Unknown
[ChipType.ISO14443A_UNKNOWN]: 'ISO 14443-A Tag',
[ChipType.ISO14443B_UNKNOWN]: 'ISO 14443-B Tag',
[ChipType.UNKNOWN]: 'Unknown NFC Tag',
};
/**
* Memory sizes for known chip types (in bytes)
*/
export const CHIP_MEMORY_SIZES: Partial<Record<ChipType, number>> = {
// NTAG 21x
[ChipType.NTAG213]: 144,
[ChipType.NTAG215]: 504,
[ChipType.NTAG216]: 888,
[ChipType.NTAG_I2C_1K]: 888,
[ChipType.NTAG_I2C_2K]: 1912,
[ChipType.NTAG_I2C_PLUS_1K]: 888,
[ChipType.NTAG_I2C_PLUS_2K]: 1912,
// NTAG 5 family
[ChipType.NTAG5_LINK]: 496, // 496 bytes user memory
[ChipType.NTAG5_BOOST]: 2000, // 2000 bytes user memory
[ChipType.NTAG5_SWITCH]: 256, // 256 bytes user memory
// NTAG DNA family
[ChipType.NTAG413_DNA]: 160, // 160 bytes user memory
[ChipType.NTAG424_DNA]: 416, // 416 bytes user memory (NDEF)
[ChipType.NTAG424_DNA_TT]: 416,
// MIFARE Classic
[ChipType.MIFARE_CLASSIC_1K]: 1024,
[ChipType.MIFARE_CLASSIC_4K]: 4096,
[ChipType.MIFARE_CLASSIC_MINI]: 320,
// MIFARE Ultralight
[ChipType.ULTRALIGHT]: 48, // 48 bytes user memory
[ChipType.ULTRALIGHT_C]: 144, // 144 bytes user memory
[ChipType.ULTRALIGHT_EV1]: 128, // 128 bytes (EV1 80 page variant)
[ChipType.ULTRALIGHT_NANO]: 48,
[ChipType.ULTRALIGHT_AES]: 540, // 540 bytes user memory
};
/**
* Cloneability information for chip types
*/
export const CHIP_CLONEABILITY: Record<
ChipType,
{cloneable: boolean; note?: string}
> = {
// NTAG 21x - all cloneable
[ChipType.NTAG213]: {cloneable: true},
[ChipType.NTAG215]: {cloneable: true},
[ChipType.NTAG216]: {cloneable: true},
[ChipType.NTAG_I2C_1K]: {cloneable: true, note: 'I2C interface not cloneable'},
[ChipType.NTAG_I2C_2K]: {cloneable: true, note: 'I2C interface not cloneable'},
[ChipType.NTAG_I2C_PLUS_1K]: {cloneable: true, note: 'I2C interface not cloneable'},
[ChipType.NTAG_I2C_PLUS_2K]: {cloneable: true, note: 'I2C interface not cloneable'},
// NTAG 5 family - NOT cloneable (originality signature, password protection)
[ChipType.NTAG5_LINK]: {
cloneable: false,
note: 'Originality signature prevents cloning',
},
[ChipType.NTAG5_BOOST]: {
cloneable: false,
note: 'Originality signature prevents cloning',
},
[ChipType.NTAG5_SWITCH]: {
cloneable: false,
note: 'Originality signature prevents cloning',
},
// NTAG DNA family - NOT cloneable (AES-128 crypto, originality signature)
[ChipType.NTAG413_DNA]: {
cloneable: false,
note: 'AES-128 authentication and SUN messaging prevent cloning',
},
[ChipType.NTAG424_DNA]: {
cloneable: false,
note: 'AES-128 authentication and SUN messaging prevent cloning',
},
[ChipType.NTAG424_DNA_TT]: {
cloneable: false,
note: 'AES-128 authentication and tamper detection prevent cloning',
},
[ChipType.NTAG_UNKNOWN]: {cloneable: true, note: 'May require verification'},
// MIFARE Classic - cloneable with keys
[ChipType.MIFARE_CLASSIC_1K]: {
cloneable: true,
note: 'Requires key knowledge; Android only for sector operations',
},
[ChipType.MIFARE_CLASSIC_4K]: {
cloneable: true,
note: 'Requires key knowledge; Android only for sector operations',
},
[ChipType.MIFARE_CLASSIC_MINI]: {
cloneable: true,
note: 'Requires key knowledge; Android only for sector operations',
},
// MIFARE DESFire - NOT cloneable (strong crypto)
[ChipType.DESFIRE_EV1]: {
cloneable: false,
note: 'Cryptographic protection prevents cloning',
},
[ChipType.DESFIRE_EV2]: {
cloneable: false,
note: 'Cryptographic protection prevents cloning',
},
[ChipType.DESFIRE_EV3]: {
cloneable: false,
note: 'Cryptographic protection prevents cloning',
},
[ChipType.DESFIRE_LIGHT]: {
cloneable: false,
note: 'Cryptographic protection prevents cloning',
},
[ChipType.DESFIRE_UNKNOWN]: {
cloneable: false,
note: 'Cryptographic protection prevents cloning',
},
// MIFARE Plus - NOT cloneable (AES crypto)
[ChipType.MIFARE_PLUS_S]: {
cloneable: false,
note: 'AES cryptographic protection prevents cloning',
},
[ChipType.MIFARE_PLUS_X]: {
cloneable: false,
note: 'AES cryptographic protection prevents cloning',
},
[ChipType.MIFARE_PLUS_SE]: {
cloneable: false,
note: 'AES cryptographic protection prevents cloning',
},
[ChipType.MIFARE_PLUS_EV1]: {
cloneable: false,
note: 'AES cryptographic protection prevents cloning',
},
[ChipType.MIFARE_PLUS]: {
cloneable: false,
note: 'AES cryptographic protection prevents cloning',
},
// MIFARE Ultralight - cloneable (basic variants)
[ChipType.ULTRALIGHT]: {cloneable: true},
[ChipType.ULTRALIGHT_C]: {
cloneable: true,
note: '3DES protection may require key knowledge',
},
[ChipType.ULTRALIGHT_EV1]: {cloneable: true},
[ChipType.ULTRALIGHT_NANO]: {cloneable: true},
[ChipType.ULTRALIGHT_AES]: {
cloneable: false,
note: 'AES authentication prevents cloning without keys',
},
// ICODE family - cloneable (basic variants), DNA has crypto
[ChipType.SLIX]: {cloneable: true},
[ChipType.SLIX2]: {cloneable: true},
[ChipType.SLIX_S]: {cloneable: true},
[ChipType.SLIX_L]: {cloneable: true},
[ChipType.ICODE_DNA]: {
cloneable: false,
note: 'Cryptographic authentication prevents cloning',
},
[ChipType.ISO15693_UNKNOWN]: {
cloneable: true,
note: 'May require verification',
},
// JavaCard - NOT cloneable
[ChipType.JCOP4]: {
cloneable: false,
note: 'Secure element prevents cloning',
},
[ChipType.JAVACARD_UNKNOWN]: {
cloneable: false,
note: 'Secure element prevents cloning',
},
// Unknown types
[ChipType.ISO14443A_UNKNOWN]: {
cloneable: false,
note: 'Unknown chip - cannot determine cloneability',
},
[ChipType.ISO14443B_UNKNOWN]: {
cloneable: false,
note: 'Unknown chip - cannot determine cloneability',
},
[ChipType.UNKNOWN]: {
cloneable: false,
note: 'Unknown chip - cannot determine cloneability',
},
};

View File

@@ -1,5 +1,6 @@
import type {NativeStackScreenProps} from '@react-navigation/native-stack'; import type {NativeStackScreenProps} from '@react-navigation/native-stack';
import type {NfcTechType} from './nfc'; import type {NfcTechType} from './nfc';
import type {Transponder} from './detection';
export type TagDataParam = { export type TagDataParam = {
uid: string; uid: string;
@@ -15,6 +16,7 @@ export type RootStackParamList = {
Scan: undefined; Scan: undefined;
Result: { Result: {
tagData?: TagDataParam; tagData?: TagDataParam;
transponder?: Transponder;
}; };
}; };

View File

@@ -2,7 +2,10 @@
"compilerOptions": { "compilerOptions": {
"strict": true, "strict": true,
"target": "ESNext", "target": "ESNext",
"lib": ["ES2020", "DOM"], "lib": [
"ES2020",
"DOM"
],
"module": "ESNext", "module": "ESNext",
"moduleResolution": "bundler", "moduleResolution": "bundler",
"jsx": "react-jsx", "jsx": "react-jsx",
@@ -13,8 +16,18 @@
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"resolveJsonModule": true, "resolveJsonModule": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"types": ["jest"] "types": [
"jest"
]
}, },
"include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"], "include": [
"exclude": ["node_modules", "android", "ios"] "**/*.ts",
"**/*.tsx"
],
"exclude": [
"node_modules",
"android",
"ios"
],
"extends": "expo/tsconfig.base"
} }