{ config, lib, pkgs, ... }: let cfg = config.nixosWorkstations.workstationSetup; passwordHelper = pkgs.writeTextFile { name = "nixos-workstations-password-helper"; executable = true; text = '' #!${pkgs.expect}/bin/expect -f # Les secrets arrivent uniquement par stdin depuis l'application. # Ils ne sont jamais placés dans argv. La sortie du processus passwd # reste masquée : seuls des jetons techniques non sensibles sont renvoyés. log_user 0 exp_internal 0 set timeout 30 proc fail {token code} { puts stderr $token exit $code } if {[gets stdin current] < 0 || [gets stdin newpass] < 0 || [gets stdin confirm] < 0} { fail "INPUT_ERROR" 20 } if {$newpass ne $confirm} { fail "CONFIRM_MISMATCH" 21 } spawn -noecho ${pkgs.coreutils}/bin/env LC_ALL=C LANG=C /run/wrappers/bin/passwd # Étape 1 : authentification du mot de passe actuel. # Selon la pile PAM, l'invite peut être "Current password:", # "(current) UNIX password:" OU simplement "Password:". # Il ne faut donc pas dépendre uniquement des mots Current/Old/UNIX. expect { -re {(?i)(authentication failure|incorrect password|password unchanged|authentication token manipulation error)} { fail "CURRENT_REJECTED" 23 } # Si passwd passe directement au nouveau mot de passe, nous refusons : # l'assistant doit toujours vérifier le mot de passe temporaire actuel. -re {(?i)(new|retype|repeat|confirm)[^\r\n]*(password|passphrase)[^\r\n]*[:?]} { fail "CURRENT_PROMPT_MISSING" 22 } # Invite PAM générique, notamment "Password:". -re {(?i)(password|passphrase)[^\r\n]*[:?]} { send -- "$current\r" } eof { fail "EARLY_EOF_CURRENT" 24 } timeout { fail "TIMEOUT_CURRENT_PROMPT" 124 } } set current "" # Étape 2 : le nouveau mot de passe n'est envoyé qu'après validation # du mot de passe actuel par passwd/PAM. expect { -re {(?i)(authentication failure|incorrect password|password unchanged)} { fail "CURRENT_REJECTED" 25 } -re {(?i)new[^\r\n]*(password|passphrase)[^\r\n]*[:?]} { send -- "$newpass\r" } # Une invite générique "Password:" à ce stade est ambiguë : elle peut # être une nouvelle demande du mot de passe actuel. Par sécurité nous # ne tentons jamais une seconde authentification automatiquement. -re {(?i)(password|passphrase)[^\r\n]*[:?]} { fail "CURRENT_REPROMPT" 25 } eof { fail "EARLY_EOF_NEW" 26 } timeout { fail "TIMEOUT_NEW_PROMPT" 124 } } # Étape 3 : confirmation du nouveau mot de passe. expect { -re {(?i)(bad password|password unchanged|authentication token manipulation error)} { fail "NEW_REJECTED" 27 } -re {(?i)(retype|repeat|confirm)[^\r\n]*(password|passphrase)[^\r\n]*[:?]} { send -- "$confirm\r" } -re {(?i)new[^\r\n]*(password|passphrase)[^\r\n]*[:?]} { fail "NEW_REJECTED" 27 } eof { fail "EARLY_EOF_CONFIRM" 28 } timeout { fail "TIMEOUT_CONFIRM_PROMPT" 124 } } set newpass "" set confirm "" # Étape 4 : passwd doit maintenant terminer. Toute nouvelle invite de # mot de passe signifie que la modification n'a pas été acceptée. expect { eof {} -re {(?i)(password|passphrase)[^\r\n]*[:?]} { fail "UNEXPECTED_PASSWORD_REPROMPT" 29 } timeout { fail "TIMEOUT_FINISH" 124 } } set waitResult [wait] set exitCode [lindex $waitResult 3] if {$exitCode == 0} { puts "OK" exit 0 } fail "PASSWD_FAILED" $exitCode ''; }; pinHelper = pkgs.writeTextFile { name = "nixos-workstations-pin-helper"; executable = true; text = '' #!${pkgs.expect}/bin/expect -f # Une seule invocation de ykman par clic : aucune tentative automatique # supplémentaire n'est faite en cas de mauvais PIN. log_user 0 exp_internal 0 set timeout 30 proc fail {token code} { puts stderr $token exit $code } if {[gets stdin current] < 0 || [gets stdin newpin] < 0 || [gets stdin confirm] < 0} { fail "INPUT_ERROR" 30 } if {$newpin ne $confirm} { fail "CONFIRM_MISMATCH" 31 } spawn -noecho ${pkgs.coreutils}/bin/env LC_ALL=C LANG=C ${pkgs.yubikey-manager}/bin/ykman fido access change-pin set transcript "" # Une clé déjà provisionnée doit demander le PIN actuel. Si ykman passe # directement au nouveau PIN, la clé n'a pas le pré-provisionnement attendu. expect { -re {(?i)enter.*current.*pin.*:} { append transcript $expect_out(buffer) send -- "$current\r" } -re {(?i)enter.*new.*pin.*:} { append transcript $expect_out(buffer) fail "PIN_NOT_CONFIGURED" 32 } -re {(?i)(no yubikey|multiple yubikey|failed to connect|no fido|device.*not found|permission denied|access denied)} { append transcript $expect_out(buffer) fail "NO_YUBIKEY" 33 } eof { append transcript $expect_out(buffer) if {[regexp -nocase {no yubikey|multiple yubikey|failed to connect|no fido|device.*not found|permission denied|access denied} $transcript]} { fail "NO_YUBIKEY" 34 } fail "PIN_FAILED" 35 } timeout { fail "TIMEOUT_CURRENT_PIN" 124 } } set current "" expect { -re {(?i)enter.*new.*pin.*:} { append transcript $expect_out(buffer) send -- "$newpin\r" } eof { append transcript $expect_out(buffer) fail "PIN_FAILED" 36 } timeout { fail "TIMEOUT_NEW_PIN" 124 } } expect { -re {(?i)(repeat|confirm).*:} { append transcript $expect_out(buffer) send -- "$confirm\r" } eof { append transcript $expect_out(buffer) fail "PIN_FAILED" 37 } timeout { fail "TIMEOUT_CONFIRM_PIN" 124 } } set newpin "" set confirm "" expect eof append transcript $expect_out(buffer) set waitResult [wait] set exitCode [lindex $waitResult 3] if {$exitCode == 0} { puts "OK" exit 0 } if {[regexp -nocase {pin[_ ]auth[_ ]blocked|ctaperr_pin_auth_blocked|temporarily blocked} $transcript]} { fail "PIN_AUTH_BLOCKED" $exitCode } elseif {[regexp -nocase {pin[_ ]blocked|ctaperr_pin_blocked|pin is blocked} $transcript]} { fail "PIN_BLOCKED" $exitCode } elseif {[regexp -nocase {wrong pin|pin_invalid|pin auth invalid|pin verification failed|ctaperr_pin_invalid} $transcript]} { fail "WRONG_PIN" $exitCode } elseif {[regexp -nocase {no yubikey|multiple yubikey|failed to connect|no fido|device.*not found|permission denied|access denied} $transcript]} { fail "NO_YUBIKEY" $exitCode } elseif {[regexp -nocase {complexity|policy|minimum pin length|must be at least|too short} $transcript]} { fail "PIN_POLICY" $exitCode } fail "PIN_FAILED" $exitCode ''; }; workstationSetup = pkgs.stdenv.mkDerivation { pname = "nixos-workstations-setup"; version = "1.4.0"; 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}" ]; }; launcher = pkgs.writeShellScript "nixos-workstations-setup-launcher" '' set -eu current_user="$(${pkgs.coreutils}/bin/id -un)" target_user=${lib.escapeShellArg cfg.user} # L'autostart ne doit concerner que l'utilisateur configuré. if [ "$current_user" != "$target_user" ]; then exit 0 fi state_dir="''${XDG_STATE_HOME:-$HOME/.local/state}/nixos-workstations" password_marker="$state_dir/password-initialized" pin_marker="$state_dir/yubikey-pin-initialized" # Une session déjà finalisée n'affiche plus l'assistant. if [ -e "$password_marker" ] && [ -e "$pin_marker" ]; then exit 0 fi # Ne jamais générer de core dump contenant potentiellement un secret. ulimit -c 0 # Laisse Plasma terminer son démarrage. ${pkgs.coreutils}/bin/sleep 3 exec ${workstationSetup}/bin/nixos-workstations-setup \ --target-user "$target_user" ''; in { options.nixosWorkstations.workstationSetup = { enable = lib.mkEnableOption "assistant plein écran de finalisation du poste"; user = lib.mkOption { type = lib.types.str; default = "alice"; description = "Utilisateur devant effectuer la personnalisation initiale."; }; }; config = lib.mkIf cfg.enable { environment.systemPackages = [ workstationSetup ]; 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 ''; }; }