/* Copyright (C) 2020 Anders Löfgren This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ #include extern "C" { #include } #include "EspnowEncryptionBroker.h" #include "EspnowMeshBackend.h" #include "JsonTranslator.h" #include "UtilityFunctions.h" #include "Serializer.h" #include "MeshCryptoInterface.h" namespace { using EspnowProtocolInterpreter::encryptedConnectionKeyLength; using EspnowProtocolInterpreter::hashKeyLength; using EspnowProtocolInterpreter::maxEncryptedConnections; using connectionLogIterator = EspnowConnectionManager::connectionLogIterator; namespace TypeCast = MeshTypeConversionFunctions; String _ongoingPeerRequestNonce; EspnowMeshBackend *_ongoingPeerRequester = nullptr; EncryptedConnectionStatus _ongoingPeerRequestResult = EncryptedConnectionStatus::MAX_CONNECTIONS_REACHED_SELF; ExpiringTimeTracker _ongoingPeerRequestEncryptionTimeout([](){ return EspnowDatabase::getEncryptionRequestTimeout(); }); uint8_t _ongoingPeerRequestMac[6] = {0}; bool _reciprocalPeerRequestConfirmation = false; } EspnowEncryptionBroker::EspnowEncryptionBroker(ConditionalPrinter &conditionalPrinterInstance, EspnowDatabase &databaseInstance, EspnowConnectionManager &connectionManagerInstance, EspnowTransmitter &transmitterInstance) : _conditionalPrinter(conditionalPrinterInstance), _database(databaseInstance), _connectionManager(connectionManagerInstance), _transmitter(transmitterInstance) { } void EspnowEncryptionBroker::handlePeerRequest(const uint8_t *macaddr, uint8_t *dataArray, const uint8_t len, const uint64_t uint64StationMac, const uint64_t receivedMessageID) { // Pairing process ends when encryptedConnectionVerificationHeader is received, maxConnectionsReachedHeader is sent or timeout is reached. // Pairing process stages for request receiver: // Receive: encryptionRequestHeader or temporaryEncryptionRequestHeader. // Send: maxConnectionsReachedHeader / basicConnectionInfoHeader -> encryptedConnectionInfoHeader or softLimitEncryptedConnectionInfoHeader or maxConnectionsReachedHeader. // Receive: encryptedConnectionVerificationHeader. using namespace EspnowProtocolInterpreter; if(!EspnowDatabase::requestReceived(uint64StationMac, receivedMessageID)) { EspnowDatabase::storeReceivedRequest(uint64StationMac, receivedMessageID, TimeTracker(millis())); bool encryptedCorrectly = synchronizePeerSessionKey(receivedMessageID, macaddr); String message = getHashKeyLength(dataArray, len); int32_t messageHeaderEndIndex = message.indexOf(':'); String messageHeader = message.substring(0, messageHeaderEndIndex + 1); if(messageHeader == FPSTR(encryptedConnectionVerificationHeader)) { if(encryptedCorrectly) { int32_t connectionRequestTypeEndIndex = message.indexOf(':', messageHeaderEndIndex + 1); String connectionRequestType = message.substring(messageHeaderEndIndex + 1, connectionRequestTypeEndIndex + 1); connectionLogIterator encryptedConnection = EspnowConnectionManager::connectionLogEndIterator(); if(!EspnowConnectionManager::getEncryptedConnectionIterator(macaddr, encryptedConnection)) assert(false && String(F("We must have an encrypted connection if we received an encryptedConnectionVerificationHeader which was encryptedCorrectly."))); if(connectionRequestType == FPSTR(encryptionRequestHeader)) { EspnowConnectionManager::temporaryEncryptedConnectionToPermanent(macaddr); } else if(connectionRequestType == FPSTR(temporaryEncryptionRequestHeader)) { if(encryptedConnection->temporary()) // Should not change duration for existing permanent connections. { uint32_t connectionDuration = 0; if(JsonTranslator::getDuration(message, connectionDuration)) { encryptedConnection->setRemainingDuration(connectionDuration); } } } else { assert(false && String(F("Unknown P-type verification message received!"))); } } } else if(messageHeader == FPSTR(encryptionRequestHeader) || messageHeader == FPSTR(temporaryEncryptionRequestHeader)) { // If there is a espnowRequestManager, get it if(EspnowMeshBackend *currentEspnowRequestManager = EspnowMeshBackend::getEspnowRequestManager()) { String requestNonce; if(JsonTranslator::getNonce(message, requestNonce) && requestNonce.length() >= 12) // The destination MAC address requires 12 characters. { uint8_t destinationMac[6] = {0}; TypeCast::stringToMac(requestNonce, destinationMac); uint8_t apMac[6] {0}; WiFi.softAPmacAddress(apMac); bool correctDestination = false; if(MeshUtilityFunctions::macEqual(destinationMac, apMac)) { correctDestination = true; } else { uint8_t staMac[6] {0}; WiFi.macAddress(staMac); if(MeshUtilityFunctions::macEqual(destinationMac, staMac)) { correctDestination = true; } } uint8_t apMacArray[6] = { 0 }; if(correctDestination && verifyEncryptionRequestHmac(message, macaddr, getTransmissionMac(dataArray, apMacArray), currentEspnowRequestManager->getEspnowHashKey(), hashKeyLength)) EspnowDatabase::peerRequestConfirmationsToSend().emplace_back(receivedMessageID, encryptedCorrectly, currentEspnowRequestManager->getMeshPassword(), currentEspnowRequestManager->encryptedConnectionsSoftLimit(), requestNonce, macaddr, apMacArray, currentEspnowRequestManager->getEspnowHashKey()); } } } else if(messageHeader == FPSTR(encryptedConnectionRemovalRequestHeader)) { if(encryptedCorrectly) EspnowConnectionManager::removeEncryptedConnection(macaddr); } else { assert(false && String(F("Unknown P-type message received!"))); } } } void EspnowEncryptionBroker::handlePeerRequestConfirmation(uint8_t *macaddr, uint8_t *dataArray, const uint8_t len) { // Pairing process ends when _ongoingPeerRequestNonce == "" or timeout is reached. // Pairing process stages for request sender: // Send: encryptionRequestHeader or temporaryEncryptionRequestHeader. // Receive: maxConnectionsReachedHeader / basicConnectionInfoHeader -> encryptedConnectionInfoHeader or softLimitEncryptedConnectionInfoHeader or maxConnectionsReachedHeader. // Send: encryptedConnectionVerificationHeader. using namespace EspnowProtocolInterpreter; if(!_ongoingPeerRequestNonce.isEmpty()) { String message = getHashKeyLength(dataArray, len); String requestNonce; if(JsonTranslator::getNonce(message, requestNonce) && requestNonce == _ongoingPeerRequestNonce) { int32_t messageHeaderEndIndex = message.indexOf(':'); String messageHeader = message.substring(0, messageHeaderEndIndex + 1); String messageBody = message.substring(messageHeaderEndIndex + 1); uint8_t apMacArray[6] = { 0 }; getTransmissionMac(dataArray, apMacArray); if(messageHeader == FPSTR(basicConnectionInfoHeader)) { // encryptedConnectionEstablished(_ongoingPeerRequestResult) means we have already received a basicConnectionInfoHeader if(!encryptedConnectionEstablished(_ongoingPeerRequestResult) && verifyEncryptionRequestHmac(message, macaddr, apMacArray, _ongoingPeerRequester->getEspnowHashKey(), hashKeyLength)) { _ongoingPeerRequestEncryptionTimeout.reset(); connectionLogIterator existingEncryptedConnection = EspnowConnectionManager::connectionLogEndIterator(); if(!EspnowConnectionManager::getEncryptedConnectionIterator(macaddr, existingEncryptedConnection)) { // Although the newly created session keys are normally never used (they are replaced with synchronized ones later), the session keys must still be randomized to prevent attacks until replaced. _ongoingPeerRequestResult = _ongoingPeerRequester->addTemporaryEncryptedConnection(macaddr, apMacArray, createSessionKey(), createSessionKey(), EspnowDatabase::getEncryptionRequestTimeout()); } else { // Encrypted connection already exists _ongoingPeerRequestResult = EncryptedConnectionStatus::CONNECTION_ESTABLISHED; if(auto timeTrackerPointer = existingEncryptedConnection->temporary()) { if(timeTrackerPointer->remainingDuration() < EspnowDatabase::getEncryptionRequestTimeout()) // Should only extend duration for existing connections. { existingEncryptedConnection->setRemainingDuration(EspnowDatabase::getEncryptionRequestTimeout()); } } } if(!encryptedConnectionEstablished(_ongoingPeerRequestResult)) { // Adding connection failed, abort ongoing peer request. _ongoingPeerRequestNonce.clear(); } } } else if(messageHeader == FPSTR(encryptedConnectionInfoHeader) || messageHeader == FPSTR(softLimitEncryptedConnectionInfoHeader)) { String messagePassword; if(JsonTranslator::getPassword(messageBody, messagePassword) && messagePassword == _ongoingPeerRequester->getMeshPassword()) { // The mesh password is only shared via encrypted messages, so now we know this message is valid since it was encrypted and contained the correct nonce. EncryptedConnectionLog *encryptedConnection = EspnowConnectionManager::getEncryptedConnection(macaddr); uint64_t peerSessionKey = 0; uint64_t ownSessionKey = 0; if(encryptedConnection && JsonTranslator::getPeerSessionKey(messageBody, peerSessionKey) && JsonTranslator::getOwnSessionKey(messageBody, ownSessionKey)) { encryptedConnection->setPeerSessionKey(peerSessionKey); encryptedConnection->setOwnSessionKey(ownSessionKey); if(messageHeader == FPSTR(encryptedConnectionInfoHeader)) _ongoingPeerRequestResult = EncryptedConnectionStatus::CONNECTION_ESTABLISHED; else if(messageHeader == FPSTR(softLimitEncryptedConnectionInfoHeader)) _ongoingPeerRequestResult = EncryptedConnectionStatus::SOFT_LIMIT_CONNECTION_ESTABLISHED; else assert(false && String(F("Unknown _ongoingPeerRequestResult!"))); } else { _ongoingPeerRequestResult = EncryptedConnectionStatus::REQUEST_TRANSMISSION_FAILED; } _ongoingPeerRequestNonce.clear(); } } else if(messageHeader == FPSTR(maxConnectionsReachedHeader)) { if(verifyEncryptionRequestHmac(message, macaddr, apMacArray, _ongoingPeerRequester->getEspnowHashKey(), hashKeyLength)) { _ongoingPeerRequestResult = EncryptedConnectionStatus::MAX_CONNECTIONS_REACHED_PEER; _ongoingPeerRequestNonce.clear(); } } else { assert(messageHeader == FPSTR(basicConnectionInfoHeader) || messageHeader == FPSTR(encryptedConnectionInfoHeader) || messageHeader == FPSTR(softLimitEncryptedConnectionInfoHeader) || messageHeader == FPSTR(maxConnectionsReachedHeader)); } } } } void EspnowEncryptionBroker::sendPeerRequestConfirmations(const ExpiringTimeTracker *estimatedMaxDurationTracker) { uint32_t bufferedCriticalHeapLevel = EspnowDatabase::criticalHeapLevel() + EspnowDatabase::criticalHeapLevelBuffer(); // We preferably want to start clearing the logs a bit before things get critical. // _ongoingPeerRequestNonce can change during every delay(), but we need to remember the initial value to know from where sendPeerRequestConfirmations was called. String initialOngoingPeerRequestNonce = _ongoingPeerRequestNonce; for(std::list::iterator confirmationsIterator = EspnowDatabase::peerRequestConfirmationsToSend().begin(); confirmationsIterator != EspnowDatabase::peerRequestConfirmationsToSend().end(); ) { using namespace EspnowProtocolInterpreter; // True if confirmationsIterator contains a peer request received from the same node we are currently sending a peer request to. bool reciprocalPeerRequest = !initialOngoingPeerRequestNonce.isEmpty() && confirmationsIterator->connectedTo(_ongoingPeerRequestMac); auto timeTrackerPointer = confirmationsIterator->temporary(); assert(timeTrackerPointer); // peerRequestConfirmations should always expire and so should always have a timeTracker if(timeTrackerPointer->elapsedTime() > EspnowDatabase::getEncryptionRequestTimeout() || (reciprocalPeerRequest && confirmationsIterator->getPeerRequestNonce() <= initialOngoingPeerRequestNonce)) { // The peer request has expired, // or the peer request comes from the node we are currently making a peer request to ourselves and we are supposed to wait in this event to avoid simultaneous session key transfer. ++confirmationsIterator; continue; } uint8_t defaultBSSID[6] {0}; confirmationsIterator->getEncryptedPeerMac(defaultBSSID); uint8_t unencryptedBSSID[6] {0}; confirmationsIterator->getUnencryptedPeerMac(unencryptedBSSID); uint8_t hashKey[hashKeyLength] {0}; confirmationsIterator->getHashKey(hashKey); EncryptedConnectionLog *existingEncryptedConnection = EspnowConnectionManager::getEncryptedConnection(defaultBSSID); // If we receive a non-encrypted request for encrypted connection from a node that already exists as an encrypted peer for us we cannot send a response to the encrypted MAC // since that transmission will then be encrypted and impossible for the request sender to read. Of course, removing the existing encrypted connection would also work, // but make it very simple for a third party to disrupt an encrypted connection by just sending random requests for encrypted connection. bool sendToDefaultBSSID = confirmationsIterator->requestEncrypted() || !existingEncryptedConnection; // Note that callbacks can be called during delay time, so it is possible to receive a transmission during espnowSendToNode // (which may add an element to the peerRequestConfirmationsToSend list). if(!existingEncryptedConnection && ((reciprocalPeerRequest && EspnowConnectionManager::encryptedConnections().size() >= maxEncryptedConnections) || (!reciprocalPeerRequest && reservedEncryptedConnections() >= maxEncryptedConnections))) { EspnowTransmitter::espnowSendPeerRequestConfirmationsUnsynchronized(Serializer::createEncryptionRequestHmacMessage(FPSTR(maxConnectionsReachedHeader), confirmationsIterator->getPeerRequestNonce(), hashKey, hashKeyLength), defaultBSSID, 'C'); // Generates a new message ID to avoid sending encrypted sessionKeys over unencrypted connections. confirmationsIterator = EspnowDatabase::peerRequestConfirmationsToSend().erase(confirmationsIterator); } else if(EspnowTransmitter::espnowSendPeerRequestConfirmationsUnsynchronized(Serializer::createEncryptionRequestHmacMessage(FPSTR(basicConnectionInfoHeader), confirmationsIterator->getPeerRequestNonce(), hashKey, hashKeyLength), sendToDefaultBSSID ? defaultBSSID : unencryptedBSSID, 'C') // Generates a new message ID to avoid sending encrypted sessionKeys over unencrypted connections. == TransmissionStatusType::TRANSMISSION_COMPLETE) { // Try to add encrypted connection. If connection added send confirmation with encryptedConnection->getOwnSessionKey() as session key and C type message (won't increment key). Then proceed with next request (no need to wait for answer). if(existingEncryptedConnection) { if(auto timeTrackerPointer = existingEncryptedConnection->temporary()) { if(EspnowDatabase::getEncryptionRequestTimeout() > timeTrackerPointer->remainingDuration()) { existingEncryptedConnection->setRemainingDuration(EspnowDatabase::getEncryptionRequestTimeout()); } } } else if(EspnowMeshBackend *currentEspnowRequestManager = EspnowMeshBackend::getEspnowRequestManager()) { uint8_t staMacArray[6] = { 0 }; uint8_t apMacArray[6] = { 0 }; currentEspnowRequestManager->addTemporaryEncryptedConnection(confirmationsIterator->getPeerStaMac(staMacArray), confirmationsIterator->getPeerApMac(apMacArray), createSessionKey(), createSessionKey(), EspnowDatabase::getEncryptionRequestTimeout()); existingEncryptedConnection = EspnowConnectionManager::getEncryptedConnection(defaultBSSID); } else { ConditionalPrinter::warningPrint(String(F("WARNING! Ignoring received encrypted connection request since no EspnowRequestManager is assigned."))); } if(!existingEncryptedConnection) { // Send "node full" message EspnowTransmitter::espnowSendPeerRequestConfirmationsUnsynchronized(Serializer::createEncryptionRequestHmacMessage(FPSTR(maxConnectionsReachedHeader), confirmationsIterator->getPeerRequestNonce(), hashKey, hashKeyLength), defaultBSSID, 'C'); // Generates a new message ID to avoid sending encrypted sessionKeys over unencrypted connections. } else { if(reciprocalPeerRequest) _reciprocalPeerRequestConfirmation = true; delay(5); // Give some time for the peer to add an encrypted connection assert(esp_now_is_peer_exist(defaultBSSID) > 0 && String(F("ERROR! Attempting to send content marked as encrypted via unencrypted connection!"))); String messageHeader; if(existingEncryptedConnection->temporary() && // Should never change permanent connections ((reciprocalPeerRequest && EspnowConnectionManager::encryptedConnections().size() > confirmationsIterator->getEncryptedConnectionsSoftLimit()) || (!reciprocalPeerRequest && reservedEncryptedConnections() > confirmationsIterator->getEncryptedConnectionsSoftLimit()))) { messageHeader = FPSTR(softLimitEncryptedConnectionInfoHeader); } else { messageHeader = FPSTR(encryptedConnectionInfoHeader); } // Send password and keys. // Probably no need to know which connection type to use, that is stored in request node and will be sent over for finalization. EspnowTransmitter::espnowSendPeerRequestConfirmationsUnsynchronized(Serializer::createEncryptedConnectionInfo(messageHeader, confirmationsIterator->getPeerRequestNonce(), confirmationsIterator->getAuthenticationPassword(), existingEncryptedConnection->getOwnSessionKey(), existingEncryptedConnection->getPeerSessionKey()), defaultBSSID, 'C'); // Generates a new message ID to avoid sending encrypted sessionKeys over unencrypted connections. } confirmationsIterator = EspnowDatabase::peerRequestConfirmationsToSend().erase(confirmationsIterator); } else { ++confirmationsIterator; } if(ESP.getFreeHeap() <= bufferedCriticalHeapLevel) { // Heap is getting very low, which probably means we are receiving a lot of transmissions while trying to transmit responses. // Clear all old data to try to avoid running out of memory. ConditionalPrinter::warningPrint("WARNING! Free heap below chosen minimum. Performing emergency log clearing."); EspnowDatabase::clearOldLogEntries(true); return; // confirmationsIterator may be invalid now. Also, we should give the main loop a chance to respond to the situation. } if(estimatedMaxDurationTracker && estimatedMaxDurationTracker->expired()) return; } } EncryptedConnectionStatus EspnowEncryptionBroker::requestEncryptedConnection(const uint8_t *peerMac, EspnowMeshBackend &espnowInstance) { using namespace std::placeholders; return requestEncryptedConnectionKernel(peerMac, std::bind(defaultEncryptionRequestBuilder, FPSTR(EspnowProtocolInterpreter::encryptionRequestHeader), 0, _connectionManager.getEspnowHashKey(), _1, _2), espnowInstance); } EncryptedConnectionStatus EspnowEncryptionBroker::requestTemporaryEncryptedConnection(const uint8_t *peerMac, const uint32_t durationMs, EspnowMeshBackend &espnowInstance) { using namespace std::placeholders; return requestEncryptedConnectionKernel(peerMac, std::bind(defaultEncryptionRequestBuilder, FPSTR(EspnowProtocolInterpreter::temporaryEncryptionRequestHeader), durationMs, _connectionManager.getEspnowHashKey(), _1, _2), espnowInstance); } EncryptedConnectionStatus EspnowEncryptionBroker::requestFlexibleTemporaryEncryptedConnection(const uint8_t *peerMac, const uint32_t minDurationMs, EspnowMeshBackend &espnowInstance) { using namespace std::placeholders; return requestEncryptedConnectionKernel(peerMac, std::bind(flexibleEncryptionRequestBuilder, minDurationMs, _connectionManager.getEspnowHashKey(), _1, _2), espnowInstance); } EncryptedConnectionRemovalOutcome EspnowEncryptionBroker::requestEncryptedConnectionRemoval(const uint8_t *peerMac) { using EspnowProtocolInterpreter::encryptedConnectionRemovalRequestHeader; assert(EspnowConnectionManager::encryptedConnections().size() <= maxEncryptedConnections); // If this is not the case, ESP-NOW is no longer in sync with the library MutexTracker mutexTracker(EspnowTransmitter::captureEspnowTransmissionMutex(EspnowConnectionManager::handlePostponedRemovals)); if(!mutexTracker.mutexCaptured()) { assert(false && String(F("ERROR! Transmission in progress. Don't call requestEncryptedConnectionRemoval from callbacks as this may corrupt program state! Aborting."))); return EncryptedConnectionRemovalOutcome::REMOVAL_REQUEST_FAILED; } if(EncryptedConnectionLog *encryptedConnection = EspnowConnectionManager::getEncryptedConnection(peerMac)) { if(EspnowTransmitter::espnowSendToNode(FPSTR(encryptedConnectionRemovalRequestHeader), peerMac, 'P') == TransmissionStatusType::TRANSMISSION_COMPLETE) { return EspnowConnectionManager::removeEncryptedConnectionUnprotected(peerMac); } else { if(encryptedConnection->removalScheduled()) return EncryptedConnectionRemovalOutcome::REMOVAL_SUCCEEDED; // Removal will be completed by mutex destructorHook. else return EncryptedConnectionRemovalOutcome::REMOVAL_REQUEST_FAILED; } } // peerMac is already removed return EncryptedConnectionRemovalOutcome::REMOVAL_SUCCEEDED; } bool EspnowEncryptionBroker::encryptedConnectionEstablished(const EncryptedConnectionStatus connectionStatus) { return static_cast(connectionStatus) > 0; } void EspnowEncryptionBroker::setReceivedEncryptedTransmission(const bool receivedEncryptedTransmission) { _receivedEncryptedTransmission = receivedEncryptedTransmission; } bool EspnowEncryptionBroker::receivedEncryptedTransmission() const {return _receivedEncryptedTransmission;} bool EspnowEncryptionBroker::verifyEncryptionRequestHmac(const String &encryptionRequestHmacMessage, const uint8_t *requesterStaMac, const uint8_t *requesterApMac, const uint8_t *hashKey, const uint8_t hashKeyLength) { using MeshCryptoInterface::verifyMeshHmac; using namespace JsonTranslator; String hmac; if(getHmac(encryptionRequestHmacMessage, hmac)) { int32_t hmacStartIndex = encryptionRequestHmacMessage.indexOf(String('"') + FPSTR(jsonHmac) + F("\":")); if(hmacStartIndex < 0) return false; if(hmac.length() == 2*experimental::crypto::SHA256::NATURAL_LENGTH // We know that each HMAC byte should become 2 String characters due to uint8ArrayToHexString. && verifyMeshHmac(TypeCast::macToString(requesterStaMac) + TypeCast::macToString(requesterApMac) + encryptionRequestHmacMessage.substring(0, hmacStartIndex), hmac, hashKey, hashKeyLength)) { return true; } } return false; } bool EspnowEncryptionBroker::verifyPeerSessionKey(const uint64_t sessionKey, const uint8_t *peerMac, const char messageType) { if(EncryptedConnectionLog *encryptedConnection = EspnowConnectionManager::getEncryptedConnection(peerMac)) { return verifyPeerSessionKey(sessionKey, *encryptedConnection, TypeCast::macToUint64(peerMac), messageType); } return false; } bool EspnowEncryptionBroker::verifyPeerSessionKey(const uint64_t sessionKey, const EncryptedConnectionLog &encryptedConnection, const uint64_t uint64PeerMac, const char messageType) { using namespace EspnowProtocolInterpreter; if(usesEncryption(sessionKey)) { if(sessionKey == encryptedConnection.getPeerSessionKey() || EspnowDatabase::receivedEspnowTransmissions().find(std::make_pair(createMacAndTypeValue(uint64PeerMac, messageType), sessionKey)) != EspnowDatabase::receivedEspnowTransmissions().end()) { // If sessionKey is correct or sessionKey is one part of a multi-part transmission. return true; } } return false; } bool EspnowEncryptionBroker::synchronizePeerSessionKey(const uint64_t sessionKey, const uint8_t *peerMac) { if(EncryptedConnectionLog *encryptedConnection = EspnowConnectionManager::getEncryptedConnection(peerMac)) { return synchronizePeerSessionKey(sessionKey, *encryptedConnection); } return false; } bool EspnowEncryptionBroker::synchronizePeerSessionKey(const uint64_t sessionKey, EncryptedConnectionLog &encryptedConnection) { if(EspnowProtocolInterpreter::usesEncryption(sessionKey)) { if(sessionKey == encryptedConnection.getPeerSessionKey()) { uint8_t hashKey[hashKeyLength] {0}; encryptedConnection.setPeerSessionKey(EncryptedConnectionLog::incrementSessionKey(sessionKey, encryptedConnection.getHashKey(hashKey), hashKeyLength)); return true; } } return false; } uint8_t EspnowEncryptionBroker::reservedEncryptedConnections() { if(!_ongoingPeerRequestNonce.isEmpty()) if(!EspnowConnectionManager::getEncryptedConnection(_ongoingPeerRequestMac)) return EspnowConnectionManager::encryptedConnections().size() + 1; // Reserve one connection spot if we are currently making a peer request to a new node. return EspnowConnectionManager::encryptedConnections().size(); } EncryptedConnectionStatus EspnowEncryptionBroker::requestEncryptedConnectionKernel(const uint8_t *peerMac, const encryptionRequestBuilderType &encryptionRequestBuilder, EspnowMeshBackend &espnowInstance) { using namespace EspnowProtocolInterpreter; assert(EspnowConnectionManager::encryptedConnections().size() <= maxEncryptedConnections); // If this is not the case, ESP-NOW is no longer in sync with the library MutexTracker mutexTracker(EspnowTransmitter::captureEspnowTransmissionMutex(EspnowConnectionManager::handlePostponedRemovals)); if(!mutexTracker.mutexCaptured()) { assert(false && String(F("ERROR! Transmission in progress. Don't call requestEncryptedConnection from callbacks as this may corrupt program state! Aborting."))); return EncryptedConnectionStatus::REQUEST_TRANSMISSION_FAILED; } EncryptedConnectionLog *existingEncryptedConnection = EspnowConnectionManager::getEncryptedConnection(peerMac); ExpiringTimeTracker existingTimeTracker = existingEncryptedConnection && existingEncryptedConnection->temporary() ? *existingEncryptedConnection->temporary() : ExpiringTimeTracker(0); if(!existingEncryptedConnection && EspnowConnectionManager::encryptedConnections().size() >= maxEncryptedConnections) { assert(EspnowConnectionManager::encryptedConnections().size() == maxEncryptedConnections); // No capacity for more encrypted connections. return EncryptedConnectionStatus::MAX_CONNECTIONS_REACHED_SELF; } String requestNonce = TypeCast::macToString(peerMac) + TypeCast::uint64ToString(MeshUtilityFunctions::randomUint64()) + TypeCast::uint64ToString(MeshUtilityFunctions::randomUint64()); _ongoingPeerRequestResult = EncryptedConnectionStatus::REQUEST_TRANSMISSION_FAILED; _ongoingPeerRequestNonce = requestNonce; _ongoingPeerRequester = &espnowInstance; _reciprocalPeerRequestConfirmation = false; std::copy_n(peerMac, 6, _ongoingPeerRequestMac); String requestMessage = encryptionRequestBuilder(requestNonce, existingTimeTracker); _conditionalPrinter.verboseModePrint(String(F("Sending encrypted connection request to: ")) + TypeCast::macToString(peerMac)); if(EspnowTransmitter::espnowSendToNode(requestMessage, peerMac, 'P') == TransmissionStatusType::TRANSMISSION_COMPLETE) { ExpiringTimeTracker requestTimeout([](){ return EspnowDatabase::getEncryptionRequestTimeout(); }); // _ongoingPeerRequestNonce is set to "" when a peer confirmation response from the mac is received while(!requestTimeout && !_ongoingPeerRequestNonce.isEmpty()) { // For obvious reasons dividing by exactly 10 is a good choice. ExpiringTimeTracker maxDurationTracker = ExpiringTimeTracker(EspnowDatabase::getEncryptionRequestTimeout()/10); sendPeerRequestConfirmations(&maxDurationTracker); // Must be called before delay() to ensure !_ongoingPeerRequestNonce.isEmpty() is still true, so reciprocal peer request order is preserved. delay(1); } } if(!_ongoingPeerRequestNonce.isEmpty()) { // If nonce != "" we only received the basic connection info, so the pairing process is incomplete _ongoingPeerRequestResult = EncryptedConnectionStatus::REQUEST_TRANSMISSION_FAILED; _ongoingPeerRequestNonce.clear(); } else if(encryptedConnectionEstablished(_ongoingPeerRequestResult)) { if(_ongoingPeerRequestResult == EncryptedConnectionStatus::CONNECTION_ESTABLISHED) // Give the builder a chance to update the message requestMessage = encryptionRequestBuilder(requestNonce, existingTimeTracker); else if(_ongoingPeerRequestResult == EncryptedConnectionStatus::SOFT_LIMIT_CONNECTION_ESTABLISHED) // We will only get a soft limit connection. Adjust future actions based on this. requestMessage = Serializer::createEncryptionRequestHmacMessage(FPSTR(temporaryEncryptionRequestHeader), requestNonce, _connectionManager.getEspnowHashKey(), hashKeyLength, _database.getAutoEncryptionDuration()); else assert(false && String(F("Unknown _ongoingPeerRequestResult during encrypted connection finalization!"))); int32_t messageHeaderEndIndex = requestMessage.indexOf(':'); String messageHeader = requestMessage.substring(0, messageHeaderEndIndex + 1); String messageBody = requestMessage.substring(messageHeaderEndIndex + 1); // If we do not get an ack within getEncryptionRequestTimeout() the peer has probably had the time to delete the temporary encrypted connection. if(EspnowTransmitter::espnowSendToNode(String(FPSTR(encryptedConnectionVerificationHeader)) + requestMessage, peerMac, 'P') == TransmissionStatusType::TRANSMISSION_COMPLETE && !_ongoingPeerRequestEncryptionTimeout) { EncryptedConnectionLog *encryptedConnection = EspnowConnectionManager::getEncryptedConnection(peerMac); if(!encryptedConnection) { assert(encryptedConnection && String(F("requestEncryptedConnectionKernel cannot find an encrypted connection!"))); // requestEncryptedConnectionRemoval received. _ongoingPeerRequestResult = EncryptedConnectionStatus::REQUEST_TRANSMISSION_FAILED; } else if(encryptedConnection->removalScheduled() || (encryptedConnection->temporary() && encryptedConnection->temporary()->expired())) { // Could possibly be caused by a simultaneous temporary peer request from the peer. _ongoingPeerRequestResult = EncryptedConnectionStatus::REQUEST_TRANSMISSION_FAILED; } else { // Finalize connection if(messageHeader == FPSTR(encryptionRequestHeader)) { EspnowConnectionManager::temporaryEncryptedConnectionToPermanent(peerMac); } else if(messageHeader == FPSTR(temporaryEncryptionRequestHeader)) { if(encryptedConnection->temporary()) { // Should not change duration of existing permanent connections. uint32_t connectionDuration = 0; bool durationFound = JsonTranslator::getDuration(messageBody, connectionDuration); assert(durationFound); encryptedConnection->setRemainingDuration(connectionDuration); } } else { assert(false && String(F("Unknown messageHeader during encrypted connection finalization!"))); _ongoingPeerRequestResult = EncryptedConnectionStatus::API_CALL_FAILED; } } } else { _ongoingPeerRequestResult = EncryptedConnectionStatus::REQUEST_TRANSMISSION_FAILED; } } if(!encryptedConnectionEstablished(_ongoingPeerRequestResult)) { if(!existingEncryptedConnection && !_reciprocalPeerRequestConfirmation) { // Remove any connection that was added during the request attempt and is no longer in use. EspnowConnectionManager::removeEncryptedConnectionUnprotected(peerMac); } } _ongoingPeerRequester = nullptr; return _ongoingPeerRequestResult; } EncryptedConnectionStatus EspnowEncryptionBroker::initiateAutoEncryptingConnection(const EspnowNetworkInfo &recipientInfo, const bool requestPermanentConnection, uint8_t *targetBSSID, EncryptedConnectionLog **existingEncryptedConnection, EspnowMeshBackend &espnowInstance) { assert(recipientInfo.BSSID() != nullptr); // We need at least the BSSID to connect recipientInfo.getBSSID(targetBSSID); if(_conditionalPrinter.verboseMode()) // Avoid string generation if not required { espnowInstance.printAPInfo(recipientInfo); _conditionalPrinter.verboseModePrint(emptyString); } *existingEncryptedConnection = EspnowConnectionManager::getEncryptedConnection(targetBSSID); EncryptedConnectionStatus connectionStatus = EncryptedConnectionStatus::MAX_CONNECTIONS_REACHED_SELF; if(requestPermanentConnection) connectionStatus = requestEncryptedConnection(targetBSSID, espnowInstance); else connectionStatus = requestFlexibleTemporaryEncryptedConnection(targetBSSID, _database.getAutoEncryptionDuration(), espnowInstance); return connectionStatus; } void EspnowEncryptionBroker::finalizeAutoEncryptingConnection(const uint8_t *targetBSSID, const EncryptedConnectionLog *existingEncryptedConnection, const bool requestPermanentConnection) { if(!existingEncryptedConnection && !requestPermanentConnection && !_reciprocalPeerRequestConfirmation) { // Remove any connection that was added during the transmission attempt and is no longer in use. EspnowConnectionManager::removeEncryptedConnectionUnprotected(targetBSSID); } } String EspnowEncryptionBroker::defaultEncryptionRequestBuilder(const String &requestHeader, const uint32_t durationMs, const uint8_t *hashKey, const String &requestNonce, const ExpiringTimeTracker &existingTimeTracker) { (void)existingTimeTracker; // This removes a "unused parameter" compiler warning. Does nothing else. return Serializer::createEncryptionRequestHmacMessage(requestHeader, requestNonce, hashKey, hashKeyLength, durationMs); } String EspnowEncryptionBroker::flexibleEncryptionRequestBuilder(const uint32_t minDurationMs, const uint8_t *hashKey, const String &requestNonce, const ExpiringTimeTracker &existingTimeTracker) { using namespace JsonTranslator; using EspnowProtocolInterpreter::temporaryEncryptionRequestHeader; uint32_t connectionDuration = minDurationMs >= existingTimeTracker.remainingDuration() ? minDurationMs : existingTimeTracker.remainingDuration(); return Serializer::createEncryptionRequestHmacMessage(FPSTR(temporaryEncryptionRequestHeader), requestNonce, hashKey, hashKeyLength, connectionDuration); }