#include #include #include #include #include #include #include #include #ifdef ESP_PLATFORM #include #include #include #else #include #endif namespace tt::settings::webserver { static const auto LOGGER = Logger("WebServerSettings"); static std::string getSettingsFilePath() { return getUserDataPath() + "/settings/webserver.properties"; } // Property keys constexpr auto* KEY_WIFI_ENABLED = "wifiEnabled"; constexpr auto* KEY_WIFI_MODE = "wifiMode"; constexpr auto* KEY_AP_SSID = "apSsid"; constexpr auto* KEY_AP_PASSWORD = "apPassword"; constexpr auto* KEY_AP_OPEN_NETWORK = "apOpenNetwork"; constexpr auto* KEY_AP_CHANNEL = "apChannel"; constexpr auto* KEY_WEBSERVER_ENABLED = "webServerEnabled"; constexpr auto* KEY_WEBSERVER_PORT = "webServerPort"; constexpr auto* KEY_WEBSERVER_AUTH_ENABLED = "webServerAuthEnabled"; constexpr auto* KEY_WEBSERVER_USERNAME = "webServerUsername"; constexpr auto* KEY_WEBSERVER_PASSWORD = "webServerPassword"; std::string generateDefaultApSsid() { #ifdef ESP_PLATFORM uint8_t mac[6]; if (esp_read_mac(mac, ESP_MAC_WIFI_STA) != ESP_OK) { return "Tactility-0000"; } char ssid[16]; snprintf(ssid, sizeof(ssid), "Tactility-%02X%02X", mac[2] ^ mac[3], mac[4] ^ mac[5]); return std::string(ssid); #else return "Tactility-0000"; #endif } /** * @brief Generate a cryptographically secure random string for credentials * @param length The desired length of the string * @return A random alphanumeric string */ std::string generateRandomCredential(size_t length) { static constexpr char charset[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; static constexpr size_t charsetSize = sizeof(charset) - 1; std::string result; result.reserve(length); #ifdef ESP_PLATFORM for (size_t i = 0; i < length; ++i) { uint32_t randomValue = esp_random(); result += charset[randomValue % charsetSize]; } #else static std::random_device rd; static std::mt19937 gen(rd()); std::uniform_int_distribution<> dis(0, charsetSize - 1); for (size_t i = 0; i < length; ++i) { result += charset[dis(gen)]; } #endif return result; } /** * @brief Check if a credential value is empty (needs auto-generation) * @param value The credential value to check * @return true if the credential is empty and needs generation */ static bool isEmptyCredential(const std::string& value) { return value.empty(); } bool load(WebServerSettings& settings) { auto settings_path = getSettingsFilePath(); if (!file::isFile(settings_path)) { return false; } std::map map; if (!file::loadPropertiesFile(settings_path, map)) { return false; } // Parse all settings from the map auto wifi_enabled = map.find(KEY_WIFI_ENABLED); auto wifi_mode = map.find(KEY_WIFI_MODE); auto ap_ssid = map.find(KEY_AP_SSID); auto ap_password = map.find(KEY_AP_PASSWORD); auto ap_open_network = map.find(KEY_AP_OPEN_NETWORK); auto ap_channel = map.find(KEY_AP_CHANNEL); auto webserver_enabled = map.find(KEY_WEBSERVER_ENABLED); auto webserver_port = map.find(KEY_WEBSERVER_PORT); auto webserver_auth_enabled = map.find(KEY_WEBSERVER_AUTH_ENABLED); auto webserver_username = map.find(KEY_WEBSERVER_USERNAME); auto webserver_password = map.find(KEY_WEBSERVER_PASSWORD); // WiFi settings settings.wifiEnabled = (wifi_enabled != map.end()) ? (wifi_enabled->second == "1" || wifi_enabled->second == "true") : false; // Default disabled settings.wifiMode = (wifi_mode != map.end() && wifi_mode->second == "1") ? WiFiMode::AccessPoint : WiFiMode::Station; auto parseInt = [](const std::string& value, int min, int max, int fallback) -> int { int v = 0; auto [ptr, ec] = std::from_chars(value.data(), value.data() + value.size(), v); if (ec != std::errc{} || v < min || v > max) { return fallback; } return v; }; // AP mode settings settings.apSsid = (ap_ssid != map.end() && !ap_ssid->second.empty()) ? ap_ssid->second : generateDefaultApSsid(); settings.apPassword = (ap_password != map.end()) ? ap_password->second : ""; settings.apOpenNetwork = (ap_open_network != map.end()) ? (ap_open_network->second == "1" || ap_open_network->second == "true") : false; settings.apChannel = (ap_channel != map.end()) ? static_cast(parseInt(ap_channel->second, 1, 13, 1)) : 1; // Security: If AP password is empty, generate a strong random password. // Skip this if user explicitly wants an open network. // Note: We only auto-generate for EMPTY passwords, not user-set ones. if (!settings.apOpenNetwork && isEmptyCredential(settings.apPassword)) { LOGGER.info("AP password is empty - generating secure random password"); // Generate 12-character random password (alphanumeric, ~71 bits of entropy) // WPA2 requires 8-63 characters, so 12 is well within range settings.apPassword = generateRandomCredential(12); // Persist the generated password immediately map[KEY_AP_PASSWORD] = settings.apPassword; if (file::savePropertiesFile(getSettingsFilePath(), map)) { LOGGER.info("Generated and saved new secure AP password"); } else { LOGGER.error("Failed to save generated AP password"); } } // Web server settings settings.webServerEnabled = (webserver_enabled != map.end()) ? (webserver_enabled->second == "1" || webserver_enabled->second == "true") : false; settings.webServerPort = (webserver_port != map.end()) ? static_cast(parseInt(webserver_port->second, 1, 65535, 80)) : 80; // Web server auth settings settings.webServerAuthEnabled = (webserver_auth_enabled != map.end()) ? (webserver_auth_enabled->second == "1" || webserver_auth_enabled->second == "true") : false; // Load credentials from file, defaulting to empty if not present settings.webServerUsername = (webserver_username != map.end()) ? webserver_username->second : ""; settings.webServerPassword = (webserver_password != map.end()) ? webserver_password->second : ""; // Security: If auth is enabled but credentials are empty, // generate strong random credentials and persist them immediately. // Note: We only auto-generate for EMPTY credentials, allowing users to set their own. if (settings.webServerAuthEnabled && (isEmptyCredential(settings.webServerUsername) || isEmptyCredential(settings.webServerPassword))) { LOGGER.info("Auth enabled with empty credentials - generating secure random credentials"); // Generate 12-character random credentials (alphanumeric, ~71 bits of entropy each) settings.webServerUsername = generateRandomCredential(12); settings.webServerPassword = generateRandomCredential(12); // Persist the generated credentials immediately // We need to save these to the file so they're consistent across reboots map[KEY_WEBSERVER_USERNAME] = settings.webServerUsername; map[KEY_WEBSERVER_PASSWORD] = settings.webServerPassword; if (file::savePropertiesFile(getSettingsFilePath(), map)) { LOGGER.info("Generated and saved new secure credentials"); } else { LOGGER.error("Failed to save generated credentials - auth may be inconsistent across reboots"); } } return true; } WebServerSettings getDefault() { return WebServerSettings{ .wifiEnabled = false, // Default WiFi OFF .wifiMode = WiFiMode::Station, .apSsid = generateDefaultApSsid(), .apPassword = "", // Empty - will be auto-generated on first use (unless apOpenNetwork) .apOpenNetwork = false, // Default to secured network .apChannel = 1, .webServerEnabled = false, // Default WebServer OFF for security .webServerPort = 80, .webServerAuthEnabled = false, // Auth disabled by default .webServerUsername = "", // Empty - will be generated if auth is enabled .webServerPassword = "" // Empty - will be generated if auth is enabled }; } WebServerSettings loadOrGetDefault() { WebServerSettings settings; bool loadedFromFlash = load(settings); if (!loadedFromFlash) { // First boot - use defaults (WiFi OFF, WebServer OFF) settings = getDefault(); // Save defaults to flash so toggle states persist if (save(settings)) { LOGGER.info("First boot - saved default settings (WiFi OFF WebServer OFF)"); } else { LOGGER.warn("First boot - failed to save default settings to flash"); } } return settings; } bool save(const WebServerSettings& settings) { std::map map; // WiFi settings map[KEY_WIFI_ENABLED] = settings.wifiEnabled ? "1" : "0"; map[KEY_WIFI_MODE] = (settings.wifiMode == WiFiMode::AccessPoint) ? "1" : "0"; // AP mode settings map[KEY_AP_SSID] = settings.apSsid; map[KEY_AP_PASSWORD] = settings.apPassword; map[KEY_AP_OPEN_NETWORK] = settings.apOpenNetwork ? "1" : "0"; map[KEY_AP_CHANNEL] = std::to_string(settings.apChannel); // Web server settings map[KEY_WEBSERVER_ENABLED] = settings.webServerEnabled ? "1" : "0"; map[KEY_WEBSERVER_PORT] = std::to_string(settings.webServerPort); // Web server auth settings map[KEY_WEBSERVER_AUTH_ENABLED] = settings.webServerAuthEnabled ? "1" : "0"; map[KEY_WEBSERVER_USERNAME] = settings.webServerUsername; map[KEY_WEBSERVER_PASSWORD] = settings.webServerPassword; auto settings_path = getSettingsFilePath(); if (!file::findOrCreateParentDirectory(settings_path, 0755)) { LOGGER.error("Failed to create parent dir for {}", settings_path); return false; } return file::savePropertiesFile(settings_path, map); } }