// Author: cirakg & claude ai // Summary: Tests for the encrypted barcode matrix cache: round trip, packed format, runs, keys of locked entries, invalidation and tampering. import Toybox.Lang; import Toybox.Test; (:test) function codeCacheRoundTripsAndInvalidates(logger as Test.Logger) as Boolean { var store = new MemoryBackend(); var vault = new Vault(store, new FakeTime()); var e = StorageFixtures.code("QR card"); e.type = Codes.QR; var id = vault.add(e); var qr = Qr.encode("cache me", 1, 0, -1) as Qr.MatrixCode; var matrix = new Codes.Matrix(qr.size, qr.size, 4, qr.modules); vault.saveCodeCache(id, matrix.toBytes()); var back = Codes.Matrix.fromBytes(vault.loadCodeCache(id) as ByteArray); var same = back != null && back.width == qr.size && back.quiet == 4 && Bytes.equal(back.modules, qr.modules); // The cache is sealed: tampering gives no cache rather than a wrong code. var stored = store.raw("x." + id) as ByteArray; stored[stored.size() - 1] = stored[stored.size() - 1] ^ 1; var tampered = vault.loadCodeCache(id) == null; vault.saveCodeCache(id, matrix.toBytes()); var opened = vault.open(id); vault.update(opened); opened.wipeSecrets(); var clearedByUpdate = store.getValue("x." + id) == null; vault.saveCodeCache(id, matrix.toBytes()); vault.remove(id); return same && tampered && clearedByUpdate && store.getValue("x." + id) == null; } // A locked entry's cache uses the master key: unreadable while locked. (:test) function codeCacheOfLockedEntriesNeedsTheSession(logger as Test.Logger) as Boolean { var vault = new Vault(new MemoryBackend(), new FakeTime()); StorageFixtures.setPin(vault, StorageFixtures.PIN); var e = StorageFixtures.code("Locked QR"); e.type = Codes.QR; e.locked = true; var id = vault.add(e); vault.saveCodeCache(id, [3, 1, 4, 1, 0xE0]b); vault.lock(); var code = 0; try { vault.loadCodeCache(id); } catch (err instanceof VaultError) { code = err.code; } StorageFixtures.unlock(vault, StorageFixtures.PIN); var back = vault.loadCodeCache(id); var pdf = new Codes.Matrix(3, 2, 2, [1, 0, 1, 0, 1, 1]b); pdf.rowHeight = 3; var round = Codes.Matrix.fromBytes(pdf.toBytes()); return code == VaultError.LOCKED && back != null && Bytes.equal(back, [3, 1, 4, 1, 0xE0]b) && Codes.Matrix.fromBytes([2, 0, 3, 0, 1, 4, 1, 0xE0]b) != null && Codes.Matrix.fromBytes([2, 0, 3, 0, 1, 4]b) == null && round != null && round.rowHeight == 3 && Bytes.equal(round.modules, [1, 0, 1, 0, 1, 1]b); } // Format 2 holds widths over 255 (wide PDF417); a 0.7 cache (one width // byte) is refused, so the code is encoded again. (:test) function codeCachePackedFormat(logger as Test.Logger) as Boolean { var bits = new [300 * 2]b; for (var i = 0; i < bits.size(); i++) { bits[i] = (i * 7 / 3) % 2; } var wide = new Codes.Matrix(300, 2, 2, bits); var bytes = wide.toBytes(); var back = Codes.Matrix.fromBytes(bytes); var oldFormat = Codes.Packed.parse([3, 1, 4, 1, 0xE0]b); var wrongSize = Codes.Packed.parse(bytes.slice(0, bytes.size() - 1)); return bytes[0] == 2 && back != null && back.width == 300 && back.height == 2 && Bytes.equal(back.modules, bits) && oldFormat == null && wrongSize == null && Codes.Packed.parse(null) == null; } // The runs drawn on screen are exactly the dark modules, row by row, // including runs across byte boundaries and whole dark or light bytes. (:test) function codeRunsCoverExactlyTheDarkModules(logger as Test.Logger) as Boolean { var codes = [Qr.encode("runs, runs, runs", 1, 0, -1) as Qr.MatrixCode]; var matrices = [new Codes.Matrix(codes[0].size, codes[0].size, 4, codes[0].modules)] as Array; var stripes = new [37 * 5]b; for (var i = 0; i < stripes.size(); i++) { stripes[i] = (i % 37) < 20 || (i % 37) > 33 ? 1 : 0; } matrices.add(new Codes.Matrix(37, 5, 1, stripes)); for (var c = 0; c < matrices.size(); c++) { var m = matrices[c]; var finder = new Codes.RunFinder(Codes.Packed.parse(m.toBytes()) as Codes.Packed); Jobs.runToCompletion(finder, 1); var drawn = new [m.width * m.height]b; var runs = finder.result.runs; for (var i = 0; i < runs.size(); i++) { var r = runs[i]; var y = r >> 20; var x = (r >> 10) & 0x3FF; for (var k = 0; k < (r & 0x3FF); k++) { if (drawn[y * m.width + x + k] == 1) { logger.error("matrix " + c + ": run " + i + " overlaps"); return false; } drawn[y * m.width + x + k] = 1; } } if (!Bytes.equal(drawn, m.modules)) { logger.error("matrix " + c + ": runs differ from the modules"); return false; } } return true; } // Steps a display job to the end and checks its percentage follows the // work: never down, never above 99, no big jumps, and a real spread (the // "Encoding" screen once showed 90 % at the first frame and sat there). function progressIsSteady(job as Codes.DisplayJob) as Boolean { var last = job.percent(); var biggest = last; var steps = 0; var steady = last < 35; while (!job.step(1)) { var now = job.percent(); steady = steady && now >= last && now <= 99; biggest = now - last > biggest ? now - last : biggest; last = now; steps++; } return steady && biggest <= 35 && steps >= 3 && last >= 60 && job.error == null; } (:test) function displayProgressFollowsTheWork(logger as Test.Logger) as Boolean { var member = "https://example.com/member/fake-0042?name=Alice%20Example&card=0000111122223333&valid=2027-12-31&level=gold"; var long = ""; for (var i = 0; i < 40; i++) { long += "abcdefghij"; } var qr = progressIsSteady(new Codes.DisplayJob(new Qr.QrJob(member, 1, 0, -1), null)); var qrBig = progressIsSteady(new Codes.DisplayJob(new Qr.QrJob(long, 0, 0, -1), null)); var qrForced = progressIsSteady(new Codes.DisplayJob(new Qr.QrJob(member, 1, 0, 3), null)); var dm = progressIsSteady(new Codes.DisplayJob(new DataMatrix.DataMatrixJob(long.substring(0, 200) as String), null)); var az = progressIsSteady(new Codes.DisplayJob(new Aztec.AztecJob(long.substring(0, 200) as String, 1), null)); var pdf = progressIsSteady(new Codes.DisplayJob(new Pdf417.Pdf417Job(long.substring(0, 200) as String, 1, 260, 260, true), null)); // A cached code only runs the finder: 0 at the start, then up to 99. var bits = new [60 * 60]b; for (var i = 0; i < bits.size(); i++) { bits[i] = (i * 7 / 3) % 2; } var packed = Codes.Packed.parse(new Codes.Matrix(60, 60, 4, bits).toBytes()) as Codes.Packed; var cached = new Codes.DisplayJob(null, packed); var start = cached.percent(); var cachedSteady = progressIsSteady(cached); logger.debug("progress qr=" + qr + " big=" + qrBig + " forced=" + qrForced + " dm=" + dm + " az=" + az + " pdf=" + pdf + " cached=" + cachedSteady); return qr && qrBig && qrForced && dm && az && pdf && start == 0 && cachedSteady; }