UNPKG

emv

Version:

EMV / Chip and PIN CLI and library for PC/SC card readers

255 lines 9.97 kB
#!/usr/bin/env node import { jsx as _jsx } from "react/jsx-runtime"; /** * Interactive EMV CLI with a beautiful, modern UX * Inspired by Claude Code and OpenCode */ import { useState, useEffect, useCallback } from 'react'; import { render, useApp, useInput } from 'ink'; import { WelcomeScreen, ReadersScreen, WaitingScreen, AppsScreen, SelectedAppScreen, PinScreen, PinResultScreen, ExploreScreen, ErrorScreen, } from './screens/index.js'; import { SCARD_STATE_PRESENT, } from './types.js'; function App() { const { exit } = useApp(); const [screen, setScreen] = useState('welcome'); const [readers, setReaders] = useState([]); const [loading, setLoading] = useState(false); const [selectedReader, setSelectedReader] = useState(null); const [devices, setDevices] = useState(null); const [emv, setEmv] = useState(null); const [apps, setApps] = useState([]); const [atr, setAtr] = useState(''); const [selectedApp, setSelectedApp] = useState(null); const [pinLoading, setPinLoading] = useState(false); const [pinResult, setPinResult] = useState(null); const [pinAttemptsLeft, setPinAttemptsLeft] = useState(undefined); const [error, setError] = useState(null); // Handle quit useInput((input) => { if (input === 'q') { if (devices) { devices.stop(); } exit(); } }); // Initialize devices useEffect(() => { let mounted = true; const initDevices = async () => { try { const { Devices } = await import('smartcard'); const d = new Devices(); if (mounted) { setDevices(d); } } catch { if (mounted) { setError('Failed to initialize PC/SC. Is pcscd running?'); setScreen('error'); } } }; void initDevices(); return () => { mounted = false; }; }, []); // Refresh readers const refreshReaders = useCallback(() => { if (!devices) return; setLoading(true); devices.start(); setTimeout(() => { const readerList = devices.listReaders(); setReaders(readerList); setLoading(false); }, 200); }, [devices]); // Helper function to read apps from a card const readAppsFromCard = useCallback(async (readerName, card) => { setLoading(true); setScreen('apps'); try { const { EmvApplication: EmvApp } = await import('../emv-application.js'); const emvApp = new EmvApp({ name: readerName }, card); setEmv(emvApp); setAtr(card.atr?.toString('hex') ?? ''); // Use the discoverApplications method to read apps from PSE const result = await emvApp.discoverApplications(); if (result.success) { setApps(result.apps); } setLoading(false); } catch (err) { setError(err instanceof Error ? err.message : String(err)); setScreen('error'); setLoading(false); } }, []); // Handle reader selection const handleReaderSelect = useCallback((reader) => { setSelectedReader(reader); // Clear previous state when selecting a new reader setApps([]); setEmv(null); setAtr(''); const hasCard = (reader.state & SCARD_STATE_PRESENT) !== 0; if (hasCard && devices) { // Get the already-connected card directly from the library const card = devices.getCard(reader.name); if (card) { void readAppsFromCard(reader.name, card); } else { // Card present but not yet connected - wait for card-inserted event setLoading(true); setScreen('apps'); const handleCardInserted = (event) => { const cardEvent = event; if (cardEvent.reader.name === reader.name) { devices.off('card-inserted', handleCardInserted); void readAppsFromCard(reader.name, cardEvent.card); } }; devices.on('card-inserted', handleCardInserted); } } else { // Wait for card insertion setScreen('waiting'); if (devices) { const handleCardInserted = (event) => { const cardEvent = event; if (cardEvent.reader.name === reader.name) { devices.off('card-inserted', handleCardInserted); void readAppsFromCard(reader.name, cardEvent.card); } }; devices.on('card-inserted', handleCardInserted); } } }, [devices, readAppsFromCard]); // Handle app selection const handleAppSelect = useCallback(async (app) => { if (!emv) return; setSelectedApp(app); setLoading(true); try { const aidBuffer = Buffer.from(app.aid, 'hex'); await emv.selectApplication(aidBuffer); setScreen('selected'); } catch (err) { setError(err instanceof Error ? err.message : String(err)); setScreen('error'); } finally { setLoading(false); } }, [emv]); // Handle PIN verification const handlePinSubmit = useCallback(async (pin) => { if (!emv) return; setPinLoading(true); try { const response = await emv.verifyPin(pin); if (response.isOk()) { setPinResult({ success: true, message: 'PIN verified successfully!' }); } else if (response.sw1 === 0x63 && (response.sw2 & 0xf0) === 0xc0) { const attempts = response.sw2 & 0x0f; setPinAttemptsLeft(attempts); setPinResult({ success: false, message: 'Wrong PIN.', attemptsLeft: attempts }); } else if (response.sw1 === 0x69 && response.sw2 === 0x83) { setPinResult({ success: false, message: 'PIN is blocked! Card cannot be used.', }); } else { setPinResult({ success: false, message: `Verification failed: SW=${response.sw1.toString(16)}${response.sw2.toString(16)}`, }); } setScreen('pin-result'); } catch (err) { setPinResult({ success: false, message: err instanceof Error ? err.message : String(err), }); setScreen('pin-result'); } finally { setPinLoading(false); } }, [emv]); // Render current screen switch (screen) { case 'welcome': return (_jsx(WelcomeScreen, { onContinue: () => { setScreen('readers'); refreshReaders(); } })); case 'readers': return (_jsx(ReadersScreen, { readers: readers, onSelect: handleReaderSelect, onRefresh: refreshReaders, loading: loading })); case 'waiting': return _jsx(WaitingScreen, { readerName: selectedReader?.name ?? 'Unknown' }); case 'apps': return (_jsx(AppsScreen, { apps: apps, readerName: selectedReader?.name ?? 'Unknown', atr: atr, onSelect: (app) => void handleAppSelect(app), onBack: () => { setScreen('readers'); }, loading: loading })); case 'selected': return selectedApp ? (_jsx(SelectedAppScreen, { app: selectedApp, onVerifyPin: () => { setScreen('pin'); }, onExplore: () => { setScreen('explore'); }, onBack: () => { setScreen('apps'); } })) : (_jsx(ErrorScreen, { message: "No app selected", onBack: () => { setScreen('apps'); } })); case 'pin': return (_jsx(PinScreen, { onSubmit: (pin) => void handlePinSubmit(pin), onBack: () => { setScreen('selected'); }, loading: pinLoading, attemptsLeft: pinAttemptsLeft })); case 'pin-result': return pinResult ? (_jsx(PinResultScreen, { success: pinResult.success, message: pinResult.message, attemptsLeft: pinResult.attemptsLeft, onContinue: () => { setScreen('selected'); } })) : (_jsx(ErrorScreen, { message: "No result", onBack: () => { setScreen('selected'); } })); case 'explore': return emv && selectedApp ? (_jsx(ExploreScreen, { emv: emv, app: selectedApp, onBack: () => { setScreen('selected'); } })) : (_jsx(ErrorScreen, { message: "No EMV connection", onBack: () => { setScreen('selected'); } })); case 'error': return (_jsx(ErrorScreen, { message: error ?? 'Unknown error', onBack: () => { setScreen('readers'); } })); default: return (_jsx(ErrorScreen, { message: "Unknown screen", onBack: () => { setScreen('welcome'); } })); } } export function runInteractive() { render(_jsx(App, {})); } // Export PinScreen for testing export { PinScreen } from './screens/index.js'; // Run if executed directly const isMainModule = process.argv[1]?.endsWith('interactive.js') ?? false; if (isMainModule) { runInteractive(); } //# sourceMappingURL=index.js.map