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];
}
}
|