Sesame-Device/src/controller.cpp
2023-12-05 22:54:47 +01:00

237 lines
8.5 KiB
C++

#include "controller.h"
#include "crypto.h"
#include "config.h"
#include <SPI.h>
#include <Ethernet.h>
SesameController::SesameController(uint16_t localWebServerPort) : localWebServer(localWebServerPort) {
}
void SesameController::configure(ServoConfiguration servoConfig, ServerConfiguration serverConfig, EthernetConfiguration ethernetConfig, KeyConfiguration keyConfig) {
this->ethernetConfig = ethernetConfig;
this->keyConfig = keyConfig;
// Ensure source of random numbers without WiFi and Bluetooth
enableCrypto();
// Initialize SPI interface to Ethernet module
SPI.begin(ethernetConfig.spiPinSclk, ethernetConfig.spiPinMiso, ethernetConfig.spiPinMosi, ethernetConfig.spiPinSS); //SCLK, MISO, MOSI, SS
pinMode(ethernetConfig.spiPinSS, OUTPUT);
Ethernet.init(ethernetConfig.spiPinSS);
if (Ethernet.begin(ethernetConfig.macAddress, ethernetConfig.dhcpLeaseTimeoutMs, ethernetConfig.dhcpLeaseResponseTimeoutMs) == 1) {
Serial.print("[INFO] DHCP assigned IP ");
Serial.println(Ethernet.localIP());
ethernetIsConfigured = true;
} else {
// Check for Ethernet hardware present
if (Ethernet.hardwareStatus() == EthernetNoHardware) {
Serial.println("[ERROR] Ethernet shield not found.");
} else if (Ethernet.linkStatus() == LinkOFF) {
Serial.println("[ERROR] Ethernet cable is not connected.");
} else if (Ethernet.linkStatus() == Unknown) {
Serial.println("[ERROR] Ethernet cable status unknown.");
} else if (Ethernet.linkStatus() == LinkON) {
Serial.println("[INFO] Ethernet cable is connected.");
// Try to configure using IP address instead of DHCP
Ethernet.begin(ethernetConfig.macAddress, ethernetConfig.manualIp, ethernetConfig.manualDnsAddress);
Serial.print("[WARNING] DHCP failed, using self-assigned IP ");
Serial.println(Ethernet.localIP());
ethernetIsConfigured = true;
}
}
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");
// 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");
}
void SesameController::loop(uint32_t millis) {
currentTime = millis;
server.loop(millis);
servo.loop(millis);
}
// MARK: Local
void SesameController::handleLocalMessage(AsyncWebServerRequest *request) {
if (!request->hasParam(messageUrlParameter)) {
Serial.println("Missing url parameter");
prepareResponseBuffer(MessageResult::InvalidUrlParameter);
return;
}
String encoded = request->getParam(messageUrlParameter)->value();
if (!convertHexMessageToBinary(encoded.c_str())) {
Serial.println("Invalid hex encoding");
prepareResponseBuffer(MessageResult::InvalidMessageSize);
return;
}
processMessage(&receivedLocalMessage);
}
void SesameController::sendPreparedLocalResponse(AsyncWebServerRequest *request) {
request->send_P(200, "application/octet-stream", (uint8_t*) &outgoingMessage, SIGNED_MESSAGE_SIZE);
Serial.printf("[INFO] Local response %u\n", outgoingMessage.message.messageType);
}
// MARK: Server
void SesameController::sendServerError(MessageResult result) {
prepareResponseBuffer(result); // No message to echo
sendPreparedResponseToServer();
}
void SesameController::handleServerMessage(uint8_t* payload, size_t length) {
if (length != SIGNED_MESSAGE_SIZE) {
// No message saved to discard, don't accidentally delete for other operation
sendServerError(MessageResult::InvalidMessageSize);
return;
}
processMessage((SignedMessage*) payload);
sendPreparedResponseToServer();
}
void SesameController::sendPreparedResponseToServer() {
server.sendResponse((uint8_t*) &outgoingMessage, SIGNED_MESSAGE_SIZE);
Serial.printf("[INFO] Server response %u\n", outgoingMessage.message.messageType);
}
// MARK: Message handling
void SesameController::processMessage(SignedMessage* message) {
// Result must be empty
if (message->message.result != MessageResult::MessageAccepted) {
prepareResponseBuffer(MessageResult::ClientChallengeInvalid);
return;
}
if (!isAuthenticMessage(message, keyConfig.remoteKey)) {
prepareResponseBuffer(MessageResult::MessageAuthenticationFailed);
return;
}
switch (message->message.messageType) {
case MessageType::initial:
prepareChallenge(&message->message);
return;
case MessageType::request:
completeUnlockRequest(&message->message);
return;
default:
prepareResponseBuffer(MessageResult::InvalidMessageType);
return;
}
}
void SesameController::prepareChallenge(Message* message) {
// Server challenge must be empty
if (message->serverChallenge != 0) {
prepareResponseBuffer(MessageResult::ClientChallengeInvalid);
return;
}
if (hasCurrentChallenge()) {
Serial.println("[INFO] Overwriting old challenge");
}
// Set challenge and respond
currentClientChallenge = message->clientChallenge;
currentServerChallenge = randomChallenge();
currentChallengeExpiry = currentTime + keyConfig.challengeExpiryMs;
prepareResponseBuffer(MessageResult::MessageAccepted, message);
}
void SesameController::completeUnlockRequest(Message* message) {
if (!hasCurrentChallenge()) {
prepareResponseBuffer(MessageResult::ClientChallengeInvalid, message);
return;
}
// Client and server challenge must match
if (message->clientChallenge != currentClientChallenge) {
prepareResponseBuffer(MessageResult::ClientChallengeInvalid, message);
return;
}
if (message->serverChallenge != currentServerChallenge) {
prepareResponseBuffer(MessageResult::ServerChallengeMismatch, message);
return;
}
clearCurrentChallenge();
// Move servo
servo.pressButton();
prepareResponseBuffer(MessageResult::MessageAccepted, message);
Serial.println("[INFO] Accepted message");
}
void SesameController::prepareResponseBuffer(MessageResult result, Message* message) {
outgoingMessage.message.result = result;
if (message != NULL) {
outgoingMessage.message.clientChallenge = message->clientChallenge;
outgoingMessage.message.serverChallenge = message->serverChallenge;
// All outgoing messages are responses, except if an initial message is accepted
if (message->messageType == MessageType::initial && result == MessageResult::MessageAccepted) {
outgoingMessage.message.messageType = MessageType::challenge;
} else {
outgoingMessage.message.messageType = MessageType::response;
}
} else {
outgoingMessage.message.clientChallenge = message->clientChallenge;
outgoingMessage.message.serverChallenge = message->serverChallenge;
outgoingMessage.message.messageType = MessageType::response;
}
if (!authenticateMessage(&outgoingMessage, keyConfig.localKey)) {
Serial.println("[ERROR] Failed to sign message");
}
}
// MARK: Helper
/**
* @brief
*
* Based on https://stackoverflow.com/a/23898449/266720
*
* @param str
* @return true
* @return false
*/
bool SesameController::convertHexMessageToBinary(const char* str) {
uint8_t* buffer = (uint8_t*) &receivedLocalMessage;
// 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 != SIGNED_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;
buffer[pos/2] = (uint8_t)(hashmap[idx0] << 4) | hashmap[idx1];
};
return true;
}