feat(satellite): Sicherungs-Kennwort je FritzBox — hinterlegt, stark erzwungen, ARIA kennt es nicht

- Zugangsdaten FritzBox: neues Feld "Kennwort fuer Sicherungen"
  (backupPass, verschluesselt auf dem Satelliten, mit 👁 wie die anderen).
- fritzbox.backup nimmt: ausdruecklich angegebenes > hinterlegtes
  Sicherungs-Kennwort > FritzBox-Login. Das Ergebnis nennt nur die Quelle,
  nie das Kennwort. ARIAs Beschreibung: kein 'password' uebergeben.
- Kennwort-Regel (Formular UND Satellit): mind. 12 Zeichen, Gross- und
  Kleinbuchstabe, Ziffer, Sonderzeichen. Schwaches Kennwort wird beim
  Speichern abgelehnt (Meldung im Formular), und eine Sicherung mit
  schwachem Kennwort (auch ueber das Login) bricht mit Hinweis ab.
- Formular: Eingabefelder mit Auge gleich hoch wie die anderen.

Getestet im Satelliten: Regel, Speichern schwach/stark (Login bleibt),
Sicherung ohne/mit hinterlegtem Kennwort; das Kennwort taucht im Ergebnis
nicht auf.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-31 17:42:24 +01:00
co-authored by Claude Opus 5.5
parent e91435a9f6
commit c545a1cd77
3 changed files with 73 additions and 14 deletions
+44 -3
View File
@@ -290,7 +290,25 @@ def _creds_for(ip: str) -> dict:
return _CREDS.get((ip or "").strip(), {}) if ip else {}
_CRED_SECRETS = ("pass", "key", "community", "authKey", "privKey")
_CRED_SECRETS = ("pass", "key", "community", "authKey", "privKey", "backupPass")
def _strong_password_problem(pw: str) -> str:
""""" wenn stark genug, sonst was fehlt. Regel fuer Sicherungs-Kennwoerter:
mind. 12 Zeichen, Gross- und Kleinbuchstabe, Ziffer, Sonderzeichen."""
pw = str(pw or "")
missing = []
if len(pw) < 12:
missing.append("mindestens 12 Zeichen")
if not re.search(r"[A-Z]", pw):
missing.append("ein Grossbuchstabe")
if not re.search(r"[a-z]", pw):
missing.append("ein Kleinbuchstabe")
if not re.search(r"\d", pw):
missing.append("eine Ziffer")
if not re.search(r"[^A-Za-z0-9]", pw):
missing.append("ein Sonderzeichen")
return ", ".join(missing)
def _creds_detail(d: dict, reveal: bool) -> dict:
@@ -1415,7 +1433,19 @@ def _fb_backup(ip: str, user: str, pw: str, params: dict) -> dict:
noetig). Braucht meist die Taste an der Box (button_wait)."""
import requests
from requests.auth import HTTPDigestAuth
export_pw = str(params.get("password") or pw)
# Kennwort der Sicherung: ausdruecklich angegeben > im Satelliten hinterlegt
# (Feld "Sicherungs-Kennwort" — ARIA kennt es nicht) > FritzBox-Login.
stored = _creds_for(ip).get("fritzbox", {}).get("backupPass")
export_pw = str(params.get("password") or stored or pw)
weak = _strong_password_problem(export_pw)
if weak:
src = ("Das angegebene Kennwort" if params.get("password")
else "Das hinterlegte Sicherungs-Kennwort" if stored
else "Es ist kein Sicherungs-Kennwort hinterlegt, und das FritzBox-Login")
return {"ok": False, "error": (
f"{src} ist fuer eine Sicherung zu schwach (es fehlt: {weak}). Stefan soll im "
"Diagnostic beim Geraet (🔑) ein Sicherungs-Kennwort hinterlegen: mind. 12 Zeichen, "
"Gross-/Kleinbuchstabe, Ziffer, Sonderzeichen.")}
r = _fb_call(ip, user, pw, {"service": "DeviceConfig", "action": "X_AVM-DE_GetConfigFile",
"args": {"X_AVM-DE_Password": export_pw},
"button_wait": params.get("button_wait")})
@@ -1453,7 +1483,9 @@ def _fb_backup(ip: str, user: str, pw: str, params: dict) -> dict:
return {"ok": True, "result": {
"ip": ip, "datei": path, "bytes": len(d.content), "auf": "Satellit",
**({"file_b64": file_b64, "filename": fname} if file_b64 else {}),
"kennwort": "das angegebene Kennwort" if params.get("password") else "das FritzBox-Login-Kennwort",
"kennwort": ("das angegebene Kennwort" if params.get("password")
else "das im Satelliten hinterlegte Sicherungs-Kennwort" if stored
else "das FritzBox-Login-Kennwort"),
"hinweis": f"Liegt auf dem Satelliten, die letzten {FB_BACKUP_KEEP} je Box bleiben. "
"Zum Wiederherstellen in der FritzBox: System → Sicherung → Wiederherstellen, mit dem Kennwort."}}
@@ -2504,6 +2536,15 @@ class Satellite:
ip = (payload.get("ip") or "").strip()
creds = payload.get("creds") or {}
ok = False
weak = _strong_password_problem((creds.get("fritzbox") or {}).get("backupPass")) \
if isinstance(creds, dict) and (creds.get("fritzbox") or {}).get("backupPass") else ""
if weak:
await self._send({"type": "sat_creds_result",
"payload": {"requestId": payload.get("requestId", ""),
"satellite": SATELLITE_ID, "ok": False, "ip": ip,
"error": f"Sicherungs-Kennwort zu schwach — es fehlt: {weak}."},
"timestamp": int(time.time() * 1000)})
return
if ip and isinstance(creds, dict):
entry = _CREDS.setdefault(ip, {})
for t in ("snmp", "http", "fritzbox", "ssh"):