summaryrefslogtreecommitdiff
path: root/source/app/AppState.mc
blob: d7db6dcb2954b9c964430b83c8dc6b3a24452ffa (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
// Author:  cirakg & claude ai
// Summary: The app's shared state: options, the vault, the code cache, start-up errors and the crypto self-test.
//
// Created in getInitialView (never in glance mode, which has too little
// memory for the vault). Views reach the vault and options through here.

import Toybox.Application;
import Toybox.Lang;
import Toybox.Time;
import Toybox.WatchUi;

module AppState {

    const SELF_TEST_KEY = "st";
    const SECONDS_PER_DAY = 86400;

    var options as Options = new Options();
    var vault as Vault? = null;
    var codes as CodeCache? = null;
    var processor as SettingsProcessor? = null;
    // A message that replaces the whole app (newer data, damaged metadata,
    // failed self-test), or null.
    var startError as String? = null;
    // Codes are shown only after the self-test passed (README 7.10).
    var cryptoReady as Boolean = false;
    var selfTestSummary as String = "Not run yet";

    // Load options and open the vault. Errors end up in startError.
    function start() as Void {
        options = new Options();
        options.load();
        if (TestMode.ON) {
            Fixtures.resetStore();
        }
        try {
            var v = new Vault(new Persist.AppStorageBackend(), new TimeSource());
            applyOptions(v);
            vault = v;
            codes = new CodeCache(v);
            processor = new SettingsProcessor();
            if (TestMode.ON) {
                Fixtures.load(v);
            }
        } catch (e instanceof VaultError) {
            startError = e.code == VaultError.NEWER_SCHEMA
                ? "Data from a newer version of OTP Wallet. Update the app."
                : "Stored data is damaged. Reinstall the app to start over.";
        }
    }

    // The vault; only call after start() succeeded.
    function theVault() as Vault {
        return vault as Vault;
    }

    function theCodes() as CodeCache {
        return codes as CodeCache;
    }

    // Run the known-answer self-test on the first start of each version and
    // then once a day. Returns false (and sets startError) when it failed.
    function runSelfTestIfDue() as Boolean {
        var day = (Clock.nowUnix(0) / SECONDS_PER_DAY).toNumber();
        var last = Application.Storage.getValue(SELF_TEST_KEY);
        var due = true;
        if (last instanceof Array && last.size() == 2 && last[0] instanceof String && last[1] instanceof Number) {
            due = !(last[0] as String).equals(AppInfo.VERSION) || (last[1] as Number) != day;
        }
        if (!due) {
            selfTestSummary = "Passed (today)";
            cryptoReady = true;
            return true;
        }
        var failed = SelfTest.run();
        if (failed != null) {
            selfTestSummary = "FAILED: " + failed;
            startError = "Crypto self-test failed on this watch (" + failed + "). Codes are not shown.";
            return false;
        }
        Application.Storage.setValue(SELF_TEST_KEY, [AppInfo.VERSION, day] as Array<Application.Storage.ValueType>);
        selfTestSummary = "Passed";
        cryptoReady = true;
        return true;
    }

    // The phone changed the settings: reload the options and apply forms.
    function settingsChanged() as Void {
        options.load();
        var v = vault;
        if (v != null) {
            applyOptions(v);
            processSettings();
        }
        // Theme, code size and groups are read at every draw.
        WatchUi.requestUpdate();
    }

    // The options the vault itself uses.
    function applyOptions(v as Vault) as Void {
        v.eraseAfterFailures = options.eraseAfterFailures;
        v.encryptNames = options.encryptNames;
        v.glanceCode = options.glanceCode;
        v.session.autoLockMs = TestMode.ON ? TestMode.AUTO_LOCK_MS : options.autoLockMs;
    }

    // Apply any settings form that asks for it (after start-up and on change).
    function processSettings() as Void {
        var p = processor;
        if (p != null && cryptoReady) {
            p.start();
        }
    }

    // Note user activity (restarts the auto-lock timeout).
    function activity() as Void {
        var v = vault;
        if (v != null) {
            v.session.touch();
        }
    }

    // Lock if the inactivity timeout passed. True when this call locked.
    function checkAutoLock() as Boolean {
        var v = vault;
        if (v != null && v.session.checkExpiry()) {
            theCodes().clear();
            return true;
        }
        return false;
    }

    // Lock now and forget every computed code.
    function lock() as Void {
        var v = vault;
        if (v != null) {
            v.lock();
            theCodes().clear();
        }
    }

    function now() as Long {
        return Clock.nowUnix(options.timeOffset);
    }
}