aboutsummaryrefslogtreecommitdiff
path: root/test/otp/OtpTest.mc
diff options
context:
space:
mode:
authorcirakg <cirakg@cirakg.xyz>2026-10-18 23:44:18 +0200
committercirakg <cirakg@cirakg.xyz>2026-10-18 23:44:18 +0200
commit93aa7e3aa291f3328cf34a5a2f86172f41d0affd (patch)
treeac44438e7340fde609ad6458e31e122612cf35d1 /test/otp/OtpTest.mc
parent5646ae7f40dc53c9ebed0b3b6e907cc85fcd5993 (diff)
test: add cross-checked crypto/OTP vectors, unit tests and stricter screenshots
- gen_vectors.py: RFC/FIPS/NIST vectors recomputed with Python's stdlib (openssl for AES) before use, plus seeded cases around SHA padding boundaries, 2038 and 2100; generates test/data/Vectors.mc. - 31 Monkey C tests (hashes, HMAC, PBKDF2, HKDF, AES, sealed records incl. every-byte tamper detection, Base32 errors, 100 OTP and 12 Steam vectors, clock, jobs) and a benchmark; pass on all four test devices. - Screenshot tolerance 0.01 %: one changed character no longer passes. - Golden images updated for version 0.2.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'test/otp/OtpTest.mc')
-rw-r--r--test/otp/OtpTest.mc118
1 files changed, 118 insertions, 0 deletions
diff --git a/test/otp/OtpTest.mc b/test/otp/OtpTest.mc
new file mode 100644
index 0000000..598dd89
--- /dev/null
+++ b/test/otp/OtpTest.mc
@@ -0,0 +1,118 @@
+// Author: cirakg & claude ai
+// Summary: Base32, HOTP, TOTP and Steam tests (RFC 4648, RFC 4226, RFC 6238, seeded vectors incl. 2038 and 2100).
+
+import Toybox.Lang;
+import Toybox.Test;
+
+(:test)
+function base32MatchesVectors(logger as Test.Logger) as Boolean {
+ var ok = true;
+ for (var i = 0; i < TestVectors.BASE32.size(); i++) {
+ var r = TestSupport.row(TestVectors.BASE32, i);
+ var data = TestSupport.bytes(r, 0);
+ var text = TestSupport.str(r, 1);
+ ok = TestSupport.sameString(logger, "encode " + i, Base32.encode(data), text) && ok;
+ var decoded = Base32.decode(text);
+ if (!(decoded instanceof Lang.ByteArray) || !Bytes.equal(decoded, data)) {
+ logger.error("decode " + i + " failed");
+ ok = false;
+ }
+ }
+ return ok;
+}
+
+// Lower case, spaces, tabs, hyphens and missing padding are all accepted.
+(:test)
+function base32IsLenient(logger as Test.Logger) as Boolean {
+ var expected = Bytes.fromUtf8("foobar");
+ var inputs = ["mzxw6ytboi", "MZXW 6YTB OI", "mzxw-6ytb-oi======", "MZXW6YTB\tOI"];
+ for (var i = 0; i < inputs.size(); i++) {
+ var decoded = Base32.decode(inputs[i] as String);
+ if (!(decoded instanceof Lang.ByteArray) || !Bytes.equal(decoded, expected)) {
+ logger.error("lenient input " + i + " rejected");
+ return false;
+ }
+ }
+ return true;
+}
+
+// Errors name the right position (1-based) or say it's a length problem (0).
+(:test)
+function base32ReportsErrors(logger as Test.Logger) as Boolean {
+ var cases = [["MZX1W6", 4], ["MZ X0", 5], ["MZ=X", 4], ["M", 0], ["MZX", 0], ["MY=", 0], ["MZÄW", 3]];
+ for (var i = 0; i < cases.size(); i++) {
+ var c = cases[i] as Array;
+ var input = c[0] as String;
+ var result = Base32.decode(input);
+ if (!(result instanceof Base32.DecodeError) || result.position != (c[1] as Number)) {
+ logger.error("case " + i + " (" + input + ") not reported as expected");
+ return false;
+ }
+ logger.debug(input + " -> " + result.message);
+ }
+ return true;
+}
+
+// RFC 4226, RFC 6238 and seeded HOTP/TOTP vectors (period 0 means HOTP).
+(:test)
+function otpMatchesVectors(logger as Test.Logger) as Boolean {
+ var ok = true;
+ for (var i = 0; i < TestVectors.OTP.size(); i++) {
+ var r = TestSupport.row(TestVectors.OTP, i);
+ var alg = TestSupport.alg(r, 0);
+ var secret = TestSupport.bytes(r, 1);
+ var time = TestSupport.long(r, 2);
+ var period = TestSupport.num(r, 3);
+ var digits = TestSupport.num(r, 4);
+ var code = period == 0 ? Otp.hotp(alg, secret, time, digits) : Otp.totp(alg, secret, time, period, digits);
+ ok = TestSupport.sameString(logger, "OTP vector " + i, code, TestSupport.str(r, 5)) && ok;
+ }
+ logger.debug(TestVectors.OTP.size() + " OTP vectors");
+ return ok;
+}
+
+(:test)
+function steamMatchesVectors(logger as Test.Logger) as Boolean {
+ var ok = true;
+ for (var i = 0; i < TestVectors.STEAM.size(); i++) {
+ var r = TestSupport.row(TestVectors.STEAM, i);
+ var code = Otp.steam(TestSupport.bytes(r, 0), TestSupport.long(r, 1));
+ ok = TestSupport.sameString(logger, "Steam vector " + i, code, TestSupport.str(r, 2)) && ok;
+ }
+ return ok;
+}
+
+// Period boundaries: the counter changes exactly at k * period.
+(:test)
+function totpPeriodBoundaries(logger as Test.Logger) as Boolean {
+ return Otp.totpCounter(29l, 30) == 0l && Otp.totpCounter(30l, 30) == 1l && Otp.totpCounter(31l, 30) == 1l
+ && Otp.secondsRemaining(29l, 30) == 1 && Otp.secondsRemaining(30l, 30) == 30
+ && Otp.totpCounter(-5l, 30) == 0l && Otp.secondsRemaining(-5l, 30) == 30
+ && Otp.totpCounter(4294967295l, 30) == 143165576l;
+}
+
+(:test)
+function otpRejectsBadParameters(logger as Test.Logger) as Boolean {
+ var rejected = 0;
+ try {
+ Otp.hotp(Hashes.SHA1, [1]b, 0l, 5);
+ } catch (e instanceof Lang.InvalidValueException) {
+ rejected++;
+ }
+ try {
+ Otp.hotp(Hashes.SHA1, [1]b, 0l, 9);
+ } catch (e instanceof Lang.InvalidValueException) {
+ rejected++;
+ }
+ try {
+ Otp.totp(Hashes.SHA1, [1]b, 0l, 0, 6);
+ } catch (e instanceof Lang.InvalidValueException) {
+ rejected++;
+ }
+ try {
+ Otp.totp(Hashes.SHA1, [1]b, 0l, 301, 6);
+ } catch (e instanceof Lang.InvalidValueException) {
+ rejected++;
+ }
+ return rejected == 4;
+}