docs: README with Arch/Omarchy setup and troubleshooting
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
f37026793b
commit
6d4dbc31cd
1 changed files with 209 additions and 0 deletions
209
README.md
Normal file
209
README.md
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
# boltcard-writer
|
||||
|
||||
Write, verify and wipe **Bolt Cards** (NXP NTAG 424 DNA) from a Linux PC with a
|
||||
USB NFC reader, straight from the LNbits **Bolt Cards** extension's auth link.
|
||||
No phone app needed.
|
||||
|
||||
Tested target: **ACS ACR1252U** reader, Arch Linux / Omarchy. Any PC/SC (CCID)
|
||||
reader that supports ISO 14443-4 should work.
|
||||
|
||||
What it does is exactly what the official
|
||||
[Bolt Card programmer app](https://github.com/boltcard/bolt-card-programmer)
|
||||
does with the same link: write the `lnurlw://` URL, enable SDM mirroring
|
||||
(UID + tap counter encrypted with k1, CMAC with k2), lock the file to k0, set
|
||||
keys 1–4 then 0. After writing it reads the card back and checks the mirrored
|
||||
`p`/`c` values with k1/k2 — the same check LNbits does on every tap.
|
||||
|
||||
## Setup (Arch Linux / Omarchy)
|
||||
|
||||
```sh
|
||||
sudo pacman -S --needed pcsclite ccid uv git
|
||||
sudo systemctl enable --now pcscd.socket
|
||||
|
||||
git clone https://git.atitlan.io/aiolabs/boltcard-writer.git
|
||||
cd boltcard-writer
|
||||
uv run boltcard-writer readers
|
||||
```
|
||||
|
||||
`uv run` creates a virtualenv and installs the two Python dependencies on
|
||||
first use (needs internet once). With the ACR1252U plugged in you should see
|
||||
two readers, the starred one is used:
|
||||
|
||||
```
|
||||
* ACS ACR1252 1S CL Reader PICC 00 00
|
||||
ACS ACR1252 1S CL Reader SAM 00 01
|
||||
```
|
||||
|
||||
Smoke test with any NTAG 424 DNA card (blank or not):
|
||||
|
||||
```sh
|
||||
uv run boltcard-writer read
|
||||
```
|
||||
|
||||
```
|
||||
Tap the card on: ACS ACR1252 1S CL Reader PICC 00 00
|
||||
Card:
|
||||
UID: 04A1B2C3D4E5F6
|
||||
chip: NTAG 424 DNA (HW 30.00, SW 01.00, storage 0x11)
|
||||
ATR: 3B8180018080
|
||||
key versions: k0=0 k1=0 k2=0 k3=0 k4=0
|
||||
NDEF file: SDM off, comm plain, read=free write=free change=key0
|
||||
NDEF URL: (empty)
|
||||
state: blank / factory
|
||||
```
|
||||
|
||||
## Writing a card
|
||||
|
||||
1. In LNbits open the **Bolt Cards** extension and create the card as usual
|
||||
(name, limits, **card UID**: get it with `read` above, keys auto-generated).
|
||||
2. Click the QR icon on the card row, then **Keys / Auth link**: this copies
|
||||
`https://<your lnbits>/boltcards/api/v1/auth?a=<code>` to the clipboard.
|
||||
Back up the keys from that dialog somewhere safe.
|
||||
3. Put a blank card on the reader and run:
|
||||
|
||||
```sh
|
||||
uv run boltcard-writer write 'https://<your lnbits>/boltcards/api/v1/auth?a=<code>'
|
||||
```
|
||||
|
||||
```
|
||||
Card: my card
|
||||
LNURLw: lnurlw://lnbits.example.com/boltcards/api/v1/scan/9f2c…
|
||||
Keys backed up to /home/you/.local/share/boltcard-writer/backups/20260920-181200-write-my_card.json
|
||||
Tap the card on: ACS ACR1252 1S CL Reader PICC 00 00
|
||||
UID: 04A1B2C3D4E5F6
|
||||
…
|
||||
state: blank / factory
|
||||
Write this card now? Keep it still on the reader until done [y/N]: y
|
||||
|
||||
… writing NDEF URL (119 chars, p@88 c@124)
|
||||
… authenticating with key 0
|
||||
… enabling SDM mirroring on the NDEF file
|
||||
… changing key 1
|
||||
… changing key 2
|
||||
… changing key 3
|
||||
… changing key 4
|
||||
… changing key 0
|
||||
… reading back and verifying p/c
|
||||
… verified: uid 04A1B2C3D4E5F6 counter 1
|
||||
|
||||
Done. Bolt Card 04A1B2C3D4E5F6 written and verified (counter 1).
|
||||
```
|
||||
|
||||
**Do not move the card until "Done"** — the whole write takes about a second.
|
||||
|
||||
The auth link is **single-use**: LNbits rotates the code every time it is
|
||||
fetched. The keys are saved to a backup file *before* the card is touched, so
|
||||
if something fails mid-way you can retry with the backup instead of a new link:
|
||||
|
||||
```sh
|
||||
uv run boltcard-writer write ~/.local/share/boltcard-writer/backups/<file>.json
|
||||
```
|
||||
|
||||
The `boltcard://program?url=…` deep link behind the QR code is accepted too,
|
||||
and `-` reads a keys JSON from stdin.
|
||||
|
||||
## Checking a written card
|
||||
|
||||
```sh
|
||||
uv run boltcard-writer read --keys ~/.local/share/boltcard-writer/backups/<file>.json
|
||||
```
|
||||
|
||||
reads the live `p`/`c` the card mirrors and verifies them with k1/k2, printing
|
||||
the tap counter. Or just tap it on any Lightning POS / wallet that speaks
|
||||
LNURL-withdraw.
|
||||
|
||||
## Wiping a card
|
||||
|
||||
Resets keys to zero, disables SDM and clears the NDEF so the card is blank
|
||||
again (delete it in LNbits afterwards).
|
||||
|
||||
1. In LNbits, QR icon on the card row → **Wipe** → copy the JSON (`WIPE DATA`).
|
||||
2. Save it to a file, or pipe it:
|
||||
|
||||
```sh
|
||||
uv run boltcard-writer wipe wipe.json
|
||||
# or
|
||||
pbpaste | uv run boltcard-writer wipe - # wl-paste on Wayland, xclip -o on X11
|
||||
```
|
||||
|
||||
A backup file from `write` or the card's auth link work as well. The JSON's
|
||||
`uid` is checked against the card on the reader so you cannot wipe the wrong
|
||||
card by mistake.
|
||||
|
||||
## Options
|
||||
|
||||
```
|
||||
boltcard-writer [-r READER] [--debug] [--timeout SECS] COMMAND
|
||||
|
||||
readers list readers
|
||||
read [--keys FILE] inspect the card, optionally verify p/c
|
||||
write SOURCE [-y] [--force] [--no-verify] [--backup-dir DIR]
|
||||
wipe SOURCE [-y] [--backup-dir DIR]
|
||||
```
|
||||
|
||||
`--debug` prints every APDU exchanged with the card to stderr. **Include that
|
||||
output when reporting a problem.** Key material is never in the trace (key
|
||||
changes travel encrypted).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Fix |
|
||||
|---|---|
|
||||
| `libpcsclite.so.1 not found` | `sudo pacman -S pcsclite` (NixOS: use `nix-shell`) |
|
||||
| `SCARD_E_NO_SERVICE` | `sudo systemctl enable --now pcscd.socket` |
|
||||
| `SCARD_E_NO_READERS_AVAILABLE` | reader unplugged, or the `ccid` package is missing. `lsusb` should list `072f:223b Advanced Card Systems`; `pcsc_scan` (package `pcsc-tools`) should show it |
|
||||
| `SCARD_E_SHARING_VIOLATION` | close `pcsc_scan` or the other program using the reader |
|
||||
| Waits forever at "Tap the card" | card not detected: put it flat on the reader's centre; a different card type (NTAG 213, credit card) is rejected on first command instead |
|
||||
| `AUTHENTICATION_ERROR` / `RndA' mismatch` on write | the card is not factory-fresh; its k0 is not zero. Wipe it with the keys it was written with |
|
||||
| `AUTHENTICATION_ERROR` on wipe | wrong keys for this card |
|
||||
| `AUTHENTICATION_DELAY` | too many bad auths; leave the card on the reader ~1 s and retry |
|
||||
| `card already looks provisioned` | it has SDM on or non-zero key versions; wipe first (`--force` only if you know k0 is zero) |
|
||||
| `this is not an NTAG 424 DNA` | wrong tag; Bolt Cards need NTAG 424 DNA |
|
||||
|
||||
If the user cannot access the reader (permission errors from pcscd), Arch's
|
||||
pcsclite uses polkit: the session must be a local active login, or add a rule
|
||||
for `org.debian.pcsc-lite.access_pcsc` / `access_card`.
|
||||
|
||||
## NixOS
|
||||
|
||||
```nix
|
||||
services.pcscd.enable = true; # ships the ccid driver
|
||||
```
|
||||
|
||||
then in the repo `nix-shell` (provides `uv` + `libpcsclite`) and use
|
||||
`uv run boltcard-writer …` as above.
|
||||
|
||||
## Development
|
||||
|
||||
```sh
|
||||
uv run pytest # AN12196 vectors + a software NTAG 424 simulator
|
||||
uv run ruff check
|
||||
```
|
||||
|
||||
No hardware is needed for the tests. `tests/test_vectors.py` replays the
|
||||
worked examples from NXP application note AN12196; `tests/simcard.py` is a
|
||||
minimal software card that verifies MACs, decrypts, changes keys and renders
|
||||
SDM mirroring, so the write/wipe flows run end to end.
|
||||
|
||||
## How it maps to the card
|
||||
|
||||
| Step | Command | Comm mode |
|
||||
|---|---|---|
|
||||
| NDEF URL with `p=`/`c=` placeholders | ISOSelectFile + ISOUpdateBinary | plain (factory file is free-write) |
|
||||
| Session | AuthenticateEV2First key 0 | AES, LRP not used |
|
||||
| SDM on, offsets, write locked to k0 | ChangeFileSettings file 02 | full (`40 00E0 C1 FF12 …`) |
|
||||
| k1..k4 then k0, key version 1 | ChangeKey | full |
|
||||
| Verify | ISOReadBinary, decrypt `p` with k1, CMAC with k2 | plain |
|
||||
|
||||
LNbits stores `k3 = k1` and `k4 = k2`, as the phone app does. Random-UID mode
|
||||
(`uid_privacy`) is not used by LNbits and not implemented.
|
||||
|
||||
## Limitations
|
||||
|
||||
- Linux only (ctypes bindings to `libpcsclite`; no compiled extension).
|
||||
- Assumes factory keys (all zero) when writing; a card written with other
|
||||
keys must be wiped with them first.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
Loading…
Add table
Add a link
Reference in a new issue