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;
}
|