{"id":50362144,"url":"https://github.com/deadboy18/tapread","last_synced_at":"2026-05-30T02:05:28.891Z","repository":{"id":360371563,"uuid":"1249117976","full_name":"deadboy18/tapread","owner":"deadboy18","description":"NFC EMV card reader for Android — reads Visa, Mastercard, Amex, JCB, UnionPay contactless cards. Shows PAN, expiry, Track 1/2, AIDs, ATR, CPLC, transactions with time, CVM, TLV-parsed APDU log. No internet permission. Built for fintech professionals.","archived":false,"fork":false,"pushed_at":"2026-05-26T06:50:23.000Z","size":1499,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-26T07:08:01.073Z","etag":null,"topics":["android","apdu","card-reader","contactless","credit-card","emv","fintech","kotlin","nfc","payment","pos","tlv"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/deadboy18.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":"ROADMAP.md","authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-25T11:19:15.000Z","updated_at":"2026-05-26T06:50:27.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/deadboy18/tapread","commit_stats":null,"previous_names":["deadboy18/tapread"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/deadboy18/tapread","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/deadboy18%2Ftapread","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/deadboy18%2Ftapread/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/deadboy18%2Ftapread/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/deadboy18%2Ftapread/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/deadboy18","download_url":"https://codeload.github.com/deadboy18/tapread/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/deadboy18%2Ftapread/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33677261,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-30T02:00:06.278Z","response_time":92,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["android","apdu","card-reader","contactless","credit-card","emv","fintech","kotlin","nfc","payment","pos","tlv"],"created_at":"2026-05-30T02:05:27.137Z","updated_at":"2026-05-30T02:05:28.877Z","avatar_url":"https://github.com/deadboy18.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"# TapRead\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://github.com/deadboy18/tapread/releases/latest\"\u003e\u003cimg src=\"https://img.shields.io/github/v/release/deadboy18/tapread?color=blue\u0026label=release\" alt=\"Latest release\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://github.com/deadboy18/tapread/blob/main/LICENSE\"\u003e\u003cimg src=\"https://img.shields.io/github/license/deadboy18/tapread?color=green\u0026v=2\" alt=\"License\"\u003e\u003c/a\u003e\n  \u003cimg src=\"https://img.shields.io/badge/platform-Android%207.0%2B-3DDC84?logo=android\u0026logoColor=white\" alt=\"Platform: Android 7.0+\"\u003e\n  \u003cimg src=\"https://img.shields.io/badge/language-Kotlin-7F52FF?logo=kotlin\u0026logoColor=white\" alt=\"Language: Kotlin\"\u003e\n  \u003cimg src=\"https://img.shields.io/badge/internet-no-red\" alt=\"No internet permission\"\u003e\n\u003c/p\u003e\n\n\u003e **Tap. Read. Know.**\n\u003e\n\u003e An Android NFC reader for EMV contactless bank cards.\n\u003e Built for fintech professionals — POS sellers, terminal installers, payment gateway developers, NFC/RFID engineers, and anyone who's ever wondered what *actually* gets transmitted when you tap your card.\n\u003e\n\u003e This README is two things at once: project documentation and a complete A-to-Z reverse-engineering reference for how contactless EMV cards work. From the 13.56 MHz RF carrier all the way up to the parsed transaction history, every layer is explained — and every code path in the app is mapped back to the EMV spec that drives it.\n\nMade with 💀 by [deadboy](https://github.com/deadboy18)\n\n---\n\n## Table of Contents\n\n\u003cdetails open\u003e\n\u003csummary\u003e\u003cb\u003ePart I — Project\u003c/b\u003e\u003c/summary\u003e\n\u003cbr\u003e\n\n- **1.** [What It Does](#1-what-it-does)\n- **2.** [Features](#2-features)\n- **3.** [Screenshots](#3-screenshots)\n- **4.** [Supported Cards](#4-supported-cards)\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003ePart II — The Science\u003c/b\u003e\u003c/summary\u003e\n\u003cbr\u003e\n\n- **5.** [How a Contactless Card Actually Works (RF Physics)](#5-how-a-contactless-card-actually-works-rf-physics)\n- **6.** [The Protocol Stack — ISO 14443 → ISO 7816 → EMV](#6-the-protocol-stack)\n- **7.** [APDU Anatomy — Every Byte Explained](#7-apdu-anatomy)\n- **8.** [The EMV Application Hierarchy](#8-the-emv-application-hierarchy)\n- **9.** [BER-TLV — How the Chip Encodes Data](#9-ber-tlv-encoding)\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003ePart III — The Full Read Flow\u003c/b\u003e \u003csub\u003e\u003ci\u003e(matched to the code)\u003c/i\u003e\u003c/sub\u003e\u003c/summary\u003e\n\u003cbr\u003e\n\n- **10.** [Step-by-Step Walkthrough with Real APDUs](#10-step-by-step-walkthrough)\n- **11.** [The Complete EMV Tag Dictionary](#11-the-complete-emv-tag-dictionary)\n- **12.** [Track 2 Decoded](#12-track-2-decoded)\n- **13.** [The Service Code (5F30)](#13-the-service-code-5f30)\n- **14.** [Cardholder Verification Method List (8E)](#14-cvm-list-decoded)\n- **15.** [Application Interchange Profile (82)](#15-application-interchange-profile-aip-decoded)\n- **16.** [Application Usage Control (9F07)](#16-application-usage-control-auc-decoded)\n- **17.** [Terminal \u0026 Card Transaction Qualifiers (9F66 / 9F6C)](#17-terminal--card-transaction-qualifiers-ttq--ctq)\n- **18.** [Application Cryptograms — TC, ARQC, AAC](#18-application-cryptograms--tc-arqc-aac)\n- **19.** [GENERATE AC — The Command You'll Never See in TapRead](#19-generate-ac--the-command-youll-never-see-in-tapread)\n- **20.** [Offline Data Authentication — The RSA Cert Chain](#20-offline-data-authentication--the-rsa-cert-chain)\n- **21.** [The 9F4F Trick — Transaction Time Extraction](#21-the-9f4f-trick--transaction-time-extraction)\n- **22.** [ATR, ATS, and CPLC — Identifying the Chip](#22-atr-ats-and-cplc)\n- **23.** [Historical Bytes — Decoding the ATS Payload](#23-historical-bytes--decoding-the-ats-payload)\n- **24.** [Scheme Detection Logic](#24-scheme-detection-logic)\n- **25.** [PAN, IIN/BIN, and Luhn — How Numbers Are Built](#25-pan-iinbin-and-luhn)\n- **26.** [Tokenized Wallet Detection](#26-tokenized-wallet-detection)\n- **27.** [EMV Payment Tokenisation — How DPANs Get Provisioned](#27-emv-payment-tokenisation--how-dpans-get-provisioned)\n- **28.** [Contactless-Disabled Card Detection](#28-contactless-disabled-card-detection)\n- **29.** [EMV Contactless Kernels — C-1 through C-8](#29-emv-contactless-kernels)\n- **30.** [Visa MSD vs qVSDC vs VSDC — Three Modes, One Card](#30-visa-msd-vs-qvsdc-vs-vsdc)\n- **31.** [Mastercard PayPass — MagStripe Mode vs M/Chip](#31-mastercard-paypass--magstripe-mode-vs-mchip)\n- **32.** [Relay Attacks — Why Contactless Has Distance-Bounding Now](#32-relay-attacks)\n- **33.** [Android HCE — The Other Side of the Conversation](#33-android-hce)\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003ePart IV — The Code\u003c/b\u003e\u003c/summary\u003e\n\u003cbr\u003e\n\n- **34.** [App Architecture](#34-app-architecture)\n- **35.** [The devnied Library Bridge](#35-the-devnied-library-bridge)\n- **36.** [File-by-File Code Walkthrough](#36-file-by-file-code-walkthrough)\n- **37.** [Threading, State, and Persistence](#37-threading-state-and-persistence)\n- **38.** [Reflection Tricks (Where \u0026 Why)](#38-reflection-tricks)\n- **39.** [Build \u0026 Install](#39-build--install)\n- **40.** [Project Structure](#40-project-structure)\n- **41.** [Dependencies](#41-dependencies)\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003ePart V — Security \u0026 Operations\u003c/b\u003e\u003c/summary\u003e\n\u003cbr\u003e\n\n- **42.** [Permissions \u0026 Privacy Model](#42-permissions--privacy-model)\n- **43.** [Security \u0026 Threat Model](#43-security--threat-model)\n- **44.** [Troubleshooting](#44-troubleshooting)\n- **45.** [Limitations](#45-limitations)\n- **46.** [Roadmap](#46-roadmap)\n- **47.** [References \u0026 Further Reading](#47-references--further-reading)\n- **48.** [Credits \u0026 License](#48-credits--license)\n\n\u003c/details\u003e\n\n---\n\n# Part I — Project\n\n## 1. What It Does\n\nHold any contactless bank card against your phone. TapRead opens an ISO 14443 RF link, negotiates ISO-DEP transport, walks the EMV application discovery protocol per EMVCo Book B, reads every record the chip exposes, parses BER-TLV with a 60+ tag dictionary, and displays the result in three tabs:\n\n- **CARD DETAIL** — Visual card face with scheme branding + all extracted EMV fields\n- **TRANSACTIONS** — Recent contactless transactions with date, time, amount, currency, cryptogram\n- **LOG** — Raw APDU command/response log with a parsed TLV tree\n\n**No internet. No ads. No analytics. No background services. Everything stays on your device.**\n\nThe data TapRead reads is the same data your card transmits to every contactless POS terminal you tap at a store. No encryption is broken. No secrets are extracted. The card exposes this data by design — that's how EMV works.\n\n---\n\n## 2. Features\n\n### Card Reading\n\n- Reads Visa, Mastercard, Amex, JCB, UnionPay, Discover, RuPay, Maestro, CB, Dankort, Interac, and more\n- Automatic scheme detection from AID prefix → app label → PAN range (three-tier fallback)\n- Multi-application support (reads **all** AIDs on the chip, not just the highest-priority one)\n- Contactless-disabled detection (PPSE returns SW1SW2 = `6A82` / `6985` → \"NFC is locked on your card\")\n- Tokenized card detection (Apple Pay, Google Pay, Samsung Pay, Garmin, Fitbit, Huawei, Xiaomi)\n\n### Data Extracted\n\n| Data | Source | Notes |\n|------|--------|-------|\n| Card number (PAN) | Tag `5A` | Full or masked, copyable |\n| Expiry date | Tag `5F24` | YYMMDD BCD → MM/YY |\n| Cardholder name | Tag `5F20` | Often blank on modern cards |\n| Track 1 data | Tag `56` | Raw hex |\n| Track 2 equivalent | Tag `57` | Raw hex + parsed PAN/expiry/service code |\n| Service code | Track 2 / Tag `5F30` | Decoded into human-readable string |\n| All AIDs | Tag `4F` | Hex + label (`50`) + priority (`87`) |\n| ATR/ATS | `IsoDep.historicalBytes` / `hiLayerResponse` | Chip platform ID |\n| Card issuer (best-effort) | devnied ATR database | Pattern match against historical bytes |\n| CPLC | Tag `9F7F` | IC fabricator, type, OS, manufacturer |\n| CVM list | Tag `8E` | Decoded: PIN, signature, CDCVM, no CVM, fail-actions |\n| Transaction log | `9F4D` (entry) + `9F4F` (format) | Date, time, amount, currency, country, cryptogram |\n| Transaction time | Tag `9F21` via `9F4F` offset | Parsed manually because devnied doesn't expose it |\n| NFC status | PPSE response SW | Active / Disabled / Blocked |\n| Wallet type | Application labels | Physical card or tokenized |\n\n### APDU Log\n\n- Full command/response hex with color coding (green commands, blue responses)\n- BER-TLV tree parsing with the [60-entry tag dictionary](#11-the-complete-emv-tag-dictionary)\n- ASCII decoding where applicable (application labels, language preferences)\n- Status word descriptions (`9000` → \"Command OK\", `6A82` → \"File not found\")\n- Per-entry timestamps (`System.currentTimeMillis()` capture in `ApduLogger`)\n- Shareable as text with the TLV tree included\n\n### App Features\n\n- **Navigation drawer** — Cards, Settings, About\n- **Persistent storage** — Scanned cards survive app restarts (SharedPreferences + Gson, key `tapread_cards/scans`)\n- **Mask PAN** — Toggle to show/hide middle digits (default ON)\n- **Dark mode** — System default or manual toggle\n- **Export JSON** — Share all stored card data via the Android share sheet\n- **Copy buttons** — Copy PAN, copy extended details to clipboard\n- **BIN Lookup** — Opens `bincheck.io` with the card's BIN pre-filled (in the browser, not in-app)\n- **Haptic feedback** — Five patterns: `tick`, `pulse`, `success`, `error`, `posTerminal`, `heartbeat` (see `HapticUtil.kt`)\n- **Reading dialog** — \"Reading in progress… Please do not remove or move card\"\n- **NFC intent filter** — App appears in \"Choose an action\" when tapping a card outside the app\n- **NFC status detection** — Prompts to enable NFC if disabled, warns if hardware missing\n\n### Easter Eggs 🥚\n\n- Tap the title on the About screen **7 times** → POS terminal beep pattern\n- Shake your phone on the About screen → heartbeat haptic pattern\n\n---\n\n## 3. Screenshots\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"screenshots/screenshot_nfc_scan.png\" width=\"220\" alt=\"Home — tap to scan\"\u003e\n  \u003cimg src=\"screenshots/screenshot_card_detail.png\" width=\"220\" alt=\"Home — with card\"\u003e\n  \u003cimg src=\"screenshots/screenshot_card_detail_extended.png\" width=\"220\" alt=\"Card Detail + Extended\"\u003e\n\u003c/p\u003e\n\u003cp align=\"center\"\u003e\u003csub\u003e\u003cb\u003eHome (empty)\u003c/b\u003e \u0026nbsp;·\u0026nbsp; \u003cb\u003eHome (with card)\u003c/b\u003e \u0026nbsp;·\u0026nbsp; \u003cb\u003eCard Detail + Extended\u003c/b\u003e\u003c/sub\u003e\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"screenshots/screenshot_transactions_dark.png\" width=\"220\" alt=\"Transactions\"\u003e\n  \u003cimg src=\"screenshots/screenshot_apdu_log.png\" width=\"220\" alt=\"APDU Log\"\u003e\n\u003c/p\u003e\n\u003cp align=\"center\"\u003e\u003csub\u003e\u003cb\u003eTransactions\u003c/b\u003e (with parsed time + cryptogram via the \u003ca href=\"#21-the-9f4f-trick--transaction-time-extraction\"\u003e9F4F trick\u003c/a\u003e) \u0026nbsp;·\u0026nbsp; \u003cb\u003eAPDU Log + TLV Tree\u003c/b\u003e\u003c/sub\u003e\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"screenshots/screenshot_settings.png\" width=\"220\" alt=\"Settings\"\u003e\n  \u003cimg src=\"screenshots/screenshot_navigation_drawer.png\" width=\"220\" alt=\"Navigation Drawer\"\u003e\n\u003c/p\u003e\n\u003cp align=\"center\"\u003e\u003csub\u003e\u003cb\u003eSettings\u003c/b\u003e \u0026nbsp;·\u0026nbsp; \u003cb\u003eNavigation Drawer\u003c/b\u003e\u003c/sub\u003e\u003c/p\u003e\n\n---\n\n## 4. Supported Cards\n\n### Payment Schemes\n\nVisa, Mastercard, American Express, JCB, UnionPay, Discover, Maestro, CB (France), Dankort (Denmark), CoGeBan (Italy), Banrisul (Brazil), SPAN (Saudi Arabia), Interac (Canada), RuPay (India), Verve (Nigeria), TROY (Turkey), MIR (Russia)\n\n### Tokenized Wallets\n\nApple Pay, Google Pay, Samsung Pay, Garmin Pay, Fitbit Pay, Huawei Pay, Xiaomi Pay\n\n### Contactless-Disabled Cards\n\nCards with NFC payment turned off in the bank app are detected and labeled with an orange warning banner.\n\n---\n\n# Part II — The Science\n\n## 5. How a Contactless Card Actually Works (RF Physics)\n\nBefore any byte gets transmitted, there's physics.\n\n### The Card Has No Battery\n\nA contactless EMV card is a **passive device**. It contains:\n\n- A loop antenna — typically 3–4 turns of etched or printed copper around the card's perimeter\n- A microcontroller die bonded to the antenna (NXP, Infineon, Samsung, Renesas, or STMicroelectronics — see [CPLC fabricator codes](#20-atr-ats-and-cplc))\n- No battery, no oscillator, no power source of any kind\n\nThe card is powered entirely by the magnetic field generated by your phone (or the POS terminal).\n\n### Inductive Coupling at 13.56 MHz\n\nWhen TapRead calls `nfcAdapter.enableReaderMode(...)`, Android tells the NFC controller to drive its antenna with an alternating current at **13.56 MHz** — an ISM (Industrial, Scientific, Medical) band reserved internationally for short-range RF.\n\nThe phone's antenna becomes an electromagnet. When the card enters the field (typically within 4 cm), the alternating magnetic flux passes through the card's antenna loop and **induces an alternating current in it** — exactly the same principle as a transformer, just with air instead of an iron core, and with both windings being a few millimeters apart.\n\nThat induced AC is rectified inside the chip's front-end to produce DC power. The chip boots. Boot time is typically 5–20 milliseconds.\n\n### Load Modulation — How the Card Talks Back\n\nThe card has no transmitter — no battery, no oscillator. Instead, it uses **load modulation**:\n\n1. The card briefly switches a load resistor across its antenna\n2. The connected load draws more current from the magnetic field\n3. The extra current draw is detectable at the phone's antenna as a tiny voltage dip\n4. The phone reads the pattern of dips as the card's response\n\nThe modulation rides on a **subcarrier at 847.5 kHz** (13.56 MHz ÷ 16), giving a base bit rate of 106 kbit/s — scalable up to 848 kbit/s for faster cards.\n\nWhy this matters for TapRead: the load modulation signal is **very** weak (microvolts at the reader antenna), and any change in coupling distorts it. That's why the reading dialog explicitly says \"Please do not remove or move card.\" Mid-read motion → `TagLostException` from `IsoDep.transceive()` → the app falls into the catch block in `EmvReader.read()` and surfaces an error.\n\n### Coding (NFC-A vs NFC-B)\n\nThe two ISO 14443 flavors differ in how they encode bits over the RF link:\n\n- **Type A (NFC-A)** — Modified Miller coding (reader → card) and Manchester coding (card → reader). Used by Mastercard, Visa, Amex, JCB, UnionPay contactless. Most common worldwide.\n- **Type B (NFC-B)** — NRZ-L coding both ways, with BPSK subcarrier modulation card → reader. Used by some French CB cards and certain Asian schemes.\n\nTapRead enables both — from `NfcDispatcher.kt`:\n\n```kotlin\nval flags = NfcAdapter.FLAG_READER_NFC_A or\n        NfcAdapter.FLAG_READER_NFC_B or\n        NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK or\n        NfcAdapter.FLAG_READER_NO_PLATFORM_SOUNDS\n```\n\n`SKIP_NDEF_CHECK` matters: without it, Android would try to interpret the tag as an NDEF data tag first, which is meaningless for an EMV card and slows the read. `NO_PLATFORM_SOUNDS` silences the OS-level NFC \"blip\" sound — TapRead uses its own haptic feedback instead.\n\n### Anti-Collision\n\nIf two cards are in the field at once, both try to respond and their load modulations collide. The reader runs an **anti-collision algorithm** (ISO 14443-3): each card has a UID, the reader broadcasts bit-by-bit, and cards drop out of the conversation when their UID bit doesn't match. The surviving card is the one selected.\n\nTapRead doesn't expose UID — Android's NFC stack singulates one card before invoking `ReaderCallback.onTagDiscovered(tag)`. If two cards are in your wallet, the chipset picks one and the other is ignored.\n\n### Presence Check Delay\n\nAndroid's NFC stack has a recurring \"is the card still there?\" check that runs **independently** of `IsoDep.transceive()` timeouts. On Broadcom-based chipsets and Android 13+, this check can fire mid-command and cause spurious disconnects. TapRead works around this in `NfcDispatcher.kt`:\n\n```kotlin\nval options = Bundle().apply {\n    putInt(NfcAdapter.EXTRA_READER_PRESENCE_CHECK_DELAY, 250)\n}\n```\n\nBumping the check delay from the default 125 ms to 250 ms gives the card more time to complete each command before Android decides it's gone.\n\n---\n\n## 6. The Protocol Stack\n\nContactless EMV is layered. Each layer adds structure on top of the one below. TapRead writes to the top three layers; Android's NFC controller handles the bottom three.\n\n```\n┌─────────────────────────────────────────────────────────────┐\n│  EMV (Books 1-4, A-D)   \"SELECT PPSE\", \"READ RECORD\", etc.  │  ← TapRead lives here\n├─────────────────────────────────────────────────────────────┤\n│  ISO 7816-4             APDU command/response structure     │\n├─────────────────────────────────────────────────────────────┤\n│  ISO-DEP (14443-4)      Block transmission, T=CL protocol   │\n├─────────────────────────────────────────────────────────────┤\n│  ISO 14443-3            Anti-collision, frame format        │  ← Android NFC stack\n├─────────────────────────────────────────────────────────────┤\n│  ISO 14443-2            Modulation, coding (Type A / B)     │\n├─────────────────────────────────────────────────────────────┤\n│  ISO 14443-1            Physical characteristics            │\n└─────────────────────────────────────────────────────────────┘\n```\n\n### ISO-DEP — The Reliable Transport\n\nISO 14443-4 defines **ISO-DEP** (aka T=CL — \"T equals contactless\"), which provides:\n\n- Reliable block transmission with CRC\n- Chaining (for APDUs longer than the max frame size, which is typically 253 bytes for Android)\n- Frame waiting time extension (so slow card operations don't time out)\n\nAndroid exposes this via the `android.nfc.tech.IsoDep` class. Once `IsoDep.connect()` succeeds, you can call `transceive(byte[])` and get a byte array back. That's an APDU exchange — see [APDU Anatomy](#7-apdu-anatomy).\n\n### How TapRead Gets to IsoDep\n\nFrom `MainActivity.onTagDiscovered()` → `NfcDispatcher.getIsoDep(tag)`:\n\n```kotlin\ncompanion object {\n    fun getIsoDep(tag: Tag): IsoDep? {\n        return IsoDep.get(tag)\n    }\n}\n```\n\n`IsoDep.get()` returns `null` if the discovered tag doesn't speak ISO-DEP. That happens for MIFARE Classic, NTAG, FeliCa, and similar non-EMV cards. TapRead shows an \"Unsupported card type\" snackbar with the tag's UID and bails out.\n\n---\n\n## 7. APDU Anatomy\n\nEvery command TapRead sends to the card and every response it receives is an **APDU** — Application Protocol Data Unit. Defined by ISO 7816-4.\n\n### Command APDU (C-APDU)\n\n```\n┌─────┬─────┬─────┬─────┬────┬──────────────┬────┐\n│ CLA │ INS │ P1  │ P2  │ Lc │     Data     │ Le │\n└─────┴─────┴─────┴─────┴────┴──────────────┴────┘\n  1B    1B    1B    1B    1B    0–255 B       1B\n```\n\n| Field | Meaning |\n|-------|---------|\n| **CLA** | Class byte. `00` = ISO standard. `80` = proprietary. |\n| **INS** | Instruction. `A4` = SELECT, `B2` = READ RECORD, `A8` = GET PROCESSING OPTIONS, `CA` = GET DATA, `AE` = GENERATE AC. |\n| **P1, P2** | Parameters. Meaning depends on INS. |\n| **Lc** | Length of the Data field. Omitted if no Data. |\n| **Data** | Command-specific payload. |\n| **Le** | Length expected in response. `00` = \"send me up to 256 bytes\". |\n\nThe **four \"Cases\"** of an APDU structure:\n\n- **Case 1**: CLA INS P1 P2 (no data, no expected response data)\n- **Case 2**: CLA INS P1 P2 Le (no data, response expected)\n- **Case 3**: CLA INS P1 P2 Lc Data (data sent, no response data)\n- **Case 4**: CLA INS P1 P2 Lc Data Le (data sent, response data expected) ← most EMV commands\n\nTapRead labels the INS byte for the log via `IsoDepProvider.guessCommandLabel()`:\n\n```kotlin\nprivate fun guessCommandLabel(cmd: ByteArray): String {\n    if (cmd.size \u003c 4) return \"UNKNOWN\"\n    val ins = cmd[1].toInt() and 0xFF\n    return when (ins) {\n        0xA4 -\u003e \"SELECT\"\n        0xB2 -\u003e \"READ RECORD\"\n        0xCA -\u003e \"GET DATA\"\n        0xA8 -\u003e \"GET PROCESSING OPTIONS\"\n        0x88 -\u003e \"INTERNAL AUTHENTICATE\"\n        0x82 -\u003e \"EXTERNAL AUTHENTICATE\"\n        0x84 -\u003e \"GET CHALLENGE\"\n        else -\u003e \"INS ${HexUtil.toHex(byteArrayOf(cmd[1]))}\"\n    }\n}\n```\n\n### Response APDU (R-APDU)\n\n```\n┌──────────────┬─────┬─────┐\n│   Data       │ SW1 │ SW2 │\n└──────────────┴─────┴─────┘\n   0–256 B       1B    1B\n```\n\nThe last two bytes are always the **Status Word** (SW1SW2).\n\n### The Full Status Word Table (EMV-relevant)\n\n| SW1SW2 | Meaning |\n|--------|---------|\n| `9000` | Command OK |\n| `6100`–`61FF` | More data available — use GET RESPONSE with Le = SW2 |\n| `6200`–`62FF` | Warning, response data may be incorrect but is returned |\n| `6283` | Selected file invalidated |\n| `6300` | No information given (warning) |\n| `63CX` | Counter (X is the counter value) |\n| `6700` | Wrong length |\n| `6800` | No information given (functions in CLA not supported) |\n| `6881` | Logical channel not supported |\n| `6882` | Secure messaging not supported |\n| `6900` | No information given (command not allowed) |\n| `6981` | Command incompatible with file structure |\n| `6982` | Security status not satisfied |\n| `6983` | Authentication method blocked |\n| `6984` | Reference data invalidated |\n| `6985` | Conditions of use not satisfied ← contactless blocked |\n| `6986` | Command not allowed (no current EF) |\n| `6A00` | No information given (wrong parameters) |\n| `6A80` | Incorrect parameters in the data field |\n| `6A81` | Function not supported |\n| `6A82` | File or application not found ← no payment app on contactless interface |\n| `6A83` | Record not found |\n| `6A86` | Incorrect P1/P2 |\n| `6A88` | Referenced data not found ← common for GET DATA on missing tag |\n| `6B00` | Wrong parameters (offset outside EF) |\n| `6CXX` | Wrong Le field; SW2 indicates the exact length |\n| `6D00` | Instruction not supported or invalid |\n| `6E00` | Class not supported |\n| `6F00` | No precise diagnosis |\n\nTapRead's `EmvReader.detectContactlessStatus()` specifically watches for `6A82` and `6985` on the SELECT command:\n\n```kotlin\nprivate fun detectContactlessStatus(logger: ApduLogger): ContactlessStatus {\n    for (entry in logger.entries) {\n        if (entry.label == \"SELECT\" \u0026\u0026 entry.response.size \u003e= 2) {\n            val sw = ((entry.response[entry.response.size - 2].toInt() and 0xFF) shl 8) or\n                    (entry.response[entry.response.size - 1].toInt() and 0xFF)\n            return when (sw) {\n                0x6A82 -\u003e ContactlessStatus.DISABLED\n                0x6985 -\u003e ContactlessStatus.BLOCKED\n                else -\u003e ContactlessStatus.ACTIVE\n            }\n        }\n    }\n    return ContactlessStatus.ACTIVE\n}\n```\n\n---\n\n## 8. The EMV Application Hierarchy\n\nEvery EMV card is structured as a tree of **applications**, where each application has its own data file system. Here's the model:\n\n```\n┌─────────────────────────────────────────────────┐\n│   CARD                                          │\n│   ├── Master File (MF)                          │\n│   ├── PSE/PPSE Directory                        │\n│   │   \"2PAY.SYS.DDF01\" (contactless)            │\n│   │   \"1PAY.SYS.DDF01\" (contact)                │\n│   │   └── List of installed payment apps        │\n│   └── Application 1 (e.g. Visa Debit)           │\n│       ├── AID: A0000000031010                   │\n│       ├── Label: \"VISA DEBIT\"                   │\n│       ├── EF (SFI 1) Record 1 → PAN, expiry...  │\n│       ├── EF (SFI 2) Record 1 → CVM list...     │\n│       ├── EF (SFI 11) Records 1-10 → Tx log     │\n│       └── PDOL, CDOL1, CDOL2                    │\n│   └── Application 2 (e.g. US Common Debit)      │\n│       └── ...                                   │\n└─────────────────────────────────────────────────┘\n```\n\nA few things to note:\n\n- **PPSE** (\"Proximity Payment System Environment\") is the contactless equivalent of PSE. Every contactless card has it under the DF name `2PAY.SYS.DDF01`.\n- **AID** (Application Identifier) is up to 16 bytes. The first 5 bytes (the **RID** — Registered Application Provider Identifier) identify the scheme; the rest identifies the product.\n- **EF** (Elementary File) is identified by an **SFI** (Short File Identifier, 1–30). Each EF holds one or more **records**, addressable by record number.\n- **PDOL** (Processing Options Data Object List) tells the terminal what data to provide in GPO.\n- **CDOL1/CDOL2** (Card Risk Management Data Object List) tells the terminal what data to provide for GENERATE AC.\n\n---\n\n## 9. BER-TLV Encoding\n\nEverything the card returns is **BER-TLV**: Basic Encoding Rules — Tag, Length, Value. It's a recursive binary format defined in ISO/IEC 8825-1. Every byte TapRead's `TlvParser.kt` parses follows these rules.\n\n### Tag\n\nTags are 1 or 2 bytes:\n\n- **1-byte tag**: First byte's low 5 bits are NOT all set. e.g. `5A`, `50`, `87`, `94`.\n- **2-byte tag**: First byte's low 5 bits are all set (`xxx11111`). Second byte continues. e.g. `5F20`, `9F02`, `9F4F`.\n\nFrom `TlvParser.kt`:\n\n```kotlin\nval tagByte = hex.substring(pos, pos + 2).toInt(16)\nval tag: String\nif ((tagByte and 0x1F) == 0x1F) {\n    // Multi-byte tag\n    if (pos + 4 \u003e hex.length) break\n    tag = hex.substring(pos, pos + 4)\n    pos += 4\n} else {\n    tag = hex.substring(pos, pos + 2)\n    pos += 2\n}\n```\n\nThe **top 3 bits** of the first byte encode metadata:\n\n| Bits 8-7 | Class |\n|----------|-------|\n| `00` | Universal |\n| `01` | Application class (`4F`, `50`, `5A`) |\n| `10` | Context-specific (`70`, `77`, `80`–`9F`, `A5`, `BF`) |\n| `11` | Private |\n\n| Bit 6 | Form |\n|-------|------|\n| `0` | **Primitive** — the value is raw data |\n| `1` | **Constructed** — the value is itself more TLV (nested) |\n\nExamples:\n- `5A` = `0101 1010` → application class, primitive → PAN, raw BCD\n- `70` = `0111 0000` → application class, constructed → contains nested TLVs\n- `9F02` → context-specific, primitive (bit 6 of first byte = 0), multi-byte\n\nTapRead detects constructed tags this way:\n\n```kotlin\nval isConstructed = (tag.substring(0, 2).toInt(16) and 0x20) != 0\n```\n\nBit position 6 (counting from 1) = bit mask `0x20`.\n\n### Length\n\nLengths come in three forms (TapRead handles all three):\n\n| First byte | Meaning | Code path |\n|------------|---------|-----------|\n| `00`–`7F` | Length is this byte directly (0–127) | `length = lenByte; pos += 2` |\n| `81 XX` | Length is the next 1 byte (128–255) | `length = ...substring(...).toInt(16); pos += 4` |\n| `82 XX XX` | Length is the next 2 bytes (256–65535) | `length = ...substring(...).toInt(16); pos += 6` |\n| `83 XX XX XX` | Length is the next 3 bytes (rare) | Not handled — would break (intentional, EMV doesn't use it) |\n\n### Value\n\nJust `length` bytes of raw data. Interpretation depends on the tag — see the [dictionary](#11-the-complete-emv-tag-dictionary).\n\n### Walking the Tree\n\nTapRead's parser is a recursive descent. Pseudocode:\n\n```\nfunction parse(bytes):\n    while bytes remaining:\n        tag = readTag(bytes)         // 1 or 2 bytes\n        length = readLength(bytes)   // 1, 2, or 3 bytes\n        value = bytes[next length]\n        if tag is constructed:\n            children = parse(value)\n            emit Node(tag, children)\n        else:\n            emit Leaf(tag, value)\n```\n\nThat's the whole binary EMV world, in ~50 lines of Kotlin (`TlvParser.parseNodes`).\n\n---\n\n# Part III — The Full Read Flow\n\n## 10. Step-by-Step Walkthrough\n\nHere's the exact sequence TapRead executes against the card. Every byte is real EMV. Code paths are cited.\n\n### Step 0 — Tag Discovery\n\nWhen you tap a card, Android's NFC controller does the ISO 14443 anti-collision and ISO 14443-4 RATS exchange. The result is delivered to `ReaderCallback` in `NfcDispatcher.kt`:\n\n```kotlin\nprivate val readerCallback = NfcAdapter.ReaderCallback { tag -\u003e\n    log.info(\"Tag discovered: {}\", tag.techList?.joinToString())\n    onTagDiscovered(tag)\n}\n```\n\nThat callback hops to `MainActivity.onTagDiscovered(tag)`, which extracts `IsoDep.get(tag)`. If it's null, the tag isn't EMV — snackbar + bail. If it's not null, TapRead vibrates (`HapticUtil.pulse()`), shows the reading dialog, and launches a coroutine on `Dispatchers.IO`:\n\n```kotlin\nscope.launch {\n    val result = withContext(Dispatchers.IO) { emvReader.read(isoDep) }\n    ...\n}\n```\n\n### Step 1 — Connect\n\nIn `EmvReader.read()`:\n\n```kotlin\nisoDep.connect()\nisoDep.timeout = 5000\nprovider.setTagCom(isoDep)\n```\n\nNow you have a live ISO-DEP channel. Behind the scenes, the controller has already done anti-collision and RATS, and the historical bytes of the ATS are available via `isoDep.historicalBytes`. The 5-second timeout applies to each `transceive()` call.\n\nTapRead then hands the IsoDep to the devnied library via `IProvider`:\n\n```kotlin\nval config = EmvTemplate.Config()\n    .setContactLess(true)\n    .setReadAllAids(true)\n    .setReadTransactions(true)\n    .setReadCplc(true)\n    .setRemoveDefaultParsers(false)\n    .setReadAt(true)\n\nval template = EmvTemplate.Builder()\n    .setProvider(provider)\n    .setConfig(config)\n    .build()\n\nval emvCard: EmvCard = template.readEmvCard()\n```\n\nFrom this point, devnied drives the conversation. TapRead's `IsoDepProvider` is the bridge, and every APDU exchange is captured by `ApduLogger`.\n\n### Step 2 — SELECT PPSE\n\nThe first command devnied sends:\n\n```\n→ Command:  00 A4 04 00 0E 32 50 41 59 2E 53 59 53 2E 44 44 46 30 31 00\n            │  │  │  │  │  └──────────────────────────────────────┘  └─ Le = 00 (\"give me everything\")\n            │  │  │  │  │     \"2PAY.SYS.DDF01\" in ASCII\n            │  │  │  │  └─ Lc = 14 bytes (0x0E)\n            │  │  │  └─ P2 = 00 (first occurrence)\n            │  │  └─ P1 = 04 (select by DF name)\n            │  └─ INS = A4 (SELECT)\n            └─ CLA = 00 (ISO)\n```\n\n`2PAY.SYS.DDF01` is the universal \"directory\" application defined by EMV Book B. Every contactless card supports it. It's the card's table of contents.\n\n```\n← Response: 6F XX 84 0E 32 50 41 59 2E 53 59 53 2E 44 44 46 30 31 A5 XX\n            BF 0C XX 61 XX 4F XX \u003cAID-bytes\u003e 50 XX \u003clabel-bytes\u003e 87 01 \u003cpriority\u003e\n            61 XX 4F XX \u003cAID-bytes\u003e 50 XX \u003clabel-bytes\u003e 87 01 \u003cpriority\u003e\n            ...\n            90 00\n```\n\nStructure (constructed templates marked with `*`):\n\n- `6F` * — File Control Information (FCI) template\n  - `84` — DF Name (echoes back the PPSE name)\n  - `A5` * — FCI Proprietary template\n    - `BF0C` * — FCI Issuer Discretionary Data\n      - `61` * — Application Template (one per app on the card)\n        - `4F` — **AID**\n        - `50` — Application Label (\"VISA DEBIT\", \"MASTERCARD\", etc.)\n        - `87` — Application Priority Indicator\n\nA typical dual-AID Visa debit card returns something like:\n\n```\n4F 07 A0000000031010  →  Visa Credit/Debit (international AID)\n50 0A \"VISA DEBIT\"\n87 01 01\n\n4F 07 A0000000980840  →  US Common Debit AID\n50 0F \"US COMMON DEBIT\"\n87 01 02\n```\n\n### Step 3 — SELECT AID\n\nFor each AID returned by PPSE, devnied sends a SELECT to activate that application:\n\n```\n→ Command:  00 A4 04 00 07 A0 00 00 00 03 10 10 00\n                              └────────────────┘\n                              Visa international AID\n```\n\nResponse:\n\n```\n← Response: 6F XX 84 07 A0 00 00 00 03 10 10\n            A5 XX 50 \u003clabel\u003e 87 01 \u003cpriority\u003e\n            9F38 XX \u003cPDOL\u003e          ← Processing Options Data Object List\n            BF0C XX \u003cFCI issuer data\u003e\n            90 00\n```\n\nThe critical new tag is **`9F38` — PDOL**. It tells you what data the card needs from the terminal before it'll cough up its processing options.\n\nA typical Visa contactless PDOL: `9F66 04 9F02 06 9F03 06 9F1A 02 95 05 5F2A 02 9A 03 9C 01 9F37 04`\n\nParsed:\n\n| Tag | Length | Meaning |\n|-----|--------|---------|\n| `9F66` | 4 | Terminal Transaction Qualifiers (TTQ) — see §17 |\n| `9F02` | 6 | Amount Authorized |\n| `9F03` | 6 | Amount Other |\n| `9F1A` | 2 | Terminal Country Code |\n| `95`   | 5 | Terminal Verification Results |\n| `5F2A` | 2 | Transaction Currency Code |\n| `9A`   | 3 | Transaction Date |\n| `9C`   | 1 | Transaction Type |\n| `9F37` | 4 | Unpredictable Number |\n\nFor pure data reading (no transaction), devnied fills these with safe defaults — usually zeros or library-defined constants. The TTQ is typically set to declare \"online cryptogram required, no CVM\" so the card stays in a simple flow.\n\n### Step 4 — GET PROCESSING OPTIONS (GPO)\n\n```\n→ Command:  80 A8 00 00 \u003cLc\u003e 83 \u003cPDOL-data-length\u003e \u003cPDOL-data\u003e 00\n            │  │  │  │       │\n            │  │  │  │       └─ Tag 83 wraps the PDOL data\n            │  │  │  └─ P2 = 00\n            │  │  └─ P1 = 00\n            │  └─ INS = A8 (GPO)\n            └─ CLA = 80 (proprietary EMV)\n```\n\nThe card responds in one of two formats:\n\n**Format 1 (legacy, fixed binary):**\n```\n80 XX \u003cAIP (2B)\u003e \u003cAFL (variable)\u003e\n```\n\n**Format 2 (BER-TLV, modern):**\n```\n77 XX 82 02 \u003cAIP\u003e 94 XX \u003cAFL\u003e [other tags like 9F36, 9F26 for fast-path]\n```\n\nThe two fields that matter:\n\n- **`82` AIP** — Application Interchange Profile (2 bytes of bit flags — see §15)\n- **`94` AFL** — Application File Locator (the map of records to read)\n\n### Step 5 — Parse the AFL\n\nThe AFL is the key to everything. It's a list of **4-byte entries**:\n\n```\n┌────────┬───────────────┬─────────────┬──────────────────────┐\n│  SFI   │ First record  │ Last record │  Records used for    │\n│  \u003c\u003c 3  │               │             │  offline auth        │\n└────────┴───────────────┴─────────────┴──────────────────────┘\n  Byte 0     Byte 1         Byte 2         Byte 3\n```\n\n- **SFI (Short File Identifier)**: The upper 5 bits of byte 0. Values 1–30 are valid file IDs.\n- **First/Last record**: The range of records in that file to read.\n- **Auth count**: How many of those records go into the offline data authentication hash.\n\nA real-world AFL: `08 01 03 00 10 01 01 00 18 01 02 00`\n\nDecoded:\n\n| Entry | SFI byte | SFI (\u003e\u003e3) | First | Last | Auth |\n|-------|----------|-----------|-------|------|------|\n| 1 | `08` (`0000 1000`) | 1 | 1 | 3 | 0 |\n| 2 | `10` (`0001 0000`) | 2 | 1 | 1 | 0 |\n| 3 | `18` (`0001 1000`) | 3 | 1 | 2 | 0 |\n\n→ 6 records to read total.\n\n### Step 6 — READ RECORD (loop)\n\nFor each (SFI, record number) pair from the AFL:\n\n```\n→ Command:  00 B2 \u003crecord\u003e \u003cP2\u003e 00\n                            │\n                            └─ P2 = (SFI \u003c\u003c 3) | 04\n                               The \"| 04\" tells the card \"find by SFI, not curr EF\"\n```\n\nExample for SFI=2, record=1:\n\n```\n→ Command:  00 B2 01 14 00\n                     │\n                     └─ 0x14 = (2 \u003c\u003c 3) | 4 = 0x10 | 0x04\n```\n\nThe response is a `70` template (Application Elementary File data template) containing all the EMV data tags for that record:\n\n```\n← Response: 70 XX\n              5A 08 \u003cPAN\u003e\n              5F24 03 \u003cexpiry YYMMDD\u003e\n              5F20 XX \u003ccardholder name\u003e\n              57 XX \u003cTrack 2 equivalent\u003e\n              5F30 02 \u003cservice code\u003e\n              8C XX \u003cCDOL1\u003e\n              8D XX \u003cCDOL2\u003e\n              8E XX \u003cCVM list\u003e\n              9F07 02 \u003cApplication Usage Control\u003e      ← see §16\n              9F08 02 \u003cApplication Version Number\u003e\n              9F0D 05 \u003cIssuer Action Code - Default\u003e\n              9F0E 05 \u003cIssuer Action Code - Denial\u003e\n              9F0F 05 \u003cIssuer Action Code - Online\u003e\n              5F28 02 \u003cIssuer Country Code\u003e\n              5F34 01 \u003cPAN Sequence Number\u003e\n              9F1F XX \u003cTrack 1 Discretionary Data\u003e\n              9F4A 01 \u003cStatic Data Authentication Tag List\u003e\n              ...\n            90 00\n```\n\ndevnied passes each response into its TLV parser; TapRead also captures the raw bytes via `ApduLogger` for the in-app log view.\n\n### Step 7 — Read Transaction Log (if present)\n\nTag **`9F4D`** is the **Log Entry**. If it appeared in any prior response, it tells you:\n\n- Byte 1: SFI of the transaction log\n- Byte 2: Number of log records\n\nTag **`9F4F`** is the **Log Format** — the schema of each log record. The whole \"extract transaction time\" mechanism revolves around this — see §21.\n\nTapRead then loops READ RECORD against the log SFI to pull the actual transactions.\n\n### Step 8 — CPLC (Optional)\n\nTapRead also tries to fetch the Card Production Life Cycle:\n\n```\n→ Command:  80 CA 9F 7F 00\n            │  │  └──┘  └─ Le = 0\n            │  │   └─ P1P2 = 9F7F (the requested tag)\n            │  └─ INS = CA (GET DATA)\n            └─ CLA = 80\n```\n\nNot every card responds — some return `6A88` (Referenced data not found). TapRead extracts CPLC via reflection on the devnied `EmvCard.cplc` field — see §22.\n\n### Step 9 — Close\n\n```kotlin\nfinally {\n    try { isoDep.close() } catch (_: Exception) {}\n}\n```\n\nThe RF link drops. The card loses power. The whole thing took ~200–800 ms depending on the card and how many records were read.\n\n---\n\n## 11. The Complete EMV Tag Dictionary\n\nThe 60-entry dictionary from `TlvParser.kt`'s `EMV_TAGS` map, organized for human readers. Tags marked **\\*** are constructed (contain nested TLVs).\n\n### Application Discovery \u0026 FCI\n\n| Tag | Name | Format |\n|-----|------|--------|\n| `6F` * | FCI Template | Constructed |\n| `84` | Dedicated File (DF) Name | Variable |\n| `A5` * | FCI Proprietary Template | Constructed |\n| `BF0C` * | FCI Issuer Discretionary Data | Constructed |\n| `61` * | Application Template | Constructed |\n| `4F` | Application Identifier (AID) | 5–16 bytes |\n| `50` | Application Label | ASCII |\n| `87` | Application Priority Indicator | 1 byte |\n| `9F12` | Application Preferred Name | ASCII |\n| `9F11` | Issuer Code Table Index | 1 byte |\n| `9F38` | PDOL (Processing Data Object List) | Variable |\n\n### Record Templates\n\n| Tag | Name |\n|-----|------|\n| `70` * | EMV Record Template (response to READ RECORD) |\n| `77` * | Response Template Format 2 (TLV) |\n| `80` | Response Template Format 1 (fixed) |\n\n### Primary Account Data\n\n| Tag | Name | Format |\n|-----|------|--------|\n| `5A` | Application PAN | BCD, up to 19 digits |\n| `5F20` | Cardholder Name | ASCII, padded with spaces, up to 26 chars |\n| `5F24` | Application Expiration Date | YYMMDD BCD |\n| `5F25` | Application Effective Date | YYMMDD BCD |\n| `5F28` | Issuer Country Code | 2-digit BCD, ISO 3166 |\n| `5F2A` | Transaction Currency Code | ISO 4217 numeric |\n| `5F2D` | Language Preference | ASCII, up to 8 chars |\n| `5F34` | PAN Sequence Number | 1 byte BCD |\n\n### Track Data\n\n| Tag | Name |\n|-----|------|\n| `56` | Track 1 Data |\n| `57` | Track 2 Equivalent Data |\n| `9F1F` | Track 1 Discretionary Data |\n| `9F20` | Track 2 Discretionary Data |\n\n### GPO / Processing\n\n| Tag | Name |\n|-----|------|\n| `82` | Application Interchange Profile (AIP) |\n| `94` | Application File Locator (AFL) |\n| `8C` | CDOL1 (Card Risk Mgmt Data Object List 1) |\n| `8D` | CDOL2 (Card Risk Mgmt Data Object List 2) |\n| `8E` | CVM List |\n\n### Risk Management\n\n| Tag | Name |\n|-----|------|\n| `9F07` | Application Usage Control |\n| `9F08` | Application Version Number (card) |\n| `9F09` | Application Version Number (terminal) |\n| `9F0D` | Issuer Action Code – Default |\n| `9F0E` | Issuer Action Code – Denial |\n| `9F0F` | Issuer Action Code – Online |\n| `9F10` | Issuer Application Data |\n\n### Transaction / Cryptogram\n\n| Tag | Name |\n|-----|------|\n| `9A` | Transaction Date |\n| `9C` | Transaction Type |\n| `9F02` | Amount, Authorized |\n| `9F03` | Amount, Other |\n| `9F1A` | Terminal Country Code |\n| `9F21` | Transaction Time |\n| `9F26` | Application Cryptogram |\n| `9F27` | Cryptogram Information Data |\n| `9F33` | Terminal Capabilities |\n| `9F34` | CVM Results |\n| `9F35` | Terminal Type |\n| `9F36` | Application Transaction Counter (ATC) |\n| `9F37` | Unpredictable Number |\n| `9F42` | Application Currency Code |\n| `9F44` | Application Currency Exponent |\n\n### Offline Data Authentication\n\n| Tag | Name |\n|-----|------|\n| `9F45` | Data Authentication Code |\n| `9F46` | ICC Public Key Certificate |\n| `9F47` | ICC Public Key Exponent |\n| `9F48` | ICC Public Key Remainder |\n| `9F49` | DDOL (Dynamic Data Object List) |\n| `9F4A` | Static Data Authentication Tag List |\n\n### Contactless-Specific\n\n| Tag | Name |\n|-----|------|\n| `9F4D` | Log Entry |\n| `9F4F` | Log Format |\n| `9F66` | Terminal Transaction Qualifiers (TTQ) — Visa |\n| `9F6C` | Card Transaction Qualifiers (CTQ) — Visa |\n| `9F6E` | Form Factor Indicator / Third-Party Data |\n| `9F7C` | Customer Exclusive Data |\n| `9F7F` | Card Production Life Cycle (CPLC) |\n\n---\n\n## 12. Track 2 Decoded\n\nTag `57` — Track 2 Equivalent — is the single most useful field on a contactless card. It packs the magstripe equivalent of the same card into one tidy nibble-aligned binary blob:\n\n```\n\u003cPAN\u003e D \u003cexpiry YYMM\u003e \u003cservice code\u003e \u003cdiscretionary data\u003e [F (pad)]\n```\n\nThe `D` is the field separator (hex `D`, nibble-aligned), and the optional `F` is a padding nibble if the total nibble count is odd.\n\nExample raw Track 2 (hex): `4111111111111111D2512201F123456789F`\n\nDecoded by reading nibble-by-nibble until the `D`:\n\n- PAN: `4111111111111111` (16 digits)\n- Separator: `D`\n- Expiry: `2512` (YYMM) → December 2025\n- Service code: `201` (see §13)\n- Discretionary data: `F123456789` (variable; issuer-specific, often contains PIN verification value, CVV equivalent)\n- Trailing `F` is the odd-nibble pad\n\nTapRead extracts the raw Track 2 bytes via reflection (the devnied library wraps it in a `Track2` object):\n\n```kotlin\nprivate fun extractTrack2(card: EmvCard): String? {\n    return try {\n        val t = card.track2 ?: return null\n        val f = t.javaClass.getDeclaredField(\"raw\"); f.isAccessible = true\n        (f.get(t) as? ByteArray)?.let { HexUtil.toHexSpaced(it) }\n    } catch (_: Exception) {\n        try { card.track2?.toString() } catch (_: Exception) { null }\n    }\n}\n```\n\nThe result is displayed as space-separated hex in the UI.\n\n---\n\n## 13. The Service Code (5F30)\n\nThe 3-digit service code in Track 2 (also separately stored in tag `5F30`) is one of the most under-documented but most useful fields. Each digit means something different.\n\nFrom `TlvParser.decodeServiceCode()`:\n\n### Digit 1 — Interchange and Technology\n\n| Value | Meaning |\n|-------|---------|\n| `1` | International interchange OK |\n| `2` | International interchange + IC chip required |\n| `5` | National interchange only |\n| `6` | National interchange + IC chip required |\n| `7` | Private use / no interchange except bilateral agreement |\n| `9` | Test card |\n\n### Digit 2 — Authorization Processing\n\n| Value | Meaning |\n|-------|---------|\n| `0` | Normal authorization |\n| `2` | Contact issuer for every transaction (online-only) |\n| `4` | Contact issuer except under bilateral agreement |\n\n### Digit 3 — Range of Services / CVM\n\n| Value | Meaning |\n|-------|---------|\n| `0` | No restrictions, PIN required |\n| `1` | No restrictions |\n| `2` | Goods and services only |\n| `3` | ATM only, PIN required |\n| `4` | Cash only |\n| `5` | Goods and services only, PIN required |\n| `6` | No restrictions, PIN prompting if PIN pad present |\n| `7` | Goods and services only, PIN prompting |\n\nA service code of **`201`** therefore decodes as:\n- \"International + IC chip\"\n- \"Normal authorization\"\n- \"No restrictions\"\n\nWhich is a common configuration for international chip + contactless debit cards.\n\n---\n\n## 14. CVM List Decoded\n\nTag `8E` — Cardholder Verification Method List. This is how the card tells the terminal which verification methods are acceptable, and in what order. TapRead decodes it in `TlvParser.decodeCvmList()`.\n\n### Structure\n\n```\n┌──────────────────┬──────────────────┬─────────────────────┐\n│ Amount X (4B)    │ Amount Y (4B)    │  CV Rules (n × 2B)  │\n└──────────────────┴──────────────────┴─────────────────────┘\n```\n\nThe two amounts are reference thresholds in the smallest currency unit used by the CV rules below (used by conditions `06`–`09`).\n\nEach **CV Rule** is 2 bytes:\n\n#### Byte 1 — Method byte\n\n```\n┌─┬─┬─┬─┬─┬─┬─┬─┐\n│0│X│M M M M M M│\n└─┴─┴─┴─┴─┴─┴─┴─┘\n   │ └──┬───────┘\n   │    └─ CVM Code (bits 6-1)\n   └─ Apply succeeding CVM if this one fails? (bit 7)\n```\n\nBit 7 (the `X`):\n- `0` = Fail CVM processing if this method fails\n- `1` = Apply succeeding CV Rule if this fails\n\nCVM Codes (the low 6 bits, from `TlvParser.kt`):\n\n| Code (hex) | Method |\n|------------|--------|\n| `00` | Fail CVM processing |\n| `01` | Plaintext PIN verified by ICC |\n| `02` | Enciphered PIN verified online |\n| `03` | Plaintext PIN by ICC + signature |\n| `04` | Enciphered PIN verified by ICC |\n| `05` | Enciphered PIN by ICC + signature |\n| `1E` | Signature |\n| `1F` | No CVM required |\n| `20` | CDCVM (on-device CVM — used by mobile wallets) |\n\n#### Byte 2 — Condition byte\n\nWhen does this rule apply? From `TlvParser.kt`:\n\n| Code (hex) | Condition |\n|------------|-----------|\n| `00` | Always |\n| `01` | If unattended cash |\n| `02` | If not unattended cash or manual |\n| `03` | If terminal supports the CVM |\n| `06` | If amount under X |\n| `07` | If amount over X |\n| `08` | If amount under Y |\n| `09` | If amount over Y |\n\nSo when you see a CVM list like:\n\n```\n00 00 00 00  00 00 00 00  1F 03 1F 00\n└─ X = 0 ─┘  └─ Y = 0 ─┘  └R1┘  └R2┘\n```\n\nDecoded:\n- Rule 1: `1F 03` = \"No CVM required\" if terminal supports it\n- Rule 2: `1F 00` = \"No CVM required\" always\n\nThat's a typical no-CVM-required contactless card for low-value transactions.\n\nA CDCVM card (Apple Pay, Google Pay) typically shows:\n\n```\n... 42 03 1E 03 1F 00\n    │     │     │\n    │     │     └─ Fallback: no CVM, always\n    │     └─ Fallback 2: signature, if terminal supports\n    └─ Method 0x42 = 0x40 (apply next if fail) | 0x02 (enciphered PIN online)\n```\n\nNote the high bit `0x40` set on the method byte = \"if this fails, try the next rule\".\n\n---\n\n## 15. Application Interchange Profile (AIP) Decoded\n\nTag `82` — 2 bytes, **the** capability bitmap of the card application. Returned in the GPO response. TapRead displays the raw value; here's the full decode for reference.\n\n### Byte 1\n\n| Bit | Meaning if set |\n|-----|----------------|\n| 8 | RFU |\n| 7 | **SDA** supported (Static Data Authentication) |\n| 6 | **DDA** supported (Dynamic Data Authentication) |\n| 5 | Cardholder verification is supported |\n| 4 | Terminal risk management is to be performed |\n| 3 | Issuer authentication is supported |\n| 2 | On-device cardholder verification supported (CDCVM) |\n| 1 | **CDA** supported (Combined Dynamic Data Authentication / Generate AC) |\n\n### Byte 2\n\n| Bit | Meaning if set |\n|-----|----------------|\n| 8 | EMV mode supported (contactless) |\n| 7–1 | RFU (some kernels use these for scheme-specific flags) |\n\nSo an AIP of `7C 00`:\n\n```\n0111 1100  0000 0000\n ││││││ │     reserved\n ││││││ └─ Cardholder verification supported\n │││││└─ Terminal risk mgmt to be performed\n ││││└─ Issuer auth supported\n │││└─ CDCVM supported\n ││└─ CDA supported\n │└─ DDA supported\n └─ SDA supported (here = 1, supports SDA)\n```\n\n→ Card supports SDA, DDA, CV, terminal risk mgmt, issuer auth, CDCVM, and CDA — a fully-featured modern chip card.\n\n### How a Terminal Picks an ODA Method\n\nThe priority is **CDA \u003e DDA \u003e SDA**:\n\n- If both card and terminal support CDA → CDA is performed\n- Else if both support DDA → DDA is performed\n- Else if both support SDA → SDA is performed\n- Else → \"ODA not performed\" bit in TVR (`95`) is set\n\nCDA is strongest because it binds the dynamic signature to the application cryptogram itself, making relay attacks much harder.\n\n---\n\n## 16. Application Usage Control (AUC) Decoded\n\nTag `9F07` — 2 bytes — the issuer's restrictions on where and how the card can be used.\n\n### Byte 1\n\n| Bit | Meaning if set |\n|-----|----------------|\n| 8 | Valid for domestic cash transactions |\n| 7 | Valid for international cash transactions |\n| 6 | Valid for domestic goods |\n| 5 | Valid for international goods |\n| 4 | Valid for domestic services |\n| 3 | Valid for international services |\n| 2 | Valid at ATMs |\n| 1 | Valid at terminals other than ATMs |\n\n### Byte 2\n\n| Bit | Meaning if set |\n|-----|----------------|\n| 8 | Domestic cashback allowed |\n| 7 | International cashback allowed |\n| 6–1 | RFU |\n\nSo `9F07 = FF00`:\n\n- `FF` = all bits of byte 1 set → valid everywhere, all transaction types, ATMs + POS\n- `00` = no cashback bits set\n\nThat's a typical unrestricted credit card.\n\nA \"domestic only\" card might be `9F07 = AB 00`:\n\n- Byte 1: `1010 1011`\n- Bit 8: domestic cash ✓\n- Bit 6: domestic goods ✓\n- Bit 4: domestic services ✓\n- Bit 2: at ATMs ✓ (needed for the card to work in any ATM)\n- Bit 1: at non-ATM terminals ✓ (needed for the card to work in POS)\n- All international bits (7, 5, 3) cleared → card refuses cross-border use\n\nNote that even a restricted card must set bits 2 and 1 in byte 1 — otherwise the card would be unusable anywhere. A bare `A8 00` (bits 8/6/4 only, no terminal-type bits) would refuse every transaction.\n\nThe card uses AUC to refuse transactions outside its allowed scope before even generating a cryptogram.\n\n---\n\n## 17. Terminal \u0026 Card Transaction Qualifiers (TTQ \u0026 CTQ)\n\nTwo Visa-defined contactless tags (also used by other schemes in equivalent kernels). Both are 4-byte (TTQ) and 2-byte (CTQ) bit arrays that negotiate how a contactless transaction will be processed.\n\n### TTQ — Tag 9F66, 4 bytes (terminal → card via PDOL)\n\n#### Byte 1\n\n| Bit | Meaning if set |\n|-----|----------------|\n| 8 | Contactless MSD supported (Magnetic Stripe Data emulation, legacy) |\n| 7 | Contactless VSDC supported (full EMV contactless) |\n| 6 | Contactless qVSDC supported (Visa quick chip) |\n| 5 | Contact chip supported |\n| 4 | Offline-only reader |\n| 3 | Online PIN supported |\n| 2 | Signature supported |\n| 1 | ODA for Online Authorizations supported |\n\n#### Byte 2\n\n| Bit | Meaning if set |\n|-----|----------------|\n| 8 | Online cryptogram required |\n| 7 | CVM required |\n| 6 | Contact chip offline PIN supported |\n| 5–1 | RFU |\n\n#### Byte 3\n\n| Bit | Meaning if set |\n|-----|----------------|\n| 8 | Issuer Update Processing supported |\n| 7 | Mobile functionality supported (Consumer Device CVM) |\n| 6–1 | RFU |\n\n#### Byte 4\n\nAll RFU.\n\n### CTQ — Tag 9F6C, 2 bytes (card → terminal in GPO response)\n\n#### Byte 1\n\n| Bit | Meaning if set |\n|-----|----------------|\n| 8 | Online PIN required |\n| 7 | Signature required |\n| 6 | Go online if offline authentication fails and reader is online-capable |\n| 5 | Switch interface if offline authentication fails and reader supports VIS |\n| 4 | Go online if AAC was generated (decline) |\n| 3 | Switch interface for cash transactions |\n| 2 | Switch interface for cashback transactions |\n| 1 | RFU |\n\n#### Byte 2\n\n| Bit | Meaning if set |\n|-----|----------------|\n| 8 | Consumer Device CVM performed |\n| 7 | Card supports Issuer Update Processing at POS |\n| 6–1 | RFU |\n\nThe interaction: terminal advertises capabilities via TTQ → card responds with CTQ that *overrides* or refines TTQ based on transaction context (amount, merchant category, card profile).\n\n---\n\n## 18. Application Cryptograms — TC, ARQC, AAC\n\nWhen a real POS terminal completes a transaction, it sends a **GENERATE AC** command to the card. The card responds with an 8-byte cryptogram (tag `9F26`) and a 1-byte type indicator (tag `9F27`).\n\nTapRead does **not** issue GENERATE AC — it's a read-only tool. But the transaction log on the card stores past cryptograms, which TapRead surfaces. Here's what they mean.\n\n### The Three Cryptogram Types\n\n| Type | Tag 9F27 (top nibble) | Meaning |\n|------|----------------------|---------|\n| **TC** (Transaction Certificate) | `40` | Offline approval — card says \"I'll honor this transaction offline\" |\n| **ARQC** (Authorization Request Cryptogram) | `80` | Online auth requested — card needs the issuer to decide |\n| **AAC** (Application Authentication Cryptogram) | `00` | Offline decline — card refuses the transaction |\n\nThere's also a fourth, rare type:\n\n| Type | 9F27 | Meaning |\n|------|------|---------|\n| **AAR** (Application Authorization Referral) | `10` | \"Refer to issuer\" — used in some markets, between ARQC and AAC in severity |\n\n### Hierarchy\n\nThe decision hierarchy is **TC \u003e ARQC \u003e AAC** (highest to lowest). Card risk management can downgrade — if the terminal proposes TC but the card disagrees, it returns ARQC or AAC.\n\n### How It's Generated\n\nThe cryptogram is an **8-byte MAC** computed over CDOL data using a session key derived from the **MK_AC** master key (3DES-CBC, or AES on newer cards):\n\n```\nSK_AC = derive(MK_AC, ATC, [UN])           // session key per transaction\nAC = MAC_SK_AC(amount || country || ... || ATC)\n```\n\nThe MAC inputs come from CDOL1 (first GENERATE AC) or CDOL2 (second GENERATE AC). The unique inputs are:\n- **ATC** (Application Transaction Counter, tag `9F36`) — increments on every transaction\n- **Unpredictable Number** (tag `9F37`) — generated by the terminal, prevents replay\n\nBecause the ATC always increments and the UN is unpredictable, every cryptogram is unique. Even if you read the card 1000 times, you'd see no useful pattern. This is what makes EMV secure against simple cloning.\n\n### What TapRead Shows\n\nIn the TRANSACTIONS tab, the \"Cryptogram\" field next to each transaction is the AC from that historical transaction's log entry. The library returns it as a hex string. Some cards (especially modern ones) don't expose this in the transaction log at all — only at the moment of the transaction.\n\n### Why You Can't Replay It\n\nEven with TapRead reading the card and capturing every byte, you cannot use the cryptogram to fake a transaction. The issuer validates the cryptogram by re-deriving the session key (using the ATC and stored MK_AC) and re-computing the MAC. If the ATC has been seen before, or if the cryptogram inputs don't match what the terminal claims, the transaction is declined.\n\n---\n\n## 19. GENERATE AC — The Command You'll Never See in TapRead\n\nTapRead never sends this command — it's read-only. But every real POS terminal sends it, and understanding it explains why TapRead *can't* clone a card no matter how much data it reads. This section is the bridge between \"this is what the chip stores\" and \"this is what the chip actually does.\"\n\n### The Command\n\n```\n→ Command:  80 AE \u003cP1\u003e 00 \u003cLc\u003e \u003cCDOL-data\u003e 00\n            │  │  │              │\n            │  │  │              └─ The data the card requested via CDOL1 (or CDOL2)\n            │  │  └─ P1 = Reference Control Parameter (decides what the card should return)\n            │  └─ INS = AE (GENERATE AC)\n            └─ CLA = 80 (proprietary EMV)\n```\n\nThe Reference Control Parameter (P1) is what makes this command interesting. It's a single byte:\n\n```\n┌─┬─┬─┬─┬─┬─┬─┬─┐\n│Type │R│CDA│ - │\n└─┴─┴─┴─┴─┴─┴─┴─┘\n 8 7   6   5   4-1 (RFU)\n```\n\n| Bits 8-7 | Cryptogram terminal asks for |\n|----------|------------------------------|\n| `00` | AAC (Application Authentication Cryptogram — decline) |\n| `01` | TC (Transaction Certificate — offline approve) |\n| `10` | ARQC (Authorization Request Cryptogram — go online) |\n| `11` | RFU |\n\nBit 6 — Combined DDA/AC signature requested (only valid if AIP says CDA is supported).\n\nSo `P1 = 0x40` means \"give me a TC\" → P1 `0100 0000`. `P1 = 0x80` means \"give me an ARQC\". `P1 = 0x00` means \"give me an AAC\".\n\n### Why The Card Might Not Comply\n\nThe card runs its **own** risk management. Even if the terminal asks for a TC (\"approve offline\"), the card can refuse and return an ARQC instead (\"no, ask the issuer\"). It can even override an ARQC request and return AAC (\"nope, declined\").\n\nThe hierarchy is **TC \u003e ARQC \u003e AAC** — the card can only downgrade, never upgrade. This is why a \"fail-secure\" model works: a malicious terminal can't force the card to approve a transaction that the card's risk rules don't allow.\n\n### Two-Phase Flow (Contact Cards)\n\nContact cards do this in two steps:\n1. **First GENERATE AC** with CDOL1 data → card responds with first AC (TC, ARQC, or AAC)\n2. If first AC was ARQC, terminal goes online → issuer returns ARPC → terminal sends **Second GENERATE AC** with CDOL2 data → card responds with second AC (TC or AAC — the final word)\n\n### One-Phase Flow (Contactless)\n\nContactless transactions compress this. Per Visa's qVSDC and Mastercard's PayPass M/Chip, the card returns its AC **in the GPO response itself** (tags `9F26` cryptogram + `9F27` cryptogram info data + `9F36` ATC, all inline) — there's no separate GENERATE AC command at all. That's the \"fast path\" in [Visa MSD vs qVSDC vs VSDC](#30-visa-msd-vs-qvsdc-vs-vsdc) below.\n\n### Real CDOL1\n\nA real CDOL1 from a Visa card:\n\n```\n8C 21  9F02 06  9F03 06  9F1A 02  95 05  5F2A 02  9A 03  9C 01  9F37 04  9F35 01  9F45 02  9F4C 08  9F34 03\n└─┘└┘\n │  │\n │  └─ length of the tag-length list itself = 33 bytes (0x21)\n └─ tag 8C (CDOL1)\n```\n\nThe card is saying: \"If you want me to generate an AC, send me these in this order: amount authorized (6B), amount other (6B), terminal country (2B), TVR (5B), transaction currency (2B), transaction date (3B), transaction type (1B), unpredictable number (4B), terminal type (1B), data auth code (2B), ICC dynamic number (8B), CVM results (3B). **Total CDOL data sent by terminal = 43 bytes** (the sum of the lengths).\"\n\nTwo different sizes are at play here, which is easy to confuse:\n- **0x21 = 33** — the size of the CDOL1 *itself* (the tag-length list inside the card)\n- **43 bytes** — the size of the *data* the terminal builds and sends back in GENERATE AC\n\nThe card uses these 43 bytes to compute the AC: `AC = MAC_SK_AC(amount || country || ... || ATC)`.\n\n### Why TapRead Doesn't Do This\n\nTapRead could *technically* send GENERATE AC with all-zero CDOL data. The card would generate an AC and return it. But that's pointless — it would burn an ATC value (which is permanent and visible to your bank's fraud system) without producing any useful information for the user. TapRead reads what the card already stores; it doesn't trigger new transactions.\n\n---\n\n## 20. Offline Data Authentication — The RSA Cert Chain\n\nThis is the cryptographic heart of EMV. It's how a terminal proves a card is real *without* needing to call the issuer.\n\n### Why It Exists\n\nWhen EMV launched in the mid-1990s, going online for every transaction was expensive and slow. Cards needed a way to prove they were genuine using only data the card itself carried. The answer: a 3-level RSA public-key infrastructure.\n\n### The Three Levels\n\n```\n┌─────────────────────────────────────────────┐\n│  Scheme CA Public Key  (Visa CA, Mastercard CA, ...)\n│  ↓ signs                                    │\n│  Issuer Public Key Certificate (tag 90)     │\n│  ↓ verifies issuer pub key, which signs:    │\n│  ICC Public Key Certificate (tag 9F46)      │\n│  ↓ verifies ICC pub key                     │\n│  Then ICC pub key verifies the card's signature\n└─────────────────────────────────────────────┘\n```\n\nThe terminal ships with **CA public keys for every scheme it supports** — Visa CA #9, Mastercard CA #5, Amex CA #3, etc. The CA index used by the card is in tag `8F` (Certification Authority Public Key Index). Terminals carry a database that maps `(RID, CA index) → CA modulus + exponent`.\n\n### Three Modes (in order of strength)\n\n#### SDA — Static Data Authentication\n\nThe card stores a **pre-signed** static blob. The terminal verifies the signature using the CA → issuer → static data chain.\n\nTags involved:\n- `8F` — CA Public Key Index\n- `90` — Issuer Public Key Certificate\n- `92` — Issuer Public Key Remainder (if the modulus is too big to fit in the certificate)\n- `9F32` — Issuer Public Key Exponent\n- `93` — Signed Static Application Data (the pre-computed signature)\n- `9F4A` — Static Data Authentication Tag List (which tags went into the hash)\n\n**Weakness**: the signed blob never changes. An attacker can copy it to a clone card. SDA doesn't prove the *card* is real, only that *some* card with this data exists.\n\nEMVCo formally deprecated SDA for new cards in 2024 — but SDA cards issued before then are still in circulation.\n\n#### DDA — Dynamic Data Authentication\n\nThe card has its **own RSA key pair** (ICC private + ICC public). The ICC public key is signed by the issuer (in the ICC PK certificate). For each transaction:\n\n1. Terminal sends `INTERNAL AUTHENTICATE` with an Unpredictable Number\n2. Card signs `(UN || ICC dynamic number)` with the ICC private key\n3. Terminal verifies the signature with the recovered ICC public key\n\nTags involved (in addition to SDA's):\n- `9F46` — ICC Public Key Certificate\n- `9F47` — ICC Public Key Exponent\n- `9F48` — ICC Public Key Remainder (if too big)\n- `9F49` — DDOL (what the terminal should put in the INTERNAL AUTHENTICATE)\n- `9F4B` — Signed Dynamic Application Data (the card's signature)\n\n**Strength**: the signature is unique per transaction. A clone can't replay it because it'd need the ICC private key.\n\n#### CDA — Combined DDA / Application Cryptogram Generation\n\nCDA is DDA + binding to the AC in one step. The card signs `(AC || transaction data || UN)` — so the dynamic signature is cryptographically bound to the specific cryptogram for this specific transaction.\n\nThis blocks the [pre-play attack](#32-relay-attacks) where an attacker captures a card's signature before the transaction details are decided.\n\n### How a Terminal Picks Which Mode\n\nThe terminal reads tag `82` (AIP) to learn what the card supports. The priority is:\n\n1. If both card and terminal support **CDA** → CDA\n2. Else if both support **DDA** → DDA\n3. Else if both support **SDA** → SDA\n4. Else → \"ODA not performed\" bit set in TVR (`95`), terminal must go online for auth\n\n### Verifying the Chain\n\nThe full verification of a DDA card:\n\n```\nStep 1: Retrieve CA Public Key\n        terminal_DB[8F + RID] → (CA_modulus, CA_exponent)\n\nStep 2: Decrypt Issuer PK Certificate (tag 90)\n        IssuerPKCert_recovered = RSA_pubkey(CA, tag_90)\n        Verify trailer = 'BC', hash inside certificate matches hash of issuer data\n        Extract: issuer modulus (or its head + tag 92 remainder)\n\nStep 3: Decrypt ICC PK Certificate (tag 9F46)\n        ICCPKCert_recovered = RSA_pubkey(IssuerPK, tag_9F46)\n        Verify trailer = 'BC', hash matches hash of ICC data\n        Extract: ICC modulus (or head + tag 9F48 remainder)\n\nStep 4: Verify Card's Signature (tag 9F4B for DDA/CDA, tag 93 for SDA)\n        signed_data = RSA_pubkey(ICCPK, tag_9F4B)\n        Verify trailer = 'BC', verify the signed UN and dynamic data\n```\n\nHash algorithm: SHA-1 (yes, still — EMV uses SHA-1 because of the cert chain depth and key size limits, and EMVCo is migrating to ECC + SHA-256 in newer kernel revisions).\n\n### Why TapRead Doesn't Verify\n\nTapRead reads the certificates (tags `90`, `9F46`, `8F`, etc.) and shows them in the APDU log. But it doesn't *verify* them, because:\n\n1. To verify, you need the **CA public key database** — TapRead would have to ship Visa/Mastercard CA modulus values for every key index\n2. Verification needs RSA modular exponentiation — straightforward but adds dependency weight\n3. Even if verified, it wouldn't tell the user anything new — the card *was already legit* if it responded to PPSE\n\nA future version (roadmap §46) could add CA key bundles for SDA/DDA verification. For now, the cert blobs are just hex in the log.\n\n---\n\n## 21. The 9F4F Trick — Transaction Time Extraction\n\nThis is the most technically interesting part of TapRead — and the part that required the most reverse engineering.\n\n### The Problem\n\nThe devnied/EMV-NFC-Paycard-Enrollment library parses card data brilliantly, but **it does not expose tag `9F21` (Transaction Time)**.\n\nWhy? Because tag `9F21` doesn't appear in the transaction log as a discrete TLV. **The log records are flat** — no tags, just concatenated values. The schema is described separately by tag `9F4F` (Log Format).\n\n### The Solution — Step by Step\n\nTapRead does this manually in `EmvReader.extractTimesFromApdu()` and `parseLogFormat()`:\n\n#### 1. Find the Log Format (9F4F)\n\nTag `9F4F` appears somewhere in the SELECT AID response or in one of the regular READ RECORD responses, depending on the card. It contains a list of tag-length pairs that define the structure of each flat log record.\n\nExample:\n```\n9F 4F 18 9A 03 9F 21 03 9F 02 06 5F 2A 02 9A 03 9F 36 02 9F 27 01 9F 1A 02 95 05\n└─┘└┘└┘ └─the format data, 0x18 = 24 bytes total─────────────────────────────┘\n │  │  └─ first tag in the format\n │  └─ length of the format = 24 bytes\n └─ tag 9F4F\n```\n\nTapRead finds it by scanning every APDU response for the substring \"9F4F\":\n\n```kotlin\nfor (entry in logger.entries) {\n    val hex = HexUtil.toHex(entry.response).uppercase()\n    logFormat = parseLogFormat(hex)\n    if (logFormat != null) break\n}\n```\n\nThen `parseLogFormat()` walks the format bytes, reading tag-length pairs:\n\n```kotlin\nwhile (i \u003c formatData.length - 3) {\n    val firstByte = formatData.substring(i, i + 2).toIntOrNull(16) ?: break\n    val tag: String\n    if ((firstByte and 0x1F) == 0x1F) {\n        // 2-byte tag\n        if (i + 4 \u003e formatData.length) break\n        tag = formatData.substring(i, i + 4)\n        i += 4\n    } else {\n        tag = formatData.substring(i, i + 2)\n        i += 2\n    }\n    val len = formatData.substring(i, i + 2).toIntOrNull(16) ?: break\n    i += 2\n    tags.add(Pair(tag, len))\n}\n```\n\nNote: the parser treats every length byte as a single byte (no `81`/`82` long-form handling in log format — log records never need it).\n\nFor the example above, the parsed format list:\n\n| Tag | Length |\n|-----|--------|\n| `9A` | 3 |\n| `9F21` | 3 |\n| `9F02` | 6 |\n| `5F2A` | 2 |\n| `9A` | 3 |\n| `9F36` | 2 |\n| `9F27` | 1 |\n| `9F1A` | 2 |\n| `95` | 5 |\n| | **24 total** |\n\n#### 2. Compute the Byte Offset of 9F21\n\nWalk the parsed Log Format list and sum up the lengths until you reach `9F21`:\n\n```kotlin\nvar timeOffset = -1\nvar totalRecordLen = 0\nfor ((tag, len) in logFormat) {\n    if (tag == \"9F21\") {\n        timeOffset = totalRecordLen\n    }\n    totalRecordLen += len\n}\n```\n\nFor the example:\n- `9A`:    bytes 0–2   (3 bytes)\n- `9F21`:  bytes 3–5   ← **transaction time lives here**\n- `9F02`:  bytes 6–11\n- `5F2A`:  bytes 12–13\n- ...\n\nSo for this card, **bytes 3, 4, 5** of every log record contain the transaction time in BCD.\n\n#### 3. Read Each Log Record\n\nFor each captured READ RECORD response, TapRead unwraps it. The response is either:\n\n- `70 LEN \u003cflat record data\u003e 90 00` (record wrapped in template `70`)\n- Or just `\u003cflat record data\u003e 90 00` (raw)\n\nThe code handles both:\n\n```kotlin\nval recordHex: String? = if (hex.startsWith(\"70\")) {\n    val lenStart = 2\n    val lenByte = hex.substring(lenStart, lenStart + 2).toIntOrNull(16) ?: continue\n    val dataStart = if (lenByte == 0x81) lenStart + 4\n        else if (lenByte == 0x82) lenStart + 6\n        else lenStart + 2\n    if (dataStart + totalRecordLen * 2 \u003c= hex.length - 4) {\n        hex.substring(dataStart, dataStart + totalRecordLen * 2)\n    } else null\n} else null\n\nval data = recordHex ?: if (resp.size \u003e= totalRecordLen + 2) {\n    hex.substring(0, totalRecordLen * 2)\n} else continue\n```\n\n#### 4. Extract the 3 BCD Bytes at Offset 3\n\n```kotlin\nval timeStart = timeOffset * 2  // * 2 because we're working in hex chars\nval hh = data.substring(timeStart, timeStart + 2)\nval mm = data.substring(timeStart + 2, timeStart + 4)\nval ss = data.substring(timeStart + 4, timeStart + 6)\n```\n\nExample: bytes `13 32 08` → \"13:32:08\"\n\n#### 5. Sanity Checks\n\nReal cards sometimes return garbage rows (records that were initialized but never used). TapRead validates the BCD values are sane:\n\n```kotlin\nval h = hh.toInt(); val m = mm.toInt(); val s = ss.toInt()\nif (h in 0..23 \u0026\u0026 m in 0..59 \u0026\u0026 s in 0..59) {\n    times.add(\"$hh:$mm:$ss\")\n}\n```\n\nPlus a separate cleanup pass removes `00:00:00` times (which usually mean \"no time data\" rather than \"midnight transaction\"):\n\n```kotlin\nfor (i in all.indices) {\n    if (all[i].time == \"00:00:00\") {\n        all[i] = all[i].copy(time = null)\n    }\n}\n```\n\nThis is the kind of detail you only learn by reading 200+ cards and watching what real-world data looks like.\n\n---\n\n## 22. ATR, ATS, and CPLC\n\n### ATR (Answer To Reset) — for contact cards\n\nIn contact cards, the ATR is the first thing the chip transmits after voltage is applied. It identifies the chip platform — manufacturer, OS, generation.\n\n### ATS (Answer To Select) — for contactless\n\nFor contactless, the equivalent is the **ATS** returned during ISO 14443-4 RATS. Android exposes the historical bytes via:\n\n```kotlin\noverride fun getAt(): ByteArray {\n    return isoDep?.historicalBytes ?: isoDep?.hiLayerResponse ?: byteArrayOf()\n}\n```\n\n`historicalBytes` is for Type A; `hiLayerResponse` is for Type B. TapRead falls back from one to the other.\n\ndevnied ships an **ATR database** mapping known historical-byte patterns to chip platforms (\"NXP P60D080 JCOP\", \"Infineon SLE 78\", etc.). TapRead pulls this via the `EmvCard.atrDescription` field:\n\n```kotlin\nval atrDesc = emvCard.atrDescription?.joinToString(\"; \")\n```\n\n### CPLC (Card Production Life Cycle)\n\nTag `9F7F` — 42 bytes of structured data identifying who made the chip and when. Layout per Visa/Mastercard spec:\n\n| Offset | Length | Field |\n|--------|--------|-------|\n| 0 | 2 | IC Fabricator |\n| 2 | 2 | IC Type |\n| 4 | 2 | Operating System ID |\n| 6 | 2 | OS release date (YDDD) |\n| 8 | 2 | OS release level |\n| 10 | 2 | IC fabrication date (YDDD) |\n| 12 | 4 | IC serial number |\n| 16 | 2 | IC batch identifier |\n| 18 | 2 | IC module fabricator |\n| 20 | 2 | IC module packaging date (YDDD) |\n| 22 | 2 | ICC manufacturer |\n| 24 | 2 | IC embedding date (YDDD) |\n| 26 | 2 | IC pre-personalizer ID |\n| 28 | 2 | IC pre-perso equipment date (YDDD) |\n| 30 | 4 | IC pre-perso equipment ID |\n| 34 | 2 | IC personalizer ID |\n| 36 | 2 | IC personalization date (YDDD) |\n| 38 | 4 | IC personalization equipment ID |\n\nThe IC Fabricator codes are revealing:\n\n| Code | Fabricator |\n|------|-----------|\n| `4090` | Infineon |\n| `4790` | NXP |\n| `2050` | Renesas |\n| `3060` | Samsung |\n| `4250` | STMicroelectronics |\n\nSo `9F7F 2A 4790 5031 ...` tells you the chip is NXP, type `5031` (a P5 family chip — common on Visa).\n\nTapRead extracts CPLC via reflection (the devnied `Cplc` class has private fields):\n\n```kotlin\nprivate fun extractCplc(card: EmvCard): String? {\n    val cplc = card.cplc ?: return null\n    val sb = StringBuilder()\n    reflectField\u003cAny\u003e(cplc, \"icFabricator\")?.let { sb.appendLine(\"IC Fabricator     :  $it\") }\n    reflectField\u003cAny\u003e(cplc, \"icType\")?.let { sb.appendLine(\"IC Type           :  $it\") }\n    reflectField\u003cAny\u003e(cplc, \"operatingSystemId\")?.let { sb.appendLine(\"OS ID             :  $it\") }\n    reflectField\u003cAny\u003e(cplc, \"operatingSystemDate\")?.let { sb.appendLine(\"OS Release Date   :  $it\") }\n    reflectField\u003cAny\u003e(cplc, \"icManufacturer\")?.let { sb.appendLine(\"IC Manufacturer   :  $it\") }\n    reflectField\u003cAny\u003e(cplc, \"icEmbedder\")?.let { sb.appendLine(\"IC Embedder       :  $it\") }\n    reflectField\u003cAny\u003e(cplc, \"icPrePersonalizer\")?.let { sb.appendLine(\"IC Pre-Personal.  :  $it\") }\n    reflectField\u003cAny\u003e(cplc, \"icPersonalizer\")?.let { sb.appendLine(\"IC Personalizer   :  $it\") }\n    return if (sb.isEmpty()) null else sb.toString().trimEnd()\n}\n```\n\nHow CPLC is fetched: the GET DATA command `80 CA 9F 7F 00`. Not every card responds — some return `6A88` (Referenced data not found). TapRead silently omits CPLC on failure.\n\n---\n\n## 23. Historical Bytes — Decoding the ATS Payload\n\n`IsoDep.historicalBytes` returns a byte array of variable length (typically 4–15 bytes) that comes from the card's Answer To Select (ATS) during the ISO 14443-4 RATS exchange. Per ISO 7816-4 §8, the **first byte is the category indicator**, and the rest may follow one of three structures.\n\n### Category Indicator (Byte 0)\n\n| Value | Format |\n|-------|--------|\n| `00` | \"Status information presented at the end of historical bytes\" (rare) |\n| `10` | Reference to an EF.ATR data object |\n| `8X` | Compact-TLV format (the most common — `8X` where X is the number of objects) |\n\nFor EMV cards, the category is almost always `80` or `81` — meaning Compact-TLV follows.\n\n### Compact-TLV Structure\n\nAfter byte 0, the historical bytes are a sequence of TLV-like objects, but with a compressed encoding:\n\n```\nTag nibble | Length nibble | Value bytes\n   4 bits  |    4 bits     | variable\n```\n\nThe tag nibble is one of `1`–`F`, the length nibble is 0–15. So `45 12 34 56 78 90` means \"tag 4, length 5, value = 12 34 56 78 90\".\n\nCommon compact-TLV tags in EMV historical bytes:\n\n| Tag nibble | Meaning |\n|------------|---------|\n| `1` | Country code (ISO 3166) |\n| `2` | Issuer identification number |\n| `3` | Card service data (capability bits) |\n| `4` | Initial access data |\n| `5` | Card issuer data |\n| `6` | Pre-issuing data (chip OS, ROM mask) |\n| `7` | Card capabilities |\n| `F` | Status information |\n\n### A Real Example\n\nA Visa card returned historical bytes: `80 31 80 65 B0 84 12 17 E0 83 01 90 00`\n\nParsed:\n\n- `80` — Category indicator: compact-TLV follows\n- `31` — Tag nibble `3`, length `1` → Card service data\n  - Value: `80` (10000000 → \"application selection by full DF name, no I/O, no auth\")\n- `80` — Tag nibble `8`, length `0` → \"no value\" — this is actually how some EMV cards encode \"TLV of length 0\"\n- `65 B0 84 12 17` — Looks like a pre-issuing block: `65` = tag 6, length 5 → \"B0 84 12 17\"\n- `E0 83 01` — `E0` = tag 14, length 0; then continues...\n- `90 00` — Trailer indicating status SW = `9000` (some cards append this; some don't)\n\nIn practice, parsing historical bytes is messy because card vendors interpret the spec differently. The devnied library's ATR database does a pattern-match against known prefixes:\n\n```\n\"80 31 80 65 B0 84 12 17 E0 83 01 90 00\" → \"NXP P5Cx012/021/041 SmartMX (JCOP41)\"\n```\n\nThat's how the \"atrDescription\" field in TapRead's CARD DETAIL tab gets populated.\n\n### Why It Matters for Card Identification\n\nThe historical bytes are essentially a fingerprint of the chip manufacturer + OS combination. Common patterns:\n\n| Pattern (first few bytes) | Chip + OS |\n|---------------------------|-----------|\n| `80 31 80 65 B0` | NXP SmartMX (JCOP) — most Visa/MC cards |\n| `80 80 01 01` | Infineon SLE 78 — JCOP-compatible |\n| `80 6A 84 09 47` | Samsung S3FT9MF — used by some Asian cards |\n| `00 78 80 02 47` | Renesas — older bank cards |\n| `B0 50 12` | Specifically NXP JCOP 4.5 |\n\ndevnied ships about 200 patterns. The match-or-fail behavior means new chip generations may show as \"unknown\" until devnied updates its database.\n\n### Where TapRead Surfaces This\n\n`EmvReader.kt`:\n```kotlin\nval atrBytes = provider.getAt()  // → historicalBytes or hiLayerResponse\nval atr = when {\n    atrBytes.isNotEmpty() -\u003e HexUtil.toHexSpaced(atrBytes)\n    !emvCard.at.isNullOrBlank() -\u003e emvCard.at\n    else -\u003e null\n}\nval atrDesc = emvCard.atrDescription?.joinToString(\"; \")\n```\n\nBoth the raw hex and the database description are stored in `CardData.atr` and `CardData.atrDescription`, then shown side-by-side in the CARD DETAIL tab.\n\n---\n\n## 24. Scheme Detection Logic\n\nTapRead's `EmvReader.detectScheme()` uses three independent signals, ranked by reliability. Code:\n\n```kotlin\nprivate fun detectScheme(card: EmvCard, aidHex: String?, appLabel: String?): String? {\n    // 1. AID prefix — most reliable\n    val fromAid = aidHex?.let { schemeFromAid(it) }\n    if (fromAid != null) return fromAid\n\n    // 2. devnied's library-detected card type\n    val cardType = card.type\n    if (cardType != null) { /* reflection-extract name */ }\n\n    // 3. Application label keyword\n    if (!appLabel.isNullOrBlank()) {\n        return when {\n            appLabel.contains(\"visa\", true) -\u003e \"Visa\"\n            appLabel.contains(\"master\", true) -\u003e \"Mastercard\"\n            appLabel.contains(\"amex\", true) -\u003e \"Amex\"\n            appLabel.contains(\"jcb\", true) -\u003e \"JCB\"\n            appLabel.contains(\"union\", true) -\u003e \"UnionPay\"\n            else -\u003e appLabel\n        }\n    }\n\n    // 4. PAN first-digit fallback\n    val pan = card.cardNumber\n    if (!pan.isNullOrBlank()) {\n        return when (pan[0]) {\n            '4' -\u003e \"Visa\"; '5' -\u003e \"Mastercard\"\n            '3' -\u003e if (pan.startsWith(\"34\") || pan.startsWith(\"37\")) \"Amex\" else \"JCB\"\n            '6' -\u003e \"Discover/UnionPay\"; else -\u003e null\n        }\n    }\n    return null\n}\n```\n\n### AID Prefixes Mapped\n\nEvery payment scheme has a Registered Application Provider Identifier (RID) — the first 5 bytes of the AID. From `schemeFromAid()`:\n\n| RID prefix | Scheme |\n|------------|--------|\n| `A000000003` | Visa |\n| `A000000004` | Mastercard |\n| `A000000025` | Amex |\n| `A000000065` | JCB |\n| `A000000333` | UnionPay |\n| `A000000152` | Discover |\n| `A000000615` | Mastercard (additional) |\n\nThe full industry RID list (not all in the code but useful reference):\n\n| RID | Scheme |\n|-----|--------|\n| `A000000003` | Visa |\n| `A000000004` | Mastercard |\n| `A000000025` | American Express |\n| `A000000065` | JCB |\n| `A000000277` | Interac |\n| `A000000324` | Discover (Diners) |\n| `A000000333` | UnionPay |\n| `A000000337` | TROY (Turkey) |\n| `A000000476` | RuPay (India) |\n| `A000000054` | CB (France) |\n| `A000000152` | Discover |\n| `A000000167` | Dankort |\n| `A000000228` | SPAN (Saudi Arabia) |\n| `A000000615` | Mastercard |\n| `A000000658` | MIR (Russia) |\n\nThe remaining bytes of the AID identify the product within the scheme:\n\n| Full AID | Product |\n|----------|---------|\n| `A0000000031010` | Visa Credit/Debit |\n| `A0000000032010` | Visa Electron |\n| `A0000000033010` | Visa Interlink |\n| `A0000000041010` | Mastercard |\n| `A0000000043060` | Maestro |\n| `A0000000044010` | Mastercard Specific |\n| `A000000025010402` | Amex |\n| `A000000025010701` | Amex Express Pay |\n\n---\n\n## 25. PAN, IIN/BIN, and Luhn\n\nThe PAN (Primary Account Number) isn't a random string — it's a highly structured identifier defined by ISO/IEC 7812. Understanding its structure tells you a lot about a card before you even look at the AID.\n\n### The Three Parts\n\n```\n┌─┬───────────┬─────────────────────┬─┐\n│M│   IIN     │  Account number     │C│\n└─┴───────────┴─────────────────────┴─┘\n 1   5 or 7         variable        1\n```\n\n- **MII** (Major Industry Identifier) — the first digit. Identifies the *industry*, not the bank.\n- **IIN/BIN** (Issuer Identification Number / Bank Identification Number) — the first 6 or 8 digits **including** the MII. Identifies the issuing institution.\n- **Account number** — variable length, identifies the specific account.\n- **Luhn check digit** — last digit, validates the rest.\n\nPAN length is 8–19 digits per ISO 7812, but in practice 13, 15, 16, or 19 digits are common. Most banks issue 16-digit PANs.\n\n### The MII Table\n\n| MII digit | Industry |\n|-----------|----------|\n| `0` | ISO/TC 68 and other industry assignments |\n| `1` | Airlines |\n| `2` | Airlines, financial, and other future assignments |\n| `3` | Travel and entertainment (Amex, Diners) |\n| `4` | Banking and financial (Visa) |\n| `5` | Banking and financial (Mastercard) |\n| `6` | Merchandising and banking/financial (Discover) |\n| `7` | Petroleum and other future industry |\n| `8` | Healthcare, telecommunications |\n| `9` | For assignment by national standards bodies |\n\nThis is why every Visa starts with `4`, every Mastercard with `5` (or the new `2221`–`2720` range), Amex with `34`/`37`, etc.\n\n### 6-Digit vs 8-Digit IIN\n\nIn **April 2022**, Visa and Mastercard officially migrated to 8-digit IINs (ISO/IEC 7812-1 revision published 2017). Two reasons:\n\n1. **Exhaustion** — the 6-digit space had run out as new fintechs, neobanks, and prepaid issuers proliferated\n2. **Granularity** — 8-digit BINs allow much finer issuer-level routing\n\nVisa: stopped assigning new 6-digit BINs after April 2022. Mastercard: still assigns both, but new programs default to 8-digit.\n\n**What this means for TapRead's scheme detection**: the first-character fallback in `detectScheme()` still works (every Visa still starts with `4`). But if you're building a BIN lookup feature (roadmap §46), you need to support **both** 6-digit and 8-digit lookups. Some lookup services key by 6-digit; some by 8-digit. The trick is: try 8-digit first, fall back to 6-digit.\n\n### Luhn Check Digit\n\nThe last digit of the PAN is the Luhn checksum. It catches accidental typos (single-digit errors and most adjacent-transposition errors). Algorithm:\n\n```\n1. From the rightmost digit (the check digit), move left.\n2. Double every second digit (skip the check digit itself).\n3. If doubling produces a 2-digit number, sum those two digits (or equivalently, subtract 9).\n4. Sum all the digits.\n5. The sum should be divisible by 10.\n```\n\nExample for PAN `4111111111111111`:\n\n```\n4  1  1  1  1  1  1  1  1  1  1  1  1  1  1  1\n×1 ×2 ×1 ×2 ×1 ×2 ×1 ×2 ×1 ×2 ×1 ×2 ×1 ×2 ×1 ×2  (alternate from right)\n4  2  1  2  1  2  1  2  1  2  1  2  1  2  1  2\nSum = 4+2+1+2+1+2+1+2+1+2+1+2+1+2+1+2 = 27 ... not divisible by 10\n```\n\nWait — let me redo. The check digit alternation starts from the right, **skipping** the check digit. For `4111 1111 1111 1111`:\n\n```\nPosition (from right):  16 15 14 13 12 11 10 9  8  7  6  5  4  3  2  1\nDigit:                   4  1  1  1  1  1  1  1  1  1  1  1  1  1  1  1\nDouble (positions 2,4,...):\n                         ×2    ×2    ×2  ×2  ×2  ×2  ×2  ×2\n                         8     2     2   2   2   2   2   2\nElse keep:               4     1     1   1   1   1   1   1   1\n                                                                  └─ check digit (not doubled)\nSum of all = 8+1+2+1+2+1+2+1+2+1+2+1+2+1+2+1 = 30 ✓ divisible by 10\n```\n\nSo `4111111111111111` is Luhn-valid. (It's also the canonical test number — never a real card.)\n\n### TapRead Doesn't Validate Luhn\n\nTapRead trusts whatever the card returns. If a card returned `5xxx xxxx xxxx xxx0` with bad Luhn, the PAN field would still display it — because the chip is the authoritative source. If the Luhn is wrong, it's the bank's manufacturing problem, not the user's.\n\nA future BIN-lookup feature could add a Luhn-validity badge. Not in v1.0.0.\n\n### BIN Lookup Mechanics\n\nWhen TapRead's \"BIN Lookup\" button opens `bincheck.io`, it appends the first 6 or 8 digits of the PAN to the URL. The remote service queries its database (typically scraped from open lists like `iin-list/iin-list-data` plus commercial sources) and returns:\n\n- Issuing bank name\n- Card type (debit/credit/prepaid)\n- Card level (standard/gold/platinum/business)\n- Issuing country + currency\n\nFor an offline future implementation (roadmap §46): the open dataset has ~100K BIN entries totaling ~10 MB compressed. That fits comfortably in `assets/` and can be loaded into a `HashMap\u003cString, BinEntry\u003e` at app start for O(1) lookups.\n\n---\n\n## 26. Tokenized Wallet Detection\n\nWhen you add a card to Apple Pay, Google Pay, etc., the wallet doesn't store your real PAN. Instead, it provisions a **Device PAN (DPAN)** — a different number issued by the network specifically for that device.\n\nTapRead's `detectWallet()` uses **application label matching** as the primary signal:\n\n```kotlin\nfor (app in apps) {\n    val label = app.applicationLabel?.uppercase() ?: continue\n    when {\n        label.contains(\"APPLE\") -\u003e return Pair(\"Apple Pay\", true)\n        label.contains(\"GOOGLE\") -\u003e return Pair(\"Google Pay\", true)\n        label.contains(\"SAMSUNG\") -\u003e return Pair(\"Samsung Pay\", true)\n        label.contains(\"GARMIN\") -\u003e return Pair(\"Garmin Pay\", true)\n        label.contains(\"FITBIT\") -\u003e return Pair(\"Fitbit Pay\", true)\n        label.contains(\"HUAWEI\") -\u003e return Pair(\"Huawei Pay\", true)\n        label.contains(\"XIAOMI\") || label.contains(\"MI PAY\") -\u003e return Pair(\"Xiaomi Pay\", true)\n        label.contains(\"WEARABLE\") -\u003e return Pair(\"Wearable Payment\", true)\n        label.contains(\"PAY\") \u0026\u0026 !label.contains(\"PAYWAVE\") \u0026\u0026 !label.contains(\"PAYPASS\") -\u003e\n            return Pair(app.applicationLabel ?: \"Mobile Pay\", true)\n    }\n}\n```\n\n### A Subtle Trap — Tag 9F6E\n\nThere's a comment in the code explaining a detection trap:\n\n\u003e Tag 9F6E (Form Factor Indicator) exists on ALL Visa cards including physical. Only flag as tokenized when we're confident (label-based above). Physical cards have 9F6E with consumer device = 00 (standard card).\n\nEarlier versions of similar tools used the presence of tag `9F6E` as a tokenized-card indicator, but it turns out physical Visa cards also include it (as the FFI with zero values), so it's not a reliable distinguisher.\n\nIf detected, the card face in CARD DETAIL gets a small wallet logo overlay.\n\n---\n\n## 27. EMV Payment Tokenisation — How DPANs Get Provisioned\n\n§26 shows how TapRead *detects* a tokenized card. This section explains *what's actually going on under the hood* — what happens between you tapping \"Add to Apple Wallet\" and your phone responding to a contactless POS terminal.\n\n### The Players\n\nThe EMV Payment Tokenisation Specification (EMVCo, published 2014, current version 2.3 as of 2025) defines four roles:\n\n| Role | Who | What they do |\n|------|-----|--------------|\n| **Card Issuer** | Your bank | Owns the real PAN (\"FPAN\" — Funding PAN); authorizes token creation |\n| **Token Service Provider (TSP)** | Almost always Visa or Mastercard themselves | Generates the token (DPAN), keeps the FPAN↔DPAN mapping |\n| **Token Requestor** | The wallet — Apple, Google, Samsung | Asks the TSP for a token on behalf of the user |\n| **Token User** | The merchant POS | Accepts the DPAN during a transaction |\n\n### Provisioning Flow\n\n```\nYou: \"Add card to Apple Wallet\"\n  ↓\nApple Wallet (Token Requestor)\n  ↓ POST /tokenize\n     {FPAN: 4111-...-1111, device_id: ..., ...}\n  ↓\nVisa/Mastercard (TSP)\n  ↓ Token Activation Request → Issuer host (your bank)\n  ↓\nBank: \"Is this really my customer?\"\n  ↓ Decides: Approve / Approve-with-Auth (3DS step-up) / Decline\n  ↓\nTSP generates DPAN (a brand-new, never-before-used PAN in the bank's range)\n  ↓\nTSP issues a key set (per-device, per-token):\n  - Limited-Use Key (LUK) for online transactions\n  - DDA/CDA key pair for offline contactless\n  ↓\nApple secure enclave receives:\n  - DPAN\n  - Keys\n  - Personalization data (CVM lists, AIDs, AIP, etc.)\n  ↓\nDone — the iPhone now contains a complete EMV chip\n```\n\n### What's on Your iPhone\n\nAfter provisioning, your phone's Secure Element (or in Android's case, an HCE service backed by either a cloud or local crypto store) contains an entire virtual EMV card. It responds to the same SELECT PPSE / SELECT AID / GPO / READ RECORD sequence TapRead uses. But:\n\n- The PAN is the **DPAN**, not your real card number\n- Track 2 contains the **DPAN**\n- The cryptogram key is the LUK, which has limited use (typically a few hundred transactions before the wallet refreshes it)\n- The application label often includes \"Token\" or the wallet brand\n\nThis is why TapRead's tokenized-card detection works on labels — the underlying EMV layer is identical to a physical card, but the metadata gives it away.\n\n### Why DPAN ≠ FPAN\n\nThe security advantage of tokenization comes from the **domain restriction**:\n\n- The DPAN works **only for contactless / in-app payments** on this specific device\n- The DPAN **cannot** be used for card-not-present web checkout (those use a different token type, \"e-commerce tokens\")\n- If your phone is stolen, you can revoke the DPAN — but your real card stays alive\n- If a merchant's database is breached, only DPANs leak — they're worthless without the device's keys\n\n### Network-Specific Token AIDs\n\nSome schemes provision tokenized apps under different AIDs:\n\n| Wallet | AID seen in PPSE |\n|--------|------------------|\n| Apple Pay (Mastercard) | `A0000000041010` (same as physical) |\n| Apple Pay (Visa) | `A0000000031010` (same as physical) |\n| Google Pay (Visa) | `A0000000031010` |\n| Samsung Pay | Often same as scheme AID + wallet identifier in label |\n\nThe AID by itself is **not** a reliable tokenization signal — that's why TapRead's `detectWallet()` uses application labels, not AIDs. The label is where the wallet brand actually appears.\n\n### TR-31 — Not What You Think\n\nThe TR-31 key block format from ASC X9 is the **inter-server** key wrapping standard used during token provisioning between the bank's HSM and the TSP. It doesn't appear on the card itself. Mentioning it in case you see references to it — TR-31 lives in the back-office key transport layer, not in any APDU TapRead sees.\n\n### Why TapRead Reads All This Anyway\n\nA tokenized Visa card looks **identical** to a physical Visa card from TapRead's perspective — same PPSE, same AID, same record structure, same Track 2 layout. The only differences are:\n\n1. Application label contains \"Apple Pay\" / \"Google Pay\" / etc.\n2. The PAN that comes out is a DPAN (which TapRead can't tell from an FPAN by inspection alone)\n3. Sometimes there's a `9F6E` (Form Factor Indicator) with non-zero values\n\nTapRead surfaces the wallet name and an \"isTokenized: true\" flag in CARD DETAIL. The DPAN itself is shown — masked by default — same as any other PAN.\n\n---\n\n## 28. Contactless-Disabled Card Detection\n\nWhen you disable contactless in your bank app, the card itself isn't reprogrammed — the bank's backend just flags the account. But on most modern cards, the *chip* responds to that flag too. Two patterns:\n\n### Pattern A — PPSE returns 6A82\n\n```\n→ 00 A4 04 00 0E 32 50 41 59 2E 53 59 53 2E 44 44 46 30 31 00\n← 6A 82\n```\n\n`6A82` = \"File or application not found\". The PPSE has been wiped from the contactless interface. The card still works in contact mode, just not contactless.\n\n### Pattern B — PPSE returns 6985\n\n```\n→ 00 A4 04 00 0E 32 50 41 59 2E 53 59 53 2E 44 44 46 30 31 00\n← 69 85\n```\n\n`6985` = \"Conditions of use not satisfied\". The PPSE exists but refuses to enumerate apps because contactless is administratively disabled.\n\nIn both cases TapRead surfaces an orange banner with the appropriate message. From `EmvReader.kt`:\n\n```kotlin\nreturn when (sw) {\n    0x6A82 -\u003e ContactlessStatus.DISABLED\n    0x6985 -\u003e ContactlessStatus.BLOCKED\n    else -\u003e ContactlessStatus.ACTIVE\n}\n```\n\nThis check runs even when the EMV read throws an exception, ensuring users get a meaningful message instead of just \"read failed\".\n\n---\n\n## 29. EMV Contactless Kernels\n\nEMVCo defines **eight kernels** (per [EMVCo Book A, Contactless Architecture](https://www.emvco.com/specifications/)) — each is a specification for how a terminal should process a particular scheme's contactless transactions. The kernel runs on the terminal, not the card; the card just responds to commands.\n\n| Kernel | Scheme | Common Name |\n|--------|--------|-------------|\n| C-1 | JCB | J/Speedy |\n| C-2 | Mastercard | PayPass |\n| C-3 | Visa | payWave (VCPS / qVSDC) |\n| C-4 | American Express | ExpressPay |\n| C-5 | Discover | D-PAS |\n| C-6 | UnionPay | UnionPay QuickPass |\n| C-7 | UnionPay (additional) | QuickPass v2 |\n| C-8 | Universal | EMV Contactless Kernel (2022+) — unified spec to consolidate the above |\n\nTapRead is **not a terminal**, so it doesn't implement any kernel — it just reads card data. But knowing which scheme = which kernel is useful for understanding why different cards behave differently:\n\n- **PayPass (Kernel 2)** cards typically use full BER-TLV in GPO response (format `77`)\n- **payWave qVSDC (Kernel 3)** cards often use the fast-path `80` response format with embedded AC\n- **D-PAS (Kernel 5)** uses a slightly different transaction flow with DRDOL\n- **C-8 (Universal)** is being rolled out from 2022+ to consolidate all the above into one terminal codebase\n\nThe kernel decides the terminal-side flow; the card just answers. For pure data reading like TapRead does, the kernel difference doesn't matter much — every card responds to SELECT PPSE, SELECT AID, GPO, and READ RECORD.\n\n---\n\n# Part IV — The Code\n\n## 30. Visa MSD vs qVSDC vs VSDC\n\nVisa contactless cards actually support **three different transaction modes**, and which one runs depends on a negotiation in the first 50 ms of the tap.\n\n### MSD — Magnetic Stripe Data Emulation\n\nThe oldest and simplest mode. The card returns its Track 1 + Track 2 + a dynamic CVV3 (computed per-transaction using a derivation key + ATC). The terminal forwards this to the acquirer as if it were a magstripe swipe.\n\nPros: requires no terminal changes beyond accepting NFC input.\nCons: weakest security. The dynamic CVV3 is only 3 digits — easy to brute-force given thousands of attempts.\n\nThis mode is officially being phased out. Many regions (especially in EU) have explicitly disabled it in newer cards.\n\n### qVSDC — Quick Visa Smart Debit Credit (the fast path)\n\nThe standard contactless EMV mode for Visa. The whole transaction is one round trip:\n\n```\nTerminal: SELECT PPSE → SELECT AID → GPO (with TTQ and Amount)\nCard: AIP, AFL, AND the cryptogram + ATC inline (all in the GPO response)\nTerminal: READ RECORD × N\nTerminal: DONE — sends auth to issuer with the AC already in hand\n```\n\nThere's **no GENERATE AC** command. The card decides what cryptogram to return based on the TTQ flags and the amount. This is what makes qVSDC fast — typically 300–500 ms end-to-end.\n\n### VSDC — Full Visa Smart Debit Credit\n\nSame as contact EMV but over contactless. Two GENERATE AC commands, optional INTERNAL AUTHENTICATE for DDA, full terminal action analysis. Slower (often 800 ms+) but uses the same security flow as a contact chip dip.\n\nVSDC is used at:\n- ATMs (slower is fine, the user is already standing there)\n- Cardholder-verified transactions over the limit\n- Markets where qVSDC isn't certified\n\n### Who Picks the Mode?\n\nThe terminal's TTQ (tag `9F66`) advertises which modes it supports:\n- Byte 1 bit 8 = 1 → MSD supported\n- Byte 1 bit 7 = 1 → VSDC supported\n- Byte 1 bit 6 = 1 → qVSDC supported\n\nThe card looks at the TTQ and picks the highest-security mode the terminal advertises. Modern cards skip MSD entirely (treat bit 8 as 0).\n\n### Why This Matters for TapRead\n\nWhen TapRead reads a Visa card, the GPO response contains a TTQ that devnied constructs internally. devnied advertises both MSD and qVSDC by default. The card picks qVSDC (the higher-security mode) and returns the AC inline — TapRead sees it in tag `9F26` in the GPO response. This is why TapRead can show cryptograms in the transaction log even though it never explicitly asked for one.\n\n---\n\n## 31. Mastercard PayPass — MagStripe Mode vs M/Chip\n\nMastercard's PayPass kernel (EMVCo Book C-2) has two equivalent modes to Visa's:\n\n### PayPass MagStripe Mode (PCS)\n\nEquivalent to Visa MSD. Card returns Track 1 + Track 2 + dynamic CVC3. Same security trade-off, same deprecation trend.\n\n### PayPass M/Chip\n\nEquivalent to Visa qVSDC. Full EMV-style transaction in one round trip:\n\n```\nTerminal: SELECT PPSE → SELECT AID → GPO (with PDOL data)\nCard: AIP, AFL, ATC, AC, IAD — all inline\nTerminal: READ RECORD × N (for record data + DDA cert if doing CDA)\nTerminal: Build authorization with AC already in hand\n```\n\nThe differences from Visa qVSDC are subtle:\n\n| Aspect | Visa qVSDC | Mastercard M/Chip |\n|--------|------------|-------------------|\n| Terminal Qualifiers tag | `9F66` (TTQ) | `9F66` not used; uses `9F35` Terminal Type + `9F33` Terminal Capabilities |\n| GPO response format | Often Format 2 (`77 ... 9F26 ... 9F27 ... 9F36 ...`) | Almost always Format 2 |\n| Cryptogram derivation | Per Visa CKM | Per Mastercard MKD-AC |\n| CDA support | Optional | Mandatory for new cards |\n\n### Mastercard CDA Is Mandatory\n\nThis is a big practical difference. Mastercard requires CDA on all new contactless cards — meaning the cryptogram is bound to the dynamic signature in one operation. Visa requires DDA but CDA is \"preferred not required\" for contactless.\n\nIn practice, you can often tell a Mastercard from a Visa in the APDU log just by looking at how many records get read: Mastercard cards typically expose more records (because CDA needs more data signed) than Visa.\n\n---\n\n## 32. Relay Attacks\n\nA relay attack is the closest thing to a \"real-world hack\" against contactless EMV. Understanding it explains why distance-bounding is becoming part of the spec.\n\n### The Setup\n\n```\n┌─────────┐                              ┌──────────┐\n│ Victim  │ ←--NFC--- [ Mole Device ]    │  Real    │\n│  card   │           ↕ Internet/BT      │ Terminal │\n│         │  [ Relay Device ] ----NFC---→│   POS    │\n└─────────┘                              └──────────┘\n```\n\n- **Mole device** — sits near the victim's card (or wallet/pocket), powered by NFC field, relays APDU commands it gets from the relay device to the card\n- **Relay device** — sits at the merchant's POS, powered by the terminal's NFC field, receives commands from the terminal and forwards them via Bluetooth/internet to the mole\n\nThe merchant's terminal thinks it's talking to the victim's card directly. The victim's card thinks it's at a POS. Both are fooled. The transaction completes — the victim's account is charged for the attacker's purchase.\n\n### Why It Works\n\nEMV contactless was designed assuming the card is **physically close** to the terminal. Nothing in the protocol explicitly verifies that. As long as the relay devices add \u003c ~80 ms of latency, the terminal won't notice.\n\nThe 2015 van den Breek","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdeadboy18%2Ftapread","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdeadboy18%2Ftapread","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdeadboy18%2Ftapread/lists"}