Politician 1.0.0
WiFi Auditing Library for ESP32
Loading...
Searching...
No Matches
PoliticianStorage.h
Go to the documentation of this file.
1#pragma once
2
3#ifndef ARDUINO
4#error "PoliticianStorage.h requires the Arduino framework. Use ESP-IDF VFS and nvs_flash APIs directly."
5#endif
6
7#include <Arduino.h>
8#include <FS.h>
9#include <Preferences.h>
10#include <string>
11#include "Politician.h"
12#include "PoliticianFormat.h"
13
14namespace politician {
15namespace storage {
16
17namespace detail {
18/**
19 * @brief Writes a CSV-safe quoted field into output (RFC 4180).
20 * Wraps in double-quotes and escapes embedded double-quotes by doubling them.
21 */
22inline void escapeCsvField(const char *input, char *output, size_t maxLen) {
23 size_t out = 0;
24 if (out < maxLen - 1) output[out++] = '"';
25 for (size_t i = 0; input[i] && out < maxLen - 2; i++) {
26 if (input[i] == '"' && out < maxLen - 2) output[out++] = '"';
27 output[out++] = input[i];
28 }
29 if (out < maxLen - 1) output[out++] = '"';
30 output[out] = '\0';
31}
32} // namespace detail
33
34/**
35 * @brief Helper for writing HandshakeRecords and raw packets to a standard PCAPNG file.
36 *
37 * Two usage modes:
38 * - **Streaming (recommended for continuous capture):** call open() once, write()/writePacket()
39 * repeatedly, then close(). The file handle stays open across writes, avoiding repeated
40 * open/close overhead and SD wear.
41 * - **One-shot (legacy):** use the static append() / appendPacket() helpers which open and
42 * close the file on every call.
43 */
45public:
46 PcapngFileLogger() : _fs(nullptr), _open(false) {}
47
49
50 /**
51 * @brief Opens the PCAPNG file for streaming writes.
52 * Writes the global Section Header Block if the file is new or empty.
53 *
54 * @param fs The filesystem (e.g., SD, LittleFS)
55 * @param path The file path (e.g., "/captures.pcapng")
56 * @return true if the file was opened successfully
57 */
58 bool open(fs::FS &fs, const char *path) {
59 if (_open) close();
60 _fs = &fs;
61
62 bool isNew = !fs.exists(path);
63 if (!isNew) {
64 fs::File check = fs.open(path, FILE_READ);
65 if (check) { isNew = (check.size() == 0); check.close(); }
66 }
67
68 _file = fs.open(path, FILE_APPEND);
69 if (!_file) { _fs = nullptr; return false; }
70
71 if (isNew) {
72 uint8_t hdr[48];
73 size_t hl = format::writePcapngGlobalHeader(hdr);
74 _file.write(hdr, hl);
75 }
76 _open = true;
77 return true;
78 }
79
80 /**
81 * @brief Writes a HandshakeRecord to the open file.
82 * @return true if data was written, false if the logger is not open or serialization failed
83 */
84 bool write(const HandshakeRecord &rec) {
85 if (!_open) return false;
86 uint8_t buf[512];
87 size_t len = format::writePcapngRecord(rec, buf, sizeof(buf));
88 if (len > 0) { _file.write(buf, len); _file.flush(); }
89 return len > 0;
90 }
91
92 /**
93 * @brief Writes a raw 802.11 sniffer frame to the open file.
94 * @return true if data was written, false if the logger is not open or serialization failed
95 */
96 bool writePacket(const uint8_t *payload, uint16_t len, int8_t rssi, uint8_t channel, uint32_t ts_usec) {
97 if (!_open) return false;
98 uint8_t buf[2500]; // Max 802.11 frame is 2346 bytes
99 size_t wlen = format::writePcapngPacket(payload, len, rssi, channel, ts_usec, buf, sizeof(buf));
100 if (wlen > 0) { _file.write(buf, wlen); _file.flush(); }
101 return wlen > 0;
102 }
103
104 /** @brief Closes the underlying file handle. Safe to call multiple times. */
105 void close() {
106 if (_open) { _file.close(); _open = false; _fs = nullptr; }
107 }
108
109 /** @brief Returns true if the file is currently open for streaming. */
110 bool isOpen() const { return _open; }
111
112 // ── Static one-shot helpers (backward compatible) ──────────────────────────
113
114 /**
115 * @brief Appends a HandshakeRecord to a file as PCAPNG (opens and closes per call).
116 * Prefer the streaming API (open/write/close) for continuous capture.
117 */
118 static bool append(fs::FS &fs, const char *path, const HandshakeRecord &rec) {
119 PcapngFileLogger logger;
120 return logger.open(fs, path) && logger.write(rec);
121 }
122
123 /**
124 * @brief Appends a raw 802.11 sniffer frame to a PCAPNG file (opens and closes per call).
125 * Prefer the streaming API (open/writePacket/close) for continuous capture.
126 */
127 static bool appendPacket(fs::FS &fs, const char *path, const uint8_t *payload, uint16_t len,
128 int8_t rssi, uint8_t channel, uint32_t ts_usec) {
129 PcapngFileLogger logger;
130 return logger.open(fs, path) && logger.writePacket(payload, len, rssi, channel, ts_usec);
131 }
132
133private:
134 fs::FS *_fs;
135 fs::File _file;
136 bool _open;
137};
138
139/**
140 * @brief Helper for writing HandshakeRecords to an HC22000 text file.
141 *
142 * Supports both streaming (open/write/close) and one-shot (static append()) usage.
143 */
145public:
146 Hc22000FileLogger() : _open(false) {}
147
149
150 /**
151 * @brief Opens the HC22000 file for streaming writes.
152 * @return true if successful
153 */
154 bool open(fs::FS &fs, const char *path) {
155 if (_open) close();
156 _file = fs.open(path, FILE_APPEND);
157 if (!_file) return false;
158 _open = true;
159 return true;
160 }
161
162 /**
163 * @brief Writes a HandshakeRecord to the open file.
164 * @return true if data was written
165 */
166 bool write(const HandshakeRecord &rec) {
167 if (!_open) return false;
168 std::string str = format::toHC22000(rec);
169 if (!str.empty()) { _file.println(str.c_str()); _file.flush(); }
170 return !str.empty();
171 }
172
173 /** @brief Closes the underlying file handle. Safe to call multiple times. */
174 void close() {
175 if (_open) { _file.close(); _open = false; }
176 }
177
178 /** @brief Returns true if the file is currently open for streaming. */
179 bool isOpen() const { return _open; }
180
181 /**
182 * @brief Appends a HandshakeRecord to a file as an HC22000 string (opens and closes per call).
183 * Prefer the streaming API (open/write/close) for continuous capture.
184 */
185 static bool append(fs::FS &fs, const char *path, const HandshakeRecord &rec) {
186 Hc22000FileLogger logger;
187 return logger.open(fs, path) && logger.write(rec);
188 }
189
190private:
191 fs::File _file;
192 bool _open;
193};
194
195/**
196 * @brief Helper for writing precise GPS location coordinates to a Wigle.net compatible CSV file.
197 *
198 * Wigle.net has a strict CSV format starting with a specific header:
199 * MAC,SSID,AuthMode,FirstSeen,Channel,RSSI,CurrentLatitude,CurrentLongitude,AltitudeMeters,AccuracyMeters,Type
200 */
202public:
203 /**
204 * @brief Appends a HandshakeRecord's details alongside GPS coordinates to a Wigle CSV.
205 *
206 * @param fs The filesystem (e.g., SD, LittleFS)
207 * @param path The path to the file (e.g., "/wardrive.csv")
208 * @param rec The captured HandshakeRecord
209 * @param lat Current GPS Latitude
210 * @param lon Current GPS Longitude
211 * @param alt (Optional) Current GPS Altitude in meters
212 * @param acc (Optional) GPS Accuracy radius in meters
213 * @return true if successful, false if file could not be opened
214 */
215 static bool append(fs::FS &fs, const char* path, const HandshakeRecord& rec,
216 float lat, float lon, float alt = 0.0, float acc = 10.0,
217 const char* timestamp = nullptr) {
218 fs::File file = _openWithHeader(fs, path);
219 if (!file) return false;
220
221 char ssidEscaped[72]; // 32 chars worst-case doubled + 2 quotes + NUL
222 detail::escapeCsvField(rec.ssid, ssidEscaped, sizeof(ssidEscaped));
223
224 char line[256];
225 snprintf(line, sizeof(line), "%02X:%02X:%02X:%02X:%02X:%02X,%s,%s,%s,%d,%d,%.6f,%.6f,%.1f,%.1f,WIFI",
226 rec.bssid[0], rec.bssid[1], rec.bssid[2], rec.bssid[3], rec.bssid[4], rec.bssid[5],
227 ssidEscaped, _authStr(rec.enc), timestamp ? timestamp : "1970-01-01 00:00:00",
228 rec.channel, rec.rssi, lat, lon, alt, acc);
229
230 file.println(line);
231 file.flush();
232 file.close();
233 return true;
234 }
235
236 /**
237 * @brief Appends any discovered ApRecord alongside GPS coordinates to a Wigle CSV.
238 * Use this with setApFoundCallback() to log all networks, not just captured ones.
239 *
240 * @param fs The filesystem (e.g., SD, LittleFS)
241 * @param path The path to the file (e.g., "/wardrive.csv")
242 * @param ap The discovered ApRecord
243 * @param lat Current GPS Latitude
244 * @param lon Current GPS Longitude
245 * @param alt (Optional) Current GPS Altitude in meters
246 * @param acc (Optional) GPS Accuracy radius in meters
247 * @return true if successful, false if file could not be opened
248 */
249 static bool appendAp(fs::FS &fs, const char* path, const ApRecord& ap,
250 float lat, float lon, float alt = 0.0, float acc = 10.0,
251 const char* timestamp = nullptr) {
252 fs::File file = _openWithHeader(fs, path);
253 if (!file) return false;
254
255 char ssidEscaped[72];
256 detail::escapeCsvField(ap.ssid, ssidEscaped, sizeof(ssidEscaped));
257
258 char line[256];
259 snprintf(line, sizeof(line), "%02X:%02X:%02X:%02X:%02X:%02X,%s,%s,%s,%d,%d,%.6f,%.6f,%.1f,%.1f,WIFI",
260 ap.bssid[0], ap.bssid[1], ap.bssid[2], ap.bssid[3], ap.bssid[4], ap.bssid[5],
261 ssidEscaped, _authStr(ap.enc), timestamp ? timestamp : "1970-01-01 00:00:00",
262 ap.channel, ap.rssi, lat, lon, alt, acc);
263
264 file.println(line);
265 file.flush();
266 file.close();
267 return true;
268 }
269
270private:
271 static const char* _authStr(uint8_t enc) {
272 switch (enc) {
273 case 1: return "[WEP][ESS]";
274 case 2: return "[WPA-PSK-TKIP][ESS]";
275 case 3: return "[WPA2-PSK-CCMP][ESS]";
276 case 4: return "[WPA2-EAP-CCMP][ESS]";
277 default: return "[ESS]";
278 }
279 }
280
281 static fs::File _openWithHeader(fs::FS &fs, const char* path) {
282 bool isNew = !fs.exists(path);
283 if (!isNew) {
284 fs::File check = fs.open(path, FILE_READ);
285 if (check) { isNew = (check.size() == 0); check.close(); }
286 }
287 fs::File file = fs.open(path, FILE_APPEND);
288 if (!file) return file;
289 if (isNew) {
290 file.println("WigleWifi-1.4,appRelease=1.0,model=Politician,release=1.0,device=ESP32,display=1.0,board=ESP32,brand=Espressif");
291 file.println("MAC,SSID,AuthMode,FirstSeen,Channel,RSSI,CurrentLatitude,CurrentLongitude,AltitudeMeters,AccuracyMeters,Type");
292 }
293 return file;
294 }
295};
296
297/**
298 * @brief Helper for logging harvested 802.1X Enterprise Credentials.
299 * It writes a Clean CSV file containing BSSID, Client, and Plaintext Identity.
300 */
302public:
303 static bool append(fs::FS &fs, const char *path, const EapIdentityRecord &rec) {
304 bool isNew = !fs.exists(path);
305
306 fs::File file = fs.open(path, FILE_APPEND);
307 if (!file) return false;
308
309 if (isNew) {
310 file.println("Enterprise BSSID,Client MAC,Plaintext Identity,Channel,RSSI");
311 }
312
313 char identityEscaped[136]; // 64 chars worst-case doubled + 2 quotes + NUL
314 detail::escapeCsvField(rec.identity, identityEscaped, sizeof(identityEscaped));
315
316 char line[256];
317 snprintf(line, sizeof(line), "%02X:%02X:%02X:%02X:%02X:%02X,%02X:%02X:%02X:%02X:%02X:%02X,%s,%d,%d",
318 rec.bssid[0], rec.bssid[1], rec.bssid[2], rec.bssid[3], rec.bssid[4], rec.bssid[5],
319 rec.client[0], rec.client[1], rec.client[2], rec.client[3], rec.client[4], rec.client[5],
320 identityEscaped, rec.channel, rec.rssi);
321
322 file.println(line);
323 file.flush();
324 file.close();
325 return true;
326 }
327};
328
329/**
330 * @brief Helper for persistently storing captured BSSIDs in NVS memory.
331 * This ensures that previously captured networks aren't attacked again after a reboot.
332 */
334private:
335 Preferences _prefs;
336 char _ns[16];
337 static const int MAX_STORED = 128;
338 uint8_t _cache[MAX_STORED][6];
339 size_t _count;
340 bool _dirty;
341
342public:
343 NvsBssidCache(const char* ns = "wardrive") : _count(0), _dirty(false) {
344 if (strlen(ns) > 15)
345 Serial.println("[NvsBssidCache] WARNING: namespace name exceeds 15 chars and will be truncated");
346 strncpy(_ns, ns, sizeof(_ns) - 1);
347 _ns[sizeof(_ns) - 1] = '\0';
348 memset(_cache, 0, sizeof(_cache));
349 }
350
351 /**
352 * @brief Initializes the NVS memory and loads the cached BSSIDs into RAM.
353 */
354 void begin() {
355 _prefs.begin(_ns, false);
356 size_t bytes = _prefs.getBytes("bssids", _cache, sizeof(_cache));
357 _count = bytes / 6;
358 if (_count > MAX_STORED) _count = MAX_STORED; // Safety parameter
359 _dirty = false;
360 }
361
362 /**
363 * @brief Feeds the loaded BSSIDs into the Politician engine so it knows to ignore them.
364 * @param engine Reference to your active Politician instance
365 */
367 for (size_t i = 0; i < _count; i++) {
368 engine.markCaptured(_cache[i]);
369 }
370 }
371
372 /**
373 * @brief Adds a newly captured BSSID to the in-RAM cache.
374 * The change is not written to NVS until flush() is called.
375 * @param bssid The 6-byte BSSID to save.
376 * @return true if added, false if it already exists or the cache is full.
377 */
378 bool add(const uint8_t* bssid) {
379 for (size_t i = 0; i < _count; i++) {
380 if (memcmp(_cache[i], bssid, 6) == 0) return false; // Already cached
381 }
382 if (_count >= MAX_STORED) return false; // Cache full
383
384 memcpy(_cache[_count], bssid, 6);
385 _count++;
386 _dirty = true;
387 return true;
388 }
389
390 /**
391 * @brief Writes any pending in-RAM changes to NVS.
392 * Call this periodically (e.g., in end() or on a timer) to batch flash writes.
393 * @return true if data was written, false if there were no pending changes.
394 */
395 bool flush() {
396 if (!_dirty) return false;
397 _prefs.putBytes("bssids", _cache, _count * 6);
398 _dirty = false;
399 return true;
400 }
401
402 /** @brief Returns true if there are in-RAM changes not yet written to NVS. */
403 bool isDirty() const { return _dirty; }
404
405 /**
406 * @brief Returns the number of BSSIDs currently stored.
407 */
408 size_t count() const { return _count; }
409
410 /**
411 * @brief Clears the entire cache from both RAM and NVS immediately.
412 */
413 void clear() {
414 _count = 0;
415 _dirty = false;
416 _prefs.remove("bssids");
417 }
418};
419
420} // namespace storage
421} // namespace politician
The core WiFi handshake capturing engine.
Definition Politician.h:91
Helper for logging harvested 802.1X Enterprise Credentials.
static bool append(fs::FS &fs, const char *path, const EapIdentityRecord &rec)
Helper for writing HandshakeRecords to an HC22000 text file.
bool open(fs::FS &fs, const char *path)
Opens the HC22000 file for streaming writes.
void close()
Closes the underlying file handle.
bool isOpen() const
Returns true if the file is currently open for streaming.
bool write(const HandshakeRecord &rec)
Writes a HandshakeRecord to the open file.
static bool append(fs::FS &fs, const char *path, const HandshakeRecord &rec)
Appends a HandshakeRecord to a file as an HC22000 string (opens and closes per call).
Helper for persistently storing captured BSSIDs in NVS memory.
NvsBssidCache(const char *ns="wardrive")
bool isDirty() const
Returns true if there are in-RAM changes not yet written to NVS.
bool add(const uint8_t *bssid)
Adds a newly captured BSSID to the in-RAM cache.
bool flush()
Writes any pending in-RAM changes to NVS.
size_t count() const
Returns the number of BSSIDs currently stored.
void loadInto(Politician &engine)
Feeds the loaded BSSIDs into the Politician engine so it knows to ignore them.
void begin()
Initializes the NVS memory and loads the cached BSSIDs into RAM.
void clear()
Clears the entire cache from both RAM and NVS immediately.
Helper for writing HandshakeRecords and raw packets to a standard PCAPNG file.
static bool appendPacket(fs::FS &fs, const char *path, const uint8_t *payload, uint16_t len, int8_t rssi, uint8_t channel, uint32_t ts_usec)
Appends a raw 802.11 sniffer frame to a PCAPNG file (opens and closes per call).
bool isOpen() const
Returns true if the file is currently open for streaming.
void close()
Closes the underlying file handle.
bool open(fs::FS &fs, const char *path)
Opens the PCAPNG file for streaming writes.
static bool append(fs::FS &fs, const char *path, const HandshakeRecord &rec)
Appends a HandshakeRecord to a file as PCAPNG (opens and closes per call).
bool write(const HandshakeRecord &rec)
Writes a HandshakeRecord to the open file.
bool writePacket(const uint8_t *payload, uint16_t len, int8_t rssi, uint8_t channel, uint32_t ts_usec)
Writes a raw 802.11 sniffer frame to the open file.
Helper for writing precise GPS location coordinates to a Wigle.net compatible CSV file.
static bool append(fs::FS &fs, const char *path, const HandshakeRecord &rec, float lat, float lon, float alt=0.0, float acc=10.0, const char *timestamp=nullptr)
Appends a HandshakeRecord's details alongside GPS coordinates to a Wigle CSV.
static bool appendAp(fs::FS &fs, const char *path, const ApRecord &ap, float lat, float lon, float alt=0.0, float acc=10.0, const char *timestamp=nullptr)
Appends any discovered ApRecord alongside GPS coordinates to a Wigle CSV.
Politician engine
Definition main.cpp:6
size_t writePcapngPacket(const uint8_t *payload, size_t payload_len, int8_t rssi, uint8_t channel, uint64_t ts_usec, uint8_t *buffer, size_t max_len)
Serializes a Raw 802.11 Frame into a PCAPNG Enhanced Packet Block.
std::string toHC22000(const HandshakeRecord &rec)
Converts a captured HandshakeRecord into the Hashcat 22000 (hc22000) text format.
size_t writePcapngRecord(const HandshakeRecord &rec, uint8_t *buffer, size_t max_len)
Serializes a HandshakeRecord into PCAPNG Enhanced Packet Blocks.
size_t writePcapngGlobalHeader(uint8_t *buffer)
Writes a PCAPNG Global Header (SHB + IDB).
void escapeCsvField(const char *input, char *output, size_t maxLen)
Writes a CSV-safe quoted field into output (RFC 4180).
Snapshot of a discovered Access Point from the internal cache.
A harvested 802.1X Enterprise plaintext identity, delivered to the IdentityCb callback.
A captured handshake or PMKID record delivered to the EapolCb callback.