Add scan progress indicator, payment card detection, and production cleanup
Features: - Add scanning progress indicator showing current detection step - Detect payment cards (Visa, Mastercard, Amex, etc.) via PPSE/EMV AIDs - Show "Payment Device Detected" instead of "Implant Detected" for payment cards - Hide compatible implants section for payment devices, show conversion instead - Add Spark 1/2 implant detection via vivokey.co NDEF URL - Hide conversion card when actual implant is detected Technical: - Add DetectionProgressCallback for real-time detection status - Add payment network AIDs to commands.ts - Configure babel to strip console.log/warn in production builds - Add logger utility for development-only logging Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,19 @@
|
|||||||
module.exports = function (api) {
|
module.exports = function (api) {
|
||||||
api.cache(true);
|
api.cache(true);
|
||||||
|
|
||||||
|
const plugins = [];
|
||||||
|
|
||||||
|
// Remove console.log and console.warn in production builds
|
||||||
|
// Keep console.error for actual error reporting
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
plugins.push([
|
||||||
|
'transform-remove-console',
|
||||||
|
{exclude: ['error']},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
presets: ['babel-preset-expo'],
|
presets: ['babel-preset-expo'],
|
||||||
|
plugins,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
8
package-lock.json
generated
8
package-lock.json
generated
@@ -32,6 +32,7 @@
|
|||||||
"@types/react": "^19.2.0",
|
"@types/react": "^19.2.0",
|
||||||
"@types/react-native-vector-icons": "^6.4.18",
|
"@types/react-native-vector-icons": "^6.4.18",
|
||||||
"@types/react-test-renderer": "^19.1.0",
|
"@types/react-test-renderer": "^19.1.0",
|
||||||
|
"babel-plugin-transform-remove-console": "^6.9.4",
|
||||||
"eslint": "^8.19.0",
|
"eslint": "^8.19.0",
|
||||||
"jest": "^29.6.3",
|
"jest": "^29.6.3",
|
||||||
"prettier": "2.8.8",
|
"prettier": "2.8.8",
|
||||||
@@ -5311,6 +5312,13 @@
|
|||||||
"@babel/plugin-syntax-flow": "^7.12.1"
|
"@babel/plugin-syntax-flow": "^7.12.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/babel-plugin-transform-remove-console": {
|
||||||
|
"version": "6.9.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/babel-plugin-transform-remove-console/-/babel-plugin-transform-remove-console-6.9.4.tgz",
|
||||||
|
"integrity": "sha512-88blrUrMX3SPiGkT1GnvVY8E/7A+k6oj3MNvUtTIxJflFzXTw1bHkuJ/y039ouhFMp2prRn5cQGzokViYi1dsg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/babel-preset-current-node-syntax": {
|
"node_modules/babel-preset-current-node-syntax": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz",
|
||||||
|
|||||||
@@ -39,6 +39,7 @@
|
|||||||
"@types/react": "^19.2.0",
|
"@types/react": "^19.2.0",
|
||||||
"@types/react-native-vector-icons": "^6.4.18",
|
"@types/react-native-vector-icons": "^6.4.18",
|
||||||
"@types/react-test-renderer": "^19.1.0",
|
"@types/react-test-renderer": "^19.1.0",
|
||||||
|
"babel-plugin-transform-remove-console": "^6.9.4",
|
||||||
"eslint": "^8.19.0",
|
"eslint": "^8.19.0",
|
||||||
"jest": "^29.6.3",
|
"jest": "^29.6.3",
|
||||||
"prettier": "2.8.8",
|
"prettier": "2.8.8",
|
||||||
|
|||||||
@@ -9,6 +9,16 @@ import {detectChip} from '../services/detection';
|
|||||||
import type {ScanState, RawTagData, ScanError, NFCStatus} from '../types/nfc';
|
import type {ScanState, RawTagData, ScanError, NFCStatus} from '../types/nfc';
|
||||||
import type {Transponder} from '../types/detection';
|
import type {Transponder} from '../types/detection';
|
||||||
|
|
||||||
|
/** Progress update during scanning */
|
||||||
|
export interface ScanProgress {
|
||||||
|
/** Current step description */
|
||||||
|
step: string;
|
||||||
|
/** Step number (1-indexed) */
|
||||||
|
current: number;
|
||||||
|
/** Total steps (if known) */
|
||||||
|
total?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface UseScanResult {
|
export interface UseScanResult {
|
||||||
/** Current scan state */
|
/** Current scan state */
|
||||||
state: ScanState;
|
state: ScanState;
|
||||||
@@ -20,6 +30,8 @@ export interface UseScanResult {
|
|||||||
error: ScanError | null;
|
error: ScanError | null;
|
||||||
/** NFC status (supported and enabled) */
|
/** NFC status (supported and enabled) */
|
||||||
nfcStatus: NFCStatus;
|
nfcStatus: NFCStatus;
|
||||||
|
/** Current scan progress (during 'scanning' state) */
|
||||||
|
scanProgress: ScanProgress | null;
|
||||||
/** Start scanning for a tag */
|
/** Start scanning for a tag */
|
||||||
startScan: () => Promise<void>;
|
startScan: () => Promise<void>;
|
||||||
/** Cancel ongoing scan */
|
/** Cancel ongoing scan */
|
||||||
@@ -42,6 +54,7 @@ export function useScan(): UseScanResult {
|
|||||||
isSupported: false,
|
isSupported: false,
|
||||||
isEnabled: false,
|
isEnabled: false,
|
||||||
});
|
});
|
||||||
|
const [scanProgress, setScanProgress] = useState<ScanProgress | null>(null);
|
||||||
|
|
||||||
// Track if component is mounted to prevent state updates after unmount
|
// Track if component is mounted to prevent state updates after unmount
|
||||||
const isMounted = useRef(true);
|
const isMounted = useRef(true);
|
||||||
@@ -83,6 +96,7 @@ export function useScan(): UseScanResult {
|
|||||||
setTag(null);
|
setTag(null);
|
||||||
setTransponder(null);
|
setTransponder(null);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
setScanProgress({step: 'Waiting for tag...', current: 1});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check NFC status first
|
// Check NFC status first
|
||||||
@@ -115,8 +129,23 @@ export function useScan(): UseScanResult {
|
|||||||
|
|
||||||
// Perform scan with detection in one session
|
// Perform scan with detection in one session
|
||||||
const result = await nfcManager.scanWithDetection(async tagData => {
|
const result = await nfcManager.scanWithDetection(async tagData => {
|
||||||
|
// Update progress: tag detected
|
||||||
|
if (isMounted.current) {
|
||||||
|
setScanProgress({step: 'Tag detected, identifying chip...', current: 2});
|
||||||
|
}
|
||||||
|
|
||||||
// Run chip detection while NFC session is still active
|
// Run chip detection while NFC session is still active
|
||||||
const detectionResult = await detectChip(tagData);
|
const detectionResult = await detectChip(tagData, step => {
|
||||||
|
// Progress callback from detection
|
||||||
|
if (isMounted.current) {
|
||||||
|
setScanProgress({step, current: 3});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isMounted.current) {
|
||||||
|
setScanProgress({step: 'Detection complete', current: 4});
|
||||||
|
}
|
||||||
|
|
||||||
return detectionResult.success ? detectionResult.transponder : null;
|
return detectionResult.success ? detectionResult.transponder : null;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -175,6 +204,7 @@ export function useScan(): UseScanResult {
|
|||||||
setTag(null);
|
setTag(null);
|
||||||
setTransponder(null);
|
setTransponder(null);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
setScanProgress(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Open device NFC settings
|
// Open device NFC settings
|
||||||
@@ -193,6 +223,7 @@ export function useScan(): UseScanResult {
|
|||||||
transponder,
|
transponder,
|
||||||
error,
|
error,
|
||||||
nfcStatus,
|
nfcStatus,
|
||||||
|
scanProgress,
|
||||||
startScan,
|
startScan,
|
||||||
cancelScan,
|
cancelScan,
|
||||||
reset,
|
reset,
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import React, {useMemo} from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
import {StyleSheet, View, ScrollView, Linking} from 'react-native';
|
import { StyleSheet, View, ScrollView, Linking, TouchableOpacity } from 'react-native';
|
||||||
import { Button, Text, Surface, Divider, Chip } from 'react-native-paper';
|
import { Button, Text, Surface, Divider, Chip } from 'react-native-paper';
|
||||||
import type { ResultScreenProps } from '../types/navigation';
|
import type { ResultScreenProps } from '../types/navigation';
|
||||||
import { DTColors } from '../theme';
|
import { DTColors } from '../theme';
|
||||||
import { matchChipToProducts, getMatchSummary, getDesfireEvMismatchWarning } from '../services/matching';
|
import { matchChipToProducts, getMatchSummary, getDesfireEvMismatchWarning } from '../services/matching';
|
||||||
import { Product } from '../types/products';
|
import { Product } from '../types/products';
|
||||||
|
import { getChipInfo, getChipFamilyInfo, getSecurityLevelDescription } from '../data/chipInfo';
|
||||||
|
import { getChipFamily } from '../types/detection';
|
||||||
|
|
||||||
export function ResultScreen({ route, navigation }: ResultScreenProps) {
|
export function ResultScreen({ route, navigation }: ResultScreenProps) {
|
||||||
const { tagData, transponder } = route.params;
|
const { tagData, transponder } = route.params;
|
||||||
|
const [showChipInfo, setShowChipInfo] = useState(false);
|
||||||
|
|
||||||
// Match chip to products
|
// Match chip to products
|
||||||
const matchResult = useMemo(() => {
|
const matchResult = useMemo(() => {
|
||||||
@@ -15,6 +18,17 @@ export function ResultScreen({route, navigation}: ResultScreenProps) {
|
|||||||
return matchChipToProducts(transponder.type);
|
return matchChipToProducts(transponder.type);
|
||||||
}, [transponder]);
|
}, [transponder]);
|
||||||
|
|
||||||
|
// Get educational chip info
|
||||||
|
const chipInfo = useMemo(() => {
|
||||||
|
if (!transponder) return null;
|
||||||
|
return getChipInfo(transponder.type);
|
||||||
|
}, [transponder]);
|
||||||
|
|
||||||
|
const chipFamilyInfo = useMemo(() => {
|
||||||
|
if (!transponder) return null;
|
||||||
|
return getChipFamilyInfo(getChipFamily(transponder.type));
|
||||||
|
}, [transponder]);
|
||||||
|
|
||||||
const handleConversionLink = () => {
|
const handleConversionLink = () => {
|
||||||
Linking.openURL(matchResult?.conversionUrl || 'https://dngr.us/conversion');
|
Linking.openURL(matchResult?.conversionUrl || 'https://dngr.us/conversion');
|
||||||
};
|
};
|
||||||
@@ -54,6 +68,29 @@ export function ResultScreen({route, navigation}: ResultScreenProps) {
|
|||||||
{transponder.chipName}
|
{transponder.chipName}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
|
{/* Show implant name if detected from memory, or payment device */}
|
||||||
|
{transponder.implantName && (
|
||||||
|
transponder.implantName.includes('Payment Card') ? (
|
||||||
|
<View style={styles.paymentDeviceRow}>
|
||||||
|
<Text variant="labelMedium" style={styles.paymentDeviceLabel}>
|
||||||
|
PAYMENT DEVICE DETECTED
|
||||||
|
</Text>
|
||||||
|
<Text variant="titleLarge" style={styles.paymentDeviceValue}>
|
||||||
|
{transponder.implantName.replace(' Payment Card', '')}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<View style={styles.implantNameRow}>
|
||||||
|
<Text variant="labelMedium" style={styles.implantNameLabel}>
|
||||||
|
IMPLANT DETECTED
|
||||||
|
</Text>
|
||||||
|
<Text variant="titleLarge" style={styles.implantNameValue}>
|
||||||
|
{transponder.implantName}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
|
||||||
<View style={styles.chipMeta}>
|
<View style={styles.chipMeta}>
|
||||||
<Chip
|
<Chip
|
||||||
style={[styles.familyChip, { borderColor: DTColors.modeNormal }]}
|
style={[styles.familyChip, { borderColor: DTColors.modeNormal }]}
|
||||||
@@ -99,8 +136,109 @@ export function ResultScreen({route, navigation}: ResultScreenProps) {
|
|||||||
</Surface>
|
</Surface>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Product Matches Card */}
|
{/* Educational Chip Info Card */}
|
||||||
{transponder && matchResult && (matchResult.exactMatches.length > 0 || matchResult.cloneTargets.length > 0) && (
|
{transponder && (chipInfo || chipFamilyInfo) && (
|
||||||
|
<Surface style={styles.chipInfoCard} elevation={1}>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => setShowChipInfo(!showChipInfo)}
|
||||||
|
style={styles.chipInfoHeader}
|
||||||
|
activeOpacity={0.7}>
|
||||||
|
<Text variant="labelLarge" style={styles.chipInfoLabel}>
|
||||||
|
ABOUT THIS CHIP
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.expandIndicator}>
|
||||||
|
{showChipInfo ? '▼' : '▶'}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
{showChipInfo && (
|
||||||
|
<>
|
||||||
|
<Divider style={[styles.divider, { backgroundColor: DTColors.modeOther }]} />
|
||||||
|
|
||||||
|
{/* Family description */}
|
||||||
|
{chipFamilyInfo && (
|
||||||
|
<Text variant="bodyMedium" style={styles.chipFamilyDescription}>
|
||||||
|
{chipFamilyInfo}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Detailed chip info */}
|
||||||
|
{chipInfo && (
|
||||||
|
<>
|
||||||
|
<Text variant="bodyMedium" style={styles.chipDescription}>
|
||||||
|
{chipInfo.description}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{chipInfo.memoryNote && (
|
||||||
|
<View style={styles.chipInfoRow}>
|
||||||
|
<Text variant="bodySmall" style={styles.chipInfoRowLabel}>
|
||||||
|
Memory:
|
||||||
|
</Text>
|
||||||
|
<Text variant="bodySmall" style={styles.chipInfoRowValue}>
|
||||||
|
{chipInfo.memoryNote}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<View style={styles.chipInfoRow}>
|
||||||
|
<Text variant="bodySmall" style={styles.chipInfoRowLabel}>
|
||||||
|
Security:
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
variant="bodySmall"
|
||||||
|
style={[
|
||||||
|
styles.chipInfoRowValue,
|
||||||
|
{
|
||||||
|
color:
|
||||||
|
chipInfo.securityLevel === 'high'
|
||||||
|
? DTColors.modeSuccess
|
||||||
|
: chipInfo.securityLevel === 'medium'
|
||||||
|
? DTColors.modeEmphasis
|
||||||
|
: DTColors.modeWarning,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
{chipInfo.securityLevel.toUpperCase()}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Text variant="bodySmall" style={styles.securityNote}>
|
||||||
|
{getSecurityLevelDescription(chipInfo.securityLevel)}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{chipInfo.commonUses.length > 0 && (
|
||||||
|
<>
|
||||||
|
<Text variant="labelSmall" style={styles.chipInfoSectionLabel}>
|
||||||
|
COMMON USES
|
||||||
|
</Text>
|
||||||
|
{chipInfo.commonUses.map((use, idx) => (
|
||||||
|
<Text key={idx} variant="bodySmall" style={styles.chipInfoListItem}>
|
||||||
|
• {use}
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{chipInfo.capabilities.length > 0 && (
|
||||||
|
<>
|
||||||
|
<Text variant="labelSmall" style={styles.chipInfoSectionLabel}>
|
||||||
|
CAPABILITIES
|
||||||
|
</Text>
|
||||||
|
{chipInfo.capabilities.map((cap, idx) => (
|
||||||
|
<Text key={idx} variant="bodySmall" style={styles.chipInfoListItem}>
|
||||||
|
• {cap}
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Surface>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Product Matches Card - hide for payment devices */}
|
||||||
|
{transponder && matchResult && (matchResult.exactMatches.length > 0 || matchResult.cloneTargets.length > 0) && !transponder.implantName?.includes('Payment Card') && (
|
||||||
<Surface style={styles.productsCard} elevation={1}>
|
<Surface style={styles.productsCard} elevation={1}>
|
||||||
<Text variant="labelLarge" style={styles.productsLabel}>
|
<Text variant="labelLarge" style={styles.productsLabel}>
|
||||||
COMPATIBLE IMPLANTS
|
COMPATIBLE IMPLANTS
|
||||||
@@ -300,11 +438,14 @@ export function ResultScreen({route, navigation}: ResultScreenProps) {
|
|||||||
</Surface>
|
</Surface>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Conversion Service Card - show when recommended */}
|
{/* Conversion Service Card - show for payment devices, or when recommended and not a real implant */}
|
||||||
{(matchResult?.conversionRecommended || !transponder) && (
|
{(transponder?.implantName?.includes('Payment Card') ||
|
||||||
|
((matchResult?.conversionRecommended || !transponder) && !transponder?.implantName)) && (
|
||||||
<Surface style={styles.conversionCard} elevation={1}>
|
<Surface style={styles.conversionCard} elevation={1}>
|
||||||
<Text variant="bodyMedium" style={styles.conversionText}>
|
<Text variant="bodyMedium" style={styles.conversionText}>
|
||||||
{!transponder
|
{transponder?.implantName?.includes('Payment Card')
|
||||||
|
? "Payment cards can't be converted to implants due to security restrictions. Our conversion service can help you find an implant that works with your use case."
|
||||||
|
: !transponder
|
||||||
? 'Unknown chip? Our conversion service can help.'
|
? 'Unknown chip? Our conversion service can help.'
|
||||||
: matchResult?.isCloneable === false
|
: matchResult?.isCloneable === false
|
||||||
? "This chip uses cryptographic protection and can't be cloned. Our conversion service can help find alternatives."
|
? "This chip uses cryptographic protection and can't be cloned. Our conversion service can help find alternatives."
|
||||||
@@ -536,7 +677,7 @@ const styles = StyleSheet.create({
|
|||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: DTColors.modeNormal,
|
borderColor: DTColors.modeNormal,
|
||||||
height: 24,
|
height: 35,
|
||||||
},
|
},
|
||||||
formFactorChipText: {
|
formFactorChipText: {
|
||||||
color: DTColors.modeNormal,
|
color: DTColors.modeNormal,
|
||||||
@@ -645,4 +786,109 @@ const styles = StyleSheet.create({
|
|||||||
color: DTColors.light,
|
color: DTColors.light,
|
||||||
opacity: 0.6,
|
opacity: 0.6,
|
||||||
},
|
},
|
||||||
|
// Chip Info Card
|
||||||
|
chipInfoCard: {
|
||||||
|
backgroundColor: '#0a0a0a',
|
||||||
|
borderRadius: 4,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: DTColors.modeOther,
|
||||||
|
padding: 20,
|
||||||
|
marginBottom: 20,
|
||||||
|
},
|
||||||
|
chipInfoHeader: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
chipInfoLabel: {
|
||||||
|
color: DTColors.modeOther,
|
||||||
|
letterSpacing: 2,
|
||||||
|
},
|
||||||
|
expandIndicator: {
|
||||||
|
color: DTColors.modeOther,
|
||||||
|
fontSize: 12,
|
||||||
|
},
|
||||||
|
chipFamilyDescription: {
|
||||||
|
color: DTColors.light,
|
||||||
|
opacity: 0.8,
|
||||||
|
marginBottom: 12,
|
||||||
|
lineHeight: 22,
|
||||||
|
},
|
||||||
|
chipDescription: {
|
||||||
|
color: DTColors.light,
|
||||||
|
marginBottom: 16,
|
||||||
|
lineHeight: 22,
|
||||||
|
},
|
||||||
|
chipInfoRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
chipInfoRowLabel: {
|
||||||
|
color: DTColors.light,
|
||||||
|
opacity: 0.6,
|
||||||
|
marginRight: 8,
|
||||||
|
},
|
||||||
|
chipInfoRowValue: {
|
||||||
|
color: DTColors.light,
|
||||||
|
},
|
||||||
|
securityNote: {
|
||||||
|
color: DTColors.light,
|
||||||
|
opacity: 0.5,
|
||||||
|
fontStyle: 'italic',
|
||||||
|
marginTop: 4,
|
||||||
|
marginBottom: 16,
|
||||||
|
},
|
||||||
|
chipInfoSectionLabel: {
|
||||||
|
color: DTColors.modeOther,
|
||||||
|
letterSpacing: 1,
|
||||||
|
marginTop: 8,
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
chipInfoListItem: {
|
||||||
|
color: DTColors.light,
|
||||||
|
opacity: 0.8,
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
// Implant name styles
|
||||||
|
implantNameRow: {
|
||||||
|
backgroundColor: 'rgba(0, 255, 0, 0.1)',
|
||||||
|
borderRadius: 4,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: DTColors.modeSuccess,
|
||||||
|
padding: 12,
|
||||||
|
marginTop: 12,
|
||||||
|
marginBottom: 8,
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
implantNameLabel: {
|
||||||
|
color: DTColors.modeSuccess,
|
||||||
|
letterSpacing: 2,
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
implantNameValue: {
|
||||||
|
color: DTColors.modeSuccess,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
letterSpacing: 1,
|
||||||
|
},
|
||||||
|
// Payment device styles
|
||||||
|
paymentDeviceRow: {
|
||||||
|
backgroundColor: 'rgba(255, 255, 0, 0.1)',
|
||||||
|
borderRadius: 4,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: DTColors.modeEmphasis,
|
||||||
|
padding: 12,
|
||||||
|
marginTop: 12,
|
||||||
|
marginBottom: 8,
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
paymentDeviceLabel: {
|
||||||
|
color: DTColors.modeEmphasis,
|
||||||
|
letterSpacing: 2,
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
paymentDeviceValue: {
|
||||||
|
color: DTColors.modeEmphasis,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
letterSpacing: 1,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import React, {useCallback, useEffect} from 'react';
|
import React, {useCallback, useEffect} from 'react';
|
||||||
import {StyleSheet, View, Platform} from 'react-native';
|
import {StyleSheet, View, Platform} from 'react-native';
|
||||||
import {Button, Text, ActivityIndicator} from 'react-native-paper';
|
import {Button, Text} from 'react-native-paper';
|
||||||
import type {ScanScreenProps} from '../types/navigation';
|
import type {ScanScreenProps} from '../types/navigation';
|
||||||
import {DTColors} from '../theme';
|
import {DTColors} from '../theme';
|
||||||
import {useScan} from '../hooks';
|
import {useScan} from '../hooks';
|
||||||
import {getScanInstructions} from '../services/nfc';
|
import {getScanInstructions} from '../services/nfc';
|
||||||
|
import {ScanAnimation} from '../components';
|
||||||
|
|
||||||
export function ScanScreen({navigation}: ScanScreenProps) {
|
export function ScanScreen({navigation}: ScanScreenProps) {
|
||||||
const {
|
const {
|
||||||
@@ -13,6 +14,7 @@ export function ScanScreen({navigation}: ScanScreenProps) {
|
|||||||
transponder,
|
transponder,
|
||||||
error,
|
error,
|
||||||
nfcStatus,
|
nfcStatus,
|
||||||
|
scanProgress,
|
||||||
startScan,
|
startScan,
|
||||||
cancelScan,
|
cancelScan,
|
||||||
openSettings,
|
openSettings,
|
||||||
@@ -56,16 +58,46 @@ export function ScanScreen({navigation}: ScanScreenProps) {
|
|||||||
startScan();
|
startScan();
|
||||||
}, [startScan]);
|
}, [startScan]);
|
||||||
|
|
||||||
|
// Get contextual error hints based on error type
|
||||||
|
const getErrorHint = () => {
|
||||||
|
if (!error?.type) return null;
|
||||||
|
|
||||||
|
switch (error.type) {
|
||||||
|
case 'TAG_LOST':
|
||||||
|
return 'Hold the tag steady against the back of your phone until scanning completes.';
|
||||||
|
case 'TIMEOUT':
|
||||||
|
return 'The tag took too long to respond. Try repositioning it closer to the NFC reader.';
|
||||||
|
case 'SCAN_CANCELLED':
|
||||||
|
return null; // User cancelled, no hint needed
|
||||||
|
case 'NFC_NOT_ENABLED':
|
||||||
|
return Platform.OS === 'ios'
|
||||||
|
? 'Go to Settings > NFC to enable NFC scanning.'
|
||||||
|
: 'Enable NFC in your device settings.';
|
||||||
|
case 'PERMISSION_DENIED':
|
||||||
|
return 'NFC permission was denied. Please grant NFC access in your device settings.';
|
||||||
|
case 'UNKNOWN':
|
||||||
|
return 'An unexpected error occurred. Make sure the tag is positioned correctly and try again.';
|
||||||
|
default:
|
||||||
|
return 'Make sure the tag is positioned correctly and try again.';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Render NFC not supported state
|
// Render NFC not supported state
|
||||||
if (!nfcStatus.isSupported && state !== 'scanning') {
|
if (!nfcStatus.isSupported && state !== 'scanning') {
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<View style={styles.content}>
|
<View style={styles.content}>
|
||||||
|
<View style={styles.errorIcon}>
|
||||||
|
<Text style={styles.errorIconText}>✕</Text>
|
||||||
|
</View>
|
||||||
<Text variant="headlineMedium" style={styles.errorText}>
|
<Text variant="headlineMedium" style={styles.errorText}>
|
||||||
NFC NOT SUPPORTED
|
NFC NOT SUPPORTED
|
||||||
</Text>
|
</Text>
|
||||||
<Text variant="bodyLarge" style={styles.errorMessage}>
|
<Text variant="bodyLarge" style={styles.errorMessage}>
|
||||||
This device does not support NFC scanning.
|
This device does not have NFC hardware.
|
||||||
|
</Text>
|
||||||
|
<Text variant="bodyMedium" style={styles.hintText}>
|
||||||
|
NFC scanning requires a device with NFC capability. Most modern smartphones have NFC, but it may need to be enabled in settings.
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.footer}>
|
<View style={styles.footer}>
|
||||||
@@ -85,18 +117,26 @@ export function ScanScreen({navigation}: ScanScreenProps) {
|
|||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<View style={styles.content}>
|
<View style={styles.content}>
|
||||||
|
<View style={styles.warningIcon}>
|
||||||
|
<Text style={styles.warningIconText}>⚡</Text>
|
||||||
|
</View>
|
||||||
<Text variant="headlineMedium" style={styles.warningText}>
|
<Text variant="headlineMedium" style={styles.warningText}>
|
||||||
NFC DISABLED
|
NFC DISABLED
|
||||||
</Text>
|
</Text>
|
||||||
<Text variant="bodyLarge" style={styles.errorMessage}>
|
<Text variant="bodyLarge" style={styles.errorMessage}>
|
||||||
Please enable NFC in your device settings to scan tags.
|
NFC is turned off on this device.
|
||||||
|
</Text>
|
||||||
|
<Text variant="bodyMedium" style={styles.hintText}>
|
||||||
|
{Platform.OS === 'ios'
|
||||||
|
? 'Go to Settings and enable NFC scanning to continue.'
|
||||||
|
: 'Enable NFC in your device settings to scan tags.'}
|
||||||
</Text>
|
</Text>
|
||||||
{Platform.OS === 'android' && (
|
{Platform.OS === 'android' && (
|
||||||
<Button
|
<Button
|
||||||
mode="outlined"
|
mode="outlined"
|
||||||
onPress={openSettings}
|
onPress={openSettings}
|
||||||
style={styles.settingsButton}
|
style={styles.settingsButton}
|
||||||
labelStyle={styles.buttonLabel}>
|
labelStyle={styles.settingsButtonLabel}>
|
||||||
OPEN SETTINGS
|
OPEN SETTINGS
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
@@ -124,34 +164,41 @@ export function ScanScreen({navigation}: ScanScreenProps) {
|
|||||||
<View style={styles.content}>
|
<View style={styles.content}>
|
||||||
{state === 'scanning' && (
|
{state === 'scanning' && (
|
||||||
<>
|
<>
|
||||||
<View style={styles.scanIndicator}>
|
<ScanAnimation isActive={true} size={180} />
|
||||||
<ActivityIndicator
|
|
||||||
size="large"
|
|
||||||
color={DTColors.modeNormal}
|
|
||||||
style={styles.spinner}
|
|
||||||
/>
|
|
||||||
<View style={styles.pulseRing} />
|
|
||||||
</View>
|
|
||||||
<Text variant="headlineMedium" style={styles.scanningText}>
|
<Text variant="headlineMedium" style={styles.scanningText}>
|
||||||
SCANNING
|
SCANNING
|
||||||
</Text>
|
</Text>
|
||||||
<Text variant="bodyLarge" style={styles.instructionText}>
|
<Text variant="bodyLarge" style={styles.instructionText}>
|
||||||
{getScanInstructions()}
|
{getScanInstructions()}
|
||||||
</Text>
|
</Text>
|
||||||
|
{scanProgress && scanProgress.current > 1 && (
|
||||||
|
<Text variant="bodySmall" style={styles.progressText}>
|
||||||
|
{scanProgress.step}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{state === 'error' && (
|
{state === 'error' && (
|
||||||
<>
|
<>
|
||||||
|
<View style={styles.errorIcon}>
|
||||||
|
<Text style={styles.errorIconText}>!</Text>
|
||||||
|
</View>
|
||||||
<Text variant="headlineMedium" style={styles.errorText}>
|
<Text variant="headlineMedium" style={styles.errorText}>
|
||||||
SCAN FAILED
|
{error?.type === 'TAG_LOST'
|
||||||
|
? 'TAG LOST'
|
||||||
|
: error?.type === 'TIMEOUT'
|
||||||
|
? 'TIMEOUT'
|
||||||
|
: error?.type === 'PERMISSION_DENIED'
|
||||||
|
? 'PERMISSION DENIED'
|
||||||
|
: 'SCAN FAILED'}
|
||||||
</Text>
|
</Text>
|
||||||
<Text variant="bodyLarge" style={styles.errorMessage}>
|
<Text variant="bodyLarge" style={styles.errorMessage}>
|
||||||
{error?.message || 'Unable to read NFC tag'}
|
{error?.message || 'Unable to read NFC tag'}
|
||||||
</Text>
|
</Text>
|
||||||
{error?.type === 'TAG_LOST' && (
|
{getErrorHint() && (
|
||||||
<Text variant="bodyMedium" style={styles.hintText}>
|
<Text variant="bodyMedium" style={styles.hintText}>
|
||||||
Hold the tag steady until the scan completes
|
{getErrorHint()}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
@@ -181,10 +228,10 @@ export function ScanScreen({navigation}: ScanScreenProps) {
|
|||||||
|
|
||||||
{state === 'processing' && (
|
{state === 'processing' && (
|
||||||
<>
|
<>
|
||||||
<ActivityIndicator
|
<ScanAnimation
|
||||||
size="large"
|
isActive={true}
|
||||||
|
size={180}
|
||||||
color={DTColors.modeEmphasis}
|
color={DTColors.modeEmphasis}
|
||||||
style={styles.processingSpinner}
|
|
||||||
/>
|
/>
|
||||||
<Text variant="headlineMedium" style={styles.processingText}>
|
<Text variant="headlineMedium" style={styles.processingText}>
|
||||||
IDENTIFYING
|
IDENTIFYING
|
||||||
@@ -219,27 +266,10 @@ const styles = StyleSheet.create({
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
scanIndicator: {
|
|
||||||
width: 150,
|
|
||||||
height: 150,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginBottom: 40,
|
|
||||||
},
|
|
||||||
spinner: {
|
|
||||||
position: 'absolute',
|
|
||||||
},
|
|
||||||
pulseRing: {
|
|
||||||
width: 120,
|
|
||||||
height: 120,
|
|
||||||
borderRadius: 60,
|
|
||||||
borderWidth: 2,
|
|
||||||
borderColor: DTColors.modeNormal,
|
|
||||||
opacity: 0.3,
|
|
||||||
},
|
|
||||||
scanningText: {
|
scanningText: {
|
||||||
color: DTColors.modeNormal,
|
color: DTColors.modeNormal,
|
||||||
letterSpacing: 4,
|
letterSpacing: 4,
|
||||||
|
marginTop: 32,
|
||||||
marginBottom: 16,
|
marginBottom: 16,
|
||||||
},
|
},
|
||||||
instructionText: {
|
instructionText: {
|
||||||
@@ -248,11 +278,47 @@ const styles = StyleSheet.create({
|
|||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
paddingHorizontal: 20,
|
paddingHorizontal: 20,
|
||||||
},
|
},
|
||||||
|
progressText: {
|
||||||
|
color: DTColors.modeNormal,
|
||||||
|
opacity: 0.7,
|
||||||
|
marginTop: 16,
|
||||||
|
textAlign: 'center',
|
||||||
|
fontStyle: 'italic',
|
||||||
|
},
|
||||||
|
errorIcon: {
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
borderRadius: 40,
|
||||||
|
borderWidth: 3,
|
||||||
|
borderColor: DTColors.modeWarning,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: 24,
|
||||||
|
},
|
||||||
|
errorIconText: {
|
||||||
|
color: DTColors.modeWarning,
|
||||||
|
fontSize: 48,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
errorText: {
|
errorText: {
|
||||||
color: DTColors.modeWarning,
|
color: DTColors.modeWarning,
|
||||||
letterSpacing: 2,
|
letterSpacing: 2,
|
||||||
marginBottom: 16,
|
marginBottom: 16,
|
||||||
},
|
},
|
||||||
|
warningIcon: {
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
borderRadius: 40,
|
||||||
|
borderWidth: 3,
|
||||||
|
borderColor: DTColors.modeEmphasis,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: 24,
|
||||||
|
},
|
||||||
|
warningIconText: {
|
||||||
|
color: DTColors.modeEmphasis,
|
||||||
|
fontSize: 36,
|
||||||
|
},
|
||||||
warningText: {
|
warningText: {
|
||||||
color: DTColors.modeEmphasis,
|
color: DTColors.modeEmphasis,
|
||||||
letterSpacing: 2,
|
letterSpacing: 2,
|
||||||
@@ -282,6 +348,10 @@ const styles = StyleSheet.create({
|
|||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
marginTop: 16,
|
marginTop: 16,
|
||||||
},
|
},
|
||||||
|
settingsButtonLabel: {
|
||||||
|
color: DTColors.modeEmphasis,
|
||||||
|
letterSpacing: 2,
|
||||||
|
},
|
||||||
readyText: {
|
readyText: {
|
||||||
color: DTColors.modeEmphasis,
|
color: DTColors.modeEmphasis,
|
||||||
letterSpacing: 2,
|
letterSpacing: 2,
|
||||||
@@ -295,12 +365,10 @@ const styles = StyleSheet.create({
|
|||||||
color: DTColors.modeNormal,
|
color: DTColors.modeNormal,
|
||||||
letterSpacing: 2,
|
letterSpacing: 2,
|
||||||
},
|
},
|
||||||
processingSpinner: {
|
|
||||||
marginBottom: 24,
|
|
||||||
},
|
|
||||||
processingText: {
|
processingText: {
|
||||||
color: DTColors.modeEmphasis,
|
color: DTColors.modeEmphasis,
|
||||||
letterSpacing: 4,
|
letterSpacing: 4,
|
||||||
|
marginTop: 32,
|
||||||
},
|
},
|
||||||
detectingHint: {
|
detectingHint: {
|
||||||
color: DTColors.light,
|
color: DTColors.light,
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import {
|
|||||||
DESFIRE_GET_VERSION_CONTINUE,
|
DESFIRE_GET_VERSION_CONTINUE,
|
||||||
sendIsoDepCommand,
|
sendIsoDepCommand,
|
||||||
parseApduResponse,
|
parseApduResponse,
|
||||||
|
selectAid,
|
||||||
|
KNOWN_AIDS,
|
||||||
} from '../nfc/commands';
|
} from '../nfc/commands';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -405,3 +407,110 @@ export function formatDesfireVersionInfo(info: DesfireVersionInfo): string {
|
|||||||
: '';
|
: '';
|
||||||
return version + software;
|
return version + software;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result of Spark 2 detection
|
||||||
|
*/
|
||||||
|
export interface Spark2DetectionResult {
|
||||||
|
found: boolean;
|
||||||
|
name?: string;
|
||||||
|
url?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect Spark 2 implant by reading NDEF from NTAG 424 DNA
|
||||||
|
* Spark 2 implants have a URI record pointing to vivokey.co/$code
|
||||||
|
*
|
||||||
|
* Uses Type 4 Tag NDEF reading:
|
||||||
|
* 1. SELECT NDEF application
|
||||||
|
* 2. SELECT NDEF file (E104)
|
||||||
|
* 3. READ BINARY to get NDEF message
|
||||||
|
*/
|
||||||
|
export async function detectSpark2Implant(): Promise<Spark2DetectionResult> {
|
||||||
|
try {
|
||||||
|
console.log('[DESFire] Checking for Spark 2 implant NDEF...');
|
||||||
|
|
||||||
|
// Step 1: Select NDEF application
|
||||||
|
const selectNdefApp = await sendIsoDepCommand(selectAid(KNOWN_AIDS.ndefTag));
|
||||||
|
const selectAppResponse = parseApduResponse(selectNdefApp);
|
||||||
|
|
||||||
|
if (!selectAppResponse.isSuccess) {
|
||||||
|
console.log('[DESFire] NDEF app selection failed');
|
||||||
|
return {found: false};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Select NDEF file (file ID E104 for standard NDEF)
|
||||||
|
// SELECT command: 00 A4 00 0C 02 E104
|
||||||
|
const selectFileCmd = [0x00, 0xa4, 0x00, 0x0c, 0x02, 0xe1, 0x04];
|
||||||
|
const selectFileResponse = await sendIsoDepCommand(selectFileCmd);
|
||||||
|
const selectFileParsed = parseApduResponse(selectFileResponse);
|
||||||
|
|
||||||
|
if (!selectFileParsed.isSuccess) {
|
||||||
|
console.log('[DESFire] NDEF file selection failed');
|
||||||
|
return {found: false};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: Read NDEF length (first 2 bytes)
|
||||||
|
// READ BINARY: 00 B0 00 00 02
|
||||||
|
const readLengthCmd = [0x00, 0xb0, 0x00, 0x00, 0x02];
|
||||||
|
const readLengthResponse = await sendIsoDepCommand(readLengthCmd);
|
||||||
|
const lengthParsed = parseApduResponse(readLengthResponse);
|
||||||
|
|
||||||
|
if (!lengthParsed.isSuccess || lengthParsed.data.length < 2) {
|
||||||
|
console.log('[DESFire] NDEF length read failed');
|
||||||
|
return {found: false};
|
||||||
|
}
|
||||||
|
|
||||||
|
const ndefLength = (lengthParsed.data[0] << 8) | lengthParsed.data[1];
|
||||||
|
console.log('[DESFire] NDEF length:', ndefLength);
|
||||||
|
|
||||||
|
if (ndefLength === 0 || ndefLength > 500) {
|
||||||
|
console.log('[DESFire] Invalid NDEF length');
|
||||||
|
return {found: false};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 4: Read NDEF message (starting after length bytes)
|
||||||
|
// READ BINARY: 00 B0 00 02 <length>
|
||||||
|
const readDataCmd = [0x00, 0xb0, 0x00, 0x02, Math.min(ndefLength, 128)];
|
||||||
|
const readDataResponse = await sendIsoDepCommand(readDataCmd);
|
||||||
|
const dataParsed = parseApduResponse(readDataResponse);
|
||||||
|
|
||||||
|
if (!dataParsed.isSuccess || dataParsed.data.length === 0) {
|
||||||
|
console.log('[DESFire] NDEF data read failed');
|
||||||
|
return {found: false};
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
'[DESFire] NDEF data:',
|
||||||
|
dataParsed.data.map(b => b.toString(16).padStart(2, '0')).join(' '),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Convert to ASCII and look for vivokey.co pattern
|
||||||
|
const asciiStr = dataParsed.data
|
||||||
|
.filter(b => b >= 0x20 && b <= 0x7e)
|
||||||
|
.map(b => String.fromCharCode(b))
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
console.log('[DESFire] NDEF ASCII:', asciiStr);
|
||||||
|
|
||||||
|
// Check for vivokey.co URL pattern
|
||||||
|
const vivokeyMatch = asciiStr.match(/vivokey\.co\/([A-Za-z0-9]+)/i);
|
||||||
|
if (vivokeyMatch) {
|
||||||
|
const code = vivokeyMatch[1];
|
||||||
|
const url = `https://vivokey.co/${code}`;
|
||||||
|
console.log('[DESFire] Found Spark 2 URL:', url);
|
||||||
|
|
||||||
|
return {
|
||||||
|
found: true,
|
||||||
|
name: 'Spark 2',
|
||||||
|
url,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[DESFire] No vivokey.co URL found');
|
||||||
|
return {found: false};
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[DESFire] Spark 2 detection failed:', error);
|
||||||
|
return {found: false};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,15 +14,15 @@ import {
|
|||||||
CHIP_MEMORY_SIZES,
|
CHIP_MEMORY_SIZES,
|
||||||
CHIP_CLONEABILITY,
|
CHIP_CLONEABILITY,
|
||||||
} from '../../types/detection';
|
} from '../../types/detection';
|
||||||
import {detectNtag, mightBeNtag} from './ntag';
|
import {detectNtag, mightBeNtag, detectImplantNameInMemory} from './ntag';
|
||||||
import {
|
import {
|
||||||
detectMifareClassic,
|
detectMifareClassic,
|
||||||
isMifareClassicSak,
|
isMifareClassicSak,
|
||||||
hasIsoDepCapability,
|
hasIsoDepCapability,
|
||||||
detectSakSwap,
|
detectSakSwap,
|
||||||
} from './mifare';
|
} from './mifare';
|
||||||
import {detectDesfire, detectDesfireFromAts} from './desfire';
|
import {detectDesfire, detectDesfireFromAts, detectSpark2Implant} from './desfire';
|
||||||
import {detectIso15693, isIso15693} from './iso15693';
|
import {detectIso15693, isIso15693, detectSparkImplant} from './iso15693';
|
||||||
import {detectJavaCard, mightBeJavaCard, detectJavaCardFromAts} from './javacard';
|
import {detectJavaCard, mightBeJavaCard, detectJavaCardFromAts} from './javacard';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -36,6 +36,7 @@ function createTransponder(
|
|||||||
versionInfo?: Transponder['versionInfo'];
|
versionInfo?: Transponder['versionInfo'];
|
||||||
confidence?: Transponder['confidence'];
|
confidence?: Transponder['confidence'];
|
||||||
sakSwapInfo?: Transponder['sakSwapInfo'];
|
sakSwapInfo?: Transponder['sakSwapInfo'];
|
||||||
|
implantName?: string;
|
||||||
} = {},
|
} = {},
|
||||||
): Transponder {
|
): Transponder {
|
||||||
const cloneInfo = CHIP_CLONEABILITY[type];
|
const cloneInfo = CHIP_CLONEABILITY[type];
|
||||||
@@ -67,11 +68,47 @@ function createTransponder(
|
|||||||
},
|
},
|
||||||
versionInfo: options.versionInfo,
|
versionInfo: options.versionInfo,
|
||||||
sakSwapInfo,
|
sakSwapInfo,
|
||||||
|
implantName: options.implantName,
|
||||||
confidence: options.confidence ?? 'medium',
|
confidence: options.confidence ?? 'medium',
|
||||||
detectedOn: Platform.OS as 'ios' | 'android',
|
detectedOn: Platform.OS as 'ios' | 'android',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine implant name based on detected JavaCard applets
|
||||||
|
* - Fidesmo indicates Apex (only Apex has Fidesmo platform)
|
||||||
|
* - JavaCard Memory indicates Apex or flexSecure (both have it)
|
||||||
|
* - Payment applets indicate this is a payment card, not an implant
|
||||||
|
*/
|
||||||
|
function getJavacardImplantName(installedApplets?: string[]): string | undefined {
|
||||||
|
if (!installedApplets || installedApplets.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Payment card detection - not an implant
|
||||||
|
if (installedApplets.includes('Payment (PPSE)')) {
|
||||||
|
const network = installedApplets.find(a =>
|
||||||
|
['Visa', 'Mastercard', 'American Express', 'Discover', 'Maestro'].includes(a),
|
||||||
|
);
|
||||||
|
return network ? `${network} Payment Card` : 'Payment Card';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fidesmo is only on Apex
|
||||||
|
if (installedApplets.includes('Fidesmo')) {
|
||||||
|
return 'Apex';
|
||||||
|
}
|
||||||
|
|
||||||
|
// JavaCard Memory is on both Apex and flexSecure
|
||||||
|
if (installedApplets.includes('JavaCard Memory')) {
|
||||||
|
return 'Apex/flexSecure';
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Progress callback type for detection updates */
|
||||||
|
export type DetectionProgressCallback = (step: string) => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detect chip type using waterfall approach
|
* Detect chip type using waterfall approach
|
||||||
*
|
*
|
||||||
@@ -81,7 +118,10 @@ function createTransponder(
|
|||||||
* 3. Check for ISO-DEP capable chips (Phase 4: DESFire, Plus, JavaCard)
|
* 3. Check for ISO-DEP capable chips (Phase 4: DESFire, Plus, JavaCard)
|
||||||
* 4. Fall back to generic type based on technology
|
* 4. Fall back to generic type based on technology
|
||||||
*/
|
*/
|
||||||
export async function detectChip(rawData: RawTagData): Promise<DetectionResult> {
|
export async function detectChip(
|
||||||
|
rawData: RawTagData,
|
||||||
|
onProgress?: DetectionProgressCallback,
|
||||||
|
): Promise<DetectionResult> {
|
||||||
try {
|
try {
|
||||||
const {sak, techTypes} = rawData;
|
const {sak, techTypes} = rawData;
|
||||||
|
|
||||||
@@ -97,6 +137,8 @@ export async function detectChip(rawData: RawTagData): Promise<DetectionResult>
|
|||||||
// Step 1: Check for MIFARE Classic
|
// Step 1: Check for MIFARE Classic
|
||||||
// Use tech type detection first (most reliable on Android), then SAK
|
// Use tech type detection first (most reliable on Android), then SAK
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
onProgress?.('Checking MIFARE Classic...');
|
||||||
|
|
||||||
const hasMifareClassicTech = techTypes.some(t =>
|
const hasMifareClassicTech = techTypes.some(t =>
|
||||||
t.includes('MifareClassic'),
|
t.includes('MifareClassic'),
|
||||||
);
|
);
|
||||||
@@ -157,6 +199,7 @@ export async function detectChip(rawData: RawTagData): Promise<DetectionResult>
|
|||||||
console.log('[Detector] mightBeNtag result:', couldBeNtag);
|
console.log('[Detector] mightBeNtag result:', couldBeNtag);
|
||||||
|
|
||||||
if (couldBeNtag) {
|
if (couldBeNtag) {
|
||||||
|
onProgress?.('Reading NTAG version...');
|
||||||
console.log('[Detector] Attempting NTAG detection...');
|
console.log('[Detector] Attempting NTAG detection...');
|
||||||
const ntagResult = await detectNtag();
|
const ntagResult = await detectNtag();
|
||||||
console.log('[Detector] NTAG detection result:', {
|
console.log('[Detector] NTAG detection result:', {
|
||||||
@@ -166,6 +209,19 @@ export async function detectChip(rawData: RawTagData): Promise<DetectionResult>
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (ntagResult.success && ntagResult.chipType) {
|
if (ntagResult.success && ntagResult.chipType) {
|
||||||
|
// Try to detect implant name in last memory pages
|
||||||
|
let implantName: string | undefined;
|
||||||
|
try {
|
||||||
|
onProgress?.('Checking for implant signature...');
|
||||||
|
const implantResult = await detectImplantNameInMemory(ntagResult.chipType);
|
||||||
|
if (implantResult.found && implantResult.name) {
|
||||||
|
implantName = implantResult.name;
|
||||||
|
console.log('[Detector] Found implant name in memory:', implantName);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[Detector] Implant name detection failed:', e);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
transponder: createTransponder(ntagResult.chipType, rawData, {
|
transponder: createTransponder(ntagResult.chipType, rawData, {
|
||||||
@@ -173,6 +229,7 @@ export async function detectChip(rawData: RawTagData): Promise<DetectionResult>
|
|||||||
versionInfo: ntagResult.versionInfo,
|
versionInfo: ntagResult.versionInfo,
|
||||||
confidence:
|
confidence:
|
||||||
ntagResult.chipType === ChipType.NTAG_UNKNOWN ? 'medium' : 'high',
|
ntagResult.chipType === ChipType.NTAG_UNKNOWN ? 'medium' : 'high',
|
||||||
|
implantName,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -197,8 +254,29 @@ export async function detectChip(rawData: RawTagData): Promise<DetectionResult>
|
|||||||
) {
|
) {
|
||||||
// 3a: Always try DESFire/NTAG 424 DNA detection first via GET_VERSION
|
// 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
|
// This command works on DESFire EV1/2/3, DESFire Light, NTAG 424 DNA
|
||||||
|
onProgress?.('Reading DESFire version...');
|
||||||
const desfireResult = await detectDesfire();
|
const desfireResult = await detectDesfire();
|
||||||
if (desfireResult.success && desfireResult.chipType) {
|
if (desfireResult.success && desfireResult.chipType) {
|
||||||
|
// Check for Spark 2 implant on NTAG 424 DNA / NTAG 413 DNA
|
||||||
|
let implantName: string | undefined;
|
||||||
|
const isNtagDna =
|
||||||
|
desfireResult.chipType === ChipType.NTAG424_DNA ||
|
||||||
|
desfireResult.chipType === ChipType.NTAG424_DNA_TT ||
|
||||||
|
desfireResult.chipType === ChipType.NTAG413_DNA;
|
||||||
|
|
||||||
|
if (isNtagDna) {
|
||||||
|
try {
|
||||||
|
onProgress?.('Reading NDEF for Spark 2...');
|
||||||
|
const spark2Result = await detectSpark2Implant();
|
||||||
|
if (spark2Result.found && spark2Result.name) {
|
||||||
|
implantName = spark2Result.name;
|
||||||
|
console.log('[Detector] Found Spark 2 implant:', implantName);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[Detector] Spark 2 detection failed:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
transponder: createTransponder(desfireResult.chipType, rawData, {
|
transponder: createTransponder(desfireResult.chipType, rawData, {
|
||||||
@@ -208,6 +286,7 @@ export async function detectChip(rawData: RawTagData): Promise<DetectionResult>
|
|||||||
desfireResult.chipType === ChipType.DESFIRE_UNKNOWN
|
desfireResult.chipType === ChipType.DESFIRE_UNKNOWN
|
||||||
? 'medium'
|
? 'medium'
|
||||||
: 'high',
|
: 'high',
|
||||||
|
implantName,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -231,13 +310,17 @@ export async function detectChip(rawData: RawTagData): Promise<DetectionResult>
|
|||||||
|
|
||||||
// 3c: Try JavaCard detection (check historical bytes and CPLC)
|
// 3c: Try JavaCard detection (check historical bytes and CPLC)
|
||||||
if (mightBeJavaCard(rawData.historicalBytes, rawData.ats)) {
|
if (mightBeJavaCard(rawData.historicalBytes, rawData.ats)) {
|
||||||
|
onProgress?.('Probing JavaCard applets...');
|
||||||
const jcResult = await detectJavaCard();
|
const jcResult = await detectJavaCard();
|
||||||
if (jcResult.success && jcResult.chipType) {
|
if (jcResult.success && jcResult.chipType) {
|
||||||
|
// Determine implant name based on detected applets
|
||||||
|
const implantName = getJavacardImplantName(jcResult.installedApplets);
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
transponder: createTransponder(jcResult.chipType, rawData, {
|
transponder: createTransponder(jcResult.chipType, rawData, {
|
||||||
confidence:
|
confidence:
|
||||||
jcResult.chipType === ChipType.JCOP4 ? 'high' : 'medium',
|
jcResult.chipType === ChipType.JCOP4 ? 'high' : 'medium',
|
||||||
|
implantName,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -259,12 +342,16 @@ export async function detectChip(rawData: RawTagData): Promise<DetectionResult>
|
|||||||
|
|
||||||
// 3d: If DESFire and likely JavaCard checks failed, try JavaCard as general fallback
|
// 3d: If DESFire and likely JavaCard checks failed, try JavaCard as general fallback
|
||||||
// (some JavaCards don't have obvious historical bytes)
|
// (some JavaCards don't have obvious historical bytes)
|
||||||
|
onProgress?.('Probing for smartcard applets...');
|
||||||
const jcFallback = await detectJavaCard();
|
const jcFallback = await detectJavaCard();
|
||||||
if (jcFallback.success && jcFallback.chipType) {
|
if (jcFallback.success && jcFallback.chipType) {
|
||||||
|
// Determine implant name based on detected applets
|
||||||
|
const implantName = getJavacardImplantName(jcFallback.installedApplets);
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
transponder: createTransponder(jcFallback.chipType, rawData, {
|
transponder: createTransponder(jcFallback.chipType, rawData, {
|
||||||
confidence: 'medium',
|
confidence: 'medium',
|
||||||
|
implantName,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -296,6 +383,7 @@ export async function detectChip(rawData: RawTagData): Promise<DetectionResult>
|
|||||||
// Step 4: Check for ISO 15693 (NFC-V) - SLIX and NTAG 5 detection
|
// Step 4: Check for ISO 15693 (NFC-V) - SLIX and NTAG 5 detection
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
if (isIso15693(techTypes)) {
|
if (isIso15693(techTypes)) {
|
||||||
|
onProgress?.('Reading ISO 15693 system info...');
|
||||||
const iso15693Result = await detectIso15693();
|
const iso15693Result = await detectIso15693();
|
||||||
if (iso15693Result.success && iso15693Result.chipType) {
|
if (iso15693Result.success && iso15693Result.chipType) {
|
||||||
// Determine confidence based on whether we got a specific chip type
|
// Determine confidence based on whether we got a specific chip type
|
||||||
@@ -315,10 +403,24 @@ export async function detectChip(rawData: RawTagData): Promise<DetectionResult>
|
|||||||
? 'high'
|
? 'high'
|
||||||
: 'medium';
|
: 'medium';
|
||||||
|
|
||||||
|
// Check for Spark implant by reading NDEF for vivokey.co URL
|
||||||
|
let implantName: string | undefined;
|
||||||
|
try {
|
||||||
|
onProgress?.('Reading NDEF for Spark 1...');
|
||||||
|
const sparkResult = await detectSparkImplant(iso15693Result.chipType);
|
||||||
|
if (sparkResult.found && sparkResult.name) {
|
||||||
|
implantName = sparkResult.name;
|
||||||
|
console.log('[Detector] Found Spark implant:', implantName);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[Detector] Spark detection failed:', e);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
transponder: createTransponder(iso15693Result.chipType, rawData, {
|
transponder: createTransponder(iso15693Result.chipType, rawData, {
|
||||||
confidence,
|
confidence,
|
||||||
|
implantName,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export {detectChip, getDetectionSummary, canDoAdvancedDetection} from './detector';
|
export {detectChip, getDetectionSummary, canDoAdvancedDetection} from './detector';
|
||||||
export {detectNtag, mightBeNtag, formatNtagVersionInfo} from './ntag';
|
export type {DetectionProgressCallback} from './detector';
|
||||||
|
export {detectNtag, mightBeNtag, formatNtagVersionInfo, detectImplantNameInMemory} from './ntag';
|
||||||
|
export type {ImplantNameResult} from './ntag';
|
||||||
export {
|
export {
|
||||||
detectMifareClassic,
|
detectMifareClassic,
|
||||||
isMifareClassicSak,
|
isMifareClassicSak,
|
||||||
@@ -15,9 +17,9 @@ export {
|
|||||||
IOS_MIFARE_CLASSIC_NOTE,
|
IOS_MIFARE_CLASSIC_NOTE,
|
||||||
} from './mifare';
|
} from './mifare';
|
||||||
export type {SakSwapDetection} from './mifare';
|
export type {SakSwapDetection} from './mifare';
|
||||||
export {detectDesfire, mightBeDesfire, formatDesfireVersionInfo} from './desfire';
|
export {detectDesfire, mightBeDesfire, formatDesfireVersionInfo, detectSpark2Implant} from './desfire';
|
||||||
export type {DesfireDetectionResult} from './desfire';
|
export type {DesfireDetectionResult, Spark2DetectionResult} from './desfire';
|
||||||
export {detectIso15693, isIso15693, getIcManufacturerName} from './iso15693';
|
export {detectIso15693, isIso15693, getIcManufacturerName, detectSparkImplant} from './iso15693';
|
||||||
export type {Iso15693DetectionResult} from './iso15693';
|
export type {Iso15693DetectionResult, SparkDetectionResult} from './iso15693';
|
||||||
export {detectJavaCard, mightBeJavaCard, formatCPLC} from './javacard';
|
export {detectJavaCard, mightBeJavaCard, formatCPLC, hasJavacardMemory} from './javacard';
|
||||||
export type {JavaCardDetectionResult, CPLCData} from './javacard';
|
export type {JavaCardDetectionResult, CPLCData} from './javacard';
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
import {Platform} from 'react-native';
|
import {Platform} from 'react-native';
|
||||||
import NfcManager from 'react-native-nfc-manager';
|
import NfcManager from 'react-native-nfc-manager';
|
||||||
import {ChipType} from '../../types/detection';
|
import {ChipType} from '../../types/detection';
|
||||||
import {getIso15693SystemInfo} from '../nfc/commands';
|
import {getIso15693SystemInfo, transceiveNfcV, iso15693ReadSingleBlock} from '../nfc/commands';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* NXP ICODE IC manufacturer code
|
* NXP ICODE IC manufacturer code
|
||||||
@@ -191,12 +191,12 @@ function getIcodeTypeFromIcReference(icRef: number): ChipType | null {
|
|||||||
return ChipType.ICODE_DNA;
|
return ChipType.ICODE_DNA;
|
||||||
}
|
}
|
||||||
|
|
||||||
// High IC references (0x80-0x8F) might be older SLIX or special variants
|
// High IC references (0x80-0x8F) - unknown, don't guess
|
||||||
if (icRef >= 0x80 && icRef < 0x90) {
|
if (icRef >= 0x80 && icRef < 0x90) {
|
||||||
console.log(
|
console.log(
|
||||||
`[ISO15693] High IC ref 0x${icRef.toString(16)}, treating as SLIX (legacy/special)`,
|
`[ISO15693] High IC ref 0x${icRef.toString(16)} - unknown variant`,
|
||||||
);
|
);
|
||||||
return ChipType.SLIX;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
@@ -431,10 +431,10 @@ export async function detectIso15693(): Promise<Iso15693DetectionResult> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Got manufacturer NXP but couldn't determine exact variant
|
// Got manufacturer NXP but couldn't determine exact variant
|
||||||
// Default to SLIX as it's the most common NXP ISO 15693 chip
|
// Don't assume SLIX - return unknown with NXP manufacturer info
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
chipType: ChipType.SLIX,
|
chipType: ChipType.ISO15693_UNKNOWN,
|
||||||
uid,
|
uid,
|
||||||
icManufacturer: icManufacturer ?? NXP_IC_MFG_CODE,
|
icManufacturer: icManufacturer ?? NXP_IC_MFG_CODE,
|
||||||
blockSize: sysInfo.blockSize,
|
blockSize: sysInfo.blockSize,
|
||||||
@@ -463,9 +463,9 @@ export async function detectIso15693(): Promise<Iso15693DetectionResult> {
|
|||||||
console.log('[ISO15693] NXP manufacturer detected');
|
console.log('[ISO15693] NXP manufacturer detected');
|
||||||
// Check if we got IC reference from UID parsing
|
// Check if we got IC reference from UID parsing
|
||||||
if (uidIcReference !== undefined) {
|
if (uidIcReference !== undefined) {
|
||||||
const chipType = getIcodeTypeFromIcReference(uidIcReference) || ChipType.SLIX;
|
const chipType = getIcodeTypeFromIcReference(uidIcReference);
|
||||||
|
if (chipType) {
|
||||||
console.log('[ISO15693] UID-based detection result:', chipType);
|
console.log('[ISO15693] UID-based detection result:', chipType);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
chipType,
|
chipType,
|
||||||
@@ -474,14 +474,16 @@ export async function detectIso15693(): Promise<Iso15693DetectionResult> {
|
|||||||
icReference: uidIcReference,
|
icReference: uidIcReference,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// NXP but no IC reference - assume SLIX (most common)
|
// NXP but couldn't determine exact variant - return unknown with NXP info
|
||||||
console.log('[ISO15693] NXP but no IC reference, defaulting to SLIX');
|
console.log('[ISO15693] NXP but could not determine specific chip type');
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
chipType: ChipType.SLIX,
|
chipType: ChipType.ISO15693_UNKNOWN,
|
||||||
uid,
|
uid,
|
||||||
icManufacturer,
|
icManufacturer,
|
||||||
|
icReference: uidIcReference,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -746,3 +748,103 @@ export function getIcManufacturerName(code: number): string {
|
|||||||
|
|
||||||
return manufacturers[code] || `Unknown (0x${code.toString(16)})`;
|
return manufacturers[code] || `Unknown (0x${code.toString(16)})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result of Spark implant detection
|
||||||
|
*/
|
||||||
|
export interface SparkDetectionResult {
|
||||||
|
found: boolean;
|
||||||
|
name?: string;
|
||||||
|
url?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect Spark implant by reading NDEF from ISO 15693 tag
|
||||||
|
* Spark implants have a URI record pointing to vivokey.co/$code
|
||||||
|
*
|
||||||
|
* @param chipType - The detected chip type (used to determine Spark 1 vs 2)
|
||||||
|
*/
|
||||||
|
export async function detectSparkImplant(
|
||||||
|
chipType: ChipType,
|
||||||
|
): Promise<SparkDetectionResult> {
|
||||||
|
try {
|
||||||
|
console.log('[ISO15693] Checking for Spark implant NDEF...');
|
||||||
|
|
||||||
|
// Read first few blocks to find NDEF data
|
||||||
|
// ISO 15693 NDEF layout: Block 0 = CC (Capability Container), Blocks 1+ = NDEF message
|
||||||
|
const ndefBytes: number[] = [];
|
||||||
|
|
||||||
|
// Read blocks 0-7 (typically enough for a short NDEF URI)
|
||||||
|
for (let block = 0; block < 8; block++) {
|
||||||
|
try {
|
||||||
|
const response = await transceiveNfcV(iso15693ReadSingleBlock(block));
|
||||||
|
// Response format: [flags] [data...]
|
||||||
|
// Skip first byte (flags) if present
|
||||||
|
if (response.length > 1 && (response[0] & 0x01) === 0) {
|
||||||
|
// No error, data follows
|
||||||
|
ndefBytes.push(...response.slice(1));
|
||||||
|
} else if (response.length >= 4) {
|
||||||
|
// Some readers don't include flags
|
||||||
|
ndefBytes.push(...response);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`[ISO15693] Block ${block} read failed:`, e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ndefBytes.length === 0) {
|
||||||
|
console.log('[ISO15693] No NDEF data read');
|
||||||
|
return {found: false};
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
'[ISO15693] Read NDEF bytes:',
|
||||||
|
ndefBytes.map(b => b.toString(16).padStart(2, '0')).join(' '),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Convert to ASCII and look for vivokey.co pattern
|
||||||
|
const asciiStr = ndefBytes
|
||||||
|
.filter(b => b >= 0x20 && b <= 0x7e)
|
||||||
|
.map(b => String.fromCharCode(b))
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
console.log('[ISO15693] ASCII content:', asciiStr);
|
||||||
|
|
||||||
|
// Check for vivokey.co URL pattern
|
||||||
|
const vivokeyMatch = asciiStr.match(/vivokey\.co\/([A-Za-z0-9]+)/i);
|
||||||
|
if (vivokeyMatch) {
|
||||||
|
const code = vivokeyMatch[1];
|
||||||
|
const url = `https://vivokey.co/${code}`;
|
||||||
|
console.log('[ISO15693] Found Spark URL:', url);
|
||||||
|
|
||||||
|
// Determine Spark version based on chip type
|
||||||
|
// SLIX = Spark 1, NTAG 424 DNA = Spark 2
|
||||||
|
let sparkName: string;
|
||||||
|
if (
|
||||||
|
chipType === ChipType.SLIX ||
|
||||||
|
chipType === ChipType.SLIX_S ||
|
||||||
|
chipType === ChipType.SLIX_L ||
|
||||||
|
chipType === ChipType.SLIX2
|
||||||
|
) {
|
||||||
|
sparkName = 'Spark 1';
|
||||||
|
} else if (chipType === ChipType.NTAG424_DNA) {
|
||||||
|
sparkName = 'Spark 2';
|
||||||
|
} else {
|
||||||
|
sparkName = 'Spark';
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
found: true,
|
||||||
|
name: sparkName,
|
||||||
|
url,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[ISO15693] No vivokey.co URL found');
|
||||||
|
return {found: false};
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[ISO15693] Spark detection failed:', error);
|
||||||
|
return {found: false};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -116,59 +116,87 @@ export async function detectJavaCard(): Promise<JavaCardDetectionResult> {
|
|||||||
// First, try to select the Card Manager (ISD)
|
// First, try to select the Card Manager (ISD)
|
||||||
// Card Manager selection might fail, but we can still try CPLC
|
// Card Manager selection might fail, but we can still try CPLC
|
||||||
// Some cards allow CPLC without selecting an applet
|
// Some cards allow CPLC without selecting an applet
|
||||||
|
let cardManagerSelected = false;
|
||||||
try {
|
try {
|
||||||
await sendIsoDepCommand(selectAid(KNOWN_AIDS.cardManager));
|
const cmResponse = await sendIsoDepCommand(
|
||||||
|
selectAid(KNOWN_AIDS.cardManager),
|
||||||
|
);
|
||||||
|
const cmParsed = parseApduResponse(cmResponse);
|
||||||
|
cardManagerSelected = cmParsed.isSuccess;
|
||||||
} catch {
|
} catch {
|
||||||
// Ignore - some cards don't support Card Manager selection
|
// Ignore - some cards don't support Card Manager selection
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get CPLC data
|
// Get CPLC data
|
||||||
|
let cplc: CPLCData | null = null;
|
||||||
|
let fabricatorName: string | undefined;
|
||||||
|
let osName: string | undefined;
|
||||||
|
|
||||||
|
if (cardManagerSelected) {
|
||||||
|
try {
|
||||||
const cplcResponse = await sendIsoDepCommand(GET_CPLC);
|
const cplcResponse = await sendIsoDepCommand(GET_CPLC);
|
||||||
const cplcParsed = parseApduResponse(cplcResponse);
|
const cplcParsed = parseApduResponse(cplcResponse);
|
||||||
|
|
||||||
if (!cplcParsed.isSuccess || cplcParsed.data.length < 20) {
|
if (cplcParsed.isSuccess && cplcParsed.data.length >= 20) {
|
||||||
// CPLC not available - might not be a JavaCard
|
cplc = parseCPLC(cplcParsed.data);
|
||||||
return {
|
if (cplc) {
|
||||||
success: false,
|
fabricatorName =
|
||||||
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] ||
|
IC_FABRICATORS[cplc.icFabricator] ||
|
||||||
`Unknown (0x${cplc.icFabricator.toString(16)})`;
|
`Unknown (0x${cplc.icFabricator.toString(16)})`;
|
||||||
|
osName = identifyJcopVersion(cplc.osId) || undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// CPLC failed - will try AID probing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Identify JCOP version
|
// Probe for installed applets (this also helps identify DT implants)
|
||||||
const osName = identifyJcopVersion(cplc.osId);
|
const installedApplets = await probeApplets();
|
||||||
|
|
||||||
// Determine chip type
|
// Determine chip type
|
||||||
let chipType: ChipType = ChipType.JAVACARD_UNKNOWN;
|
let chipType: ChipType = ChipType.JAVACARD_UNKNOWN;
|
||||||
|
|
||||||
// Check if it's NXP JCOP4 (J3R180)
|
// If we have CPLC and it's NXP JCOP4
|
||||||
if (cplc.icFabricator === 0x4790 && osName?.includes('JCOP4')) {
|
if (cplc && cplc.icFabricator === 0x4790 && osName?.includes('JCOP4')) {
|
||||||
chipType = ChipType.JCOP4;
|
chipType = ChipType.JCOP4;
|
||||||
}
|
}
|
||||||
|
// If we found JavaCard Memory, it's definitely a JCOP4 (Apex/flexSecure)
|
||||||
|
else if (installedApplets.includes('JavaCard Memory')) {
|
||||||
|
chipType = ChipType.JCOP4;
|
||||||
|
if (!osName) {
|
||||||
|
osName = 'JCOP4 (VivoKey)';
|
||||||
|
}
|
||||||
|
if (!fabricatorName) {
|
||||||
|
fabricatorName = 'NXP Semiconductors';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If Fidesmo is present, likely JCOP4
|
||||||
|
else if (installedApplets.includes('Fidesmo') && !cplc) {
|
||||||
|
chipType = ChipType.JCOP4;
|
||||||
|
if (!osName) {
|
||||||
|
osName = 'JCOP4 (Fidesmo)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Probe for installed applets
|
// If no identification was successful at all
|
||||||
const installedApplets = await probeApplets();
|
if (
|
||||||
|
chipType === ChipType.JAVACARD_UNKNOWN &&
|
||||||
|
!cplc &&
|
||||||
|
installedApplets.length === 0
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Could not identify JavaCard - CPLC unavailable and no known applets found',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
chipType,
|
chipType,
|
||||||
cplc,
|
cplc: cplc || undefined,
|
||||||
fabricatorName,
|
fabricatorName,
|
||||||
osName: osName || `Unknown OS (0x${cplc.osId.toString(16)})`,
|
osName: osName || (cplc ? `Unknown OS (0x${cplc.osId.toString(16)})` : 'Unknown'),
|
||||||
installedApplets,
|
installedApplets,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -188,6 +216,19 @@ export async function detectJavaCard(): Promise<JavaCardDetectionResult> {
|
|||||||
async function probeApplets(): Promise<string[]> {
|
async function probeApplets(): Promise<string[]> {
|
||||||
const found: string[] = [];
|
const found: string[] = [];
|
||||||
|
|
||||||
|
// Try JavaCard Memory Manager (indicates Apex or flexSecure)
|
||||||
|
try {
|
||||||
|
const response = await sendIsoDepCommand(
|
||||||
|
selectAid(KNOWN_AIDS.javacardMemory),
|
||||||
|
);
|
||||||
|
const parsed = parseApduResponse(response);
|
||||||
|
if (parsed.isSuccess) {
|
||||||
|
found.push('JavaCard Memory');
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Applet not present
|
||||||
|
}
|
||||||
|
|
||||||
// Try OpenPGP applet
|
// Try OpenPGP applet
|
||||||
try {
|
try {
|
||||||
const response = await sendIsoDepCommand(selectAid(KNOWN_AIDS.openPgp));
|
const response = await sendIsoDepCommand(selectAid(KNOWN_AIDS.openPgp));
|
||||||
@@ -210,9 +251,115 @@ async function probeApplets(): Promise<string[]> {
|
|||||||
// Applet not present
|
// Applet not present
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Try Fidesmo service discovery
|
||||||
|
try {
|
||||||
|
const response = await sendIsoDepCommand(selectAid(KNOWN_AIDS.fidesmo));
|
||||||
|
const parsed = parseApduResponse(response);
|
||||||
|
if (parsed.isSuccess) {
|
||||||
|
found.push('Fidesmo');
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Applet not present
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try PPSE (Proximity Payment System Environment) - indicates contactless payment card
|
||||||
|
try {
|
||||||
|
const response = await sendIsoDepCommand(selectAid(KNOWN_AIDS.ppse));
|
||||||
|
const parsed = parseApduResponse(response);
|
||||||
|
if (parsed.isSuccess) {
|
||||||
|
found.push('Payment (PPSE)');
|
||||||
|
// If PPSE is present, try to identify specific payment network
|
||||||
|
await probePaymentNetworks(found);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Not a payment card
|
||||||
|
}
|
||||||
|
|
||||||
return found;
|
return found;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Probe for specific payment network applets
|
||||||
|
* Only called if PPSE is present (i.e., this is a payment card)
|
||||||
|
*/
|
||||||
|
async function probePaymentNetworks(found: string[]): Promise<void> {
|
||||||
|
// Try Visa
|
||||||
|
try {
|
||||||
|
const response = await sendIsoDepCommand(selectAid(KNOWN_AIDS.visaCredit));
|
||||||
|
const parsed = parseApduResponse(response);
|
||||||
|
if (parsed.isSuccess) {
|
||||||
|
found.push('Visa');
|
||||||
|
return; // Found payment network, no need to check others
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Not present
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try Mastercard
|
||||||
|
try {
|
||||||
|
const response = await sendIsoDepCommand(selectAid(KNOWN_AIDS.mastercard));
|
||||||
|
const parsed = parseApduResponse(response);
|
||||||
|
if (parsed.isSuccess) {
|
||||||
|
found.push('Mastercard');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Not present
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try Amex
|
||||||
|
try {
|
||||||
|
const response = await sendIsoDepCommand(selectAid(KNOWN_AIDS.amex));
|
||||||
|
const parsed = parseApduResponse(response);
|
||||||
|
if (parsed.isSuccess) {
|
||||||
|
found.push('American Express');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Not present
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try Discover
|
||||||
|
try {
|
||||||
|
const response = await sendIsoDepCommand(selectAid(KNOWN_AIDS.discover));
|
||||||
|
const parsed = parseApduResponse(response);
|
||||||
|
if (parsed.isSuccess) {
|
||||||
|
found.push('Discover');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Not present
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try Maestro
|
||||||
|
try {
|
||||||
|
const response = await sendIsoDepCommand(selectAid(KNOWN_AIDS.maestro));
|
||||||
|
const parsed = parseApduResponse(response);
|
||||||
|
if (parsed.isSuccess) {
|
||||||
|
found.push('Maestro');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Not present
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the JavaCard Memory Manager applet is present
|
||||||
|
* This indicates an Apex or flexSecure implant
|
||||||
|
*/
|
||||||
|
export async function hasJavacardMemory(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const response = await sendIsoDepCommand(
|
||||||
|
selectAid(KNOWN_AIDS.javacardMemory),
|
||||||
|
);
|
||||||
|
const parsed = parseApduResponse(response);
|
||||||
|
return parsed.isSuccess;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if tag might be a JavaCard based on ATS/historical bytes
|
* Check if tag might be a JavaCard based on ATS/historical bytes
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {ChipType, NtagVersionInfo} from '../../types/detection';
|
import {ChipType, NtagVersionInfo} from '../../types/detection';
|
||||||
import {NTAG_GET_VERSION, sendType2Command} from '../nfc/commands';
|
import {NTAG_GET_VERSION, sendType2Command, ntagRead} from '../nfc/commands';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET_VERSION response structure (same for NTAG and Ultralight):
|
* GET_VERSION response structure (same for NTAG and Ultralight):
|
||||||
@@ -303,3 +303,133 @@ export function formatNtagVersionInfo(info: NtagVersionInfo): string {
|
|||||||
`Storage: 0x${info.storageSize.toString(16)}`,
|
`Storage: 0x${info.storageSize.toString(16)}`,
|
||||||
].join(', ');
|
].join(', ');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Known Dangerous Things implant names that may be written to Type 2 tag memory
|
||||||
|
* Looking for 4+ character partial matches (case-insensitive)
|
||||||
|
* Note: Only includes Type 2 (NTAG/Ultralight) based implants
|
||||||
|
*/
|
||||||
|
const KNOWN_IMPLANT_NAMES = [
|
||||||
|
// X-Series NTAG implants
|
||||||
|
'xNT',
|
||||||
|
'xSIID',
|
||||||
|
'NExT',
|
||||||
|
// Bioresin NTAG implants
|
||||||
|
'dNExT',
|
||||||
|
// Flex NTAG implants
|
||||||
|
'flexNT',
|
||||||
|
// Generic identifiers
|
||||||
|
'VivoKey',
|
||||||
|
'Dangerous',
|
||||||
|
'DNGRTHNG',
|
||||||
|
'DNGR',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Memory layout for NTAG chips
|
||||||
|
* Last user page = total pages - config pages - 1
|
||||||
|
*/
|
||||||
|
const NTAG_MEMORY_LAYOUT: Partial<Record<ChipType, {totalPages: number; lastUserPage: number}>> = {
|
||||||
|
[ChipType.NTAG213]: {totalPages: 45, lastUserPage: 39},
|
||||||
|
[ChipType.NTAG215]: {totalPages: 135, lastUserPage: 129},
|
||||||
|
[ChipType.NTAG216]: {totalPages: 231, lastUserPage: 225},
|
||||||
|
[ChipType.NTAG_I2C_1K]: {totalPages: 231, lastUserPage: 225},
|
||||||
|
[ChipType.NTAG_I2C_2K]: {totalPages: 485, lastUserPage: 479},
|
||||||
|
[ChipType.NTAG_I2C_PLUS_1K]: {totalPages: 231, lastUserPage: 225},
|
||||||
|
[ChipType.NTAG_I2C_PLUS_2K]: {totalPages: 485, lastUserPage: 479},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert bytes to ASCII string, filtering non-printable characters
|
||||||
|
*/
|
||||||
|
function bytesToAscii(bytes: number[]): string {
|
||||||
|
return bytes
|
||||||
|
.filter(b => b >= 0x20 && b <= 0x7e) // Printable ASCII only
|
||||||
|
.map(b => String.fromCharCode(b))
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result of implant name detection
|
||||||
|
*/
|
||||||
|
export interface ImplantNameResult {
|
||||||
|
found: boolean;
|
||||||
|
name?: string;
|
||||||
|
rawBytes?: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check memory pages for implant name (4+ character partial match)
|
||||||
|
* Reads the last 16 bytes (4 pages) of user memory where implant names are typically stored
|
||||||
|
*/
|
||||||
|
export async function detectImplantNameInMemory(
|
||||||
|
chipType: ChipType,
|
||||||
|
): Promise<ImplantNameResult> {
|
||||||
|
const layout = NTAG_MEMORY_LAYOUT[chipType];
|
||||||
|
if (!layout) {
|
||||||
|
console.log(`[NTAG] No memory layout for chip type: ${chipType}`);
|
||||||
|
return {found: false};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Read last 4 pages (16 bytes) - where implant names are typically stored
|
||||||
|
// For NTAG216, this is pages 222-225 (lastUserPage - 3 to lastUserPage)
|
||||||
|
const startPage = layout.lastUserPage - 3;
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`[NTAG] Reading pages ${startPage}-${layout.lastUserPage} (0x${startPage.toString(16)}-0x${layout.lastUserPage.toString(16)}) for implant name`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// READ command returns 4 pages (16 bytes) starting at the given page
|
||||||
|
const response = await sendType2Command(ntagRead(startPage));
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`[NTAG] Read ${response.length} bytes:`,
|
||||||
|
response.map(b => b.toString(16).padStart(2, '0')).join(' '),
|
||||||
|
);
|
||||||
|
|
||||||
|
const asciiStr = bytesToAscii(response);
|
||||||
|
console.log(`[NTAG] ASCII: "${asciiStr}"`);
|
||||||
|
|
||||||
|
// Look for 4+ character matches (case-insensitive)
|
||||||
|
const upperAscii = asciiStr.toUpperCase();
|
||||||
|
for (const name of KNOWN_IMPLANT_NAMES) {
|
||||||
|
if (name.length >= 4 && upperAscii.includes(name.toUpperCase())) {
|
||||||
|
console.log(`[NTAG] Found implant name: ${name}`);
|
||||||
|
return {
|
||||||
|
found: true,
|
||||||
|
name,
|
||||||
|
rawBytes: response,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not found in last 4 pages, try the 4 pages before that
|
||||||
|
const earlierStartPage = Math.max(4, startPage - 4);
|
||||||
|
if (earlierStartPage < startPage) {
|
||||||
|
console.log(
|
||||||
|
`[NTAG] Trying earlier pages ${earlierStartPage}-${earlierStartPage + 3}`,
|
||||||
|
);
|
||||||
|
const earlierResponse = await sendType2Command(ntagRead(earlierStartPage));
|
||||||
|
const earlierAscii = bytesToAscii(earlierResponse);
|
||||||
|
const upperEarlierAscii = earlierAscii.toUpperCase();
|
||||||
|
|
||||||
|
for (const name of KNOWN_IMPLANT_NAMES) {
|
||||||
|
if (name.length >= 4 && upperEarlierAscii.includes(name.toUpperCase())) {
|
||||||
|
console.log(`[NTAG] Found implant name in earlier pages: ${name}`);
|
||||||
|
return {
|
||||||
|
found: true,
|
||||||
|
name,
|
||||||
|
rawBytes: earlierResponse,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[NTAG] No implant name found in memory');
|
||||||
|
return {found: false, rawBytes: response};
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[NTAG] Failed to read memory for implant name:', error);
|
||||||
|
return {found: false};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
import {Platform} from 'react-native';
|
import {Platform} from 'react-native';
|
||||||
import NfcManager, {NfcTech} from 'react-native-nfc-manager';
|
import NfcManager, {NfcTech} from 'react-native-nfc-manager';
|
||||||
|
|
||||||
|
export {NfcTech};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* APDU response with status word
|
* APDU response with status word
|
||||||
*/
|
*/
|
||||||
@@ -130,6 +132,20 @@ export function iso15693ReadSingleBlock(blockNumber: number): number[] {
|
|||||||
return [0x02, 0x20, blockNumber];
|
return [0x02, 0x20, blockNumber];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ISO 15693 READ_MULTIPLE_BLOCKS command (0x23)
|
||||||
|
* Reads multiple consecutive blocks
|
||||||
|
*/
|
||||||
|
export function iso15693ReadMultipleBlocks(
|
||||||
|
startBlock: number,
|
||||||
|
numBlocks: number,
|
||||||
|
): number[] {
|
||||||
|
// Flags 0x02: unaddressed mode
|
||||||
|
// Command 0x23: READ_MULTIPLE_BLOCKS
|
||||||
|
// numBlocks is 0-indexed (0 = 1 block, so subtract 1)
|
||||||
|
return [0x02, 0x23, startBlock, numBlocks - 1];
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Known AIDs
|
// Known AIDs
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -141,6 +157,42 @@ export const KNOWN_AIDS = {
|
|||||||
openPgp: [0xd2, 0x76, 0x00, 0x01, 0x24, 0x01],
|
openPgp: [0xd2, 0x76, 0x00, 0x01, 0x24, 0x01],
|
||||||
/** FIDO/U2F applet */
|
/** FIDO/U2F applet */
|
||||||
fido: [0xa0, 0x00, 0x00, 0x06, 0x47, 0x2f, 0x00, 0x01],
|
fido: [0xa0, 0x00, 0x00, 0x06, 0x47, 0x2f, 0x00, 0x01],
|
||||||
|
/** FIDO2/WebAuthn applet (CTAP2) */
|
||||||
|
fido2: [0xa0, 0x00, 0x00, 0x06, 0x47, 0x2f, 0x00, 0x01],
|
||||||
|
/** Fidesmo service discovery applet */
|
||||||
|
fidesmo: [0xa0, 0x00, 0x00, 0x06, 0x17, 0x00],
|
||||||
|
/** NFC Forum Type 4 Tag NDEF applet */
|
||||||
|
ndefTag: [0xd2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x01],
|
||||||
|
/** VivoKey OTP applet */
|
||||||
|
vivokeyOtp: [0xa0, 0x00, 0x00, 0x05, 0x27, 0x21, 0x01],
|
||||||
|
/** PIV (Personal Identity Verification) */
|
||||||
|
piv: [0xa0, 0x00, 0x00, 0x03, 0x08],
|
||||||
|
/** OATH (OTP) applet */
|
||||||
|
oath: [0xa0, 0x00, 0x00, 0x05, 0x27, 0x21, 0x01],
|
||||||
|
/** JavaCard Memory Manager - present on Apex and flexSecure implants */
|
||||||
|
javacardMemory: [
|
||||||
|
0xa0, 0x00, 0x00, 0x08, 0x46, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x01,
|
||||||
|
],
|
||||||
|
|
||||||
|
// Payment network AIDs (EMV)
|
||||||
|
/** Visa Credit/Debit */
|
||||||
|
visaCredit: [0xa0, 0x00, 0x00, 0x00, 0x03, 0x10, 0x10],
|
||||||
|
/** Visa Electron */
|
||||||
|
visaElectron: [0xa0, 0x00, 0x00, 0x00, 0x03, 0x20, 0x10],
|
||||||
|
/** Mastercard Credit/Debit */
|
||||||
|
mastercard: [0xa0, 0x00, 0x00, 0x00, 0x04, 0x10, 0x10],
|
||||||
|
/** Mastercard Maestro */
|
||||||
|
maestro: [0xa0, 0x00, 0x00, 0x00, 0x04, 0x30, 0x60],
|
||||||
|
/** American Express */
|
||||||
|
amex: [0xa0, 0x00, 0x00, 0x00, 0x25, 0x01, 0x08, 0x01],
|
||||||
|
/** Discover */
|
||||||
|
discover: [0xa0, 0x00, 0x00, 0x01, 0x52, 0x30, 0x10],
|
||||||
|
/** JCB */
|
||||||
|
jcb: [0xa0, 0x00, 0x00, 0x00, 0x65, 0x10, 0x10],
|
||||||
|
/** UnionPay */
|
||||||
|
unionpay: [0xa0, 0x00, 0x00, 0x03, 0x33, 0x01, 0x01, 0x01],
|
||||||
|
/** PPSE (Proximity Payment System Environment) - present on all contactless payment cards */
|
||||||
|
ppse: [0x32, 0x50, 0x41, 0x59, 0x2e, 0x53, 0x59, 0x53, 0x2e, 0x44, 0x44, 0x46, 0x30, 0x31], // "2PAY.SYS.DDF01"
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@@ -190,6 +190,9 @@ export interface Transponder {
|
|||||||
/** SAK swap detection results */
|
/** SAK swap detection results */
|
||||||
sakSwapInfo?: SakSwapInfo;
|
sakSwapInfo?: SakSwapInfo;
|
||||||
|
|
||||||
|
/** Implant name found in memory (for Type 2 tags) */
|
||||||
|
implantName?: string;
|
||||||
|
|
||||||
/** Detection confidence level */
|
/** Detection confidence level */
|
||||||
confidence: 'high' | 'medium' | 'low';
|
confidence: 'high' | 'medium' | 'low';
|
||||||
|
|
||||||
|
|||||||
35
src/utils/logger.ts
Normal file
35
src/utils/logger.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
/**
|
||||||
|
* Logger utility that only outputs in development mode
|
||||||
|
* All console.log statements in the app should use this logger
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable no-console */
|
||||||
|
|
||||||
|
const isDev = __DEV__;
|
||||||
|
|
||||||
|
export const logger = {
|
||||||
|
log: (...args: unknown[]) => {
|
||||||
|
if (isDev) {
|
||||||
|
console.log(...args);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
warn: (...args: unknown[]) => {
|
||||||
|
if (isDev) {
|
||||||
|
console.warn(...args);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
error: (...args: unknown[]) => {
|
||||||
|
// Always log errors, even in production
|
||||||
|
console.error(...args);
|
||||||
|
},
|
||||||
|
|
||||||
|
debug: (...args: unknown[]) => {
|
||||||
|
if (isDev) {
|
||||||
|
console.log('[DEBUG]', ...args);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default logger;
|
||||||
Reference in New Issue
Block a user