Rework nuki to use indications

master
Simon Stürz 2021-03-18 10:37:27 +01:00
parent 6f46c2c16a
commit c1da14fd55
8 changed files with 197 additions and 191 deletions

View File

@ -37,13 +37,6 @@
#include <QDataStream>
#include <QTimer>
static QBluetoothUuid initializationServiceUuid = QBluetoothUuid(QUuid("a92ee000-5501-11e4-916c-0800200c9a66"));
static QBluetoothUuid pairingServiceUuid = QBluetoothUuid(QUuid("a92ee100-5501-11e4-916c-0800200c9a66"));
static QBluetoothUuid pairingDataCharacteristicUuid = QBluetoothUuid(QUuid("a92ee101-5501-11e4-916c-0800200c9a66"));
static QBluetoothUuid keyturnerServiceUuid = QBluetoothUuid(QUuid("a92ee200-5501-11e4-916c-0800200c9a66"));
static QBluetoothUuid keyturnerDataCharacteristicUuid = QBluetoothUuid(QUuid("a92ee201-5501-11e4-916c-0800200c9a66"));
static QBluetoothUuid keyturnerUserDataCharacteristicUuid = QBluetoothUuid(QUuid("a92ee202-5501-11e4-916c-0800200c9a66"));
Nuki::Nuki(Thing *thing, BluetoothDevice *bluetoothDevice, QObject *parent) :
QObject(parent),
m_thing(thing),
@ -204,40 +197,12 @@ void Nuki::executeCurrentAction()
bool Nuki::enableNotificationsIndications(BluetoothGattCharacteristic *characteristic)
{
qCDebug(dcNuki()) << "Start enable notifications/indications for" << characteristic;
// Get the client configuration descriptor
// https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Descriptors/org.bluetooth.descriptor.gatt.client_characteristic_configuration.xml
BluetoothGattDescriptor *clientConfiguration = characteristic->getDescriptor(QBluetoothUuid::ClientCharacteristicConfiguration);
if (!clientConfiguration) {
qCWarning(dcNuki()) << "Could not start notification/indications for" << characteristic << "because the client configuration descriptor is missing";
qCDebug(dcNuki()) << "Enable notifications on" << characteristic;
if (!characteristic->startNotifications()) {
qCDebug(dcNuki()) << "Failed to start notifications on" << characteristic;
return false;
}
// qCDebug(dcNuki()) << "Enable notifications on" << characteristic;
// if (!characteristic->startNotifications()) {
// qCDebug(dcNuki()) << "Failed to start notifications on" << characteristic;
// return false;
// }
if (characteristic->properties().testFlag(BluetoothGattCharacteristic::Indicate)) {
qCDebug(dcNuki()) << "Enable indications on" << characteristic;
QByteArray configuration;
QDataStream stream(&configuration, QIODevice::WriteOnly);
stream.setByteOrder(QDataStream::LittleEndian);
stream << static_cast<quint16>(2);
if (!clientConfiguration->writeValue(configuration)) {
qCWarning(dcNuki()) << "Failed to write client configuration descriptor on" << characteristic;
return false;
}
} else {
qCWarning(dcNuki()) << "Could not enable notifications. Access properties do not allow indicate or notify" << characteristic;
return false;
}
clientConfiguration->readValue();
return true;
}
@ -451,12 +416,12 @@ bool Nuki::init()
return false;
}
if (!m_bluetoothDevice->hasService(pairingServiceUuid)) {
if (!m_bluetoothDevice->hasService(pairingServiceUuid())) {
qCWarning(dcNuki()) << "Could not find pairing service on device" << m_bluetoothDevice;
return false;
}
if (!m_bluetoothDevice->hasService(keyturnerServiceUuid)) {
if (!m_bluetoothDevice->hasService(keyturnerServiceUuid())) {
qCWarning(dcNuki()) << "Could not find key turner service on device" << m_bluetoothDevice;
return false;
}
@ -467,25 +432,25 @@ bool Nuki::init()
connect(m_deviceInformationService, &BluetoothGattService::characteristicReadFinished, this, &Nuki::onDeviceInfoCharacteristicReadFinished);
// Keyturner service
m_keyturnerService = m_bluetoothDevice->getService(keyturnerServiceUuid);
if (!m_keyturnerService->hasCharacteristic(keyturnerUserDataCharacteristicUuid)) {
m_keyturnerService = m_bluetoothDevice->getService(keyturnerServiceUuid());
if (!m_keyturnerService->hasCharacteristic(keyturnerUserDataCharacteristicUuid())) {
qCWarning(dcNuki()) << "Could not find user data characteristc on device" << m_bluetoothDevice;
return false;
}
// Set key turner characteristics for data and user data
if (!m_keyturnerService->hasCharacteristic(keyturnerDataCharacteristicUuid)) {
if (!m_keyturnerService->hasCharacteristic(keyturnerDataCharacteristicUuid())) {
qCWarning(dcNuki()) << "Could not find data characteristc on device" << m_bluetoothDevice;
return false;
}
// Enable notifications/indications
m_keyturnerUserDataCharacteristic = m_keyturnerService->getCharacteristic(keyturnerUserDataCharacteristicUuid);
m_keyturnerUserDataCharacteristic = m_keyturnerService->getCharacteristic(keyturnerUserDataCharacteristicUuid());
if (!enableNotificationsIndications(m_keyturnerUserDataCharacteristic)) {
qCWarning(dcNuki()) << "Could not enable notifications/indications for user data characteristic.";
return false;
}
m_keyturnerDataCharacteristic = m_keyturnerService->getCharacteristic(keyturnerDataCharacteristicUuid);
m_keyturnerDataCharacteristic = m_keyturnerService->getCharacteristic(keyturnerDataCharacteristicUuid());
if (!enableNotificationsIndications(m_keyturnerDataCharacteristic)) {
qCWarning(dcNuki()) << "Could not enable notifications/indications for key turner data characteristic.";
return false;
@ -494,13 +459,13 @@ bool Nuki::init()
// Pairing service
m_pairingService = m_bluetoothDevice->getService(pairingServiceUuid);
if (!m_pairingService->hasCharacteristic(pairingDataCharacteristicUuid)) {
m_pairingService = m_bluetoothDevice->getService(pairingServiceUuid());
if (!m_pairingService->hasCharacteristic(pairingDataCharacteristicUuid())) {
qCWarning(dcNuki()) << "Could not find pairing data characteristc on device" << m_bluetoothDevice;
return false;
}
m_pairingDataCharacteristic = m_pairingService->getCharacteristic(pairingDataCharacteristicUuid);
m_pairingDataCharacteristic = m_pairingService->getCharacteristic(pairingDataCharacteristicUuid());
if (!enableNotificationsIndications(m_pairingDataCharacteristic)) {
qCWarning(dcNuki()) << "Could not enable notifications for pairing characteristic.";
return false;
@ -590,8 +555,6 @@ void Nuki::setAvailable(bool available)
if (m_available) {
executeCurrentAction();
} else {
// Finish any running actions
finishCurrentAction(false);

View File

@ -50,7 +50,6 @@
class Nuki : public QObject
{
Q_OBJECT
public:
@ -78,6 +77,12 @@ public:
void clearSettings();
static QBluetoothUuid initializationServiceUuid() { return QBluetoothUuid(QUuid("a92ee000-5501-11e4-916c-0800200c9a66")); }
static QBluetoothUuid pairingServiceUuid() { return QBluetoothUuid(QUuid("a92ee100-5501-11e4-916c-0800200c9a66")); }
static QBluetoothUuid pairingDataCharacteristicUuid() { return QBluetoothUuid(QUuid("a92ee101-5501-11e4-916c-0800200c9a66")); }
static QBluetoothUuid keyturnerServiceUuid() { return QBluetoothUuid(QUuid("a92ee200-5501-11e4-916c-0800200c9a66")); }
static QBluetoothUuid keyturnerDataCharacteristicUuid() { return QBluetoothUuid(QUuid("a92ee201-5501-11e4-916c-0800200c9a66")); }
static QBluetoothUuid keyturnerUserDataCharacteristicUuid() { return QBluetoothUuid(QUuid("a92ee202-5501-11e4-916c-0800200c9a66")); }
private:
Thing *m_thing = nullptr;

View File

@ -58,6 +58,7 @@ NukiAuthenticator::NukiAuthenticator(const QBluetoothHostInfo &hostInfo, Bluetoo
// Check if we have authentication data for this thing and set initial state
loadData();
if (isValid()) {
qCDebug(dcNuki()) << "Found valid authroization data for" << hostInfo.address().toString();
setState(AuthenticationStateAuthenticated);
} else {
setState(AuthenticationStateUnauthenticated);
@ -81,7 +82,7 @@ bool NukiAuthenticator::isValid() const
return !m_privateKey.isEmpty() &&
!m_publicKey.isEmpty() &&
!m_publicKeyNuki.isEmpty() &&
!m_authorizationId == 0 &&
m_authorizationId != 0 &&
!m_authorizationIdRawData.isEmpty() &&
!m_uuid.isEmpty();
}
@ -208,7 +209,6 @@ void NukiAuthenticator::setState(NukiAuthenticator::AuthenticationState state)
switch (m_state) {
case AuthenticationStateUnauthenticated:
resetExpectedData();
break;
case AuthenticationStateAuthenticated:
qCDebug(dcNuki()) << "Device" << m_hostInfo.address().toString() << "authenticated.";
@ -218,47 +218,37 @@ void NukiAuthenticator::setState(NukiAuthenticator::AuthenticationState state)
if (m_debug) qCDebug(dcNuki()) << " Authorization ID:" << NukiUtils::convertByteArrayToHexStringCompact(m_authorizationIdRawData) << m_authorizationId;
break;
case AuthenticationStateRequestPublicKey:
resetExpectedData(NukiUtils::CommandPublicKey, 2);
requestPublicKey();
break;
case AuthenticationStateGenerateKeyPair:
resetExpectedData();
generateKeyPair();
setState(AuthenticationStateSendPublicKey);
break;
case AuthenticationStateSendPublicKey:
resetExpectedData(NukiUtils::CommandChallenge, 2);
sendPublicKey();
setState(AuthenticationStateReadChallenge);
break;
case AuthenticationStateReadChallenge:
resetExpectedData(NukiUtils::CommandChallenge, 2);
break;
case AuthenticationStateAutorization:
sendAuthorizationAuthenticator();
setState(AuthenticationStateReadSecondChallenge);
break;
case AuthenticationStateReadSecondChallenge:
resetExpectedData(NukiUtils::CommandChallenge, 2);
break;
case AuthenticationStateAuthenticateData:
resetExpectedData();
sendAuthenticateData();
setState(AuthenticationStateAuthorizationId);
break;
case AuthenticationStateAuthorizationId:
resetExpectedData(NukiUtils::CommandAuthorizationId, 5);
break;
case AuthenticationStateAuthorizationIdConfirm:
resetExpectedData();
sendAuthoizationIdConfirm();
setState(AuthenticationStateStatus);
break;
case AuthenticationStateStatus:
resetExpectedData(NukiUtils::CommandStatus);
break;
case AuthenticationStateError:
resetExpectedData();
emit errorOccured(m_error);
emit authenticationProcessFinished(false);
break;
@ -268,14 +258,6 @@ void NukiAuthenticator::setState(NukiAuthenticator::AuthenticationState state)
}
}
void NukiAuthenticator::resetExpectedData(NukiUtils::Command command, int expectedCount)
{
m_currentReceivingCommand = command;
m_currentReceivingCurrentCount = 0;
m_currentReceivingExpectedCount = expectedCount;
m_currentReceivingData.clear();
}
bool NukiAuthenticator::createAuthenticator(const QByteArray content)
{
// Create shared key
@ -469,7 +451,6 @@ void NukiAuthenticator::loadData()
void NukiAuthenticator::onPairingDataCharacteristicChanged(const QByteArray &value)
{
if (m_debug) qCDebug(dcNuki()) << "Authenticator data received: <--" << NukiUtils::convertByteArrayToHexStringCompact(value);
// Process pairing characteristic data
QByteArray data = QByteArray(value);
@ -478,12 +459,9 @@ void NukiAuthenticator::onPairingDataCharacteristicChanged(const QByteArray &val
quint16 command;
stream >> command;
// Check if we are collecting data for multi part notification
if (m_currentReceivingCurrentCount > 0) {
command = m_currentReceivingCommand;
}
m_currentReceivingData = value;
if (m_debug) qCDebug(dcNuki()) << static_cast<NukiUtils::Command>(command);
if (m_debug) qCDebug(dcNuki()) << "Authenticator data received: <--" << static_cast<NukiUtils::Command>(command) << "|" << NukiUtils::convertByteArrayToHexStringCompact(value);
switch (command) {
case NukiUtils::CommandErrorReport:
@ -500,9 +478,6 @@ void NukiAuthenticator::onPairingDataCharacteristicChanged(const QByteArray &val
setState(AuthenticationStateError);
break;
case NukiUtils::CommandPublicKey:
m_currentReceivingCurrentCount++;
m_currentReceivingData.append(value);
if (m_currentReceivingCurrentCount == m_currentReceivingExpectedCount) {
if (!NukiUtils::validateMessageCrc(m_currentReceivingData)) {
qCWarning(dcNuki()) << "Invalid CRC CCITT value for public key message.";
// FIXME: check what to do if crc is invalid
@ -513,12 +488,8 @@ void NukiAuthenticator::onPairingDataCharacteristicChanged(const QByteArray &val
if (m_debug) qCDebug(dcNuki()) << "Authenticator: --> Nuki public key:" << NukiUtils::convertByteArrayToHexStringCompact(m_publicKeyNuki);
setState(AuthenticationStateGenerateKeyPair);
}
break;
case NukiUtils::CommandChallenge:
m_currentReceivingCurrentCount++;
m_currentReceivingData.append(value);
if (m_currentReceivingCurrentCount == m_currentReceivingExpectedCount) {
qCDebug(dcNuki()) << "Authenticator: Nuki challenge message received" << (m_debug ? NukiUtils::convertByteArrayToHexStringCompact(m_currentReceivingData) : "");
if (!NukiUtils::validateMessageCrc(m_currentReceivingData)) {
qCWarning(dcNuki()) << "Invalid CRC CCITT value for challenge message.";
@ -536,12 +507,8 @@ void NukiAuthenticator::onPairingDataCharacteristicChanged(const QByteArray &val
qCWarning(dcNuki()) << "Received a challenge without expecting one.";
setState(AuthenticationStateError);
}
}
break;
case NukiUtils::CommandAuthorizationId:
m_currentReceivingCurrentCount++;
m_currentReceivingData.append(value);
if (m_currentReceivingCurrentCount == m_currentReceivingExpectedCount) {
case NukiUtils::CommandAuthorizationId: {
qCDebug(dcNuki()) << "Authenticator: Nuki authorization ID message received" << (m_debug ? NukiUtils::convertByteArrayToHexStringCompact(m_currentReceivingData) : "");
if (!NukiUtils::validateMessageCrc(m_currentReceivingData)) {
@ -561,7 +528,7 @@ void NukiAuthenticator::onPairingDataCharacteristicChanged(const QByteArray &val
stream >> m_authorizationId;
m_uuid = message.mid(36, 16);
Q_ASSERT_X(m_uuid.count() == 16, "data length", "UUIS has not the correct length.");
Q_ASSERT_X(m_uuid.count() == 16, "data length", "UUID has not the correct length.");
m_nonceNuki = message.mid(52, 32);
Q_ASSERT_X(m_nonceNuki.count() == 32, "data length", "Nuki nonce has not the correct length.");
@ -573,8 +540,8 @@ void NukiAuthenticator::onPairingDataCharacteristicChanged(const QByteArray &val
if (m_debug) qCDebug(dcNuki()) << " Nuki nonce :" << NukiUtils::convertByteArrayToHexStringCompact(m_nonceNuki);
setState(AuthenticationStateAuthorizationIdConfirm);
}
break;
}
case NukiUtils::CommandStatus: {
quint8 status;
stream >> status;
@ -604,7 +571,6 @@ void NukiAuthenticator::onPairingDataCharacteristicChanged(const QByteArray &val
}
default:
qCWarning(dcNuki()) << "Authenticator: Unhandled command identifier for parining charateristic" << NukiUtils::convertUint16ToHexString(command);
resetExpectedData();
break;
}
}

View File

@ -110,7 +110,6 @@ private:
// State machine
void setState(AuthenticationState state);
void resetExpectedData(NukiUtils::Command command = NukiUtils::CommandRequestData, int expectedCount = 1);
// Helper methods
bool createAuthenticator(const QByteArray content);

View File

@ -88,6 +88,23 @@ bool NukiController::readLockState()
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) {
@ -154,6 +171,15 @@ void NukiController::setState(NukiController::NukiControllerState state)
case NukiControllerStateReadingLockStates:
sendReadLockStateRequest();
break;
case NukiControllerStateReadingConfigurationRequestChallange:
sendRequestChallengeRequest();
break;
case NukiControllerStateReadingConfigurationExecute:
sendReadConfigurationRequest();
setState(NukiControllerStateReadingConfiguration);
break;
case NukiControllerStateReadingConfiguration:
break;
case NukiControllerStateLockActionRequestChallange:
sendRequestChallengeRequest();
break;
@ -235,6 +261,12 @@ void NukiController::processNukiStatesData(const QByteArray &data)
emit nukiStatesChanged();
}
void NukiController::processNukiConfigData(const QByteArray &data)
{
qCDebug(dcNuki()) << "Processing config data from nuki" << data;
}
void NukiController::processNukiErrorReport(const QByteArray &data)
{
qint8 errorCode;
@ -280,6 +312,20 @@ void NukiController::processUserDataNotification(const QByteArray nonce, quint32
if (command == NukiUtils::CommandNukiStates) {
processNukiStatesData(payload);
emit readNukiStatesFinished(true);
setState(NukiControllerStateReadingConfigurationRequestChallange);
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;
}
@ -424,6 +470,45 @@ void NukiController::sendReadLockStateRequest()
m_userDataCharacteristic->writeCharacteristic(message);
}
void NukiController::sendReadConfigurationRequest()
{
qCDebug(dcNuki()) << "Controller: Reading configurations";
// Create data for encryption
QByteArray payload;
QDataStream stream(&payload, QIODevice::WriteOnly);
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";
@ -508,17 +593,8 @@ void NukiController::onUserDataCharacteristicChanged(const QByteArray &value)
{
if (m_debug) qCDebug(dcNuki()) << "Controller: Data received: <--" << NukiUtils::convertByteArrayToHexStringCompact(value);
if (m_messageBufferCounter <= 0) {
// New data arrived
m_messageBuffer.append(value);
m_messageBufferCounter++;
} else {
// We are currently collecting
m_messageBuffer.append(value);
m_messageBufferCounter++;
m_messageBuffer = value;
// In the second buffer message is the complete message length
if (m_messageBufferCounter == 2) {
if (m_messageBuffer.count() < 30) {
qCWarning(dcNuki()) << "Controller: Cannot understand message. Rejecting.";
resetMessageBuffer();
@ -529,7 +605,6 @@ void NukiController::onUserDataCharacteristicChanged(const QByteArray &value)
// 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.count() - 30);
m_messageBufferNonce = m_messageBufferAData.left(24);
QByteArray messageInformation = m_messageBufferAData.right(6);
@ -541,14 +616,4 @@ void NukiController::onUserDataCharacteristicChanged(const QByteArray &value)
resetMessageBuffer();
}
} else {
// We already know the message length and are still collecting p data
m_messageBufferPData.append(value);
if (m_messageBufferPData.count() == m_messageBufferLength) {
// Message finished
processUserDataNotification(m_messageBufferNonce, m_messageBufferIdentifier, m_messageBufferPData);
resetMessageBuffer();
}
}
}
}

View File

@ -49,6 +49,11 @@ public:
// Read state
NukiControllerStateReadingLockStates,
// Read configuration
NukiControllerStateReadingConfigurationRequestChallange,
NukiControllerStateReadingConfigurationExecute,
NukiControllerStateReadingConfiguration,
// Lock action
NukiControllerStateLockActionRequestChallange,
NukiControllerStateLockActionExecute,
@ -80,6 +85,7 @@ public:
// Actions
bool readLockState();
bool readConfiguration();
bool lock();
bool unlock();
bool unlatch();
@ -119,11 +125,13 @@ private:
// Data processors
void processNukiStatesData(const QByteArray &data);
void processNukiConfigData(const QByteArray &data);
void processNukiErrorReport(const QByteArray &data);
void processUserDataNotification(const QByteArray nonce, quint32 authorizationIdentifier, const QByteArray &privateData);
// State action methods
void sendReadLockStateRequest();
void sendReadConfigurationRequest();
void sendRequestChallengeRequest();
void sendLockActionRequest(NukiUtils::LockAction lockAction, quint8 flag = 0);

View File

@ -143,9 +143,9 @@ public:
CommandOpeningsClosingsSummary = 0x0010,
CommandBatteryReport = 0x0011,
CommandErrorReport = 0x0012,
CommandSetConG = 0x0013,
CommandRequestConG = 0x0014,
CommandConG = 0x0015,
CommandSetConfig = 0x0013,
CommandRequestConfig = 0x0014,
CommandConfig = 0x0015,
CommandSetSecurityPIN = 0x0019,
CommandRequestCalibration = 0x001A,
CommandRequestReboot = 0x001D,
@ -190,7 +190,7 @@ public:
// Message helper
static QByteArray createRequestMessageForUnencrypted(NukiUtils::Command command, const QByteArray &payload);
static QByteArray createRequestMessageForUnencryptedForEncryption(quint32 authenticationId, NukiUtils::Command command, const QByteArray &payload);
static QByteArray createRequestMessageForUnencryptedForEncryption(quint32 authenticationId, NukiUtils::Command command, const QByteArray &payload = QByteArray());
};