1
0
mirror of https://github.com/esp8266/Arduino.git synced 2025-04-19 23:22:16 +03:00

- Move all serialization code to separate Serializer namespace and files.

- Generalize and improve JSON processing code.

- Prevent mesh passwords from containing " characters to avoid messing up the JSON processing.

- Improve documentation.
This commit is contained in:
Anders 2020-05-10 16:42:57 +02:00
parent e64125a53c
commit 2ec2679d6e
14 changed files with 464 additions and 232 deletions

View File

@ -19,7 +19,7 @@ namespace TypeCast = MeshTypeConversionFunctions;
https://arduino-esp8266.readthedocs.io/en/latest/PROGMEM.html
*/
constexpr char exampleMeshName[] PROGMEM = "MeshNode_"; // The name of the mesh network. Used as prefix for the node SSID and to find other network nodes in the example networkFilter and broadcastFilter functions below.
constexpr char exampleWiFiPassword[] PROGMEM = "ChangeThisWiFiPassword_TODO"; // The password has to be min 8 and max 64 characters long, otherwise an AP which uses it will not be found during scans.
constexpr char exampleWiFiPassword[] PROGMEM = "ChangeThisWiFiPassword_TODO"; // Note: " is an illegal character. The password has to be min 8 and max 64 characters long, otherwise an AP which uses it will not be found during scans.
// A custom encryption key is required when using encrypted ESP-NOW transmissions. There is always a default Kok set, but it can be replaced if desired.
// All ESP-NOW keys below must match in an encrypted connection pair for encrypted communication to be possible.

View File

@ -26,7 +26,7 @@ namespace TypeCast = MeshTypeConversionFunctions;
https://arduino-esp8266.readthedocs.io/en/latest/PROGMEM.html
*/
constexpr char exampleMeshName[] PROGMEM = "MeshNode_"; // The name of the mesh network. Used as prefix for the node SSID and to find other network nodes in the example networkFilter and broadcastFilter functions below.
constexpr char exampleWiFiPassword[] PROGMEM = "ChangeThisWiFiPassword_TODO"; // The password has to be min 8 and max 64 characters long, otherwise an AP which uses it will not be found during scans.
constexpr char exampleWiFiPassword[] PROGMEM = "ChangeThisWiFiPassword_TODO"; // Note: " is an illegal character. The password has to be min 8 and max 64 characters long, otherwise an AP which uses it will not be found during scans.
// A custom encryption key is required when using encrypted ESP-NOW transmissions. There is always a default Kok set, but it can be replaced if desired.
// All ESP-NOW keys below must match in an encrypted connection pair for encrypted communication to be possible.

View File

@ -19,7 +19,7 @@ namespace TypeCast = MeshTypeConversionFunctions;
https://arduino-esp8266.readthedocs.io/en/latest/PROGMEM.html
*/
constexpr char exampleMeshName[] PROGMEM = "MeshNode_";
constexpr char exampleWiFiPassword[] PROGMEM = "ChangeThisWiFiPassword_TODO";
constexpr char exampleWiFiPassword[] PROGMEM = "ChangeThisWiFiPassword_TODO"; // Note: " is an illegal character. The password has to be min 8 and max 64 characters long, otherwise an AP which uses it will not be found during scans.
unsigned int requestNumber = 0;
unsigned int responseNumber = 0;

View File

@ -27,6 +27,7 @@
#include "TypeConversionFunctions.h"
#include "JsonTranslator.h"
#include "MeshCryptoInterface.h"
#include "Serializer.h"
namespace
{
@ -157,16 +158,8 @@ bool EncryptedConnectionData::desync() const { return _desync; }
String EncryptedConnectionData::serialize() const
{
// Returns: {"connectionState":{"duration":"123","password":"abc","ownSK":"1A2","peerSK":"3B4","peerStaMac":"F2","peerApMac":"E3"}}
return
String(FPSTR(JsonTranslator::jsonConnectionState))
+ (temporary() ? String(FPSTR(JsonTranslator::jsonDuration)) + '\"' + String(temporary()->remainingDuration()) + F("\",") : emptyString)
+ FPSTR(JsonTranslator::jsonDesync) + '\"' + String(desync()) + F("\",")
+ FPSTR(JsonTranslator::jsonOwnSessionKey) + '\"' + TypeCast::uint64ToString(getOwnSessionKey()) + F("\",")
+ FPSTR(JsonTranslator::jsonPeerSessionKey) + '\"' + TypeCast::uint64ToString(getPeerSessionKey()) + F("\",")
+ FPSTR(JsonTranslator::jsonPeerStaMac) + '\"' + TypeCast::macToString(_peerStaMac) + F("\",")
+ FPSTR(JsonTranslator::jsonPeerApMac) + '\"' + TypeCast::macToString(_peerApMac) + F("\"}}");
return Serializer:: serializeEncryptedConnection((temporary() ? String(temporary()->remainingDuration()) : emptyString), String(desync()), TypeCast::uint64ToString(getOwnSessionKey()),
TypeCast::uint64ToString(getPeerSessionKey()), TypeCast::macToString(_peerStaMac), TypeCast::macToString(_peerApMac));
}
const ExpiringTimeTracker *EncryptedConnectionData::temporary() const

View File

@ -27,6 +27,8 @@ extern "C" {
#include "MutexTracker.h"
#include "JsonTranslator.h"
#include "MeshCryptoInterface.h"
#include "EspnowUtility.h"
#include "Serializer.h"
namespace
{
@ -1112,7 +1114,7 @@ TransmissionStatusType EspnowMeshBackend::espnowSendToNode(const String &message
TransmissionStatusType EspnowMeshBackend::espnowSendToNodeUnsynchronized(const String message, const uint8_t *targetBSSID, const char messageType, const uint64_t messageID, EspnowMeshBackend *espnowInstance)
{
using namespace EspnowProtocolInterpreter;
MutexTracker mutexTracker(_espnowSendToNodeMutex);
if(!mutexTracker.mutexCaptured())
{
@ -1773,7 +1775,7 @@ EncryptedConnectionStatus EspnowMeshBackend::requestEncryptedConnectionKernel(co
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 = JsonTranslator::createEncryptionRequestHmacMessage(FPSTR(temporaryEncryptionRequestHeader), requestNonce, getEspnowHashKey(),
requestMessage = Serializer::createEncryptionRequestHmacMessage(FPSTR(temporaryEncryptionRequestHeader), requestNonce, getEspnowHashKey(),
hashKeyLength, getAutoEncryptionDuration());
else
assert(false && String(F("Unknown _ongoingPeerRequestResult during encrypted connection finalization!")));
@ -1848,7 +1850,7 @@ String EspnowMeshBackend::defaultEncryptionRequestBuilder(const String &requestH
{
(void)existingTimeTracker; // This removes a "unused parameter" compiler warning. Does nothing else.
return JsonTranslator::createEncryptionRequestHmacMessage(requestHeader, requestNonce, hashKey, hashKeyLength, durationMs);
return Serializer::createEncryptionRequestHmacMessage(requestHeader, requestNonce, hashKey, hashKeyLength, durationMs);
}
String EspnowMeshBackend::flexibleEncryptionRequestBuilder(const uint32_t minDurationMs, const uint8_t *hashKey,
@ -1860,7 +1862,7 @@ String EspnowMeshBackend::flexibleEncryptionRequestBuilder(const uint32_t minDur
uint32_t connectionDuration = minDurationMs >= existingTimeTracker.remainingDuration() ?
minDurationMs : existingTimeTracker.remainingDuration();
return createEncryptionRequestHmacMessage(FPSTR(temporaryEncryptionRequestHeader), requestNonce, hashKey, hashKeyLength, connectionDuration);
return Serializer::createEncryptionRequestHmacMessage(FPSTR(temporaryEncryptionRequestHeader), requestNonce, hashKey, hashKeyLength, connectionDuration);
}
EncryptedConnectionStatus EspnowMeshBackend::requestEncryptedConnection(const uint8_t *peerMac)
@ -2437,13 +2439,13 @@ void EspnowMeshBackend::sendPeerRequestConfirmations(const ExpiringTimeTracker *
if(!existingEncryptedConnection &&
((reciprocalPeerRequest && encryptedConnections.size() >= maxEncryptedConnections) || (!reciprocalPeerRequest && reservedEncryptedConnections() >= maxEncryptedConnections)))
{
espnowSendToNodeUnsynchronized(JsonTranslator::createEncryptionRequestHmacMessage(FPSTR(maxConnectionsReachedHeader),
espnowSendToNodeUnsynchronized(Serializer::createEncryptionRequestHmacMessage(FPSTR(maxConnectionsReachedHeader),
confirmationsIterator->getPeerRequestNonce(), hashKey, hashKeyLength),
defaultBSSID, 'C', generateMessageID(nullptr)); // Generates a new message ID to avoid sending encrypted sessionKeys over unencrypted connections.
confirmationsIterator = peerRequestConfirmationsToSend.erase(confirmationsIterator);
}
else if(espnowSendToNodeUnsynchronized(JsonTranslator::createEncryptionRequestHmacMessage(FPSTR(basicConnectionInfoHeader),
else if(espnowSendToNodeUnsynchronized(Serializer::createEncryptionRequestHmacMessage(FPSTR(basicConnectionInfoHeader),
confirmationsIterator->getPeerRequestNonce(), hashKey, hashKeyLength),
sendToDefaultBSSID ? defaultBSSID : unencryptedBSSID, 'C', generateMessageID(nullptr)) // Generates a new message ID to avoid sending encrypted sessionKeys over unencrypted connections.
== TransmissionStatusType::TRANSMISSION_COMPLETE)
@ -2475,7 +2477,7 @@ void EspnowMeshBackend::sendPeerRequestConfirmations(const ExpiringTimeTracker *
if(!existingEncryptedConnection)
{
// Send "node full" message
espnowSendToNodeUnsynchronized(JsonTranslator::createEncryptionRequestHmacMessage(FPSTR(maxConnectionsReachedHeader),
espnowSendToNodeUnsynchronized(Serializer::createEncryptionRequestHmacMessage(FPSTR(maxConnectionsReachedHeader),
confirmationsIterator->getPeerRequestNonce(), hashKey, hashKeyLength),
defaultBSSID, 'C', generateMessageID(nullptr)); // Generates a new message ID to avoid sending encrypted sessionKeys over unencrypted connections.
}
@ -2503,7 +2505,7 @@ void EspnowMeshBackend::sendPeerRequestConfirmations(const ExpiringTimeTracker *
// 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.
espnowSendToNodeUnsynchronized(JsonTranslator::createEncryptedConnectionInfo(messageHeader,
espnowSendToNodeUnsynchronized(Serializer::createEncryptedConnectionInfo(messageHeader,
confirmationsIterator->getPeerRequestNonce(), confirmationsIterator->getAuthenticationPassword(),
existingEncryptedConnection->getOwnSessionKey(), existingEncryptedConnection->getPeerSessionKey()),
defaultBSSID, 'C', generateMessageID(nullptr)); // Generates a new message ID to avoid sending encrypted sessionKeys over unencrypted connections.
@ -2678,12 +2680,8 @@ void EspnowMeshBackend::resetTransmissionFailRate()
}
String EspnowMeshBackend::serializeUnencryptedConnection()
{
using namespace JsonTranslator;
// Returns: {"connectionState":{"unsyncMsgID":"123"}}
return String(FPSTR(jsonConnectionState)) + createJsonEndPair(FPSTR(jsonUnsynchronizedMessageID), String(_unsynchronizedMessageID));
{
return Serializer::serializeUnencryptedConnection(String(_unsynchronizedMessageID));
}
String EspnowMeshBackend::serializeEncryptedConnection(const uint8_t *peerMac)

View File

@ -657,11 +657,11 @@ public:
* @param intervalMs The time to wait for an ack after having made an ESP-NOW transmission, in milliseconds. Defaults to 15 ms.
*/
static void setEspnowRetransmissionInterval(const uint32_t intervalMs);
static uint32_t getEspnowRetransmissionInterval();
static uint32_t getEspnowRetransmissionInterval();
// The maximum amount of time each of the two stages in an encrypted connection request may take.
static void setEncryptionRequestTimeout(const uint32_t timeoutMs);
static uint32_t getEncryptionRequestTimeout();
static uint32_t getEncryptionRequestTimeout();
void setAutoEncryptionDuration(const uint32_t duration);
uint32_t getAutoEncryptionDuration() const;
@ -1217,7 +1217,7 @@ private:
* @param peerMac The MAC of the node with which an encrypted connection should be established.
* @param encryptionRequestBuilder A function which is responsible for constructing the request message to send.
* Called twice when the request is successful. First to build the initial request message and then to build the connection verification message.
* The request message should typically be of the form: JsonTranslator::createEncryptionRequestIntro() + JsonTranslator::createEncryptionRequestEnding().
* The request message should typically be of the form found in Serializer::createEncryptionRequestHmacMessage.
* @return The ultimate status of the requested encrypted connection, as EncryptedConnectionStatus.
*/
EncryptedConnectionStatus requestEncryptedConnectionKernel(const uint8_t *peerMac, const encryptionRequestBuilderType &encryptionRequestBuilder);

View File

@ -25,6 +25,7 @@
#include "FloodingMesh.h"
#include "TypeConversionFunctions.h"
#include "JsonTranslator.h"
#include "Serializer.h"
namespace
{
@ -153,16 +154,11 @@ void FloodingMesh::performMeshInstanceMaintenance()
String FloodingMesh::serializeMeshState() const
{
using namespace JsonTranslator;
// Returns: {"meshState":{"connectionState":{"unsyncMsgID":"123"},"meshMsgCount":"123"}}
String connectionState = getEspnowMeshBackendConst().serializeUnencryptedConnection();
uint32_t unsyncMsgID = 0;
JsonTranslator::getUnsynchronizedMessageID(connectionState, unsyncMsgID);
return
String(F("{\"meshState\":{"))
+ connectionState.substring(1, connectionState.length() - 1) + String(',')
+ createJsonEndPair(FPSTR(jsonMeshMessageCount), String(_messageCount));
return Serializer::serializeMeshState(String(unsyncMsgID), String(_messageCount));
}
void FloodingMesh::loadMeshState(const String &serializedMeshState)

View File

@ -25,59 +25,171 @@
#include "JsonTranslator.h"
#include "EspnowProtocolInterpreter.h"
#include "TypeConversionFunctions.h"
#include "MeshCryptoInterface.h"
#include "MeshCryptoInterface.h" // TODO: Remove?
namespace
{
namespace TypeCast = MeshTypeConversionFunctions;
bool getMac(const String &jsonString, const String &valueIdentifier, uint8_t *resultArray)
{
String jsonValue;
bool decoded = JsonTranslator::decode(jsonString, valueIdentifier, jsonValue);
if(jsonValue.length() != 12)
decoded = false; // Mac String is always 12 characters long
if(decoded)
TypeCast::stringToMac(jsonValue, resultArray);
return decoded;
}
}
namespace JsonTranslator
{
String createJsonPair(const String &valueIdentifier, const String &value)
int32_t getStartIndex(const String &jsonString, const String &valueIdentifier, const int32_t searchStartIndex)
{
return valueIdentifier + '\"' + value + F("\",");
int32_t startIndex = jsonString.indexOf(String('"') + valueIdentifier + F("\":"), searchStartIndex);
if(startIndex < 0)
return startIndex;
startIndex += valueIdentifier.length() + 3; // Do not include valueIdentifier and associated characters
return startIndex;
}
int32_t getEndIndex(const String &jsonString, const int32_t searchStartIndex)
{
int32_t endIndex = -1;
if(jsonString[searchStartIndex] == '"')
{
endIndex = jsonString.indexOf('"', searchStartIndex + 1);
}
else if(jsonString[searchStartIndex] == '{')
{
uint32_t depth = 1;
bool withinString = false;
for(uint32_t index = searchStartIndex + 1; depth != 0 && index < jsonString.length(); ++index)
{
if(jsonString[index] == '"')
withinString = !withinString;
else if(!withinString)
{
if(jsonString[index] == '{')
++depth;
else if(jsonString[index] == '}')
--depth;
}
if(depth == 0)
{
assert(index < 0x80000000); // Must avoid int32_t overflow
endIndex = index;
}
}
}
return endIndex;
}
String encode(std::initializer_list<String> identifiersAndValues)
{
assert(identifiersAndValues.size() % 2 == 0); // List must consist of identifer-value pairs.
String result = String('{');
bool isIdentifier = true;
for(String element : identifiersAndValues)
{
bool isObject = !isIdentifier && element[0] == '{';
if(isObject)
result += element;
else
result += String('"') + element + String('"');
if(isIdentifier)
result += ':';
else
result += ',';
isIdentifier = !isIdentifier;
}
result[result.length() - 1] = '}';
return result;
}
String createJsonEndPair(const String &valueIdentifier, const String &value)
String encodeLiterally(std::initializer_list<String> identifiersAndValues)
{
return valueIdentifier + '\"' + value + F("\"}}");
}
String createEncryptedConnectionInfo(const String &infoHeader, const String &requestNonce, const String &authenticationPassword, const uint64_t ownSessionKey, const uint64_t peerSessionKey)
{
// Returns: Encrypted connection info:{"arguments":{"nonce":"1F2","password":"abc","ownSK":"3B4","peerSK":"1A2"}}
assert(identifiersAndValues.size() % 2 == 0); // List must consist of identifer-value pairs.
String result = String('{');
return
infoHeader + String(F("{\"arguments\":{"))
+ createJsonPair(FPSTR(jsonNonce), requestNonce)
+ createJsonPair(FPSTR(jsonPassword), authenticationPassword)
+ createJsonPair(FPSTR(jsonOwnSessionKey), TypeCast::uint64ToString(peerSessionKey)) // Exchanges session keys since it should be valid for the receiver.
+ createJsonEndPair(FPSTR(jsonPeerSessionKey), TypeCast::uint64ToString(ownSessionKey));
}
String createEncryptionRequestIntro(const String &requestHeader, const uint32_t duration)
{
return
requestHeader + String(F("{\"arguments\":{"))
+ (requestHeader == FPSTR(EspnowProtocolInterpreter::temporaryEncryptionRequestHeader) ? createJsonPair(FPSTR(jsonDuration), String(duration)) : emptyString);
}
String createEncryptionRequestEnding(const String &requestNonce)
{
return createJsonEndPair(FPSTR(jsonNonce), requestNonce);
}
String createEncryptionRequestHmacMessage(const String &requestHeader, const String &requestNonce, const uint8_t *hashKey, const uint8_t hashKeyLength, const uint32_t duration)
{
String mainMessage = createEncryptionRequestIntro(requestHeader, duration) + createJsonPair(FPSTR(jsonNonce), requestNonce);
uint8_t staMac[6] {0};
uint8_t apMac[6] {0};
String requesterStaApMac = TypeCast::macToString(WiFi.macAddress(staMac)) + TypeCast::macToString(WiFi.softAPmacAddress(apMac));
String hmac = MeshCryptoInterface::createMeshHmac(requesterStaApMac + mainMessage, hashKey, hashKeyLength);
return mainMessage + createJsonEndPair(FPSTR(jsonHmac), hmac);
bool isIdentifier = true;
for(String element : identifiersAndValues)
{
if(isIdentifier)
result += String('"') + element + String('"') + ':';
else
result += element + ',';
isIdentifier = !isIdentifier;
}
result[result.length() - 1] = '}';
return result;
}
bool decode(const String &jsonString, const String &valueIdentifier, String &value)
{
int32_t startIndex = getStartIndex(jsonString, valueIdentifier);
if(startIndex < 0)
return false;
int32_t endIndex = getEndIndex(jsonString, startIndex);
if(endIndex < 0)
return false;
if(jsonString[startIndex] == '"')
++startIndex; // Should not include starting "
else if(jsonString[startIndex] == '{')
++endIndex; // Should include ending }
else
assert(false && F("Illegal JSON starting character!"));
value = jsonString.substring(startIndex, endIndex);
return true;
}
bool decode(const String &jsonString, const String &valueIdentifier, uint32_t &value)
{
String jsonValue;
bool decoded = decode(jsonString, valueIdentifier, jsonValue);
if(decoded)
value = strtoul(jsonValue.c_str(), nullptr, 0); // strtoul stops reading input when an invalid character is discovered.
return decoded;
}
bool decodeRadix(const String &jsonString, const String &valueIdentifier, uint64_t &value, const uint8_t radix)
{
String jsonValue;
bool decoded = decode(jsonString, valueIdentifier, jsonValue);
if(decoded)
value = TypeCast::stringToUint64(jsonValue, radix);
return decoded;
}
// TODO: Move to encryptedEspnow class?
bool verifyEncryptionRequestHmac(const String &encryptionRequestHmacMessage, const uint8_t *requesterStaMac, const uint8_t *requesterApMac,
const uint8_t *hashKey, const uint8_t hashKeyLength)
{
@ -86,7 +198,7 @@ namespace JsonTranslator
String hmac;
if(getHmac(encryptionRequestHmacMessage, hmac))
{
int32_t hmacStartIndex = encryptionRequestHmacMessage.indexOf(FPSTR(jsonHmac));
int32_t hmacStartIndex = encryptionRequestHmacMessage.indexOf(String('"') + FPSTR(jsonHmac) + F("\":"));
if(hmacStartIndex < 0)
return false;
@ -99,180 +211,79 @@ namespace JsonTranslator
return false;
}
int32_t getStartIndex(const String &jsonString, const String &valueIdentifier, const int32_t searchStartIndex)
{
int32_t startIndex = jsonString.indexOf(valueIdentifier, searchStartIndex);
if(startIndex < 0)
return startIndex;
startIndex += valueIdentifier.length() + 1; // Do not include valueIdentifier and initial quotation mark
return startIndex;
}
int32_t getEndIndex(const String &jsonString, const int32_t searchStartIndex)
{
int32_t endIndex = jsonString.indexOf(',', searchStartIndex);
if(endIndex < 0)
endIndex = jsonString.indexOf('}', searchStartIndex);
endIndex -= 1; // End index will be at the character after the closing quotation mark, so need to subtract 1.
return endIndex;
}
bool getConnectionState(const String &jsonString, String &result)
{
int32_t startIndex = jsonString.indexOf(FPSTR(jsonConnectionState));
if(startIndex < 0)
return false;
int32_t endIndex = jsonString.indexOf('}');
if(endIndex < 0)
return false;
result = jsonString.substring(startIndex, endIndex + 1);
return true;
return decode(jsonString, FPSTR(jsonConnectionState), result);
}
bool getPassword(const String &jsonString, String &result)
{
int32_t startIndex = getStartIndex(jsonString, FPSTR(jsonPassword));
if(startIndex < 0)
return false;
int32_t endIndex = getEndIndex(jsonString, startIndex);
if(endIndex < 0)
return false;
result = jsonString.substring(startIndex, endIndex);
return true;
return decode(jsonString, FPSTR(jsonPassword), result);
}
bool getOwnSessionKey(const String &jsonString, uint64_t &result)
{
int32_t startIndex = getStartIndex(jsonString, FPSTR(jsonOwnSessionKey));
if(startIndex < 0)
return false;
int32_t endIndex = getEndIndex(jsonString, startIndex);
if(endIndex < 0)
return false;
result = TypeCast::stringToUint64(jsonString.substring(startIndex, endIndex));
return true;
return decodeRadix(jsonString, FPSTR(jsonOwnSessionKey), result);
}
bool getPeerSessionKey(const String &jsonString, uint64_t &result)
{
int32_t startIndex = getStartIndex(jsonString, FPSTR(jsonPeerSessionKey));
if(startIndex < 0)
return false;
int32_t endIndex = getEndIndex(jsonString, startIndex);
if(endIndex < 0)
return false;
result = TypeCast::stringToUint64(jsonString.substring(startIndex, endIndex));
return true;
return decodeRadix(jsonString, FPSTR(jsonPeerSessionKey), result);
}
bool getPeerStaMac(const String &jsonString, uint8_t *resultArray)
{
int32_t startIndex = getStartIndex(jsonString, FPSTR(jsonPeerStaMac));
if(startIndex < 0)
return false;
int32_t endIndex = getEndIndex(jsonString, startIndex);
if(endIndex < 0 || endIndex - startIndex != 12) // Mac String is always 12 characters long
return false;
TypeCast::stringToMac(jsonString.substring(startIndex, endIndex), resultArray);
return true;
return getMac(jsonString, FPSTR(jsonPeerStaMac), resultArray);
}
bool getPeerApMac(const String &jsonString, uint8_t *resultArray)
{
int32_t startIndex = getStartIndex(jsonString, FPSTR(jsonPeerApMac));
if(startIndex < 0)
return false;
int32_t endIndex = getEndIndex(jsonString, startIndex);
if(endIndex < 0 || endIndex - startIndex != 12) // Mac String is always 12 characters long
return false;
TypeCast::stringToMac(jsonString.substring(startIndex, endIndex), resultArray);
return true;
{
return getMac(jsonString, FPSTR(jsonPeerApMac), resultArray);
}
bool getDuration(const String &jsonString, uint32_t &result)
{
int32_t startIndex = getStartIndex(jsonString, FPSTR(jsonDuration));
if(startIndex < 0)
return false;
result = strtoul(jsonString.substring(startIndex).c_str(), nullptr, 0); // strtoul stops reading input when an invalid character is discovered.
return true;
return decode(jsonString, FPSTR(jsonDuration), result);
}
bool getNonce(const String &jsonString, String &result)
{
int32_t startIndex = getStartIndex(jsonString, FPSTR(jsonNonce));
if(startIndex < 0)
return false;
int32_t endIndex = getEndIndex(jsonString, startIndex);
if(endIndex < 0)
return false;
result = jsonString.substring(startIndex, endIndex);
return true;
return decode(jsonString, FPSTR(jsonNonce), result);
}
bool getHmac(const String &jsonString, String &result)
{
int32_t startIndex = getStartIndex(jsonString, FPSTR(jsonHmac));
if(startIndex < 0)
return false;
int32_t endIndex = getEndIndex(jsonString, startIndex);
if(endIndex < 0)
return false;
result = jsonString.substring(startIndex, endIndex);
return true;
return decode(jsonString, FPSTR(jsonHmac), result);
}
bool getDesync(const String &jsonString, bool &result)
{
int32_t startIndex = getStartIndex(jsonString, FPSTR(jsonDesync));
if(startIndex < 0)
return false;
String jsonValue;
bool decoded = decode(jsonString, FPSTR(jsonDesync), jsonValue);
result = bool(strtoul(jsonString.substring(startIndex).c_str(), nullptr, 0)); // strtoul stops reading input when an invalid character is discovered.
return true;
if(decoded)
result = bool(strtoul(jsonValue.c_str(), nullptr, 0)); // strtoul stops reading input when an invalid character is discovered.
return decoded;
}
bool getUnsynchronizedMessageID(const String &jsonString, uint32_t &result)
{
int32_t startIndex = getStartIndex(jsonString, FPSTR(jsonUnsynchronizedMessageID));
if(startIndex < 0)
return false;
result = strtoul(jsonString.substring(startIndex).c_str(), nullptr, 0); // strtoul stops reading input when an invalid character is discovered.
return true;
return decode(jsonString, FPSTR(jsonUnsynchronizedMessageID), result);
}
bool getMeshMessageCount(const String &jsonString, uint16_t &result)
{
int32_t startIndex = getStartIndex(jsonString, FPSTR(jsonMeshMessageCount));
if(startIndex < 0)
return false;
uint32_t longResult = 0;
bool decoded = decode(jsonString, FPSTR(jsonMeshMessageCount), longResult);
uint32_t longResult = strtoul(jsonString.substring(startIndex).c_str(), nullptr, 0); // strtoul stops reading input when an invalid character is discovered.
assert(longResult <= 65535); // Must fit within uint16_t
result = longResult;
return true;
if(longResult > 65535) // Must fit within uint16_t
decoded = false;
if(decoded)
result = longResult;
return decoded;
}
}

View File

@ -26,34 +26,29 @@
#define __ESPNOWJSONTRANSLATOR_H__
#include <WString.h>
#include <initializer_list>
namespace JsonTranslator
{
constexpr char jsonConnectionState[] PROGMEM = "{\"connectionState\":{";
constexpr char jsonPassword[] PROGMEM = "\"password\":";
constexpr char jsonOwnSessionKey[] PROGMEM = "\"ownSK\":";
constexpr char jsonPeerSessionKey[] PROGMEM = "\"peerSK\":";
constexpr char jsonPeerStaMac[] PROGMEM = "\"peerStaMac\":";
constexpr char jsonPeerApMac[] PROGMEM = "\"peerApMac\":";
constexpr char jsonDuration[] PROGMEM = "\"duration\":";
constexpr char jsonNonce[] PROGMEM = "\"nonce\":";
constexpr char jsonHmac[] PROGMEM = "\"hmac\":";
constexpr char jsonDesync[] PROGMEM = "\"desync\":";
constexpr char jsonUnsynchronizedMessageID[] PROGMEM = "\"unsyncMsgID\":";
constexpr char jsonMeshMessageCount[] PROGMEM = "\"meshMsgCount\":";
String createJsonPair(const String &valueIdentifier, const String &value);
String createJsonEndPair(const String &valueIdentifier, const String &value);
String createEncryptedConnectionInfo(const String &infoHeader, const String &requestNonce, const String &authenticationPassword, const uint64_t ownSessionKey, const uint64_t peerSessionKey);
String createEncryptionRequestIntro(const String &requestHeader, const uint32_t duration = 0);
String createEncryptionRequestEnding(const String &requestNonce);
String createEncryptionRequestHmacMessage(const String &requestHeader, const String &requestNonce, const uint8_t *hashKey, const uint8_t hashKeyLength, const uint32_t duration = 0);
bool verifyEncryptionRequestHmac(const String &encryptionRequestHmacMessage, const uint8_t *requesterStaMac, const uint8_t *requesterApMac, const uint8_t *hashKey, const uint8_t hashKeyLength);
constexpr char jsonConnectionState[] PROGMEM = "connectionState";
constexpr char jsonMeshState[] PROGMEM = "meshState";
constexpr char jsonPassword[] PROGMEM = "password";
constexpr char jsonOwnSessionKey[] PROGMEM = "ownSK";
constexpr char jsonPeerSessionKey[] PROGMEM = "peerSK";
constexpr char jsonPeerStaMac[] PROGMEM = "peerStaMac";
constexpr char jsonPeerApMac[] PROGMEM = "peerApMac";
constexpr char jsonDuration[] PROGMEM = "duration";
constexpr char jsonNonce[] PROGMEM = "nonce";
constexpr char jsonHmac[] PROGMEM = "hmac";
constexpr char jsonDesync[] PROGMEM = "desync";
constexpr char jsonUnsynchronizedMessageID[] PROGMEM = "unsyncMsgID";
constexpr char jsonMeshMessageCount[] PROGMEM = "meshMsgCount";
constexpr char jsonArguments[] PROGMEM = "arguments";
/**
* Provides the index within jsonString where the value of valueIdentifier starts.
* Note that including " within a JSON string value will result in errors.
*
* @param jsonString The String to search within.
* @param valueIdentifier The identifier to search for.
@ -64,14 +59,86 @@ namespace JsonTranslator
int32_t getStartIndex(const String &jsonString, const String &valueIdentifier, const int32_t searchStartIndex = 0);
/**
* Provides the index within jsonString where the next JSON termination character (',' or '}') is found, starting from searchStartIndex.
* Provides the index within jsonString where the JSON object or JSON string value ends, starting the search from searchStartIndex.
* Note that including " within a JSON string value will result in errors.
*
* The character at searchStartIndex must be either " (for a string) or { (for an object), otherwise the search fails.
*
* @param jsonString The String to search within.
* @param searchStartIndex The index of jsonString where the search will start.
* @param searchStartIndex The index of jsonString where the search will start. The index position should contain either " or {.
*
* @return An int32_t containing the index within jsonString where the next JSON termination character is found, or a negative value if no such character was found.
* @return An int32_t containing the index within jsonString where the JSON string/object ends, or a negative value if no such character was found.
*/
int32_t getEndIndex(const String &jsonString, const int32_t searchStartIndex);
/*
* Create a JSON String based on the identifiers and values given.
*
* Assumes all values are either strings or JSON objects. A value is interpreted as a JSON object if it starts with {
* Assumes all identifiers are strings.
*
* @param identifiersAndValues Any even number of String arguments. It is assumed that the identifiers and values are given in an alternating manner, as in encode({Identifier1, Value1, Identifier2, Value2, ...})
*/
String encode(std::initializer_list<String> identifiersAndValues);
/*
* Create a JSON String based on the identifiers and values given.
*
* Does not make any assumptions regarding value types. " must be added manually around string values.
* Useful for example if your JSON values can contain starting { characters, since the regular encode() will then interpret them as JSON objects.
* Assumes all identifiers are strings.
*
* @param identifiersAndValues Any even number of String arguments. It is assumed that the identifiers and values are given in an alternating manner, as in encodeLiterally({Identifier1, Value1, Identifier2, Value2, ...})
*/
String encodeLiterally(std::initializer_list<String> identifiersAndValues);
/*
* Get a value from a JSON String.
* Assumes all values are either JSON strings ( starting with " ) or JSON objects ( starting with { ).
*
* Note that including " within a JSON string value will result in errors.
* Escape characters are not supported at this moment, since we do not want string length modification to occur during ESP-NOW protocol transmissions.
*
* @param jsonString The String to search within.
* @param valueIdentifier The identifier to search for.
* @param value The String variable to put the result in.
*
* @return True if a value was found. False otherwise. The value argument is not modified if false is returned.
*/
bool decode(const String &jsonString, const String &valueIdentifier, String &value);
/*
* Get a value from a JSON String.
* Assumes all values are stored as strings in standard C-format (i.e. decimal by default).
*
* Note that including " within a JSON string value will result in errors.
* Escape characters are not supported at this moment, since we do not want string length modification to occur during ESP-NOW protocol transmissions.
*
* @param jsonString The String to search within.
* @param valueIdentifier The identifier to search for.
* @param value The uint32_t variable to put the result in.
*
* @return True if a value was found. False otherwise. The value argument is not modified if false is returned.
*/
bool decode(const String &jsonString, const String &valueIdentifier, uint32_t &value);
/*
* Get a value from a JSON String.
* Assumes all values are stored as strings encoded in the specified radix. Hexadecimal encoding is the default.
*
* Note that including " within a JSON string value will result in errors.
* Escape characters are not supported at this moment, since we do not want string length modification to occur during ESP-NOW protocol transmissions.
*
* @param jsonString The String to search within.
* @param valueIdentifier The identifier to search for.
* @param value The uint64_t variable to put the result in.
* @param radix The base to use when converting the string value to uint64_t. Must be between 2 and 36.
*
* @return True if a value was found. False otherwise. The value argument is not modified if false is returned.
*/
bool decodeRadix(const String &jsonString, const String &valueIdentifier, uint64_t &value, const uint8_t radix = 16);
bool verifyEncryptionRequestHmac(const String &encryptionRequestHmacMessage, const uint8_t *requesterStaMac, const uint8_t *requesterApMac, const uint8_t *hashKey, const uint8_t hashKeyLength);
bool getConnectionState(const String &jsonString, String &result);
/**

View File

@ -203,6 +203,7 @@ String MeshBackendBase::getNodeID() const {return getSSIDSuffix();}
void MeshBackendBase::setMeshPassword(const String &newMeshPassword)
{
assert(8 <= newMeshPassword.length() && newMeshPassword.length() <= 64); // Limited by the ESP8266 API.
assert(newMeshPassword.indexOf('"') == -1); // " is not allowed in passwords to allow for easier JSON parsing and predictable password length (no need for extra escape characters).
_meshPassword = newMeshPassword;

View File

@ -194,7 +194,7 @@ public:
* Will also change the setting for the active AP (via an AP restart)
* if this MeshBackendBase instance is the current AP controller.
*
* @param newMeshPassword The password to use.
* @param newMeshPassword The password to use. Must be between 8 and 64 characters long. " is an illegal character because of JSON parsing requirements.
*/
void setMeshPassword(const String &newMeshPassword);
String getMeshPassword() const;

View File

@ -0,0 +1,122 @@
/*
* Copyright (C) 2020 Anders Löfgren
*
* License (MIT license):
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#include "Serializer.h"
#include "JsonTranslator.h"
#include "TypeConversionFunctions.h"
#include "MeshCryptoInterface.h"
namespace
{
namespace TypeCast = MeshTypeConversionFunctions;
String createJsonEndPair(const String &valueIdentifier, const String &value)
{
const String q = String('"');
return q + valueIdentifier + q + ':' + q + value + F("\"}}");
}
}
namespace Serializer
{
/*
* NOTE: The internal states may be changed in future updates, so the function signatures here are not guaranteed to be stable.
*/
String serializeMeshState(const String &unsyncMsgID, const String &meshMsgCount)
{
using namespace JsonTranslator;
// Returns: {"meshState":{"connectionState":{"unsyncMsgID":"123"},"meshMsgCount":"123"}}
return encode({FPSTR(jsonMeshState), encode({FPSTR(jsonConnectionState), encode({FPSTR(jsonUnsynchronizedMessageID), unsyncMsgID}), FPSTR(jsonMeshMessageCount), meshMsgCount})});
}
String serializeUnencryptedConnection(const String &unsyncMsgID)
{
using namespace JsonTranslator;
// Returns: {"connectionState":{"unsyncMsgID":"123"}}
return encode({FPSTR(jsonConnectionState), encode({FPSTR(jsonUnsynchronizedMessageID), unsyncMsgID})});
}
String serializeEncryptedConnection(const String &duration, const String &desync, const String &ownSK, const String &peerSK, const String &peerStaMac, const String &peerApMac)
{
using namespace JsonTranslator;
if(duration.isEmpty())
{
// Returns: {"connectionState":{"desync":"0","ownSK":"1A2","peerSK":"3B4","peerStaMac":"F2","peerApMac":"E3"}}
return encode({FPSTR(jsonConnectionState), encode({FPSTR(jsonDesync), desync, FPSTR(jsonOwnSessionKey), ownSK, FPSTR(jsonPeerSessionKey), peerSK,
FPSTR(jsonPeerStaMac), peerStaMac, FPSTR(jsonPeerApMac), peerApMac})});
}
// Returns: {"connectionState":{"duration":"123","desync":"0","ownSK":"1A2","peerSK":"3B4","peerStaMac":"F2","peerApMac":"E3"}}
return encode({FPSTR(jsonConnectionState), encode({FPSTR(jsonDuration), duration, FPSTR(jsonDesync), desync, FPSTR(jsonOwnSessionKey), ownSK, FPSTR(jsonPeerSessionKey), peerSK,
FPSTR(jsonPeerStaMac), peerStaMac, FPSTR(jsonPeerApMac), peerApMac})});
}
String createEncryptedConnectionInfo(const String &infoHeader, const String &requestNonce, const String &authenticationPassword, const uint64_t ownSessionKey, const uint64_t peerSessionKey)
{
using namespace JsonTranslator;
const String q = String('"');
// Returns: infoHeader{"arguments":{"nonce":"1F2","password":"abc","ownSK":"3B4","peerSK":"1A2"}}
return
infoHeader +
encode({FPSTR(jsonArguments),
encodeLiterally({FPSTR(jsonNonce), q + requestNonce + q,
FPSTR(jsonPassword), q + authenticationPassword + q,
FPSTR(jsonOwnSessionKey), q + TypeCast::uint64ToString(peerSessionKey) + q, // Exchanges session keys since it should be valid for the receiver.
FPSTR(jsonPeerSessionKey), q + TypeCast::uint64ToString(ownSessionKey) + q})});
}
String createEncryptionRequestHmacMessage(const String &requestHeader, const String &requestNonce, const uint8_t *hashKey, const uint8_t hashKeyLength, const uint32_t duration)
{
using namespace JsonTranslator;
String mainMessage = requestHeader;
if(requestHeader == FPSTR(EspnowProtocolInterpreter::temporaryEncryptionRequestHeader))
{
mainMessage += encode({FPSTR(jsonArguments), encode({FPSTR(jsonDuration), String(duration), FPSTR(jsonNonce), requestNonce})});
}
else
{
mainMessage += encode({FPSTR(jsonArguments), encode({FPSTR(jsonNonce), requestNonce})});
}
// We need to have an open JSON object so we can add the HMAC later.
mainMessage.remove(mainMessage.length() - 2);
mainMessage += ',';
uint8_t staMac[6] {0};
uint8_t apMac[6] {0};
String requesterStaApMac = TypeCast::macToString(WiFi.macAddress(staMac)) + TypeCast::macToString(WiFi.softAPmacAddress(apMac));
String hmac = MeshCryptoInterface::createMeshHmac(requesterStaApMac + mainMessage, hashKey, hashKeyLength);
// Returns: requestHeader{"arguments":{"duration":"123","nonce":"1F2","hmac":"3B4"}}
return mainMessage + createJsonEndPair(FPSTR(jsonHmac), hmac);
}
}

View File

@ -0,0 +1,44 @@
/*
* Copyright (C) 2020 Anders Löfgren
*
* License (MIT license):
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#ifndef __ESP8266MESHSERIALIZER_H__
#define __ESP8266MESHSERIALIZER_H__
#include <WString.h>
namespace Serializer
{
/*
* NOTE: The internal states may be changed in future updates, so the function signatures here are not guaranteed to be stable.
*/
String serializeMeshState(const String &unsyncMsgID, const String &meshMsgCount);
String serializeUnencryptedConnection(const String &unsyncMsgID);
String serializeEncryptedConnection(const String &duration, const String &desync, const String &ownSK, const String &peerSK, const String &peerStaMac, const String &peerApMac);
String createEncryptedConnectionInfo(const String &infoHeader, const String &requestNonce, const String &authenticationPassword, const uint64_t ownSessionKey, const uint64_t peerSessionKey);
String createEncryptionRequestHmacMessage(const String &requestHeader, const String &requestNonce, const uint8_t *hashKey, const uint8_t hashKeyLength, const uint32_t duration = 0);
}
#endif

View File

@ -42,7 +42,7 @@ namespace MeshTypeConversionFunctions
* @param base The radix to convert "number" into. Must be between 2 and 36.
* @return A string of "number" encoded in radix "base".
*/
String uint64ToString(uint64_t number, const byte base = 16);
String uint64ToString(uint64_t number, const uint8_t base = 16);
/**
* Note that using base 10 instead of 16 increases conversion time by roughly a factor of 2, due to unfavourable 64-bit arithmetic.
@ -52,7 +52,7 @@ namespace MeshTypeConversionFunctions
* @param base The radix of "string". Must be between 2 and 36.
* @return A uint64_t of the string, using radix "base" during decoding.
*/
uint64_t stringToUint64(const String &string, const byte base = 16);
uint64_t stringToUint64(const String &string, const uint8_t base = 16);
/**
* Convert the contents of a uint8_t array to a String in HEX format. The resulting String starts from index 0 of the array.