summaryrefslogtreecommitdiff
path: root/source/settings/AboutScan.mc
diff options
context:
space:
mode:
Diffstat (limited to 'source/settings/AboutScan.mc')
-rw-r--r--source/settings/AboutScan.mc174
1 files changed, 174 insertions, 0 deletions
diff --git a/source/settings/AboutScan.mc b/source/settings/AboutScan.mc
new file mode 100644
index 0000000..8fc898a
--- /dev/null
+++ b/source/settings/AboutScan.mc
@@ -0,0 +1,174 @@
+// Author: cirakg & claude ai
+// Summary: Writes the read-only About fields (catalog, version, storage use) and applies "Encrypt names" to existing entries.
+//
+// One pass over every entry header, a few per step:
+// * catalog: "K7F2 GitHub (alice) · 9QXR Gym card · ...", so the Manage
+// form's IDs can be looked up on the phone. With "Encrypt names" on it
+// lists only IDs and types: names stay off the phone too.
+// * storageUse: entries, waiting items and the bytes their records take.
+// * version: the app version.
+// * Entries whose names are stored the other way from the current
+// "Encrypt names" option are re-sealed, one per step. Locked ones need
+// the session; if one can't be done now, the next scan retries (after
+// the next PIN entry, which runs the processor again).
+// The scan only runs when something it reports changed: a form was
+// applied, or the signature of the entry list (IDs, pending, option,
+// version) differs from the last complete scan.
+
+import Toybox.Lang;
+
+class AboutScan extends Jobs.Job {
+
+ static const SIGNATURE_KEY = "cs";
+ static const HEADERS_PER_STEP = 4;
+ static const CATALOG_MAX_CHARS = 800; // Garmin Express caps a text field at 800 (FEASIBILITY 3d)
+ static const SEPARATOR = " · ";
+ // README 6.2: the stricter of Garmin's two documented totals.
+ static const STORE_KB = 100;
+
+ private var _vault as Vault;
+ private var _store as Forms.Store;
+ private var _ids as Array<String>;
+ private var _pending as Array<String>;
+ private var _index as Number = 0;
+ private var _catalog as String = "";
+ private var _left as Number = 0;
+ private var _bytes as Number = 0;
+ private var _incomplete as Boolean = false;
+
+ function initialize(vault as Vault, store as Forms.Store) {
+ Job.initialize();
+ _vault = vault;
+ _store = store;
+ _ids = vault.ids();
+ _pending = vault.pendingIds();
+ }
+
+ // True when the About fields may be out of date.
+ static function needed(vault as Vault) as Boolean {
+ var stored = vault.storage().getValue(SIGNATURE_KEY);
+ return !(stored instanceof String) || !stored.equals(signature(vault));
+ }
+
+ static function signature(vault as Vault) as String {
+ var content = new Forms.Content("about");
+ content.add(vault.ids().toString());
+ content.add(vault.pendingIds().toString());
+ content.add(vault.encryptNames);
+ content.add(ChangeQueue.count(vault));
+ content.add(AppInfo.VERSION);
+ return Forms.hash(vault, content);
+ }
+
+ function step(budget as Number) as Boolean {
+ var total = _ids.size() + _pending.size();
+ for (var n = 0; n < HEADERS_PER_STEP && _index < total; n++) {
+ var converted = _index < _ids.size() ? visit(_ids[_index]) : visitPending(_pending[_index - _ids.size()]);
+ _index++;
+ if (converted) {
+ return false; // one re-seal per step
+ }
+ }
+ if (_index < total) {
+ return false;
+ }
+ writeFields();
+ return true;
+ }
+
+ // Catalog and size of a listed entry; re-seals it when its names are
+ // stored the other way. True when it re-sealed.
+ private function visit(id as String) as Boolean {
+ var header = _vault.anyHeader(id);
+ addBytes("e." + id);
+ addBytes("x." + id);
+ if (header == null) {
+ addItem(id + " damaged");
+ return false;
+ }
+ var converted = false;
+ if (header.namesEncrypted != _vault.encryptNames) {
+ if (!header.locked || _vault.session.isUnlocked()) {
+ try {
+ _vault.setNamesEncrypted(id, _vault.encryptNames);
+ converted = true;
+ var after = _vault.anyHeader(id); // names readable again after decrypting
+ header = after != null ? after : header;
+ } catch (e instanceof VaultError) {
+ _incomplete = true;
+ }
+ } else {
+ _incomplete = true;
+ }
+ }
+ addItem(id + " " + describe(header) + (header.locked ? " (PIN)" : ""));
+ return converted;
+ }
+
+ private function visitPending(id as String) as Boolean {
+ var header = _vault.anyHeader(id);
+ addBytes("p." + id);
+ addItem(id + " " + (header != null ? describe(header) : "entry") + " (waits for the PIN)");
+ return false;
+ }
+
+ // The names, or the type when names are hidden or encrypted.
+ private function describe(header as Entry) as String {
+ var label = Forms.label(header.name, header.account);
+ if (!_vault.encryptNames && !header.namesEncrypted && label.length() > 0) {
+ return label;
+ }
+ if (header.kind == Entry.KIND_CODE) {
+ return Codes.name(header.type);
+ }
+ return header.type == Entry.OTP_HOTP ? "HOTP" : (header.type == Entry.OTP_STEAM ? "Steam" : "TOTP");
+ }
+
+ private function addItem(item as String) as Void {
+ if (_left > 0 || _catalog.length() + SEPARATOR.length() + item.length() > CATALOG_MAX_CHARS) {
+ _left++;
+ return;
+ }
+ _catalog += (_catalog.length() > 0 ? SEPARATOR : "") + item;
+ }
+
+ private function addBytes(key as String) as Void {
+ var value = _vault.storage().getValue(key);
+ if (value instanceof ByteArray) {
+ _bytes += value.size();
+ }
+ }
+
+ private function writeFields() as Void {
+ var catalog = _catalog.length() > 0 ? _catalog : "No entries yet";
+ if (_left > 0) {
+ catalog += SEPARATOR + "and " + _left + " more (see Entry IDs on the watch)";
+ }
+ var use = _ids.size() + " of " + Vault.MAX_ENTRIES + " entries";
+ if (_pending.size() > 0) {
+ use += " (+" + _pending.size() + " waiting for the PIN)";
+ }
+ use += ", " + (_bytes + 1023) / 1024 + " KB of about " + STORE_KB + " KB";
+ var waiting = ChangeQueue.count(_vault);
+ if (waiting > 0) {
+ use += ". " + waiting + (waiting == 1 ? " change waits" : " changes wait") + " for the PIN on the watch";
+ }
+ setIfChanged("catalog", catalog);
+ setIfChanged("storageUse", use);
+ setIfChanged("version", AppInfo.VERSION);
+ if (!_incomplete) {
+ try {
+ _vault.storage().setValue(SIGNATURE_KEY, signature(_vault));
+ } catch (e instanceof Lang.Exception) {
+ // A full store: the scan just runs again next time.
+ }
+ }
+ }
+
+ private function setIfChanged(key as String, value as String) as Void {
+ var old = _store.get(key);
+ if (!(old instanceof String) || !old.equals(value)) {
+ _store.set(key, value);
+ }
+ }
+}