From 16ebd52f7920d4ce932886347e674fc7de58bddd Mon Sep 17 00:00:00 2001 From: Olivier <1+olivier@noreply.raspot.in> Date: Wed, 19 Aug 2026 15:24:29 +0200 Subject: [PATCH] =?UTF-8?q?T=C3=A9l=C3=A9verser=20les=20fichiers=20vers=20?= =?UTF-8?q?"workstation-setup/src"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- workstation-setup/src/Main.qml | 1477 +++++------------ workstation-setup/src/ProvisioningBackend.cpp | 402 +++-- workstation-setup/src/ProvisioningBackend.h | 47 +- 3 files changed, 641 insertions(+), 1285 deletions(-) diff --git a/workstation-setup/src/Main.qml b/workstation-setup/src/Main.qml index 2d4feb8..ebbacc8 100644 --- a/workstation-setup/src/Main.qml +++ b/workstation-setup/src/Main.qml @@ -12,16 +12,20 @@ Kirigami.ApplicationWindow { color: "#eef2f7" property int currentStep: 0 + property string operationError: "" + property bool closeAttempted: false + property var steps: [ - { "title": "Bienvenue", "subtitle": "Présentation" }, - { "title": "Mot de passe", "subtitle": "Accès de secours" }, + { "title": "Bienvenue", "subtitle": "Première connexion" }, + { "title": "Mot de passe", "subtitle": "Protection LUKS2" }, { "title": "YubiKey", "subtitle": "Création du PIN" }, + { "title": "Association", "subtitle": "Déverrouillage FIDO2" }, { "title": "Terminé", "subtitle": "Poste prêt" } ] property bool passwordStepValid: - currentPassword.text.trim().length > 0 && - newPassword.text.length > 0 && + currentPassword.text.length > 0 && + newPassword.text.length >= 8 && confirmPassword.text.length > 0 && newPassword.text === confirmPassword.text @@ -30,25 +34,18 @@ Kirigami.ApplicationWindow { confirmPin.text.length >= 4 && newPin.text === confirmPin.text - // En production, l'état réel vient du backend et des marqueurs locaux. - // La simple saisie des champs ne signifie jamais que l'opération a réussi. + property bool fidoStepValid: + fidoPassword.text.length > 0 && + fidoPin.text.length >= 4 + property bool workflowComplete: provisioning.complete - property bool closeAttempted: false - property string operationError: "" - - function goNext() { - if (currentStep === 0) - currentStep = 1 - } - - function goBack() { - // Une étape déjà appliquée au système n'est jamais rejouée. - if (currentStep === 1 && !provisioning.passwordDone) - currentStep = 0 - } Component.onCompleted: { if (provisioning.complete) + currentStep = 4 + else if (provisioning.fidoDone) + currentStep = 4 + else if (provisioning.pinDone) currentStep = 3 else if (provisioning.passwordDone) currentStep = 2 @@ -70,6 +67,12 @@ Kirigami.ApplicationWindow { if (success) root.currentStep = 3 } + + function onFidoEnrollmentFinished(success, message) { + root.operationError = success ? "" : message + if (success) + root.currentStep = 4 + } } onClosing: function(close) { @@ -92,49 +95,28 @@ Kirigami.ApplicationWindow { onTriggered: root.closeAttempted = false } - // Indicateur volontairement informatif : il ne bloque jamais - // la validation du mot de passe. La longueur est privilégiée, - // la diversité des caractères apporte un bonus visuel. function passwordStrengthLevel(password) { - if (password.length === 0) - return 0 - + if (password.length === 0) return 0 var classes = 0 - if (/[a-z]/.test(password)) classes += 1 - if (/[A-Z]/.test(password)) classes += 1 - if (/[0-9]/.test(password)) classes += 1 - if (/[^A-Za-z0-9]/.test(password)) classes += 1 - - var level = 1 - - if (password.length >= 16) - level = classes >= 2 ? 4 : 3 - else if (password.length >= 12) - level = classes >= 2 ? 3 : 2 - else if (password.length >= 8) - level = classes >= 3 ? 2 : 1 - - // Quelques motifs manifestement faibles : on réduit - // l'indication sans empêcher l'utilisateur de continuer. - var lower = password.toLowerCase() - if (lower === "password" || lower === "motdepasse" || - lower === "azerty" || lower === "qwerty" || - lower === "12345678" || /^(.)\1+$/.test(password)) { - level = 1 - } - - return level + if (/[a-z]/.test(password)) classes++ + if (/[A-Z]/.test(password)) classes++ + if (/[0-9]/.test(password)) classes++ + if (/[^A-Za-z0-9]/.test(password)) classes++ + if (password.length >= 16 && classes >= 2) return 4 + if (password.length >= 12 && classes >= 2) return 3 + if (password.length >= 8 && classes >= 2) return 2 + return 1 } - function passwordStrengthLabel(level) { + function strengthLabel(level) { if (level === 1) return "Faible" if (level === 2) return "Moyen" if (level === 3) return "Bon" if (level === 4) return "Très bon" - return "Non évalué" + return "" } - function passwordStrengthColor(level) { + function strengthColor(level) { if (level === 1) return "#dc2626" if (level === 2) return "#d97706" if (level === 3) return "#2563eb" @@ -142,64 +124,32 @@ Kirigami.ApplicationWindow { return "#cbd5e1" } - function passwordStrengthHint(level) { - if (level === 1) return "Privilégiez surtout la longueur : une phrase de passe de 12 caractères ou plus est un bon point de départ." - if (level === 2) return "Correct pour un usage courant, mais quelques caractères supplémentaires amélioreraient sensiblement la robustesse." - if (level === 3) return "Bonne robustesse apparente : la longueur et la diversité sont satisfaisantes." - if (level === 4) return "Très bonne robustesse apparente : mot de passe long et suffisamment diversifié." - return "Saisissez votre nouveau mot de passe pour obtenir une indication de robustesse." - } - component PrimaryButton: Controls.Button { id: control implicitHeight: 48 - implicitWidth: 170 + implicitWidth: 180 leftPadding: 24 rightPadding: 24 contentItem: Controls.Label { text: control.text + color: "white" font.pixelSize: 15 font.weight: Font.DemiBold - color: "white" horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter } background: Rectangle { - radius: 12 - color: control.down ? "#1e40af" : control.hovered ? "#2563eb" : "#1d4ed8" - Behavior on color { ColorAnimation { duration: 120 } } - } - } - - component SecondaryButton: Controls.Button { - id: control - implicitHeight: 48 - implicitWidth: 140 - leftPadding: 22 - rightPadding: 22 - - contentItem: Controls.Label { - text: control.text - font.pixelSize: 15 - font.weight: Font.Medium - color: "#334155" - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - } - - background: Rectangle { - radius: 12 - color: control.down ? "#dbe3ee" : control.hovered ? "#edf2f7" : "transparent" - border.width: 1 - border.color: "#cbd5e1" + radius: 11 + color: control.enabled + ? (control.down ? "#1e40af" : control.hovered ? "#2563eb" : "#1d4ed8") + : "#94a3b8" } } component SecretField: Controls.TextField { id: field - property bool secretVisible: false Layout.fillWidth: true @@ -208,91 +158,98 @@ Kirigami.ApplicationWindow { passwordCharacter: "●" font.pixelSize: 15 leftPadding: 16 - rightPadding: 56 + rightPadding: 54 selectByMouse: true background: Rectangle { radius: 10 - color: field.activeFocus ? "#ffffff" : "#f8fafc" + color: "#ffffff" border.width: field.activeFocus ? 2 : 1 border.color: field.activeFocus ? "#2563eb" : "#cbd5e1" } Controls.ToolButton { - id: visibilityButton anchors.right: parent.right anchors.rightMargin: 8 anchors.verticalCenter: parent.verticalCenter - width: 38 - height: 38 - - hoverEnabled: true + width: 36 + height: 36 focusPolicy: Qt.NoFocus - Accessible.name: field.secretVisible ? "Masquer la valeur" : "Afficher la valeur" - onClicked: field.secretVisible = !field.secretVisible contentItem: Kirigami.Icon { source: field.secretVisible ? "view-hidden" : "view-visible" - implicitWidth: 20 - implicitHeight: 20 - color: visibilityButton.hovered ? "#2563eb" : "#64748b" + implicitWidth: 19 + implicitHeight: 19 + color: "#64748b" } + } + } - background: Rectangle { - radius: 8 - color: visibilityButton.pressed ? "#dbeafe" - : visibilityButton.hovered ? "#eff6ff" - : "transparent" + component FieldLabel: Controls.Label { + font.pixelSize: 13 + font.weight: Font.DemiBold + color: "#334155" + } - Behavior on color { - ColorAnimation { duration: 100 } + component InfoBox: Rectangle { + id: infoBox + property alias text: infoLabel.text + property string iconText: "i" + Layout.fillWidth: true + implicitHeight: infoRow.implicitHeight + 28 + radius: 12 + color: "#eff6ff" + border.width: 1 + border.color: "#bfdbfe" + + RowLayout { + id: infoRow + anchors.fill: parent + anchors.margins: 14 + spacing: 12 + + Rectangle { + Layout.preferredWidth: 30 + Layout.preferredHeight: 30 + radius: 15 + color: "#dbeafe" + Controls.Label { + anchors.centerIn: parent + text: infoBox.iconText + color: "#1d4ed8" + font.weight: Font.Bold } } + + Controls.Label { + id: infoLabel + Layout.fillWidth: true + wrapMode: Text.WordWrap + color: "#334155" + font.pixelSize: 13 + lineHeight: 1.2 + } } } - component FieldLabel: RowLayout { - id: fieldLabel - - property string iconName: "dialog-password" - property string labelText: "" - - spacing: 8 - - Kirigami.Icon { - source: fieldLabel.iconName - implicitWidth: 17 - implicitHeight: 17 - color: "#64748b" - } + component ErrorBox: Rectangle { + visible: root.operationError.length > 0 + Layout.fillWidth: true + implicitHeight: errorLabel.implicitHeight + 28 + radius: 12 + color: "#fef2f2" + border.width: 1 + border.color: "#fecaca" Controls.Label { - text: fieldLabel.labelText - color: "#334155" + id: errorLabel + anchors.fill: parent + anchors.margins: 14 + text: root.operationError + wrapMode: Text.WordWrap + color: "#991b1b" font.pixelSize: 13 - font.weight: Font.DemiBold - } - } - - component StepDot: Rectangle { - required property int stepIndex - - width: 34 - height: 34 - radius: 17 - color: stepIndex < root.currentStep ? "#22c55e" - : stepIndex === root.currentStep ? "#ffffff" - : "#17345f" - border.width: stepIndex === root.currentStep ? 2 : 0 - border.color: "#93c5fd" - - Controls.Label { - anchors.centerIn: parent - text: parent.stepIndex < root.currentStep ? "✓" : (parent.stepIndex + 1) - color: parent.stepIndex === root.currentStep ? "#0f2b50" : "white" - font.pixelSize: 14 - font.weight: Font.Bold } } @@ -300,129 +257,102 @@ Kirigami.ApplicationWindow { anchors.fill: parent spacing: 0 - // ----------------------------------------------------- - // Colonne gauche : identité + progression - // ----------------------------------------------------- Rectangle { - Layout.preferredWidth: Math.max(330, root.width * 0.29) + Layout.preferredWidth: Math.max(310, root.width * 0.27) Layout.fillHeight: true - - gradient: Gradient { - GradientStop { position: 0.0; color: "#081a33" } - GradientStop { position: 0.55; color: "#0b2a50" } - GradientStop { position: 1.0; color: "#123d70" } - } + color: "#0f2b50" ColumnLayout { anchors.fill: parent - anchors.margins: 44 - spacing: 0 + anchors.margins: 34 + spacing: 26 RowLayout { - spacing: 14 - + spacing: 12 Rectangle { - width: 54 - height: 54 - radius: 16 + Layout.preferredWidth: 44 + Layout.preferredHeight: 44 + radius: 12 color: "#1d4ed8" - Controls.Label { anchors.centerIn: parent text: "N" color: "white" - font.pixelSize: 25 + font.pixelSize: 21 font.weight: Font.Bold } } - ColumnLayout { spacing: 2 - Controls.Label { text: "NixOS Workstations" color: "white" - font.pixelSize: 19 - font.weight: Font.DemiBold + font.pixelSize: 18 + font.weight: Font.Bold } - Controls.Label { - text: "Configuration du poste" - color: "#a8c1e1" - font.pixelSize: 13 + text: "Provisioning sécurisé" + color: "#b9cee5" + font.pixelSize: 12 } } } - Item { Layout.preferredHeight: 70 } - - Controls.Label { - text: "VOTRE CONFIGURATION" - color: "#7fa4d1" - font.pixelSize: 11 - font.weight: Font.Bold - font.letterSpacing: 1.5 + Rectangle { + Layout.fillWidth: true + implicitHeight: 1 + color: "#28517e" } - Item { Layout.preferredHeight: 26 } + ColumnLayout { + Layout.fillWidth: true + spacing: 8 - Repeater { - model: root.steps + Repeater { + model: root.steps - delegate: Item { - required property int index - required property var modelData + Rectangle { + required property var modelData + required property int index + Layout.fillWidth: true + implicitHeight: 66 + radius: 12 + color: index === root.currentStep ? "#173f6f" : "transparent" - Layout.fillWidth: true - Layout.preferredHeight: 78 - - RowLayout { - anchors.fill: parent - spacing: 16 - - // Colonne de largeur fixe : tous les marqueurs et tous - // les titres commencent exactement au même endroit. - Item { - Layout.preferredWidth: 42 - Layout.fillHeight: true - - StepDot { - anchors.top: parent.top - anchors.horizontalCenter: parent.horizontalCenter - stepIndex: index - } + RowLayout { + anchors.fill: parent + anchors.margins: 10 + spacing: 13 Rectangle { - visible: index < root.steps.length - 1 - anchors.top: parent.top - anchors.topMargin: 34 - anchors.bottom: parent.bottom - anchors.horizontalCenter: parent.horizontalCenter - width: 2 - color: index < root.currentStep ? "#22c55e" : "#28517e" - } - } - - ColumnLayout { - Layout.fillWidth: true - Layout.alignment: Qt.AlignVCenter - spacing: 3 - - Controls.Label { - Layout.fillWidth: true - text: modelData.title - color: index === root.currentStep ? "white" : "#c1d3e8" - font.pixelSize: 15 - font.weight: index === root.currentStep ? Font.DemiBold : Font.Medium - horizontalAlignment: Text.AlignLeft + Layout.preferredWidth: 34 + Layout.preferredHeight: 34 + radius: 17 + color: index < root.currentStep ? "#22c55e" + : index === root.currentStep ? "#ffffff" + : "#28517e" + Controls.Label { + anchors.centerIn: parent + text: index < root.currentStep ? "✓" : (index + 1) + color: index === root.currentStep ? "#0f2b50" : "white" + font.weight: Font.Bold + } } - Controls.Label { + ColumnLayout { Layout.fillWidth: true - text: modelData.subtitle - color: "#7699c2" - font.pixelSize: 12 - horizontalAlignment: Text.AlignLeft + spacing: 2 + Controls.Label { + text: modelData.title + color: "white" + font.pixelSize: 14 + font.weight: index === root.currentStep ? Font.DemiBold : Font.Medium + } + Controls.Label { + text: modelData.subtitle + color: "#b9cee5" + font.pixelSize: 11 + } } } } @@ -431,880 +361,329 @@ Kirigami.ApplicationWindow { Item { Layout.fillHeight: true } - Rectangle { + Controls.Label { + visible: root.closeAttempted Layout.fillWidth: true - implicitHeight: 86 - radius: 14 - color: "#102f55" - border.width: 1 - border.color: "#214b78" - - RowLayout { - anchors.fill: parent - anchors.margins: 16 - spacing: 12 - - Controls.Label { - text: "🛡" - font.pixelSize: 23 - } - - ColumnLayout { - Layout.fillWidth: true - spacing: 3 - - Controls.Label { - text: "Configuration locale" - color: "white" - font.pixelSize: 13 - font.weight: Font.DemiBold - } - - Controls.Label { - Layout.fillWidth: true - text: "Les secrets définitifs ne seront pas enregistrés dans Git." - wrapMode: Text.WordWrap - color: "#91add0" - font.pixelSize: 11 - } - } - } - } - - Item { Layout.preferredHeight: 20 } - - RowLayout { - Layout.alignment: Qt.AlignHCenter - spacing: 6 - - Kirigami.Icon { - source: "lock" - implicitWidth: 14 - implicitHeight: 14 - color: "#6289b6" - } - - Controls.Label { - text: "Assistant obligatoire jusqu'à la fin" - color: "#6289b6" - font.pixelSize: 11 - } + text: "La configuration doit être terminée avant de fermer cet assistant." + wrapMode: Text.WordWrap + color: "#fde68a" + font.pixelSize: 12 } } } - // ----------------------------------------------------- - // Partie droite - // ----------------------------------------------------- Rectangle { - id: contentArea Layout.fillWidth: true Layout.fillHeight: true - color: "#eef2f7" + color: "#f8fafc" ColumnLayout { anchors.fill: parent - anchors.leftMargin: Math.max(48, contentArea.width * 0.07) - anchors.rightMargin: Math.max(48, contentArea.width * 0.07) - anchors.topMargin: 34 - anchors.bottomMargin: 34 - spacing: 20 + anchors.margins: Math.max(34, Math.min(64, parent.width * 0.055)) + spacing: 24 RowLayout { Layout.fillWidth: true - - Rectangle { - implicitWidth: previewLabel.implicitWidth + 28 - implicitHeight: 32 - radius: 16 - color: provisioning.busy ? "#eff6ff" : "#f0fdf4" - border.width: 1 - border.color: provisioning.busy ? "#bfdbfe" : "#bbf7d0" - - Controls.Label { - id: previewLabel - anchors.centerIn: parent - text: provisioning.busy ? "OPÉRATION SÉCURISÉE EN COURS" : "FINALISATION SÉCURISÉE DU POSTE" - color: provisioning.busy ? "#1d4ed8" : "#166534" - font.pixelSize: 11 - font.weight: Font.Bold - } - } - - Item { Layout.fillWidth: true } - - Rectangle { - implicitWidth: lockStatusRow.implicitWidth + 24 - implicitHeight: 34 - radius: 17 - color: root.closeAttempted ? "#fef2f2" : "#f8fafc" - border.width: 1 - border.color: root.closeAttempted ? "#fecaca" : "#cbd5e1" - - RowLayout { - id: lockStatusRow - anchors.centerIn: parent - spacing: 7 - - Kirigami.Icon { - source: root.workflowComplete ? "lock-open" : "lock" - implicitWidth: 16 - implicitHeight: 16 - color: root.closeAttempted ? "#dc2626" : "#64748b" - } - - Controls.Label { - text: root.closeAttempted - ? "Terminez la configuration avant de quitter" - : root.workflowComplete - ? "Configuration complète" - : "Configuration en cours" - color: root.closeAttempted ? "#b91c1c" : "#475569" - font.pixelSize: 11 - font.weight: Font.DemiBold - } + spacing: 10 + Repeater { + model: root.steps.length + Rectangle { + required property int index + Layout.fillWidth: true + implicitHeight: 5 + radius: 3 + color: index <= root.currentStep ? "#2563eb" : "#e2e8f0" } } } - Rectangle { + StackLayout { Layout.fillWidth: true Layout.fillHeight: true - radius: 24 - color: "white" - border.width: 1 - border.color: "#dce3ec" + currentIndex: root.currentStep - ColumnLayout { - anchors.fill: parent - anchors.margins: Math.max(34, Math.min(64, parent.width * 0.055)) - spacing: 24 - - // Petit indicateur de progression supérieur - RowLayout { - Layout.fillWidth: true - spacing: 10 - - Repeater { - model: root.steps.length - - Rectangle { - required property int index - Layout.fillWidth: true - implicitHeight: 5 - radius: 3 - color: index <= root.currentStep ? "#2563eb" : "#e2e8f0" - - Behavior on color { ColorAnimation { duration: 180 } } + // 0 - Bienvenue + Item { + ColumnLayout { + anchors.fill: parent + spacing: 20 + Item { Layout.fillHeight: true } + Controls.Label { + Layout.fillWidth: true + text: "Bienvenue sur votre poste" + color: "#0f172a" + font.pixelSize: 32 + font.weight: Font.Bold + horizontalAlignment: Text.AlignHCenter + } + Controls.Label { + Layout.maximumWidth: 720 + Layout.alignment: Qt.AlignHCenter + text: "Votre espace personnel est déjà protégé par un volume LUKS2. Il reste à remplacer le mot de passe temporaire et à préparer votre YubiKey." + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + color: "#64748b" + font.pixelSize: 16 + lineHeight: 1.25 + } + InfoBox { + Layout.maximumWidth: 720 + Layout.alignment: Qt.AlignHCenter + iconText: "🔐" + text: "À la fin, votre home pourra être déverrouillé soit avec votre mot de passe personnel, soit avec votre YubiKey et son PIN." + } + Item { Layout.fillHeight: true } + PrimaryButton { + Layout.alignment: Qt.AlignHCenter + text: "Commencer" + onClicked: { + root.operationError = "" + root.currentStep = 1 } } } + } - StackLayout { - id: wizard - Layout.fillWidth: true - Layout.fillHeight: true - currentIndex: root.currentStep + // 1 - Mot de passe + Item { + ColumnLayout { + anchors.fill: parent + spacing: 16 - // --------------------------------- - // 0 - Bienvenue - // --------------------------------- - Item { - ColumnLayout { + Controls.Label { + text: "Remplacez le mot de passe temporaire" + color: "#0f172a" + font.pixelSize: 28 + font.weight: Font.Bold + } + Controls.Label { + Layout.fillWidth: true + text: "Ce mot de passe reste votre solution de secours si la YubiKey est oubliée ou indisponible. Le changement est appliqué directement à systemd-homed et au chiffrement LUKS2." + wrapMode: Text.WordWrap + color: "#64748b" + font.pixelSize: 14 + } + + Item { Layout.preferredHeight: 8 } + FieldLabel { text: "Mot de passe temporaire actuel" } + SecretField { id: currentPassword } + + FieldLabel { text: "Nouveau mot de passe" } + SecretField { id: newPassword } + + FieldLabel { text: "Confirmation du nouveau mot de passe" } + SecretField { id: confirmPassword } + + Rectangle { + id: strengthCard + Layout.fillWidth: true + implicitHeight: 52 + radius: 10 + color: "#ffffff" + border.width: 1 + border.color: "#e2e8f0" + property int level: root.passwordStrengthLevel(newPassword.text) + RowLayout { anchors.fill: parent - spacing: 20 - - Item { Layout.fillHeight: true } - - Rectangle { - Layout.preferredWidth: 86 - Layout.preferredHeight: 86 - Layout.alignment: Qt.AlignHCenter - radius: 24 - color: "#eff6ff" - border.width: 1 - border.color: "#bfdbfe" - - Controls.Label { - anchors.centerIn: parent - text: "✦" - color: "#2563eb" - font.pixelSize: 42 - font.weight: Font.Bold - } - } - + anchors.margins: 13 Controls.Label { - Layout.fillWidth: true - text: "Bienvenue sur votre nouveau poste" - color: "#0f172a" - font.pixelSize: 32 - font.weight: Font.Bold - horizontalAlignment: Text.AlignHCenter - } - - Controls.Label { - Layout.maximumWidth: 720 - Layout.alignment: Qt.AlignHCenter - text: "Quelques étapes rapides vont personnaliser vos moyens d'authentification et finaliser la préparation de votre environnement de travail." - wrapMode: Text.WordWrap + text: "Robustesse indicative" color: "#64748b" - font.pixelSize: 16 - lineHeight: 1.25 - horizontalAlignment: Text.AlignHCenter } - - Item { Layout.preferredHeight: 12 } - - RowLayout { - Layout.alignment: Qt.AlignHCenter - spacing: 14 - - Repeater { - model: [ - { "icon": "🔑", "title": "Mot de passe", "text": "Définir votre accès local de secours" }, - { "icon": "🔐", "title": "YubiKey", "text": "Créer votre PIN FIDO2" } - ] - - Rectangle { - required property var modelData - Layout.preferredWidth: 290 - Layout.preferredHeight: 128 - radius: 16 - color: "#f8fafc" - border.width: 1 - border.color: "#e2e8f0" - - RowLayout { - anchors.fill: parent - anchors.margins: 18 - spacing: 14 - - Controls.Label { - text: modelData.icon - font.pixelSize: 28 - } - - ColumnLayout { - Layout.fillWidth: true - spacing: 5 - - Controls.Label { - text: modelData.title - color: "#0f172a" - font.pixelSize: 15 - font.weight: Font.DemiBold - } - - Controls.Label { - Layout.fillWidth: true - text: modelData.text - wrapMode: Text.WordWrap - color: "#64748b" - font.pixelSize: 12 - } - } - } - } - } + Item { Layout.fillWidth: true } + Rectangle { + width: 92 + height: 8 + radius: 4 + color: strengthCard.level > 0 ? root.strengthColor(strengthCard.level) : "#e2e8f0" } - - Item { Layout.fillHeight: true } - - PrimaryButton { - Layout.alignment: Qt.AlignHCenter - text: "Commencer" - onClicked: root.goNext() + Controls.Label { + text: root.strengthLabel(strengthCard.level) + color: root.strengthColor(strengthCard.level) + font.weight: Font.DemiBold } } } - // --------------------------------- - // 1 - Mot de passe - // --------------------------------- - Item { - ColumnLayout { - anchors.fill: parent - spacing: 18 + ErrorBox { } + Item { Layout.fillHeight: true } - RowLayout { - Layout.fillWidth: true - spacing: 20 + PrimaryButton { + Layout.alignment: Qt.AlignRight + text: provisioning.busy ? "Modification…" : "Modifier le mot de passe" + enabled: root.passwordStepValid && !provisioning.busy + onClicked: { + root.operationError = "" + var c = currentPassword.text + var n = newPassword.text + var r = confirmPassword.text + provisioning.changePassword(c, n, r) + currentPassword.text = "" + newPassword.text = "" + confirmPassword.text = "" + } + } + } + } - Rectangle { - Layout.preferredWidth: 80 - Layout.preferredHeight: 80 - radius: 22 - color: "#eff6ff" - border.width: 1 - border.color: "#bfdbfe" + // 2 - PIN YubiKey + Item { + ColumnLayout { + anchors.fill: parent + spacing: 16 - Kirigami.Icon { - anchors.centerIn: parent - source: "dialog-password" - implicitWidth: 38 - implicitHeight: 38 - color: "#2563eb" - } - } + Controls.Label { + text: "Initialisez votre YubiKey vierge" + color: "#0f172a" + font.pixelSize: 28 + font.weight: Font.Bold + } + Controls.Label { + Layout.fillWidth: true + text: "Branchez une seule YubiKey vierge puis choisissez son premier PIN FIDO2. Aucun PIN temporaire n'est utilisé." + wrapMode: Text.WordWrap + color: "#64748b" + font.pixelSize: 14 + } - ColumnLayout { - Layout.fillWidth: true - spacing: 5 + InfoBox { + iconText: "☝" + text: "La clé doit sortir d'un reset FIDO2 et ne posséder aucun PIN. Cette étape ne crée aucun fichier u2f-mappings." + } - Controls.Label { - text: "Mot de passe local" - color: "#0f172a" - font.pixelSize: 29 - font.weight: Font.Bold - } + Item { Layout.preferredHeight: 8 } + FieldLabel { text: "Nouveau PIN FIDO2" } + SecretField { id: newPin } + FieldLabel { text: "Confirmation du PIN" } + SecretField { id: confirmPin } - Controls.Label { - Layout.fillWidth: true - text: "Ce mot de passe reste disponible comme solution de secours si votre YubiKey est momentanément indisponible." - wrapMode: Text.WordWrap - color: "#64748b" - font.pixelSize: 15 - lineHeight: 1.2 - } - } - } + ErrorBox { } + Item { Layout.fillHeight: true } - Rectangle { - Layout.fillWidth: true - implicitHeight: 70 - radius: 12 - color: "#eff6ff" - border.width: 1 - border.color: "#bfdbfe" + PrimaryButton { + Layout.alignment: Qt.AlignRight + text: provisioning.busy ? "Initialisation…" : "Créer le PIN" + enabled: root.pinStepValid && !provisioning.busy + onClicked: { + root.operationError = "" + var p = newPin.text + var r = confirmPin.text + provisioning.initializePin(p, r) + newPin.text = "" + confirmPin.text = "" + } + } + } + } - RowLayout { - anchors.fill: parent - anchors.margins: 16 - spacing: 12 + // 3 - Association FIDO2 au home + Item { + ColumnLayout { + anchors.fill: parent + spacing: 16 - Kirigami.Icon { - source: "emblem-information" - implicitWidth: 22 - implicitHeight: 22 - color: "#2563eb" - } + Controls.Label { + text: "Associez la YubiKey à votre home chiffré" + color: "#0f172a" + font.pixelSize: 28 + font.weight: Font.Bold + } + Controls.Label { + Layout.fillWidth: true + text: "systemd-homed va ajouter la YubiKey comme second moyen de déverrouillage du volume LUKS2. Votre mot de passe personnel reste valide en secours." + wrapMode: Text.WordWrap + color: "#64748b" + font.pixelSize: 14 + } - Controls.Label { - Layout.fillWidth: true - text: "Le changement est effectué localement par le mécanisme NixOS prévu à cet effet. Votre nouveau mot de passe n’est ni enregistré dans Git, ni transmis au service informatique." - wrapMode: Text.WordWrap - color: "#1e40af" - font.pixelSize: 13 - } - } - } + InfoBox { + iconText: "☝" + text: "Saisissez le mot de passe que vous venez de choisir et le PIN de la YubiKey. Touchez la clé à chaque fois qu'elle clignote pendant l'enrôlement. Aucune recovery key n'est demandée dans ce workflow." + } - Item { Layout.preferredHeight: 4 } + Item { Layout.preferredHeight: 8 } + FieldLabel { text: "Votre mot de passe personnel actuel" } + SecretField { id: fidoPassword } + FieldLabel { text: "PIN de la YubiKey" } + SecretField { id: fidoPin } - FieldLabel { - iconName: "dialog-password" - labelText: "Mot de passe temporaire actuel" - } + ErrorBox { } + Item { Layout.fillHeight: true } - SecretField { - id: currentPassword - placeholderText: "Saisissez le mot de passe temporaire" - } + PrimaryButton { + Layout.alignment: Qt.AlignRight + text: provisioning.busy ? "Association…" : "Associer la YubiKey" + enabled: root.fidoStepValid && !provisioning.busy + onClicked: { + root.operationError = "" + var p = fidoPassword.text + var k = fidoPin.text + provisioning.enrollFido(p, k) + fidoPassword.text = "" + fidoPin.text = "" + } + } + } + } - FieldLabel { - iconName: "document-edit" - labelText: "Nouveau mot de passe" - } + // 4 - Terminé + Item { + ColumnLayout { + anchors.fill: parent + spacing: 18 + Item { Layout.fillHeight: true } - SecretField { - id: newPassword - placeholderText: "Choisissez votre mot de passe personnel" - } - - // Indicateur de robustesse purement informatif. - // Il ne participe pas à la condition d'activation - // du bouton Continuer. - Rectangle { - id: strengthCard - Layout.fillWidth: true - implicitHeight: 112 - radius: 12 - color: "#f8fafc" - border.width: 1 - border.color: "#e2e8f0" - - property int strengthLevel: root.passwordStrengthLevel(newPassword.text) - - ColumnLayout { - anchors.fill: parent - anchors.margins: 14 - spacing: 8 - - RowLayout { - Layout.fillWidth: true - spacing: 8 - - Kirigami.Icon { - source: "security-high" - implicitWidth: 17 - implicitHeight: 17 - color: root.passwordStrengthColor(strengthCard.strengthLevel) - } - - Controls.Label { - text: "Robustesse du mot de passe" - color: "#334155" - font.pixelSize: 12 - font.weight: Font.DemiBold - } - - Item { Layout.fillWidth: true } - - Controls.Label { - text: root.passwordStrengthLabel(strengthCard.strengthLevel) - color: root.passwordStrengthColor(strengthCard.strengthLevel) - font.pixelSize: 12 - font.weight: Font.Bold - } - } - - RowLayout { - Layout.fillWidth: true - spacing: 6 - - Repeater { - model: 4 - - Rectangle { - required property int index - Layout.fillWidth: true - implicitHeight: 7 - radius: 4 - color: index < parent.parent.parent.strengthLevel - ? root.passwordStrengthColor(strengthCard.strengthLevel) - : "#e2e8f0" - - Behavior on color { - ColorAnimation { duration: 140 } - } - } - } - } - - Controls.Label { - Layout.fillWidth: true - text: root.passwordStrengthHint(strengthCard.strengthLevel) - wrapMode: Text.WordWrap - color: "#64748b" - font.pixelSize: 11 - } - } - } - - Controls.Label { - text: "Indication uniquement : un mot de passe faible n'est pas bloqué." - color: "#94a3b8" - font.pixelSize: 11 - } - - FieldLabel { - iconName: "dialog-ok-apply" - labelText: "Confirmation" - } - - SecretField { - id: confirmPassword - placeholderText: "Confirmez votre nouveau mot de passe" - } - - Controls.Label { - visible: confirmPassword.text.length > 0 && newPassword.text !== confirmPassword.text - text: "Les deux nouveaux mots de passe ne correspondent pas." - color: "#dc2626" - font.pixelSize: 12 - } - - Rectangle { - visible: root.operationError.length > 0 && root.currentStep === 1 - Layout.fillWidth: true - implicitHeight: errorPasswordRow.implicitHeight + 24 - radius: 12 - color: "#fef2f2" - border.width: 1 - border.color: "#fecaca" - - RowLayout { - id: errorPasswordRow - anchors.fill: parent - anchors.margins: 12 - spacing: 10 - - Kirigami.Icon { - source: "dialog-error" - implicitWidth: 20 - implicitHeight: 20 - color: "#dc2626" - } - - Controls.Label { - Layout.fillWidth: true - text: root.operationError - wrapMode: Text.WordWrap - color: "#991b1b" - font.pixelSize: 12 - } - } - } - - Item { Layout.fillHeight: true } - - RowLayout { - Layout.fillWidth: true - - SecondaryButton { - text: "Retour" - onClicked: root.goBack() - } - - Item { Layout.fillWidth: true } - - PrimaryButton { - text: provisioning.busy ? "Modification…" : "Enregistrer et continuer" - enabled: root.passwordStepValid && !provisioning.busy - opacity: enabled ? 1.0 : 0.45 - onClicked: { - root.operationError = "" - var oldValue = currentPassword.text - var newValue = newPassword.text - var confirmationValue = confirmPassword.text - - provisioning.changePassword(oldValue, newValue, confirmationValue) - - // Les champs graphiques sont vidés immédiatement après transmission - // au backend afin de réduire leur durée de présence en mémoire UI. - currentPassword.text = "" - newPassword.text = "" - confirmPassword.text = "" - } - } - } + Rectangle { + Layout.preferredWidth: 96 + Layout.preferredHeight: 96 + Layout.alignment: Qt.AlignHCenter + radius: 48 + color: "#dcfce7" + border.width: 1 + border.color: "#86efac" + Kirigami.Icon { + anchors.centerIn: parent + source: "dialog-ok-apply" + implicitWidth: 52 + implicitHeight: 52 + color: "#16a34a" } } - // --------------------------------- - // 2 - YubiKey - // --------------------------------- - Item { - ColumnLayout { - anchors.fill: parent - spacing: 18 - - RowLayout { - Layout.fillWidth: true - spacing: 20 - - Rectangle { - Layout.preferredWidth: 80 - Layout.preferredHeight: 80 - radius: 22 - color: "#f0fdf4" - border.width: 1 - border.color: "#bbf7d0" - - Controls.Label { - anchors.centerIn: parent - text: "🔐" - font.pixelSize: 34 - } - } - - ColumnLayout { - Layout.fillWidth: true - spacing: 5 - - Controls.Label { - text: "PIN de votre YubiKey" - color: "#0f172a" - font.pixelSize: 29 - font.weight: Font.Bold - } - - Controls.Label { - Layout.fillWidth: true - text: "Votre YubiKey doit être vierge : aucun PIN FIDO2 n’est configuré. Choisissez maintenant votre PIN personnel." - wrapMode: Text.WordWrap - color: "#64748b" - font.pixelSize: 15 - } - } - } - - Rectangle { - Layout.fillWidth: true - implicitHeight: 78 - radius: 12 - color: "#fffbeb" - border.width: 1 - border.color: "#fde68a" - - RowLayout { - anchors.fill: parent - anchors.margins: 16 - spacing: 12 - - Controls.Label { - text: "⌨" - color: "#b45309" - font.pixelSize: 23 - } - - Controls.Label { - Layout.fillWidth: true - text: "La YubiKey ne doit avoir aucun PIN FIDO2 existant. Le setup crée le premier PIN sans PIN temporaire ni mapping U2F." - wrapMode: Text.WordWrap - color: "#92400e" - font.pixelSize: 13 - } - } - } - - FieldLabel { - iconName: "security-high" - labelText: "Nouveau PIN" - } - - SecretField { - id: newPin - placeholderText: "Nouveau PIN personnel" - } - - FieldLabel { - iconName: "dialog-ok-apply" - labelText: "Confirmation du PIN" - } - - SecretField { - id: confirmPin - placeholderText: "Confirmez le PIN" - } - - Controls.Label { - visible: confirmPin.text.length > 0 && newPin.text !== confirmPin.text - text: "Les deux PIN ne correspondent pas." - color: "#dc2626" - font.pixelSize: 12 - } - - Rectangle { - visible: root.operationError.length > 0 && root.currentStep === 2 - Layout.fillWidth: true - implicitHeight: errorPinRow.implicitHeight + 24 - radius: 12 - color: "#fef2f2" - border.width: 1 - border.color: "#fecaca" - - RowLayout { - id: errorPinRow - anchors.fill: parent - anchors.margins: 12 - spacing: 10 - - Kirigami.Icon { - source: "dialog-error" - implicitWidth: 20 - implicitHeight: 20 - color: "#dc2626" - } - - Controls.Label { - Layout.fillWidth: true - text: root.operationError - wrapMode: Text.WordWrap - color: "#991b1b" - font.pixelSize: 12 - } - } - } - - Item { Layout.fillHeight: true } - - RowLayout { - Layout.fillWidth: true - - SecondaryButton { - visible: !provisioning.passwordDone - text: "Retour" - enabled: !provisioning.busy - onClicked: root.goBack() - } - - Item { Layout.fillWidth: true } - - PrimaryButton { - text: provisioning.busy ? "Configuration…" : "Configurer la YubiKey" - enabled: root.pinStepValid && !provisioning.busy - opacity: enabled ? 1.0 : 0.45 - onClicked: { - root.operationError = "" - var newValue = newPin.text - var confirmationValue = confirmPin.text - - provisioning.initializePin(newValue, confirmationValue) - - newPin.text = "" - confirmPin.text = "" - } - } - } - } + Controls.Label { + Layout.fillWidth: true + text: "Votre poste est prêt" + color: "#0f172a" + font.pixelSize: 32 + font.weight: Font.Bold + horizontalAlignment: Text.AlignHCenter } - // --------------------------------- - // 3 - Terminé - // --------------------------------- - Item { - ColumnLayout { - anchors.fill: parent - spacing: 18 + Controls.Label { + Layout.maximumWidth: 720 + Layout.alignment: Qt.AlignHCenter + text: "Votre mot de passe personnel protège le home LUKS2 et votre YubiKey est désormais enrôlée dans systemd-homed. Vous pourrez ouvrir votre session avec le mot de passe ou avec la YubiKey + PIN." + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + color: "#64748b" + font.pixelSize: 15 + lineHeight: 1.25 + } - Item { Layout.fillHeight: true } + InfoBox { + Layout.maximumWidth: 720 + Layout.alignment: Qt.AlignHCenter + iconText: "✓" + text: "Aucun u2f-mappings n'est utilisé. Les données FIDO2 nécessaires au déverrouillage sont gérées par systemd-homed." + } - Rectangle { - Layout.preferredWidth: 96 - Layout.preferredHeight: 96 - Layout.alignment: Qt.AlignHCenter - radius: 48 - color: "#dcfce7" - border.width: 1 - border.color: "#86efac" + Item { Layout.fillHeight: true } - Kirigami.Icon { - anchors.centerIn: parent - source: "dialog-ok-apply" - implicitWidth: 52 - implicitHeight: 52 - color: "#16a34a" - } - } - - Controls.Label { - Layout.fillWidth: true - text: "Votre poste est prêt" - color: "#0f172a" - font.pixelSize: 32 - font.weight: Font.Bold - horizontalAlignment: Text.AlignHCenter - } - - Controls.Label { - Layout.maximumWidth: 680 - Layout.alignment: Qt.AlignHCenter - text: "Votre mot de passe local de secours et le PIN FIDO2 de votre YubiKey ont été configurés avec succès. Vos nouveaux secrets restent personnels et ne sont pas enregistrés dans Git." - wrapMode: Text.WordWrap - color: "#64748b" - font.pixelSize: 15 - lineHeight: 1.25 - horizontalAlignment: Text.AlignHCenter - } - - Item { Layout.preferredHeight: 12 } - - RowLayout { - Layout.alignment: Qt.AlignHCenter - spacing: 12 - - Repeater { - model: [ - { "icon": "dialog-password", "title": "Mot de passe", "text": "Accès local configuré" }, - { "icon": "security-high", "title": "YubiKey", "text": "PIN FIDO2 initialisé" }, - { "icon": "computer-laptop", "title": "Poste", "text": "Configuration finalisée" } - ] - - Rectangle { - required property var modelData - Layout.preferredWidth: 205 - Layout.preferredHeight: 126 - radius: 16 - color: "#f8fafc" - border.width: 1 - border.color: "#e2e8f0" - - ColumnLayout { - anchors.fill: parent - anchors.margins: 16 - spacing: 7 - - RowLayout { - Layout.fillWidth: true - - Rectangle { - Layout.preferredWidth: 38 - Layout.preferredHeight: 38 - radius: 11 - color: "#eff6ff" - - Kirigami.Icon { - anchors.centerIn: parent - source: modelData.icon - implicitWidth: 21 - implicitHeight: 21 - color: "#2563eb" - } - } - - Item { Layout.fillWidth: true } - - Rectangle { - Layout.preferredWidth: 25 - Layout.preferredHeight: 25 - radius: 13 - color: "#dcfce7" - - Kirigami.Icon { - anchors.centerIn: parent - source: "dialog-ok-apply" - implicitWidth: 15 - implicitHeight: 15 - color: "#16a34a" - } - } - } - - Controls.Label { - text: modelData.title - color: "#0f172a" - font.pixelSize: 14 - font.weight: Font.DemiBold - } - - Controls.Label { - Layout.fillWidth: true - text: modelData.text - color: "#64748b" - font.pixelSize: 11 - wrapMode: Text.WordWrap - } - } - } - } - } - - Item { Layout.fillHeight: true } - - RowLayout { - Layout.alignment: Qt.AlignHCenter - spacing: 12 - - PrimaryButton { - text: "Commencer à travailler" - enabled: root.workflowComplete && !provisioning.busy - opacity: enabled ? 1.0 : 0.45 - onClicked: root.close() - } - } - } + PrimaryButton { + Layout.alignment: Qt.AlignHCenter + text: "Commencer à travailler" + enabled: root.workflowComplete && !provisioning.busy + onClicked: root.close() } } } diff --git a/workstation-setup/src/ProvisioningBackend.cpp b/workstation-setup/src/ProvisioningBackend.cpp index 306f65e..79bb267 100644 --- a/workstation-setup/src/ProvisioningBackend.cpp +++ b/workstation-setup/src/ProvisioningBackend.cpp @@ -17,17 +17,20 @@ #include namespace { -constexpr int kHelperTimeoutMs = 60000; +constexpr int kHelperTimeoutMs = 120000; bool containsLineBreak(const QString &value) { return value.contains(QLatin1Char('\n')) || value.contains(QLatin1Char('\r')); } -QString homeDirectoryForEffectiveUser() +QString effectiveHomeDirectory() { if (passwd *entry = getpwuid(geteuid())) { - return QString::fromLocal8Bit(entry->pw_dir); + const QString home = QString::fromLocal8Bit(entry->pw_dir); + if (!home.isEmpty() && home != QStringLiteral("/")) { + return home; + } } return QDir::homePath(); } @@ -39,12 +42,13 @@ ProvisioningBackend::ProvisioningBackend(QString targetUser, QObject *parent) , m_targetUser(std::move(targetUser)) { refreshState(); - appendLog(QStringLiteral("START currentUser=%1 targetUser=%2 authorized=%3 passwordDone=%4 pinDone=%5") + appendLog(QStringLiteral("START currentUser=%1 targetUser=%2 authorized=%3 passwordDone=%4 pinDone=%5 fidoDone=%6") .arg(m_currentUser, m_targetUser, authorizedUser() ? QStringLiteral("yes") : QStringLiteral("no"), m_passwordDone ? QStringLiteral("yes") : QStringLiteral("no"), - m_pinDone ? QStringLiteral("yes") : QStringLiteral("no"))); + m_pinDone ? QStringLiteral("yes") : QStringLiteral("no"), + m_fidoDone ? QStringLiteral("yes") : QStringLiteral("no"))); } ProvisioningBackend::~ProvisioningBackend() @@ -58,25 +62,22 @@ ProvisioningBackend::~ProvisioningBackend() QString ProvisioningBackend::effectiveUserName() { - const uid_t uid = geteuid(); - if (passwd *entry = getpwuid(uid)) { + if (passwd *entry = getpwuid(geteuid())) { return QString::fromLocal8Bit(entry->pw_name); } return {}; } - bool ProvisioningBackend::authorizedUser() const { if (m_targetUser.isEmpty()) { return false; } - const QByteArray targetName = m_targetUser.toLocal8Bit(); - if (passwd *entry = getpwnam(targetName.constData())) { + const QByteArray name = m_targetUser.toLocal8Bit(); + if (passwd *entry = getpwnam(name.constData())) { return entry->pw_uid == geteuid(); } - return false; } @@ -89,8 +90,7 @@ QString ProvisioningBackend::stateDirectory() const return QDir::cleanPath(candidate + QStringLiteral("/nixos-workstations")); } } - - return QDir::cleanPath(homeDirectoryForEffectiveUser() + return QDir::cleanPath(effectiveHomeDirectory() + QStringLiteral("/.local/state/nixos-workstations")); } @@ -109,15 +109,22 @@ QString ProvisioningBackend::pinMarker() const return stateDirectory() + QStringLiteral("/yubikey-pin-created"); } +QString ProvisioningBackend::fidoMarker() const +{ + return stateDirectory() + QStringLiteral("/yubikey-fido-enrolled"); +} + void ProvisioningBackend::refreshState() { const bool oldPassword = m_passwordDone; const bool oldPin = m_pinDone; + const bool oldFido = m_fidoDone; m_passwordDone = QFileInfo::exists(passwordMarker()); m_pinDone = QFileInfo::exists(pinMarker()); + m_fidoDone = QFileInfo::exists(fidoMarker()); - if (oldPassword != m_passwordDone || oldPin != m_pinDone) { + if (oldPassword != m_passwordDone || oldPin != m_pinDone || oldFido != m_fidoDone) { emit stateChanged(); } } @@ -139,11 +146,10 @@ bool ProvisioningBackend::ensureStateWritable() if (!probe.open(QIODevice::WriteOnly | QIODevice::Truncate)) { return false; } - - const bool writeOk = probe.write("ok\n") == 3; + const bool ok = probe.write("ok\n") == 3; probe.close(); probe.remove(); - return writeOk; + return ok; } bool ProvisioningBackend::createMarker(const QString &path) @@ -157,8 +163,7 @@ bool ProvisioningBackend::createMarker(const QString &path) return false; } - const QByteArray content = - QByteArrayLiteral("completed=") + const QByteArray content = QByteArrayLiteral("completed=") + QDateTime::currentDateTimeUtc().toString(Qt::ISODate).toUtf8() + '\n'; @@ -166,14 +171,12 @@ bool ProvisioningBackend::createMarker(const QString &path) marker.cancelWriting(); return false; } - if (!marker.commit()) { return false; } return QFile::setPermissions(path, - QFileDevice::ReadOwner - | QFileDevice::WriteOwner); + QFileDevice::ReadOwner | QFileDevice::WriteOwner); } void ProvisioningBackend::appendLog(const QString &message) const @@ -182,7 +185,6 @@ void ProvisioningBackend::appendLog(const QString &message) const if (!QDir().mkpath(dir)) { return; } - QFile::setPermissions(dir, QFileDevice::ReadOwner | QFileDevice::WriteOwner @@ -193,17 +195,13 @@ void ProvisioningBackend::appendLog(const QString &message) const return; } - const QByteArray line = - QDateTime::currentDateTimeUtc().toString(Qt::ISODateWithMs).toUtf8() - + QByteArrayLiteral(" ") - + message.toUtf8() - + '\n'; - - log.write(line); + log.write(QDateTime::currentDateTimeUtc().toString(Qt::ISODateWithMs).toUtf8() + + QByteArrayLiteral(" ") + + message.toUtf8() + + '\n'); log.close(); QFile::setPermissions(diagnosticLogPath(), - QFileDevice::ReadOwner - | QFileDevice::WriteOwner); + QFileDevice::ReadOwner | QFileDevice::WriteOwner); } void ProvisioningBackend::setBusy(bool busy) @@ -229,9 +227,30 @@ void ProvisioningBackend::secureClear(QByteArray &data) QString ProvisioningBackend::operationName(Operation operation) { - return operation == Operation::Password - ? QStringLiteral("PASSWORD") - : QStringLiteral("PIN"); + 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::changePassword(const QString ¤tPassword, @@ -239,47 +258,31 @@ void ProvisioningBackend::changePassword(const QString ¤tPassword, const QString &confirmation) { if (!authorizedUser()) { - appendLog(QStringLiteral("PASSWORD refused: unauthorized user")); - emit passwordChangeFinished(false, - QStringLiteral("Cette opération n'est pas autorisée pour cet utilisateur.")); + 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.")); + emit passwordChangeFinished(false, QStringLiteral("Une opération de configuration est déjà en cours.")); return; } - if (m_passwordDone) { - emit passwordChangeFinished(true, - QStringLiteral("Le mot de passe a déjà été personnalisé.")); + 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.")); + 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.")); + 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.")); + 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()) { - 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.")); + emit passwordChangeFinished(false, QStringLiteral("Impossible d'enregistrer l'état de configuration dans votre profil.")); return; } @@ -291,65 +294,75 @@ void ProvisioningBackend::changePassword(const QString ¤tPassword, } void ProvisioningBackend::initializePin(const QString &newPin, - const QString &confirmation) + const QString &confirmation) { if (!authorizedUser()) { - appendLog(QStringLiteral("PIN refused: unauthorized user")); - emit pinChangeFinished(false, - QStringLiteral("Cette opération n'est pas autorisée pour cet utilisateur.")); + 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.")); + emit pinChangeFinished(false, QStringLiteral("Une opération de configuration est déjà en cours.")); return; } - if (!m_passwordDone) { - emit pinChangeFinished(false, - QStringLiteral("Le mot de passe doit être personnalisé avant d'initialiser la YubiKey.")); + emit pinChangeFinished(false, QStringLiteral("Personnalisez d'abord votre mot de passe.")); return; } - if (m_pinDone) { - emit pinChangeFinished(true, - QStringLiteral("Le PIN FIDO2 de la YubiKey a déjà été initialisé.")); + emit pinChangeFinished(true, QStringLiteral("Le PIN FIDO2 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.")); + 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.")); + 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.")); + emit pinChangeFinished(false, QStringLiteral("Le PIN contient un caractère non pris en charge.")); return; } - 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; - } - - // Le helper Étape 1 lit uniquement les deux premières lignes : - // nouveau PIN puis confirmation. Aucun ancien PIN n'est transmis. startHelper(Operation::Pin, QString::fromUtf8(NIXOS_WORKSTATIONS_PIN_HELPER_PATH), newPin, - confirmation, - QString()); + 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 (!m_passwordDone || !m_pinDone) { + emit fidoEnrollmentFinished(false, QStringLiteral("Le mot de passe et le PIN doivent être configurés avant l'association FIDO2.")); + return; + } + if (m_fidoDone) { + emit fidoEnrollmentFinished(true, QStringLiteral("La YubiKey est déjà associée au home chiffré.")); + return; + } + if (currentPassword.isEmpty() || pin.size() < 4) { + emit fidoEnrollmentFinished(false, QStringLiteral("Saisissez votre mot de passe actuel et le 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; + } + + startHelper(Operation::Fido, + QString::fromUtf8(NIXOS_WORKSTATIONS_FIDO_HELPER_PATH), + currentPassword, + pin); } void ProvisioningBackend::startHelper(Operation operation, @@ -360,14 +373,8 @@ void ProvisioningBackend::startHelper(Operation operation, { 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 { - emit pinChangeFinished(false, message); - } + appendLog(operationName(operation) + QStringLiteral(" failed: helper unavailable")); + emitResult(operation, false, QStringLiteral("Le composant système requis est introuvable ou inutilisable.")); return; } @@ -396,17 +403,16 @@ void ProvisioningBackend::startHelper(Operation operation, 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); + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + env.insert(QStringLiteral("LC_ALL"), QStringLiteral("C")); + env.insert(QStringLiteral("LANG"), QStringLiteral("C")); + process->setProcessEnvironment(env); - auto *timeout = new QTimer(process); - timeout->setSingleShot(true); - timeout->setInterval(kHelperTimeoutMs); + auto *timer = new QTimer(process); + timer->setSingleShot(true); + timer->setInterval(kHelperTimeoutMs); - connect(timeout, &QTimer::timeout, process, - [this, process, operation]() { + connect(timer, &QTimer::timeout, process, [this, process, operation]() { if (m_process != process || process->state() == QProcess::NotRunning) { return; } @@ -415,125 +421,101 @@ void ProvisioningBackend::startHelper(Operation operation, process->kill(); }); - connect(process, &QProcess::started, this, - [this, process, timeout, operation]() { + connect(process, &QProcess::started, this, [this, process, timer, operation]() { if (m_process != process) { return; } - - const qint64 expected = m_pendingPayload.size(); - const qint64 written = process->write(m_pendingPayload); + 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(); + timer->start(); }); connect(process, &QProcess::errorOccurred, this, - [this, process, timeout, operation](QProcess::ProcessError error) { + [this, process, timer, operation](QProcess::ProcessError error) { if (m_process != process || error != QProcess::FailedToStart) { return; } - - timeout->stop(); + timer->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 (operation == Operation::Password) { - emit passwordChangeFinished(false, message); - } else { - emit pinChangeFinished(false, message); - } + appendLog(operationName(operation) + QStringLiteral(" failed to start")); + emitResult(operation, false, QStringLiteral("Impossible de démarrer le composant de configuration.")); }); connect(process, qOverload(&QProcess::finished), this, - [this, process, timeout, operation](int exitCode, QProcess::ExitStatus exitStatus) { + [this, process, timer, operation](int exitCode, QProcess::ExitStatus exitStatus) { if (m_process != process) { return; } - timeout->stop(); + timer->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; + QByteArray combined = stderrData + '\n' + stdoutData; const bool timedOut = m_helperTimedOut; const bool helperSucceeded = !timedOut && exitStatus == QProcess::NormalExit && exitCode == 0; - QString message; bool markerCreated = false; + QString message; if (helperSucceeded) { - if (operation == Operation::Password) { + switch (operation) { + case 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 { + ? QStringLiteral("Votre nouveau mot de passe protège maintenant votre home chiffré.") + : 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 PIN FIDO2 de votre YubiKey est maintenant initialisé.") - : QStringLiteral("Le PIN a été initialisé, mais l'état local n'a pas pu être enregistré. Contactez le service informatique avant de fermer la session."); + : 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 déverrouiller votre home chiffré.") + : QStringLiteral("La YubiKey a été enrôlée mais l'état local n'a pas pu être enregistré."); + break; } } else { - message = safeMessageForFailure(operation, exitCode, combinedOutput, timedOut); + message = safeMessageForFailure(operation, exitCode, combined, timedOut); } - QString helperToken = QString::fromUtf8(combinedOutput).trimmed(); - helperToken.remove(QRegularExpression(QStringLiteral("[^A-Za-z0-9_\\-]"))); - if (helperToken.size() > 80) { - helperToken.truncate(80); - } - if (helperToken.isEmpty()) { - helperToken = QStringLiteral("none"); - } + 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 normalExit=%2 timeout=%3 marker=%4 token=%5") + + QStringLiteral(" helper finished exitCode=%1 timeout=%2 marker=%3 token=%4") .arg(exitCode) - .arg(exitStatus == QProcess::NormalExit ? QStringLiteral("yes") : QStringLiteral("no")) .arg(timedOut ? QStringLiteral("yes") : QStringLiteral("no")) .arg(markerCreated ? QStringLiteral("yes") : QStringLiteral("no")) - .arg(helperToken)); + .arg(token)); secureClear(stderrData); secureClear(stdoutData); - secureClear(combinedOutput); + secureClear(combined); m_process = nullptr; process->deleteLater(); setBusy(false); refreshState(); - const bool completed = helperSucceeded && markerCreated; - if (operation == Operation::Password) { - emit passwordChangeFinished(completed, message); - } else { - emit pinChangeFinished(completed, message); - } + emitResult(operation, helperSucceeded && markerCreated, message); }); process->start(); @@ -547,70 +529,58 @@ QString ProvisioningBackend::safeMessageForFailure(Operation operation, const QString output = QString::fromUtf8(outputData).toLower(); if (timedOut || exitCode == 124 || output.contains(QStringLiteral("timeout"))) { - if (operation == Operation::Password) { - if (output.contains(QStringLiteral("timeout_current_prompt"))) { - return QStringLiteral("Le système n'a pas présenté l'invite d'authentification du mot de passe actuel."); - } - if (output.contains(QStringLiteral("timeout_new_prompt"))) { - return QStringLiteral("Le mot de passe actuel a été envoyé, mais le système n'a pas demandé le nouveau mot de passe."); - } - if (output.contains(QStringLiteral("timeout_confirm_prompt"))) { - return QStringLiteral("Le nouveau mot de passe a été envoyé, mais le système n'a pas demandé sa confirmation."); - } - if (output.contains(QStringLiteral("timeout_finish"))) { - return QStringLiteral("Le système a reçu les mots de passe mais n'a pas terminé l'opération."); - } - return QStringLiteral("Le changement de mot de passe a expiré avant la fin de l'opération."); + if (operation == Operation::Fido) { + return QStringLiteral("L'association FIDO2 a expiré. Laissez la YubiKey branchée et touchez-la lorsqu'elle clignote."); } - return QStringLiteral("L'opération YubiKey a expiré. Vérifiez que la clé est connectée puis réessayez."); + return QStringLiteral("L'opération a expiré avant de pouvoir être terminée."); } if (operation == Operation::Password) { - if (output.contains(QStringLiteral("current_rejected")) - || output.contains(QStringLiteral("current_prompt_missing")) - || output.contains(QStringLiteral("current_reprompt"))) { - return QStringLiteral("Le mot de passe temporaire actuel est incorrect ou n'a pas pu être vérifié."); + 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 nouveau mot de passe a été refusé par la politique de sécurité."); } - 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."); + return QStringLiteral("Le mot de passe n'a pas pu être modifié par systemd-homed."); } - if (output.contains(QStringLiteral("pin_already_configured"))) { - return QStringLiteral("Cette YubiKey possède déjà un PIN FIDO2. Pour l’étape 1, utilisez une clé vierge ou réinitialisée ; aucune tentative de PIN n’a été effectuée."); + if (operation == Operation::Pin) { + if (output.contains(QStringLiteral("pin_already_configured"))) { + return QStringLiteral("Cette YubiKey possède déjà un PIN FIDO2. 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 PIN FIDO2 est bloqué. La YubiKey doit être réinitialisée avant provisioning."); + } + if (output.contains(QStringLiteral("multiple_yubikey"))) { + return QStringLiteral("Plusieurs clés FIDO2 sont détectées. Ne laissez branchée que la YubiKey d'Alice."); + } + if (output.contains(QStringLiteral("no_yubikey"))) { + return QStringLiteral("Aucune YubiKey FIDO2 compatible n'a été détectée."); + } + if (output.contains(QStringLiteral("pin_policy"))) { + return QStringLiteral("Le PIN choisi ne respecte pas la politique FIDO2 de la YubiKey."); + } + return QStringLiteral("Le PIN FIDO2 n'a pas pu être initialisé."); } - if (output.contains(QStringLiteral("pin_auth_blocked"))) { - return QStringLiteral("Les opérations PIN sont temporairement bloquées. Débranchez puis rebranchez la YubiKey avant toute nouvelle opération."); + + if (output.contains(QStringLiteral("fido_bad_password"))) { + return QStringLiteral("Le mot de passe personnel fourni est incorrect ou insuffisant pour mettre à jour le home."); } - if (output.contains(QStringLiteral("pin_blocked"))) { - return QStringLiteral("Le PIN FIDO2 de la YubiKey est bloqué. Cette clé n’est pas vierge et ne peut pas être initialisée par l’étape 1."); + if (output.contains(QStringLiteral("fido_bad_pin"))) { + return QStringLiteral("Le PIN de la YubiKey est incorrect. Ne réessayez pas plusieurs fois : vérifiez le PIN avant une nouvelle tentative."); } if (output.contains(QStringLiteral("multiple_yubikey"))) { - return QStringLiteral("Plusieurs clés FIDO2 sont détectées. Ne laissez branchée que la YubiKey à initialiser puis réessayez."); - } - if (output.contains(QStringLiteral("no_fido2"))) { - return QStringLiteral("La clé détectée ne fournit pas l’interface FIDO2 nécessaire."); + return QStringLiteral("Plusieurs clés FIDO2 sont détectées. Ne laissez branchée que la YubiKey d'Alice."); } if (output.contains(QStringLiteral("no_yubikey"))) { - return QStringLiteral("Aucune YubiKey FIDO2 compatible n’a été détectée ou le système n’a pas les droits d’accès nécessaires."); + return QStringLiteral("Aucune YubiKey FIDO2 compatible n'a été détectée."); } - if (output.contains(QStringLiteral("pin_policy"))) { - return QStringLiteral("Le PIN choisi ne respecte pas la politique FIDO2 de cette YubiKey."); + if (output.contains(QStringLiteral("fido_enroll_failed"))) { + return QStringLiteral("La YubiKey n'a pas pu être associée au home. Vérifiez le mot de passe, le PIN et touchez la clé lorsqu'elle clignote."); } - if (output.contains(QStringLiteral("pin_state_not_updated")) - || output.contains(QStringLiteral("pin_state_error"))) { - return QStringLiteral("La commande FIDO2 a été envoyée, mais l’état PIN retourné par la clé est incohérent. Ne réessayez pas avant vérification avec ykman fido info."); - } - if (output.contains(QStringLiteral("input_error"))) { - return QStringLiteral("Le composant d’initialisation de la YubiKey n’a pas reçu les données attendues."); - } - if (output.contains(QStringLiteral("pin_helper_error"))) { - return QStringLiteral("Le composant FIDO2 a rencontré une erreur interne sans pouvoir confirmer l’initialisation du PIN."); - } - - return QStringLiteral("Le PIN FIDO2 n’a pas pu être initialisé. Vérifiez que la YubiKey est vierge, connectée et compatible FIDO2."); + return QStringLiteral("L'association FIDO2 au home chiffré a échoué."); } diff --git a/workstation-setup/src/ProvisioningBackend.h b/workstation-setup/src/ProvisioningBackend.h index 242946c..19274e0 100644 --- a/workstation-setup/src/ProvisioningBackend.h +++ b/workstation-setup/src/ProvisioningBackend.h @@ -12,6 +12,7 @@ class ProvisioningBackend final : public QObject 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 fidoDone READ fidoDone NOTIFY stateChanged) Q_PROPERTY(bool complete READ complete NOTIFY stateChanged) Q_PROPERTY(QString currentUser READ currentUser CONSTANT) Q_PROPERTY(QString targetUser READ targetUser CONSTANT) @@ -24,60 +25,66 @@ public: 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; } + bool fidoDone() const { return m_fidoDone; } + bool complete() const { return m_passwordDone && m_pinDone && m_fidoDone; } QString currentUser() const { return m_currentUser; } QString targetUser() const { return m_targetUser; } bool authorizedUser() const; - 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); - Q_INVOKABLE void initializePin(const QString &newPin, const QString &confirmation); + Q_INVOKABLE void enrollFido(const QString ¤tPassword, + const QString &pin); signals: void busyChanged(); void stateChanged(); void passwordChangeFinished(bool success, const QString &message); void pinChangeFinished(bool success, const QString &message); + void fidoEnrollmentFinished(bool success, const QString &message); private: enum class Operation { Password, - Pin + Pin, + Fido }; + QString stateDirectory() const; + QString diagnosticLogPath() const; + QString passwordMarker() const; + QString pinMarker() const; + QString fidoMarker() const; + + static QString effectiveUserName(); + static void secureClear(QByteArray &data); + static QString operationName(Operation operation); + + void refreshState(); + bool ensureStateWritable(); + bool createMarker(const QString &path); + void appendLog(const QString &message) const; + void setBusy(bool busy); + 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); - void appendLog(const QString &message) const; + const QString &thirdSecret = {}); + void emitResult(Operation operation, bool success, const QString &message); QString safeMessageForFailure(Operation operation, int exitCode, const QByteArray &output, bool timedOut) const; - static void secureClear(QByteArray &data); - static QString operationName(Operation operation); - bool m_busy = false; bool m_passwordDone = false; bool m_pinDone = false; + bool m_fidoDone = false; bool m_helperTimedOut = false; QString m_currentUser; QString m_targetUser;