Compare commits

...

14 Commits

Author SHA1 Message Date
michael
d5a89f4cd9 Merge branch 'feature/new-architecture' 2026-01-28 15:12:08 -08:00
michael
b558231d6c Added in the theme archive as the package is not in npm yet 2026-01-28 15:08:48 -08:00
michael
cce7629eb6 Fix NTAG 413/424 DNA detection using correct GET_VERSION parsing
Per NXP datasheets (NT4H1321, NT4H2421), NTAG DNA chips return product
type 0x04 with major version 0x04, distinguished by subtype:
- NTAG 413 DNA: subtype 0x02
- NTAG 424 DNA: subtype 0x05
- NTAG 424 DNA TT: subtype 0x04

Previously incorrectly expected product type 0x21 or hwMajor >= 0x30,
causing Spark 2 implants to be detected as "DESFire Unknown" when the
vivokey.co NDEF record was not present.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 17:10:50 -08:00
michael
77bce6a815 Added formfactor explanations 2026-01-27 13:31:41 -08:00
michael
3e9411fc7d ResultsScreen refinements 2026-01-27 12:16:15 -08:00
michael
83e5e1ceeb Theme updates 2026-01-27 11:33:43 -08:00
michael
199146d0b9 Theme has been updated to be more inline with the storefront. 2026-01-26 15:50:46 -08:00
michael
2409ec3625 Fixed MFC 1k/4k issue and Ultralight detection 2026-01-26 14:06:49 -08:00
michael
b27baf5dec Fixed UID length bug for product suggestions re: MFC and payment verbiage 2026-01-26 12:42:27 -08:00
michael
e597728883 Add UTM tracking to external links
- Add buildTrackedUrl helper for analytics tracking
- Track conversion service and product link clicks
- Parameters: utm_source=dt_nfc_identifier, utm_medium=app,
  utm_campaign=chip_scan, utm_content=<chip_type|product_name>

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 12:10:13 -08:00
michael
bb6a9bbc22 Prepare for production build
- Downgrade react-native to 0.81.5 (matches Expo SDK 54)
- Downgrade react to 19.1.0 and related @react-native packages
- Disable New Architecture (react-native-nfc-manager untested)
- Add expo install/doctor exclusions for version-locked deps
- Configure babel to strip console.log/warn in production
- Add logger utility for development-only logging
- Add component and data module exports

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 10:10:30 -08:00
michael
63d815c1c2 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>
2026-01-23 09:29:17 -08:00
michael
7596e4cbfa Add product matching service with DT implant catalog
- Add product catalog with current DT implants (X-Series, Flex, Bioresin)
- Implement chip-to-product matching algorithm
- Add DESFire EV level tracking with mismatch warnings
- Display compatible implants on ResultScreen with product details
- Remove discontinued products: xDF (EV1), xDF2 (EV2), flexDF (EV1)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 19:36:58 -08:00
michael
fb7f836abd Add chip detection system for NTAG, DESFire, ISO 15693, and JavaCard
Implement comprehensive NFC chip identification using platform-specific
commands and heuristics:

- NTAG/Ultralight detection via GET_VERSION command
- DESFire EV1/EV2/EV3 and NTAG 424 DNA detection
- ISO 15693 detection with memory-based SLIX/ICODE DNA differentiation
- JavaCard/JCOP detection via CPLC data
- MIFARE Classic detection via SAK values

Use block count as authoritative source for NXP ISO 15693 chips since
IC reference values overlap between SLIX and ICODE DNA families.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 18:08:38 -08:00
33 changed files with 8175 additions and 2682 deletions

View File

@@ -6,7 +6,7 @@
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "dark",
"newArchEnabled": true,
"newArchEnabled": false,
"splash": {
"backgroundColor": "#000000"
},
@@ -14,10 +14,12 @@
"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": {
"versionCode": 8,
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#000000"
@@ -47,4 +49,4 @@
}
}
}
}
}

View File

@@ -1,6 +1,19 @@
module.exports = function (api) {
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 {
presets: ['babel-preset-expo'],
plugins,
};
};

View File

@@ -11,6 +11,12 @@
"preview": {
"distribution": "internal"
},
"preview-apk": {
"distribution": "internal",
"android": {
"buildType": "apk"
}
},
"production": {
"autoIncrement": true
}

3623
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -17,35 +17,58 @@
"dependencies": {
"@react-navigation/native": "^7.1.27",
"@react-navigation/native-stack": "^7.9.1",
"babel-preset-expo": "^54.0.9",
"expo": "^54.0.31",
"babel-preset-expo": "~54.0.10",
"expo": "~54.0.32",
"expo-dev-client": "^6.0.20",
"expo-status-bar": "^3.0.9",
"react": "19.2.0",
"react-native": "0.83.1",
"react": "19.1.0",
"react-native": "0.81.5",
"react-native-dt-theme": "file:react-native-dt-theme-0.2.2.tgz",
"react-native-nfc-manager": "^3.17.2",
"react-native-paper": "^5.14.5",
"react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.16.0",
"react-native-svg": "15.12.1",
"react-native-vector-icons": "^10.2.0"
},
"devDependencies": {
"@babel/core": "^7.25.2",
"@babel/preset-env": "^7.25.3",
"@babel/runtime": "^7.25.0",
"@react-native/babel-preset": "0.83.1",
"@react-native/eslint-config": "0.83.1",
"@react-native/babel-preset": "0.81.1",
"@react-native/eslint-config": "0.81.1",
"@types/jest": "^29.5.13",
"@types/react": "^19.2.0",
"@types/react": "~19.1.0",
"@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",
"jest": "^29.6.3",
"prettier": "2.8.8",
"react-test-renderer": "19.2.0",
"react-test-renderer": "19.1.0",
"typescript": "^5.8.3"
},
"engines": {
"node": ">=20"
},
"expo": {
"install": {
"exclude": [
"react",
"react-native",
"@types/react",
"@react-native/babel-preset",
"@react-native/eslint-config",
"react-test-renderer"
]
},
"doctor": {
"reactNativeDirectoryCheck": {
"exclude": [
"react-native-nfc-manager",
"react-native-vector-icons"
]
}
}
}
}

Binary file not shown.

1
src/components/index.ts Normal file
View File

@@ -0,0 +1 @@
export * from './scan';

View File

@@ -0,0 +1,177 @@
/**
* Animated NFC Scan Indicator
*
* Shows pulsing concentric rings during NFC scanning
*/
import React, {useEffect, useRef} from 'react';
import {View, StyleSheet, Animated, Easing} from 'react-native';
import {DTColors} from '../../theme';
interface ScanAnimationProps {
/** Whether the animation is active */
isActive: boolean;
/** Color for the rings (defaults to modeNormal/cyan) */
color?: string;
/** Size of the component */
size?: number;
}
export function ScanAnimation({
isActive,
color = DTColors.modeNormal,
size = 200,
}: ScanAnimationProps) {
// Animation values for each ring
const ring1 = useRef(new Animated.Value(0)).current;
const ring2 = useRef(new Animated.Value(0)).current;
const ring3 = useRef(new Animated.Value(0)).current;
useEffect(() => {
if (!isActive) {
// Reset animations when not active
ring1.setValue(0);
ring2.setValue(0);
ring3.setValue(0);
return;
}
// Create staggered pulsing animations
const createPulse = (animatedValue: Animated.Value, delay: number) => {
return Animated.loop(
Animated.sequence([
Animated.delay(delay),
Animated.timing(animatedValue, {
toValue: 1,
duration: 2000,
easing: Easing.out(Easing.ease),
useNativeDriver: true,
}),
Animated.timing(animatedValue, {
toValue: 0,
duration: 0,
useNativeDriver: true,
}),
]),
);
};
const animation1 = createPulse(ring1, 0);
const animation2 = createPulse(ring2, 666);
const animation3 = createPulse(ring3, 1333);
animation1.start();
animation2.start();
animation3.start();
return () => {
animation1.stop();
animation2.stop();
animation3.stop();
};
}, [isActive, ring1, ring2, ring3]);
const createRingStyle = (animatedValue: Animated.Value) => {
const scale = animatedValue.interpolate({
inputRange: [0, 1],
outputRange: [0.3, 1],
});
const opacity = animatedValue.interpolate({
inputRange: [0, 0.5, 1],
outputRange: [0.8, 0.4, 0],
});
return {
transform: [{scale}],
opacity,
borderColor: color,
};
};
return (
<View style={[styles.container, {width: size, height: size}]}>
{/* Pulsing rings */}
<Animated.View
style={[
styles.ring,
{width: size, height: size, borderRadius: size / 2},
createRingStyle(ring1),
]}
/>
<Animated.View
style={[
styles.ring,
{width: size, height: size, borderRadius: size / 2},
createRingStyle(ring2),
]}
/>
<Animated.View
style={[
styles.ring,
{width: size, height: size, borderRadius: size / 2},
createRingStyle(ring3),
]}
/>
{/* Center dot */}
<View style={[styles.centerDot, {backgroundColor: color}]} />
{/* NFC icon representation */}
<View style={styles.nfcIcon}>
<View style={[styles.nfcArc, styles.nfcArc1, {borderColor: color}]} />
<View style={[styles.nfcArc, styles.nfcArc2, {borderColor: color}]} />
<View style={[styles.nfcArc, styles.nfcArc3, {borderColor: color}]} />
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
justifyContent: 'center',
alignItems: 'center',
},
ring: {
position: 'absolute',
borderWidth: 2,
},
centerDot: {
width: 12,
height: 12,
borderRadius: 6,
position: 'absolute',
},
nfcIcon: {
position: 'absolute',
width: 60,
height: 60,
justifyContent: 'center',
alignItems: 'flex-start',
paddingLeft: 10,
},
nfcArc: {
position: 'absolute',
borderWidth: 2,
borderLeftWidth: 0,
borderTopWidth: 0,
borderBottomWidth: 0,
borderTopRightRadius: 50,
borderBottomRightRadius: 50,
},
nfcArc1: {
width: 15,
height: 20,
left: 20,
},
nfcArc2: {
width: 25,
height: 34,
left: 20,
},
nfcArc3: {
width: 35,
height: 48,
left: 20,
},
});

View File

@@ -0,0 +1 @@
export {ScanAnimation} from './ScanAnimation';

243
src/data/chipInfo.ts Normal file
View File

@@ -0,0 +1,243 @@
/**
* Educational Chip Information
*
* Provides user-friendly explanations of NFC chip types
*/
import {ChipType, ChipFamily} from '../types/detection';
export interface ChipInfo {
/** Brief description of the chip */
description: string;
/** What this chip is commonly used for */
commonUses: string[];
/** Key capabilities */
capabilities: string[];
/** Security level indicator */
securityLevel: 'low' | 'medium' | 'high';
/** Memory capacity description */
memoryNote?: string;
}
export const CHIP_FAMILY_INFO: Record<ChipFamily, string> = {
[ChipFamily.NTAG]:
'NTAG chips are NXP-branded NFC Type 2 tags designed for consumer applications like sharing URLs, contact info, and app launches. This family includes MIFARE Ultralight variants.',
[ChipFamily.MIFARE_CLASSIC]:
'MIFARE Classic is a legacy contactless smart card technology commonly used in access control and transit systems.',
[ChipFamily.MIFARE_DESFIRE]:
'MIFARE DESFire is a high-security contactless smart card with advanced encryption, used in transit, access control, and payment applications.',
[ChipFamily.MIFARE_PLUS]:
'MIFARE Plus offers enhanced security over Classic, with optional AES encryption while maintaining backward compatibility.',
[ChipFamily.ISO15693]:
'ISO 15693 (NFC-V) tags operate at longer read distances than standard NFC, commonly used in library systems and industrial applications.',
[ChipFamily.JAVACARD]:
'JavaCard chips are programmable secure elements that can run multiple cryptographic applications, used in identity and payment systems.',
[ChipFamily.UNKNOWN]:
'This chip type could not be fully identified. It may be a less common or proprietary tag.',
};
export const CHIP_INFO: Partial<Record<ChipType, ChipInfo>> = {
// NTAG family
[ChipType.NTAG213]: {
description:
'Entry-level NTAG with 144 bytes of user memory, ideal for simple NFC applications.',
commonUses: ['URL sharing', 'Smart posters', 'Product authentication'],
capabilities: ['NFC Forum Type 2', 'Password protection', 'Counter function'],
securityLevel: 'low',
memoryNote: '144 bytes user memory',
},
[ChipType.NTAG215]: {
description:
'Mid-range NTAG commonly used for amiibo and similar applications.',
commonUses: ['Gaming (amiibo)', 'Access tokens', 'Loyalty cards'],
capabilities: ['NFC Forum Type 2', 'Password protection', 'Counter function'],
securityLevel: 'low',
memoryNote: '504 bytes user memory',
},
[ChipType.NTAG216]: {
description:
'Largest standard NTAG with 888 bytes, popular for implants and data storage.',
commonUses: ['NFC implants', 'vCards', 'Complex data storage'],
capabilities: ['NFC Forum Type 2', 'Password protection', 'Counter function'],
securityLevel: 'low',
memoryNote: '888 bytes user memory',
},
[ChipType.NTAG_I2C_1K]: {
description:
'NTAG with I2C interface allowing microcontroller communication alongside NFC.',
commonUses: ['IoT devices', 'Energy harvesting', 'Field detection'],
capabilities: ['NFC + I2C interface', 'Energy harvesting', 'Field detection'],
securityLevel: 'low',
memoryNote: '888 bytes user memory',
},
[ChipType.NTAG_I2C_2K]: {
description:
'Extended memory NTAG I2C with 1904 bytes and dual interface capability.',
commonUses: ['IoT devices', 'Data logging', 'Connected products'],
capabilities: ['NFC + I2C interface', 'Energy harvesting', 'SRAM buffer'],
securityLevel: 'low',
memoryNote: '1904 bytes user memory',
},
// MIFARE Ultralight family
[ChipType.ULTRALIGHT]: {
description:
'Original MIFARE Ultralight - a simple, low-cost NFC tag with minimal memory.',
commonUses: ['Single-use tickets', 'Event passes', 'Disposable tags'],
capabilities: ['NFC Forum Type 2', 'OTP area', 'Lock bits'],
securityLevel: 'low',
memoryNote: '48 bytes user memory',
},
[ChipType.ULTRALIGHT_C]: {
description:
'Ultralight with 3DES authentication for basic security applications.',
commonUses: ['Limited-use tickets', 'Access tokens', 'Loyalty cards'],
capabilities: ['NFC Forum Type 2', '3DES authentication', 'Counter'],
securityLevel: 'medium',
memoryNote: '144 bytes user memory',
},
[ChipType.ULTRALIGHT_EV1]: {
description:
'Enhanced Ultralight with password protection and originality check.',
commonUses: ['Event tickets', 'Brand protection', 'Gaming tokens'],
capabilities: ['Password protection', 'Originality signature', 'Counter'],
securityLevel: 'low',
memoryNote: '48 or 128 bytes user memory',
},
[ChipType.ULTRALIGHT_AES]: {
description:
'Ultralight with AES-128 authentication for secure applications.',
commonUses: ['Secure tickets', 'Brand protection', 'Limited-use passes'],
capabilities: ['AES-128 authentication', 'Counter', 'Originality signature'],
securityLevel: 'high',
memoryNote: '540 bytes user memory',
},
// MIFARE Classic
[ChipType.MIFARE_CLASSIC_1K]: {
description:
'Classic 1K is the most common access card, using Crypto-1 encryption (now considered weak).',
commonUses: ['Building access', 'Hotel keys', 'Transit cards'],
capabilities: ['16 sectors', 'Key-based authentication', 'Sector permissions'],
securityLevel: 'low',
memoryNote: '1KB (752 bytes usable)',
},
[ChipType.MIFARE_CLASSIC_4K]: {
description:
'Extended capacity Classic card with 4KB memory, same security as 1K.',
commonUses: ['Building access', 'Parking systems', 'Legacy transit'],
capabilities: ['40 sectors', 'Key-based authentication', 'Sector permissions'],
securityLevel: 'low',
memoryNote: '4KB (3440 bytes usable)',
},
// DESFire
[ChipType.DESFIRE_EV1]: {
description:
'First generation DESFire with AES-128 encryption for secure applications.',
commonUses: ['Secure access control', 'Transit systems', 'Campus cards'],
capabilities: ['AES-128/3DES', 'Multiple applications', 'Flexible file system'],
securityLevel: 'high',
memoryNote: '2KB, 4KB, or 8KB options',
},
[ChipType.DESFIRE_EV2]: {
description:
'Enhanced DESFire with improved security features and proximity check.',
commonUses: ['High-security access', 'Payment cards', 'Government ID'],
capabilities: [
'AES-128',
'Proximity check',
'Transaction MAC',
'Virtual Card Architecture',
],
securityLevel: 'high',
memoryNote: '2KB, 4KB, 8KB, 16KB, or 32KB options',
},
[ChipType.DESFIRE_EV3]: {
description:
'Latest DESFire generation with enhanced cryptography and secure messaging.',
commonUses: ['Transit', 'Access control', 'Micropayment', 'Loyalty'],
capabilities: [
'AES-128',
'Secure Dynamic Messaging',
'Transaction MAC',
'Sun authentication',
],
securityLevel: 'high',
memoryNote: '2KB, 4KB, 8KB, 16KB, or 32KB options',
},
// ISO 15693
[ChipType.SLIX]: {
description:
'Basic ISO 15693 (NFC-V) tag with longer read range than standard NFC.',
commonUses: ['Library systems', 'Inventory tracking', 'Industrial tags'],
capabilities: ['Long range', 'Anti-collision', 'AFI/DSFID support'],
securityLevel: 'low',
memoryNote: '128 bytes',
},
[ChipType.SLIX2]: {
description:
'Enhanced SLIX with more memory and improved security features.',
commonUses: ['Asset tracking', 'Brand protection', 'Library systems'],
capabilities: ['Long range', 'Password protection', 'Originality signature'],
securityLevel: 'medium',
memoryNote: '316 bytes',
},
[ChipType.ICODE_DNA]: {
description:
'Cryptographically secured NFC-V tag for brand protection and authentication.',
commonUses: ['Brand protection', 'Anti-counterfeiting', 'Secure supply chain'],
capabilities: [
'Cryptographic authentication',
'Rolling code',
'Originality check',
],
securityLevel: 'high',
memoryNote: '256 bytes',
},
// JavaCard
[ChipType.JCOP4]: {
description:
'Programmable secure element supporting Java Card applets for cryptographic applications.',
commonUses: ['Identity cards', 'Digital signatures', 'Secure authentication'],
capabilities: [
'Java Card OS',
'Multiple applets',
'FIDO2/WebAuthn',
'OpenPGP',
],
securityLevel: 'high',
},
};
/**
* Get educational info for a chip type
*/
export function getChipInfo(chipType: ChipType): ChipInfo | undefined {
return CHIP_INFO[chipType];
}
/**
* Get family description for a chip family
*/
export function getChipFamilyInfo(family: ChipFamily): string {
return CHIP_FAMILY_INFO[family];
}
/**
* Get security level description
*/
export function getSecurityLevelDescription(
level: 'low' | 'medium' | 'high',
): string {
switch (level) {
case 'low':
return 'Basic security - data can be read/written with standard tools';
case 'medium':
return 'Moderate security - some protection against casual attacks';
case 'high':
return 'Strong security - cryptographic protection against cloning';
}
}

2
src/data/index.ts Normal file
View File

@@ -0,0 +1,2 @@
export * from './products';
export * from './chipInfo';

519
src/data/products.ts Normal file
View File

@@ -0,0 +1,519 @@
/**
* Dangerous Things Product Catalog
*
* This contains the implant products available from Dangerous Things
* and their chip compatibility information.
*
* Updated based on https://dangerousthings.com/category/implants/
* Discontinued products removed per https://forum.dangerousthings.com/t/shes-dead-jim/26308
*/
import { ChipType } from '../types/detection';
import {
Product,
FormFactor,
ProductCategory,
ChipProductMap,
} from '../types/products';
/**
* Chip types that can be cloned to NTAG216-based implants
* Any NTAG21x data can be written to an NTAG216 (same NFC Type 2 protocol)
*/
const NTAG21X_COMPATIBLE: ChipType[] = [
ChipType.NTAG213,
ChipType.NTAG215,
ChipType.NTAG216,
];
/**
* NTAG I2C compatible chips
*/
const NTAG_I2C_COMPATIBLE: ChipType[] = [
ChipType.NTAG_I2C_1K,
ChipType.NTAG_I2C_2K,
ChipType.NTAG_I2C_PLUS_1K,
ChipType.NTAG_I2C_PLUS_2K,
];
/**
* MIFARE Classic compatible chips (for magic card cloning)
*/
const MIFARE_CLASSIC_COMPATIBLE: ChipType[] = [
ChipType.MIFARE_CLASSIC_1K,
ChipType.MIFARE_CLASSIC_4K,
ChipType.MIFARE_CLASSIC_MINI,
];
/**
* All DESFire chips - any DESFire product works with any DESFire chip
*/
const DESFIRE_ALL: ChipType[] = [
ChipType.DESFIRE_EV1,
ChipType.DESFIRE_EV2,
ChipType.DESFIRE_EV3,
];
/**
* ICODE SLIX compatible
*/
const SLIX_COMPATIBLE: ChipType[] = [
ChipType.SLIX,
ChipType.SLIX2,
ChipType.SLIX_S,
ChipType.SLIX_L,
];
/**
* MIFARE Ultralight compatible chips (for Gen4 magic implants)
* Note: Ultralight AES cannot be cloned if AES protection is active
*/
const ULTRALIGHT_COMPATIBLE: ChipType[] = [
ChipType.ULTRALIGHT,
ChipType.ULTRALIGHT_C,
ChipType.ULTRALIGHT_EV1,
ChipType.ULTRALIGHT_NANO,
ChipType.ULTRALIGHT_AES,
];
export const PRODUCTS: Product[] = [
// ==========================================================================
// X-Series (Glass Capsule) Implants - NFC
// ==========================================================================
{
id: 'xnt',
name: 'xNT',
description:
'NFC Type 2 implant with 888 bytes of user memory.',
formFactor: FormFactor.X_SERIES,
categories: [ProductCategory.NFC],
compatibleChips: [...NTAG21X_COMPATIBLE, ...NTAG_I2C_COMPATIBLE],
features: [
'NTAG216 chip',
'888 bytes user memory',
'NFC Type 2 tag',
'URL/contact sharing',
],
url: 'https://dangerousthings.com/product/xnt/',
canReceiveClone: true,
exactMatch: true,
notes: "UID cannot be changed"
},
{
id: 'xsiid',
name: 'xSIID',
description:
'NFC implant with built-in LED that blinks when scanned. Multiple color options available.',
formFactor: FormFactor.X_SERIES,
categories: [ProductCategory.NFC, ProductCategory.LED],
compatibleChips: [...NTAG21X_COMPATIBLE, ...NTAG_I2C_COMPATIBLE],
features: [
'NTAG I2C chip',
'1kB user memory',
'URL/contact sharing',
'Red, Green, Blue, and White LEDs available',
],
url: 'https://dangerousthings.com/product/xsiid/',
canReceiveClone: true,
exactMatch: true,
notes: "UID cannot be changed"
},
{
id: 'xslx',
name: 'xSLX',
description: 'NFC Type 5 implant using ICODE SLIX chip.',
formFactor: FormFactor.X_SERIES,
categories: [ProductCategory.NFC],
compatibleChips: [...SLIX_COMPATIBLE],
features: [
'ICODE SLIX chip',
'320 bbytes user memory',
'URL/contact sharing',
],
url: 'https://dangerousthings.com/product/xslx/',
canReceiveClone: true,
exactMatch: true,
notes: "UID cannot be changed"
},
// ==========================================================================
// X-Series - Dual Frequency (NFC + 125kHz)
// ==========================================================================
{
id: 'next',
name: 'NExT',
description:
'Original dual-frequency implant combining NTAG216 (NFC) with T5577 (125kHz).',
formFactor: FormFactor.X_SERIES,
categories: [ProductCategory.DUAL_FREQUENCY, ProductCategory.ACCESS],
compatibleChips: [...NTAG21X_COMPATIBLE, ...NTAG_I2C_COMPATIBLE],
features: [
'Dual-frequency',
'NTAG216 NFC chip',
'888 bytes user memory',
'URL/contact sharing',
'T5577 125kHz chip',
'Access control (LF)',
],
url: 'https://dangerousthings.com/product/next/',
canReceiveClone: true,
exactMatch: true,
notes: 'HF UID cannot be changed. Also includes T5577 for 125kHz access systems',
},
{
id: 'next-v2',
name: 'NExT v2',
description:
'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',
'Access control (LF)',
],
url: 'https://dangerousthings.com/product/next-v2/',
canReceiveClone: true,
exactMatch: true,
notes: 'HF UID cannot be changed. Also includes T5577 for 125kHz access systems',
},
{
id: 'xmagic',
name: 'xMagic',
description:
'Dual-frequency implant with magic MIFARE Classic (changeable UID) and T5577.',
formFactor: FormFactor.X_SERIES,
categories: [ProductCategory.DUAL_FREQUENCY, ProductCategory.ACCESS],
compatibleChips: [...MIFARE_CLASSIC_COMPATIBLE],
features: [
'Magic MIFARE Classic chip',
'Changeable UID (4-byte)',
'Gen1a and gen2 magic options',
'T5577 125kHz chip',
'Access control (LF)',
],
url: 'https://dangerousthings.com/product/xmagic/',
canReceiveClone: true,
exactMatch: false,
},
// ==========================================================================
// X-Series - DESFire (Secure NFC)
// Note: xDF (EV1) and xDF2 (EV2) discontinued - see flexDF/flexDF2 for those
// ==========================================================================
{
id: 'xdf3',
name: 'xDF3',
description: 'Latest generation secure NFC implant using MIFARE DESFire EV3.',
formFactor: FormFactor.X_SERIES,
categories: [ProductCategory.SECURE, ProductCategory.ACCESS],
compatibleChips: [...DESFIRE_ALL],
features: [
'DESFire EV3 chip',
'8KB memory',
'Latest security features',
'Access control (HF)',
'URL/contact sharing',
],
url: 'https://dangerousthings.com/product/xdf3/',
canReceiveClone: false,
exactMatch: true,
notes: 'UID cannot be changed. Cannot clone data due to cryptographic protection',
desfireEvLevel: 3,
},
// ==========================================================================
// X-Series - Magic/Cloning
// ==========================================================================
{
id: 'xm1',
name: 'xM1',
description: 'Magic MIFARE Classic 1K implant with changeable UID for cloning access cards.',
formFactor: FormFactor.X_SERIES,
categories: [ProductCategory.ACCESS],
compatibleChips: [...MIFARE_CLASSIC_COMPATIBLE],
features: [
'Magic MIFARE Classic 1K',
'Gen1a and gen2 magic options',
'Changeable UID (4-byte)',
],
url: 'https://dangerousthings.com/product/xm1/',
canReceiveClone: true,
exactMatch: false,
},
// ==========================================================================
// X-Series - Cryptographic/Identity
// ==========================================================================
{
id: 'spark2',
name: 'VivoKey Spark 2',
description:
'Cryptobionic identity implant for secure authentication and digital identity.',
formFactor: FormFactor.X_SERIES,
categories: [ProductCategory.SECURE],
compatibleChips: [ChipType.NTAG424_DNA, ChipType.NTAG424_DNA_TT, ChipType.ICODE_DNA],
features: [
'VivoKey Ecosystem',
'Spark Actions',
'Verify API',
],
url: 'https://dangerousthings.com/product/vivokey-spark/',
canReceiveClone: false,
exactMatch: true,
notes: 'UID cannot be changed. Uses cryptographic authentication - cannot clone',
},
// ==========================================================================
// Bioresin Implants - Larger capsules (incision install)
// ==========================================================================
{
id: 'dnext',
name: 'dNExT',
description:
'Jumbo-sized bioresin dual-frequency implant with NTAG216 (NFC) and T5577 (125kHz). Enhanced read range.',
formFactor: FormFactor.BIORESIN,
categories: [ProductCategory.DUAL_FREQUENCY, ProductCategory.ACCESS],
compatibleChips: [...NTAG21X_COMPATIBLE, ...NTAG_I2C_COMPATIBLE],
features: [
'NTAG216 NFC chip',
'888 bytes user memory',
'URL/contact sharing',
'T5577 125kHz chip',
'Bioresin encapsulation',
'Enhanced read range',
],
url: 'https://dangerousthings.com/product/dnext/',
canReceiveClone: true,
exactMatch: true,
notes: 'HF UID cannot be changed. Larger size provides improved performance over x-series',
},
{
id: 'dug4t',
name: 'dUG4T',
description:
'Bioresin dual-frequency implant with Ultimate Gen4 magic MIFARE and T5577 for maximum compatibility.',
formFactor: FormFactor.BIORESIN,
categories: [ProductCategory.DUAL_FREQUENCY, ProductCategory.ACCESS],
compatibleChips: [...MIFARE_CLASSIC_COMPATIBLE, ...ULTRALIGHT_COMPATIBLE],
features: [
'Ultimate Gen4 magic chip',
'Changeable UID',
'Access control (HF)',
'T5577 125kHz chip',
'Changeable UID',
'Enhanced cloning capability',
'Access control (LF)',
],
url: 'https://dangerousthings.com/product/dug4t/',
canReceiveClone: true,
exactMatch: false,
notes: 'Supports 1K and 4K cloning. Also includes T5577 for LF.',
},
// ==========================================================================
// Flex Implants - NFC
// ==========================================================================
{
id: 'flexnt',
name: 'flexNT',
description:
'Flexible NTAG216 implant with larger antenna for improved read range.',
formFactor: FormFactor.FLEX,
categories: [ProductCategory.NFC],
compatibleChips: [...NTAG21X_COMPATIBLE, ...NTAG_I2C_COMPATIBLE],
features: [
'NTAG216 chip',
'888 bytes user memory',
'URL/contact sharing',
'Extended read range',
],
url: 'https://dangerousthings.com/product/flexnt/',
canReceiveClone: true,
exactMatch: true,
notes: "UID cannot be changed"
},
// ==========================================================================
// Flex Implants - DESFire (Secure)
// Note: flexDF (EV1) discontinued - flexDF2 is current
// ==========================================================================
{
id: 'flexdf2',
name: 'flexDF2',
description: 'Flexible DESFire EV2 implant with enhanced security.',
formFactor: FormFactor.FLEX,
categories: [ProductCategory.SECURE, ProductCategory.ACCESS],
compatibleChips: [...DESFIRE_ALL],
features: [
'DESFire EV2 chip',
'8KB memory',
'AES-128 encryption',
'URL/contact sharing',
],
url: 'https://dangerousthings.com/product/flexdf2/',
canReceiveClone: false,
exactMatch: true,
notes: 'UID cannot be changed. Cannot clone data due to cryptographic protection',
desfireEvLevel: 2,
},
// ==========================================================================
// Flex Implants - Magic/Cloning
// ==========================================================================
{
id: 'flexm1-v2',
name: 'flexM1 v2',
description: 'Updated flexible magic MIFARE Classic 1K implant.',
formFactor: FormFactor.FLEX,
categories: [ProductCategory.ACCESS],
compatibleChips: [...MIFARE_CLASSIC_COMPATIBLE],
features: [
'Magic MIFARE Classic 1K',
'Improved antenna design',
'Changeable UID',
'Better compatibility',
],
url: 'https://dangerousthings.com/product/flexm1-v2/',
canReceiveClone: true,
exactMatch: false,
},
// ==========================================================================
// Flex Implants - Secure Element / JavaCard
// ==========================================================================
{
id: 'apex-flex',
name: 'Apex Flex',
description:
'The ultimate subdermal security key for digital identity, cryptography, and blockchain applications.',
formFactor: FormFactor.FLEX,
categories: [ProductCategory.SECURE],
compatibleChips: [ChipType.JCOP4, ChipType.JAVACARD_UNKNOWN],
features: [
'JCOP4 secure element',
'Fidesmo platform',
'FIDO2/WebAuthn',
'OTP support',
'URL/contact sharing',
],
url: 'https://dangerousthings.com/product/apex-flex/',
canReceiveClone: false,
exactMatch: true,
notes: 'UID cannot be changed. Cannot clone - programmable via Fidesmo app',
},
{
id: 'flexsecure',
name: 'flexSecure',
description:
'Developer-friendly JavaCard implant for custom secure applications.',
formFactor: FormFactor.FLEX,
categories: [ProductCategory.SECURE],
compatibleChips: [ChipType.JCOP4, ChipType.JAVACARD_UNKNOWN],
features: [
'SmartMX3 P71 chip',
'Full JavaCard access',
'GlobalPlatformPro compatible',
'Custom applet support',
'FIDO2/WebAuthn',
'OTP support',
'URL/contact sharing',
],
url: 'https://dangerousthings.com/product/flexsecure/',
canReceiveClone: false,
exactMatch: true,
notes: 'UID cannot be changed. Developer-focused - requires technical knowledge',
},
// ==========================================================================
// Flex Implants - Access Control
// ==========================================================================
{
id: 'flexclass',
name: 'flexClass',
description: 'Flexible HID iCLASS compatible implant for enterprise access control.',
formFactor: FormFactor.FLEX,
categories: [ProductCategory.SECURE, ProductCategory.ACCESS],
compatibleChips: [], // iCLASS uses proprietary protocol
features: [
'HID iCLASS SE compatible',
'Enterprise access control',
],
url: 'https://dangerousthings.com/product/flexclass/',
canReceiveClone: false,
exactMatch: false,
notes: 'For HID iCLASS systems only',
},
{
id: 'flexug4',
name: 'flexUG4',
description: 'Flexible Ultimate Gen4 magic MIFARE implant for cloning access cards.',
formFactor: FormFactor.FLEX,
categories: [ProductCategory.ACCESS],
compatibleChips: [...MIFARE_CLASSIC_COMPATIBLE, ...ULTRALIGHT_COMPATIBLE],
features: [
'Ultimate Gen4 magic chip',
'Changeable UID',
'Enhanced cloning capability',
],
url: 'https://dangerousthings.com/product/flexug4/',
canReceiveClone: true,
exactMatch: false,
notes: 'Supports 1K and 4K cloning.',
},
];
/**
* Build a map of chip types to compatible products
*/
export function buildChipProductMap(): ChipProductMap {
const map: ChipProductMap = new Map();
for (const product of PRODUCTS) {
for (const chipType of product.compatibleChips) {
const existing = map.get(chipType) || [];
existing.push(product);
map.set(chipType, existing);
}
}
return map;
}
/**
* Cached chip-to-product map
*/
let chipProductMapCache: ChipProductMap | null = null;
/**
* Get the chip-to-product map (cached)
*/
export function getChipProductMap(): ChipProductMap {
if (!chipProductMapCache) {
chipProductMapCache = buildChipProductMap();
}
return chipProductMapCache;
}
/**
* Conversion service URL for chips that don't have direct matches
*/
export const CONVERSION_SERVICE_URL = 'https://dngr.us/conversion';
/**
* Get all products
*/
export function getAllProducts(): Product[] {
return PRODUCTS;
}
/**
* Get product by ID
*/
export function getProductById(id: string): Product | undefined {
return PRODUCTS.find(p => p.id === id);
}

View File

@@ -5,17 +5,33 @@
import {useState, useCallback, useEffect, useRef} from 'react';
import {nfcManager} from '../services/nfc';
import {detectChip} from '../services/detection';
import type {ScanState, RawTagData, ScanError, NFCStatus} from '../types/nfc';
import type {Transponder} from '../types/detection';
/** 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 {
/** Current scan state */
state: ScanState;
/** Scanned tag data (when state is 'success') */
tag: RawTagData | null;
/** Detected transponder info (when detection succeeds) */
transponder: Transponder | null | undefined;
/** Scan error (when state is 'error') */
error: ScanError | null;
/** NFC status (supported and enabled) */
nfcStatus: NFCStatus;
/** Current scan progress (during 'scanning' state) */
scanProgress: ScanProgress | null;
/** Start scanning for a tag */
startScan: () => Promise<void>;
/** Cancel ongoing scan */
@@ -29,14 +45,25 @@ 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);
// 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,
isEnabled: false,
});
const [scanProgress, setScanProgress] = useState<ScanProgress | null>(null);
// Track if component is mounted to prevent state updates after unmount
const isMounted = useRef(true);
@@ -75,8 +102,9 @@ export function useScan(): UseScanResult {
scanInProgress.current = true;
setState('scanning');
setTag(null);
setScanResult({ tag: null, transponder: undefined });
setError(null);
setScanProgress({step: 'Waiting for tag...', current: 1});
try {
// Check NFC status first
@@ -107,8 +135,27 @@ export function useScan(): UseScanResult {
return;
}
// Perform the scan
const result = await nfcManager.scanTag();
// Perform scan with detection in one session
const result = await nfcManager.scanWithDetection(async tagData => {
// Update progress: tag detected
if (isMounted.current) {
setScanProgress({step: 'Tag detected, identifying chip...', current: 2});
}
// Run chip detection while NFC session is still active
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;
});
if (!isMounted.current) {
return;
@@ -123,8 +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);
} else {
setState('error');
setError({
@@ -159,8 +216,9 @@ export function useScan(): UseScanResult {
// Reset state to idle
const reset = useCallback(() => {
setState('idle');
setTag(null);
setScanResult({ tag: null, transponder: undefined });
setError(null);
setScanProgress(null);
}, []);
// Open device NFC settings
@@ -175,9 +233,11 @@ export function useScan(): UseScanResult {
return {
state,
tag,
tag: scanResult.tag,
transponder: scanResult.transponder,
error,
nfcStatus,
scanProgress,
startScan,
cancelScan,
reset,

View File

@@ -1,8 +1,8 @@
import React from 'react';
import {StyleSheet, View} from 'react-native';
import {Button, Text, Surface} from 'react-native-paper';
import {Text, Surface} from 'react-native-paper';
import {DTButton, DTColors} from 'react-native-dt-theme';
import type {HomeScreenProps} from '../types/navigation';
import {DTColors} from '../theme';
export function HomeScreen({navigation}: HomeScreenProps) {
return (
@@ -21,14 +21,11 @@ export function HomeScreen({navigation}: HomeScreenProps) {
Scan any NFC transponder to find compatible Dangerous Things implants.
</Text>
<Button
mode="outlined"
onPress={() => navigation.navigate('Scan')}
style={styles.scanButton}
labelStyle={styles.scanButtonLabel}
contentStyle={styles.scanButtonContent}>
<DTButton
variant="normal"
onPress={() => navigation.navigate('Scan')}>
START SCAN
</Button>
</DTButton>
</View>
<View style={styles.footer}>
@@ -74,21 +71,6 @@ const styles = StyleSheet.create({
opacity: 0.9,
paddingHorizontal: 20,
},
scanButton: {
borderColor: DTColors.modeNormal,
borderWidth: 2,
borderRadius: 4,
},
scanButtonLabel: {
color: DTColors.modeNormal,
fontSize: 18,
fontWeight: '600',
letterSpacing: 2,
},
scanButtonContent: {
paddingVertical: 12,
paddingHorizontal: 32,
},
footer: {
alignItems: 'center',
paddingBottom: 20,

File diff suppressed because it is too large Load Diff

View File

@@ -1,25 +1,44 @@
import React, {useCallback, useEffect} from 'react';
import {StyleSheet, View, Platform} from 'react-native';
import {Button, Text, ActivityIndicator} from 'react-native-paper';
import type {ScanScreenProps} from '../types/navigation';
import {DTColors} from '../theme';
import {useScan} from '../hooks';
import {getScanInstructions} from '../services/nfc';
import React, { useCallback, useEffect } from 'react';
import { StyleSheet, View, Platform } from 'react-native';
import { Button, Text } from 'react-native-paper';
import { DTButton, DTColors } from 'react-native-dt-theme';
import type { ScanScreenProps } from '../types/navigation';
import { useScan } from '../hooks';
import { getScanInstructions } from '../services/nfc';
import { ScanAnimation } from '../components';
export function ScanScreen({navigation}: ScanScreenProps) {
export function ScanScreen({ navigation }: ScanScreenProps) {
const {
state,
tag,
transponder,
error,
nfcStatus,
scanProgress,
startScan,
cancelScan,
openSettings,
} = useScan();
// Navigate to results when scan succeeds
// 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,
@@ -29,9 +48,17 @@ export function ScanScreen({navigation}: ScanScreenProps) {
ats: tag.ats,
historicalBytes: tag.historicalBytes,
},
transponder: transponder ?? undefined,
});
}
}, [state, tag, navigation]);
}, [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(() => {
@@ -54,16 +81,46 @@ export function ScanScreen({navigation}: ScanScreenProps) {
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
if (!nfcStatus.isSupported && state !== 'scanning') {
return (
<View style={styles.container}>
<View style={styles.content}>
<View style={styles.errorIcon}>
<Text style={styles.errorIconText}></Text>
</View>
<Text variant="headlineMedium" style={styles.errorText}>
NFC NOT SUPPORTED
</Text>
<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>
</View>
<View style={styles.footer}>
@@ -83,20 +140,27 @@ export function ScanScreen({navigation}: ScanScreenProps) {
return (
<View style={styles.container}>
<View style={styles.content}>
<View style={styles.warningIcon}>
<Text style={styles.warningIconText}></Text>
</View>
<Text variant="headlineMedium" style={styles.warningText}>
NFC DISABLED
</Text>
<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>
{Platform.OS === 'android' && (
<Button
mode="outlined"
<DTButton
variant="emphasis"
onPress={openSettings}
style={styles.settingsButton}
labelStyle={styles.buttonLabel}>
style={styles.settingsButton}>
OPEN SETTINGS
</Button>
</DTButton>
)}
</View>
<View style={styles.footer}>
@@ -122,43 +186,54 @@ export function ScanScreen({navigation}: ScanScreenProps) {
<View style={styles.content}>
{state === 'scanning' && (
<>
<View style={styles.scanIndicator}>
<ActivityIndicator
size="large"
color={DTColors.modeNormal}
style={styles.spinner}
/>
<View style={styles.pulseRing} />
</View>
<ScanAnimation isActive={true} size={180} />
<Text variant="headlineMedium" style={styles.scanningText}>
SCANNING
</Text>
<Text variant="bodyLarge" style={styles.instructionText}>
{getScanInstructions()}
</Text>
<Text variant="headlineSmall" style={{ paddingTop: 15, paddingBottom: 10 }}>
Not Scanning?
</Text>
<Text variant="bodyLarge" style={styles.instructionText}>
Phones can only read high frequency (13.56 MHz) -- low and ultra high frequency transponders cannot be scanned.
</Text>
{scanProgress && scanProgress.current > 1 && (
<Text variant="bodySmall" style={styles.progressText}>
{scanProgress.step}
</Text>
)}
</>
)}
{state === 'error' && (
<>
<View style={styles.errorIcon}>
<Text style={styles.errorIconText}>!</Text>
</View>
<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 variant="bodyLarge" style={styles.errorMessage}>
{error?.message || 'Unable to read NFC tag'}
</Text>
{error?.type === 'TAG_LOST' && (
{getErrorHint() && (
<Text variant="bodyMedium" style={styles.hintText}>
Hold the tag steady until the scan completes
{getErrorHint()}
</Text>
)}
<Button
mode="outlined"
onPress={handleRetry}
style={styles.retryButton}
labelStyle={styles.buttonLabel}>
<DTButton
variant="normal"
onPress={handleRetry}>
TRY AGAIN
</Button>
</DTButton>
</>
)}
@@ -167,25 +242,26 @@ export function ScanScreen({navigation}: ScanScreenProps) {
<Text variant="headlineMedium" style={styles.readyText}>
READY TO SCAN
</Text>
<Button
mode="outlined"
onPress={startScan}
style={styles.startButton}
labelStyle={styles.buttonLabel}>
<DTButton
variant="normal"
onPress={startScan}>
START
</Button>
</DTButton>
</>
)}
{state === 'processing' && (
<>
<ActivityIndicator
size="large"
<ScanAnimation
isActive={true}
size={180}
color={DTColors.modeEmphasis}
style={styles.processingSpinner}
/>
<Text variant="headlineMedium" style={styles.processingText}>
PROCESSING
IDENTIFYING
</Text>
<Text variant="bodyMedium" style={styles.detectingHint}>
Analyzing chip type...
</Text>
</>
)}
@@ -214,27 +290,10 @@ const styles = StyleSheet.create({
justifyContent: '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: {
color: DTColors.modeNormal,
letterSpacing: 4,
marginTop: 32,
marginBottom: 16,
},
instructionText: {
@@ -243,11 +302,47 @@ const styles = StyleSheet.create({
textAlign: 'center',
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: {
color: DTColors.modeWarning,
letterSpacing: 2,
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: {
color: DTColors.modeEmphasis,
letterSpacing: 2,
@@ -268,13 +363,7 @@ const styles = StyleSheet.create({
paddingHorizontal: 20,
fontStyle: 'italic',
},
retryButton: {
borderColor: DTColors.modeNormal,
borderWidth: 2,
},
settingsButton: {
borderColor: DTColors.modeEmphasis,
borderWidth: 2,
marginTop: 16,
},
readyText: {
@@ -282,20 +371,15 @@ const styles = StyleSheet.create({
letterSpacing: 2,
marginBottom: 32,
},
startButton: {
borderColor: DTColors.modeNormal,
borderWidth: 2,
},
buttonLabel: {
color: DTColors.modeNormal,
letterSpacing: 2,
},
processingSpinner: {
marginBottom: 24,
},
processingText: {
color: DTColors.modeEmphasis,
letterSpacing: 4,
marginTop: 32,
},
detectingHint: {
color: DTColors.light,
opacity: 0.7,
marginTop: 12,
},
footer: {
alignItems: 'center',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,387 @@
/**
* MIFARE Detector
* Identifies MIFARE Classic 1K/4K/Mini based on SAK values
* Also provides stubs for DESFire and Plus detection (Phase 4)
*/
import {Platform} from 'react-native';
import {ChipType, CHIP_MEMORY_SIZES} from '../../types/detection';
/**
* SAK (Select Acknowledge) values for MIFARE chips
*
* SAK is returned during ISO 14443-3A anticollision and indicates card capabilities
*/
const MIFARE_SAK_VALUES = {
// MIFARE Classic 1K variants
CLASSIC_1K: 0x08,
CLASSIC_1K_SMARTMX: 0x28, // Classic 1K emulation on SmartMX
CLASSIC_1K_INFINEON: 0x88, // Infineon variant
// MIFARE Classic 4K variants
CLASSIC_4K: 0x18,
CLASSIC_4K_SMARTMX: 0x38, // Classic 4K emulation on SmartMX
CLASSIC_4K_INFINEON: 0x98, // Infineon variant
// MIFARE Classic 2K (rare)
CLASSIC_2K: 0x19,
// MIFARE Classic Mini
CLASSIC_MINI: 0x09,
// MIFARE Classic 1K with UID changeable (magic cards often)
CLASSIC_1K_UID_CHANGEABLE: 0x01,
// Cards with ISO 14443-4 support (bit 5 set)
// These might be DESFire, Plus, or SmartMX
ISO_DEP_CAPABLE: 0x20,
} as const;
// All SAK values that indicate MIFARE Classic
const ALL_CLASSIC_1K_SAKS: number[] = [
MIFARE_SAK_VALUES.CLASSIC_1K,
MIFARE_SAK_VALUES.CLASSIC_1K_SMARTMX,
MIFARE_SAK_VALUES.CLASSIC_1K_INFINEON,
MIFARE_SAK_VALUES.CLASSIC_1K_UID_CHANGEABLE,
];
const ALL_CLASSIC_4K_SAKS: number[] = [
MIFARE_SAK_VALUES.CLASSIC_4K,
MIFARE_SAK_VALUES.CLASSIC_4K_SMARTMX,
MIFARE_SAK_VALUES.CLASSIC_4K_INFINEON,
MIFARE_SAK_VALUES.CLASSIC_2K, // 2K treated as 4K variant
];
/**
* Result of MIFARE Classic detection
*/
export interface MifareClassicDetectionResult {
success: boolean;
chipType?: ChipType;
memorySize?: number;
sectorCount?: number;
blockCount?: number;
note?: string;
}
/**
* Detect MIFARE Classic variant from SAK value
*/
export function detectMifareClassic(sak: number): MifareClassicDetectionResult {
// Check for MIFARE Classic 1K variants
if (ALL_CLASSIC_1K_SAKS.includes(sak)) {
return {
success: true,
chipType: ChipType.MIFARE_CLASSIC_1K,
memorySize: CHIP_MEMORY_SIZES[ChipType.MIFARE_CLASSIC_1K],
sectorCount: 16,
blockCount: 64,
note:
Platform.OS === 'ios'
? 'Sector operations require Android'
: undefined,
};
}
// Check for MIFARE Classic 4K variants
if (ALL_CLASSIC_4K_SAKS.includes(sak)) {
return {
success: true,
chipType: ChipType.MIFARE_CLASSIC_4K,
memorySize: CHIP_MEMORY_SIZES[ChipType.MIFARE_CLASSIC_4K],
sectorCount: 40, // 32 small sectors + 8 large sectors
blockCount: 256,
note:
Platform.OS === 'ios'
? 'Sector operations require Android'
: undefined,
};
}
// Check for MIFARE Classic Mini (SAK 0x09)
if (sak === MIFARE_SAK_VALUES.CLASSIC_MINI) {
return {
success: true,
chipType: ChipType.MIFARE_CLASSIC_MINI,
memorySize: CHIP_MEMORY_SIZES[ChipType.MIFARE_CLASSIC_MINI],
sectorCount: 5,
blockCount: 20,
note:
Platform.OS === 'ios'
? 'Sector operations require Android'
: undefined,
};
}
return {
success: false,
};
}
/**
* Check if SAK indicates a MIFARE Classic chip
*/
export function isMifareClassicSak(sak: number): boolean {
return (
ALL_CLASSIC_1K_SAKS.includes(sak) ||
ALL_CLASSIC_4K_SAKS.includes(sak) ||
sak === MIFARE_SAK_VALUES.CLASSIC_MINI
);
}
/**
* Check if SAK indicates ISO 14443-4 (ISO-DEP) capability
* This means the chip might be DESFire, Plus, or SmartMX
*/
export function hasIsoDepCapability(sak: number): boolean {
// Bit 5 (0x20) indicates ISO 14443-4 compliance
return (sak & 0x20) !== 0;
}
/**
* Get human-readable description of SAK value
*/
export function describeSak(sak: number): string {
if (sak === MIFARE_SAK_VALUES.CLASSIC_1K) {
return 'MIFARE Classic 1K';
}
if (sak === MIFARE_SAK_VALUES.CLASSIC_4K) {
return 'MIFARE Classic 4K';
}
if (sak === MIFARE_SAK_VALUES.CLASSIC_2K) {
return 'MIFARE Classic 2K';
}
if (sak === MIFARE_SAK_VALUES.CLASSIC_MINI) {
return 'MIFARE Classic Mini';
}
if (sak === 0x00) {
return 'Type 2 Tag (NTAG/Ultralight)';
}
if (hasIsoDepCapability(sak)) {
return 'ISO 14443-4 capable (DESFire/Plus/SmartMX)';
}
return `Unknown (SAK: 0x${sak.toString(16).padStart(2, '0')})`;
}
/**
* iOS MIFARE Classic limitation info
*/
export const IOS_MIFARE_CLASSIC_NOTE =
'iOS can detect MIFARE Classic but cannot perform sector-level operations. ' +
'For cloning or data extraction, an Android device is required.';
// ============================================================================
// SAK Swap Detection
// ============================================================================
/**
* SAK values that indicate potential SAK swap capability
*
* SAK swap refers to chips that can operate in multiple modes:
* - MIFARE Plus in SL1 emulates Classic but can switch to SL3
* - Some magic/clone cards have mutable SAK values
* - DESFire cards with MIFARE Classic emulation
*/
const SAK_SWAP_INDICATORS = {
// MIFARE Plus SL1 (emulating Classic 1K but can upgrade)
PLUS_SL1_2K: 0x08, // Same as Classic 1K but actually Plus
PLUS_SL1_4K: 0x18, // Same as Classic 4K but actually Plus
// MIFARE Plus SL2/SL3 (ISO-DEP mode)
PLUS_SL2_2K: 0x10,
PLUS_SL2_4K: 0x11,
PLUS_SL3_2K: 0x20,
PLUS_SL3_4K: 0x20,
// DESFire with MIFARE Application
DESFIRE_WITH_CLASSIC: 0x28, // DESFire + Classic emulation
// Known magic card indicators (Gen2/CUID often have unusual ATQA)
MAGIC_INDICATOR: 0x00,
} as const;
/**
* ATQA patterns that might indicate special cards
*/
const SUSPICIOUS_ATQA_PATTERNS = {
// Gen1a magic cards often have ATQA 0x0400
GEN1A_MAGIC: '04:00',
// Gen2/CUID cards
GEN2_MAGIC: '08:04',
// Standard Classic 1K
CLASSIC_1K: '00:04',
// Standard Classic 4K
CLASSIC_4K: '00:02',
};
/**
* SAK swap detection result
*/
export interface SakSwapDetection {
/** Whether SAK swap capability was detected */
hasSakSwap: boolean;
/** Type of SAK swap if detected */
swapType?:
| 'mifare_plus_sl1'
| 'desfire_with_classic'
| 'magic_card'
| 'unknown';
/** Confidence in the detection */
confidence: 'high' | 'medium' | 'low';
/** Human-readable description */
description: string;
/** Additional notes */
notes?: string[];
}
/**
* Detect if a tag might have SAK swap capability
*
* This checks for indicators that suggest the tag can operate in
* multiple modes or has been modified from factory defaults.
*/
export function detectSakSwap(
sak: number,
atqa?: string,
historicalBytes?: string,
): SakSwapDetection {
const notes: string[] = [];
// Check for MIFARE Plus in SL1 mode
// Plus in SL1 looks identical to Classic, but historical bytes may differ
if (
(sak === SAK_SWAP_INDICATORS.PLUS_SL1_2K ||
sak === SAK_SWAP_INDICATORS.PLUS_SL1_4K) &&
historicalBytes
) {
// MIFARE Plus typically has specific historical bytes patterns
if (
historicalBytes.includes('C1') ||
historicalBytes.includes('80:02')
) {
notes.push('Historical bytes suggest MIFARE Plus in SL1 mode');
return {
hasSakSwap: true,
swapType: 'mifare_plus_sl1',
confidence: 'medium',
description:
'MIFARE Plus in Security Level 1 (emulating Classic). Can be switched to SL2/SL3 with cryptographic authentication.',
notes,
};
}
}
// Check for DESFire with MIFARE Classic application
if (sak === SAK_SWAP_INDICATORS.DESFIRE_WITH_CLASSIC) {
return {
hasSakSwap: true,
swapType: 'desfire_with_classic',
confidence: 'high',
description:
'DESFire with MIFARE Classic emulation. Tag operates as both DESFire and Classic.',
notes: ['Full DESFire functionality available via ISO-DEP'],
};
}
// Check for Magic card indicators via ATQA
if (atqa) {
const cleanAtqa = atqa.toUpperCase();
// Gen1a magic cards have unusual ATQA patterns
if (
cleanAtqa === SUSPICIOUS_ATQA_PATTERNS.GEN1A_MAGIC &&
(sak === 0x08 || sak === 0x18)
) {
notes.push('ATQA pattern suggests Gen1a magic card');
return {
hasSakSwap: true,
swapType: 'magic_card',
confidence: 'medium',
description:
'Possible Gen1a magic card (UID-writable). SAK and UID can be modified with special commands.',
notes,
};
}
// Check for mismatched ATQA/SAK (common in clones)
const isClassic1kSak = sak === 0x08;
const isClassic4kSak = sak === 0x18;
const isClassic1kAtqa = cleanAtqa === SUSPICIOUS_ATQA_PATTERNS.CLASSIC_1K;
const isClassic4kAtqa = cleanAtqa === SUSPICIOUS_ATQA_PATTERNS.CLASSIC_4K;
if (
(isClassic1kSak && isClassic4kAtqa) ||
(isClassic4kSak && isClassic1kAtqa)
) {
notes.push('SAK/ATQA mismatch suggests modified or clone card');
return {
hasSakSwap: true,
swapType: 'magic_card',
confidence: 'low',
description:
'SAK and ATQA values are inconsistent. May be a magic/clone card with modified parameters.',
notes,
};
}
}
// Check for Plus SL2/SL3 modes
if (
sak === SAK_SWAP_INDICATORS.PLUS_SL2_2K ||
sak === SAK_SWAP_INDICATORS.PLUS_SL2_4K
) {
return {
hasSakSwap: true,
swapType: 'mifare_plus_sl1',
confidence: 'high',
description:
'MIFARE Plus in Security Level 2. Supports both Classic commands and AES authentication.',
notes: ['Can fall back to SL1 (Classic) mode in some configurations'],
};
}
if (
sak === SAK_SWAP_INDICATORS.PLUS_SL3_2K ||
sak === SAK_SWAP_INDICATORS.PLUS_SL3_4K
) {
// SL3 may look like generic ISO-DEP
if (historicalBytes?.includes('C1')) {
return {
hasSakSwap: true,
swapType: 'mifare_plus_sl1',
confidence: 'medium',
description:
'MIFARE Plus in Security Level 3 (AES-only mode). May have originated from SL1 configuration.',
notes: ['Cannot fall back to Classic mode once in SL3'],
};
}
}
// No SAK swap detected
return {
hasSakSwap: false,
confidence: 'high',
description: 'Standard tag with no SAK swap capability detected.',
};
}
/**
* Check if tag might be a magic/clone card based on behavior
*/
export function mightBeMagicCard(sak: number, atqa?: string): boolean {
// Gen1a magic cards often have ATQA 0x0400
if (atqa === SUSPICIOUS_ATQA_PATTERNS.GEN1A_MAGIC) {
return true;
}
// SAK 0x00 with NfcA tech might be magic NTAG
if (sak === 0x00 && atqa === '00:44') {
return true;
}
return false;
}

View File

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

View File

@@ -0,0 +1,15 @@
/**
* Product Matching Service
*/
export {
matchChipToProducts,
getMatchSummary,
getRecommendedAction,
formatProductFeatures,
getCloneTargetsForChip,
canCloneToProduct,
getDesfireEvLevel,
getDesfireEvMismatchWarning,
getMifareClassicCapacityWarning,
} from './matcher';

View File

@@ -0,0 +1,232 @@
/**
* Product Matching Service
*
* Matches detected NFC chips to compatible Dangerous Things products
*/
import { ChipType, getChipFamily, CHIP_CLONEABILITY, ChipFamily, Transponder } from '../../types/detection';
import { MatchResult, Product, DesfireEvLevel } from '../../types/products';
import {
PRODUCTS,
getChipProductMap,
CONVERSION_SERVICE_URL,
} from '../../data/products';
/**
* Match a detected chip to compatible products
*/
export function matchChipToProducts(chip: Transponder): MatchResult {
const chipType = chip.type as ChipType
const chipProductMap = getChipProductMap();
const cloneability = CHIP_CLONEABILITY[chipType];
const chipFamily = getChipFamily(chipType);
// Get products that directly support this chip
const directMatches = chipProductMap.get(chipType) || [];
// Separate exact matches from clone targets
const exactMatches: Product[] = [];
const cloneTargets: Product[] = [];
for (const product of directMatches) {
if (product.exactMatch) {
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);
// Determine if conversion is recommended
// Conversion is recommended when:
// 1. No exact matches AND no clone targets
// 2. Chip is not cloneable (crypto protection)
const hasMatches = exactMatches.length > 0 || cloneTargets.length > 0;
const conversionRecommended = !hasMatches || !cloneability?.cloneable;
return {
exactMatches,
cloneTargets,
familyMatches,
isCloneable: cloneability?.cloneable ?? false,
cloneabilityNote: cloneability?.note,
conversionRecommended,
conversionUrl: CONVERSION_SERVICE_URL,
};
}
/**
* Find products in the same chip family that might be of interest
*/
function findFamilyMatches(
chipType: ChipType,
chipFamily: ChipFamily,
excludeProducts: Product[],
): Product[] {
const excludeIds = new Set(excludeProducts.map(p => p.id));
const familyMatches: Product[] = [];
for (const product of PRODUCTS) {
// Skip already matched products
if (excludeIds.has(product.id)) {
continue;
}
// Check if any of the product's compatible chips are in the same family
const productChipFamilies = product.compatibleChips.map(getChipFamily);
if (productChipFamilies.includes(chipFamily)) {
familyMatches.push(product);
}
}
return familyMatches;
}
/**
* Get a summary message for the match result
*/
export function getMatchSummary(result: MatchResult, chipName: string): string {
if (result.exactMatches.length > 0) {
const names = result.exactMatches.map(p => p.name).join(', ');
return `Your ${chipName} is compatible with: ${names}`;
}
if (result.cloneTargets.length > 0) {
const names = result.cloneTargets.map(p => p.name).join(', ');
return `Your ${chipName} data can be cloned to: ${names}`;
}
if (!result.isCloneable) {
return `${chipName} uses cryptographic protection and cannot be cloned. Consider our conversion service for compatible options.`;
}
return `No direct product match found for ${chipName}. Check our conversion service for options.`;
}
/**
* Get recommended action based on match result
*/
export function getRecommendedAction(
result: MatchResult,
): 'purchase' | 'clone' | 'conversion' | 'contact' {
if (result.exactMatches.length > 0) {
return 'purchase';
}
if (result.cloneTargets.length > 0 && result.isCloneable) {
return 'clone';
}
if (result.conversionRecommended) {
return 'conversion';
}
return 'contact';
}
/**
* Format product features for display
*/
export function formatProductFeatures(product: Product): string[] {
return product.features;
}
/**
* Get products that can receive cloned data from a chip type
*/
export function getCloneTargetsForChip(chipType: ChipType): Product[] {
const cloneability = CHIP_CLONEABILITY[chipType];
if (!cloneability?.cloneable) {
return [];
}
const chipProductMap = getChipProductMap();
const directMatches = chipProductMap.get(chipType) || [];
return directMatches.filter(p => p.canReceiveClone);
}
/**
* Check if a chip can be cloned to any product
*/
export function canCloneToProduct(chipType: ChipType): boolean {
return getCloneTargetsForChip(chipType).length > 0;
}
/**
* Get DESFire EV level from chip type
*/
export function getDesfireEvLevel(chipType: ChipType): DesfireEvLevel | null {
switch (chipType) {
case ChipType.DESFIRE_EV1:
return 1;
case ChipType.DESFIRE_EV2:
return 2;
case ChipType.DESFIRE_EV3:
return 3;
default:
return null;
}
}
/**
* IDs of products that only support MIFARE Classic 1K (not 4K)
*/
const MIFARE_1K_ONLY_PRODUCTS = new Set(['xmagic', 'xm1', 'flexm1-v2']);
/**
* Check if a scanned 4K card is being matched to a 1K-only implant
* Returns warning message if capacity mismatch, null if no issue
*/
export function getMifareClassicCapacityWarning(
chipType: ChipType,
product: Product,
): string | null {
// Only applies to MIFARE Classic 4K cards
if (chipType !== ChipType.MIFARE_CLASSIC_4K) {
return null;
}
// Only warn for 1K-only implants
if (!MIFARE_1K_ONLY_PRODUCTS.has(product.id)) {
return null;
}
return 'This implant has 1K memory only - might not have capacity to clone your 4K card.';
}
/**
* Check if there's a DESFire EV mismatch between chip and product
* Returns warning message if mismatch, null if no issue
*/
export function getDesfireEvMismatchWarning(
chipType: ChipType,
product: Product,
): string | null {
const chipEvLevel = getDesfireEvLevel(chipType);
const productEvLevel = product.desfireEvLevel;
// Not a DESFire chip or product doesn't have EV level
if (chipEvLevel === null || productEvLevel === undefined) {
return null;
}
// Perfect match
if (chipEvLevel === productEvLevel) {
return null;
}
// Mismatch - warn user
if (chipEvLevel < productEvLevel) {
return `Your card uses DESFire EV${chipEvLevel}, but this implant uses EV${productEvLevel}. Some newer features may not be compatible with your existing system.`;
} else {
return `Your card uses DESFire EV${chipEvLevel}, but this implant uses EV${productEvLevel}. This should work, but you won't have access to EV${chipEvLevel} features.`;
}
}

View File

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

View File

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

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

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

View File

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

View File

@@ -18,6 +18,32 @@ 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[];
}
/**
* MIFARE Classic specific info from Android
*/
export interface MifareClassicInfo {
/** Total memory size in bytes */
size: number;
/** Number of sectors */
sectorCount: number;
/** Total number of blocks */
blockCount: number;
}
/**
* Raw tag data from NFC scan
*/
@@ -36,6 +62,10 @@ export interface RawTagData {
historicalBytes?: string;
/** Maximum transceive length */
maxTransceiveLength?: number;
/** Cached NDEF records (if tag supports NDEF) */
ndefRecords?: NdefRecord[];
/** MIFARE Classic info (Android only) */
mifareClassic?: MifareClassicInfo;
}
/**

78
src/types/products.ts Normal file
View File

@@ -0,0 +1,78 @@
/**
* Product Types for Dangerous Things Implants
*/
import {ChipType} from './detection';
/**
* Product form factor
*/
export enum FormFactor {
X_SERIES = 'X_SERIES', // 2x12mm injectable capsule (glass or bioresin)
FLEX = 'FLEX', // Flexible PCB implant (incision install)
BIORESIN = 'BIORESIN', // Larger bioresin capsule (incision install)
CARD = 'CARD', // ISO card format
}
/**
* Product category
*/
export enum ProductCategory {
NFC = 'NFC', // NFC-only implants
DUAL_FREQUENCY = 'DUAL_FREQUENCY', // NFC + 125kHz
SECURE = 'SECURE', // Cryptographic/secure elements
LED = 'LED', // Implants with LED indicators
ACCESS = 'ACCESS', // Access control focused
}
/**
* DESFire EV level for version matching
*/
export type DesfireEvLevel = 1 | 2 | 3;
/**
* A Dangerous Things product
*/
export interface Product {
id: string;
name: string;
description: string;
formFactor: FormFactor;
categories: ProductCategory[];
compatibleChips: ChipType[];
features: string[];
url: string;
/** Whether this product can have data cloned TO it from the scanned chip */
canReceiveClone: boolean;
/** Whether this product uses the same chip type as scanned */
exactMatch: boolean;
/** Notes about compatibility or limitations */
notes?: string;
/** DESFire EV level (1, 2, or 3) for version mismatch warnings */
desfireEvLevel?: DesfireEvLevel;
}
/**
* Result of product matching
*/
export interface MatchResult {
/** Products that use the exact same chip */
exactMatches: Product[];
/** Products that can receive cloned data from this chip */
cloneTargets: Product[];
/** Products in the same chip family */
familyMatches: Product[];
/** Is this chip cloneable at all? */
isCloneable: boolean;
/** Note about cloneability */
cloneabilityNote?: string;
/** Conversion service recommendation */
conversionRecommended: boolean;
/** Conversion URL */
conversionUrl: string;
}
/**
* Chip to product mapping for quick lookup
*/
export type ChipProductMap = Map<ChipType, Product[]>;

33
src/utils/logger.ts Normal file
View File

@@ -0,0 +1,33 @@
/**
* Logger utility that only outputs in development mode
* All console.log statements in the app should use this logger
*/
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;

View File

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