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