Restructure, use HMAC, NTP

Remove config details
This commit is contained in:
Christoph Hagen
2022-04-07 17:45:23 +02:00
parent 35b171e0c4
commit 1c7011400c
10 changed files with 792 additions and 334 deletions

45
src/crypto.cpp Normal file
View File

@@ -0,0 +1,45 @@
#include "crypto.h"
#include <string.h>
#include <mbedtls/md.h>
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;
}

92
src/fresh.cpp Normal file
View File

@@ -0,0 +1,92 @@
#include "fresh.h"
#include <Arduino.h>
#include <time.h>
#include <EEPROM.h>
/**
* @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");
}

View File

@@ -7,11 +7,12 @@
*/
#include <Arduino.h>
#include <WiFi.h>
#include <WiFiMulti.h>
#include <WiFiClientSecure.h>
#include <WebSocketsClient.h>
#include <EEPROM.h> // To mark used keys as expired
#include <ESP32Servo.h> // 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<uint8_t>(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;
}

96
src/server.cpp Normal file
View File

@@ -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<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) {
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<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]);
}

37
src/servo.cpp Normal file
View File

@@ -0,0 +1,37 @@
#include "servo.h"
#include <esp32-hal.h> // 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();
}
}