handshake-capture-c6 v1.0.4: persist captured BSSIDs, PCAP EAPOL recount
- /cap_bssids.txt append on save; handshakeLoadPersistedBssids after SD mount - Boot order: SD then load file then first WiFi scan (reds match SD) - Pre-save walk of in-memory PCAP counting EAPOL-Key frames - SD_AppendLine / SD_ForEachLine; drop g_sd_ready clear on write fail - README + MUST_DO updates Made-with: Cursor
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
#include "SD_Card.h"
|
#include "SD_Card.h"
|
||||||
#include "LVGL_Driver.h"
|
#include "LVGL_Driver.h"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <cstdio>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|
||||||
// ─── Internal prototypes ───
|
// ─── Internal prototypes ───
|
||||||
@@ -15,6 +16,11 @@ static bool addClientToSlot(CaptureSlot* slot, const uint8_t* mac);
|
|||||||
static int getNextChannelSweep();
|
static int getNextChannelSweep();
|
||||||
static void sanitizeFilenameForFat(String& path);
|
static void sanitizeFilenameForFat(String& path);
|
||||||
static void discardSlotCapture(CaptureSlot* s, int slot_index);
|
static void discardSlotCapture(CaptureSlot* s, int slot_index);
|
||||||
|
static bool parsePersistedBssidLine(const char* s, uint8_t mac[6]);
|
||||||
|
static void formatBssidHexLine(const uint8_t* bssid, char* out, size_t cap);
|
||||||
|
static bool framePayloadIsEapolKey(const uint8_t* payload, uint16_t len);
|
||||||
|
static unsigned countEapolKeyFramesInPcap(const uint8_t* buf, size_t sz);
|
||||||
|
static void persistBssidLineCallback(const char* line, void* user);
|
||||||
|
|
||||||
// ─── Shared state ───
|
// ─── Shared state ───
|
||||||
|
|
||||||
@@ -63,6 +69,76 @@ static void markBssidCaptured(const uint8_t* bssid) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool parsePersistedBssidLine(const char* s, uint8_t mac[6]) {
|
||||||
|
if (s == nullptr) return false;
|
||||||
|
unsigned a, b, c, d, e, f;
|
||||||
|
if (sscanf(s, " %x:%x:%x:%x:%x:%x", &a, &b, &c, &d, &e, &f) != 6) return false;
|
||||||
|
mac[0] = (uint8_t)a;
|
||||||
|
mac[1] = (uint8_t)b;
|
||||||
|
mac[2] = (uint8_t)c;
|
||||||
|
mac[3] = (uint8_t)d;
|
||||||
|
mac[4] = (uint8_t)e;
|
||||||
|
mac[5] = (uint8_t)f;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void formatBssidHexLine(const uint8_t* bssid, char* out, size_t cap) {
|
||||||
|
snprintf(out, cap, "%02X:%02X:%02X:%02X:%02X:%02X\n",
|
||||||
|
bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool framePayloadIsEapolKey(const uint8_t* payload, uint16_t len) {
|
||||||
|
if (len < 24) return false;
|
||||||
|
uint8_t frame_type = payload[0];
|
||||||
|
bool is_qos = (frame_type == 0x88);
|
||||||
|
bool is_data = (frame_type == 0x08) || is_qos;
|
||||||
|
if (!is_data) return false;
|
||||||
|
bool to_ds = (payload[1] & 0x01) != 0;
|
||||||
|
bool from_ds = (payload[1] & 0x02) != 0;
|
||||||
|
uint16_t mhl = 24;
|
||||||
|
if (to_ds && from_ds) mhl += 6;
|
||||||
|
if (is_qos) mhl += 2;
|
||||||
|
if ((payload[1] & 0x40) != 0 || len < mhl + 8 + 4) return false;
|
||||||
|
if (payload[mhl] != 0xAA || payload[mhl + 1] != 0xAA || payload[mhl + 2] != 0x03) return false;
|
||||||
|
if (payload[mhl + 6] != 0x88 || payload[mhl + 7] != 0x8E) return false;
|
||||||
|
uint8_t eapol_type = payload[mhl + 9];
|
||||||
|
return eapol_type == 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint32_t pcapRdU32(const uint8_t* p) {
|
||||||
|
return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
|
||||||
|
}
|
||||||
|
|
||||||
|
static unsigned countEapolKeyFramesInPcap(const uint8_t* buf, size_t sz) {
|
||||||
|
if (sz < sizeof(pcap_global_header_t)) return 0;
|
||||||
|
size_t off = sizeof(pcap_global_header_t);
|
||||||
|
unsigned n = 0;
|
||||||
|
while (off + sizeof(pcap_record_header_t) <= sz) {
|
||||||
|
uint32_t incl = pcapRdU32(buf + off + 8);
|
||||||
|
off += sizeof(pcap_record_header_t);
|
||||||
|
if (incl > PCAP_MAX_SIZE || off + incl > sz) break;
|
||||||
|
if (incl >= 24 && framePayloadIsEapolKey(buf + off, (uint16_t)incl)) n++;
|
||||||
|
off += incl;
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void persistBssidLineCallback(const char* line, void* user) {
|
||||||
|
(void)user;
|
||||||
|
uint8_t mac[6];
|
||||||
|
if (!parsePersistedBssidLine(line, mac)) return;
|
||||||
|
markBssidCaptured(mac);
|
||||||
|
}
|
||||||
|
|
||||||
|
void handshakeLoadPersistedBssids() {
|
||||||
|
if (!SD_IsReady()) return;
|
||||||
|
if (!SD_ForEachLine(HANDSHAKE_CAPTURED_BSSID_FILE, persistBssidLineCallback, nullptr)) {
|
||||||
|
HANDSHAKE_LOGLN("[PERSIST] cap_bssids read failed");
|
||||||
|
} else {
|
||||||
|
HANDSHAKE_LOGF("[PERSIST] loaded BSSID cache from %s\n", HANDSHAKE_CAPTURED_BSSID_FILE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static void sanitizeFilenameForFat(String& path) {
|
static void sanitizeFilenameForFat(String& path) {
|
||||||
for (unsigned i = 0; i < path.length(); i++) {
|
for (unsigned i = 0; i < path.length(); i++) {
|
||||||
char c = path[i];
|
char c = path[i];
|
||||||
@@ -149,6 +225,16 @@ void handshakeCaptureProcessPending() {
|
|||||||
filename.replace(" ", "_");
|
filename.replace(" ", "_");
|
||||||
sanitizeFilenameForFat(filename);
|
sanitizeFilenameForFat(filename);
|
||||||
|
|
||||||
|
unsigned eapolInPcap = countEapolKeyFramesInPcap(s->pcap_buffer, s->pcap_size);
|
||||||
|
if (eapolInPcap < HANDSHAKE_EAPOL_FRAMES) {
|
||||||
|
HANDSHAKE_LOGF("[VALIDATE] PCAP has %u EAPOL-Key (need %d) — discard\n",
|
||||||
|
eapolInPcap, HANDSHAKE_EAPOL_FRAMES);
|
||||||
|
Ui_SetWifiStatus("Bad PCAP");
|
||||||
|
pending_save_slots[i] = -1;
|
||||||
|
discardSlotCapture(s, i);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
bool ok = SD_WriteFileAtomic(filename.c_str(), s->pcap_buffer, s->pcap_size);
|
bool ok = SD_WriteFileAtomic(filename.c_str(), s->pcap_buffer, s->pcap_size);
|
||||||
if (ok) {
|
if (ok) {
|
||||||
HANDSHAKE_LOGF("Saved 4-way %s (%d bytes)\n", filename.c_str(), (int)s->pcap_size);
|
HANDSHAKE_LOGF("Saved 4-way %s (%d bytes)\n", filename.c_str(), (int)s->pcap_size);
|
||||||
@@ -161,6 +247,11 @@ void handshakeCaptureProcessPending() {
|
|||||||
if (ok) {
|
if (ok) {
|
||||||
pending_save_slots[i] = -1;
|
pending_save_slots[i] = -1;
|
||||||
markBssidCaptured(s->bssid);
|
markBssidCaptured(s->bssid);
|
||||||
|
char pline[24];
|
||||||
|
formatBssidHexLine(s->bssid, pline, sizeof(pline));
|
||||||
|
if (!SD_AppendLine(HANDSHAKE_CAPTURED_BSSID_FILE, pline)) {
|
||||||
|
HANDSHAKE_LOGLN("[PERSIST] append BSSID failed (RAM cache still set)");
|
||||||
|
}
|
||||||
if (s->network_index >= 0 && s->network_index < MAX_NETWORKS) {
|
if (s->network_index >= 0 && s->network_index < MAX_NETWORKS) {
|
||||||
networks[s->network_index].handshake_captured = true;
|
networks[s->network_index].handshake_captured = true;
|
||||||
Ui_SetNetworkColor(s->network_index, lv_palette_main(LV_PALETTE_RED));
|
Ui_SetNetworkColor(s->network_index, lv_palette_main(LV_PALETTE_RED));
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
#include <SD.h>
|
#include <SD.h>
|
||||||
|
|
||||||
/** Semantic version (Serial banner + support). */
|
/** Semantic version (Serial banner + support). */
|
||||||
#define HANDSHAKE_FIRMWARE_VERSION "1.0.3"
|
#define HANDSHAKE_FIRMWARE_VERSION "1.0.4"
|
||||||
|
|
||||||
#ifndef HANDSHAKE_DEBUG
|
#ifndef HANDSHAKE_DEBUG
|
||||||
#define HANDSHAKE_DEBUG 0
|
#define HANDSHAKE_DEBUG 0
|
||||||
@@ -38,6 +38,9 @@
|
|||||||
/** Heuristic threshold only; frames are not validated as distinct 4-way steps. */
|
/** Heuristic threshold only; frames are not validated as distinct 4-way steps. */
|
||||||
#define HANDSHAKE_EAPOL_FRAMES 4
|
#define HANDSHAKE_EAPOL_FRAMES 4
|
||||||
|
|
||||||
|
/** SD file: one line per BSSID `AA:BB:CC:DD:EE:FF` — loaded on boot so restarts skip re-capture. */
|
||||||
|
#define HANDSHAKE_CAPTURED_BSSID_FILE "/cap_bssids.txt"
|
||||||
|
|
||||||
// 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.
|
||||||
#define MAX_SLOTS 2
|
#define MAX_SLOTS 2
|
||||||
#define MAX_NETWORKS 20
|
#define MAX_NETWORKS 20
|
||||||
@@ -103,6 +106,8 @@ extern int current_channel;
|
|||||||
extern CapturePhase capturePhase;
|
extern CapturePhase capturePhase;
|
||||||
|
|
||||||
void handshakeCaptureInit();
|
void handshakeCaptureInit();
|
||||||
|
/** Call after SD_Init() succeeds — merges file into RAM BSSID cache (survives reboot). */
|
||||||
|
void handshakeLoadPersistedBssids();
|
||||||
void handshakeCaptureProcessPending();
|
void handshakeCaptureProcessPending();
|
||||||
void refillSlots();
|
void refillSlots();
|
||||||
bool wasBssidCaptured(const uint8_t* bssid);
|
bool wasBssidCaptured(const uint8_t* bssid);
|
||||||
|
|||||||
@@ -2,6 +2,25 @@
|
|||||||
|
|
||||||
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**.
|
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**.
|
||||||
|
|
||||||
|
## Status vs **v1.0.3** (re-review)
|
||||||
|
|
||||||
|
| # | Item | Status |
|
||||||
|
|---|------|--------|
|
||||||
|
| 1 | Serialize shared SPI (LCD + SD) | **Done** — `SPI_Bus_Lock.cpp` recursive mutex; all `LCD_Write*` paths lock; `LCD_addWindow` wraps full flush; `SD_Init` / `SD_WriteFileAtomic` lock + `SPIBus_SetDisplayPaused` so `Timer_Loop` skips LVGL during SD. |
|
||||||
|
| 2 | Cap / document `eapol_count` | **Done** — increment only while `eapol_count < HANDSHAKE_EAPOL_FRAMES`; README notes heuristic. |
|
||||||
|
| 3 | 2.4 GHz–only sweep | **Done** — `isSupportedCaptureChannel`, `isCaptureable` filters; list shows `5G+` + grey for off-band secure APs; status line `off-band` count. |
|
||||||
|
| 4 | Production vs debug serial | **Done** — `HANDSHAKE_DEBUG` (default 0) + `HANDSHAKE_LOGF` / `HANDSHAKE_LOGLN`; boot version line always. |
|
||||||
|
| 5 | `USE_SOFTAP` default | **Done** — default `0` in `HandshakeCapture.h`. |
|
||||||
|
| 6 | Long SD write / WDT / UI | **Partially** — `yield()` after large write in `SD_WriteFileAtomic`; WDT not explicitly fed (usually OK on Arduino main task). |
|
||||||
|
| 7 | Rescan vs `network_index` | **Unchanged** — still no mid-capture rescan; README covers behavior. |
|
||||||
|
| 8 | Magic numbers | **Done** — timing + channel range centralized in `HandshakeCapture.h`. |
|
||||||
|
|
||||||
|
### Remaining nits (optional follow-ups)
|
||||||
|
|
||||||
|
1. **`SD_WriteFileAtomic`:** On failure it sets `g_sd_ready = false` even for a **single-file** error (e.g. disk full, bad filename) while the card may still be mounted. Consider clearing `g_sd_ready` only on errors that imply unmount, or always pair with a defined recovery path.
|
||||||
|
2. **`SD_Card.cpp` → `HandshakeCapture.h`:** Include is only for log macros — a tiny `HandshakeLog.h` would remove coupling.
|
||||||
|
3. **`LCD_Reset`:** Still toggles CS without the SPI mutex (boot-only; low risk).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. **Serialize shared SPI (LCD + SD)** — *critical*
|
## 1. **Serialize shared SPI (LCD + SD)** — *critical*
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ Production-oriented WiFi 4-way handshake capture for Waveshare ESP32-C6-LCD-1.47
|
|||||||
- **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.
|
- **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 **and across reboots**: each successful save appends that AP’s BSSID to **`/cap_bssids.txt`** (one line `AA:BB:…:FF`). On boot (after SD mounts), that file is merged into RAM so the device **does not re-target** APs already marked good.
|
||||||
|
- **Pre-save PCAP check:** Before writing a `.pcap`, the firmware **walks the in-memory PCAP** and counts **EAPOL-Key** frames (type 3, same LLC/SNAP rules as live capture). If the count is below four, the buffer is discarded (no file, no BSSID append). This does **not** verify MIC, nonces, or distinct M1–M4 — that still requires offline tools / knowing the passphrase.
|
||||||
- **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.
|
- **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).
|
||||||
@@ -24,7 +25,7 @@ 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).
|
||||||
- **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.
|
- **EAPOL counting is still heuristic:** Live path stops after **four** matching frames; the **PCAP recount** before SD write catches obvious buffer/counter mismatch. It still does **not** cryptographically validate the 4-way (MIC / replay / message order) — use Wireshark or `hcxpcapngtool` on a PC for that.
|
||||||
- **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.
|
- **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
|
||||||
|
|||||||
@@ -69,12 +69,49 @@ bool SD_WriteFileAtomic(const char* path, const uint8_t* data, size_t len) {
|
|||||||
ok = (file.write(data, len) == len);
|
ok = (file.write(data, len) == len);
|
||||||
file.close();
|
file.close();
|
||||||
}
|
}
|
||||||
if (!ok) {
|
|
||||||
g_sd_ready = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
endSDOperation();
|
endSDOperation();
|
||||||
|
|
||||||
if (len >= 1024) yield();
|
if (len >= 1024) yield();
|
||||||
return ok;
|
return ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool SD_AppendLine(const char* path, const char* line) {
|
||||||
|
if (path == nullptr || line == nullptr) return false;
|
||||||
|
beginSDOperation();
|
||||||
|
bool ok = false;
|
||||||
|
File f = SD.open(path, FILE_APPEND);
|
||||||
|
if (f) {
|
||||||
|
size_t n = strlen(line);
|
||||||
|
ok = (f.write((const uint8_t*)line, n) == n);
|
||||||
|
f.close();
|
||||||
|
}
|
||||||
|
endSDOperation();
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SD_ForEachLine(const char* path, SD_LineCallback cb, void* user) {
|
||||||
|
if (path == nullptr || cb == nullptr) return false;
|
||||||
|
beginSDOperation();
|
||||||
|
if (!SD.exists(path)) {
|
||||||
|
endSDOperation();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
File f = SD.open(path, FILE_READ);
|
||||||
|
if (!f) {
|
||||||
|
endSDOperation();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
char buf[80];
|
||||||
|
while (f.available()) {
|
||||||
|
int n = f.readBytesUntil('\n', buf, (int)sizeof(buf) - 1);
|
||||||
|
if (n < 0) n = 0;
|
||||||
|
buf[n] = '\0';
|
||||||
|
while (n > 0 && (buf[n - 1] == '\r' || buf[n - 1] == ' ')) {
|
||||||
|
buf[--n] = '\0';
|
||||||
|
}
|
||||||
|
if (n > 0) cb(buf, user);
|
||||||
|
}
|
||||||
|
f.close();
|
||||||
|
endSDOperation();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,3 +16,8 @@ bool SD_Init();
|
|||||||
*/
|
*/
|
||||||
bool SD_IsReady();
|
bool SD_IsReady();
|
||||||
bool SD_WriteFileAtomic(const char* path, const uint8_t* data, size_t len);
|
bool SD_WriteFileAtomic(const char* path, const uint8_t* data, size_t len);
|
||||||
|
/** Append one line (caller supplies \\n if desired). Uses same SPI lock as other SD ops. */
|
||||||
|
bool SD_AppendLine(const char* path, const char* line);
|
||||||
|
typedef void (*SD_LineCallback)(const char* line, void* user);
|
||||||
|
/** Read text file line-by-line; missing file is OK (returns true). */
|
||||||
|
bool SD_ForEachLine(const char* path, SD_LineCallback cb, void* user);
|
||||||
|
|||||||
@@ -117,15 +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.
|
// WiFi up before SD (shared SPI); first scan only after persistence load so reds match SD file.
|
||||||
handshakeCaptureInit();
|
handshakeCaptureInit();
|
||||||
WiFiScanner_Refresh();
|
|
||||||
|
|
||||||
HANDSHAKE_LOGLN("[SETUP] mounting SD after WiFi scan");
|
HANDSHAKE_LOGLN("[SETUP] mounting SD after WiFi init");
|
||||||
if (!SD_Init()) {
|
if (!SD_Init()) {
|
||||||
Ui_SetWifiStatus("SD: no card");
|
Ui_SetWifiStatus("SD: no card");
|
||||||
|
WiFiScanner_Refresh();
|
||||||
} else {
|
} else {
|
||||||
Ui_SetWifiStatus("SD OK");
|
Ui_SetWifiStatus("SD OK");
|
||||||
|
handshakeLoadPersistedBssids();
|
||||||
|
WiFiScanner_Refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
captureState = STATE_SCANNING;
|
captureState = STATE_SCANNING;
|
||||||
|
|||||||
Reference in New Issue
Block a user