diff options
Diffstat (limited to 'source/ui/CodeCache.mc')
| -rw-r--r-- | source/ui/CodeCache.mc | 127 |
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]; + } +} |
