summaryrefslogtreecommitdiff
path: root/test/import/OtpAuthTest.mc
blob: 67f43f55a6a8225f18d5d69c9e2e260c69231d74 (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
// Author:  cirakg & claude ai
// Summary: Tests for otpauth:// parsing: splitting, labels, parameters, Steam, separators and every validation error.

import Toybox.Lang;
import Toybox.Test;

(:test)
module OtpAuthFixtures {

    // RFC 4226 test key "12345678901234567890" in Base32.
    const SECRET = "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ"; // lint: allow-literal (RFC test key)

    function parse(uri as String) as ImportItem {
        return OtpAuth.parse(uri, 1);
    }

    function entry(logger as Test.Logger, uri as String) as Entry? {
        var item = parse(uri);
        if (item.entry == null) {
            logger.error("no entry for " + uri + ": " + item.problem + " / " + item.skipped);
        }
        return item.entry;
    }
}

(:test)
function otpauthSplitsRunTogetherUris(logger as Test.Logger) as Boolean {
    var s = OtpAuthFixtures.SECRET;
    var uris = OtpAuth.splitUris("junk otpauth://totp/A?secret=" + s + " \n OTPAUTH://totp/B?secret=" + s
        + "otpauth://hotp/C?secret=" + s + "&counter=1\n");
    return uris.size() == 3 && (uris[0] as String).find("totp/A") != null && (uris[1] as String).find("totp/B") != null
        && (uris[2] as String).find("counter=1") != null && (uris[2] as String).find("\n") == null;
}

(:test)
function otpauthLabelsAndDefaults(logger as Test.Logger) as Boolean {
    var e = OtpAuthFixtures.entry(logger, "otpauth://totp/ACME%20Co:john%40example.com?secret=" + OtpAuthFixtures.SECRET);
    var bare = OtpAuthFixtures.entry(logger, "otpauth://totp/alice?secret=" + OtpAuthFixtures.SECRET.toLower());
    if (e == null || bare == null) {
        return false;
    }
    return e.name.equals("ACME Co") && e.account.equals("john@example.com") && e.algorithm == Hashes.SHA1
        && e.digits == 6 && e.period == 30 && e.type == Entry.OTP_TOTP && e.source == Entry.SOURCE_IMPORT
        && Bytes.equal(e.secret as ByteArray, Bytes.fromUtf8("12345678901234567890"))
        && bare.name.equals("") && bare.account.equals("alice");
}

(:test)
function otpauthParametersAndSeparators(logger as Test.Logger) as Boolean {
    var s = OtpAuthFixtures.SECRET;
    var e = OtpAuthFixtures.entry(logger,
        "otpauth://totp/Label:bob?SECRET=" + s + ";Algorithm=sha512;digits=8&period=60&image=https%3A%2F%2Fx&issuer=Big+Bank");
    var item = OtpAuthFixtures.parse("otpauth://totp/Old:bob?secret=" + s + "&issuer=New");
    if (e == null) {
        return false;
    }
    return e.name.equals("Big Bank") && e.algorithm == Hashes.SHA512 && e.digits == 8 && e.period == 60
        && item.entry != null && (item.entry as Entry).name.equals("New") && item.note != null
        && e.account.equals("bob");
}

(:test)
function otpauthHotpAndSteam(logger as Test.Logger) as Boolean {
    var s = OtpAuthFixtures.SECRET;
    var h = OtpAuthFixtures.entry(logger, "otpauth://hotp/VPN:dave?secret=" + s + "&counter=42");
    var noCounter = OtpAuthFixtures.parse("otpauth://hotp/VPN:dave?secret=" + s);
    var steam = OtpAuthFixtures.entry(logger, "otpauth://steam/Steam:erin?secret=" + s);
    var encoder = OtpAuthFixtures.entry(logger, "otpauth://totp/Steam:erin?secret=" + s + "&encoder=steam&digits=5");
    return h != null && h.type == Entry.OTP_HOTP && h.counter == 42l
        && noCounter.problem != null && (noCounter.problem as String).find("counter") != null
        && steam != null && steam.type == Entry.OTP_STEAM && encoder != null && encoder.type == Entry.OTP_STEAM;
}

(:test)
function otpauthReportsEveryProblem(logger as Test.Logger) as Boolean {
    var s = OtpAuthFixtures.SECRET;
    var cases = [
        ["otpauth://totp/A:b", "secret missing"],
        ["otpauth://totp/A:b?secret=" + s + "&digits=9", "9 digits"],
        ["otpauth://totp/A:b?secret=" + s + "&digits=x", "not a whole number"],
        ["otpauth://totp/A:b?secret=" + s + "&period=0", "period 0"],
        ["otpauth://totp/A:b?secret=" + s + "&algorithm=MD5", "algorithm"],
        ["otpauth://totp/A:b?secret=ABC1DEFG", "position 4"],
        ["otpauth://totp/A%2:b?secret=" + s, "% escape"],
        ["otpauth://totp/?secret=" + s, "issuer or an account"],
        ["otpauth://totp", "no '/'"]
    ];
    for (var i = 0; i < cases.size(); i++) {
        var c = cases[i] as Array<String>;
        var item = OtpAuthFixtures.parse(c[0]);
        var problem = item.problem;
        if (item.entry != null || problem == null || problem.find(c[1]) == null) {
            logger.error("case " + i + ": problem " + problem);
            return false;
        }
    }
    var motp = OtpAuthFixtures.parse("otpauth://motp/A:b?secret=" + s);
    return motp.skipped != null && motp.entry == null;
}