Compare commits

..

No commits in common. "e84e3885212e041e34d0335d20c9d3c3547f9add" and "e631ea0a2072d8be9e3e02d1643909e74948da3d" have entirely different histories.

13 changed files with 497 additions and 697 deletions

View File

@ -1,79 +0,0 @@
#pragma once
#include "server.h"
#include "servo.h"
#include "message.h"
#include "storage.h"
#include "fresh.h"
#include <ESPAsyncWebServer.h>
struct WifiConfiguration {
// The WiFi network to connect to
const char* ssid;
// The WiFi password to connect to the above network
const char* password;
// The name of the device on the network
const char* networkName;
// The interval to reconnect to WiFi if the connection is broken
uint32_t reconnectInterval;
};
struct KeyConfiguration {
const uint8_t* remoteKey;
const uint8_t* localKey;
};
class SesameController: public ServerConnectionCallbacks {
public:
SesameController(uint16_t localWebServerPort, uint8_t remoteDeviceCount);
void configure(ServoConfiguration servoConfig, ServerConfiguration serverConfig, TimeConfiguration timeConfig, WifiConfiguration wifiConfig, KeyConfiguration keyConfig);
void loop(uint32_t millis);
private:
ServerConnection server;
ServoController servo;
AsyncWebServer localWebServer;
TimeCheck timeCheck;
Storage storage;
WifiConfiguration wifiConfig;
KeyConfiguration keyConfig;
bool isReconnecting = false;
// The buffer to hold a received message while it is read
uint8_t receivedMessageBuffer[AUTHENTICATED_MESSAGE_SIZE];
// The buffer to hold a response while it is sent
uint8_t responseBuffer[AUTHENTICATED_MESSAGE_SIZE+1];
SesameEvent* responseStatus;
AuthenticatedMessage* responseMessage;
uint16_t responseSize = 0;
void ensureWiFiConnection(uint32_t time);
void ensureWebSocketConnection();
void handleLocalMessage(AsyncWebServerRequest *request);
// Based on https://stackoverflow.com/a/23898449/266720
bool convertHexMessageToBinary(const char* str);
void handleServerMessage(uint8_t* payload, size_t length);
void sendServerError(SesameEvent event);
void processMessage(AuthenticatedMessage* message);
SesameEvent verifyAndProcessReceivedMessage(AuthenticatedMessage* message);
uint16_t prepareResponseBuffer(SesameEvent event, uint8_t deviceId = 0);
void sendPreparedLocalResponse(AsyncWebServerRequest *request);
void sendPreparedServerResponse();
};

View File

@ -52,7 +52,11 @@ constexpr uint32_t wifiReconnectInterval = 10000;
/* Local server */ /* Local server */
// The port for the local server to directly receive messages over WiFi // The port for the local server to directly receive messages over WiFi
constexpr uint16_t localWebServerPort = 80; constexpr uint16_t localPort = 80;
// The url parameter to send the message to the local server
constexpr char messageUrlParameter[] = "m";
/* Server */ /* Server */

View File

@ -1,67 +1,49 @@
#pragma once #pragma once
#include <stdint.h> #include <stdint.h>
#include "config.h"
struct TimeConfiguration { /**
* @brief The size of the message counter in bytes (uint32_t)
/**
* @brief The timezone offset in seconds
*/ */
int32_t offsetToGMT; #define MESSAGE_COUNTER_SIZE sizeof(uint32_t)
/** /**
* @brief The daylight savings offset in seconds * @brief Configure an NTP server to get the current time
*/
int32_t offsetDaylightSavings;
/**
* @brief The url of the NTP server
*/
const char* ntpServerUrl;
/**
* @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, * @param offsetToGMT The timezone offset in seconds
* but may lead to issues when dealing with slow networks and other * @param offsetDaylightSavings The daylight savings offset in seconds
* routing delays. * @param serverUrl The url of the NTP server
*/ */
uint32_t allowedTimeOffset; void configureNTP(int32_t offsetToGMT, int32_t offsetDaylightSavings, const char* serverUrl);
};
class TimeCheck { /**
public:
/**
* @brief Create a time checker instance
*/
TimeCheck();
/**
* @brief Set the configuration
*/
void configure(TimeConfiguration configuration);
/**
* @brief Configure the NTP server to get the current time
*/
void startNTP();
/**
* @brief Print the current time to the serial output * @brief Print the current time to the serial output
* *
* The time must be initialized by calling `configureNTP()` before use. * The time must be initialized by calling `configureNTP()` before use.
*/ */
void printLocalTime(); void printLocalTime();
/** /**
* Gets the current epoch time * Gets the current epoch time
*/ */
uint32_t getEpochTime(); 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. * @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, * The timestamp is used to ensure 'freshness' of the messages,
@ -72,9 +54,64 @@ public:
* @return true The time is within the acceptable offset of the local time * @return true The time is within the acceptable offset of the local time
* @return false The message time is invalid * @return false The message time is invalid
*/ */
bool isMessageTimeAcceptable(uint32_t messageTime); bool isMessageTimeAcceptable(uint32_t messageTime);
private: /**
* @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();
TimeConfiguration config; /**
}; * @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(uint8_t deviceId);
/**
* @brief Print info about the current message counter to the serial output
*
*/
void printMessageCounters();
bool isDeviceIdValid(uint8_t deviceId);
/**
* @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, uint8_t deviceId);
/**
* @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, uint8_t deviceId);
/**
* @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 resetMessageCounters();

View File

@ -1,7 +1,6 @@
#pragma once #pragma once
#include <stdint.h> #include "stdint.h"
#include <stddef.h>
/** /**
* @brief The size of a message authentication code * @brief The size of a message authentication code
@ -53,8 +52,6 @@ typedef struct {
} Message; } Message;
constexpr size_t messageCounterSize = sizeof(uint32_t);
/** /**
* @brief An authenticated message by the mobile device to command unlocking. * @brief An authenticated message by the mobile device to command unlocking.
* *
@ -90,9 +87,9 @@ typedef struct {
} AuthenticatedMessage; } AuthenticatedMessage;
#pragma pack(pop) #pragma pack(pop)
constexpr int MESSAGE_CONTENT_SIZE = sizeof(Message); #define MESSAGE_CONTENT_SIZE sizeof(Message)
constexpr int AUTHENTICATED_MESSAGE_SIZE = sizeof(AuthenticatedMessage); #define AUTHENTICATED_MESSAGE_SIZE sizeof(AuthenticatedMessage)
/** /**
* An event signaled from the device * An event signaled from the device
@ -100,27 +97,19 @@ constexpr int AUTHENTICATED_MESSAGE_SIZE = sizeof(AuthenticatedMessage);
enum class SesameEvent { enum class SesameEvent {
TextReceived = 1, TextReceived = 1,
UnexpectedSocketEvent = 2, UnexpectedSocketEvent = 2,
InvalidMessageSize = 3, InvalidMessageData = 3,
MessageAuthenticationFailed = 4, MessageAuthenticationFailed = 4,
MessageTimeMismatch = 5, MessageTimeMismatch = 5,
MessageCounterInvalid = 6, MessageCounterInvalid = 6,
MessageAccepted = 7, MessageAccepted = 7,
MessageDeviceInvalid = 8, MessageDeviceInvalid = 8,
InvalidUrlParameter = 9,
InvalidResponseAuthentication = 10,
DeviceSetupIncomplete = 11,
}; };
/** /**
* @brief A callback for messages received over the socket * @brief A callback for messages received over the socket
* *
* The first parameter is a pointer to the byte buffer. * The first parameter is the received message.
* The second parameter indicates the number of received bytes. * The second parameter is the response to the remote.
* The return value is the type of event to respond with.
*/ */
typedef void (*MessageCallback)(uint8_t* payload, size_t length); typedef SesameEvent (*MessageCallback)(AuthenticatedMessage*, AuthenticatedMessage*);
/**
* @brief A callback for socket errors
*/
typedef void (*ErrorCallback)(SesameEvent event);

View File

@ -6,81 +6,42 @@
#include <WiFiClientSecure.h> #include <WiFiClientSecure.h>
#include <WebSocketsClient.h> #include <WebSocketsClient.h>
struct ServerConfiguration {
/**
* @brief The url of the remote server to connect to
*/
const char* url;
/**
* @brief The server port
*/
int port;
/**
* @brief The path on the server
*/
const char* path;
/**
* @brief The authentication key for the server
*/
const char* key;
uint32_t reconnectTime;
};
class ServerConnectionCallbacks {
public:
virtual void sendServerError(SesameEvent event) = 0;
virtual void handleServerMessage(uint8_t* payload, size_t length) = 0;
};
class ServerConnection { class ServerConnection {
public: public:
ServerConnection(); ServerConnection(const char* url, int port, const char* path);
/** void connect(const char* key, uint32_t reconnectTime = 5000);
* @brief Set the configuration and the callback handler
*
* @param callback The handler to handle messages and errors
*/
void configure(ServerConfiguration configuration, ServerConnectionCallbacks* callbacks);
void connect(); void connectSSL(const char* key, uint32_t reconnectTime = 5000);
void loop(); void loop();
/** void onMessage(MessageCallback callback);
* @brief Send a response message over the socket
*
* @param buffer The data buffer
* @param length The number of bytes to send
*/
void sendResponse(uint8_t* buffer, uint16_t length);
bool isSocketConnected() {
return socketIsConnected;
}
private:
ServerConfiguration configuration;
// Indicator that the socket is connected. // Indicator that the socket is connected.
bool socketIsConnected = false; bool socketIsConnected = false;
ServerConnectionCallbacks* controller = NULL; private:
const char* url;
int port;
const char* path;
const char* key = NULL;
MessageCallback messageCallback = NULL;
// WebSocket to connect to the control server // WebSocket to connect to the control server
WebSocketsClient webSocket; WebSocketsClient webSocket;
void reconnectAfter(uint32_t reconnectTime);
void registerEventCallback();
/** /**
* Callback for WebSocket events. * Callback for WebSocket events.
* *
@ -90,4 +51,35 @@ private:
* @param length The number of bytes received * @param length The number of bytes received
*/ */
void webSocketEventHandler(WStype_t type, uint8_t * payload, size_t length); 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);
}; };

View File

@ -3,40 +3,6 @@
#include <stdint.h> #include <stdint.h>
#include <ESP32Servo.h> // To control the servo #include <ESP32Servo.h> // To control the servo
struct ServoConfiguration {
/**
* @brief The timer to use for the servo control
* number 0-3 indicating which timer to allocate in this library
*/
int pwmTimer;
/**
* @brief The servo frequency (depending on the model used)
*/
int pwmFrequency;
/**
* @brief The pin where the servo PWM line is connected
*/
int pin;
/**
* @brief The duration (in ms) for which the button should remain pressed
*/
uint32_t openDuration;
/**
* @brief The servo value (in µs) that specifies the 'pressed' state
*/
int pressedValue;
/**
* @brief The servo value (in µs) that specifies the 'released' state
*/
int releasedValue;
};
/** /**
* @brief A controller for the button control servo * @brief A controller for the button control servo
* *
@ -51,16 +17,22 @@ class ServoController {
public: public:
/** /**
* @brief Construct a new servo controller * @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(); ServoController(int timer, int frequency, int pin);
/** /**
* @brief Configure the servo * @brief Configure the button values
* *
* @param The configuration for the servo * @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(ServoConfiguration configuration); void configure(uint32_t openDuration, int pressedValue, int releasedValue);
/** /**
* @brief Update the servo state periodically * @brief Update the servo state periodically
@ -71,7 +43,7 @@ public:
* There is no required interval to call this function, but the accuracy of * There is no required interval to call this function, but the accuracy of
* the opening interval is dependent on the calling frequency. * the opening interval is dependent on the calling frequency.
*/ */
void loop(uint32_t millis); void loop();
/** /**
* Push the door opener button down by moving the servo arm. * Push the door opener button down by moving the servo arm.
@ -88,6 +60,12 @@ private:
// Indicator that the door button is pushed // Indicator that the door button is pushed
bool buttonIsPressed = false; bool buttonIsPressed = false;
int timer;
int frequency;
int pin;
uint32_t openDuration = 0; uint32_t openDuration = 0;
int pressedValue = 0; int pressedValue = 0;

View File

@ -1,83 +0,0 @@
#pragma once
#include <stdint.h>
class Storage {
public:
Storage(uint8_t remoteDeviceCount) : remoteDeviceCount(remoteDeviceCount) { };
/**
* @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 configure();
/**
* @brief Check if a device ID is allowed
*
* @param deviceId The ID to check
* @return true The id is valid
* @return false The id is invalid
*/
bool isDeviceIdValid(uint8_t deviceId);
/**
* @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, uint8_t deviceId);
/**
* @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, uint8_t deviceId);
/**
* @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(uint8_t deviceId);
/**
* @brief Print info about the current message counter to the serial output
*
*/
void printMessageCounters();
/**
* @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 resetMessageCounters();
private:
uint8_t remoteDeviceCount;
void setMessageCounter(uint32_t counter, uint8_t deviceId);
};

View File

@ -1,218 +0,0 @@
#include "controller.h"
#include "crypto.h"
#include "config.h"
#include <WiFi.h>
// The url parameter to send the message to the local server
constexpr char messageUrlParameter[] = "m";
SesameController::SesameController(uint16_t localWebServerPort, uint8_t remoteDeviceCount) :
storage(remoteDeviceCount), localWebServer(localWebServerPort) {
// Set up response buffer
responseStatus = (SesameEvent*) responseBuffer;
responseMessage = (AuthenticatedMessage*) (responseBuffer + 1);
}
void SesameController::configure(ServoConfiguration servoConfig, ServerConfiguration serverConfig, TimeConfiguration timeConfig, WifiConfiguration wifiConfig, KeyConfiguration keyConfig) {
this->wifiConfig = wifiConfig;
this->keyConfig = keyConfig;
// Prepare EEPROM for reading and writing
storage.configure();
Serial.println("[INFO] Storage configured");
servo.configure(servoConfig);
Serial.println("[INFO] Servo configured");
// Direct messages and errors over the websocket to the controller
server.configure(serverConfig, this);
Serial.println("[INFO] Server connection configured");
timeCheck.configure(timeConfig);
// Direct messages from the local web server to the controller
localWebServer.on("/message", HTTP_POST, [this] (AsyncWebServerRequest *request) {
this->handleLocalMessage(request);
this->sendPreparedLocalResponse(request);
});
Serial.println("[INFO] Local web server configured");
//storage.resetMessageCounters();
storage.printMessageCounters();
}
void SesameController::loop(uint32_t millis) {
server.loop();
servo.loop(millis);
ensureWiFiConnection(millis);
ensureWebSocketConnection();
}
// MARK: Local
void
SesameController::handleLocalMessage(AsyncWebServerRequest *request) {
if (!request->hasParam(messageUrlParameter)) {
Serial.println("Missing url parameter");
prepareResponseBuffer(SesameEvent::InvalidUrlParameter);
return;
}
String encoded = request->getParam(messageUrlParameter)->value();
if (!convertHexMessageToBinary(encoded.c_str())) {
Serial.println("Invalid hex encoding");
prepareResponseBuffer(SesameEvent::InvalidMessageSize);
return;
}
processMessage((AuthenticatedMessage*) receivedMessageBuffer);
}
void SesameController::sendPreparedLocalResponse(AsyncWebServerRequest *request) {
request->send_P(200, "application/octet-stream", responseBuffer, responseSize);
Serial.printf("[INFO] Local response %u (%u bytes)\n", responseBuffer[0], responseSize);
}
// MARK: Server
void SesameController::sendServerError(SesameEvent event) {
prepareResponseBuffer(event);
sendPreparedServerResponse();
}
void SesameController::handleServerMessage(uint8_t* payload, size_t length) {
if (length != AUTHENTICATED_MESSAGE_SIZE) {
prepareResponseBuffer(SesameEvent::InvalidMessageSize);
return;
}
processMessage((AuthenticatedMessage*) payload);
sendPreparedServerResponse();
}
void SesameController::sendPreparedServerResponse() {
server.sendResponse(responseBuffer, responseSize);
Serial.printf("[INFO] Server response %u (%u bytes)\n", responseBuffer[0], responseSize);
}
// MARK: Message handling
void SesameController::processMessage(AuthenticatedMessage* message) {
SesameEvent event = verifyAndProcessReceivedMessage(message);
prepareResponseBuffer(event, message->message.device);
}
/**
* Process a received message.
*
* Checks whether the received data is a valid,
* and then signals that the motor should move.
*
* @param message The message received from the remote
* @return The response to signal to the server.
*/
SesameEvent SesameController::verifyAndProcessReceivedMessage(AuthenticatedMessage* message) {
if (!isAuthenticMessage(message, keyConfig.remoteKey, keySize)) {
return SesameEvent::MessageAuthenticationFailed;
}
if (!storage.isDeviceIdValid(message->message.device)) {
return SesameEvent::MessageDeviceInvalid;
}
if (!storage.isMessageCounterValid(message->message.id, message->message.device)) {
return SesameEvent::MessageCounterInvalid;
}
if (!timeCheck.isMessageTimeAcceptable(message->message.time)) {
return SesameEvent::MessageTimeMismatch;
}
storage.didUseMessageCounter(message->message.id, message->message.device);
// Move servo
servo.pressButton();
Serial.printf("[Info] Accepted message %d\n", message->message.id);
return SesameEvent::MessageAccepted;
}
bool allowMessageResponse(SesameEvent event) {
switch (event) {
case SesameEvent::MessageTimeMismatch:
case SesameEvent::MessageCounterInvalid:
case SesameEvent::MessageAccepted:
case SesameEvent::MessageDeviceInvalid:
return true;
default:
return false;
}
}
uint16_t SesameController::prepareResponseBuffer(SesameEvent event, uint8_t deviceId) {
*responseStatus = event;
if (!allowMessageResponse(event)) {
return 1;
}
responseMessage->message.time = timeCheck.getEpochTime();
responseMessage->message.id = storage.getNextMessageCounter(deviceId);
responseMessage->message.device = deviceId;
if (!authenticateMessage(responseMessage, keyConfig.localKey, keySize)) {
*responseStatus = SesameEvent::InvalidResponseAuthentication;
return 1;
}
return 1 + AUTHENTICATED_MESSAGE_SIZE;
}
// MARK: Reconnecting
void SesameController::ensureWiFiConnection(uint32_t millis) {
static uint32_t nextWifiReconnect = 0;
// Reconnect to WiFi
if(millis > nextWifiReconnect && WiFi.status() != WL_CONNECTED) {
Serial.println("[INFO] Reconnecting WiFi...");
WiFi.setHostname(wifiConfig.networkName);
WiFi.begin(wifiConfig.ssid, wifiConfig.password);
isReconnecting = true;
nextWifiReconnect = millis + wifiConfig.reconnectInterval;
}
}
void SesameController::ensureWebSocketConnection() {
if (isReconnecting && WiFi.status() == WL_CONNECTED) {
isReconnecting = false;
Serial.print("WiFi IP address: ");
Serial.println(WiFi.localIP());
server.connect();
timeCheck.startNTP();
timeCheck.printLocalTime();
localWebServer.begin();
}
}
// MARK: Helper
// Based on https://stackoverflow.com/a/23898449/266720
bool SesameController::convertHexMessageToBinary(const char* str) {
// TODO: Fail if invalid hex values are used
uint8_t idx0, idx1;
// mapping of ASCII characters to hex values
const uint8_t hashmap[] = {
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, // 01234567
0x08, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 89:;<=>?
0x00, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x00, // @ABCDEFG
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // HIJKLMNO
};
size_t len = strlen(str);
if (len != AUTHENTICATED_MESSAGE_SIZE * 2) {
// Require exact message size
return false;
}
for (size_t pos = 0; pos < len; pos += 2) {
idx0 = ((uint8_t)str[pos+0] & 0x1F) ^ 0x10;
idx1 = ((uint8_t)str[pos+1] & 0x1F) ^ 0x10;
receivedMessageBuffer[pos/2] = (uint8_t)(hashmap[idx0] << 4) | hashmap[idx1];
};
return true;
}

View File

@ -1,19 +1,28 @@
#include "fresh.h" #include "fresh.h"
#include <Arduino.h> // configTime() #include <Arduino.h>
#include <time.h> #include <time.h>
#include <EEPROM.h>
TimeCheck::TimeCheck() { } /**
* @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 TimeCheck::configure(TimeConfiguration configuration) { void setMessageTimeAllowedOffset(uint32_t offset) {
config = configuration; allowedOffset = offset;
} }
void TimeCheck::startNTP() { void configureNTP(int32_t offsetToGMT, int32_t offsetDaylightSavings, const char* serverUrl) {
configTime(config.offsetToGMT, config.offsetDaylightSavings, config.ntpServerUrl); configTime(offsetToGMT, offsetDaylightSavings, serverUrl);
} }
void TimeCheck::printLocalTime() { void printLocalTime() {
struct tm timeinfo; struct tm timeinfo;
if (getLocalTime(&timeinfo)) { if (getLocalTime(&timeinfo)) {
Serial.println(&timeinfo, "[INFO] Time is %A, %d. %B %Y %H:%M:%S"); Serial.println(&timeinfo, "[INFO] Time is %A, %d. %B %Y %H:%M:%S");
@ -22,7 +31,7 @@ void TimeCheck::printLocalTime() {
} }
} }
uint32_t TimeCheck::getEpochTime() { uint32_t getEpochTime() {
time_t now; time_t now;
struct tm timeinfo; struct tm timeinfo;
if (!getLocalTime(&timeinfo)) { if (!getLocalTime(&timeinfo)) {
@ -33,17 +42,67 @@ uint32_t TimeCheck::getEpochTime() {
return now; return now;
} }
bool TimeCheck::isMessageTimeAcceptable(uint32_t t) { bool isMessageTimeAcceptable(uint32_t t) {
uint32_t localTime = getEpochTime(); uint32_t localTime = getEpochTime();
if (localTime == 0) { if (localTime == 0) {
Serial.println("No epoch time available"); Serial.println("No epoch time available");
return false; return false;
} }
if (t > localTime + config.allowedTimeOffset) { if (t > localTime + allowedOffset) {
return false; return false;
} }
if (t < localTime - config.allowedTimeOffset) { if (t < localTime - allowedOffset) {
return false; return false;
} }
return true; return true;
} }
void prepareMessageCounterUsage() {
EEPROM.begin(MESSAGE_COUNTER_SIZE * remoteDeviceCount);
}
uint32_t getNextMessageCounter(uint8_t deviceId) {
int offset = deviceId * MESSAGE_COUNTER_SIZE;
uint32_t counter = (uint32_t) EEPROM.read(offset + 0) << 24;
counter += (uint32_t) EEPROM.read(offset + 1) << 16;
counter += (uint32_t) EEPROM.read(offset + 2) << 8;
counter += (uint32_t) EEPROM.read(offset + 3);
return counter;
}
void printMessageCounters() {
Serial.print("[INFO] Next message numbers:");
for (uint8_t i = 0; i < remoteDeviceCount; i += 1) {
Serial.printf(" %u", getNextMessageCounter(i));
}
Serial.println("");
}
bool isDeviceIdValid(uint8_t deviceId) {
return deviceId < remoteDeviceCount;
}
bool isMessageCounterValid(uint32_t counter, uint8_t deviceId) {
return counter >= getNextMessageCounter(deviceId);
}
void setMessageCounter(uint32_t counter, uint8_t deviceId) {
int offset = deviceId * MESSAGE_COUNTER_SIZE;
EEPROM.write(offset + 0, (counter >> 24) & 0xFF);
EEPROM.write(offset + 1, (counter >> 16) & 0xFF);
EEPROM.write(offset + 2, (counter >> 8) & 0xFF);
EEPROM.write(offset + 3, counter & 0xFF);
EEPROM.commit();
}
void didUseMessageCounter(uint32_t counter, uint8_t deviceId) {
// Store the next counter, so that resetting starts at 0
setMessageCounter(counter+1, deviceId);
}
void resetMessageCounters() {
for (uint8_t i = 0; i < remoteDeviceCount; i += 1) {
setMessageCounter(0, i);
}
Serial.println("[WARN] Message counters reset");
}

View File

@ -6,61 +6,196 @@
* physical button. * physical button.
*/ */
#include <Arduino.h> #include <Arduino.h>
#include <WiFi.h>
#include <ESPAsyncWebServer.h>
#include "crypto.h"
#include "fresh.h"
#include "message.h" #include "message.h"
#include "server.h" #include "server.h"
#include "servo.h" #include "servo.h"
#include "controller.h"
#include "config.h" #include "config.h"
SesameController controller(localWebServerPort, remoteDeviceCount); /* Global variables */
ServerConnection server(serverUrl, serverPort, serverPath);
ServoController servo(pwmTimer, servoFrequency, servoPin);
AsyncWebServer local(localPort);
// The buffer to hold a received message while it is read
uint8_t receivedMessageBuffer[AUTHENTICATED_MESSAGE_SIZE];
/* Event callbacks */
SesameEvent handleReceivedMessage(AuthenticatedMessage* payload, AuthenticatedMessage* response);
// Forward declare monitoring functions
void ensureWiFiConnection(uint32_t time);
void ensureWebSocketConnection(uint32_t time);
void sendFailureResponse(AsyncWebServerRequest *request, SesameEvent event);
void sendMessageResponse(AsyncWebServerRequest *request, SesameEvent event, AuthenticatedMessage* message);
void sendResponse(AsyncWebServerRequest *request, uint8_t* buffer, uint8_t size);
void hexToBin(const char * str, uint8_t * bytes, size_t blen);
/* Logic */
void setup() { void setup() {
Serial.begin(serialBaudRate); Serial.begin(serialBaudRate);
Serial.setDebugOutput(true); Serial.setDebugOutput(true);
Serial.println("[INFO] Device started"); Serial.println("[INFO] Device started");
ServoConfiguration servoConfig { servo.configure(lockOpeningDuration, servoPressedState, servoReleasedState);
.pwmTimer = pwmTimer, Serial.println("[INFO] Servo configured");
.pwmFrequency = servoFrequency,
.pin = servoPin,
.openDuration = lockOpeningDuration,
.pressedValue = servoPressedState,
.releasedValue = servoReleasedState,
};
ServerConfiguration serverConfig { prepareMessageCounterUsage();
.url = serverUrl, //resetMessageCounters();
.port = serverPort, printMessageCounters();
.path = serverPath,
.key = serverAccessKey,
.reconnectTime = 5000,
};
TimeConfiguration timeConfig { server.onMessage(handleReceivedMessage);
.offsetToGMT = timeOffsetToGMT,
.offsetDaylightSavings = timeOffsetDaylightSavings,
.ntpServerUrl = ntpServerUrl,
.allowedTimeOffset = 60,
};
WifiConfiguration wifiConfig { local.on("/message", HTTP_POST, [] (AsyncWebServerRequest *request) {
.ssid = wifiSSID, if (!request->hasParam(messageUrlParameter)) {
.password = wifiPassword, Serial.println("Missing url parameter");
.networkName = networkName, sendFailureResponse(request, SesameEvent::InvalidMessageData);
.reconnectInterval = wifiReconnectInterval, return;
}; }
String encoded = request->getParam(messageUrlParameter)->value();
KeyConfiguration keyConfig { hexToBin(encoded.c_str(), receivedMessageBuffer, AUTHENTICATED_MESSAGE_SIZE);
.remoteKey = remoteKey, // Process received message
.localKey = localKey, AuthenticatedMessage* message = (AuthenticatedMessage*) receivedMessageBuffer;
}; AuthenticatedMessage responseMessage;
SesameEvent event = handleReceivedMessage(message, &responseMessage);
controller.configure(servoConfig, serverConfig, timeConfig, wifiConfig, keyConfig); sendMessageResponse(request, event, &responseMessage);
});
} }
void loop() { void loop() {
uint32_t time = millis(); uint32_t time = millis();
controller.loop(time); server.loop();
servo.loop();
ensureWiFiConnection(time);
ensureWebSocketConnection(time);
}
uint32_t nextWifiReconnect = 0;
bool isReconnecting = false;
void ensureWiFiConnection(uint32_t time) {
// Reconnect to WiFi
if(time > nextWifiReconnect && WiFi.status() != WL_CONNECTED) {
Serial.println("[INFO] Reconnecting WiFi...");
WiFi.setHostname(networkName);
WiFi.begin(wifiSSID, wifiPassword);
isReconnecting = true;
nextWifiReconnect = time + wifiReconnectInterval;
}
}
void ensureWebSocketConnection(uint32_t time) {
if (isReconnecting && WiFi.status() == WL_CONNECTED) {
isReconnecting = false;
Serial.println("IP address: ");
Serial.println(WiFi.localIP());
Serial.println("[INFO] WiFi connected, opening socket");
server.connectSSL(serverAccessKey);
configureNTP(timeOffsetToGMT, timeOffsetDaylightSavings, ntpServerUrl);
printLocalTime();
local.begin();
}
}
SesameEvent processMessage(AuthenticatedMessage* message) {
if (!isDeviceIdValid(message->message.device)) {
return SesameEvent::MessageDeviceInvalid;
}
if (!isMessageCounterValid(message->message.id, message->message.device)) {
return SesameEvent::MessageCounterInvalid;
}
if (!isMessageTimeAcceptable(message->message.time)) {
return SesameEvent::MessageTimeMismatch;
}
if (!isAuthenticMessage(message, remoteKey, keySize)) {
return SesameEvent::MessageAuthenticationFailed;
}
return SesameEvent::MessageAccepted;
}
/**
* Process received binary data.
*
* Checks whether the received data is a valid and unused key,
* and then signals that the motor should move.
*
* @param payload The pointer to the received data.
* @param length The number of bytes received.
* @return The event to signal to the server.
*/
SesameEvent handleReceivedMessage(AuthenticatedMessage* message, AuthenticatedMessage* response) {
SesameEvent event = processMessage(message);
// Only open when message is valid
if (event == SesameEvent::MessageAccepted) {
didUseMessageCounter(message->message.id, message->message.device);
// Move servo
servo.pressButton();
Serial.printf("[Info] Accepted message %d\n", message->message.id);
}
// Create response for all cases
response->message.time = getEpochTime();
response->message.id = getNextMessageCounter(message->message.device);
if (!authenticateMessage(response, localKey, keySize)) {
return SesameEvent::MessageAuthenticationFailed;
}
return event;
}
void sendFailureResponse(AsyncWebServerRequest *request, SesameEvent event) {
uint8_t response = static_cast<uint8_t>(event);
sendResponse(request, &response, 1);
}
void sendMessageResponse(AsyncWebServerRequest *request, SesameEvent event, AuthenticatedMessage* message) {
uint8_t response[AUTHENTICATED_MESSAGE_SIZE+1];
response[0] = static_cast<uint8_t>(event);
memcpy(response+1, (uint8_t*) message, AUTHENTICATED_MESSAGE_SIZE);
sendResponse(request, response, AUTHENTICATED_MESSAGE_SIZE+1);
}
void sendResponse(AsyncWebServerRequest *request, uint8_t* buffer, uint8_t size) {
request->send_P(200, "application/octet-stream", buffer, size);
Serial.printf("[INFO] Local response %d\n", buffer[0]);
}
// Based on https://stackoverflow.com/a/23898449/266720
void hexToBin(const char * str, uint8_t * bytes, size_t blen) {
uint8_t idx0, idx1;
// mapping of ASCII characters to hex values
const uint8_t hashmap[] = {
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, // 01234567
0x08, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 89:;<=>?
0x00, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x00, // @ABCDEFG
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // HIJKLMNO
};
memset(bytes, 0, blen);
size_t len = strlen(str);
if (len % 2) {
// Require two chars per byte
return;
}
size_t end = min(blen*2, len);
for (size_t pos = 0; pos < end; pos += 2) {
idx0 = ((uint8_t)str[pos+0] & 0x1F) ^ 0x10;
idx1 = ((uint8_t)str[pos+1] & 0x1F) ^ 0x10;
bytes[pos/2] = (uint8_t)(hashmap[idx0] << 4) | hashmap[idx1];
};
} }

View File

@ -4,35 +4,46 @@ constexpr int32_t pingInterval = 10000;
constexpr uint32_t pongTimeout = 5000; constexpr uint32_t pongTimeout = 5000;
uint8_t disconnectTimeoutCount = 3; uint8_t disconnectTimeoutCount = 3;
ServerConnection::ServerConnection() { } ServerConnection::ServerConnection(const char* url, int port, const char* path) :
url(url), port(port), path(path) {
void ServerConnection::configure(ServerConfiguration configuration, ServerConnectionCallbacks *callbacks) {
controller = callbacks;
this->configuration = configuration;
} }
void ServerConnection::connect() { 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) {
if (socketIsConnected) { if (socketIsConnected) {
return; return;
} }
if (controller == NULL) { this->key = key;
Serial.println("[ERROR] No callbacks set for server"); webSocket.beginSSL(url, port, path);
return; registerEventCallback();
} reconnectAfter(reconnectTime);
webSocket.beginSSL(configuration.url, configuration.port, configuration.path);
std::function<void(WStype_t, uint8_t *, size_t)> f = [this](WStype_t type, uint8_t *payload, size_t length) {
this->webSocketEventHandler(type, payload, length);
};
webSocket.onEvent(f);
webSocket.setReconnectInterval(configuration.reconnectTime);
} }
void ServerConnection::loop() { void ServerConnection::loop() {
webSocket.loop(); webSocket.loop();
} }
void ServerConnection::onMessage(MessageCallback callback) {
messageCallback = callback;
}
void ServerConnection::reconnectAfter(uint32_t reconnectTime) {
webSocket.setReconnectInterval(reconnectTime);
}
void ServerConnection::registerEventCallback() {
std::function<void(WStype_t, uint8_t *, size_t)> 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) { void ServerConnection::webSocketEventHandler(WStype_t type, uint8_t * payload, size_t length) {
switch(type) { switch(type) {
case WStype_DISCONNECTED: case WStype_DISCONNECTED:
@ -41,15 +52,15 @@ switch(type) {
break; break;
case WStype_CONNECTED: case WStype_CONNECTED:
socketIsConnected = true; socketIsConnected = true;
webSocket.sendTXT(configuration.key); webSocket.sendTXT(key);
Serial.printf("[INFO] Socket connected to url: %s\n", payload); Serial.printf("[INFO] Socket connected to url: %s\n", payload);
webSocket.enableHeartbeat(pingInterval, pongTimeout, disconnectTimeoutCount); webSocket.enableHeartbeat(pingInterval, pongTimeout, disconnectTimeoutCount);
break; break;
case WStype_TEXT: case WStype_TEXT:
controller->sendServerError(SesameEvent::TextReceived); sendFailureResponse(SesameEvent::TextReceived);
break; break;
case WStype_BIN: case WStype_BIN:
controller->handleServerMessage(payload, length); processReceivedBytes(payload, length);
break; break;
case WStype_PONG: case WStype_PONG:
break; break;
@ -59,11 +70,36 @@ switch(type) {
case WStype_FRAGMENT_BIN_START: case WStype_FRAGMENT_BIN_START:
case WStype_FRAGMENT: case WStype_FRAGMENT:
case WStype_FRAGMENT_FIN: case WStype_FRAGMENT_FIN:
controller->sendServerError(SesameEvent::UnexpectedSocketEvent); sendFailureResponse(SesameEvent::UnexpectedSocketEvent);
break; break;
} }
} }
void ServerConnection::sendResponse(uint8_t* buffer, uint16_t length) { void ServerConnection::processReceivedBytes(uint8_t* payload, size_t length) {
webSocket.sendBIN(buffer, length); if (length != AUTHENTICATED_MESSAGE_SIZE) {
sendFailureResponse(SesameEvent::InvalidMessageData);
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<uint8_t>(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<uint8_t>(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]);
} }

View File

@ -2,15 +2,18 @@
#include <esp32-hal.h> // For `millis()` #include <esp32-hal.h> // For `millis()`
ServoController::ServoController() { } ServoController::ServoController(int timer, int frequency, int pin)
: timer(timer), frequency(frequency), pin(pin) {
void ServoController::configure(ServoConfiguration configuration) { }
openDuration = configuration.openDuration;
pressedValue = configuration.pressedValue; void ServoController::configure(uint32_t openDuration, int pressedValue, int releasedValue) {
releasedValue = configuration.releasedValue; this->openDuration = openDuration;
ESP32PWM::allocateTimer(configuration.pwmTimer); this->pressedValue = pressedValue;
servo.setPeriodHertz(configuration.pwmFrequency); this->releasedValue = releasedValue;
servo.attach(configuration.pin); ESP32PWM::allocateTimer(timer);
servo.setPeriodHertz(frequency);
servo.attach(pin);
releaseButton(); releaseButton();
} }
@ -25,8 +28,8 @@ void ServoController::releaseButton() {
buttonIsPressed = false; buttonIsPressed = false;
} }
void ServoController::loop(uint32_t millis) { void ServoController::loop() {
if (buttonIsPressed && millis > openingEndTime) { if (buttonIsPressed && millis() > openingEndTime) {
releaseButton(); releaseButton();
} }
} }

View File

@ -1,53 +0,0 @@
#include "storage.h"
#include "message.h"
#include <EEPROM.h>
void Storage::configure() {
EEPROM.begin(messageCounterSize * remoteDeviceCount);
}
bool Storage::isDeviceIdValid(uint8_t deviceId) {
return deviceId < remoteDeviceCount;
}
bool Storage::isMessageCounterValid(uint32_t counter, uint8_t deviceId) {
return counter >= getNextMessageCounter(deviceId);
}
void Storage::didUseMessageCounter(uint32_t counter, uint8_t deviceId) {
// Store the next counter, so that resetting starts at 0
setMessageCounter(counter+1, deviceId);
}
void Storage::setMessageCounter(uint32_t counter, uint8_t deviceId) {
int offset = deviceId * messageCounterSize;
EEPROM.write(offset + 0, (counter >> 24) & 0xFF);
EEPROM.write(offset + 1, (counter >> 16) & 0xFF);
EEPROM.write(offset + 2, (counter >> 8) & 0xFF);
EEPROM.write(offset + 3, counter & 0xFF);
EEPROM.commit();
}
uint32_t Storage::getNextMessageCounter(uint8_t deviceId) {
int offset = deviceId * messageCounterSize;
uint32_t counter = (uint32_t) EEPROM.read(offset + 0) << 24;
counter += (uint32_t) EEPROM.read(offset + 1) << 16;
counter += (uint32_t) EEPROM.read(offset + 2) << 8;
counter += (uint32_t) EEPROM.read(offset + 3);
return counter;
}
void Storage::printMessageCounters() {
Serial.print("[INFO] Next message numbers:");
for (uint8_t i = 0; i < remoteDeviceCount; i += 1) {
Serial.printf(" %u", getNextMessageCounter(i));
}
Serial.println("");
}
void Storage::resetMessageCounters() {
for (uint8_t i = 0; i < remoteDeviceCount; i += 1) {
setMessageCounter(0, i);
}
Serial.println("[WARN] Message counters reset");
}