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
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
|
#!/usr/bin/env python3
import base64
import os
import subprocess
import tempfile
import otp
SEED20 = base64.b32encode(b"12345678901234567890").decode()
SEED32 = base64.b32encode(b"12345678901234567890123456789012").decode()
SEED64 = base64.b32encode(
b"1234567890123456789012345678901234567890123456789012345678901234"
).decode()
# RFC 4226 appendix D, counters 0-9.
HOTP = "755224 287082 359152 969429 338314 254676 287922 162583 399871 520489".split()
# RFC 6238 appendix B, 8 digits: time -> (sha1, sha256, sha512).
TOTP = {
59: ("94287082", "46119246", "90693936"),
1111111109: ("07081804", "68084774", "25091201"),
1111111111: ("14050471", "67062674", "99943326"),
1234567890: ("89005924", "91819424", "93441116"),
2000000000: ("69279037", "90698825", "38618901"),
20000000000: ("65353130", "77737706", "47863826"),
}
def check(got, want, label):
if got != want:
raise SystemExit(f"{label}: got {got!r}, want {want!r}")
def fails(call, message):
try:
call()
except (otp.SecretsError, ValueError) as error:
if message not in str(error):
raise SystemExit(f"wrong error: {error!r}") from error
return
raise SystemExit(f"expected error containing {message!r}")
def test_vectors():
for counter, want in enumerate(HOTP):
check(otp.hotp(SEED20, counter), want, f"HOTP counter {counter}")
for t, (sha1, sha256, sha512) in TOTP.items():
check(otp.hotp(SEED20, t // 30, 8, "sha1"), sha1, f"SHA-1 time {t}")
check(otp.hotp(SEED32, t // 30, 8, "sha256"), sha256, f"SHA-256 time {t}")
check(otp.hotp(SEED64, t // 30, 8, "sha512"), sha512, f"SHA-512 time {t}")
def test_validation():
key = "JBSWY3DPEHPK3PXP"
entries = otp.parse_entries(f"demo {key}\n", "test")
check(entries, [("demo", key, 30, 6, "sha1")], "default entry")
invalid = (
("broken\n", "expected 2-5 fields"),
(f"demo {key} 0\n", "period must be between"),
(f"demo {key} 30 5\n", "digits must be between"),
(f"demo {key} 30 6 md5\n", "digest must be one of"),
(f"demo {key} 30 6 sha1 extra\n", "expected 2-5 fields"),
("demo invalid!\n", "secret is not valid base32"),
(f"bad\x1bname {key}\n", "invalid name"),
(f"demo {key}\ndemo {key}\n", "duplicate name"),
)
for text, message in invalid:
fails(lambda text=text: otp.parse_entries(text, "test"), message)
fails(lambda: otp.hotp("", 0), "secret must not be empty")
def test_file_permissions():
with tempfile.NamedTemporaryFile("w", delete=False) as f:
f.write("demo JBSWY3DPEHPK3PXP\n")
path = f.name
try:
os.chmod(path, 0o644)
env = os.environ.copy()
env.pop("OTP_SECRETS_CMD", None)
env["OTP_SECRETS"] = path
proc = subprocess.run(["./cli.py"], env=env, capture_output=True, text=True)
check(proc.returncode, 1, "unsafe file exit status")
want = f"cli: {path}: permissions must be 0600 or stricter\n"
check(proc.stderr, want, "unsafe file error")
finally:
os.unlink(path)
def test_commands():
env = os.environ.copy()
env["OTP_SECRETS_CMD"] = "exit 42"
proc = subprocess.run(["./cli.py"], env=env, capture_output=True, text=True)
check(proc.returncode, 42, "secret command exit status")
check(proc.stderr, "cli: secrets command failed\n", "secret command error")
proc = subprocess.run(["./tui.py"], env=env, capture_output=True, text=True)
check(proc.returncode, 42, "TUI secret command exit status")
check(proc.stderr, "tui: secrets command failed\n", "TUI secret command error")
proc = subprocess.run(["./otp.py"], input="\n", capture_output=True, text=True)
check(proc.returncode, 1, "blank raw key exit status")
check(proc.stderr, "otp: secret must not be empty\n", "blank raw key error")
proc = subprocess.run(
["./otp.py", "30", "5"], input="", capture_output=True, text=True
)
check(proc.returncode, 1, "raw digits exit status")
check(proc.stderr, "otp: digits must be between 6 and 10\n", "raw digits error")
def main():
test_vectors()
test_validation()
test_file_permissions()
test_commands()
print("ok")
if __name__ == "__main__":
main()
|