Fixed UID length bug for product suggestions re: MFC and payment verbiage
This commit is contained in:
3
app.json
3
app.json
@@ -14,7 +14,8 @@
|
||||
"supportsTablet": false,
|
||||
"bundleIdentifier": "com.dangerousthings.nfcidentifier",
|
||||
"infoPlist": {
|
||||
"NFCReaderUsageDescription": "This app uses NFC to scan transponders and identify compatible Dangerous Things implants."
|
||||
"NFCReaderUsageDescription": "This app uses NFC to scan transponders and identify compatible Dangerous Things implants.",
|
||||
"ITSAppUsesNonExemptEncryption": false
|
||||
}
|
||||
},
|
||||
"android": {
|
||||
|
||||
6
eas.json
6
eas.json
@@ -11,6 +11,12 @@
|
||||
"preview": {
|
||||
"distribution": "internal"
|
||||
},
|
||||
"preview-apk": {
|
||||
"distribution": "internal",
|
||||
"android": {
|
||||
"buildType": "apk"
|
||||
}
|
||||
},
|
||||
"production": {
|
||||
"autoIncrement": true
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
* Discontinued products removed per https://forum.dangerousthings.com/t/shes-dead-jim/26308
|
||||
*/
|
||||
|
||||
import {ChipType} from '../types/detection';
|
||||
import { ChipType } from '../types/detection';
|
||||
import {
|
||||
Product,
|
||||
FormFactor,
|
||||
@@ -72,7 +72,7 @@ export const PRODUCTS: Product[] = [
|
||||
id: 'xnt',
|
||||
name: 'xNT',
|
||||
description:
|
||||
'NFC Type 2 implant with 888 bytes of user memory. The original and most popular NFC implant.',
|
||||
'NFC Type 2 implant with 888 bytes of user memory.',
|
||||
formFactor: FormFactor.X_SERIES,
|
||||
categories: [ProductCategory.NFC],
|
||||
compatibleChips: [...NTAG21X_COMPATIBLE, ...NTAG_I2C_COMPATIBLE],
|
||||
@@ -96,9 +96,9 @@ export const PRODUCTS: Product[] = [
|
||||
compatibleChips: [...NTAG21X_COMPATIBLE, ...NTAG_I2C_COMPATIBLE],
|
||||
features: [
|
||||
'NTAG I2C chip',
|
||||
'888 bytes user memory',
|
||||
'Built-in LED indicator',
|
||||
'Multiple colors available',
|
||||
'1kB user memory',
|
||||
'URL/contact sharing',
|
||||
'Red, Green, Blue, and White LEDs available',
|
||||
],
|
||||
url: 'https://dangerousthings.com/product/xsiid/',
|
||||
canReceiveClone: true,
|
||||
@@ -107,14 +107,14 @@ export const PRODUCTS: Product[] = [
|
||||
{
|
||||
id: 'xslx',
|
||||
name: 'xSLX',
|
||||
description: 'ISO 15693 NFC-V implant using ICODE SLIX chip.',
|
||||
description: 'NFC Type 5 implant using ICODE SLIX chip.',
|
||||
formFactor: FormFactor.X_SERIES,
|
||||
categories: [ProductCategory.NFC],
|
||||
compatibleChips: [...SLIX_COMPATIBLE],
|
||||
features: [
|
||||
'ICODE SLIX chip',
|
||||
'ISO 15693 (NFC-V)',
|
||||
'Long range capability',
|
||||
'320 bbytes user memory',
|
||||
'URL/contact sharing',
|
||||
],
|
||||
url: 'https://dangerousthings.com/product/xslx/',
|
||||
canReceiveClone: true,
|
||||
@@ -133,10 +133,11 @@ export const PRODUCTS: Product[] = [
|
||||
categories: [ProductCategory.DUAL_FREQUENCY, ProductCategory.ACCESS],
|
||||
compatibleChips: [...NTAG21X_COMPATIBLE, ...NTAG_I2C_COMPATIBLE],
|
||||
features: [
|
||||
'Dual-frequency',
|
||||
'NTAG216 NFC chip',
|
||||
'888 bytes user memory',
|
||||
'T5577 125kHz chip',
|
||||
'Dual-frequency capability',
|
||||
'HID/EM compatible',
|
||||
'LF access control',
|
||||
],
|
||||
url: 'https://dangerousthings.com/product/next/',
|
||||
canReceiveClone: true,
|
||||
@@ -147,15 +148,17 @@ export const PRODUCTS: Product[] = [
|
||||
id: 'next-v2',
|
||||
name: 'NExT v2',
|
||||
description:
|
||||
'Updated dual-frequency implant with NTAG I2C, T5577, and built-in LED indicator.',
|
||||
'Updated dual-frequency implant with NTAG I2C, T5577, and built-in LED.',
|
||||
formFactor: FormFactor.X_SERIES,
|
||||
categories: [ProductCategory.DUAL_FREQUENCY, ProductCategory.ACCESS, ProductCategory.LED],
|
||||
compatibleChips: [...NTAG21X_COMPATIBLE, ...NTAG_I2C_COMPATIBLE],
|
||||
features: [
|
||||
'NTAG I2C NFC chip',
|
||||
'1kB user memory',
|
||||
'URL/contact sharing',
|
||||
'Green, Blue, and White LEDs available (HF)',
|
||||
'T5577 125kHz chip',
|
||||
'Built-in LED indicator',
|
||||
'888 bytes user memory',
|
||||
'LF access control',
|
||||
],
|
||||
url: 'https://dangerousthings.com/product/next-v2/',
|
||||
canReceiveClone: true,
|
||||
@@ -240,38 +243,15 @@ export const PRODUCTS: Product[] = [
|
||||
categories: [ProductCategory.SECURE],
|
||||
compatibleChips: [ChipType.NTAG424_DNA, ChipType.NTAG424_DNA_TT, ChipType.ICODE_DNA],
|
||||
features: [
|
||||
'Cryptographic authentication',
|
||||
'VivoKey identity platform',
|
||||
'Secure web login',
|
||||
'Digital signatures',
|
||||
'Spark Actions',
|
||||
'Verify API',
|
||||
],
|
||||
url: 'https://dangerousthings.com/product/vivokey-spark-2/',
|
||||
url: 'https://dangerousthings.com/product/vivokey-spark/',
|
||||
canReceiveClone: false,
|
||||
exactMatch: true,
|
||||
notes: 'Uses cryptographic authentication - cannot clone',
|
||||
},
|
||||
|
||||
// ==========================================================================
|
||||
// X-Series - LED/Special
|
||||
// ==========================================================================
|
||||
{
|
||||
id: 'xglow',
|
||||
name: 'xGlow',
|
||||
description: 'Implant with LED that glows when exposed to NFC field.',
|
||||
formFactor: FormFactor.X_SERIES,
|
||||
categories: [ProductCategory.LED],
|
||||
compatibleChips: [], // No NFC storage
|
||||
features: [
|
||||
'LED indicator',
|
||||
'Multiple colors',
|
||||
'Field detection',
|
||||
],
|
||||
url: 'https://dangerousthings.com/product/xglow/',
|
||||
canReceiveClone: false,
|
||||
exactMatch: false,
|
||||
notes: 'LED only - no NFC data storage',
|
||||
},
|
||||
|
||||
// ==========================================================================
|
||||
// Bioresin Implants - Larger capsules (incision install)
|
||||
// ==========================================================================
|
||||
@@ -313,25 +293,6 @@ export const PRODUCTS: Product[] = [
|
||||
exactMatch: false,
|
||||
notes: 'Can clone MIFARE Classic cards (requires Android + keys)',
|
||||
},
|
||||
{
|
||||
id: 'dt5',
|
||||
name: 'dT5',
|
||||
description:
|
||||
'Bioresin 125kHz implant with T5577 chip for access control systems.',
|
||||
formFactor: FormFactor.BIORESIN,
|
||||
categories: [ProductCategory.ACCESS],
|
||||
compatibleChips: [], // 125kHz only, no NFC
|
||||
features: [
|
||||
'T5577 125kHz chip',
|
||||
'EM41xx emulation',
|
||||
'HID ProxCard compatible',
|
||||
'Bioresin encapsulation',
|
||||
],
|
||||
url: 'https://dangerousthings.com/product/dt5/',
|
||||
canReceiveClone: false,
|
||||
exactMatch: false,
|
||||
notes: '125kHz only - no NFC capability',
|
||||
},
|
||||
|
||||
// ==========================================================================
|
||||
// Flex Implants - NFC
|
||||
@@ -482,23 +443,6 @@ export const PRODUCTS: Product[] = [
|
||||
exactMatch: false,
|
||||
notes: 'For HID iCLASS systems only',
|
||||
},
|
||||
{
|
||||
id: 'teslaflex',
|
||||
name: 'TeslaFlex',
|
||||
description: 'Implant designed to work as a Tesla key card.',
|
||||
formFactor: FormFactor.FLEX,
|
||||
categories: [ProductCategory.ACCESS],
|
||||
compatibleChips: [], // Tesla proprietary
|
||||
features: [
|
||||
'Tesla key card compatible',
|
||||
'Flexible form factor',
|
||||
'Direct Tesla integration',
|
||||
],
|
||||
url: 'https://dangerousthings.com/product/teslaflex/',
|
||||
canReceiveClone: false,
|
||||
exactMatch: false,
|
||||
notes: 'Specifically for Tesla vehicles',
|
||||
},
|
||||
{
|
||||
id: 'flexug4',
|
||||
name: 'flexUG4',
|
||||
|
||||
@@ -25,7 +25,7 @@ export interface UseScanResult {
|
||||
/** Scanned tag data (when state is 'success') */
|
||||
tag: RawTagData | null;
|
||||
/** Detected transponder info (when detection succeeds) */
|
||||
transponder: Transponder | null;
|
||||
transponder: Transponder | null | undefined;
|
||||
/** Scan error (when state is 'error') */
|
||||
error: ScanError | null;
|
||||
/** NFC status (supported and enabled) */
|
||||
@@ -45,10 +45,19 @@ export interface UseScanResult {
|
||||
/**
|
||||
* Hook for managing NFC scanning
|
||||
*/
|
||||
// Combined scan result to ensure atomic updates
|
||||
interface ScanResult {
|
||||
tag: RawTagData | null;
|
||||
transponder: Transponder | null | undefined;
|
||||
}
|
||||
|
||||
export function useScan(): UseScanResult {
|
||||
const [state, setState] = useState<ScanState>('idle');
|
||||
const [tag, setTag] = useState<RawTagData | null>(null);
|
||||
const [transponder, setTransponder] = useState<Transponder | null>(null);
|
||||
// Use combined state to ensure tag and transponder update atomically
|
||||
const [scanResult, setScanResult] = useState<ScanResult>({
|
||||
tag: null,
|
||||
transponder: undefined,
|
||||
});
|
||||
const [error, setError] = useState<ScanError | null>(null);
|
||||
const [nfcStatus, setNfcStatus] = useState<NFCStatus>({
|
||||
isSupported: false,
|
||||
@@ -93,8 +102,7 @@ export function useScan(): UseScanResult {
|
||||
|
||||
scanInProgress.current = true;
|
||||
setState('scanning');
|
||||
setTag(null);
|
||||
setTransponder(null);
|
||||
setScanResult({ tag: null, transponder: undefined });
|
||||
setError(null);
|
||||
setScanProgress({step: 'Waiting for tag...', current: 1});
|
||||
|
||||
@@ -162,11 +170,18 @@ export function useScan(): UseScanResult {
|
||||
setError(result.error);
|
||||
}
|
||||
} else if (result.tag) {
|
||||
const detectedTransponder = result.detection ?? null;
|
||||
console.log('[useScan] Scan complete:', {
|
||||
hasTag: !!result.tag,
|
||||
hasDetection: !!result.detection,
|
||||
transponderType: detectedTransponder?.type,
|
||||
});
|
||||
// Update tag and transponder atomically in a single state update
|
||||
setScanResult({
|
||||
tag: result.tag,
|
||||
transponder: detectedTransponder,
|
||||
});
|
||||
setState('success');
|
||||
setTag(result.tag);
|
||||
if (result.detection) {
|
||||
setTransponder(result.detection);
|
||||
}
|
||||
} else {
|
||||
setState('error');
|
||||
setError({
|
||||
@@ -201,8 +216,7 @@ export function useScan(): UseScanResult {
|
||||
// Reset state to idle
|
||||
const reset = useCallback(() => {
|
||||
setState('idle');
|
||||
setTag(null);
|
||||
setTransponder(null);
|
||||
setScanResult({ tag: null, transponder: undefined });
|
||||
setError(null);
|
||||
setScanProgress(null);
|
||||
}, []);
|
||||
@@ -219,8 +233,8 @@ export function useScan(): UseScanResult {
|
||||
|
||||
return {
|
||||
state,
|
||||
tag,
|
||||
transponder,
|
||||
tag: scanResult.tag,
|
||||
transponder: scanResult.transponder,
|
||||
error,
|
||||
nfcStatus,
|
||||
scanProgress,
|
||||
|
||||
@@ -15,7 +15,7 @@ export function ResultScreen({ route, navigation }: ResultScreenProps) {
|
||||
// Match chip to products
|
||||
const matchResult = useMemo(() => {
|
||||
if (!transponder) return null;
|
||||
return matchChipToProducts(transponder.type);
|
||||
return matchChipToProducts(transponder);
|
||||
}, [transponder]);
|
||||
|
||||
// Get educational chip info
|
||||
@@ -449,16 +449,26 @@ export function ResultScreen({ route, navigation }: ResultScreenProps) {
|
||||
<Text variant="bodyMedium" style={styles.noDetectionText}>
|
||||
Unable to identify this chip type. It may be unsupported or require advanced detection.
|
||||
</Text>
|
||||
<Text variant="bodyMedium" style={styles.noDetectionText}>
|
||||
Need help? Ask on our forum for assistance.
|
||||
</Text>
|
||||
<Button
|
||||
mode="outlined"
|
||||
onPress={() => Linking.openURL(buildTrackedUrl('https://dngr.us/forum', 'unknown_chip'))}
|
||||
style={styles.forumButton}
|
||||
labelStyle={styles.forumButtonLabel}>
|
||||
VISIT FORUM
|
||||
</Button>
|
||||
</Surface>
|
||||
)}
|
||||
|
||||
{/* Conversion Service Card - show for payment devices, or when recommended and not a real implant */}
|
||||
{(transponder?.implantName?.includes('Payment Card') ||
|
||||
((matchResult?.conversionRecommended || !transponder) && !transponder?.implantName)) && (
|
||||
{/* Conversion Service Card - show for payment devices, or when recommended (but NOT for unidentified chips) */}
|
||||
{transponder && (transponder?.implantName?.includes('Payment Card') ||
|
||||
(matchResult?.conversionRecommended && !transponder?.implantName)) && (
|
||||
<Surface style={styles.conversionCard} elevation={1}>
|
||||
<Text variant="bodyMedium" style={styles.conversionText}>
|
||||
{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."
|
||||
? "Payment cards can't be copied/cloned to implants. Our conversion service might be an option."
|
||||
: !transponder
|
||||
? 'Unknown chip? Our conversion service can help.'
|
||||
: matchResult?.isCloneable === false
|
||||
@@ -650,6 +660,16 @@ const styles = StyleSheet.create({
|
||||
noDetectionText: {
|
||||
color: DTColors.light,
|
||||
opacity: 0.8,
|
||||
marginBottom: 12,
|
||||
},
|
||||
forumButton: {
|
||||
borderColor: DTColors.modeWarning,
|
||||
borderWidth: 1,
|
||||
marginTop: 8,
|
||||
},
|
||||
forumButtonLabel: {
|
||||
color: DTColors.modeWarning,
|
||||
fontSize: 12,
|
||||
},
|
||||
// Products Card
|
||||
productsCard: {
|
||||
|
||||
@@ -20,9 +20,25 @@ export function ScanScreen({navigation}: ScanScreenProps) {
|
||||
openSettings,
|
||||
} = useScan();
|
||||
|
||||
// Navigate to results when scan succeeds (detection happens in the hook)
|
||||
// Track if we've already navigated to prevent double navigation
|
||||
const hasNavigated = React.useRef(false);
|
||||
|
||||
// Navigate to results when scan succeeds AND transponder is set
|
||||
// We need transponder to be set (even if null for failed detection) before navigating
|
||||
useEffect(() => {
|
||||
if (state === 'success' && tag) {
|
||||
// Only navigate once per scan session
|
||||
if (hasNavigated.current) return;
|
||||
|
||||
// Wait for success state, tag data, AND transponder to be set
|
||||
// transponder: undefined = not yet set, null = detection failed, Transponder = success
|
||||
if (state === 'success' && tag && transponder !== undefined) {
|
||||
hasNavigated.current = true;
|
||||
console.log('[ScanScreen] Navigating to Result:', {
|
||||
hasTag: !!tag,
|
||||
hasTransponder: !!transponder,
|
||||
transponderType: transponder?.type,
|
||||
transponderChipName: transponder?.chipName,
|
||||
});
|
||||
navigation.replace('Result', {
|
||||
tagData: {
|
||||
uid: tag.uid,
|
||||
@@ -37,6 +53,13 @@ export function ScanScreen({navigation}: ScanScreenProps) {
|
||||
}
|
||||
}, [state, tag, transponder, navigation]);
|
||||
|
||||
// Reset navigation flag when starting a new scan
|
||||
useEffect(() => {
|
||||
if (state === 'scanning') {
|
||||
hasNavigated.current = false;
|
||||
}
|
||||
}, [state]);
|
||||
|
||||
// Auto-start scan when screen loads
|
||||
useEffect(() => {
|
||||
// Small delay to ensure screen is mounted
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
import {ChipType, DesfireVersionInfo} from '../../types/detection';
|
||||
import type {NdefRecord} from '../../types/nfc';
|
||||
import {
|
||||
DESFIRE_GET_VERSION,
|
||||
DESFIRE_GET_VERSION_CONTINUE,
|
||||
@@ -14,11 +15,73 @@ import {
|
||||
KNOWN_AIDS,
|
||||
} from '../nfc/commands';
|
||||
|
||||
/**
|
||||
* URI record type name format prefixes
|
||||
* https://www.nfc-forum.org/specs/
|
||||
*/
|
||||
const URI_PREFIXES: Record<number, string> = {
|
||||
0x00: '',
|
||||
0x01: 'http://www.',
|
||||
0x02: 'https://www.',
|
||||
0x03: 'http://',
|
||||
0x04: 'https://',
|
||||
0x05: 'tel:',
|
||||
0x06: 'mailto:',
|
||||
};
|
||||
|
||||
/**
|
||||
* Detect Spark 2 implant from cached NDEF records
|
||||
* This should be called BEFORE any APDU commands to avoid putting the tag
|
||||
* into native mode which breaks ISO 7816-4 NDEF reading
|
||||
*/
|
||||
export function detectSpark2FromNdef(
|
||||
ndefRecords?: NdefRecord[],
|
||||
): Spark2DetectionResult {
|
||||
if (!ndefRecords || ndefRecords.length === 0) {
|
||||
return {found: false};
|
||||
}
|
||||
|
||||
console.log('[DESFire] Checking cached NDEF records for Spark 2...');
|
||||
|
||||
for (const record of ndefRecords) {
|
||||
// Check for URI record (TNF=1, type="U")
|
||||
if (record.tnf === 1 && record.type === 'U' && record.payload.length > 0) {
|
||||
// First byte is URI prefix code
|
||||
const prefixCode = record.payload[0];
|
||||
const prefix = URI_PREFIXES[prefixCode] || '';
|
||||
const uriBody = record.payload
|
||||
.slice(1)
|
||||
.map(b => String.fromCharCode(b))
|
||||
.join('');
|
||||
const fullUri = prefix + uriBody;
|
||||
|
||||
console.log('[DESFire] Found URI record:', fullUri);
|
||||
|
||||
// Check for vivokey.co pattern
|
||||
const vivokeyMatch = fullUri.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 in NDEF records');
|
||||
return {found: false};
|
||||
}
|
||||
|
||||
/**
|
||||
* Product type values from GET_VERSION byte 1
|
||||
*/
|
||||
const PRODUCT_TYPES = {
|
||||
DESFIRE: 0x01, // Standard DESFire
|
||||
NTAG: 0x04, // NTAG family (including NTAG 413 DNA which uses 0x04)
|
||||
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
|
||||
@@ -184,7 +247,7 @@ export async function detectDesfire(): Promise<DesfireDetectionResult> {
|
||||
let chipType: ChipType;
|
||||
|
||||
if (hwProductType === PRODUCT_TYPES.NTAG_424_DNA) {
|
||||
// NTAG DNA family (413 DNA and 424 DNA share product type 0x21)
|
||||
// NTAG 424 DNA family uses 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)
|
||||
@@ -198,6 +261,14 @@ export async function detectDesfire(): Promise<DesfireDetectionResult> {
|
||||
// Larger storage indicates NTAG 424 DNA (~416 bytes)
|
||||
chipType = ChipType.NTAG424_DNA;
|
||||
}
|
||||
} else if (hwProductType === PRODUCT_TYPES.NTAG && hwMajor >= 0x30) {
|
||||
// NTAG 413 DNA returns product type 0x04 (like regular NTAG) but with:
|
||||
// - hwMajor >= 0x30 (typically 0x30)
|
||||
// - hwStorageSize 0x11 (different meaning than regular NTAG)
|
||||
// - Responds to IsoDep (unlike regular NTAG 21x)
|
||||
// Per NXP NT4H1321 datasheet, NTAG 413 DNA uses this response format
|
||||
console.log('[DESFire] Detected NTAG 413 DNA (product type 0x04, hwMajor 0x30+)');
|
||||
chipType = ChipType.NTAG413_DNA;
|
||||
} else if (hwProductType === PRODUCT_TYPES.NTAG_I2C) {
|
||||
// NTAG I2C family (can have ISO-DEP on Plus variants)
|
||||
// Per NXP NT3H2111/NT3H2211 datasheet:
|
||||
@@ -430,31 +501,58 @@ export async function detectSpark2Implant(): Promise<Spark2DetectionResult> {
|
||||
try {
|
||||
console.log('[DESFire] Checking for Spark 2 implant NDEF...');
|
||||
|
||||
// Step 1: Select NDEF application
|
||||
// Step 1: Select NDEF application (D2760000850101)
|
||||
console.log('[DESFire] Selecting NDEF application...');
|
||||
const selectNdefApp = await sendIsoDepCommand(selectAid(KNOWN_AIDS.ndefTag));
|
||||
const selectAppResponse = parseApduResponse(selectNdefApp);
|
||||
console.log('[DESFire] NDEF app select result:', {
|
||||
sw: `${selectAppResponse.sw1.toString(16)}${selectAppResponse.sw2.toString(16)}`,
|
||||
success: selectAppResponse.isSuccess,
|
||||
});
|
||||
|
||||
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
|
||||
// Step 2: Select NDEF file (file ID E104 for standard Type 4 Tag)
|
||||
// SELECT by file ID: 00 A4 00 0C 02 E104
|
||||
console.log('[DESFire] Selecting NDEF file E104...');
|
||||
const selectFileCmd = [0x00, 0xa4, 0x00, 0x0c, 0x02, 0xe1, 0x04];
|
||||
const selectFileResponse = await sendIsoDepCommand(selectFileCmd);
|
||||
const selectFileParsed = parseApduResponse(selectFileResponse);
|
||||
console.log('[DESFire] NDEF file select result:', {
|
||||
sw: `${selectFileParsed.sw1.toString(16)}${selectFileParsed.sw2.toString(16)}`,
|
||||
success: selectFileParsed.isSuccess,
|
||||
});
|
||||
|
||||
if (!selectFileParsed.isSuccess) {
|
||||
// Try alternate file ID (some Type 4 tags use E102 or just 02)
|
||||
console.log('[DESFire] Trying alternate NDEF file 0002...');
|
||||
const altSelectCmd = [0x00, 0xa4, 0x00, 0x0c, 0x02, 0x00, 0x02];
|
||||
const altSelectResponse = await sendIsoDepCommand(altSelectCmd);
|
||||
const altSelectParsed = parseApduResponse(altSelectResponse);
|
||||
console.log('[DESFire] Alternate file select result:', {
|
||||
sw: `${altSelectParsed.sw1.toString(16)}${altSelectParsed.sw2.toString(16)}`,
|
||||
success: altSelectParsed.isSuccess,
|
||||
});
|
||||
|
||||
if (!altSelectParsed.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
|
||||
console.log('[DESFire] Reading NDEF length...');
|
||||
const readLengthCmd = [0x00, 0xb0, 0x00, 0x00, 0x02];
|
||||
const readLengthResponse = await sendIsoDepCommand(readLengthCmd);
|
||||
const lengthParsed = parseApduResponse(readLengthResponse);
|
||||
console.log('[DESFire] NDEF length read result:', {
|
||||
sw: `${lengthParsed.sw1.toString(16)}${lengthParsed.sw2.toString(16)}`,
|
||||
data: lengthParsed.data,
|
||||
});
|
||||
|
||||
if (!lengthParsed.isSuccess || lengthParsed.data.length < 2) {
|
||||
console.log('[DESFire] NDEF length read failed');
|
||||
@@ -462,39 +560,58 @@ export async function detectSpark2Implant(): Promise<Spark2DetectionResult> {
|
||||
}
|
||||
|
||||
const ndefLength = (lengthParsed.data[0] << 8) | lengthParsed.data[1];
|
||||
console.log('[DESFire] NDEF length:', ndefLength);
|
||||
console.log('[DESFire] NDEF message length:', ndefLength, 'bytes');
|
||||
|
||||
if (ndefLength === 0 || ndefLength > 500) {
|
||||
console.log('[DESFire] Invalid NDEF length');
|
||||
console.log('[DESFire] Invalid or empty 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)];
|
||||
// For longer NDEF messages, we may need to read in chunks
|
||||
const allNdefData: number[] = [];
|
||||
let offset = 2; // Start after the 2-byte length field
|
||||
let remaining = ndefLength;
|
||||
|
||||
while (remaining > 0) {
|
||||
const chunkSize = Math.min(remaining, 128); // Read up to 128 bytes at a time
|
||||
// READ BINARY: 00 B0 <offset high> <offset low> <length>
|
||||
const readDataCmd = [0x00, 0xb0, (offset >> 8) & 0xff, offset & 0xff, chunkSize];
|
||||
const readDataResponse = await sendIsoDepCommand(readDataCmd);
|
||||
const dataParsed = parseApduResponse(readDataResponse);
|
||||
|
||||
if (!dataParsed.isSuccess || dataParsed.data.length === 0) {
|
||||
console.log('[DESFire] NDEF data read failed');
|
||||
console.log('[DESFire] NDEF data read failed at offset', offset);
|
||||
break;
|
||||
}
|
||||
|
||||
allNdefData.push(...dataParsed.data);
|
||||
offset += dataParsed.data.length;
|
||||
remaining -= dataParsed.data.length;
|
||||
}
|
||||
|
||||
if (allNdefData.length === 0) {
|
||||
console.log('[DESFire] No NDEF data read');
|
||||
return {found: false};
|
||||
}
|
||||
|
||||
console.log(
|
||||
'[DESFire] NDEF data:',
|
||||
dataParsed.data.map(b => b.toString(16).padStart(2, '0')).join(' '),
|
||||
'[DESFire] NDEF data (' + allNdefData.length + ' bytes):',
|
||||
allNdefData.slice(0, 50).map(b => b.toString(16).padStart(2, '0')).join(' ') +
|
||||
(allNdefData.length > 50 ? '...' : ''),
|
||||
);
|
||||
|
||||
// Convert to ASCII and look for vivokey.co pattern
|
||||
const asciiStr = dataParsed.data
|
||||
const asciiStr = allNdefData
|
||||
.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);
|
||||
// Check for vivokey.co URL pattern - match any characters after the domain
|
||||
// Pattern allows alphanumeric, hyphens, underscores, and other URL-safe chars
|
||||
const vivokeyMatch = asciiStr.match(/vivokey\.co\/([A-Za-z0-9_\-]+)/i);
|
||||
if (vivokeyMatch) {
|
||||
const code = vivokeyMatch[1];
|
||||
const url = `https://vivokey.co/${code}`;
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
hasIsoDepCapability,
|
||||
detectSakSwap,
|
||||
} from './mifare';
|
||||
import {detectDesfire, detectDesfireFromAts, detectSpark2Implant} from './desfire';
|
||||
import {detectDesfire, detectDesfireFromAts, detectSpark2Implant, detectSpark2FromNdef} from './desfire';
|
||||
import {detectIso15693, isIso15693, detectSparkImplant} from './iso15693';
|
||||
import {detectJavaCard, mightBeJavaCard, detectJavaCardFromAts} from './javacard';
|
||||
|
||||
@@ -265,15 +265,24 @@ export async function detectChip(
|
||||
desfireResult.chipType === ChipType.NTAG413_DNA;
|
||||
|
||||
if (isNtagDna) {
|
||||
// First, try to detect from cached NDEF records (doesn't require APDU)
|
||||
// This works even after DESFire GET_VERSION puts the tag in native mode
|
||||
const cachedNdefResult = detectSpark2FromNdef(rawData.ndefRecords);
|
||||
if (cachedNdefResult.found && cachedNdefResult.name) {
|
||||
implantName = cachedNdefResult.name;
|
||||
console.log('[Detector] Found Spark 2 implant from cached NDEF:', implantName);
|
||||
} else {
|
||||
// Fallback: Try APDU-based NDEF reading (may fail if tag is in native mode)
|
||||
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);
|
||||
console.log('[Detector] Found Spark 2 implant via APDU:', implantName);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[Detector] Spark 2 detection failed:', e);
|
||||
console.warn('[Detector] Spark 2 APDU detection failed:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -819,19 +819,20 @@ export async function detectSparkImplant(
|
||||
console.log('[ISO15693] Found Spark URL:', url);
|
||||
|
||||
// Determine Spark version based on chip type
|
||||
// SLIX = Spark 1, NTAG 424 DNA = Spark 2
|
||||
// SLIX/ICODE DNA = Spark 1 (ISO 15693)
|
||||
// NTAG 424 DNA = Spark 2 (ISO 14443-4, handled in desfire.ts)
|
||||
let sparkName: string;
|
||||
if (
|
||||
chipType === ChipType.SLIX ||
|
||||
chipType === ChipType.SLIX_S ||
|
||||
chipType === ChipType.SLIX_L ||
|
||||
chipType === ChipType.SLIX2
|
||||
chipType === ChipType.SLIX2 ||
|
||||
chipType === ChipType.ICODE_DNA
|
||||
) {
|
||||
sparkName = 'Spark 1';
|
||||
} else if (chipType === ChipType.NTAG424_DNA) {
|
||||
sparkName = 'Spark 2';
|
||||
} else {
|
||||
sparkName = 'Spark';
|
||||
// Any other ISO 15693 chip with vivokey.co URL is likely a Spark variant
|
||||
sparkName = 'Spark 1';
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
* Matches detected NFC chips to compatible Dangerous Things products
|
||||
*/
|
||||
|
||||
import {ChipType, getChipFamily, CHIP_CLONEABILITY, ChipFamily} from '../../types/detection';
|
||||
import {MatchResult, Product, DesfireEvLevel} from '../../types/products';
|
||||
import { ChipType, getChipFamily, CHIP_CLONEABILITY, ChipFamily, Transponder } from '../../types/detection';
|
||||
import { MatchResult, Product, DesfireEvLevel } from '../../types/products';
|
||||
import {
|
||||
PRODUCTS,
|
||||
getChipProductMap,
|
||||
@@ -15,7 +15,8 @@ import {
|
||||
/**
|
||||
* Match a detected chip to compatible products
|
||||
*/
|
||||
export function matchChipToProducts(chipType: ChipType): MatchResult {
|
||||
export function matchChipToProducts(chip: Transponder): MatchResult {
|
||||
const chipType = chip.type as ChipType
|
||||
const chipProductMap = getChipProductMap();
|
||||
const cloneability = CHIP_CLONEABILITY[chipType];
|
||||
const chipFamily = getChipFamily(chipType);
|
||||
@@ -32,9 +33,14 @@ export function matchChipToProducts(chipType: ChipType): MatchResult {
|
||||
exactMatches.push(product);
|
||||
}
|
||||
if (product.canReceiveClone && cloneability?.cloneable) {
|
||||
if ((product.name.startsWith("xMagic") || product.name.startsWith("xM1") || product.name.startsWith("flexM1")) && chip.rawData.uid.replaceAll(":", "").length / 2 !== 4) {
|
||||
// This skips things without a 4-byte UID
|
||||
|
||||
} else {
|
||||
cloneTargets.push(product);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find products in the same chip family
|
||||
const familyMatches = findFamilyMatches(chipType, chipFamily, directMatches);
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
ScanError,
|
||||
ScanErrorType,
|
||||
NfcTechType,
|
||||
NdefRecord,
|
||||
} from '../../types/nfc';
|
||||
|
||||
/**
|
||||
@@ -115,6 +116,47 @@ function parseAts(tag: TagEvent): {ats?: string; historicalBytes?: string} {
|
||||
return {ats, historicalBytes};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse NDEF records from TagEvent
|
||||
*/
|
||||
function parseNdefRecords(tag: TagEvent): NdefRecord[] | undefined {
|
||||
const ndefMessage = (tag as any).ndefMessage;
|
||||
if (!ndefMessage || !Array.isArray(ndefMessage) || ndefMessage.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const records: NdefRecord[] = [];
|
||||
for (const record of ndefMessage) {
|
||||
if (!record) continue;
|
||||
|
||||
// react-native-nfc-manager returns NDEF records with these properties
|
||||
const tnf = record.tnf ?? 0;
|
||||
const type = record.type
|
||||
? typeof record.type === 'string'
|
||||
? record.type
|
||||
: String.fromCharCode(...(Array.isArray(record.type) ? record.type : []))
|
||||
: '';
|
||||
const id = record.id
|
||||
? typeof record.id === 'string'
|
||||
? record.id
|
||||
: String.fromCharCode(...(Array.isArray(record.id) ? record.id : []))
|
||||
: undefined;
|
||||
const payload = Array.isArray(record.payload)
|
||||
? record.payload
|
||||
: typeof record.payload === 'string'
|
||||
? record.payload.split('').map((c: string) => c.charCodeAt(0))
|
||||
: [];
|
||||
|
||||
records.push({tnf, type, id, payload});
|
||||
}
|
||||
|
||||
if (records.length > 0) {
|
||||
console.log('[NFCManager] Parsed NDEF records:', records.length);
|
||||
}
|
||||
|
||||
return records.length > 0 ? records : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert TagEvent to RawTagData
|
||||
*/
|
||||
@@ -124,6 +166,7 @@ function tagEventToRawData(tag: TagEvent): RawTagData {
|
||||
const sak = parseSak(tag);
|
||||
const atqa = parseAtqa(tag);
|
||||
const {ats, historicalBytes} = parseAts(tag);
|
||||
const ndefRecords = parseNdefRecords(tag);
|
||||
|
||||
const isoDep = (tag as any).isoDep;
|
||||
const iso7816 = (tag as any).iso7816;
|
||||
@@ -160,6 +203,7 @@ function tagEventToRawData(tag: TagEvent): RawTagData {
|
||||
ats,
|
||||
historicalBytes,
|
||||
maxTransceiveLength,
|
||||
ndefRecords,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,20 @@ export type NfcTechType =
|
||||
| 'Iso7816'
|
||||
| 'Iso15693';
|
||||
|
||||
/**
|
||||
* NDEF record from tag
|
||||
*/
|
||||
export interface NdefRecord {
|
||||
/** Type Name Format */
|
||||
tnf: number;
|
||||
/** Record type as string */
|
||||
type: string;
|
||||
/** Record ID */
|
||||
id?: string;
|
||||
/** Payload data as byte array */
|
||||
payload: number[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Raw tag data from NFC scan
|
||||
*/
|
||||
@@ -36,6 +50,8 @@ export interface RawTagData {
|
||||
historicalBytes?: string;
|
||||
/** Maximum transceive length */
|
||||
maxTransceiveLength?: number;
|
||||
/** Cached NDEF records (if tag supports NDEF) */
|
||||
ndefRecords?: NdefRecord[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user