{ config, lib, pkgs, ... }: let cfg = config.nixosWorkstations.workstationSetup; passwordHelper = pkgs.writeShellScript "nixos-workstations-homed-password-helper" '' set -eu umask 077 fail() { printf '%s\n' "$1" >&2 exit "$2" } IFS= read -r current || fail INPUT_ERROR 20 IFS= read -r newpass || fail INPUT_ERROR 20 IFS= read -r confirm || fail INPUT_ERROR 20 [ "$newpass" = "$confirm" ] || fail CONFIRM_MISMATCH 21 [ -n "$current" ] && [ -n "$newpass" ] || fail INPUT_ERROR 20 runtime_dir="''${XDG_RUNTIME_DIR:-/run/user/$(${pkgs.coreutils}/bin/id -u)}" cred_dir="$(${pkgs.coreutils}/bin/mktemp -d "$runtime_dir/nixos-workstations-passwd.XXXXXX")" log_file="$cred_dir/homectl.log" cleanup() { current="" newpass="" confirm="" ${pkgs.coreutils}/bin/rm -f \ "$cred_dir/home.password" \ "$cred_dir/home.new-password" \ "$log_file" 2>/dev/null || true ${pkgs.coreutils}/bin/rmdir "$cred_dir" 2>/dev/null || true } trap cleanup EXIT HUP INT TERM ${pkgs.coreutils}/bin/printf '%s' "$current" > "$cred_dir/home.password" ${pkgs.coreutils}/bin/printf '%s' "$newpass" > "$cred_dir/home.new-password" ${pkgs.coreutils}/bin/chmod 600 "$cred_dir/home.password" "$cred_dir/home.new-password" user="$(${pkgs.coreutils}/bin/id -un)" current="" newpass="" confirm="" set +e CREDENTIALS_DIRECTORY="$cred_dir" \ LC_ALL=C LANG=C \ ${pkgs.coreutils}/bin/timeout 60 \ ${pkgs.systemd}/bin/homectl --no-ask-password --no-pager passwd "$user" \ >"$log_file" 2>&1 rc=$? set -e if [ "$rc" -eq 0 ]; then printf '%s\n' OK exit 0 fi if [ "$rc" -eq 124 ]; then fail TIMEOUT_PASSWORD 124 fi if ${pkgs.gnugrep}/bin/grep -Eqi 'password incorrect|not sufficient|bad password' "$log_file"; then fail CURRENT_REJECTED 22 fi if ${pkgs.gnugrep}/bin/grep -Eqi 'quality|too short|weak|dictionary' "$log_file"; then fail NEW_REJECTED 23 fi fail HOMECTL_PASSWD_FAILED 24 ''; pinPython = pkgs.python3.withPackages (ps: [ ps.fido2 ]); pinHelper = pkgs.writeTextFile { name = "nixos-workstations-pin-helper"; executable = true; text = '' #!${pinPython}/bin/python3 import sys from fido2.ctap import CtapError from fido2.ctap2 import Ctap2 from fido2.ctap2.pin import ClientPin from fido2.hid import CAPABILITY, CtapHidDevice def fail(token: str, code: int) -> "None": print(token, file=sys.stderr, flush=True) raise SystemExit(code) def read_secret() -> str: value = sys.stdin.readline() if value == "": fail("INPUT_ERROR", 30) return value.rstrip("\\r\\n") new_pin = read_secret() confirmation = read_secret() if new_pin != confirmation: fail("CONFIRM_MISMATCH", 31) if len(new_pin) < 4 or len(new_pin.encode("utf-8")) > 63: fail("PIN_POLICY", 32) devices = [] try: devices = list(CtapHidDevice.list_devices()) if len(devices) == 0: fail("NO_YUBIKEY", 33) if len(devices) > 1: fail("MULTIPLE_YUBIKEY", 34) device = devices[0] if not (device.capabilities & CAPABILITY.CBOR): fail("NO_FIDO2", 35) ctap = Ctap2(device) if ctap.get_info().options.get("clientPin", False): fail("PIN_ALREADY_CONFIGURED", 36) ClientPin(ctap).set_pin(new_pin) if not ctap.get_info().options.get("clientPin", False): fail("PIN_STATE_NOT_UPDATED", 37) new_pin = "" confirmation = "" print("OK", flush=True) raise SystemExit(0) except CtapError as exc: code = exc.code if code == CtapError.ERR.PIN_AUTH_BLOCKED: fail("PIN_AUTH_BLOCKED", 41) if code == CtapError.ERR.PIN_BLOCKED: fail("PIN_BLOCKED", 42) if code == CtapError.ERR.PIN_POLICY_VIOLATION: fail("PIN_POLICY", 44) fail("PIN_FAILED", 46) except (PermissionError, OSError): fail("NO_YUBIKEY", 47) except ValueError: fail("PIN_POLICY", 48) except SystemExit: raise except Exception: fail("PIN_HELPER_ERROR", 49) finally: new_pin = "" confirmation = "" for dev in devices: try: dev.close() except Exception: pass ''; }; fidoEnrollHelper = pkgs.writeShellScript "nixos-workstations-homed-fido-helper" '' set -eu umask 077 fail() { printf '%s\n' "$1" >&2 exit "$2" } IFS= read -r current_password || fail INPUT_ERROR 50 IFS= read -r token_pin || fail INPUT_ERROR 50 [ -n "$current_password" ] && [ -n "$token_pin" ] || fail INPUT_ERROR 50 runtime_dir="''${XDG_RUNTIME_DIR:-/run/user/$(${pkgs.coreutils}/bin/id -u)}" cred_dir="$(${pkgs.coreutils}/bin/mktemp -d "$runtime_dir/nixos-workstations-fido.XXXXXX")" log_file="$cred_dir/homectl.log" cleanup() { current_password="" token_pin="" ${pkgs.coreutils}/bin/rm -f \ "$cred_dir/home.password" \ "$cred_dir/home.token-pin" \ "$log_file" 2>/dev/null || true ${pkgs.coreutils}/bin/rmdir "$cred_dir" 2>/dev/null || true } trap cleanup EXIT HUP INT TERM ${pkgs.coreutils}/bin/printf '%s' "$current_password" > "$cred_dir/home.password" ${pkgs.coreutils}/bin/printf '%s' "$token_pin" > "$cred_dir/home.token-pin" ${pkgs.coreutils}/bin/chmod 600 "$cred_dir/home.password" "$cred_dir/home.token-pin" user="$(${pkgs.coreutils}/bin/id -un)" current_password="" token_pin="" set +e CREDENTIALS_DIRECTORY="$cred_dir" \ LC_ALL=C LANG=C \ ${pkgs.coreutils}/bin/timeout 120 \ ${pkgs.systemd}/bin/homectl --no-ask-password --no-pager update "$user" \ --fido2-device=auto \ --fido2-with-client-pin=yes \ --fido2-with-user-presence=yes \ --fido2-with-user-verification=no \ >"$log_file" 2>&1 rc=$? set -e if [ "$rc" -eq 0 ]; then printf '%s\n' OK exit 0 fi if [ "$rc" -eq 124 ]; then fail TIMEOUT_FIDO 124 fi if ${pkgs.gnugrep}/bin/grep -Eqi 'PIN.*incorrect|Bad PIN|bad pin' "$log_file"; then fail FIDO_BAD_PIN 51 fi if ${pkgs.gnugrep}/bin/grep -Eqi 'password.*incorrect|not sufficient|BadPassword' "$log_file"; then fail FIDO_BAD_PASSWORD 52 fi if ${pkgs.gnugrep}/bin/grep -Eqi 'multiple|more than one.*FIDO|auto.*device' "$log_file"; then fail MULTIPLE_YUBIKEY 53 fi if ${pkgs.gnugrep}/bin/grep -Eqi 'no.*FIDO|No such device|not found|not inserted' "$log_file"; then fail NO_YUBIKEY 54 fi fail FIDO_ENROLL_FAILED 55 ''; workstationSetup = pkgs.stdenv.mkDerivation { pname = "nixos-workstations-setup"; version = "1.8.1"; src = ../workstation-setup; nativeBuildInputs = [ pkgs.cmake pkgs.ninja pkgs.pkg-config pkgs.kdePackages.wrapQtAppsHook ]; buildInputs = with pkgs.kdePackages; [ qtbase qtdeclarative qtwayland kirigami ]; cmakeFlags = [ "-DPASSWORD_HELPER_PATH=${passwordHelper}" "-DPIN_HELPER_PATH=${pinHelper}" "-DFIDO_HELPER_PATH=${fidoEnrollHelper}" ]; }; launcher = pkgs.writeShellScript "nixos-workstations-setup-launcher" '' set -eu current_user="$(${pkgs.coreutils}/bin/id -un)" target_user=${lib.escapeShellArg cfg.user} [ "$current_user" = "$target_user" ] || exit 0 state_dir="''${XDG_STATE_HOME:-$HOME/.local/state}/nixos-workstations" password_marker="$state_dir/password-initialized" pin_marker="$state_dir/yubikey-pin-created" fido_marker="$state_dir/yubikey-fido-enrolled" if [ -e "$password_marker" ] && [ -e "$pin_marker" ] && [ -e "$fido_marker" ]; then exit 0 fi ulimit -c 0 ${pkgs.coreutils}/bin/sleep 3 exec ${workstationSetup}/bin/nixos-workstations-setup \ --target-user "$target_user" ''; in { options.nixosWorkstations.workstationSetup = { enable = lib.mkEnableOption "assistant de première session"; user = lib.mkOption { type = lib.types.str; default = "alice"; description = "Utilisateur devant finaliser son authentification."; }; }; config = lib.mkIf cfg.enable { environment.systemPackages = [ workstationSetup pkgs.systemd ]; environment.etc."xdg/autostart/nixos-workstations-setup.desktop".text = '' [Desktop Entry] Type=Application Name=Finalisation du poste Comment=Personnalisation sécurisée des moyens d'authentification Exec=${launcher} OnlyShowIn=KDE; NoDisplay=true StartupNotify=false ''; }; }