Trezor as a TOTP vault: 2FA codes that never touch disk in plaintext
Authenticator apps (Google Authenticator, Authy, etc.) store the TOTP seed on the phone, usually in an app-sandboxed but still plaintext-adjacent form. If the phone is compromised, cloned, or the backup gets exposed, every 2FA code it can generate is exposed with it.
Trezor firmware exposes a primitive called CipherKeyValue —
symmetric encrypt/decrypt where the AES key is derived on-device from a
BIP32 node plus an arbitrary label string, and the key itself never leaves
the device. It's the same primitive the (now discontinued) Trezor Password
Manager used to encrypt its vault. It's still fully supported in firmware,
just under-documented for anything other than that original use case.
Wiring it up: encrypt the base32-decoded secret via
trezorlib.misc.encrypt_keyvalue at enrollment time (one device
touch), store only ciphertext + IV in a small JSON vault, and decrypt via
decrypt_keyvalue (another device touch) whenever a code is
needed. No session caching by design — every code generation re-touches the
device, so a compromised host can prompt for confirmation but never
exfiltrate the seed.
Usage
./run.sh add github --secret JBSWY3DPEHPK3PXP # confirm + PIN on device
./run.sh code github # decrypt via device, print current code
./run.sh list
./run.sh remove github
input() for the position-cipher PIN matrix, which needs a real
TTY to read from.
Verified against
| Component | Version |
|---|---|
| Trezor | Trezor One, firmware 1.12.1 |
trezorlib (trezor package) | 0.20.1 |
| pyotp | 2.10.0 |
Code / skill
Repo: cyb3ralbert/trezor-totp
Claude Code skill: trezor-totp-vault
Related: git-crypt + Trezor, using the same device for a different purpose (a repo encryption key instead of a 2FA seed) · Recovering a Trezor GPG identity from a seed phrase.