A desktop app that traces APDUs (Application Protocol Data Units) sent to
and received from a smart card reader. It captures live USB traffic with
Wireshark's tshark over a USBPcap interface — or replays a saved
.pcap / .pcapng capture file — decodes the CCID frames, and shows each
APDU with full ISO 7816-4, EMV, PIV, OpenPGP and GlobalPlatform decoding.
Built with Electron and TypeScript.
- USBPcap interface picker with one-click start / stop tracing.
- Open a
.pcap/.pcapngfile to analyse a trace offline (tshark -r). - Pairs each command with its response per USB device address, so multiple readers on one host controller don't cross-contaminate latency or TLV gating.
- Captures and decodes the ATR from
IccPowerOn— convention (direct / inverse), offered transmission protocols, and historical bytes (with the category indicator and COMPACT-TLV data objects named). - Configurable
tshark.exepath; a Locate tshark… button surfaces in the header when tshark can't be invoked.
- Simple — one APDU per line, with the time gap since the previous APDU.
- Detailed — each APDU as a card:
- Decoded instruction name (ISO 7816-4, with GlobalPlatform names under a
proprietary
CLA),CLA/INS/P1/P2/Lc/Lefields, ISO case 1–4, a secure-messaging chip when the CLA flags SM, and aP1/P2note (SELECT method, READ BINARY/RECORD offsets, INSTALL phase, GET STATUS subset, …). - Response status word with its meaning, command↔response latency, and a collapsible BER-TLV tree for well-formed data.
- Per-card Copy button puts the raw hex on the clipboard.
- Decoded instruction name (ISO 7816-4, with GlobalPlatform names under a
proprietary
- Summary — per-session totals, error rate, latency min/avg/max and the slowest exchange, plus a command-by-instruction breakdown.
- Decode — paste APDU hex and decode it without capturing, using the active TLV profile.
- Application-aware tag dictionaries — EMV, PIV, OpenPGP — layered
over the generic ISO 7816-4 set, auto-selected from the
SELECTAID or chosen manually from the profile dropdown. - Primitive values rendered in human-readable form: ASCII text, life-cycle byte,
BCD dates (
20YY-MM-DD) and times (HH:MM:SS), numeric fields, and binary counters. - Under the EMV profile, structured / bit-field tags are decoded too:
- AIP (
82), TVR (95), AUC (9F07) and TSI (9B) — listed as their set-flag meanings. - AFL (
94) — record ranges per file (SFI, first–last, offline-auth count). - CVM List (
8E) — verification rules (method + condition).
- AIP (
- Filter & search by direction, Issues only, or hex / instruction /
status text (
Ctrl + F).F3jumps the Detailed view to the next error or warning, flashing the target card. - Copy any APDU's hex; Import a saved JSON trace; Export to
.txt,.jsonor.csv— honouring the active filter. - Light / dark / system theme, auto-scroll toggle, configurable
tsharkpath, and the last active view all persist between sessions. - Detailed-view cards are virtualised (Chromium
content-visibility), so traces with thousands of APDUs stay smooth.
| Requirement | Notes |
|---|---|
| Node.js | v18 or newer. |
| Wireshark | Provides tshark. The app looks in C:\Program Files\Wireshark\ and the (x86) variant, then falls back to PATH. If it lives elsewhere, use Locate tshark… in the header. |
| USBPcap | USB capture driver. Install via the Wireshark installer ("USBPcap" component) or from usbpcap.com. A reboot is required after installing. |
git clone https://github.com/Vakho10/apdu-tracer.git
cd apdu-tracer
npm installIf the Electron binary download stalls, use a mirror:
# Windows (PowerShell) $env:ELECTRON_MIRROR="https://npmmirror.com/mirrors/electron/"; npm install
npm run dev- Pick a USBPcap interface (use Refresh to rescan). To analyse a
saved capture instead, Open file… loads a
.pcap/.pcapng. - Press Start tracing.
- Interact with your smart card reader — captured APDUs appear live in the active tab.
- Switch between Simple, Detailed, Summary and Decode tabs at any time; arrow keys cycle the tab list when focused.
- Press Stop tracing when done.
- Save trace writes to
.txt,.jsonor.csv(the format follows the chosen extension); Import reloads a saved JSON trace.
| Key | Action |
|---|---|
Ctrl + F |
Focus the filter / search box. |
F3 |
Jump the Detailed view to the next error or warning. |
← / → |
Cycle through the view tabs (with the tab list focused). |
Each line is formatted as:
[HH:MM:SS +120ms] >> 00 A4 04 00 07 A0 00 00 00 03 00 00
[HH:MM:SS +12ms] << 6F 1A 84 ... 90 00
>> is a command sent to the card, << is the response received from it.
The +Nms is the gap since the previous APDU (absent for the first APDU and
for imported traces).
Each APDU is a card:
- Command — decoded instruction name (GlobalPlatform names take precedence
under a proprietary CLA),
CLA/INS/P1/P2/Lc/Le, ISO case 1–4, optional SM chip when secure messaging is in use, and a P1/P2 note. - Response — data, status word and its meaning (e.g.
6A82— "file or application not found"), round-trip latency since the matching command, and a collapsible BER-TLV tree where applicable. Each tag is named from the active TLV profile (EMV / PIV / OpenPGP, falling back to ISO 7816-4) and shown with its length, raw value, and a decoded interpretation when known. - ATR — a card-power-on event, decoded into convention, protocols, and historical bytes (COMPACT-TLV data objects listed by name).
The main process spawns:
tshark -i <interface> -l -n \
-Y "usbccid.bMessageType == 0x6f || usbccid.bMessageType == 0x80 || usbccid.bMessageType == 0x62" \
-T fields -e usbccid.bMessageType -e usb.endpoint_address.direction \
-e data.data -e usb.capdata -e usb.device_address
0x6f=PC_to_RDR_XfrBlock(command),0x80=RDR_to_PC_DataBlock(response),0x62=PC_to_RDR_IccPowerOn(the next DataBlock carries the ATR).- The APDU payload is read from the CCID
abDatafield, with a fallback that strips the 10-byte CCID header from rawusb.capdata. usb.device_addresskeys command/response pairing to each reader.- A capture file is read the same way with
tshark -r <file>instead of-i.
Captured records are streamed to the renderer over a context-isolated IPC bridge, where ISO 7816-4 / EMV / PIV / OpenPGP / GlobalPlatform decoding happens.
| Command | Description |
|---|---|
npm run dev |
Start the app with hot reload. |
npm run build |
Build the production bundle into out/. |
npm run build:win |
Build and package a Windows installer. |
npm run typecheck |
Run the TypeScript compiler with no emit. |
npm test |
Run the unit tests once (Vitest). |
npm run lint |
Lint the sources with ESLint. |
npm run format |
Format the sources with Prettier. |
typecheck, lint, format:check and test also run on every push to
main and on pull requests via the CI workflow.
src/
main/ Electron main process — tshark capture, IPC, persisted settings
preload/ Context-isolated bridge exposing window.api
renderer/ UI — semantic HTML5, CSS3, TypeScript
src/apdu.ts APDU parsing — ISO + GP instruction names, status words,
secure-messaging indication, case 1-4 derivation
src/tlv.ts BER-TLV parser; ISO / EMV / PIV / OpenPGP tag dictionaries;
value decoding (text, dates, BCD, bit fields, AFL, CVM List)
src/atr.ts ATR parser; historical-byte COMPACT-TLV decoding
src/dom.ts Shared DOM element and hex/time formatting helpers
src/cards.ts Detailed-view card builders and the BER-TLV tree renderer
src/summary.ts Summary view, rendered from the running session aggregates
src/main.ts UI orchestration — state, views, filter, decode, IPC wiring
src/*.test.ts Vitest unit tests for the decoders (apdu, tlv, atr)
Tagged versions are built by the Release
GitHub Actions workflow on windows-latest and published to the
Releases page with the
NSIS installer (.exe), its .blockmap and latest.yml attached.
To cut a new release:
-
Bump
versioninpackage.json, runnpm installsopackage-lock.jsonpicks up the new version (the release job'snpm cirequires the two to match), then commit both. -
Tag the commit
vX.Y.Z(matching thepackage.jsonversion) and push the tag:git tag -a v1.0.1 -m "Release v1.0.1" git push origin v1.0.1
Any tag matching v* triggers the workflow. The release notes are generated
from the commit messages since the previous tag by
git-cliff (configured in
cliff.toml) and used as the release body — no hand-written
notes required. Commits are grouped into sections (Features, Bug Fixes,
Refactor, …) by their subject; using Conventional Commit
prefixes (feat:, fix:, docs:, …) makes that grouping exact, but plain
prose subjects are categorised on a best-effort basis too.
Run npm run changelog to regenerate the project-wide
CHANGELOG.md locally.
- No USBPcap interfaces listed — USBPcap is not installed, or the machine has not been rebooted since installing it.
tsharknot found — install Wireshark, or click Locate tshark… in the header to point the app at yourtshark.exe.- Frames captured but APDUs are empty — the CCID payload field can vary
between Wireshark builds; adjust the field extraction in
src/main/index.ts(handleLine). - Permission errors on capture — USBPcap may require running the app as Administrator.
MIT