From 096b5a4d17c85fa5f87db3e72364e9c3506d1e64 Mon Sep 17 00:00:00 2001 From: cirakg Date: Sun, 25 Oct 2026 12:46:42 +0100 Subject: 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 --- source/settings/ChangeQueue.mc | 76 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) (limited to 'source/settings/ChangeQueue.mc') 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 = [] as Array; + 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; + 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; + 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 { + var lines = _lockLines; + _lockLines = [] as Array; + 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 { -- cgit v1.2.3