1
0
mirror of https://github.com/esp8266/Arduino.git synced 2025-04-19 23:22:16 +03:00
esp8266/libraries/ESP8266WiFiMesh/src/EspnowEncryptionBroker.cpp
Anders f059e57322 - Use the new Crypto, TypeConversion and random() functionality added to the Arduino core, instead of the versions local to the mesh library.
- Rearrange class variables to minimize storage padding.

- Add protected getters for EspnowMeshBackend and MeshBackendBase components.

- Partially update README.md
2020-05-18 22:09:34 +02:00

722 lines
37 KiB
C++

/*
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 <ESP8266WiFi.h>
extern "C" {
#include <espnow.h>
}
#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<PeerRequestLog>::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<int>(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);
}