237 lines
8.5 KiB
C++
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;
|
|
} |