aboutsummaryrefslogtreecommitdiff
path: root/test.py
blob: 56d0380459ff50010425a44963f88cd7b2676e1b (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
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()