nymea-plugins/nuki/nukicontroller.cpp

643 lines
23 KiB
C++

// SPDX-License-Identifier: GPL-3.0-or-later
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright (C) 2013 - 2024, nymea GmbH
* Copyright (C) 2024 - 2025, chargebyte austria GmbH
*
* This file is part of nymea-plugins.
*
* nymea-plugins is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* nymea-plugins is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with nymea-plugins. If not, see <https://www.gnu.org/licenses/>.
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "nukicontroller.h"
#include "extern-plugininfo.h"
#include <QByteArray>
#include <QDataStream>
extern "C" {
#include "sodium.h"
}
NukiController::NukiController(NukiAuthenticator *nukiAuthenticator, BluetoothGattCharacteristic *userDataCharacteristic, QObject *parent) :
QObject(parent),
m_nukiAuthenticator(nukiAuthenticator),
m_userDataCharacteristic(userDataCharacteristic)
{
#ifdef QT_DEBUG
// Enable full debug messages containing sensible data for debug builds
m_debug = true;
#endif
connect(m_userDataCharacteristic, &BluetoothGattCharacteristic::valueChanged, this, &NukiController::onUserDataCharacteristicChanged);
}
NukiUtils::NukiState NukiController::nukiState() const
{
return m_nukiState;
}
NukiUtils::LockState NukiController::nukiLockState() const
{
return m_nukiLockState;
}
NukiUtils::LockTrigger NukiController::nukiLockTrigger() const
{
return m_nukiLockTrigger;
}
bool NukiController::batteryCritical() const
{
return m_batteryCritical;
}
bool NukiController::readLockState()
{
if (m_state != NukiControllerStateIdle) {
// TODO: maybe queue commands
qCWarning(dcNuki()) << "Controller: Could not read lock state, Nuki is currenty busy";
return false;
}
if (!m_nukiAuthenticator->isValid()) {
qCWarning(dcNuki()) << "Invalid authenticator. Please authenticate the thing first.";
return false;
}
setState(NukiControllerStateReadingLockStates);
return true;
}
bool NukiController::readConfiguration()
{
if (m_state != NukiControllerStateIdle) {
// TODO: maybe queue commands
qCWarning(dcNuki()) << "Controller: Could not read lock state, Nuki is currenty busy";
return false;
}
if (!m_nukiAuthenticator->isValid()) {
qCWarning(dcNuki()) << "Invalid authenticator. Please authenticate the thing first.";
return false;
}
setState(NukiControllerStateReadingConfiguration);
return true;
}
bool NukiController::lock()
{
if (m_state != NukiControllerStateIdle) {
// TODO: maybe queue commands
qCWarning(dcNuki()) << "Controller: Could not lock, Nuki is currenty busy";
return false;
}
if (!m_nukiAuthenticator->isValid()) {
qCWarning(dcNuki()) << "Invalid authenticator. Please authenticate the thing first.";
return false;
}
setState(NukiControllerStateLockActionRequestChallange);
return true;
}
bool NukiController::unlock()
{
if (m_state != NukiControllerStateIdle) {
// TODO: maybe queue commands
qCWarning(dcNuki()) << "Controller: Could not lock, Nuki is currenty busy";
return false;
}
if (!m_nukiAuthenticator->isValid()) {
qCWarning(dcNuki()) << "Invalid authenticator. Please authenticate the thing first.";
return false;
}
setState(NukiControllerStateUnlockActionRequestChallange);
return true;
}
bool NukiController::unlatch()
{
if (m_state != NukiControllerStateIdle) {
// TODO: maybe queue commands
qCWarning(dcNuki()) << "Controller: Could not unlatch, Nuki is currenty busy";
return false;
}
if (!m_nukiAuthenticator->isValid()) {
qCWarning(dcNuki()) << "Invalid authenticator. Please authenticate the thing first.";
return false;
}
setState(NukiControllerStateUnlatchActionRequestChallange);
return true;
}
void NukiController::setState(NukiController::NukiControllerState state)
{
if (m_state == state)
return;
m_state = state;
qCDebug(dcNuki()) << m_state;
switch (m_state) {
case NukiControllerStateIdle:
break;
case NukiControllerStateReadingLockStates:
sendReadLockStateRequest();
break;
case NukiControllerStateReadingConfigurationRequestChallange:
sendRequestChallengeRequest();
break;
case NukiControllerStateReadingConfigurationExecute:
sendReadConfigurationRequest();
setState(NukiControllerStateReadingConfiguration);
break;
case NukiControllerStateReadingConfiguration:
break;
case NukiControllerStateLockActionRequestChallange:
sendRequestChallengeRequest();
break;
case NukiControllerStateLockActionExecute:
sendLockActionRequest(NukiUtils::LockActionLock);
setState(NukiControllerStateLockActionAccepted);
break;
case NukiControllerStateLockActionAccepted:
break;
case NukiControllerStateUnlockActionRequestChallange:
sendRequestChallengeRequest();
break;
case NukiControllerStateUnlockActionExecute:
sendLockActionRequest(NukiUtils::LockActionUnlock);
setState(NukiControllerStateUnlockActionAccepted);
break;
case NukiControllerStateUnlockActionAccepted:
break;
case NukiControllerStateUnlatchActionRequestChallange:
sendRequestChallengeRequest();
break;
case NukiControllerStateUnlatchActionExecute:
sendLockActionRequest(NukiUtils::LockActionUnlatch);
setState(NukiControllerStateUnlatchActionAccepted);
break;
case NukiControllerStateUnlatchActionAccepted:
break;
default:
break;
}
emit stateChanged(m_state);
}
void NukiController::resetMessageBuffer()
{
m_messageBuffer.clear();
m_messageBufferNonce.clear();
m_messageBufferIdentifier = 0;
m_messageBufferLength = 0;
m_messageBufferCounter = 0;
}
void NukiController::processNukiStatesData(const QByteArray &data)
{
quint8 nukiState = 0;
quint8 nukiLockState = 0;
quint8 nukiLockTrigger = 0;
quint16 year = 1970;
quint8 month = 1;
quint8 day = 1;
quint8 hour = 0;
quint8 minute = 0;
quint8 second = 0;
qint16 utcOffset = 0;
quint8 batteryCritical = 0;
QByteArray payload = data;
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
QDataStream stream(&payload, QDataStream::ReadOnly);
#else
QDataStream stream(&payload, QIODevice::ReadOnly);
#endif
stream.setByteOrder(QDataStream::LittleEndian);
stream >> nukiState >> nukiLockState >> nukiLockTrigger >> year >> month >> day >> hour >> minute >> second >> utcOffset >> batteryCritical;
m_nukiState = static_cast<NukiUtils::NukiState>(nukiState);
m_nukiLockState = static_cast<NukiUtils::LockState>(nukiLockState);
m_nukiLockTrigger = static_cast<NukiUtils::LockTrigger>(nukiLockTrigger);
m_nukiDateTime = QDateTime(QDate(year, month, day), QTime(hour, minute, second));
m_nukiUtcOffset = utcOffset;
m_batteryCritical = (batteryCritical == 0 ? false : true);
if (m_debug) qCDebug(dcNuki()) << "--------------------:" << m_state;
if (m_debug) qCDebug(dcNuki()) << " Nuki state :" << m_nukiState;
if (m_debug) qCDebug(dcNuki()) << " Nuki lock state :" << m_nukiLockState;
if (m_debug) qCDebug(dcNuki()) << " Lock trigger :" << m_nukiLockTrigger;
if (m_debug) qCDebug(dcNuki()) << " Date time :" << m_nukiDateTime.toString("dd.MM.yyyy hh:mm:ss") << "UTC offset:" << m_nukiUtcOffset;
if (m_debug) qCDebug(dcNuki()) << " Battery critical:" << m_batteryCritical;
qCDebug(dcNuki()) << "Nuki states refreshed.";
emit nukiStatesChanged();
}
void NukiController::processNukiConfigData(const QByteArray &data)
{
qCDebug(dcNuki()) << "Processing config data from nuki" << data;
}
void NukiController::processNukiErrorReport(const QByteArray &data)
{
qint8 errorCode;
quint16 nukiCommand;
QByteArray payload = data;
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
QDataStream stream(&payload, QDataStream::ReadOnly);
#else
QDataStream stream(&payload, QIODevice::ReadOnly);
#endif
stream.setByteOrder(QDataStream::LittleEndian);
stream >> errorCode >> nukiCommand;
qCDebug(dcNuki()) << "Received error report" << static_cast<NukiUtils::ErrorCode>(errorCode) << static_cast<NukiUtils::Command>(nukiCommand);
}
void NukiController::processUserDataNotification(const QByteArray nonce, quint32 authorizationIdentifier, const QByteArray &privateData)
{
QByteArray decryptedMessage = m_nukiAuthenticator->decryptData(privateData, nonce);
// Process decrypted data
if (!NukiUtils::validateMessageCrc(decryptedMessage)) {
qCWarning(dcNuki()) << "Controller: User notification data has invalid CRC CCITT value. Rejecting data.";
return;
}
// We have the unencrypted and valid PDATA, let's see what this is
quint32 decryptedAuthenticationId = NukiUtils::convertByteArrayToUint32BigEndian(decryptedMessage.left(4));
NukiUtils::Command command = static_cast<NukiUtils::Command>(NukiUtils::convertByteArrayToUint16BigEndian(decryptedMessage.mid(4, 2)));
QByteArray payload = decryptedMessage.mid(6, decryptedMessage.length() - 8);
qCDebug(dcNuki()) << "Controller: Processing notification" << command;
if (m_debug) qCDebug(dcNuki()) << " Nonce :" << NukiUtils::convertByteArrayToHexStringCompact(nonce);
if (m_debug) qCDebug(dcNuki()) << " Authorization ID:" << authorizationIdentifier;
if (m_debug) qCDebug(dcNuki()) << " Encrypted data :" << NukiUtils::convertByteArrayToHexStringCompact(privateData) << privateData.length();
if (m_debug) qCDebug(dcNuki()) << " Decrypted data :" << NukiUtils::convertByteArrayToHexStringCompact(decryptedMessage) << decryptedMessage.length();
if (m_debug) qCDebug(dcNuki()) << " Command :" << command;
if (m_debug) qCDebug(dcNuki()) << " Authorization ID:" << NukiUtils::convertByteArrayToHexStringCompact(decryptedMessage.left(4)) << decryptedAuthenticationId;
if (m_debug) qCDebug(dcNuki()) << " Payload :" << NukiUtils::convertByteArrayToHexStringCompact(payload);
// Lets see if this was an expected
switch (m_state) {
case NukiControllerStateReadingLockStates:
// We are expecting the states notification
if (command == NukiUtils::CommandNukiStates) {
processNukiStatesData(payload);
emit readNukiStatesFinished(true);
// TODO: read configuration
setState(NukiControllerStateIdle);
return;
}
break;
case NukiControllerStateReadingConfigurationRequestChallange:
if (command == NukiUtils::CommandChallenge) {
m_nukiNonce = payload;
setState(NukiControllerStateReadingConfigurationExecute);
return;
}
break;
case NukiControllerStateReadingConfiguration:
if (command == NukiUtils::CommandConfig) {
processNukiConfigData(payload);
setState(NukiControllerStateIdle);
return;
}
break;
case NukiControllerStateLockActionRequestChallange:
// We are expecting callange message
if (command == NukiUtils::CommandChallenge) {
m_nukiNonce = payload;
setState(NukiControllerStateLockActionExecute);
return;
}
break;
case NukiControllerStateLockActionAccepted:
// We are expecting the status
if (command == NukiUtils::CommandStatus) {
NukiUtils::StatusCode statusCode = static_cast<NukiUtils::StatusCode>((quint8)payload.at(0));
qCDebug(dcNuki()) << "Controller:" << statusCode;
switch (statusCode) {
case NukiUtils::StatusCodeAccepted:
// Lets wait for completed
break;
case NukiUtils::StatusCodeCompeted:
emit lockFinished(true);
setState(NukiControllerStateIdle);
break;
default:
break;
}
}
break;
case NukiControllerStateUnlockActionRequestChallange:
// We are expecting callenge message
if (command == NukiUtils::CommandChallenge) {
m_nukiNonce = payload;
setState(NukiControllerStateUnlockActionExecute);
return;
}
break;
case NukiControllerStateUnlockActionAccepted:
// We are expecting the status
if (command == NukiUtils::CommandStatus) {
NukiUtils::StatusCode statusCode = static_cast<NukiUtils::StatusCode>((quint8)payload.at(0));
qCDebug(dcNuki()) << "Controller:" << statusCode;
switch (statusCode) {
case NukiUtils::StatusCodeAccepted:
// Lets wait for completed
break;
case NukiUtils::StatusCodeCompeted:
emit unlockFinished(true);
setState(NukiControllerStateIdle);
break;
default:
break;
}
}
break;
case NukiControllerStateUnlatchActionRequestChallange:
// We are expecting callenge message
if (command == NukiUtils::CommandChallenge) {
m_nukiNonce = payload;
setState(NukiControllerStateUnlatchActionExecute);
return;
}
break;
case NukiControllerStateUnlatchActionAccepted:
// We are expecting the status
if (command == NukiUtils::CommandStatus) {
NukiUtils::StatusCode statusCode = static_cast<NukiUtils::StatusCode>((quint8)payload.at(0));
qCDebug(dcNuki()) << "Controller:" << statusCode;
switch (statusCode) {
case NukiUtils::StatusCodeAccepted:
// Lets wait for completed
break;
case NukiUtils::StatusCodeCompeted:
emit unlatchFinished(true);
setState(NukiControllerStateIdle);
break;
default:
break;
}
}
break;
default:
break;
}
// Other notification
switch (command) {
case NukiUtils::CommandNukiStates:
processNukiStatesData(payload);
break;
case NukiUtils::CommandErrorReport:
processNukiErrorReport(payload);
break;
case NukiUtils::CommandStatus: {
NukiUtils::StatusCode statusCode = static_cast<NukiUtils::StatusCode>((quint8)payload.at(0));
qCDebug(dcNuki()) << "Controller:" << statusCode;
break;
}
default:
qCWarning(dcNuki()) << "Controller: Received unhandled notification:" << command;
break;
}
}
void NukiController::sendReadLockStateRequest()
{
qCDebug(dcNuki()) << "Controller: Reading lock state";
// Create data for encryption
QByteArray payload;
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
QDataStream stream(&payload, QDataStream::WriteOnly);
#else
QDataStream stream(&payload, QIODevice::WriteOnly);
#endif
stream.setByteOrder(QDataStream::LittleEndian);
stream << static_cast<quint16>(NukiUtils::CommandNukiStates);
// Create unencrypted PDATA
QByteArray unencryptedMessage = NukiUtils::createRequestMessageForUnencryptedForEncryption(m_nukiAuthenticator->authorizationId(), NukiUtils::CommandRequestData, payload);
// Encrypt PDATA
QByteArray nonce = m_nukiAuthenticator->generateNonce(crypto_box_NONCEBYTES);
QByteArray encryptedMessage = m_nukiAuthenticator->encryptData(unencryptedMessage, nonce);
// Create ADATA
QByteArray header;
header.append(nonce);
header.append(m_nukiAuthenticator->authorizationIdRawData());
header.append(NukiUtils::converUint16ToByteArrayLittleEndian(static_cast<quint16>(encryptedMessage.length())));
// Message ADATA + PDATA
QByteArray message;
message.append(header);
message.append(encryptedMessage);
// Send data
qCDebug(dcNuki()) << "Controller: Sending read lock states request";
if (m_debug) qCDebug(dcNuki()) << " Nonce :" << NukiUtils::convertByteArrayToHexStringCompact(nonce);
if (m_debug) qCDebug(dcNuki()) << " Header :" << NukiUtils::convertByteArrayToHexStringCompact(header);
if (m_debug) qCDebug(dcNuki()) << "Controller: -->" << NukiUtils::convertByteArrayToHexStringCompact(message);
m_userDataCharacteristic->writeCharacteristic(message);
}
void NukiController::sendReadConfigurationRequest()
{
qCDebug(dcNuki()) << "Controller: Reading configurations";
// Create data for encryption
QByteArray payload;
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
QDataStream stream(&payload, QDataStream::WriteOnly);
#else
QDataStream stream(&payload, QIODevice::WriteOnly);
#endif
stream.setByteOrder(QDataStream::LittleEndian);
stream << static_cast<quint16>(NukiUtils::CommandRequestConfig);
for (int i = 0; i < m_nukiNonce.length(); i++) {
stream << static_cast<quint8>(m_nukiNonce.at(i));
}
// Create unencrypted PDATA
QByteArray unencryptedMessage = NukiUtils::createRequestMessageForUnencryptedForEncryption(m_nukiAuthenticator->authorizationId(), NukiUtils::CommandRequestData, payload);
// Encrypt PDATA
QByteArray nonce = m_nukiAuthenticator->generateNonce(crypto_box_NONCEBYTES);
QByteArray encryptedMessage = m_nukiAuthenticator->encryptData(unencryptedMessage, nonce);
// Create ADATA
QByteArray header;
header.append(nonce);
header.append(m_nukiAuthenticator->authorizationIdRawData());
header.append(NukiUtils::converUint16ToByteArrayLittleEndian(static_cast<quint16>(encryptedMessage.length())));
// Message ADATA + PDATA
QByteArray message;
message.append(header);
message.append(encryptedMessage);
// Send data
qCDebug(dcNuki()) << "Controller: Sending get config request";
if (m_debug) qCDebug(dcNuki()) << " Nonce :" << NukiUtils::convertByteArrayToHexStringCompact(nonce);
if (m_debug) qCDebug(dcNuki()) << " Header :" << NukiUtils::convertByteArrayToHexStringCompact(header);
if (m_debug) qCDebug(dcNuki()) << "Controller: -->" << NukiUtils::convertByteArrayToHexStringCompact(message);
m_userDataCharacteristic->writeCharacteristic(message);
}
void NukiController::sendRequestChallengeRequest()
{
qCDebug(dcNuki()) << "Controller: Request challenge";
// Create data for encryption
QByteArray payload;
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
QDataStream stream(&payload, QDataStream::WriteOnly);
#else
QDataStream stream(&payload, QIODevice::WriteOnly);
#endif
stream.setByteOrder(QDataStream::LittleEndian);
stream << static_cast<quint16>(NukiUtils::CommandChallenge);
// Create unencrypted PDATA
QByteArray unencryptedMessage = NukiUtils::createRequestMessageForUnencryptedForEncryption(m_nukiAuthenticator->authorizationId(), NukiUtils::CommandRequestData, payload);
// Encrypt PDATA
QByteArray nonce = m_nukiAuthenticator->generateNonce(crypto_box_NONCEBYTES);
QByteArray encryptedMessage = m_nukiAuthenticator->encryptData(unencryptedMessage, nonce);
// Create ADATA
QByteArray header;
header.append(nonce);
header.append(m_nukiAuthenticator->authorizationIdRawData());
header.append(NukiUtils::converUint16ToByteArrayLittleEndian(static_cast<quint16>(encryptedMessage.length())));
// Message ADATA + PDATA
QByteArray message;
message.append(header);
message.append(encryptedMessage);
// Send data
qCDebug(dcNuki()) << "Controller: Sending challange request";
if (m_debug) qCDebug(dcNuki()) << " Nonce :" << NukiUtils::convertByteArrayToHexStringCompact(nonce);
if (m_debug) qCDebug(dcNuki()) << " Header :" << NukiUtils::convertByteArrayToHexStringCompact(header);
if (m_debug) qCDebug(dcNuki()) << "Controller: -->" << NukiUtils::convertByteArrayToHexStringCompact(message);
m_userDataCharacteristic->writeCharacteristic(message);
}
void NukiController::sendLockActionRequest(NukiUtils::LockAction lockAction, quint8 flag)
{
qCDebug(dcNuki()) << "Controller: Send lock request" << lockAction;
QByteArray nonce = m_nukiAuthenticator->generateNonce(crypto_box_NONCEBYTES);
// Create data for encryption
QByteArray payload;
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
QDataStream stream(&payload, QDataStream::WriteOnly);
#else
QDataStream stream(&payload, QIODevice::WriteOnly);
#endif
stream.setByteOrder(QDataStream::LittleEndian);
stream << static_cast<quint8>(lockAction);
stream << static_cast<quint32>(m_nukiAuthenticator->authorizationId());
stream << flag;
for (int i = 0; i < m_nukiNonce.length(); i++) {
stream << static_cast<quint8>(m_nukiNonce.at(i));
}
// Create unencrypted PDATA
QByteArray unencryptedMessage = NukiUtils::createRequestMessageForUnencryptedForEncryption(m_nukiAuthenticator->authorizationId(), NukiUtils::CommandLockAction, payload);
// Encrypt PDATA
QByteArray encryptedMessage = m_nukiAuthenticator->encryptData(unencryptedMessage, nonce);
// Create ADATA
QByteArray header;
header.append(nonce);
header.append(m_nukiAuthenticator->authorizationIdRawData());
header.append(NukiUtils::converUint16ToByteArrayLittleEndian(static_cast<quint16>(encryptedMessage.length())));
// Message ADATA + PDATA
QByteArray message;
message.append(header);
message.append(encryptedMessage);
// Send data
qCDebug(dcNuki()) << "Controller: Sending lock request";
if (m_debug) qCDebug(dcNuki()) << " Nonce :" << NukiUtils::convertByteArrayToHexStringCompact(nonce);
if (m_debug) qCDebug(dcNuki()) << " Header :" << NukiUtils::convertByteArrayToHexStringCompact(header);
if (m_debug) qCDebug(dcNuki()) << "Controller: -->" << NukiUtils::convertByteArrayToHexStringCompact(message);
m_userDataCharacteristic->writeCharacteristic(message);
}
void NukiController::onUserDataCharacteristicChanged(const QByteArray &value)
{
if (m_debug) qCDebug(dcNuki()) << "Controller: Data received: <--" << NukiUtils::convertByteArrayToHexStringCompact(value);
m_messageBuffer = value;
if (m_messageBuffer.length() < 30) {
qCWarning(dcNuki()) << "Controller: Cannot understand message. Rejecting.";
resetMessageBuffer();
return;
}
// Parse message length
// ADATA: 24 byte nonce, 4 byte autorization, 2 byte encrypted message length
m_messageBufferAData = m_messageBuffer.left(30);
m_messageBufferPData = m_messageBuffer.right(m_messageBuffer.length() - 30);
m_messageBufferNonce = m_messageBufferAData.left(24);
QByteArray messageInformation = m_messageBufferAData.right(6);
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
QDataStream stream(&messageInformation, QDataStream::ReadOnly);
#else
QDataStream stream(&messageInformation, QIODevice::ReadOnly);
#endif
stream.setByteOrder(QDataStream::LittleEndian);
stream >> m_messageBufferIdentifier >> m_messageBufferLength;
if (m_messageBufferPData.length() == m_messageBufferLength) {
processUserDataNotification(m_messageBufferNonce, m_messageBufferIdentifier, m_messageBufferPData);
resetMessageBuffer();
}
}