#include "controller.h" #include "crypto.h" #include "config.h" #include #include 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; }