Téléverser les fichiers vers "workstation-setup/src"
This commit is contained in:
@@ -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 ¤tPassword,
|
||||
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 ¤tPin,
|
||||
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.");
|
||||
}
|
||||
Reference in New Issue
Block a user