diff --git a/delphi-backend/UMainForm.pas b/delphi-backend/UMainForm.pas index 47af1c2..21051d3 100644 --- a/delphi-backend/UMainForm.pas +++ b/delphi-backend/UMainForm.pas @@ -1280,6 +1280,25 @@ begin 'Bridge.onPrefResult("' + LEscapedKey + '","' + LEscapedVal + '")'); end + // Existence probe. Some prefs ARE secrets (the auto-backup password, the sync + // passwords) and the UI mostly only needs to know whether one is set. Asking + // through prefs/get would interpolate the secret into an ExecuteJavaScript + // string — measured in a renderer dump, where it also showed up in the + // DevTools protocol buffers, and it stayed there after the vault auto-locked. + // The answer rides the same callback under "#has". + else if ACmd = 'prefs/has' then + begin + var LHasKey := GetParam('key'); + if LHasKey = '' then Exit; + var LEsc := StringReplace(LHasKey, '\', '\\', [rfReplaceAll]); + LEsc := StringReplace(LEsc, '"', '\"', [rfReplaceAll]); + var LSet := ''; + if PM.UserPrefs.GetPref(LHasKey) <> '' then LSet := '1'; + WebBrowser.ExecuteJavaScript( + 'if(window.Bridge&&Bridge.onPrefResult)' + + 'Bridge.onPrefResult("' + LEsc + '#has","' + LSet + '")'); + end + else if ACmd = 'prefs/set' then begin var LKey := GetParam('key'); diff --git a/js/app.backup.js b/js/app.backup.js index 366ca8f..4c02a9f 100644 --- a/js/app.backup.js +++ b/js/app.backup.js @@ -24,15 +24,21 @@ const ABK = { }; const AUTO_BACKUP_PREFIX = 'vault-autobackup-'; -async function loadAutoBackupConfig() { +// withPwd defaults to FALSE on purpose: the UI only ever needs to know whether +// a backup password exists. Pulling the value across the bridge leaves it in +// the renderer for the life of the process — verified in a memory dump, where +// it was still there after the vault had auto-locked and the vault key was +// gone. Only the two paths that actually encrypt a backup ask for it. +async function loadAutoBackupConfig(withPwd) { if (!Bridge.active) return null; - const [enabled, dir, interval, keep, last, pwd] = await Promise.all([ + const [enabled, dir, interval, keep, last, pwd, hasPwd] = await Promise.all([ Bridge.getPref(ABK.enabled), Bridge.getPref(ABK.dir), Bridge.getPref(ABK.interval), Bridge.getPref(ABK.keep), Bridge.getPref(ABK.last), - Bridge.getPref(ABK.pwd), + withPwd ? Bridge.getPref(ABK.pwd) : Promise.resolve(''), + Bridge.hasPref(ABK.pwd), ]); return { enabled: enabled === '1', @@ -40,7 +46,7 @@ async function loadAutoBackupConfig() { interval: Math.max(1, parseInt(interval, 10) || 7), keep: Math.max(1, parseInt(keep, 10) || 10), last: last || '', - hasPwd: !!pwd, + hasPwd: !!hasPwd, pwd, }; } @@ -139,7 +145,8 @@ async function changeBackupPwd() { } async function runAutoBackupNow(silent) { - const cfg = await loadAutoBackupConfig(); + // withPwd: this is the one place that encrypts a backup. + const cfg = await loadAutoBackupConfig(true); if (!cfg) return silent || toast('Bridge not available', 'error'); if (!cfg.dir) return silent || toast('Choose a backup folder first', 'warning'); if (!cfg.hasPwd) return silent || toast('Backup password not set', 'warning'); @@ -263,6 +270,8 @@ async function applyAutoBackupRetention(dir, keep) { async function runAutoBackupIfDue() { if (!Bridge.active) return; + // No password yet: the due-check reads only metadata. runAutoBackupNow + // fetches the secret itself, and only if a run is actually starting. const cfg = await loadAutoBackupConfig(); if (!cfg || !cfg.enabled || !cfg.dir || !cfg.hasPwd) return; if (!state.cryptoKey) return; diff --git a/js/app.js b/js/app.js index 3c0e7e4..da844de 100644 --- a/js/app.js +++ b/js/app.js @@ -269,6 +269,25 @@ const Bridge = (() => { }, 2000); }); }, + // "Is this pref set?" without pulling the value across. For the ones + // that are secrets (backup / sync passwords) the UI usually only needs + // the boolean, and fetching the value would leave it sitting in the + // renderer — and in the DevTools protocol buffers — until the process + // dies, surviving even an auto-lock. + hasPref(key) { + if (!active) return Promise.resolve(false); + return new Promise(resolve => { + const slot = key + '#has'; + prefResolvers[slot] = v => resolve(v === '1'); + cmd('cmd://prefs/has?key=' + encodeURIComponent(key)); + setTimeout(() => { + if (prefResolvers[slot]) { + delete prefResolvers[slot]; + resolve(false); + } + }, 2000); + }); + }, setPref(key, value) { if (!active) return; cmd('cmd://prefs/set?key=' + encodeURIComponent(key) + diff --git a/js/app.sync.js b/js/app.sync.js index b0a912c..43fa426 100644 --- a/js/app.sync.js +++ b/js/app.sync.js @@ -106,10 +106,16 @@ function refreshSyncUI(cfg) { const pre = document.getElementById('syncPreBackup'); if (url) url.value = cfg.url || ''; if (user) user.value = cfg.user || ''; - if (pwd) pwd.value = cfg.pwd || ''; + if (pwd) { + // Left EMPTY on purpose — the settings panel no longer pulls the stored + // password across. The field only saves on `change`, so an untouched + // empty field never overwrites anything; the placeholder says so. + pwd.value = cfg.pwd || ''; + pwd.placeholder = cfg.hasPwd ? 'Saved — type to replace' : ''; + } if (pre) pre.checked = !!cfg.preBackup; const pwdStatus = document.getElementById('syncPwdStatus'); - if (pwdStatus) pwdStatus.textContent = cfg.encPwd ? 'Set.' : 'Not set.'; + if (pwdStatus) pwdStatus.textContent = cfg.hasEncPwd ? 'Set.' : 'Not set.'; const last = document.getElementById('syncLast'); if (last) last.textContent = cfg.last ? ' · Last: ' + cfg.last.replace('T', ' ').slice(0, 16) @@ -146,16 +152,23 @@ async function syncSetEncPwdFlow() { } } -async function loadSyncConfig() { +// withSecrets defaults to FALSE: the settings panel only needs to know whether +// the two passwords are set. Fetching them plants the values in the renderer +// for the life of the process — seen in a memory dump, still present after the +// vault had auto-locked. Only the paths that actually talk to the server or +// encrypt a snapshot ask for them. +async function loadSyncConfig(withSecrets) { if (!Bridge.active) return null; - const [enabled, url, user, pwd, encPwd, pre, last] = await Promise.all([ + const [enabled, url, user, pwd, encPwd, pre, last, hasPwd, hasEnc] = await Promise.all([ Bridge.getPref(SYNC_PREFS.enabled), Bridge.getPref(SYNC_PREFS.url), Bridge.getPref(SYNC_PREFS.user), - Bridge.getPref(SYNC_PREFS.pwd), - Bridge.getPref(SYNC_PREFS.encPwd), + withSecrets ? Bridge.getPref(SYNC_PREFS.pwd) : Promise.resolve(''), + withSecrets ? Bridge.getPref(SYNC_PREFS.encPwd) : Promise.resolve(''), Bridge.getPref(SYNC_PREFS.preBackup), Bridge.getPref(SYNC_PREFS.last), + Bridge.hasPref(SYNC_PREFS.pwd), + Bridge.hasPref(SYNC_PREFS.encPwd), ]); return { enabled: enabled === '1', @@ -163,13 +176,15 @@ async function loadSyncConfig() { user: user || '', pwd: pwd || '', encPwd: encPwd || '', + hasPwd: !!hasPwd, + hasEncPwd: !!hasEnc, preBackup: pre === '1', last: last || '', }; } async function syncTestConnection() { - const cfg = await loadSyncConfig(); + const cfg = await loadSyncConfig(true); // needs the WebDAV credentials if (!cfg || !cfg.url) return toast('Set the WebDAV URL first', 'warning'); toast('Testing connection…'); const r = await _webdavCall('test', cfg.url, cfg.user, cfg.pwd); @@ -464,7 +479,7 @@ function syncStatus(text) { async function runSyncNow(_attempt) { _attempt = _attempt || 0; - const cfg = await loadSyncConfig(); + const cfg = await loadSyncConfig(true); // talks to the server + encrypts if (!cfg) return toast('Bridge not available', 'error'); if (!cfg.url) return toast('Sync not configured', 'warning'); if (!cfg.encPwd) return toast('Set the sync password first', 'warning');