summaryrefslogtreecommitdiff
path: root/source/settings/ChangeQueue.mc
diff options
context:
space:
mode:
authorcirakg <cirakg@cirakg.xyz>2026-10-20 18:36:54 +0200
committercirakg <cirakg@cirakg.xyz>2026-10-20 18:36:54 +0200
commit151bfda5d4c567ff909a61dc9c90bf8eece1db37 (patch)
treef8487b91cf6a9ce59dba02e95e67250fc84d8e80 /source/settings/ChangeQueue.mc
parent25910f3a7a7fb28c4a58367b6e56dd2468b60933 (diff)
feat(settings): add the Add OTP, Add code and Manage entry forms with a PIN queue for locked entries
The full phone settings form (README 10.2) and its processing (10.3) as one resumable job: Import, Add OTP, Add code, Manage, then the About fields. Forms are applied only when their switch is on, cleared afterwards (secret first; only the secret after a problem), deduplicated by keyed hash, and report into their Result field and the results screen. - Add OTP validates per README 10.4; Add code runs the real encoder (CodeCheck, a job for 2D codes) and caches 2D codes on the way in. - Manage entry: edit, move, require/drop PIN, replace, counter, delete. Changes to locked entries are sealed under "q" and applied only after the PIN is entered under "Phone changes" in the menu. - About: catalog (800 characters max), version, storage use. - "Encrypt names" now applies to new and existing entries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'source/settings/ChangeQueue.mc')
-rw-r--r--source/settings/ChangeQueue.mc143
1 files changed, 143 insertions, 0 deletions
diff --git a/source/settings/ChangeQueue.mc b/source/settings/ChangeQueue.mc
new file mode 100644
index 0000000..12f88f7
--- /dev/null
+++ b/source/settings/ChangeQueue.mc
@@ -0,0 +1,143 @@
+// Author: cirakg & claude ai
+// Summary: Changes from the phone that wait for the PIN on the watch (README 7.9), sealed with the device key under "q".
+//
+// Anyone who controls the phone can send settings, so edits, new secrets,
+// "Don't require PIN" and deletes of locked entries are not applied when
+// they arrive: they wait here until someone confirms them with the PIN on
+// the watch (or rejects them). Each item is one ManageAction sealed on its
+// 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).
+
+import Toybox.Application;
+import Toybox.Lang;
+
+module ChangeQueue {
+
+ const KEY = "q";
+ const MAX_ITEMS = 16;
+ const AAD = "OTPW-Q-1";
+
+ function count(vault as Vault) as Number {
+ var stored = vault.storage().getValue(KEY);
+ return stored instanceof Array ? stored.size() : 0;
+ }
+
+ // Append an action. Throws VaultError LIMIT (too many waiting) or FULL.
+ function add(vault as Vault, action as ManageAction) as Void {
+ var stored = vault.storage().getValue(KEY);
+ var items = stored instanceof Array ? stored as Array<Application.Storage.ValueType> : [] as Array<Application.Storage.ValueType>;
+ if (items.size() >= MAX_ITEMS) {
+ throw new VaultError(VaultError.LIMIT, MAX_ITEMS + " changes already wait for your PIN. Confirm or reject them on the watch first");
+ }
+ var plain = action.toBytes();
+ items.add(SealedBox.seal(vault.keyManager.deviceKeys(), Bytes.fromUtf8(AAD), plain));
+ Bytes.wipe(plain);
+ try {
+ vault.storage().setValue(KEY, items);
+ } catch (e instanceof Lang.Exception) {
+ throw new VaultError(VaultError.FULL, "storage full");
+ }
+ }
+
+ // The waiting actions in arrival order; items that don't verify are
+ // left out (and dropped by the next clear()).
+ function load(vault as Vault) as Array<ManageAction> {
+ var out = [] as Array<ManageAction>;
+ var stored = vault.storage().getValue(KEY);
+ if (!(stored instanceof Array)) {
+ return out;
+ }
+ var keys = vault.keyManager.deviceKeys();
+ var aad = Bytes.fromUtf8(AAD);
+ for (var i = 0; i < stored.size(); i++) {
+ var box = stored[i];
+ var plain = box instanceof ByteArray ? SealedBox.open(keys, aad, box) : null;
+ if (plain != null) {
+ var action = ManageAction.fromBytes(plain);
+ Bytes.wipe(plain);
+ if (action != null) {
+ out.add(action);
+ }
+ }
+ }
+ return out;
+ }
+
+ // Short descriptions for the confirmation screen ("Delete: K7F2 Bank").
+ function descriptions(vault as Vault) as Array<String> {
+ var actions = load(vault);
+ var out = [] as Array<String>;
+ for (var i = 0; i < actions.size(); i++) {
+ out.add(actions[i].describe());
+ actions[i].wipe();
+ }
+ return out;
+ }
+
+ function clear(vault as Vault) as Void {
+ vault.storage().deleteValue(KEY);
+ }
+
+ // Remove the first n items (the ones just applied), keeping any that
+ // arrived from the phone meanwhile.
+ function drop(vault as Vault, n as Number) as Void {
+ var stored = vault.storage().getValue(KEY);
+ if (!(stored instanceof Array) || stored.size() <= n) {
+ clear(vault);
+ return;
+ }
+ try {
+ vault.storage().setValue(KEY, stored.slice(n, null) as Array<Application.Storage.ValueType>);
+ } catch (e instanceof Lang.Exception) {
+ clear(vault); // can't happen when shrinking; never re-apply
+ }
+ }
+}
+
+// Applies every waiting change after the PIN was entered, one per step
+// (each re-seals a record; a code may need its CodeCheck). The applied
+// items leave the queue only at the end, so a crash midway loses nothing;
+// applying one again is harmless (a deleted entry is then just missing).
+class ChangeQueueJob extends Jobs.Job {
+
+ var lines as Array<String> = [] as Array<String>;
+
+ private var _vault as Vault;
+ private var _actions as Array<ManageAction>;
+ private var _loaded as Number = 0;
+ private var _index as Number = 0;
+ private var _run as ManageRun? = null;
+
+ function initialize(vault as Vault) {
+ Job.initialize();
+ _vault = vault;
+ var stored = vault.storage().getValue(ChangeQueue.KEY);
+ _loaded = stored instanceof Array ? stored.size() : 0;
+ _actions = ChangeQueue.load(vault);
+ }
+
+ function step(budget as Number) as Boolean {
+ var run = _run;
+ if (run == null) {
+ if (_index >= _actions.size()) {
+ ChangeQueue.drop(_vault, _loaded);
+ if (lines.size() == 0) {
+ lines.add("No changes were waiting.");
+ }
+ return true;
+ }
+ run = new ManageRun(_vault, _actions[_index], true);
+ _run = run;
+ }
+ if (run.step(budget)) {
+ var problem = run.failed ? "Not applied: " + _actions[_index].describe() + ". " : "";
+ lines.add(problem + run.message);
+ lines.addAll(run.notes);
+ _actions[_index].wipe();
+ _index++;
+ _run = null;
+ }
+ return false;
+ }
+}