diff options
-rw-r--r-- | qtconnectivity.pro | 1 | ||||
-rw-r--r-- | src/bluetooth/bluetooth.pro | 8 | ||||
-rw-r--r-- | src/bluetooth/bluez/bluez_data_p.h | 24 | ||||
-rw-r--r-- | src/bluetooth/bluez/hcimanager.cpp | 123 | ||||
-rw-r--r-- | src/bluetooth/bluez/hcimanager_p.h | 4 | ||||
-rw-r--r-- | src/bluetooth/lecmacverifier.cpp | 171 | ||||
-rw-r--r-- | src/bluetooth/lecmacverifier_p.h | 76 | ||||
-rw-r--r-- | src/bluetooth/qlowenergycontroller.cpp | 4 | ||||
-rw-r--r-- | src/bluetooth/qlowenergycontroller_bluez.cpp | 115 | ||||
-rw-r--r-- | src/bluetooth/qlowenergycontroller_p.h | 18 | ||||
-rw-r--r-- | tests/auto/qlowenergycontroller-gattserver/server/qlowenergycontroller-gattserver.cpp | 2 | ||||
-rw-r--r-- | tests/auto/qlowenergycontroller-gattserver/test/test.pro | 4 | ||||
-rw-r--r-- | tests/auto/qlowenergycontroller-gattserver/test/tst_qlowenergycontroller-gattserver.cpp | 43 |
13 files changed, 562 insertions, 31 deletions
diff --git a/qtconnectivity.pro b/qtconnectivity.pro index 5eb864b8..4b7b9da9 100644 --- a/qtconnectivity.pro +++ b/qtconnectivity.pro @@ -3,4 +3,5 @@ require(!android|qtHaveModule(androidextras)) load(configure) qtCompileTest(bluez) qtCompileTest(bluez_le) +qtCompileTest(linux_crypto_api) load(qt_parts) diff --git a/src/bluetooth/bluetooth.pro b/src/bluetooth/bluetooth.pro index ef5337b5..99c9fe07 100644 --- a/src/bluetooth/bluetooth.pro +++ b/src/bluetooth/bluetooth.pro @@ -1,5 +1,5 @@ TARGET = QtBluetooth -QT = core +QT = core core-private QT_PRIVATE = concurrent @@ -51,6 +51,7 @@ PRIVATE_HEADERS += \ qlowenergycontroller_p.h \ qlowenergyserviceprivate_p.h \ qleadvertiser_p.h \ + lecmacverifier_p.h SOURCES += \ qbluetoothaddress.cpp\ @@ -102,7 +103,10 @@ config_bluez:qtHaveModule(dbus) { config_bluez_le { SOURCES += \ qleadvertiser_bluez.cpp \ - qlowenergycontroller_bluez.cpp + qlowenergycontroller_bluez.cpp \ + lecmacverifier.cpp + config_linux_crypto_api:DEFINES += CONFIG_LINUX_CRYPTO_API + else:message("Linux crypto API not present, signed writes will not work.") } else { message("Bluez version is too old to support Bluetooth Low Energy.") message("Only classic Bluetooth will be available.") diff --git a/src/bluetooth/bluez/bluez_data_p.h b/src/bluetooth/bluez/bluez_data_p.h index e8d1ec62..8c2dc43e 100644 --- a/src/bluetooth/bluez/bluez_data_p.h +++ b/src/bluetooth/bluez/bluez_data_p.h @@ -58,6 +58,10 @@ QT_BEGIN_NAMESPACE +#define ATTRIBUTE_CHANNEL_ID 4 +#define SIGNALING_CHANNEL_ID 5 +#define SECURITY_CHANNEL_ID 6 + #define BTPROTO_L2CAP 0 #define BTPROTO_HCI 1 #define BTPROTO_RFCOMM 3 @@ -173,9 +177,14 @@ static inline void ntoh128(const quint128 *src, quint128 *dst) #error "Unknown byte order" #endif +template<typename T> inline T getBtData(const void *ptr) +{ + return qFromLittleEndian<T>(reinterpret_cast<const uchar *>(ptr)); +} + static inline quint16 bt_get_le16(const void *ptr) { - return qFromLittleEndian<quint16>(reinterpret_cast<const uchar *>(ptr)); + return getBtData<quint16>(ptr); } template<typename T> inline void putBtData(T src, void *dst) @@ -200,6 +209,7 @@ template<> inline void putBtData(quint128 src, void *dst) // HCI packet types #define HCI_COMMAND_PKT 0x01 +#define HCI_ACL_PKT 0x02 #define HCI_EVENT_PKT 0x04 #define HCI_VENDOR_PKT 0xff @@ -337,6 +347,18 @@ struct evt_cmd_complete { quint16 opcode; } __attribute__ ((packed)); +struct AclData { + quint16 handle: 12; + quint16 pbFlag: 2; + quint16 bcFlag: 2; + quint16 dataLen; +}; + +struct L2CapHeader { + quint16 length; + quint16 channelId; +}; + struct hci_command_hdr { quint16 opcode; /* OCF & OGF */ quint8 plen; diff --git a/src/bluetooth/bluez/hcimanager.cpp b/src/bluetooth/bluez/hcimanager.cpp index 123a16ab..35ce2184 100644 --- a/src/bluetooth/bluez/hcimanager.cpp +++ b/src/bluetooth/bluez/hcimanager.cpp @@ -183,6 +183,29 @@ bool HciManager::monitorEvent(HciManager::HciEvent event) return true; } +bool HciManager::monitorAclPackets() +{ + if (!isValid()) + return false; + + hci_filter filter; + socklen_t length = sizeof(hci_filter); + if (getsockopt(hciSocket, SOL_HCI, HCI_FILTER, &filter, &length) < 0) { + qCWarning(QT_BT_BLUEZ) << "Cannot retrieve HCI filter settings"; + return false; + } + + hci_filter_set_ptype(HCI_ACL_PKT, &filter); + hci_filter_all_events(&filter); + + if (setsockopt(hciSocket, SOL_HCI, HCI_FILTER, &filter, sizeof(hci_filter)) < 0) { + qCWarning(QT_BT_BLUEZ) << "Could not set HCI socket options:" << strerror(errno); + return false; + } + + return true; +} + bool HciManager::sendCommand(OpCodeGroupField ogf, OpCodeCommandField ocf, const QByteArray ¶meters) { qCDebug(QT_BT_BLUEZ) << "sending command; ogf:" << ogf << "ocf:" << ocf; @@ -325,28 +348,20 @@ bool HciManager::sendConnectionParameterUpdateRequest(quint16 handle, const quint16 sigPacketLen = sizeof connUpdateData; signalingPacket.length = qToLittleEndian(sigPacketLen); - struct L2CapHeader { - quint16 length; - quint16 channelId; - } l2CapHeader; + L2CapHeader l2CapHeader; const quint16 l2CapHeaderLen = sizeof signalingPacket + sigPacketLen; l2CapHeader.length = qToLittleEndian(l2CapHeaderLen); - l2CapHeader.channelId = qToLittleEndian(quint16(5)); + l2CapHeader.channelId = qToLittleEndian(quint16(SIGNALING_CHANNEL_ID)); // Vol 2, part E, 5.4.2 - struct AclData { - quint16 handle: 12; - quint16 pbFlag: 2; - quint16 bcFlag: 2; - quint16 dataLen; - } aclData; + AclData aclData; aclData.handle = qToLittleEndian(handle); // Works because the next two values are zero. aclData.pbFlag = 0; aclData.bcFlag = 0; aclData.dataLen = qToLittleEndian(quint16(sizeof l2CapHeader + l2CapHeaderLen)); struct iovec iv[5]; - quint8 packetType = 2; + quint8 packetType = HCI_ACL_PKT; iv[0].iov_base = &packetType; iv[0].iov_len = 1; iv[1].iov_base = &aclData; @@ -372,8 +387,7 @@ bool HciManager::sendConnectionParameterUpdateRequest(quint16 handle, */ void HciManager::_q_readNotify() { - - unsigned char buffer[HCI_MAX_EVENT_SIZE]; + unsigned char buffer[qMax<int>(HCI_MAX_EVENT_SIZE, sizeof(AclData))]; int size; size = ::read(hciSocket, buffer, sizeof(buffer)); @@ -384,16 +398,29 @@ void HciManager::_q_readNotify() return; } - const unsigned char *data = buffer; + switch (buffer[0]) { + case HCI_EVENT_PKT: + handleHciEventPacket(buffer + 1, size - 1); + break; + case HCI_ACL_PKT: + handleHciAclPacket(buffer + 1, size - 1); + break; + default: + qCWarning(QT_BT_BLUEZ) << "Ignoring unexpected HCI packet type" << buffer[0]; + } +} - // Not interested in anything but valid HCI events - if ((size < HCI_EVENT_HDR_SIZE + 1) || buffer[0] != HCI_EVENT_PKT) +void HciManager::handleHciEventPacket(const quint8 *data, int size) +{ + if (size < HCI_EVENT_HDR_SIZE) { + qCWarning(QT_BT_BLUEZ) << "Unexpected HCI event packet size:" << size; return; + } - hci_event_hdr *header = (hci_event_hdr *)(&buffer[1]); + hci_event_hdr *header = (hci_event_hdr *) data; - size = size - HCI_EVENT_HDR_SIZE - 1; - data = data + HCI_EVENT_HDR_SIZE + 1; + size -= HCI_EVENT_HDR_SIZE; + data += HCI_EVENT_HDR_SIZE; if (header->plen != size) { qCWarning(QT_BT_BLUEZ) << "Invalid HCI event packet size"; @@ -434,6 +461,62 @@ void HciManager::_q_readNotify() default: break; } + +} + +void HciManager::handleHciAclPacket(const quint8 *data, int size) +{ + if (size < int(sizeof(AclData))) { + qCWarning(QT_BT_BLUEZ) << "Unexpected HCI ACL packet size"; + return; + } + + quint16 rawAclData[sizeof(AclData) / sizeof(quint16)]; + rawAclData[0] = bt_get_le16(data); + rawAclData[1] = bt_get_le16(data + sizeof(quint16)); + const AclData *aclData = reinterpret_cast<AclData *>(rawAclData); + data += sizeof *aclData; + size -= sizeof *aclData; + if (size < aclData->dataLen) { + qCWarning(QT_BT_BLUEZ) << "HCI ACL packet data size" << size + << "is smaller than specified size" << aclData->dataLen; + return; + } + +// qCDebug(QT_BT_BLUEZ) << "handle:" << aclData->handle << "PB:" << aclData->pbFlag +// << "BC:" << aclData->bcFlag << "data len:" << aclData->dataLen; + + // Consider only directed, complete messages from controller to host (i.e. incoming packets). + if (aclData->pbFlag != 2 || aclData->bcFlag != 0) + return; + + if (size < int(sizeof(L2CapHeader))) { + qCWarning(QT_BT_BLUEZ) << "Unexpected HCI ACL packet size"; + return; + } + L2CapHeader l2CapHeader = *reinterpret_cast<const L2CapHeader*>(data); + l2CapHeader.channelId = qFromLittleEndian(l2CapHeader.channelId); + l2CapHeader.length = qFromLittleEndian(l2CapHeader.length); + data += sizeof l2CapHeader; + size -= sizeof l2CapHeader; + if (size < l2CapHeader.length) { + qCWarning(QT_BT_BLUEZ) << "L2Cap payload size" << size << "is smaller than specified size" + << l2CapHeader.length; + return; + } +// qCDebug(QT_BT_BLUEZ) << "l2cap channel id:" << l2CapHeader.channelId +// << "payload length:" << l2CapHeader.length; + if (l2CapHeader.channelId != SECURITY_CHANNEL_ID) + return; + if (*data != 0xa) // "Signing Information". Spec v4.2, Vol 3, Part H, 3.6.6 + return; + if (size != 17) { + qCWarning(QT_BT_BLUEZ) << "Unexpected key size" << size << "in Signing Information packet"; + return; + } + quint128 csrk; + memcpy(&csrk, data + 1, sizeof csrk); + emit signatureResolvingKeyReceived(aclData->handle, csrk); } void HciManager::handleLeMetaEvent(const quint8 *data) diff --git a/src/bluetooth/bluez/hcimanager_p.h b/src/bluetooth/bluez/hcimanager_p.h index b2edb15b..eb899c79 100644 --- a/src/bluetooth/bluez/hcimanager_p.h +++ b/src/bluetooth/bluez/hcimanager_p.h @@ -76,6 +76,7 @@ public: bool isValid() const; bool monitorEvent(HciManager::HciEvent event); + bool monitorAclPackets(); bool sendCommand(OpCodeGroupField ogf, OpCodeCommandField ocf, const QByteArray ¶meters); void stopEvents(); @@ -90,12 +91,15 @@ signals: void commandCompleted(quint16 opCode, quint8 status, const QByteArray &data); void connectionComplete(quint16 handle); void connectionUpdate(quint16 handle, const QLowEnergyConnectionParameters ¶meters); + void signatureResolvingKeyReceived(quint16 connHandle, const quint128 &csrk); private slots: void _q_readNotify(); private: int hciForAddress(const QBluetoothAddress &deviceAdapter); + void handleHciEventPacket(const quint8 *data, int size); + void handleHciAclPacket(const quint8 *data, int size); void handleLeMetaEvent(const quint8 *data); int hciSocket; diff --git a/src/bluetooth/lecmacverifier.cpp b/src/bluetooth/lecmacverifier.cpp new file mode 100644 index 00000000..f8ded361 --- /dev/null +++ b/src/bluetooth/lecmacverifier.cpp @@ -0,0 +1,171 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtBluetooth 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 "lecmacverifier_p.h" + +#include "bluez/bluez_data_p.h" + +#include <QtCore/qbytearray.h> +#include <QtCore/qloggingcategory.h> +#include <QtCore/private/qcore_unix_p.h> + +#include <cstring> +#include <sys/socket.h> +#include <sys/types.h> +#include <unistd.h> + +#ifdef CONFIG_LINUX_CRYPTO_API +#include <linux/if_alg.h> +#endif + +QT_BEGIN_NAMESPACE + +Q_DECLARE_LOGGING_CATEGORY(QT_BT_BLUEZ) + +LeCmacVerifier::LeCmacVerifier() +{ +#ifdef CONFIG_LINUX_CRYPTO_API + m_baseSocket = socket(AF_ALG, SOCK_SEQPACKET, 0); + if (m_baseSocket == -1) { + qCWarning(QT_BT_BLUEZ) << "failed to create first level crypto socket:" + << strerror(errno); + return; + } + sockaddr_alg sa; + using namespace std; + memset(&sa, 0, sizeof sa); + sa.salg_family = AF_ALG; + strcpy(reinterpret_cast<char *>(sa.salg_type), "hash"); + strcpy(reinterpret_cast<char *>(sa.salg_name), "cmac(aes)"); + if (bind(m_baseSocket, reinterpret_cast<sockaddr *>(&sa), sizeof sa) == -1) { + qCWarning(QT_BT_BLUEZ) << "bind() failed for crypto socket:" << strerror(errno); + return; + } +#else // CONFIG_LINUX_CRYPTO_API + qCWarning(QT_BT_BLUEZ) << "Linux crypto API not present, CMAC verification will fail."; +#endif +} + +LeCmacVerifier::~LeCmacVerifier() +{ + if (m_baseSocket != -1) + close(m_baseSocket); +} + +QByteArray LeCmacVerifier::createFullMessage(const QByteArray &message, quint32 signCounter) +{ + // Spec v4.2, Vol 3, Part H, 2.4.5 + QByteArray fullMessage = message; + fullMessage.resize(fullMessage.count() + sizeof signCounter); + putBtData(signCounter, fullMessage.data() + message.count()); + return fullMessage; +} + +bool LeCmacVerifier::verify(const QByteArray &message, const quint128 &csrk, + quint64 expectedMac) const +{ +#ifdef CONFIG_LINUX_CRYPTO_API + if (m_baseSocket == -1) + return false; + quint128 csrkMsb; + std::reverse_copy(std::begin(csrk.data), std::end(csrk.data), std::begin(csrkMsb.data)); + qCDebug(QT_BT_BLUEZ) << "CSRK (MSB):" << QByteArray(reinterpret_cast<char *>(csrkMsb.data), + sizeof csrkMsb).toHex(); + if (setsockopt(m_baseSocket, 279 /* SOL_ALG */, ALG_SET_KEY, csrkMsb.data, sizeof csrkMsb) == -1) { + qCWarning(QT_BT_BLUEZ) << "setsockopt() failed for crypto socket:" << strerror(errno); + return false; + } + + class SocketWrapper + { + public: + SocketWrapper(int socket) : m_socket(socket) {} + ~SocketWrapper() { + if (m_socket != -1) + close(m_socket); + } + + int value() const { return m_socket; } + private: + int m_socket; + }; + SocketWrapper cryptoSocket(accept(m_baseSocket, nullptr, 0)); + if (cryptoSocket.value() == -1) { + qCWarning(QT_BT_BLUEZ) << "accept() failed for crypto socket:" << strerror(errno); + return false; + } + + QByteArray messageSwapped(message.count(), Qt::Uninitialized); + std::reverse_copy(message.begin(), message.end(), messageSwapped.begin()); + qint64 totalBytesWritten = 0; + do { + const qint64 bytesWritten = qt_safe_write(cryptoSocket.value(), + messageSwapped.constData() + totalBytesWritten, + messageSwapped.count() - totalBytesWritten); + if (bytesWritten == -1) { + qCWarning(QT_BT_BLUEZ) << "writing to crypto socket failed:" << strerror(errno); + return false; + } + totalBytesWritten += bytesWritten; + } while (totalBytesWritten < messageSwapped.count()); + quint64 mac; + quint8 * const macPtr = reinterpret_cast<quint8 *>(&mac); + qint64 totalBytesRead = 0; + do { + const qint64 bytesRead = qt_safe_read(cryptoSocket.value(), macPtr + totalBytesRead, + sizeof mac - totalBytesRead); + if (bytesRead == -1) { + qCWarning(QT_BT_BLUEZ) << "reading from crypto socket failed:" << strerror(errno); + return false; + } + totalBytesRead += bytesRead; + } while (totalBytesRead < qint64(sizeof mac)); + mac = qFromBigEndian(mac); + if (mac != expectedMac) { + qCWarning(QT_BT_BLUEZ) << hex << "signature verification failed: calculated mac:" << mac + << "expected mac:" << expectedMac; + return false; + } + return true; +#else // CONFIG_LINUX_CRYPTO_API + qCWarning(QT_BT_BLUEZ) << "CMAC verification failed due to missing Linux crypto API."; + return false; +#endif +} + +QT_END_NAMESPACE diff --git a/src/bluetooth/lecmacverifier_p.h b/src/bluetooth/lecmacverifier_p.h new file mode 100644 index 00000000..e09b6013 --- /dev/null +++ b/src/bluetooth/lecmacverifier_p.h @@ -0,0 +1,76 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtBluetooth 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 LECMACVERIFIER_H +#define LECMACVERIFIER_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtCore/qglobal.h> + +QT_BEGIN_NAMESPACE + +struct quint128; + +class Q_AUTOTEST_EXPORT LeCmacVerifier +{ +public: + LeCmacVerifier(); + ~LeCmacVerifier(); + + static QByteArray createFullMessage(const QByteArray &message, quint32 signCounter); + + bool verify(const QByteArray &message, const quint128 &csrk, quint64 expectedMac) const; + +private: + int m_baseSocket = -1; +}; + + +QT_END_NAMESPACE + +#endif // Header guard diff --git a/src/bluetooth/qlowenergycontroller.cpp b/src/bluetooth/qlowenergycontroller.cpp index 0cbf6582..238ed92d 100644 --- a/src/bluetooth/qlowenergycontroller.cpp +++ b/src/bluetooth/qlowenergycontroller.cpp @@ -185,7 +185,9 @@ Q_DECLARE_LOGGING_CATEGORY(QT_BT) \sa QLowEnergyController::createCentral() \sa QLowEnergyController::createPeripheral() \since 5.7 - \note The peripheral role is currently only supported on Linux. + \note The peripheral role is currently only supported on Linux. In addition, handling the + "Signed Write" ATT command on the server side requires BlueZ 5 and kernel version 3.7 + or newer. */ diff --git a/src/bluetooth/qlowenergycontroller_bluez.cpp b/src/bluetooth/qlowenergycontroller_bluez.cpp index 1562c3f9..06586192 100644 --- a/src/bluetooth/qlowenergycontroller_bluez.cpp +++ b/src/bluetooth/qlowenergycontroller_bluez.cpp @@ -38,13 +38,16 @@ ** ****************************************************************************/ +#include "lecmacverifier_p.h" #include "qlowenergycontroller_p.h" #include "qbluetoothsocket_p.h" #include "qleadvertiser_p.h" #include "bluez/bluez_data_p.h" #include "bluez/hcimanager_p.h" +#include <QtCore/QFileInfo> #include <QtCore/QLoggingCategory> +#include <QtCore/QSettings> #include <QtBluetooth/QBluetoothLocalDevice> #include <QtBluetooth/QBluetoothSocket> #include <QtBluetooth/QLowEnergyCharacteristicData> @@ -60,8 +63,6 @@ #include <sys/socket.h> #include <unistd.h> -#define ATTRIBUTE_CHANNEL_ID 4 - #define ATT_DEFAULT_LE_MTU 23 #define ATT_MAX_LE_MTU 0x200 @@ -271,6 +272,7 @@ QLowEnergyControllerPrivate::QLowEnergyControllerPrivate() connect(hciManager, SIGNAL(encryptionChangedEvent(QBluetoothAddress,bool)), this, SLOT(encryptionChangedEvent(QBluetoothAddress,bool))); hciManager->monitorEvent(HciManager::LeMetaEvent); + hciManager->monitorAclPackets(); connect(hciManager, &HciManager::connectionComplete, [this](quint16 handle) { connectionHandle = handle; qCDebug(QT_BT_BLUEZ) << "received connection complete event, handle:" << handle; @@ -281,11 +283,22 @@ QLowEnergyControllerPrivate::QLowEnergyControllerPrivate() emit q_ptr->connectionUpdated(params); } ); + connect(hciManager, &HciManager::signatureResolvingKeyReceived, + [this](quint16 handle, const quint128 &csrk) { + if (handle == connectionHandle) { + qCDebug(QT_BT_BLUEZ) << "received new signature resolving key" + << QByteArray(reinterpret_cast<const char *>(csrk.data), + sizeof csrk).toHex(); + signingData.insert(remoteDevice.toUInt64(), SigningData(csrk)); + } + } + ); } QLowEnergyControllerPrivate::~QLowEnergyControllerPrivate() { closeServerSocket(); + delete cmacVerifier; } class ServerSocket @@ -337,6 +350,7 @@ private: int m_socket = -1; }; + void QLowEnergyControllerPrivate::startAdvertising(const QLowEnergyAdvertisingParameters ¶ms, const QLowEnergyAdvertisingData &advertisingData, const QLowEnergyAdvertisingData &scanResponseData) @@ -2462,9 +2476,32 @@ void QLowEnergyControllerPrivate::handleWriteRequestOrCommand(const QByteArray & qCWarning(QT_BT_BLUEZ) << "Ignoring signed write on encrypted link."; return; } - // const QByteArray signature = packet.right(12); - qCWarning(QT_BT_BLUEZ) << "signed write not implemented, ignoring."; - return; // TODO: Check signature and continue if it's valid. Check and update sign counter. + const auto signingDataIt = signingData.find(remoteDevice.toUInt64()); + if (signingDataIt == signingData.constEnd()) { + qCWarning(QT_BT_BLUEZ) << "No CSRK found for peer device, ignoring signed write"; + return; + } + + const quint32 signCounter = getBtData<quint32>(packet.data() + packet.count() - 12); + if (signCounter < signingDataIt.value().counter + 1) { + qCWarning(QT_BT_BLUEZ) << "Client's' sign counter" << signCounter + << "not greater than local sign counter" + << signingDataIt.value().counter + << "; ignoring signed write command."; + return; + } + + const quint64 macFromClient = getBtData<quint64>(packet.data() + packet.count() - 8); + const bool signatureCorrect = verifyMac(packet.left(packet.count() - 12), + signingDataIt.value().key, signCounter, macFromClient); + if (!signatureCorrect) { + qCWarning(QT_BT_BLUEZ) << "Signed Write packet has wrong signature, disconnecting"; + disconnectFromDevice(); // Recommended by spec v4.2, Vol 3, part C, 10.4.2 + return; + } + + signingDataIt.value().counter = signCounter; + storeSignCounter(); valueLength = packet.count() - 15; } else { valueLength = packet.count() - 3; @@ -2678,6 +2715,7 @@ void QLowEnergyControllerPrivate::handleConnectionRequest() l2cpSocket->setSocketDescriptor(clientSocket, QBluetoothServiceInfo::L2capProtocol, QBluetoothSocket::ConnectedState, QIODevice::ReadWrite | QIODevice::Unbuffered); restoreClientConfigurations(); + loadSigningDataIfNecessary(); setState(QLowEnergyController::ConnectedState); } @@ -2773,6 +2811,64 @@ void QLowEnergyControllerPrivate::restoreClientConfigurations() sendNextIndication(); } +void QLowEnergyControllerPrivate::loadSigningDataIfNecessary() +{ + const auto signingDataIt = signingData.constFind(remoteDevice.toUInt64()); + if (signingDataIt != signingData.constEnd()) + return; // We are up to date for this device. + const QString settingsFilePath = keySettingsFilePath(); + if (!QFileInfo(settingsFilePath).exists()) { + qCDebug(QT_BT_BLUEZ) << "No settings found for peer device."; + return; + } + QSettings settings(settingsFilePath, QSettings::IniFormat); + settings.beginGroup(QLatin1String("RemoteSignatureKey")); + const QByteArray keyString = settings.value(QLatin1String("Key")).toByteArray(); + if (keyString.isEmpty()) { + qCDebug(QT_BT_BLUEZ) << "No remote signature key found in settings file"; + return; + } + const QByteArray keyData = QByteArray::fromHex(keyString); + if (keyData.count() != int(sizeof(quint128))) { + qCWarning(QT_BT_BLUEZ) << "Remote signature key in settings file has invalid size" + << keyString.count(); + return; + } + qCDebug(QT_BT_BLUEZ) << "CSRK of peer device is" << keyString; + const quint32 counter = settings.value(QLatin1String("Counter"), 0).toUInt(); + quint128 csrk; + using namespace std; + memcpy(csrk.data, keyData.constData(), keyData.count()); + signingData.insert(remoteDevice.toUInt64(), SigningData(csrk, counter - 1)); +} + +void QLowEnergyControllerPrivate::storeSignCounter() +{ + const auto signingDataIt = signingData.constFind(remoteDevice.toUInt64()); + if (signingDataIt == signingData.constEnd()) + return; + const QString settingsFilePath = keySettingsFilePath(); + if (!QFileInfo(settingsFilePath).exists()) + return; + QSettings settings(settingsFilePath, QSettings::IniFormat); + if (!settings.isWritable()) + return; + settings.beginGroup(QLatin1String("RemoteSignatureKey")); + const QString counterKey = QLatin1String("Counter"); + if (!settings.allKeys().contains(counterKey)) + return; + const quint32 counterValue = signingDataIt.value().counter + 1; + if (counterValue == settings.value(counterKey).toUInt()) + return; + settings.setValue(counterKey, counterValue); +} + +QString QLowEnergyControllerPrivate::keySettingsFilePath() const +{ + return QString::fromLatin1("/var/lib/bluetooth/%1/%2/info") + .arg(localAdapter.toString(), remoteDevice.toString()); +} + static QByteArray uuidToByteArray(const QBluetoothUuid &uuid) { QByteArray ba; @@ -2999,4 +3095,13 @@ int QLowEnergyControllerPrivate::checkReadPermissions(QVector<Attribute> &attrib return 0; } +bool QLowEnergyControllerPrivate::verifyMac(const QByteArray &message, const quint128 &csrk, + quint32 signCounter, quint64 expectedMac) +{ + if (!cmacVerifier) + cmacVerifier = new LeCmacVerifier; + return cmacVerifier->verify(LeCmacVerifier::createFullMessage(message, signCounter), csrk, + expectedMac); +} + QT_END_NAMESPACE diff --git a/src/bluetooth/qlowenergycontroller_p.h b/src/bluetooth/qlowenergycontroller_p.h index 095f838f..51ea5a83 100644 --- a/src/bluetooth/qlowenergycontroller_p.h +++ b/src/bluetooth/qlowenergycontroller_p.h @@ -92,6 +92,7 @@ class QLowEnergyServiceData; #if defined(QT_BLUEZ_BLUETOOTH) && !defined(QT_BLUEZ_NO_BTLE) class HciManager; +class LeCmacVerifier; class QSocketNotifier; #elif defined(QT_ANDROID_BLUETOOTH) class LowEnergyNotificationHub; @@ -248,6 +249,17 @@ private: }; QHash<quint64, QVector<ClientConfigurationData>> clientConfigData; + struct SigningData { + SigningData() = default; + SigningData(const quint128 &csrk, quint32 signCounter = quint32(-1)) + : key(csrk), counter(signCounter) {} + + quint128 key; + quint32 counter = quint32(-1); + }; + QHash<quint64, SigningData> signingData; + LeCmacVerifier *cmacVerifier = nullptr; + bool requestPending; quint16 mtuSize; int securityLevelValue; @@ -265,6 +277,9 @@ private: QVector<TempClientConfigurationData> gatherClientConfigData(); void storeClientConfigurations(); void restoreClientConfigurations(); + void loadSigningDataIfNecessary(); + void storeSignCounter(); + QString keySettingsFilePath() const; void sendPacket(const QByteArray &packet); void sendNextPendingRequest(); @@ -339,6 +354,9 @@ private: int checkReadPermissions(const Attribute &attr); int checkReadPermissions(QVector<Attribute> &attributes); + bool verifyMac(const QByteArray &message, const quint128 &csrk, quint32 signCounter, + quint64 expectedMac); + void updateLocalAttributeValue( QLowEnergyHandle handle, const QByteArray &value, diff --git a/tests/auto/qlowenergycontroller-gattserver/server/qlowenergycontroller-gattserver.cpp b/tests/auto/qlowenergycontroller-gattserver/server/qlowenergycontroller-gattserver.cpp index 4efa20ba..b7c95816 100644 --- a/tests/auto/qlowenergycontroller-gattserver/server/qlowenergycontroller-gattserver.cpp +++ b/tests/auto/qlowenergycontroller-gattserver/server/qlowenergycontroller-gattserver.cpp @@ -121,7 +121,7 @@ void addCustomService() serviceData.addCharacteristic(charData); charData.setUuid(QBluetoothUuid(quint16(0x5001))); - charData.setProperties(QLowEnergyCharacteristic::Read); + charData.setProperties(QLowEnergyCharacteristic::Read | QLowEnergyCharacteristic::WriteSigned); charData.setReadConstraints(QBluetooth::AttAuthorizationRequired); // To test read failure. serviceData.addCharacteristic(charData); charData.setValue("something"); diff --git a/tests/auto/qlowenergycontroller-gattserver/test/test.pro b/tests/auto/qlowenergycontroller-gattserver/test/test.pro index bd1f0874..8c95106f 100644 --- a/tests/auto/qlowenergycontroller-gattserver/test/test.pro +++ b/tests/auto/qlowenergycontroller-gattserver/test/test.pro @@ -1,6 +1,8 @@ -QT = core bluetooth testlib +QT = core bluetooth bluetooth-private testlib TARGET = tst_qlowenergycontroller-gattserver CONFIG += testcase c++11 +config_linux_crypto_api:DEFINES += CONFIG_LINUX_CRYPTO_API + SOURCES += tst_qlowenergycontroller-gattserver.cpp diff --git a/tests/auto/qlowenergycontroller-gattserver/test/tst_qlowenergycontroller-gattserver.cpp b/tests/auto/qlowenergycontroller-gattserver/test/tst_qlowenergycontroller-gattserver.cpp index 00fc2152..25cbc17a 100644 --- a/tests/auto/qlowenergycontroller-gattserver/test/tst_qlowenergycontroller-gattserver.cpp +++ b/tests/auto/qlowenergycontroller-gattserver/test/tst_qlowenergycontroller-gattserver.cpp @@ -42,7 +42,12 @@ #include <QtTest/qsignalspy.h> #include <QtTest/QtTest> +#ifdef Q_OS_LINUX +#include <QtBluetooth/private/lecmacverifier_p.h> +#endif + #include <algorithm> +#include <cstring> using namespace QBluetooth; @@ -56,6 +61,8 @@ private slots: // Static, local stuff goes here. void advertisingParameters(); void advertisingData(); + void cmacVerifier(); + void cmacVerifier_data(); void connectionParameters(); void controllerType(); void serviceData(); @@ -145,6 +152,42 @@ void TestQLowEnergyControllerGattServer::advertisingData() QVERIFY(data != QLowEnergyAdvertisingData()); } +void TestQLowEnergyControllerGattServer::cmacVerifier() +{ +#ifdef CONFIG_LINUX_CRYPTO_API + // Test data comes from spec v4.2, Vol 3, Part H, Appendix D.1 + const quint128 csrk = { + { 0x3c, 0x4f, 0xcf, 0x09, 0x88, 0x15, 0xf7, 0xab, + 0xa6, 0xd2, 0xae, 0x28, 0x16, 0x15, 0x7e, 0x2b } + }; + QFETCH(QByteArray, message); + QFETCH(quint64, expectedMac); + const bool success = LeCmacVerifier().verify(message, csrk, expectedMac); + QVERIFY(success); +#else // CONFIG_LINUX_CRYPTO_API + QSKIP("CMAC verification test only applicable on Linux with crypto API"); +#endif // Q_OS_LINUX +} + +void TestQLowEnergyControllerGattServer::cmacVerifier_data() +{ + QTest::addColumn<QByteArray>("message"); + QTest::addColumn<quint64>("expectedMac"); + QTest::newRow("D1.1") << QByteArray() << Q_UINT64_C(0xbb1d6929e9593728); + QTest::newRow("D1.2") << QByteArray::fromHex("2a179373117e3de9969f402ee2bec16b") + << Q_UINT64_C(0x070a16b46b4d4144); + QByteArray messageD13 = QByteArray::fromHex("6bc1bee22e409f96e93d7e117393172aae2d8a57" + "1e03ac9c9eb76fac45af8e5130c81c46a35ce411"); + std::reverse(messageD13.begin(), messageD13.end()); + QTest::newRow("D1.3") << messageD13 << Q_UINT64_C(0xdfa66747de9ae630); + QByteArray messageD14 = QByteArray::fromHex("6bc1bee22e409f96e93d7e117393172a" + "ae2d8a571e03ac9c9eb76fac45af8e51" + "30c81c46a35ce411e5fbc1191a0a52ef" + "f69f2445df4f9b17ad2b417be66c3710"); + std::reverse(messageD14.begin(), messageD14.end()); + QTest::newRow("D1.4") << messageD14 << Q_UINT64_C(0x51f0bebf7e3b9d92); +} + void TestQLowEnergyControllerGattServer::connectionParameters() { QLowEnergyConnectionParameters connParams; |