diff --git a/src/helpers/esp32/WebAuthCode.h b/src/helpers/esp32/WebAuthCode.h new file mode 100644 index 00000000..a6c2c26a --- /dev/null +++ b/src/helpers/esp32/WebAuthCode.h @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include +#include + +#ifdef ARDUINO +#include +#include +#endif +// Off-device (host tests) the caller provides millis() and esp_random(). + +// The one 6-digit code that unlocks every browser feature on the web port +// (File Transfer, VNC / Remote UI, Remote terminal). A browser sends it as the +// first WebSocket text frame "AUTH nnnnnn"; 3 wrong tries lock everyone out +// for 30 s. The code is created when the first feature turns on and dropped +// when the last one turns off, so enabling a second feature never rotates it. +class WebAuthCode { +public: + void ensure() { + if (_code != 0) return; + _failures = 0; + _blocked_until_ms = 0; + _code = 100000u + esp_random() % 900000u; + } + void disarm() { + _code = 0; + _failures = 0; + _blocked_until_ms = 0; + } + uint32_t code() const { return _code; } + + bool matches(const uint8_t* text, size_t len) const { + if (_code == 0 || !text || len != 6) return false; + uint32_t value = 0; + for (size_t i = 0; i < len; ++i) { + if (text[i] < '0' || text[i] > '9') return false; + value = value * 10u + static_cast(text[i] - '0'); + } + return value == _code; + } + bool allowed() const { + return _blocked_until_ms == 0 || + static_cast(millis() - _blocked_until_ms) >= 0; + } + void noteFailure() { + if (++_failures >= 3) { + _failures = 0; + _blocked_until_ms = millis() + 30000u; + } + } + void noteSuccess() { + _failures = 0; + _blocked_until_ms = 0; + } + +private: + volatile uint32_t _code = 0; + volatile uint32_t _blocked_until_ms = 0; + volatile uint8_t _failures = 0; +}; + +extern WebAuthCode g_web_auth; diff --git a/src/helpers/esp32/WebFileTransfer.cpp b/src/helpers/esp32/WebFileTransfer.cpp index 203cbc78..f3abeabc 100644 --- a/src/helpers/esp32/WebFileTransfer.cpp +++ b/src/helpers/esp32/WebFileTransfer.cpp @@ -6,6 +6,7 @@ #include #include #include +#include "WebAuthCode.h" WebFileTransfer g_web_file_transfer; @@ -24,57 +25,25 @@ bool WebFileTransfer::begin() { return _inbound != nullptr && _outbound != nullptr; } -bool WebFileTransfer::setEnabled(bool enabled, uint32_t code) { +bool WebFileTransfer::setEnabled(bool enabled) { if (!enabled) { _enabled = false; __sync_synchronize(); - _code = 0; + g_web_auth.disarm(); _clients = 0; - _auth_failures = 0; - _auth_blocked_until_ms = 0; discardTraffic(); return true; } - if (!begin() || code < 100000 || code > 999999) return false; + if (!begin()) return false; discardTraffic(); - _code = code; + g_web_auth.ensure(); _clients = 0; - _auth_failures = 0; - _auth_blocked_until_ms = 0; _last_activity_ms = millis(); __sync_synchronize(); _enabled = true; return true; } -bool WebFileTransfer::matchesCode(const uint8_t* text, size_t len) const { - if (!_enabled || !text || len != 6) return false; - uint32_t value = 0; - for (size_t i = 0; i < len; ++i) { - if (text[i] < '0' || text[i] > '9') return false; - value = value * 10u + static_cast(text[i] - '0'); - } - return value == _code; -} - -bool WebFileTransfer::authAllowed() const { - return _auth_blocked_until_ms == 0 || - static_cast(millis() - _auth_blocked_until_ms) >= 0; -} - -void WebFileTransfer::noteAuthFailure() { - if (++_auth_failures >= 3) { - _auth_failures = 0; - _auth_blocked_until_ms = millis() + 30000u; - } -} - -void WebFileTransfer::noteAuthSuccess() { - _auth_failures = 0; - _auth_blocked_until_ms = 0; - touch(); -} - void WebFileTransfer::touch() { _last_activity_ms = millis(); } diff --git a/src/helpers/esp32/WebFileTransfer.h b/src/helpers/esp32/WebFileTransfer.h index 5408fc9d..174bafde 100644 --- a/src/helpers/esp32/WebFileTransfer.h +++ b/src/helpers/esp32/WebFileTransfer.h @@ -14,13 +14,8 @@ public: static constexpr size_t MAX_OUTBOUND_BYTES = 2053; // type + LE32 offset + 2 KiB bool begin(); - bool setEnabled(bool enabled, uint32_t code = 0); + bool setEnabled(bool enabled); bool enabled() const { return _enabled; } - uint32_t code() const { return _code; } - bool matchesCode(const uint8_t* text, size_t len) const; - bool authAllowed() const; - void noteAuthFailure(); - void noteAuthSuccess(); bool pushInbound(uint8_t opcode, const uint8_t* data, size_t len); size_t popInbound(uint8_t* opcode, uint8_t* dst, size_t max_len); @@ -59,11 +54,8 @@ private: portMUX_TYPE _outbound_mux = portMUX_INITIALIZER_UNLOCKED; volatile bool _enabled = false; - volatile uint32_t _code = 0; volatile int _clients = 0; volatile uint32_t _last_activity_ms = 0; - volatile uint32_t _auth_blocked_until_ms = 0; - volatile uint8_t _auth_failures = 0; }; extern WebFileTransfer g_web_file_transfer; diff --git a/src/helpers/esp32/WebMirror.cpp b/src/helpers/esp32/WebMirror.cpp index ad8c5060..a2d19198 100644 --- a/src/helpers/esp32/WebMirror.cpp +++ b/src/helpers/esp32/WebMirror.cpp @@ -4,6 +4,7 @@ #include WebMirror g_web_mirror; +WebAuthCode g_web_auth; #ifndef WEB_MIRROR_RING_BYTES #define WEB_MIRROR_RING_BYTES (384 * 1024) // PSRAM; holds several banded frames diff --git a/src/helpers/esp32/WebMirror.h b/src/helpers/esp32/WebMirror.h index 3f5901a5..419673c3 100644 --- a/src/helpers/esp32/WebMirror.h +++ b/src/helpers/esp32/WebMirror.h @@ -16,6 +16,7 @@ // ============================================================================ #include #include +#include "WebAuthCode.h" // g_web_auth: the one code shared by every browser feature class WebMirror { public: diff --git a/src/helpers/esp32/WebSocketCompanionServer.cpp b/src/helpers/esp32/WebSocketCompanionServer.cpp index c08691a4..b035ac17 100644 --- a/src/helpers/esp32/WebSocketCompanionServer.cpp +++ b/src/helpers/esp32/WebSocketCompanionServer.cpp @@ -1298,18 +1298,19 @@ void WebSocketCompanionServer::drainFileInput(int idx) { if (c->ws_payload_len > WebFileTransfer::MAX_INBOUND_BYTES) { if (c->tx_len == 0) sendFileReply(idx, "ERR frame too large"); } else if (!c->files_authed) { - const bool allowed = g_web_file_transfer.authAllowed(); + const bool allowed = g_web_auth.allowed(); const bool auth = allowed && c->ws_opcode == 0x01 && c->files_rx_len == 11 && memcmp(c->files_rx_buf, "AUTH ", 5) == 0 && - g_web_file_transfer.matchesCode(c->files_rx_buf + 5, 6); + g_web_auth.matches(c->files_rx_buf + 5, 6); if (auth) { c->files_authed = true; c->files_auth_failures = 0; - g_web_file_transfer.noteAuthSuccess(); + g_web_auth.noteSuccess(); + g_web_file_transfer.touch(); if (c->tx_len == 0) sendFileReply(idx, "AUTH OK"); } else { c->files_auth_failures++; - if (allowed) g_web_file_transfer.noteAuthFailure(); + if (allowed) g_web_auth.noteFailure(); if (c->tx_len == 0) sendFileReply(idx, allowed ? "ERR invalid code" : "ERR try again in 30 seconds"); if (c->files_auth_failures >= 3) c->files_close_after_tx = true; diff --git a/src/ui-touch/UITask.cpp b/src/ui-touch/UITask.cpp index 80c8af65..8b5367ba 100644 --- a/src/ui-touch/UITask.cpp +++ b/src/ui-touch/UITask.cpp @@ -32966,9 +32966,8 @@ static void openFileTransferPage() { g_lv.task->enableTcp(); s_file_transfer_started_tcp = true; } - const uint32_t code = 100000u + esp_random() % 900000u; s_file_transfer_result[0] = '\0'; - if (!g_web_file_transfer.setEnabled(true, code)) { + if (!g_web_file_transfer.setEnabled(true)) { if (s_file_transfer_started_tcp && g_lv.task) g_lv.task->disableTcp(); s_file_transfer_started_tcp = false; if (g_lv.task) g_lv.task->showAlert(TR("File Transfer: low memory"), 1800); @@ -33036,7 +33035,7 @@ static void openFileTransferPage() { lv_obj_set_style_text_color(code_caption, lv_color_hex(COLOR_SUB), LV_PART_MAIN); lv_obj_t* code_label = lv_label_create(s_file_transfer_root); - lv_label_set_text_fmt(code_label, "%06lu", static_cast(code)); + lv_label_set_text_fmt(code_label, "%06lu", static_cast(g_web_auth.code())); lv_obj_set_style_text_font(code_label, &lv_font_montserrat_28, LV_PART_MAIN); lv_obj_set_style_text_color(code_label, lv_color_hex(COLOR_TEXT), LV_PART_MAIN); diff --git a/test/test_web_auth_code.cpp b/test/test_web_auth_code.cpp new file mode 100644 index 00000000..8b14e0e0 --- /dev/null +++ b/test/test_web_auth_code.cpp @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include +#include +#include + +static uint32_t g_now = 1000; +static uint32_t g_rand = 123456; +static uint32_t millis() { return g_now; } +static uint32_t esp_random() { return g_rand; } + +#include "helpers/esp32/WebAuthCode.h" + +static bool tryCode(WebAuthCode& a, const char* text) { + return a.matches(reinterpret_cast(text), strlen(text)); +} + +int main() { + WebAuthCode a; + assert(!tryCode(a, "000000")); // nothing armed yet + assert(a.code() == 0); + + a.ensure(); + assert(a.code() == 100000u + 123456u % 900000u); + assert(tryCode(a, "223456")); + assert(!tryCode(a, "223457")); + assert(!tryCode(a, "22345")); + assert(!tryCode(a, "2234567")); + assert(!tryCode(a, "22345a")); + + g_rand = 777777; // a second feature turning on must not rotate the code + a.ensure(); + assert(a.code() == 223456u); + + assert(a.allowed()); + a.noteFailure(); + a.noteFailure(); + assert(a.allowed()); + a.noteFailure(); // third wrong try locks for 30 s + assert(!a.allowed()); + g_now += 29999; + assert(!a.allowed()); + g_now += 1; + assert(a.allowed()); + + a.noteFailure(); + a.noteFailure(); + a.noteSuccess(); // success clears the failure count + a.noteFailure(); + a.noteFailure(); + assert(a.allowed()); + + a.disarm(); + assert(a.code() == 0); + assert(!tryCode(a, "223456")); + a.ensure(); // next session gets a fresh code and no leftover lockout + assert(a.code() == 100000u + 777777u % 900000u); + assert(a.allowed()); + return 0; +}