Trezor as a TOTP vault: 2FA codes that never touch disk in plaintext

cyb3ralbert · 2026-07-19 · ← other posts

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.

TOTP secret → CipherKeyValue (on-device AES) → ciphertext on disk

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
Gotcha: PIN entry (if the device has one set) needs a real interactive terminal, same as the git-crypt+Trezor setup — it uses input() for the position-cipher PIN matrix, which needs a real TTY to read from.

Verified against

ComponentVersion
TrezorTrezor One, firmware 1.12.1
trezorlib (trezor package)0.20.1
pyotp2.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.