otp === TOTP/HOTP codes from the command line. `otp.py` is the stdlib crypto core; `cli.py` and `tui.py` read named secrets from a file and behave like an authenticator. Secrets file ------------ Location is `$OTP_SECRETS`, default `~/.config/otp_secrets`. Keep it private: chmod 600 ~/.config/otp_secrets The file must be regular, owned by the current user, and have no group or other permissions. Unsafe files are rejected. One entry per line, whitespace-delimited fields: name secret [period] [digits] [digest] `name` and `secret` (base32, single token) are required. Names may contain ASCII letters, digits, `.`, `_`, `@`, `+` and `-`. `period` must be 1-3600 seconds (default 30), `digits` must be 6-10 (default 6), and `digest` must be sha1, sha256 or sha512 (default sha1). Blank lines and lines starting with `#` are ignored. Duplicate names and extra fields are rejected. See `otp_secrets.example`. Encrypted secrets ----------------- Optionally set `OTP_SECRETS_CMD` to any command that writes secrets-file text to stdout; it is run with `sh -c` instead of reading `$OTP_SECRETS`, so any secret store works: OTP_SECRETS_CMD='age -d ~/.config/otp_secrets.age' ./tui.py OTP_SECRETS_CMD='gpg -qd ~/.config/otp_secrets.gpg' ./cli.py OTP_SECRETS_CMD='pass show otp' ./cli.py github Passphrase prompts and errors go to the terminal; a non-zero exit aborts with the command's exit code. To encrypt an existing file with age: age -p -o ~/.config/otp_secrets.age ~/.config/otp_secrets Usage ----- List a code for every entry: ./cli.py Print just one code (for scripting): ./cli.py github Live full-screen view with countdown bars; `q` quits, `r` reloads: ./tui.py Raw filter, no secrets file, reads base32 keys on stdin; optional positional arguments are period, digits, digest: echo JBSWY3DPEHPK3PXP | ./otp.py echo JBSWY3DPEHPK3PXP | ./otp.py 60 8 sha256 Debug ----- `test.py` asserts the RFC 4226 and RFC 6238 test vectors against the core, and checks entry parsing, file permissions and the command path: ./test.py A named code must equal the raw filter for the same secret in the same window: ./cli.py github echo JBSWY3DPEHPK3PXP | ./otp.py Point at a private throwaway file to test without touching your real secrets: umask 077 TMP=$(mktemp) printf 'demo JBSWY3DPEHPK3PXP\n' > "$TMP" OTP_SECRETS="$TMP" ./cli.py demo rm "$TMP" Exercise the command path without any encryption: OTP_SECRETS_CMD='printf "demo JBSWY3DPEHPK3PXP\n"' ./cli.py demo