Skip to content

feat: MIFARE stored-value awareness — identify, verify --deep, fidelity, value command - #2

Merged
edobry merged 5 commits into
mainfrom
feat/identify-command
Jul 3, 2026
Merged

feat: MIFARE stored-value awareness — identify, verify --deep, fidelity, value command#2
edobry merged 5 commits into
mainfrom
feat/identify-command

Conversation

@edobry

@edobry edobry commented Jul 3, 2026

Copy link
Copy Markdown
Owner

Motivation

Debugging a building laundry card exposed a class of gaps around MIFARE Classic stored-value cards. A UID-only clone (correct UID, factory-default keys, empty data) passed keyfabe verify — which only compared the UID — yet a stored-value reader rejects it as unformatted. Two physically-identical same-UID cards were indistinguishable by hand and by the tool. This PR makes keyfabe understand the difference between a UID and the data a stored-value card actually needs.

What's in it

  • identify — read the tag and match it against all saved identities at once; for MIFARE Classic, probe data fidelity to distinguish a full clone (custom keys → real data) from a UID-only clone (default keys → blank).
  • verify --deep — go beyond the UID: read the live card's value blocks with the saved dump's keys, report the current on-card balance, and fail on a UID-only clone that would otherwise pass.
  • Store fidelitytagFidelity() classifies identities as full / uid-only / n/a; list shows a Data column, show and write warn on UID-only identities.
  • value — read or set a MIFARE value block (a stored-value balance) via hf mf value, with saved-key resolution, confirmation, and read-back. Honest about its limits (MAC / counter / server reconciliation).
  • Docsdocs/stored-value-cards.md explains how balance-on-card systems work, the value-block format, the full-vs-UID-only distinction, and why hollow clones format-error.
  • Also brings the previously-unmerged inspect command along.

Verification

  • 347 tests pass (up from 312), lint clean, build clean — all re-run by the pre-commit hook.
  • identify verified live against the real laundry card: matched both saved identities and reported "custom keys — carries real data."
  • CLI registration for identify / verify --deep / value smoke-tested via --help.

🤖 Generated with Claude Code

edobry and others added 5 commits April 30, 2026 00:44
Decodes value blocks (laundry/vending balances, with USD-cents
interpretation), surfaces printable ASCII markers, and emits a compact
hex dump with zero-runs collapsed. Locates dumps by UID via tag.dumpFile,
~/hf-mf-<UID>-dump.bin, or CWD — so existing tags saved before the
dumpFile field will still work.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`verify` only compares one chosen identity by UID, so it green-lights a
UID-only clone (correct UID, empty data sectors, factory keys) that a
stored-value reader rejects as unformatted. `identify` reads the tag on the
antenna and cross-references every saved identity at once, and for MIFARE
Classic probes data fidelity — reading a data block with the default key to
tell a full clone (custom sector keys) from a UID-only clone (blank/default).

- add probeMifareDataFidelity() + parseReadBlock() with the default-key probe
- add DEFAULT_MIFARE_KEY constant
- register `identify` in the CLI and interactive menu
- tests for the parser, the probe, and all identify branches
- README: document the command and the full-vs-UID-only distinction

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`verify` matched on UID alone, so a UID-only clone (right UID, empty data)
passed. `--deep` reads the live card's value blocks with the keys from the
saved dump, reports the current on-card balances, and fails when nothing is
readable under those keys — the signature of a UID-only clone.

- readLiveValueBlocks() reads live value blocks using the saved dump's keys
- sectorKeyA() / decodeValueBlockBytes() helpers in mf-data
- --deep flag on the verify CLI command
- tests for the helpers, the op, and the deep-verify pass/fail paths

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A MIFARE Classic identity with no saved dump can only be written as a
UID-only clone — which `verify` passed but a stored-value reader rejects.
Nothing in the tool flagged that ambiguity, so two same-UID identities
looked identical.

- tagFidelity() classifies an identity as full / uid-only / n/a
- `show` reports the data fidelity and warns on UID-only identities
- `list` adds a Data column for MIFARE Classic tags
- `write` warns before writing a UID-only MIFARE identity
- tests for the classifier and each surfaced warning/column

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds `keyfabe value <name> --block <n> [--get|--set|--inc|--dec]` to read or
write a MIFARE Classic value block (a stored-value balance), wrapping pm3's
`hf mf value`. The sector key comes from the named identity's saved dump or an
explicit --key. Writes warn, confirm, and read back to verify.

- readValueBlock() / writeValueBlock() ops + HF_MF_VALUE command
- value command with key resolution, confirmation, and read-back
- docs/stored-value-cards.md: how balance-on-card systems work, value-block
  format, full-vs-UID-only clones, why hollow clones format-error, and the
  real limits of writing a balance (MAC / counter / server reconciliation)
- README + CLAUDE.md reference the new command and doc
- tests for the ops and every value command branch

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@edobry edobry changed the title feat: identify command + data-fidelity awareness for MIFARE clones feat: MIFARE stored-value awareness — identify, verify --deep, fidelity, value command Jul 3, 2026
@edobry
edobry merged commit 8de6643 into main Jul 3, 2026
1 check passed
@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown

🎉 This PR is included in version 1.1.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant