// Author: cirakg & claude ai // Summary: Tests for the Aegis reader: database versions 1-3, types, parameters, groups, icons, encryption and bad input. import Toybox.Lang; import Toybox.Test; // Database version 3: types, algorithms, digits, periods, groups listed // after the entries, favourites, icons and Unicode. (:test) function aegisReadsDatabaseVersion3(logger as Test.Logger) as Boolean { var icon = ""; for (var i = 0; i < 200; i++) { icon += "iVBORw0KGgoAAAANSUhEUgAAA"; } var entries = [ AegisFixtures.entry("totp", "GitHub", "alice", AegisFixtures.SECRET_1, "SHA1", 6, 30, "\"icon\": \"" + icon + "\", \"icon_mime\": \"image/png\", \"groups\": [\"g-work\", \"g-other\"], "), AegisFixtures.entry("totp", "Bänk €", "bob", AegisFixtures.SECRET_2, "SHA256", 8, 60, "\"favorite\": true, "), "{\"type\": \"hotp\", \"name\": \"carol\", \"issuer\": \"VPN\", \"info\": {\"secret\": \"" + AegisFixtures.SECRET_3 + "\", \"algo\": \"SHA512\", \"digits\": 7, \"period\": 30, \"counter\": 12}}", AegisFixtures.entry("steam", "Steam", "dave", AegisFixtures.SECRET_1, "SHA1", 5, 30, ""), AegisFixtures.entry("motp", "Old", "erin", AegisFixtures.SECRET_1, "MD5", 6, 10, ""), AegisFixtures.entry("yandex", "Ya", "frank", AegisFixtures.SECRET_1, "SHA256", 8, 30, ""), AegisFixtures.entry("totp", "Odd", "gina", AegisFixtures.SECRET_1, "SHA1", 6, 20, "\"groups\": [\"g-missing\"], "), AegisFixtures.entry("totp", "Slow", "hal", AegisFixtures.SECRET_1, "SHA1", 6, 50, "") ]; var groups = "[{\"uuid\": \"g-work\", \"name\": \"Work\"}, {\"uuid\": \"g-other\", \"name\": \"Other\"}]"; var reader = AegisFixtures.read(AegisFixtures.export(3, entries, groups)); if (reader.error != null || reader.items.size() != 8) { logger.error("error " + reader.error + ", items " + reader.items.size()); return false; } var it = reader.items; var github = it[0].entry; var bank = it[1].entry; var vpn = it[2].entry; var steam = it[3].entry; var odd = it[6].entry; var slow = it[7].entry; if (github == null || bank == null || vpn == null || steam == null || odd == null || slow == null) { for (var i = 0; i < it.size(); i++) { logger.error("item " + i + ": " + it[i].problem + " / " + it[i].skipped); } return false; } var checks = [ github.name.equals("GitHub") && github.account.equals("alice"), github.group.equals("Work"), bank.name.equals("Bänk €") && bank.algorithm == Hashes.SHA256 && bank.digits == 8 && bank.period == 60, bank.favorite, vpn.type == Entry.OTP_HOTP && vpn.counter == 12l && vpn.digits == 7 && vpn.algorithm == Hashes.SHA512, steam.type == Entry.OTP_STEAM, it[4].skipped != null && it[5].skipped != null, odd.group.equals("") && odd.period == 20 && slow.period == 50, Bytes.equal(github.secret as ByteArray, Bytes.fromUtf8("12345678901234567890")) ]; for (var i = 0; i < checks.size(); i++) { if (!(checks[i] as Boolean)) { logger.error("check " + i + " failed"); return false; } } return true; } // Database versions 1 and 2 name the group directly. (:test) function aegisReadsOlderDatabaseVersions(logger as Test.Logger) as Boolean { for (var v = 1; v <= 2; v++) { var reader = AegisFixtures.read(AegisFixtures.export(v, [AegisFixtures.entry("totp", "Mail", "ann", AegisFixtures.SECRET_1, "SHA1", 6, 30, "\"group\": \"Private\", ")], "")); var e = reader.items.size() == 1 ? reader.items[0].entry : null; if (e == null || !e.group.equals("Private")) { logger.error("database version " + v + " failed: " + reader.error); return false; } } return true; } (:test) function aegisRefusesEncryptedExports(logger as Test.Logger) as Boolean { var slots = "{\"version\": 1, \"header\": {\"slots\": [{\"type\": 1}], \"params\": {}}, \"db\": \"c2VjcmV0\"}"; var dbString = "{\"version\": 1, \"header\": {\"slots\": null}, \"db\": \"c2VjcmV0\"}"; var a = AegisFixtures.read(slots); var b = AegisFixtures.read(dbString); return a.error != null && (a.error as String).find("encrypted") != null && b.error != null && (b.error as String).find("encrypted") != null; } (:test) function aegisReportsBadEntriesAndDocuments(logger as Test.Logger) as Boolean { var entries = [ AegisFixtures.entry("totp", "Nine", "x", AegisFixtures.SECRET_1, "SHA1", 9, 30, ""), "{\"type\": \"totp\", \"name\": \"dup\", \"info\": {\"secret\": \"" + AegisFixtures.SECRET_1 + "\", \"secret\": \"" + AegisFixtures.SECRET_2 + "\", \"algo\": \"SHA1\", \"digits\": 6, \"period\": 30}}", AegisFixtures.entry("totp", "Bad", "y", "NOT-BASE32!", "SHA1", 6, 30, ""), "{\"type\": \"totp\", \"name\": \"z\", \"info\": {\"secret\": \"" + AegisFixtures.SECRET_1 + "\", \"digits\": 6.5}}" ]; var reader = AegisFixtures.read(AegisFixtures.export(3, entries, "")); var it = reader.items; if (reader.error != null || it.size() != 4) { logger.error("error " + reader.error); return false; } var problems = [it[0].problem, it[1].problem, it[2].problem, it[3].problem]; var needles = ["9 digits", "twice", "Base32", "whole number"]; for (var i = 0; i < 4; i++) { var p = problems[i]; if (p == null || (p as String).find(needles[i]) == null) { logger.error("entry " + i + ": " + p); return false; } } var notJson = AegisFixtures.read("{\"version\": 1, \"db\": [1,]}"); var notAegis = AegisFixtures.read("{\"hello\": \"world\"}"); var newer = AegisFixtures.read("{\"version\": 2, \"db\": {\"version\": 3, \"entries\": []}}"); var deep = "{\"version\": 1, \"x\": " + "[[[[[[[[[[[[[[[[[[1]]]]]]]]]]]]]]]]]]" + "}"; return notJson.error != null && notAegis.error != null && newer.error != null && (newer.error as String).find("version 2") != null && AegisFixtures.read(deep).error != null; } (:test) function aegisSaysWhenTheTextIsIncomplete(logger as Test.Logger) as Boolean { var full = AegisFixtures.export(3, [AegisFixtures.totp("A", "a", AegisFixtures.SECRET_1), AegisFixtures.totp("B", "b", AegisFixtures.SECRET_2)], ""); return AegisFixtures.status(full.substring(0, full.length() / 2) as String) == AegisReader.INCOMPLETE && AegisFixtures.status(full) == AegisReader.DONE; }