From 38acee731926fc4d46006ffe0f347e70f6ac8672 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Apr 2026 15:01:51 +0000 Subject: [PATCH 1/2] Initial plan From 17f89fa773511ca9cd65e888e787ed1467da020e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Apr 2026 15:08:25 +0000 Subject: [PATCH 2/2] fix: disable auto-login, diceware passwords, improved security reset UX, fix GNOME keyring Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/82a54a25-4844-4a41-afcc-c034cebbd6ed Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com> --- app/sovran_systemsos_web/server.py | 126 +++++++++++++++++- .../static/css/security.css | 54 ++++++++ .../static/js/security.js | 42 +++++- app/sovran_systemsos_web/templates/index.html | 19 ++- configuration.nix | 4 +- modules/credentials.nix | 68 ++++++---- 6 files changed, 268 insertions(+), 45 deletions(-) diff --git a/app/sovran_systemsos_web/server.py b/app/sovran_systemsos_web/server.py index fce9ebb..6660154 100644 --- a/app/sovran_systemsos_web/server.py +++ b/app/sovran_systemsos_web/server.py @@ -429,6 +429,32 @@ SERVICE_DESCRIPTIONS: dict[str, str] = { ), } +# ── Diceware password generation ───────────────────────────────── + +_DICEWARE_WORDS = [ + "apple", "barn", "brook", "cabin", "cedar", "cloud", "coral", "crane", + "delta", "eagle", "ember", "fern", "field", "flame", "flora", "flint", + "frost", "grove", "haven", "hedge", "holly", "heron", "jade", "juniper", + "kelp", "larch", "lemon", "lilac", "linden", "loch", "lotus", "maple", + "marsh", "meadow", "mist", "mossy", "mount", "oak", "ocean", "olive", + "petal", "pine", "pixel", "plum", "pond", "prism", "quartz", "raven", + "ridge", "river", "robin", "rocky", "rose", "rowan", "sage", "sand", + "sierra", "silver", "slate", "snow", "solar", "spark", "spruce", "stone", + "storm", "summit", "swift", "thorn", "tide", "timber", "torch", "trout", + "vale", "vault", "vine", "walnut", "wave", "willow", "wren", "amber", + "aspen", "birch", "blaze", "bloom", "bluff", "coast", "copper", "crest", + "dune", "elder", "fjord", "forge", "glade", "glen", "glow", "gulf", +] + + +def _generate_diceware_password() -> str: + """Generate a human-readable diceware-style passphrase: word-word-word-N.""" + import secrets as _secrets + words = [_secrets.choice(_DICEWARE_WORDS) for _ in range(3)] + digit = _secrets.randbelow(10) + return "-".join(words) + f"-{digit}" + + # ── App setup ──────────────────────────────────────────────────── _BASE_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -3040,7 +3066,10 @@ async def api_security_reset(): Wipes secrets, LND wallet, SSH keys, drops databases, removes app configs, clears Vaultwarden data, removes the onboarding-complete flag so onboarding - re-runs, and reboots the system. + re-runs. Generates a new diceware password for the 'free' and 'root' users, + deletes the GNOME Keyring so a fresh one is created on next GDM login, and + returns the new password to the caller. Does NOT reboot — the caller must + trigger a separate POST /api/reboot after showing the password to the user. """ wipe_paths = [ "/var/lib/secrets", @@ -3099,13 +3128,79 @@ async def api_security_reset(): except Exception as exc: errors.append(f"mariadb wipe: {exc}") - # Reboot - try: - subprocess.Popen(["systemctl", "reboot"]) - except Exception as exc: - raise HTTPException(status_code=500, detail=f"Reboot failed: {exc}") + # Generate new diceware passwords + new_free_password = _generate_diceware_password() + new_root_password = _generate_diceware_password() - return {"ok": True, "errors": errors} + # Locate chpasswd + chpasswd_bin = ( + shutil.which("chpasswd") + or ("/run/current-system/sw/bin/chpasswd" + if os.path.isfile("/run/current-system/sw/bin/chpasswd") else None) + ) + + if chpasswd_bin: + # Set free user password + try: + result = subprocess.run( + [chpasswd_bin], + input=f"free:{new_free_password}", + capture_output=True, text=True, + ) + if result.returncode != 0: + errors.append(f"chpasswd free: {(result.stderr or result.stdout).strip()}") + except Exception as exc: + errors.append(f"chpasswd free: {exc}") + + # Set root password + try: + result = subprocess.run( + [chpasswd_bin], + input=f"root:{new_root_password}", + capture_output=True, text=True, + ) + if result.returncode != 0: + errors.append(f"chpasswd root: {(result.stderr or result.stdout).strip()}") + except Exception as exc: + errors.append(f"chpasswd root: {exc}") + else: + errors.append("chpasswd not found; passwords not reset") + + # Write new passwords to secrets files + try: + os.makedirs("/var/lib/secrets", exist_ok=True) + with open("/var/lib/secrets/free-password", "w") as f: + f.write(new_free_password) + os.chmod("/var/lib/secrets/free-password", 0o600) + except Exception as exc: + errors.append(f"write free-password: {exc}") + + try: + os.makedirs("/var/lib/secrets", exist_ok=True) + with open("/var/lib/secrets/root-password", "w") as f: + f.write(new_root_password) + os.chmod("/var/lib/secrets/root-password", 0o600) + except Exception as exc: + errors.append(f"write root-password: {exc}") + + # Delete GNOME Keyring files so a fresh keyring is created with the new + # password on the next GDM login (PAM unlocks it automatically). + keyring_dir = "/home/free/.local/share/keyrings" + try: + if os.path.isdir(keyring_dir): + for entry in os.listdir(keyring_dir): + entry_path = os.path.join(keyring_dir, entry) + try: + if os.path.isfile(entry_path) or os.path.islink(entry_path): + os.unlink(entry_path) + elif os.path.isdir(entry_path): + shutil.rmtree(entry_path, ignore_errors=True) + except Exception: + pass + except Exception as exc: + errors.append(f"keyring wipe: {exc}") + + return {"ok": True, "new_password": new_free_password, "errors": errors} @app.post("/api/security/verify-integrity") @@ -3265,6 +3360,23 @@ async def api_change_password(req: ChangePasswordRequest): except Exception as exc: raise HTTPException(status_code=500, detail=f"Failed to write secrets file: {exc}") + # Delete GNOME Keyring files so a fresh keyring is created with the new + # password on the next GDM login (PAM will unlock it automatically). + keyring_dir = "/home/free/.local/share/keyrings" + try: + if os.path.isdir(keyring_dir): + for entry in os.listdir(keyring_dir): + entry_path = os.path.join(keyring_dir, entry) + try: + if os.path.isfile(entry_path) or os.path.islink(entry_path): + os.unlink(entry_path) + elif os.path.isdir(entry_path): + shutil.rmtree(entry_path, ignore_errors=True) + except Exception: + pass + except Exception: + pass # Non-fatal: keyring will be re-created on next login regardless + return {"ok": True} diff --git a/app/sovran_systemsos_web/static/css/security.css b/app/sovran_systemsos_web/static/css/security.css index d7ff22a..f40dea6 100644 --- a/app/sovran_systemsos_web/static/css/security.css +++ b/app/sovran_systemsos_web/static/css/security.css @@ -242,6 +242,60 @@ margin-bottom: 16px; } +/* ── Phase 2: password display box ──────────────────────────────── */ + +.security-reset-password-label { + font-size: 0.88rem; + color: var(--text-secondary); + margin: 16px 0 8px 0; +} + +.security-reset-password-box { + font-family: monospace; + font-size: 1.35rem; + font-weight: 700; + color: var(--text-primary); + background: rgba(109, 191, 139, 0.10); + border: 1.5px solid rgba(109, 191, 139, 0.35); + border-radius: 8px; + padding: 14px 24px; + letter-spacing: 0.04em; + text-align: center; + word-break: break-all; + margin-bottom: 16px; + min-width: 260px; +} + +.security-reset-password-warning { + font-size: 0.84rem; + color: var(--text-secondary); + line-height: 1.6; + margin-bottom: 20px; + text-align: center; +} + +.security-reset-reboot-btn { + background-color: #6DBF8B; + color: #0a0c0b; + border: none; + border-radius: 7px; + padding: 11px 22px; + font-size: 0.88rem; + font-weight: 700; + cursor: pointer; + transition: background-color 0.15s, opacity 0.15s; + white-space: nowrap; +} + +.security-reset-reboot-btn:hover:not(:disabled) { + background-color: #5aab78; +} + +.security-reset-reboot-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + /* ── First-login security banner ─────────────────────────────────── */ .security-first-login-banner { diff --git a/app/sovran_systemsos_web/static/js/security.js b/app/sovran_systemsos_web/static/js/security.js index 13cac7c..a2a1b29 100644 --- a/app/sovran_systemsos_web/static/js/security.js +++ b/app/sovran_systemsos_web/static/js/security.js @@ -128,11 +128,43 @@ function openSecurityModal() { if (resetStatus) { resetStatus.textContent = "Running security reset\u2026"; resetStatus.className = "security-status-msg security-status-info"; } try { - await apiFetch("/api/security/reset", { method: "POST" }); - if ($secResetStep) $secResetStep.textContent = "Reset complete. Rebooting now\u2026"; - if (resetStatus) { resetStatus.textContent = "\u2713 Reset complete. Rebooting\u2026"; resetStatus.className = "security-status-msg security-status-ok"; } - if ($rebootOverlay) $rebootOverlay.classList.add("visible"); - setTimeout(waitForServerReboot, REBOOT_CHECK_INTERVAL); + var data = await apiFetch("/api/security/reset", { method: "POST" }); + + // Switch to Phase 2: show the new password and wait for user confirmation + var phase1 = document.getElementById("security-reset-phase1"); + var phase2 = document.getElementById("security-reset-phase2"); + var passwordBox = document.getElementById("security-reset-new-password"); + var rebootBtn = document.getElementById("security-reset-reboot-btn"); + + if (phase1) phase1.style.display = "none"; + if (phase2) phase2.style.display = ""; + if (passwordBox && data.new_password) passwordBox.textContent = data.new_password; + + if (rebootBtn) { + // Keep button disabled for 5 seconds to prevent accidental clicks + var countdown = 5; + rebootBtn.textContent = "I have written down my new password \u2014 Reboot now (" + countdown + ")"; + var timer = setInterval(function() { + countdown--; + if (countdown <= 0) { + clearInterval(timer); + rebootBtn.disabled = false; + rebootBtn.textContent = "I have written down my new password \u2014 Reboot now"; + } else { + rebootBtn.textContent = "I have written down my new password \u2014 Reboot now (" + countdown + ")"; + } + }, 1000); + + rebootBtn.addEventListener("click", async function() { + rebootBtn.disabled = true; + rebootBtn.textContent = "Rebooting\u2026"; + try { + await apiFetch("/api/reboot", { method: "POST" }); + } catch (_) {} + if ($rebootOverlay) $rebootOverlay.classList.add("visible"); + setTimeout(waitForServerReboot, REBOOT_CHECK_INTERVAL); + }, { once: true }); + } } catch (err) { if ($secResetOverlay) $secResetOverlay.classList.remove("visible"); if (resetStatus) { resetStatus.textContent = "\u2717 Error: " + (err.message || "Reset failed."); resetStatus.className = "security-status-msg security-status-error"; } diff --git a/app/sovran_systemsos_web/templates/index.html b/app/sovran_systemsos_web/templates/index.html index 1a3693d..c101dfa 100644 --- a/app/sovran_systemsos_web/templates/index.html +++ b/app/sovran_systemsos_web/templates/index.html @@ -212,13 +212,14 @@