From 151bfda5d4c567ff909a61dc9c90bf8eece1db37 Mon Sep 17 00:00:00 2001 From: cirakg Date: Tue, 20 Oct 2026 18:36:54 +0200 Subject: 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 --- source/storage/KeyManager.mc | 24 ++++++++++++++++++++++++ source/storage/Vault.mc | 19 +++++++++++++++++++ 2 files changed, 43 insertions(+) (limited to 'source/storage') diff --git a/source/storage/KeyManager.mc b/source/storage/KeyManager.mc index 567288f..47dbd59 100644 --- a/source/storage/KeyManager.mc +++ b/source/storage/KeyManager.mc @@ -84,6 +84,30 @@ class KeyManager { return out; } + // A keyed hash (16 hex characters) of a settings form's content, for + // the applied-forms log (README 10.3). Keyed like the fingerprints, with + // its own label, because the content includes secrets. The parts are + // fed to the native HMAC one by one, each preceded by `separator`, so a + // large import form costs no byte loop here (the watchdog). + function formHash(parts as Array, separator as Number) as String { + deviceKeys(); + var mac = new Cryptography.HashBasedMessageAuthenticationCode({ + :algorithm => Cryptography.HASH_SHA256, + :key => _fingerprintKey as ByteArray + }); + mac.update(Bytes.fromUtf8("OTPW-FH-1")); + for (var i = 0; i < parts.size(); i++) { + mac.update([separator]b); + if (parts[i].size() > 0) { + mac.update(parts[i]); + } + } + var digest = mac.digest(); + var out = Bytes.toHex(digest.slice(0, FINGERPRINT_BYTES)); + Bytes.wipe(digest); + return out; + } + // True when a PIN has been set. function hasPin() as Boolean { return _store.getValue(WRAP_KEY) != null; diff --git a/source/storage/Vault.mc b/source/storage/Vault.mc index 46c8de2..88054e0 100644 --- a/source/storage/Vault.mc +++ b/source/storage/Vault.mc @@ -31,6 +31,9 @@ class Vault { var lockout as Lockout; // The "Erase locked entries after 10 wrong PINs" setting (off by default). var eraseAfterFailures as Boolean = false; + // The "Encrypt names" setting: new entries keep their names in the + // encrypted part (existing ones are converted by AboutScan). + var encryptNames as Boolean = false; private var _store as Persist.Backend; private var _meta as Dictionary; @@ -126,6 +129,7 @@ class Vault { throw new VaultError(VaultError.LIMIT, "at most " + MAX_ENTRIES + " entries"); } entry.id = newId(); + entry.namesEncrypted = encryptNames; entry.fingerprint = keyManager.fingerprint(entry); if (entry.created == 0) { entry.created = Clock.nowUnix(0); @@ -156,6 +160,21 @@ class Vault { _store.deleteValue("x." + entry.id); // the cached matrix may be stale } + // Re-seal an entry with or without encrypted names ("Encrypt names"). + // Nothing else changes, so the barcode cache stays. Throws NOT_FOUND, + // LOCKED, DAMAGED or FULL. + function setNamesEncrypted(id as String, encrypted as Boolean) as Void { + var entry = open(id); + entry.namesEncrypted = encrypted; + try { + write("e." + id, seal(entry)); + } catch (e instanceof VaultError) { + entry.wipeSecrets(); + throw e; + } + entry.wipeSecrets(); + } + // ---- Encoded barcode cache (README 12.2) ----------------------------- // Store an encoded matrix for a code entry, sealed with the entry's own -- cgit v1.2.3