diff --git a/src/WifiCredentialStore.cpp b/src/WifiCredentialStore.cpp new file mode 100644 index 0000000..b0b26c2 --- /dev/null +++ b/src/WifiCredentialStore.cpp @@ -0,0 +1,155 @@ +#include "WifiCredentialStore.h" + +#include +#include +#include + +#include + +// Initialize the static instance +WifiCredentialStore WifiCredentialStore::instance; + +// File format version +constexpr uint8_t WIFI_FILE_VERSION = 1; + +// WiFi credentials file path +constexpr char WIFI_FILE[] = "/sd/.crosspoint/wifi.bin"; + +// Obfuscation key - "CrossPoint" in ASCII +// This is NOT cryptographic security, just prevents casual file reading +constexpr uint8_t OBFUSCATION_KEY[] = {0x43, 0x72, 0x6F, 0x73, 0x73, + 0x50, 0x6F, 0x69, 0x6E, 0x74}; +constexpr size_t KEY_LENGTH = sizeof(OBFUSCATION_KEY); + +void WifiCredentialStore::obfuscate(std::string& data) const { + for (size_t i = 0; i < data.size(); i++) { + data[i] ^= OBFUSCATION_KEY[i % KEY_LENGTH]; + } +} + +bool WifiCredentialStore::saveToFile() const { + // Make sure the directory exists + SD.mkdir("/.crosspoint"); + + std::ofstream file(WIFI_FILE, std::ios::binary); + if (!file) { + Serial.printf("[%lu] [WCS] Failed to open wifi.bin for writing\n", millis()); + return false; + } + + // Write header + serialization::writePod(file, WIFI_FILE_VERSION); + serialization::writePod(file, static_cast(credentials.size())); + + // Write each credential + for (const auto& cred : credentials) { + // Write SSID (plaintext - not sensitive) + serialization::writeString(file, cred.ssid); + + // Write password (obfuscated) + std::string obfuscatedPwd = cred.password; + obfuscate(obfuscatedPwd); + serialization::writeString(file, obfuscatedPwd); + } + + file.close(); + Serial.printf("[%lu] [WCS] Saved %zu WiFi credentials to file\n", millis(), credentials.size()); + return true; +} + +bool WifiCredentialStore::loadFromFile() { + if (!SD.exists(WIFI_FILE + 3)) { // +3 to skip "/sd" prefix + Serial.printf("[%lu] [WCS] WiFi credentials file does not exist\n", millis()); + return false; + } + + std::ifstream file(WIFI_FILE, std::ios::binary); + if (!file) { + Serial.printf("[%lu] [WCS] Failed to open wifi.bin for reading\n", millis()); + return false; + } + + // Read and verify version + uint8_t version; + serialization::readPod(file, version); + if (version != WIFI_FILE_VERSION) { + Serial.printf("[%lu] [WCS] Unknown file version: %u\n", millis(), version); + file.close(); + return false; + } + + // Read credential count + uint8_t count; + serialization::readPod(file, count); + + // Read credentials + credentials.clear(); + for (uint8_t i = 0; i < count && i < MAX_NETWORKS; i++) { + WifiCredential cred; + + // Read SSID + serialization::readString(file, cred.ssid); + + // Read and deobfuscate password + serialization::readString(file, cred.password); + obfuscate(cred.password); // XOR is symmetric, so same function deobfuscates + + credentials.push_back(cred); + } + + file.close(); + Serial.printf("[%lu] [WCS] Loaded %zu WiFi credentials from file\n", millis(), credentials.size()); + return true; +} + +bool WifiCredentialStore::addCredential(const std::string& ssid, const std::string& password) { + // Check if this SSID already exists and update it + for (auto& cred : credentials) { + if (cred.ssid == ssid) { + cred.password = password; + Serial.printf("[%lu] [WCS] Updated credentials for: %s\n", millis(), ssid.c_str()); + return saveToFile(); + } + } + + // Check if we've reached the limit + if (credentials.size() >= MAX_NETWORKS) { + Serial.printf("[%lu] [WCS] Cannot add more networks, limit of %zu reached\n", millis(), MAX_NETWORKS); + return false; + } + + // Add new credential + credentials.push_back({ssid, password}); + Serial.printf("[%lu] [WCS] Added credentials for: %s\n", millis(), ssid.c_str()); + return saveToFile(); +} + +bool WifiCredentialStore::removeCredential(const std::string& ssid) { + for (auto it = credentials.begin(); it != credentials.end(); ++it) { + if (it->ssid == ssid) { + credentials.erase(it); + Serial.printf("[%lu] [WCS] Removed credentials for: %s\n", millis(), ssid.c_str()); + return saveToFile(); + } + } + return false; // Not found +} + +const WifiCredential* WifiCredentialStore::findCredential(const std::string& ssid) const { + for (const auto& cred : credentials) { + if (cred.ssid == ssid) { + return &cred; + } + } + return nullptr; +} + +bool WifiCredentialStore::hasSavedCredential(const std::string& ssid) const { + return findCredential(ssid) != nullptr; +} + +void WifiCredentialStore::clearAll() { + credentials.clear(); + saveToFile(); + Serial.printf("[%lu] [WCS] Cleared all WiFi credentials\n", millis()); +} diff --git a/src/WifiCredentialStore.h b/src/WifiCredentialStore.h new file mode 100644 index 0000000..0004dc9 --- /dev/null +++ b/src/WifiCredentialStore.h @@ -0,0 +1,56 @@ +#pragma once +#include +#include + +struct WifiCredential { + std::string ssid; + std::string password; // Stored obfuscated in file +}; + +/** + * Singleton class for storing WiFi credentials on the SD card. + * Credentials are stored in /sd/.crosspoint/wifi.bin with basic + * XOR obfuscation to prevent casual reading (not cryptographically secure). + */ +class WifiCredentialStore { + private: + static WifiCredentialStore instance; + std::vector credentials; + + static constexpr size_t MAX_NETWORKS = 8; + + // Private constructor for singleton + WifiCredentialStore() = default; + + // XOR obfuscation (symmetric - same for encode/decode) + void obfuscate(std::string& data) const; + + public: + // Delete copy constructor and assignment + WifiCredentialStore(const WifiCredentialStore&) = delete; + WifiCredentialStore& operator=(const WifiCredentialStore&) = delete; + + // Get singleton instance + static WifiCredentialStore& getInstance() { return instance; } + + // Save/load from SD card + bool saveToFile() const; + bool loadFromFile(); + + // Credential management + bool addCredential(const std::string& ssid, const std::string& password); + bool removeCredential(const std::string& ssid); + const WifiCredential* findCredential(const std::string& ssid) const; + + // Get all stored credentials (for UI display) + const std::vector& getCredentials() const { return credentials; } + + // Check if a network is saved + bool hasSavedCredential(const std::string& ssid) const; + + // Clear all credentials + void clearAll(); +}; + +// Helper macro to access credentials store +#define WIFI_STORE WifiCredentialStore::getInstance() diff --git a/src/screens/WifiScreen.cpp b/src/screens/WifiScreen.cpp index 106575d..4e9d47e 100644 --- a/src/screens/WifiScreen.cpp +++ b/src/screens/WifiScreen.cpp @@ -4,6 +4,7 @@ #include #include "CrossPointWebServer.h" +#include "WifiCredentialStore.h" #include "config.h" void WifiScreen::taskTrampoline(void* param) { @@ -14,6 +15,9 @@ void WifiScreen::taskTrampoline(void* param) { void WifiScreen::onEnter() { renderingMutex = xSemaphoreCreateMutex(); + // Load saved WiFi credentials + WIFI_STORE.loadFromFile(); + // Reset state selectedNetworkIndex = 0; networks.clear(); @@ -21,6 +25,9 @@ void WifiScreen::onEnter() { selectedSSID.clear(); connectedIP.clear(); connectionError.clear(); + enteredPassword.clear(); + usedSavedPassword = false; + savePromptSelection = 0; keyboard.reset(); // Trigger first update to show scanning message @@ -93,6 +100,7 @@ void WifiScreen::processWifiScanResults() { network.ssid = WiFi.SSID(i).c_str(); network.rssi = WiFi.RSSI(i); network.isEncrypted = (WiFi.encryptionType(i) != WIFI_AUTH_OPEN); + network.hasSavedPassword = WIFI_STORE.hasSavedCredential(network.ssid); // Skip hidden networks (empty SSID) if (!network.ssid.empty()) { @@ -118,6 +126,18 @@ void WifiScreen::selectNetwork(int index) { const auto& network = networks[index]; selectedSSID = network.ssid; selectedRequiresPassword = network.isEncrypted; + usedSavedPassword = false; + enteredPassword.clear(); + + // Check if we have saved credentials for this network + const auto* savedCred = WIFI_STORE.findCredential(selectedSSID); + if (savedCred && !savedCred->password.empty()) { + // Use saved password - connect directly + enteredPassword = savedCred->password; + usedSavedPassword = true; + attemptConnection(); + return; + } if (selectedRequiresPassword) { // Show password entry @@ -145,8 +165,13 @@ void WifiScreen::attemptConnection() { WiFi.mode(WIFI_STA); - if (selectedRequiresPassword && keyboard) { - WiFi.begin(selectedSSID.c_str(), keyboard->getText().c_str()); + // Get password from keyboard if we just entered it + if (keyboard && !usedSavedPassword) { + enteredPassword = keyboard->getText(); + } + + if (selectedRequiresPassword && !enteredPassword.empty()) { + WiFi.begin(selectedSSID.c_str(), enteredPassword.c_str()); } else { WiFi.begin(selectedSSID.c_str()); } @@ -165,12 +190,19 @@ void WifiScreen::checkConnectionStatus() { char ipStr[16]; snprintf(ipStr, sizeof(ipStr), "%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]); connectedIP = ipStr; - state = WifiScreenState::CONNECTED; - updateRequired = true; // Start the web server crossPointWebServer.begin(); + // If we used a saved password, go directly to connected screen + // If we entered a new password, ask if user wants to save it + if (usedSavedPassword || enteredPassword.empty()) { + state = WifiScreenState::CONNECTED; + } else { + state = WifiScreenState::SAVE_PROMPT; + savePromptSelection = 0; // Default to "Yes" + } + updateRequired = true; return; } @@ -227,6 +259,36 @@ void WifiScreen::handleInput() { return; } + // Handle save prompt state + if (state == WifiScreenState::SAVE_PROMPT) { + if (inputManager.wasPressed(InputManager::BTN_LEFT) || + inputManager.wasPressed(InputManager::BTN_UP)) { + if (savePromptSelection > 0) { + savePromptSelection--; + updateRequired = true; + } + } else if (inputManager.wasPressed(InputManager::BTN_RIGHT) || + inputManager.wasPressed(InputManager::BTN_DOWN)) { + if (savePromptSelection < 1) { + savePromptSelection++; + updateRequired = true; + } + } else if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) { + if (savePromptSelection == 0) { + // User chose "Yes" - save the password + WIFI_STORE.addCredential(selectedSSID, enteredPassword); + } + // Move to connected screen + state = WifiScreenState::CONNECTED; + updateRequired = true; + } else if (inputManager.wasPressed(InputManager::BTN_BACK)) { + // Skip saving, go to connected screen + state = WifiScreenState::CONNECTED; + updateRequired = true; + } + return; + } + // Handle connected/failed states if (state == WifiScreenState::CONNECTED || state == WifiScreenState::CONNECTION_FAILED) { if (inputManager.wasPressed(InputManager::BTN_BACK) || @@ -321,6 +383,9 @@ void WifiScreen::render() const { case WifiScreenState::CONNECTED: renderConnected(); break; + case WifiScreenState::SAVE_PROMPT: + renderSavePrompt(); + break; case WifiScreenState::CONNECTION_FAILED: renderConnectionFailed(); break; @@ -367,14 +432,19 @@ void WifiScreen::renderNetworkList() const { // Draw network name (truncate if too long) std::string displayName = network.ssid; - if (displayName.length() > 18) { - displayName = displayName.substr(0, 15) + "..."; + if (displayName.length() > 16) { + displayName = displayName.substr(0, 13) + "..."; } renderer.drawText(UI_FONT_ID, 20, networkY, displayName.c_str()); // Draw signal strength indicator std::string signalStr = getSignalStrengthIndicator(network.rssi); - renderer.drawText(UI_FONT_ID, pageWidth - 80, networkY, signalStr.c_str()); + renderer.drawText(UI_FONT_ID, pageWidth - 90, networkY, signalStr.c_str()); + + // Draw saved indicator (checkmark) for networks with saved passwords + if (network.hasSavedPassword) { + renderer.drawText(UI_FONT_ID, pageWidth - 50, networkY, "+"); + } // Draw lock icon for encrypted networks if (network.isEncrypted) { @@ -397,7 +467,7 @@ void WifiScreen::renderNetworkList() const { } // Draw help text - renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 30, "OK: Connect | BACK: Exit | * = Encrypted"); + renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 30, "OK: Connect | * = Encrypted | + = Saved"); } void WifiScreen::renderPasswordEntry() const { @@ -461,6 +531,46 @@ void WifiScreen::renderConnected() const { renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, "Press any button to continue", true, REGULAR); } +void WifiScreen::renderSavePrompt() const { + const auto pageWidth = GfxRenderer::getScreenWidth(); + const auto pageHeight = GfxRenderer::getScreenHeight(); + const auto height = renderer.getLineHeight(UI_FONT_ID); + const auto top = (pageHeight - height * 3) / 2; + + renderer.drawCenteredText(READER_FONT_ID, top - 40, "Connected!", true, BOLD); + + std::string ssidInfo = "Network: " + selectedSSID; + if (ssidInfo.length() > 28) { + ssidInfo = ssidInfo.substr(0, 25) + "..."; + } + renderer.drawCenteredText(UI_FONT_ID, top, ssidInfo.c_str(), true, REGULAR); + + renderer.drawCenteredText(UI_FONT_ID, top + 40, "Save password for next time?", true, REGULAR); + + // Draw Yes/No buttons + const int buttonY = top + 80; + const int buttonWidth = 60; + const int buttonSpacing = 30; + const int totalWidth = buttonWidth * 2 + buttonSpacing; + const int startX = (pageWidth - totalWidth) / 2; + + // Draw "Yes" button + if (savePromptSelection == 0) { + renderer.drawText(UI_FONT_ID, startX, buttonY, "[Yes]"); + } else { + renderer.drawText(UI_FONT_ID, startX + 4, buttonY, "Yes"); + } + + // Draw "No" button + if (savePromptSelection == 1) { + renderer.drawText(UI_FONT_ID, startX + buttonWidth + buttonSpacing, buttonY, "[No]"); + } else { + renderer.drawText(UI_FONT_ID, startX + buttonWidth + buttonSpacing + 4, buttonY, "No"); + } + + renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, "LEFT/RIGHT: Select | OK: Confirm", true, REGULAR); +} + void WifiScreen::renderConnectionFailed() const { const auto pageHeight = GfxRenderer::getScreenHeight(); const auto height = renderer.getLineHeight(UI_FONT_ID); diff --git a/src/screens/WifiScreen.h b/src/screens/WifiScreen.h index fe97862..935ba51 100644 --- a/src/screens/WifiScreen.h +++ b/src/screens/WifiScreen.h @@ -17,6 +17,7 @@ struct WifiNetworkInfo { std::string ssid; int32_t rssi; bool isEncrypted; + bool hasSavedPassword; // Whether we have saved credentials for this network }; // WiFi screen states @@ -26,6 +27,7 @@ enum class WifiScreenState { PASSWORD_ENTRY, // Entering password for selected network CONNECTING, // Attempting to connect CONNECTED, // Successfully connected, showing IP + SAVE_PROMPT, // Asking user if they want to save the password CONNECTION_FAILED // Connection failed }; @@ -49,6 +51,15 @@ class WifiScreen final : public Screen { std::string connectedIP; std::string connectionError; + // Password to potentially save (from keyboard or saved credentials) + std::string enteredPassword; + + // Whether network was connected using a saved password (skip save prompt) + bool usedSavedPassword = false; + + // Save prompt selection (0 = Yes, 1 = No) + int savePromptSelection = 0; + // Connection timeout static constexpr unsigned long CONNECTION_TIMEOUT_MS = 15000; unsigned long connectionStartTime = 0; @@ -60,6 +71,7 @@ class WifiScreen final : public Screen { void renderPasswordEntry() const; void renderConnecting() const; void renderConnected() const; + void renderSavePrompt() const; void renderConnectionFailed() const; void startWifiScan();