Files
otp-import/README.md
Amal Graafstra 07f65835d0 Add QR-to-CSV utility and switch canonical format to CSV
Add qr_to_sheet.py: decode saved OTP QR screenshots and append new
credentials to the canonical CSV, deduplicated on the otpauth:// data.
Add otpauth.py (parse_otpauth / canonical_key / build_otpauth), shared
by both tools. The canonical format becomes a minimal CSV (issuer,
account, otpauth_uri, source_file); load_credentials reads CSV or xlsx
and derives crypto from the immutable otpauth_uri while taking
issuer/account from their editable label columns.
2026-07-08 14:49:59 -07:00

4.0 KiB

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 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

# 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