From 67107fba37a4dcdfffe869046342ae8829ce2bdc Mon Sep 17 00:00:00 2001 From: r-zakarya <82443831+r-zakarya@users.noreply.github.com> Date: Tue, 28 Jul 2026 22:50:28 +0100 Subject: [PATCH] fix(unlock): persist unlockMode in DPAPI so a cold start still asks for the PIN unlockMode lived only in localStorage, wiped on every launch since the ephemeral port changes the origin, and in settings_json, which needs a session to read. The auth screen therefore always fell back to the master password on a cold start even though the PIN blob was there. Mirror it to a DPAPI pref and read it back in showAuth, like rememberedUsername. --- TEST_SCENARIOS.md | 7 +++++++ js/app.js | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/TEST_SCENARIOS.md b/TEST_SCENARIOS.md index 3d8c59d..ddfb0d7 100644 --- a/TEST_SCENARIOS.md +++ b/TEST_SCENARIOS.md @@ -255,6 +255,13 @@ BuildAssets pour le JS. - **Historique** : la réponse redeem n'echoait pas `argon2` → params par défaut (correct aujourd'hui, lockout latent si params non-défaut). +### E8 · le mode de déverrouillage survit au redémarrage ✅ +- **ACTION** : créer un PIN → *Unlock method* = **PIN** → verrouiller (le PIN est bien demandé) → **fermer et relancer l'app**. +- **ATTENDU** : le PIN est demandé, pas le mot de passe maître. +- **Variantes** : mode `both` → mot de passe **puis** PIN au démarrage à froid ; retour en mode `pw` → mot de passe seul ; 5 PIN faux (le blob DPAPI est effacé) → relancer → retour au mot de passe, sans blocage. +- **Historique** : `unlockMode` n'existait que dans `localStorage` — vidé à **chaque** lancement, le port éphémère changeant l'origine — et dans `settings_json`, illisible avant d'être connecté. Au démarrage à froid l'écran d'auth retombait donc toujours sur `'pw'`, alors que le blob PIN était bien présent. Écrit désormais aussi en pref DPAPI (`Bridge.setPref`) et relu par `showAuth`, comme `rememberedUsername`. **Le réglage est device-local par nature** (le blob PIN est lié à ce Windows) : il reste dans `settings_json` pour l'historique, mais c'est la copie DPAPI qui fait autorité au boot. +- **Auto-correction** : si la pref dit `pin` alors que le blob n'existe plus, `applyAuthScreenMode` force `pw` — pas de verrouillage possible. + ### E5 · la clé du coffre n'est plus en clair dans le web storage - **ACTION** : coffre déverrouillé → console WebView2 : `sessionStorage.getItem('cryptoKey')`. diff --git a/js/app.js b/js/app.js index 04d2795..c8af3de 100644 --- a/js/app.js +++ b/js/app.js @@ -6584,6 +6584,18 @@ async function showAuth() { // user may have set/removed it from another instance, and we want // the correct fields to show up without forcing a full reload. state.pinConfigured = await bridgePinStatus(); + // unlockMode lives in settings_json (needs a session) and in localStorage + // (wiped every launch — the port changes, so the origin does too). Neither + // is readable here, on a cold start before any login, so the DPAPI copy is + // the only one that survives: without it a PIN-only vault always came back + // asking for the master password. + if (Bridge.active) { + const savedMode = await Bridge.getPref('unlockMode'); + if (savedMode === 'pw' || savedMode === 'pin' || savedMode === 'both') { + state.unlockMode = savedMode; + localStorage.setItem('unlockMode', savedMode); + } + } applyAuthScreenMode(); // Clear residual PIN input from a previous attempt. const pinInput = document.getElementById('loginPin'); @@ -6845,6 +6857,9 @@ async function loadServerSettings() { break; case 'unlockMode': localStorage.setItem('unlockMode', String(v || 'pw')); + // Mirror to DPAPI so the next cold start's auth screen knows + // which method to offer, before any login can read settings. + if (Bridge.active) Bridge.setPref('unlockMode', String(v || 'pw')); break; case 'autofillClearField': localStorage.setItem('autofillClearField', v ? '1' : '0'); @@ -7596,6 +7611,10 @@ async function init() { } state.unlockMode = next; localStorage.setItem('unlockMode', next); + // Also DPAPI: the auth screen needs this BEFORE any login, and + // localStorage is wiped on every launch (ephemeral port = new origin), + // while settings_json requires being signed in to read. + if (Bridge.active) Bridge.setPref('unlockMode', next); saveServerSettings(); toast('Unlock method updated'); });