From 1c7011400c01b2df1e29a142040a1b984c14d4e7 Mon Sep 17 00:00:00 2001 From: Christoph Hagen Date: Thu, 7 Apr 2022 17:45:23 +0200 Subject: [PATCH] Restructure, use HMAC, NTP Remove config details --- include/crypto.h | 55 +++++++ include/fresh.h | 114 +++++++++++++ include/message.h | 87 ++++++++++ include/server.h | 108 ++++++++++++ include/servo.h | 84 ++++++++++ src/crypto.cpp | 45 +++++ src/fresh.cpp | 92 +++++++++++ src/main.cpp | 408 +++++++++------------------------------------- src/server.cpp | 96 +++++++++++ src/servo.cpp | 37 +++++ 10 files changed, 792 insertions(+), 334 deletions(-) create mode 100644 include/crypto.h create mode 100644 include/fresh.h create mode 100644 include/message.h create mode 100644 include/server.h create mode 100644 include/servo.h create mode 100644 src/crypto.cpp create mode 100644 src/fresh.cpp create mode 100644 src/server.cpp create mode 100644 src/servo.cpp diff --git a/include/crypto.h b/include/crypto.h new file mode 100644 index 0000000..202d1e5 --- /dev/null +++ b/include/crypto.h @@ -0,0 +1,55 @@ +#pragma once + +#include "message.h" +#include + +/** + * @brief Create a message authentication code (MAC) for some data. + * + * @param data The data to authenticate + * @param dataLength The number of bytes to authenticate + * @param mac The output to store the MAC (must be at least 32 bytes) + * @param key The secret key used for authentication + * @param keyLength The length of the secret key + * @return true The MAC was successfully written + * @return false The MAC could not be created + */ +bool authenticateData(const uint8_t* data, size_t dataLength, uint8_t* mac, const uint8_t* key, size_t keyLength); + +/** + * @brief Calculate a MAC for message content. + * + * @param message The message for which to calculate the MAC. + * @param mac The output where the computed MAC is stored + * @param key The secret key used for authentication + * @param keyLength The length of the secret key + * @return true The MAC was successfully computed + * @return false The MAC could not be created + */ +bool authenticateMessage(Message* message, uint8_t* mac, const uint8_t* key, size_t keyLength); + +/** + * @brief Create a message authentication code (MAC) for a message. + * + * @param message The message to authenticate + * @param key The secret key used for authentication + * @param keyLength The length of the secret key + * @return true The MAC was successfully added to the message + * @return false The MAC could not be created + */ +bool authenticateMessage(AuthenticatedMessage* message, const uint8_t* key, size_t keyLength); + +/** + * @brief Check if a received unlock message is authentic + * + * This function computes the MAC of the message and compares it with + * the MAC included in the message. The message is authentic if both + * MACs are identical. + * + * @param message The message to authenticate + * @param key The secret key used for authentication + * @param keyLength The length of the key in bytes + * @return true The message is authentic + * @return false The message is invalid, or the MAC could not be calculated + */ +bool isAuthenticMessage(AuthenticatedMessage* message, const uint8_t* key, size_t keyLength); \ No newline at end of file diff --git a/include/fresh.h b/include/fresh.h new file mode 100644 index 0000000..de8ab60 --- /dev/null +++ b/include/fresh.h @@ -0,0 +1,114 @@ +#pragma once + +#include + +/** + * @brief The size of the message counter in bytes (uint32_t) + */ +#define MESSAGE_COUNTER_SIZE sizeof(uint32_t) + +/** + * @brief Configure an NTP server to get the current time + * + * @param offsetToGMT The timezone offset in seconds + * @param offsetDaylightSavings The daylight savings offset in seconds + * @param serverUrl The url of the NTP server + */ +void configureNTP(int32_t offsetToGMT, int32_t offsetDaylightSavings, const char* serverUrl); + +/** + * @brief Print the current time to the serial output + * + * The time must be initialized by calling `configureNTP()` before use. + */ +void printLocalTime(); + +/** + * Gets the current epoch time + */ +uint32_t getEpochTime(); + +/** + * @brief The allowed time discrepancy (in seconds) + * + * Specifies the allowed discrepancy between the time of a received message + * and the device time (in seconds). + * + * A stricter (lower) value better prevents against replay attacks, + * but may lead to issues when dealing with slow networks and other + * routing delays. + * + * @param offset The offset in both directions (seconds) + */ +void setMessageTimeAllowedOffset(uint32_t offset); + +/** + * @brief Check wether the time of a message is within the allowed bounds regarding freshness. + * + * The timestamp is used to ensure 'freshness' of the messages, + * i.e. that they are not unreasonably delayed or captured and + * later replayed by an attacker. + * + * @param messageTime The timestamp of the message (seconds since epoch) + * @return true The time is within the acceptable offset of the local time + * @return false The message time is invalid + */ +bool isMessageTimeAcceptable(uint32_t messageTime); + +/** + * @brief Initialize the use of the message counter API + * + * The message counter is stored in EEPROM, which must be initialized before use. + * + * @note The ESP32 does not have a true EEPROM, + * which is emulated using a section of the flash memory. + */ +void prepareMessageCounterUsage(); + +/** + * @brief Get the expected count for the next message. + * + * The counter is stored in EEPROM to persist across restarts + * + * @return The next counter to use by the remote + */ +uint32_t getNextMessageCounter(); + +/** + * @brief Print info about the current message counter to the serial output + * + */ +void printMessageCounter(); + +/** + * @brief Check if a received counter is valid + * + * The counter is valid if it is larger than the previous counter + * (larger or equal to the next expected counter). + * + * @param counter The counter to check + * @return true The counter is valid + * @return false The counter belongs to an old message + */ +bool isMessageCounterValid(uint32_t counter); + +/** + * @brief Mark a counter of a message as used. + * + * The counter value is stored in EEPROM to persist across restarts. + * + * All messages with counters lower than the given one will become invalid. + * + * @param counter The counter used in the last message. + */ +void didUseMessageCounter(uint32_t counter); + +/** + * @brief Reset the message counter. + * + * @warning The counter should never be reset in production environments, + * and only together with a new secret key. Otherwise old messages may be + * used for replay attacks. + * + */ +void resetMessageCounter(); diff --git a/include/message.h b/include/message.h new file mode 100644 index 0000000..d112204 --- /dev/null +++ b/include/message.h @@ -0,0 +1,87 @@ +#pragma once + +#include "stdint.h" + +/** + * @brief The size of a message authentication code + * + * The MAC size is determined by the size of the output + * of the hash function used. In this case, for SHA256, + * the size is 32 bytes (= 256 bit) + */ +#define SHA256_MAC_SIZE 32 + +#pragma pack(push, 1) + +/** + * @brief The content of an unlock message. + * + * The content is necessary to ensure freshness of the message + * by requiring a recent time and a monotonously increasing counter. + * This prevents messages from being delayed or being blocked and + * replayed later. + */ +typedef struct { + + /** + * The timestamp of message creation + * + * The timestamp is encoded as the epoch time, i.e. seconds since 1970 (GMT). + * + * The timestamp is used to ensure 'freshness' of the messages, + * i.e. that they are not unreasonably delayed or captured and + * later replayed by an attacker. + */ + uint32_t time; + + /** + * The counter of unlock messages + * + * This counter must always increase with each message from the remote + * in order for the messages to be deemed valid. Transfering the counters + * back and forth also gives information about lost messages and potential + * attacks. Both the remote and the device keep a record of at least the + * last used counter. + */ + uint32_t id; + +} Message; + +/** + * @brief An authenticated message by the mobile device to command unlocking. + * + * The message is protected by a message authentication code (MAC) based on + * a symmetric key shared by the device and the remote. This code ensures + * that the contents of the request were not altered. The message further + * contains a timestamp to ensure that the message is recent, and not replayed + * by an attacker. An additional counter is also included for this purpose, + * which must continously increase for a message to be valid. This increases + * security a bit, since the timestamp validation must be tolerant to some + * inaccuracy due to mismatching clocks. + */ +typedef struct { + + /** + * @brief The authentication code of the message + * + * The code is created by performing HMAC-SHA256 + * over the bytes of the `Message`. + */ + uint8_t mac[SHA256_MAC_SIZE]; + + /** + * @brief The message content. + * + * The content is necessary to ensure freshness of the message + * by requiring a recent time and a monotonously increasing counter. + * This prevents messages from being delayed or being blocked and + * replayed later. + */ + Message message; + +} AuthenticatedMessage; +#pragma pack(pop) + +#define MESSAGE_CONTENT_SIZE sizeof(Message) + +#define AUTHENTICATED_MESSAGE_SIZE sizeof(AuthenticatedMessage) \ No newline at end of file diff --git a/include/server.h b/include/server.h new file mode 100644 index 0000000..a720900 --- /dev/null +++ b/include/server.h @@ -0,0 +1,108 @@ +#pragma once + +#include "message.h" +#include "crypto.h" +#include +#include +#include + +/** + * An event signaled from the device + */ +enum class SesameEvent { + TextReceived = 1, + UnexpectedSocketEvent = 2, + InvalidPayloadSize = 3, + MessageAuthenticationFailed = 4, + MessageTimeMismatch = 5, + MessageCounterInvalid = 6, + MessageAccepted = 7, + InfoMessage = 8, +}; + +/** + * @brief A callback for messages received over the socket + * + * The first parameter is the received message. + * The second parameter is the response to the remote. + * The return value is the type of event to respond with. + */ +typedef SesameEvent (*MessageCallback)(AuthenticatedMessage*, AuthenticatedMessage*); + +class ServerConnection { + +public: + + ServerConnection(const char* url, int port, const char* path); + + void connect(const char* key, uint32_t reconnectTime = 5000); + + void connectSSL(const char* key, uint32_t reconnectTime = 5000); + + void loop(); + + void onMessage(MessageCallback callback); + + // Indicator that the socket is connected. + bool socketIsConnected = false; + +private: + + const char* url; + + int port; + + const char* path; + + const char* key = NULL; + + MessageCallback messageCallback = NULL; + + // WebSocket to connect to the control server + WebSocketsClient webSocket; + + void reconnectAfter(uint32_t reconnectTime); + + void registerEventCallback(); + + /** + * Callback for WebSocket events. + * + * Updates the connection state and processes received keys. + * + * @param payload The pointer to received data + * @param length The number of bytes received + */ + void webSocketEventHandler(WStype_t type, uint8_t * payload, size_t length); + + /** + * Process received binary data. + * + * Checks whether the received data is a valid and unused key, + * and then signals that the motor should move. + * Sends the event id to the server as a response to the request. + * + * If the key is valid, then `shouldStartOpening` is set to true. + * + * @param payload The pointer to the received data. + * @param length The number of bytes received. + */ + void processReceivedBytes(uint8_t* payload, size_t length); + + /** + * Send a response event to the server and include the next key index. + * + * Sends the event type as three byte. + * @param event The event type + */ + void sendFailureResponse(SesameEvent event); + + /** + * Send a response event to the server and include the next key index. + * + * Sends the event type as three byte. + * @param event The event type + */ + void sendResponse(SesameEvent event, AuthenticatedMessage* message); + +}; \ No newline at end of file diff --git a/include/servo.h b/include/servo.h new file mode 100644 index 0000000..5859068 --- /dev/null +++ b/include/servo.h @@ -0,0 +1,84 @@ +#pragma once + +#include +#include // To control the servo + +/** + * @brief A controller for the button control servo + * + * The controller simply configures the servo for operation, + * and then sets the desired servo value for the 'pressed' and 'released' states. + * The controller requires periodic updating through the `loop()` function + * in order to release the button after the specified time. + * + */ +class ServoController { + +public: + + /** + * @brief Construct a new Servo Controller object + * + * @param timer The timer to use for the servo control + * @param frequency The servo frequency (depending on the model used) + * @param pin The pin where the servo PWM line is connected + */ + ServoController(int timer, int frequency, int pin); + + /** + * @brief Configure the button values + * + * @param openDuration The duration (in ms) for which the button should remain pressed + * @param pressedValue The servo value (in µs) that specifies the 'pressed' state + * @param releasedValue The servo value (in µs) that specifies the 'released' state + */ + void configure(uint32_t openDuration, int pressedValue, int releasedValue); + + /** + * @brief Update the servo state periodically + * + * This function should be periodically called to update the servo state, + * specifically to release the button after the opening time has elapsed. + * + * There is no required interval to call this function, but the accuracy of + * the opening interval is dependent on the calling frequency. + */ + void loop(); + + /** + * Push the door opener button down by moving the servo arm. + */ + void pressButton(); + + /** + * Release the door opener button by moving the servo arm. + */ + void releaseButton(); + +private: + + // Indicator that the door button is pushed + bool buttonIsPressed = false; + + int timer; + + int frequency; + + int pin; + + uint32_t openDuration = 0; + + int pressedValue = 0; + + int releasedValue = 0; + + // The time (in ms since start) when the door opening should end + uint32_t openingEndTime = 0; + + // Servo controller + Servo servo; + + // PWM Module needed for the servo + ESP32PWM pwm; + +}; \ No newline at end of file diff --git a/src/crypto.cpp b/src/crypto.cpp new file mode 100644 index 0000000..6c162ed --- /dev/null +++ b/src/crypto.cpp @@ -0,0 +1,45 @@ +#include "crypto.h" +#include +#include + +bool authenticateData(const uint8_t* data, size_t dataLength, uint8_t* mac, const uint8_t* key, size_t keyLength) { + mbedtls_md_context_t ctx; + mbedtls_md_type_t md_type = MBEDTLS_MD_SHA256; + int result; + + mbedtls_md_init(&ctx); + result = mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(md_type), 1); + if (result) { + return false; + } + result = mbedtls_md_hmac_starts(&ctx, key, keyLength); + if (result) { + return false; + } + result = mbedtls_md_hmac_update(&ctx, data, dataLength); + if (result) { + return false; + } + result = mbedtls_md_hmac_finish(&ctx, mac); + if (result) { + return false; + } + mbedtls_md_free(&ctx); + return true; +} + +bool authenticateMessage(Message* message, uint8_t* mac, const uint8_t* key, size_t keyLength) { + return authenticateData((const uint8_t*) message, MESSAGE_CONTENT_SIZE, mac, key, keyLength); +} + +bool authenticateMessage(AuthenticatedMessage* message, const uint8_t* key, size_t keyLength) { + return authenticateMessage(&message->message, message->mac, key, keyLength); +} + +bool isAuthenticMessage(AuthenticatedMessage* message, const uint8_t* key, size_t keyLength) { + uint8_t mac[SHA256_MAC_SIZE]; + if (!authenticateMessage(&message->message, mac, key, keyLength)) { + return false; + } + return memcmp(mac, message->mac, SHA256_MAC_SIZE) == 0; +} \ No newline at end of file diff --git a/src/fresh.cpp b/src/fresh.cpp new file mode 100644 index 0000000..3d78a43 --- /dev/null +++ b/src/fresh.cpp @@ -0,0 +1,92 @@ +#include "fresh.h" + +#include +#include +#include + +/** + * @brief The size of the message counter in bytes (uint32_t) + */ +#define MESSAGE_COUNTER_SIZE sizeof(uint32_t) + +/** + * @brief The allowed discrepancy between the time of a received message + * and the device time (in seconds) + * + * A stricter (lower) value better prevents against replay attacks, + * but may lead to issues when dealing with slow networks and other + * routing delays. + */ +uint32_t allowedOffset = 60; + +void setMessageTimeAllowedOffset(uint32_t offset) { + allowedOffset = offset; +} + +void configureNTP(int32_t offsetToGMT, int32_t offsetDaylightSavings, const char* serverUrl) { + configTime(offsetToGMT, offsetDaylightSavings, serverUrl); +} + +void printLocalTime() { + struct tm timeinfo; + if (getLocalTime(&timeinfo)) { + Serial.println(&timeinfo, "[INFO] Time is %A, %d. %B %Y %H:%M:%S"); + } else { + Serial.println("[WARN] No local time available"); + } +} + +uint32_t getEpochTime() { + time_t now; + struct tm timeinfo; + if (!getLocalTime(&timeinfo)) { + Serial.println("[WARN] Failed to obtain local time"); + return(0); + } + time(&now); + return now; +} + +bool isMessageTimeAcceptable(uint32_t t) { + uint32_t localTime = getEpochTime(); + if (localTime == 0) { + return false; + } + return t < localTime + allowedOffset && t > localTime - allowedOffset; +} + +void prepareMessageCounterUsage() { + EEPROM.begin(MESSAGE_COUNTER_SIZE); +} + +uint32_t getNextMessageCounter() { + uint32_t counter = (uint32_t) EEPROM.read(0) << 24; + counter += (uint32_t) EEPROM.read(1) << 16; + counter += (uint32_t) EEPROM.read(2) << 8; + counter += (uint32_t) EEPROM.read(3); + return counter; +} + +void printMessageCounter() { + Serial.printf("[INFO] Next message number: %d\n", getNextMessageCounter()); +} + +bool isMessageCounterValid(uint32_t counter) { + return counter >= getNextMessageCounter(); +} + +void didUseMessageCounter(uint32_t counter) { + // Store the next counter, so that resetting starts at 0 + counter += 1; + EEPROM.write(0, (counter >> 24) & 0xFF); + EEPROM.write(1, (counter >> 16) & 0xFF); + EEPROM.write(2, (counter >> 8) & 0xFF); + EEPROM.write(3, counter & 0xFF); + + EEPROM.commit(); +} + +void resetMessageCounter() { + didUseMessageCounter(0); + Serial.println("[WARN] Message counter reset"); +} diff --git a/src/main.cpp b/src/main.cpp index ce3c56f..743eee4 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -7,11 +7,12 @@ */ #include #include -#include -#include -#include -#include // To mark used keys as expired -#include // To control the servo + +#include "crypto.h" +#include "fresh.h" +#include "message.h" +#include "server.h" +#include "servo.h" // TODO: // - Handle WiFi disconnect @@ -20,41 +21,34 @@ /* Settings */ -#define SERIAL_BAUD_RATE 115200 +constexpr uint32_t serialBaudRate = 115200; + +constexpr size_t keySize = 32; +constexpr const uint8_t remoteKey[keySize] = { 1, 2, 3}; +constexpr const uint8_t localKey[keySize] = { 1, 2, 3}; // The WiFi network to connect to -constexpr const char* WIFI_SSID = "MyNetwork"; -constexpr const char* WIFI_PWD = "MyPassword"; +constexpr const char* wifiSSID = "MyNetwork"; +constexpr const char* wifiPassword = "MyPassword"; +constexpr uint32_t wifiReconnectInterval = 10000; // The remote server to connect to -constexpr const char* SERVER_URL = "christophhagen.de"; -constexpr const int SERVER_PORT = 443; -constexpr const char* SERVER_PATH = "/sesame/listen"; -constexpr const char* SERVER_PSK = "access token"; -#define USE_SSL // Use SSL for the Websocket connection to the server - -// The interval to attempt to reconnect the socket to the server -constexpr unsigned long SOCKET_RECONNECT_TIME = 5000; - -// Crypto setting for the security of keys -constexpr uint8_t KEY_STRENGTH = 128; -constexpr uint8_t KEY_BYTE_COUNT = KEY_STRENGTH / 8; - -// The keys defined to allow opening -// Once a key is used, the corresponding byte in EEPROM is set to 1, -// to prevent it from being used again. -const uint8_t keys[][KEY_BYTE_COUNT] = { - {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, -}; -const uint16_t KEY_COUNT = sizeof keys / KEY_BYTE_COUNT; +constexpr const char* serverUrl = "mydomain.com"; +constexpr const int serverPort = 443; +constexpr const char* serverPath = "/sesame/listen"; +constexpr const char* serverAccessKey = "MyAccessToken"; +/* Time */ +constexpr const char* ntpServerUrl = "pool.ntp.org"; +constexpr int32_t timeOffsetToGMT = 3600; +constexpr int32_t timeOffsetDaylightSavings = 3600; /* Servo */ // Servo is Emax ES08MA II // The time (in ms) to keep the door button pressed -constexpr uint32_t OPENING_DURATION = 2000; +constexpr uint32_t lockOpeningDuration = 2000; // The timer to use to control the servo constexpr int pwmTimer = 0; @@ -65,141 +59,52 @@ constexpr int servoPin = 14; // The Emax is a standard 50 Hz servo constexpr int servoFrequency = 50; -// The microseconds to set the servo to the pressed state -constexpr int PRESS_STATE_INTERVAL = 1600; - -// The microseconds to set the servo to the released state -constexpr int RELEASE_STATE_INTERVAL = 1520; +// The microseconds to set the servo to the pressed and released states +constexpr int servoPressedState = 1600; +constexpr int servoReleasedState = 1520; /* Global variables */ -// Servo controller -Servo servo; +ServerConnection server(serverUrl, serverPort, serverPath); -// PWM Module needed for the servo -ESP32PWM pwm; +ServoController servo(pwmTimer, servoFrequency, servoPin); -// WiFi module to connect to the network -WiFiMulti WiFiMulti; - -// WebSocket to connect to the control server -WebSocketsClient webSocket; - -// Indicator that the socket is connected. -bool socketIsConnected = false; - -// The index of the next valid key, which is the next key after the highest used key. -// If the index is larger or equal to the total key count, then no usable keys exist. -uint16_t nextKeyIndex = KEY_COUNT; - -// Flag to signal that the door button should be pressed -bool shouldStartOpening = false; - -// Indicator that the door button is pushed -bool buttonIsPressed = false; - -// The time (in ms since start) when the door opening should end -uint32_t openingEndTime = 0; - - -/* Events */ - -/** - * An event occuring due to a server request - */ -enum class SesameEvent { - TextReceived = 1, - UnexpectedSocketEvent = 2, - InvalidPayloadSize = 3, - InvalidKeyIndex = 4, - InvalidKey = 5, - KeyAlreadyUsed = 6, - KeyWasSkipped = 7, - KeyAccepted = 8, -}; - - -/* Key management */ - -/** - * Get the index of the next key which wasn't used yet - * - * A key is unused, if the EEPROM corresponding to the key is zero. - * If all keys have been used, returns KEY_COUNT - */ -uint16_t indexOfNextUsableKey(); - -/** - * Checks if a key was already marked as used. - * - * A key was used, if the EEPROM byte corresponding to the key is non-zero. - * @param keyIndex The index of the key in the 'keys' array. - * @return 0, if the key is unused, else non-zero. - */ -bool keyWasAlreadyUsed(uint16_t keyIndex); - -/** - * Marks a key as used - * - * Sets the corresponding EEPROM byte to non-zero. - * @param keyIndex The index of the key in the 'keys' array. - */ -void markKeyUsed(uint16_t keyIndex); - -/** - * Marks all keys unused. - * - * Sets the EEPROM data for each key index to zero. - * - * WARNING: Only to be used when a new set of keys has been set. - * Otherwise replay attacks are possible with observed old keys. - */ -void markAllKeysUnused(); - -/** - * Compares two keys in constant time - * @return 1, if the keys are equal, else 0 - */ -bool keysAreEqual(const uint8_t* key1, const uint8_t* key2); - - -/* Servo management */ - -/** - * Push the door opener button down by moving the servo arm. - */ -void pressButton(); - -/** - * Release the door opener button by moving the servo arm. - */ -void releaseButton(); +/* Event callbacks */ +SesameEvent handleReceivedMessage(AuthenticatedMessage* payload, AuthenticatedMessage* response); /* Logic */ -/** - * Send a response event to the server and include the next key index. - * - * Sends the event type as three byte. - * @param event The event type - */ -void sendResponse(SesameEvent event) { - uint8_t response[3]; - response[0] = static_cast(event); - response[1] = nextKeyIndex >> 8; - response[2] = nextKeyIndex & 0xFF; - webSocket.sendBIN(response, 3); - //webSocket.sendTXT(text); - Serial.printf("[INFO] Event %d\n", response[0]); +void setup() { + Serial.begin(serialBaudRate); + Serial.setDebugOutput(true); + Serial.println("[INFO] Device started"); + + servo.configure(lockOpeningDuration, servoPressedState, servoReleasedState); + Serial.println("[INFO] Servo configured"); + + prepareMessageCounterUsage(); + printMessageCounter(); + + Serial.printf("[INFO] Connecting to WiFi '%s'\n", wifiSSID); + WiFi.begin(wifiSSID, wifiPassword); + while(WiFi.status() != WL_CONNECTED) { + delay(100); + } + Serial.println("[INFO] WiFi connected"); + + configureNTP(timeOffsetToGMT, timeOffsetDaylightSavings, ntpServerUrl); + printLocalTime(); + + server.onMessage(handleReceivedMessage); + Serial.printf("[INFO] Opening SSL socket %s%s on port %d\n", serverUrl, serverPath, serverPort); + server.connectSSL(serverAccessKey); } -/** - * Send the pre-shared key to the server to complete the socket connection. - */ -void authenticateDevice() { - webSocket.sendTXT(SERVER_PSK); +void loop() { + server.loop(); + servo.loop(); } /** @@ -212,191 +117,26 @@ void authenticateDevice() { * @param length The number of bytes received. * @return The event to signal to the server. */ -SesameEvent handleReceivedRequest(const uint8_t* payload, size_t length) { - if (length != KEY_BYTE_COUNT + 2) { - return SesameEvent::InvalidPayloadSize; +SesameEvent handleReceivedMessage(AuthenticatedMessage* message, AuthenticatedMessage* response) { + + if (!isMessageCounterValid(message->message.id)) { + return SesameEvent::MessageCounterInvalid; } - Serial.println("Key received"); - uint16_t keyIndex = ((uint16_t) payload[0] << 8) + payload[1]; - if (keyIndex >= KEY_COUNT) { - return SesameEvent::InvalidKeyIndex; + if (!isMessageTimeAcceptable(message->message.time)) { + return SesameEvent::MessageTimeMismatch; } - if (!keysAreEqual(payload + 2, keys[keyIndex])) { - return SesameEvent::InvalidKey; + if (!isAuthenticMessage(message, remoteKey, keySize)) { + return SesameEvent::MessageAuthenticationFailed; } - if (keyWasAlreadyUsed(keyIndex)) { - return SesameEvent::KeyAlreadyUsed; + + response->message.time = getEpochTime(); + response->message.id = getNextMessageCounter(); + if (!authenticateMessage(response, localKey, keySize)) { + return SesameEvent::MessageAuthenticationFailed; } - if (nextKeyIndex > keyIndex) { - return SesameEvent::KeyWasSkipped; - } - markKeyUsed(keyIndex); - nextKeyIndex = keyIndex + 1; + // Move servo - shouldStartOpening = true; - Serial.printf("[Info] Used key %d\n", keyIndex); - return SesameEvent::KeyAccepted; -} - -/** - * Process received binary data. - * - * Checks whether the received data is a valid and unused key, - * and then signals that the motor should move. - * Sends the event id to the server as a response to the request. - * - * If the key is valid, then `shouldStartOpening` is set to true. - * - * @param payload The pointer to the received data. - * @param length The number of bytes received. - */ -void processReceivedBytes(uint8_t * payload, size_t length) { - SesameEvent event = handleReceivedRequest(payload, length); - sendResponse(event); -} - -/** - * Callback for WebSocket events. - * - * Updates the connection state and processes received keys. - * - * @param payload The pointer to received data - * @param length The number of bytes received - */ -void webSocketEvent(WStype_t type, uint8_t * payload, size_t length) { - switch(type) { - case WStype_DISCONNECTED: - socketIsConnected = false; - Serial.println("[INFO] Socket disconnected."); - break; - case WStype_CONNECTED: - socketIsConnected = true; - authenticateDevice(); - Serial.printf("[INFO] Socket connected to url: %s\n", payload); - break; - case WStype_TEXT: - sendResponse(SesameEvent::TextReceived); - break; - case WStype_BIN: - processReceivedBytes(payload, length); - break; - case WStype_PONG: - case WStype_PING: - case WStype_ERROR: - case WStype_FRAGMENT_TEXT_START: - case WStype_FRAGMENT_BIN_START: - case WStype_FRAGMENT: - case WStype_FRAGMENT_FIN: - sendResponse(SesameEvent::UnexpectedSocketEvent); - break; - } -} - -void setup() { - Serial.begin(115200); - Serial.setDebugOutput(true); - Serial.println("[INFO] Device started"); - - //markAllKeysUnused(); - //Serial.println("[WARN] All keys reset"); - - ESP32PWM::allocateTimer(pwmTimer); - servo.setPeriodHertz(servoFrequency); - servo.attach(servoPin); - releaseButton(); - Serial.println("[INFO] Servo configured"); - - Serial.printf("[INFO] Key security: %d bit (%d byte keys)\n", KEY_STRENGTH, KEY_BYTE_COUNT); - nextKeyIndex = indexOfNextUsableKey(); - uint8_t percentage = (KEY_COUNT - nextKeyIndex) * 100 / KEY_COUNT; - Serial.printf("[INFO] %d of %d keys remaining (%d %%)\n", KEY_COUNT - nextKeyIndex, KEY_COUNT, percentage); - Serial.printf("[INFO] Connecting to WiFi '%s'\n", WIFI_SSID); - WiFiMulti.addAP(WIFI_SSID, WIFI_PWD); - - while(WiFiMulti.run() != WL_CONNECTED) { - delay(100); - } - - Serial.println("[INFO] WiFi connected"); - - #ifdef USE_SSL - Serial.printf("[INFO] Opening SSL socket %s%s on port %d\n", SERVER_URL, SERVER_PATH, SERVER_PORT); - webSocket.beginSSL(SERVER_URL, SERVER_PORT, SERVER_PATH); - #else - Serial.printf("[INFO] Opening insecure socket %s%s on port %d\n", SERVER_URL, SERVER_PATH, SERVER_PORT); - webSocket.begin(SERVER_URL, SERVER_PORT, SERVER_PATH); - #endif - webSocket.onEvent(webSocketEvent); - - // try again every 5000 ms if connection has failed - webSocket.setReconnectInterval(SOCKET_RECONNECT_TIME); - - // Creates an EEPROM fake in RAM, which is committed to flash - // The RAM data can be released by calling `EEPROM.end()` - EEPROM.begin(KEY_COUNT); -} - -void loop() { - webSocket.loop(); - if (shouldStartOpening) { - shouldStartOpening = false; - openingEndTime = millis() + OPENING_DURATION; - pressButton(); - } - if (buttonIsPressed && millis() > openingEndTime) { - releaseButton(); - } -} - -/* Key management */ - -uint16_t indexOfNextUsableKey() { - // Find the highest key which was previously used - for (uint16_t keyIndex = 0; keyIndex < KEY_COUNT; keyIndex += 1) { - if (EEPROM.read(KEY_COUNT - keyIndex)) { - // The following key is the next unused - return keyIndex + 1; - } - } - // No key previously used - return 0; -} - -bool keyWasAlreadyUsed(uint16_t keyIndex) { - return EEPROM.read(keyIndex) > 0; -} - -void markKeyUsed(uint16_t keyIndex) { - EEPROM.write(keyIndex, 1); - EEPROM.commit(); -} - -void markAllKeysUnused() { - for (uint16_t keyIndex = 0; keyIndex < KEY_COUNT; keyIndex += 1) { - EEPROM.write(keyIndex, 0); - } - EEPROM.commit(); -} - -bool keysAreEqual(const uint8_t* key1, const uint8_t* key2) { - uint8_t result = 0; - for (uint8_t i = 0; i < KEY_BYTE_COUNT; i += 1) { - result |= key1[i] ^ key2[i]; - } - if (result) { - return false; - } - return true; -} - -/* Servo management */ - -void pressButton() { - servo.write(PRESS_STATE_INTERVAL); - buttonIsPressed = true; -} - -void releaseButton() { - servo.write(RELEASE_STATE_INTERVAL); - buttonIsPressed = false; + servo.pressButton(); + Serial.printf("[Info] Accepted message %d\n", message->message.id); + return SesameEvent::MessageAccepted; } diff --git a/src/server.cpp b/src/server.cpp new file mode 100644 index 0000000..f80fd3b --- /dev/null +++ b/src/server.cpp @@ -0,0 +1,96 @@ +#include "server.h" + +ServerConnection::ServerConnection(const char* url, int port, const char* path) : + url(url), port(port), path(path) { + +} + +void ServerConnection::connect(const char* key, uint32_t reconnectTime) { + webSocket.begin(url, port, path); + registerEventCallback(); + reconnectAfter(reconnectTime); +} + +void ServerConnection::connectSSL(const char* key, uint32_t reconnectTime) { + this->key = key; + webSocket.beginSSL(url, port, path); + registerEventCallback(); + reconnectAfter(reconnectTime); +} + +void ServerConnection::loop() { + webSocket.loop(); +} + +void ServerConnection::onMessage(MessageCallback callback) { + messageCallback = callback; +} + +void ServerConnection::reconnectAfter(uint32_t reconnectTime) { + webSocket.setReconnectInterval(reconnectTime); +} + +void ServerConnection::registerEventCallback() { + std::function f = [this](WStype_t type, uint8_t *payload, size_t length) { + this->webSocketEventHandler(type, payload, length); + }; + webSocket.onEvent(f); +} + +void ServerConnection::webSocketEventHandler(WStype_t type, uint8_t * payload, size_t length) { +switch(type) { + case WStype_DISCONNECTED: + socketIsConnected = false; + Serial.println("[INFO] Socket disconnected."); + break; + case WStype_CONNECTED: + socketIsConnected = true; + webSocket.sendTXT(key); + Serial.printf("[INFO] Socket connected to url: %s\n", payload); + break; + case WStype_TEXT: + sendFailureResponse(SesameEvent::TextReceived); + break; + case WStype_BIN: + processReceivedBytes(payload, length); + break; + case WStype_PONG: + case WStype_PING: + case WStype_ERROR: + case WStype_FRAGMENT_TEXT_START: + case WStype_FRAGMENT_BIN_START: + case WStype_FRAGMENT: + case WStype_FRAGMENT_FIN: + sendFailureResponse(SesameEvent::UnexpectedSocketEvent); + break; + } +} + +void ServerConnection::processReceivedBytes(uint8_t* payload, size_t length) { + if (length != AUTHENTICATED_MESSAGE_SIZE) { + sendFailureResponse(SesameEvent::InvalidPayloadSize); + return; + } + AuthenticatedMessage* message = (AuthenticatedMessage*) payload; + if (messageCallback == NULL) { + sendFailureResponse(SesameEvent::MessageAuthenticationFailed); + return; + } + AuthenticatedMessage responseMessage; + SesameEvent event = messageCallback(message, &responseMessage); + sendResponse(event, &responseMessage); +} + +void ServerConnection::sendFailureResponse(SesameEvent event) { + uint8_t response = static_cast(event); + webSocket.sendBIN(&response, 1); + Serial.printf("[INFO] Socket failure %d\n", response); +} + +void ServerConnection::sendResponse(SesameEvent event, AuthenticatedMessage* message) { + uint8_t response[AUTHENTICATED_MESSAGE_SIZE+1]; + response[0] = static_cast(event); + memcpy(response+1, (uint8_t*) message, AUTHENTICATED_MESSAGE_SIZE); + webSocket.sendBIN(response, AUTHENTICATED_MESSAGE_SIZE+1); + Serial.printf("[INFO] Socket response %d\n", response[0]); +} \ No newline at end of file diff --git a/src/servo.cpp b/src/servo.cpp new file mode 100644 index 0000000..5d7cc20 --- /dev/null +++ b/src/servo.cpp @@ -0,0 +1,37 @@ +#include "servo.h" + +#include // For `millis()` + +ServoController::ServoController(int timer, int frequency, int pin) + : timer(timer), frequency(frequency), pin(pin) { + +} + +void ServoController::configure(uint32_t openDuration, int pressedValue, int releasedValue) { + this->openDuration = openDuration; + this->pressedValue = pressedValue; + this->releasedValue = releasedValue; + ESP32PWM::allocateTimer(timer); + servo.setPeriodHertz(frequency); + servo.attach(pin); + releaseButton(); +} + +void ServoController::pressButton() { + servo.write(pressedValue); + buttonIsPressed = true; + openingEndTime = millis() + openDuration; +} + +void ServoController::releaseButton() { + servo.write(releasedValue); + buttonIsPressed = false; +} + +void ServoController::loop() { + if (buttonIsPressed && millis() > openingEndTime) { + releaseButton(); + } +} + +