// Author: cirakg & claude ai // Summary: Tests for the vault: add/open/update/move/remove, persistence, locked and pending entries, quotas, store full. import Toybox.Lang; import Toybox.Test; (:test) function vaultRoundTripsAndPersists(logger as Test.Logger) as Boolean { var store = new MemoryBackend(); var time = new FakeTime(); var vault = new Vault(store, time); var originals = [StorageFixtures.totp("A"), StorageFixtures.hotp("B", 3l), StorageFixtures.steam("C"), StorageFixtures.code("D")]; for (var i = 0; i < originals.size(); i++) { vault.add(originals[i] as Entry); } // A new Vault on the same store is what the next app start sees. var reopened = new Vault(store, time); var ids = reopened.ids(); if (ids.size() != 4) { logger.error("expected 4 ids, got " + ids.size()); return false; } for (var i = 0; i < ids.size(); i++) { var expected = originals[i] as Entry; if (!ids[i].equals(expected.id) || !Entry.isValidId(ids[i])) { logger.error("order or id wrong at " + i); return false; } var opened = reopened.open(ids[i]); var same = StorageFixtures.same(logger, opened, expected); opened.wipeSecrets(); var header = reopened.header(ids[i]); if (!same || header == null || !header.name.equals(expected.name) || header.secret != null) { return false; } } return true; } (:test) function vaultUpdatesMovesAndRemoves(logger as Test.Logger) as Boolean { var vault = new Vault(new MemoryBackend(), new FakeTime()); var a = vault.add(StorageFixtures.totp("A")); var b = vault.add(StorageFixtures.totp("B")); var c = vault.add(StorageFixtures.hotp("C", 0l)); vault.move(c, 0); vault.move(a, 99); var order = vault.ids(); if (!order[0].equals(c) || !order[1].equals(b) || !order[2].equals(a)) { logger.error("move gave " + order.toString()); return false; } var e = vault.open(b); e.name = "B renamed"; vault.update(e); e.wipeSecrets(); vault.setCounter(c, 42l); vault.remove(a); var header = vault.header(b); var hotp = vault.open(c); var counter = hotp.counter; hotp.wipeSecrets(); var notFound = 0; try { vault.open(a); } catch (err instanceof VaultError) { notFound = err.code; } return header != null && header.name.equals("B renamed") && counter == 42l && vault.ids().size() == 2 && notFound == VaultError.NOT_FOUND; } (:test) function vaultRejectsInvalidEntries(logger as Test.Logger) as Boolean { var vault = new Vault(new MemoryBackend(), new FakeTime()); var e = StorageFixtures.totp("bad"); e.digits = 4; var code = 0; try { vault.add(e); } catch (err instanceof VaultError) { code = err.code; } return code == VaultError.INVALID && vault.ids().size() == 0; } // A damaged record shows as null from header(), DAMAGED from open(), and // can still be deleted. (:test) function vaultReportsDamagedRecords(logger as Test.Logger) as Boolean { var store = new MemoryBackend(); var vault = new Vault(store, new FakeTime()); var id = vault.add(StorageFixtures.totp("A")); var record = store.raw("e." + id) as ByteArray; record[record.size() - 1] = record[record.size() - 1] ^ 0x80; var code = 0; try { vault.open(id); } catch (err instanceof VaultError) { code = err.code; } // The header itself is intact, so the list can still show the name. var headerStillReadable = vault.header(id) != null; store.setValue("e." + id, "not a record"); var headerGone = vault.header(id) == null; vault.remove(id); return code == VaultError.DAMAGED && headerStillReadable && headerGone && vault.ids().size() == 0; } // Swapping two records' storage keys must not make either open. (:test) function vaultDetectsSwappedRecords(logger as Test.Logger) as Boolean { var store = new MemoryBackend(); var vault = new Vault(store, new FakeTime()); var a = vault.add(StorageFixtures.totp("A")); var b = vault.add(StorageFixtures.totp("B")); var recordA = store.getValue("e." + a); store.setValue("e." + a, store.getValue("e." + b)); store.setValue("e." + b, recordA); var damaged = 0; var ids = [a, b]; for (var i = 0; i < ids.size(); i++) { try { vault.open(ids[i]); } catch (err instanceof VaultError) { if (err.code == VaultError.DAMAGED) { damaged++; } } } return damaged == 2; } (:test) function vaultLockedEntriesNeedTheSession(logger as Test.Logger) as Boolean { var store = new MemoryBackend(); var vault = new Vault(store, new FakeTime()); StorageFixtures.setPin(vault, StorageFixtures.PIN); var e = StorageFixtures.totp("Locked"); e.locked = true; var id = vault.add(e); var record = store.raw("e." + id) as ByteArray; if (Record.keyRef(record) != Record.KEY_MASTER) { logger.error("locked entry not sealed with the master key"); return false; } vault.lock(); var codes = [] as Array; try { vault.open(id); } catch (err instanceof VaultError) { codes.add(err.code); } try { vault.remove(id); } catch (err instanceof VaultError) { codes.add(err.code); } try { vault.setCounter(id, 1l); } catch (err instanceof VaultError) { codes.add(err.code); } var header = vault.header(id); if (codes.size() != 3 || codes[0] != VaultError.LOCKED || codes[1] != VaultError.LOCKED || header == null || !header.locked) { logger.error("locked entry accessible while locked: " + codes.toString()); return false; } if (!StorageFixtures.unlock(vault, StorageFixtures.PIN)) { logger.error("unlock failed"); return false; } var opened = vault.open(id); var ok = StorageFixtures.same(logger, opened, e); opened.wipeSecrets(); return ok; } (:test) function vaultQueuesLockedEntriesWhileLocked(logger as Test.Logger) as Boolean { var store = new MemoryBackend(); var vault = new Vault(store, new FakeTime()); // Before any PIN exists. var first = StorageFixtures.totp("Pending 1"); first.locked = true; var id1 = vault.add(first); StorageFixtures.setPin(vault, StorageFixtures.PIN); vault.lock(); // While locked. var second = StorageFixtures.code("Pending 2"); second.locked = true; var id2 = vault.add(second); if (vault.ids().size() != 0 || vault.pendingIds().size() != 2 || store.getValue("p." + id1) == null) { logger.error("pending entries not queued"); return false; } if (!StorageFixtures.unlock(vault, StorageFixtures.PIN) || vault.promotePending() != 2) { logger.error("promotion failed"); return false; } var ids = vault.ids(); var record = store.raw("e." + id2) as ByteArray; var opened = vault.open(id1); var ok = StorageFixtures.same(logger, opened, first); opened.wipeSecrets(); return ok && ids.size() == 2 && vault.pendingIds().size() == 0 && store.getValue("p." + id1) == null && store.getValue("p." + id2) == null && Record.keyRef(record) == Record.KEY_MASTER; } (:test) function vaultEnforcesTheEntryCap(logger as Test.Logger) as Boolean { var vault = new Vault(new MemoryBackend(), new FakeTime()); for (var i = 0; i < Vault.MAX_ENTRIES; i++) { vault.add(StorageFixtures.totp("E" + i)); } var code = 0; try { vault.add(StorageFixtures.totp("one too many")); } catch (err instanceof VaultError) { code = err.code; } return code == VaultError.LIMIT && vault.ids().size() == Vault.MAX_ENTRIES; } // A full store gives VaultError FULL and leaves no half-added entry behind, // whether the record write or the metadata write is the one that fails. (:test) function vaultHandlesAFullStore(logger as Test.Logger) as Boolean { var sawFull = false; var sawSuccess = false; for (var extra = 0; extra < 600; extra += 9) { var store = new MemoryBackend(); var vault = new Vault(store, new FakeTime()); vault.add(StorageFixtures.totp("fits")); store.capacity = store.usedBytes() + extra; var expected = 2; try { vault.add(StorageFixtures.totp("more")); sawSuccess = true; } catch (err instanceof VaultError) { if (err.code != VaultError.FULL) { logger.error("unexpected error " + err.code); return false; } sawFull = true; expected = 1; } var reopened = new Vault(store, new FakeTime()); var keys = store.keys(); var records = 0; for (var i = 0; i < keys.size(); i++) { if (keys[i].find("e.") == 0) { records++; } } if (reopened.ids().size() != expected || records != expected) { logger.error("inconsistent store at extra=" + extra + ": ids " + reopened.ids().size() + ", records " + records); return false; } } return sawFull && sawSuccess; }