summaryrefslogtreecommitdiff
path: root/test/ui/GlanceCodeTest.mc
blob: 25c7dd744f36a731fa95d00a58bafba56942b11b (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
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
// 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");
}