From f3beb1624c24012c246d17a40c4e10c1c6b3b5b5 Mon Sep 17 00:00:00 2001 From: Thomas Vanbesien Date: Tue, 31 Mar 2026 17:44:35 +0200 Subject: Add passphrase-protected private key support Wire up QOpcUaClient::passwordForPrivateKeyRequired to a QML dialog, mirroring the existing certificate trust flow (local QEventLoop + 30s timeout). --- demo/CMakeLists.txt | 1 + demo/KeyPasswordDialog.qml | 53 ++++++++++++++++++++++++++++++++++++++++++++++ demo/Main.qml | 12 +++++++++++ src/OpcUaClient.cpp | 37 ++++++++++++++++++++++++++++++++ src/OpcUaClient.h | 17 +++++++++++++++ 5 files changed, 120 insertions(+) create mode 100644 demo/KeyPasswordDialog.qml diff --git a/demo/CMakeLists.txt b/demo/CMakeLists.txt index 96dade8..ca39de8 100644 --- a/demo/CMakeLists.txt +++ b/demo/CMakeLists.txt @@ -9,6 +9,7 @@ qt_add_qml_module( QML_FILES Main.qml CertTrustDialog.qml + KeyPasswordDialog.qml ConnectionPage.qml DebugConsole.qml NodePage.qml diff --git a/demo/KeyPasswordDialog.qml b/demo/KeyPasswordDialog.qml new file mode 100644 index 0000000..4bf094c --- /dev/null +++ b/demo/KeyPasswordDialog.qml @@ -0,0 +1,53 @@ +// KeyPasswordDialog.qml — Modal dialog for encrypted private-key passphrase. + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Bobink + +Dialog { + id: keyPasswordDialog + + property string keyFilePath + property bool previousTryWasInvalid: false + + anchors.centerIn: parent + implicitWidth: 400 + modal: true + standardButtons: Dialog.Ok | Dialog.Cancel + title: "Private Key Password" + + onAccepted: Bobink.provideKeyPassword(passwordField.text) + onOpened: { + passwordField.text = ""; + passwordField.forceActiveFocus(); + } + onRejected: Bobink.cancelKeyPassword() + + ColumnLayout { + width: parent.width + + Label { + Layout.fillWidth: true + text: "Enter the password for:\n" + keyPasswordDialog.keyFilePath + wrapMode: Text.Wrap + } + + Label { + Layout.fillWidth: true + color: "red" + text: "Invalid password, please try again." + visible: keyPasswordDialog.previousTryWasInvalid + } + + TextField { + id: passwordField + + Layout.fillWidth: true + echoMode: TextInput.Password + placeholderText: "Password" + + onAccepted: keyPasswordDialog.accept() + } + } +} diff --git a/demo/Main.qml b/demo/Main.qml index 908dcbd..f0f3674 100644 --- a/demo/Main.qml +++ b/demo/Main.qml @@ -44,6 +44,13 @@ ApplicationWindow { debugConsole.appendLog("Discovering: " + Bobink.discovering); } + function onPrivateKeyPasswordRequired(keyFilePath, + previousTryWasInvalid) { + keyPasswordDialog.keyFilePath = keyFilePath; + keyPasswordDialog.previousTryWasInvalid = previousTryWasInvalid; + keyPasswordDialog.open(); + } + function onServersChanged() { debugConsole.appendLog("Discovered server list updated"); } @@ -60,6 +67,11 @@ ApplicationWindow { } + KeyPasswordDialog { + id: keyPasswordDialog + + } + ColumnLayout { anchors.fill: parent spacing: 0 diff --git a/src/OpcUaClient.cpp b/src/OpcUaClient.cpp index a319f29..d212ab2 100644 --- a/src/OpcUaClient.cpp +++ b/src/OpcUaClient.cpp @@ -90,6 +90,8 @@ OpcUaClient::setupClient () &OpcUaClient::handleFindServersFinished); connect (m_client, &QOpcUaClient::errorChanged, this, &OpcUaClient::handleClientError); + connect (m_client, &QOpcUaClient::passwordForPrivateKeyRequired, this, + &OpcUaClient::handlePasswordRequired); } /* ====================================== @@ -326,6 +328,41 @@ OpcUaClient::handleConnectError (QOpcUaErrorState *errorState) } } +void +OpcUaClient::provideKeyPassword (const QString &password) +{ + m_keyPassword = password; + if (m_keyPassLoop) + m_keyPassLoop->quit (); +} + +void +OpcUaClient::cancelKeyPassword () +{ + m_keyPassword.clear (); + if (m_keyPassLoop) + m_keyPassLoop->quit (); +} + +void +OpcUaClient::handlePasswordRequired (QString keyFilePath, QString *password, + bool previousTryWasInvalid) +{ + // passwordForPrivateKeyRequired uses BlockingQueuedConnection — the backend + // thread is blocked waiting for us to return. Spin a local event loop so + // QML can show a dialog and call provideKeyPassword() / cancelKeyPassword(). + m_keyPassword.clear (); + emit privateKeyPasswordRequired (keyFilePath, previousTryWasInvalid); + + QEventLoop loop; + m_keyPassLoop = &loop; + QTimer::singleShot (30000, &loop, &QEventLoop::quit); + loop.exec (); + m_keyPassLoop = nullptr; + + *password = m_keyPassword; +} + void OpcUaClient::handleClientError (QOpcUaClient::ClientError error) { diff --git a/src/OpcUaClient.h b/src/OpcUaClient.h index 1476911..7ecd11b 100644 --- a/src/OpcUaClient.h +++ b/src/OpcUaClient.h @@ -88,6 +88,11 @@ public: /** @brief Reject the pending server certificate. */ Q_INVOKABLE void rejectCertificate (); + /** @brief Provide the password for an encrypted private key. */ + Q_INVOKABLE void provideKeyPassword (const QString &password); + /** @brief Cancel the private-key password prompt (abort connection). */ + Q_INVOKABLE void cancelKeyPassword (); + /* -- Discovery -- */ QString discoveryUrl () const; @@ -136,6 +141,14 @@ signals: void certificateTrustRequested (const QString &certInfo); void connectionError (const QString &message); void statusMessage (const QString &message); + /** + * @brief Emitted when the private key is encrypted. + * + * The connection blocks until provideKeyPassword() or + * cancelKeyPassword() is called (30 s timeout, auto-cancels). + */ + void privateKeyPasswordRequired (const QString &keyFilePath, + bool previousTryWasInvalid); /* -- Discovery -- */ void discoveryUrlChanged (); @@ -156,6 +169,8 @@ private slots: const QUrl &requestUrl); void handleConnectError (QOpcUaErrorState *errorState); void handleClientError (QOpcUaClient::ClientError error); + void handlePasswordRequired (QString keyFilePath, QString *password, + bool previousTryWasInvalid); /* -- Discovery -- */ void handleFindServersFinished ( @@ -174,6 +189,8 @@ private: bool m_connected = false; QEventLoop *m_certLoop = nullptr; bool m_certAccepted = false; + QEventLoop *m_keyPassLoop = nullptr; + QString m_keyPassword; /* -- Discovery -- */ QString m_discoveryUrl; -- cgit v1.2.3