Politician 1.0.0
WiFi Auditing Library for ESP32
Loading...
Searching...
No Matches
PoliticianFingerprint.h
Go to the documentation of this file.
1#pragma once
2#include "Politician.h"
3#include <string.h>
4
5/**
6 * @brief PoliticianFingerprint: Passive WiFi Device Fingerprinting
7 *
8 * Identifies devices by matching observed MAC OUIs, probe request SSID
9 * patterns, HT Capabilities, Supported Rates, and IE presence flags against a
10 * built-in database. Fires a callback once per unique device per session
11 * (seen-MAC cache). RSSI is silently refreshed on re-sightings.
12 *
13 * DATABASE TIERS — define before including this header, or in build_flags:
14 * -DPOLITICIAN_FP_DB=FP_DB_BUILTIN curated WiFi consumer devices [default]
15 * -DPOLITICIAN_FP_DB=FP_DB_NONE no built-ins; user-defined entries only
16 *
17 * CAPACITY OVERRIDES (build_flags):
18 * -DPOLITICIAN_MAX_FP_USER=N max user-defined fingerprints [default 16]
19 * -DPOLITICIAN_MAX_FP_SEEN=N seen-MAC dedup cache size [default 64]
20 *
21 * USAGE:
22 * #include <PoliticianFingerprint.h>
23 * fingerprint::Detector fp(engine);
24 * fp.setCallback([](const DeviceRecord& dev) {
25 * Serial.printf("[FP] %s %s conf=%d%% flags=0x%02X\n",
26 * dev.vendor, dev.model, dev.confidence, dev.match_flags);
27 * });
28 * fp.setMinConfidence(60);
29 * fp.addFingerprint({"Acme", "Plug", {0xAA,0xBB,0xCC}, nullptr, 75});
30 *
31 * NOTE: Include in a single translation unit (your main sketch).
32 */
33
34// ─── Database Tier Defines ────────────────────────────────────────────────────
35#define FP_DB_NONE 0
36#define FP_DB_BUILTIN 1
37
38#ifndef POLITICIAN_FP_DB
39#define POLITICIAN_FP_DB FP_DB_BUILTIN
40#endif
41
42#if POLITICIAN_FP_DB > FP_DB_NONE
44#endif
45
46namespace politician {
47namespace fingerprint {
48
49#ifndef POLITICIAN_MAX_FP_USER
50#define POLITICIAN_MAX_FP_USER 16
51#endif
52
53#ifndef POLITICIAN_MAX_FP_SEEN
54#define POLITICIAN_MAX_FP_SEEN 64
55#endif
56
57// ─── Callback Type ────────────────────────────────────────────────────────────
58using DeviceFoundCb = void (*)(const DeviceRecord &rec);
59
60// ─── Detector ─────────────────────────────────────────────────────────────────
61class Detector {
62public:
64 _inst = this;
65 _userFpCount = 0;
66 _seenHead = 0;
67 _seenFill = 0;
68 memset(_seen, 0, sizeof(_seen));
69 memset(_userFps, 0, sizeof(_userFps));
70 engine._setFingerprintHook(_hook);
71 }
72
73 void setCallback(DeviceFoundCb cb) { _cb = cb; }
74 void setMinConfidence(uint8_t pct) { _minConf = pct; }
75
77 if (_userFpCount >= POLITICIAN_MAX_FP_USER) return false;
78 _userFps[_userFpCount++] = fp;
79 return true;
80 }
81
82 void resetCache() {
83 memset(_seen, 0, sizeof(_seen));
84 _seenHead = 0;
85 _seenFill = 0;
86 }
87
88private:
89 // ── IE signal container (parsed once per frame) ───────────────────────────
90 struct IeSignals {
91 bool has_ht;
92 uint8_t ht_cap_info[2];
93 bool has_ext_cap;
94 bool has_wmm;
95 bool has_wps;
96 uint8_t rate_sig[4];
97 bool has_rates;
98 };
99
100 static IeSignals _parseIe(const uint8_t *ie, uint16_t ie_len) {
101 IeSignals s = {};
102 if (!ie || !ie_len) return s;
103 uint16_t pos = 0;
104 while (pos + 2 <= ie_len) {
105 uint8_t tag = ie[pos];
106 uint8_t len = ie[pos + 1];
107 if (pos + 2 + (uint16_t)len > ie_len) break;
108 const uint8_t *body = ie + pos + 2;
109 switch (tag) {
110 case 1: // Supported Rates
111 if (len >= 4) { s.has_rates = true; memcpy(s.rate_sig, body, 4); }
112 break;
113 case 45: // HT Capabilities
114 if (len >= 2) { s.has_ht = true; s.ht_cap_info[0] = body[0]; s.ht_cap_info[1] = body[1]; }
115 break;
116 case 127: // Extended Capabilities
117 s.has_ext_cap = true;
118 break;
119 case 221: // Vendor Specific
120 if (len >= 4 && body[0] == 0x00 && body[1] == 0x50 && body[2] == 0xF2) {
121 if (body[3] == 0x01) s.has_wmm = true;
122 if (body[3] == 0x04) s.has_wps = true;
123 }
124 break;
125 }
126 pos += 2 + len;
127 }
128 return s;
129 }
130
131 static void _hook(const uint8_t *mac, const char *ssid, uint8_t ssid_len,
132 uint8_t ch, int8_t rssi, const uint8_t *ie, uint16_t ie_len) {
133 if (_inst) _inst->_process(mac, ssid, ssid_len, ch, rssi, ie, ie_len);
134 }
135
136 void _process(const uint8_t *mac, const char *ssid, uint8_t ssid_len,
137 uint8_t ch, int8_t rssi, const uint8_t *ie, uint16_t ie_len) {
138 if (mac[0] & 0x02) return; // skip randomized MACs
139
140 int idx = _findSeen(mac);
141 if (idx >= 0) { _seen[idx].rssi = rssi; return; } // silent RSSI refresh
142
143 IeSignals sig = _parseIe(ie, ie_len);
144
145 const char *bestVendor = nullptr;
146 const char *bestModel = nullptr;
147 uint8_t bestConf = 0;
148 uint8_t bestFlags = 0;
149
150 auto applyRule = [&](const char* vendor, const char* probeSsid, const char* model, uint8_t baseConf,
151 const uint8_t* ht_mask, const uint8_t* ht_info, const uint8_t* rate_sig,
152 uint8_t ie_flags_mask, uint8_t ie_flags) {
153 uint8_t conf = baseConf;
154 uint8_t flags = FP_MATCH_OUI;
155
156 // Probe SSID prefix match → +20
157 if (probeSsid && ssid_len > 0) {
158 size_t plen = strlen(probeSsid);
159 if ((size_t)ssid_len >= plen && memcmp(ssid, probeSsid, plen) == 0) {
160 conf = (conf + 20u > 100u) ? 100u : conf + 20u;
161 flags |= FP_MATCH_PROBE_SSID;
162 }
163 }
164
165 // HT Cap Info match → +15
166 if (ie && ht_mask && (ht_mask[0] || ht_mask[1]) && sig.has_ht) {
167 if ((sig.ht_cap_info[0] & ht_mask[0]) == (ht_info[0] & ht_mask[0]) &&
168 (sig.ht_cap_info[1] & ht_mask[1]) == (ht_info[1] & ht_mask[1])) {
169 conf = (conf + 15u > 100u) ? 100u : conf + 15u;
170 flags |= FP_MATCH_HT_CAP;
171 }
172 }
173
174 // Supported Rates signature match → +10
175 if (ie && rate_sig && rate_sig[0] && sig.has_rates && memcmp(sig.rate_sig, rate_sig, 4) == 0) {
176 conf = (conf + 10u > 100u) ? 100u : conf + 10u;
177 flags |= FP_MATCH_RATES;
178 }
179
180 // IE flags match → +5
181 if (ie && ie_flags_mask) {
182 uint8_t obs = 0;
183 if (!sig.has_ht) obs |= FP_IEF_NO_HT;
184 if (!sig.has_ext_cap) obs |= FP_IEF_NO_EXT_CAP;
185 if (sig.has_wmm) obs |= FP_IEF_HAS_WMM;
186 if (sig.has_wps) obs |= FP_IEF_HAS_WPS;
187 if ((obs & ie_flags_mask) == (ie_flags & ie_flags_mask)) {
188 conf = (conf + 5u > 100u) ? 100u : conf + 5u;
189 flags |= FP_MATCH_IE_FLAGS;
190 }
191 }
192
193 if (conf > bestConf) {
194 bestConf = conf;
195 bestFlags = flags;
196 bestVendor = vendor;
197 bestModel = model;
198 }
199 };
200
201 auto scanUser = [&](const DeviceFingerprint *tbl, size_t cnt) {
202 for (size_t i = 0; i < cnt; i++) {
203 const DeviceFingerprint &f = tbl[i];
204 if (memcmp(f.oui, mac, 3) != 0) continue;
205 applyRule(f.vendor, f.probeSsid, f.model, f.confidence, f.ht_cap_mask, f.ht_cap_info, f.rate_sig, f.ie_flags_mask, f.ie_flags);
206 }
207 };
208
209 scanUser(_userFps, _userFpCount);
210
211 #ifndef POLITICIAN_NO_DB
212 int left = 0, right = _FP_OUI_DB_COUNT - 1;
213 while (left <= right) {
214 int mid = left + (right - left) / 2;
215 int cmp = memcmp(_FP_OUI_DB[mid].oui, mac, 3);
216 if (cmp == 0) {
217 uint8_t v_idx = _FP_OUI_DB[mid].vendor_idx;
218 const BuiltinVendorRule &r = _FP_VENDOR_RULES[v_idx];
219 applyRule(_FP_VENDORS[v_idx], r.probeSsid, r.model, r.confidence, nullptr, nullptr, nullptr, 0, 0);
220 break;
221 }
222 if (cmp < 0) left = mid + 1;
223 else right = mid - 1;
224 }
225 #endif
226
227 if (!bestVendor || bestConf < _minConf) return;
228 _markSeen(mac, rssi);
229
230 if (!_cb) return;
231 DeviceRecord rec;
232 memset(&rec, 0, sizeof(rec));
233 memcpy(rec.mac, mac, 6);
234 strncpy(rec.vendor, bestVendor ? bestVendor : "", sizeof(rec.vendor) - 1);
235 if (bestModel) strncpy(rec.model, bestModel, sizeof(rec.model) - 1);
236 rec.channel = ch;
237 rec.rssi = rssi;
238 rec.confidence = bestConf;
239 rec.match_flags = bestFlags;
240 _cb(rec);
241 }
242
243 int _findSeen(const uint8_t *mac) const {
244 for (int i = 0; i < _seenFill; i++)
245 if (memcmp(_seen[i].mac, mac, 6) == 0) return i;
246 return -1;
247 }
248
249 void _markSeen(const uint8_t *mac, int8_t rssi) {
250 _seen[_seenHead].active = true;
251 memcpy(_seen[_seenHead].mac, mac, 6);
252 _seen[_seenHead].rssi = rssi;
253 _seenHead = (_seenHead + 1) % POLITICIAN_MAX_FP_SEEN;
254 if (_seenFill < POLITICIAN_MAX_FP_SEEN) _seenFill++;
255 }
256
257 inline static Detector *_inst = nullptr;
258
259 DeviceFoundCb _cb = nullptr;
260 uint8_t _minConf = 50;
261
262 DeviceFingerprint _userFps[POLITICIAN_MAX_FP_USER];
263 uint8_t _userFpCount;
264
265 struct SeenEntry { bool active; uint8_t mac[6]; int8_t rssi; };
266 SeenEntry _seen[POLITICIAN_MAX_FP_SEEN];
267 uint8_t _seenHead;
268 uint8_t _seenFill;
269};
270
271} // namespace fingerprint
272} // namespace politician
#define POLITICIAN_MAX_FP_SEEN
#define POLITICIAN_MAX_FP_USER
#define FP_MATCH_IE_FLAGS
#define FP_IEF_HAS_WMM
#define FP_MATCH_HT_CAP
#define FP_IEF_NO_EXT_CAP
#define FP_MATCH_OUI
#define FP_IEF_NO_HT
#define FP_MATCH_RATES
#define FP_MATCH_PROBE_SSID
#define FP_IEF_HAS_WPS
The core WiFi handshake capturing engine.
Definition Politician.h:91
bool addFingerprint(const DeviceFingerprint &fp)
Politician engine
Definition main.cpp:6
static const BuiltinOui _FP_OUI_DB[]
void(*)(const DeviceRecord &rec) DeviceFoundCb
static const char *const _FP_VENDORS[]
static const BuiltinVendorRule _FP_VENDOR_RULES[]
One fingerprint entry in the built-in or user-defined database.
A matched device, delivered to the DeviceFoundCb callback.