aboutsummaryrefslogtreecommitdiff

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