diff options
Diffstat (limited to 'source/settings/ChangeQueue.mc')
| -rw-r--r-- | source/settings/ChangeQueue.mc | 143 |
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; + } +} |
