1. Why I Built pynfc – From Black-Box Tools to Transparent Labs

NFC is everywhere: metro cards, office badges, vending machines, access systems… but the tools we use to interact with them are often black boxes. Vendor GUIs, half-documented Windows tools, or one-off C utilities that are painful to set up and almost impossible to extend.

I wanted something different for my own learning and labs:

  • A small, auditable codebase written in Python.
  • A simple web UI I can open from any browser on my LAN.
  • Support for a cheap USB reader like the ACR122U.
  • Focus on MIFARE Classic 1K operations: read, dump, write, value blocks.
  • A clear, explicit place to manage keys (keys.json), not some magic registry.

pynfc is my answer: a Flask app and REST API on top of PySCard and PC/SC that makes NFC experimentation feel like using a web tool instead of fighting low-level utilities. It’s explicitly meant for lab cards that you own, not for attacking production systems.

2. Hardware, Stack & Project Layout

Before touching any code, the first constraint is always hardware and drivers. pynfc is built around the idea that the NFC reader is just another PC/SC device.

2.1 Hardware & OS Support

The reference setup looks like this:

  • Reader: ACR122U (USB), or any PC/SC-compatible NFC reader.
  • Tags: Focus on MIFARE Classic 1K cards (lab/test cards).
  • OS: Linux (Ubuntu 22.04/24.04), Windows 10/11, macOS 12+.

On Linux, pcscd does the heavy lifting. On Windows and macOS, PC/SC support is already there; you just need the reader drivers and Python.

2.2 Tech Stack

The software stack is intentionally small and understandable:

  • Flask – HTTP API and minimal web UI.
  • PySCard – Python interface to PC/SC smart card stack.
  • PC/SC – System-level smart card middleware (pcsc-lite on Linux).
  • keys.json – JSON file with default and per-sector keys.
  • mfc_default_keys.disc – wordlist of common MIFARE Classic keys for labs.

In the repo you’ll find a compact structure:

pynfc/
├─ app.py                # Flask app: endpoints + NFC operations
├─ requirements.txt      # Python dependencies
├─ keys.json             # Default & per-sector keys for your cards
├─ mfc_default_keys.disc # Optional wordlist of common test keys
├─ README.md             # Project documentation
└─ ...                   # (future) templates/, static/, dumps/ etc.

At the moment, app.py concentrates both the API and core NFC logic. As the project grows, the plan is to extract a small nfc/ package and keep Flask as an interface layer only.

pynfc architecture – browser, Flask backend, PC/SC and NFC reader
Conceptual architecture – browser → Flask (pynfc) → PySCard → PC/SC → NFC reader → tag.

2.3 What pynfc Is Not

It’s worth stating explicitly what pynfc is not trying to be:

  • Not a replacement for Proxmark or hardcore RF tooling.
  • Not a “click-to-hack” solution for production systems.
  • Not a full-blown key cracking framework.

Instead, it’s closer to a transparent lab notebook for NFC: you can see the code, tweak flows, and connect it with other tools (logs, dashboards, teaching material, etc.).

3. Implementation – From Reader Plug-In to First Dump

Let’s walk through the main flow: installing the dependencies, wiring the reader, configuring keys and taking your first dump of a MIFARE Classic card.

3.1 Preparing the System (Linux Example)

On Linux, the flow usually starts with installing PC/SC components and verifying that the OS actually sees your reader and tag:

# 1) Install PC/SC stack and ACS CCID driver
sudo apt update
sudo apt install -y pcscd pcsc-tools libacsccid1

# 2) Start / enable the PC/SC service
sudo systemctl enable --now pcscd.socket
# or: sudo systemctl restart pcscd

# 3) Verify that the reader and tag are visible
pcsc_scan

When you see your ACR122U and a tag being detected via pcsc_scan, you know the OS and reader are good. From there, PySCard can talk to the device, and pynfc can sit above it.

3.2 Python Environment & Dependencies

Next comes the Python setup. From the repo root:

python3 -m venv .venv
source .venv/bin/activate      # Windows: .venv\Scripts\activate
pip install -U pip wheel
pip install -r requirements.txt

requirements.txt is intentionally minimal: Flask for the HTTP part and PySCard for NFC. You can always add logging, dotenv support, or other utilities as the project grows.

Under the hood, a typical PySCard snippet looks like this:

from smartcard.System import readers
from smartcard.util import toHexString

# List available PC/SC readers
r = readers()
if not r:
    raise RuntimeError("No PC/SC readers found")

reader = r[0]
connection = reader.createConnection()
connection.connect()

# Simple APDU example: get card ATR
atr = connection.getATR()
print("Reader:", reader)
print("ATR:", toHexString(atr))

pynfc wraps this core flow and adds higher-level operations for MIFARE Classic tags: selecting a sector, authenticating with the right key, then reading/writing blocks and saving dumps.

3.3 keys.json – Making Keys Explicit

One of the first things I wanted was a clear, versionable place to store default keys and per-sector overrides. That’s what keys.json is for:

{
  "default_key_type": "A",
  "default_key_hex": "FFFFFFFFFFFF",
  "candidate_keys": [
    "FFFFFFFFFFFF",
    "A0A1A2A3A4A5",
    "B0B1B2B3B4B5"
  ],
  "per_sector_keys": {
    "0": { "A": "FFFFFFFFFFFF", "B": "FFFFFFFFFFFF" },
    "1": { "A": "FFFFFFFFFFFF" },
    "9": { "B": "1848A8D1E4C5" }
  }
}

The app reads this file on startup and uses it when trying to authenticate sectors during a dump or a targeted read. That makes testing repeatable: every time you come back to the lab, the keys are exactly where you left them.

For anything sensitive, you can keep a private version of keys.json outside version control and mount it or inject it via environment variables in production.

3.4 Running pynfc & Taking a First Dump

Once the dependencies and keys are ready, running pynfc is straightforward:

export FLASK_APP=app.py
export FLASK_ENV=development  # or production
flask run  # or: python app.py

By default, Flask will bind to http://127.0.0.1:5000. From here you have two options:

  • Use a minimal web UI (when templates/static are present) to scan, read and dump.
  • Hit the REST endpoints directly with curl or a tool like Hoppscotch/Postman.

An example of what a REST-style flow could look like:

# List readers
curl http://localhost:5000/api/readers

# Poll for a card and read basic info
curl http://localhost:5000/api/tag/info

# Dump a full MIFARE Classic 1K card to a file under dumps/
curl -X POST http://localhost:5000/api/tag/dump \
     -H "Content-Type: application/json" \
     -d '{"format": "json"}'

Dumps are stored under a dumps/ directory (by timestamp or UID) so you can come back later, compare states, or restore them onto another card for lab scenarios.

4. Lab Scenarios & What I Learned About NFC

Writing pynfc wasn’t just a coding exercise – it forced me to properly understand how MIFARE Classic and PC/SC behave in practice. A few of the lab scenarios I’ve played with:

  • Baseline dumps: take a dump of a fresh lab card, perform an action (tap on a door, buy something), then dump again and diff the results.
  • Value blocks: decode how counters are stored, incremented and mirrored across blocks with the value block format.
  • Sector-level access: see exactly which sectors are open/closed with default keys versus custom keys.
  • Card cloning (for labs): duplicate the state of one lab card into another and verify that behaviour is preserved in a test environment.

A few key lessons:

  • Good logs matter: when dealing with keys and auth failures, clear logs are the difference between “this card is broken” and “you’re using the wrong key for sector 9”.
  • PC/SC quirks are real: some readers or OSes behave slightly differently around timeouts and reconnects – you have to handle that gracefully in code.
  • Legal & ethical boundaries are important: having your own cards and a lab is the only sane way to learn NFC without causing trouble.

pynfc sits nicely in that space: a teaching and exploration tool that is small enough to read end-to-end, while still being useful for day-to-day experiments.

5. Next Steps – Where pynfc Can Go

Right now, pynfc is a solid starting point. But there are plenty of directions it can grow:

  • Proper web UI: Tailwind-based UI with live logs, tag status, and “one-click” dump/restore buttons.
  • Per-lab profiles: separate configurations for different lab scenarios (e.g., transit card lab, office access lab, test vending machine lab).
  • Integration with other projects: export NFC data into other tools, or use a card as a physical token for side projects (time tracking, homelab access, etc.).
  • More tag types: basic support for other chip families (read-only info, UID scanning, etc.), still keeping the UI simple.
  • Better safety rails: “read-only” mode, confirmation prompts for dangerous operations, and per-endpoint access control.

The goal is to keep pynfc opinionated but hackable: you can clone the repo, understand it in an afternoon, and then bend it to your own NFC lab needs without feeling like you’re fighting a framework.

💬 Want to talk about NFC labs?

Reach out on LinkedIn or send me an email. I’m always happy to talk about NFC, lab setups, security training and small tools that demystify complex tech.