summaryrefslogtreecommitdiff
path: root/source/ui/CodeCache.mc
diff options
context:
space:
mode:
Diffstat (limited to 'source/ui/CodeCache.mc')
-rw-r--r--source/ui/CodeCache.mc127
1 files changed, 127 insertions, 0 deletions
diff --git a/source/ui/CodeCache.mc b/source/ui/CodeCache.mc
new file mode 100644
index 0000000..d8c61f2
--- /dev/null
+++ b/source/ui/CodeCache.mc
@@ -0,0 +1,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];
+ }
+}