aboutsummaryrefslogtreecommitdiff
path: root/test/codes/CodeCacheTest.mc
blob: 2ad7f057da12b04ded42eca606d635cf4190f674 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
// 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<Codes.Matrix>;
    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;
}