diff options
Diffstat (limited to 'test/codes')
| -rw-r--r-- | test/codes/CodeCacheTest.mc | 54 | ||||
| -rw-r--r-- | test/codes/QrTest.mc | 120 |
2 files changed, 174 insertions, 0 deletions
diff --git a/test/codes/CodeCacheTest.mc b/test/codes/CodeCacheTest.mc new file mode 100644 index 0000000..e09d902 --- /dev/null +++ b/test/codes/CodeCacheTest.mc @@ -0,0 +1,54 @@ +// Author: cirakg & claude ai +// Summary: Tests for the encrypted barcode matrix cache: round trip, 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, 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); + return code == VaultError.LOCKED && back != null && Bytes.equal(back, [3, 1, 4, 0xE0]b) + && Codes.Matrix.fromBytes([3, 1, 4, 0xE0]b) != null && Codes.Matrix.fromBytes([3, 1, 4]b) == null; +} diff --git a/test/codes/QrTest.mc b/test/codes/QrTest.mc new file mode 100644 index 0000000..50cb35a --- /dev/null +++ b/test/codes/QrTest.mc @@ -0,0 +1,120 @@ +// Author: cirakg & claude ai +// Summary: Tests for the QR encoder: segno reference matrices (forced and automatic version and mask), decoding dumps, capacity, timing. + +import Toybox.Lang; +import Toybox.System; +import Toybox.Test; + +(:test) +module QrFixtures { + + // Row-major modules packed 8 per byte, as hex (the CodeVectors format). + function packedHex(code as Qr.MatrixCode) as String { + var m = code.modules; + var out = new [(m.size() + 7) / 8]b; + for (var i = 0; i < m.size(); i++) { + if (m[i] == 1) { + out[i / 8] = out[i / 8] | (0x80 >> (i % 8)); + } + } + return Bytes.toHex(out); + } +} + +// README 12.4 check 1 for QR: identical to segno with forced version and +// mask, and the same automatic version and mask choice. +(:test) +function qrMatchesSegno(logger as Test.Logger) as Boolean { + var rows = CodeVectors.QR; + for (var i = 0; i < rows.size(); i++) { + var row = rows[i] as Array; + var payload = row[0] as String; + var result = Qr.encode(payload, row[1] as Number, row[2] as Number, row[3] as Number); + if (result instanceof Codes.CodeError) { + logger.error("row " + i + ": " + result.message); + return false; + } + var expectedVersion = row[4] as Number; + var expectedMask = row[5] as Number; + if (result.version != expectedVersion || result.mask != expectedMask) { + logger.error("row " + i + " '" + Text.clip(payload, 20) + "': version " + result.version + " mask " + result.mask + + ", segno " + expectedVersion + " / " + expectedMask); + return false; + } + var got = QrFixtures.packedHex(result); + var expected = row[6] as String; + if (!got.equals(expected)) { + logger.error("row " + i + " '" + Text.clip(payload, 20) + "' (version " + result.version + ") matrix differs"); + logger.debug("QRGOT " + got); + return false; + } + } + logger.debug("CODES qr reference rows=" + rows.size()); + return true; +} + +// README 12.4 check 2: mixed-mode (segmented), Unicode and long payloads, +// decoded by tools/check_codes.py with zxing-cpp. +(:test) +function qrDumpForDecoding(logger as Test.Logger) as Boolean { + var payloads = [ + "ABC12345678901234xyz", "Membership 0042-7731-55 · Zürich", "12345678901234567890HELLO WORLD hello world", + "WIFI:T:WPA;S:FakeNet;P:not-a-real-password;;", "😀 emoji and Ünïcödé", "a" + ]; + var seed = 777; + for (var i = 0; i < 24; i++) { + var text = ""; + var length = 1 + i * 13 % 90; + for (var k = 0; k < length; k++) { + seed = (seed * 1103515245 + 12345) & 0x7FFFFFFF; + var pool = (seed >> 16) % 3 == 0 ? "0123456789" : ((seed >> 16) % 3 == 1 ? "ABCXYZ $%*+-./:" : "abcxyz,!?é"); + var at = (seed >> 4) % pool.length(); + text += pool.substring(at, at + 1); + } + payloads.add(text); + } + for (var i = 0; i < payloads.size(); i++) { + var text = payloads[i] as String; + var result = Qr.encode(text, i % 4, 0, -1); + if (result instanceof Codes.CodeError) { + logger.error("'" + text + "': " + result.message); + return false; + } + logger.debug("QRDUMP " + result.size + " " + Bytes.toHex(Bytes.fromUtf8(text)) + " " + QrFixtures.packedHex(result)); + } + return true; +} + +(:test) +function qrReportsCapacity(logger as Test.Logger) as Boolean { + var big = ""; + for (var i = 0; i < 300; i++) { + big += "abcdefghij"; + } + var tooLong = Qr.encode(big, 3, 0, -1); + var forced = Qr.encode("hello world, too long for version 1", 3, 1, -1); + var empty = Qr.encode("", 1, 0, -1); + return tooLong instanceof Codes.CodeError && (tooLong.message.find("too long") != null) + && forced instanceof Codes.CodeError && empty instanceof Codes.CodeError; +} + +// Timing and step counts for PERFORMANCE.md (simulator). +(:test) +function qrBenchmark(logger as Test.Logger) as Boolean { + var text = ""; + for (var i = 0; i < 20; i++) { + text += "abcdefghij"; + } + var inputs = ["https://example.com/member/12345", text, text + text + text + text + text]; + for (var i = 0; i < inputs.size(); i++) { + var job = new Qr.QrJob(inputs[i] as String, 1, 0, -1); + var steps = 0; + var start = System.getTimer(); + while (!job.step(1)) { + steps++; + } + var code = job.result as Qr.MatrixCode; + logger.debug("BENCH qr_v" + code.version + " ms=" + (System.getTimer() - start) + " steps=" + steps); + } + return true; +} |
