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:
@@ -5,13 +5,17 @@
|
||||
|
||||
import {useState, useCallback, useEffect, useRef} from 'react';
|
||||
import {nfcManager} from '../services/nfc';
|
||||
import {detectChip} from '../services/detection';
|
||||
import type {ScanState, RawTagData, ScanError, NFCStatus} from '../types/nfc';
|
||||
import type {Transponder} from '../types/detection';
|
||||
|
||||
export interface UseScanResult {
|
||||
/** Current scan state */
|
||||
state: ScanState;
|
||||
/** Scanned tag data (when state is 'success') */
|
||||
tag: RawTagData | null;
|
||||
/** Detected transponder info (when detection succeeds) */
|
||||
transponder: Transponder | null;
|
||||
/** Scan error (when state is 'error') */
|
||||
error: ScanError | null;
|
||||
/** NFC status (supported and enabled) */
|
||||
@@ -32,6 +36,7 @@ export interface UseScanResult {
|
||||
export function useScan(): UseScanResult {
|
||||
const [state, setState] = useState<ScanState>('idle');
|
||||
const [tag, setTag] = useState<RawTagData | null>(null);
|
||||
const [transponder, setTransponder] = useState<Transponder | null>(null);
|
||||
const [error, setError] = useState<ScanError | null>(null);
|
||||
const [nfcStatus, setNfcStatus] = useState<NFCStatus>({
|
||||
isSupported: false,
|
||||
@@ -76,6 +81,7 @@ export function useScan(): UseScanResult {
|
||||
scanInProgress.current = true;
|
||||
setState('scanning');
|
||||
setTag(null);
|
||||
setTransponder(null);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
@@ -107,8 +113,12 @@ export function useScan(): UseScanResult {
|
||||
return;
|
||||
}
|
||||
|
||||
// Perform the scan
|
||||
const result = await nfcManager.scanTag();
|
||||
// Perform scan with detection in one session
|
||||
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) {
|
||||
return;
|
||||
@@ -125,6 +135,9 @@ export function useScan(): UseScanResult {
|
||||
} else if (result.tag) {
|
||||
setState('success');
|
||||
setTag(result.tag);
|
||||
if (result.detection) {
|
||||
setTransponder(result.detection);
|
||||
}
|
||||
} else {
|
||||
setState('error');
|
||||
setError({
|
||||
@@ -160,6 +173,7 @@ export function useScan(): UseScanResult {
|
||||
const reset = useCallback(() => {
|
||||
setState('idle');
|
||||
setTag(null);
|
||||
setTransponder(null);
|
||||
setError(null);
|
||||
}, []);
|
||||
|
||||
@@ -176,6 +190,7 @@ export function useScan(): UseScanResult {
|
||||
return {
|
||||
state,
|
||||
tag,
|
||||
transponder,
|
||||
error,
|
||||
nfcStatus,
|
||||
startScan,
|
||||
|
||||
@@ -1,22 +1,124 @@
|
||||
import React from 'react';
|
||||
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 {DTColors} from '../theme';
|
||||
|
||||
export function ResultScreen({route, navigation}: ResultScreenProps) {
|
||||
const {tagData} = route.params;
|
||||
const {tagData, transponder} = route.params;
|
||||
|
||||
const handleConversionLink = () => {
|
||||
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 (
|
||||
<ScrollView style={styles.container}>
|
||||
<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}>
|
||||
<Text variant="labelLarge" style={styles.cardLabel}>
|
||||
TAG DETECTED
|
||||
RAW TAG DATA
|
||||
</Text>
|
||||
<Divider style={styles.divider} />
|
||||
|
||||
@@ -80,25 +182,24 @@ export function ResultScreen({route, navigation}: ResultScreenProps) {
|
||||
)}
|
||||
</Surface>
|
||||
|
||||
<Surface style={styles.matchCard} elevation={1}>
|
||||
<Text variant="labelLarge" style={styles.matchLabel}>
|
||||
COMPATIBLE IMPLANTS
|
||||
{/* No Detection Card (fallback) */}
|
||||
{!transponder && (
|
||||
<Surface style={styles.noDetectionCard} elevation={1}>
|
||||
<Text variant="labelLarge" style={styles.noDetectionLabel}>
|
||||
CHIP NOT IDENTIFIED
|
||||
</Text>
|
||||
<Divider style={styles.divider} />
|
||||
|
||||
<Text variant="bodyLarge" style={styles.placeholderText}>
|
||||
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.
|
||||
<Divider style={[styles.divider, {backgroundColor: DTColors.modeWarning}]} />
|
||||
<Text variant="bodyMedium" style={styles.noDetectionText}>
|
||||
Unable to identify this chip type. It may be unsupported or require advanced detection.
|
||||
</Text>
|
||||
</Surface>
|
||||
)}
|
||||
|
||||
{/* Conversion Service Card - only show if chip is NOT cloneable */}
|
||||
{(!transponder || !transponder.isCloneable) && (
|
||||
<Surface style={styles.conversionCard} elevation={1}>
|
||||
<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>
|
||||
<Button
|
||||
mode="outlined"
|
||||
@@ -108,7 +209,9 @@ export function ResultScreen({route, navigation}: ResultScreenProps) {
|
||||
CONVERSION SERVICE
|
||||
</Button>
|
||||
</Surface>
|
||||
)}
|
||||
|
||||
{/* Action Buttons */}
|
||||
<View style={styles.actions}>
|
||||
<Button
|
||||
mode="outlined"
|
||||
@@ -138,6 +241,98 @@ const styles = StyleSheet.create({
|
||||
content: {
|
||||
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: {
|
||||
backgroundColor: '#0a0a0a',
|
||||
borderRadius: 4,
|
||||
@@ -174,29 +369,25 @@ const styles = StyleSheet.create({
|
||||
opacity: 0.6,
|
||||
fontStyle: 'italic',
|
||||
},
|
||||
matchCard: {
|
||||
// No Detection Card
|
||||
noDetectionCard: {
|
||||
backgroundColor: '#0a0a0a',
|
||||
borderRadius: 4,
|
||||
borderWidth: 1,
|
||||
borderColor: DTColors.modeEmphasis,
|
||||
borderColor: DTColors.modeWarning,
|
||||
padding: 20,
|
||||
marginBottom: 20,
|
||||
},
|
||||
matchLabel: {
|
||||
color: DTColors.modeEmphasis,
|
||||
noDetectionLabel: {
|
||||
color: DTColors.modeWarning,
|
||||
letterSpacing: 2,
|
||||
marginBottom: 12,
|
||||
},
|
||||
placeholderText: {
|
||||
noDetectionText: {
|
||||
color: DTColors.light,
|
||||
opacity: 0.8,
|
||||
marginBottom: 8,
|
||||
},
|
||||
hintText: {
|
||||
color: DTColors.light,
|
||||
opacity: 0.5,
|
||||
fontStyle: 'italic',
|
||||
},
|
||||
// Conversion Card
|
||||
conversionCard: {
|
||||
backgroundColor: '#0a0a0a',
|
||||
borderRadius: 4,
|
||||
@@ -220,6 +411,7 @@ const styles = StyleSheet.create({
|
||||
color: DTColors.modeOther,
|
||||
letterSpacing: 1,
|
||||
},
|
||||
// Actions
|
||||
actions: {
|
||||
alignItems: 'center',
|
||||
gap: 16,
|
||||
|
||||
@@ -10,6 +10,7 @@ export function ScanScreen({navigation}: ScanScreenProps) {
|
||||
const {
|
||||
state,
|
||||
tag,
|
||||
transponder,
|
||||
error,
|
||||
nfcStatus,
|
||||
startScan,
|
||||
@@ -17,7 +18,7 @@ export function ScanScreen({navigation}: ScanScreenProps) {
|
||||
openSettings,
|
||||
} = useScan();
|
||||
|
||||
// Navigate to results when scan succeeds
|
||||
// Navigate to results when scan succeeds (detection happens in the hook)
|
||||
useEffect(() => {
|
||||
if (state === 'success' && tag) {
|
||||
navigation.replace('Result', {
|
||||
@@ -29,9 +30,10 @@ export function ScanScreen({navigation}: ScanScreenProps) {
|
||||
ats: tag.ats,
|
||||
historicalBytes: tag.historicalBytes,
|
||||
},
|
||||
transponder: transponder ?? undefined,
|
||||
});
|
||||
}
|
||||
}, [state, tag, navigation]);
|
||||
}, [state, tag, transponder, navigation]);
|
||||
|
||||
// Auto-start scan when screen loads
|
||||
useEffect(() => {
|
||||
@@ -185,7 +187,10 @@ export function ScanScreen({navigation}: ScanScreenProps) {
|
||||
style={styles.processingSpinner}
|
||||
/>
|
||||
<Text variant="headlineMedium" style={styles.processingText}>
|
||||
PROCESSING
|
||||
IDENTIFYING
|
||||
</Text>
|
||||
<Text variant="bodyMedium" style={styles.detectingHint}>
|
||||
Analyzing chip type...
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
@@ -297,6 +302,11 @@ const styles = StyleSheet.create({
|
||||
color: DTColors.modeEmphasis,
|
||||
letterSpacing: 4,
|
||||
},
|
||||
detectingHint: {
|
||||
color: DTColors.light,
|
||||
opacity: 0.7,
|
||||
marginTop: 12,
|
||||
},
|
||||
footer: {
|
||||
alignItems: 'center',
|
||||
paddingBottom: 20,
|
||||
|
||||
407
src/services/detection/desfire.ts
Normal file
407
src/services/detection/desfire.ts
Normal 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;
|
||||
}
|
||||
406
src/services/detection/detector.ts
Normal file
406
src/services/detection/detector.ts
Normal 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};
|
||||
}
|
||||
23
src/services/detection/index.ts
Normal file
23
src/services/detection/index.ts
Normal 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';
|
||||
748
src/services/detection/iso15693.ts
Normal file
748
src/services/detection/iso15693.ts
Normal 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)})`;
|
||||
}
|
||||
342
src/services/detection/javacard.ts
Normal file
342
src/services/detection/javacard.ts
Normal 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)}`;
|
||||
}
|
||||
387
src/services/detection/mifare.ts
Normal file
387
src/services/detection/mifare.ts
Normal 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;
|
||||
}
|
||||
305
src/services/detection/ntag.ts
Normal file
305
src/services/detection/ntag.ts
Normal 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(', ');
|
||||
}
|
||||
@@ -15,12 +15,32 @@ import type {
|
||||
|
||||
/**
|
||||
* Convert byte array to hex string
|
||||
* Handles number[], Uint8Array, string, or undefined
|
||||
*/
|
||||
function bytesToHex(bytes: number[] | undefined): string {
|
||||
if (!bytes || bytes.length === 0) {
|
||||
function bytesToHex(bytes: number[] | Uint8Array | string | undefined): string {
|
||||
if (!bytes) {
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -76,15 +119,39 @@ function parseAts(tag: TagEvent): {ats?: string; historicalBytes?: string} {
|
||||
* Convert TagEvent to RawTagData
|
||||
*/
|
||||
function tagEventToRawData(tag: TagEvent): RawTagData {
|
||||
const techTypes = (tag.techTypes || []) as NfcTechType[];
|
||||
const uid = tag.id ? bytesToHex(tag.id as unknown as number[]) : '';
|
||||
let techTypes = (tag.techTypes || []) as NfcTechType[];
|
||||
const uid = tag.id ? bytesToHex(tag.id as string | number[] | Uint8Array) : '';
|
||||
const sak = parseSak(tag);
|
||||
const atqa = parseAtqa(tag);
|
||||
const {ats, historicalBytes} = parseAts(tag);
|
||||
|
||||
const isoDep = (tag as any).isoDep;
|
||||
const iso7816 = (tag as any).iso7816;
|
||||
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 {
|
||||
uid,
|
||||
techTypes,
|
||||
@@ -191,8 +258,13 @@ class NFCManagerService {
|
||||
/**
|
||||
* Request NFC technology and scan for a tag
|
||||
* 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 {
|
||||
// Ensure initialized
|
||||
if (!this.initialized) {
|
||||
@@ -220,17 +292,21 @@ class NFCManagerService {
|
||||
|
||||
// Request technology based on platform
|
||||
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, {
|
||||
alertMessage: 'Hold your NFC tag near the top of your iPhone',
|
||||
});
|
||||
} else {
|
||||
// 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([
|
||||
NfcTech.NfcA,
|
||||
NfcTech.NfcB,
|
||||
NfcTech.NfcV,
|
||||
NfcTech.IsoDep,
|
||||
NfcTech.NfcA,
|
||||
NfcTech.NfcV,
|
||||
NfcTech.NfcB,
|
||||
NfcTech.MifareClassic,
|
||||
]);
|
||||
}
|
||||
@@ -238,6 +314,9 @@ class NFCManagerService {
|
||||
// Get the tag
|
||||
const tag = await NfcManager.getTag();
|
||||
if (!tag) {
|
||||
if (!keepAlive) {
|
||||
await this.cancelScan();
|
||||
}
|
||||
return {
|
||||
error: createScanError('UNKNOWN', 'No tag data received'),
|
||||
};
|
||||
@@ -247,7 +326,39 @@ class NFCManagerService {
|
||||
} catch (error) {
|
||||
return {error: categorizeError(error)};
|
||||
} 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();
|
||||
}
|
||||
}
|
||||
|
||||
341
src/services/nfc/commands.ts
Normal file
341
src/services/nfc/commands.ts
Normal 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
468
src/types/detection.ts
Normal 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',
|
||||
},
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
import type {NativeStackScreenProps} from '@react-navigation/native-stack';
|
||||
import type {NfcTechType} from './nfc';
|
||||
import type {Transponder} from './detection';
|
||||
|
||||
export type TagDataParam = {
|
||||
uid: string;
|
||||
@@ -15,6 +16,7 @@ export type RootStackParamList = {
|
||||
Scan: undefined;
|
||||
Result: {
|
||||
tagData?: TagDataParam;
|
||||
transponder?: Transponder;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"target": "ESNext",
|
||||
"lib": ["ES2020", "DOM"],
|
||||
"lib": [
|
||||
"ES2020",
|
||||
"DOM"
|
||||
],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"jsx": "react-jsx",
|
||||
@@ -13,8 +16,18 @@
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"resolveJsonModule": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"types": ["jest"]
|
||||
"types": [
|
||||
"jest"
|
||||
]
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"],
|
||||
"exclude": ["node_modules", "android", "ios"]
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"android",
|
||||
"ios"
|
||||
],
|
||||
"extends": "expo/tsconfig.base"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user