// 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 : [] as Array; 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 { var out = [] as Array; 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 { var actions = load(vault); var out = [] as Array; 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); } 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 = [] as Array; private var _vault as Vault; private var _actions as Array; 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; } }