Files
2026-10-22 12:32:47 +02:00

143 lines
4.4 KiB
Python

"""Linux-Konten auf einer gemounteten Partition zuruecksetzen.
Arbeitet direkt auf ``<mnt>/etc/shadow``. Vor jeder Aenderung wird eine
Sicherungskopie ``shadow.puk.bak`` abgelegt. Passwort-Hashes werden lokal mit
``crypt`` (SHA-512) erzeugt.
"""
from __future__ import annotations
import os
import shutil
import subprocess
import time
from dataclasses import dataclass
try:
import crypt # entfernt in Python 3.13
_HAVE_CRYPT = True
except ImportError: # pragma: no cover
_HAVE_CRYPT = False
def _hash_password(password: str) -> str:
"""SHA-512 crypt-Hash erzeugen; Fallback ueber openssl, falls crypt fehlt."""
if _HAVE_CRYPT:
return crypt.crypt(password, crypt.mksalt(crypt.METHOD_SHA512))
res = subprocess.run(
["openssl", "passwd", "-6", "-stdin"],
input=password, capture_output=True, text=True, check=True,
)
return res.stdout.strip()
@dataclass
class LinuxUser:
name: str
uid: int
locked: bool
has_password: bool
shell: str
@property
def display(self) -> str:
flags = []
if self.locked:
flags.append("gesperrt")
if not self.has_password:
flags.append("kein PW")
tail = f" ({', '.join(flags)})" if flags else ""
return f"{self.name} uid={self.uid} {self.shell}{tail}"
def _shadow_path(mnt: str) -> str:
return os.path.join(mnt, "etc", "shadow")
def _passwd_path(mnt: str) -> str:
return os.path.join(mnt, "etc", "passwd")
def list_users(mnt: str) -> list[LinuxUser]:
"""Login-faehige Konten aus passwd/shadow lesen."""
shells: dict[str, tuple[int, str]] = {}
with open(_passwd_path(mnt), encoding="utf-8", errors="replace") as fh:
for line in fh:
f = line.rstrip("\n").split(":")
if len(f) >= 7:
shells[f[0]] = (int(f[2] or -1), f[6])
users: list[LinuxUser] = []
with open(_shadow_path(mnt), encoding="utf-8", errors="replace") as fh:
for line in fh:
f = line.rstrip("\n").split(":")
if len(f) < 2:
continue
name, pw = f[0], f[1]
uid, shell = shells.get(name, (-1, ""))
# System-/Dienstkonten ausblenden, echte Logins zeigen
is_login = (uid == 0) or (uid >= 1000) or shell.endswith(
("/bash", "/sh", "/zsh", "/fish"))
if not is_login:
continue
locked = pw.startswith("!") or pw.startswith("*")
has_pw = bool(pw) and not locked and pw not in ("!", "*", "!!")
users.append(LinuxUser(name, uid, locked, has_pw, shell))
users.sort(key=lambda u: (u.uid != 0, u.uid))
return users
def _rewrite(mnt: str, username: str, new_field: str) -> None:
path = _shadow_path(mnt)
shutil.copy2(path, path + ".puk.bak")
with open(path, encoding="utf-8", errors="replace") as fh:
lines = fh.readlines()
now_days = int(time.time() // 86400)
out = []
found = False
for line in lines:
f = line.rstrip("\n").split(":")
if f and f[0] == username:
found = True
f[1] = new_field
# Feld 3 = letzte Aenderung; aktualisieren, Rest auffuellen
while len(f) < 9:
f.append("")
f[2] = str(now_days)
out.append(":".join(f) + "\n")
else:
out.append(line)
if not found:
raise RuntimeError(f"Benutzer {username} nicht in shadow gefunden")
tmp = path + ".puk.tmp"
with open(tmp, "w", encoding="utf-8") as fh:
fh.writelines(out)
os.chmod(tmp, 0o640)
os.replace(tmp, path)
def blank_password(mnt: str, username: str) -> None:
"""Passwortfeld leeren -> Login ohne Passwort moeglich."""
_rewrite(mnt, username, "")
def set_password(mnt: str, username: str, password: str) -> None:
"""Neues SHA-512-Passwort setzen."""
_rewrite(mnt, username, _hash_password(password))
def unlock(mnt: str, username: str) -> None:
"""Fuehrendes ! / * entfernen (Konto entsperren) ohne Passwort zu aendern."""
path = _shadow_path(mnt)
with open(path, encoding="utf-8", errors="replace") as fh:
lines = fh.readlines()
for line in lines:
f = line.rstrip("\n").split(":")
if f and f[0] == username:
pw = f[1].lstrip("!*")
_rewrite(mnt, username, pw)
return
raise RuntimeError(f"Benutzer {username} nicht gefunden")