Merge pull request #203 from naturallaw777/copilot/fix-gnome-keyring-authentication
fix: disable auto-login, diceware passwords, decoupled security reset UX
This commit is contained in:
@@ -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 ────────────────────────────────────────────────────
|
# ── App setup ────────────────────────────────────────────────────
|
||||||
|
|
||||||
_BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
_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,
|
Wipes secrets, LND wallet, SSH keys, drops databases, removes app configs,
|
||||||
clears Vaultwarden data, removes the onboarding-complete flag so onboarding
|
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 = [
|
wipe_paths = [
|
||||||
"/var/lib/secrets",
|
"/var/lib/secrets",
|
||||||
@@ -3099,13 +3128,79 @@ async def api_security_reset():
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
errors.append(f"mariadb wipe: {exc}")
|
errors.append(f"mariadb wipe: {exc}")
|
||||||
|
|
||||||
# Reboot
|
# Generate new diceware passwords
|
||||||
try:
|
new_free_password = _generate_diceware_password()
|
||||||
subprocess.Popen(["systemctl", "reboot"])
|
new_root_password = _generate_diceware_password()
|
||||||
except Exception as exc:
|
|
||||||
raise HTTPException(status_code=500, detail=f"Reboot failed: {exc}")
|
|
||||||
|
|
||||||
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")
|
@app.post("/api/security/verify-integrity")
|
||||||
@@ -3265,6 +3360,23 @@ async def api_change_password(req: ChangePasswordRequest):
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise HTTPException(status_code=500, detail=f"Failed to write secrets file: {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}
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -242,6 +242,60 @@
|
|||||||
margin-bottom: 16px;
|
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 ─────────────────────────────────── */
|
/* ── First-login security banner ─────────────────────────────────── */
|
||||||
|
|
||||||
.security-first-login-banner {
|
.security-first-login-banner {
|
||||||
|
|||||||
@@ -128,11 +128,43 @@ function openSecurityModal() {
|
|||||||
|
|
||||||
if (resetStatus) { resetStatus.textContent = "Running security reset\u2026"; resetStatus.className = "security-status-msg security-status-info"; }
|
if (resetStatus) { resetStatus.textContent = "Running security reset\u2026"; resetStatus.className = "security-status-msg security-status-info"; }
|
||||||
try {
|
try {
|
||||||
await apiFetch("/api/security/reset", { method: "POST" });
|
var data = 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"; }
|
// 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");
|
if ($rebootOverlay) $rebootOverlay.classList.add("visible");
|
||||||
setTimeout(waitForServerReboot, REBOOT_CHECK_INTERVAL);
|
setTimeout(waitForServerReboot, REBOOT_CHECK_INTERVAL);
|
||||||
|
}, { once: true });
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if ($secResetOverlay) $secResetOverlay.classList.remove("visible");
|
if ($secResetOverlay) $secResetOverlay.classList.remove("visible");
|
||||||
if (resetStatus) { resetStatus.textContent = "\u2717 Error: " + (err.message || "Reset failed."); resetStatus.className = "security-status-msg security-status-error"; }
|
if (resetStatus) { resetStatus.textContent = "\u2717 Error: " + (err.message || "Reset failed."); resetStatus.className = "security-status-msg security-status-error"; }
|
||||||
|
|||||||
@@ -212,13 +212,14 @@
|
|||||||
|
|
||||||
<!-- Security Reset overlay -->
|
<!-- Security Reset overlay -->
|
||||||
<div class="security-reset-overlay" id="security-reset-overlay">
|
<div class="security-reset-overlay" id="security-reset-overlay">
|
||||||
<div class="reboot-card">
|
<!-- Phase 1: wiping in progress -->
|
||||||
|
<div class="reboot-card" id="security-reset-phase1">
|
||||||
<div class="security-reset-overlay-icon">🛡</div>
|
<div class="security-reset-overlay-icon">🛡</div>
|
||||||
<h2 class="reboot-title">Security Reset In Progress</h2>
|
<h2 class="reboot-title">Security Reset In Progress</h2>
|
||||||
<p class="reboot-message">
|
<p class="reboot-message">
|
||||||
⚠️ Wiping all data and credentials.<br />
|
⚠️ Wiping all data and credentials.<br />
|
||||||
<strong>Do not power off your computer.</strong><br />
|
<strong>Do not power off your computer.</strong><br />
|
||||||
This may take several minutes. The system will reboot automatically when complete.
|
This may take several minutes.
|
||||||
</p>
|
</p>
|
||||||
<div class="reboot-dots">
|
<div class="reboot-dots">
|
||||||
<span class="reboot-dot"></span>
|
<span class="reboot-dot"></span>
|
||||||
@@ -227,6 +228,20 @@
|
|||||||
</div>
|
</div>
|
||||||
<p class="reboot-submessage" id="security-reset-overlay-step">Erasing data and resetting credentials…</p>
|
<p class="reboot-submessage" id="security-reset-overlay-step">Erasing data and resetting credentials…</p>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Phase 2: password display -->
|
||||||
|
<div class="reboot-card" id="security-reset-phase2" style="display:none;">
|
||||||
|
<div class="security-reset-overlay-icon">🔑</div>
|
||||||
|
<h2 class="reboot-title">Security Reset Complete</h2>
|
||||||
|
<p class="security-reset-password-label">Your new login password is:</p>
|
||||||
|
<div class="security-reset-password-box" id="security-reset-new-password"> </div>
|
||||||
|
<p class="security-reset-password-warning">
|
||||||
|
✍️ <strong>Write this down now.</strong><br />
|
||||||
|
You will need it to log in to your computer<br />and the Sovran Hub at <em>sovransystemsos.local</em>.
|
||||||
|
</p>
|
||||||
|
<button class="security-reset-reboot-btn" id="security-reset-reboot-btn" disabled>
|
||||||
|
I have written down my new password — Reboot now
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Reboot overlay -->
|
<!-- Reboot overlay -->
|
||||||
|
|||||||
@@ -88,8 +88,8 @@
|
|||||||
extraGroups = [ "networkmanager" ];
|
extraGroups = [ "networkmanager" ];
|
||||||
};
|
};
|
||||||
|
|
||||||
services.displayManager.autoLogin.enable = true;
|
services.displayManager.autoLogin.enable = false;
|
||||||
services.displayManager.autoLogin.user = "free";
|
# services.displayManager.autoLogin.user = "free"; # Disabled — user logs in via GDM
|
||||||
|
|
||||||
# ── Flatpak ────────────────────────────────────────────────
|
# ── Flatpak ────────────────────────────────────────────────
|
||||||
services.flatpak.enable = true;
|
services.flatpak.enable = true;
|
||||||
|
|||||||
@@ -33,8 +33,9 @@ let
|
|||||||
echo "$NEW_PASS" > "$SECRET_FILE"
|
echo "$NEW_PASS" > "$SECRET_FILE"
|
||||||
chmod 600 "$SECRET_FILE"
|
chmod 600 "$SECRET_FILE"
|
||||||
echo "Password for 'free' updated and saved."
|
echo "Password for 'free' updated and saved."
|
||||||
echo "$NEW_PASS" | ${pkgs.gnome-keyring}/bin/gnome-keyring-daemon --unlock || echo "Warning: GNOME Keyring re-key failed." >&2
|
# Delete the old GNOME Keyring so it is recreated with the new password on next GDM login.
|
||||||
echo "GNOME Keyring re-keyed with new password."
|
rm -rf /home/free/.local/share/keyrings/*
|
||||||
|
echo "GNOME Keyring files cleared — a fresh keyring will be created on next login."
|
||||||
'';
|
'';
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
@@ -84,12 +85,28 @@ in
|
|||||||
Type = "oneshot";
|
Type = "oneshot";
|
||||||
RemainAfterExit = true;
|
RemainAfterExit = true;
|
||||||
};
|
};
|
||||||
path = [ pkgs.pwgen pkgs.shadow pkgs.coreutils ];
|
path = [ pkgs.shadow pkgs.coreutils ];
|
||||||
script = ''
|
script = ''
|
||||||
SECRET_FILE="/var/lib/secrets/root-password"
|
SECRET_FILE="/var/lib/secrets/root-password"
|
||||||
if [ ! -f "$SECRET_FILE" ]; then
|
if [ ! -f "$SECRET_FILE" ]; then
|
||||||
mkdir -p /var/lib/secrets
|
mkdir -p /var/lib/secrets
|
||||||
ROOT_PASS=$(pwgen -s 20 1)
|
# Generate a diceware-style passphrase: word-word-word-N
|
||||||
|
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"
|
||||||
|
WORD_ARRAY=($WORDS)
|
||||||
|
COUNT=''${#WORD_ARRAY[@]}
|
||||||
|
W1=''${WORD_ARRAY[$((RANDOM % COUNT))]}
|
||||||
|
W2=''${WORD_ARRAY[$((RANDOM % COUNT))]}
|
||||||
|
W3=''${WORD_ARRAY[$((RANDOM % COUNT))]}
|
||||||
|
DIGIT=$((RANDOM % 10))
|
||||||
|
ROOT_PASS="$W1-$W2-$W3-$DIGIT"
|
||||||
echo "root:$ROOT_PASS" | chpasswd
|
echo "root:$ROOT_PASS" | chpasswd
|
||||||
echo "$ROOT_PASS" > "$SECRET_FILE"
|
echo "$ROOT_PASS" > "$SECRET_FILE"
|
||||||
chmod 600 "$SECRET_FILE"
|
chmod 600 "$SECRET_FILE"
|
||||||
@@ -105,12 +122,28 @@ in
|
|||||||
Type = "oneshot";
|
Type = "oneshot";
|
||||||
RemainAfterExit = true;
|
RemainAfterExit = true;
|
||||||
};
|
};
|
||||||
path = [ pkgs.pwgen pkgs.shadow pkgs.coreutils ];
|
path = [ pkgs.shadow pkgs.coreutils ];
|
||||||
script = ''
|
script = ''
|
||||||
SECRET_FILE="/var/lib/secrets/free-password"
|
SECRET_FILE="/var/lib/secrets/free-password"
|
||||||
if [ ! -f "$SECRET_FILE" ]; then
|
if [ ! -f "$SECRET_FILE" ]; then
|
||||||
mkdir -p /var/lib/secrets
|
mkdir -p /var/lib/secrets
|
||||||
FREE_PASS=$(pwgen -s 20 1)
|
# Generate a diceware-style passphrase: word-word-word-N
|
||||||
|
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"
|
||||||
|
WORD_ARRAY=($WORDS)
|
||||||
|
COUNT=''${#WORD_ARRAY[@]}
|
||||||
|
W1=''${WORD_ARRAY[$((RANDOM % COUNT))]}
|
||||||
|
W2=''${WORD_ARRAY[$((RANDOM % COUNT))]}
|
||||||
|
W3=''${WORD_ARRAY[$((RANDOM % COUNT))]}
|
||||||
|
DIGIT=$((RANDOM % 10))
|
||||||
|
FREE_PASS="$W1-$W2-$W3-$DIGIT"
|
||||||
echo "free:$FREE_PASS" | chpasswd
|
echo "free:$FREE_PASS" | chpasswd
|
||||||
echo "$FREE_PASS" > "$SECRET_FILE"
|
echo "$FREE_PASS" > "$SECRET_FILE"
|
||||||
chmod 600 "$SECRET_FILE"
|
chmod 600 "$SECRET_FILE"
|
||||||
@@ -118,27 +151,4 @@ in
|
|||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
# ── 2. Unlock GNOME Keyring on graphical session start ─────
|
|
||||||
systemd.services.gnome-keyring-unlock = {
|
|
||||||
description = "Unlock GNOME Keyring with stored free password";
|
|
||||||
after = [ "free-password-setup.service" "display-manager.service" ];
|
|
||||||
wants = [ "free-password-setup.service" ];
|
|
||||||
wantedBy = [ "graphical-session.target" ];
|
|
||||||
serviceConfig = {
|
|
||||||
Type = "oneshot";
|
|
||||||
User = "free";
|
|
||||||
ExecStartPre = "${pkgs.coreutils}/bin/sleep 3";
|
|
||||||
};
|
|
||||||
path = [ pkgs.gnome-keyring pkgs.coreutils ];
|
|
||||||
script = ''
|
|
||||||
SECRET_FILE="/var/lib/secrets/free-password"
|
|
||||||
if [ -f "$SECRET_FILE" ]; then
|
|
||||||
gnome-keyring-daemon --unlock < "$SECRET_FILE"
|
|
||||||
echo "GNOME Keyring unlocked with stored password."
|
|
||||||
else
|
|
||||||
echo "No password file found, skipping keyring unlock."
|
|
||||||
fi
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user