Téléverser les fichiers vers "workstation-setup/src"

This commit is contained in:
2026-08-18 17:33:33 +02:00
parent 6ff336493c
commit 45cd0f1e57
5 changed files with 1930 additions and 0 deletions
+33
View File
@@ -0,0 +1,33 @@
qt_add_executable(nixos-workstations-setup
main.cpp
ProvisioningBackend.cpp
ProvisioningBackend.h
)
qt_add_qml_module(nixos-workstations-setup
URI org.raspot.nixosworkstations.setup
VERSION 1.0
QML_FILES
Main.qml
)
target_compile_definitions(nixos-workstations-setup
PRIVATE
PASSWORD_HELPER_PATH="${PASSWORD_HELPER_PATH}"
PIN_HELPER_PATH="${PIN_HELPER_PATH}"
TARGET_USER="${TARGET_USER}"
)
target_link_libraries(nixos-workstations-setup
PRIVATE
Qt6::Core
Qt6::Gui
Qt6::Qml
Qt6::Quick
Qt6::QuickControls2
)
install(
TARGETS nixos-workstations-setup
RUNTIME DESTINATION bin
)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,435 @@
#include "ProvisioningBackend.h"
#include <QDateTime>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QProcessEnvironment>
#include <QTimer>
#include <utility>
#include <pwd.h>
#include <sys/types.h>
#include <unistd.h>
#ifndef PASSWORD_HELPER_PATH
#error PASSWORD_HELPER_PATH must be defined
#endif
#ifndef PIN_HELPER_PATH
#error PIN_HELPER_PATH must be defined
#endif
#ifndef TARGET_USER
#error TARGET_USER must be defined
#endif
namespace {
constexpr int kHelperTimeoutMs = 60000;
bool containsLineBreak(const QString &value)
{
return value.contains(QLatin1Char('\n')) || value.contains(QLatin1Char('\r'));
}
}
ProvisioningBackend::ProvisioningBackend(QObject *parent)
: QObject(parent)
, m_currentUser(effectiveUserName())
, m_targetUser(QStringLiteral(TARGET_USER))
, m_authorizedUser(m_currentUser == m_targetUser)
{
refreshState();
}
QString ProvisioningBackend::effectiveUserName()
{
const uid_t uid = geteuid();
if (passwd *entry = getpwuid(uid)) {
return QString::fromLocal8Bit(entry->pw_name);
}
return {};
}
QString ProvisioningBackend::stateDirectory() const
{
const QByteArray xdgState = qgetenv("XDG_STATE_HOME");
if (!xdgState.isEmpty()) {
return QString::fromLocal8Bit(xdgState)
+ QStringLiteral("/nixos-workstations");
}
if (passwd *entry = getpwuid(geteuid())) {
return QString::fromLocal8Bit(entry->pw_dir)
+ QStringLiteral("/.local/state/nixos-workstations");
}
return QDir::homePath() + QStringLiteral("/.local/state/nixos-workstations");
}
QString ProvisioningBackend::passwordMarker() const
{
return stateDirectory() + QStringLiteral("/password-initialized");
}
QString ProvisioningBackend::pinMarker() const
{
return stateDirectory() + QStringLiteral("/yubikey-pin-initialized");
}
void ProvisioningBackend::refreshState()
{
const bool oldPassword = m_passwordDone;
const bool oldPin = m_pinDone;
m_passwordDone = QFileInfo::exists(passwordMarker());
m_pinDone = QFileInfo::exists(pinMarker());
if (oldPassword != m_passwordDone || oldPin != m_pinDone) {
emit stateChanged();
}
}
bool ProvisioningBackend::ensureStateWritable()
{
const QString dir = stateDirectory();
if (!QDir().mkpath(dir)) {
return false;
}
const QString probePath = dir + QStringLiteral("/.write-test");
QFile probe(probePath);
if (!probe.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
return false;
}
probe.write("ok\n");
probe.close();
probe.remove();
return true;
}
bool ProvisioningBackend::createMarker(const QString &path)
{
const QFileInfo info(path);
if (!QDir().mkpath(info.absolutePath())) {
return false;
}
QFile marker(path);
if (!marker.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
return false;
}
const QByteArray content =
QByteArrayLiteral("completed=")
+ QDateTime::currentDateTimeUtc().toString(Qt::ISODate).toUtf8()
+ '\n';
if (marker.write(content) != content.size()) {
marker.close();
return false;
}
marker.close();
marker.setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner);
return true;
}
void ProvisioningBackend::setBusy(bool busy)
{
if (m_busy == busy) {
return;
}
m_busy = busy;
emit busyChanged();
}
void ProvisioningBackend::secureClear(QByteArray &data)
{
volatile char *p = data.data();
for (qsizetype i = 0; i < data.size(); ++i) {
p[i] = 0;
}
data.clear();
}
void ProvisioningBackend::changePassword(const QString &currentPassword,
const QString &newPassword,
const QString &confirmation)
{
if (!m_authorizedUser) {
emit passwordChangeFinished(false,
QStringLiteral("Cette opération n'est pas autorisée pour cet utilisateur."));
return;
}
if (m_busy) {
return;
}
if (m_passwordDone) {
emit passwordChangeFinished(true,
QStringLiteral("Le mot de passe a déjà été personnalisé."));
return;
}
if (currentPassword.isEmpty() || newPassword.isEmpty() || confirmation.isEmpty()) {
emit passwordChangeFinished(false,
QStringLiteral("Tous les champs du mot de passe doivent être renseignés."));
return;
}
if (newPassword != confirmation) {
emit passwordChangeFinished(false,
QStringLiteral("Les deux nouveaux mots de passe ne correspondent pas."));
return;
}
// Les helpers utilisent un protocole interne délimité par des retours ligne.
// Un secret interactif passwd/FIDO2 ne doit donc jamais en contenir.
if (containsLineBreak(currentPassword) || containsLineBreak(newPassword)
|| containsLineBreak(confirmation)) {
emit passwordChangeFinished(false,
QStringLiteral("Le mot de passe contient un caractère non pris en charge."));
return;
}
// Vérifie avant toute modification que l'état de provisioning pourra
// effectivement être persisté dans le HOME de l'utilisateur.
if (!ensureStateWritable()) {
emit passwordChangeFinished(false,
QStringLiteral("Impossible d'enregistrer l'état de configuration dans votre profil. Contactez le service informatique."));
return;
}
startHelper(Operation::Password,
QStringLiteral(PASSWORD_HELPER_PATH),
currentPassword,
newPassword,
confirmation);
}
void ProvisioningBackend::changePin(const QString &currentPin,
const QString &newPin,
const QString &confirmation)
{
if (!m_authorizedUser) {
emit pinChangeFinished(false,
QStringLiteral("Cette opération n'est pas autorisée pour cet utilisateur."));
return;
}
if (m_busy) {
return;
}
if (!m_passwordDone) {
emit pinChangeFinished(false,
QStringLiteral("Le mot de passe doit être personnalisé avant le PIN YubiKey."));
return;
}
if (m_pinDone) {
emit pinChangeFinished(true,
QStringLiteral("Le PIN YubiKey a déjà été personnalisé."));
return;
}
if (currentPin.size() < 4 || newPin.size() < 4 || confirmation.size() < 4) {
emit pinChangeFinished(false,
QStringLiteral("Les PIN doivent contenir au moins 4 caractères."));
return;
}
if (newPin != confirmation) {
emit pinChangeFinished(false,
QStringLiteral("Les deux nouveaux PIN ne correspondent pas."));
return;
}
if (containsLineBreak(currentPin) || containsLineBreak(newPin)
|| containsLineBreak(confirmation)) {
emit pinChangeFinished(false,
QStringLiteral("Le PIN contient un caractère non pris en charge."));
return;
}
if (!ensureStateWritable()) {
emit pinChangeFinished(false,
QStringLiteral("Impossible d'enregistrer l'état de configuration dans votre profil. Contactez le service informatique."));
return;
}
startHelper(Operation::Pin,
QStringLiteral(PIN_HELPER_PATH),
currentPin,
newPin,
confirmation);
}
void ProvisioningBackend::startHelper(Operation operation,
const QString &helperPath,
const QString &firstSecret,
const QString &secondSecret,
const QString &thirdSecret)
{
if (!QFileInfo::exists(helperPath)) {
const QString message = QStringLiteral("Le composant système requis est introuvable.");
if (operation == Operation::Password) {
emit passwordChangeFinished(false, message);
} else {
emit pinChangeFinished(false, message);
}
return;
}
m_operation = operation;
setBusy(true);
m_process = new QProcess(this);
m_process->setProgram(helperPath);
m_process->setProcessChannelMode(QProcess::SeparateChannels);
QProcessEnvironment environment = QProcessEnvironment::systemEnvironment();
environment.insert(QStringLiteral("LC_ALL"), QStringLiteral("C"));
environment.insert(QStringLiteral("LANG"), QStringLiteral("C"));
m_process->setProcessEnvironment(environment);
auto *timeout = new QTimer(m_process);
timeout->setSingleShot(true);
timeout->setInterval(kHelperTimeoutMs);
connect(timeout, &QTimer::timeout, m_process, [process = m_process]() {
if (process->state() != QProcess::NotRunning) {
process->kill();
}
});
QByteArray first = firstSecret.toUtf8();
QByteArray second = secondSecret.toUtf8();
QByteArray third = thirdSecret.toUtf8();
connect(m_process, &QProcess::started, this,
[this, first = std::move(first), second = std::move(second),
third = std::move(third), timeout]() mutable {
QByteArray payload;
payload.reserve(first.size() + second.size() + third.size() + 3);
payload += first;
payload += '\n';
payload += second;
payload += '\n';
payload += third;
payload += '\n';
m_process->write(payload);
m_process->closeWriteChannel();
// Réduit la durée de vie des copies explicites côté backend.
secureClear(first);
secureClear(second);
secureClear(third);
secureClear(payload);
timeout->start();
});
connect(m_process, &QProcess::errorOccurred, this,
[this](QProcess::ProcessError error) {
if (error != QProcess::FailedToStart) {
return;
}
setBusy(false);
const QString message = QStringLiteral("Impossible de démarrer le composant de configuration.");
if (m_operation == Operation::Password) {
emit passwordChangeFinished(false, message);
} else {
emit pinChangeFinished(false, message);
}
});
connect(m_process,
qOverload<int, QProcess::ExitStatus>(&QProcess::finished),
this,
[this, timeout](int exitCode, QProcess::ExitStatus exitStatus) {
timeout->stop();
const QByteArray stderrData = m_process->readAllStandardError();
const QByteArray stdoutData = m_process->readAllStandardOutput();
const bool success = exitStatus == QProcess::NormalExit && exitCode == 0;
const Operation operation = m_operation;
QString message;
bool markerCreated = false;
if (success) {
if (operation == Operation::Password) {
markerCreated = createMarker(passwordMarker());
message = markerCreated
? QStringLiteral("Votre mot de passe personnel est maintenant actif.")
: QStringLiteral("Le mot de passe a été modifié, mais l'état local n'a pas pu être enregistré. Contactez le service informatique avant de fermer la session.");
} else {
markerCreated = createMarker(pinMarker());
message = markerCreated
? QStringLiteral("Le PIN personnel de votre YubiKey est maintenant actif.")
: QStringLiteral("Le PIN a été modifié, mais l'état local n'a pas pu être enregistré. Contactez le service informatique avant de fermer la session.");
}
} else {
message = safeMessageForFailure(operation, exitCode, stderrData + stdoutData);
}
m_process->deleteLater();
m_process = nullptr;
setBusy(false);
refreshState();
const bool completed = success && markerCreated;
if (operation == Operation::Password) {
emit passwordChangeFinished(completed, message);
} else {
emit pinChangeFinished(completed, message);
}
});
m_process->start();
}
QString ProvisioningBackend::safeMessageForFailure(Operation operation,
int exitCode,
const QByteArray &stderrData) const
{
const QString output = QString::fromUtf8(stderrData).toLower();
if (exitCode == 124 || output.contains(QStringLiteral("timeout"))) {
return QStringLiteral("L'opération a expiré. Vérifiez le périphérique puis réessayez.");
}
if (operation == Operation::Password) {
if (output.contains(QStringLiteral("current_rejected"))) {
return QStringLiteral("Le mot de passe temporaire actuel est incorrect.");
}
if (output.contains(QStringLiteral("new_rejected"))) {
return QStringLiteral("Le système a refusé le nouveau mot de passe. Choisissez-en un autre puis réessayez.");
}
return QStringLiteral("Le mot de passe n'a pas été modifié. Vérifiez le mot de passe actuel puis réessayez.");
}
if (output.contains(QStringLiteral("wrong_pin"))) {
return QStringLiteral("Le PIN temporaire est incorrect. N'essayez pas au hasard : le nombre de tentatives FIDO2 est limité.");
}
if (output.contains(QStringLiteral("pin_auth_blocked"))) {
return QStringLiteral("Les tentatives de PIN sont temporairement bloquées. Débranchez puis rebranchez la YubiKey avant de réessayer avec le bon PIN.");
}
if (output.contains(QStringLiteral("pin_blocked"))) {
return QStringLiteral("Le PIN FIDO2 de la YubiKey est bloqué. Contactez le service informatique ; ne réinitialisez pas la clé.");
}
if (output.contains(QStringLiteral("no_yubikey"))) {
return QStringLiteral("Aucune YubiKey compatible n'a été détectée. Branchez la clé puis réessayez.");
}
if (output.contains(QStringLiteral("pin_policy"))) {
return QStringLiteral("Le nouveau PIN ne respecte pas la politique configurée sur cette YubiKey.");
}
return QStringLiteral("Le PIN YubiKey n'a pas été modifié. Vérifiez que la clé est branchée et que le PIN actuel est correct.");
}
@@ -0,0 +1,78 @@
#pragma once
#include <QObject>
#include <QProcess>
#include <QString>
class ProvisioningBackend final : public QObject
{
Q_OBJECT
Q_PROPERTY(bool busy READ busy NOTIFY busyChanged)
Q_PROPERTY(bool passwordDone READ passwordDone NOTIFY stateChanged)
Q_PROPERTY(bool pinDone READ pinDone NOTIFY stateChanged)
Q_PROPERTY(bool complete READ complete NOTIFY stateChanged)
Q_PROPERTY(QString currentUser READ currentUser CONSTANT)
Q_PROPERTY(QString targetUser READ targetUser CONSTANT)
Q_PROPERTY(bool authorizedUser READ authorizedUser CONSTANT)
public:
explicit ProvisioningBackend(QObject *parent = nullptr);
bool busy() const { return m_busy; }
bool passwordDone() const { return m_passwordDone; }
bool pinDone() const { return m_pinDone; }
bool complete() const { return m_passwordDone && m_pinDone; }
QString currentUser() const { return m_currentUser; }
QString targetUser() const { return m_targetUser; }
bool authorizedUser() const { return m_authorizedUser; }
Q_INVOKABLE void changePassword(const QString &currentPassword,
const QString &newPassword,
const QString &confirmation);
Q_INVOKABLE void changePin(const QString &currentPin,
const QString &newPin,
const QString &confirmation);
signals:
void busyChanged();
void stateChanged();
void passwordChangeFinished(bool success, const QString &message);
void pinChangeFinished(bool success, const QString &message);
private:
enum class Operation {
Password,
Pin
};
void startHelper(Operation operation,
const QString &helperPath,
const QString &firstSecret,
const QString &secondSecret,
const QString &thirdSecret);
void setBusy(bool busy);
void refreshState();
bool ensureStateWritable();
bool createMarker(const QString &path);
QString stateDirectory() const;
QString passwordMarker() const;
QString pinMarker() const;
QString safeMessageForFailure(Operation operation,
int exitCode,
const QByteArray &stderrData) const;
static QString effectiveUserName();
static void secureClear(QByteArray &data);
bool m_busy = false;
bool m_passwordDone = false;
bool m_pinDone = false;
bool m_authorizedUser = false;
QString m_currentUser;
QString m_targetUser;
QProcess *m_process = nullptr;
Operation m_operation = Operation::Password;
};
+53
View File
@@ -0,0 +1,53 @@
#include "ProvisioningBackend.h"
#include <QGuiApplication>
#include <QIcon>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QQuickStyle>
#include <sys/resource.h>
int main(int argc, char *argv[])
{
// Les champs contiennent temporairement des secrets : pas de core dump.
rlimit coreLimit { 0, 0 };
setrlimit(RLIMIT_CORE, &coreLimit);
QGuiApplication app(argc, argv);
QGuiApplication::setOrganizationName(QStringLiteral("Raspot"));
QGuiApplication::setOrganizationDomain(QStringLiteral("raspot.in"));
QGuiApplication::setApplicationName(QStringLiteral("NixOS Workstations Setup"));
QGuiApplication::setDesktopFileName(QStringLiteral("org.raspot.nixosworkstations.setup"));
QIcon::setThemeName(QStringLiteral("breeze"));
if (qEnvironmentVariableIsEmpty("QT_QUICK_CONTROLS_STYLE")) {
QQuickStyle::setStyle(QStringLiteral("Basic"));
}
ProvisioningBackend provisioning;
// Défense en profondeur : le lanceur filtre déjà l'utilisateur, mais
// le backend refuse également toute opération pour un autre compte.
if (!provisioning.authorizedUser()) {
return 0;
}
// Si les deux marqueurs existent, l'assistant n'a plus rien à faire.
if (provisioning.complete()) {
return 0;
}
QQmlApplicationEngine engine;
engine.rootContext()->setContextProperty(QStringLiteral("provisioning"), &provisioning);
engine.loadFromModule(QStringLiteral("org.raspot.nixosworkstations.setup"),
QStringLiteral("Main"));
if (engine.rootObjects().isEmpty()) {
return 1;
}
return app.exec();
}