diff options
| author | cirakg <cirakg@cirakg.xyz> | 2026-10-18 23:44:18 +0200 |
|---|---|---|
| committer | cirakg <cirakg@cirakg.xyz> | 2026-10-18 23:44:18 +0200 |
| commit | 93aa7e3aa291f3328cf34a5a2f86172f41d0affd (patch) | |
| tree | ac44438e7340fde609ad6458e31e122612cf35d1 /test/otp | |
| parent | 5646ae7f40dc53c9ebed0b3b6e907cc85fcd5993 (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')
| -rw-r--r-- | test/otp/OtpTest.mc | 118 |
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; +} |
