diff options
| author | cirakg <cirakg@cirakg.xyz> | 2026-10-25 12:46:42 +0100 |
|---|---|---|
| committer | cirakg <cirakg@cirakg.xyz> | 2026-10-25 12:46:42 +0100 |
| commit | 096b5a4d17c85fa5f87db3e72364e9c3506d1e64 (patch) | |
| tree | 9846544fe7d76d849df4a1ee2058f4847f674e7e /source/settings/ChangeQueue.mc | |
| parent | e0384274bbeab999279bdc35bdddbd8a3a451aeb (diff) | |
feat(pin): exactly 4 digits, entered on the 4th digit
The keypad has no OK key; the 4th digit submits, so a tap that also
arrives as START can no longer submit a partial PIN (the likely cause
of "Wrong PIN" before the code opened). PINs set by earlier versions
still unlock and are then replaced by a 4-digit one. Opening an entry
ignores a second request from the same tap. A Require PIN sent from
the phone while the watch is locked now applies at the next PIN
instead of waiting for Confirm all. The linter skips .claude.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'source/settings/ChangeQueue.mc')
| -rw-r--r-- | source/settings/ChangeQueue.mc | 76 |
1 files changed, 76 insertions, 0 deletions
diff --git a/source/settings/ChangeQueue.mc b/source/settings/ChangeQueue.mc index 12f88f7..c5919f6 100644 --- a/source/settings/ChangeQueue.mc +++ b/source/settings/ChangeQueue.mc @@ -8,6 +8,11 @@ // own with the device key (aad "OTPW-Q-1"), so a damaged item costs only // itself. A new secret in an item is no more exposed than it was in the // settings it came from, as with pending entries (README 7.7). +// +// "Require PIN" waits here only while the watch is locked (it needs the +// master key). It only adds protection, so applyLocks() applies it at the +// next correct (or newly set) PIN, without "Confirm all", the way pending +// entries are promoted. import Toybox.Application; import Toybox.Lang; @@ -18,6 +23,10 @@ module ChangeQueue { const MAX_ITEMS = 16; const AAD = "OTPW-Q-1"; + // What applyLocks() did at the last PIN, for the results of a "Confirm + // all" that follows it (ChangeQueueJob). Replaced at every PIN. + var _lockLines as Array<String> = [] as Array<String>; + function count(vault as Vault) as Number { var stored = vault.storage().getValue(KEY); return stored instanceof Array ? stored.size() : 0; @@ -75,6 +84,71 @@ module ChangeQueue { return out; } + // The number of waiting "Require PIN" actions (applied at the next PIN). + function countLocks(vault as Vault) as Number { + var actions = load(vault); + var n = 0; + for (var i = 0; i < actions.size(); i++) { + if (actions[i].action == ManageAction.LOCK) { + n++; + } + actions[i].wipe(); + } + return n; + } + + // After a correct or new PIN (session unlocked): apply the waiting + // "Require PIN" actions and remove them from the queue; the other items + // keep waiting, in order. Each is one re-seal (ManageRun, no code + // check), like promotePending. One that fails (the entry is gone or + // already locked) is dropped too. + function applyLocks(vault as Vault) as Void { + _lockLines = [] as Array<String>; + var stored = vault.storage().getValue(KEY); + if (!(stored instanceof Array) || !vault.session.isUnlocked()) { + return; + } + var keys = vault.keyManager.deviceKeys(); + var aad = Bytes.fromUtf8(AAD); + var kept = [] as Array<Application.Storage.ValueType>; + for (var i = 0; i < stored.size(); i++) { + var box = stored[i]; + var plain = box instanceof ByteArray ? SealedBox.open(keys, aad, box) : null; + var action = plain != null ? ManageAction.fromBytes(plain) : null; + Bytes.wipe(plain); + if (action != null && action.action == ManageAction.LOCK) { + var run = new ManageRun(vault, action, true); + Jobs.runToCompletion(run, 1); + _lockLines.add(run.failed ? "Not applied: " + action.describe() + ". " + run.message : run.message); + } else { + if (action != null) { + action.wipe(); + } + kept.add(box as Application.Storage.ValueType); // damaged items are dropped by the next clear() + } + } + if (kept.size() == stored.size()) { + return; + } + if (kept.size() == 0) { + clear(vault); + return; + } + try { + vault.storage().setValue(KEY, kept); + } catch (e instanceof Lang.Exception) { + // Can't happen when shrinking; if it did, the locks just apply + // again at the next PIN ("already requires the PIN"). + } + } + + // The lines of the last applyLocks(), once (then forgotten). + function takeLockLines() as Array<String> { + var lines = _lockLines; + _lockLines = [] as Array<String>; + return lines; + } + function clear(vault as Vault) as Void { vault.storage().deleteValue(KEY); } @@ -115,6 +189,8 @@ class ChangeQueueJob extends Jobs.Job { var stored = vault.storage().getValue(ChangeQueue.KEY); _loaded = stored instanceof Array ? stored.size() : 0; _actions = ChangeQueue.load(vault); + // "Require PIN" changes the PIN just applied (ChangeQueue.applyLocks). + lines.addAll(ChangeQueue.takeLockLines()); } function step(budget as Number) as Boolean { |
