747 lines
27 KiB
C++
747 lines
27 KiB
C++
#include "ProvisioningBackend.h"
|
||
#include "Config.h"
|
||
|
||
#include <QDateTime>
|
||
#include <QDir>
|
||
#include <QFile>
|
||
#include <QFileInfo>
|
||
#include <QProcessEnvironment>
|
||
#include <QRegularExpression>
|
||
#include <QSaveFile>
|
||
#include <QTimer>
|
||
|
||
#include <pwd.h>
|
||
#include <sys/types.h>
|
||
#include <unistd.h>
|
||
|
||
#include <utility>
|
||
|
||
namespace {
|
||
constexpr int kHelperTimeoutMs = 120000;
|
||
|
||
bool containsLineBreak(const QString &value)
|
||
{
|
||
return value.contains(QLatin1Char('\n')) || value.contains(QLatin1Char('\r'));
|
||
}
|
||
|
||
QString effectiveHomeDirectory()
|
||
{
|
||
if (passwd *entry = getpwuid(geteuid())) {
|
||
const QString home = QString::fromLocal8Bit(entry->pw_dir);
|
||
if (!home.isEmpty() && home != QStringLiteral("/")) {
|
||
return home;
|
||
}
|
||
}
|
||
return QDir::homePath();
|
||
}
|
||
}
|
||
|
||
ProvisioningBackend::ProvisioningBackend(QString targetUser, QObject *parent)
|
||
: QObject(parent)
|
||
, m_currentUser(effectiveUserName())
|
||
, m_targetUser(std::move(targetUser))
|
||
{
|
||
refreshState();
|
||
appendLog(QStringLiteral("START currentUser=%1 targetUser=%2 authorized=%3 passwordDone=%4 pinDone=%5 fidoDone=%6 yubiKeyChoice=%7 yubiKeyRequested=%8")
|
||
.arg(m_currentUser,
|
||
m_targetUser,
|
||
authorizedUser() ? QStringLiteral("yes") : QStringLiteral("no"),
|
||
m_passwordDone ? QStringLiteral("yes") : QStringLiteral("no"),
|
||
m_pinDone ? QStringLiteral("yes") : QStringLiteral("no"),
|
||
m_fidoDone ? QStringLiteral("yes") : QStringLiteral("no"),
|
||
yubiKeyChoiceMade() ? QStringLiteral("yes") : QStringLiteral("no"),
|
||
yubiKeyRequested() ? 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()
|
||
{
|
||
if (passwd *entry = getpwuid(geteuid())) {
|
||
return QString::fromLocal8Bit(entry->pw_name);
|
||
}
|
||
return {};
|
||
}
|
||
|
||
bool ProvisioningBackend::authorizedUser() const
|
||
{
|
||
if (m_targetUser.isEmpty()) {
|
||
return false;
|
||
}
|
||
|
||
const QByteArray name = m_targetUser.toLocal8Bit();
|
||
if (passwd *entry = getpwnam(name.constData())) {
|
||
return entry->pw_uid == geteuid();
|
||
}
|
||
return false;
|
||
}
|
||
|
||
QString ProvisioningBackend::stateDirectory() const
|
||
{
|
||
const QByteArray xdgState = qgetenv("XDG_STATE_HOME");
|
||
if (!xdgState.isEmpty()) {
|
||
const QString candidate = QString::fromLocal8Bit(xdgState);
|
||
if (QDir::isAbsolutePath(candidate)) {
|
||
return QDir::cleanPath(candidate + QStringLiteral("/nixos-workstations"));
|
||
}
|
||
}
|
||
return QDir::cleanPath(effectiveHomeDirectory()
|
||
+ QStringLiteral("/.local/state/nixos-workstations"));
|
||
}
|
||
|
||
QString ProvisioningBackend::diagnosticLogPath() const
|
||
{
|
||
return stateDirectory() + QStringLiteral("/setup.log");
|
||
}
|
||
|
||
QString ProvisioningBackend::passwordMarker() const
|
||
{
|
||
return stateDirectory() + QStringLiteral("/password-initialized");
|
||
}
|
||
|
||
QString ProvisioningBackend::pinMarker() const
|
||
{
|
||
return stateDirectory() + QStringLiteral("/yubikey-pin-created");
|
||
}
|
||
|
||
QString ProvisioningBackend::fidoMarker() const
|
||
{
|
||
return stateDirectory() + QStringLiteral("/yubikey-fido-enrolled");
|
||
}
|
||
|
||
QString ProvisioningBackend::yubiKeyEnabledMarker() const
|
||
{
|
||
return stateDirectory() + QStringLiteral("/yubikey-enabled");
|
||
}
|
||
|
||
QString ProvisioningBackend::yubiKeySkippedMarker() const
|
||
{
|
||
return stateDirectory() + QStringLiteral("/yubikey-skipped");
|
||
}
|
||
|
||
void ProvisioningBackend::refreshState()
|
||
{
|
||
const bool oldPassword = m_passwordDone;
|
||
const bool oldPin = m_pinDone;
|
||
const bool oldFido = m_fidoDone;
|
||
const bool oldYubiKeyEnabled = m_yubiKeyEnabled;
|
||
const bool oldYubiKeySkipped = m_yubiKeySkipped;
|
||
|
||
m_passwordDone = QFileInfo::exists(passwordMarker());
|
||
m_pinDone = QFileInfo::exists(pinMarker());
|
||
m_fidoDone = QFileInfo::exists(fidoMarker());
|
||
m_yubiKeyEnabled = QFileInfo::exists(yubiKeyEnabledMarker());
|
||
m_yubiKeySkipped = QFileInfo::exists(yubiKeySkippedMarker());
|
||
|
||
if (oldPassword != m_passwordDone
|
||
|| oldPin != m_pinDone
|
||
|| oldFido != m_fidoDone
|
||
|| oldYubiKeyEnabled != m_yubiKeyEnabled
|
||
|| oldYubiKeySkipped != m_yubiKeySkipped) {
|
||
emit stateChanged();
|
||
}
|
||
}
|
||
|
||
bool ProvisioningBackend::ensureStateWritable()
|
||
{
|
||
const QString dir = stateDirectory();
|
||
if (!QDir().mkpath(dir)) {
|
||
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;
|
||
}
|
||
const bool ok = probe.write("ok\n") == 3;
|
||
probe.close();
|
||
probe.remove();
|
||
return ok;
|
||
}
|
||
|
||
bool ProvisioningBackend::createMarker(const QString &path)
|
||
{
|
||
if (!ensureStateWritable()) {
|
||
return false;
|
||
}
|
||
|
||
QSaveFile marker(path);
|
||
if (!marker.open(QIODevice::WriteOnly)) {
|
||
return false;
|
||
}
|
||
|
||
const QByteArray content = QByteArrayLiteral("completed=")
|
||
+ QDateTime::currentDateTimeUtc().toString(Qt::ISODate).toUtf8()
|
||
+ '\n';
|
||
|
||
if (marker.write(content) != content.size()) {
|
||
marker.cancelWriting();
|
||
return false;
|
||
}
|
||
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;
|
||
}
|
||
|
||
log.write(QDateTime::currentDateTimeUtc().toString(Qt::ISODateWithMs).toUtf8()
|
||
+ QByteArrayLiteral(" ")
|
||
+ message.toUtf8()
|
||
+ '\n');
|
||
log.close();
|
||
QFile::setPermissions(diagnosticLogPath(),
|
||
QFileDevice::ReadOwner | QFileDevice::WriteOwner);
|
||
}
|
||
|
||
void ProvisioningBackend::setBusy(bool busy)
|
||
{
|
||
if (m_busy == busy) {
|
||
return;
|
||
}
|
||
m_busy = busy;
|
||
emit busyChanged();
|
||
}
|
||
|
||
|
||
void ProvisioningBackend::resetFidoInteraction()
|
||
{
|
||
const bool changed = m_fidoTouchRequired
|
||
|| m_fidoTouchRequestCount != 0
|
||
|| !m_fidoStatus.isEmpty();
|
||
|
||
m_fidoTouchRequired = false;
|
||
m_fidoTouchRequestCount = 0;
|
||
m_fidoStatus.clear();
|
||
m_helperStdoutBuffer.clear();
|
||
|
||
if (changed) {
|
||
emit fidoInteractionChanged();
|
||
}
|
||
}
|
||
|
||
void ProvisioningBackend::handleHelperStdout(Operation operation, const QByteArray &data)
|
||
{
|
||
if (operation != Operation::Fido || data.isEmpty()) {
|
||
return;
|
||
}
|
||
|
||
m_helperStdoutBuffer += data;
|
||
|
||
qsizetype newlineIndex = -1;
|
||
while ((newlineIndex = m_helperStdoutBuffer.indexOf('\n')) >= 0) {
|
||
QByteArray line = m_helperStdoutBuffer.left(newlineIndex);
|
||
m_helperStdoutBuffer.remove(0, newlineIndex + 1);
|
||
if (!line.isEmpty() && line.endsWith('\r')) {
|
||
line.chop(1);
|
||
}
|
||
|
||
bool changed = false;
|
||
if (line == QByteArrayLiteral("EVENT_FIDO_INITIALIZING")) {
|
||
m_fidoTouchRequired = true;
|
||
++m_fidoTouchRequestCount;
|
||
m_fidoStatus = QStringLiteral("Touchez la YubiKey dès qu’elle clignote. Plusieurs touchers peuvent être demandés pendant cette étape. (%1)")
|
||
.arg(m_fidoTouchRequestCount);
|
||
changed = true;
|
||
} else if (line == QByteArrayLiteral("EVENT_FIDO_GENERATING")) {
|
||
m_fidoTouchRequired = true;
|
||
++m_fidoTouchRequestCount;
|
||
m_fidoStatus = QStringLiteral("Touchez de nouveau la YubiKey dès qu’elle clignote. (%1)")
|
||
.arg(m_fidoTouchRequestCount);
|
||
changed = true;
|
||
} else if (line == QByteArrayLiteral("EVENT_FIDO_UPDATING_HOME")) {
|
||
m_fidoTouchRequired = false;
|
||
m_fidoStatus = QStringLiteral("Mise à jour de votre espace personnel…");
|
||
changed = true;
|
||
} else if (line == QByteArrayLiteral("EVENT_FIDO_SYNCHRONIZING")) {
|
||
m_fidoTouchRequired = false;
|
||
m_fidoStatus = QStringLiteral("Finalisation de la protection…");
|
||
changed = true;
|
||
} else if (line == QByteArrayLiteral("EVENT_FIDO_TOUCH_REQUIRED")) {
|
||
m_fidoTouchRequired = true;
|
||
++m_fidoTouchRequestCount;
|
||
m_fidoStatus = QStringLiteral("Touchez la YubiKey maintenant. Plusieurs touchers peuvent être demandés pendant cette étape. (%1)")
|
||
.arg(m_fidoTouchRequestCount);
|
||
changed = true;
|
||
}
|
||
|
||
if (changed) {
|
||
emit fidoInteractionChanged();
|
||
}
|
||
}
|
||
}
|
||
|
||
void ProvisioningBackend::secureClear(QByteArray &data)
|
||
{
|
||
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)
|
||
{
|
||
switch (operation) {
|
||
case Operation::Password:
|
||
return QStringLiteral("PASSWORD");
|
||
case Operation::Pin:
|
||
return QStringLiteral("PIN");
|
||
case Operation::Fido:
|
||
return QStringLiteral("FIDO");
|
||
}
|
||
return QStringLiteral("UNKNOWN");
|
||
}
|
||
|
||
void ProvisioningBackend::emitResult(Operation operation, bool success, const QString &message)
|
||
{
|
||
switch (operation) {
|
||
case Operation::Password:
|
||
emit passwordChangeFinished(success, message);
|
||
break;
|
||
case Operation::Pin:
|
||
emit pinChangeFinished(success, message);
|
||
break;
|
||
case Operation::Fido:
|
||
emit fidoEnrollmentFinished(success, message);
|
||
break;
|
||
}
|
||
}
|
||
|
||
void ProvisioningBackend::setYubiKeyUsage(bool enabled)
|
||
{
|
||
if (!authorizedUser()) {
|
||
emit yubiKeyChoiceFinished(false, QStringLiteral("Cette opération n'est pas autorisée pour cet utilisateur."));
|
||
return;
|
||
}
|
||
if (m_busy) {
|
||
emit yubiKeyChoiceFinished(false, QStringLiteral("Une opération de configuration est déjà en cours."));
|
||
return;
|
||
}
|
||
|
||
// Un compte déjà passé par les étapes YubiKey est considéré comme utilisant
|
||
// toujours sa clé. Cela assure la compatibilité avec les états v1.9.0.
|
||
if (!enabled && (m_pinDone || m_fidoDone)) {
|
||
emit yubiKeyChoiceFinished(false, QStringLiteral("Une YubiKey est déjà configurée pour ce compte."));
|
||
return;
|
||
}
|
||
|
||
const QString selectedMarker = enabled ? yubiKeyEnabledMarker() : yubiKeySkippedMarker();
|
||
const QString obsoleteMarker = enabled ? yubiKeySkippedMarker() : yubiKeyEnabledMarker();
|
||
|
||
if (!createMarker(selectedMarker)) {
|
||
emit yubiKeyChoiceFinished(false, QStringLiteral("Impossible d'enregistrer votre choix."));
|
||
return;
|
||
}
|
||
|
||
QFile::remove(obsoleteMarker);
|
||
refreshState();
|
||
appendLog(QStringLiteral("YUBIKEY choice=%1").arg(enabled ? QStringLiteral("enabled") : QStringLiteral("skipped")));
|
||
emit yubiKeyChoiceFinished(true, QString());
|
||
}
|
||
|
||
void ProvisioningBackend::changePassword(const QString ¤tPassword,
|
||
const QString &newPassword,
|
||
const QString &confirmation)
|
||
{
|
||
if (!authorizedUser()) {
|
||
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;
|
||
}
|
||
if (!yubiKeyChoiceMade()) {
|
||
emit passwordChangeFinished(false, QStringLiteral("Indiquez d'abord si vous disposez d'une YubiKey."));
|
||
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;
|
||
}
|
||
if (containsLineBreak(currentPassword) || containsLineBreak(newPassword) || containsLineBreak(confirmation)) {
|
||
emit passwordChangeFinished(false, QStringLiteral("Le mot de passe contient un caractère non pris en charge."));
|
||
return;
|
||
}
|
||
if (!ensureStateWritable()) {
|
||
emit passwordChangeFinished(false, QStringLiteral("Impossible d'enregistrer l'état de configuration dans votre profil."));
|
||
return;
|
||
}
|
||
|
||
startHelper(Operation::Password,
|
||
QString::fromUtf8(NIXOS_WORKSTATIONS_PASSWORD_HELPER_PATH),
|
||
currentPassword,
|
||
newPassword,
|
||
confirmation);
|
||
}
|
||
|
||
void ProvisioningBackend::initializePin(const QString &newPin,
|
||
const QString &confirmation)
|
||
{
|
||
if (!authorizedUser()) {
|
||
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;
|
||
}
|
||
if (!yubiKeyRequested()) {
|
||
emit pinChangeFinished(false, QStringLiteral("Aucune YubiKey n'a été sélectionnée pour ce compte."));
|
||
return;
|
||
}
|
||
if (!m_passwordDone) {
|
||
emit pinChangeFinished(false, QStringLiteral("Personnalisez d'abord votre mot de passe."));
|
||
return;
|
||
}
|
||
if (m_pinDone) {
|
||
emit pinChangeFinished(true, QStringLiteral("Le code PIN de la YubiKey a déjà été initialisé."));
|
||
return;
|
||
}
|
||
if (newPin.size() < 4 || confirmation.size() < 4) {
|
||
emit pinChangeFinished(false, QStringLiteral("Le PIN doit contenir au moins 4 caractères."));
|
||
return;
|
||
}
|
||
if (newPin != confirmation) {
|
||
emit pinChangeFinished(false, QStringLiteral("Les deux PIN ne correspondent pas."));
|
||
return;
|
||
}
|
||
if (containsLineBreak(newPin) || containsLineBreak(confirmation)) {
|
||
emit pinChangeFinished(false, QStringLiteral("Le PIN contient un caractère non pris en charge."));
|
||
return;
|
||
}
|
||
|
||
startHelper(Operation::Pin,
|
||
QString::fromUtf8(NIXOS_WORKSTATIONS_PIN_HELPER_PATH),
|
||
newPin,
|
||
confirmation);
|
||
}
|
||
|
||
void ProvisioningBackend::enrollFido(const QString ¤tPassword,
|
||
const QString &pin)
|
||
{
|
||
if (!authorizedUser()) {
|
||
emit fidoEnrollmentFinished(false, QStringLiteral("Cette opération n'est pas autorisée pour cet utilisateur."));
|
||
return;
|
||
}
|
||
if (m_busy) {
|
||
emit fidoEnrollmentFinished(false, QStringLiteral("Une opération de configuration est déjà en cours."));
|
||
return;
|
||
}
|
||
if (!yubiKeyRequested()) {
|
||
emit fidoEnrollmentFinished(false, QStringLiteral("Aucune YubiKey n'a été sélectionnée pour ce compte."));
|
||
return;
|
||
}
|
||
if (!m_passwordDone || !m_pinDone) {
|
||
emit fidoEnrollmentFinished(false, QStringLiteral("Le mot de passe et le code PIN doivent être configurés avant d'associer la YubiKey."));
|
||
return;
|
||
}
|
||
if (m_fidoDone) {
|
||
emit fidoEnrollmentFinished(true, QStringLiteral("La YubiKey est déjà associée à votre espace personnel."));
|
||
return;
|
||
}
|
||
if (currentPassword.isEmpty() || pin.size() < 4) {
|
||
emit fidoEnrollmentFinished(false, QStringLiteral("Saisissez le nouveau mot de passe choisi à l’étape 1 et le code PIN de la YubiKey."));
|
||
return;
|
||
}
|
||
if (containsLineBreak(currentPassword) || containsLineBreak(pin)) {
|
||
emit fidoEnrollmentFinished(false, QStringLiteral("Un des secrets contient un caractère non pris en charge."));
|
||
return;
|
||
}
|
||
|
||
resetFidoInteraction();
|
||
m_fidoStatus = QStringLiteral("Préparation de l'association de la YubiKey…");
|
||
emit fidoInteractionChanged();
|
||
|
||
startHelper(Operation::Fido,
|
||
QString::fromUtf8(NIXOS_WORKSTATIONS_FIDO_HELPER_PATH),
|
||
currentPassword,
|
||
pin);
|
||
}
|
||
|
||
void ProvisioningBackend::startHelper(Operation operation,
|
||
const QString &helperPath,
|
||
const QString &firstSecret,
|
||
const QString &secondSecret,
|
||
const QString &thirdSecret)
|
||
{
|
||
const QFileInfo helperInfo(helperPath);
|
||
if (!helperInfo.exists() || !helperInfo.isFile() || !helperInfo.isExecutable()) {
|
||
appendLog(operationName(operation) + QStringLiteral(" failed: helper unavailable"));
|
||
emitResult(operation, false, QStringLiteral("Le composant système requis est introuvable ou inutilisable."));
|
||
return;
|
||
}
|
||
|
||
QByteArray first = firstSecret.toUtf8();
|
||
QByteArray second = secondSecret.toUtf8();
|
||
QByteArray third = thirdSecret.toUtf8();
|
||
|
||
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';
|
||
|
||
secureClear(first);
|
||
secureClear(second);
|
||
secureClear(third);
|
||
|
||
m_helperTimedOut = false;
|
||
m_helperStdoutBuffer.clear();
|
||
setBusy(true);
|
||
|
||
auto *process = new QProcess(this);
|
||
m_process = process;
|
||
process->setProgram(helperPath);
|
||
process->setProcessChannelMode(QProcess::SeparateChannels);
|
||
|
||
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
|
||
env.insert(QStringLiteral("LC_ALL"), QStringLiteral("C"));
|
||
env.insert(QStringLiteral("LANG"), QStringLiteral("C"));
|
||
process->setProcessEnvironment(env);
|
||
|
||
auto *timer = new QTimer(process);
|
||
timer->setSingleShot(true);
|
||
timer->setInterval(kHelperTimeoutMs);
|
||
|
||
connect(timer, &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(process, &QProcess::started, this, [this, process, timer, operation]() {
|
||
if (m_process != process) {
|
||
return;
|
||
}
|
||
process->write(m_pendingPayload);
|
||
process->closeWriteChannel();
|
||
secureClear(m_pendingPayload);
|
||
appendLog(operationName(operation) + QStringLiteral(" helper started"));
|
||
timer->start();
|
||
});
|
||
|
||
connect(process, &QProcess::readyReadStandardOutput, this,
|
||
[this, process, operation]() {
|
||
if (m_process != process) {
|
||
return;
|
||
}
|
||
handleHelperStdout(operation, process->readAllStandardOutput());
|
||
});
|
||
|
||
connect(process, &QProcess::errorOccurred, this,
|
||
[this, process, timer, operation](QProcess::ProcessError error) {
|
||
if (m_process != process || error != QProcess::FailedToStart) {
|
||
return;
|
||
}
|
||
timer->stop();
|
||
secureClear(m_pendingPayload);
|
||
m_process = nullptr;
|
||
process->deleteLater();
|
||
setBusy(false);
|
||
appendLog(operationName(operation) + QStringLiteral(" failed to start"));
|
||
if (operation == Operation::Fido) {
|
||
resetFidoInteraction();
|
||
}
|
||
emitResult(operation, false, QStringLiteral("Impossible de démarrer le composant de configuration."));
|
||
});
|
||
|
||
connect(process,
|
||
qOverload<int, QProcess::ExitStatus>(&QProcess::finished),
|
||
this,
|
||
[this, process, timer, operation](int exitCode, QProcess::ExitStatus exitStatus) {
|
||
if (m_process != process) {
|
||
return;
|
||
}
|
||
|
||
timer->stop();
|
||
secureClear(m_pendingPayload);
|
||
|
||
QByteArray stderrData = process->readAllStandardError();
|
||
QByteArray stdoutData = process->readAllStandardOutput();
|
||
if (operation == Operation::Fido && !stdoutData.isEmpty()) {
|
||
handleHelperStdout(operation, stdoutData);
|
||
stdoutData.clear();
|
||
}
|
||
QByteArray combined = stderrData + '\n' + stdoutData;
|
||
|
||
const bool timedOut = m_helperTimedOut;
|
||
const bool helperSucceeded = !timedOut
|
||
&& exitStatus == QProcess::NormalExit
|
||
&& exitCode == 0;
|
||
|
||
bool markerCreated = false;
|
||
QString message;
|
||
|
||
if (helperSucceeded) {
|
||
switch (operation) {
|
||
case Operation::Password:
|
||
markerCreated = createMarker(passwordMarker());
|
||
message = markerCreated
|
||
? QStringLiteral("Votre nouveau mot de passe protège maintenant votre espace personnel.")
|
||
: QStringLiteral("Le mot de passe a été modifié mais l'état local n'a pas pu être enregistré.");
|
||
break;
|
||
case Operation::Pin:
|
||
markerCreated = createMarker(pinMarker());
|
||
message = markerCreated
|
||
? QStringLiteral("Le code PIN de votre YubiKey est maintenant initialisé.")
|
||
: QStringLiteral("Le PIN a été créé mais l'état local n'a pas pu être enregistré.");
|
||
break;
|
||
case Operation::Fido:
|
||
markerCreated = createMarker(fidoMarker());
|
||
message = markerCreated
|
||
? QStringLiteral("Votre YubiKey peut maintenant ouvrir votre espace personnel chiffré.")
|
||
: QStringLiteral("La YubiKey a été associée mais l'état local n'a pas pu être enregistré.");
|
||
break;
|
||
}
|
||
} else {
|
||
message = safeMessageForFailure(operation, exitCode, combined, timedOut);
|
||
}
|
||
|
||
QString token = QString::fromUtf8(combined).trimmed();
|
||
token.remove(QRegularExpression(QStringLiteral("[^A-Za-z0-9_\\-]")));
|
||
if (token.size() > 80) token.truncate(80);
|
||
if (token.isEmpty()) token = QStringLiteral("none");
|
||
|
||
appendLog(operationName(operation)
|
||
+ QStringLiteral(" helper finished exitCode=%1 timeout=%2 marker=%3 token=%4")
|
||
.arg(exitCode)
|
||
.arg(timedOut ? QStringLiteral("yes") : QStringLiteral("no"))
|
||
.arg(markerCreated ? QStringLiteral("yes") : QStringLiteral("no"))
|
||
.arg(token));
|
||
|
||
secureClear(stderrData);
|
||
secureClear(stdoutData);
|
||
secureClear(combined);
|
||
|
||
m_process = nullptr;
|
||
process->deleteLater();
|
||
setBusy(false);
|
||
refreshState();
|
||
|
||
if (operation == Operation::Fido) {
|
||
m_fidoTouchRequired = false;
|
||
m_fidoStatus = helperSucceeded && markerCreated
|
||
? QStringLiteral("Association de la YubiKey terminée.")
|
||
: QStringLiteral("Association de la YubiKey interrompue.");
|
||
emit fidoInteractionChanged();
|
||
}
|
||
|
||
emitResult(operation, helperSucceeded && markerCreated, message);
|
||
});
|
||
|
||
process->start();
|
||
}
|
||
|
||
QString ProvisioningBackend::safeMessageForFailure(Operation operation,
|
||
int exitCode,
|
||
const QByteArray &outputData,
|
||
bool timedOut) const
|
||
{
|
||
const QString output = QString::fromUtf8(outputData).toLower();
|
||
|
||
if (timedOut || exitCode == 124 || output.contains(QStringLiteral("timeout"))) {
|
||
if (operation == Operation::Fido) {
|
||
return QStringLiteral("L'association de la YubiKey a expiré. Laissez-la branchée et recommencez l'opération.");
|
||
}
|
||
return QStringLiteral("L'opération a expiré avant de pouvoir être terminée.");
|
||
}
|
||
|
||
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 nouveau mot de passe a été refusé par la politique de sécurité.");
|
||
}
|
||
return QStringLiteral("Le mot de passe n'a pas pu être modifié.");
|
||
}
|
||
|
||
if (operation == Operation::Pin) {
|
||
if (output.contains(QStringLiteral("pin_already_configured"))) {
|
||
return QStringLiteral("Cette YubiKey possède déjà un code PIN. Utilisez une YubiKey vierge ou réinitialisée.");
|
||
}
|
||
if (output.contains(QStringLiteral("pin_auth_blocked"))) {
|
||
return QStringLiteral("Les opérations PIN sont temporairement bloquées. Débranchez puis rebranchez la YubiKey.");
|
||
}
|
||
if (output.contains(QStringLiteral("pin_blocked"))) {
|
||
return QStringLiteral("Le code PIN est bloqué. La YubiKey doit être réinitialisée avant de recommencer.");
|
||
}
|
||
if (output.contains(QStringLiteral("multiple_yubikey"))) {
|
||
return QStringLiteral("Plusieurs clés de sécurité sont détectées. Ne laissez branchée que votre YubiKey.");
|
||
}
|
||
if (output.contains(QStringLiteral("no_yubikey"))) {
|
||
return QStringLiteral("Aucune YubiKey compatible n'a été détectée.");
|
||
}
|
||
if (output.contains(QStringLiteral("pin_policy"))) {
|
||
return QStringLiteral("Le code PIN choisi n'est pas accepté par la YubiKey.");
|
||
}
|
||
return QStringLiteral("Le code PIN de la YubiKey n'a pas pu être initialisé.");
|
||
}
|
||
|
||
if (output.contains(QStringLiteral("fido_bad_password"))) {
|
||
return QStringLiteral("Le mot de passe fourni est incorrect.");
|
||
}
|
||
if (output.contains(QStringLiteral("fido_bad_pin"))) {
|
||
return QStringLiteral("Le code PIN de la YubiKey est incorrect. Vérifiez-le avant une nouvelle tentative.");
|
||
}
|
||
if (output.contains(QStringLiteral("multiple_yubikey"))) {
|
||
return QStringLiteral("Plusieurs clés de sécurité sont détectées. Ne laissez branchée que votre YubiKey.");
|
||
}
|
||
if (output.contains(QStringLiteral("no_yubikey"))) {
|
||
return QStringLiteral("Aucune YubiKey compatible n'a été détectée.");
|
||
}
|
||
if (output.contains(QStringLiteral("fido_enroll_failed"))) {
|
||
return QStringLiteral("La YubiKey n'a pas pu être associée à votre espace personnel. Vérifiez le mot de passe et le code PIN.");
|
||
}
|
||
return QStringLiteral("L'association de la YubiKey à votre espace personnel a échoué.");
|
||
}
|