// Author: cirakg & claude ai // Summary: Tests for the glance code: the sealed item round trip and its codes, tampering, the scan's choice of entry, the option, and the Manage favourite actions. import Toybox.Lang; import Toybox.Test; (:test) module GlanceFixtures { const NOW = 1800000026l; // The item the glance would read from the store, or null. function item(v as Vault) as GlanceCode.Item? { var store = v.storage(); var dk = store.getValue(GlanceCode.DEVICE_KEY); var sealed = store.getValue(GlanceCode.KEY); return GlanceCode.open(dk instanceof ByteArray ? dk : null, sealed instanceof ByteArray ? sealed : null); } function favorite(e as Entry) as Entry { e.favorite = true; return e; } } // The glance item opens to the entry's label, and its codes are the // entry's codes (TOTP with SHA-256, 8 digits, 60 s; Steam). (:test) function glanceItemRoundTripsWithTheSameCodes(logger as Test.Logger) as Boolean { var vault = SettingsFixtures.vault(); vault.glanceCode = true; var github = vault.add(StorageFixtures.totp("GitHub")); var steam = vault.add(StorageFixtures.steam("Steam")); GlanceStore.update(vault, github); var item = GlanceFixtures.item(vault); var secret = Bytes.fromUtf8("12345678901234567890"); var now = GlanceFixtures.NOW; if (item == null) { logger.error("no item"); return false; } var totpOk = item.label.equals("GitHub") && item.code(now).equals(Otp.totp(Hashes.SHA256, secret, now, 60, 8)) && item.code(now).length() == 8 && item.counter(now) == now / 60 && item.secondsLeft(now) == 60 - (now % 60).toNumber(); GlanceStore.update(vault, steam); var s = GlanceFixtures.item(vault); var steamOk = s != null && s.code(now).equals(Otp.steam(secret, now)) && s.periodSeconds() == 30; return totpOk && steamOk && GlanceCode.TYPE_STEAM == Entry.OTP_STEAM && GlanceCode.DEVICE_KEY.equals(KeyManager.DEVICE_KEY); } // A changed byte, another device key or another format gives nothing. (:test) function glanceItemRefusesTamperingAndOtherKeys(logger as Test.Logger) as Boolean { var vault = SettingsFixtures.vault(); vault.glanceCode = true; GlanceStore.update(vault, vault.add(StorageFixtures.totp("GitHub"))); var store = vault.storage(); var sealed = store.getValue(GlanceCode.KEY) as ByteArray; var dk = store.getValue(GlanceCode.DEVICE_KEY) as ByteArray; var tampered = Bytes.copy(sealed); tampered[20] = tampered[20] ^ 1; var otherKey = Bytes.copy(dk); otherKey[0] = otherKey[0] ^ 1; var bad = GlanceCode.encode("x", 0, 9, 6, 30, [1]b); // algorithm 9 doesn't exist return GlanceCode.open(dk, sealed) != null && GlanceCode.open(dk, tampered) == null && GlanceCode.open(otherKey, sealed) == null && GlanceCode.open(null, sealed) == null && GlanceCode.open(dk, null) == null && GlanceCode.decode(bad) == null && GlanceCode.decode(GlanceCode.encode("x", 0, 0, 6, 0, [1]b)) == null; } // The About scan picks the first favourite TOTP or Steam entry without the // PIN (not a locked, HOTP or barcode favourite), and removes the item when // the option goes off or the entry is no longer a favourite. (:test) function glanceScanChoosesTheFirstQualifyingFavourite(logger as Test.Logger) as Boolean { var vault = SettingsFixtures.vault(); StorageFixtures.setPin(vault, StorageFixtures.PIN); vault.glanceCode = true; var locked = GlanceFixtures.favorite(StorageFixtures.totp("Bank")); locked.locked = true; vault.add(locked); vault.add(GlanceFixtures.favorite(StorageFixtures.hotp("VPN", 3l))); vault.add(StorageFixtures.code("Gym")); // a favourite barcode vault.add(StorageFixtures.totp("Plain")); var mail = vault.add(GlanceFixtures.favorite(StorageFixtures.totp("Mail"))); var s = SettingsFixtures.store(); SettingsFixtures.run(vault, s); var chosen = GlanceFixtures.item(vault); var first = chosen != null && chosen.label.equals("Mail"); // "Remove favourite" from the phone: the item goes. SettingsFixtures.manage(s, mail, ManageAction.UNFAVORITE); var job = SettingsFixtures.run(vault, s); var gone = vault.storage().getValue(GlanceCode.KEY) == null && SettingsFixtures.text(s, ManageForm.STATUS).find("no longer a favourite") != null; // Marked again, then the option off: the item goes too. SettingsFixtures.manage(s, mail.toLower(), ManageAction.FAVORITE); SettingsFixtures.run(vault, s); var back = GlanceFixtures.item(vault) != null; vault.glanceCode = false; var needed = AboutScan.needed(vault); SettingsFixtures.run(vault, s); var off = vault.storage().getValue(GlanceCode.KEY) == null; if (!(first && gone && back && needed && off)) { logger.error("first " + first + " gone " + gone + " (" + job.lines + ") back " + back + " needed " + needed + " off " + off); return false; } return true; } // Favourite actions check the entry's current mark, and wait for the PIN // on a locked entry like the other changes. (:test) function manageFavouriteActions(logger as Test.Logger) as Boolean { var vault = ManageFixtures.vault(); var github = vault.ids()[0]; var s = ManageFixtures.send(vault, github, ManageAction.UNFAVORITE); SettingsFixtures.run(vault, s); var notYet = SettingsFixtures.text(s, ManageForm.STATUS).find("is not a favourite") != null; s = ManageFixtures.send(vault, github, ManageAction.FAVORITE); SettingsFixtures.run(vault, s); var marked = (vault.header(github) as Entry).favorite; // (The same form again would be a repeat; mark another entry directly.) var vpn = vault.ids()[1]; var e = vault.open(vpn); e.favorite = true; vault.update(e); e.wipeSecrets(); s = ManageFixtures.send(vault, vpn, ManageAction.FAVORITE); SettingsFixtures.run(vault, s); var already = SettingsFixtures.text(s, ManageForm.STATUS).find("already a favourite") != null; var bank = ManageFixtures.addLocked(vault, "Bank"); vault.lock(); s = ManageFixtures.send(vault, bank, ManageAction.FAVORITE); SettingsFixtures.run(vault, s); var waits = ChangeQueue.count(vault) == 1 && !(vault.anyHeader(bank) as Entry).favorite; var a = new ManageAction(); a.id = bank; a.action = ManageAction.UNFAVORITE; var parsed = ManageAction.fromBytes(a.toBytes()); var ok = notYet && marked && already && waits && parsed != null && parsed.action == ManageAction.UNFAVORITE && a.describe().equals("Remove favourite: " + bank) && ManageAction.NAMES.size() == ManageAction.UNFAVORITE + 1; if (!ok) { logger.error("notYet " + notYet + " marked " + marked + " already " + already + " waits " + waits + " queue " + ChangeQueue.count(vault) + " describe '" + a.describe() + "'"); } return ok; } (:test) function glanceGroupsDigits(logger as Test.Logger) as Boolean { return GlanceCode.grouped("123456").equals("123 456") && GlanceCode.grouped("1234567").equals("1234 567") && GlanceCode.grouped("12345678").equals("1234 5678") && GlanceCode.grouped("VK4T9").equals("VK4T9"); }