App-Sperre mit Passwort und/oder FIDO2-Sicherheitsschlüssel (PRF/hmac-secret): Vault zusätzlich verschlüsselt, Sperrbildschirm, Strg+Shift+L, automatische Sperre
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+129
-8
@@ -137,6 +137,9 @@ const S = {
|
||||
const settings = () => S.vault.settings;
|
||||
|
||||
async function reload() {
|
||||
S.lock = await call('lock:status');
|
||||
$('#lockBtn').style.display = S.lock.enabled ? '' : 'none';
|
||||
if (S.lock.locked) return showLock();
|
||||
S.vault = await call('vault:get');
|
||||
I18N.setLanguage(settings().language, navigator.language);
|
||||
applyStatic();
|
||||
@@ -838,6 +841,10 @@ async function viewSettings(page) {
|
||||
rdmBtn.onclick = importRdm;
|
||||
importCard.append(rdmBtn);
|
||||
|
||||
// ---- App-Sperre
|
||||
const secCard = h(`<div class="settings-card"><h3>${T('App lock')}</h3><p style="color:var(--muted);margin-top:0">${T('Lock MrTerm with a password and/or a FIDO2 security key (e.g. YubiKey). The vault is then additionally encrypted and can only be opened with one of these methods.')}</p><div class="lock-rows"></div></div>`);
|
||||
renderLockSettings($('.lock-rows', secCard));
|
||||
|
||||
// ---- Updates
|
||||
const updCard = h(`<div class="settings-card"><h3>Updates</h3><p class="upd-info" style="color:var(--muted);margin-top:0">${T('Installed version: {v}', { v: '…' })}</p></div>`);
|
||||
api.call('app:version').then((v) => { $('.upd-info', updCard).textContent = T('Installed version: {v}', { v }); });
|
||||
@@ -868,8 +875,121 @@ async function viewSettings(page) {
|
||||
<span>${C}+<kbd>1..9</kbd></span><span>${T('Switch to tab')}</span>
|
||||
<span>${C}+<kbd>Shift</kbd>+<kbd>C/V</kbd></span><span>${T('Copy / paste in terminal')}</span>
|
||||
<span>${C}+<kbd>Shift</kbd>+<kbd>F</kbd></span><span>${T('Search in terminal')}</span>
|
||||
<span>${C}+<kbd>+/−/0</kbd></span><span>${T('Font size')}</span></div></div>`);
|
||||
c.append(langCard, designCard, themeCard, termCard, rdpCard, importCard, dataCard, updCard, keysCard);
|
||||
<span>${C}+<kbd>+/−/0</kbd></span><span>${T('Font size')}</span>
|
||||
<span>${C}+<kbd>Shift</kbd>+<kbd>L</kbd></span><span>${T('Lock now')}</span></div></div>`);
|
||||
c.append(langCard, secCard, designCard, themeCard, termCard, rdpCard, importCard, dataCard, updCard, keysCard);
|
||||
}
|
||||
|
||||
// ============================================================ App-Sperre
|
||||
function showLock() {
|
||||
if ($('.lock-screen')) return;
|
||||
const m = S.lock.meta || {};
|
||||
I18N.setLanguage(m.language, navigator.language);
|
||||
applyStatic();
|
||||
applyAppTheme(m);
|
||||
closeDrawer();
|
||||
$$('.ctx').forEach((x) => x.remove());
|
||||
$('.palette')?.closest('.modal-bg')?.remove();
|
||||
const el = h(`<div class="lock-screen"><div class="lock-box">
|
||||
<img class="lock-logo" src="logo.png" alt=""/><h2>${T('MrTerm is locked')}</h2>
|
||||
${S.lock.hasPassword ? `<form class="lock-pw"><input type="password" name="pw" placeholder="${esc(T('Password'))}" autocomplete="off"/><button class="btn primary">${ICONS.unlock}${T('Unlock')}</button></form>` : ''}
|
||||
${S.lock.fido.length ? `<button class="btn lock-fido">${ICONS.key}${T('Unlock with security key')}</button>` : ''}
|
||||
<div class="lock-err"></div></div></div>`);
|
||||
const err = $('.lock-err', el);
|
||||
const done = async () => { el.remove(); await reload(); };
|
||||
const form = $('.lock-pw', el);
|
||||
if (form) form.onsubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
const inp = $('input', form);
|
||||
err.textContent = '';
|
||||
try { await api.call('lock:unlockPassword', inp.value); done(); } catch (x) { err.textContent = x.message; inp.select(); }
|
||||
};
|
||||
const fb = $('.lock-fido', el);
|
||||
if (fb) fb.onclick = async () => {
|
||||
err.textContent = ''; fb.disabled = true;
|
||||
try { await api.call('lock:unlockFido'); done(); } catch (x) { err.textContent = x.message; }
|
||||
fb.disabled = false;
|
||||
};
|
||||
document.body.append(el);
|
||||
($('input', el) || fb)?.focus();
|
||||
}
|
||||
|
||||
async function lockNow() {
|
||||
if (!S.lock?.enabled) return;
|
||||
S.lock = await call('lock:lock');
|
||||
if (S.lock.locked) showLock();
|
||||
}
|
||||
$('#lockBtn').onclick = lockNow;
|
||||
|
||||
// Automatische Sperre nach Inaktivität (Einstellung autoLock in Minuten, 0 = aus)
|
||||
let lastActivity = Date.now();
|
||||
['keydown', 'mousedown', 'mousemove', 'wheel'].forEach((ev) => document.addEventListener(ev, () => { lastActivity = Date.now(); }, { capture: true, passive: true }));
|
||||
setInterval(() => {
|
||||
const min = Number(S.vault?.settings.autoLock) || 0;
|
||||
if (!min || !S.lock?.enabled || S.lock.locked || $('.lock-screen')) return;
|
||||
// In einem eingebetteten RDP-Fenster sieht MrTerm keine Eingaben – dort nicht automatisch sperren
|
||||
if (S.tabs.some((t) => t.id === S.active && t.kind === 'rdp')) { lastActivity = Date.now(); return; }
|
||||
if (Date.now() - lastActivity > min * 60000) lockNow();
|
||||
}, 15000);
|
||||
|
||||
function renderLockSettings(box) {
|
||||
const L = S.lock;
|
||||
box.innerHTML = '';
|
||||
const refresh = (st) => { S.lock = st; $('#lockBtn').style.display = st.enabled ? '' : 'none'; renderLockSettings(box); };
|
||||
const disableWarn = async () => (L.hasPassword ? 1 : 0) + L.fido.length > 1 || confirmBox(T('Disable app lock?'), T('This is the last unlock method. MrTerm will no longer be locked.'), T('Disable'));
|
||||
|
||||
// Passwort
|
||||
const pwRow = h(`<div class="lock-row"><div class="grow"><b>${T('Password')}</b><div class="sub">${L.hasPassword ? T('Set') : T('Not set')}</div></div></div>`);
|
||||
const pwBtn = h(`<button class="btn">${L.hasPassword ? T('Change password') : T('Set password')}</button>`);
|
||||
pwBtn.onclick = async () => {
|
||||
const r = await modal({ title: L.hasPassword ? T('Change password') : T('Set password'),
|
||||
body: `<div class="field"><label>${T('New password')}</label><input name="a" type="password" autocomplete="off"/></div><div class="field"><label>${T('Repeat password')}</label><input name="b" type="password" autocomplete="off"/></div>`,
|
||||
buttons: [{ label: T('Cancel'), value: null, cls: 'ghost' }, { label: T('Save'), value: 'form', cls: 'primary' }] });
|
||||
if (!r) return;
|
||||
if (r.a !== r.b) return toast(T('The passwords do not match.'), 'error');
|
||||
try { refresh(await call('lock:setPassword', r.a)); toast(T('Password saved'), 'ok'); } catch {}
|
||||
};
|
||||
pwRow.append(pwBtn);
|
||||
if (L.hasPassword) {
|
||||
const rm = h(`<button class="btn ghost" title="${esc(T('Remove'))}">${ICONS.trash}</button>`);
|
||||
rm.onclick = async () => { if (await disableWarn()) refresh(await call('lock:removePassword')); };
|
||||
pwRow.append(rm);
|
||||
}
|
||||
box.append(pwRow);
|
||||
|
||||
// Sicherheitsschlüssel
|
||||
for (const k of L.fido) {
|
||||
const row = h(`<div class="lock-row"><div class="grow"><b>${ICONS.key}${esc(k.label)}</b><div class="sub">${T('Security key (FIDO2)')}</div></div></div>`);
|
||||
const rm = h(`<button class="btn ghost" title="${esc(T('Remove'))}">${ICONS.trash}</button>`);
|
||||
rm.onclick = async () => { if (await disableWarn() && await confirmBox(T('Remove security key?'), k.label, T('Remove'))) refresh(await call('lock:removeFido', k.id)); };
|
||||
row.append(rm);
|
||||
box.append(row);
|
||||
}
|
||||
const add = h(`<button class="btn">${ICONS.plus}${T('Add security key')}</button>`);
|
||||
add.onclick = async () => {
|
||||
const label = await promptBox(T('Add security key'), T('Name'), 'YubiKey');
|
||||
if (label == null) return;
|
||||
add.disabled = true;
|
||||
try { refresh(await call('lock:addFido', label)); toast(T('Security key added'), 'ok'); } catch {}
|
||||
add.disabled = false;
|
||||
};
|
||||
const actions = h('<div class="row" style="flex-wrap:wrap;margin-top:12px"></div>');
|
||||
actions.append(add);
|
||||
if (L.enabled) {
|
||||
const now = h(`<button class="btn primary">${ICONS.lock}${T('Lock now')}</button>`);
|
||||
now.onclick = lockNow;
|
||||
actions.append(now);
|
||||
}
|
||||
box.append(actions);
|
||||
|
||||
if (L.enabled) {
|
||||
const al = field(T('Lock automatically after inactivity'), 'autoLock', String(settings().autoLock || 0), { type: 'select',
|
||||
options: [['0', T('Never')], ...[1, 5, 10, 15, 30, 60].map((n) => [String(n), T('{n} minutes', { n })])] });
|
||||
al.style.marginTop = '14px';
|
||||
al.onchange = async () => { const v = Number($('select', al).value); await call('vault:settings', { autoLock: v }); S.vault.settings.autoLock = v; };
|
||||
box.append(al);
|
||||
box.append(h(`<div class="hint" style="color:var(--faint);font-size:11px;margin-top:10px">${T('If you forget the password and lose all security keys, the vault cannot be recovered.')}</div>`));
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================ Design
|
||||
@@ -878,8 +998,7 @@ function termTheme() {
|
||||
if (id === 'auto') return TERM_THEMES[(APP_THEMES[settings().appTheme] || APP_THEMES.midnight).term] || TERM_THEMES.mrterm;
|
||||
return TERM_THEMES[id] || TERM_THEMES.mrterm;
|
||||
}
|
||||
function applyAppTheme() {
|
||||
const st = settings();
|
||||
function applyAppTheme(st = settings()) {
|
||||
const root = document.documentElement;
|
||||
if (st.appTheme && st.appTheme !== 'midnight') root.dataset.theme = st.appTheme; else delete root.dataset.theme;
|
||||
if (st.accent) root.style.setProperty('--accent', st.accent); else root.style.removeProperty('--accent');
|
||||
@@ -1028,7 +1147,7 @@ class TerminalSession {
|
||||
if (e.ctrlKey && e.shiftKey && e.code === 'KeyV') { this.paste(); return false; }
|
||||
if (e.ctrlKey && e.shiftKey && e.code === 'KeyF') { this.toggleFind(true); return false; }
|
||||
// App-Kürzel im Terminal nur mit Strg+Shift, damit Strg+W/K/T (nano, bash, …) im Terminal ankommen
|
||||
if (e.ctrlKey && e.shiftKey && ['KeyK', 'KeyT', 'KeyW'].includes(e.code)) return false;
|
||||
if (e.ctrlKey && e.shiftKey && ['KeyK', 'KeyT', 'KeyW', 'KeyL'].includes(e.code)) return false;
|
||||
if (e.ctrlKey && e.code === 'Tab') return false;
|
||||
if (e.ctrlKey && /^Digit[1-9]$/.test(e.code)) return false;
|
||||
if (e.ctrlKey && (e.key === '+' || e.key === '=' || e.key === '-' || e.key === '0')) { this.zoom(e.key); return false; }
|
||||
@@ -1154,7 +1273,7 @@ function openTerminal(host) { return new TerminalSession(host); }
|
||||
// Deshalb wird es ausgeblendet, solange ein Dialog, Menü oder die Befehlspalette offen ist.
|
||||
S.covered = false;
|
||||
function updateCover() {
|
||||
const covered = !!document.querySelector('.modal-bg, .ctx');
|
||||
const covered = !!document.querySelector('.modal-bg, .ctx, .lock-screen');
|
||||
if (covered === S.covered) return;
|
||||
S.covered = covered;
|
||||
const t = S.tabs.find((x) => x.id === S.active && x.kind === 'rdp');
|
||||
@@ -1507,7 +1626,9 @@ if (api.platform === 'darwin') $('.win-ctrls').style.display = 'none';
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (!e.ctrlKey) return;
|
||||
const inTerm = !!e.target.closest?.('.xterm');
|
||||
if ((e.code === 'KeyK' || e.code === 'KeyT') && (e.shiftKey || !inTerm)) { e.preventDefault(); openPalette(); }
|
||||
if ($('.lock-screen')) return;
|
||||
if (e.shiftKey && e.code === 'KeyL') { e.preventDefault(); lockNow(); }
|
||||
else if ((e.code === 'KeyK' || e.code === 'KeyT') && (e.shiftKey || !inTerm)) { e.preventDefault(); openPalette(); }
|
||||
else if (e.shiftKey && e.code === 'KeyW') { e.preventDefault(); if (S.active !== 'home') closeTab(S.active); }
|
||||
else if (e.code === 'Tab') {
|
||||
e.preventDefault();
|
||||
@@ -1535,5 +1656,5 @@ api.on('fw:event', () => reload());
|
||||
api.on('toast', (m, t) => toast(m, t));
|
||||
api.on('win:state', (max) => { $('#winMax').innerHTML = max ? '<svg viewBox="0 0 12 12"><rect x="2" y="4" width="6" height="6"/><path d="M4 4V2h6v6H8"/></svg>' : '<svg viewBox="0 0 12 12"><rect x="2" y="2" width="8" height="8"/></svg>'; });
|
||||
|
||||
reload();
|
||||
reload(); // zeigt bei aktiver Sperre zuerst den Sperrbildschirm
|
||||
$('#updateBadge').onclick = () => updateInfo && showUpdate(updateInfo);
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
</div>
|
||||
<button id="newTabBtn" class="icon-btn" title="Quick Connect (Ctrl+Shift+K)" data-i18n-title="Quick Connect (Ctrl+Shift+K)">+</button>
|
||||
<div class="drag-fill"></div>
|
||||
<button id="lockBtn" class="icon-btn lock-btn" style="display:none" title="Lock (Ctrl+Shift+L)" data-i18n-title="Lock (Ctrl+Shift+L)"><svg viewBox="0 0 24 24"><rect x="5" y="11" width="14" height="10" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></svg></button>
|
||||
<div class="win-ctrls">
|
||||
<button id="winMin" title="Minimize" data-i18n-title="Minimize"><svg viewBox="0 0 12 12"><path d="M2 6h8"/></svg></button>
|
||||
<button id="winMax" title="Maximize" data-i18n-title="Maximize"><svg viewBox="0 0 12 12"><rect x="2" y="2" width="8" height="8"/></svg></button>
|
||||
|
||||
@@ -343,3 +343,20 @@ kbd { background: var(--card); border: 1px solid var(--border); border-bottom-wi
|
||||
#sidebar.nav-collapsed .nav-menu { flex-direction: row; flex-wrap: wrap; gap: 2px; }
|
||||
#sidebar.nav-collapsed .nav-menu .nav { padding: 7px; flex: 0 0 auto; }
|
||||
#sidebar.nav-collapsed .nav-menu .nav span { display: none; }
|
||||
|
||||
/* App-Sperre */
|
||||
.lock-btn svg { width: 15px; height: 15px; fill: none; stroke: currentColor; stroke-width: 2; }
|
||||
.lock-screen { position: fixed; inset: 0; top: var(--titlebar); z-index: 200; background: var(--bg); display: flex; align-items: center; justify-content: center; animation: fade .15s; }
|
||||
.lock-box { width: 340px; display: flex; flex-direction: column; align-items: center; gap: 14px; text-align: center; }
|
||||
.lock-box h2 { margin: 0 0 6px; font-size: 18px; }
|
||||
.lock-logo { width: 64px; height: 64px; }
|
||||
.lock-pw { display: flex; gap: 8px; width: 100%; }
|
||||
.lock-pw input { flex: 1; min-width: 0; background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 9px 11px; color: var(--text); outline: none; }
|
||||
.lock-pw input:focus { border-color: var(--accent); }
|
||||
.lock-fido { width: 100%; justify-content: center; }
|
||||
.lock-err { color: var(--red); min-height: 18px; font-size: 13px; }
|
||||
.lock-row { display: flex; align-items: center; gap: 8px; padding: 10px 0; border-bottom: 1px solid var(--border); }
|
||||
.lock-row .grow { flex: 1; min-width: 0; }
|
||||
.lock-row b { display: flex; align-items: center; gap: 6px; }
|
||||
.lock-row b svg { width: 14px; height: 14px; }
|
||||
.lock-row .sub { color: var(--muted); font-size: 12px; margin-top: 2px; }
|
||||
|
||||
Reference in New Issue
Block a user