Files
otp-import/README.md
Amal Graafstra 08c77e1124 Fix pipeline diagram and document backup habits and dedup behavior
Replace the misaligned box diagram with a clean vertical flow. Expand
the intro to explain bulk-loading one or many authenticators and how
duplicates are handled at each stage. Add a "Backing up your OTP keys"
section covering screenshotting QR codes to secure storage with a
filename convention, and saving one-time backup/recovery codes.
2026-07-08 16:08:12 -07:00

355 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# otp-import
Bulk-load your saved authenticator **QR-code screenshots** onto a **VivoKey
Apex** OTP applet (the ykneo-oath applet) — or onto several implants at once — by
way of a plain, human-editable **CSV**.
If you screenshot your two-factor QR codes as you enroll them (see
[Backing up your OTP keys](#backing-up-your-otp-keys) below), these two tools
turn that pile of screenshots into working TOTP credentials on a new or
replacement authenticator, and can load the identical set onto multiple devices.
- `qr_to_sheet.py` — decode QR screenshots into the CSV (no hardware needed).
- `apex_otp_import.py` — write the CSV's credentials onto the applet over PC/SC.
**Duplicates are handled at both stages, so you can re-run freely:**
- **QR → CSV** deduplicates on the `otpauth://` data: re-scanning a folder only
appends codes that aren't already in the CSV; ones already present are silently
skipped. Two screenshots of the *same account with different secrets* (e.g. a
re-enrollment) are both kept — they are genuinely different credentials.
- **CSV → applet** keys credentials by name (`issuer:account`): if a name is
already on the applet you are asked to overwrite or skip it (or pass
`--overwrite-all` / `--skip-existing`). Loading the same CSV onto a second
implant just repeats the write. And before writing, the importer halts if two
rows would collide on the same name — so a re-enrollment with a new secret
can't silently clobber the old one; you relabel one of them first.
## Backing up your OTP keys
This utility is only useful if you saved your OTP keys in the first place. When
you turn on TOTP two-factor authentication somewhere, the site shows a QR code
(and usually a "can't scan it? enter this code instead" secret). That QR encodes
an `otpauth://` URI containing the shared secret — capture it **then**, because
you generally cannot retrieve it later without disabling and re-enrolling 2FA.
A good habit, every time you enable 2FA:
1. **Screenshot the QR code** and save the image to secure storage. The tools
read the `otpauth://` data inside the image, not the filename, so the naming
scheme is purely for your own organization — but a consistent one keeps the
folder browsable and sortable. A practical convention is
`YYYY-MM-DD-<relying-party>-<account>`:
```
2024-05-01-github-alice@example.com.png
2025-11-02-aws-root@example.com.png
```
Date first so the folder sorts chronologically, then the service (relying
party), then which account it is.
2. **Save the one-time backup/recovery codes too.** Most sites offer a set of
single-use recovery codes next to the QR — copy them into a text file beside
the screenshot (e.g. `2024-05-01-github-alice@example.com-backup-codes.txt`).
If you ever lose every authenticator, those codes are how you get back in.
(This tool doesn't process them; they're your independent recovery path.)
3. **Keep it all in secure storage.** These files contain live secrets — treat
them like passwords. Good homes: a password-manager vault, an encrypted
volume, or an end-to-end-encrypted cloud folder. Never commit them to a
repository (this repo's `.gitignore` deliberately excludes `*.png`, `*.csv`,
and backup-code text files).
With that habit in place, provisioning a new, replacement, or spare Apex is just
pointing these tools at the folder.
---
## The pipeline
```
QR screenshots (*.png / *.jpg, each holding an otpauth:// URI)
|
| (1) qr_to_sheet.py
| decode each QR; append only NEW otpauth:// data to the CSV
v
otp-keys.csv columns: issuer | account | otpauth_uri | source_file
|
| (2) optional: edit the issuer / account labels (never otpauth_uri)
|
| (3) apex_otp_import.py
| validate the CSV, authenticate if needed, PUT each credential
v
Apex OTP applet (ykneo-oath) one TOTP credential per row
```
1. **QR → CSV** (`qr_to_sheet.py`): decode each screenshot and append any
**new** credential to the CSV. Deduplication is on the `otpauth://` data, so
re-running never adds a credential that is already present.
2. **Edit labels (optional)**: adjust the `issuer` and `account` columns to
taste. **Never edit the `otpauth_uri` column** — it is the immutable source
of truth (secret and parameters live inside it).
3. **CSV → applet** (`apex_otp_import.py`): validate the CSV and write each
credential onto the applet, authenticating first if the applet has a
password.
The two stages guard different things: stage 1 stops you re-importing the same
QR; stage 3 stops you writing something you broke while editing labels.
---
## The CSV
Four columns:
| Column | Edit it? | Meaning |
| ------ | -------- | ------- |
| `issuer` | yes | Issuer shown on the applet (the "Issuer:" part of the name). |
| `account` | yes | Account shown on the applet (the part after "Issuer:"). |
| `otpauth_uri` | **no** | The raw `otpauth://` string from the QR. Dedup key and the source of the secret/algorithm/digits/period. |
| `source_file` | reference | The image filename the row was decoded from. |
The credential **name** written to the applet is `issuer:account` from the
editable columns. The **secret** and other crypto parameters are always taken
from `otpauth_uri`, so editing a label can never corrupt the code that gets
generated. The secret is still visible — it lives inside `otpauth_uri`.
The CSV is disposable: it can be regenerated from the QR screenshots at any time
with `qr_to_sheet.py`.
---
## Requirements
- Python 3.9+
- `pyscard`, `cryptography`, `openpyxl` (for reading legacy `.xlsx`), and
`opencv-python` (for QR decoding).
- For writing to a card: a PC/SC reader with the Apex (over NFC) or a JavaCard
carrying the applet presented to it.
```
pip install pyscard cryptography openpyxl opencv-python
```
---
## Tool 1 — `qr_to_sheet.py` (QR → CSV, no card)
Decode a folder of QR screenshots and append new credentials to the CSV. This
tool never touches a reader, so run it any time — the Apex does not need to be
present.
```
python qr_to_sheet.py --images <folder> --csv <path/to/otp-keys.csv>
```
Options:
| Option | Default | Meaning |
| ------ | ------- | ------- |
| `--images DIR` | `.` | Folder of QR images (`*.png`, `*.jpg`, `*.jpeg`). |
| `--csv PATH` | `otp-keys.csv` | The CSV to create or append to. |
| `--dry-run` | off | Report what would be added; write nothing. |
What it does per run:
- Decodes each image (a multi-pass decode copes with scaling/thresholding).
- Parses the `otpauth://totp/...` URI; seeds `issuer`/`account` from it.
- Skips any credential whose `otpauth://` data is already in the CSV
(deduplicated, existing rows never touched), and any duplicate within the same
run.
- Appends the genuinely-new rows and prints a summary of added / skipped /
undecodable.
Example:
```
$ python qr_to_sheet.py --images ./screenshots --csv otp-keys.csv
Scanned 34 image(s): 32 otpauth QR(s) decoded.
3 new, 29 duplicate(s) skipped.
could not be decoded (add manually): coinbase-...png, plaid-...png
Appended 3 row(s) to otp-keys.csv.
```
**Undecodable images** — stylized/colored QR codes, or screenshots that only
show the secret as text — are listed by filename. Add those by hand: create a
row with `issuer`, `account`, `source_file`, and an `otpauth_uri` you build from
the printed secret, e.g.
`otpauth://totp/Coinbase:you@example.com?secret=THESECRET&issuer=Coinbase`.
`otpauth-migration://` payloads (Google Authenticator's batch-export QR) are
reported as unsupported, not decoded.
---
## Tool 2 — `apex_otp_import.py` (CSV → applet)
Validate the CSV and write its credentials onto the applet. Needs the Apex on a
reader (except `--dry-run`).
```
python apex_otp_import.py --input otp-keys.csv [options]
```
Options:
| Option | Meaning |
| ------ | ------- |
| `--input PATH` | CSV (or legacy `.xlsx`) to import. Default `otp-keys.csv`. |
| `--reader SUBSTR` | Pick a reader by name substring (e.g. `VivoKey`, `3700 F CL`). Omit to auto-detect the single card holding an OATH applet; you are prompted if more than one is present. |
| `--dry-run` | Validate and print the APDUs that would be sent; no card I/O. |
| `--list` | Read-only: list the credentials already on the applet. |
| `--overwrite-all` | Overwrite any credential that already exists (non-interactive). |
| `--skip-existing` | Skip any credential that already exists (non-interactive). |
| `--password PASS` | Applet password (otherwise prompted if the applet is locked). |
| `--profile-memory` | Record per-credential EEPROM consumption to a CSV log (needs the `javacard-memory` applet). |
| `--set-password PASS` | After a successful import, set an access password — only if the applet has none. |
| `--reset` | Standalone: wipe all credentials and any password, then exit. |
| `--import-qr` | Run the QR → CSV step first (see below), then import. |
| `--qr-images DIR` | QR folder for `--import-qr` (default: the folder of `--input`). |
| `--memlog PATH` | Path for the `--profile-memory` log (default `apex-otp-memlog.csv`). |
By default, when a credential name already exists on the applet you are asked
whether to overwrite or skip it (with all/none shortcuts). Use `--overwrite-all`
or `--skip-existing` for unattended runs.
Recommended sequence for a first import:
```
python apex_otp_import.py --input otp-keys.csv --dry-run # (1) preview, no card
python apex_otp_import.py --input otp-keys.csv --list # (2) see what's on the applet
python apex_otp_import.py --input otp-keys.csv # (3) import for real
python apex_otp_import.py --input otp-keys.csv --list # (4) confirm
```
### Passwords
Authenticating to a password-protected applet uses the standard YKOATH scheme:
PBKDF2-HMAC-SHA1 of the password with the applet's device id as salt, then a
mutual challenge-response. `--set-password` sets a password only on an applet
that has none; it does not change or remove an existing one.
Note: unlike a YubiKey, this applet requires authentication before `--reset`
when a password is set. If you set a password and forget it, the OATH applet
cannot be reset over the OATH protocol — it must be deleted/reinstalled with
GlobalPlatform (card-manager keys).
### Memory profiling
`--profile-memory` measures how much persistent memory each credential consumes
by reading the `javacard-memory` applet before and after each `PUT`. Each row of
the log records the timestamp, card UID, memory-applet batch id, credential
name (never the secret), and bytes consumed. It is slower (a memory read per
credential) and off by default.
---
## The three modes
| Command | Does | Card needed |
| ------- | ---- | ----------- |
| `qr_to_sheet.py --images D --csv C` | QR → CSV | no |
| `apex_otp_import.py --input C` | CSV → applet | yes |
| `apex_otp_import.py --input C --import-qr` | QR → CSV → applet (one shot) | yes |
`--import-qr` is the whole pipeline in one command: it decodes new QRs into the
CSV (deduped on `otpauth://`, existing entries silently skipped), then validates
and imports. The QR folder defaults to the CSV's folder; override with
`--qr-images`. If you only want to refresh the CSV, use `qr_to_sheet.py` — the
Apex does not need to be on a reader.
```
# whole pipeline, single command
python apex_otp_import.py --input otp-keys.csv --import-qr --reader VivoKey
# same, with an explicit QR folder and unattended overwrite
python apex_otp_import.py --input otp-keys.csv --import-qr --qr-images ./screenshots --overwrite-all
```
---
## Common workflows
**Add newly-enrolled services.** Save the new QR screenshots into your folder,
then:
```
python qr_to_sheet.py --images ./screenshots --csv otp-keys.csv # appends only the new ones
# (edit issuer/account labels if you like)
python apex_otp_import.py --input otp-keys.csv --reader VivoKey --overwrite-all
```
**Re-provision a fresh/wiped Apex from scratch.**
```
python apex_otp_import.py --input otp-keys.csv --reader VivoKey
```
**Password-protect after loading.**
```
python apex_otp_import.py --input otp-keys.csv --reader VivoKey --set-password 'mypass'
```
**Wipe an applet (know the password if it has one).**
```
python apex_otp_import.py --reset --reader VivoKey [--password 'mypass']
```
---
## Validation (stage 3)
The CSV is validated before any reader is touched; the run halts with a per-row
report if it finds any of:
- Duplicate credential name (`issuer:account`).
- A secret that is not valid Base32.
- A decoded key longer than 64 bytes, or a composed name longer than 64 bytes.
- An unsupported algorithm (the applet accepts SHA1/SHA256, not SHA512).
- Control characters, or a literal `:` or `/`, inside an issuer or account
(they collide with the OATH `issuer:account` separator and `period/` prefix).
- A digit count outside 68, or a non-positive period.
Fix the offending cells and re-run. The applet itself only enforces the length
limits; the character and digit/period rules are host-side guards for
OATH-convention and authenticator-app compatibility.
---
## Security
This tooling handles live TOTP secrets. **Keep your secrets out of this
repository.** `otp-keys.csv`, any `.xlsx`, the memory log, and QR screenshots
are excluded by `.gitignore`; keep the real data in a private location (e.g. a
synced personal folder) and point the tools at it with `--csv` / `--input`.
---
## Tests
Hardware-free unit tests cover the OATH protocol encoding, password derivation,
input validation, response reassembly, otpauth parsing, dedup, and CSV I/O:
```
python test_oath.py
python test_qr.py
```
---
## How it works
The OATH protocol layer is implemented directly over PC/SC (`pyscard`) with no
Yubico dependency, and is verified against the applet sources
(`com.vivokey.otp.YkneoOath` / `OathObj`) and `doc/Protocol.txt`. See
[docs/design.md](docs/design.md) for the importer/protocol design and
[docs/qr-to-csv-design.md](docs/qr-to-csv-design.md) for the QR-to-CSV design.
- `otpauth.py` — parse/canonicalize `otpauth://` URIs (shared by both tools).
- `qr_to_sheet.py` — QR decode, dedup, CSV append.
- `apex_otp_import.py` — CSV/xlsx load, OATH SELECT/VALIDATE/LIST/PUT, password
auth, reset, memory profiling, and the CLI.