Switch to ethernet, challenge-response
This commit is contained in:
@ -3,25 +3,37 @@
|
||||
#include "server.h"
|
||||
#include "servo.h"
|
||||
#include "message.h"
|
||||
#include "storage.h"
|
||||
#include "fresh.h"
|
||||
#include <ESPAsyncWebServer.h>
|
||||
|
||||
struct WifiConfiguration {
|
||||
struct EthernetConfiguration {
|
||||
|
||||
// The WiFi network to connect to
|
||||
const char* ssid;
|
||||
// The MAC address of the ethernet connection
|
||||
uint8_t macAddress[6];
|
||||
|
||||
// The WiFi password to connect to the above network
|
||||
const char* password;
|
||||
// The master-in slave-out pin of the SPI connection for the Ethernet module
|
||||
int8_t spiPinMiso;
|
||||
|
||||
// The name of the device on the network
|
||||
const char* networkName;
|
||||
// The master-out slave-in pin of the SPI connection for the Ethernet module
|
||||
int8_t spiPinMosi;
|
||||
|
||||
// The interval to reconnect to WiFi if the connection is broken
|
||||
uint32_t reconnectInterval;
|
||||
// The slave clock pin of the SPI connection for the Ethernet module
|
||||
int8_t spiPinSclk;
|
||||
|
||||
uint32_t periodicReconnectInterval;
|
||||
// The slave-select pin of the SPI connection for the Ethernet module
|
||||
int8_t spiPinSS;
|
||||
|
||||
unsigned long dhcpLeaseTimeoutMs;
|
||||
unsigned long dhcpLeaseResponseTimeoutMs;
|
||||
|
||||
// The static IP address to assign if DHCP fails
|
||||
uint8_t manualIp[4];
|
||||
|
||||
// The IP address of the DNS server, if DHCP fails
|
||||
uint8_t manualDnsAddress[4];
|
||||
|
||||
uint32_t socketHeartbeatIntervalMs;
|
||||
uint32_t socketHeartbeatTimeoutMs;
|
||||
uint8_t socketHeartbeatFailureReconnectCount;
|
||||
};
|
||||
|
||||
struct KeyConfiguration {
|
||||
@ -29,55 +41,87 @@ struct KeyConfiguration {
|
||||
const uint8_t* remoteKey;
|
||||
|
||||
const uint8_t* localKey;
|
||||
|
||||
uint32_t challengeExpiryMs;
|
||||
};
|
||||
|
||||
class SesameController: public ServerConnectionCallbacks {
|
||||
|
||||
public:
|
||||
SesameController(uint16_t localWebServerPort, uint8_t remoteDeviceCount);
|
||||
SesameController(uint16_t localWebServerPort);
|
||||
|
||||
void configure(ServoConfiguration servoConfig, ServerConfiguration serverConfig, TimeConfiguration timeConfig, WifiConfiguration wifiConfig, KeyConfiguration keyConfig);
|
||||
void configure(ServoConfiguration servoConfig, ServerConfiguration serverConfig, EthernetConfiguration ethernetConfig, KeyConfiguration keyConfig);
|
||||
|
||||
void loop(uint32_t millis);
|
||||
|
||||
private:
|
||||
|
||||
uint32_t currentTime = 0;
|
||||
|
||||
ServerConnection server;
|
||||
ServoController servo;
|
||||
AsyncWebServer localWebServer;
|
||||
TimeCheck timeCheck;
|
||||
Storage storage;
|
||||
|
||||
WifiConfiguration wifiConfig;
|
||||
EthernetConfiguration ethernetConfig;
|
||||
bool ethernetIsConfigured = false;
|
||||
|
||||
KeyConfiguration keyConfig;
|
||||
|
||||
bool isReconnecting = false;
|
||||
|
||||
// The buffer to hold a received message while it is read
|
||||
uint8_t receivedMessageBuffer[AUTHENTICATED_MESSAGE_SIZE];
|
||||
// Buffer to get local message
|
||||
SignedMessage receivedLocalMessage;
|
||||
|
||||
// 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;
|
||||
uint32_t currentClientChallenge;
|
||||
uint32_t currentChallengeExpiry = 0;
|
||||
uint32_t currentServerChallenge;
|
||||
|
||||
SignedMessage outgoingMessage;
|
||||
|
||||
bool hasCurrentChallenge() {
|
||||
return currentChallengeExpiry > currentTime;
|
||||
}
|
||||
|
||||
void clearCurrentChallenge() {
|
||||
currentClientChallenge = 0;
|
||||
currentServerChallenge = 0;
|
||||
currentChallengeExpiry = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Send an error Response over the web socket.
|
||||
*
|
||||
* @param result The error result to send
|
||||
* @param discardMessage Indicate if the stored message should be cleared.
|
||||
*
|
||||
* Note: Only clear the message if no other operation is in progress.
|
||||
*/
|
||||
void sendErrorResponseToServer(MessageResult result, bool discardMessage = true);
|
||||
|
||||
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);
|
||||
/**
|
||||
* @brief Callback to send an error back to the server via the web socket.
|
||||
*
|
||||
* This function is called when the socket get's an error.
|
||||
*
|
||||
* @param event The error to report back
|
||||
*/
|
||||
void sendServerError(MessageResult event);
|
||||
|
||||
void prepareResponseBuffer(SesameEvent event, uint8_t deviceId = 0);
|
||||
void processMessage(SignedMessage* message);
|
||||
MessageResult verifyAndProcessReceivedMessage(SignedMessage* message);
|
||||
|
||||
void prepareResponseBuffer(MessageResult event, Message* message = NULL);
|
||||
void sendPreparedLocalResponse(AsyncWebServerRequest *request);
|
||||
void sendPreparedServerResponse();
|
||||
void sendPreparedResponseToServer();
|
||||
|
||||
void periodicallyReconnectWifiAndSocket(uint32_t millis);
|
||||
void prepareChallenge(Message* message);
|
||||
void completeUnlockRequest(Message* message);
|
||||
};
|
@ -3,6 +3,15 @@
|
||||
#include "message.h"
|
||||
#include <stddef.h>
|
||||
|
||||
void enableCrypto();
|
||||
|
||||
/**
|
||||
* @brief Create a random server challenge.
|
||||
*
|
||||
* @return uint32_t
|
||||
*/
|
||||
uint32_t randomChallenge();
|
||||
|
||||
/**
|
||||
* @brief Create a message authentication code (MAC) for some data.
|
||||
*
|
||||
@ -10,11 +19,10 @@
|
||||
* @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);
|
||||
bool authenticateData(const uint8_t* data, size_t dataLength, uint8_t* mac, const uint8_t* key);
|
||||
|
||||
/**
|
||||
* @brief Calculate a MAC for message content.
|
||||
@ -22,22 +30,20 @@ bool authenticateData(const uint8_t* data, size_t dataLength, uint8_t* mac, cons
|
||||
* @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);
|
||||
bool authenticateMessage(Message* message, uint8_t* mac, const uint8_t* key);
|
||||
|
||||
/**
|
||||
* @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);
|
||||
bool authenticateMessage(SignedMessage* message, const uint8_t* key);
|
||||
|
||||
/**
|
||||
* @brief Check if a received unlock message is authentic
|
||||
@ -48,8 +54,7 @@ bool authenticateMessage(AuthenticatedMessage* message, const uint8_t* key, size
|
||||
*
|
||||
* @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);
|
||||
bool isAuthenticMessage(SignedMessage* message, const uint8_t* key);
|
@ -1,80 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
struct TimeConfiguration {
|
||||
|
||||
/**
|
||||
* @brief The timezone offset in seconds
|
||||
*/
|
||||
int32_t offsetToGMT;
|
||||
|
||||
/**
|
||||
* @brief The daylight savings offset in seconds
|
||||
*/
|
||||
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,
|
||||
* but may lead to issues when dealing with slow networks and other
|
||||
* routing delays.
|
||||
*/
|
||||
uint32_t allowedTimeOffset;
|
||||
};
|
||||
|
||||
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
|
||||
*
|
||||
* The time must be initialized by calling `configureNTP()` before use.
|
||||
*/
|
||||
void printLocalTime();
|
||||
|
||||
/**
|
||||
* Gets the current epoch time
|
||||
*/
|
||||
uint32_t getEpochTime();
|
||||
|
||||
/**
|
||||
* @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);
|
||||
|
||||
private:
|
||||
|
||||
TimeConfiguration config;
|
||||
};
|
@ -14,58 +14,93 @@
|
||||
|
||||
#pragma pack(push, 1)
|
||||
|
||||
typedef enum {
|
||||
|
||||
/// @brief The initial message from remote to device to request a challenge.
|
||||
initial = 0,
|
||||
|
||||
/// @brief The second message in an unlock with the challenge from the device to the remote
|
||||
challenge = 1,
|
||||
|
||||
/// @brief The third message with the signed challenge from the remote to the device
|
||||
request = 2,
|
||||
|
||||
/// @brief The final message with the unlock result from the device to the remote
|
||||
response = 3,
|
||||
|
||||
} MessageType;
|
||||
|
||||
enum class MessageResult: uint8_t {
|
||||
|
||||
/// @brief The message was accepted.
|
||||
MessageAccepted = 0,
|
||||
|
||||
/// @brief The web socket received text while waiting for binary data.
|
||||
TextReceived = 1,
|
||||
|
||||
/// @brief An unexpected socket event occured while performing the exchange.
|
||||
UnexpectedSocketEvent = 2,
|
||||
|
||||
/// @brief The received message size is invalid.
|
||||
InvalidMessageSize = 3,
|
||||
|
||||
/// @brief The message signature was incorrect.
|
||||
MessageAuthenticationFailed = 4,
|
||||
|
||||
/// @brief The server challenge of the message did not match previous messages
|
||||
ServerChallengeMismatch = 5,
|
||||
|
||||
/// @brief The client challenge of the message did not match previous messages
|
||||
ClientChallengeInvalid = 6,
|
||||
|
||||
/// @brief An unexpected or unsupported message type was received
|
||||
InvalidMessageType = 7,
|
||||
|
||||
/// @brief A message is already being processed
|
||||
TooManyRequests = 8,
|
||||
|
||||
InvalidUrlParameter = 10,
|
||||
InvalidResponseAuthentication = 11,
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* @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.
|
||||
* @brief A generic message to exchange during challenge-response authentication.
|
||||
*/
|
||||
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;
|
||||
/// @brief The type of message being sent.
|
||||
MessageType messageType;
|
||||
|
||||
/**
|
||||
* @brief The id of the device sending the message
|
||||
* @brief The random nonce created by the remote
|
||||
*
|
||||
* This nonce is a random number created by the remote, different for each unlock request.
|
||||
* It is set for all message types.
|
||||
*/
|
||||
uint8_t device;
|
||||
uint32_t clientChallenge;
|
||||
|
||||
/**
|
||||
* @brief A random number to sign by the remote
|
||||
*
|
||||
* This nonce is set by the server after receiving an initial message.
|
||||
* It is set for the message types `challenge`, `request`, and `response`.
|
||||
*/
|
||||
uint32_t serverChallenge;
|
||||
|
||||
/**
|
||||
* @brief The response status for the previous message.
|
||||
*
|
||||
* It is set only for messages from the server, e.g. the `challenge` and `response` message types.
|
||||
* Must be set to `MessageAccepted` for other messages.
|
||||
*/
|
||||
MessageResult result;
|
||||
|
||||
} Message;
|
||||
|
||||
constexpr size_t messageCounterSize = sizeof(uint32_t);
|
||||
|
||||
/**
|
||||
* @brief An authenticated message by the mobile device to command unlocking.
|
||||
* @brief The signed version of a message.
|
||||
*
|
||||
* 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 {
|
||||
|
||||
@ -77,38 +112,18 @@ typedef struct {
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
/// @brief The message
|
||||
Message message;
|
||||
|
||||
} AuthenticatedMessage;
|
||||
} SignedMessage;
|
||||
|
||||
constexpr size_t messageCounterSize = sizeof(uint32_t);
|
||||
|
||||
#pragma pack(pop)
|
||||
|
||||
constexpr int MESSAGE_CONTENT_SIZE = sizeof(Message);
|
||||
|
||||
constexpr int AUTHENTICATED_MESSAGE_SIZE = sizeof(AuthenticatedMessage);
|
||||
|
||||
/**
|
||||
* An event signaled from the device
|
||||
*/
|
||||
enum class SesameEvent {
|
||||
TextReceived = 1,
|
||||
UnexpectedSocketEvent = 2,
|
||||
InvalidMessageSize = 3,
|
||||
MessageAuthenticationFailed = 4,
|
||||
MessageTimeMismatch = 5,
|
||||
MessageCounterInvalid = 6,
|
||||
MessageAccepted = 7,
|
||||
MessageDeviceInvalid = 8,
|
||||
InvalidUrlParameter = 20,
|
||||
InvalidResponseAuthentication = 21,
|
||||
};
|
||||
constexpr int SIGNED_MESSAGE_SIZE = sizeof(SignedMessage);
|
||||
|
||||
/**
|
||||
* @brief A callback for messages received over the socket
|
||||
@ -121,5 +136,5 @@ typedef void (*MessageCallback)(uint8_t* payload, size_t length);
|
||||
/**
|
||||
* @brief A callback for socket errors
|
||||
*/
|
||||
typedef void (*ErrorCallback)(SesameEvent event);
|
||||
typedef void (*ErrorCallback)(MessageResult event);
|
||||
|
||||
|
@ -2,8 +2,6 @@
|
||||
|
||||
#include "message.h"
|
||||
#include "crypto.h"
|
||||
#include <WiFiMulti.h>
|
||||
#include <WiFiClientSecure.h>
|
||||
#include <WebSocketsClient.h>
|
||||
|
||||
struct ServerConfiguration {
|
||||
@ -36,7 +34,7 @@ class ServerConnectionCallbacks {
|
||||
|
||||
public:
|
||||
|
||||
virtual void sendServerError(SesameEvent event) = 0;
|
||||
virtual void sendServerError(MessageResult event) = 0;
|
||||
virtual void handleServerMessage(uint8_t* payload, size_t length) = 0;
|
||||
};
|
||||
|
||||
|
@ -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);
|
||||
};
|
Reference in New Issue
Block a user