diff --git a/workstation-setup/src/ProvisioningBackend.cpp b/workstation-setup/src/ProvisioningBackend.cpp index bcac72c..48e4b56 100644 --- a/workstation-setup/src/ProvisioningBackend.cpp +++ b/workstation-setup/src/ProvisioningBackend.cpp @@ -1,29 +1,19 @@ #include "ProvisioningBackend.h" +#include "Config.h" #include #include #include #include #include +#include #include -#include - #include #include #include -#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 +#include namespace { constexpr int kHelperTimeoutMs = 60000; @@ -32,15 +22,38 @@ bool containsLineBreak(const QString &value) { return value.contains(QLatin1Char('\n')) || value.contains(QLatin1Char('\r')); } + +QString homeDirectoryForEffectiveUser() +{ + if (passwd *entry = getpwuid(geteuid())) { + return QString::fromLocal8Bit(entry->pw_dir); + } + return QDir::homePath(); +} } -ProvisioningBackend::ProvisioningBackend(QObject *parent) +ProvisioningBackend::ProvisioningBackend(QString targetUser, QObject *parent) : QObject(parent) , m_currentUser(effectiveUserName()) - , m_targetUser(QStringLiteral(TARGET_USER)) - , m_authorizedUser(m_currentUser == m_targetUser) + , m_targetUser(std::move(targetUser)) + , m_authorizedUser(!m_targetUser.isEmpty() && m_currentUser == m_targetUser) { refreshState(); + appendLog(QStringLiteral("START currentUser=%1 targetUser=%2 authorized=%3 passwordDone=%4 pinDone=%5") + .arg(m_currentUser, + m_targetUser, + m_authorizedUser ? QStringLiteral("yes") : QStringLiteral("no"), + m_passwordDone ? QStringLiteral("yes") : QStringLiteral("no"), + m_pinDone ? QStringLiteral("yes") : QStringLiteral("no"))); +} + +ProvisioningBackend::~ProvisioningBackend() +{ + secureClear(m_pendingPayload); + if (m_process && m_process->state() != QProcess::NotRunning) { + m_process->kill(); + m_process->waitForFinished(1000); + } } QString ProvisioningBackend::effectiveUserName() @@ -56,16 +69,19 @@ QString ProvisioningBackend::stateDirectory() const { const QByteArray xdgState = qgetenv("XDG_STATE_HOME"); if (!xdgState.isEmpty()) { - return QString::fromLocal8Bit(xdgState) - + QStringLiteral("/nixos-workstations"); + const QString candidate = QString::fromLocal8Bit(xdgState); + if (QDir::isAbsolutePath(candidate)) { + return QDir::cleanPath(candidate + QStringLiteral("/nixos-workstations")); + } } - if (passwd *entry = getpwuid(geteuid())) { - return QString::fromLocal8Bit(entry->pw_dir) - + QStringLiteral("/.local/state/nixos-workstations"); - } + return QDir::cleanPath(homeDirectoryForEffectiveUser() + + QStringLiteral("/.local/state/nixos-workstations")); +} - return QDir::homePath() + QStringLiteral("/.local/state/nixos-workstations"); +QString ProvisioningBackend::diagnosticLogPath() const +{ + return stateDirectory() + QStringLiteral("/setup.log"); } QString ProvisioningBackend::passwordMarker() const @@ -91,7 +107,6 @@ void ProvisioningBackend::refreshState() } } - bool ProvisioningBackend::ensureStateWritable() { const QString dir = stateDirectory(); @@ -99,26 +114,31 @@ bool ProvisioningBackend::ensureStateWritable() return false; } + QFile::setPermissions(dir, + QFileDevice::ReadOwner + | QFileDevice::WriteOwner + | QFileDevice::ExeOwner); + const QString probePath = dir + QStringLiteral("/.write-test"); QFile probe(probePath); if (!probe.open(QIODevice::WriteOnly | QIODevice::Truncate)) { return false; } - probe.write("ok\n"); + + const bool writeOk = probe.write("ok\n") == 3; probe.close(); probe.remove(); - return true; + return writeOk; } bool ProvisioningBackend::createMarker(const QString &path) { - const QFileInfo info(path); - if (!QDir().mkpath(info.absolutePath())) { + if (!ensureStateWritable()) { return false; } - QFile marker(path); - if (!marker.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + QSaveFile marker(path); + if (!marker.open(QIODevice::WriteOnly)) { return false; } @@ -128,13 +148,47 @@ bool ProvisioningBackend::createMarker(const QString &path) + '\n'; if (marker.write(content) != content.size()) { - marker.close(); + marker.cancelWriting(); return false; } - marker.close(); - marker.setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner); - return true; + if (!marker.commit()) { + return false; + } + + return QFile::setPermissions(path, + QFileDevice::ReadOwner + | QFileDevice::WriteOwner); +} + +void ProvisioningBackend::appendLog(const QString &message) const +{ + const QString dir = stateDirectory(); + if (!QDir().mkpath(dir)) { + return; + } + + QFile::setPermissions(dir, + QFileDevice::ReadOwner + | QFileDevice::WriteOwner + | QFileDevice::ExeOwner); + + QFile log(diagnosticLogPath()); + if (!log.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Text)) { + return; + } + + const QByteArray line = + QDateTime::currentDateTimeUtc().toString(Qt::ISODateWithMs).toUtf8() + + QByteArrayLiteral(" ") + + message.toUtf8() + + '\n'; + + log.write(line); + log.close(); + QFile::setPermissions(diagnosticLogPath(), + QFileDevice::ReadOwner + | QFileDevice::WriteOwner); } void ProvisioningBackend::setBusy(bool busy) @@ -148,11 +202,21 @@ void ProvisioningBackend::setBusy(bool busy) void ProvisioningBackend::secureClear(QByteArray &data) { - volatile char *p = data.data(); - for (qsizetype i = 0; i < data.size(); ++i) { - p[i] = 0; + if (!data.isEmpty()) { + volatile char *p = data.data(); + for (qsizetype i = 0; i < data.size(); ++i) { + p[i] = 0; + } } data.clear(); + data.squeeze(); +} + +QString ProvisioningBackend::operationName(Operation operation) +{ + return operation == Operation::Password + ? QStringLiteral("PASSWORD") + : QStringLiteral("PIN"); } void ProvisioningBackend::changePassword(const QString ¤tPassword, @@ -160,12 +224,15 @@ void ProvisioningBackend::changePassword(const QString ¤tPassword, const QString &confirmation) { if (!m_authorizedUser) { + appendLog(QStringLiteral("PASSWORD refused: unauthorized user")); emit passwordChangeFinished(false, QStringLiteral("Cette opération n'est pas autorisée pour cet utilisateur.")); return; } if (m_busy) { + emit passwordChangeFinished(false, + QStringLiteral("Une opération de configuration est déjà en cours.")); return; } @@ -187,8 +254,6 @@ void ProvisioningBackend::changePassword(const QString ¤tPassword, 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, @@ -196,16 +261,15 @@ void ProvisioningBackend::changePassword(const QString ¤tPassword, return; } - // Vérifie avant toute modification que l'état de provisioning pourra - // effectivement être persisté dans le HOME de l'utilisateur. if (!ensureStateWritable()) { + appendLog(QStringLiteral("PASSWORD refused: state directory is not writable")); 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), + QString::fromUtf8(NIXOS_WORKSTATIONS_PASSWORD_HELPER_PATH), currentPassword, newPassword, confirmation); @@ -216,12 +280,15 @@ void ProvisioningBackend::changePin(const QString ¤tPin, const QString &confirmation) { if (!m_authorizedUser) { + appendLog(QStringLiteral("PIN refused: unauthorized user")); emit pinChangeFinished(false, QStringLiteral("Cette opération n'est pas autorisée pour cet utilisateur.")); return; } if (m_busy) { + emit pinChangeFinished(false, + QStringLiteral("Une opération de configuration est déjà en cours.")); return; } @@ -257,13 +324,14 @@ void ProvisioningBackend::changePin(const QString ¤tPin, } if (!ensureStateWritable()) { + appendLog(QStringLiteral("PIN refused: state directory is not writable")); 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), + QString::fromUtf8(NIXOS_WORKSTATIONS_PIN_HELPER_PATH), currentPin, newPin, confirmation); @@ -275,8 +343,11 @@ void ProvisioningBackend::startHelper(Operation operation, const QString &secondSecret, const QString &thirdSecret) { - if (!QFileInfo::exists(helperPath)) { - const QString message = QStringLiteral("Le composant système requis est introuvable."); + const QFileInfo helperInfo(helperPath); + if (!helperInfo.exists() || !helperInfo.isFile() || !helperInfo.isExecutable()) { + appendLog(operationName(operation) + + QStringLiteral(" failed: helper missing or not executable")); + const QString message = QStringLiteral("Le composant système requis est introuvable ou inutilisable."); if (operation == Operation::Password) { emit passwordChangeFinished(false, message); } else { @@ -285,85 +356,122 @@ void ProvisioningBackend::startHelper(Operation operation, 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_pendingPayload.clear(); + m_pendingPayload.reserve(first.size() + second.size() + third.size() + 3); + m_pendingPayload += first; + m_pendingPayload += '\n'; + m_pendingPayload += second; + m_pendingPayload += '\n'; + m_pendingPayload += third; + m_pendingPayload += '\n'; - m_process->write(payload); - m_process->closeWriteChannel(); + secureClear(first); + secureClear(second); + secureClear(third); - // Réduit la durée de vie des copies explicites côté backend. - secureClear(first); - secureClear(second); - secureClear(third); - secureClear(payload); - timeout->start(); + m_helperTimedOut = false; + setBusy(true); + + auto *process = new QProcess(this); + m_process = process; + process->setProgram(helperPath); + process->setProcessChannelMode(QProcess::SeparateChannels); + + QProcessEnvironment environment = QProcessEnvironment::systemEnvironment(); + environment.insert(QStringLiteral("LC_ALL"), QStringLiteral("C")); + environment.insert(QStringLiteral("LANG"), QStringLiteral("C")); + process->setProcessEnvironment(environment); + + auto *timeout = new QTimer(process); + timeout->setSingleShot(true); + timeout->setInterval(kHelperTimeoutMs); + + connect(timeout, &QTimer::timeout, process, + [this, process, operation]() { + if (m_process != process || process->state() == QProcess::NotRunning) { + return; + } + m_helperTimedOut = true; + appendLog(operationName(operation) + QStringLiteral(" timeout")); + process->kill(); }); - connect(m_process, &QProcess::errorOccurred, this, - [this](QProcess::ProcessError error) { - if (error != QProcess::FailedToStart) { + connect(process, &QProcess::started, this, + [this, process, timeout, operation]() { + if (m_process != process) { return; } + const qint64 expected = m_pendingPayload.size(); + const qint64 written = process->write(m_pendingPayload); + process->closeWriteChannel(); + secureClear(m_pendingPayload); + + if (written != expected) { + appendLog(operationName(operation) + + QStringLiteral(" warning: incomplete helper input write")); + } + + appendLog(operationName(operation) + QStringLiteral(" helper started")); + timeout->start(); + }); + + connect(process, &QProcess::errorOccurred, this, + [this, process, timeout, operation](QProcess::ProcessError error) { + if (m_process != process || error != QProcess::FailedToStart) { + return; + } + + timeout->stop(); + secureClear(m_pendingPayload); + appendLog(operationName(operation) + + QStringLiteral(" failed to start: ") + + process->errorString()); + + m_process = nullptr; + process->deleteLater(); setBusy(false); + const QString message = QStringLiteral("Impossible de démarrer le composant de configuration."); - if (m_operation == Operation::Password) { + if (operation == Operation::Password) { emit passwordChangeFinished(false, message); } else { emit pinChangeFinished(false, message); } }); - connect(m_process, + connect(process, qOverload(&QProcess::finished), this, - [this, timeout](int exitCode, QProcess::ExitStatus exitStatus) { - timeout->stop(); + [this, process, timeout, operation](int exitCode, QProcess::ExitStatus exitStatus) { + if (m_process != process) { + return; + } - 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; + timeout->stop(); + secureClear(m_pendingPayload); + + QByteArray stderrData = process->readAllStandardError(); + QByteArray stdoutData = process->readAllStandardOutput(); + QByteArray combinedOutput; + combinedOutput.reserve(stderrData.size() + stdoutData.size() + 1); + combinedOutput += stderrData; + combinedOutput += '\n'; + combinedOutput += stdoutData; + + const bool timedOut = m_helperTimedOut; + const bool helperSucceeded = !timedOut + && exitStatus == QProcess::NormalExit + && exitCode == 0; QString message; bool markerCreated = false; - if (success) { + if (helperSucceeded) { if (operation == Operation::Password) { markerCreated = createMarker(passwordMarker()); message = markerCreated @@ -376,15 +484,26 @@ void ProvisioningBackend::startHelper(Operation operation, : 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); + message = safeMessageForFailure(operation, exitCode, combinedOutput, timedOut); } - m_process->deleteLater(); + appendLog(operationName(operation) + + QStringLiteral(" helper finished exitCode=%1 normalExit=%2 timeout=%3 marker=%4") + .arg(exitCode) + .arg(exitStatus == QProcess::NormalExit ? QStringLiteral("yes") : QStringLiteral("no")) + .arg(timedOut ? QStringLiteral("yes") : QStringLiteral("no")) + .arg(markerCreated ? QStringLiteral("yes") : QStringLiteral("no"))); + + secureClear(stderrData); + secureClear(stdoutData); + secureClear(combinedOutput); + m_process = nullptr; + process->deleteLater(); setBusy(false); refreshState(); - const bool completed = success && markerCreated; + const bool completed = helperSucceeded && markerCreated; if (operation == Operation::Password) { emit passwordChangeFinished(completed, message); } else { @@ -392,31 +511,36 @@ void ProvisioningBackend::startHelper(Operation operation, } }); - m_process->start(); + process->start(); } QString ProvisioningBackend::safeMessageForFailure(Operation operation, int exitCode, - const QByteArray &stderrData) const + const QByteArray &outputData, + bool timedOut) const { - const QString output = QString::fromUtf8(stderrData).toLower(); + const QString output = QString::fromUtf8(outputData).toLower(); - if (exitCode == 124 || output.contains(QStringLiteral("timeout"))) { + if (timedOut || 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("current_rejected")) + || output.contains(QStringLiteral("current_prompt_missing"))) { + return QStringLiteral("Le mot de passe temporaire actuel est incorrect ou n'a pas pu être vérifié."); } 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."); } + if (output.contains(QStringLiteral("input_error"))) { + return QStringLiteral("Le composant de changement de mot de passe n'a pas reçu les données attendues."); + } 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_not_configured"))) { + return QStringLiteral("Aucun PIN FIDO2 actuel n'a été détecté sur la YubiKey. Contactez le service informatique pour initialiser la clé."); } 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."); @@ -424,12 +548,18 @@ QString ProvisioningBackend::safeMessageForFailure(Operation operation, 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("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("no_yubikey"))) { - return QStringLiteral("Aucune YubiKey compatible n'a été détectée. Branchez la clé puis réessayez."); + return QStringLiteral("Aucune YubiKey compatible n'a été détectée ou le système n'a pas les droits d'accès nécessaires."); } if (output.contains(QStringLiteral("pin_policy"))) { return QStringLiteral("Le nouveau PIN ne respecte pas la politique configurée sur cette YubiKey."); } + if (output.contains(QStringLiteral("input_error"))) { + return QStringLiteral("Le composant de configuration de la YubiKey n'a pas reçu les données attendues."); + } 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."); } diff --git a/workstation-setup/src/ProvisioningBackend.h b/workstation-setup/src/ProvisioningBackend.h index 94a8594..b4a5e0f 100644 --- a/workstation-setup/src/ProvisioningBackend.h +++ b/workstation-setup/src/ProvisioningBackend.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -17,7 +18,8 @@ class ProvisioningBackend final : public QObject Q_PROPERTY(bool authorizedUser READ authorizedUser CONSTANT) public: - explicit ProvisioningBackend(QObject *parent = nullptr); + explicit ProvisioningBackend(QString targetUser, QObject *parent = nullptr); + ~ProvisioningBackend() override; bool busy() const { return m_busy; } bool passwordDone() const { return m_passwordDone; } @@ -27,6 +29,13 @@ public: QString targetUser() const { return m_targetUser; } bool authorizedUser() const { return m_authorizedUser; } + QString stateDirectory() const; + QString diagnosticLogPath() const; + QString passwordMarker() const; + QString pinMarker() const; + + static QString effectiveUserName(); + Q_INVOKABLE void changePassword(const QString ¤tPassword, const QString &newPassword, const QString &confirmation); @@ -57,22 +66,23 @@ private: void refreshState(); bool ensureStateWritable(); bool createMarker(const QString &path); - QString stateDirectory() const; - QString passwordMarker() const; - QString pinMarker() const; + void appendLog(const QString &message) const; + QString safeMessageForFailure(Operation operation, int exitCode, - const QByteArray &stderrData) const; + const QByteArray &output, + bool timedOut) const; - static QString effectiveUserName(); static void secureClear(QByteArray &data); + static QString operationName(Operation operation); bool m_busy = false; bool m_passwordDone = false; bool m_pinDone = false; bool m_authorizedUser = false; + bool m_helperTimedOut = false; QString m_currentUser; QString m_targetUser; QProcess *m_process = nullptr; - Operation m_operation = Operation::Password; + QByteArray m_pendingPayload; };