feat: Kennwort je FritzBox-Sicherung — gespeichert, per 👁 anzeigen, kopieren

Bisher merkte sich der Satellit nur die QUELLE des Kennworts. Wurde das
Sicherungs-Kennwort spaeter geaendert, passte es nicht mehr zu aelteren
Sicherungen — auch "Wiederherstellen" haette das falsche genommen.

- Satellit: das benutzte Kennwort liegt verschluesselt (Fernet, wie der
  Zugangsdaten-Speicher) in der Begleitdatei; Liste/Ansicht geben es nie
  heraus. Wiederherstellen nimmt genau dieses. fritzbox.backup_password
  liefert es (aeltere Sicherungen: aktuelles der Quelle + Hinweis).
  Feld "kennwort" im Sicherungs-Ergebnis heisst jetzt "verschluesselt_mit".
- Diagnostic-Server: Ergebnisse mit Kennwort gehen nur an den Browser,
  der gefragt hat; ohne Fragenden werden sie verworfen, nie an alle.
- Brain: fritzbox.backup_password ist fuer ARIA gesperrt (Kennwoerter
  gehen nicht ans Sprachmodell).
- Diagnostic: 👁 in der Kennwort-Spalte → Zeile mit dem Kennwort und
  "Kopieren" — klappt auch ohne https (Fallback ueber execCommand).

Getestet: Satellit (Kennwort nicht im Klartext gespeichert, nach
Kennwort-Aenderung zeigt 👁 und nimmt Wiederherstellen das alte), Fenster
in headless Chromium ohne Clipboard-API.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-31 18:26:37 +01:00
co-authored by Claude Opus 5.5
parent e44e2c2a70
commit 2cd8f1a0de
4 changed files with 108 additions and 8 deletions
+45 -3
View File
@@ -6349,7 +6349,7 @@
m.innerHTML = `<div style="background:#12121E;border:1px solid #444466;border-radius:8px;padding:16px 18px;max-width:760px;width:100%;max-height:88vh;overflow-y:auto;color:#E0E0F0;font-size:13px;">${html}</div>`;
return m;
}
function closeFbBackups() { const m = document.getElementById('fb-backups-modal'); if (m) m.remove(); fbB = null; }
function closeFbBackups() { const m = document.getElementById('fb-backups-modal'); if (m) m.remove(); fbB = null; } // vergisst auch angezeigte Kennwoerter
const _fbFmtDate = iso => { try { return new Date(iso).toLocaleString('de-DE', { dateStyle: 'medium', timeStyle: 'short' }); } catch (_) { return iso; } };
const _fbPwSrc = { backupPass: 'Sicherungs-Kennwort', login: 'FritzBox-Kennwort', angegeben: 'eigenes Kennwort' };
async function openFbBackups(sat, ip, name) {
@@ -6368,12 +6368,19 @@
<td style="padding:6px;">${escapeHtml(_fbFmtDate(b.erstellt) || b.datei)}</td>
<td style="padding:6px;color:#9a9ab5;">${escapeHtml(b.modell || '')}${b.firmware ? ' · FW ' + escapeHtml(b.firmware) : ''}</td>
<td style="padding:6px;color:#9a9ab5;">${_fmtBytes(b.bytes)}</td>
<td style="padding:6px;color:#9a9ab5;" title="Verschlüsselt mit diesem Kennwort — zum Wiederherstellen nötig. „Wiederherstellen“ nimmt es automatisch; beim Einspielen von Hand in der FritzBox-Oberfläche musst du genau dieses eingeben.">🔒 ${escapeHtml(_fbPwSrc[b.kennwort] || b.kennwort || '')}</td>
<td style="padding:6px;color:#9a9ab5;" title="Verschlüsselt mit diesem Kennwort — zum Wiederherstellen nötig. „Wiederherstellen“ nimmt es automatisch; beim Einspielen von Hand in der FritzBox-Oberfläche musst du genau dieses eingeben.">🔒 ${escapeHtml(_fbPwSrc[b.kennwort] || b.kennwort || '')}
<button class="btn secondary" onclick="fbShowPw(${i})" title="Kennwort dieser Sicherung anzeigen" style="padding:1px 6px;font-size:11px;margin-left:4px;">👁</button></td>
<td style="padding:6px;white-space:nowrap;">
<button class="btn secondary" onclick="fbViewBackup(${i})" title="Inhalt ansehen" style="padding:2px 8px;font-size:11px;">ℹ</button>
<button class="btn secondary" onclick="fbDownloadBackup(${i})" title="Herunterladen (.export, verschlüsselt mit dem Sicherungs-Kennwort)" style="padding:2px 8px;font-size:11px;">⬇</button>
<button class="btn secondary" onclick="fbRestoreBackup(${i})" title="Diese Sicherung einspielen" style="padding:2px 8px;font-size:11px;">Wiederherstellen</button>
<button class="btn secondary" onclick="fbDeleteBackup(${i})" title="Löschen" style="padding:2px 8px;font-size:11px;color:#FF6E6E;">🗑</button></td></tr>`).join('');
<button class="btn secondary" onclick="fbDeleteBackup(${i})" title="Löschen" style="padding:2px 8px;font-size:11px;color:#FF6E6E;">🗑</button></td></tr>`
+ (fbB.pw && fbB.pw[b.datei] ? `<tr><td colspan="5" style="padding:4px 6px 10px;">
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
<span style="color:#8888AA;font-size:11px;">Kennwort dieser Sicherung:</span>
<code style="background:#000;border:1px solid #333;border-radius:4px;padding:3px 8px;font-size:13px;color:#E0E0F0;user-select:all;">${escapeHtml(fbB.pw[b.datei].kennwort)}</code>
<button class="btn secondary" onclick="fbCopyPw('${escapeHtmlAttr(b.datei)}', this)" style="padding:2px 8px;font-size:11px;">Kopieren</button></div>
${fbB.pw[b.datei].hinweis ? `<div style="color:#FF9F0A;font-size:11px;margin-top:4px;">${escapeHtml(fbB.pw[b.datei].hinweis)}</div>` : ''}</td></tr>` : '')).join('');
_fbModal(`<div style="display:flex;justify-content:space-between;align-items:center;gap:10px;margin-bottom:8px;">
<h3 style="margin:0;">💾 Sicherungen: ${escapeHtml(fbB.name)} <span style="color:#8888AA;font-size:12px;">(${escapeHtml(fbB.ip)})</span></h3>
<button onclick="closeFbBackups()" style="background:none;border:none;color:#8888AA;font-size:22px;cursor:pointer;">&times;</button></div>
@@ -6472,6 +6479,41 @@
const r = await _fbRun('Wiederherstellen läuft', 'fritzbox.restore', { file: b.datei, confirmed: true });
if (fbB) _fbShowResult(r, 'Sicherung eingespielt');
}
// Kopieren auch ohne https: navigator.clipboard gibt es nur in "sicheren
// Kontexten" — sonst ueber ein unsichtbares Textfeld + execCommand('copy').
function copyText(t) {
const fallback = () => {
const ta = document.createElement('textarea');
ta.value = t; ta.setAttribute('readonly', '');
ta.style.cssText = 'position:fixed;top:-1000px;left:0;opacity:0;';
document.body.appendChild(ta); ta.select();
let ok = false;
try { ok = document.execCommand('copy'); } catch (_) {}
ta.remove();
return ok;
};
if (navigator.clipboard && window.isSecureContext) return navigator.clipboard.writeText(t).then(() => true, fallback);
return Promise.resolve(fallback());
}
async function fbShowPw(i) {
const b = fbB.list[i];
if (!b) return;
fbB.pw = fbB.pw || {};
if (fbB.pw[b.datei]) { delete fbB.pw[b.datei]; _fbRender(''); return; } // zweiter Klick: verbergen
_fbRender('Hole Kennwort …');
const r = await fbCmd('fritzbox.backup_password', { file: b.datei });
if (!fbB) return;
if (!r.ok) { _fbRender('⚠ ' + (r.error || 'Kennwort nicht abrufbar')); return; }
fbB.pw[b.datei] = { kennwort: r.result.kennwort, hinweis: r.result.hinweis || '' };
_fbRender('');
}
async function fbCopyPw(datei, btn) {
const e = fbB && fbB.pw && fbB.pw[datei];
if (!e) return;
const ok = await copyText(e.kennwort);
btn.textContent = ok ? '✓ kopiert' : 'Kopieren ging nicht — markieren + Strg+C';
setTimeout(() => { if (btn.isConnected) btn.textContent = 'Kopieren'; }, 2500);
}
async function fbDownloadBackup(i) {
const b = fbB.list[i];
if (!b) return;