summaryrefslogtreecommitdiffstats
path: root/src/knx/ssl/qknxkeyring.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/knx/ssl/qknxkeyring.cpp')
-rw-r--r--src/knx/ssl/qknxkeyring.cpp515
1 files changed, 515 insertions, 0 deletions
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