summaryrefslogtreecommitdiff
path: root/source/storage/Session.mc
diff options
context:
space:
mode:
authorcirakg <cirakg@cirakg.xyz>2026-10-19 15:27:37 +0200
committercirakg <cirakg@cirakg.xyz>2026-10-19 15:27:37 +0200
commitdd5d0f68726fe56ee4d199867d414aea81905cc4 (patch)
tree1cb40b024ac13f454633198ad50cbc55f7283f39 /source/storage/Session.mc
parent94181d58cf66c5b7b263fd435a344f1cd8872cff (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.mc89
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;
+ }
+}