diff options
| author | cirakg <cirakg@cirakg.xyz> | 2026-10-19 15:27:37 +0200 |
|---|---|---|
| committer | cirakg <cirakg@cirakg.xyz> | 2026-10-19 15:27:37 +0200 |
| commit | dd5d0f68726fe56ee4d199867d414aea81905cc4 (patch) | |
| tree | 1cb40b024ac13f454633198ad50cbc55f7283f39 /source/storage/Session.mc | |
| parent | 94181d58cf66c5b7b263fd435a344f1cd8872cff (diff) | |
feat(storage): add the encrypted vault, PIN keys, lockout, session and migrations
Records are one ByteArray each: a readable TLV header plus an
encrypt-then-MAC box whose aad binds the entry ID and the header. The
device key seals unlocked entries and pending items; a random master key,
wrapped under a PBKDF2 PIN key, seals locked ones. Wrong PINs are counted
before the check, with delays on the trusted clock and an uptime fallback.
Bytes.toUtf8 validates UTF-8 first, because the native conversion crashes
on invalid input.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'source/storage/Session.mc')
| -rw-r--r-- | source/storage/Session.mc | 89 |
1 files changed, 89 insertions, 0 deletions
diff --git a/source/storage/Session.mc b/source/storage/Session.mc new file mode 100644 index 0000000..891b713 --- /dev/null +++ b/source/storage/Session.mc @@ -0,0 +1,89 @@ +// Author: cirakg & claude ai +// Summary: The unlocked session: holds the master key in RAM and locks after inactivity or on request. +// +// README 7.5: unlocking puts the locked master key (and its derived keys) in +// RAM; locking zeroes and drops them. The UI calls touch() on every user +// action and checkExpiry() from its 1 Hz timer; the app calls lock() in +// onStop. An auto-lock time of 0 means "Immediately": the UI locks as soon as +// you leave a locked entry, and checkExpiry() locks on its next call. + +import Toybox.Lang; + +class Session { + + static const DEFAULT_AUTO_LOCK_MS = 60000; + + // Inactivity time before locking, in ms (0 = immediately). + var autoLockMs as Number = DEFAULT_AUTO_LOCK_MS; + + private var _time as TimeSource; + private var _masterKey as ByteArray? = null; + private var _keys as SealedBox.Keys? = null; + private var _lastActivity as Number = 0; + + function initialize(time as TimeSource) { + _time = time; + } + + // Take ownership of the master key (the caller must not wipe or keep it). + function unlock(masterKey as ByteArray) as Void { + lock(); + _masterKey = masterKey; + _keys = new SealedBox.Keys(masterKey); + touch(); + } + + function isUnlocked() as Boolean { + return _keys != null; + } + + // The master key's derived keys. Throws VaultError LOCKED when locked. + function keys() as SealedBox.Keys { + var keys = _keys; + if (keys == null) { + throw new VaultError(VaultError.LOCKED, "locked"); + } + return keys; + } + + // The master key itself (only to re-wrap it under a new PIN). Throws + // VaultError LOCKED when locked. + function masterKey() as ByteArray { + var key = _masterKey; + if (key == null) { + throw new VaultError(VaultError.LOCKED, "locked"); + } + return key; + } + + // Note user activity; restarts the inactivity timeout. + function touch() as Void { + _lastActivity = _time.uptimeMs(); + } + + // Lock if the inactivity timeout has passed. Returns true when this call + // locked the session, so the UI can close open locked entries. + function checkExpiry() as Boolean { + if (!isUnlocked()) { + return false; + } + var now = _time.uptimeMs(); + // A timer that went backwards (watch restart, wrap) counts as expired. + if (now < _lastActivity || now - _lastActivity >= autoLockMs) { + lock(); + return true; + } + return false; + } + + // Zero and drop every key. Safe to call when already locked. + function lock() as Void { + var keys = _keys; + if (keys != null) { + keys.wipe(); + } + Bytes.wipe(_masterKey); + _keys = null; + _masterKey = null; + } +} |
