aboutsummaryrefslogtreecommitdiff
path: root/README
blob: 66acfba78b74c00c0fddbf91516b0e2d6c84df04 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
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