aboutsummaryrefslogtreecommitdiff
path: root/README
diff options
context:
space:
mode:
authorLena <lena@omega>2026-03-01 00:00:00 +0000
committerLena <lena@omega>2026-03-01 00:00:00 +0000
commit14c1e3039f80a2fc50194b4da13a4f9964b7a1b3 (patch)
tree3b28cb379bd19456cc166f48ec814c36fb0ecaa0 /README
downloadotp-14c1e3039f80a2fc50194b4da13a4f9964b7a1b3.tar.gz
Enter otpHEADmaster
Authenticator codes from the terminal, stdlib only, no phone and no third-party dependency. Secrets are a private plain-text file, or the output of a command so encrypted stores work unchanged.
Diffstat (limited to 'README')
-rw-r--r--README88
1 files changed, 88 insertions, 0 deletions
diff --git a/README b/README
new file mode 100644
index 0000000..66acfba
--- /dev/null
+++ b/README
@@ -0,0 +1,88 @@
+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