summaryrefslogtreecommitdiff
path: root/source/ui/CodeCache.mc
blob: d8c61f2f14ddaa0d471e02118a5bfdfc0141acab (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
// Author:  cirakg & claude ai
// Summary: Computes OTP codes for display and caches each until it expires, so secrets are decrypted once per period.
//
// README 7.8: to show a code the entry is decrypted, the code computed and the
// secret wiped at once; only the short code string is kept, with the unix
// time it stops being valid. The 1 Hz redraw then reuses the string.
//
// Watchdog: an SHA-512 code costs about 50 times an SHA-1 code (PERFORMANCE.md),
// so callers that compute many codes in one event (the list) spend a budget
// in cost units and continue in the next event.

import Toybox.Lang;

class CodeCache {

    // Cost units per code: SHA-512 runs in Monkey C, the others natively.
    static const COST_NATIVE = 1;
    static const COST_SHA512 = 4;
    // Units the list may spend per event.
    static const BUDGET_PER_EVENT = 4;
    // A code that can't be computed (damaged entry) is shown as this.
    static const UNAVAILABLE = "------";
    static const FOREVER = 0x7FFFFFFFFFFFFFFFl;

    private var _vault as Vault;
    private var _codes as Dictionary<String, String> = {} as Dictionary<String, String>;
    private var _validUntil as Dictionary<String, Long> = {} as Dictionary<String, Long>;
    // What the encrypted part revealed when the code was computed: the period
    // (for countdowns) and the cost (for the list's budget). Not secret.
    private var _periods as Dictionary<String, Number> = {} as Dictionary<String, Number>;
    private var _costs as Dictionary<String, Number> = {} as Dictionary<String, Number>;

    function initialize(vault as Vault) {
        _vault = vault;
    }

    // The cached code, or null when there is none or it has expired.
    function cached(key as String, now as Long) as String? {
        var until = _validUntil[key];
        if (until == null || now >= until) {
            return null;
        }
        return _codes[key];
    }

    // Cost units computing this entry's code takes. The algorithm is
    // encrypted, so until the code was computed once, assume the worst (Steam
    // is always SHA-1).
    function costOf(header as Entry) as Number {
        var known = _costs[header.id];
        if (known != null) {
            return known;
        }
        return header.type == Entry.OTP_STEAM ? COST_NATIVE : COST_SHA512;
    }

    // The entry's period in seconds, once its code was computed (else 30).
    function periodOf(id as String) as Number {
        var p = _periods[id];
        return p != null ? p : Otp.STEAM_PERIOD;
    }

    // The current code (offset 0) or the next one (offset 1) of an OTP entry,
    // computing and caching it when needed. Throws VaultError LOCKED for a
    // locked entry while the session is locked.
    function code(id as String, now as Long, offset as Number) as String {
        var key = offset == 0 ? id : id + "+";
        var hit = cached(key, now);
        if (hit != null) {
            return hit;
        }
        var text = UNAVAILABLE;
        var until = now + 1;
        try {
            var entry = _vault.open(id);
            var period = entry.type == Entry.OTP_STEAM ? Otp.STEAM_PERIOD : entry.period;
            var secret = entry.secret as ByteArray;
            var at = now + offset * period;
            _periods[id] = period;
            _costs[id] = entry.algorithm == Hashes.SHA512 ? COST_SHA512 : COST_NATIVE;
            if (entry.type == Entry.OTP_HOTP) {
                text = Otp.hotp(entry.algorithm as Hashes.Algorithm, secret, entry.counter + offset, entry.digits);
                until = FOREVER;
            } else {
                text = entry.type == Entry.OTP_STEAM ? Otp.steam(secret, at)
                    : Otp.totp(entry.algorithm as Hashes.Algorithm, secret, at, period, entry.digits);
                until = now + Otp.secondsRemaining(now, period);
            }
            entry.wipeSecrets();
        } catch (e instanceof VaultError) {
            if (e.code == VaultError.LOCKED) {
                throw e;
            }
            // Damaged: show the placeholder, try again in a second.
        }
        _codes[key] = text;
        _validUntil[key] = until;
        return text;
    }

    // Forget one entry's codes (after a HOTP "next" or an edit).
    function invalidate(id as String) as Void {
        _codes.remove(id);
        _validUntil.remove(id);
        _codes.remove(id + "+");
        _validUntil.remove(id + "+");
    }

    // Forget everything (on lock, so no locked entry's code stays in memory).
    function clear() as Void {
        _codes = {} as Dictionary<String, String>;
        _validUntil = {} as Dictionary<String, Long>;
    }

    // The code split for reading: 3+3, 3+4 or 4+4 digits; other lengths
    // (Steam's 5 characters) stay whole.
    static function split(text as String) as Array<String> {
        var n = text.length();
        if (n == 6 || n == 7) {
            return [text.substring(0, 3) as String, text.substring(3, n) as String];
        }
        if (n == 8) {
            return [text.substring(0, 4) as String, text.substring(4, 8) as String];
        }
        return [text];
    }
}