diff options
106 files changed, 9313 insertions, 1893 deletions
diff --git a/.qmake.conf b/.qmake.conf index bbb67e5..b5e35ea 100644 --- a/.qmake.conf +++ b/.qmake.conf @@ -3,4 +3,4 @@ load(qt_build_config) CONFIG += warning_clean DEFINES += QT_NO_FOREACH -MODULE_VERSION = 5.12.5 +MODULE_VERSION = 5.13.1 diff --git a/examples/knx/device/device.pro b/examples/knx/device/device.pro new file mode 100644 index 0000000..c8510f1 --- /dev/null +++ b/examples/knx/device/device.pro @@ -0,0 +1,18 @@ +TEMPLATE = app +TARGET = device +INCLUDEPATH += . + +CONFIG += c++11 +QT += knx widgets network core + +FORMS += mainwindow.ui + +HEADERS += deviceitem.h \ + mainwindow.h + +SOURCES += deviceitem.cpp \ + main.cpp \ + mainwindow.cpp + +target.path = $$[QT_INSTALL_EXAMPLES]/knx/device +INSTALLS += target diff --git a/examples/knx/device/deviceitem.cpp b/examples/knx/device/deviceitem.cpp new file mode 100644 index 0000000..c353d1b --- /dev/null +++ b/examples/knx/device/deviceitem.cpp @@ -0,0 +1,66 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtKnx module. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +******************************************************************************/ + +#include "deviceitem.h" + +DeviceItem::DeviceItem(const QKnxNetIpServerInfo &info) + : QStandardItem(info.deviceName()) + , m_serverInfo(info) +{} + +DeviceItem *DeviceItem::clone() const +{ + return new DeviceItem(m_serverInfo); +} + +const QKnxNetIpServerInfo &DeviceItem::info() const +{ + return m_serverInfo; +} diff --git a/examples/knx/device/deviceitem.h b/examples/knx/device/deviceitem.h new file mode 100644 index 0000000..5f59407 --- /dev/null +++ b/examples/knx/device/deviceitem.h @@ -0,0 +1,70 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtKnx module. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +******************************************************************************/ + +#ifndef DEVICEITEM_H +#define DEVICEITEM_H + +#include <QtGui> +#include <QtKnx> + +class DeviceItem : public QStandardItem +{ +public: + DeviceItem(const QKnxNetIpServerInfo &info); + ~DeviceItem() = default; + + DeviceItem *clone() const override; + const QKnxNetIpServerInfo &info() const; + +private: + QKnxNetIpServerInfo m_serverInfo; +}; + +#endif // DEVICEITEM_H diff --git a/examples/knx/device/main.cpp b/examples/knx/device/main.cpp new file mode 100644 index 0000000..e3e9a74 --- /dev/null +++ b/examples/knx/device/main.cpp @@ -0,0 +1,62 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtKnx module. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +******************************************************************************/ + +#include "mainwindow.h" + +#include <QApplication> + +int main(int argc, char *argv[]) +{ + QApplication a(argc, argv); + MainWindow w; + w.show(); + + return a.exec(); +} diff --git a/examples/knx/device/mainwindow.cpp b/examples/knx/device/mainwindow.cpp new file mode 100644 index 0000000..54d3b6a --- /dev/null +++ b/examples/knx/device/mainwindow.cpp @@ -0,0 +1,387 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtKnx module. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +******************************************************************************/ + +#include "mainwindow.h" +#include "ui_mainwindow.h" + +#include "deviceitem.h" + +MainWindow::MainWindow(QWidget *parent) + : QMainWindow(parent) + , ui(new Ui::MainWindow) +{ + ui->setupUi(this); + + for (int i = 0; i < ui->communication->columnCount(); ++i) + ui->communication->resizeColumnToContents(i); + + setupInterfaces(); + + auto treeWidget = new QTreeWidget; + treeWidget->setHeaderHidden(true); + treeWidget->setUniformRowHeights(true); + + ui->propertyId->setModel(treeWidget->model()); + ui->propertyId->setView(treeWidget); + on_interfaceObjectTypes_currentTextChanged(QStringLiteral("General")); + + populateInterfaceObjectTypesComboBox(); + + connect(ui->actionExit, &QAction::triggered, this, &QApplication::quit); + connect(ui->actionClear, &QAction::triggered, ui->communication, &QTreeWidget::clear); + + connect(&m_management, &QKnxNetIpDeviceManagement::connected, this, &MainWindow::onConnected); + connect(&m_management, &QKnxNetIpDeviceManagement::disconnected, this, &MainWindow::onDisconnected); + connect(&m_management, &QKnxNetIpDeviceManagement::frameReceived, this, &MainWindow::populateFrame); + connect(&m_management, &QKnxNetIpDeviceManagement::errorOccurred, this, &MainWindow::onErrorOccurred); + + connect(ui->communication, &QTreeWidget::currentItemChanged, this, + [&](QTreeWidgetItem *current, QTreeWidgetItem * /* previuos */) { + m_current = current; + }); + + m_discoveryAgent.setTimeout(-1); + m_discoveryAgent.setSearchFrequency(6); + m_discoveryAgent.setDiscoveryMode(QKnxNetIpServerDiscoveryAgent::DiscoveryMode::CoreV1 + | QKnxNetIpServerDiscoveryAgent::DiscoveryMode::CoreV2); + connect(&m_discoveryAgent, &QKnxNetIpServerDiscoveryAgent::deviceDiscovered, + this, &MainWindow::onDeviceDiscovered); + + m_discoveryAgent.start(); +} + +MainWindow::~MainWindow() +{ + m_discoveryAgent.stop(); + + delete ui; + ui = nullptr; + delete m_device; +} + +void MainWindow::onConnected() +{ + toggleUi(true); + ui->connection->setText(tr("Disconnect")); + + m_last = new QTreeWidgetItem(ui->communication, m_last); + m_last->setText(0, tr("Successfully connected to: %1 (%2 : %3)") + .arg(m_device->info().deviceName()) + .arg(m_device->info().controlEndpointAddress().toString()) + .arg(m_device->info().controlEndpointPort())); + m_last->setFirstColumnSpanned(true); +} + +void MainWindow::onDisconnected() +{ + toggleUi(false); + ui->connection->setText(tr("Connect")); + + m_last = new QTreeWidgetItem(ui->communication, m_last); + m_last->setText(0, tr("Successfully disconnected from: %1 (%2 : %3)") + .arg(m_device->info().deviceName()) + .arg(m_device->info().controlEndpointAddress().toString()) + .arg(m_device->info().controlEndpointPort())); + m_last->setFirstColumnSpanned(true); +} + +void MainWindow::onDeviceDiscovered(QKnxNetIpServerInfo info) +{ + if (ui->devices->findText(info.deviceName()) == -1) + qobject_cast<QStandardItemModel*>(ui->devices->model())->appendRow(new DeviceItem(info)); + + if (m_management.state() == QKnxNetIpTunnel::State::Disconnected) + ui->devices->setEnabled(bool(ui->devices->count())); +} + +void MainWindow::onErrorOccurred(QKnxNetIpEndpointConnection::Error error, QString errorString) +{ + m_last = new QTreeWidgetItem(ui->communication, m_last); + const auto metaEnum = QMetaEnum::fromType<QKnxNetIpEndpointConnection::Error>(); + m_last->setText(0, tr("Error occurred: %1 (%2).").arg(errorString) + .arg(metaEnum.valueToKey(int(error)))); + m_last->setFirstColumnSpanned(true); +} + +void MainWindow::on_actionImport_triggered() +{ + const auto fileName = QFileDialog::getOpenFileName(this, tr("Import keyring file"), + QStandardPaths::standardLocations(QStandardPaths::DesktopLocation).value(0), + tr("KNX keyring file (*.knxkeys)")); + + if (fileName.isEmpty()) + return; + + bool ok; + const auto password = QInputDialog::getText(this, tr("Import keyring file"), + tr("Keyring file password:"), QLineEdit::Normal, {}, &ok); + if (!ok || password.isEmpty()) + return; + + m_secureConfigs = QKnxNetIpSecureConfiguration::fromKeyring(QKnxNetIpSecureConfiguration + ::Type::DeviceManagement, fileName, password.toUtf8(), true); + on_devices_currentIndexChanged(ui->devices->currentIndex()); +} + +void MainWindow::on_devices_currentIndexChanged(int /* index */) +{ + delete m_device; + ui->secureConfigs->clear(); + + const auto model = qobject_cast<QStandardItemModel*>(ui->devices->model()); + m_device = static_cast<DeviceItem *> (model->item(ui->devices->currentIndex()))->clone(); + + if (m_device) { + const auto deviceInfo = m_device->info(); + for (int i = 0; i < m_secureConfigs.size(); ++i) { + const auto &config = m_secureConfigs[i]; + if (deviceInfo.individualAddress() != config.host()) + continue; + + const auto ia = config.individualAddress(); + ui->secureConfigs->addItem(tr("User ID: %1 (Individual Address: %2)") + .arg(config.userId()) + .arg(ia.isValid() ? ia.toString() : tr("No specific address")), i); + } + } + + ui->secureConfigs->setEnabled(bool(ui->secureConfigs->count()) + && ui->secureSession->isChecked()); + ui->secureSession->setEnabled(bool(ui->secureConfigs->count())); +} + +void MainWindow::on_interfaceObjectTypes_currentTextChanged(const QString &type) +{ + auto treeWidget = qobject_cast<QTreeWidget *>(ui->propertyId->view()); + if (!treeWidget) + return; + + // keep the 'General' property top level item + auto topLevelItem = treeWidget->takeTopLevelItem(0); + ui->propertyId->clear(); + treeWidget->addTopLevelItem(topLevelItem); + + int index = QKnxInterfaceObjectProperty::staticMetaObject.indexOfEnumerator(type.toLatin1()); + if (index >= 0) { + const auto typeEnum = QKnxInterfaceObjectProperty::staticMetaObject.enumerator(index); + topLevelItem = new QTreeWidgetItem(treeWidget, { typeEnum.name() }); + for (auto a = 0; a < typeEnum.keyCount(); ++a) { + auto item = new QTreeWidgetItem({ typeEnum.key(a) }); + item->setData(0, Qt::UserRole, typeEnum.value(a)); + topLevelItem->addChild(item); + } + topLevelItem->setFlags(topLevelItem->flags() &~Qt::ItemIsSelectable); + } + + // select first sub item + treeWidget->expandItem(topLevelItem); + treeWidget->setCurrentItem(topLevelItem, 0); + ui->propertyId->setRootModelIndex(treeWidget->currentIndex()); + ui->propertyId->setCurrentIndex(0); + treeWidget->setCurrentItem(treeWidget->invisibleRootItem(), 0); + ui->propertyId->setRootModelIndex(treeWidget->currentIndex()); +} + +void MainWindow::on_sendRead_clicked() +{ + const auto frame = QKnxDeviceManagementFrame::propertyReadBuilder() + .setObjectType(ui->interfaceObjectTypes->currentData().toInt()) + .setObjectInstance(quint8(ui->objectInstance->value())) + .setProperty(quint16(ui->propertyId->currentData().toUInt())) + .setNumberOfElements(quint8(ui->numberOfElements->value())) + .setStartIndex(quint16(ui->startIndex->value())) + .createRequest(); + + populateFrame(frame); + m_management.sendFrame(frame); +} + +void MainWindow::on_sendWrite_clicked() +{ + const auto frame = QKnxDeviceManagementFrame::propertyWriteBuilder() + .setObjectType(ui->interfaceObjectTypes->currentData().toInt()) + .setObjectInstance(quint8(ui->objectInstance->value())) + .setProperty(quint16(ui->propertyId->currentData().toUInt())) + .setNumberOfElements(quint8(ui->numberOfElements->value())) + .setStartIndex(quint16(ui->startIndex->value())) + .createRequest(QKnxByteArray::fromHex(ui->data->text().toLatin1())); + + populateFrame(frame); + m_management.sendFrame(frame); +} + +void MainWindow::on_connection_clicked() +{ + if (ui->devices->count() <= 0) + return; + + if (m_management.state() == QKnxNetIpTunnel::State::Connected) + return m_management.disconnectFromHost(); + + const auto list = ui->interfaces->currentData().toStringList(); + m_management.setLocalAddress(QHostAddress(list.first())); + m_management.setSerialNumber(QKnxByteArray::fromHex(list.last().toLatin1())); + + m_last = new QTreeWidgetItem(ui->communication, m_last); + m_last->setText(0, tr("Establish connection to: %1 (%2 : %3)") + .arg(m_device->info().deviceName()) + .arg(m_device->info().controlEndpointAddress().toString()) + .arg(m_device->info().controlEndpointPort())); + m_last->setFirstColumnSpanned(true); + + if (ui->secureSession->isChecked()) { + auto secureConfiguration = m_secureConfigs.value(ui->secureConfigs->currentData().toInt()); + secureConfiguration.setKeepSecureSessionAlive(true); + m_management.setSecureConfiguration(secureConfiguration); + m_management.connectToHostEncrypted(m_device->info().controlEndpointAddress(), + m_device->info().controlEndpointPort()); + } else { + m_management.connectToHost(m_device->info().controlEndpointAddress(), + m_device->info().controlEndpointPort(), QKnxNetIp::HostProtocol::UDP_IPv4); + } +} + +void MainWindow::setupInterfaces() +{ + auto firstItem = new QStandardItem(tr("Interface: --Select One--")); + qobject_cast<QStandardItemModel*>(ui->interfaces->model())->appendRow(firstItem); + firstItem->setSelectable(false); + + const auto interfaces = QNetworkInterface::allInterfaces(); + for (const auto &iface : interfaces) { + const auto addressEntries = iface.addressEntries(); + for (int j = 0; j < addressEntries.size(); j++) { + const auto ip = addressEntries[j].ip(); + if (ip.isLoopback() || ip.toIPv4Address() == 0) + continue; + ui->interfaces->addItem(iface.name() + ": " + ip.toString(), + QStringList { ip.toString(), iface.hardwareAddress().remove(QLatin1String(":")) }); + } + } + ui->interfaces->setCurrentIndex(bool(ui->interfaces->count())); + + connect(ui->interfaces, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [&](int i) { + if (i < 0) + return; + m_discoveryAgent.stop(); + m_discoveryAgent.setLocalAddress(QHostAddress(ui->interfaces->currentData() + .toStringList().first())); + m_discoveryAgent.start(); + }); +} + +void MainWindow::toggleUi(bool value) +{ + ui->interfaceObjectTypes->setEnabled(value); + ui->objectInstance->setEnabled(value); + ui->propertyId->setEnabled(value); + ui->numberOfElements->setEnabled(value); + ui->startIndex->setEnabled(value); + ui->sendRead->setEnabled(value); + ui->data->setEnabled(value); + ui->sendWrite->setEnabled(value); + ui->interfaces->setDisabled(value); + ui->devices->setDisabled(value && bool(ui->devices->count())); +} + +void MainWindow::populateInterfaceObjectTypesComboBox() +{ + const auto typeEnum = QMetaEnum::fromType<QKnxInterfaceObjectType::System>(); + for (int i = 0; i < typeEnum.keyCount(); ++i) + ui->interfaceObjectTypes->addItem(typeEnum.key(i), typeEnum.value(i)); +} + +void MainWindow::populateFrame(const QKnxDeviceManagementFrame &frame) +{ + m_last = new QTreeWidgetItem(ui->communication, m_last); + + if (frame.isNegativeConfirmation()) { + const auto metaEnum = QMetaEnum::fromType<QKnxNetIpCemiServer::Error>(); + m_last->setText(0, tr("Received negative confirmation: Error (%1)") + .arg(metaEnum.valueToKey(int(frame.error())))); + return m_last->setFirstColumnSpanned(true); + } + + auto metaEnum = QMetaEnum::fromType<QKnxDeviceManagementFrame::MessageCode>(); + m_last->setText(1, metaEnum.valueToKey(int(frame.messageCode()))); + + metaEnum = QMetaEnum::fromType<QKnxInterfaceObjectType::System>(); + const auto type = metaEnum.valueToKey(int(frame.objectType())); + m_last->setText(2, QString::fromLatin1(type)); + + m_last->setText(3, QString::number(frame.objectInstance())); + + int index = QKnxInterfaceObjectProperty::staticMetaObject.indexOfEnumerator(type); + if (index >= 0) { + metaEnum = QKnxInterfaceObjectProperty::staticMetaObject.enumerator(index); + m_last->setText(4, metaEnum.valueToKey(int(frame.property()))); + } else { + m_last->setText(4, QString::number(int(frame.property()))); + } + + m_last->setText(5, QString::number(frame.numberOfElements())); + m_last->setText(6, QString::number(frame.startIndex())); + + auto bytes = frame.data().toHex(); + m_last->setText(7, QString::fromLatin1(bytes.toByteArray(), bytes.size())); + bytes = frame.bytes().toHex(); + m_last->setText(8, QString::fromLatin1(bytes.toByteArray(), bytes.size())); + + if (m_current) { + const auto region = ui->communication->viewport()->visibleRegion(); + const auto currentRect = ui->communication->visualItemRect(m_current); + if (region.contains({ currentRect.left(), currentRect.top() - currentRect.height() }) + && region.contains(ui->communication->visualItemRect(m_last))) { + ui->communication->scrollToItem(m_last); + } + } else { + ui->communication->scrollToItem(m_last); + } +} diff --git a/examples/knx/device/mainwindow.h b/examples/knx/device/mainwindow.h new file mode 100644 index 0000000..3676cc9 --- /dev/null +++ b/examples/knx/device/mainwindow.h @@ -0,0 +1,106 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtKnx module. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +******************************************************************************/ + +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include <QtKnx> +#include <QtWidgets> + +QT_BEGIN_NAMESPACE + +namespace Ui { + class MainWindow; +} + +QT_END_NAMESPACE + +class DeviceItem; +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + explicit MainWindow(QWidget *parent = 0); + ~MainWindow(); + +private slots: + void onConnected(); + void onDisconnected(); + void onDeviceDiscovered(QKnxNetIpServerInfo info); + void onErrorOccurred(QKnxNetIpEndpointConnection::Error error, QString errorString); + + void on_actionImport_triggered(); + void on_devices_currentIndexChanged(int index); + void on_interfaceObjectTypes_currentTextChanged(const QString &type); + + void on_sendRead_clicked(); + void on_sendWrite_clicked(); + void on_connection_clicked(); + +private: + void setupInterfaces(); + void toggleUi(bool enable); + void populateInterfaceObjectTypesComboBox(); + void populateFrame(const QKnxDeviceManagementFrame &frame); + +private: + Ui::MainWindow *ui { nullptr }; + + DeviceItem *m_device { nullptr }; + QTreeWidgetItem *m_last { nullptr }; + QTreeWidgetItem *m_current { nullptr }; + + QKnxNetIpDeviceManagement m_management; + QKnxNetIpServerDiscoveryAgent m_discoveryAgent; + QVector<QKnxNetIpSecureConfiguration> m_secureConfigs; +}; + +#endif // MAINWINDOW_H diff --git a/examples/knx/device/mainwindow.ui b/examples/knx/device/mainwindow.ui new file mode 100644 index 0000000..6e5791b --- /dev/null +++ b/examples/knx/device/mainwindow.ui @@ -0,0 +1,469 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>MainWindow</class> + <widget class="QMainWindow" name="MainWindow"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>867</width> + <height>416</height> + </rect> + </property> + <property name="windowTitle"> + <string>KNX local device management example</string> + </property> + <widget class="QWidget" name="centralwidget"> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QGridLayout" name="gridLayout" columnstretch="0,1,0,0,0,1,0,0"> + <item row="0" column="0"> + <widget class="QLabel" name="interfaceObjectTypeLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Interface object type:</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QComboBox" name="interfaceObjectTypes"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </item> + <item row="0" column="2"> + <widget class="QLabel" name="objectInstanceLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Object instance:</string> + </property> + </widget> + </item> + <item row="0" column="3"> + <widget class="QSpinBox" name="objectInstance"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="minimumSize"> + <size> + <width>50</width> + <height>0</height> + </size> + </property> + <property name="minimum"> + <number>1</number> + </property> + <property name="maximum"> + <number>255</number> + </property> + </widget> + </item> + <item row="0" column="4"> + <widget class="QLabel" name="propertyIdLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Property ID:</string> + </property> + </widget> + </item> + <item row="0" column="5"> + <widget class="QComboBox" name="propertyId"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </item> + <item row="0" column="6"> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item row="0" column="7"> + <widget class="QPushButton" name="sendRead"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Read</string> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="numberOfElementsLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Number of elements:</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <item> + <widget class="QSpinBox" name="numberOfElements"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="minimumSize"> + <size> + <width>50</width> + <height>0</height> + </size> + </property> + <property name="minimum"> + <number>1</number> + </property> + <property name="maximum"> + <number>15</number> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="startIndexLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Start index:</string> + </property> + </widget> + </item> + <item> + <widget class="QSpinBox" name="startIndex"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="minimumSize"> + <size> + <width>50</width> + <height>0</height> + </size> + </property> + <property name="maximum"> + <number>4095</number> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + <item row="1" column="2"> + <widget class="QLabel" name="dataLabel"> + <property name="text"> + <string>Data:</string> + </property> + </widget> + </item> + <item row="1" column="3" colspan="3"> + <widget class="QLineEdit" name="data"> + <property name="enabled"> + <bool>false</bool> + </property> + </widget> + </item> + <item row="1" column="7"> + <widget class="QPushButton" name="sendWrite"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Write</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QTreeWidget" name="communication"> + <property name="contextMenuPolicy"> + <enum>Qt::DefaultContextMenu</enum> + </property> + <property name="uniformRowHeights"> + <bool>true</bool> + </property> + <attribute name="headerShowSortIndicator" stdset="0"> + <bool>false</bool> + </attribute> + <column> + <property name="text"> + <string>#</string> + </property> + </column> + <column> + <property name="text"> + <string>Message code</string> + </property> + </column> + <column> + <property name="text"> + <string>Interface object type</string> + </property> + </column> + <column> + <property name="text"> + <string>Object instance</string> + </property> + </column> + <column> + <property name="text"> + <string>Property ID</string> + </property> + </column> + <column> + <property name="text"> + <string>Number of elements</string> + </property> + </column> + <column> + <property name="text"> + <string>Start index</string> + </property> + </column> + <column> + <property name="text"> + <string>Data</string> + </property> + </column> + <column> + <property name="text"> + <string>Frame</string> + </property> + </column> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout" stretch="0,1,0,1,0,1,0,0"> + <item> + <widget class="QLabel" name="interfaceLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Interface:</string> + </property> + </widget> + </item> + <item> + <widget class="QComboBox" name="interfaces"> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="deviceLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Device:</string> + </property> + </widget> + </item> + <item> + <widget class="QComboBox" name="devices"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="secureSession"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>Use secure session</string> + </property> + </widget> + </item> + <item> + <widget class="QComboBox" name="secureConfigs"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </item> + <item> + <spacer name="spacerRight"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeType"> + <enum>QSizePolicy::Expanding</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="connection"> + <property name="text"> + <string>Connect</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <widget class="QMenuBar" name="menuBar"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>867</width> + <height>21</height> + </rect> + </property> + <widget class="QMenu" name="menuFile"> + <property name="title"> + <string>File</string> + </property> + <widget class="QMenu" name="menuImport"> + <property name="title"> + <string>Import</string> + </property> + <addaction name="actionImport"/> + </widget> + <addaction name="menuImport"/> + <addaction name="separator"/> + <addaction name="actionExit"/> + </widget> + <widget class="QMenu" name="menuEdit"> + <property name="title"> + <string>Edit</string> + </property> + <addaction name="actionClear"/> + </widget> + <addaction name="menuFile"/> + <addaction name="menuEdit"/> + </widget> + <action name="actionExit"> + <property name="text"> + <string>Exit</string> + </property> + </action> + <action name="actionImport"> + <property name="text"> + <string>KNX keyring file...</string> + </property> + </action> + <action name="actionClear"> + <property name="text"> + <string>Clear telegram list</string> + </property> + </action> + </widget> + <resources/> + <connections> + <connection> + <sender>secureSession</sender> + <signal>toggled(bool)</signal> + <receiver>secureConfigs</receiver> + <slot>setEnabled(bool)</slot> + <hints> + <hint type="sourcelabel"> + <x>516</x> + <y>391</y> + </hint> + <hint type="destinationlabel"> + <x>634</x> + <y>393</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/examples/knx/discoverer/main.cpp b/examples/knx/discoverer/main.cpp index cce5a52..3cf564b 100644 --- a/examples/knx/discoverer/main.cpp +++ b/examples/knx/discoverer/main.cpp @@ -58,7 +58,7 @@ #include <QtNetwork/QNetworkInterface> -static QString familieToString(QKnxNetIp::ServiceFamily id) +static QString familyToString(QKnxNetIp::ServiceFamily id) { switch (id) { case QKnxNetIp::ServiceFamily::Core: @@ -214,7 +214,7 @@ int main(int argc, char *argv[]) qInfo().noquote() << QString::fromLatin1(" Supported services:"); for (const auto service : services) { qInfo().noquote() << QString::fromLatin1(" KNXnet/IP %1, Version: %2") - .arg(familieToString(service.ServiceFamily)).arg(service.ServiceFamilyVersion); + .arg(familyToString(service.ServiceFamily)).arg(service.ServiceFamilyVersion); } const auto dib = server.extendedHardware(); diff --git a/examples/knx/doc/device.qdoc b/examples/knx/doc/device.qdoc new file mode 100644 index 0000000..33cd8e6 --- /dev/null +++ b/examples/knx/doc/device.qdoc @@ -0,0 +1,165 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the documentation of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:FDL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Free Documentation License Usage +** Alternatively, this file may be used under the terms of the GNU Free +** Documentation License version 1.3 as published by the Free Software +** Foundation and appearing in the file included in the packaging of +** this file. Please review the following information to ensure +** the GNU Free Documentation License version 1.3 requirements +** will be met: https://www.gnu.org/licenses/fdl-1.3.html. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +/*! + \example device + \title KNX Local Device Management Example + \ingroup qtknx-examples + + \brief A KNX client for handling KNXnet/IP local device management. + + \image device.png "KNX local device management example" + + The KNX local device management example user interface contains + various Qt Widgets, most prominently a \l QTreeWidget to display + detailed information about sent and received KNX property read, + write, and confirmation messages. + + To get started, users select one of the network interfaces on their + machine in the \uicontrol {Interface} field. Once that is done, the + application automatically performs a continuous search for available + KNXnet/IP devices and displays the results in the \uicontrol {Device} + field. + + To connect to a KNXnet/IP device, either the one preselected in the + \uicontrol {Device} can be used or a different one must be chosen from + the list of discovered devices. + + The application also supports KNXnet/IP secure devices, but to be able to + connect to such a device, a KNX ETS keyring file needs to be imported via + the \uicontrol {File} menu. + + Once a connection is successfully established, the user has the + possibility to issue a property read or write command to an existing + \c {Interface Object} in the management server. The example currently + limits the possible read or write operations that can be used to + \c {M_PropRead.req} and \c {m_PropWrite.req} messages. + + The application consists of two classes: + + \list + \li \c MainWindow is a \l QMainWindow that renders the general layout + of the application. + \li \c DeviceItem is a \l QStandardItem that is used to display + and store information about discovered KNXnet/IP devices. + \endlist + + \section1 Main Window Class Definition and Implementation + + \quotefromfile device/mainwindow.h + \skipto class MainWindow : + \printuntil /^\}/ + + The \c MainWindow class uses a \l QKnxNetIpServerDiscoveryAgent instance + that allows discovering KNXnet/IP servers by sending continuous search + requests to the network that the client is connected to. + It also saves an instance of the \l QKnxNetIpDeviceManagement used to + establish the connection to the KNXnet/Ip device and a list of imported + KNX \l QKnxNetIpSecureConfiguration secure configurations. + + There are signal handlers installed for every signal emitted by the + \l QKnxNetIpDeviceManagement. Here is an example of the setup capturing the + signals emitted when an event occurs targeting the KNXnet/IP connection. + In this specific example, we will also see how to set up the KNXnet/IP + device management connection and connect to the KNXnet/IP device: + + \quotefromfile device/mainwindow.cpp + \skipto MainWindow::MainWindow + \printuntil { + \dots + \skipto QKnxNetIpDeviceManagement::connected + \printuntil MainWindow::onErrorOccurred); + \dots + \skipto /^\}/ + \printuntil /^\}/ + \skipto void MainWindow::on_connection_clicked + \printuntil /^\}/ + + The \c QKnxNetIpServerDiscoveryAgent is initialized and started after + the user interface has been set up and the necessary device management + connections have been made. Here is the code snippet doing it: + + \quotefromfile device/mainwindow.cpp + \skipto MainWindow::MainWindow + \printuntil { + \dots + \skipto m_discoveryAgent.setTimeout + \printuntil /^\}/ + + There is a signal handler installed for the device discovered signal + emitted by the discovery agent. + When the signal \l QKnxNetIpServerDiscoveryAgent::deviceDiscovered is + triggered, the function \c MainWindow::onDeviceDiscovered() is called. + It adds a new device item to the \uicontrol {Device} if it is not already + there. + + \quotefromfile device/mainwindow.cpp + \skipto void MainWindow::onDeviceDiscovered + \printuntil /^\}/ + + At this point, users can select one of the available devices to establish + a connection, create and send the different types of management frames. The + \c MainWindow::on_devices_currentIndexChanged method saves the selected + KNXnet/IP device in the the \c MainWindow instance. + + To populate the \uicontrol {Interface object type} drop-down box with + available KNX interface object types, the function + \c MainWindow::populateInterfaceObjectTypesComboBox is invoked. When the + application user switches the currently selected object type to a different + one, the \uicontrol {Property ID} gets updated with matching property IDs + for the active interface object: + + \quotefromfile device/mainwindow.cpp + \skipto MainWindow::on_interfaceObjectTypes_currentTextChanged + \printuntil { + \dots + \skipto QKnxInterfaceObjectProperty::staticMetaObject.indexOfEnumerator + \printuntil topLevelItem->setFlags + \skipto /^ {4}\}/ + \printuntil /^ {4}\}/ + \dots + \skipto /^\}/ + \printuntil /^\}/ + + In this last example, when the user triggers the \uicontrol {Read} button, + the function \c MainWindow::on_sendRead_clicked() is called and a frame is + sent to the KNX network to trigger a property read from the given interface + object type and for selected property ID: + + \quotefromfile device/mainwindow.cpp + \skipto MainWindow::on_sendRead_clicked + \printuntil /^\}/ + + \section1 The Main Function + + The KNX local device management example \c main() function does not have + any special handling. It looks like the main function for any Qt app: + + \quotefromfile group/main.cpp + \skipto #include + \printuntil /^\}/ +*/ diff --git a/examples/knx/doc/feature.qdoc b/examples/knx/doc/feature.qdoc new file mode 100644 index 0000000..8daa8e5 --- /dev/null +++ b/examples/knx/doc/feature.qdoc @@ -0,0 +1,144 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the documentation of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:FDL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Free Documentation License Usage +** Alternatively, this file may be used under the terms of the GNU Free +** Documentation License version 1.3 as published by the Free Software +** Foundation and appearing in the file included in the packaging of +** this file. Please review the following information to ensure +** the GNU Free Documentation License version 1.3 requirements +** will be met: https://www.gnu.org/licenses/fdl-1.3.html. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +/*! + \example feature + \title KNX Tunneling Features Example + \ingroup qtknx-examples + + \brief A KNX client for handling KNXnet/IP tunneling features. + + \image features.png "KNX tunneling features example" + + The \e{KNX Tunneling Features} user interface contains various + Qt Widgets, most prominently a \l QTreeWidget to display detailed + information about sent and received KNX tunneling feature messages. + + To get started, users select one of the network interfaces on their + machine in the \uicontrol {Interface} field. Once that is done, the + application automatically performs a continuous search for available + KNXnet/IP devices and displays the results in the \uicontrol {Device} + field. + + To connect to a KKXnet/IP device, either the one preselected in the + \uicontrol {Device} can be used or a different one must be chosen from + the list of discovered devices. + + The application also supports KNXnet/IP secure devices, but to be able to + connect to such a device, a KNX ETS keyring file needs to be imported via + the \uicontrol {File} menu. + + Once a connection is successfully established, the user has the possibility + to send tunneling feature get or set commands and to monitor incoming tunnel + feature info messages. + + The application consists of two classes: + + \list + \li \c MainWindow is a \l QMainWindow that renders the general layout + of the application. + \li \c DeviceItem is a \l QStandardItem that is used to display + and store information about discovered KNXnet/IP devices. + \endlist + + \section1 Main Window Class Definition and Implementation + + \quotefromfile feature/mainwindow.h + \skipto class MainWindow : + \printuntil /^\}/ + + The \c MainWindow class uses a \l QKnxNetIpServerDiscoveryAgent instance + that allows discovering KNXnet/IP servers by sending continuous search + requests to the network that the client is connected to. It also saves an + instance of the \l QKnxNetIpTunnel used to establish the connection to the + KNX network and a list of imported KNX \l QKnxNetIpSecureConfiguration + secure configurations. + + There are signal handlers installed for every signal emitted by the + \l QKnxNetIpTunnel. Here is an example of the setup capturing the + signals emitted when an event occurs targeting the KNXnet/IP connection. + In this specific example, we will also see how to set up the KNXnet/IP + tunnel and connect to the KNXnet/IP device: + + \quotefromfile feature/mainwindow.cpp + \skipto MainWindow::MainWindow + \printuntil { + \dots + \skipto QKnxNetIpTunnel::connected + \printuntil MainWindow::onErrorOccurred); + \dots + \skipto /^\}/ + \printuntil /^\}/ + \skipto void MainWindow::on_connection_clicked + \printuntil /^\}/ + + The \c QKnxNetIpServerDiscoveryAgent is initialized and started after the + user interface has been set up and the necessary tunnel connections have + been made. Here is the code snippet doing it: + + \quotefromfile feature/mainwindow.cpp + \skipto MainWindow::MainWindow + \printuntil { + \dots + \skipto m_discoveryAgent.setTimeout + \printuntil /^\}/ + + There is a signal handler installed for the device discovered signal + emitted by the discovery agent. + When the signal \l QKnxNetIpServerDiscoveryAgent::deviceDiscovered is + triggered, the function \c MainWindow::onDeviceDiscovered() is called. + It adds a new device item to the \uicontrol {Device} if it is not already + there. + + \quotefromfile device/mainwindow.cpp + \skipto void MainWindow::onDeviceDiscovered + \printuntil /^\}/ + + At this point, users can select one of the available devices to establish + a connection, create and send the different types of frames or monitor the + KNX tunneling feature info messages. + The \c MainWindow::on_devices_currentIndexChanged method saves the selected + KNXnet/IP device in the the \c MainWindow instance. + + In this last example, after the user has triggered the \uicontrol {Read} + button and a valid tunneling feature response was received, the function + \c MainWindow::setText() is called and the frame content gets extracted + and visually processed: + + \quotefromfile feature/mainwindow.cpp + \skipto MainWindow::setText + \printuntil /^\}/ + + \section1 The Main Function + + The KNX tunneling feature example \c main() function does not have any + special handling. It looks like the main function for any Qt application: + + \quotefromfile feature/main.cpp + \skipto #include + \printuntil /^\}/ +*/ diff --git a/examples/knx/doc/group.qdoc b/examples/knx/doc/group.qdoc new file mode 100644 index 0000000..9d9dd15 --- /dev/null +++ b/examples/knx/doc/group.qdoc @@ -0,0 +1,147 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the documentation of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:FDL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Free Documentation License Usage +** Alternatively, this file may be used under the terms of the GNU Free +** Documentation License version 1.3 as published by the Free Software +** Foundation and appearing in the file included in the packaging of +** this file. Please review the following information to ensure +** the GNU Free Documentation License version 1.3 requirements +** will be met: https://www.gnu.org/licenses/fdl-1.3.html. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +/*! + \example group + \title KNX Group Communication Example + \ingroup qtknx-examples + + \brief A KNX client for handling KNXnet/IP group communication. + + \image group.png "KNX group communication example" + + The KNX group communication example user interface contains various + Qt Widgets, most prominently a \l QTreeWidget to display detailed + information about sent and received KNX group messages. + + To get started, users select one of the network interfaces on their + machine in the \uicontrol {Interface} field. Once that is done, the + application automatically performs a continuous search for available + KNXnet/IP devices and displays the results in the \uicontrol {Device} + field. + + To connect to a KKXnet/IP device, either the one preselected in the + \uicontrol {Device} can be used or a different one must be chosen from + the list of discovered devices. + + The application also supports KNXnet/IP secure devices, but to be able to + connect to such a device a KNX ETS keyring file needs to be imported via + the \uicontrol {File} menu. + + Once a connection is successfully established, the user has the + possibility to just monitor KNX group messages or issue a read or write + command to an existing KNX group address. The example currently limits + the possible datapoint types that can be used to DPT1. + + The application consists of three classes: + + \list + \li \c MainWindow is a \l QMainWindow that renders the general layout + of the application. + \li \c GroupAddressValidator is a \l QValidator that provides + validation of group address input text. + \li \c DeviceItem is a \l QStandardItem that is used to display + and store information about discovered KNXnet/IP devices. + \endlist + + \section1 Main Window Class Definition and Implementation + + \quotefromfile group/mainwindow.h + \skipto class MainWindow : + \printuntil /^\}/ + + The \c MainWindow class uses a \l QKnxNetIpServerDiscoveryAgent instance + that allows discovering KNXnet/IP servers by sending continuous search + requests to the network that the client is connected to. It also saves an + instance of the \l QKnxNetIpTunnel used to establish the connection to the + KNX network and a list of imported KNX \l QKnxNetIpSecureConfiguration + secure configurations. + + There are signal handlers installed for every signal emitted by the + \l QKnxNetIpTunnel. Here is an example of the setup capturing the + signals emitted when an event occurs targeting the KNXnet/IP connection. + In this specific example, we will also see how to set up the KNXnet/IP + tunnel and connect to the KNXnet/IP device: + + \quotefromfile group/mainwindow.cpp + \skipto MainWindow::MainWindow + \printuntil { + \dots + \skipto QKnxNetIpTunnel::connected + \printuntil MainWindow::onErrorOccurred); + \dots + \skipto /^\}/ + \printuntil /^\}/ + \skipto void MainWindow::on_connection_clicked + \printuntil /^\}/ + + The \c QKnxNetIpServerDiscoveryAgent is initialized and started after the + user interface has been set up and the necessary tunnel connections have + been made. Here is the code snippet doing it: + + \quotefromfile group/mainwindow.cpp + \skipto MainWindow::MainWindow + \printuntil { + \dots + \skipto m_discoveryAgent.setTimeout + \printuntil /^\}/ + + There is a signal handler installed for the device discovered signal + emitted by the discovery agent. + When the signal \l QKnxNetIpServerDiscoveryAgent::deviceDiscovered is + triggered, the function \c MainWindow::onDeviceDiscovered() is called. + It adds a new device item to the \uicontrol {Device} if it is not already + there. + + \quotefromfile device/mainwindow.cpp + \skipto void MainWindow::onDeviceDiscovered + \printuntil /^\}/ + + At this point, users can select one of the available devices to establish + a connection, create and send the different types of frames or simply + monitor the KNX bus traffic. + The \c MainWindow::on_devices_currentIndexChanged method saves the selected + KNXnet/IP device in the the \c MainWindow instance. + + In this last example, when the user triggers the \uicontrol {Read} button, + the function \c MainWindow::on_sendRead_clicked() is called and a frame is + sent to the KNX network to trigger a group value read for the given group + address: + + \quotefromfile group/mainwindow.cpp + \skipto MainWindow::on_sendRead_clicked + \printuntil /^\}/ + + \section1 The Main Function + + The KNX group communication example \c main() function does not have any + special handling. It looks like the main function for any Qt app: + + \quotefromfile group/main.cpp + \skipto #include + \printuntil /^\}/ +*/ diff --git a/examples/knx/doc/images/device.png b/examples/knx/doc/images/device.png Binary files differnew file mode 100644 index 0000000..7df2d17 --- /dev/null +++ b/examples/knx/doc/images/device.png diff --git a/examples/knx/doc/images/features.png b/examples/knx/doc/images/features.png Binary files differnew file mode 100644 index 0000000..b4e0f25 --- /dev/null +++ b/examples/knx/doc/images/features.png diff --git a/examples/knx/doc/images/group.png b/examples/knx/doc/images/group.png Binary files differnew file mode 100644 index 0000000..849e835 --- /dev/null +++ b/examples/knx/doc/images/group.png diff --git a/examples/knx/feature/deviceitem.cpp b/examples/knx/feature/deviceitem.cpp new file mode 100644 index 0000000..c353d1b --- /dev/null +++ b/examples/knx/feature/deviceitem.cpp @@ -0,0 +1,66 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtKnx module. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +******************************************************************************/ + +#include "deviceitem.h" + +DeviceItem::DeviceItem(const QKnxNetIpServerInfo &info) + : QStandardItem(info.deviceName()) + , m_serverInfo(info) +{} + +DeviceItem *DeviceItem::clone() const +{ + return new DeviceItem(m_serverInfo); +} + +const QKnxNetIpServerInfo &DeviceItem::info() const +{ + return m_serverInfo; +} diff --git a/examples/knx/feature/deviceitem.h b/examples/knx/feature/deviceitem.h new file mode 100644 index 0000000..5f59407 --- /dev/null +++ b/examples/knx/feature/deviceitem.h @@ -0,0 +1,70 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtKnx module. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +******************************************************************************/ + +#ifndef DEVICEITEM_H +#define DEVICEITEM_H + +#include <QtGui> +#include <QtKnx> + +class DeviceItem : public QStandardItem +{ +public: + DeviceItem(const QKnxNetIpServerInfo &info); + ~DeviceItem() = default; + + DeviceItem *clone() const override; + const QKnxNetIpServerInfo &info() const; + +private: + QKnxNetIpServerInfo m_serverInfo; +}; + +#endif // DEVICEITEM_H diff --git a/examples/knx/feature/feature.pro b/examples/knx/feature/feature.pro new file mode 100644 index 0000000..f75ad7a --- /dev/null +++ b/examples/knx/feature/feature.pro @@ -0,0 +1,18 @@ +TEMPLATE = app +TARGET = feature +INCLUDEPATH += . + +CONFIG += c++11 +QT += knx widgets network core + +FORMS += mainwindow.ui + +HEADERS += deviceitem.h \ + mainwindow.h + +SOURCES += deviceitem.cpp \ + main.cpp \ + mainwindow.cpp + +target.path = $$[QT_INSTALL_EXAMPLES]/knx/feature +INSTALLS += target diff --git a/examples/knx/feature/main.cpp b/examples/knx/feature/main.cpp new file mode 100644 index 0000000..e3e9a74 --- /dev/null +++ b/examples/knx/feature/main.cpp @@ -0,0 +1,62 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtKnx module. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +******************************************************************************/ + +#include "mainwindow.h" + +#include <QApplication> + +int main(int argc, char *argv[]) +{ + QApplication a(argc, argv); + MainWindow w; + w.show(); + + return a.exec(); +} diff --git a/examples/knx/feature/mainwindow.cpp b/examples/knx/feature/mainwindow.cpp new file mode 100644 index 0000000..7e44327 --- /dev/null +++ b/examples/knx/feature/mainwindow.cpp @@ -0,0 +1,401 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtKnx module. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +******************************************************************************/ + +#include "mainwindow.h" +#include "ui_mainwindow.h" + +#include "deviceitem.h" + +MainWindow::MainWindow(QWidget *parent) + : QMainWindow(parent) + , ui(new Ui::MainWindow) +{ + ui->setupUi(this); + + for (int i = 0; i < ui->communication->columnCount(); ++i) + ui->communication->resizeColumnToContents(i); + + setupInterfaces(); + populateServiceTypesComboBox(); + populateInterfaceFeaturesComboBox(); + + connect(ui->actionExit, &QAction::triggered, this, &QApplication::quit); + connect(ui->actionClear, &QAction::triggered, ui->communication, &QTreeWidget::clear); + + connect(&m_tunnel, &QKnxNetIpTunnel::connected, this, &MainWindow::onConnected); + connect(&m_tunnel, &QKnxNetIpTunnel::disconnected, this, &MainWindow::onDisconnected); + connect(&m_tunnel, &QKnxNetIpTunnel::tunnelingFeatureInfoReceived, this, + &MainWindow::onFeatureInfoReceived); + connect(&m_tunnel, &QKnxNetIpTunnel::tunnelingFeatureResponseReceived, this, + &MainWindow::onFeatureResponseReceived); + connect(&m_tunnel, &QKnxNetIpTunnel::errorOccurred, this, &MainWindow::onErrorOccurred); + + connect(ui->communication, &QTreeWidget::currentItemChanged, this, + [&](QTreeWidgetItem *current, QTreeWidgetItem * /* previuos */) { + m_current = current; + }); + + m_discoveryAgent.setTimeout(-1); + m_discoveryAgent.setSearchFrequency(6); + connect(&m_discoveryAgent, &QKnxNetIpServerDiscoveryAgent::deviceDiscovered, this, + &MainWindow::onDeviceDiscovered); + m_discoveryAgent.setDiscoveryMode(QKnxNetIpServerDiscoveryAgent::DiscoveryMode::CoreV2); + + m_discoveryAgent.start(); +} + +MainWindow::~MainWindow() +{ + m_discoveryAgent.stop(); + + delete ui; + ui = nullptr; + delete m_device; +} + +void MainWindow::onConnected() +{ + toggleUi(true); + ui->connection->setText(tr("Disconnect")); + + m_last = new QTreeWidgetItem(ui->communication, m_last); + m_last->setText(0, tr("Successfully connected to: %1 (%2 : %3)") + .arg(m_device->info().deviceName()) + .arg(m_device->info().controlEndpointAddress().toString()) + .arg(m_device->info().controlEndpointPort())); + m_last->setFirstColumnSpanned(true); +} + +void MainWindow::onDisconnected() +{ + toggleUi(false); + ui->connection->setText(tr("Connect")); + + m_last = new QTreeWidgetItem(ui->communication, m_last); + m_last->setText(0, tr("Successfully disconnected from: %1 (%2 : %3)") + .arg(m_device->info().deviceName()) + .arg(m_device->info().controlEndpointAddress().toString()) + .arg(m_device->info().controlEndpointPort())); + m_last->setFirstColumnSpanned(true); +} + +void MainWindow::onDeviceDiscovered(QKnxNetIpServerInfo info) +{ + if (ui->devices->findText(info.deviceName()) == -1) + qobject_cast<QStandardItemModel*>(ui->devices->model())->appendRow(new DeviceItem(info)); + + if (m_tunnel.state() == QKnxNetIpTunnel::State::Disconnected) + ui->devices->setEnabled(bool(ui->devices->count())); +} + +void MainWindow::onErrorOccurred(QKnxNetIpEndpointConnection::Error error, QString errorString) +{ + m_last = new QTreeWidgetItem(ui->communication, m_last); + const auto metaEnum = QMetaEnum::fromType<QKnxNetIpEndpointConnection::Error>(); + m_last->setText(0, tr("Error occurred: %1 (%2).").arg(errorString) + .arg(metaEnum.valueToKey(int(error)))); + m_last->setFirstColumnSpanned(true); +} + +void MainWindow::on_actionImport_triggered() +{ + const auto fileName = QFileDialog::getOpenFileName(this, tr("Import keyring file"), + QStandardPaths::standardLocations(QStandardPaths::DesktopLocation).value(0), + tr("KNX keyring file (*.knxkeys)")); + + if (fileName.isEmpty()) + return; + + bool ok; + const auto password = QInputDialog::getText(this, tr("Import keyring file"), + tr("Keyring file password:"), QLineEdit::Normal, {}, &ok); + if (!ok || password.isEmpty()) + return; + + m_secureConfigs = QKnxNetIpSecureConfiguration::fromKeyring(QKnxNetIpSecureConfiguration + ::Type::DeviceManagement, fileName, password.toUtf8(), true); + for (auto &config : m_secureConfigs) + config.setIndividualAddress({}); + + m_secureConfigs += QKnxNetIpSecureConfiguration::fromKeyring(QKnxNetIpSecureConfiguration + ::Type::Tunneling, fileName, password.toUtf8(), true); + + on_devices_currentIndexChanged(ui->devices->currentIndex()); +} + +void MainWindow::on_devices_currentIndexChanged(int /* index */) +{ + delete m_device; + ui->secureConfigs->clear(); + + const auto model = qobject_cast<QStandardItemModel*>(ui->devices->model()); + m_device = static_cast<DeviceItem *> (model->item(ui->devices->currentIndex()))->clone(); + + if (m_device) { + const auto deviceInfo = m_device->info(); + for (int i = 0; i < m_secureConfigs.size(); ++i) { + const auto &config = m_secureConfigs[i]; + if (deviceInfo.individualAddress() != config.host()) + continue; + + const auto ia = config.individualAddress(); + ui->secureConfigs->addItem(tr("User ID: %1 (Individual Address: %2)") + .arg(config.userId()) + .arg(ia.isValid() ? ia.toString() : tr("No specific address")), i); + } + } + + ui->secureConfigs->setEnabled(bool(ui->secureConfigs->count()) + && ui->secureSession->isChecked()); + ui->secureSession->setEnabled(bool(ui->secureConfigs->count())); +} + +void MainWindow::on_serviceTypes_currentIndexChanged(int index) +{ + bool enable = m_tunnel.state() == QKnxNetIpTunnel::State::Connected; + const auto type = ui->serviceTypes->itemData(index).value<QKnxNetIp::ServiceType>(); + + ui->sendRead->setEnabled(enable && type == QKnxNetIp::ServiceType::TunnelingFeatureGet); + ui->sendWrite->setEnabled(enable && type == QKnxNetIp::ServiceType::TunnelingFeatureSet); + ui->data->setEnabled(enable && type == QKnxNetIp::ServiceType::TunnelingFeatureSet); +} + +void MainWindow::on_sendRead_clicked() +{ + const auto feature = ui->interfaceFeatures->itemData(ui->interfaceFeatures->currentIndex()) + .value<QKnx::InterfaceFeature>(); + + m_tunnel.sendTunnelingFeatureGet(feature); + populateFrame(QKnxNetIp::ServiceType::TunnelingFeatureGet, feature, {}); +} + +void MainWindow::on_sendWrite_clicked() +{ + const auto feature = ui->interfaceFeatures->itemData(ui->interfaceFeatures->currentIndex()) + .value<QKnx::InterfaceFeature>(); + + const auto data = QKnxByteArray::fromHex(ui->data->text().toLatin1()); + m_tunnel.sendTunnelingFeatureSet(feature, data); + populateFrame(QKnxNetIp::ServiceType::TunnelingFeatureSet, feature, data); +} + +void MainWindow::on_connection_clicked() +{ + if (ui->devices->count() <= 0) + return; + + if (m_tunnel.state() == QKnxNetIpTunnel::State::Connected) + return m_tunnel.disconnectFromHost(); + + const auto list = ui->interfaces->currentData().toStringList(); + m_tunnel.setLocalAddress(QHostAddress(list.first())); + m_tunnel.setSerialNumber(QKnxByteArray::fromHex(list.last().toLatin1())); + + m_last = new QTreeWidgetItem(ui->communication, m_last); + m_last->setText(0, tr("Establish connection to: %1 (%2 : %3)") + .arg(m_device->info().deviceName()) + .arg(m_device->info().controlEndpointAddress().toString()) + .arg(m_device->info().controlEndpointPort())); + m_last->setFirstColumnSpanned(true); + + if (ui->secureSession->isChecked()) { + auto secureConfiguration = m_secureConfigs.value(ui->secureConfigs->currentData().toInt()); + secureConfiguration.setKeepSecureSessionAlive(true); + m_tunnel.setSecureConfiguration(secureConfiguration); + m_tunnel.connectToHostEncrypted(m_device->info().controlEndpointAddress(), + m_device->info().controlEndpointPort()); + } else { + m_tunnel.connectToHost(m_device->info().controlEndpointAddress(), + m_device->info().controlEndpointPort(), QKnxNetIp::HostProtocol::UDP_IPv4); + } +} + +void MainWindow::setupInterfaces() +{ + auto firstItem = new QStandardItem(tr("Interface: --Select One--")); + qobject_cast<QStandardItemModel*>(ui->interfaces->model())->appendRow(firstItem); + firstItem->setSelectable(false); + + const auto interfaces = QNetworkInterface::allInterfaces(); + for (const auto &iface : interfaces) { + const auto addressEntries = iface.addressEntries(); + for (int j = 0; j < addressEntries.size(); j++) { + const auto ip = addressEntries[j].ip(); + if (ip.isLoopback() || ip.toIPv4Address() == 0) + continue; + ui->interfaces->addItem(iface.name() + ": " + ip.toString(), + QStringList { ip.toString(), iface.hardwareAddress().remove(QLatin1String(":")) }); + } + } + ui->interfaces->setCurrentIndex(bool(ui->interfaces->count())); + + connect(ui->interfaces, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [&](int i) { + if (i < 0) + return; + m_discoveryAgent.stop(); + m_discoveryAgent.setLocalAddress(QHostAddress(ui->interfaces->currentData() + .toStringList().first())); + m_discoveryAgent.start(); + }); +} + +void MainWindow::toggleUi(bool value) +{ + ui->serviceTypes->setEnabled(value); + ui->interfaceFeatures->setEnabled(value); + const auto type = ui->serviceTypes->itemData(ui->serviceTypes->currentIndex()) + .value<QKnxNetIp::ServiceType>(); + ui->sendRead->setEnabled(value && type == QKnxNetIp::ServiceType::TunnelingFeatureGet); + ui->sendWrite->setEnabled(value && type == QKnxNetIp::ServiceType::TunnelingFeatureSet); + ui->data->setEnabled(value && type == QKnxNetIp::ServiceType::TunnelingFeatureSet); + ui->interfaces->setDisabled(value); + ui->devices->setDisabled(value && bool(ui->devices->count())); +} + +void MainWindow::populateServiceTypesComboBox() +{ + const auto typeEnum = QMetaEnum::fromType<QKnxNetIp::ServiceType>(); + ui->serviceTypes->addItem(tr("Tunneling Feature Get"), typeEnum.keyToValue("TunnelingFeatureGet")); + ui->serviceTypes->addItem(tr("Tunneling Feature Set"), typeEnum.keyToValue("TunnelingFeatureSet")); +} + +void MainWindow::populateInterfaceFeaturesComboBox() +{ + const auto typeEnum = QMetaEnum::fromType<QKnx::InterfaceFeature>(); + for (int i = 0; i < typeEnum.keyCount(); ++i) { + if (!QKnx::isInterfaceFeature(QKnx::InterfaceFeature(typeEnum.value(i)))) + continue; + ui->interfaceFeatures->addItem(typeEnum.key(i), typeEnum.value(i)); + } +} + +void MainWindow::setText(QKnx::InterfaceFeature feature, const QKnxByteArray &data) +{ + const auto hex = data.toByteArray().toHex(); + m_last->setText(5, QStringLiteral("0x") + QLatin1String(hex, hex.size())); + + QString value; + switch (feature) { + case QKnx::InterfaceFeature::ActiveEmiType: + case QKnx::InterfaceFeature::SupportedEmiType: { + QStringList types; + const auto emi = QKnx::EmiTypes(hex.toUShort(nullptr, 16)); + const auto metaEnum = QMetaEnum::fromType<QKnx::EmiType>(); + if (emi.testFlag(QKnx::EmiType::EMI1)) + types += QLatin1String(metaEnum.valueToKey(int(QKnx::EmiType::EMI1))); + if (emi.testFlag(QKnx::EmiType::EMI2)) + types += QLatin1String(metaEnum.valueToKey(int(QKnx::EmiType::EMI2))); + if (emi.testFlag(QKnx::EmiType::cEMI)) + types += QLatin1String(metaEnum.valueToKey(int(QKnx::EmiType::cEMI))); + value = types.join(QLatin1Char('|')); + } break; + + case QKnx::InterfaceFeature::HostDeviceDescriptorType0: + value = QString::number(hex.toUInt(nullptr, 16)); + break; + + case QKnx::InterfaceFeature::BusConnectionStatus: { + const auto metaEnum = QMetaEnum::fromType<QKnxState::State>(); + value = metaEnum.valueToKey(int(QKnxState::State(data.value(0)))); + } break; + + case QKnx::InterfaceFeature::MaximumApduLength: + value = QString::number(hex.toUShort(nullptr, 16)); + break; + + case QKnx::InterfaceFeature::KnxManufacturerCode: { + const auto id = hex.toUShort(nullptr, 16); + value = QKnx::Ets::Manufacturers::fromId(id, QString::number(id)); + } break; + + case QKnx::InterfaceFeature::IndividualAddress: + value = QKnxAddress(QKnxAddress::Type::Individual, data).toString(); + break; + + case QKnx::InterfaceFeature::InterfaceFeatureInfoServiceEnable: { + const auto metaEnum = QMetaEnum::fromType<QKnxEnable::State>(); + value = metaEnum.valueToKey(int(QKnxEnable::State(data.value(0)))); + } break; + + default: + break; + } + m_last->setText(3, value); +} + +void MainWindow::populateFrame(QKnxNetIp::ServiceType type, QKnx::InterfaceFeature feature, + const QKnxByteArray &value, int returnCode) +{ + m_last = new QTreeWidgetItem(ui->communication, m_last); + + auto metaEnum = QMetaEnum::fromType<QKnxNetIp::ServiceType>(); + m_last->setText(1, metaEnum.valueToKey(int(type))); + + metaEnum = QMetaEnum::fromType<QKnx::InterfaceFeature>(); + m_last->setText(2, metaEnum.valueToKey(int(feature))); + + if (type != QKnxNetIp::ServiceType::TunnelingFeatureGet) + setText(feature, value); + + metaEnum = QMetaEnum::fromType<QKnx::ReturnCode>(); + if (const auto code = metaEnum.valueToKey(returnCode)) + m_last->setText(4, code); +} + +void MainWindow::onFeatureResponseReceived(QKnx::InterfaceFeature feature, QKnx::ReturnCode code, + const QKnxByteArray &value) +{ + populateFrame(QKnxNetIp::ServiceType::TunnelingFeatureResponse, feature, value, int(code)); +} + +void MainWindow::onFeatureInfoReceived(QKnx::InterfaceFeature feature, const QKnxByteArray &value) +{ + populateFrame(QKnxNetIp::ServiceType::TunnelingFeatureInfo, feature, value); +} diff --git a/examples/knx/feature/mainwindow.h b/examples/knx/feature/mainwindow.h new file mode 100644 index 0000000..42d827b --- /dev/null +++ b/examples/knx/feature/mainwindow.h @@ -0,0 +1,113 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtKnx module. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +******************************************************************************/ + +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include <QtKnx> +#include <QtWidgets> + +QT_BEGIN_NAMESPACE + +namespace Ui { + class MainWindow; +} + +QT_END_NAMESPACE + +class DeviceItem; +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + explicit MainWindow(QWidget *parent = 0); + ~MainWindow(); + +private slots: + void onConnected(); + void onDisconnected(); + void onDeviceDiscovered(QKnxNetIpServerInfo info); + void onErrorOccurred(QKnxNetIpEndpointConnection::Error error, QString errorString); + + void on_actionImport_triggered(); + void on_devices_currentIndexChanged(int index); + void on_serviceTypes_currentIndexChanged(int index); + + void on_sendRead_clicked(); + void on_sendWrite_clicked(); + void on_connection_clicked(); + +private: + void setupInterfaces(); + void toggleUi(bool enable); + void populateServiceTypesComboBox(); + void populateInterfaceFeaturesComboBox(); + + void setText(QKnx::InterfaceFeature feature, const QKnxByteArray &value); + void populateFrame(QKnxNetIp::ServiceType type, QKnx::InterfaceFeature feature, + const QKnxByteArray &value, int returnCode = -1); + void onFeatureResponseReceived(QKnx::InterfaceFeature feature, QKnx::ReturnCode code, + const QKnxByteArray &value); + void onFeatureInfoReceived(QKnx::InterfaceFeature feature, const QKnxByteArray &value); + +private: + Ui::MainWindow *ui { nullptr }; + + DeviceItem *m_device { nullptr }; + QTreeWidgetItem *m_last { nullptr }; + QTreeWidgetItem *m_current { nullptr }; + + QKnxNetIpTunnel m_tunnel; + QKnxNetIpServerDiscoveryAgent m_discoveryAgent; + QVector<QKnxNetIpSecureConfiguration> m_secureConfigs; +}; + +#endif // MAINWINDOW_H diff --git a/examples/knx/feature/mainwindow.ui b/examples/knx/feature/mainwindow.ui new file mode 100644 index 0000000..ec92b4f --- /dev/null +++ b/examples/knx/feature/mainwindow.ui @@ -0,0 +1,332 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>MainWindow</class> + <widget class="QMainWindow" name="MainWindow"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>867</width> + <height>416</height> + </rect> + </property> + <property name="windowTitle"> + <string>KNX tunneling features example</string> + </property> + <widget class="QWidget" name="centralwidget"> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QGridLayout" name="gridLayout" columnstretch="0,1,0,1,0,0"> + <item row="0" column="0"> + <widget class="QLabel" name="serviceTypesLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Service type:</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QComboBox" name="serviceTypes"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </item> + <item row="0" column="2"> + <widget class="QLabel" name="interfaceFeaturesLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Interface feature:</string> + </property> + </widget> + </item> + <item row="0" column="3"> + <widget class="QComboBox" name="interfaceFeatures"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </item> + <item row="0" column="4"> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item row="0" column="5"> + <widget class="QPushButton" name="sendRead"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>Read</string> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="dataLabel"> + <property name="text"> + <string>Data:</string> + </property> + </widget> + </item> + <item row="1" column="5"> + <widget class="QPushButton" name="sendWrite"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>Write</string> + </property> + </widget> + </item> + <item row="1" column="1" colspan="3"> + <widget class="QLineEdit" name="data"> + <property name="enabled"> + <bool>false</bool> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QTreeWidget" name="communication"> + <property name="contextMenuPolicy"> + <enum>Qt::DefaultContextMenu</enum> + </property> + <property name="uniformRowHeights"> + <bool>true</bool> + </property> + <attribute name="headerShowSortIndicator" stdset="0"> + <bool>false</bool> + </attribute> + <column> + <property name="text"> + <string>#</string> + </property> + </column> + <column> + <property name="text"> + <string>Service type</string> + </property> + </column> + <column> + <property name="text"> + <string>Interface feature</string> + </property> + </column> + <column> + <property name="text"> + <string>Value</string> + </property> + </column> + <column> + <property name="text"> + <string>Status code</string> + </property> + </column> + <column> + <property name="text"> + <string>Data</string> + </property> + </column> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout" stretch="0,1,0,1,0,1,0,0"> + <item> + <widget class="QLabel" name="interfaceLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Interface:</string> + </property> + </widget> + </item> + <item> + <widget class="QComboBox" name="interfaces"> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="deviceLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Device:</string> + </property> + </widget> + </item> + <item> + <widget class="QComboBox" name="devices"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="secureSession"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>Use secure session</string> + </property> + </widget> + </item> + <item> + <widget class="QComboBox" name="secureConfigs"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </item> + <item> + <spacer name="spacerRight"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeType"> + <enum>QSizePolicy::Expanding</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="connection"> + <property name="text"> + <string>Connect</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <widget class="QMenuBar" name="menuBar"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>867</width> + <height>21</height> + </rect> + </property> + <widget class="QMenu" name="menuFile"> + <property name="title"> + <string>File</string> + </property> + <widget class="QMenu" name="menuImport"> + <property name="title"> + <string>Import</string> + </property> + <addaction name="actionImport"/> + </widget> + <addaction name="menuImport"/> + <addaction name="separator"/> + <addaction name="actionExit"/> + </widget> + <widget class="QMenu" name="menuEdit"> + <property name="title"> + <string>Edit</string> + </property> + <addaction name="actionClear"/> + </widget> + <addaction name="menuFile"/> + <addaction name="menuEdit"/> + </widget> + <action name="actionExit"> + <property name="text"> + <string>Exit</string> + </property> + </action> + <action name="actionImport"> + <property name="text"> + <string>KNX keyring file...</string> + </property> + </action> + <action name="actionClear"> + <property name="text"> + <string>Clear telegram list</string> + </property> + </action> + </widget> + <resources/> + <connections> + <connection> + <sender>secureSession</sender> + <signal>toggled(bool)</signal> + <receiver>secureConfigs</receiver> + <slot>setEnabled(bool)</slot> + <hints> + <hint type="sourcelabel"> + <x>516</x> + <y>391</y> + </hint> + <hint type="destinationlabel"> + <x>634</x> + <y>393</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/examples/knx/group/deviceitem.cpp b/examples/knx/group/deviceitem.cpp new file mode 100644 index 0000000..c353d1b --- /dev/null +++ b/examples/knx/group/deviceitem.cpp @@ -0,0 +1,66 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtKnx module. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +******************************************************************************/ + +#include "deviceitem.h" + +DeviceItem::DeviceItem(const QKnxNetIpServerInfo &info) + : QStandardItem(info.deviceName()) + , m_serverInfo(info) +{} + +DeviceItem *DeviceItem::clone() const +{ + return new DeviceItem(m_serverInfo); +} + +const QKnxNetIpServerInfo &DeviceItem::info() const +{ + return m_serverInfo; +} diff --git a/examples/knx/group/deviceitem.h b/examples/knx/group/deviceitem.h new file mode 100644 index 0000000..5f59407 --- /dev/null +++ b/examples/knx/group/deviceitem.h @@ -0,0 +1,70 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtKnx module. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +******************************************************************************/ + +#ifndef DEVICEITEM_H +#define DEVICEITEM_H + +#include <QtGui> +#include <QtKnx> + +class DeviceItem : public QStandardItem +{ +public: + DeviceItem(const QKnxNetIpServerInfo &info); + ~DeviceItem() = default; + + DeviceItem *clone() const override; + const QKnxNetIpServerInfo &info() const; + +private: + QKnxNetIpServerInfo m_serverInfo; +}; + +#endif // DEVICEITEM_H diff --git a/examples/knx/group/group.pro b/examples/knx/group/group.pro new file mode 100644 index 0000000..95d739e --- /dev/null +++ b/examples/knx/group/group.pro @@ -0,0 +1,20 @@ +TEMPLATE = app +TARGET = group +INCLUDEPATH += . + +CONFIG += c++11 +QT += knx widgets network core + +FORMS += mainwindow.ui + +HEADERS += deviceitem.h \ + groupaddressvalidater.h \ + mainwindow.h + +SOURCES += deviceitem.cpp \ + groupaddressvalidater.cpp \ + main.cpp \ + mainwindow.cpp + +target.path = $$[QT_INSTALL_EXAMPLES]/knx/group +INSTALLS += target diff --git a/examples/knx/group/groupaddressvalidater.cpp b/examples/knx/group/groupaddressvalidater.cpp new file mode 100644 index 0000000..af03d1b --- /dev/null +++ b/examples/knx/group/groupaddressvalidater.cpp @@ -0,0 +1,67 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtKnx module. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +******************************************************************************/ + +#include "groupaddressvalidater.h" + +GroupAddressValidator::GroupAddressValidator(QObject *parent) + : QValidator(parent) +{} + +QValidator::State GroupAddressValidator::validate(QString &input, int & /* pos */) const +{ + const auto result = m_groupRegExp.match(input); + if (result.hasMatch()) + return State::Invalid; + + auto n = input.split(QLatin1Char('/')); + if (n.value(0).toInt() > 15 || n.value(1).toInt() > 15 || n.value(2).toInt() > 255) + return State::Invalid; + return State::Intermediate; +} diff --git a/examples/knx/group/groupaddressvalidater.h b/examples/knx/group/groupaddressvalidater.h new file mode 100644 index 0000000..ed629cb --- /dev/null +++ b/examples/knx/group/groupaddressvalidater.h @@ -0,0 +1,68 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtKnx module. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +******************************************************************************/ + +#ifndef GROUPADDRESSVALIDATER_H +#define GROUPADDRESSVALIDATER_H + +#include <QtGui> + +class GroupAddressValidator : public QValidator +{ + Q_OBJECT + +public: + GroupAddressValidator(QObject *parent = nullptr); + QValidator::State validate(QString &input, int &pos) const override; + +private: + QRegularExpression m_groupRegExp { QStringLiteral("[^0-9\\/]") }; +}; + +#endif // GROUPADDRESSVALIDATER_H diff --git a/examples/knx/group/main.cpp b/examples/knx/group/main.cpp new file mode 100644 index 0000000..e3e9a74 --- /dev/null +++ b/examples/knx/group/main.cpp @@ -0,0 +1,62 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtKnx module. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +******************************************************************************/ + +#include "mainwindow.h" + +#include <QApplication> + +int main(int argc, char *argv[]) +{ + QApplication a(argc, argv); + MainWindow w; + w.show(); + + return a.exec(); +} diff --git a/examples/knx/group/mainwindow.cpp b/examples/knx/group/mainwindow.cpp new file mode 100644 index 0000000..4100e03 --- /dev/null +++ b/examples/knx/group/mainwindow.cpp @@ -0,0 +1,374 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtKnx module. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +******************************************************************************/ + +#include "mainwindow.h" +#include "ui_mainwindow.h" + +#include "deviceitem.h" +#include "groupaddressvalidater.h" + +MainWindow::MainWindow(QWidget *parent) + : QMainWindow(parent) + , ui(new Ui::MainWindow) +{ + ui->setupUi(this); + ui->groupAddress->setValidator(new GroupAddressValidator); + + for (int i = 0; i < ui->communication->columnCount(); ++i) + ui->communication->resizeColumnToContents(i); + + setupInterfaces(); + populateDatapointComboBox(); + + connect(ui->actionExit, &QAction::triggered, this, &QApplication::quit); + connect(ui->actionClear, &QAction::triggered, ui->communication, &QTreeWidget::clear); + + connect(&m_tunnel, &QKnxNetIpTunnel::connected, this, &MainWindow::onConnected); + connect(&m_tunnel, &QKnxNetIpTunnel::disconnected, this, &MainWindow::onDisconnected); + connect(&m_tunnel, &QKnxNetIpTunnel::frameReceived, this, &MainWindow::populateFrame); + connect(&m_tunnel, &QKnxNetIpTunnel::errorOccurred, this, &MainWindow::onErrorOccurred); + + connect(ui->communication, &QTreeWidget::currentItemChanged, this, + [&](QTreeWidgetItem *current, QTreeWidgetItem * /* previuos */) { + m_current = current; + }); + + m_discoveryAgent.setTimeout(-1); + m_discoveryAgent.setSearchFrequency(6); + m_discoveryAgent.setDiscoveryMode(QKnxNetIpServerDiscoveryAgent::DiscoveryMode::CoreV1 + | QKnxNetIpServerDiscoveryAgent::DiscoveryMode::CoreV2); + connect(&m_discoveryAgent, &QKnxNetIpServerDiscoveryAgent::deviceDiscovered, + this, &MainWindow::onDeviceDiscovered); + + m_discoveryAgent.start(); +} + +MainWindow::~MainWindow() +{ + m_discoveryAgent.stop(); + + delete ui; + ui = nullptr; + delete m_device; +} + +void MainWindow::onConnected() +{ + toggleUi(true); + ui->connection->setText(tr("Disconnect")); + + m_last = new QTreeWidgetItem(ui->communication, m_last); + m_last->setText(0, tr("Successfully connected to: %1 (%2 : %3)") + .arg(m_device->info().deviceName()) + .arg(m_device->info().controlEndpointAddress().toString()) + .arg(m_device->info().controlEndpointPort())); + m_last->setFirstColumnSpanned(true); +} + +void MainWindow::onDisconnected() +{ + toggleUi(false); + ui->connection->setText(tr("Connect")); + + m_last = new QTreeWidgetItem(ui->communication, m_last); + m_last->setText(0, tr("Successfully disconnected from: %1 (%2 : %3)") + .arg(m_device->info().deviceName()) + .arg(m_device->info().controlEndpointAddress().toString()) + .arg(m_device->info().controlEndpointPort())); + m_last->setFirstColumnSpanned(true); +} + +void MainWindow::onDeviceDiscovered(QKnxNetIpServerInfo info) +{ + if (ui->devices->findText(info.deviceName()) == -1) + qobject_cast<QStandardItemModel*>(ui->devices->model())->appendRow(new DeviceItem(info)); + + if (m_tunnel.state() == QKnxNetIpTunnel::State::Disconnected) + ui->devices->setEnabled(bool(ui->devices->count())); +} + +void MainWindow::onErrorOccurred(QKnxNetIpEndpointConnection::Error error, QString errorString) +{ + m_last = new QTreeWidgetItem(ui->communication, m_last); + const auto metaEnum = QMetaEnum::fromType<QKnxNetIpEndpointConnection::Error>(); + m_last->setText(0, tr("Error occurred: %1 (%2).").arg(errorString) + .arg(metaEnum.valueToKey(int(error)))); + m_last->setFirstColumnSpanned(true); +} + +void MainWindow::on_actionImport_triggered() +{ + const auto fileName = QFileDialog::getOpenFileName(this, tr("Import keyring file"), + QStandardPaths::standardLocations(QStandardPaths::DesktopLocation).value(0), + tr("KNX keyring file (*.knxkeys)")); + + if (fileName.isEmpty()) + return; + + bool ok; + const auto password = QInputDialog::getText(this, tr("Import keyring file"), + tr("Keyring file password:"), QLineEdit::Normal, {}, &ok); + if (!ok || password.isEmpty()) + return; + + m_secureConfigs = QKnxNetIpSecureConfiguration::fromKeyring(QKnxNetIpSecureConfiguration + ::Type::DeviceManagement, fileName, password.toUtf8(), true); + for (auto &config : m_secureConfigs) + config.setIndividualAddress({}); + + m_secureConfigs += QKnxNetIpSecureConfiguration::fromKeyring(QKnxNetIpSecureConfiguration + ::Type::Tunneling, fileName, password.toUtf8(), true); + + on_devices_currentIndexChanged(ui->devices->currentIndex()); +} + +void MainWindow::on_devices_currentIndexChanged(int /* index */) +{ + delete m_device; + ui->secureConfigs->clear(); + + const auto model = qobject_cast<QStandardItemModel*>(ui->devices->model()); + m_device = static_cast<DeviceItem *> (model->item(ui->devices->currentIndex()))->clone(); + + if (m_device) { + const auto deviceInfo = m_device->info(); + for (int i = 0; i < m_secureConfigs.size(); ++i) { + const auto &config = m_secureConfigs[i]; + if (deviceInfo.individualAddress() != config.host()) + continue; + + const auto ia = config.individualAddress(); + ui->secureConfigs->addItem(tr("User ID: %1 (Individual Address: %2)") + .arg(config.userId()) + .arg(ia.isValid() ? ia.toString() : tr("No specific address")), i); + } + } + + ui->secureConfigs->setEnabled(bool(ui->secureConfigs->count()) + && ui->secureSession->isChecked()); + ui->secureSession->setEnabled(bool(ui->secureConfigs->count())); +} + +void MainWindow::on_datapointTypes_currentIndexChanged(int index) +{ + ui->values->clear(); + + const auto type = ui->datapointTypes->itemData(index).value<QKnxDatapointType::Type>(); + QScopedPointer<QKnxDatapointType> p(QKnxDatapointTypeFactory::instance().createType(type)); + if (p) { + ui->values->addItem(p->minimumText(), p->minimum()); + ui->values->addItem(p->maximumText(), p->maximum()); + } +} + +void MainWindow::on_sendRead_clicked() +{ + const auto frame = QKnxLinkLayerFrame::builder() + .setSourceAddress(m_tunnel.individualAddress()) + .setDestinationAddress({ QKnxAddress::Type::Group, ui->groupAddress->text() }) + .createFrame(); + + populateFrame(frame); + m_tunnel.sendFrame(frame); +} + +void MainWindow::on_sendWrite_clicked() +{ + const auto frame = QKnxLinkLayerFrame::builder() + .setSourceAddress(m_tunnel.individualAddress()) + .setDestinationAddress({ QKnxAddress::Type::Group, ui->groupAddress->text() }) + .setTpdu({ + QKnxTpdu::TransportControlField::DataGroup, + QKnxTpdu::ApplicationControlField::GroupValueWrite, + QKnx1Bit(ui->values->currentData().toBool()).bytes() + }).createFrame(); + + populateFrame(frame); + m_tunnel.sendFrame(frame); +} + +void MainWindow::on_connection_clicked() +{ + if (ui->devices->count() <= 0) + return; + + if (m_tunnel.state() == QKnxNetIpTunnel::State::Connected) + return m_tunnel.disconnectFromHost(); + + const auto list = ui->interfaces->currentData().toStringList(); + m_tunnel.setLocalAddress(QHostAddress(list.first())); + m_tunnel.setSerialNumber(QKnxByteArray::fromHex(list.last().toLatin1())); + + m_last = new QTreeWidgetItem(ui->communication, m_last); + m_last->setText(0, tr("Establish connection to: %1 (%2 : %3)") + .arg(m_device->info().deviceName()) + .arg(m_device->info().controlEndpointAddress().toString()) + .arg(m_device->info().controlEndpointPort())); + m_last->setFirstColumnSpanned(true); + + if (ui->secureSession->isChecked()) { + auto secureConfiguration = m_secureConfigs.value(ui->secureConfigs->currentData().toInt()); + secureConfiguration.setKeepSecureSessionAlive(true); + m_tunnel.setSecureConfiguration(secureConfiguration); + m_tunnel.connectToHostEncrypted(m_device->info().controlEndpointAddress(), + m_device->info().controlEndpointPort()); + } else { + m_tunnel.connectToHost(m_device->info().controlEndpointAddress(), + m_device->info().controlEndpointPort(), QKnxNetIp::HostProtocol::UDP_IPv4); + } +} + +void MainWindow::setupInterfaces() +{ + auto firstItem = new QStandardItem(tr("Interface: --Select One--")); + qobject_cast<QStandardItemModel*>(ui->interfaces->model())->appendRow(firstItem); + firstItem->setSelectable(false); + + const auto interfaces = QNetworkInterface::allInterfaces(); + for (const auto &iface : interfaces) { + const auto addressEntries = iface.addressEntries(); + for (int j = 0; j < addressEntries.size(); j++) { + const auto ip = addressEntries[j].ip(); + if (ip.isLoopback() || ip.toIPv4Address() == 0) + continue; + ui->interfaces->addItem(iface.name() + ": " + ip.toString(), + QStringList { ip.toString(), iface.hardwareAddress().remove(QLatin1String(":")) }); + } + } + ui->interfaces->setCurrentIndex(bool(ui->interfaces->count())); + + connect(ui->interfaces, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [&](int i) { + if (i < 0) + return; + m_discoveryAgent.stop(); + m_discoveryAgent.setLocalAddress(QHostAddress(ui->interfaces->currentData() + .toStringList().first())); + m_discoveryAgent.start(); + }); +} + +void MainWindow::toggleUi(bool value) +{ + ui->groupAddress->setEnabled(value); + ui->datapointTypes->setEnabled(value); + ui->values->setEnabled(value); + ui->sendRead->setEnabled(value); + ui->sendWrite->setEnabled(value); + ui->interfaces->setDisabled(value); + ui->devices->setDisabled(value && bool(ui->devices->count())); +} + +void MainWindow::populateDatapointComboBox() +{ + const auto typeEnum = QMetaEnum::fromType<QKnxDatapointType::Type>(); + for (int i = 0; i < typeEnum.keyCount(); ++i) { + const auto type = QKnxDatapointType::Type(typeEnum.value(i)); + if (type >= QKnxDatapointType::Type::Dpt2_1BitControlled) + break; + QScopedPointer<QKnxDatapointType> p(QKnxDatapointTypeFactory::instance().createType(type)); + if (p) + ui->datapointTypes->addItem(p->description(), QVariant::fromValue(type)); + } +} + +void MainWindow::populateFrame(const QKnxLinkLayerFrame &frame) +{ + m_last = new QTreeWidgetItem(ui->communication, m_last); + + auto metaEnum = QMetaEnum::fromType<QKnxLinkLayerFrame::MessageCode>(); + m_last->setText(1, metaEnum.valueToKey(int(frame.messageCode()))); + m_last->setText(2, frame.sourceAddress().toString()); + m_last->setText(3, frame.destinationAddress().toString()); + + const auto ctrl = frame.controlField(); + metaEnum = QMetaEnum::fromType<QKnxControlField::FrameFormat>(); + m_last->setText(4, metaEnum.valueToKey(int(ctrl.frameFormat()))); + metaEnum = QMetaEnum::fromType<QKnxControlField::Repeat>(); + m_last->setText(5, metaEnum.valueToKey(int(ctrl.repeat()))); + metaEnum = QMetaEnum::fromType<QKnxControlField::Broadcast>(); + m_last->setText(6, metaEnum.valueToKey(int(ctrl.broadcast()))); + metaEnum = QMetaEnum::fromType<QKnxControlField::Priority>(); + m_last->setText(7, metaEnum.valueToKey(int(ctrl.priority()))); + metaEnum = QMetaEnum::fromType<QKnxControlField::Acknowledge>(); + m_last->setText(8, metaEnum.valueToKey(int(ctrl.acknowledge()))); + metaEnum = QMetaEnum::fromType<QKnxControlField::Confirm>(); + m_last->setText(9, metaEnum.valueToKey(int(ctrl.confirm()))); + + const auto extCtrl = frame.extendedControlField(); + m_last->setText(10, (extCtrl.destinationAddressType() == QKnxAddress::Type::Group + ? tr("Group") + : tr("Individual"))); + m_last->setText(11, QString::number(extCtrl.hopCount())); + metaEnum = QMetaEnum::fromType<QKnxExtendedControlField::ExtendedFrameFormat>(); + m_last->setText(12, metaEnum.valueToKey(int(extCtrl.format()))); + + auto tpdu = frame.tpdu(); + metaEnum = QMetaEnum::fromType<QKnxTpdu::TransportControlField>(); + m_last->setText(13, metaEnum.valueToKey(int(tpdu.transportControlField()))); + metaEnum = QMetaEnum::fromType<QKnxTpdu::ApplicationControlField>(); + m_last->setText(14, metaEnum.valueToKey(int(tpdu.applicationControlField()))); + + auto bytes = tpdu.data().toHex(); + m_last->setText(15, QString::fromLatin1(bytes.toByteArray(), bytes.size())); + + bytes = frame.bytes().toHex(); + m_last->setText(16, QString::fromLatin1(bytes.toByteArray(), bytes.size())); + + if (m_current) { + const auto region = ui->communication->viewport()->visibleRegion(); + const auto currentRect = ui->communication->visualItemRect(m_current); + if (region.contains({ currentRect.left(), currentRect.top() - currentRect.height() }) + && region.contains(ui->communication->visualItemRect(m_last))) { + ui->communication->scrollToItem(m_last); + } + } else { + ui->communication->scrollToItem(m_last); + } +} diff --git a/examples/knx/group/mainwindow.h b/examples/knx/group/mainwindow.h new file mode 100644 index 0000000..ceee2df --- /dev/null +++ b/examples/knx/group/mainwindow.h @@ -0,0 +1,106 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtKnx module. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +******************************************************************************/ + +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include <QtKnx> +#include <QtWidgets> + +QT_BEGIN_NAMESPACE + +namespace Ui { + class MainWindow; +} + +QT_END_NAMESPACE + +class DeviceItem; +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + explicit MainWindow(QWidget *parent = 0); + ~MainWindow(); + +private slots: + void onConnected(); + void onDisconnected(); + void onDeviceDiscovered(QKnxNetIpServerInfo info); + void onErrorOccurred(QKnxNetIpEndpointConnection::Error error, QString errorString); + + void on_actionImport_triggered(); + void on_devices_currentIndexChanged(int index); + void on_datapointTypes_currentIndexChanged(int index); + + void on_sendRead_clicked(); + void on_sendWrite_clicked(); + void on_connection_clicked(); + +private: + void setupInterfaces(); + void toggleUi(bool enable); + void populateDatapointComboBox(); + void populateFrame(const QKnxLinkLayerFrame &frame); + +private: + Ui::MainWindow *ui { nullptr }; + + DeviceItem *m_device { nullptr }; + QTreeWidgetItem *m_last { nullptr }; + QTreeWidgetItem *m_current { nullptr }; + + QKnxNetIpTunnel m_tunnel; + QKnxNetIpServerDiscoveryAgent m_discoveryAgent; + QVector<QKnxNetIpSecureConfiguration> m_secureConfigs; +}; + +#endif // MAINWINDOW_H diff --git a/examples/knx/group/mainwindow.ui b/examples/knx/group/mainwindow.ui new file mode 100644 index 0000000..5714453 --- /dev/null +++ b/examples/knx/group/mainwindow.ui @@ -0,0 +1,402 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>MainWindow</class> + <widget class="QMainWindow" name="MainWindow"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>867</width> + <height>416</height> + </rect> + </property> + <property name="windowTitle"> + <string>KNX group communication example</string> + </property> + <widget class="QWidget" name="centralwidget"> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_2" stretch="0,0,0,1,0,1,0,0,0"> + <item> + <widget class="QLabel" name="groupAddressLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Group address:</string> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="groupAddress"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>0/0/0</string> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="datappointTypeLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Datapoint type:</string> + </property> + </widget> + </item> + <item> + <widget class="QComboBox" name="datapointTypes"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="valueLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Value:</string> + </property> + </widget> + </item> + <item> + <widget class="QComboBox" name="values"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </item> + <item> + <spacer name="spacerTop"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="sendRead"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>Read</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="sendWrite"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>Write</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QTreeWidget" name="communication"> + <property name="contextMenuPolicy"> + <enum>Qt::DefaultContextMenu</enum> + </property> + <property name="uniformRowHeights"> + <bool>true</bool> + </property> + <attribute name="headerShowSortIndicator" stdset="0"> + <bool>false</bool> + </attribute> + <column> + <property name="text"> + <string>#</string> + </property> + </column> + <column> + <property name="text"> + <string>Message code</string> + </property> + </column> + <column> + <property name="text"> + <string>Source address</string> + </property> + </column> + <column> + <property name="text"> + <string>Destination address</string> + </property> + </column> + <column> + <property name="text"> + <string>Frame format</string> + </property> + </column> + <column> + <property name="text"> + <string>Repeat</string> + </property> + </column> + <column> + <property name="text"> + <string>Broadcast</string> + </property> + </column> + <column> + <property name="text"> + <string>Priority</string> + </property> + </column> + <column> + <property name="text"> + <string>Acknowledge</string> + </property> + </column> + <column> + <property name="text"> + <string>Confirm</string> + </property> + </column> + <column> + <property name="text"> + <string>Destination address type</string> + </property> + </column> + <column> + <property name="text"> + <string>Hop count</string> + </property> + </column> + <column> + <property name="text"> + <string>Extended frame format</string> + </property> + </column> + <column> + <property name="text"> + <string>TCPI</string> + </property> + </column> + <column> + <property name="text"> + <string>APCI</string> + </property> + </column> + <column> + <property name="text"> + <string>Data</string> + </property> + </column> + <column> + <property name="text"> + <string>Frame</string> + </property> + </column> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout" stretch="0,1,0,1,0,1,0,0"> + <item> + <widget class="QLabel" name="interfaceLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Interface:</string> + </property> + </widget> + </item> + <item> + <widget class="QComboBox" name="interfaces"> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="deviceLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Device:</string> + </property> + </widget> + </item> + <item> + <widget class="QComboBox" name="devices"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="secureSession"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>Use secure session</string> + </property> + </widget> + </item> + <item> + <widget class="QComboBox" name="secureConfigs"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </item> + <item> + <spacer name="spacerRight"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeType"> + <enum>QSizePolicy::Expanding</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="connection"> + <property name="text"> + <string>Connect</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <widget class="QMenuBar" name="menuBar"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>867</width> + <height>21</height> + </rect> + </property> + <widget class="QMenu" name="menuFile"> + <property name="title"> + <string>File</string> + </property> + <widget class="QMenu" name="menuImport"> + <property name="title"> + <string>Import</string> + </property> + <addaction name="actionImport"/> + </widget> + <addaction name="menuImport"/> + <addaction name="separator"/> + <addaction name="actionExit"/> + </widget> + <widget class="QMenu" name="menuEdit"> + <property name="title"> + <string>Edit</string> + </property> + <addaction name="actionClear"/> + </widget> + <addaction name="menuFile"/> + <addaction name="menuEdit"/> + </widget> + <action name="actionExit"> + <property name="text"> + <string>Exit</string> + </property> + </action> + <action name="actionImport"> + <property name="text"> + <string>KNX keyring file...</string> + </property> + </action> + <action name="actionClear"> + <property name="text"> + <string>Clear telegram list</string> + </property> + </action> + </widget> + <resources/> + <connections> + <connection> + <sender>secureSession</sender> + <signal>toggled(bool)</signal> + <receiver>secureConfigs</receiver> + <slot>setEnabled(bool)</slot> + <hints> + <hint type="sourcelabel"> + <x>516</x> + <y>391</y> + </hint> + <hint type="destinationlabel"> + <x>634</x> + <y>393</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/examples/knx/knx.pro b/examples/knx/knx.pro index 2628db9..2a74801 100644 --- a/examples/knx/knx.pro +++ b/examples/knx/knx.pro @@ -5,5 +5,5 @@ TEMPLATE = subdirs SUBDIRS += discoverer knxproj tunnelclient router qtHaveModule(widgets) { - SUBDIRS += knxeditor + SUBDIRS += knxeditor group device feature } diff --git a/examples/knx/knxeditor/localdevicemanagement.cpp b/examples/knx/knxeditor/localdevicemanagement.cpp index 803badf..dd52493 100644 --- a/examples/knx/knxeditor/localdevicemanagement.cpp +++ b/examples/knx/knxeditor/localdevicemanagement.cpp @@ -52,8 +52,9 @@ #include "ui_localdevicemanagement.h" #include <QKnxByteArray> -#include <QtKnx/QKnxDeviceManagementFrameBuilder> +#include <QKnxDeviceManagementFrameBuilder> #include <QKnxInterfaceObjectPropertyDataType> +#include <QKnxNetIpSecureConfiguration> #include <QMetaEnum> #include <QMetaType> #include <QTreeWidget> @@ -82,9 +83,16 @@ LocalDeviceManagement::LocalDeviceManagement(QWidget* parent) connect(ui->connectRequestDeviceManagement, &QPushButton::clicked, this, [&]() { m_management.setLocalPort(0); - m_management.connectToHost(m_server.controlEndpointAddress(), - m_server.controlEndpointPort(), - m_proto); + if (ui->secureSessionCheckBox->isChecked()) { + auto config = m_configs.value(ui->secureSessionCb->currentIndex()); + config.setKeepSecureSessionAlive(true); + m_management.setSecureConfiguration(config); + m_management.connectToHostEncrypted(m_server.controlEndpointAddress(), + m_server.controlEndpointPort()); + } else { + m_management.connectToHost(m_server.controlEndpointAddress(), + m_server.controlEndpointPort(), m_proto); + } }); connect(&m_management, &QKnxNetIpDeviceManagement::connected, this, [&] { @@ -107,6 +115,8 @@ LocalDeviceManagement::LocalDeviceManagement(QWidget* parent) }); connect(&m_management, &QKnxNetIpDeviceManagement::disconnected, this, [&] { + if (!ui) + return; m_awaitIoListResponse = true; ui->deviceManagementSendRequest->setEnabled(false); ui->connectRequestDeviceManagement->setEnabled(true); @@ -135,6 +145,11 @@ LocalDeviceManagement::LocalDeviceManagement(QWidget* parent) handleIoListResponse(frame); }); + connect(&m_management, &QKnxNetIpDeviceManagement::errorOccurred, this, + [&] (QKnxNetIpEndpointConnection::Error, QString errorString) { + ui->textOuputDeviceManagement->append(errorString); + }); + ui->cemiData->setValidator(new QRegExpValidator(QRegExp("[0-9a-fA-F]+"))); ui->cemiFrame->setValidator(new QRegExpValidator(QRegExp("[0-9a-fA-F]+"))); } @@ -142,6 +157,7 @@ LocalDeviceManagement::LocalDeviceManagement(QWidget* parent) LocalDeviceManagement::~LocalDeviceManagement() { delete ui; + ui = nullptr; } void LocalDeviceManagement::setNatAware(bool isNatAware) @@ -164,6 +180,8 @@ void LocalDeviceManagement::setKnxNetIpServer(const QKnxNetIpServerInfo &server) ui->connectRequestDeviceManagement->setEnabled(true); ui->disconnectRequestDeviceManagement->setEnabled(false); } + + updateSecureConfigCombo(); ui->deviceManagementSendRequest->setEnabled(false); } @@ -273,6 +291,12 @@ void LocalDeviceManagement::on_manualInput_clicked(bool checked) } } +void LocalDeviceManagement::onKeyringChanged(const QVector<QKnxNetIpSecureConfiguration> &configs) +{ + m_configs = configs; + updateSecureConfigCombo(); +} + void LocalDeviceManagement::setupMessageCodeComboBox() { ui->mc->addItem("M_PropRead.req", @@ -362,7 +386,7 @@ int LocalDeviceManagement::keyToValue(const QMetaObject &object, const QString & auto enumCount = object.enumeratorCount(); for (auto i = 0; i < enumCount; ++i) { int value = object.enumerator(i).keyToValue(key.toLatin1(), ok); - if (value != -1 && ok) + if (value != -1 && *ok) return value; } return -1; @@ -403,3 +427,20 @@ void LocalDeviceManagement::selectFirstSubitem(QTreeWidget *treeWidget, QTreeWid treeWidget->setCurrentItem(treeWidget->invisibleRootItem(), 0); comboBox->setRootModelIndex(treeWidget->currentIndex()); } + +void LocalDeviceManagement::updateSecureConfigCombo() +{ + ui->secureSessionCb->clear(); + + ui->secureSessionCheckBox->setEnabled(!m_configs.isEmpty()); + ui->secureSessionCheckBox->setChecked(m_proto == QKnxNetIp::HostProtocol::TCP_IPv4); + + for (int i = 0; i < m_configs.size(); ++i) { + const auto &config = m_configs[i]; + if (m_server.individualAddress() != config.host()) + continue; + + ui->secureSessionCb->addItem(tr("User ID: %1 (Individual Address: %2)").arg(config.userId()) + .arg(config.individualAddress().toString()), i); + } +} diff --git a/examples/knx/knxeditor/localdevicemanagement.h b/examples/knx/knxeditor/localdevicemanagement.h index 218ad95..195023f 100644 --- a/examples/knx/knxeditor/localdevicemanagement.h +++ b/examples/knx/knxeditor/localdevicemanagement.h @@ -51,9 +51,9 @@ #ifndef LOCALDEVICEMANAGEMENT_H #define LOCALDEVICEMANAGEMENT_H -#include <QWidget> #include <QKnxNetIpDeviceManagement> #include <QKnxNetIpServerInfo> +#include <QWidget> QT_BEGIN_NAMESPACE namespace Ui { @@ -78,6 +78,7 @@ public: void setLocalAddress(const QHostAddress &address); void setKnxNetIpServer(const QKnxNetIpServerInfo &server); void setTcpEnable(bool value); + void onKeyringChanged(const QVector<QKnxNetIpSecureConfiguration> &configs); public slots: void clearLogging(); @@ -100,6 +101,7 @@ private: int keyToValue(const QMetaObject &object, const QString &key, bool *ok); void setupComboBox(QComboBox *comboBox, const QMetaObject &object, const QSet<int> &values = {}); void selectFirstSubitem(QTreeWidget *treeView, QTreeWidgetItem *rootItem, QComboBox *comboBox); + void updateSecureConfigCombo(); private: Ui::LocalDeviceManagement *ui { nullptr }; @@ -110,6 +112,7 @@ private: QKnxNetIpServerInfo m_server; QKnxNetIpDeviceManagement m_management; QKnxNetIp::HostProtocol m_proto { QKnxNetIp::HostProtocol::UDP_IPv4 }; + QVector<QKnxNetIpSecureConfiguration> m_configs; }; #endif diff --git a/examples/knx/knxeditor/localdevicemanagement.ui b/examples/knx/knxeditor/localdevicemanagement.ui index 05c3145..99d809f 100644 --- a/examples/knx/knxeditor/localdevicemanagement.ui +++ b/examples/knx/knxeditor/localdevicemanagement.ui @@ -6,7 +6,7 @@ <rect> <x>0</x> <y>0</y> - <width>830</width> + <width>856</width> <height>385</height> </rect> </property> @@ -265,6 +265,29 @@ <item> <layout class="QHBoxLayout" name="horizontalLayout_3"> <item> + <widget class="QCheckBox" name="secureSessionCheckBox"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>Use secure session</string> + </property> + </widget> + </item> + <item> + <widget class="QComboBox" name="secureSessionCb"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </item> + <item> <spacer name="horizontalSpacer"> <property name="orientation"> <enum>Qt::Horizontal</enum> @@ -387,8 +410,8 @@ <y>45</y> </hint> <hint type="destinationlabel"> - <x>332</x> - <y>16</y> + <x>384</x> + <y>26</y> </hint> </hints> </connection> @@ -403,8 +426,8 @@ <y>42</y> </hint> <hint type="destinationlabel"> - <x>439</x> - <y>22</y> + <x>485</x> + <y>29</y> </hint> </hints> </connection> @@ -419,8 +442,8 @@ <y>49</y> </hint> <hint type="destinationlabel"> - <x>753</x> - <y>19</y> + <x>845</x> + <y>29</y> </hint> </hints> </connection> @@ -451,8 +474,8 @@ <y>49</y> </hint> <hint type="destinationlabel"> - <x>666</x> - <y>19</y> + <x>752</x> + <y>29</y> </hint> </hints> </connection> @@ -467,8 +490,24 @@ <y>49</y> </hint> <hint type="destinationlabel"> - <x>378</x> - <y>19</y> + <x>433</x> + <y>29</y> + </hint> + </hints> + </connection> + <connection> + <sender>secureSessionCheckBox</sender> + <signal>toggled(bool)</signal> + <receiver>secureSessionCb</receiver> + <slot>setEnabled(bool)</slot> + <hints> + <hint type="sourcelabel"> + <x>16</x> + <y>368</y> + </hint> + <hint type="destinationlabel"> + <x>147</x> + <y>369</y> </hint> </hints> </connection> diff --git a/examples/knx/knxeditor/mainwindow.cpp b/examples/knx/knxeditor/mainwindow.cpp index 17200ef..c967ca2 100644 --- a/examples/knx/knxeditor/mainwindow.cpp +++ b/examples/knx/knxeditor/mainwindow.cpp @@ -52,12 +52,15 @@ #include "ui_mainwindow.h" #include <QElapsedTimer> +#include <QFileDialog> +#include <QInputDialog> #include <QNetworkInterface> #include <QStandardItem> +#include <QStandardPaths> Ui::MainWindow *MainWindow::s_ui { nullptr }; -static QString familieToString(QKnxNetIp::ServiceFamily id) +static QString familyToString(QKnxNetIp::ServiceFamily id) { switch (id) { case QKnxNetIp::ServiceFamily::Core: @@ -198,7 +201,7 @@ void MainWindow::newServerSelected(int serverBoxIndex) const auto services = info.supportedServices(); for (const auto &service : services) { value.append(tr("<tr><td class=\"padding\">%1</td></th>") - .arg(tr("KNXnet/IP %1, Version: %2").arg(familieToString(service.ServiceFamily)) + .arg(tr("KNXnet/IP %1, Version: %2").arg(familyToString(service.ServiceFamily)) .arg(service.ServiceFamilyVersion))); if (service.ServiceFamilyVersion >= 2) version2Supported = true; @@ -260,7 +263,7 @@ void MainWindow::showServerAndServices(const QKnxNetIpServerInfo &info) const auto services = info.supportedServices(); for (const auto service : services) { ui->outputEdit->append(tr(" KNXnet/IP %1, Version: %2") - .arg(familieToString(service.ServiceFamily)).arg(service.ServiceFamilyVersion)); + .arg(familyToString(service.ServiceFamily)).arg(service.ServiceFamilyVersion)); } ui->serverBox->addItem(tr("%1 (%2:%3)").arg(info.deviceName(), info.controlEndpointAddress() @@ -274,6 +277,34 @@ void MainWindow::on_radioButtonTCP_toggled(bool checked) ui->tunnelingFeatures->setTcpEnable(checked); } +void MainWindow::on_actionEtsKeyringImport_triggered() +{ + auto fileName = QFileDialog::getOpenFileName(this, tr("Import keyring file"), + QStandardPaths::standardLocations(QStandardPaths::DesktopLocation).value(0), + tr("KNX keyring file (*.knxkeys)")); + + if (fileName.isEmpty()) + return; + + bool ok; + auto password = QInputDialog::getText(this, tr("Import keyring file"), + tr("Keyring file password:"), QLineEdit::Normal, {}, &ok); + if (!ok || password.isEmpty()) + return; + + auto mgmtConfigs = QKnxNetIpSecureConfiguration::fromKeyring(QKnxNetIpSecureConfiguration + ::Type::DeviceManagement, fileName, password.toUtf8(), true); + ui->deviceManagement->onKeyringChanged(mgmtConfigs); + + for (auto &config : mgmtConfigs) + config.setIndividualAddress({}); + + auto tunnelConfigs = QKnxNetIpSecureConfiguration::fromKeyring(QKnxNetIpSecureConfiguration + ::Type::Tunneling, fileName, password.toUtf8(), true); + ui->tunneling->onKeyringChanged(mgmtConfigs + tunnelConfigs); + ui->tunnelingFeatures->onKeyringChanged(mgmtConfigs + tunnelConfigs); +} + void MainWindow::fillLocalIpBox() { auto firstItem = new QStandardItem(tr("Interface: IP address --Select One--")); diff --git a/examples/knx/knxeditor/mainwindow.h b/examples/knx/knxeditor/mainwindow.h index 8aa256f..6c1a3d7 100644 --- a/examples/knx/knxeditor/mainwindow.h +++ b/examples/knx/knxeditor/mainwindow.h @@ -69,11 +69,15 @@ public: explicit MainWindow(QWidget *parent = 0); ~MainWindow(); +signals: + void keyringChanged(const QString &fileName, const QString &password); + private slots: void newServerSelected(int serverBoxIndex); void newIPAddressSelected(int localIpBoxIndex); void showServerAndServices(const QKnxNetIpServerInfo &info); void on_radioButtonTCP_toggled(bool checked); + void on_actionEtsKeyringImport_triggered(); private: void fillLocalIpBox(); diff --git a/examples/knx/knxeditor/mainwindow.ui b/examples/knx/knxeditor/mainwindow.ui index 0682a17..65ff6a3 100644 --- a/examples/knx/knxeditor/mainwindow.ui +++ b/examples/knx/knxeditor/mainwindow.ui @@ -247,7 +247,13 @@ <property name="title"> <string>File</string> </property> - <addaction name="separator"/> + <widget class="QMenu" name="menuImport"> + <property name="title"> + <string>Import</string> + </property> + <addaction name="actionEtsKeyringImport"/> + </widget> + <addaction name="menuImport"/> <addaction name="separator"/> <addaction name="actionExit"/> </widget> @@ -310,6 +316,11 @@ <string>Clear All</string> </property> </action> + <action name="actionEtsKeyringImport"> + <property name="text"> + <string>KNX keyring file...</string> + </property> + </action> </widget> <customwidgets> <customwidget> diff --git a/examples/knx/knxeditor/tunneling.cpp b/examples/knx/knxeditor/tunneling.cpp index 7b21ee0..6a5e967 100644 --- a/examples/knx/knxeditor/tunneling.cpp +++ b/examples/knx/knxeditor/tunneling.cpp @@ -51,11 +51,13 @@ #include "tunneling.h" #include "ui_tunneling.h" -#include <QtCore/QMetaEnum> -#include <QtCore/QMetaType> -#include <QtGui/QStandardItemModel> -#include <QtKnx/QKnxLinkLayerFrameBuilder> -#include <QtWidgets/QTreeView> +#include <QKnxLinkLayerFrameBuilder> +#include <QKnxNetIpSecureConfiguration> +#include <QMetaEnum> +#include <QMetaType> +#include <QStandardItemModel> +#include <QTreeView> +#include <QTreeWidget> // -- KnxAddressValidator @@ -94,9 +96,16 @@ Tunneling::Tunneling(QWidget* parent) connect(ui->connectTunneling, &QPushButton::clicked, this, [&]() { m_tunnel.setLocalPort(0); - m_tunnel.connectToHost(m_server.controlEndpointAddress(), - m_server.controlEndpointPort(), - m_proto); + if (ui->secureSessionCheckBox->isChecked()) { + auto config = m_configs.value(ui->secureSessionCb->currentIndex()); + config.setKeepSecureSessionAlive(true); + m_tunnel.setSecureConfiguration(config); + m_tunnel.connectToHostEncrypted(m_server.controlEndpointAddress(), + m_server.controlEndpointPort()); + } else { + m_tunnel.connectToHost(m_server.controlEndpointAddress(), + m_server.controlEndpointPort(), m_proto); + } }); connect(&m_tunnel, &QKnxNetIpTunnel::connected, this, [&] { @@ -113,6 +122,8 @@ Tunneling::Tunneling(QWidget* parent) }); connect(&m_tunnel, &QKnxNetIpTunnel::disconnected, this, [&] { + if (!ui) + return; ui->connectTunneling->setEnabled(true); ui->disconnectTunneling->setEnabled(false); ui->tunnelingSendRequest->setEnabled(false); @@ -241,6 +252,7 @@ Tunneling::Tunneling(QWidget* parent) Tunneling::~Tunneling() { delete ui; + ui = nullptr; } void Tunneling::setNatAware(bool isNatAware) @@ -263,6 +275,8 @@ void Tunneling::setKnxNetIpServer(const QKnxNetIpServerInfo &server) ui->connectTunneling->setEnabled(true); ui->disconnectTunneling->setEnabled(false); } + + updateSecureConfigCombo(); ui->tunnelingSendRequest->setEnabled(false); } @@ -331,6 +345,12 @@ void Tunneling::on_manualInput_clicked(bool checked) ui->cemiFrame->setFocus(); } +void Tunneling::onKeyringChanged(const QVector<QKnxNetIpSecureConfiguration> &configs) +{ + m_configs = configs; + updateSecureConfigCombo(); +} + void Tunneling::setupApciTpciComboBox() { int index = QKnxTpdu::staticMetaObject.indexOfEnumerator("ApplicationControlField"); @@ -384,3 +404,22 @@ void Tunneling::updateAdditionalInfoTypesComboBox() model->item(0)->setEnabled(false); model->item(model->rowCount() - 1)->setEnabled(false); } + +void Tunneling::updateSecureConfigCombo() +{ + ui->secureSessionCb->clear(); + + ui->secureSessionCheckBox->setEnabled(!m_configs.isEmpty()); + ui->secureSessionCheckBox->setChecked(m_proto == QKnxNetIp::HostProtocol::TCP_IPv4); + + for (int i = 0; i < m_configs.size(); ++i) { + const auto &config = m_configs[i]; + if (m_server.individualAddress() != config.host()) + continue; + + const auto ia = config.individualAddress(); + ui->secureSessionCb->addItem(tr("User ID: %1 (Individual Address: %2)") + .arg(config.userId()) + .arg(ia.isValid() ? ia.toString() : tr("No specific address")), i); + } +} diff --git a/examples/knx/knxeditor/tunneling.h b/examples/knx/knxeditor/tunneling.h index 4431223..c95e41b 100644 --- a/examples/knx/knxeditor/tunneling.h +++ b/examples/knx/knxeditor/tunneling.h @@ -53,11 +53,11 @@ #include <QKnxControlField> #include <QKnxExtendedControlField> +#include <QKnxLinkLayerFrame> #include <QKnxNetIpTunnel> #include <QKnxNetIpServerInfo> -#include <QKnxLinkLayerFrame> -#include <QValidator> #include <QRegularExpression> +#include <QValidator> #include <QWidget> QT_BEGIN_NAMESPACE @@ -94,6 +94,7 @@ public: void setLocalAddress(const QHostAddress &address); void setKnxNetIpServer(const QKnxNetIpServerInfo &server); void setTcpEnable(bool value); + void onKeyringChanged(const QVector<QKnxNetIpSecureConfiguration> &configs); public slots: void clearLogging(); @@ -108,6 +109,7 @@ private: void setupApciTpciComboBox(); void setupMessageCodeComboBox(); void updateAdditionalInfoTypesComboBox(); + void updateSecureConfigCombo(); private: Ui::Tunneling *ui { nullptr }; @@ -119,6 +121,8 @@ private: QKnxNetIpTunnel m_tunnel; QKnxNetIpServerInfo m_server; QKnxNetIp::HostProtocol m_proto { QKnxNetIp::HostProtocol::UDP_IPv4 }; + QVector<QKnxNetIpSecureConfiguration> m_configs; + }; #endif diff --git a/examples/knx/knxeditor/tunneling.ui b/examples/knx/knxeditor/tunneling.ui index 072211b..ddf01a3 100644 --- a/examples/knx/knxeditor/tunneling.ui +++ b/examples/knx/knxeditor/tunneling.ui @@ -7,13 +7,13 @@ <x>0</x> <y>0</y> <width>856</width> - <height>385</height> + <height>404</height> </rect> </property> <property name="windowTitle"> <string>Form</string> </property> - <layout class="QVBoxLayout" name="verticalLayout_4" stretch="0,0,0,1,0"> + <layout class="QVBoxLayout" name="verticalLayout_2"> <item> <layout class="QHBoxLayout" name="horizontalLayout_3" stretch="0,1,0,0,0,0,0"> <item> @@ -540,7 +540,7 @@ </layout> </item> <item> - <layout class="QHBoxLayout" name="horizontalLayout_5"> + <layout class="QHBoxLayout" name="horizontalLayout_4"> <item> <widget class="QLabel" name="label_3"> <property name="sizePolicy"> @@ -608,26 +608,7 @@ </layout> </item> <item> - <widget class="QTextEdit" name="textOuputTunneling"> - <property name="focusPolicy"> - <enum>Qt::StrongFocus</enum> - </property> - <property name="contextMenuPolicy"> - <enum>Qt::DefaultContextMenu</enum> - </property> - <property name="acceptDrops"> - <bool>false</bool> - </property> - <property name="undoRedoEnabled"> - <bool>false</bool> - </property> - <property name="readOnly"> - <bool>true</bool> - </property> - </widget> - </item> - <item> - <layout class="QHBoxLayout" name="horizontalLayout_4"> + <layout class="QHBoxLayout" name="horizontalLayout_5"> <item> <widget class="QCheckBox" name="manualInput"> <property name="text"> @@ -636,7 +617,7 @@ </widget> </item> <item> - <spacer name="horizontalSpacer_3"> + <spacer name="horizontalSpacer_7"> <property name="orientation"> <enum>Qt::Horizontal</enum> </property> @@ -679,18 +660,70 @@ </item> <item> <widget class="QPushButton" name="tunnelingSendRequest"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> <property name="text"> <string>Send Request</string> </property> </widget> </item> + </layout> + </item> + <item> + <widget class="QTextEdit" name="textOuputTunneling"> + <property name="focusPolicy"> + <enum>Qt::StrongFocus</enum> + </property> + <property name="contextMenuPolicy"> + <enum>Qt::DefaultContextMenu</enum> + </property> + <property name="acceptDrops"> + <bool>false</bool> + </property> + <property name="undoRedoEnabled"> + <bool>false</bool> + </property> + <property name="readOnly"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <item> + <widget class="QCheckBox" name="secureSessionCheckBox"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>Use secure session</string> + </property> + </widget> + </item> + <item> + <widget class="QComboBox" name="secureSessionCb"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </item> <item> <spacer name="horizontalSpacer_2"> <property name="orientation"> <enum>Qt::Horizontal</enum> </property> <property name="sizeType"> - <enum>QSizePolicy::Fixed</enum> + <enum>QSizePolicy::Expanding</enum> </property> <property name="sizeHint" stdset="0"> <size> @@ -740,8 +773,6 @@ <tabstop>data</tabstop> <tabstop>manualInput</tabstop> <tabstop>tunnelingSendRequest</tabstop> - <tabstop>connectTunneling</tabstop> - <tabstop>disconnectTunneling</tabstop> </tabstops> <resources/> <connections> @@ -752,8 +783,8 @@ <slot>setDisabled(bool)</slot> <hints> <hint type="sourcelabel"> - <x>16</x> - <y>364</y> + <x>26</x> + <y>282</y> </hint> <hint type="destinationlabel"> <x>23</x> @@ -768,8 +799,8 @@ <slot>setDisabled(bool)</slot> <hints> <hint type="sourcelabel"> - <x>37</x> - <y>363</y> + <x>47</x> + <y>282</y> </hint> <hint type="destinationlabel"> <x>94</x> @@ -784,8 +815,8 @@ <slot>setDisabled(bool)</slot> <hints> <hint type="sourcelabel"> - <x>58</x> - <y>361</y> + <x>68</x> + <y>282</y> </hint> <hint type="destinationlabel"> <x>42</x> @@ -800,8 +831,8 @@ <slot>setDisabled(bool)</slot> <hints> <hint type="sourcelabel"> - <x>69</x> - <y>362</y> + <x>79</x> + <y>282</y> </hint> <hint type="destinationlabel"> <x>462</x> @@ -816,8 +847,8 @@ <slot>setDisabled(bool)</slot> <hints> <hint type="sourcelabel"> - <x>14</x> - <y>359</y> + <x>24</x> + <y>282</y> </hint> <hint type="destinationlabel"> <x>576</x> @@ -832,8 +863,8 @@ <slot>setDisabled(bool)</slot> <hints> <hint type="sourcelabel"> - <x>56</x> - <y>360</y> + <x>66</x> + <y>282</y> </hint> <hint type="destinationlabel"> <x>402</x> @@ -848,8 +879,8 @@ <slot>setDisabled(bool)</slot> <hints> <hint type="sourcelabel"> - <x>57</x> - <y>364</y> + <x>67</x> + <y>282</y> </hint> <hint type="destinationlabel"> <x>706</x> @@ -864,8 +895,8 @@ <slot>setDisabled(bool)</slot> <hints> <hint type="sourcelabel"> - <x>50</x> - <y>357</y> + <x>60</x> + <y>282</y> </hint> <hint type="destinationlabel"> <x>845</x> @@ -880,12 +911,12 @@ <slot>setDisabled(bool)</slot> <hints> <hint type="sourcelabel"> - <x>29</x> - <y>355</y> + <x>39</x> + <y>282</y> </hint> <hint type="destinationlabel"> - <x>181</x> - <y>256</y> + <x>539</x> + <y>254</y> </hint> </hints> </connection> @@ -896,8 +927,8 @@ <slot>setDisabled(bool)</slot> <hints> <hint type="sourcelabel"> - <x>79</x> - <y>355</y> + <x>89</x> + <y>282</y> </hint> <hint type="destinationlabel"> <x>680</x> @@ -912,12 +943,12 @@ <slot>setDisabled(bool)</slot> <hints> <hint type="sourcelabel"> - <x>12</x> - <y>365</y> + <x>22</x> + <y>282</y> </hint> <hint type="destinationlabel"> - <x>19</x> - <y>251</y> + <x>280</x> + <y>254</y> </hint> </hints> </connection> @@ -928,12 +959,12 @@ <slot>setDisabled(bool)</slot> <hints> <hint type="sourcelabel"> - <x>15</x> - <y>369</y> + <x>25</x> + <y>282</y> </hint> <hint type="destinationlabel"> - <x>116</x> - <y>245</y> + <x>411</x> + <y>254</y> </hint> </hints> </connection> @@ -944,12 +975,12 @@ <slot>setDisabled(bool)</slot> <hints> <hint type="sourcelabel"> - <x>51</x> - <y>363</y> + <x>61</x> + <y>282</y> </hint> <hint type="destinationlabel"> - <x>416</x> - <y>256</y> + <x>36</x> + <y>254</y> </hint> </hints> </connection> @@ -960,12 +991,28 @@ <slot>setDisabled(bool)</slot> <hints> <hint type="sourcelabel"> - <x>51</x> - <y>363</y> + <x>61</x> + <y>282</y> + </hint> + <hint type="destinationlabel"> + <x>254</x> + <y>254</y> + </hint> + </hints> + </connection> + <connection> + <sender>secureSessionCheckBox</sender> + <signal>toggled(bool)</signal> + <receiver>secureSessionCb</receiver> + <slot>setEnabled(bool)</slot> + <hints> + <hint type="sourcelabel"> + <x>75</x> + <y>384</y> </hint> <hint type="destinationlabel"> - <x>472</x> - <y>256</y> + <x>150</x> + <y>383</y> </hint> </hints> </connection> diff --git a/examples/knx/knxeditor/tunnelingfeatures.cpp b/examples/knx/knxeditor/tunnelingfeatures.cpp index 851cc5f..dedec6d 100644 --- a/examples/knx/knxeditor/tunnelingfeatures.cpp +++ b/examples/knx/knxeditor/tunnelingfeatures.cpp @@ -74,12 +74,21 @@ TunnelingFeatures::TunnelingFeatures(QWidget *parent) ui->featureValue->setEnabled(false); connect(ui->connectTunneling, &QPushButton::clicked, this, [&]() { - ui->textOuputTunneling->append(tr("Connecting to: %1 on port: %2 protocol: %3").arg(m_server - .controlEndpointAddress().toString()).arg(m_server.controlEndpointPort()).arg(int(m_protocol))); + ui->textOuputTunneling->append(tr("Connecting to: %1 on port: %2 protocol: %3") + .arg(m_server.controlEndpointAddress().toString()) + .arg(m_server.controlEndpointPort()).arg(int(m_protocol))); + m_tunnel.setLocalPort(0); - m_tunnel.connectToHost(m_server.controlEndpointAddress(), - m_server.controlEndpointPort(), - m_protocol); + if (ui->secureSessionCheckBox->isChecked()) { + auto config = m_configs.value(ui->secureSessionCb->currentIndex()); + config.setKeepSecureSessionAlive(true); + m_tunnel.setSecureConfiguration(config); + m_tunnel.connectToHostEncrypted(m_server.controlEndpointAddress(), + m_server.controlEndpointPort()); + } else { + m_tunnel.connectToHost(m_server.controlEndpointAddress(), + m_server.controlEndpointPort(), m_protocol); + } }); connect(ui->tunnelingSend, &QPushButton::clicked, this, [&]() { @@ -100,7 +109,7 @@ TunnelingFeatures::TunnelingFeatures(QWidget *parent) else if (type == ServType::TunnelingFeatureSet) m_tunnel.sendTunnelingFeatureSet(featureType, bytes); - ui->statusBar->setText(tr("Status: (%1) Messages sent.").arg(m_tunnel + ui->textOuputTunneling->append(tr("Status: (%1) Messages sent.").arg(m_tunnel .sequenceCount(QKnxNetIpEndpointConnection::SequenceType::Send) + 1)); }); @@ -190,7 +199,7 @@ TunnelingFeatures::TunnelingFeatures(QWidget *parent) ui->textOuputTunneling->append(tr("Successfully connected to: %1 on port: %2").arg(m_server .controlEndpointAddress().toString()).arg(m_server.controlEndpointPort())); - ui->statusBar->setText("Status: Connected."); + ui->textOuputTunneling->append("Status: Connected."); }); connect(ui->disconnectTunneling, &QPushButton::clicked, this, [&]() { @@ -206,7 +215,7 @@ TunnelingFeatures::TunnelingFeatures(QWidget *parent) ui->featureValue->setEnabled(false); ui->textOuputTunneling->append(tr("Successfully disconnected from: %1 on port: %2\n") .arg(m_server.controlEndpointAddress().toString()).arg(m_server.controlEndpointPort())); - ui->statusBar->setText("Status: Disconnected."); + ui->textOuputTunneling->append("Status: Disconnected."); }); connect(&m_tunnel, &QKnxNetIpTunnel::errorOccurred, this, @@ -217,10 +226,10 @@ TunnelingFeatures::TunnelingFeatures(QWidget *parent) connect(ui->tunnelServiceType, &QComboBox::currentTextChanged, this, [&](const QString &text) { if (text == QString("TunnelingFeatureSet")) { ui->featureValue->setEnabled(true); - ui->statusBar->setText("Status: Fill in the feature type and value fields."); + ui->textOuputTunneling->append("Status: Fill in the feature type and value fields."); } else { ui->featureValue->setEnabled(false); - ui->statusBar->setText(""); + ui->textOuputTunneling->append(""); } checkFeatureValue(); }); @@ -259,8 +268,10 @@ void TunnelingFeatures::setKnxNetIpServer(const QKnxNetIpServerInfo &server) ui->connectTunneling->setEnabled(true); ui->disconnectTunneling->setEnabled(false); } + + updateSecureConfigCombo(); ui->tunnelingSend->setEnabled(false); - ui->statusBar->setText("Status: Start by clicking connect."); + ui->textOuputTunneling->append("Status: Start by clicking connect."); } void TunnelingFeatures::setTcpEnable(bool value) @@ -268,10 +279,16 @@ void TunnelingFeatures::setTcpEnable(bool value) m_protocol = (value ? QKnxNetIp::HostProtocol::TCP_IPv4 : QKnxNetIp::HostProtocol::UDP_IPv4); } +void TunnelingFeatures::onKeyringChanged(const QVector<QKnxNetIpSecureConfiguration> &configs) +{ + m_configs = configs; + updateSecureConfigCombo(); +} + void TunnelingFeatures::checkFeatureValue() { if (!ui->featureValue->isEnabled()) { - ui->statusBar->setText(""); + ui->textOuputTunneling->append(""); ui->tunnelingSend->setEnabled(m_tunnel.state() == QKnxNetIpEndpointConnection::State::Connected); return; } @@ -289,10 +306,29 @@ void TunnelingFeatures::checkFeatureValue() auto text = ui->featureValue->text(); if (text.isEmpty() || !validFeature(type, featureType, bytes) || ((text.size() % 2) != 0)) { - ui->statusBar->setText("Status: Invalid value entered"); + ui->textOuputTunneling->append("Status: Invalid value entered"); ui->tunnelingSend->setEnabled(false); return; } - ui->statusBar->setText("Status: Valid value entered, click send."); + ui->textOuputTunneling->append("Status: Valid value entered, click send."); ui->tunnelingSend->setEnabled(m_tunnel.state() == QKnxNetIpEndpointConnection::State::Connected); } + +void TunnelingFeatures::updateSecureConfigCombo() +{ + ui->secureSessionCb->clear(); + + ui->secureSessionCheckBox->setEnabled(!m_configs.isEmpty()); + ui->secureSessionCheckBox->setChecked(m_protocol == QKnxNetIp::HostProtocol::TCP_IPv4); + + for (int i = 0; i < m_configs.size(); ++i) { + const auto &config = m_configs[i]; + if (m_server.individualAddress() != config.host()) + continue; + + const auto ia = config.individualAddress(); + ui->secureSessionCb->addItem(tr("User ID: %1 (Individual Address: %2)") + .arg(config.userId()) + .arg(ia.isValid() ? ia.toString() : tr("No specific address")), i); + } +} diff --git a/examples/knx/knxeditor/tunnelingfeatures.h b/examples/knx/knxeditor/tunnelingfeatures.h index 8c93667..162497e 100644 --- a/examples/knx/knxeditor/tunnelingfeatures.h +++ b/examples/knx/knxeditor/tunnelingfeatures.h @@ -53,15 +53,20 @@ public: void setLocalAddress(const QHostAddress &address); void setKnxNetIpServer(const QKnxNetIpServerInfo &server); void setTcpEnable(bool value); + void onKeyringChanged(const QVector<QKnxNetIpSecureConfiguration> &configs); private: void checkFeatureValue(); - Ui::TunnelingFeatures *ui; + void updateSecureConfigCombo(); + +private: + Ui::TunnelingFeatures *ui { nullptr }; QKnxNetIpServerInfo m_server; QKnxNetIpTunnel m_tunnel; QKnxNetIp::HostProtocol m_protocol = { QKnxNetIp::HostProtocol::UDP_IPv4 }; + QVector<QKnxNetIpSecureConfiguration> m_configs; }; #endif // TUNNELINGFEATURES_H diff --git a/examples/knx/knxeditor/tunnelingfeatures.ui b/examples/knx/knxeditor/tunnelingfeatures.ui index 50abfc8..112eb27 100644 --- a/examples/knx/knxeditor/tunnelingfeatures.ui +++ b/examples/knx/knxeditor/tunnelingfeatures.ui @@ -13,8 +13,8 @@ <property name="windowTitle"> <string>Form</string> </property> - <layout class="QGridLayout" name="gridLayout_2"> - <item row="0" column="0" colspan="4"> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> <layout class="QGridLayout" name="gridLayout" columnstretch="0,1,0,0"> <item row="0" column="0"> <widget class="QLabel" name="label_13"> @@ -184,7 +184,7 @@ </item> </layout> </item> - <item row="1" column="0" colspan="4"> + <item> <widget class="QTextEdit" name="textOuputTunneling"> <property name="focusPolicy"> <enum>Qt::StrongFocus</enum> @@ -203,48 +203,93 @@ </property> </widget> </item> - <item row="2" column="0"> - <widget class="QLabel" name="statusBar"> - <property name="text"> - <string/> - </property> - </widget> - </item> - <item row="2" column="1"> - <spacer name="horizontalSpacer_2"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeType"> - <enum>QSizePolicy::Expanding</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>662</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - <item row="2" column="2"> - <widget class="QPushButton" name="connectTunneling"> - <property name="text"> - <string>Connect</string> - </property> - </widget> - </item> - <item row="2" column="3"> - <widget class="QPushButton" name="disconnectTunneling"> - <property name="text"> - <string>Disconnect</string> - </property> - </widget> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QCheckBox" name="secureSessionCheckBox"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>Use secure session</string> + </property> + </widget> + </item> + <item> + <widget class="QComboBox" name="secureSessionCb"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeType"> + <enum>QSizePolicy::Expanding</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>388</width> + <height>17</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="connectTunneling"> + <property name="text"> + <string>Connect</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="disconnectTunneling"> + <property name="text"> + <string>Disconnect</string> + </property> + </widget> + </item> + </layout> </item> </layout> </widget> <tabstops> + <tabstop>tunnelServiceType</tabstop> + <tabstop>featureIdentifier</tabstop> + <tabstop>featureValue</tabstop> + <tabstop>tunnelingSend</tabstop> + <tabstop>textOuputTunneling</tabstop> + <tabstop>secureSessionCheckBox</tabstop> + <tabstop>secureSessionCb</tabstop> + <tabstop>connectTunneling</tabstop> <tabstop>disconnectTunneling</tabstop> </tabstops> <resources/> - <connections/> + <connections> + <connection> + <sender>secureSessionCheckBox</sender> + <signal>toggled(bool)</signal> + <receiver>secureSessionCb</receiver> + <slot>setEnabled(bool)</slot> + <hints> + <hint type="sourcelabel"> + <x>42</x> + <y>363</y> + </hint> + <hint type="destinationlabel"> + <x>153</x> + <y>363</y> + </hint> + </hints> + </connection> + </connections> </ui> diff --git a/src/knx/doc/qtknx.qdocconf b/src/knx/doc/qtknx.qdocconf index 64deeb8..a20ee87 100644 --- a/src/knx/doc/qtknx.qdocconf +++ b/src/knx/doc/qtknx.qdocconf @@ -39,7 +39,7 @@ moduleheader = QtKnx Cpp.ignoretokens += Q_KNX_EXPORT -depends += qtcore qtdoc qtnetwork qmake +depends += qtcore qtdoc qtgui qtnetwork qmake qtwidgets # Custom module header for Clang doc builds moduleheader = QtKnx_pch.h @@ -50,3 +50,4 @@ manifestmeta.thumbnail.names += "QtKnx/Discoverer*" \ navigation.landingpage = "Qt KNX" navigation.cppclassespage = "Qt KNX C++ Classes" +navigation.homepage = Qt for Automation diff --git a/src/knx/dpt/qknx1bit.cpp b/src/knx/dpt/qknx1bit.cpp index 7e0f64c..2bbf3af 100644 --- a/src/knx/dpt/qknx1bit.cpp +++ b/src/knx/dpt/qknx1bit.cpp @@ -111,7 +111,7 @@ QKnx1Bit::QKnx1Bit(int subType, bool bit) : QKnxFixedSizeDatapointType(MainType, subType, TypeSize) { setDescription(tr("1-bit")); - setRangeText(tr("true"), tr("false")); + setRangeText(tr("false"), tr("true")); setRange(QVariant(0x00), QVariant(0x01)); setBit(bit); diff --git a/src/knx/dpt/qknxdatapointtype.cpp b/src/knx/dpt/qknxdatapointtype.cpp index b3fa6f0..7ee6368 100644 --- a/src/knx/dpt/qknxdatapointtype.cpp +++ b/src/knx/dpt/qknxdatapointtype.cpp @@ -698,8 +698,8 @@ QKnxDatapointType::QKnxDatapointType(Type type, int size) // Datapoint Type shall be identified by a 16 bit main number separated // by a dot from a 16 bit sub number. The assumption being made is that // QKnxDatapointType::Type is encoded in that way while omitting the dot. - int mainType = number.left(number.size() - 5).toInt(&okMain); - int subType = number.right(5).toInt(&okSub); + quint16 mainType = number.leftRef(number.size() - 5).toUShort(&okMain); + quint16 subType = number.rightRef(5).toUShort(&okSub); if (okMain && okSub) d_ptr->setup(mainType, subType, quint32(type), size); @@ -1052,7 +1052,7 @@ QKnxDatapointType::Type QKnxDatapointType::toType(const QString &dpt) } quint32 type; - if (dtp.toType(mainType, subType, &type)) + if (QKnxDatapointTypePrivate::toType(mainType, subType, &type)) return static_cast<Type> (type); return QKnxDatapointType::Type::Unknown; } diff --git a/src/knx/dpt/qknxdatapointtypefactory.cpp b/src/knx/dpt/qknxdatapointtypefactory.cpp index 4ab11a0..184e843 100644 --- a/src/knx/dpt/qknxdatapointtypefactory.cpp +++ b/src/knx/dpt/qknxdatapointtypefactory.cpp @@ -138,8 +138,8 @@ QKnxDatapointType *QKnxDatapointTypeFactory::createType(QKnxDatapointType::Type // Datapoint Type shall be identified by a 16 bit main number separated by a dot from a 16 bit // sub number. The assumption being made is that QKnxDatapointType::Type is encoded in that way // while omitting the dot. - int mainType = number.left(number.size() - 5).toInt(&okMain); - int subType = number.right(5).toInt(&okSub); + int mainType = number.leftRef(number.size() - 5).toInt(&okMain); + int subType = number.rightRef(5).toInt(&okSub); if (okMain && okSub) return createType(mainType, subType); @@ -223,6 +223,7 @@ QKnxDatapointTypeFactory::QKnxDatapointTypeFactory() registerType<QKnxLogicalFunction>(); registerType<QKnxSceneAB>(); registerType<QKnxShutterBlindsMode>(); + registerType<QKnxHeatCool>(); // DPT-2 registerType<QKnx1BitControlled>(); diff --git a/src/knx/ets/ets.pri b/src/knx/ets/ets.pri new file mode 100644 index 0000000..f6b40fd --- /dev/null +++ b/src/knx/ets/ets.pri @@ -0,0 +1,4 @@ +INCLUDEPATH += $$PWD + +SOURCES += $$PWD/manufacturers.cpp +PUBLIC_HEADERS += $$PWD/manufacturers.h diff --git a/src/knx/ets/manufacturers.cpp b/src/knx/ets/manufacturers.cpp new file mode 100644 index 0000000..064675d --- /dev/null +++ b/src/knx/ets/manufacturers.cpp @@ -0,0 +1,490 @@ +/****************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtKnx module. +** +** $QT_BEGIN_LICENSE:GPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 or (at your option) any later version +** approved by the KDE Free Qt Foundation. The licenses are as published by +** the Free Software Foundation and appearing in the file LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +******************************************************************************/ + +#include "manufacturers.h" + +#include <QtCore/qvector.h> + +QT_BEGIN_NAMESPACE + +struct Manufacturer +{ + Manufacturer() = default; + Manufacturer(int i, const char *mId, const char *n) + : id(i) + , manufacturerId(mId) + , name(QString::fromUtf8(QByteArray::fromHex(n))) + {} + + int id; + QLatin1String manufacturerId; + QString name; + + static const QVector<Manufacturer> &manufacturers() + { + static QVector<Manufacturer> sManufacturers { + { 1, "M-0001", "5369656d656e73" }, + { 2, "M-0002", "414242" }, + { 4, "M-0004", "416c627265636874204a756e67" }, + { 5, "M-0005", "42746963696e6f" }, + { 6, "M-0006", "4265726b6572" }, + { 7, "M-0007", "42757363682d4a616567657220456c656b74726f" }, + { 11, "M-000B", "4c454752414e4420417070617265696c6c61676520c3a96c6563747269717565" }, + { 12, "M-000C", "4d657274656e" }, + { 14, "M-000E", "4142425370412d53414345204469766973696f6e" }, + { 22, "M-0016", "536965646c6520262053c3b6686e65" }, + { 24, "M-0018", "456265726c65" }, + { 25, "M-0019", "474557495353" }, + { 27, "M-001B", "416c626572742041636b65726d616e6e" }, + { 28, "M-001C", "53636875706120476d6248" }, + { 29, "M-001D", "414242205343485745495a" }, + { 30, "M-001E", "46656c6c6572" }, + { 32, "M-0020", "4445484e20262053c396484e45" }, + { 33, "M-0021", "4352414254524545" }, + { 36, "M-0024", "5061756c20486f63686bc3b670706572" }, + { 37, "M-0025", "416c74656e62757267657220456c656374726f6e6963" }, + { 41, "M-0029", "4772c3a473736c696e" }, + { 42, "M-002A", "53696d6f6e" }, + { 44, "M-002C", "56494d4152" }, + { 45, "M-002D", "4d6f656c6c657220476562c3a47564656175746f6d6174696f6e204b47" }, + { 46, "M-002E", "456c74616b6f" }, + { 49, "M-0031", "426f7363682d5369656d656e73204861757368616c7473676572c3a47465" }, + { 52, "M-0034", "524954544f20476d624826436f2e4b47" }, + { 53, "M-0035", "506f77657220436f6e74726f6c73" }, + { 55, "M-0037", "5a554d544f42454c" }, + { 57, "M-0039", "50686f656e697820436f6e74616374" }, + { 61, "M-003D", "5741474f204b6f6e74616b74746563686e696b" }, + { 66, "M-0042", "5769656c616e6420456c656374726963" }, + { 67, "M-0043", "4865726d616e6e204b6c65696e68756973" }, + { 69, "M-0045", "5374696562656c20456c74726f6e" }, + { 71, "M-0047", "546568616c6974" }, + { 72, "M-0048", "54686562656e204147" }, + { 73, "M-0049", "57696c68656c6d20527574656e6265636b" }, + { 75, "M-004B", "57696e6b68617573" }, + { 76, "M-004C", "526f6265727420426f736368" }, + { 78, "M-004E", "536f6d6679" }, + { 80, "M-0050", "576f6572747a" }, + { 81, "M-0051", "56696573736d616e6e205765726b65" }, + { 82, "M-0052", "494d4920487964726f6e696320456e67696e656572696e67" }, + { 83, "M-0053", "4a6f682e205661696c6c616e74" }, + { 85, "M-0055", "414d5020446575747363686c616e64" }, + { 89, "M-0059", "426f73636820546865726d6f746563686e696b20476d6248" }, + { 90, "M-005A", "534546202d2045434f544543" }, + { 92, "M-005C", "444f524d4120476d6248202b20436f2e204b47" }, + { 93, "M-005D", "57696e646f774d617374657220412f53" }, + { 94, "M-005E", "57616c74686572205765726b65" }, + { 95, "M-005F", "4f524153" }, + { 97, "M-0061", "44c3a47477796c6572" }, + { 98, "M-0062", "456c65637472616b" }, + { 99, "M-0063", "54656368656d" }, + { 100, "M-0064", "5363686e656964657220456c65637472696320496e647573747269657320534153" }, + { 101, "M-0065", "5748442057696c68656c6d204875626572202b2053c3b6686e65" }, + { 102, "M-0066", "42697363686f666620456c656b74726f6e696b" }, + { 104, "M-0068", "4a4550415a" }, + { 105, "M-0069", "525453204175746f6d6174696f6e" }, + { 106, "M-006A", "4549424d41524b5420476d6248" }, + { 107, "M-006B", "574152454d412052656e6b686f6666205345" }, + { 108, "M-006C", "45656c656374726f6e" }, + { 109, "M-006D", "42656c64656e20576972652026204361626c6520422e562e" }, + { 110, "M-006E", "4265636b65722d416e74726965626520476d6248" }, + { 111, "M-006F", "4a2e537465686c652b53c3b6686e6520476d6248" }, + { 112, "M-0070", "414746454f" }, + { 113, "M-0071", "5a656e6e696f" }, + { 114, "M-0072", "5441504b4f20546563686e6f6c6f67696573" }, + { 115, "M-0073", "48444c" }, + { 116, "M-0074", "55706f6e6f72" }, + { 117, "M-0075", "7365204c696768746d616e6167656d656e74204147" }, + { 118, "M-0076", "41726375732d656473" }, + { 119, "M-0077", "496e7465736973" }, + { 120, "M-0078", "486572686f6c647420436f6e74726f6c732073726c" }, + { 121, "M-0079", "4e696b6f2d5a75626c696e" }, + { 122, "M-007A", "44757261626c6520546563686e6f6c6f67696573" }, + { 123, "M-007B", "496e6e6f7465616d" }, + { 124, "M-007C", "69736520476d6248" }, + { 125, "M-007D", "5445414d20464f522054524f4e494353" }, + { 126, "M-007E", "43494154" }, + { 127, "M-007F", "52656d656861204256" }, + { 128, "M-0080", "4553594c5558" }, + { 129, "M-0081", "424153414c5445" }, + { 130, "M-0082", "56657374616d61746963" }, + { 131, "M-0083", "4d445420746563686e6f6c6f67696573" }, + { 132, "M-0084", "576172656e646f72666572204bc3bc6368656e20476d6248" }, + { 133, "M-0085", "566964656f2d53746172" }, + { 134, "M-0086", "536974656b" }, + { 135, "M-0087", "434f4e54524f4c74726f6e6963" }, + { 136, "M-0088", "66756e6374696f6e20546563686e6f6c6f6779" }, + { 137, "M-0089", "414d58" }, + { 138, "M-008A", "454c444154" }, + { 139, "M-008B", "50616e61736f6e6963" }, + { 140, "M-008C", "50756c736520546563686e6f6c6f67696573" }, + { 141, "M-008D", "4372657374726f6e" }, + { 142, "M-008E", "535445494e454c2070726f66657373696f6e616c" }, + { 143, "M-008F", "42494c544f4e204c4544204c69676874696e67" }, + { 144, "M-0090", "64656e726f204147" }, + { 145, "M-0091", "476550726f" }, + { 146, "M-0092", "707265757373656e206175746f6d6174696f6e" }, + { 147, "M-0093", "5a6f7070617320496e6475737472696573" }, + { 148, "M-0094", "4d414354454348" }, + { 149, "M-0095", "544543484e4f2d5452454e44" }, + { 150, "M-0096", "4653204361626c6573" }, + { 151, "M-0097", "44656c746120446f7265" }, + { 152, "M-0098", "456973736f756e64" }, + { 153, "M-0099", "436973636f" }, + { 154, "M-009A", "44696e7579" }, + { 155, "M-009B", "694b4e6958" }, + { 156, "M-009C", "526164656d616368657220476572c3a474652d456c656b74726f6e696b20476d6248" }, + { 157, "M-009D", "45476920456c656374726f61637573746963612047656e6572616c2049626572696361" }, + { 158, "M-009E", "42657320e2809320496e67656e69756d" }, + { 159, "M-009F", "456c61624e4554" }, + { 160, "M-00A0", "426c756d6f746978" }, + { 161, "M-00A1", "48756e74657220446f75676c6173" }, + { 162, "M-00A2", "4150524943554d" }, + { 163, "M-00A3", "5449414e5355204175746f6d6174696f6e" }, + { 164, "M-00A4", "427562656e646f726666" }, + { 165, "M-00A5", "4d425320476d6248" }, + { 166, "M-00A6", "456e65727465782042617965726e20476d6248" }, + { 167, "M-00A7", "424d53" }, + { 168, "M-00A8", "53696e61707369" }, + { 169, "M-00A9", "456d6265646465642053797374656d7320534941" }, + { 170, "M-00AA", "4b4e5831" }, + { 171, "M-00AB", "546f6b6b61" }, + { 172, "M-00AC", "4e616e6f53656e7365" }, + { 173, "M-00AD", "50454152204175746f6d6174696f6e20476d6248" }, + { 174, "M-00AE", "444741" }, + { 175, "M-00AF", "4c7574726f6e" }, + { 176, "M-00B0", "4149525a4f4e4520e2809320414c545241" }, + { 177, "M-00B1", "4c6974686f73732044657369676e205377697463686573" }, + { 178, "M-00B2", "334154454c" }, + { 179, "M-00B3", "5068696c69707320436f6e74726f6c73" }, + { 180, "M-00B4", "56454c555820412f53" }, + { 181, "M-00B5", "4c4f59544543" }, + { 182, "M-00B6", "53425320532e702e412e" }, + { 183, "M-00B7", "5349524c414e20546563686e6f6c6f67696573" }, + { 184, "M-00B8", "426c657520436f6d6d2720417a7572" }, + { 185, "M-00B9", "495420476d6248" }, + { 186, "M-00BA", "52454e534f4e" }, + { 187, "M-00BB", "4845502047726f7570" }, + { 188, "M-00BC", "42616c6d617274" }, + { 189, "M-00BD", "47465320476d6248" }, + { 190, "M-00BE", "536368656e6b65722053746f72656e204147" }, + { 191, "M-00BF", "416c676f64756520456c657474726f6e69636120532e722e4c2e" }, + { 192, "M-00C0", "4e6577726f6e2053797374656d" }, + { 193, "M-00C1", "6d61696e74726f6e6963" }, + { 194, "M-00C2", "56616e74616765" }, + { 195, "M-00C3", "466f7265736973" }, + { 196, "M-00C4", "526573656172636820262050726f64756374696f6e204173736f63696174696f6e2053454d" }, + { 197, "M-00C5", "5765696e7a6965726c20456e67696e656572696e6720476d6248" }, + { 198, "M-00C6", "4dc3b6686c656e686f66662057c3a4726d65746563686e696b20476d6248" }, + { 199, "M-00C7", "504b432d47524f5550204f796a" }, + { 200, "M-00C8", "422e452e472e" }, + { 201, "M-00C9", "456c736e657220456c656b74726f6e696b20476d6248" }, + { 202, "M-00CA", "5369656d656e73204275696c64696e6720546563686e6f6c6f676965732028484b2f4368696e6129204c74642e" }, + { 204, "M-00CC", "457574726163" }, + { 205, "M-00CD", "4775737461762048656e73656c20476d6248202620436f2e204b47" }, + { 206, "M-00CE", "4741524f204142" }, + { 207, "M-00CF", "57616c646d616e6e204c69636874746563686e696b" }, + { 208, "M-00D0", "534348c39c434f" }, + { 209, "M-00D1", "454d55" }, + { 210, "M-00D2", "4a4e65742053797374656d73204147" }, + { 214, "M-00D6", "4f2e592e4c2e20456c656374726f6e696373" }, + { 215, "M-00D7", "47616c61782053797374656d" }, + { 216, "M-00D8", "4469736368" }, + { 217, "M-00D9", "4175636f7465616d" }, + { 218, "M-00DA", "4c75786d61746520436f6e74726f6c73" }, + { 219, "M-00DB", "44616e666f7373" }, + { 220, "M-00DC", "41535420476d6248" }, + { 222, "M-00DE", "57494c41204c6575636874656e" }, + { 223, "M-00DF", "622b62204175746f6d6174696f6e732d20756e6420537465756572756e6773746563686e696b" }, + { 225, "M-00E1", "4c696e67672026204a616e6b65" }, + { 227, "M-00E3", "536175746572" }, + { 228, "M-00E4", "53494d55" }, + { 232, "M-00E8", "54686562656e20485453204147" }, + { 233, "M-00E9", "416d616e6e20476d6248" }, + { 234, "M-00EA", "4245524720456e65726769656b6f6e74726f6c6c73797374656d6520476d6248" }, + { 235, "M-00EB", "48c3bc70706520466f726d20536f6e6e656e73636875747a73797374656d6520476d6248" }, + { 237, "M-00ED", "4f76656e74726f70204b47" }, + { 238, "M-00EE", "4772696573736572204147" }, + { 239, "M-00EF", "4950415320476d6248" }, + { 240, "M-00F0", "656c65726f20476d6248" }, + { 241, "M-00F1", "417264616e2050726f64756374696f6e20616e6420496e647573747269616c20436f6e74726f6c73204c74642e" }, + { 242, "M-00F2", "4d65746563204d65c39f746563686e696b20476d6248" }, + { 244, "M-00F4", "454c4b412d456c656b74726f6e696b20476d6248" }, + { 245, "M-00F5", "454c454b54524f414e4c4147454e20442e204e4147454c" }, + { 246, "M-00F6", "547269646f6e696320426175656c656d656e746520476d6248" }, + { 248, "M-00F8", "5374656e676c657220476573656c6c736368616674" }, + { 249, "M-00F9", "5363686e656964657220456c65637472696320284d4729" }, + { 250, "M-00FA", "4b4e58204173736f63696174696f6e" }, + { 251, "M-00FB", "5649564f" }, + { 252, "M-00FC", "4875676f204dc3bc6c6c657220476d6248202620436f204b47" }, + { 253, "M-00FD", "5369656d656e732048564143" }, + { 254, "M-00FE", "415054" }, + { 256, "M-0100", "48696768446f6d" }, + { 257, "M-0101", "546f70205365727669636573" }, + { 258, "M-0102", "616d6269486f6d65" }, + { 259, "M-0103", "444154454320656c656374726f6e6963204147" }, + { 260, "M-0104", "414255532053656375726974792d43656e746572" }, + { 261, "M-0105", "4c6974652d5075746572" }, + { 262, "M-0106", "54616e74726f6e20456c656374726f6e6963" }, + { 263, "M-0107", "59c3b66e6e6574" }, + { 264, "M-0108", "444b582054656368" }, + { 265, "M-0109", "56696174726f6e" }, + { 266, "M-010A", "4e61757469627573" }, + { 267, "M-010B", "4f4e2053656d69636f6e647563746f72" }, + { 268, "M-010C", "4c6f6e67636875616e67" }, + { 269, "M-010D", "4169722d4f6e204147" }, + { 270, "M-010E", "69622d636f6d70616e7920476d6248" }, + { 271, "M-010F", "536174696f6e20466163746f7279" }, + { 272, "M-0110", "4167656e74696c6f20476d6248" }, + { 273, "M-0111", "4d616b656c20456c656b7472696b" }, + { 274, "M-0112", "48656c696f732056656e74696c61746f72656e" }, + { 275, "M-0113", "4f74746f20536f6c7574696f6e7320507465204c7464" }, + { 276, "M-0114", "4169726d6173746572" }, + { 277, "M-0115", "56616c6c6f7820476d6248" }, + { 278, "M-0116", "44616c6974656b" }, + { 279, "M-0117", "4153494e" }, + { 280, "M-0118", "4272696467657320496e74656c6c6967656e636520546563686e6f6c6f677920496e632e" }, + { 281, "M-0119", "4152424f4e4941" }, + { 282, "M-011A", "4b45524d49" }, + { 283, "M-011B", "50524f4c5558" }, + { 284, "M-011C", "436c6963486f6d65" }, + { 285, "M-011D", "434f4d4d4158" }, + { 286, "M-011E", "454145" }, + { 287, "M-011F", "54656e7365" }, + { 288, "M-0120", "5365796f756e6720456c656374726f6e696373" }, + { 289, "M-0121", "4c696665646f6d7573" }, + { 290, "M-0122", "4555524f74726f6e696320546563686e6f6c6f677920476d6248" }, + { 291, "M-0123", "746369" }, + { 292, "M-0124", "52697368756e20456c656374726f6e6963" }, + { 293, "M-0125", "5a697061746f" }, + { 294, "M-0126", "636d2d736563757269747920476d6248202620436f204b47" }, + { 295, "M-0127", "51696e67204361626c6573" }, + { 296, "M-0128", "4c4142494f" }, + { 297, "M-0129", "436f73746572205465636e6f6c6f67696520456c657474726f6e6963686520532e702e412e" }, + { 298, "M-012A", "452e472e45" }, + { 299, "M-012B", "4e4554784175746f6d6174696f6e" }, + { 300, "M-012C", "746563616c6f72" }, + { 301, "M-012D", "55726d657420456c656374726f6e69637320284875697a686f7529204c74642e" }, + { 302, "M-012E", "50656979696e67204275696c64696e6720436f6e74726f6c" }, + { 303, "M-012F", "42505420532e702e412e206120536f63696f20556e69636f" }, + { 304, "M-0130", "4b616e6f6e746563202d204b616e6f6e425553" }, + { 305, "M-0131", "495345522054656368" }, + { 306, "M-0132", "46696e656c696e65" }, + { 307, "M-0133", "435020456c656374726f6e696373204c7464" }, + { 308, "M-0134", "4e696b6f2d536572766f64616e20412f53" }, + { 309, "M-0135", "53696d6f6e" }, + { 310, "M-0136", "474d206d6f64756c6172207076742e204c74642e" }, + { 311, "M-0137", "4655204348454e4720496e74656c6c6967656e6365" }, + { 312, "M-0138", "4e65784b6f6e" }, + { 313, "M-0139", "4645454c20732e722e6c" }, + { 314, "M-013A", "4e6f742041737369676e6564" }, + { 315, "M-013B", "5368656e7a68656e2046616e6861692053616e6a69616e6720456c656374726f6e69637320436f2e2c204c74642e" }, + { 316, "M-013C", "4a69757a686f752047726565626c65" }, + { 317, "M-013D", "41756dc3bc6c6c65722041756d6174696320476d6248" }, + { 318, "M-013E", "45746d616e20456c656374726963" }, + { 319, "M-013F", "454d5420436f6e74726f6c73" }, + { 320, "M-0140", "5a69646154656368204147" }, + { 321, "M-0141", "494447532062766261" }, + { 322, "M-0142", "64616b616e696d6f" }, + { 323, "M-0143", "547265626f72204175746f6d6174696f6e204142" }, + { 324, "M-0144", "536174656c2073702e207a206f2e6f2e" }, + { 325, "M-0145", "527573736f756e642c20496e632e" }, + { 326, "M-0146", "4d696465612048656174696e6720262056656e74696c6174696e672045717569706d656e7420434f204c5444" }, + { 327, "M-0147", "436f6e736f727a696f2054657272616e756f7661" }, + { 328, "M-0148", "576f6c66204865697a746563686e696b20476d6248" }, + { 329, "M-0149", "534f4e544543" }, + { 330, "M-014A", "42656c636f6d204361626c6573204c74642e" }, + { 331, "M-014B", "4775616e677a686f752053656157696e20456c656374726963616c20546563686e6f6c6f6769657320436f2e2c204c74642e" }, + { 332, "M-014C", "416372656c" }, + { 333, "M-014D", "4672616e6b652041717561726f7474657220476d6248" }, + { 334, "M-014E", "4f72696f6e2053797374656d73" }, + { 335, "M-014F", "5363687261636b20546563686e696b20476d6248" }, + { 336, "M-0150", "494e5350524944" }, + { 337, "M-0151", "53756e726963686572" }, + { 338, "M-0152", "4d656e726564206175746f6d6174696f6e2073797374656d287368616e676861692920436f2e2c4c74642e" }, + { 339, "M-0153", "4175726578" }, + { 340, "M-0154", "4a6f736566204261727468656c6d6520476d6248202620436f2e204b47" }, + { 341, "M-0155", "417263686974656374757265204e756d657269717565" }, + { 342, "M-0156", "55502047524f5550" }, + { 343, "M-0157", "54656b6e6f732d4176696e6e6f" }, + { 344, "M-0158", "4e696e67626f20446f6f7961204d656368616e6963202620456c656374726f6e696320546563686e6f6c6f6779" }, + { 345, "M-0159", "546865726d6f6b6f6e2053656e736f72746563686e696b20476d6248" }, + { 346, "M-015A", "42454c494d4f204175746f6d6174696f6e204147" }, + { 347, "M-015B", "5a65686e6465722047726f757020496e7465726e6174696f6e616c204147" }, + { 348, "M-015C", "736b73204b696e6b656c20456c656b74726f6e696b" }, + { 349, "M-015D", "454345205775726d69747a657220476d6248" }, + { 350, "M-015E", "4c415253" }, + { 351, "M-015F", "555243" }, + { 352, "M-0160", "4c69676874436f6e74726f6c" }, + { 353, "M-0161", "5368656e5a68656e20594d" }, + { 354, "M-0162", "4d45414e2057454c4c20456e74657270726973657320436f2e204c74642e" }, + { 355, "M-0163", "4f536978" }, + { 356, "M-0164", "415950524f20546563686e6f6c6f6779" }, + { 357, "M-0165", "48656665692045636f6c69746520536f667477617265" }, + { 358, "M-0166", "456e6e6f" }, + { 359, "M-0167", "4f484f53555245" }, + { 360, "M-0168", "47617265666f776c" }, + { 361, "M-0169", "47455a45" }, + { 362, "M-016A", "4c4720456c656374726f6e69637320496e632e" }, + { 363, "M-016B", "534d4320696e746572696f7273" }, + { 364, "M-016C", "4e6f742041737369676e6564" }, + { 365, "M-016D", "534353204361626c65" }, + { 366, "M-016E", "486f76616c" }, + { 367, "M-016F", "43414e5354" }, + { 368, "M-0170", "48616e675a686f75204265726c696e" }, + { 369, "M-0171", "45564e2d4c69636874746563686e696b" }, + { 370, "M-0172", "7275746563" }, + { 371, "M-0173", "46696e646572" }, + { 372, "M-0174", "46756a697473752047656e6572616c204c696d69746564" }, + { 373, "M-0175", "5a462046726965647269636873686166656e204147" }, + { 374, "M-0176", "437265616c6564" }, + { 375, "M-0177", "4d696c6573204d61676963204175746f6d6174696f6e2050726976617465204c696d69746564" }, + { 376, "M-0178", "452b" }, + { 377, "M-0179", "4974616c636f6e64" }, + { 378, "M-017A", "534154494f4e" }, + { 379, "M-017B", "4e657742657374" }, + { 380, "M-017C", "474453204449474954414c2053595354454d53" }, + { 381, "M-017D", "49646465726f" }, + { 382, "M-017E", "4d424e4c4544" }, + { 383, "M-017F", "56495452554d" }, + { 384, "M-0180", "656b65792062696f6d65747269632073797374656d7320476d6248" }, + { 385, "M-0181", "414d43" }, + { 386, "M-0182", "5452494c555820476d6248202620436f2e204b47" }, + { 387, "M-0183", "5745786365646f" }, + { 388, "M-0184", "56454d455220535041" }, + { 389, "M-0185", "416c6578616e6465722042c3bc726b6c6520476d6248202620436f204b47" }, + { 390, "M-0186", "53656574726f6c6c" }, + { 391, "M-0187", "5368656e7a68656e2048654775616e67" }, + { 392, "M-0188", "4e6f742041737369676e6564" }, + { 393, "M-0189", "5452414e4520422e562e422e41" }, + { 394, "M-018A", "434152454c" }, + { 395, "M-018B", "50726f6c69746520436f6e74726f6c73" }, + { 396, "M-018C", "424f534d4552" }, + { 397, "M-018D", "45554348495053" }, + { 398, "M-018E", "636f6e6e65637420285468696e6b6120636f6e6e65637429" }, + { 399, "M-018F", "5045414b6e78206120444f47415749535420636f6d70616e7920" }, + { 400, "M-0190", "4143454d41544943" }, + { 401, "M-0191", "454c4155535953" }, + { 402, "M-0192", "49544b20456e67696e656572696e67204147" }, + { 403, "M-0193", "417175616d6574726f204147" }, + { 404, "M-0194", "464d5320486f73706974616c69747920507465204c7464" }, + { 405, "M-0195", "4e75766f" }, + { 406, "M-0196", "753a3a4c757820476d6248" }, + { 407, "M-0197", "4272756d62657267204c6575636874656e" }, + { 408, "M-0198", "4c696d65" }, + { 409, "M-0199", "477265617420456d7069726520496e7465726e6174696f6e616c2047726f757020436f2e2c204c74642e" }, + { 410, "M-019A", "4b61766f736870697368726f2041736961" }, + { 411, "M-019B", "563220537041" }, + { 412, "M-019C", "4a6f686e736f6e20436f6e74726f6c73" }, + { 413, "M-019D", "41726b7564" }, + { 414, "M-019E", "4972696469756d204c74642e" }, + { 415, "M-019F", "62736d617274" }, + { 416, "M-01A0", "42414220544543484e4f4c4f47494520476d6248" }, + { 417, "M-01A1", "4e49434520537061" }, + { 418, "M-01A2", "526564666973682047726f757020507479204c7464" }, + { 419, "M-01A3", "53414249414e4120737061" }, + { 420, "M-01A4", "5562656520496e746572616374697665204575726f7065" }, + { 421, "M-01A5", "526578656c" }, + { 422, "M-01A6", "4765732054656b6e696b20412e532e" }, + { 423, "M-01A7", "41766520532e702e412e20" }, + { 424, "M-01A8", "5a6875686169204c7465636820546563686e6f6c6f677920436f2e2c204c74642e20" }, + { 425, "M-01A9", "4152434f4d" }, + { 426, "M-01AA", "56494120546563686e6f6c6f676965732c20496e632e" }, + { 427, "M-01AB", "4645454c534d4152542e" }, + { 428, "M-01AC", "535550434f4e" }, + { 429, "M-01AD", "4d414e4943" }, + { 430, "M-01AE", "5472c3a4756d20646575747363686520456c656b74726f6e696b20476d6248" }, + { 431, "M-01AF", "4e616e6a696e672053687566616e20496e666f726d6174696f6e20746563686e6f6c6f677920436f2e2c4c74642e" }, + { 432, "M-01B0", "455754656368" }, + { 433, "M-01B1", "4b6c75676572204175746f6d6174696f6e20476d6248" }, + { 434, "M-01B2", "4a6f6f6e67416e6720436f6e74726f6c" }, + { 435, "M-01B3", "477265656e436f6e74726f6c7320546563686e6f6c6f67792053646e2e204268642e" }, + { 436, "M-01B4", "494d4520532e702e612e" }, + { 437, "M-01B5", "5369436875616e2048616f44696e67" } + }; + return sManufacturers; + } +}; + +/*! + \since 5.13 + \inmodule QtKnx + \namespace QKnx::Ets + + \brief Contains miscellaneous and ETS specific identifiers used throughout the QtKnx library. +*/ + +/*! + \since 5.13 + \inmodule QtKnx + \namespace QKnx::Ets::Manufacturers + + \brief Contains KNX manufacturer-specific identifiers used throughout the QtKnx library. +*/ + +/*! + \since 5.13 + + Returns the name of a KNX manufacturer for the given manufacturer ID + \a id or the given default value \a defaultValue if the \c id was not + found in the list of known manufacturers. +*/ +QString QKnx::Ets::Manufacturers::fromId(int id, const QString &defaultValue) +{ + const auto m = Manufacturer::manufacturers(); + const auto it = std::lower_bound(m.constBegin(), m.constEnd(), id, + [](const Manufacturer &mf, int id) { + return mf.id < id; + }); + if (it != m.constEnd()) + return it->name; + return defaultValue; +} + +/*! + \since 5.13 + + Returns the name of a KNX manufacturer for the given manufacturer ID + \a id or the given default value \a defaultValue if the \c id was not + found in the list of known manufacturers. + + \note: The \c id must be in the format of \b {M-\\d{4}}. +*/ +QString QKnx::Ets::Manufacturers::fromId(const QString &id, const QString &defaultValue) +{ + const auto m = Manufacturer::manufacturers(); + const auto it = std::lower_bound(m.constBegin(), m.constEnd(), id, + [](const Manufacturer &mf, const QString &id) { + return mf.id < id; + }); + if (it != m.constEnd()) + return it->name; + return defaultValue; +} + +QT_END_NAMESPACE diff --git a/src/knx/ets/manufacturers.h b/src/knx/ets/manufacturers.h new file mode 100644 index 0000000..d7c181c --- /dev/null +++ b/src/knx/ets/manufacturers.h @@ -0,0 +1,47 @@ +/****************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtKnx module. +** +** $QT_BEGIN_LICENSE:GPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 or (at your option) any later version +** approved by the KDE Free Qt Foundation. The licenses are as published by +** the Free Software Foundation and appearing in the file LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +******************************************************************************/ + +#ifndef MANUFACTURERS_H +#define MANUFACTURERS_H + +#include <QtCore/qstring.h> +#include <QtKnx/qtknxglobal.h> + +QT_BEGIN_NAMESPACE + +namespace QKnx { namespace Ets { namespace Manufacturers { + + Q_KNX_EXPORT QString fromId(int id, const QString &defaultValue = {}); + Q_KNX_EXPORT QString fromId(const QString &id, const QString &defaultValue = {}); + +}}} + +QT_END_NAMESPACE + +#endif diff --git a/src/knx/knx.pro b/src/knx/knx.pro index 083bef8..918f0fb 100644 --- a/src/knx/knx.pro +++ b/src/knx/knx.pro @@ -8,6 +8,7 @@ QMAKE_DOCS = $$PWD/doc/qtknx.qdocconf include(core/core.pri) include(dpt/dpt.pri) +include(ets/ets.pri) include(knxproj/knxproj.pri) include(netip/netip.pri) include(ssl/ssl.pri) diff --git a/src/knx/netip/netip.pri b/src/knx/netip/netip.pri index 5652563..88e2d58 100644 --- a/src/knx/netip/netip.pri +++ b/src/knx/netip/netip.pri @@ -53,7 +53,8 @@ PUBLIC_HEADERS += $$PWD/qknxnetip.h \ $$PWD/qknxnetipsessionstatus.h \ $$PWD/qknxnetiptimernotify.h \ $$PWD/qknxnetipsecurewrapper.h \ - $$PWD/qknxnetiprouter.h + $$PWD/qknxnetiprouter.h \ + $$PWD/qknxnetipsecureconfiguration.h PRIVATE_HEADERS += \ $$PWD/qknxbuilderdata_p.h \ @@ -61,7 +62,8 @@ PRIVATE_HEADERS += \ $$PWD/qknxnetipserverdescriptionagent_p.h \ $$PWD/qknxnetipserverdiscoveryagent_p.h \ $$PWD/qknxnetipserverinfo_p.h \ - $$PWD/qknxnetiptestrouter_p.h + $$PWD/qknxnetiptestrouter_p.h \ + $$PWD/qknxnetipsecureconfiguration_p.h SOURCES += $$PWD/qknxnetip.cpp \ $$PWD/qknxnetipconfigdib.cpp \ @@ -117,4 +119,5 @@ SOURCES += $$PWD/qknxnetip.cpp \ $$PWD/qknxnetiptimernotify.cpp \ $$PWD/qknxnetipsecurewrapper.cpp \ $$PWD/qknxnetiprouter.cpp \ - $$PWD/qknxnetiprouter_p.cpp + $$PWD/qknxnetiprouter_p.cpp \ + $$PWD/qknxnetipsecureconfiguration.cpp diff --git a/src/knx/netip/qknxbuilderdata_p.h b/src/knx/netip/qknxbuilderdata_p.h index 8f8b527..a449535 100644 --- a/src/knx/netip/qknxbuilderdata_p.h +++ b/src/knx/netip/qknxbuilderdata_p.h @@ -42,6 +42,8 @@ // #include <QtCore/qshareddata.h> + +#include <QtKnx/qknxcryptographicengine.h> #include <QtKnx/qknxnamespace.h> #include <QtKnx/qknxnetipframe.h> #include <QtKnx/qknxnetipservicefamiliesdib.h> @@ -126,6 +128,39 @@ public: QKnxByteArray m_authCode; }; +class QKnxNetIpTimerNotifyBuilderPrivate : public QSharedData +{ +public: + QKnxNetIpTimerNotifyBuilderPrivate() = default; + ~QKnxNetIpTimerNotifyBuilderPrivate() = default; + + quint64 m_timer { Q_UINT48_MAX + 1 }; + QKnxByteArray m_serial; + qint32 m_tag { -1 }; + QKnxByteArray m_authCode; +}; + +class QKnxNetIpSessionResponseBuilderPrivate : public QSharedData +{ +public: + QKnxNetIpSessionResponseBuilderPrivate() = default; + ~QKnxNetIpSessionResponseBuilderPrivate() = default; + + qint32 m_id { -1 }; + QKnxByteArray m_serverPublicKey; + QKnxByteArray m_authCode; +}; + +class QKnxNetIpSessionAuthenticateBuilderPrivate : public QSharedData +{ +public: + QKnxNetIpSessionAuthenticateBuilderPrivate() = default; + ~QKnxNetIpSessionAuthenticateBuilderPrivate() = default; + + QKnxNetIp::SecureUserId m_id { QKnxNetIp::SecureUserId::Invalid }; + QKnxByteArray m_authCode; +}; + QT_END_NAMESPACE #endif diff --git a/src/knx/netip/qknxnetip.cpp b/src/knx/netip/qknxnetip.cpp index 335c4a8..e68bf47 100644 --- a/src/knx/netip/qknxnetip.cpp +++ b/src/knx/netip/qknxnetip.cpp @@ -579,6 +579,15 @@ bool QKnxNetIp::isCapability(Capability capability) The timeout used to empty the incoming queue of a KNXnet/IP router or KNX IP device if the number of received datagrams exceeds the number of frames that the device can actually process. + \value SecureSessionTimeout + The maximum time an authenticated secure session can remain unused + without any communication before the secure session gets dropped. + \value SecureSessionRequestTimeout + The maximum time the KNXnet/IP client will wait for the session + response from the KNXnet/IP server after sending a session request. + \value SecureSessionAuthenticateTimeout + The maximum time the authentication process for a newly created secure + session may last until the unauthenticated session will be dropped. */ /*! @@ -727,4 +736,58 @@ bool QKnx::NetIp::isStructType(QKnx::NetIp::SearchParameterType type) \omitvalue Unknown */ +/*! + \enum QKnx::NetIp::SecureUserId + \since 5.13 + + The ID of the management client (MaC) or user that is used for the + authentication of the secure session. + + \value Reserved + Reserved, please do not use. + \value Management + The management user ID. + \value UserRole + The first value in the possible range of user IDs. + \value Invalid + Invalid, please do not use any ID equal to or more than this value. + + The management server (MaS) will use the user ID as an index into the + password hashes table to authenticate the MaC. + + The access level (management or user level access - with possibly any device + dependent role) will determine the set of services accepted by the MaS after + authentication. + + \table + \header + \li User ID + \li Description + \row + \li \c Management + \li This user ID requests access to all resources exposed by the + MaS including device management. + \row + \li \c User + \li This user ID requests access to all resources exposed by the + MaS except device management. + \endtable + + In addition to access level, the user ID serves as an index into the + tunneling user table to determine if there are individual addresses + associated with the user. The management user ID has implicit access + to all available tunneling addresses. +*/ + +/*! + \since 5.13 + + Returns \c true if the specified \a userId is a in the range of the + \l SecureUserId enumeration; otherwise returns \c false. +*/ +bool QKnx::NetIp::isSecureUserId(QKnx::NetIp::SecureUserId userId) +{ + return (userId > SecureUserId::Reserved && userId < SecureUserId::Invalid); +} + QT_END_NAMESPACE diff --git a/src/knx/netip/qknxnetip.h b/src/knx/netip/qknxnetip.h index 26a891c..96e3a63 100644 --- a/src/knx/netip/qknxnetip.h +++ b/src/knx/netip/qknxnetip.h @@ -238,7 +238,12 @@ namespace QKnx TunnelingRequestTimeout = 1000, // KNXnet/IP routing service - RoutingBusyWaitTime = 100 + RoutingBusyWaitTime = 100, + + // KNXnet/IP secure session + SecureSessionTimeout = 60000, + SecureSessionRequestTimeout = 10000, + SecureSessionAuthenticateTimeout = 10000 }; Q_ENUM_NS(Timeout) @@ -297,6 +302,16 @@ namespace QKnx Unknown = 0xff }; Q_ENUM_NS(SecureSessionStatus) + + enum SecureUserId : quint8 + { + Reserved = 0x00, + Management = 0x01, + UserRole = 0x02, + Invalid = 0x80 + }; + Q_ENUM_NS(SecureUserId) + Q_KNX_EXPORT bool isSecureUserId(SecureUserId userId); } } namespace QKnxNetIp = QKnx::NetIp; diff --git a/src/knx/netip/qknxnetipdevicemanagement.cpp b/src/knx/netip/qknxnetipdevicemanagement.cpp index eb3d5b7..1da9b15 100644 --- a/src/knx/netip/qknxnetipdevicemanagement.cpp +++ b/src/knx/netip/qknxnetipdevicemanagement.cpp @@ -87,13 +87,15 @@ public: QKnxNetIp::DeviceConfigurationRequestTimeout) {} - void process(const QKnxDeviceManagementFrame &frame) override - { - Q_Q(QKnxNetIpDeviceManagement); - emit q->frameReceived(frame); - } + void process(const QKnxDeviceManagementFrame &frame) override; }; +void QKnxNetIpDeviceManagementPrivate::process(const QKnxDeviceManagementFrame &frame) +{ + Q_Q(QKnxNetIpDeviceManagement); + emit q->frameReceived(frame); +} + /*! Creates a device management connection with the parent \a parent. */ diff --git a/src/knx/netip/qknxnetipendpointconnection.cpp b/src/knx/netip/qknxnetipendpointconnection.cpp index 18939a6..dfc6501 100644 --- a/src/knx/netip/qknxnetipendpointconnection.cpp +++ b/src/knx/netip/qknxnetipendpointconnection.cpp @@ -27,6 +27,7 @@ ** ******************************************************************************/ +#include "qknxcryptographicengine.h" #include "qknxnetipconnectrequest.h" #include "qknxnetipconnectresponse.h" #include "qknxnetipconnectionstaterequest.h" @@ -37,6 +38,11 @@ #include "qknxnetipdisconnectresponse.h" #include "qknxnetipendpointconnection.h" #include "qknxnetipendpointconnection_p.h" +#include "qknxnetipsessionauthenticate.h" +#include "qknxnetipsecurewrapper.h" +#include "qknxnetipsessionrequest.h" +#include "qknxnetipsessionresponse.h" +#include "qknxnetipsessionstatus.h" #include "qknxnetiptunnelingacknowledge.h" #include "qknxnetiptunnelingfeatureget.h" #include "qknxnetiptunnelingfeatureset.h" @@ -47,6 +53,8 @@ #include "qtcpsocket.h" #include "qudpsocket.h" +#include "private/qknxnetipsecureconfiguration_p.h" + QT_BEGIN_NAMESPACE /*! \class QKnxNetIpEndpointConnection @@ -106,7 +114,18 @@ QT_BEGIN_NAMESPACE State request timeout. \value Cemi No cEMI frame acknowledge in time. + \value SecureConfig + An invalid secure configuration was used to establish the connection. + \value SerialNumber + An invalid serial number was set for the device. + \value AuthFailed + The secure client was not successfully authenticated. + \value Timeout + A timeout occurred during secure session handshake. + \value Close + The server requested to close the secure session. \value Unknown + An unknown error occurred. */ /*! \enum QKnxNetIpEndpointConnection::SequenceType @@ -177,17 +196,18 @@ void QKnxNetIpEndpointConnectionPrivate::setupTimer() QKnxPrivate::clearTimer(&m_connectionStateTimer); QKnxPrivate::clearTimer(&m_disconnectRequestTimer); QKnxPrivate::clearTimer(&m_acknowledgeTimer); + QKnxPrivate::clearTimer(&m_secureTimer); Q_Q(QKnxNetIpEndpointConnection); m_heartbeatTimer = new QTimer(q); m_heartbeatTimer->setSingleShot(true); - QObject::connect(m_heartbeatTimer, &QTimer::timeout, [&]() { + QObject::connect(m_heartbeatTimer, &QTimer::timeout, q, [&]() { sendStateRequest(); }); m_connectRequestTimer = new QTimer(q); m_connectRequestTimer->setSingleShot(true); - QObject::connect(m_connectRequestTimer, &QTimer::timeout, [&]() { + QObject::connect(m_connectRequestTimer, &QTimer::timeout, q, [&]() { setAndEmitStateChanged(QKnxNetIpEndpointConnection::State::Bound); setAndEmitErrorOccurred(QKnxNetIpEndpointConnection::Error::Acknowledge, QKnxNetIpEndpointConnection::tr("Connect request timeout.")); @@ -198,7 +218,7 @@ void QKnxNetIpEndpointConnectionPrivate::setupTimer() m_connectionStateTimer = new QTimer(q); m_connectionStateTimer->setSingleShot(true); - QObject::connect(m_connectionStateTimer, &QTimer::timeout, [&]() { + QObject::connect(m_connectionStateTimer, &QTimer::timeout, q, [&]() { m_heartbeatTimer->stop(); m_connectionStateTimer->stop(); if (m_stateRequests > m_maxStateRequests) { @@ -214,7 +234,7 @@ void QKnxNetIpEndpointConnectionPrivate::setupTimer() m_disconnectRequestTimer = new QTimer(q); m_disconnectRequestTimer->setSingleShot(true); - QObject::connect(m_disconnectRequestTimer, &QTimer::timeout, [&] () { + QObject::connect(m_disconnectRequestTimer, &QTimer::timeout, q, [&] () { setAndEmitErrorOccurred(QKnxNetIpEndpointConnection::Error::Acknowledge, QKnxNetIpEndpointConnection::tr("Disconnect request timeout.")); processDisconnectResponse(QKnxNetIpDisconnectResponseProxy::builder() @@ -223,7 +243,7 @@ void QKnxNetIpEndpointConnectionPrivate::setupTimer() m_acknowledgeTimer = new QTimer(q); m_acknowledgeTimer->setSingleShot(true); - QObject::connect(m_acknowledgeTimer, &QTimer::timeout, [&]() { + QObject::connect(m_acknowledgeTimer, &QTimer::timeout, q, [&]() { if (m_cemiRequests > m_maxCemiRequest) { setAndEmitErrorOccurred(QKnxNetIpEndpointConnection::Error::Cemi, QKnxNetIpEndpointConnection::tr("Did not receive acknowledge in time.")); @@ -235,43 +255,26 @@ void QKnxNetIpEndpointConnectionPrivate::setupTimer() sendCemiRequest(); } }); -} -namespace QKnxPrivate -{ - static bool isNullOrLocal(const QHostAddress &address) - { - return address.isNull() || address == QHostAddress::Any || address.isLoopback(); - } + m_secureTimer = new QTimer(q); + m_secureTimer->setSingleShot(true); } -void QKnxNetIpEndpointConnectionPrivate::processReceivedFrame(const QHostAddress &address, int port) +QKnxNetIp::ServiceType + QKnxNetIpEndpointConnectionPrivate::processReceivedFrame(const QKnxNetIpFrame &frame) { - const auto frame = QKnxNetIpFrame::fromBytes(m_rxBuffer); - if (!frame.isValid()) - return; - - // remove already processed KNX frame from buffer - m_rxBuffer.remove(0, frame.size()); - // TODO: fix the version and validity checks - // if (!m_supportedVersions.contains(header.protocolVersion())) { + // if (!m_supportedVersions.contains(frame.header().protocolVersion())) { // send E_VERSION_NOT_SUPPORTED confirmation frame // send disconnect request - // } else if (header.protocolVersion() != m_controlEndpointVersion) { + // } else if (frame.header().protocolVersion() != m_controlEndpointVersion) { // send disconnect request // } else { // TODO: set the m_dataEndpointVersion once we receive or send the first frame - switch (frame.serviceType()) { + auto serviceType = frame.serviceType(); + switch (serviceType) { case QKnxNetIp::ServiceType::ConnectResponse: - if (m_nat) { // TODO: Review this for TCP connections - if (QKnxPrivate::isNullOrLocal(m_remoteDataEndpoint.address) - || m_remoteDataEndpoint.port == 0) { - m_remoteDataEndpoint.port = port; - m_remoteDataEndpoint.address = address; - } - } processConnectResponse(frame); break; case QKnxNetIp::ServiceType::ConnectionStateResponse: @@ -302,14 +305,241 @@ void QKnxNetIpEndpointConnectionPrivate::processReceivedFrame(const QHostAddress case QKnxNetIp::ServiceType::TunnelingFeatureResponse: processFeatureFrame(frame); break; + + case QKnxNetIp::ServiceType::SecureWrapper: { + qDebug() << "Received secure wrapper frame:" << frame; + + const QKnxNetIpSecureWrapperProxy proxy(frame); + if (!proxy.isValid()) + break; + + const auto seqNumber = proxy.sequenceNumber(); + const auto serialNumber = proxy.serialNumber(); + const auto messageTag = proxy.messageTag(); + + const auto decData = QKnxCryptographicEngine::decryptSecureWrapperPayload(m_sessionKey, + proxy.encapsulatedFrame(), seqNumber, serialNumber, messageTag); + + const auto mac = QKnxCryptographicEngine::computeMessageAuthenticationCode(m_sessionKey, + frame.header(), proxy.secureSessionId(), decData, seqNumber, serialNumber, messageTag); + const auto decMac = QKnxCryptographicEngine::decryptMessageAuthenticationCode(m_sessionKey, + proxy.messageAuthenticationCode(), seqNumber, serialNumber, messageTag); + + if (decMac != mac) + break; // MAC could not be verified, bail out + + return processReceivedFrame(QKnxNetIpFrame::fromBytes(decData)); + } break; + + case QKnxNetIp::ServiceType::SessionRequest: + qDebug() << "Unexpectedly received session request frame:" << frame; + break; + + case QKnxNetIp::ServiceType::SessionResponse: { + qDebug() << "Received session response frame:" << frame; + + QKnxNetIpSessionResponseProxy proxy(frame); + if (!proxy.isValid()) + break; + + const auto authHash = QKnxCryptographicEngine::deviceAuthenticationCodeHash(m_secureConfig. + d->deviceAuthenticationCode); + const auto xorX_Y = QKnxCryptographicEngine::XOR(m_secureConfig.d->publicKey.bytes(), proxy + .publicKey()); + + auto mac = QKnxCryptographicEngine::computeMessageAuthenticationCode(authHash, + frame.header(), proxy.secureSessionId(), xorX_Y); + auto decMac = QKnxCryptographicEngine::decryptMessageAuthenticationCode(authHash, + proxy.messageAuthenticationCode()); + + if (decMac != mac) + break; // MAC could not be verified, bail out + + m_secureTimer->stop(); + m_secureTimer->disconnect(); + m_sessionId = proxy.secureSessionId(); + m_sessionKey = QKnxCryptographicEngine::sessionKey(m_secureConfig.d->privateKey, + QKnxSecureKey::fromBytes(QKnxSecureKey::Type::Public, proxy.publicKey())); + + auto secureWrapper = QKnxNetIpSecureWrapperProxy::secureBuilder() + .setSecureSessionId(m_sessionId) + .setSequenceNumber(m_sequenceNumber) + .setSerialNumber(m_serialNumber) + // .setMessageTag(0x0000) TODO: Do we need an API for this? + .setEncapsulatedFrame(QKnxNetIpSessionAuthenticateProxy::secureBuilder() + .setUserId(m_secureConfig.d->userId) + .create(m_secureConfig.d->userPassword, m_secureConfig.d->publicKey.bytes(), proxy + .publicKey())) + .create(m_sessionKey); + + ++m_sequenceNumber; + m_waitForAuthentication = true; + if (m_tcpSocket) + m_tcpSocket->write(secureWrapper.bytes().toByteArray()); + + Q_Q(QKnxNetIpEndpointConnection); + QObject::connect(m_secureTimer, &QTimer::timeout, q, [&]() { + m_secureTimer->stop(); + setAndEmitErrorOccurred(QKnxNetIpEndpointConnection::Error::AuthFailed, + QKnxNetIpEndpointConnection::tr("Did not receive session status frame.")); + + auto secureStatusWrapper = QKnxNetIpSecureWrapperProxy::secureBuilder() + .setSecureSessionId(m_sessionId) + .setSequenceNumber(m_sequenceNumber) + .setSerialNumber(m_serialNumber) + // .setMessageTag(0x0000) TODO: Do we need an API for this? + .setEncapsulatedFrame(QKnxNetIpSessionStatusProxy::builder() + .setStatus(QKnxNetIp::SecureSessionStatus::Close) + .create()) + .create(m_sessionKey); + if (m_tcpSocket) + m_tcpSocket->write(secureStatusWrapper.bytes().toByteArray()); + + Q_Q(QKnxNetIpEndpointConnection); + q->disconnectFromHost(); + }); + m_secureTimer->start(QKnxNetIp::SecureSessionAuthenticateTimeout); + } break; + + case QKnxNetIp::ServiceType::SessionAuthenticate: + qDebug() << "Unexpectedly received session authenticate frame:" << frame; + break; + + case QKnxNetIp::ServiceType::SessionStatus: { + qDebug() << "Received session status frame:" << frame; + + QKnxNetIpSessionStatusProxy ssp(frame); + if (!ssp.isValid()) + break; + + bool close { false }; + switch (ssp.status()) { + case QKnxNetIp::SecureSessionStatus::AuthenticationSuccess: { + if (m_waitForAuthentication) { + m_secureTimer->stop(); + m_secureTimer->disconnect(); + m_waitForAuthentication = false; + auto ep = (m_tcpSocket ? m_routeBack : (m_nat ? m_routeBack : m_localEndpoint)); + auto secureWrapper = QKnxNetIpSecureWrapperProxy::secureBuilder() + .setSecureSessionId(m_sessionId) + .setSequenceNumber(m_sequenceNumber) + .setSerialNumber(m_serialNumber) + // .setMessageTag(0x0000) TODO: Do we need an API for this? + .setEncapsulatedFrame(QKnxNetIpConnectRequestProxy::builder() + .setControlEndpoint(ep) + .setDataEndpoint(ep) + .setRequestInformation(m_cri) + .create()) + .create(m_sessionKey); + + ++m_sequenceNumber; + if (m_tcpSocket) + m_tcpSocket->write(secureWrapper.bytes().toByteArray()); + + if (!m_secureConfig.d->keepAlive) + break; + + Q_Q(QKnxNetIpEndpointConnection); + QObject::connect(m_secureTimer, &QTimer::timeout, q, [&]() { + auto secureStatusWrapper = QKnxNetIpSecureWrapperProxy::secureBuilder() + .setSecureSessionId(m_sessionId) + .setSequenceNumber(m_sequenceNumber) + .setSerialNumber(m_serialNumber) + // .setMessageTag(0x0000) TODO: Do we need an API for this? + .setEncapsulatedFrame(QKnxNetIpSessionStatusProxy::builder() + .setStatus(QKnxNetIp::SecureSessionStatus::KeepAlive) + .create()) + .create(m_sessionKey); + qDebug() << "Sending keep alive status frame:" << secureStatusWrapper; + + ++m_sequenceNumber; + if (m_tcpSocket) + m_tcpSocket->write(secureStatusWrapper.bytes().toByteArray()); + }); + m_secureTimer->setSingleShot(false); + m_secureTimer->start(QKnxNetIp::Timeout::SecureSessionTimeout - 5000); + } + } break; + + case QKnxNetIp::SecureSessionStatus::KeepAlive: + // TODO: implement in case we're the server + qDebug() << "Unexpectedly received keep alive status frame:" << frame; + break; + + case QKnxNetIp::SecureSessionStatus::AuthenticationFailed: + setAndEmitErrorOccurred(QKnxNetIpEndpointConnection::Error::AuthFailed, + QKnxNetIpEndpointConnection::tr("Secure session authentication failed.")); + close = true; + break; + + case QKnxNetIp::SecureSessionStatus::Unauthenticated: + setAndEmitErrorOccurred(QKnxNetIpEndpointConnection::Error::AuthFailed, + QKnxNetIpEndpointConnection::tr("Secure session not authenticated.")); + close = true; + break; + + case QKnxNetIp::SecureSessionStatus::Timeout: + setAndEmitErrorOccurred(QKnxNetIpEndpointConnection::Error::Timeout, + QKnxNetIpEndpointConnection::tr("A timeout occurred during secure session handshake.")); + close = true; + break; + + case QKnxNetIp::SecureSessionStatus::Close: + setAndEmitErrorOccurred(QKnxNetIpEndpointConnection::Error::Timeout, + QKnxNetIpEndpointConnection::tr("The server requested to close the secure session.")); + close = true; + break; + + default: + qDebug() << "Received unknown status frame:" << frame; + break; + } + + if (close) { + Q_Q(QKnxNetIpEndpointConnection); + q->disconnectFromHost(); + } + } break; + + case QKnxNetIp::ServiceType::TimerNotify: + qDebug() << "Unexpectedly received timer notify frame:" << frame; + break; + default: break; } + return serviceType; } -void QKnxNetIpEndpointConnectionPrivate::setup() +bool QKnxNetIpEndpointConnectionPrivate::initConnection(const QHostAddress &a, quint16 p, + QKnxNetIp::HostProtocol hp) { - setupTimer(); + if (!QKnxNetIp::isStructType(hp)) + return false; + + if (m_state != QKnxNetIpEndpointConnection::State::Disconnected) + return false; + + auto isIPv4 = false; + a.toIPv4Address(&isIPv4); + if (!isIPv4) { + setAndEmitErrorOccurred(QKnxNetIpEndpointConnection::Error::NotIPv4, + QKnxNetIpEndpointConnection::tr("Only IPv4 addresses as remote control endpoint " + "supported.")); + return false; + } + m_remoteControlEndpoint = { a, p, hp }; + + isIPv4 = false; + m_user.address.toIPv4Address(&isIPv4); + if (!isIPv4) { + setAndEmitErrorOccurred(QKnxNetIpEndpointConnection::Error::NotIPv4, + QKnxNetIpEndpointConnection::tr("Only IPv4 addresses as local control endpoint " + "supported.")); + return false; + } + + setAndEmitStateChanged(QKnxNetIpEndpointConnection::State::Starting); m_channelId = -1; @@ -327,49 +557,67 @@ void QKnxNetIpEndpointConnectionPrivate::setup() m_errorString = QString(); m_error = QKnxNetIpEndpointConnection::Error::None; - if (m_tcpSocket) { - QObject::connect(m_tcpSocket, &QIODevice::readyRead, [&]() { + m_routeBack.hostProtocol = hp; + + m_sessionId = 0; + m_sequenceNumber = 0; + m_waitForAuthentication = false; + + setupTimer(); + + QKnxPrivate::clearSocket(&m_tcpSocket); + QKnxPrivate::clearSocket(&m_udpSocket); + + Q_Q(QKnxNetIpEndpointConnection); + QAbstractSocket *socket = nullptr; + if (hp == QKnxNetIp::HostProtocol::TCP_IPv4) { + socket = m_tcpSocket = new QTcpSocket(q_func()); + QObject::connect(m_tcpSocket, &QTcpSocket::readyRead, q, [&]() { + if (m_tcpSocket->bytesAvailable() < QKnxNetIpFrameHeader::HeaderSize10) + return; m_rxBuffer += QKnxByteArray::fromByteArray(m_tcpSocket->readAll()); - int bufferSize = m_rxBuffer.size(); - bool continueProcessingRxBuffer = false; - do { - // TODO: AN184 v03 KNXnet-IP Core v2 AS, 2.2.3.2.3.2 - processReceivedFrame(m_remoteControlEndpoint.address, - m_remoteControlEndpoint.port); - continueProcessingRxBuffer = (m_rxBuffer.size() < bufferSize); - bufferSize = m_rxBuffer.size(); - } while (continueProcessingRxBuffer); - }); - using overload = void (QTcpSocket::*)(QTcpSocket::SocketError); - QObject::connect(m_tcpSocket, - static_cast<overload>(&QTcpSocket::error), [&](QTcpSocket::SocketError) { - setAndEmitErrorOccurred(QKnxNetIpEndpointConnection::Error::Network, - m_tcpSocket->errorString()); + // TODO: AN184 v03 KNXnet-IP Core v2 AS, 2.2.3.2.3.2 + auto header = QKnxNetIpFrameHeader::fromBytes(m_rxBuffer); + if (header.isValid() && m_rxBuffer.size() < header.totalSize()) + return; - Q_Q(QKnxNetIpEndpointConnection); - q->disconnectFromHost(); + auto frame = QKnxNetIpFrame::fromBytes(m_rxBuffer); + m_rxBuffer.remove(0, header.totalSize()); + processReceivedFrame(frame); }); - } else if (m_udpSocket) { - QObject::connect(m_udpSocket, &QUdpSocket::readyRead, [&]() { + } else if (hp == QKnxNetIp::HostProtocol::UDP_IPv4) { + socket = m_udpSocket = new QUdpSocket(q_func()); + QObject::connect(m_udpSocket, &QUdpSocket::readyRead, q, [&]() { while (m_udpSocket && m_udpSocket->state() == QUdpSocket::BoundState && m_udpSocket->hasPendingDatagrams()) { - auto datagram = m_udpSocket->receiveDatagram(); - m_rxBuffer += QKnxByteArray::fromByteArray(datagram.data()); - processReceivedFrame(datagram.senderAddress(), datagram.senderPort()); + auto tmp = m_udpSocket->receiveDatagram(); + auto frame = QKnxNetIpFrame::fromBytes(QKnxByteArray::fromByteArray(tmp.data())); + if (processReceivedFrame(frame) != QKnxNetIp::ServiceType::ConnectResponse) + continue; + + if (m_nat && m_remoteDataEndpoint.isNullOrLocal()) + m_remoteDataEndpoint = { tmp.senderAddress(), quint16(tmp.senderPort())}; } }); + } else { + return false; + } - using overload = void (QUdpSocket::*)(QUdpSocket::SocketError); - QObject::connect(m_udpSocket, - static_cast<overload>(&QUdpSocket::error), [&](QUdpSocket::SocketError) { - setAndEmitErrorOccurred(QKnxNetIpEndpointConnection::Error::Network, - m_udpSocket->errorString()); - - Q_Q(QKnxNetIpEndpointConnection); - q->disconnectFromHost(); + if (socket) { + QObject::connect(socket, QOverload<QAbstractSocket::SocketError>::of(&QAbstractSocket::error), + q, [socket, this](QAbstractSocket::SocketError) { + setAndEmitErrorOccurred(QKnxNetIpEndpointConnection::Error::Network, + socket->errorString()); + Q_Q(QKnxNetIpEndpointConnection); + q->disconnectFromHost(); }); + } else { + return false; } + + setAndEmitStateChanged(QKnxNetIpEndpointConnection::State::Connecting); + return true; } void QKnxNetIpEndpointConnectionPrivate::cleanup() @@ -379,11 +627,26 @@ void QKnxNetIpEndpointConnectionPrivate::cleanup() QKnxPrivate::clearTimer(&m_connectionStateTimer); QKnxPrivate::clearTimer(&m_disconnectRequestTimer); QKnxPrivate::clearTimer(&m_acknowledgeTimer); + QKnxPrivate::clearTimer(&m_secureTimer); if (m_udpSocket) { m_udpSocket->close(); QKnxPrivate::clearSocket(&m_udpSocket); - } else { + } else if (m_tcpSocket) { + if (m_secureConfig.isValid()) { + auto secureStatusWrapper = QKnxNetIpSecureWrapperProxy::secureBuilder() + .setSecureSessionId(m_sessionId) + .setSequenceNumber(m_sequenceNumber) + .setSerialNumber(m_serialNumber) + // .setMessageTag(0x0000) TODO: Do we need an API for this? + .setEncapsulatedFrame(QKnxNetIpSessionStatusProxy::builder() + .setStatus(QKnxNetIp::SecureSessionStatus::Close) + .create()) + .create(m_sessionKey); + ++m_sequenceNumber; + m_tcpSocket->write(secureStatusWrapper.bytes().toByteArray()); + m_tcpSocket->waitForBytesWritten(); + } m_tcpSocket->close(); QKnxPrivate::clearSocket(&m_tcpSocket); } @@ -393,19 +656,37 @@ void QKnxNetIpEndpointConnectionPrivate::cleanup() bool QKnxNetIpEndpointConnectionPrivate::sendCemiRequest() { - if (!m_waitForAcknowledgement && !m_tcpSocket) { + if (m_udpSocket) { + if (m_waitForAcknowledgement) + return false; // still waiting for an ACK from an previous request + m_waitForAcknowledgement = true; m_udpSocket->writeDatagram(m_lastSendCemiRequest.bytes().toByteArray(), - m_remoteDataEndpoint.address, - m_remoteDataEndpoint.port); + m_remoteDataEndpoint.address, + m_remoteDataEndpoint.port); m_cemiRequests++; m_acknowledgeTimer->start(m_acknowledgeTimeout); - } else if (m_tcpSocket) { - m_tcpSocket->write(m_lastSendCemiRequest.bytes().toByteArray()); - m_waitForAcknowledgement = false; + return true; } - return !m_waitForAcknowledgement; + + if (m_tcpSocket) { + if (m_secureConfig.isValid()) { + auto secureFrame = QKnxNetIpSecureWrapperProxy::secureBuilder() + .setSecureSessionId(m_sessionId) + .setSequenceNumber(m_sequenceNumber) + .setSerialNumber(m_serialNumber) + // .setMessageTag(0x0000) TODO: Do we need an API for this? + .setEncapsulatedFrame(m_lastSendCemiRequest) + .create(m_sessionKey); + ++m_sequenceNumber; + m_tcpSocket->write(secureFrame.bytes().toByteArray()); + } else { + m_tcpSocket->write(m_lastSendCemiRequest.bytes().toByteArray()); + } + return true; // TCP connections do not send an ACK + } + return false; } void QKnxNetIpEndpointConnectionPrivate::sendStateRequest() @@ -414,7 +695,19 @@ void QKnxNetIpEndpointConnectionPrivate::sendStateRequest() .bytes().toHex(); if (m_tcpSocket) { - m_tcpSocket->write(m_lastStateRequest.bytes().toByteArray()); + if (m_secureConfig.isValid()) { + auto secureFrame = QKnxNetIpSecureWrapperProxy::secureBuilder() + .setSecureSessionId(m_sessionId) + .setSequenceNumber(m_sequenceNumber) + .setSerialNumber(m_serialNumber) + // .setMessageTag(0x0000) TODO: Do we need an API for this? + .setEncapsulatedFrame(m_lastStateRequest) + .create(m_sessionKey); + ++m_sequenceNumber; + m_tcpSocket->write(secureFrame.bytes().toByteArray()); + } else { + m_tcpSocket->write(m_lastStateRequest.bytes().toByteArray()); + } } else { m_udpSocket->writeDatagram(m_lastStateRequest.bytes().toByteArray(), m_remoteControlEndpoint.address, m_remoteControlEndpoint.port); @@ -653,10 +946,13 @@ void QKnxNetIpEndpointConnectionPrivate::processConnectResponse(const QKnxNetIpF m_remoteDataEndpoint = response.dataEndpoint(); m_lastStateRequest = QKnxNetIpConnectionStateRequestProxy::builder() .setChannelId(m_channelId) - .setControlEndpoint(m_nat ? m_natEndpoint : m_localEndpoint) + .setControlEndpoint( + m_tcpSocket ? m_routeBack : (m_nat ? m_routeBack : m_localEndpoint) + ) .create(); - QTimer::singleShot(0, [&]() { sendStateRequest(); }); + Q_Q(QKnxNetIpEndpointConnection); + QTimer::singleShot(0, q, [&]() { sendStateRequest(); }); setAndEmitStateChanged(QKnxNetIpEndpointConnection::State::Connected); } else { auto metaEnum = QMetaEnum::fromType<QKnxNetIp::Error>(); @@ -699,16 +995,26 @@ void QKnxNetIpEndpointConnectionPrivate::processDisconnectRequest(const QKnxNetI QKnxNetIpDisconnectRequestProxy request(frame); if (request.channelId() == m_channelId) { - auto frame = QKnxNetIpDisconnectResponseProxy::builder() + auto responseFrame = QKnxNetIpDisconnectResponseProxy::builder() .setChannelId(m_channelId) .setStatus(QKnxNetIp::Error::None) .create(); - qDebug() << "Sending disconnect response:" << frame; + qDebug() << "Sending disconnect response:" << responseFrame; if (m_tcpSocket) { - m_tcpSocket->write(frame.bytes().toByteArray()); + if (m_secureConfig.isValid()) { + responseFrame = QKnxNetIpSecureWrapperProxy::secureBuilder() + .setSecureSessionId(m_sessionId) + .setSequenceNumber(m_sequenceNumber) + .setSerialNumber(m_serialNumber) + // .setMessageTag(0x0000) TODO: Do we need an API for this? + .setEncapsulatedFrame(responseFrame) + .create(m_sessionKey); + ++m_sequenceNumber; + } + m_tcpSocket->write(responseFrame.bytes().toByteArray()); } else { - m_udpSocket->writeDatagram(frame.bytes().toByteArray(), - m_remoteControlEndpoint.address, m_remoteControlEndpoint.port); + m_udpSocket->writeDatagram(responseFrame.bytes().toByteArray(), + m_remoteControlEndpoint.address, m_remoteControlEndpoint.port); } Q_Q(QKnxNetIpEndpointConnection); @@ -757,7 +1063,8 @@ void QKnxNetIpEndpointConnectionPrivate::setAndEmitErrorOccurred( } -// -- QKnxNetIpClient +// -- QKnxNetIpEndpointConnection + /*! Destroys a connection and disconnects from the server. */ @@ -879,8 +1186,8 @@ quint32 QKnxNetIpEndpointConnection::heartbeatTimeout() const } /*! - Sets the value of the heartbeat timeout. Every \a msec a state request is sent over the - connection to keep it alive. + Sets the value of the heartbeat timeout. Every \a msec a state request is + sent over the connection to keep it alive. */ void QKnxNetIpEndpointConnection::setHeartbeatTimeout(quint32 msec) { @@ -923,139 +1230,184 @@ void QKnxNetIpEndpointConnection::connectToHost(const QKnxNetIpHpai &controlEndp } /*! - Establishes a connection to the host with \a address and \a port. + Establishes a connection to the host with \a address and \a port via UDP. */ void QKnxNetIpEndpointConnection::connectToHost(const QHostAddress &address, quint16 port) { Q_D(QKnxNetIpEndpointConnection); - if (d->m_state != State::Disconnected) - return; - - auto isIPv4 = false; - address.toIPv4Address(&isIPv4); - if (!isIPv4) { - d->setAndEmitErrorOccurred(Error::NotIPv4, tr("Only IPv4 addresses as remote control " - "endpoint supported.")); - return; - } - d->m_remoteControlEndpoint = Endpoint(address, port); - - isIPv4 = false; - d->m_user.address.toIPv4Address(&isIPv4); - if (!isIPv4) { - d->setAndEmitErrorOccurred(Error::NotIPv4, tr("Only IPv4 addresses as local control " - "endpoint supported.")); + if (!d->initConnection(address, port, QKnxNetIp::HostProtocol::UDP_IPv4)) return; - } - - d->setAndEmitStateChanged(QKnxNetIpEndpointConnection::State::Starting); - - QKnxPrivate::clearSocket(&(d->m_udpSocket)); - d->m_udpSocket = new QUdpSocket(this); - if (!d->m_udpSocket->bind(d->m_user.address, d->m_user.port)) { - d->setAndEmitErrorOccurred(QKnxNetIpEndpointConnection::Error::Network, - QKnxNetIpEndpointConnection::tr("Could not bind endpoint: %1") - .arg(d->m_udpSocket->errorString())); - QKnxPrivate::clearSocket(&d->m_udpSocket); - d->setAndEmitStateChanged(QKnxNetIpEndpointConnection::State::Disconnected); + if (!d->m_udpSocket->bind(d->m_user.address, d->m_user.port)) return; - } - d->m_localEndpoint = Endpoint(d->m_udpSocket->localAddress(), d->m_udpSocket->localPort()); + d->m_localEndpoint = { d->m_udpSocket->localAddress(), d->m_udpSocket->localPort() }; d->setAndEmitStateChanged(QKnxNetIpEndpointConnection::State::Bound); - d->setup(); - - d->setAndEmitStateChanged(QKnxNetIpEndpointConnection::State::Connecting); - auto request = QKnxNetIpConnectRequestProxy::builder() - .setControlEndpoint(d->m_nat ? d->m_natEndpoint : d->m_localEndpoint) - .setDataEndpoint(d->m_nat ? d->m_natEndpoint : d->m_localEndpoint) + .setControlEndpoint(d->m_nat ? d->m_routeBack : d->m_localEndpoint) + .setDataEndpoint(d->m_nat ? d->m_routeBack : d->m_localEndpoint) .setRequestInformation(d->m_cri) .create(); d->m_controlEndpointVersion = request.header().protocolVersion(); - qDebug() << "Sending connect request:" << request; - - d->m_connectRequestTimer->start(QKnxNetIp::ConnectRequestTimeout); + d->setAndEmitStateChanged(QKnxNetIpEndpointConnection::State::Connecting); + qDebug() << "Sending connect request:" << request; d->m_udpSocket->writeDatagram(request.bytes().toByteArray(), d->m_remoteControlEndpoint.address, d->m_remoteControlEndpoint.port); + + if (d->m_connectRequestTimer) + d->m_connectRequestTimer->start(QKnxNetIp::ConnectRequestTimeout); } /*! \since 5.12 - Establishes a connection to the host with \a address, \a port and \a proto. + Establishes a connection to the host with \a address, \a port and \a protocol. */ void QKnxNetIpEndpointConnection::connectToHost(const QHostAddress &address, quint16 port, - QKnxNetIp::HostProtocol proto) + QKnxNetIp::HostProtocol protocol) { - if (proto == QKnxNetIp::HostProtocol::Unknown) - return; + if (protocol == QKnxNetIp::HostProtocol::UDP_IPv4) + return connectToHost(address, port); - if (proto == QKnxNetIp::HostProtocol::UDP_IPv4) { - connectToHost(address, port); + Q_D(QKnxNetIpEndpointConnection); + if (!d->initConnection(address, port, protocol)) return; - } - // establishing TCP connection + connect(d->m_tcpSocket, &QTcpSocket::connected, this, [&]() { + Q_D(QKnxNetIpEndpointConnection); + d->m_localEndpoint = { d->m_tcpSocket->localAddress(), d->m_tcpSocket->localPort(), + QKnxNetIp::HostProtocol::TCP_IPv4 }; + + auto request = QKnxNetIpConnectRequestProxy::builder() + .setControlEndpoint(d->m_routeBack) + .setDataEndpoint(d->m_routeBack) + .setRequestInformation(d->m_cri) + .create(); + d->m_controlEndpointVersion = request.header().protocolVersion(); + + qDebug() << "Sending connect request:" << request; + d->m_tcpSocket->write(request.bytes().toByteArray()); + d->m_connectRequestTimer->start(QKnxNetIp::ConnectRequestTimeout); + }); + d->m_tcpSocket->connectToHost(address, port); +} + +/*! + \since 5.13 + + Returns the serial number of the device using this connection. The default + value is set to \c {0x000000000000}. +*/ +QKnxByteArray QKnxNetIpEndpointConnection::serialNumber() const +{ + Q_D(const QKnxNetIpEndpointConnection); + return d->m_serialNumber; +} + +/*! + \since 5.13 + + Sets the serial number to \a serialNumber of the device using this + connection. + + \note The serial number must contain exactly 6 bytes and cannot be changed + while the secure session is established. +*/ +void QKnxNetIpEndpointConnection::setSerialNumber(const QKnxByteArray &serialNumber) +{ Q_D(QKnxNetIpEndpointConnection); - if (d->m_state != State::Disconnected) - return; + if (d->m_state == QKnxNetIpEndpointConnection::State::Disconnected) + d->m_serialNumber = serialNumber; +} - auto isIPv4 = false; - address.toIPv4Address(&isIPv4); - if (!isIPv4) { - d->setAndEmitErrorOccurred(Error::NotIPv4, tr("Only IPv4 addresses as remote control " - "endpoint supported.")); - return; - } - d->m_remoteControlEndpoint = Endpoint(address, port); - d->m_remoteControlEndpoint.code = QKnxNetIp::HostProtocol::TCP_IPv4; +/*! + \since 5.13 - isIPv4 = false; - d->m_user.address.toIPv4Address(&isIPv4); - if (!isIPv4) { - d->setAndEmitErrorOccurred(Error::NotIPv4, tr("Only IPv4 addresses as local control " - "endpoint supported.")); - return; + Returns the secure configuration used to establish the secure session. +*/ +QKnxNetIpSecureConfiguration QKnxNetIpEndpointConnection::secureConfiguration() const +{ + Q_D(const QKnxNetIpEndpointConnection); + return d->m_secureConfig; +} + +/*! + \since 5.13 + + Sets a secure configuration to be used to establish secure session + to \a {config}. The configuration cannot be changed while the secure + connection is established. +*/ +void QKnxNetIpEndpointConnection::setSecureConfiguration(const QKnxNetIpSecureConfiguration &config) +{ + Q_D(QKnxNetIpEndpointConnection); + if (d->m_state == QKnxNetIpEndpointConnection::State::Disconnected) { + d->m_secureConfig = config; + d->updateCri(config.individualAddress()); } +} - d->setAndEmitStateChanged(QKnxNetIpEndpointConnection::State::Starting); +/*! + \since 5.13 - QKnxPrivate::clearSocket(&(d->m_tcpSocket)); + Establishes a connection to the KNXnet/IP control endpoint \a controlEndpoint. - d->m_tcpSocket = new QTcpSocket(this); - d->setup(); + \sa setSerialNumber(), setSecureConfiguration() +*/ +void QKnxNetIpEndpointConnection::connectToHostEncrypted(const QKnxNetIpHpai &controlEndpoint) +{ + const QKnxNetIpHpaiProxy proxy(controlEndpoint); + if (proxy.isValid() && proxy.hostProtocol() == QKnxNetIp::HostProtocol::TCP_IPv4) + connectToHost(proxy.hostAddress(), proxy.port()); +} - d->setAndEmitStateChanged(QKnxNetIpEndpointConnection::State::Connecting); +/*! + \since 5.13 - d->m_tcpSocket->abort(); - d->m_tcpSocket->connectToHost(address, port); + Establishes a secure session to the host with \a address and \a port. + + \sa setSerialNumber(), setSecureConfiguration() +*/ +void QKnxNetIpEndpointConnection::connectToHostEncrypted(const QHostAddress &address, quint16 port) +{ + Q_D(QKnxNetIpEndpointConnection); + if (!d->initConnection(address, port, QKnxNetIp::HostProtocol::TCP_IPv4)) + return; + + if (!d->m_secureConfig.isValid()) + return d->setAndEmitErrorOccurred(Error::SecureConfig, tr("Invalid secure configuration.")); - d->m_localEndpoint = Endpoint(d->m_tcpSocket->localAddress(), - d->m_tcpSocket->localPort()); - d->m_localEndpoint.code = QKnxNetIp::HostProtocol::TCP_IPv4; - d->m_natEndpoint.code = QKnxNetIp::HostProtocol::TCP_IPv4; - d->m_remoteDataEndpoint.code = QKnxNetIp::HostProtocol::TCP_IPv4; + if (d->m_serialNumber.size() != 6) + return d->setAndEmitErrorOccurred(Error::SerialNumber, tr("Invalid device serial number.")); connect(d->m_tcpSocket, &QTcpSocket::connected, this, [&]() { Q_D(QKnxNetIpEndpointConnection); - auto request = QKnxNetIpConnectRequestProxy::builder() - .setControlEndpoint(d->m_nat ? d->m_natEndpoint : d->m_localEndpoint) - .setDataEndpoint(d->m_nat ? d->m_natEndpoint : d->m_localEndpoint) - .setRequestInformation(d->m_cri) + d->m_localEndpoint = Endpoint(d->m_tcpSocket->localAddress(), + d->m_tcpSocket->localPort(), QKnxNetIp::HostProtocol::TCP_IPv4); + + auto request = QKnxNetIpSessionRequestProxy::builder() + .setControlEndpoint(d->m_routeBack) + .setPublicKey(d->m_secureConfig.d->publicKey.bytes()) .create(); d->m_controlEndpointVersion = request.header().protocolVersion(); - qDebug() << "Sending connect request:" << request; + qDebug() << "Sending secure session request:" << request; d->m_tcpSocket->write(request.bytes().toByteArray()); - }); - // TODO: Implement connect request timeout. + QObject::connect(d->m_secureTimer, &QTimer::timeout, this, [&]() { + Q_D(QKnxNetIpEndpointConnection); + d->m_secureTimer->stop(); + d->setAndEmitErrorOccurred(QKnxNetIpEndpointConnection::Error::AuthFailed, + QKnxNetIpEndpointConnection::tr("Could not establish secure session.")); + disconnectFromHost(); + }); + d->m_secureTimer->start(QKnxNetIp::SecureSessionRequestTimeout); + }); + d->m_tcpSocket->connectToHost(address, port); } /*! @@ -1076,12 +1428,24 @@ void QKnxNetIpEndpointConnection::disconnectFromHost() } else { auto frame = QKnxNetIpDisconnectRequestProxy::builder() .setChannelId(d->m_channelId) - .setControlEndpoint(d->m_nat ? d->m_natEndpoint : d->m_localEndpoint) + .setControlEndpoint(d->m_nat ? d->m_routeBack : d->m_localEndpoint) .create(); qDebug() << "Sending disconnect request:" << frame; if (d->m_tcpSocket) { - d->m_tcpSocket->write(frame.bytes().toByteArray()); + if (d->m_secureConfig.isValid()) { + auto secureFrame = QKnxNetIpSecureWrapperProxy::secureBuilder() + .setSecureSessionId(d->m_sessionId) + .setSequenceNumber(d->m_sequenceNumber) + .setSerialNumber(d->m_serialNumber) + // .setMessageTag(0x0000) TODO: Do we need an API for this? + .setEncapsulatedFrame(frame) + .create(d->m_sessionKey); + ++(d->m_sequenceNumber); + d->m_tcpSocket->write(secureFrame.bytes().toByteArray()); + } else { + d->m_tcpSocket->write(frame.bytes().toByteArray()); + } } else { d->m_udpSocket->writeDatagram(frame.bytes().toByteArray(), d->m_remoteControlEndpoint.address, d->m_remoteControlEndpoint.port); @@ -1092,6 +1456,9 @@ void QKnxNetIpEndpointConnection::disconnectFromHost() } } +/*! + \internal +*/ QKnxNetIpEndpointConnection::QKnxNetIpEndpointConnection(QKnxNetIpEndpointConnectionPrivate &dd, QObject *parent) : QObject(dd, parent) diff --git a/src/knx/netip/qknxnetipendpointconnection.h b/src/knx/netip/qknxnetipendpointconnection.h index 5677805..4c94b78 100644 --- a/src/knx/netip/qknxnetipendpointconnection.h +++ b/src/knx/netip/qknxnetipendpointconnection.h @@ -32,8 +32,10 @@ #include <QtKnx/qtknxglobal.h> #include <QtKnx/qknxnetipcri.h> -#include <QtKnx/qknxnetiphpai.h> #include <QtKnx/qknxnetipframe.h> +#include <QtKnx/qknxnetiphpai.h> +#include <QtKnx/qknxnetipsecureconfiguration.h> + #include <QtNetwork/qudpsocket.h> QT_BEGIN_NAMESPACE @@ -67,6 +69,11 @@ public: Acknowledge, Heartbeat, Cemi, + SecureConfig, + SerialNumber, + AuthFailed, + Timeout, + Close, Unknown = 0x80 }; Q_ENUM(Error) @@ -111,6 +118,15 @@ public: void connectToHost(const QHostAddress &address, quint16 port); void connectToHost(const QHostAddress &address, quint16 port, QKnxNetIp::HostProtocol proto); + QKnxByteArray serialNumber() const; + void setSerialNumber(const QKnxByteArray &serialNumber); + + QKnxNetIpSecureConfiguration secureConfiguration() const; + void setSecureConfiguration(const QKnxNetIpSecureConfiguration &config); + + void connectToHostEncrypted(const QKnxNetIpHpai &controlEndpoint); + void connectToHostEncrypted(const QHostAddress &address, quint16 port); + void disconnectFromHost(); protected: diff --git a/src/knx/netip/qknxnetipendpointconnection_p.h b/src/knx/netip/qknxnetipendpointconnection_p.h index 71f4279..81c9bb6 100644 --- a/src/knx/netip/qknxnetipendpointconnection_p.h +++ b/src/knx/netip/qknxnetipendpointconnection_p.h @@ -42,12 +42,15 @@ // #include <QtCore/qtimer.h> -#include <QtKnx/qknxaddress.h> + #include <QtKnx/qtknxglobal.h> -#include <QtKnx/qknxnetipendpointconnection.h> -#include <QtNetwork/qhostaddress.h> +#include <QtKnx/qknxaddress.h> #include <QtKnx/qknxdevicemanagementframe.h> #include <QtKnx/qknxlinklayerframe.h> +#include <QtKnx/qknxnetipendpointconnection.h> +#include <QtKnx/qknxnetipsecureconfiguration.h> + +#include <QtNetwork/qhostaddress.h> #include <private/qobject_p.h> @@ -79,6 +82,11 @@ struct Endpoint final : address(addr) , port(p) {} + Endpoint(const QHostAddress &addr, quint16 p, QKnxNetIp::HostProtocol c) + : address(addr) + , port(p) + , hostProtocol(c) + {} explicit Endpoint(const QKnxNetIpHpaiProxy &hpai) : address(hpai.hostAddress()) , port(hpai.port()) @@ -90,7 +98,7 @@ struct Endpoint final Endpoint &operator=(const QKnxNetIpHpai &s) { const QKnxNetIpHpaiProxy hpai(s); - address = hpai.hostAddress(); port = hpai.port(); + hostProtocol = hpai.hostProtocol(); address = hpai.hostAddress(); port = hpai.port(); return *this; } operator QKnxNetIpHpai() const @@ -98,13 +106,17 @@ struct Endpoint final return QKnxNetIpHpaiProxy::builder() .setHostAddress(address) .setPort(port) - .setHostProtocol(code) + .setHostProtocol(hostProtocol) .create(); } + bool isNullOrLocal() const + { + return address.isNull() || address == QHostAddress::Any || address.isLoopback() || port == 0; + } - QHostAddress address { QHostAddress::LocalHost }; + QHostAddress address { QHostAddress::Null }; quint16 port { 0 }; - QKnxNetIp::HostProtocol code { QKnxNetIp::HostProtocol::UDP_IPv4 }; + QKnxNetIp::HostProtocol hostProtocol { QKnxNetIp::HostProtocol::UDP_IPv4 }; }; class Q_KNX_EXPORT QKnxNetIpEndpointConnectionPrivate : public QObjectPrivate @@ -121,14 +133,14 @@ public: {} ~QKnxNetIpEndpointConnectionPrivate() override = default; - void setup(); void setupTimer(); + bool initConnection(const QHostAddress &address, quint16 port, QKnxNetIp::HostProtocol hp); void cleanup(); bool sendCemiRequest(); void sendStateRequest(); - void processReceivedFrame(const QHostAddress &address, int port); + QKnxNetIp::ServiceType processReceivedFrame(const QKnxNetIpFrame &frame); virtual void process(const QKnxLinkLayerFrame &frame); virtual void process(const QKnxDeviceManagementFrame &frame); @@ -155,14 +167,40 @@ public: void setAndEmitStateChanged(QKnxNetIpEndpointConnection::State newState); void setAndEmitErrorOccurred(QKnxNetIpEndpointConnection::Error newError, const QString &message); - void setCri(const QKnxNetIpCri &cri) { m_cri = cri; } + QKnxNetIpCri cri() const { return m_cri; } + void updateCri(QKnxNetIp::TunnelLayer layer) + { + QKnxNetIpCriProxy proxy(m_cri); + m_cri = proxy.builder() + .setTunnelLayer(layer) + .setIndividualAddress(proxy.individualAddress()) + .create(); + } + + void updateCri(const QKnxAddress &ia) + { + QKnxNetIpCriProxy proxy(m_cri); + if (proxy.connectionType() == QKnxNetIp::ConnectionType::DeviceManagement) + return; + + if (ia.isValid()) { + m_cri = QKnxNetIpCriProxy::builder() + .setTunnelLayer(proxy.tunnelLayer()) + .setIndividualAddress(ia) + .create(); + } else { + m_cri = proxy.builder() + .setTunnelLayer(proxy.tunnelLayer()) + .create(); + } + } private: QKnxNetIpCri m_cri; Endpoint m_remoteDataEndpoint; Endpoint m_remoteControlEndpoint; - Endpoint m_natEndpoint { QHostAddress::AnyIPv4 }; + Endpoint m_routeBack { QHostAddress::AnyIPv4 }; Endpoint m_localEndpoint { QHostAddress::LocalHost }; int m_channelId { -1 }; @@ -203,6 +241,17 @@ private: QKnxByteArray m_rxBuffer; UserProperties m_user; + + quint16 m_sessionId { 0 }; + quint48 m_sequenceNumber { 0 }; + bool m_waitForAuthentication { false }; + + QKnxByteArray m_sessionKey; + QTimer *m_secureTimer { nullptr }; + QKnxNetIpSecureConfiguration m_secureConfig; + + // TODO: We need some kind of device configuration class as well. + QKnxByteArray m_serialNumber { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; }; QT_END_NAMESPACE diff --git a/src/knx/netip/qknxnetiproutingindication.cpp b/src/knx/netip/qknxnetiproutingindication.cpp index c465181..d84e732 100644 --- a/src/knx/netip/qknxnetiproutingindication.cpp +++ b/src/knx/netip/qknxnetiproutingindication.cpp @@ -59,7 +59,7 @@ QT_BEGIN_NAMESPACE if (!proxy.isValid()) return; - auto linkFrame = proxy.linkLayerFrame(); + auto linkFrame = proxy.cemi(); \endcode \sa builder(), QKnxNetIpRoutingLostMessageProxy, QKnxNetIpRoutingBusyProxy, diff --git a/src/knx/netip/qknxnetiproutingsystembroadcast.cpp b/src/knx/netip/qknxnetiproutingsystembroadcast.cpp index 66e8d6c..232a50e 100644 --- a/src/knx/netip/qknxnetiproutingsystembroadcast.cpp +++ b/src/knx/netip/qknxnetiproutingsystembroadcast.cpp @@ -67,7 +67,7 @@ QT_BEGIN_NAMESPACE if (!proxy.isValid()) return; - auto linkFrame = proxy.linkLayerFrame(); + auto linkFrame = proxy.cemi(); \endcode \sa builder(), QKnxNetIpRoutingSystemBroadcastProxy, diff --git a/src/knx/netip/qknxnetipsecureconfiguration.cpp b/src/knx/netip/qknxnetipsecureconfiguration.cpp new file mode 100644 index 0000000..8b2d03b --- /dev/null +++ b/src/knx/netip/qknxnetipsecureconfiguration.cpp @@ -0,0 +1,472 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtKnx module. +** +** $QT_BEGIN_LICENSE:GPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 or (at your option) any later version +** approved by the KDE Free Qt Foundation. The licenses are as published by +** the Free Software Foundation and appearing in the file LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qknxcryptographicengine.h" +#include "qknxnetipsecureconfiguration.h" + +#include <QtCore/qfile.h> + +#include "private/qknxkeyring_p.h" +#include "private/qknxnetipsecureconfiguration_p.h" + +QT_BEGIN_NAMESPACE + +namespace QKnxPrivate +{ + static QKnxNetIpSecureConfiguration fromInterface(const QKnx::Ets::Keyring::QKnxInterface &iface, + const QKnxByteArray &pwHash, const QKnxByteArray &createdHash) + { + QKnxNetIpSecureConfiguration s; + s.setUserId(QKnxNetIp::SecureUserId(iface.UserID)); + s.setPrivateKey(QKnxSecureKey::generatePrivateKey()); + s.setHost({ QKnxAddress::Type::Individual, iface.Host }); + s.setUserPassword(QKnxCryptographicEngine::decodeAndDecryptPassword(pwHash, + createdHash, iface.Password).toByteArray()); + s.setIndividualAddress({ QKnxAddress::Type::Individual, iface.IndividualAddress }); + s.setDeviceAuthenticationCode(QKnxCryptographicEngine::decodeAndDecryptPassword(pwHash, + createdHash, iface.Authentication).toByteArray()); + return s; + } + + static QKnxNetIpSecureConfiguration fromDevice(const QKnx::Ets::Keyring::QKnxDevice &device, + const QKnxByteArray &pwHash, const QKnxByteArray &createdHash) + { + QKnxNetIpSecureConfiguration s; + s.setUserId(QKnxNetIp::SecureUserId::Management); + s.setPrivateKey(QKnxSecureKey::generatePrivateKey()); + s.setHost({ QKnxAddress::Type::Individual, device.IndividualAddress }); + s.setUserPassword(QKnxCryptographicEngine::decodeAndDecryptPassword(pwHash, + createdHash, device.ManagementPassword).toByteArray()); + s.setIndividualAddress({ QKnxAddress::Type::Individual, device.IndividualAddress }); + s.setDeviceAuthenticationCode(QKnxCryptographicEngine::decodeAndDecryptPassword(pwHash, + createdHash, device.Authentication).toByteArray()); + return s; + } + + static QVector<QKnxNetIpSecureConfiguration> fromKeyring(QKnxNetIpSecureConfiguration::Type type, + const QKnxAddress &ia, const QString &filePath, const QByteArray &password, bool validate) + { + QFile file; + file.setFileName(filePath); + if (!file.open(QIODevice::ReadOnly)) + return {}; + + QXmlStreamReader reader(&file); + QKnx::Ets::Keyring::QKnxKeyring keyring; + + const auto pwHash = QKnxCryptographicEngine::keyringPasswordHash(password); + if (validate) { + if (!keyring.validate(&reader, pwHash)) + return {}; + file.seek(0); + reader.setDevice(&file); + } + + if (!keyring.parseElement(&reader, true)) + return {}; + const auto createdHash = QKnxCryptographicEngine::hashSha256(keyring.Created.toUtf8()); + + auto iaString = ia.toString(); + QVector<QKnxNetIpSecureConfiguration> results; + + if (type == QKnxNetIpSecureConfiguration::Type::Tunneling) { + if (keyring.Interface.isEmpty()) + return {}; + + if (!iaString.isEmpty()) { // only a single interface is requested + for (const auto iface : qAsConst(keyring.Interface)) { + if (iaString != iface.IndividualAddress) + continue; + return { QKnxPrivate::fromInterface(iface, pwHash, createdHash) }; + } + } else { + for (const auto iface : qAsConst(keyring.Interface)) + results.append(QKnxPrivate::fromInterface(iface, pwHash, createdHash)); + } + } + + if (type == QKnxNetIpSecureConfiguration::Type::DeviceManagement) { + if (keyring.Devices.isEmpty()) + return {}; + + const auto devices = keyring.Devices.value(0).Device; + if (!iaString.isEmpty()) { // only a single device is requested + for (const auto device : devices) { + if (iaString != device.IndividualAddress) + continue; + return { QKnxPrivate::fromDevice(device, pwHash, createdHash) }; + } + } else { + for (const auto device : devices) + results.append(QKnxPrivate::fromDevice(device, pwHash, createdHash)); + } + } + + return results; + } +} + +/*! + \since 5.13 + \inmodule QtKnx + + \class QKnxNetIpSecureConfiguration + \ingroup qtknx-general-classes + + \brief The QKnxNetIpSecureConfiguration class holds configuration + options used for the secure session authentication process. + + It holds information such as secure key, user ID and password, device + authentication code, and so on. + + This class is part of the Qt KNX module and currently available as a + Technology Preview, and therefore the API and functionality provided + by the class may be subject to change at any time without prior notice. +*/ + +/*! + \enum QKnxNetIpSecureConfiguration::Type + + This enum holds the type of secure configuration that can be constructed + from an ETS exported keyring (*.knxkeys) file. + + \value Tunneling + KNXnet/IP secure tunneling configuration. + \value DeviceManagement + KNXnet/IP secure device management configuration. +*/ + +/*! + Constructs a new, empty, invalid secure configuration. + + \sa isNull(), isValid() +*/ +QKnxNetIpSecureConfiguration::QKnxNetIpSecureConfiguration() + : d(new QKnxNetIpSecureConfigurationPrivate) +{} + +/*! + Releases any resources held by the secure configuration. +*/ +QKnxNetIpSecureConfiguration::~QKnxNetIpSecureConfiguration() = default; + +/*! + Constructs a vector of secure configurations for the given type + \a type from an ETS exported \a keyring (*.knxkeys) file that was + encrypted with the given password \a password. Set the \a validate + argument to \c true to verify that all data in the keyring file is + trustworthy, \c false to omit the check. + + \note If an error occurred, no or invalid information for \a type + was found in the keyring file, the returned vector can be empty. +*/ +QVector<QKnxNetIpSecureConfiguration> QKnxNetIpSecureConfiguration::fromKeyring(Type type, + const QString &keyring, const QByteArray &password, bool validate) +{ + return QKnxPrivate::fromKeyring(type, {}, keyring, password, validate); +} + +/*! + Constructs a secure configurations for the given type \a type and the + given individual address \a ia from an ETS exported \a keyring (*.knxkeys) + file that was encrypted with the given password \a password. + Set the \a validate argument to \c true to verify that all data in the + keyring file is trustworthy, \c false to omit the check. + + \note If an error occurred, no or invalid information for \a type or \a ia + was found in the keyring file; + the function returns a \l {default-constructed value} which can be invalid. +*/ +QKnxNetIpSecureConfiguration QKnxNetIpSecureConfiguration::fromKeyring(QKnxNetIpSecureConfiguration::Type type, + const QKnxAddress &ia, const QString &keyring, const QByteArray &password, bool validate) +{ + return QKnxPrivate::fromKeyring(type, ia, keyring, password, validate).value(0, {}); +} + +/*! + Returns \c true if this is a default constructed secure configuration; + otherwise returns \c false. A secure configuration is considered \c null + if it contains no initialized values. +*/ +bool QKnxNetIpSecureConfiguration::isNull() const +{ + return d->privateKey.isNull() && d->userId == QKnxNetIp::SecureUserId::Reserved + && d->userPassword.isNull() && d->deviceAuthenticationCode.isNull(); +} + +/*! + Returns \c true if the secure configuration contains initialized values and + is in itself valid, otherwise returns \c false. + + A valid secure configuration consists of at least a valid user ID, + a valid \l {QKnxSecureKey} {secure key}, and sensible device authentication + code. +*/ +bool QKnxNetIpSecureConfiguration::isValid() const +{ + if (isNull()) + return false; + + return d->privateKey.type() == QKnxSecureKey::Type::Private && d->privateKey.isValid() + && QKnxNetIp::isSecureUserId(d->userId) && (!d->deviceAuthenticationCode.isEmpty()); +} + +/*! + Returns the host address of the secure KNX device this secure configuration + targets. The host address can be empty and is not required to establish a + secure session. + + The purpose of this field is more to help GUI applications to indicate + which KNX secure device can be used with this secure configuration. +*/ +QKnxAddress QKnxNetIpSecureConfiguration::host() const +{ + return d->host; +} + +/*! + Sets the secure configurations host address to \a hostAddress. The host + address can be empty and is not required to establish a secure session. +*/ +void QKnxNetIpSecureConfiguration::setHost(const QKnxAddress &hostAddress) +{ + d->host = hostAddress; +} + +/*! + Returns the public \l {QKnxSecureKey} {secure key} used to establish the + secure session. The public key is derived from the given private key. +*/ +QKnxSecureKey QKnxNetIpSecureConfiguration::publicKey() const +{ + return d->publicKey; +} + +/*! + Returns the private \l {QKnxSecureKey} {secure key} used to establish the + secure session. +*/ +QKnxSecureKey QKnxNetIpSecureConfiguration::privateKey() const +{ + return d->privateKey; +} + +/*! + Set the \l {QKnxSecureKey} {secure key} used to establish the secure + connection to \a key and returns \c true on success; \c false otherwise. +*/ +bool QKnxNetIpSecureConfiguration::setPrivateKey(const QKnxSecureKey &key) +{ + auto valid = key.isValid() && key.type() == QKnxSecureKey::Type::Private; + if (valid) { + d->privateKey = key; + d->publicKey = QKnxSecureKey::publicKeyFromPrivate(key); + } + return valid; +} + +/*! + Returns the user ID used in the KNXnet/IP session authentication frame. +*/ +QKnxNetIp::SecureUserId QKnxNetIpSecureConfiguration::userId() const +{ + return d->userId; +} + +/*! + Sets the user ID used in the KNXnet/IP session authentication frame + to \a userId and returns \c true on success; \c false otherwise. + + \note A userId() with the value \c QKnxNetIp::SecureUserId::Reserved or + equal to or more than \c QKnxNetIp::SecureUserId::Invalid is considered + invalid according to the KNX application note AN159. +*/ +bool QKnxNetIpSecureConfiguration::setUserId(QKnxNetIp::SecureUserId userId) +{ + auto valid = QKnxNetIp::isSecureUserId(userId); + if (valid) + d->userId = userId; + return valid; +} + +/*! + Returns the user password used to authenticate the user while establishing + the secure session as an array of bytes. +*/ +QByteArray QKnxNetIpSecureConfiguration::userPassword() const +{ + return d->userPassword; +} + +/*! + Sets the user password to authenticate the user while establishing the + secure session to \a userPassword. Returns \c true on success; \c false + otherwise. +*/ +void QKnxNetIpSecureConfiguration::setUserPassword(const QByteArray &userPassword) +{ + d->userPassword = userPassword; +} + +/*! + Returns the requested individual address for the secure session. +*/ +QKnxAddress QKnxNetIpSecureConfiguration::individualAddress() const +{ + return d->ia; +} + +/*! + Sets the requested individual address of the secure session to \a address. + Returns \c true on success; \c false otherwise. + + \note To request any of the freely available addresses for the secure + session, or to reset the requested one, pass an invalid \a address to + the function. +*/ +bool QKnxNetIpSecureConfiguration::setIndividualAddress(const QKnxAddress &address) +{ + if ((address.type() == QKnxAddress::Type::Individual) || (!address.isValid())) + d->ia = address; + return d->ia == address; +} + +/*! + Returns the device authentication code to establish the secure session + as an array of bytes. +*/ +QByteArray QKnxNetIpSecureConfiguration::deviceAuthenticationCode() const +{ + return d->deviceAuthenticationCode; +} + +/*! + Sets the device authentication code used to establish the secure session + to \a authenticationCode. Returns \c true on success; \c false otherwise. + + \note The device authentication code cannot be empty. +*/ +bool QKnxNetIpSecureConfiguration::setDeviceAuthenticationCode(const QByteArray &authenticationCode) +{ + auto valid = !authenticationCode.isEmpty(); + if (valid) + d->deviceAuthenticationCode = authenticationCode; + return valid; +} + +/*! + Returns \c true if the keep alive flag is set; \c false otherwise. By + default this is set to \c false. +*/ +bool QKnxNetIpSecureConfiguration::isSecureSessionKeepAliveSet() const +{ + return d->keepAlive; +} + +/*! + Determines whether the connection should be kept alive. Set \a keepAlive to + \c true to keep a secure session alive even if there is no traffic for more + than \l {QKnx::NetIp::SecureSessionTimeout} {60 seconds}. +*/ +void QKnxNetIpSecureConfiguration::setKeepSecureSessionAlive(bool keepAlive) +{ + d->keepAlive = keepAlive; +} + +/*! + Constructs a copy of \a other. +*/ +QKnxNetIpSecureConfiguration::QKnxNetIpSecureConfiguration(const QKnxNetIpSecureConfiguration &other) + : d(other.d) +{} + +/*! + Assigns the specified \a other to this secure configuration. +*/ +QKnxNetIpSecureConfiguration &QKnxNetIpSecureConfiguration::operator=(const QKnxNetIpSecureConfiguration &other) +{ + d = other.d; + return *this; +} + +/*! + Move-constructs a secure configuration, making it point to the same secure + configuration that \a other was pointing to. +*/ +QKnxNetIpSecureConfiguration::QKnxNetIpSecureConfiguration(QKnxNetIpSecureConfiguration &&other) Q_DECL_NOTHROW + : d(other.d) +{ + other.d = nullptr; +} + +/*! + Move-assigns \a other to this secure configuration. +*/ +QKnxNetIpSecureConfiguration & + QKnxNetIpSecureConfiguration::operator=(QKnxNetIpSecureConfiguration &&other) Q_DECL_NOTHROW +{ + swap(other); + return *this; +} + +/*! + Swaps \a other with this secure configuration. This operation is very fast + and never fails. +*/ +void QKnxNetIpSecureConfiguration::swap(QKnxNetIpSecureConfiguration &other) Q_DECL_NOTHROW +{ + d.swap(other.d); +} + +/*! + Returns \c true if this secure configuration and the given \a other are + equal; otherwise returns \c false. +*/ +bool QKnxNetIpSecureConfiguration::operator==(const QKnxNetIpSecureConfiguration &other) const +{ + return d == other.d || (d->privateKey == other.d->privateKey + && d->publicKey == other.d->publicKey + && d->host == other.d->host + && d->userId == other.d->userId + && d->userPassword == other.d->userPassword + && d->ia == other.d->ia + && d->deviceAuthenticationCode == other.d->deviceAuthenticationCode + && d->keepAlive == other.d->keepAlive); +} + +/*! + Returns \c true if this secure configuration and the given \a other are not + equal; otherwise returns \c false. +*/ +bool QKnxNetIpSecureConfiguration::operator!=(const QKnxNetIpSecureConfiguration &other) const +{ + return !operator==(other); +} + +QT_END_NAMESPACE diff --git a/src/knx/netip/qknxnetipsecureconfiguration.h b/src/knx/netip/qknxnetipsecureconfiguration.h new file mode 100644 index 0000000..72b136a --- /dev/null +++ b/src/knx/netip/qknxnetipsecureconfiguration.h @@ -0,0 +1,106 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtKnx module. +** +** $QT_BEGIN_LICENSE:GPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 or (at your option) any later version +** approved by the KDE Free Qt Foundation. The licenses are as published by +** the Free Software Foundation and appearing in the file LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QKNXNETIPSECURECONFIGURATION_H +#define QKNXNETIPSECURECONFIGURATION_H + +#include <QtCore/qshareddata.h> + +#include <QtKnx/qknxaddress.h> +#include <QtKnx/qknxnetip.h> +#include <QtKnx/qknxsecurekey.h> + +QT_BEGIN_NAMESPACE + +class QKnxNetIpSecureConfigurationPrivate; +class Q_KNX_EXPORT QKnxNetIpSecureConfiguration +{ +public: + enum class Type : quint8 + { + Tunneling = 0x00, + DeviceManagement = 001 + }; + + QKnxNetIpSecureConfiguration(); + ~QKnxNetIpSecureConfiguration(); + + static QVector<QKnxNetIpSecureConfiguration> fromKeyring(QKnxNetIpSecureConfiguration::Type type, + const QString &keyring, const QByteArray &password, bool validate); + + static QKnxNetIpSecureConfiguration fromKeyring(QKnxNetIpSecureConfiguration::Type type, + const QKnxAddress &ia, const QString &keyring, const QByteArray &password, bool validate); + + bool isNull() const; + bool isValid() const; + + QKnxSecureKey publicKey() const; + + QKnxAddress host() const; + void setHost(const QKnxAddress &host); + + QKnxSecureKey privateKey() const; + bool setPrivateKey(const QKnxSecureKey &key); + + QKnxNetIp::SecureUserId userId() const; + bool setUserId(QKnxNetIp::SecureUserId userId); + + QByteArray userPassword() const; + void setUserPassword(const QByteArray &userPassword); + + QKnxAddress individualAddress() const; + bool setIndividualAddress(const QKnxAddress &address); + + QByteArray deviceAuthenticationCode() const; + bool setDeviceAuthenticationCode(const QByteArray &authenticationCode); + + bool isSecureSessionKeepAliveSet() const; + void setKeepSecureSessionAlive(bool keepAlive); + + QKnxNetIpSecureConfiguration(const QKnxNetIpSecureConfiguration &other); + QKnxNetIpSecureConfiguration &operator=(const QKnxNetIpSecureConfiguration &other); + + QKnxNetIpSecureConfiguration(QKnxNetIpSecureConfiguration &&other) Q_DECL_NOTHROW; + QKnxNetIpSecureConfiguration &operator=(QKnxNetIpSecureConfiguration &&other) Q_DECL_NOTHROW; + + void swap(QKnxNetIpSecureConfiguration &other) Q_DECL_NOTHROW; + + bool operator==(const QKnxNetIpSecureConfiguration &other) const; + bool operator!=(const QKnxNetIpSecureConfiguration &other) const; + +private: + friend class QKnxNetIpEndpointConnection; + friend class QKnxNetIpEndpointConnectionPrivate; + QSharedDataPointer<QKnxNetIpSecureConfigurationPrivate> d; +}; +Q_DECLARE_SHARED(QKnxNetIpSecureConfiguration) + +QT_END_NAMESPACE + +#endif diff --git a/src/knx/netip/qknxnetipsecureconfiguration_p.h b/src/knx/netip/qknxnetipsecureconfiguration_p.h new file mode 100644 index 0000000..1cdbd3f --- /dev/null +++ b/src/knx/netip/qknxnetipsecureconfiguration_p.h @@ -0,0 +1,68 @@ +/****************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtKnx module. +** +** $QT_BEGIN_LICENSE:GPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 or (at your option) any later version +** approved by the KDE Free Qt Foundation. The licenses are as published by +** the Free Software Foundation and appearing in the file LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +******************************************************************************/ + +#ifndef QKNXNETIPSECURECONFIGURATION_P_H +#define QKNXNETIPSECURECONFIGURATION_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt KNX API. It exists for the convenience +// of the Qt KNX implementation. This header file may change from version +// to version without notice, or even be removed. +// +// We mean it. +// + +#include <QtKnx/qknxaddress.h> +#include <QtKnx/qknxnetip.h> +#include <QtKnx/qknxsecurekey.h> + +QT_BEGIN_NAMESPACE + +class QKnxNetIpSecureConfigurationPrivate : public QSharedData +{ +public: + QKnxNetIpSecureConfigurationPrivate() = default; + ~QKnxNetIpSecureConfigurationPrivate() = default; + + QKnxSecureKey privateKey; + QKnxSecureKey publicKey; + QKnxAddress host; + QKnxNetIp::SecureUserId userId { QKnxNetIp::SecureUserId::Reserved }; + QByteArray userPassword; + QKnxAddress ia; + QByteArray deviceAuthenticationCode; + bool keepAlive { false }; +}; + +QT_END_NAMESPACE + +#endif diff --git a/src/knx/netip/qknxnetipsecurewrapper.cpp b/src/knx/netip/qknxnetipsecurewrapper.cpp index 0639eee..a100604 100644 --- a/src/knx/netip/qknxnetipsecurewrapper.cpp +++ b/src/knx/netip/qknxnetipsecurewrapper.cpp @@ -178,6 +178,14 @@ QKnxNetIpSecureWrapperProxy::Builder QKnxNetIpSecureWrapperProxy::builder() return QKnxNetIpSecureWrapperProxy::Builder(); } +/*! + Returns a builder object to create a KNXnet/IP secure wrapper frame. +*/ +QKnxNetIpSecureWrapperProxy::SecureBuilder QKnxNetIpSecureWrapperProxy::secureBuilder() +{ + return QKnxNetIpSecureWrapperProxy::SecureBuilder(); +} + /*! \class QKnxNetIpSecureWrapperProxy::Builder @@ -280,7 +288,8 @@ QKnxNetIpSecureWrapperProxy::Builder & This field contains an arbitrary value to differentiate two KNXnet/IP secure wrapper multicast frames sent by one KNXnet/IP device within the same millisecond (thus the same timer value). - For unicast connections this field is ignored and must be set to \c 0x0000. + + \note For unicast connections this field is ignored and must be set to \c 0x0000. */ QKnxNetIpSecureWrapperProxy::Builder & QKnxNetIpSecureWrapperProxy::Builder::setMessageTag(quint16 tag) @@ -330,7 +339,7 @@ QKnxNetIpFrame QKnxNetIpSecureWrapperProxy::Builder::create() const return { QKnxNetIp::ServiceType::SecureWrapper }; } - // TODO: introspect the frame and reject secure wrapper frames if possible at all + // TODO: introspect the frame and reject secure wrapper frames - if possible at all return { QKnxNetIp::ServiceType::SecureWrapper, QKnxUtils::QUint16::bytes(d_ptr->m_sessionId) + QKnxUtils::QUint48::bytes(d_ptr->m_seqNumber) + d_ptr->m_serial @@ -354,4 +363,200 @@ QKnxNetIpSecureWrapperProxy::Builder & return *this; } +/*! + \class QKnxNetIpSecureWrapperProxy::SecureBuilder + + \inmodule QtKnx + \inheaderfile QKnxNetIpSecureWrapperProxy + + \brief The QKnxNetIpSecureWrapperProxy::SecureBuilder class provides the + means to create a KNXnet/IP secure wrapper frame. + + This class is part of the Qt KNX module and currently available as a + Technology Preview, and therefore the API and functionality provided + by the class may be subject to change at any time without prior notice. + + The advantage of using this builder over the default one is that it can + take away the burden to do the encryption of the encapsulated frame and + the calculation the message authentication code (MAC). + + \note To use this class OpenSSL must be supported on your target system. + + This frame will be sent during secure KNXnet/IP communication and includes + a fully encrypted KNXnet/IP frame as well as information needed to decrypt + the encapsulated frame and for ensuring data integrity and freshness. + + The common way to create a secure wrapper frame is: + + \code + QKnxNetIpFrame frame = QKnxNetIpFrame::fromBytes(...); // create frame + + auto netIpFrame = QKnxNetIpSecureWrapperProxy::secureBuilder() + .setSequenceNumber(15021976) + .setSerialNumber(QKnxByteArray::fromHex("0123456789AB")) + .setEncapsulatedFrame(frame) + .create(sessionKey); + \endcode + + \sa QKnxCryptographicEngine +*/ + +/*! + Creates a new empty secure wrapper frame builder object. +*/ +QKnxNetIpSecureWrapperProxy::SecureBuilder::SecureBuilder() + : d_ptr(new QKnxNetIpSecureWrapperPrivate) +{} + +/*! + Destroys the object and frees any allocated resources. +*/ +QKnxNetIpSecureWrapperProxy::SecureBuilder::~SecureBuilder() = default; + +/*! + Sets the secure session ID to \a sessionId and returns a reference to the + builder. By default value is set to \c 0x0000. + + For multicast connections the fixed identifier \c 0x0000 must be used. For + unicast connections the ID was established during a previous successful + secure session setup procedure. +*/ +QKnxNetIpSecureWrapperProxy::SecureBuilder & + QKnxNetIpSecureWrapperProxy::SecureBuilder::setSecureSessionId(quint16 sessionId) +{ + d_ptr->m_sessionId = sessionId; + return *this; +} + +/*! + Sets the sequence number to \a seqNumber and returns a reference to the + builder. + + For unicast connections it is a monotonically increasing sequence number + assigned by the sender; incremented by the sender after each frame sent. + For multicast connections this is the device's current multicast timer + value in millisecond resolution. + + \note The size of a sequence number is limited to 48 bits, so the maximum + number can be \c 281474976710655. Passing a larger value will result in + creating an invalid frame. +*/ +QKnxNetIpSecureWrapperProxy::SecureBuilder & + QKnxNetIpSecureWrapperProxy::SecureBuilder::setSequenceNumber(quint48 seqNumber) +{ + d_ptr->m_seqNumber = seqNumber; + return *this; +} + +/*! + Sets the serial number to \a serialNumber of the device sending the frame + and returns a reference to the builder. + + \note The serial number must contain exactly 6 bytes. +*/ +QKnxNetIpSecureWrapperProxy::SecureBuilder & + QKnxNetIpSecureWrapperProxy::SecureBuilder::setSerialNumber(const QKnxByteArray &serialNumber) +{ + d_ptr->m_serial = serialNumber; + return *this; +} + +/*! + Sets the message tag of the generic KNXnet/IP secure wrapper frame to \a tag + and returns a reference to the builder. By default value is set to \c 0x0000. + + This field contains an arbitrary value to differentiate two KNXnet/IP + secure wrapper multicast frames sent by one KNXnet/IP device within the + same millisecond (thus the same timer value). + + \note For unicast connections this field is ignored and must be set to \c 0x0000. +*/ +QKnxNetIpSecureWrapperProxy::SecureBuilder & + QKnxNetIpSecureWrapperProxy::SecureBuilder::setMessageTag(quint16 tag) +{ + d_ptr->m_tag = tag; + return *this; +} + +/*! + Sets the encapsulated KNXnet/IP frame to \a frame and returns a reference + to the builder. + + \note A secure wrapper frame cannot be encapsulated in another secure + wrapper frame and will result in creating an invalid frame. +*/ +QKnxNetIpSecureWrapperProxy::SecureBuilder & + QKnxNetIpSecureWrapperProxy::SecureBuilder::setEncapsulatedFrame(const QKnxNetIpFrame &frame) +{ + if (frame.serviceType() != QKnxNetIp::ServiceType::SecureWrapper) + d_ptr->m_unencryptedFrame = frame; + return *this; +} + +/*! + Creates and returns a KNXnet/IP secure wrapper frame. During creation the + encapsulated frame gets encrypted and the corresponding MAC computed. + The given session key \a sessionKey takes part of the encryption process. + Both values, the encrypted frame and the MAC are appended to the KNXnet/IP + secure wrapper frame. + + \note The returned frame may be invalid depending on the values used during + setup. + + \sa isValid() +*/ +QKnxNetIpFrame + QKnxNetIpSecureWrapperProxy::SecureBuilder::create(const QKnxByteArray &sessionKey) const +{ +#if QT_CONFIG(opensslv11) + if (sessionKey.isEmpty() || d_ptr->m_seqNumber > Q_UINT48_MAX || d_ptr->m_serial.size() != 6 + || !d_ptr->m_unencryptedFrame.isValid()) { + return { QKnxNetIp::ServiceType::SecureWrapper }; + } + + auto encryptedFrame = QKnxCryptographicEngine::encryptSecureWrapperPayload(sessionKey, + d_ptr->m_unencryptedFrame, d_ptr->m_seqNumber, d_ptr->m_serial, d_ptr->m_tag); + if (encryptedFrame.isEmpty()) + return {}; + + auto builder = QKnxNetIpSecureWrapperProxy::builder(); + auto frame = builder + .setSecureSessionId(d_ptr->m_sessionId) + .setSequenceNumber(d_ptr->m_seqNumber) + .setSerialNumber(d_ptr->m_serial) + .setMessageTag(d_ptr->m_tag) + .setEncapsulatedFrame(encryptedFrame) + .setMessageAuthenticationCode(QKnxByteArray(16, 0x00)) // dummy MAC to get a proper header + .create(); + + auto mac = QKnxCryptographicEngine::computeMessageAuthenticationCode(sessionKey, + frame.header(), d_ptr->m_sessionId, d_ptr->m_unencryptedFrame.bytes(), d_ptr->m_seqNumber, + d_ptr->m_serial, d_ptr->m_tag); + mac = QKnxCryptographicEngine::encryptMessageAuthenticationCode(sessionKey, mac, + d_ptr->m_seqNumber, d_ptr->m_serial, d_ptr->m_tag); + + return builder.setMessageAuthenticationCode(mac).create(); +#else + Q_UNUSED(sessionKey) + return { QKnxNetIp::ServiceType::SecureWrapper }; +#endif +} + +/*! + Constructs a copy of \a other. +*/ +QKnxNetIpSecureWrapperProxy::SecureBuilder::SecureBuilder(const SecureBuilder &other) + : d_ptr(other.d_ptr) +{} + +/*! + Assigns the specified \a other to this object. +*/ +QKnxNetIpSecureWrapperProxy::SecureBuilder & + QKnxNetIpSecureWrapperProxy::SecureBuilder::operator=(const SecureBuilder &other) +{ + d_ptr = other.d_ptr; + return *this; +} + QT_END_NAMESPACE diff --git a/src/knx/netip/qknxnetipsecurewrapper.h b/src/knx/netip/qknxnetipsecurewrapper.h index a88c4de..16a6a66 100644 --- a/src/knx/netip/qknxnetipsecurewrapper.h +++ b/src/knx/netip/qknxnetipsecurewrapper.h @@ -77,6 +77,28 @@ public: }; static QKnxNetIpSecureWrapperProxy::Builder builder(); + class Q_KNX_EXPORT SecureBuilder final + { + public: + SecureBuilder(); + ~SecureBuilder(); + + SecureBuilder &setSecureSessionId(quint16 sessionId); + SecureBuilder &setSequenceNumber(quint48 seqNumber); + SecureBuilder &setSerialNumber(const QKnxByteArray &serialNumber); + SecureBuilder &setMessageTag(quint16 tag); + SecureBuilder &setEncapsulatedFrame(const QKnxNetIpFrame &frame); + + QKnxNetIpFrame create(const QKnxByteArray &sessionKey) const; + + SecureBuilder(const SecureBuilder &other); + SecureBuilder &operator=(const SecureBuilder &other); + + private: + QSharedDataPointer<QKnxNetIpSecureWrapperPrivate> d_ptr; + }; + static QKnxNetIpSecureWrapperProxy::SecureBuilder secureBuilder(); + private: const QKnxNetIpFrame &m_frame; }; diff --git a/src/knx/netip/qknxnetipserverdescriptionagent.cpp b/src/knx/netip/qknxnetipserverdescriptionagent.cpp index 96532ef..5735ed7 100644 --- a/src/knx/netip/qknxnetipserverdescriptionagent.cpp +++ b/src/knx/netip/qknxnetipserverdescriptionagent.cpp @@ -41,15 +41,18 @@ QT_BEGIN_NAMESPACE \brief The QKnxNetIpServerDescriptionAgent class establishes a point-to-point connection with a KNXnet/IP server and requests its description. - First, the \l QKnxNetIpServerDiscoveryAgent class is used to choose a - server, as illustrated by the following code snippet: + First, the \l QKnxNetIpServerDiscoveryAgent class is used to search the + network for available KNXnet/IP devices. After successful discovery of + such a device, the server needs to be chosen to fetch the description. + This can be been done as illustrated by the following code snippet: \code - QKnxNetIpServerDescriptionAgent agent; - QKnxAddress clientLocalAddress = ... - agent.setLocalAddress(clientLocalAddress); - QKnxNetIpServerInfo server = agent.discoveredServers()[0]; // for example - agent.start(server); + QKnxNetIpServerDescriptionAgent descriptionAgent; + QHostAddress clientLocalAddress = ... + descriptionAgent.setLocalAddress(clientLocalAddress); + + auto server = discoveryAgent.discoveredServers()[0]; // for example + descriptionAgent.start(server); \endcode When the description is received from the server, the @@ -86,6 +89,8 @@ QT_BEGIN_NAMESPACE A network error occurred. \value NotIPv4 The network protocol used is not IPv4. + \value Timeout + A timeout occurred while waiting for the description response. \value Unknown An unknown error occurred. */ @@ -145,6 +150,7 @@ namespace QKnxPrivate void QKnxNetIpServerDescriptionAgentPrivate::setupSocket() { usedPort = port; + timeoutHit = true; QKnxPrivate::clearSocket(&socket); Q_Q(QKnxNetIpServerDescriptionAgent); @@ -255,6 +261,7 @@ void QKnxNetIpServerDescriptionAgentPrivate::setAndEmitStateChanged( void QKnxNetIpServerDescriptionAgentPrivate::setAndEmitServerDescriptionReceived( const QKnxNetIpServerInfo &description) { + timeoutHit = false; m_description = description; Q_Q(QKnxNetIpServerDescriptionAgent); @@ -301,8 +308,8 @@ QKnxNetIpServerDescriptionAgent::QKnxNetIpServerDescriptionAgent(const QHostAddr Creates a KNXnet/IP server description agent with the host address \a localAddress, the port number \a port, and the parent \a parent. - \note If the port number is already bound by a different process, discovery - will fail. + \note If the port number is already bound by a different process, + description requests will fail. */ QKnxNetIpServerDescriptionAgent::QKnxNetIpServerDescriptionAgent(const QHostAddress &localAddress, quint16 port, QObject *parent) @@ -361,8 +368,8 @@ quint16 QKnxNetIpServerDescriptionAgent::localPort() const /*! Sets the port number used by a description agent to \a port. - \note If the port changes during discovery, the new port will not be used - until the next run. + \note If the port changes during a description request, the new port will + not be used until the next run. */ void QKnxNetIpServerDescriptionAgent::setLocalPort(quint16 port) { @@ -383,8 +390,8 @@ QHostAddress QKnxNetIpServerDescriptionAgent::localAddress() const /*! Sets the host address of a description agent to \a address. - \note If the address changes during discovery, the new address will not be - used until the next run. + \note If the address changes during a description request, the new address + will not be used until the next run. */ void QKnxNetIpServerDescriptionAgent::setLocalAddress(const QHostAddress &address) { @@ -430,10 +437,10 @@ bool QKnxNetIpServerDescriptionAgent::natAware() const } /*! - Sets whether the server discovery agent is using NAT to \a useNat. + Sets whether the server description agent is using NAT to \a useNat. - \note If the setting changes during discovery, it will not be used until the - next run. + \note If the setting changes during a description request, it will not be + used until the next run. */ void QKnxNetIpServerDescriptionAgent::setNatAware(bool useNat) { @@ -494,7 +501,7 @@ void QKnxNetIpServerDescriptionAgent::start(const QKnxNetIpHpai &server) */ void QKnxNetIpServerDescriptionAgent::start(const QKnxNetIpServerInfo &server) { - start(server.controlEndpointAddress(), server.controlEndpointPort()); + start(server.endpoint()); } /*! @@ -523,6 +530,11 @@ void QKnxNetIpServerDescriptionAgent::stop() QKnxPrivate::clearSocket(&(d->socket)); QKnxPrivate::clearTimer(&(d->receiveTimer)); + if (d->timeoutHit) { + emit d->setAndEmitErrorOccurred(Error::Timeout, tr("A timeout occurred while waiting for " + "the description response.")); + } + d->setAndEmitStateChanged(QKnxNetIpServerDescriptionAgent::State::NotRunning); } diff --git a/src/knx/netip/qknxnetipserverdescriptionagent.h b/src/knx/netip/qknxnetipserverdescriptionagent.h index 292db19..917945d 100644 --- a/src/knx/netip/qknxnetipserverdescriptionagent.h +++ b/src/knx/netip/qknxnetipserverdescriptionagent.h @@ -60,6 +60,7 @@ public: None, Network, NotIPv4, + Timeout, Unknown = 0x80 }; Q_ENUM(Error) diff --git a/src/knx/netip/qknxnetipserverdescriptionagent_p.h b/src/knx/netip/qknxnetipserverdescriptionagent_p.h index 9d63e8c..bc50399 100644 --- a/src/knx/netip/qknxnetipserverdescriptionagent_p.h +++ b/src/knx/netip/qknxnetipserverdescriptionagent_p.h @@ -81,6 +81,8 @@ private: quint8 ttl { 60 }; bool nat { false }; + + bool timeoutHit { true }; int timeout { QKnxNetIp::Timeout::DescriptionTimeout }; QString errorString; diff --git a/src/knx/netip/qknxnetipserverdiscoveryagent.cpp b/src/knx/netip/qknxnetipserverdiscoveryagent.cpp index 0fdc126..30c3d1d 100644 --- a/src/knx/netip/qknxnetipserverdiscoveryagent.cpp +++ b/src/knx/netip/qknxnetipserverdiscoveryagent.cpp @@ -390,7 +390,7 @@ void QKnxNetIpServerDiscoveryAgentPrivate::setupAndStartFrequencyTimer() frequencyTimer->setSingleShot(false); frequencyTimer->start(60000 / frequency); - QObject::connect(receiveTimer, &QTimer::timeout, [&]() { + QObject::connect(frequencyTimer, &QTimer::timeout, [&]() { Q_Q(QKnxNetIpServerDiscoveryAgent); if (q->state() == QKnxNetIpServerDiscoveryAgent::State::Running) { servers.clear(); diff --git a/src/knx/netip/qknxnetipservicefamiliesdib.cpp b/src/knx/netip/qknxnetipservicefamiliesdib.cpp index 6394d47..5c95f5a 100644 --- a/src/knx/netip/qknxnetipservicefamiliesdib.cpp +++ b/src/knx/netip/qknxnetipservicefamiliesdib.cpp @@ -67,6 +67,29 @@ QT_BEGIN_NAMESPACE */ /*! + \since 5.13 + + Returns \c true if this object and the given \a other are equal; otherwise + returns \c false. +*/ +bool QKnxServiceInfo::operator==(const QKnxServiceInfo &other) const +{ + return ServiceFamily == other.ServiceFamily && ServiceFamilyVersion == other.ServiceFamilyVersion; +} + +/*! + \since 5.13 + + Returns \c true if this object and the given \a other are not equal; + otherwise returns \c false. +*/ +bool QKnxServiceInfo::operator!=(const QKnxServiceInfo &other) const +{ + return !operator==(other); +} + + +/*! \class QKnxNetIpServiceFamiliesDibProxy \inmodule QtKnx diff --git a/src/knx/netip/qknxnetipservicefamiliesdib.h b/src/knx/netip/qknxnetipservicefamiliesdib.h index c686d27..1108208 100644 --- a/src/knx/netip/qknxnetipservicefamiliesdib.h +++ b/src/knx/netip/qknxnetipservicefamiliesdib.h @@ -39,6 +39,9 @@ struct Q_KNX_EXPORT QKnxServiceInfo { QKnxNetIp::ServiceFamily ServiceFamily; quint8 ServiceFamilyVersion; + + bool operator==(const QKnxServiceInfo &other) const; + bool operator!=(const QKnxServiceInfo &other) const; }; class Q_KNX_EXPORT QKnxNetIpServiceFamiliesDibProxy final diff --git a/src/knx/netip/qknxnetipsessionauthenticate.cpp b/src/knx/netip/qknxnetipsessionauthenticate.cpp index 12e38b2..70d79b9 100644 --- a/src/knx/netip/qknxnetipsessionauthenticate.cpp +++ b/src/knx/netip/qknxnetipsessionauthenticate.cpp @@ -27,6 +27,7 @@ ** ******************************************************************************/ +#include "qknxbuilderdata_p.h" #include "qknxnetipsessionauthenticate.h" #include "qknxutils.h" @@ -104,8 +105,9 @@ QKnxNetIpSessionAuthenticateProxy::QKnxNetIpSessionAuthenticateProxy(const QKnxN at least a valid header and a size in bytes corresponding to the total size of the KNXnet/IP frame header. - \note A userId() with the value \c 0x0000 or a value above \c 0x0080 - is considered invalid according to the KNX application note AN159. + \note A userId() with the value \c QKnxNetIp::SecureUserId::Reserved or + equal to or more than \c QKnxNetIp::SecureUserId::Invalid is considered + invalid according to the KNX application note AN159. \note KNXnet/IP session authentication frames currently have a fixed size of \c 24 bytes. @@ -114,19 +116,16 @@ QKnxNetIpSessionAuthenticateProxy::QKnxNetIpSessionAuthenticateProxy(const QKnxN */ bool QKnxNetIpSessionAuthenticateProxy::isValid() const { - const auto id = userId(); return m_frame.isValid() && m_frame.serviceType() == QKnxNetIp::ServiceType::SessionAuthenticate - && m_frame.size() == 24 && id > 0 && id < 0x80; // 0x00 and values above 0x80 are reserved + && m_frame.size() == 24 && QKnxNetIp::isSecureUserId(userId()); } /*! - Returns the user ID from the generic KNXnet/IP session authentication - frame. The value of \c 0 is a reserved value and indicates an invalid - user ID. + Returns the user ID from the generic KNXnet/IP session authentication frame. */ -quint8 QKnxNetIpSessionAuthenticateProxy::userId() const +QKnxNetIp::SecureUserId QKnxNetIpSessionAuthenticateProxy::userId() const { - return m_frame.constData().value(1); + return static_cast<QKnxNetIp::SecureUserId> (m_frame.constData().value(1)); } /*! @@ -135,7 +134,7 @@ quint8 QKnxNetIpSessionAuthenticateProxy::userId() const */ QKnxByteArray QKnxNetIpSessionAuthenticateProxy::messageAuthenticationCode() const { - return m_frame.constData().mid(sizeof(quint16) + 32); + return m_frame.constData().mid(sizeof(quint16)); } /*! @@ -169,11 +168,10 @@ QKnxNetIpSessionAuthenticateProxy::Builder QKnxNetIpSessionAuthenticateProxy::bu The common way to create a session authentication frame is: \code - quint16 mgmtLevelAccess = 0x0001; auto auth = ... // create the full 128 bit CCM-MAC auto netIpFrame = QKnxNetIpSessionAuthenticateProxy::builder() - .setUserId(mgmtLevelAccess) + .setUserId(QKnxNetIp::SecureUserId::Management) .setMessageAuthenticationCode(auth) .create(); \endcode @@ -181,18 +179,8 @@ QKnxNetIpSessionAuthenticateProxy::Builder QKnxNetIpSessionAuthenticateProxy::bu \sa QKnxCryptographicEngine */ -class QKnxNetIpSessionAuthenticateBuilderPrivate : public QSharedData -{ -public: - QKnxNetIpSessionAuthenticateBuilderPrivate() = default; - ~QKnxNetIpSessionAuthenticateBuilderPrivate() = default; - - quint16 m_id { 0 }; - QKnxByteArray m_authCode; -}; - /*! - Creates a new empty session authenticate frame builder. + Creates a new empty session authentication frame builder. */ QKnxNetIpSessionAuthenticateProxy::Builder::Builder() : d_ptr(new QKnxNetIpSessionAuthenticateBuilderPrivate) @@ -207,11 +195,12 @@ QKnxNetIpSessionAuthenticateProxy::Builder::~Builder() = default; Sets the user ID of the KNXnet/IP session authentication frame to \a userId and returns a reference to the builder. - \note A userId() with the value \c 0x0000 or a value above \c 0x0080 - is considered invalid according to the KNX application note AN159. + \note A userId() with the value \c QKnxNetIp::SecureUserId::Reserved or + equal to or more than \c QKnxNetIp::SecureUserId::Invalid is considered + invalid according to the KNX application note AN159. */ QKnxNetIpSessionAuthenticateProxy::Builder & - QKnxNetIpSessionAuthenticateProxy::Builder::setUserId(quint16 userId) + QKnxNetIpSessionAuthenticateProxy::Builder::setUserId(QKnxNetIp::SecureUserId userId) { d_ptr->m_id = userId; return *this; @@ -263,4 +252,131 @@ QKnxNetIpSessionAuthenticateProxy::Builder & return *this; } + +/*! + \class QKnxNetIpSessionAuthenticateProxy::SecureBuilder + + \inmodule QtKnx + \inheaderfile QKnxNetIpSessionAuthenticateProxy + + \brief The QKnxNetIpSessionAuthenticateProxy::SecureBuilder class provides the + means to create a KNXnet/IP session authentication frame. + + This class is part of the Qt KNX module and currently available as a + Technology Preview, and therefore the API and functionality provided + by the class may be subject to change at any time without prior notice. + + \note To use this class OpenSSL must be supported on your target system. + + This frame will be sent by the KNXnet/IP secure client to the control + endpoint of the KNXnet/IP secure server after the Diffie-Hellman handshake + to authenticate the user against the server device. + The maximum time a KNXnet/IP secure client will wait for an authentication + status response of the KNXnet/IP secure server is 10 seconds. + + The common way to create a session authentication frame is: + + \code + auto netIpFrame = QKnxNetIpSessionAuthenticateProxy::secureBuilder() + .setUserId(QKnxNetIp::SecureUserId::Management) + .create(passwordHash, clientKey, serverKey); + \endcode + + \sa QKnxCryptographicEngine +*/ + +/*! + Creates a new empty session authentication frame builder. +*/ +QKnxNetIpSessionAuthenticateProxy::SecureBuilder::SecureBuilder() + : d_ptr(new QKnxNetIpSessionAuthenticateBuilderPrivate) +{} + +/*! + Destroys the object and frees any allocated resources. +*/ +QKnxNetIpSessionAuthenticateProxy::SecureBuilder::~SecureBuilder() = default; + +/*! + Sets the user ID of the KNXnet/IP session authentication frame to \a userId + and returns a reference to the builder. + + \note A userId() with the value \c QKnxNetIp::SecureUserId::Reserved or + equal to or more than \c QKnxNetIp::SecureUserId::Invalid is considered + invalid according to the KNX application note AN159. +*/ +QKnxNetIpSessionAuthenticateProxy::SecureBuilder & + QKnxNetIpSessionAuthenticateProxy::SecureBuilder::setUserId(QKnxNetIp::SecureUserId userId) +{ + d_ptr->m_id = userId; + return *this; +} + +/*! + Creates and returns a KNXnet/IP session authentication frame. + + The function computes the AES128 CCM message authentication code (MAC) + with the given user session password \a sessionPassword, the Curve25519 + client public key \a clientPublicKey, the Curve25519 server public key + \a serverPublicKey and appends it to the newly created frame. + + \note The returned frame may be invalid depending on the values used during + setup. + + \sa isValid() +*/ +QKnxNetIpFrame QKnxNetIpSessionAuthenticateProxy::SecureBuilder::create( + const QByteArray &sessionPassword, + const QKnxByteArray &clientPublicKey, + const QKnxByteArray &serverPublicKey) const +{ +#if QT_CONFIG(opensslv11) + if (!QKnxNetIp::isSecureUserId(d_ptr->m_id)) + return { QKnxNetIp::ServiceType::SessionAuthenticate }; + + auto builder = QKnxNetIpSessionAuthenticateProxy::builder(); + auto frame = builder + .setUserId(d_ptr->m_id) + .setMessageAuthenticationCode(QKnxByteArray(16, 0x00)) // dummy MAC to get a proper header + .create(); + + auto userPasswordHash = QKnxCryptographicEngine::userPasswordHash(sessionPassword); + auto mac = QKnxCryptographicEngine::computeMessageAuthenticationCode(userPasswordHash, frame. + header(), d_ptr->m_id, QKnxCryptographicEngine::XOR(clientPublicKey, serverPublicKey)); + mac = QKnxCryptographicEngine::encryptMessageAuthenticationCode(userPasswordHash, mac); + + return builder.setMessageAuthenticationCode(mac).create(); +#else + Q_UNUSED(sessionPassword) + Q_UNUSED(clientPublicKey) + Q_UNUSED(serverPublicKey) + return { QKnxNetIp::ServiceType::SessionAuthenticate }; +#endif +} + +/*! + Constructs a copy of \a other. +*/ +QKnxNetIpSessionAuthenticateProxy::SecureBuilder::SecureBuilder(const SecureBuilder &other) + : d_ptr(other.d_ptr) +{} + +/*! + Assigns the specified \a other to this object. +*/ +QKnxNetIpSessionAuthenticateProxy::SecureBuilder & + QKnxNetIpSessionAuthenticateProxy::SecureBuilder::operator=(const SecureBuilder &other) +{ + d_ptr = other.d_ptr; + return *this; +} + +/*! + Returns a builder object to create a KNXnet/IP session authentication frame. +*/ +QKnxNetIpSessionAuthenticateProxy::SecureBuilder QKnxNetIpSessionAuthenticateProxy::secureBuilder() +{ + return QKnxNetIpSessionAuthenticateProxy::SecureBuilder(); +} + QT_END_NAMESPACE diff --git a/src/knx/netip/qknxnetipsessionauthenticate.h b/src/knx/netip/qknxnetipsessionauthenticate.h index 97884fd..23d5d71 100644 --- a/src/knx/netip/qknxnetipsessionauthenticate.h +++ b/src/knx/netip/qknxnetipsessionauthenticate.h @@ -47,7 +47,7 @@ public: bool isValid() const; - quint8 userId() const; + QKnxNetIp::SecureUserId userId() const; QKnxByteArray messageAuthenticationCode() const; class Q_KNX_EXPORT Builder final @@ -56,7 +56,7 @@ public: Builder(); ~Builder(); - Builder &setUserId(quint16 userId); + Builder &setUserId(QKnxNetIp::SecureUserId userId); Builder &setMessageAuthenticationCode(const QKnxByteArray &data); QKnxNetIpFrame create() const; @@ -69,6 +69,26 @@ public: }; static QKnxNetIpSessionAuthenticateProxy::Builder builder(); + class Q_KNX_EXPORT SecureBuilder final + { + public: + SecureBuilder(); + ~SecureBuilder(); + + SecureBuilder &setUserId(QKnxNetIp::SecureUserId userId); + + QKnxNetIpFrame create(const QByteArray &sessionPassword, + const QKnxByteArray &clientPublicKey, + const QKnxByteArray &serverPublicKey) const; + + SecureBuilder(const SecureBuilder &other); + SecureBuilder &operator=(const SecureBuilder &other); + + private: + QSharedDataPointer<QKnxNetIpSessionAuthenticateBuilderPrivate> d_ptr; + }; + static QKnxNetIpSessionAuthenticateProxy::SecureBuilder secureBuilder(); + private: const QKnxNetIpFrame &m_frame; }; diff --git a/src/knx/netip/qknxnetipsessionrequest.cpp b/src/knx/netip/qknxnetipsessionrequest.cpp index 2f28860..3f1ed6d 100644 --- a/src/knx/netip/qknxnetipsessionrequest.cpp +++ b/src/knx/netip/qknxnetipsessionrequest.cpp @@ -185,7 +185,7 @@ public: ~QKnxNetIpSessionRequestBuilderPrivate() = default; QKnxNetIpHpai m_hpai; - QKnxByteArray m_publicKey; + QKnxByteArray m_serverPublicKey; }; /*! @@ -220,7 +220,7 @@ QKnxNetIpSessionRequestProxy::Builder & QKnxNetIpSessionRequestProxy::Builder & QKnxNetIpSessionRequestProxy::Builder::setPublicKey(const QKnxByteArray &publicKey) { - d_ptr->m_publicKey = publicKey; + d_ptr->m_serverPublicKey = publicKey; return *this; } @@ -234,8 +234,8 @@ QKnxNetIpSessionRequestProxy::Builder & */ QKnxNetIpFrame QKnxNetIpSessionRequestProxy::Builder::create() const { - if (d_ptr->m_hpai.isValid() && d_ptr->m_publicKey.size() == 32) - return { QKnxNetIp::ServiceType::SessionRequest, d_ptr->m_hpai.bytes() + d_ptr->m_publicKey }; + if (d_ptr->m_hpai.isValid() && d_ptr->m_serverPublicKey.size() == 32) + return { QKnxNetIp::ServiceType::SessionRequest, d_ptr->m_hpai.bytes() + d_ptr->m_serverPublicKey }; return { QKnxNetIp::ServiceType::SessionRequest }; } diff --git a/src/knx/netip/qknxnetipsessionresponse.cpp b/src/knx/netip/qknxnetipsessionresponse.cpp index 341aea3..a7f0e83 100644 --- a/src/knx/netip/qknxnetipsessionresponse.cpp +++ b/src/knx/netip/qknxnetipsessionresponse.cpp @@ -27,6 +27,7 @@ ** ******************************************************************************/ +#include "qknxbuilderdata_p.h" #include "qknxnetipsessionresponse.h" QT_BEGIN_NAMESPACE @@ -66,7 +67,7 @@ QT_BEGIN_NAMESPACE return; auto endPoint = proxy.controlEndpoint(); - auto publicKey = proxy.publicKey(); + auto serverPublicKey = proxy.publicKey(); auto authenticationCode = proxy.messageAuthenticationCode(); \endcode @@ -171,12 +172,12 @@ QKnxNetIpSessionResponseProxy::Builder QKnxNetIpSessionResponseProxy::builder() The common way to create a session response frame is: \code - auto publicKey = ... // create the public key + auto serverPublicKey = ... // create the public key auto auth = ... // create the full 128 bit CCM-MAC auto netIpFrame = QKnxNetIpSessionResponseProxy::builder() .setSecureSessionId(0x1976) - .setPublicKey(publicKey) + .setPublicKey(serverPublicKey) .setMessageAuthenticationCode(auth) .create(); \endcode @@ -184,17 +185,6 @@ QKnxNetIpSessionResponseProxy::Builder QKnxNetIpSessionResponseProxy::builder() \sa QKnxCryptographicEngine */ -class QKnxNetIpSessionResponseBuilderPrivate : public QSharedData -{ -public: - QKnxNetIpSessionResponseBuilderPrivate() = default; - ~QKnxNetIpSessionResponseBuilderPrivate() = default; - - qint32 m_id { -1 }; - QKnxByteArray m_publicKey; - QKnxByteArray m_authCode; -}; - /*! Creates a new empty session response builder object. */ @@ -219,14 +209,15 @@ QKnxNetIpSessionResponseProxy::Builder & } /*! - Sets the public key of the KNXnet/IP session response frame to \a publicKey - and returns a reference to the builder. The public key needs to be generated - using the Curve25519 algorithm and has a fixed size of \c 32 bytes. + Sets the public key of the KNXnet/IP session response frame to + \a serverPublicKey and returns a reference to the builder. The public key + needs to be generated using the Curve25519 algorithm and has a fixed size + of \c 32 bytes. */ QKnxNetIpSessionResponseProxy::Builder & - QKnxNetIpSessionResponseProxy::Builder::setPublicKey(const QKnxByteArray &publicKey) + QKnxNetIpSessionResponseProxy::Builder::setPublicKey(const QKnxByteArray &serverPublicKey) { - d_ptr->m_publicKey = publicKey; + d_ptr->m_serverPublicKey = serverPublicKey; return *this; } @@ -252,11 +243,11 @@ QKnxNetIpSessionResponseProxy::Builder & */ QKnxNetIpFrame QKnxNetIpSessionResponseProxy::Builder::create() const { - if (d_ptr->m_id < 0 || d_ptr->m_publicKey.size() != 32 || d_ptr->m_authCode.size() != 16) + if (d_ptr->m_id < 0 || d_ptr->m_serverPublicKey.size() != 32 || d_ptr->m_authCode.size() != 16) return { QKnxNetIp::ServiceType::SessionResponse }; return { QKnxNetIp::ServiceType::SessionResponse, QKnxUtils::QUint16::bytes(d_ptr->m_id) - + d_ptr->m_publicKey + d_ptr->m_authCode }; + + d_ptr->m_serverPublicKey + d_ptr->m_authCode }; } /*! @@ -276,4 +267,142 @@ QKnxNetIpSessionResponseProxy::Builder & return *this; } + +/*! + \class QKnxNetIpSessionResponseProxy::SecureBuilder + + \inmodule QtKnx + \inheaderfile QKnxNetIpSessionResponseProxy + + \brief The QKnxNetIpSessionResponseProxy::SecureBuilder class provides the + means to create a KNXnet/IP session response frame. + + This class is part of the Qt KNX module and currently available as a + Technology Preview, and therefore the API and functionality provided + by the class may be subject to change at any time without prior notice. + + \note To use this class OpenSSL must be supported on your target system. + + This frame will be sent by the KNXnet/IP secure server to the KNXnet/IP + secure client control endpoint in response to a received secure session + request frame. + + The common way to create a session response frame is: + + \code + auto serverPublicKey = ... // create the public key + auto auth = ... // create the full 128 bit CCM-MAC + + auto netIpFrame = QKnxNetIpSessionResponseProxy::secureBuilder() + .setSecureSessionId(0x1976) + .setPublicKey(serverPublicKey) + .create(); + \endcode + + \sa QKnxCryptographicEngine +*/ + +/*! + Creates a new empty session response builder object. +*/ +QKnxNetIpSessionResponseProxy::SecureBuilder::SecureBuilder() + : d_ptr(new QKnxNetIpSessionResponseBuilderPrivate) +{} + +/*! + Destroys the object and frees any allocated resources. +*/ +QKnxNetIpSessionResponseProxy::SecureBuilder::~SecureBuilder() = default; + +/*! + Sets the secure session ID of the KNXnet/IP session response frame to + \a sessionId and returns a reference to the builder. +*/ +QKnxNetIpSessionResponseProxy::SecureBuilder & + QKnxNetIpSessionResponseProxy::SecureBuilder::setSecureSessionId(quint16 sessionId) +{ + d_ptr->m_id = sessionId; + return *this; +} + +/*! + Sets the public key of the KNXnet/IP session response frame to + \a serverPublicKey and returns a reference to the builder. The public key + needs to be generated using the Curve25519 algorithm and has a fixed size + of \c 32 bytes. +*/ +QKnxNetIpSessionResponseProxy::SecureBuilder & + QKnxNetIpSessionResponseProxy::SecureBuilder::setPublicKey(const QKnxByteArray &serverPublicKey) +{ + d_ptr->m_serverPublicKey = serverPublicKey; + return *this; +} + +/*! + Creates and returns a KNXnet/IP session response frame. + + The function computes the AES128 CCM message authentication code (MAC) + with the given device password \a devicePassword and the Curve25519 client + public key \a clientPublicKey and appends it to the newly created frame. + + \note The returned frame may be invalid depending on the values used during + setup. + + \sa isValid() +*/ +QKnxNetIpFrame QKnxNetIpSessionResponseProxy::SecureBuilder::create(const QByteArray &devicePassword, + const QKnxByteArray &clientPublicKey) const +{ +#if QT_CONFIG(opensslv11) + if (d_ptr->m_id < 0 || clientPublicKey.size() != 32 || d_ptr->m_serverPublicKey.size() != 32) + return { QKnxNetIp::ServiceType::SessionResponse }; + + auto deviceAuthenticationCode = + QKnxCryptographicEngine::deviceAuthenticationCodeHash(devicePassword); + auto XOR_X_Y = QKnxCryptographicEngine::XOR(clientPublicKey, d_ptr->m_serverPublicKey); + + auto builder = QKnxNetIpSessionResponseProxy::builder(); + auto frame = builder + .setSecureSessionId(d_ptr->m_id) + .setPublicKey(d_ptr->m_serverPublicKey) + .setMessageAuthenticationCode(QKnxByteArray(16, 0x00)) // dummy MAC to get a proper header + .create(); + + auto mac = QKnxCryptographicEngine::computeMessageAuthenticationCode(deviceAuthenticationCode, + frame.header(), d_ptr->m_id, XOR_X_Y); + mac = QKnxCryptographicEngine::encryptMessageAuthenticationCode(deviceAuthenticationCode, mac); + + return builder.setMessageAuthenticationCode(mac).create(); +#else + Q_UNUSED(devicePassword) + Q_UNUSED(clientPublicKey) + return { QKnxNetIp::ServiceType::SessionResponse }; +#endif +} + +/*! + Constructs a copy of \a other. +*/ +QKnxNetIpSessionResponseProxy::SecureBuilder::SecureBuilder(const SecureBuilder &other) + : d_ptr(other.d_ptr) +{} + +/*! + Assigns the specified \a other to this object. +*/ +QKnxNetIpSessionResponseProxy::SecureBuilder & + QKnxNetIpSessionResponseProxy::SecureBuilder::operator=(const SecureBuilder &other) +{ + d_ptr = other.d_ptr; + return *this; +} + +/*! + Returns a builder object to create a KNXnet/IP session response frame. +*/ +QKnxNetIpSessionResponseProxy::SecureBuilder QKnxNetIpSessionResponseProxy::secureBuilder() +{ + return QKnxNetIpSessionResponseProxy::SecureBuilder(); +} + QT_END_NAMESPACE diff --git a/src/knx/netip/qknxnetipsessionresponse.h b/src/knx/netip/qknxnetipsessionresponse.h index c2eecf6..c6a5545 100644 --- a/src/knx/netip/qknxnetipsessionresponse.h +++ b/src/knx/netip/qknxnetipsessionresponse.h @@ -59,7 +59,7 @@ public: ~Builder(); Builder &setSecureSessionId(quint16 sessionId); - Builder &setPublicKey(const QKnxByteArray &publicKey); + Builder &setPublicKey(const QKnxByteArray &serverPublicKey); Builder &setMessageAuthenticationCode(const QKnxByteArray &data); QKnxNetIpFrame create() const; @@ -72,6 +72,26 @@ public: }; static QKnxNetIpSessionResponseProxy::Builder builder(); + class Q_KNX_EXPORT SecureBuilder final + { + public: + SecureBuilder(); + ~SecureBuilder(); + + SecureBuilder &setSecureSessionId(quint16 sessionId); + SecureBuilder &setPublicKey(const QKnxByteArray &serverPublicKey); + + QKnxNetIpFrame create(const QByteArray &devicePassword, + const QKnxByteArray &clientPublicKey) const; + + SecureBuilder(const SecureBuilder &other); + SecureBuilder &operator=(const SecureBuilder &other); + + private: + QSharedDataPointer<QKnxNetIpSessionResponseBuilderPrivate> d_ptr; + }; + static QKnxNetIpSessionResponseProxy::SecureBuilder secureBuilder(); + private: const QKnxNetIpFrame &m_frame; }; diff --git a/src/knx/netip/qknxnetiptimernotify.cpp b/src/knx/netip/qknxnetiptimernotify.cpp index 69dd56d..05415e1 100644 --- a/src/knx/netip/qknxnetiptimernotify.cpp +++ b/src/knx/netip/qknxnetiptimernotify.cpp @@ -27,6 +27,7 @@ ** ******************************************************************************/ +#include "qknxbuilderdata_p.h" #include "qknxnetiptimernotify.h" #include "qknxutils.h" @@ -192,18 +193,6 @@ QKnxNetIpTimerNotifyProxy::Builder QKnxNetIpTimerNotifyProxy::builder() \sa QKnxCryptographicEngine */ -class QKnxNetIpTimerNotifyBuilderPrivate : public QSharedData -{ -public: - QKnxNetIpTimerNotifyBuilderPrivate() = default; - ~QKnxNetIpTimerNotifyBuilderPrivate() = default; - - quint64 m_timer { Q_UINT48_MAX + 1 }; - QKnxByteArray m_serial; - qint32 m_tag { -1 }; - QKnxByteArray m_authCode; -}; - /*! Creates a new empty timer notify frame builder object. */ @@ -305,4 +294,162 @@ QKnxNetIpTimerNotifyProxy::Builder & return *this; } + +/*! + \class QKnxNetIpTimerNotifyProxy::SecureBuilder + + \inmodule QtKnx + \inheaderfile QKnxNetIpTimerNotifyProxy + + \brief The QKnxNetIpTimerNotifyProxy::SecureBuilder class provides the + means to create a KNXnet/IP timer notify frame. + + This class is part of the Qt KNX module and currently available as a + Technology Preview, and therefore the API and functionality provided + by the class may be subject to change at any time without prior notice. + + \note To use this class OpenSSL must be supported on your target system. + + This frame will be sent during secure KNXnet/IP multicast group + communication to keep the multicast group member's timer values + synchronized. The frame is therefore sent to the KNXnet/IP routing + endpoint on port \c 3671 of the configured routing multicast address. + + The common way to create a timer notify frame is: + + \code + auto sessionId = ... + auto backboneKey = ... // the backbone key used + + auto netIpFrame = QKnxNetIpTimerNotifyProxy::builder() + .setTimerValue(15021976) + .setSerialNumber(QKnxByteArray::fromHex("0123456789AB")) + .setMessageTag(quint16(QRandomGenerator::global()->generate()) + .create(backboneKey, sessionId); + \endcode + + \sa QKnxCryptographicEngine +*/ + +/*! + Creates a new empty timer notify frame builder object. +*/ +QKnxNetIpTimerNotifyProxy::SecureBuilder::SecureBuilder() + : d_ptr(new QKnxNetIpTimerNotifyBuilderPrivate) +{} + +/*! + Destroys the object and frees any allocated resources. +*/ +QKnxNetIpTimerNotifyProxy::SecureBuilder::~SecureBuilder() = default; + +/*! + Sets the timer value to \a timerValue and returns a reference to the + builder. + + \note The size of a timer value is limited to 48 bits, so the maximum + number can be \c 281474976710655. Passing a larger value will result in + creating an invalid frame. +*/ +QKnxNetIpTimerNotifyProxy::SecureBuilder & + QKnxNetIpTimerNotifyProxy::SecureBuilder::setTimerValue(quint48 timerValue) +{ + d_ptr->m_timer = timerValue; + return *this; +} + +/*! + Sets the serial number to \a serialNumber and returns a reference to the + builder. + + \note The serial number must contain exactly 6 bytes. +*/ +QKnxNetIpTimerNotifyProxy::SecureBuilder & + QKnxNetIpTimerNotifyProxy::SecureBuilder::setSerialNumber(const QKnxByteArray &serialNumber) +{ + d_ptr->m_serial = serialNumber; + return *this; +} + +/*! + Sets the message tag of the generic KNXnet/IP timer notify frame to \a tag + and returns a reference to the builder. + + In case of a periodic or initial notify the tag contains a random value. In + case of an update notify this is the value of the outdated frame triggering + the update. +*/ +QKnxNetIpTimerNotifyProxy::SecureBuilder & + QKnxNetIpTimerNotifyProxy::SecureBuilder::setMessageTag(quint16 tag) +{ + d_ptr->m_tag = tag; + return *this; +} + +/*! + Creates and returns a KNXnet/IP timer notify frame. + + The function computes the AES128 CCM message authentication code (MAC) + with the given backbone key \a backboneKey and the session ID \a ssid and + appends it to the newly created frame. + + \note The returned frame may be invalid depending on the values used during + setup. + + \sa isValid() +*/ +QKnxNetIpFrame + QKnxNetIpTimerNotifyProxy::SecureBuilder::create(const QKnxByteArray &backboneKey, quint16 ssid) const +{ +#if QT_CONFIG(opensslv11) + if (d_ptr->m_timer > Q_UINT48_MAX || d_ptr->m_serial.size() != 6 || d_ptr->m_tag < 0) + return { QKnxNetIp::ServiceType::TimerNotify }; + + auto builder = QKnxNetIpTimerNotifyProxy::builder(); + auto frame = builder + .setTimerValue(d_ptr->m_timer) + .setSerialNumber(d_ptr->m_serial) + .setMessageTag(d_ptr->m_tag) + .setMessageAuthenticationCode(QKnxByteArray(16, 0x00)) // dummy MAC to get a proper header + .create(); + + auto mac = QKnxCryptographicEngine::computeMessageAuthenticationCode(backboneKey, + frame.header(), ssid, {}, d_ptr->m_timer, d_ptr->m_serial, d_ptr->m_tag); + + mac = QKnxCryptographicEngine::encryptMessageAuthenticationCode(backboneKey, mac, + d_ptr->m_timer, d_ptr->m_serial, d_ptr->m_tag); + + return builder.setMessageAuthenticationCode(mac).create(); +#else + Q_UNUSED(backboneKey) + Q_UNUSED(ssid) + return { QKnxNetIp::ServiceType::TimerNotify }; +#endif +} + +/*! + Constructs a copy of \a other. +*/ +QKnxNetIpTimerNotifyProxy::SecureBuilder::SecureBuilder(const SecureBuilder &other) + : d_ptr(other.d_ptr) +{} + +/*! + Assigns the specified \a other to this object. +*/ +QKnxNetIpTimerNotifyProxy::SecureBuilder & + QKnxNetIpTimerNotifyProxy::SecureBuilder::operator=(const SecureBuilder &other) +{ + d_ptr = other.d_ptr; + return *this; +} + +/*! + Returns a secure builder object to create a KNXnet/IP timer notify frame. +*/ +QKnxNetIpTimerNotifyProxy::SecureBuilder QKnxNetIpTimerNotifyProxy::secureBuilder() +{ + return QKnxNetIpTimerNotifyProxy::SecureBuilder(); +} + QT_END_NAMESPACE diff --git a/src/knx/netip/qknxnetiptimernotify.h b/src/knx/netip/qknxnetiptimernotify.h index 5b94acf..d862216 100644 --- a/src/knx/netip/qknxnetiptimernotify.h +++ b/src/knx/netip/qknxnetiptimernotify.h @@ -73,6 +73,26 @@ public: }; static QKnxNetIpTimerNotifyProxy::Builder builder(); + class Q_KNX_EXPORT SecureBuilder final + { + public: + SecureBuilder(); + ~SecureBuilder(); + + SecureBuilder &setTimerValue(quint48 timerValue); + SecureBuilder &setSerialNumber(const QKnxByteArray &serialNumber); + SecureBuilder &setMessageTag(quint16 tag); + + QKnxNetIpFrame create(const QKnxByteArray &backboneKey, quint16 sessionId) const; + + SecureBuilder(const SecureBuilder &other); + SecureBuilder &operator=(const SecureBuilder &other); + + private: + QSharedDataPointer<QKnxNetIpTimerNotifyBuilderPrivate> d_ptr; + }; + static QKnxNetIpTimerNotifyProxy::SecureBuilder secureBuilder(); + private: const QKnxNetIpFrame &m_frame; }; diff --git a/src/knx/netip/qknxnetiptunnel.cpp b/src/knx/netip/qknxnetiptunnel.cpp index ba6279b..5f382f3 100644 --- a/src/knx/netip/qknxnetiptunnel.cpp +++ b/src/knx/netip/qknxnetiptunnel.cpp @@ -111,68 +111,55 @@ public: , m_layer(l) {} - void process(const QKnxLinkLayerFrame &frame) override - { - Q_Q(QKnxNetIpTunnel); - emit q->frameReceived(frame); - } + void process(const QKnxLinkLayerFrame &frame) override; + void processConnectResponse(const QKnxNetIpFrame &frame) override; + void processTunnelingFeatureFrame(const QKnxNetIpFrame &frame) override; - void processConnectResponse(const QKnxNetIpFrame &frame) override - { - QKnxNetIpConnectResponseProxy response(frame); - if (response.status() == QKnxNetIp::Error::NoMoreUniqueConnections) { - Q_ASSERT_X(false, "QKnxNetIpTunnelPrivate::process", "NoMoreUniqueConnections " - "error handling not implemented yet."); - // TODO: Maybe implement 03_08_04 Tunneling v01.05.03 AS.pdf, paragraph 3.3 - } +private: + QKnxAddress m_address; + QKnxNetIp::TunnelLayer m_layer { QKnxNetIp::TunnelLayer::Unknown }; +}; - Q_Q(QKnxNetIpTunnel); - if (q->state() != QKnxNetIpTunnel::Connected) { - const auto &crd = response.responseData(); - m_address = QKnxNetIpCrdProxy(crd).individualAddress(); - } - QKnxNetIpEndpointConnectionPrivate::processConnectResponse(frame); +void QKnxNetIpTunnelPrivate::process(const QKnxLinkLayerFrame &frame) +{ + Q_Q(QKnxNetIpTunnel); + emit q->frameReceived(frame); +} + +void QKnxNetIpTunnelPrivate::processConnectResponse(const QKnxNetIpFrame &frame) +{ + QKnxNetIpConnectResponseProxy response(frame); + if (response.status() == QKnxNetIp::Error::NoMoreUniqueConnections) { + Q_ASSERT_X(false, "QKnxNetIpTunnelPrivate::process", "NoMoreUniqueConnections " + "error handling not implemented yet."); + // TODO: Maybe implement 03_08_04 Tunneling v01.05.03 AS.pdf, paragraph 3.3 } - void processTunnelingFeatureFrame(const QKnxNetIpFrame &frame) override - { - Q_Q(QKnxNetIpTunnel); - if (frame.serviceType() == QKnxNetIp::ServiceType::TunnelingFeatureInfo) { - const QKnxNetIpTunnelingFeatureInfoProxy proxy(frame); - if (proxy.isValid()) { - emit q->tunnelingFeatureInfoReceived(proxy.featureIdentifier(), - proxy.featureValue()); - } - } else if (frame.serviceType() == QKnxNetIp::ServiceType::TunnelingFeatureResponse) { - const QKnxNetIpTunnelingFeatureResponseProxy proxy(frame); - if (proxy.isValid()) { - emit q->tunnelingFeatureResponseReceived(proxy.featureIdentifier(), - proxy.returnCode(), proxy.featureValue()); - } - } + Q_Q(QKnxNetIpTunnel); + if (q->state() != QKnxNetIpTunnel::Connected) { + const auto &crd = response.responseData(); + m_address = QKnxNetIpCrdProxy(crd).individualAddress(); } + QKnxNetIpEndpointConnectionPrivate::processConnectResponse(frame); +} - void updateCri() - { - if (m_criAddress.isValid()) { - setCri(QKnxNetIpCriProxy::builder() - .setTunnelLayer(m_layer) - .setIndividualAddress(m_criAddress) - .create() - ); - } else { - setCri(QKnxNetIpCriProxy::builder() - .setTunnelLayer(m_layer) - .create() - ); +void QKnxNetIpTunnelPrivate::processTunnelingFeatureFrame(const QKnxNetIpFrame &frame) +{ + Q_Q(QKnxNetIpTunnel); + if (frame.serviceType() == QKnxNetIp::ServiceType::TunnelingFeatureInfo) { + const QKnxNetIpTunnelingFeatureInfoProxy proxy(frame); + if (proxy.isValid()) { + emit q->tunnelingFeatureInfoReceived(proxy.featureIdentifier(), + proxy.featureValue()); + } + } else if (frame.serviceType() == QKnxNetIp::ServiceType::TunnelingFeatureResponse) { + const QKnxNetIpTunnelingFeatureResponseProxy proxy(frame); + if (proxy.isValid()) { + emit q->tunnelingFeatureResponseReceived(proxy.featureIdentifier(), + proxy.returnCode(), proxy.featureValue()); } } - -private: - QKnxAddress m_address; - QKnxAddress m_criAddress; - QKnxNetIp::TunnelLayer m_layer { QKnxNetIp::TunnelLayer::Unknown }; -}; +} /*! Creates a tunnel connection with the parent \a parent. @@ -220,22 +207,17 @@ QKnxAddress QKnxNetIpTunnel::individualAddress() const return d_func()->m_address; } +#if QT_DEPRECATED_SINCE(5, 13) /*! - Sets the requested individual address of the KNXnet/IP client extended - connect request information (CRI) structure to \a address. + \obsolete - If the \a address is a valid individual address, a extended CRI is used - to send the connect request. To reset the behavior in favor of a standard - CRI pass an invalid \a address to the function. + Use \l QKnxNetIpSecureConfiguration::setIndividualAddress() instead. */ void QKnxNetIpTunnel::setIndividualAddress(const QKnxAddress &address) { - if (address.type() != QKnxAddress::Type::Individual) - return; - - d_func()->m_criAddress = address; - d_func()->updateCri(); + d_func()->updateCri(address); } +#endif /*! Returns the layer used for the tunnel connection. @@ -259,7 +241,7 @@ void QKnxNetIpTunnel::setTunnelLayer(QKnxNetIp::TunnelLayer layer) return; d_func()->m_layer = layer; - d_func()->updateCri(); + d_func()->updateCri(layer); } /*! diff --git a/src/knx/netip/qknxnetiptunnel.h b/src/knx/netip/qknxnetiptunnel.h index 9ecb526..d16b027 100644 --- a/src/knx/netip/qknxnetiptunnel.h +++ b/src/knx/netip/qknxnetiptunnel.h @@ -54,7 +54,9 @@ public: QKnxNetIp::TunnelLayer layer, QObject *parent = nullptr); QKnxAddress individualAddress() const; - void setIndividualAddress(const QKnxAddress &address); +#if QT_DEPRECATED_SINCE(5, 13) + QT_DEPRECATED void setIndividualAddress(const QKnxAddress &address); +#endif QKnxNetIp::TunnelLayer layer() const; void setTunnelLayer(QKnxNetIp::TunnelLayer layer); diff --git a/src/knx/qknxnamespace.h b/src/knx/qknxnamespace.h index dd97434..a5bb53a 100644 --- a/src/knx/qknxnamespace.h +++ b/src/knx/qknxnamespace.h @@ -39,9 +39,10 @@ namespace QKnx { Q_KNX_EXPORT Q_NAMESPACE + // ### Qt6: make this 16 bit enum class EmiType : quint8 { - Unknown, + Unknown = 0x00, EMI1 = 0x01, EMI2 = 0x02, cEMI = 0x04 diff --git a/src/knx/ssl/qknxcryptographicengine.cpp b/src/knx/ssl/qknxcryptographicengine.cpp new file mode 100644 index 0000000..c4ab26f --- /dev/null +++ b/src/knx/ssl/qknxcryptographicengine.cpp @@ -0,0 +1,494 @@ +/****************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtKnx module. +** +** $QT_BEGIN_LICENSE:GPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 or (at your option) any later version +** approved by the KDE Free Qt Foundation. The licenses are as published by +** the Free Software Foundation and appearing in the file LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +******************************************************************************/ + +#include "qknxcryptographicengine.h" +#include "qknxnetipsessionauthenticate.h" +#include "qknxnetipsecurewrapper.h" +#include "qknxnetipsessionrequest.h" +#include "qknxnetipsessionresponse.h" +#include "qknxnetipsessionstatus.h" +#include "qknxnetiptimernotify.h" + +#include "private/qknxssl_p.h" + +#include <QtCore/qcryptographichash.h> +#include <QtCore/qmutex.h> + +#include <QtNetwork/qpassworddigestor.h> + +QT_BEGIN_NAMESPACE + +/*! + \class QKnxCryptographicEngine + + \since 5.12 + \inmodule QtKnx + \ingroup qtknx-general-classes + + \brief The QKnxCryptographicEngine class provides the means to handle all + KNXnet/IP security related tasks. + + This class is part of the Qt KNX module and currently available as a + Technology Preview, and therefore the API and functionality provided + by the class may be subject to change at any time without prior notice. + + \section2 Calculating Message Authentication Codes + + The computeMessageAuthenticationCode() function can be used to compute a + message authentication code (MAC) for a KNXnet/IP secure frame. The fields + that are used to compute the MAC depend on the type of the frame, such as + \e {session response frame}, \e {session authentication frame}, or + \e {timer notify frame}. + + The example code shows how to compute the MAC for the most common secure + frames: + + \code + auto dummyMac = QKnxByteArray(16, 000); // dummy to get a valid header + + // Session Response Frame + + quint16 secureSessionIdentifier = 0x0001; + auto responseBuilder = QKnxNetIpSessionResponseProxy::builder(); + + // create an intermediate frame to fetch a valid frame header + auto netIpFrame = responseBuilder + .setSecureSessionId(secureSessionIdentifier) + .setPublicKey(serverPublicKey) + .setMessageAuthenticationCode(dummyMac) + .create(); + + auto deviceAuthenticationHash = + QKnxCryptographicEngine::deviceAuthenticationCodeHash({ "trustme" }); + auto XOR_X_Y = QKnxCryptographicEngine::XOR(clientPublicKey.bytes(), serverPublicKey.bytes()); + + auto mac = QKnxCryptographicEngine::computeMessageAuthenticationCode(deviceAuthenticationHash, + netIpFrame.header(), secureSessionIdentifier, XOR_X_Y); + + // create the final frame including the computed MAC + netIpFrame = responseBuilder. + .setMessageAuthenticationCode(mac) + .create(); + + + // Session Authenticate Frame + + auto authenticateBuilder = QKnxNetIpSessionAuthenticateProxy::builder()' + + // create an intermediate frame to fetch a valid frame header + netIpFrame = authenticateBuilder + .setUserId(QKnxNetIp::SecureUserId::Management) + .setMessageAuthenticationCode(dummyMac) + .create(); + + auto passwordHash = QKnxCryptographicEngine::userPasswordHash({ "secret" }); + + mac = QKnxCryptographicEngine::computeMessageAuthenticationCode(passwordHash, + netIpFrame.header(), userId, XOR_X_Y); + + // create the final frame including the computed MAC + netIpFrame = responseBuilder. + .setMessageAuthenticationCode(mac) + .create(); + + + // Timer Notify Frame + + quint48 timerValue = 211938428830917; + auto serialNumber = QKnxByteArray::fromHex("00fa12345678"); + quint16 messageTag = quint16(QRandomGenerator::global()->generate(); + + auto timerNotifyBuilder = QKnxNetIpTimerNotifyProxy::builder(); + + // create an intermediate frame to fetch a valid frame header + netIpFrame = timerNotifyBuilder + .setTimerValue(timerValue) + .setSerialNumber(serialNumber) + .setMessageTag(messageTag) + .setMessageAuthenticationCode(dummyMac) + .create(); + + QKnxByteArray dummyPayload; + quint16 dummySession = 0x0000; + auto backboneKey = QKnxByteArray::fromHex("000102030405060708090a0b0c0d0e0f"); + + mac = QKnxCryptographicEngine::computeMessageAuthenticationCode(backboneKey, + netIpFrame.header(), dummySession, dummyPayload, timerValue, serialNumber, messageTag); + + // create the final frame including the computed MAC + netIpFrame = responseBuilder. + .setMessageAuthenticationCode(mac) + .create(); + \endcode +*/ + +/*! + \internal + \fn QKnxCryptographicEngine::QKnxCryptographicEngine() +*/ + +/*! + \internal + \fn QKnxCryptographicEngine::~QKnxCryptographicEngine() +*/ + +/*! + Determines if cryptography support is available. Returns \c true on success; + \c false otherwise. +*/ +bool QKnxCryptographicEngine::supportsCryptography() +{ + return QKnxSsl::supportsCryptography(); +} + +/*! + Returns the OpenSSL version number of the OpenSSL library if OpenSSL is + available and used to provide cryptographic support; or \c 0 in any other + case. +*/ +long QKnxCryptographicEngine::sslLibraryVersionNumber() +{ + return QKnxSsl::sslLibraryVersionNumber(); +} + +/*! + Returns the session key calculated from the given private key + \a privateKey and the peer's public key \a peerPublicKey if OpenSSL is + available and no error occurs; + otherwise returns a \l {default-constructed value} which can be empty. +*/ +QKnxByteArray QKnxCryptographicEngine::sessionKey(const QKnxSecureKey &privateKey, + const QKnxSecureKey &peerPublicKey) +{ + return sessionKey(QKnxSecureKey::sharedSecret(privateKey, peerPublicKey)); +} + +/*! + \overload sessionKey() +*/ +QKnxByteArray QKnxCryptographicEngine::sessionKey(const QKnxByteArray &privateKey, + const QKnxByteArray &peerPublicKey) +{ + return sessionKey(QKnxSecureKey::sharedSecret(privateKey, peerPublicKey)); +} + +/*! + Returns the session key computed from the given secret \a sharedSecret. +*/ +QKnxByteArray QKnxCryptographicEngine::sessionKey(const QKnxByteArray &sharedSecret) +{ + if (sharedSecret.isEmpty()) + return {}; + + return QKnxByteArray::fromByteArray(QCryptographicHash::hash(sharedSecret.toByteArray(), + QCryptographicHash::Sha256)).mid(0, 16); +} + +/*! + Returns the password hash derived from the user chosen password \a password. + + \note The salt used in the password-based key derivation function (PBKDF2) + is set to \e {user-password.1.secure.ip.knx.org}. +*/ +QKnxByteArray QKnxCryptographicEngine::userPasswordHash(const QByteArray &password) +{ + const auto hash = QPasswordDigestor::deriveKeyPbkdf2(QCryptographicHash::Algorithm::Sha256, + password, QByteArray("user-password.1.secure.ip.knx.org"), 0x10000, 16); + return QKnxByteArray::fromByteArray(hash); +} + +/*! + Returns the keyring password hash derived from the user chosen password + \a password. + + \note The salt used in the password-based key derivation function (PBKDF2) + is set to \e {1.keyring.ets.knx.org}. +*/ +QKnxByteArray QKnxCryptographicEngine::keyringPasswordHash(const QByteArray &password) +{ + const auto hash = QPasswordDigestor::deriveKeyPbkdf2(QCryptographicHash::Algorithm::Sha256, + password, "1.keyring.ets.knx.org", 0x10000, 16); + return QKnxByteArray::fromByteArray(hash); +} + +/*! + Returns the device authentication code hash derived from the user chosen + password \a password. + + \note The salt used in the password-based key derivation function (PBKDF2) + is set to \e {device-authentication-code.1.secure.ip.knx.org}. +*/ +QKnxByteArray QKnxCryptographicEngine::deviceAuthenticationCodeHash(const QByteArray &password) +{ + const auto hash = QPasswordDigestor::deriveKeyPbkdf2(QCryptographicHash::Algorithm::Sha256, + password, "device-authentication-code.1.secure.ip.knx.org", 0x10000, 16); + return QKnxByteArray::fromByteArray(hash); +} + +/*! + Returns the hash of \a data using the \c Sha256 algorithm. +*/ +QKnxByteArray QKnxCryptographicEngine::hashSha256(const QByteArray &data) +{ + return QKnxByteArray::fromByteArray(QCryptographicHash::hash(data, QCryptographicHash::Sha256)); +} + +/*! + Performs a byte-wise XOR operation on the arguments \a left and \a right. + If the arguments are not equal in size, the function uses only the shorter + array for the operation. If \a adjust is set to \c true, the arrays are made + equal by padding them with \c 0x00 bytes. +*/ +QKnxByteArray QKnxCryptographicEngine::XOR(const QKnxByteArray &left, const QKnxByteArray &right, + bool adjust) +{ + QKnxByteArray result(adjust ? qMax(left.size(), right.size()) : qMin(left.size(), right.size()), + Qt::Uninitialized); + for (int i = result.size() - 1; i >= 0; --i) + result.set(i, left.value(i, 0x00) ^ right.value(i, 0x00)); + return result; +} + +namespace QKnxPrivate +{ + static const quint8 iv[16] { 0x00 }; + + static QKnxByteArray b0(quint48 sequence, const QKnxByteArray &serial, quint16 tag, quint16 len) + { + return QKnxUtils::QUint48::bytes(sequence) + serial + QKnxUtils::QUint16::bytes(tag) + + QKnxUtils::QUint16::bytes(len); + } + + static QKnxByteArray ctr0(quint48 sequence, const QKnxByteArray &serial, quint16 tag) + { + return QKnxPrivate::b0(sequence, serial, tag, 0xff00); + } + + static QKnxByteArray processMAC(const QKnxByteArray &key, const QKnxByteArray &mac, + quint48 sequenceNumber, const QKnxByteArray &serialNumber, quint16 messageTag) + { + if (key.isEmpty() || mac.isEmpty()) + return {}; + + auto Ctr0 = QKnxPrivate::ctr0(sequenceNumber, + (serialNumber.isEmpty() ? QKnxByteArray(6, 0x00) : serialNumber), messageTag); + + return QKnxCryptographicEngine::XOR(QKnxSsl::doCrypt(key, { iv, 16 }, Ctr0, + QKnxSsl::Encrypt).right(16), mac); + } + + static QKnxByteArray processPayload(const QKnxByteArray &key, const QKnxByteArray &payload, + quint48 sequenceNumber, const QKnxByteArray &serialNumber, quint16 messageTag) + { + if (key.isEmpty() || payload.isEmpty()) + return {}; + + auto Ctr0 = QKnxPrivate::ctr0(sequenceNumber, + (serialNumber.isEmpty() ? QKnxByteArray(6, 0x00) : serialNumber), messageTag); + + QKnxByteArray ctrArray; + for (int i = 0; i < (payload.size() + 15) >> 4; ++i) { + Ctr0.set(15, Ctr0.at(15) + 1); + ctrArray += QKnxSsl::doCrypt(key, { iv, 16 }, Ctr0, QKnxSsl::Encrypt).right(16); + } + + return QKnxCryptographicEngine::XOR(ctrArray, payload, false); + } +} + +/*! + Computes a message authentication code (MAC) using the given \a key, + \a header, and \a id for the given \a data. Returns an array of bytes that + represent the computed MAC or an empty byte array in case of an error. + + \note The \a sequenceNumber, \a serialNumber, and \a messageTag values + are required to compute a valid MAC for KNXnet/IP secure wrapper frames. + For all other types of secure frames, the possibly given values are ignored + and \c 0 is used instead. For timer notify frames, \e {default-constructed} + values are used instead of the \a id and \a data values. + + For an example of using this function, see + \l {Calculating Message Authentication Codes}. +*/ +QKnxByteArray QKnxCryptographicEngine::computeMessageAuthenticationCode(const QKnxByteArray &key, + const QKnxNetIpFrameHeader &header, quint16 id, const QKnxByteArray &data, + quint48 sequenceNumber, const QKnxByteArray &serialNumber, quint16 messageTag) +{ + if (key.isEmpty() || !header.isValid()) + return {}; + + auto sn = (serialNumber.isEmpty() ? QKnxByteArray(6, 0x00) : serialNumber); + + QKnxByteArray B0, B; + if (header.serviceType() == QKnxNetIp::ServiceType::SecureWrapper) { + if (data.isEmpty()) + return {}; + + const auto A = header.bytes() + QKnxUtils::QUint16::bytes(id); + B0 = QKnxPrivate::b0(sequenceNumber, sn, messageTag, quint16(data.size())); + B = B0 + QKnxUtils::QUint16::bytes(quint16(A.size())) + A + data; + } else if (header.serviceType() == QKnxNetIp::ServiceType::SessionResponse + || header.serviceType() == QKnxNetIp::ServiceType::SessionAuthenticate) { + if (data.isEmpty()) + return {}; + + const auto A = header.bytes() + QKnxUtils::QUint16::bytes(id); + B0 = QKnxPrivate::b0(sequenceNumber, sn, messageTag, 0); + B = B0 + QKnxUtils::QUint16::bytes(quint16(A.size() + data.size())) + A + data; + } else if (header.serviceType() == QKnxNetIp::ServiceType::TimerNotify) { + const auto A = header.bytes(); + B0 = QKnxPrivate::b0(sequenceNumber, sn, messageTag, 0); + B = B0 + QKnxUtils::QUint16::bytes(quint16(A.size())) + A; + } + + if (B.isEmpty()) + return {}; + B.resize(B.size() + (16 - (B.size() % 16))); // pad to multiple of 16 + + return QKnxSsl::doCrypt(key, { QKnxPrivate::iv, 16 }, B, QKnxSsl::Encrypt).right(16); +} + +/*! + Decrypts the given \a data with \a key and the initial vector \a iv. Returns + an array of bytes that represents the decrypted data. +*/ +QKnxByteArray QKnxCryptographicEngine::decrypt(const QKnxByteArray &key, const QKnxByteArray &iv, + const QKnxByteArray &data) +{ + return QKnxSsl::doCrypt(key, iv, data, QKnxSsl::Decrypt); +} + +/*! + Encrypts the given \a data with \a key and the initial vector \a iv. Returns + an array of bytes that represents the encrypted data. +*/ +QKnxByteArray QKnxCryptographicEngine::encrypt(const QKnxByteArray &key, const QKnxByteArray &iv, + const QKnxByteArray &data) +{ + return QKnxSsl::doCrypt(key, iv, data, QKnxSsl::Encrypt); +} + +/*! + Encrypts the given KNXnet/IP frame \a frame with the given key \a key, + sequence number \a sequenceNumber, serial number \a serialNumber, and + message tag \a messageTag. Returns an array of bytes that represent the + encrypted frame or an empty byte array in case of an error or invalid + KNXnet/IP frame \a frame. +*/ +QKnxByteArray QKnxCryptographicEngine::encryptSecureWrapperPayload(const QKnxByteArray &key, + const QKnxNetIpFrame &frame, quint48 sequenceNumber, const QKnxByteArray &serialNumber, + quint16 messageTag) +{ + if (!frame.isValid()) + return {}; + return QKnxPrivate::processPayload(key, frame.bytes(), sequenceNumber, serialNumber, messageTag); +} + +/*! + Decrypts the given KNXnet/IP frame \a frame with the given key \a key, + sequence number \a sequenceNumber, serial number \a serialNumber, and + message tag \a messageTag. Returns an array of bytes that represent the + decrypted frame or an empty byte array in case of an error. +*/ +QKnxByteArray QKnxCryptographicEngine::decryptSecureWrapperPayload(const QKnxByteArray &key, + const QKnxByteArray &frame, quint48 sequenceNumber, const QKnxByteArray &serialNumber, + quint16 messageTag) +{ + return QKnxPrivate::processPayload(key, frame, sequenceNumber, serialNumber, messageTag); +} + +/*! + Encrypts the given message authentication code (MAC) \a mac with the given + key \a key, sequence number \a sequenceNumber, serial number \a serialNumber, + and message tag \a messageTag. Returns an array of bytes that represent + the encrypted MAC or an empty byte array in case of an error. + + \note The \a sequenceNumber, \a serialNumber and \a messageTag are mandatory + to properly encrypt the MAC for KNXnet/IP secure wrapper frame, for all other + secure frames the default value of \c 0 can be used. +*/ +QKnxByteArray QKnxCryptographicEngine::encryptMessageAuthenticationCode(const QKnxByteArray &key, + const QKnxByteArray &mac, quint48 sequenceNumber, const QKnxByteArray &serialNumber, + quint16 messageTag) +{ + return QKnxPrivate::processMAC(key, mac, sequenceNumber, serialNumber, messageTag); +} + +/*! + Decrypts the given message authentication code (MAC) \a mac with the given + key \a key, sequence number \a sequenceNumber, serial number \a serialNumber, + and message tag \a messageTag. Returns an array of bytes that represent + the decrypted MAC or an empty byte array in case of an error. + + \note The \a sequenceNumber, \a serialNumber and \a messageTag values are + required to properly decrypt the MAC for KNXnet/IP secure wrapper frame. + For all other secure frames, the default value of \c 0 can be used. + +*/ +QKnxByteArray QKnxCryptographicEngine::decryptMessageAuthenticationCode(const QKnxByteArray &key, + const QKnxByteArray &mac, quint48 sequenceNumber, const QKnxByteArray &serialNumber, + quint16 messageTag) +{ + return QKnxPrivate::processMAC(key, mac, sequenceNumber, serialNumber, messageTag); +} + +/*! + Decodes and decrypts a tool key \a toolKey that was stored in an ETS + keyring (*.knxkeys) file with the given password hash \a passwordHash and + created hash \a createdHash. + + Returns an array of bytes that represent the decrypted tool key or an empty + byte array in case of an error. +*/ +QKnxByteArray QKnxCryptographicEngine::decodeAndDecryptToolKey(const QKnxByteArray &passwordHash, + const QKnxByteArray &createdHash, const QByteArray &toolKey) +{ + auto base64 = QKnxByteArray::fromByteArray(QByteArray::fromBase64(toolKey)); + return QKnxSsl::doCrypt(passwordHash, createdHash, base64, QKnxSsl::Decrypt); +} + +/*! + Decodes and decrypts a password \a password that was stored in an ETS + keyring (*.knxkeys) file with the given password hash \a passwordHash and + created hash \a createdHash. + + Returns an array of bytes that represent the decrypted password or an empty + byte array in case of an error. +*/ +QKnxByteArray QKnxCryptographicEngine::decodeAndDecryptPassword(const QKnxByteArray &passwordHash, + const QKnxByteArray &createdHash, const QByteArray &password) +{ + const auto base64 = QKnxByteArray::fromByteArray(QByteArray::fromBase64(password)); + const auto decoded = QKnxSsl::doCrypt(passwordHash, createdHash, base64, QKnxSsl::Decrypt); + + quint8 paddedSize = decoded.value(decoded.size() - 1); + return decoded.mid(8, decoded.size() - paddedSize - 8); +} + +QT_END_NAMESPACE diff --git a/src/knx/ssl/qknxcurve25519.h b/src/knx/ssl/qknxcryptographicengine.h index 2d34e8b..eb48816 100644 --- a/src/knx/ssl/qknxcurve25519.h +++ b/src/knx/ssl/qknxcryptographicengine.h @@ -1,6 +1,6 @@ /****************************************************************************** ** -** Copyright (C) 2018 The Qt Company Ltd. +** Copyright (C) 2019 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the QtKnx module. @@ -27,88 +27,51 @@ ** ******************************************************************************/ -#ifndef QKNXCURVE25519_H -#define QKNXCURVE25519_H +#ifndef QKNXCRYPTOGRAPHICENGINE_H +#define QKNXCRYPTOGRAPHICENGINE_H #include <QtCore/qshareddata.h> #include <QtKnx/qknxbytearray.h> -#include <QtKnx/qtknxglobal.h> #include <QtKnx/qknxnetipframe.h> +#include <QtKnx/qknxsecurekey.h> QT_BEGIN_NAMESPACE -class QKnxCurve25519KeyData; -class QKnxCurve25519PrivateKey; - -class Q_KNX_EXPORT QKnxCurve25519PublicKey final -{ -public: - QKnxCurve25519PublicKey(); - ~QKnxCurve25519PublicKey(); - - QKnxCurve25519PublicKey(const QKnxCurve25519PrivateKey &key); - - bool isNull() const; - bool isValid() const; - - QKnxByteArray bytes() const; - static QKnxCurve25519PublicKey fromBytes(const QKnxByteArray &data, quint16 index = 0); - - QKnxCurve25519PublicKey(const QKnxCurve25519PublicKey &other); - QKnxCurve25519PublicKey &operator=(const QKnxCurve25519PublicKey &other); - -private: - friend class QKnxCryptographicEngine; - QSharedDataPointer<QKnxCurve25519KeyData> d_ptr; -}; - -class Q_KNX_EXPORT QKnxCurve25519PrivateKey final -{ -public: - QKnxCurve25519PrivateKey(); - ~QKnxCurve25519PrivateKey(); - - bool isNull() const; - bool isValid() const; - - QKnxByteArray bytes() const; - static QKnxCurve25519PrivateKey fromBytes(const QKnxByteArray &data, quint16 index = 0); - - QKnxCurve25519PrivateKey(const QKnxCurve25519PrivateKey &other); - QKnxCurve25519PrivateKey &operator=(const QKnxCurve25519PrivateKey &other); - -private: - friend class QKnxCurve25519PublicKey; - friend class QKnxCryptographicEngine; - QSharedDataPointer<QKnxCurve25519KeyData> d_ptr; -}; - class Q_KNX_EXPORT QKnxCryptographicEngine final { public: QKnxCryptographicEngine() = delete; ~QKnxCryptographicEngine() = default; - static QKnxByteArray sharedSecret(const QKnxCurve25519PublicKey &pub, - const QKnxCurve25519PrivateKey &priv); + static bool supportsCryptography(); + static long sslLibraryVersionNumber(); + static QKnxByteArray sessionKey(const QKnxSecureKey &privateKey, + const QKnxSecureKey &peerPublicKey); + static QKnxByteArray sessionKey(const QKnxByteArray &privateKey, + const QKnxByteArray &peerPublicKey); static QKnxByteArray sessionKey(const QKnxByteArray &sharedSecret); - static QKnxByteArray sessionKey(const QKnxCurve25519PublicKey &pub, - const QKnxCurve25519PrivateKey &priv); static QKnxByteArray userPasswordHash(const QByteArray &password); + static QKnxByteArray keyringPasswordHash(const QByteArray &password); static QKnxByteArray deviceAuthenticationCodeHash(const QByteArray &password); + static QKnxByteArray hashSha256(const QByteArray &data); static QKnxByteArray XOR(const QKnxByteArray &l, const QKnxByteArray &r, bool adjust = true); - static QKnxByteArray calculateMessageAuthenticationCode(const QKnxByteArray &key, - const QKnxNetIpFrameHeader &header, - quint16 id, - const QKnxByteArray &data, - quint48 sequenceNumber = 0, - const QKnxByteArray &serialNumber = {}, - quint16 messageTag = 0); + static QKnxByteArray computeMessageAuthenticationCode(const QKnxByteArray &key, + const QKnxNetIpFrameHeader &header, + quint16 id, + const QKnxByteArray &data, + quint48 sequenceNumber = 0, + const QKnxByteArray &serialNumber = {}, + quint16 messageTag = 0); + + static QKnxByteArray decrypt(const QKnxByteArray &key, const QKnxByteArray &iv, + const QKnxByteArray &data); + static QKnxByteArray encrypt(const QKnxByteArray &key, const QKnxByteArray &iv, + const QKnxByteArray &data); static QKnxByteArray encryptSecureWrapperPayload(const QKnxByteArray &key, const QKnxNetIpFrame &frame, @@ -134,8 +97,12 @@ public: const QKnxByteArray &serialNumber = {}, quint16 messageTag = 0); - static QKnxByteArray pkcs5Pbkdf2HmacSha256(const QByteArray &password, const QKnxByteArray &salt, - qint32 iterations, quint8 derivedKeyLength); + static QKnxByteArray decodeAndDecryptToolKey(const QKnxByteArray &passwordHash, + const QKnxByteArray &createdHash, + const QByteArray &toolKey); + static QKnxByteArray decodeAndDecryptPassword(const QKnxByteArray &passwordHash, + const QKnxByteArray &createdHash, + const QByteArray &password); }; QT_END_NAMESPACE diff --git a/src/knx/ssl/qknxcurve25519.cpp b/src/knx/ssl/qknxcurve25519.cpp deleted file mode 100644 index 0881875..0000000 --- a/src/knx/ssl/qknxcurve25519.cpp +++ /dev/null @@ -1,819 +0,0 @@ -/****************************************************************************** -** -** Copyright (C) 2018 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the QtKnx module. -** -** $QT_BEGIN_LICENSE:GPL$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 3 or (at your option) any later version -** approved by the KDE Free Qt Foundation. The licenses are as published by -** the Free Software Foundation and appearing in the file LICENSE.GPL3 -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -******************************************************************************/ - -#include "qknxcurve25519.h" -#include "qknxcryptographicdata_p.h" - -#include "qknxnetipsessionauthenticate.h" -#include "qknxnetipsecurewrapper.h" -#include "qknxnetipsessionrequest.h" -#include "qknxnetipsessionresponse.h" -#include "qknxnetipsessionstatus.h" -#include "qknxnetiptimernotify.h" - -#include <QtCore/qcryptographichash.h> -#include <QMutex> - -QT_BEGIN_NAMESPACE - -bool QKnxOpenSsl::s_libraryLoaded = false; -bool QKnxOpenSsl::s_libraryEnabled = false; - -Q_GLOBAL_STATIC(QKnxOpenSsl, qt_QKnxOpenSsl) -Q_GLOBAL_STATIC_WITH_ARGS(QMutex, qt_knxOpenSslInitMutex, (QMutex::Recursive)) - -/*! - \internal -*/ -bool QKnxOpenSsl::supportsSsl() -{ - return ensureLibraryLoaded(); -} - -/*! - \internal -*/ -long QKnxOpenSsl::sslLibraryVersionNumber() -{ - if (!supportsSsl()) - return 0; - return q_OpenSSL_version_num(); -} - -/*! - \internal -*/ -QString QKnxOpenSsl::sslLibraryVersionString() -{ - if (!supportsSsl()) - return QString(); - - const char *versionString = q_OpenSSL_version(OPENSSL_VERSION); - if (!versionString) - return QString(); - - return QString::fromLatin1(versionString); -} - -/*! - \internal -*/ -long QKnxOpenSsl::sslLibraryBuildVersionNumber() -{ - return OPENSSL_VERSION_NUMBER; -} - - -QString QKnxOpenSsl::sslLibraryBuildVersionString() -{ - return QStringLiteral(OPENSSL_VERSION_TEXT); -} - -/*! - \internal -*/ -bool QKnxOpenSsl::ensureLibraryLoaded() -{ - if (!q_resolveOpenSslSymbols()) - return false; - - const QMutexLocker locker(qt_knxOpenSslInitMutex); - - if (!s_libraryLoaded) { - s_libraryLoaded = true; - - // Initialize OpenSSL. - if (q_OPENSSL_init_ssl(0, nullptr) != 1) - return false; - q_SSL_load_error_strings(); - q_OpenSSL_add_all_algorithms(); - - // Initialize OpenSSL's random seed. - if (!q_RAND_status()) { - qWarning("Random number generator not seeded, disabling SSL support"); - return false; - } - - if (q_EVP_PKEY_type(NID_X25519) == NID_undef) { - qWarning("The X25519 algorithm is not supported, disabling SSL support"); - return false; - } - s_libraryEnabled = true; - } - return s_libraryEnabled; -} - - -/*! - \inmodule QtKnx - \class QKnxCurve25519PublicKey - - \since 5.12 - \ingroup qtknx-general-classes - - \brief The QKnxCurve25519PublicKey class represents the elliptic-curve - public key to be used with the elliptic curve Diffie-Hellman (ECDH) key - agreement scheme. - - This class is part of the Qt KNX module and currently available as a - Technology Preview, and therefore the API and functionality provided - by the class may be subject to change at any time without prior notice. -*/ - -/*! - Constructs an empty invalid public key. -*/ -QKnxCurve25519PublicKey::QKnxCurve25519PublicKey() - : d_ptr(new QKnxCurve25519KeyData) -{} - -/*! - Destroys the public key and releases all allocated resources. -*/ -QKnxCurve25519PublicKey::~QKnxCurve25519PublicKey() = default; - -/*! - Creates a new public key with the given private key \a key. -*/ -QKnxCurve25519PublicKey::QKnxCurve25519PublicKey(const QKnxCurve25519PrivateKey &key) - : d_ptr(new QKnxCurve25519KeyData) -{ - if (!key.d_ptr->m_evpPKey) - return; - - if (!qt_QKnxOpenSsl->supportsSsl()) - return; - - q_EVP_PKEY_up_ref(key.d_ptr->m_evpPKey); - d_ptr->m_evpPKey = key.d_ptr->m_evpPKey; -} - -/*! - Returns \c true if this is a default constructed public key; otherwise - returns \c false. A public key is considered null if it contains no - initialized values. -*/ -bool QKnxCurve25519PublicKey::isNull() const -{ - return d_ptr->m_evpPKey == nullptr; -} - -/*! - Returns \c true if OpenSSL is available and if the public key contains - initialized values, otherwise returns \c false. -*/ -bool QKnxCurve25519PublicKey::isValid() const -{ - return qt_QKnxOpenSsl->supportsSsl() && !isNull(); -} - -/*! - Returns an array of bytes that represent the Curve25519 raw public key. -*/ -QKnxByteArray QKnxCurve25519PublicKey::bytes() const -{ - if (!isValid()) - return {}; - - size_t len = 32; - QKnxByteArray ba(int(len), 0); - if (q_EVP_PKEY_get_raw_public_key(d_ptr->m_evpPKey, ba.data(), &len) > 0) - return ba; // preferred way - - ba.resize(q_i2d_PUBKEY(d_ptr->m_evpPKey, nullptr)); - auto tmp = ba.data(); - q_i2d_PUBKEY(d_ptr->m_evpPKey, &tmp); - return ba.right(32); -} - -/*! - Constructs the public key from the byte array \a data starting at position - \a index inside the array if OpenSSL is available and no error occurs; - otherwise returns a \e {default-constructed key} which can be invalid. -*/ -QKnxCurve25519PublicKey QKnxCurve25519PublicKey::fromBytes(const QKnxByteArray &data, quint16 index) -{ - auto ba = data.mid(index, 32); - if (ba.size() < 32) - return {}; - - if (!qt_QKnxOpenSsl->supportsSsl()) - return {}; - - QKnxCurve25519PublicKey key; - key.d_ptr->m_evpPKey = q_EVP_PKEY_new_raw_public_key(NID_X25519, nullptr, ba.constData(), - ba.size()); // preferred way - if (key.d_ptr->m_evpPKey) - return key; - - key.d_ptr->m_evpPKey = q_EVP_PKEY_new(); - if (q_EVP_PKEY_set_type(key.d_ptr->m_evpPKey, NID_X25519) <= 0) - return {}; - - if (q_EVP_PKEY_set1_tls_encodedpoint(key.d_ptr->m_evpPKey, ba.constData(), ba.size()) <= 0) - return {}; - - return key; -} - -/*! - Constructs a copy of \a other. -*/ -QKnxCurve25519PublicKey::QKnxCurve25519PublicKey(const QKnxCurve25519PublicKey &other) - : d_ptr(other.d_ptr) -{} - -/*! - Assigns the specified \a other to this object. -*/ -QKnxCurve25519PublicKey &QKnxCurve25519PublicKey::operator=(const QKnxCurve25519PublicKey &other) -{ - d_ptr = other.d_ptr; - return *this; -} - - -/*! - \class QKnxCurve25519PrivateKey - - \inmodule QtKnx - - \since 5.12 - \ingroup qtknx-general-classes - - \brief The QKnxCurve25519PrivateKey class represents the elliptic-curve - private key to be used with the elliptic curve Diffie-Hellman (ECDH) key - agreement scheme. - - This class is part of the Qt KNX module and currently available as a - Technology Preview, and therefore the API and functionality provided - by the class may be subject to change at any time without prior notice. -*/ - -/*! - Constructs a valid private key if OpenSSL is available and no error occurs. -*/ -QKnxCurve25519PrivateKey::QKnxCurve25519PrivateKey() - : d_ptr(new QKnxCurve25519KeyData) -{ - if (!qt_QKnxOpenSsl->supportsSsl()) - return; - - if (auto *pctx = q_EVP_PKEY_CTX_new_id(NID_X25519, nullptr)) { - q_EVP_PKEY_keygen_init(pctx); - q_EVP_PKEY_keygen(pctx, &d_ptr->m_evpPKey); - q_EVP_PKEY_CTX_free(pctx); - } -} - -/*! - Destroys the private key and releases all allocated resources. -*/ -QKnxCurve25519PrivateKey::~QKnxCurve25519PrivateKey() = default; - -/*! - Returns \c true if this is a default constructed private key; otherwise - returns \c false. A private key is considered null if it contains no - initialized values. -*/ -bool QKnxCurve25519PrivateKey::isNull() const -{ - return d_ptr->m_evpPKey == nullptr; -} - -/*! - Returns \c true if OpenSSL is available and if the private key contains - initialized values, otherwise returns \c false. -*/ -bool QKnxCurve25519PrivateKey::isValid() const -{ - return qt_QKnxOpenSsl->supportsSsl() && !isNull(); -} - -/*! - Returns an array of bytes that represent the Curve25519 raw private key. -*/ -QKnxByteArray QKnxCurve25519PrivateKey::bytes() const -{ - if (!isValid()) - return {}; - - size_t len = 32; - QKnxByteArray ba(int(len), 0); - if (q_EVP_PKEY_get_raw_private_key(d_ptr->m_evpPKey, ba.data(), &len) <= 0) - return {}; // preferred, no other way possible - return ba; -} - -/*! - Constructs the private key from the byte array \a data starting at position - \a index inside the array if OpenSSL is available and no error occurs; - otherwise returns a \e {default-constructed key} which can be invalid. -*/ -QKnxCurve25519PrivateKey QKnxCurve25519PrivateKey::fromBytes(const QKnxByteArray &data, quint16 index) -{ - auto ba = data.mid(index, 32); - if (!qt_QKnxOpenSsl->supportsSsl() || ba.size() < 32) - return {}; - - QKnxCurve25519PrivateKey key; - key.d_ptr->m_evpPKey = q_EVP_PKEY_new_raw_private_key(NID_X25519, nullptr, ba.constData(), - ba.size()); // preferred way - if (key.d_ptr->m_evpPKey) - return key; - - static const auto pkcs8 = QKnxByteArray::fromHex("302e020100300506032b656e04220420"); - auto tmp = pkcs8 + ba; // PKCS #8 is a standard syntax for storing private key information - - BIO *bio = nullptr; - if ((bio = q_BIO_new_mem_buf(reinterpret_cast<void *> (tmp.data()), tmp.size()))) - key.d_ptr->m_evpPKey = q_d2i_PrivateKey_bio(bio, nullptr); - q_BIO_free(bio); - - return key; -} - -/*! - Constructs a copy of \a other. -*/ -QKnxCurve25519PrivateKey::QKnxCurve25519PrivateKey(const QKnxCurve25519PrivateKey &other) - : d_ptr(other.d_ptr) -{} - -/*! - Assigns the specified \a other to this object. -*/ -QKnxCurve25519PrivateKey &QKnxCurve25519PrivateKey::operator=(const QKnxCurve25519PrivateKey &other) -{ - d_ptr = other.d_ptr; - return *this; -} - - -/*! - \class QKnxCryptographicEngine - - \since 5.12 - \inmodule QtKnx - \ingroup qtknx-general-classes - - \brief The QKnxCryptographicEngine class provides the means to handle all - KNXnet/IP security related tasks. - - This class is part of the Qt KNX module and currently available as a - Technology Preview, and therefore the API and functionality provided - by the class may be subject to change at any time without prior notice. - - \section2 Calculating Message Authentication Codes - - The calculateMessageAuthenticationCode() function can be used to compute a - message authentication code (MAC) for a KNXnet/IP secure frame. The fields - that are used to calculate the MAC depend on the type of the frame, such - as \e {session response frame}, \e {session authentication frame}, or - \e {timer notify frame}. - - The example code shows how to calculate the MAC for the most common secure - frames: - - \code - auto dummyMac = QKnxByteArray(16, 000); // dummy to get a valid header - - // Session Response Frame - - quint16 secureSessionIdentifier = 0x0001; - auto responseBuilder = QKnxNetIpSessionResponseProxy::builder(); - - // create an intermediate frame to fetch a valid frame header - auto netIpFrame = responseBuilder - .setSecureSessionId(secureSessionIdentifier) - .setPublicKey(serverPublicKey) - .setMessageAuthenticationCode(dummyMac) - .create(); - - auto deviceAuthenticationHash = - QKnxCryptographicEngine::deviceAuthenticationCodeHash({ "trustme" }); - auto XOR_X_Y = QKnxCryptographicEngine::XOR(clientPublicKey.bytes(), serverPublicKey.bytes()); - - auto mac = QKnxCryptographicEngine::calculateMessageAuthenticationCode(deviceAuthenticationHash, - netIpFrame.header(), secureSessionIdentifier, XOR_X_Y); - - // create the final frame including the calculated MAC - netIpFrame = responseBuilder. - .setMessageAuthenticationCode(mac) - .create(); - - - // Session Authenticate Frame - - quint16 userId = 0x0001; // management level access - auto authenticateBuilder = QKnxNetIpSessionAuthenticateProxy::builder()' - - // create an intermediate frame to fetch a valid frame header - netIpFrame = authenticateBuilder - .setUserId(userId) - .setMessageAuthenticationCode(dummyMac) - .create(); - - auto passwordHash = QKnxCryptographicEngine::userPasswordHash({ "secret" }); - - mac = QKnxCryptographicEngine::calculateMessageAuthenticationCode(passwordHash, - netIpFrame.header(), userId, XOR_X_Y); - - // create the final frame including the calculated MAC - netIpFrame = responseBuilder. - .setMessageAuthenticationCode(mac) - .create(); - - - // Timer Notify Frame - - quint48 timerValue = 211938428830917; - auto serialNumber = QKnxByteArray::fromHex("00fa12345678"); - quint16 messageTag = quint16(QRandomGenerator::global()->generate(); - - auto timerNotifyBuilder = QKnxNetIpTimerNotifyProxy::builder(); - - // create an intermediate frame to fetch a valid frame header - netIpFrame = timerNotifyBuilder - .setTimerValue(timerValue) - .setSerialNumber(serialNumber) - .setMessageTag(messageTag) - .setMessageAuthenticationCode(dummyMac) - .create(); - - QKnxByteArray dummyPayload; - quint16 dummySession = 0x0000; - auto backboneKey = QKnxByteArray::fromHex("000102030405060708090a0b0c0d0e0f"); - - mac = QKnxCryptographicEngine::calculateMessageAuthenticationCode(backboneKey, - netIpFrame.header(), dummySession, dummyPayload, timerValue, serialNumber, messageTag); - - // create the final frame including the calculated MAC - netIpFrame = responseBuilder. - .setMessageAuthenticationCode(mac) - .create(); - \endcode -*/ - -/*! - \internal - \fn QKnxCryptographicEngine::QKnxCryptographicEngine() -*/ - -/*! - \internal - \fn QKnxCryptographicEngine::~QKnxCryptographicEngine() -*/ - -/*! - Derives and returns the shared secret from the given public peer key \a pub - and the private key \a priv if OpenSSL is available and no error occurs; - otherwise returns a \l {default-constructed value} which can be empty. -*/ -QKnxByteArray QKnxCryptographicEngine::sharedSecret(const QKnxCurve25519PublicKey &pub, - const QKnxCurve25519PrivateKey &priv) -{ - if (pub.isNull() || priv.isNull()) - return {}; - - if (!qt_QKnxOpenSsl->supportsSsl()) - return {}; - - auto evpPKeyCtx = q_EVP_PKEY_CTX_new(priv.d_ptr->m_evpPKey, nullptr); - if (!evpPKeyCtx) - return {}; - - struct ScopedFree final - { - ScopedFree(EVP_PKEY_CTX *key) : m_evpPKeyCtx(key) {} - ~ScopedFree() { q_EVP_PKEY_CTX_free(m_evpPKeyCtx); } - EVP_PKEY_CTX *m_evpPKeyCtx = nullptr; - } _ { evpPKeyCtx }; - - if (q_EVP_PKEY_derive_init(evpPKeyCtx) <= 0) - return {}; - - if (q_EVP_PKEY_derive_set_peer(evpPKeyCtx, pub.d_ptr->m_evpPKey) <= 0) - return {}; - - size_t keylen = 0; - if (q_EVP_PKEY_derive(evpPKeyCtx, nullptr, &keylen) <= 0) - return {}; - - QKnxByteArray ba(int(keylen), 0); - if (q_EVP_PKEY_derive(evpPKeyCtx, ba.data(), &keylen) <= 0) - return {}; - - return ba; -} - -/*! - Returns the session key calculated from the given secret \a sharedSecret. -*/ -QKnxByteArray QKnxCryptographicEngine::sessionKey(const QKnxByteArray &sharedSecret) -{ - if (sharedSecret.isEmpty()) - return {}; - - return QKnxByteArray::fromByteArray(QCryptographicHash::hash(sharedSecret.toByteArray(), - QCryptographicHash::Sha256)).mid(0, 16); -} - -/*! - Returns the session key calculated from the given peer public key \a pub - and the private key \a priv. -*/ -QKnxByteArray QKnxCryptographicEngine::sessionKey(const QKnxCurve25519PublicKey &pub, - const QKnxCurve25519PrivateKey &priv) -{ - return sessionKey(sharedSecret(pub, priv)); -} - -/*! - Returns the password hash derived from the user chosen password \a password. - - \note The salt used in the Password-Based Key Derivation Function (PBKDF2) - function is set to \e {user-password.1.secure.ip.knx.org}. - - \sa pkcs5Pbkdf2HmacSha256() -*/ -QKnxByteArray QKnxCryptographicEngine::userPasswordHash(const QByteArray &password) -{ - return pkcs5Pbkdf2HmacSha256(password, - QKnxByteArray("user-password.1.secure.ip.knx.org", 33), 0x10000, 16); -} - -/*! - Returns the device authentication code hash derived from the user chosen - password \a password. - - \note The salt used in the Password-Based Key Derivation Function (PBKDF2) - function is set to \e {device-authentication-code.1.secure.ip.knx.org}. - - \sa pkcs5Pbkdf2HmacSha256() -*/ -QKnxByteArray QKnxCryptographicEngine::deviceAuthenticationCodeHash(const QByteArray &password) -{ - return pkcs5Pbkdf2HmacSha256(password, - QKnxByteArray("device-authentication-code.1.secure.ip.knx.org", 46), 0x10000, 16); -} - -/*! - Performs a bytewise XOR operation on the arguments \a left and \a right. - If the arguments are not equal in size, the function uses only the shorter - array for the operation. If \a adjust is set to \c true, the arrays are made - equal by padding them with \c 0x00 bytes. -*/ -QKnxByteArray QKnxCryptographicEngine::XOR(const QKnxByteArray &left, const QKnxByteArray &right, - bool adjust) -{ - QKnxByteArray result(adjust ? qMax(left.size(), right.size()) : qMin(left.size(), right.size()), - Qt::Uninitialized); - for (int i = result.size() - 1; i >= 0; --i) - result.set(i, left.value(i, 0x00) ^ right.value(i, 0x00)); - return result; -} - -namespace QKnxPrivate -{ - static QKnxByteArray b0(quint48 sequence, const QKnxByteArray &serial, quint16 tag, quint16 len) - { - return QKnxUtils::QUint48::bytes(sequence) + serial + QKnxUtils::QUint16::bytes(tag) - + QKnxUtils::QUint16::bytes(len); - } - - static QKnxByteArray ctr0(quint48 sequence, const QKnxByteArray &serial, quint16 tag) - { - return QKnxPrivate::b0(sequence, serial, tag, 0xff00); - } - - static QKnxByteArray encrypt(const QKnxByteArray &key, const QKnxByteArray &data) - { - QSharedPointer<EVP_CIPHER_CTX> ctxPtr(q_EVP_CIPHER_CTX_new(), q_EVP_CIPHER_CTX_free); - if (ctxPtr.isNull()) - return {}; - - const auto ctx = ctxPtr.data(); - const auto c = q_EVP_aes_128_cbc(); - if (q_EVP_CipherInit_ex(ctx, c, nullptr, nullptr, nullptr, 0x01) <= 0) - return {}; - - if (q_EVP_CIPHER_CTX_set_padding(ctx, 0) <= 0) - return {}; - - Q_ASSERT(q_EVP_CIPHER_CTX_iv_length(ctx) == 16); - Q_ASSERT(q_EVP_CIPHER_CTX_key_length(ctx) == 16); - - static const quint8 iv[16] { 0x00 }; - if (q_EVP_CipherInit_ex(ctx, nullptr, nullptr, key.constData(), iv, 0x01) <= 0) - return {}; - - int outl, offset = 0; - QKnxByteArray out(data.size() + q_EVP_CIPHER_block_size(c), 0x00); - if (q_EVP_CipherUpdate(ctx, out.data(), &outl, data.constData(), data.size()) <= 0) - return {}; - offset += outl; - - if (q_EVP_CipherFinal_ex(ctx, out.data() + offset, &outl) <= 0) - return {}; - offset += outl; - - return out.mid(offset - 16, 16); - } - - static QKnxByteArray processMAC(const QKnxByteArray &key, const QKnxByteArray &mac, - quint48 sequenceNumber, const QKnxByteArray &serialNumber, quint16 messageTag) - { - if (key.isEmpty() || mac.isEmpty()) - return {}; - - auto Ctr0 = QKnxPrivate::ctr0(sequenceNumber, - (serialNumber.isEmpty() ? QKnxByteArray(6, 0x00) : serialNumber), messageTag); - - return QKnxCryptographicEngine::XOR(QKnxPrivate::encrypt(key, Ctr0), mac); - } - - static QKnxByteArray processPayload(const QKnxByteArray &key, const QKnxByteArray &payload, - quint48 sequenceNumber, const QKnxByteArray &serialNumber, quint16 messageTag) - { - if (key.isEmpty() || payload.isEmpty()) - return {}; - - auto Ctr0 = QKnxPrivate::ctr0(sequenceNumber, - (serialNumber.isEmpty() ? QKnxByteArray(6, 0x00) : serialNumber), messageTag); - - QKnxByteArray ctrArray; - for (int i = 0; i < (payload.size() + 15) >> 4; ++i) { - Ctr0.set(15, Ctr0.at(15) + 1); - ctrArray += QKnxPrivate::encrypt(key, Ctr0); - } - - return QKnxCryptographicEngine::XOR(ctrArray, payload, false); - } -} - -/*! - Computes a message authentication code (MAC) using the given \a key, - \a header, and \a id for the given \a data. Returns an array of bytes that - represent the calculated MAC or an empty byte array in case of an error. - - \note The \a sequenceNumber, \a serialNumber, and \a messageTag values - are required to calculate a valid MAC for KNXnet/IP secure wrapper frames. - For all other types of secure frames, the possibly given values are ignored - and \c 0 is used instead. For timer notify frames, \e {default-constructed} - values are used instead of the \a id and \a data values. - - For an example of using this function, see - \l {Calculating Message Authentication Codes}. -*/ -QKnxByteArray QKnxCryptographicEngine::calculateMessageAuthenticationCode(const QKnxByteArray &key, - const QKnxNetIpFrameHeader &header, quint16 id, const QKnxByteArray &data, - quint48 sequenceNumber, const QKnxByteArray &serialNumber, quint16 messageTag) -{ - if (key.isEmpty() || !header.isValid()) - return {}; - - auto sn = (serialNumber.isEmpty() ? QKnxByteArray(6, 0x00) : serialNumber); - - QKnxByteArray B0, B; - if (header.serviceType() == QKnxNetIp::ServiceType::SecureWrapper) { - if (data.isEmpty()) - return {}; - - const auto A = header.bytes() + QKnxUtils::QUint16::bytes(id); - B0 = QKnxPrivate::b0(sequenceNumber, sn, messageTag, data.size()); - B = B0 + QKnxUtils::QUint16::bytes(A.size()) + A + data; - } else if (header.serviceType() == QKnxNetIp::ServiceType::SessionResponse - || header.serviceType() == QKnxNetIp::ServiceType::SessionAuthenticate) { - if (data.isEmpty()) - return {}; - - const auto A = header.bytes() + QKnxUtils::QUint16::bytes(id); - B0 = QKnxPrivate::b0(sequenceNumber, sn, messageTag, 0); - B = B0 + QKnxUtils::QUint16::bytes(A.size() + data.size()) + A + data; - } else if (header.serviceType() == QKnxNetIp::ServiceType::TimerNotify) { - const auto A = header.bytes(); - B0 = QKnxPrivate::b0(sequenceNumber, sn, messageTag, 0); - B = B0 + QKnxUtils::QUint16::bytes(A.size()) + A; - } - - if (B.isEmpty()) - return {}; - B.resize(B.size() + (16 - (B.size() % 16))); // pad to multiple of 16 - - return QKnxPrivate::encrypt(key, B); -} - -/*! - Encrypts the given KNXnet/IP frame \a frame with the given key \a key, - sequence number \a sequenceNumber, serial number \a serialNumber, and - message tag \a messageTag. Returns an array of bytes that represent the - encrypted frame or an empty byte array in case of an error or invalid - KNXnet/IP frame \a frame. -*/ -QKnxByteArray QKnxCryptographicEngine::encryptSecureWrapperPayload(const QKnxByteArray &key, - const QKnxNetIpFrame &frame, quint48 sequenceNumber, const QKnxByteArray &serialNumber, - quint16 messageTag) -{ - if (!frame.isValid()) - return {}; - return QKnxPrivate::processPayload(key, frame.bytes(), sequenceNumber, serialNumber, messageTag); -} - -/*! - Decrypts the given KNXnet/IP frame \a frame with the given key \a key, - sequence number \a sequenceNumber, serial number \a serialNumber, and - message tag \a messageTag. Returns an array of bytes that represent the - decrypted frame or an empty byte array in case of an error. -*/ -QKnxByteArray QKnxCryptographicEngine::decryptSecureWrapperPayload(const QKnxByteArray &key, - const QKnxByteArray &frame, quint48 sequenceNumber, const QKnxByteArray &serialNumber, - quint16 messageTag) -{ - return QKnxPrivate::processPayload(key, frame, sequenceNumber, serialNumber, messageTag); -} - -/*! - Encrypts the given message authentication code (MAC) \a mac with the given - key \a key, sequence number \a sequenceNumber, serial number \a serialNumber, - and message tag \a messageTag. Returns an array of bytes that represent - the encrypted MAC or an empty byte array in case of an error. - - \note The \a sequenceNumber, \a serialNumber and \a messageTag are mandatory - to properly encrypt the MAC for KNXnet/IP secure wrapper frame, for all other - secure frames the default value of \c 0 can be used. -*/ -QKnxByteArray QKnxCryptographicEngine::encryptMessageAuthenticationCode(const QKnxByteArray &key, - const QKnxByteArray &mac, quint48 sequenceNumber, const QKnxByteArray &serialNumber, - quint16 messageTag) -{ - return QKnxPrivate::processMAC(key, mac, sequenceNumber, serialNumber, messageTag); -} - -/*! - Decrypts the given message authentication code (MAC) \a mac with the given - key \a key, sequence number \a sequenceNumber, serial number \a serialNumber, - and message tag \a messageTag. Returns an array of bytes that represent - the decrypted MAC or an empty byte array in case of an error. - - \note The \a sequenceNumber, \a serialNumber and \a messageTag values are - required to properly decrypt the MAC for KNXnet/IP secure wrapper frame. - For all other secure frames, the default value of \c 0 can be used. - -*/ -QKnxByteArray QKnxCryptographicEngine::decryptMessageAuthenticationCode(const QKnxByteArray &key, - const QKnxByteArray &mac, quint48 sequenceNumber, const QKnxByteArray &serialNumber, - quint16 messageTag) -{ - return QKnxPrivate::processMAC(key, mac, sequenceNumber, serialNumber, messageTag); -} - -/*! - Returns the hash code derived from the user chosen password \a password, - with the given \a salt and \a iterations. - The value of \a derivedKeyLength should be in the range \c 0 to \c 32. -*/ -QKnxByteArray QKnxCryptographicEngine::pkcs5Pbkdf2HmacSha256(const QByteArray &password, - const QKnxByteArray &salt, qint32 iterations, quint8 derivedKeyLength) -{ - if (derivedKeyLength > 32) - return {}; - - if (!qt_QKnxOpenSsl->supportsSsl()) - return {}; - - QKnxByteArray out(derivedKeyLength, 0x00); - if (q_PKCS5_PBKDF2_HMAC(password.constData(), password.size(), salt.constData(), salt.size(), - iterations, q_EVP_sha256(), out.size(), out.data()) <= 0) { - return {}; - } - return out; -} - -QT_END_NAMESPACE diff --git a/src/knx/ssl/qknxkeyring.cpp b/src/knx/ssl/qknxkeyring.cpp new file mode 100644 index 0000000..ff31b7b --- /dev/null +++ b/src/knx/ssl/qknxkeyring.cpp @@ -0,0 +1,515 @@ +/****************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtKnx module. +** +** $QT_BEGIN_LICENSE:GPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 or (at your option) any later version +** approved by the KDE Free Qt Foundation. The licenses are as published by +** the Free Software Foundation and appearing in the file LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +******************************************************************************/ + +#include "qknxcryptographicengine.h" +#include "private/qknxkeyring_p.h" +#include "private/qknxssl_p.h" + +#include <QtCore/qcryptographichash.h> +#include <QtCore/qregularexpression.h> +#include <QtCore/qfile.h> +#include <QtCore/qxmlstream.h> + +#include <algorithm> + +QT_BEGIN_NAMESPACE + +/*! + \internal + \inmodule QtKnx + \namespace QKnx::Ets::Keyring +*/ + +namespace QKnx { namespace Ets { namespace Keyring { + +namespace QKnxPrivate +{ + void writeBytes(QByteArray *dest, const QStringRef &source) + { + dest->append(quint8(source.size())); + dest->append(source.toString().toUtf8()); + } + + void writeBytes(QByteArray *dest, const QByteArray &source) + { + dest->append(quint8(source.size())); + dest->append(source); + } + + static void readElement(QXmlStreamReader *reader, QByteArray *bytes) + { + bytes->append(0x01); + QKnxPrivate::writeBytes(bytes, reader->name()); + + auto attributes = reader->attributes(); + std::sort(attributes.begin(), attributes.end(), + [](const QXmlStreamAttribute &lhs, const QXmlStreamAttribute &rhs) { + return lhs.name() < rhs.name(); + }); + + for (auto attribute : qAsConst(attributes)) { + QKnxPrivate::writeBytes(bytes, attribute.name()); + QKnxPrivate::writeBytes(bytes, attribute.value()); + } + + while (!reader->atEnd()) { + auto token = reader->readNext(); + if (token == QXmlStreamReader::StartElement) + QKnxPrivate::readElement(reader, bytes); + if (token == QXmlStreamReader::EndElement) + bytes->append(0x02); + } + } + + static bool fetchAttr(const QXmlStreamAttributes &attributes, const QString &attrName, + QStringRef *value, QXmlStreamReader *reader) + { + if (!value || !reader) + return false; + + *value = attributes.value(attrName); + if (value->isNull()) + reader->raiseError(QKnxKeyring::tr("Invalid or empty attribute '%1'.").arg(attrName)); + return !reader->hasError(); + } + + static bool setString(const QString &name, const QStringRef &attr, int maxSize, + QString *field, QXmlStreamReader *reader, bool pedantic) + { + if (!reader || !field) + return false; + + if (pedantic && attr.size() > maxSize) { + reader->raiseError(QKnxKeyring::tr("Pedantic error: Invalid value for attribute '%1', " + "maximum length is %2 characters, got: '%3'.").arg(name).arg(maxSize).arg(attr.size())); + } else { + *field = attr.toString(); + } + return !reader->hasError(); + } + + bool setString(const QString &name, const QStringRef &attr, const QStringList &list, + QString *field, QXmlStreamReader *reader, bool pedantic) + { + if (!reader || !field) + return false; + + if (pedantic && !list.contains(attr.toString())) { + reader->raiseError(QKnxKeyring::tr("Pedantic error: Invalid value for attribute '%1', " + "expected '%2', got: '%3'.").arg(name, list.join(QLatin1String(", ")))); + } else { + *field = attr.toString(); + } + return !reader->hasError(); + } +} + +const char oneBlockBase64[29] = "^[A-Za-z0-9\\+/]{21}[AQgw]==$"; +// const char twoBlockBase64[40] = "^[A-Za-z0-9\\+/]{42}[AEIMQUYcgkosw048]=$"; + +const char multicast[128] = "^2(2[4-9]|3[0-9])\\.((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|" + "[0-9])\\.){2}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$"; + +const char individualAddress[74] = "^((1[0-5]|[0-9])\\.){2}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]" + "[0-9]|[0-9])$"; + +bool QKnxBackbone::parseElement(QXmlStreamReader *reader, bool pedantic) +{ + if (!reader || !reader->isStartElement()) + return false; + + if (reader->name() == QStringLiteral("Backbone")) { + auto attrs = reader->attributes(); + + QStringRef attr; // mandatory attributes + if (!QKnxPrivate::fetchAttr(attrs, QStringLiteral("MulticastAddress"), &attr, reader)) + return false; + MulticastAddress = attr.toString(); + + if (!QKnxPrivate::fetchAttr(attrs, QStringLiteral("Latency"), &attr, reader)) + return false; + Latency = attr.toUShort(); + + if (!QKnxPrivate::fetchAttr(attrs, QStringLiteral("Key"), &attr, reader)) + return false; + Key = attr.toUtf8(); + + if (pedantic) { + if (!QRegularExpression(QLatin1String(multicast)).match(MulticastAddress).hasMatch()) { + reader->raiseError(tr("The 'MulticastAddress' attribute is invalid. The Pattern " + "constraint failed, got: '%1'.").arg(MulticastAddress)); + return false; + } + + if (Latency > 8000) { + reader->raiseError(tr("The 'Latency' attribute is invalid. The MaxInclusive " + "constraint failed, got: '%1', MaxInclusive: '8000'.").arg(Latency)); + return false; + } + + if (!QRegularExpression(QLatin1String(oneBlockBase64)).match(attr).hasMatch()) { + reader->raiseError(tr("The 'Key' attribute is invalid. The Pattern constraint " + "failed, got: '%1'.").arg(QString::fromUtf8(Key))); + return false; + } + } + } else { + reader->raiseError(tr("Expected element <Backbone>, got: <%1>.").arg(reader->name() + .toString())); + } + return !reader->hasError(); +} + +bool QKnxInterface::QKnxGroup::parseElement(QXmlStreamReader *reader, bool pedantic) +{ + if (!reader || !reader->isStartElement()) + return false; + + if (reader->name() == QStringLiteral("Group")) { + auto attrs = reader->attributes(); + + QStringRef attr; // mandatory attributes + if (!QKnxPrivate::fetchAttr(attrs, QStringLiteral("Address"), &attr, reader)) + return false; + Address = attr.toUShort(); + + if (!QKnxPrivate::fetchAttr(attrs, QStringLiteral("Senders"), &attr, reader)) + return false; + Senders = attr.toString().split(QLatin1Char(' ')).toVector(); + if (pedantic) { + QRegularExpression regExp; + regExp.setPattern(QLatin1String(individualAddress)); + for (auto sender : qAsConst(Senders)) { + if (!regExp.match(sender).hasMatch()) { + reader->raiseError(tr("The 'Senders' attribute is invalid. The Pattern " + "constraint failed, got: '%1'.").arg(sender)); + return false; + } + } + } + } else { + reader->raiseError(tr("Expected element <Group>, got: <%1>.").arg(reader->name() + .toString())); + } + return !reader->hasError(); +} + +bool QKnxInterface::parseElement(QXmlStreamReader *reader, bool pedantic) +{ + if (!reader || !reader->isStartElement()) + return false; + + if (reader->name() == QStringLiteral("Interface")) { + auto attrs = reader->attributes(); + + QStringRef attr; // mandatory attribute + if (!QKnxPrivate::fetchAttr(attrs, QLatin1String("Type"), &attr, reader)) + return false; + if (!QKnxPrivate::setString(QLatin1String("Type"), attr, { + QStringLiteral("Backbone"), + QStringLiteral("Tunneling"), + QStringLiteral("USB") + }, &Type, reader, pedantic)) return false; + + // optional attributes, TODO: pedantic + Host = attrs.value(QStringLiteral("Host")).toString(); + IndividualAddress = attrs.value(QStringLiteral("IndividualAddress")).toString(); + UserID = attrs.value(QStringLiteral("UserID")).toUShort(); + Password = attrs.value(QStringLiteral("Password")).toUtf8(); + Authentication = attrs.value(QStringLiteral("Authentication")).toUtf8(); + + // children + while (!reader->atEnd() && !reader->hasError()) { + auto tokenType = reader->readNext(); + if (tokenType == QXmlStreamReader::TokenType::StartElement) { + if (reader->name() == QStringLiteral("Group")) { + QKnxInterface::QKnxGroup group; + if (!group.parseElement(reader, pedantic)) + return false; + Group.append(group); + } + } else if (tokenType == QXmlStreamReader::TokenType::EndElement) { + if (reader->name() == QLatin1String("Interface")) + break; + } + } + } else { + reader->raiseError(tr("Expected element <Interface>, got: <%1>.").arg(reader->name() + .toString())); + } + return !reader->hasError(); +} + +bool QKnxGroupAddresses::QKnxGroup::parseElement(QXmlStreamReader *reader, bool pedantic) +{ + if (!reader || !reader->isStartElement()) + return false; + + if (reader->name() == QStringLiteral("Group")) { + auto attrs = reader->attributes(); + + QStringRef attr; // mandatory attributes + if (!QKnxPrivate::fetchAttr(attrs, QStringLiteral("Address"), &attr, reader)) + return false; + Address = attr.toUShort(); + + if (!QKnxPrivate::fetchAttr(attrs, QStringLiteral("Key"), &attr, reader)) + return false; + Key = attr.toUtf8(); + if (pedantic && !QRegularExpression(QLatin1String(oneBlockBase64)).match(attr).hasMatch()) { + reader->raiseError(tr("The 'Key' attribute is invalid. The Pattern " + "constraint failed, got: '%1'.").arg(QString::fromUtf8(Key))); + return false; + } + } else { + reader->raiseError(tr("Expected element <Group>, got: <%1>.").arg(reader->name() + .toString())); + } + return !reader->hasError(); +} + +bool QKnxGroupAddresses::parseElement(QXmlStreamReader *reader, bool pedantic) +{ + if (!reader || !reader->isStartElement()) + return false; + + if (reader->name() == QStringLiteral("GroupAddresses")) { + // children + while (!reader->atEnd() && !reader->hasError()) { + auto tokenType = reader->readNext(); + if (tokenType == QXmlStreamReader::TokenType::StartElement) { + if (reader->name() == QStringLiteral("Group")) { + QKnxGroupAddresses::QKnxGroup group; + if (!group.parseElement(reader, pedantic)) + return false; + Group.append(group); + } + } else if (tokenType == QXmlStreamReader::TokenType::EndElement) { + if (reader->name() == QLatin1String("GroupAddresses")) + break; + } + } + } else { + reader->raiseError(tr("Expected element <GroupAddresses>, got: <%1>.").arg(reader->name() + .toString())); + } + return !reader->hasError() && Group.size() >= 1; +} + +bool QKnxDevice::parseElement(QXmlStreamReader *reader, bool pedantic) +{ + if (!reader || !reader->isStartElement()) + return false; + + if (reader->name() == QStringLiteral("Device")) { + auto attrs = reader->attributes(); + + QStringRef attr; // mandatory attribute + if (!QKnxPrivate::fetchAttr(attrs, QLatin1String("IndividualAddress"), &attr, reader)) + return false; + IndividualAddress = attr.toString(); + if (pedantic && !QRegularExpression(QLatin1String(individualAddress)) + .match(IndividualAddress).hasMatch()) { + reader->raiseError(tr("The 'IndividualAddress' attribute is invalid. The " + "Pattern constraint failed, got: '%1'.").arg(IndividualAddress)); + return false; + } + + // optional attributes, TODO: pedantic + ToolKey = attrs.value(QStringLiteral("ToolKey")).toUtf8(); + SequenceNumber = attrs.value(QStringLiteral("SequenceNumber")).toULongLong(); + ManagementPassword = attrs.value(QStringLiteral("ManagementPassword")).toUtf8(); + Authentication = attrs.value(QStringLiteral("Authentication")).toUtf8(); + } else { + reader->raiseError(tr("Expected element <Device>, got: <%1>.").arg(reader->name() + .toString())); + } + return !reader->hasError(); +} + +bool QKnxDevices::parseElement(QXmlStreamReader *reader, bool pedantic) +{ + if (!reader || !reader->isStartElement()) + return false; + + if (reader->name() == QStringLiteral("Devices")) { + // children + while (!reader->atEnd() && !reader->hasError()) { + auto tokenType = reader->readNext(); + if (tokenType == QXmlStreamReader::TokenType::StartElement) { + if (reader->name() == QStringLiteral("Device")) { + QKnxDevice device; + if (!device.parseElement(reader, pedantic)) + return false; + Device.append(device); + } + } else if (tokenType == QXmlStreamReader::TokenType::EndElement) { + if (reader->name() == QLatin1String("Devices")) + break; + } + } + } else { + reader->raiseError(tr("Expected element <Devices>, got: <%1>.").arg(reader->name() + .toString())); + } + return !reader->hasError() && Device.size() >= 1; +} + +bool QKnxKeyring::parseElement(QXmlStreamReader *reader, bool pedantic) +{ + if (!reader || !reader->readNextStartElement()) + return false; + + if (reader->name() == QStringLiteral("Keyring")) { + auto attrs = reader->attributes(); + + QStringRef attr; // mandatory attributes + if (!QKnxPrivate::fetchAttr(attrs, QLatin1String("Project"), &attr, reader)) + return false; + if (!QKnxPrivate::setString(QLatin1String("Project"), attr, 255, &Project, reader, pedantic)) + return false; + + if (!QKnxPrivate::fetchAttr(attrs, QStringLiteral("CreatedBy"), &attr, reader)) + return false; + CreatedBy = attr.toString(); + + if (!QKnxPrivate::fetchAttr(attrs, QStringLiteral("Created"), &attr, reader)) + return false; + Created = attr.toString(); + + if (!QKnxPrivate::fetchAttr(attrs, QStringLiteral("Signature"), &attr, reader)) + return false; + Signature = attr.toUtf8(); + if (pedantic) { + if (!QRegularExpression(QLatin1String(oneBlockBase64)).match(attr).hasMatch()) { + reader->raiseError(tr("The 'Signature' attribute is invalid. The Pattern " + "constraint failed, got: '%1'.").arg(attr)); + return false; + } + } + + // children + while (!reader->atEnd() && !reader->hasError()) { + auto tokenType = reader->readNext(); + if (tokenType == QXmlStreamReader::TokenType::StartElement) { + if (reader->name() == QStringLiteral("Backbone")) { + if (pedantic && Backbone.size() >= 1) { + reader->raiseError(tr("Pedantic error: Encountered element <Backbone> " + "more than once.")); + return false; + } + QKnxBackbone backbone; + if (!backbone.parseElement(reader, pedantic)) + return false; + Backbone.append(backbone); + } else if (reader->name() == QStringLiteral("Interface")) { + QKnxInterface interface; + if (!interface.parseElement(reader, pedantic)) + return false; + Interface.append(interface); + } else if (reader->name() == QStringLiteral("GroupAddresses")) { + if (pedantic && GroupAddresses.size() >= 1) { + reader->raiseError(tr("Pedantic error: Encountered element " + "<GroupAddresses> more than once.")); + return false; + } + QKnxGroupAddresses groupAddresses; + if (!groupAddresses.parseElement(reader, pedantic)) + return false; + GroupAddresses.append(groupAddresses); + } else if (reader->name() == QStringLiteral("Devices")) { + if (pedantic && Devices.size() >= 1) { + reader->raiseError(tr("Pedantic error: Encountered element " + "<Devices> more than once.")); + return false; + } + QKnxDevices devices; + if (!devices.parseElement(reader, pedantic)) + return false; + Devices.append(devices); + } + } else if (tokenType == QXmlStreamReader::TokenType::EndElement) { + if (reader->name() == QLatin1String("Keyring")) + break; + } + } + } else { + reader->raiseError(tr("Expected element <Keyring>, got: <%1>.").arg(reader->name() + .toString())); + } + return !reader->hasError(); +} + +bool QKnxKeyring::validate(QXmlStreamReader *reader, const QKnxByteArray &pwHash) const +{ + if (!reader || !reader->readNextStartElement()) + return false; + + if (reader->name() != QStringLiteral("Keyring")) + return false; + + QByteArray bytes; + bytes.append(0x01); + + QKnxPrivate::writeBytes(&bytes, reader->name()); + + auto attributes = reader->attributes(); + if (attributes.isEmpty()) + return false; + + auto signature = attributes.value({}, QStringLiteral("Signature")).toUtf8(); + if (signature.isEmpty()) + return false; + + std::sort(attributes.begin(), attributes.end(), + [](const QXmlStreamAttribute &lhs, const QXmlStreamAttribute &rhs) { + return lhs.name() < rhs.name(); + }); + + for (auto attribute : attributes) { + if (attribute.name() == QStringLiteral("xmlns")) + continue; + if (attribute.name() == QStringLiteral("Signature")) + continue; + + QKnxPrivate::writeBytes(&bytes, attribute.name()); + QKnxPrivate::writeBytes(&bytes, attribute.value()); + } + + if (reader->readNextStartElement()) + QKnxPrivate::readElement(reader, &bytes); + QKnxPrivate::writeBytes(&bytes, pwHash.toByteArray().toBase64()); + + return QKnxCryptographicEngine::hashSha256(bytes).left(16) + == QKnxByteArray::fromByteArray(QByteArray::fromBase64(signature)); +} + +}}} // QKnx::Ets::Keyring + +QT_END_NAMESPACE diff --git a/src/knx/ssl/qknxkeyring_p.h b/src/knx/ssl/qknxkeyring_p.h new file mode 100644 index 0000000..752131b --- /dev/null +++ b/src/knx/ssl/qknxkeyring_p.h @@ -0,0 +1,160 @@ +/****************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtKnx module. +** +** $QT_BEGIN_LICENSE:GPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 or (at your option) any later version +** approved by the KDE Free Qt Foundation. The licenses are as published by +** the Free Software Foundation and appearing in the file LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +******************************************************************************/ + +#ifndef QKNXKEYRING_P_H +#define QKNXKEYRING_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt KNX API. It exists for the convenience +// of the Qt KNX implementation. This header file may change from version +// to version without notice, or even be removed. +// +// We mean it. +// + +#include <QtCore/qcoreapplication.h> +#include <QtCore/qvector.h> +#include <QtCore/qxmlstream.h> + +#include <QtKnx/qtknxglobal.h> + +QT_BEGIN_NAMESPACE + +namespace QKnx { namespace Ets { namespace Keyring { + +struct Q_KNX_EXPORT QKnxBackbone +{ + Q_DECLARE_TR_FUNCTIONS(QKnxBackbone) + +public: + QString MulticastAddress; // mandatory, pattern 2(2[4-9]|3[0-9])\.((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){2}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9]) + quint16 Latency; // mandatory, min. value 0, 8000 value max. + QByteArray Key; // mandatory, Base64 encoded, pattern [A-Za-z0-9\+/]{21}[AQgw]== + + bool parseElement(QXmlStreamReader *reader, bool pedantic); +}; + +struct Q_KNX_EXPORT QKnxInterface +{ + Q_DECLARE_TR_FUNCTIONS(QKnxInterface) + +public: + QString Type; // mandatory, Backbone, Tunneling, USB + + QString Host; // optional + QString IndividualAddress; // optional, pattern ((1[0-5]|[0-9])\.){2}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9]) + quint8 UserID; // optional + QByteArray Password; // optional, Base64 encoded, pattern [A-Za-z0-9\+/]{42}[AEIMQUYcgkosw048]= + QByteArray Authentication; // optional, Base64 encoded, pattern [A-Za-z0-9\+/]{42}[AEIMQUYcgkosw048]= + + struct Q_KNX_EXPORT QKnxGroup + { + Q_DECLARE_TR_FUNCTIONS(QKnxGroup) + + public: + quint16 Address; // mandatory + QVector<QString> Senders; // mandatory + + bool parseElement(QXmlStreamReader *reader, bool pedantic); + }; + QVector<QKnxGroup> Group; // 0..n + + bool parseElement(QXmlStreamReader *reader, bool pedantic); +}; + +struct Q_KNX_EXPORT QKnxGroupAddresses +{ + Q_DECLARE_TR_FUNCTIONS(QKnxGroupAddresses) + +public: + struct Q_KNX_EXPORT QKnxGroup + { + Q_DECLARE_TR_FUNCTIONS(QKnxGroup) + + public: + quint16 Address; // mandatory + QByteArray Key; // mandatory, pattern [A-Za-z0-9\+/]{21}[AQgw]== + + bool parseElement(QXmlStreamReader *reader, bool pedantic); + }; + QVector<QKnxGroup> Group; // 1..n + + bool parseElement(QXmlStreamReader *reader, bool pedantic); +}; + +struct Q_KNX_EXPORT QKnxDevice +{ + Q_DECLARE_TR_FUNCTIONS(QKnxDevice) + +public: + QString IndividualAddress; //mandatory, pattern ((1[0-5]|[0-9])\.){2}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9]) + QByteArray ToolKey; // optional, Base64 encoded, pattern [A-Za-z0-9\+/]{21}[AQgw]== + quint48 SequenceNumber; // optional + QByteArray ManagementPassword; // optional, Base64 encoded, pattern [A-Za-z0-9\+/]{42}[AEIMQUYcgkosw048]= + QByteArray Authentication; // optional, Base64 encoded, pattern [A-Za-z0-9\+/]{42}[AEIMQUYcgkosw048]= + + bool parseElement(QXmlStreamReader *reader, bool pedantic); +}; + +struct Q_KNX_EXPORT QKnxDevices +{ + Q_DECLARE_TR_FUNCTIONS(QKnxDevices) + +public: + QVector<QKnxDevice> Device; // 0..n + bool parseElement(QXmlStreamReader *reader, bool pedantic); +}; + +struct Q_KNX_EXPORT QKnxKeyring +{ + Q_DECLARE_TR_FUNCTIONS(QKnxKeyring) + +public: + QString Project; // mandatory, 50 character max. + QString Created; // mandatory, xs:dateTime + QString CreatedBy; // mandatory + QByteArray Signature; // mandatory, Base64 encoded, pattern [A-Za-z0-9\+/]{21}[AQgw]== + + QVector<QKnxBackbone> Backbone; // 0..1 + QVector<QKnxInterface> Interface; // 0..n + QVector<QKnxGroupAddresses> GroupAddresses; // 0..1 + QVector<QKnxDevices> Devices; // 0..1 + + bool parseElement(QXmlStreamReader *reader, bool pedantic); + bool validate(QXmlStreamReader *reader, const QKnxByteArray &pwHash) const; +}; + +}}} // QKnx::Ets::Keyring + +QT_END_NAMESPACE + +#endif diff --git a/src/knx/ssl/qknxsecurekey.cpp b/src/knx/ssl/qknxsecurekey.cpp new file mode 100644 index 0000000..5aadbf9 --- /dev/null +++ b/src/knx/ssl/qknxsecurekey.cpp @@ -0,0 +1,389 @@ +/****************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtKnx module. +** +** $QT_BEGIN_LICENSE:GPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 or (at your option) any later version +** approved by the KDE Free Qt Foundation. The licenses are as published by +** the Free Software Foundation and appearing in the file LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +******************************************************************************/ + +#include "qknxsecurekey.h" +#include "qknxcryptographicengine.h" + +#include <QtNetwork/private/qtnetworkglobal_p.h> + +#if QT_CONFIG(opensslv11) +# include "private/qsslsocket_openssl_symbols_p.h" +# include "private/qsslsocket_openssl11_symbols_p.h" +#endif + +QT_BEGIN_NAMESPACE + +class QKnxSecureKeyData : public QSharedData +{ +public: + QKnxSecureKeyData() = default; + ~QKnxSecureKeyData() + { +#if QT_CONFIG(opensslv11) + if (m_evpPKey) + q_EVP_PKEY_free(m_evpPKey); +#endif + } + + bool isTypeValid() const + { + return m_type >= QKnxSecureKey::Type::Private && m_type < QKnxSecureKey::Type::Invalid; + } + +#if QT_CONFIG(opensslv11) + EVP_PKEY *m_evpPKey{ nullptr }; +#endif + QKnxSecureKey::Type m_type{ QKnxSecureKey::Type::Invalid }; +}; + +/*! + \since 5.13 + \inmodule QtKnx + + \class QKnxSecureKey + \ingroup qtknx-general-classes + + \brief The QKnxSecureKey class represents the elliptic-curve secure key to + be used with the elliptic curve Diffie-Hellman (ECDH) key agreement scheme. + + This class is part of the Qt KNX module and currently available as a + Technology Preview, and therefore the API and functionality provided + by the class may be subject to change at any time without prior notice. +*/ + +/*! + \enum QKnxSecureKey::Type + + Describes the types of keys QKnxSecureKey supports. + + \value Private A private key. + \value Public A public key. + \value Invalid An invalid key, please do not use. +*/ + +/*! + Constructs an empty invalid secure key. +*/ +QKnxSecureKey::QKnxSecureKey() + : d_ptr(new QKnxSecureKeyData) +{} + +/*! + Destroys the secure key and releases all allocated resources. +*/ +QKnxSecureKey::~QKnxSecureKey() = default; + +/*! + Returns the type of the key: \l {QKnxSecureKey::Public}{Public} or + \l {QKnxSecureKey::Private}{Private}. +*/ +QKnxSecureKey::Type QKnxSecureKey::type() const +{ + return d_ptr->m_type; +} + +/*! + Returns \c true if this is a default constructed secure key; otherwise + returns \c false. A secure key is considered \c null if it contains no + initialized values. +*/ +bool QKnxSecureKey::isNull() const +{ +#if QT_CONFIG(opensslv11) + return d_ptr->m_evpPKey == nullptr; +#else + return true; +#endif +} + +/*! + Returns \c true if OpenSSL is available and if the key contains initialized + values, otherwise returns \c false. +*/ +bool QKnxSecureKey::isValid() const +{ +#if QT_CONFIG(opensslv11) + return !isNull() && d_ptr->isTypeValid() && QKnxCryptographicEngine::supportsCryptography(); +#else + return false; +#endif +} + +/*! + Returns an array of bytes that represent the Curve25519 raw secure key. + + \note The function will return an empty byte array for the private + key unless OpenSLL with version 1.1.1a is used as back-end. +*/ +QKnxByteArray QKnxSecureKey::bytes() const +{ +#if QT_CONFIG(opensslv11) + if (!isValid()) + return {}; + + if (d_ptr->m_type == Type::Private) { + size_t len = 32; + QKnxByteArray ba(int(len), 0); + if (q_EVP_PKEY_get_raw_private_key(d_ptr->m_evpPKey, ba.data(), &len) <= 0) + return {}; // preferred, no other way possible + return ba; + } + + size_t len = 32; + QKnxByteArray pub(32, Qt::Uninitialized); + if (q_EVP_PKEY_get_raw_public_key(d_ptr->m_evpPKey, pub.data(), &len) > 0) + return pub; // preferred way + + pub.resize(q_i2d_PUBKEY(d_ptr->m_evpPKey, nullptr)); + auto tmp = pub.data(); + q_i2d_PUBKEY(d_ptr->m_evpPKey, &tmp); + return pub.right(32); +#else + return {}; +#endif +} + +/*! + Constructs the Curve25519 secure key from the byte array \a data starting + at position \a index inside the array and sets the key type to \a type if + OpenSSL is available and no error occurs; + otherwise returns a \e {default-constructed key} which can be invalid. +*/ +QKnxSecureKey QKnxSecureKey::fromBytes(QKnxSecureKey::Type type, const QKnxByteArray &data, + quint16 index) +{ +#if QT_CONFIG(opensslv11) + auto ba = data.mid(index, 32); + if (ba.size() < 32) + return {}; + + if (!QKnxCryptographicEngine::supportsCryptography()) + return {}; + + QKnxSecureKey key; + key.d_ptr->m_type = type; + + if (type == Type::Private) { + key.d_ptr->m_evpPKey = q_EVP_PKEY_new_raw_private_key(NID_X25519, nullptr, ba.constData(), + ba.size()); // preferred way + if (key.d_ptr->m_evpPKey) + return key; + + static const auto pkcs8 = QKnxByteArray::fromHex("302e020100300506032b656e04220420"); + auto tmp = pkcs8 + ba; // PKCS #8 is a standard syntax for storing private key information + + BIO *bio = nullptr; + if ((bio = q_BIO_new_mem_buf(reinterpret_cast<void *> (tmp.data()), tmp.size()))) + key.d_ptr->m_evpPKey = q_d2i_PrivateKey_bio(bio, nullptr); + q_BIO_free(bio); + return key; + } + + if (type == Type::Public) { + key.d_ptr->m_evpPKey = q_EVP_PKEY_new_raw_public_key(NID_X25519, nullptr, ba.constData(), + ba.size()); // preferred way + if (key.d_ptr->m_evpPKey) + return key; + + key.d_ptr->m_evpPKey = q_EVP_PKEY_new(); + if (q_EVP_PKEY_set_type(key.d_ptr->m_evpPKey, NID_X25519) <= 0) + return {}; + + if (q_EVP_PKEY_set1_tls_encodedpoint(key.d_ptr->m_evpPKey, ba.constData(), ba.size()) <= 0) + return {}; + return key; + } +#else + Q_UNUSED(type) + Q_UNUSED(data) + Q_UNUSED(index) +#endif + return {}; +} + +/*! + Returns a new valid private key if OpenSSL is available and no error occurs. +*/ +QKnxSecureKey QKnxSecureKey::generatePrivateKey() +{ + QKnxSecureKey key; +#if QT_CONFIG(opensslv11) + if (!QKnxCryptographicEngine::supportsCryptography()) + return key; + + if (auto *pctx = q_EVP_PKEY_CTX_new_id(NID_X25519, nullptr)) { + q_EVP_PKEY_keygen_init(pctx); + key.d_ptr->m_type = Type::Private; + q_EVP_PKEY_keygen(pctx, &key.d_ptr->m_evpPKey); + q_EVP_PKEY_CTX_free(pctx); + } +#endif + return key; +} + +/*! + Returns a new valid public key with the given private key \a privateKey if + OpenSSL is available and no error occurs. +*/ +QKnxSecureKey QKnxSecureKey::publicKeyFromPrivate(const QKnxSecureKey &privateKey) +{ + QKnxSecureKey key; +#if QT_CONFIG(opensslv11) + if (privateKey.type() == QKnxSecureKey::Type::Private && privateKey.isValid()) { + q_EVP_PKEY_up_ref(privateKey.d_ptr->m_evpPKey); + key.d_ptr->m_type = Type::Public; + key.d_ptr->m_evpPKey = privateKey.d_ptr->m_evpPKey; + } +#else + Q_UNUSED(privateKey) +#endif + return key; +} + +/*! + \overload publicKeyFromPrivate() +*/ +QKnxSecureKey QKnxSecureKey::publicKeyFromPrivate(const QKnxByteArray &privateKey) +{ + return QKnxSecureKey::publicKeyFromPrivate(QKnxSecureKey::fromBytes(QKnxSecureKey::Type::Private, + privateKey)); +} + +/*! + Sets \a privateKey to a new valid private key and \a publicKey to a new + valid public key derived from the freshly generated private key if OpenSSL + is available and no error occurs. +*/ +void QKnxSecureKey::generateKeys(QKnxSecureKey *privateKey, QKnxSecureKey *publicKey) +{ + if (!privateKey || !publicKey) + return; + + *privateKey = generatePrivateKey(); + *publicKey = publicKeyFromPrivate(*privateKey); +} + +/*! + Derives and returns the shared secret from the given private key + \a privateKey and the peer's public key \a peerPublicKey if OpenSSL + is available and no error occurs; + otherwise returns a \l {default-constructed value} which can be empty. +*/ +QKnxByteArray QKnxSecureKey::sharedSecret(const QKnxSecureKey &privateKey, + const QKnxSecureKey &peerPublicKey) +{ +#if QT_CONFIG(opensslv11) + if (privateKey.type() != QKnxSecureKey::Type::Private || !privateKey.isValid()) + return {}; + + if (peerPublicKey.type() != QKnxSecureKey::Type::Public || !peerPublicKey.isValid()) + return {}; + + auto evpPKeyCtx = q_EVP_PKEY_CTX_new(privateKey.d_ptr->m_evpPKey, nullptr); + if (!evpPKeyCtx) + return {}; + + struct ScopedFree final + { + ScopedFree(EVP_PKEY_CTX *key) : m_evpPKeyCtx(key) {} + ~ScopedFree() { q_EVP_PKEY_CTX_free(m_evpPKeyCtx); } + EVP_PKEY_CTX *m_evpPKeyCtx = nullptr; + } _{ evpPKeyCtx }; + + if (q_EVP_PKEY_derive_init(evpPKeyCtx) <= 0) + return {}; + + if (q_EVP_PKEY_derive_set_peer(evpPKeyCtx, peerPublicKey.d_ptr->m_evpPKey) <= 0) + return {}; + + size_t keylen = 0; + if (q_EVP_PKEY_derive(evpPKeyCtx, nullptr, &keylen) <= 0) + return {}; + + QKnxByteArray ba(int(keylen), 0); + if (q_EVP_PKEY_derive(evpPKeyCtx, ba.data(), &keylen) <= 0) + return {}; + return ba; +#else + Q_UNUSED(privateKey) + Q_UNUSED(peerPublicKey) + return {}; +#endif +} + +/*! + \overload sharedSecret() +*/ +QKnxByteArray QKnxSecureKey::sharedSecret(const QKnxByteArray &privateKey, + const QKnxByteArray &peerPublicKey) +{ + return sharedSecret(QKnxSecureKey::fromBytes(QKnxSecureKey::Type::Private, privateKey), + QKnxSecureKey::fromBytes(QKnxSecureKey::Type::Public, peerPublicKey)); +} + + +/*! + Constructs a copy of \a other. +*/ +QKnxSecureKey::QKnxSecureKey(const QKnxSecureKey &other) + : d_ptr(other.d_ptr) +{} + +/*! + Assigns the specified \a other to this object. +*/ +QKnxSecureKey &QKnxSecureKey::operator=(const QKnxSecureKey &other) +{ + d_ptr = other.d_ptr; + return *this; +} + +/*! + Returns \c true if this key and the given \a other key are equal; otherwise + returns \c false. +*/ +bool QKnxSecureKey::operator==(const QKnxSecureKey &other) const +{ + return d_ptr == other.d_ptr +#if QT_CONFIG(opensslv11) + || (d_ptr->m_evpPKey == other.d_ptr->m_evpPKey && d_ptr->m_type == other.d_ptr->m_type) +#endif + || (bytes() == other.bytes()); +} + +/*! + Returns \c true if this key and the given \a other key are not equal; + otherwise returns \c false. +*/ +bool QKnxSecureKey::operator!=(const QKnxSecureKey &other) const +{ + return !operator==(other); +} + +QT_END_NAMESPACE diff --git a/src/knx/ssl/qknxsecurekey.h b/src/knx/ssl/qknxsecurekey.h new file mode 100644 index 0000000..2fbe0cd --- /dev/null +++ b/src/knx/ssl/qknxsecurekey.h @@ -0,0 +1,86 @@ +/****************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtKnx module. +** +** $QT_BEGIN_LICENSE:GPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 or (at your option) any later version +** approved by the KDE Free Qt Foundation. The licenses are as published by +** the Free Software Foundation and appearing in the file LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +******************************************************************************/ + +#ifndef QKNXSECUREKEY_H +#define QKNXSECUREKEY_H + +#include <QtCore/qshareddata.h> + +#include <QtKnx/qknxbytearray.h> +#include <QtKnx/qtknxglobal.h> + +QT_BEGIN_NAMESPACE + +class QKnxSecureKeyData; +class Q_KNX_EXPORT QKnxSecureKey final +{ +public: + enum class Type : quint8 { + Private, + Public, + Invalid + }; + QKnxSecureKey::Type type() const; + + QKnxSecureKey(); + ~QKnxSecureKey(); + + bool isNull() const; + bool isValid() const; + + QKnxByteArray bytes() const; + static QKnxSecureKey fromBytes(QKnxSecureKey::Type type, + const QKnxByteArray &data, + quint16 index = 0); + + static QKnxSecureKey generatePrivateKey(); + + static QKnxSecureKey publicKeyFromPrivate(const QKnxSecureKey &privateKey); + static QKnxSecureKey publicKeyFromPrivate(const QKnxByteArray &privateKey); + + static void generateKeys(QKnxSecureKey *privateKey, QKnxSecureKey *publicKey); + + static QKnxByteArray sharedSecret(const QKnxSecureKey &privateKey, + const QKnxSecureKey &peerPublicKey); + static QKnxByteArray sharedSecret(const QKnxByteArray &privateKey, + const QKnxByteArray &peerPublicKey); + + QKnxSecureKey(const QKnxSecureKey &other); + QKnxSecureKey &operator=(const QKnxSecureKey &other); + + bool operator==(const QKnxSecureKey &other) const; + bool operator!=(const QKnxSecureKey &other) const; + +private: + QSharedDataPointer<QKnxSecureKeyData> d_ptr; +}; + +QT_END_NAMESPACE + +#endif diff --git a/src/knx/ssl/qknxssl_openssl.cpp b/src/knx/ssl/qknxssl_openssl.cpp new file mode 100644 index 0000000..a110efb --- /dev/null +++ b/src/knx/ssl/qknxssl_openssl.cpp @@ -0,0 +1,178 @@ +/****************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtKnx module. +** +** $QT_BEGIN_LICENSE:GPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 or (at your option) any later version +** approved by the KDE Free Qt Foundation. The licenses are as published by +** the Free Software Foundation and appearing in the file LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +******************************************************************************/ + +#include "qknxssl_p.h" + +#include <private/qtnetworkglobal_p.h> + +#if QT_CONFIG(opensslv11) +# include "private/qsslsocket_openssl_symbols_p.h" +# include "private/qsslsocket_openssl11_symbols_p.h" +#endif + +#include <QtCore/qmutex.h> + +QT_BEGIN_NAMESPACE + +class QKnxOpenSsl +{ +public: + static bool supportsSsl(); + static long sslLibraryVersionNumber(); + +protected: + static bool ensureLibraryLoaded(); + +private: + static bool s_libraryLoaded; + static bool s_libraryEnabled; +}; + +bool QKnxOpenSsl::s_libraryLoaded = false; +bool QKnxOpenSsl::s_libraryEnabled = false; + +Q_GLOBAL_STATIC(QKnxOpenSsl, qt_QKnxOpenSsl) +Q_GLOBAL_STATIC_WITH_ARGS(QMutex, qt_knxOpenSslInitMutex, (QMutex::Recursive)) + +/*! + \internal +*/ +bool QKnxOpenSsl::supportsSsl() +{ +#if QT_CONFIG(opensslv11) + if (!q_resolveOpenSslSymbols()) + return false; + + const QMutexLocker locker(qt_knxOpenSslInitMutex); + if (!s_libraryLoaded) { + s_libraryLoaded = true; + + // Initialize OpenSSL. + if (q_OPENSSL_init_ssl(0, nullptr) != 1) + return false; + q_SSL_load_error_strings(); + q_OpenSSL_add_all_algorithms(); + + // Initialize OpenSSL's random seed. + if (!q_RAND_status()) { + qWarning("Random number generator not seeded, disabling SSL support"); + return false; + } + + if (q_EVP_PKEY_type(NID_X25519) == NID_undef) { + qWarning("The X25519 algorithm is not supported, disabling SSL support"); + return false; + } + s_libraryEnabled = true; + } + return s_libraryEnabled; +#else + Q_UNUSED(qt_knxOpenSslInitMutex) + return false; +#endif +} + +/*! + \internal +*/ +long QKnxOpenSsl::sslLibraryVersionNumber() +{ +#if QT_CONFIG(opensslv11) + if (supportsSsl()) + return q_OpenSSL_version_num(); +#endif + return 0; +} + +/*! + \internal +*/ +bool QKnxSsl::supportsCryptography() +{ + return qt_QKnxOpenSsl->supportsSsl(); +} + +/*! + \internal +*/ +long QKnxSsl::sslLibraryVersionNumber() +{ + return qt_QKnxOpenSsl->sslLibraryVersionNumber(); +} + +/*! + \internal +*/ +QKnxByteArray QKnxSsl::doCrypt(const QKnxByteArray &key, const QKnxByteArray &iv, + const QKnxByteArray &data, Mode mode) +{ +#if QT_CONFIG(opensslv11) + if (!qt_QKnxOpenSsl->supportsSsl()) + return {}; + + QSharedPointer<EVP_CIPHER_CTX> ctxPtr(q_EVP_CIPHER_CTX_new(), q_EVP_CIPHER_CTX_free); + if (ctxPtr.isNull()) + return {}; + q_EVP_CIPHER_CTX_reset(ctxPtr.data()); + + const auto ctx = ctxPtr.data(); + const auto c = q_EVP_aes_128_cbc(); + if (q_EVP_CipherInit_ex(ctx, c, nullptr, nullptr, nullptr, mode) <= 0) + return {}; + + if (q_EVP_CIPHER_CTX_set_padding(ctx, 0) <= 0) + return {}; + + Q_ASSERT(q_EVP_CIPHER_CTX_iv_length(ctx) == 16); + Q_ASSERT(q_EVP_CIPHER_CTX_key_length(ctx) == 16); + + if (q_EVP_CipherInit_ex(ctx, nullptr, nullptr, key.constData(), iv.constData(), mode) <= 0) + return {}; + + int outl, offset = 0; + QKnxByteArray out(data.size() + q_EVP_CIPHER_block_size(c), 0x00); + if (q_EVP_CipherUpdate(ctx, out.data(), &outl, data.constData(), data.size()) <= 0) + return {}; + offset += outl; + + if (q_EVP_CipherFinal_ex(ctx, out.data() + offset, &outl) <= 0) + return {}; + offset += outl; + + return out.left(offset); +#else + Q_UNUSED(key) + Q_UNUSED(iv) + Q_UNUSED(data) + Q_UNUSED(mode) + return {}; +#endif +} + +QT_END_NAMESPACE diff --git a/src/knx/ssl/qknxcryptographicdata_p.h b/src/knx/ssl/qknxssl_p.h index e963779..5de059e 100644 --- a/src/knx/ssl/qknxcryptographicdata_p.h +++ b/src/knx/ssl/qknxssl_p.h @@ -1,6 +1,6 @@ /****************************************************************************** ** -** Copyright (C) 2018 The Qt Company Ltd. +** Copyright (C) 2019 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the QtKnx module. @@ -27,8 +27,8 @@ ** ******************************************************************************/ -#ifndef QKNXCRYPTOGRAPHICDATA_P_H -#define QKNXCRYPTOGRAPHICDATA_P_H +#ifndef QKNXSSL_P_H +#define QKNXSSL_P_H // // W A R N I N G @@ -41,39 +41,24 @@ // We mean it. // -#include <QtKnx/qtknxglobal.h> -#include <QtKnx/private/qsslsocket_openssl_symbols_p.h> -#include <QtKnx/private/qsslsocket_openssl11_symbols_p.h> +#include <QtKnx/qknxbytearray.h> QT_BEGIN_NAMESPACE -class Q_KNX_EXPORT QKnxOpenSsl +class QKnxSsl { public: - static bool supportsSsl(); - static long sslLibraryVersionNumber(); - static QString sslLibraryVersionString(); - static long sslLibraryBuildVersionNumber(); - static QString sslLibraryBuildVersionString(); - -protected: - static bool ensureLibraryLoaded(); - -private: - static bool s_libraryLoaded; - static bool s_libraryEnabled; -}; - -class QKnxCurve25519KeyData : public QSharedData -{ -public: - QKnxCurve25519KeyData() = default; - ~QKnxCurve25519KeyData() + enum Mode { - q_EVP_PKEY_free(m_evpPKey); - } + Decrypt = 0x00, + Encrypt = 0x01 + }; + + static bool supportsCryptography(); + static long sslLibraryVersionNumber(); - EVP_PKEY *m_evpPKey { nullptr }; + static QKnxByteArray doCrypt(const QKnxByteArray &key, const QKnxByteArray &iv, + const QKnxByteArray &data, Mode mode); }; QT_END_NAMESPACE diff --git a/src/knx/ssl/qssl.cpp b/src/knx/ssl/qssl.cpp deleted file mode 100644 index 7cfa6c3..0000000 --- a/src/knx/ssl/qssl.cpp +++ /dev/null @@ -1,191 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the QtNetwork module of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 3 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL3 included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 3 requirements -** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 2.0 or (at your option) the GNU General -** Public license version 3 or any later version approved by the KDE Free -** Qt Foundation. The licenses are as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-2.0.html and -** https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - -#include "qssl_p.h" - -QT_BEGIN_NAMESPACE - -Q_LOGGING_CATEGORY(lcSsl, "qt.network.ssl"); - -/*! \namespace QSsl - - \brief The QSsl namespace declares enums common to all SSL classes in Qt Network. - \since 4.3 - - \ingroup network - \ingroup ssl - \inmodule QtNetwork -*/ - -/*! - \enum QSsl::KeyType - - Describes the two types of keys QSslKey supports. - - \value PrivateKey A private key. - \value PublicKey A public key. -*/ - -/*! - \enum QSsl::KeyAlgorithm - - Describes the different key algorithms supported by QSslKey. - - \value Rsa The RSA algorithm. - \value Dsa The DSA algorithm. - \value Ec The Elliptic Curve algorithm - \value Opaque A key that should be treated as a 'black box' by QSslKey. - - The opaque key facility allows applications to add support for facilities - such as PKCS#11 that Qt does not currently offer natively. -*/ - -/*! - \enum QSsl::EncodingFormat - - Describes supported encoding formats for certificates and keys. - - \value Pem The PEM format. - \value Der The DER format. -*/ - -/*! - \enum QSsl::AlternativeNameEntryType - - Describes the key types for alternative name entries in QSslCertificate. - - \value EmailEntry An email entry; the entry contains an email address that - the certificate is valid for. - - \value DnsEntry A DNS host name entry; the entry contains a host name - entry that the certificate is valid for. The entry may contain wildcards. - - \note In Qt 4, this enum was called \c {AlternateNameEntryType}. That name - is deprecated in Qt 5. - - \sa QSslCertificate::subjectAlternativeNames() -*/ - -/*! - \typedef QSsl::AlternateNameEntryType - \obsolete - - Use QSsl::AlternativeNameEntryType instead. -*/ - -/*! - \enum QSsl::SslProtocol - - Describes the protocol of the cipher. - - \value SslV3 SSLv3. When using the WinRT backend this option will also enable TLSv1.0 - \value SslV2 SSLv2. Note, SSLv2 support was removed in OpenSSL 1.1. - \value TlsV1_0 TLSv1.0 - \value TlsV1_0OrLater TLSv1.0 and later versions. This option is not available when using the WinRT backend due to platform limitations. - \value TlsV1 Obsolete, means the same as TlsV1_0 - \value TlsV1_1 TLSv1.1. When using the WinRT backend this option will also enable TLSv1.0. - \value TlsV1_1OrLater TLSv1.1 and later versions. This option is not available when using the WinRT backend due to platform limitations. - \value TlsV1_2 TLSv1.2. When using the WinRT backend this option will also enable TLSv1.0 and TLSv1.1. - \value TlsV1_2OrLater TLSv1.2 and later versions. This option is not available when using the WinRT backend due to platform limitations. - \value UnknownProtocol The cipher's protocol cannot be determined. - \value AnyProtocol The socket understands SSLv2, SSLv3, TLSv1.0 and all - supported later versions of TLS. This value is used by QSslSocket only. - \value TlsV1SslV3 On the client side, this will send - a TLS 1.0 Client Hello, enabling TLSv1_0 and SSLv3 connections. - On the server side, this will enable both SSLv3 and TLSv1_0 connections. - \value SecureProtocols The default option, using protocols known to be secure; - currently behaves similar to TlsV1Ssl3 except denying SSLv3 connections that does - not upgrade to TLS. - - \note most servers understand both SSL and TLS, but it is recommended to use - TLS only for security reasons. However, SSL and TLS are not compatible with - each other: if you get unexpected handshake failures, verify that you chose - the correct setting for your protocol. -*/ - -/*! - \enum QSsl::SslOption - - Describes the options that can be used to control the details of - SSL behaviour. These options are generally used to turn features off - to work around buggy servers. - - \value SslOptionDisableEmptyFragments Disables the insertion of empty - fragments into the data when using block ciphers. When enabled, this - prevents some attacks (such as the BEAST attack), however it is - incompatible with some servers. - \value SslOptionDisableSessionTickets Disables the SSL session ticket - extension. This can cause slower connection setup, however some servers - are not compatible with the extension. - \value SslOptionDisableCompression Disables the SSL compression - extension. When enabled, this allows the data being passed over SSL to - be compressed, however some servers are not compatible with this - extension. - \value SslOptionDisableServerNameIndication Disables the SSL server - name indication extension. When enabled, this tells the server the virtual - host being accessed allowing it to respond with the correct certificate. - \value SslOptionDisableLegacyRenegotiation Disables the older insecure - mechanism for renegotiating the connection parameters. When enabled, this - option can allow connections for legacy servers, but it introduces the - possibility that an attacker could inject plaintext into the SSL session. - \value SslOptionDisableSessionSharing Disables SSL session sharing via - the session ID handshake attribute. - \value SslOptionDisableSessionPersistence Disables storing the SSL session - in ASN.1 format as returned by QSslConfiguration::sessionTicket(). Enabling - this feature adds memory overhead of approximately 1K per used session - ticket. - \value SslOptionDisableServerCipherPreference Disables selecting the cipher - chosen based on the servers preferences rather than the order ciphers were - sent by the client. This option is only relevant to server sockets, and is - only honored by the OpenSSL backend. - - By default, SslOptionDisableEmptyFragments is turned on since this causes - problems with a large number of servers. SslOptionDisableLegacyRenegotiation - is also turned on, since it introduces a security risk. - SslOptionDisableCompression is turned on to prevent the attack publicised by - CRIME. - SslOptionDisableSessionPersistence is turned on to optimize memory usage. - The other options are turned off. - - \note Availability of above options depends on the version of the SSL - backend in use. -*/ - - -QT_END_NAMESPACE diff --git a/src/knx/ssl/qssl.h b/src/knx/ssl/qssl.h deleted file mode 100644 index c2a468c..0000000 --- a/src/knx/ssl/qssl.h +++ /dev/null @@ -1,114 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the QtNetwork module of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 3 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL3 included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 3 requirements -** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 2.0 or (at your option) the GNU General -** Public license version 3 or any later version approved by the KDE Free -** Qt Foundation. The licenses are as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-2.0.html and -** https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - - -#ifndef QSSL_H -#define QSSL_H - -#include <QtNetwork/qtnetworkglobal.h> -#include <QtCore/QFlags> - -QT_BEGIN_NAMESPACE - - -namespace QSsl { - enum KeyType { - PrivateKey, - PublicKey - }; - - enum EncodingFormat { - Pem, - Der - }; - - enum KeyAlgorithm { - Opaque, - Rsa, - Dsa, - Ec - }; - - enum AlternativeNameEntryType { - EmailEntry, - DnsEntry - }; - -#if QT_DEPRECATED_SINCE(5,0) - typedef AlternativeNameEntryType AlternateNameEntryType; -#endif - - enum SslProtocol { - SslV3, - SslV2, - TlsV1_0, -#if QT_DEPRECATED_SINCE(5,0) - TlsV1 = TlsV1_0, -#endif - TlsV1_1, - TlsV1_2, - AnyProtocol, - TlsV1SslV3, - SecureProtocols, - - TlsV1_0OrLater, - TlsV1_1OrLater, - TlsV1_2OrLater, - - UnknownProtocol = -1 - }; - - enum SslOption { - SslOptionDisableEmptyFragments = 0x01, - SslOptionDisableSessionTickets = 0x02, - SslOptionDisableCompression = 0x04, - SslOptionDisableServerNameIndication = 0x08, - SslOptionDisableLegacyRenegotiation = 0x10, - SslOptionDisableSessionSharing = 0x20, - SslOptionDisableSessionPersistence = 0x40, - SslOptionDisableServerCipherPreference = 0x80 - }; - Q_DECLARE_FLAGS(SslOptions, SslOption) -} - -Q_DECLARE_OPERATORS_FOR_FLAGS(QSsl::SslOptions) - -QT_END_NAMESPACE - -#endif // QSSL_H diff --git a/src/knx/ssl/qssl_p.h b/src/knx/ssl/qssl_p.h deleted file mode 100644 index 83ccdc7..0000000 --- a/src/knx/ssl/qssl_p.h +++ /dev/null @@ -1,65 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the QtNetwork module of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 3 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL3 included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 3 requirements -** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 2.0 or (at your option) the GNU General -** Public license version 3 or any later version approved by the KDE Free -** Qt Foundation. The licenses are as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-2.0.html and -** https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - - -#ifndef QSSL_P_H -#define QSSL_P_H - - -// -// W A R N I N G -// ------------- -// -// This file is not part of the Qt API. It exists for the convenience -// of qsslcertificate.cpp. This header file may change from version to version -// without notice, or even be removed. -// -// We mean it. -// - -#include <QtNetwork/private/qtnetworkglobal_p.h> -#include <QtCore/QLoggingCategory> - -QT_BEGIN_NAMESPACE - -Q_DECLARE_LOGGING_CATEGORY(lcSsl) - -QT_END_NAMESPACE - -#endif // QSSL_P_H diff --git a/src/knx/ssl/qsslsocket_openssl11_symbols_p.h b/src/knx/ssl/qsslsocket_openssl11_symbols_p.h index d8b8b3b..0fe4ad3 100644 --- a/src/knx/ssl/qsslsocket_openssl11_symbols_p.h +++ b/src/knx/ssl/qsslsocket_openssl11_symbols_p.h @@ -151,7 +151,5 @@ int q_EVP_PKEY_get_raw_private_key(const EVP_PKEY *pkey, unsigned char *priv, si EVP_PKEY *q_EVP_PKEY_new_raw_private_key(int type, ENGINE *e, const unsigned char *priv, size_t len); const EVP_CIPHER *q_EVP_aes_128_cbc(void); -int q_PKCS5_PBKDF2_HMAC(const char *pass, int passlen, const unsigned char *salt, int saltlen, - int iter, const EVP_MD *digest, int keylen, unsigned char *out); #endif diff --git a/src/knx/ssl/qsslsocket_openssl_symbols.cpp b/src/knx/ssl/qsslsocket_openssl_symbols.cpp index 7502dee..c89cafb 100644 --- a/src/knx/ssl/qsslsocket_openssl_symbols.cpp +++ b/src/knx/ssl/qsslsocket_openssl_symbols.cpp @@ -54,7 +54,6 @@ ** ****************************************************************************/ -#include "qssl_p.h" #include "qsslsocket_openssl_symbols_p.h" #ifdef Q_OS_WIN @@ -118,6 +117,8 @@ QT_BEGIN_NAMESPACE possibly with a different version of OpenSSL. */ +Q_LOGGING_CATEGORY(lcSsl, "qt.network.ssl"); + #ifndef QT_LINKED_OPENSSL namespace { @@ -579,8 +580,6 @@ DEFINEFUNC(void, PKCS12_free, PKCS12 *pkcs12, pkcs12, return, DUMMYARG) DEFINEFUNC(const EVP_CIPHER *, EVP_aes_128_cbc, DUMMYARG, DUMMYARG, return nullptr, return) DEFINEFUNC2(int, EVP_CIPHER_CTX_set_padding, EVP_CIPHER_CTX *x, x, int padding, padding, return 0, return) - DEFINEFUNC8(int, PKCS5_PBKDF2_HMAC, const char *pass, pass, int passlen, passlen, const unsigned char *salt, salt, \ - int saltlen, saltlen, int iter, iter, const EVP_MD *digest, digest, int keylen, keylen, unsigned char *out, out, return 0, return) DEFINEFUNC(const EVP_MD *, EVP_sha256, DUMMYARG, DUMMYARG, return nullptr, return) #endif @@ -1304,7 +1303,6 @@ bool q_resolveOpenSslSymbols() RESOLVEFUNC(EVP_aes_128_cbc) RESOLVEFUNC(EVP_CIPHER_CTX_set_padding) - RESOLVEFUNC(PKCS5_PBKDF2_HMAC) RESOLVEFUNC(EVP_sha256) #endif diff --git a/src/knx/ssl/qsslsocket_openssl_symbols_p.h b/src/knx/ssl/qsslsocket_openssl_symbols_p.h index 49a7e95..c5ae895 100644 --- a/src/knx/ssl/qsslsocket_openssl_symbols_p.h +++ b/src/knx/ssl/qsslsocket_openssl_symbols_p.h @@ -103,10 +103,13 @@ #include <openssl/dh.h> #endif +#include <QtCore/qloggingcategory.h> #include <QtCore/qglobal.h> QT_BEGIN_NAMESPACE +Q_DECLARE_LOGGING_CATEGORY(lcSsl) + #define DUMMYARG #if !defined QT_LINKED_OPENSSL diff --git a/src/knx/ssl/ssl.pri b/src/knx/ssl/ssl.pri index 2a0938f..35f2730 100644 --- a/src/knx/ssl/ssl.pri +++ b/src/knx/ssl/ssl.pri @@ -1,16 +1,17 @@ -qtConfig(opensslv11) { # OpenSSL 1.1 support is required. - HEADERS += ssl/qssl.h \ - ssl/qssl_p.h \ - ssl/qknxcurve25519.h \ - ssl/qknxcryptographicdata_p.h +HEADERS += ssl/qknxcryptographicengine.h \ + ssl/qknxsecurekey.h \ + ssl/qknxssl_p.h \ + ssl/qknxkeyring_p.h - SOURCES += ssl/qssl.cpp \ - ssl/qknxcurve25519.cpp +SOURCES += ssl/qknxcryptographicengine.cpp \ + ssl/qknxsecurekey.cpp \ + ssl/qknxssl_openssl.cpp \ + ssl/qknxkeyring.cpp - HEADERS += ssl/qsslsocket_openssl_symbols_p.h +qtConfig(opensslv11) { # OpenSSL 1.1 support is required. SOURCES += ssl/qsslsocket_openssl_symbols.cpp - - HEADERS += ssl/qsslsocket_openssl11_symbols_p.h + HEADERS += ssl/qsslsocket_openssl_symbols_p.h \ + ssl/qsslsocket_openssl11_symbols_p.h QMAKE_CXXFLAGS += -DOPENSSL_API_COMPAT=0x10100000L diff --git a/tests/auto/auto.pro b/tests/auto/auto.pro index be105c4..2d09f65 100644 --- a/tests/auto/auto.pro +++ b/tests/auto/auto.pro @@ -46,7 +46,5 @@ SUBDIRS += cmake \ qknxnetipsecuredservicefamiliesdib \ qknxnetipsessionrequest \ qknxnetipsessionresponse \ - qknxnetiprouter - -QT_FOR_CONFIG += network -qtConfig(opensslv11):SUBDIRS+=qknxcryptographicengine + qknxnetiprouter \ + qknxcryptographicengine diff --git a/tests/auto/qknxcryptographicengine/qknxcryptographicengine.pro b/tests/auto/qknxcryptographicengine/qknxcryptographicengine.pro index e91f20d..5b9b9bb 100644 --- a/tests/auto/qknxcryptographicengine/qknxcryptographicengine.pro +++ b/tests/auto/qknxcryptographicengine/qknxcryptographicengine.pro @@ -1,6 +1,6 @@ TARGET = tst_qknxcryptographicengine -QT = core testlib knx knx-private +QT = core testlib knx CONFIG += testcase c++11 CONFIG -= app_bundle diff --git a/tests/auto/qknxcryptographicengine/tst_qknxcryptographicengine.cpp b/tests/auto/qknxcryptographicengine/tst_qknxcryptographicengine.cpp index eb03cfd..8ab0b8c 100644 --- a/tests/auto/qknxcryptographicengine/tst_qknxcryptographicengine.cpp +++ b/tests/auto/qknxcryptographicengine/tst_qknxcryptographicengine.cpp @@ -28,14 +28,14 @@ #include <QtCore/qdebug.h> #include <QtCore/qloggingcategory.h> -#include <QtKnx/qknxcurve25519.h> +#include <QtKnx/qknxcryptographicengine.h> #include <QtKnx/qknxlinklayerframebuilder.h> #include <QtKnx/qknxnetiproutingindication.h> #include <QtKnx/qknxnetipsecurewrapper.h> #include <QtKnx/qknxnetipsessionauthenticate.h> #include <QtKnx/qknxnetipsessionresponse.h> #include <QtKnx/qknxnetipsessionstatus.h> -#include <QtKnx/private/qknxcryptographicdata_p.h> +#include <QtKnx/qknxnetiptimernotify.h> #include <QtTest/qtest.h> QT_BEGIN_NAMESPACE @@ -60,95 +60,101 @@ private slots: void testPublicKey() { - QKnxCurve25519PublicKey key; + QKnxSecureKey key; QCOMPARE(key.isNull(), true); QCOMPARE(key.isValid(), false); QCOMPARE(key.bytes(), QKnxByteArray()); + QCOMPARE(key.type(), QKnxSecureKey::Type::Invalid); - if (QKnxOpenSsl::sslLibraryVersionNumber() < 0x1010000fL) + if (QKnxCryptographicEngine::sslLibraryVersionNumber() < 0x1010000fL) return; auto bytes = QKnxByteArray::fromHex("bdf099909923143ef0a5de0b3be3687b" "c5bd3cf5f9e6f901699cd870ec1ff824"); - key = QKnxCurve25519PublicKey::fromBytes(bytes); + key = QKnxSecureKey::fromBytes(QKnxSecureKey::Type::Public, bytes); QCOMPARE(key.isNull(), false); QCOMPARE(key.isValid(), true); QCOMPARE(key.bytes(), bytes); + QCOMPARE(key.type(), QKnxSecureKey::Type::Public); bytes = QKnxByteArray::fromHex("0aa227b4fd7a32319ba9960ac036ce0e" "5c4507b5ae55161f1078b1dcfb3cb631"); - key = QKnxCurve25519PublicKey::fromBytes(bytes); + key = QKnxSecureKey::fromBytes(QKnxSecureKey::Type::Public, bytes); QCOMPARE(key.isNull(), false); QCOMPARE(key.isValid(), true); QCOMPARE(key.bytes(), bytes); + QCOMPARE(key.type(), QKnxSecureKey::Type::Public); // derive public key from private key - key = { QKnxCurve25519PrivateKey::fromBytes(QKnxByteArray::fromHex("b8fabd62665d8b9e8a9d" - "8b1f4bca42c8c2789a6110f50e9dd785b3ede883f378")) }; + key = QKnxSecureKey::publicKeyFromPrivate(QKnxByteArray::fromHex("b8fabd62665d8b9e8a9d" + "8b1f4bca42c8c2789a6110f50e9dd785b3ede883f378")); QCOMPARE(key.isNull(), false); QCOMPARE(key.isValid(), true); QCOMPARE(key.bytes(), bytes); + QCOMPARE(key.type(), QKnxSecureKey::Type::Public); } void testPrivateKey() { - if (QKnxOpenSsl::sslLibraryVersionNumber() >= 0x1010000fL) { - QKnxCurve25519PrivateKey key; - QCOMPARE(key.isNull(), false); - QCOMPARE(key.isValid(), true); - if (QKnxOpenSsl::sslLibraryVersionNumber() > 0x1010008fL) - QCOMPARE(key.bytes().size(), 32); - - auto bytes = QKnxByteArray::fromHex("b8fabd62665d8b9e8a9d8b1f4bca42c8" - "c2789a6110f50e9dd785b3ede883f378"); - key = QKnxCurve25519PrivateKey::fromBytes(bytes); - QCOMPARE(key.isNull(), false); - QCOMPARE(key.isValid(), true); - if (QKnxOpenSsl::sslLibraryVersionNumber() > 0x1010008fL) - QCOMPARE(key.bytes(), bytes); - - bytes = QKnxByteArray::fromHex("68c1744813f4e65cf10cca671caa1336" - "a796b4ac40cc5cf2655674225c1e5264"); - key = QKnxCurve25519PrivateKey::fromBytes(bytes); - QCOMPARE(key.isNull(), false); - QCOMPARE(key.isValid(), true); - if (QKnxOpenSsl::sslLibraryVersionNumber() > 0x1010008fL) - QCOMPARE(key.bytes(), bytes); - - } else { - QKnxCurve25519PrivateKey key; - QCOMPARE(key.isNull(), true); - QCOMPARE(key.isValid(), false); - QCOMPARE(key.bytes(), QKnxByteArray()); - } + QKnxSecureKey key; + QCOMPARE(key.isNull(), true); + QCOMPARE(key.isValid(), false); + QCOMPARE(key.bytes(), QKnxByteArray()); + QCOMPARE(key.type(), QKnxSecureKey::Type::Invalid); + + if (QKnxCryptographicEngine::sslLibraryVersionNumber() < 0x1010000fL) + return; + + key = QKnxSecureKey::generatePrivateKey(); + QCOMPARE(key.isNull(), false); + QCOMPARE(key.isValid(), true); + if (QKnxCryptographicEngine::sslLibraryVersionNumber() >= 0x1010101fL) + QCOMPARE(key.bytes().size(), 32); + QCOMPARE(key.type(), QKnxSecureKey::Type::Private); + + auto bytes = QKnxByteArray::fromHex("b8fabd62665d8b9e8a9d8b1f4bca42c8" + "c2789a6110f50e9dd785b3ede883f378"); + key = QKnxSecureKey::fromBytes(QKnxSecureKey::Type::Private, bytes); + QCOMPARE(key.isNull(), false); + QCOMPARE(key.isValid(), true); + if (QKnxCryptographicEngine::sslLibraryVersionNumber() >= 0x1010101fL) + QCOMPARE(key.bytes(), bytes); + QCOMPARE(key.type(), QKnxSecureKey::Type::Private); + + bytes = QKnxByteArray::fromHex("68c1744813f4e65cf10cca671caa1336" + "a796b4ac40cc5cf2655674225c1e5264"); + key = QKnxSecureKey::fromBytes(QKnxSecureKey::Type::Private, bytes); + QCOMPARE(key.isNull(), false); + QCOMPARE(key.isValid(), true); + if (QKnxCryptographicEngine::sslLibraryVersionNumber() >= 0x1010101fL) + QCOMPARE(key.bytes(), bytes); + QCOMPARE(key.type(), QKnxSecureKey::Type::Private); } void testSharedSecret() { - if (QKnxOpenSsl::sslLibraryVersionNumber() < 0x1010000fL) + if (QKnxCryptographicEngine::sslLibraryVersionNumber() < 0x1010000fL) return; auto pubBytes = QKnxByteArray::fromHex("0aa227b4fd7a32319ba9960ac036ce0e" "5c4507b5ae55161f1078b1dcfb3cb631"); - auto pubKey = QKnxCurve25519PublicKey::fromBytes(pubBytes); + auto pubKey = QKnxSecureKey::fromBytes(QKnxSecureKey::Type::Public, pubBytes); auto privBytes = QKnxByteArray::fromHex("68c1744813f4e65cf10cca671caa1336" "a796b4ac40cc5cf2655674225c1e5264"); - auto privKey = QKnxCurve25519PrivateKey::fromBytes(privBytes); + auto privKey = QKnxSecureKey::fromBytes(QKnxSecureKey::Type::Private, privBytes); - auto secret = QKnxCryptographicEngine::sharedSecret(pubKey, privKey); + auto secret = QKnxSecureKey::sharedSecret(privKey, pubKey); QCOMPARE(secret, QKnxByteArray::fromHex("d801525217618f0da90a4ff22148aee0" "ff4c19b430e8081223ffe99c81a98b05")); pubBytes = QKnxByteArray::fromHex("bdf099909923143ef0a5de0b3be3687b" "c5bd3cf5f9e6f901699cd870ec1ff824"); - pubKey = QKnxCurve25519PublicKey::fromBytes(pubBytes); privBytes = QKnxByteArray::fromHex("b8fabd62665d8b9e8a9d8b1f4bca42c8" "c2789a6110f50e9dd785b3ede883f378"); - privKey = QKnxCurve25519PrivateKey::fromBytes(privBytes); - secret = QKnxCryptographicEngine::sharedSecret(pubKey, privKey); + secret = QKnxSecureKey::sharedSecret(privBytes, pubBytes); QCOMPARE(secret, QKnxByteArray::fromHex("d801525217618f0da90a4ff22148aee0" "ff4c19b430e8081223ffe99c81a98b05")); } @@ -174,20 +180,23 @@ private slots: void testMessageAuthenticationCode() { - if (QKnxOpenSsl::sslLibraryVersionNumber() < 0x1010000fL) + if (QKnxCryptographicEngine::sslLibraryVersionNumber() < 0x1010000fL) return; /* This test more or less follows KNX AN156 - Annex A */ - auto clientPublicKey = QKnxCurve25519PublicKey::fromBytes(QKnxByteArray::fromHex("0aa227b4" - "fd7a32319ba9960ac036ce0e5c4507b5ae55161f1078b1dcfb3cb631")); + auto clientPublicKey = QKnxSecureKey::fromBytes(QKnxSecureKey::Type::Public, + QKnxByteArray::fromHex("0aa227b4fd7a32319ba9960ac036ce0e5c4507b5ae55161f1078b1dcfb3c" + "b631")); - auto serverPrivateKey = QKnxCurve25519PrivateKey::fromBytes(QKnxByteArray::fromHex("68c174" - "4813f4e65cf10cca671caa1336a796b4ac40cc5cf2655674225c1e5264")); - auto serverPublicKey = QKnxCurve25519PublicKey::fromBytes(QKnxByteArray::fromHex("bdf09990" - "9923143ef0a5de0b3be3687bc5bd3cf5f9e6f901699cd870ec1ff824")); + auto serverPrivateKey = QKnxSecureKey::fromBytes(QKnxSecureKey::Type::Private, + QKnxByteArray::fromHex("68c1744813f4e65cf10cca671caa1336a796b4ac40cc5cf2655674225c1e" + "5264")); + auto serverPublicKey = QKnxSecureKey::fromBytes(QKnxSecureKey::Type::Public, + QKnxByteArray::fromHex("bdf099909923143ef0a5de0b3be3687bc5bd3cf5f9e6f901699cd870ec1f" + "f824")); - QCOMPARE(serverPublicKey.bytes(), QKnxCurve25519PublicKey(serverPrivateKey).bytes()); + QCOMPARE(serverPublicKey, QKnxSecureKey::publicKeyFromPrivate(serverPrivateKey)); /* Session Response */ @@ -204,7 +213,7 @@ private slots: QCOMPARE(XOR_X_Y, QKnxByteArray::fromHex("b752be246459260f6b0c4801fbd5a67599f83b4057b3ef1e79e469ac17234e15")); - auto mac = QKnxCryptographicEngine::calculateMessageAuthenticationCode(deviceAuthenticationCode, + auto mac = QKnxCryptographicEngine::computeMessageAuthenticationCode(deviceAuthenticationCode, knxNetIpSecureHeader, secureSessionIdentifier, XOR_X_Y); QCOMPARE(mac, QKnxByteArray::fromHex("da3dc6af79896aa6ee7573d69950c283")); @@ -212,10 +221,11 @@ private slots: mac); QCOMPARE(encMac, QKnxByteArray::fromHex("a922505aaa436163570bd5494c2df2a3")); - auto decMac = QKnxCryptographicEngine::decryptMessageAuthenticationCode(deviceAuthenticationCode, encMac); + auto decMac = QKnxCryptographicEngine::decryptMessageAuthenticationCode(deviceAuthenticationCode, + encMac); QCOMPARE(decMac, mac); - auto sharedSecret = QKnxCryptographicEngine::sharedSecret(clientPublicKey, serverPrivateKey); + auto sharedSecret = QKnxSecureKey::sharedSecret(serverPrivateKey, clientPublicKey); QCOMPARE(sharedSecret, QKnxByteArray::fromHex("d801525217618f0da90a4ff22148aee0ff4c19b430e8081223ffe99c81a98b05")); @@ -230,10 +240,9 @@ private slots: QCOMPARE(passwordHash, QKnxByteArray::fromHex("03fcedb66660251ec81a1a716901696a")); knxNetIpSecureHeader = QKnxNetIpFrameHeader::fromBytes(QKnxByteArray::fromHex("061009530018")); - quint16 userId = 0x0001; - mac = QKnxCryptographicEngine::calculateMessageAuthenticationCode(passwordHash, - knxNetIpSecureHeader, userId, XOR_X_Y); + mac = QKnxCryptographicEngine::computeMessageAuthenticationCode(passwordHash, + knxNetIpSecureHeader, QKnxNetIp::SecureUserId::Management, XOR_X_Y); QCOMPARE(mac, QKnxByteArray::fromHex("741669f5e32bff6fa2edf51c52d4bd8f")); encMac = QKnxCryptographicEngine::encryptMessageAuthenticationCode(passwordHash, mac); @@ -248,11 +257,11 @@ private slots: knxNetIpSecureHeader = QKnxNetIpFrameHeader::fromBytes(QKnxByteArray::fromHex("06100950003e")); auto frame = QKnxNetIpSessionAuthenticateProxy::builder() - .setUserId(userId) + .setUserId(QKnxNetIp::SecureUserId::Management) .setMessageAuthenticationCode(encMac) .create(); - mac = QKnxCryptographicEngine::calculateMessageAuthenticationCode(sessionKey, + mac = QKnxCryptographicEngine::computeMessageAuthenticationCode(sessionKey, knxNetIpSecureHeader, secureSessionIdentifier, frame.bytes(), sequenceNumber, serialNumber, messageTag); QCOMPARE(mac, QKnxByteArray::fromHex("602280d0896beaa7106e7248f67f2eef")); @@ -282,7 +291,7 @@ private slots: .setStatus(QKnxNetIp::SecureSessionStatus::AuthenticationSuccess) .create(); - mac = QKnxCryptographicEngine::calculateMessageAuthenticationCode(sessionKey, + mac = QKnxCryptographicEngine::computeMessageAuthenticationCode(sessionKey, knxNetIpSecureHeader, secureSessionIdentifier, frame.bytes(), sequenceNumber, serialNumber, messageTag); QCOMPARE(mac, QKnxByteArray::fromHex("a8ed2796a566cd60b91a4de5c1144cbc")); @@ -318,7 +327,7 @@ private slots: knxNetIpSecureHeader = QKnxNetIpFrameHeader::fromBytes(QKnxByteArray::fromHex("061009500037")); secureSessionIdentifier = 0x0000; - mac = QKnxCryptographicEngine::calculateMessageAuthenticationCode(backboneKey, + mac = QKnxCryptographicEngine::computeMessageAuthenticationCode(backboneKey, knxNetIpSecureHeader, secureSessionIdentifier, frame.bytes(), timerValue, serialNumber, messageTag); QCOMPARE(mac, QKnxByteArray::fromHex("bd0a294b952554b23539204c2271d26b")); @@ -343,7 +352,7 @@ private slots: knxNetIpSecureHeader = QKnxNetIpFrameHeader::fromBytes(QKnxByteArray::fromHex("061009550024")); - mac = QKnxCryptographicEngine::calculateMessageAuthenticationCode(backboneKey, + mac = QKnxCryptographicEngine::computeMessageAuthenticationCode(backboneKey, knxNetIpSecureHeader, secureSessionIdentifier, {}, timerValue, serialNumber, messageTag); QCOMPARE(mac, QKnxByteArray::fromHex("21631241bc1f784d6d03da070580464a")); @@ -358,10 +367,155 @@ private slots: } - void cleanupTestCase() - {} -}; + void testSecureWrapperFrame() + { + if (QKnxCryptographicEngine::sslLibraryVersionNumber() < 0x1010000fL) + return; + + auto sessionAuthenticate = QKnxNetIpSessionAuthenticateProxy::builder() + .setUserId(QKnxNetIp::SecureUserId::Management) + .setMessageAuthenticationCode(QKnxByteArray::fromHex("1f1d59ea9f12a152e5d9727f08462cde")) + .create(); + + quint48 sequenceNumber = 0x000000000000; + auto serialNumber = QKnxByteArray::fromHex("00fa12345678"); + quint16 messageTag = 0xaffe; + quint16 secureSessionIdentifier = 0x0001; + auto sessionKey = QKnxByteArray::fromHex("289426c2912535ba98279a4d1843c487"); + + auto secureWrapper = QKnxNetIpSecureWrapperProxy::secureBuilder() + .setSecureSessionId(secureSessionIdentifier) + .setSequenceNumber(sequenceNumber) + .setSerialNumber(serialNumber) + .setMessageTag(messageTag) + .setEncapsulatedFrame(sessionAuthenticate) + .create(sessionKey); + QCOMPARE(secureWrapper.isValid(), true); + + const QKnxNetIpSecureWrapperProxy proxy(secureWrapper); + QCOMPARE(proxy.isValid(), true); + QCOMPARE(proxy.secureSessionId(), secureSessionIdentifier); + QCOMPARE(proxy.sequenceNumber(), sequenceNumber); + QCOMPARE(proxy.serialNumber(), serialNumber); + QCOMPARE(proxy.messageTag(), messageTag); + QCOMPARE(proxy.messageAuthenticationCode(), + QKnxByteArray::fromHex("52dba8e7e4bd80bd7d868a3ae78749de")); + QCOMPARE(proxy.encapsulatedFrame(), + QKnxByteArray::fromHex("7915a4f36e6e4208d28b4a207d8f35c0d138c26a7b5e7169")); + } + + void testTimerNotifyFrame() + { + if (QKnxCryptographicEngine::sslLibraryVersionNumber() < 0x1010000fL) + return; + + quint48 timerValue = 211938428830917; + auto serialNumber = QKnxByteArray::fromHex("00fa12345678"); + quint16 messageTag = 0xaffe; + auto mac = QKnxByteArray::fromHex("ee7b9b3083deb1570eb38d073adad985"); + + auto timerNotify = QKnxNetIpTimerNotifyProxy::builder() + .setTimerValue(timerValue) + .setSerialNumber(serialNumber) + .setMessageTag(messageTag) + .setMessageAuthenticationCode(mac) + .create(); + + QKnxNetIpTimerNotifyProxy proxy(timerNotify); + QCOMPARE(proxy.isValid(), true); + QCOMPARE(proxy.timerValue(), timerValue); + QCOMPARE(proxy.serialNumber(), serialNumber); + QCOMPARE(proxy.messageTag(), messageTag); + QCOMPARE(proxy.messageAuthenticationCode(), mac); + + quint16 sessionId = 0x0000; + auto backboneKey = QKnxByteArray::fromHex("000102030405060708090a0b0c0d0e0f"); + + auto secureTimerNotify = QKnxNetIpTimerNotifyProxy::secureBuilder() + .setTimerValue(timerValue) + .setSerialNumber(serialNumber) + .setMessageTag(messageTag) + .create(backboneKey, sessionId); + + QKnxNetIpTimerNotifyProxy proxy2(secureTimerNotify); + QCOMPARE(proxy2.isValid(), true); + QCOMPARE(proxy2.timerValue(), timerValue); + QCOMPARE(proxy2.serialNumber(), serialNumber); + QCOMPARE(proxy2.messageTag(), messageTag); + QCOMPARE(proxy2.messageAuthenticationCode(), mac); + } + + void testSessionResponseFrame() + { + if (QKnxCryptographicEngine::sslLibraryVersionNumber() < 0x1010000fL) + return; + + auto clientPublicKey = QKnxByteArray::fromHex("0aa227b4" + "fd7a32319ba9960ac036ce0e5c4507b5ae55161f1078b1dcfb3cb631"); + auto serverPublicKey = QKnxByteArray::fromHex("bdf09990" + "9923143ef0a5de0b3be3687bc5bd3cf5f9e6f901699cd870ec1ff824"); + + quint16 secureSessionIdentifier = 0x0001; + const QByteArray deviceAuthenticationPassword { "trustme" }; + auto mac = QKnxByteArray::fromHex("a922505aaa436163570bd5494c2df2a3"); + + auto sessionResponse = QKnxNetIpSessionResponseProxy::builder() + .setSecureSessionId(secureSessionIdentifier) + .setPublicKey(serverPublicKey) + .setMessageAuthenticationCode(mac) + .create(); + + QKnxNetIpSessionResponseProxy proxy(sessionResponse); + QCOMPARE(proxy.isValid(), true); + QCOMPARE(proxy.secureSessionId(), secureSessionIdentifier); + QCOMPARE(proxy.publicKey(), serverPublicKey); + QCOMPARE(proxy.messageAuthenticationCode(), mac); + + sessionResponse = QKnxNetIpSessionResponseProxy::secureBuilder() + .setSecureSessionId(secureSessionIdentifier) + .setPublicKey(serverPublicKey) + .create(deviceAuthenticationPassword, clientPublicKey); + + QKnxNetIpSessionResponseProxy proxy2(sessionResponse); + QCOMPARE(proxy2.isValid(), true); + QCOMPARE(proxy2.secureSessionId(), secureSessionIdentifier); + QCOMPARE(proxy2.publicKey(), serverPublicKey); + QCOMPARE(proxy2.messageAuthenticationCode(), mac); + } + + void testSessionAuthenticateFrame() + { + if (QKnxCryptographicEngine::sslLibraryVersionNumber() < 0x1010000fL) + return; + auto clientPublicKey = QKnxByteArray::fromHex("0aa227b4" + "fd7a32319ba9960ac036ce0e5c4507b5ae55161f1078b1dcfb3cb631"); + auto serverPublicKey = QKnxByteArray::fromHex("bdf09990" + "9923143ef0a5de0b3be3687bc5bd3cf5f9e6f901699cd870ec1ff824"); + + const QByteArray password { "secret" }; + auto mac = QKnxByteArray::fromHex("1f1d59ea9f12a152e5d9727f08462cde"); + + auto sessionAuth = QKnxNetIpSessionAuthenticateProxy::builder() + .setUserId(QKnxNetIp::SecureUserId::Management) + .setMessageAuthenticationCode(mac) + .create(); + + QKnxNetIpSessionAuthenticateProxy proxy(sessionAuth); + QCOMPARE(proxy.isValid(), true); + QCOMPARE(proxy.userId(), QKnxNetIp::SecureUserId::Management); + QCOMPARE(proxy.messageAuthenticationCode(), mac); + + auto sessionAuth2 = QKnxNetIpSessionAuthenticateProxy::secureBuilder() + .setUserId(QKnxNetIp::SecureUserId::Management) + .create(password, clientPublicKey, serverPublicKey); + + QKnxNetIpSessionAuthenticateProxy proxy2(sessionAuth2); + QCOMPARE(proxy2.isValid(), true); + QCOMPARE(proxy2.userId(), QKnxNetIp::SecureUserId::Management); + QCOMPARE(proxy2.messageAuthenticationCode(), mac); + } +}; QTEST_APPLESS_MAIN(tst_qknxcryptographicengine) diff --git a/tests/auto/qknxdatapointtype/tst_qknxdatapointtype.cpp b/tests/auto/qknxdatapointtype/tst_qknxdatapointtype.cpp index 428bd37..8387ae1 100644 --- a/tests/auto/qknxdatapointtype/tst_qknxdatapointtype.cpp +++ b/tests/auto/qknxdatapointtype/tst_qknxdatapointtype.cpp @@ -127,6 +127,10 @@ void tst_QKnxDatapointType::dpt1_1Bit() QCOMPARE(dpt1Bit.isValid(), true); QCOMPARE(dpt1Bit.bytes(), QKnxByteArray({ 0x00 })); QCOMPARE(dpt1Bit.type(), QKnxDatapointType::Type::Dpt1_1Bit); + QCOMPARE(dpt1Bit.minimum(), QVariant::fromValue(false)); + QCOMPARE(dpt1Bit.maximum(), QVariant::fromValue(true)); + QCOMPARE(dpt1Bit.minimumText(), QString("false")); + QCOMPARE(dpt1Bit.maximumText(), QString("true")); dpt1Bit.setBit(true); QCOMPARE(dpt1Bit.bit(), true); @@ -144,6 +148,10 @@ void tst_QKnxDatapointType::dpt1_1Bit() QCOMPARE(dptSwitch.value(), QKnxSwitch::State::Off); QCOMPARE(dptSwitch.bytes(), QKnxByteArray({ 0x00 })); QCOMPARE(dptSwitch.type(), QKnxDatapointType::Type::DptSwitch); + QCOMPARE(dptSwitch.minimum(), QVariant::fromValue(false)); + QCOMPARE(dptSwitch.maximum(), QVariant::fromValue(true)); + QCOMPARE(dptSwitch.minimumText(), QString("Off")); + QCOMPARE(dptSwitch.maximumText(), QString("On")); dptSwitch.setValue(QKnxSwitch::State::On); QCOMPARE(dptSwitch.isValid(), true); @@ -187,6 +195,10 @@ void tst_QKnxDatapointType::dpt1_1Bit() QCOMPARE(dptWindowDoor.value(), QKnxWindowDoor::State::Closed); QCOMPARE(dptWindowDoor.bytes(), QKnxByteArray({ 0x00 })); QCOMPARE(dptWindowDoor.type(), QKnxDatapointType::Type::DptWindowDoor); + QCOMPARE(dptWindowDoor.minimum(), QVariant::fromValue(false)); + QCOMPARE(dptWindowDoor.maximum(), QVariant::fromValue(true)); + QCOMPARE(dptWindowDoor.minimumText(), QString("Closed")); + QCOMPARE(dptWindowDoor.maximumText(), QString("Open")); } void tst_QKnxDatapointType::dpt2_1BitControlled() diff --git a/tests/auto/qknxnetipdescriptionresponse/tst_qknxnetipdescriptionresponse.cpp b/tests/auto/qknxnetipdescriptionresponse/tst_qknxnetipdescriptionresponse.cpp index bfa5a85..e1a863a 100644 --- a/tests/auto/qknxnetipdescriptionresponse/tst_qknxnetipdescriptionresponse.cpp +++ b/tests/auto/qknxnetipdescriptionresponse/tst_qknxnetipdescriptionresponse.cpp @@ -156,15 +156,15 @@ void tst_QKnxNetIpDescriptionResponse::testSupportedFamiliesAccessor() .create(); QKnxNetIpDescriptionResponseProxy descriptionResponse(frame); - auto familie = descriptionResponse.supportedFamilies(); - const QKnxNetIpServiceFamiliesDibProxy view(familie); + auto family = descriptionResponse.supportedFamilies(); + const QKnxNetIpServiceFamiliesDibProxy view(family); QCOMPARE(view.isValid(), m_sf.isValid()); QCOMPARE(view.descriptionType(), QKnxNetIpServiceFamiliesDibProxy(m_sf).descriptionType()); - QCOMPARE(familie.size(), m_sf.size()); - QCOMPARE(familie.bytes(), m_sf.bytes()); - QCOMPARE(familie.data().size(), m_sf.data().size()); - QCOMPARE(familie.data(), m_sf.data()); + QCOMPARE(family.size(), m_sf.size()); + QCOMPARE(family.bytes(), m_sf.bytes()); + QCOMPARE(family.data().size(), m_sf.data().size()); + QCOMPARE(family.data(), m_sf.data()); } void tst_QKnxNetIpDescriptionResponse::testSupportedFamiliesVersions() diff --git a/tests/auto/qknxnetiprouter/tst_qknxnetiprouter.cpp b/tests/auto/qknxnetiprouter/tst_qknxnetiprouter.cpp index ae435a2..3474b61 100644 --- a/tests/auto/qknxnetiprouter/tst_qknxnetiprouter.cpp +++ b/tests/auto/qknxnetiprouter/tst_qknxnetiprouter.cpp @@ -286,7 +286,7 @@ void tst_QKnxNetIpRouter::test_routing_sends_indications() indicationSentEmitted = true; QKnxNetIpRoutingIndicationProxy indicationSent(frame); QVERIFY(indicationSent.isValid()); - QCOMPARE(indicationSent.linkLayerFrame().bytes(), frameSent.bytes()); + QCOMPARE(indicationSent.cemi().bytes(), frameSent.bytes()); }); auto indication = QKnxNetIpRoutingIndicationProxy::builder() .setCemi(frameSent) diff --git a/tests/auto/qknxnetipsearchrequest/tst_qknxnetipsearchrequest.cpp b/tests/auto/qknxnetipsearchrequest/tst_qknxnetipsearchrequest.cpp index 2abb15b..a97ca50 100644 --- a/tests/auto/qknxnetipsearchrequest/tst_qknxnetipsearchrequest.cpp +++ b/tests/auto/qknxnetipsearchrequest/tst_qknxnetipsearchrequest.cpp @@ -32,16 +32,6 @@ #include <QtKnx/qknxnetipsrp.h> #include <QtKnx/QKnxServiceInfo> -#if QT_VERSION < QT_VERSION_CHECK(5,13,0) - QT_BEGIN_NAMESPACE - bool operator==(const QKnxServiceInfo &lhs, const QKnxServiceInfo &rhs) - { - return (lhs.ServiceFamily == rhs.ServiceFamily) - && (lhs.ServiceFamilyVersion == rhs.ServiceFamilyVersion); - } - QT_END_NAMESPACE -#endif - char *toString(const QKnxByteArray &ba) { using QTest::toString; |