# otp-import Turn saved OTP QR-code screenshots into credentials on a VivoKey Apex OTP (ykneo-oath) applet, via a canonical CSV. ## Pipeline 1. **QR -> CSV** (`qr_to_sheet.py`): decode QR screenshots and append new credentials to `otp-keys.csv`, deduplicated on the `otpauth://` data. 2. **Edit labels (optional)**: adjust the `issuer` / `account` columns to taste. Never edit the `otpauth_uri` column - it is the immutable source of truth. 3. **CSV -> applet** (`apex_otp_import.py`): write to the Apex, validating for anything you may have broken (duplicate names, invalid characters, overlength). The CSV has four columns: `issuer`, `account` (editable labels), `otpauth_uri` (do not edit), and `source_file`. The secret and other crypto parameters live inside `otpauth_uri` and are always taken from there. ``` # Populate/refresh the CSV from a folder of QR screenshots python qr_to_sheet.py --images /path/to/qr-screenshots --csv otp-keys.csv python qr_to_sheet.py --images ... --csv ... --dry-run # preview, no write ``` Undecodable images (stylized/colored QRs, or text-only screenshots) are listed by filename so you can add them by hand. ## Importing Import TOTP credentials from the CSV (or a legacy `.xlsx`) into the applet. The importer reads `otp-keys.csv` (or a legacy `.xlsx`), takes the credential name from the `issuer`/`account` columns and the crypto from `otpauth_uri`, and writes each credential using the standard YKOATH `PUT` command. It authenticates first if the applet is password-protected, asks before overwriting credentials that already exist, and can optionally profile per-credential on-chip memory consumption via the `javacard-memory` applet. The protocol layer is verified against the applet sources (`com.vivokey.otp.YkneoOath` / `OathObj`) and `doc/Protocol.txt`. See [docs/design.md](docs/design.md) for the full design and protocol notes. ## Security This tool handles live TOTP secrets. **Keep your secrets out of this repository.** The spreadsheet, any CSV, and QR screenshots are excluded by `.gitignore`; keep the actual data in a private location (e.g. a synced personal folder) and point the importer at it with `--input`. ## Requirements - Python 3.9+ - `pyscard`, `cryptography`, `openpyxl` - A PC/SC reader with the Apex (or a JavaCard carrying the applet) presented ## Usage ``` # 1. Validate the data and preview the APDUs (no card needed) python apex_otp_import.py --input /path/to/otp-keys.csv --dry-run # 2. Read-only: list what is already on the applet python apex_otp_import.py --input /path/to/otp-keys.csv --list # 3. Import (asks before overwriting existing credentials) python apex_otp_import.py --input /path/to/otp-keys.csv # Non-interactive conflict handling python apex_otp_import.py --input ... --overwrite-all python apex_otp_import.py --input ... --skip-existing # Target a specific reader, supply a password, and profile memory python apex_otp_import.py --input ... --reader VivoKey --password '...' --profile-memory # One-shot pipeline: decode new QRs into the CSV (only new, deduped on # otpauth://), then validate and import. QR folder defaults to the CSV's folder. python apex_otp_import.py --input otp-keys.csv --import-qr --reader VivoKey python apex_otp_import.py --input otp-keys.csv --import-qr --qr-images /qr/folder --overwrite-all # Import, then set an access password (only if the applet has none yet) python apex_otp_import.py --input ... --set-password 'yourpassword' # Reset: wipe all credentials and any password (standalone; no input needed) python apex_otp_import.py --reset ``` Authenticating to a password-protected applet uses the standard YKOATH scheme (PBKDF2-HMAC-SHA1 of the password with the device id as salt, then mutual challenge-response). `--set-password` only sets a password on an applet that has none; it does not change or remove an existing one. `--reset` clears everything. The input is validated before any reader is touched. The run halts with a per-row report if it finds: duplicate credential names; invalid Base32; over-long names or keys; an unsupported algorithm; control characters or a `:`/`/` inside an issuer or account (they break the OATH name convention); or a digit count outside 6-8 / a non-positive period. Fix the source data and re-run. ## Tests Hardware-free unit tests cover the protocol encoding, password derivation, input validation, and response reassembly: ``` python test_oath.py ```