# Handshakes Mark VII Parity — Design Spec - **Date:** 2026-08-11 - **Status:** Draft (pending user review) - **Owner:** WiFi Pineapple Pager expansion project - **Applies to:** `payload/user/general/pager-webui/server.py`, `payload/user/general/pager-webui/www/`, `tests/` - **Supercedes §3.3 of:** `docs/superpowers/specs/2026-08-11-recon-markvii-design.md` (that section restyled the file-listing table; this spec replaces it with the real Mark VII handshake table) ## 1. Goal Rework the Pager WebUI **Recon → Handshakes** tab (`#/recon/handshakes`) so it looks and behaves like the stock Mark VII handshakes page: the same table (**BSSID, Client, Source, Type, Captured, Message 1–4, Beacon Frame**, action icons) with the same **Download** and **Delete** behavior, plus the **settings dialog** (handshake location + "Delete All Handshakes"). Built **entirely from Pager-local data** (loot files in `/root/loot/handshakes` + the recon.db `handshake`/`wifi_device` tables). The Pager's Go daemon does **not** expose `/api/pineap/handshakes` (verified 404 on-device + `docs/specs/2026-08-11-pineapple-ui-clone-design.md`), so the Mark VII shape is synthesized by `server.py`, matching how every other PineAP/Recon endpoint is already implemented. ## 2. Mark VII reference (captured 2026-08-11) Source of truth: live `GET /api/pineap/handshakes` on `172.16.42.1:1471` (hak5pineapple) and the handshakes component in the cached Angular bundle `%TEMP%\opencode\oldui-main.js`. ### 2.1 API shape `GET /api/pineap/handshakes` → `{ "handshakes": [ { ... } ] }`. Each record: ```json { "file_exists": true, "mac": "86:18:98:BE:CF:A5", // AP/BSSID (colon MAC, case as stored) "client": "D6:9B:56:ED:28:82", // client MAC, or an Evil-Twin label "source": "Recon", // "Recon" | "Evil WPA/2 Twin" "type": "full", // "full" | "eviltwin" "timestamp": "2023-12-05T16:03:18Z", "in_db": true, // handshake also present in recon DB "part_mask": 15, // bit 1=m1, 2=m2, 4=m3, 8=m4 "beacon": true, "extension": "pcap", // pcap | 22000 "location": "/root/handshakes/86-18-98-BE-CF-A5_D6-9B-56-ED-28-82_full.pcap" } ``` ### 2.2 Page - Card title row: **"Captured WPA Handshakes"** (left) + `settings` gear icon button (right). Gear opens a dialog: handshake **location** (from `GET /api/pineap/handshakes/location`) and a **Delete All Handshakes** button (`DELETE /api/pineap/handshakes/`). On success/error a small green check / red X flashes beside the title for ~3s / ~5s. - Table columns: `BSSID, Client, Source, Type, Captured, Message 1, Message 2, Message 3, Message 4, Beacon Frame, [action]`. - **BSSID / Client**: colon MACs as returned. - **Source**: `"Recon"` or `"Evil WPA/2 Twin"`. - **Type**: `{{type|titlecase}} {{format}}` where `format` derives from `extension` (`pcap`→`PCAP`, `22000`→`Hashcat`, else `Unknown`) — e.g. `Full PCAP`, `Hashcat`. - **Captured**: localized date-time of `timestamp`. - **Message 1–4 / Beacon Frame**: when `in_db` is true → green **check** if the bit (`m1..m4`, `beacon`) is set, red **X** if not. When `in_db` is false → grey **?** icon with tooltip "This information isn't available. This is common when a handshake file has been found, but the associated Recon scan has been lost or deleted." - **[action]**: `file_download` icon button + `delete` icon button (warn color). - Empty state: `"No Handshakes Available"`. - Component logic (from bundle): `getHandshakes()` maps each record's `extension`→`format` and computes `m1=part_mask&1 … m4=part_mask&8`; `deleteHandshake(hs)` calls `DELETE /api/pineap/handshakes/delete` with the record as body, then reloads and flashes success/error; `downloadHandshake` saves the file as `__.`. ## 3. Pager data model - **Loot files:** `/root/loot/handshakes/` (Pager native path; matches pager-webui `LOOT_HS_DIR`). Native naming inferred from on-device payloads (`handshake_sanitiser`, `deduplicate`, `handshake_2_usb`) and `pineapd` strings: - full: `___handshake.pcap|.22000` - partial: `___handshake_partial.pcap|.22000` - incomplete: `…_handshake_incomplete.pcap|.22000` - MACs colon-separated (may be uppercase/lowercase); Windows-normalised copies use dashes. The parser must accept both. - **recon.db `handshake` table:** `hash, scan, stahash, aphash, time, beacon BLOB, hs1 BLOB, hs2 BLOB, hs3 BLOB, hs4 BLOB`. The pager-webui resolves `aphash/stahash` → MACs via `wifi_device` (pattern already used by `recon_scan_data`). This supplies `part_mask`, `beacon`, `in_db`, and a fallback timestamp. - **Sources:** the Pager has no Evil-WPA/2-Twin mode, so `source` is always `"Recon"` and `type` is `full` / `partial` / `incomplete`. ## 4. Backend (`server.py`) ### 4.1 `handshakes_data()` → `{ files, handshakes }` Keep the existing `files` array unchanged (the dashboard reads it). Add a `handshakes` array in the Mark VII shape. Build order: 1. `os.listdir(LOOT_HS_DIR)` + `os.stat` per file (existing). 2. **If no files → return `{files, handshakes: []}` immediately — zero DB reads** (the compute mitigation). 3. Parse each filename (see §4.2) → `{ap, client, kind, ext, ts}`. 4. **One batched correlation read** (only when files exist): `SELECT h.time, (h.hs1 IS NOT NULL AND length(h.hs1)>0) AS m1, (h.hs2 IS NOT NULL AND length(h.hs2)>0) AS m2, (h.hs3 IS NOT NULL AND length(h.hs3)>0) AS m3, (h.hs4 IS NOT NULL AND length(h.hs4)>0) AS m4, (h.beacon IS NOT NULL AND length(h.beacon)>0) AS beacon, w1.mac AS ap, w2.mac AS sta FROM handshake h JOIN wifi_device w1 ON w1.hash = h.aphash JOIN wifi_device w2 ON w2.hash = h.stahash WHERE h.time >= ORDER BY h.time` (a single `_db_rows` call; `handshake.time` is epoch seconds). Build a dict keyed by normalized `(ap, sta)` keeping the latest row. 5. Compose one record per file (§4.3). Pure Python dict lookups — O(1) per file. The join resolves hashes to MACs directly, so exactly **one** sqlite read is needed. Total added cost per request: at most 1 `_db_rows` call, only when the loot dir is non-empty; filename parsing is microseconds. ### 4.2 Filename parser Tolerant regex, accepts colon or dash MACs, optional `_handshake` token, optional quality suffix, any extension: ``` ^(\d+)_([0-9a-fA-F-:]+)_([0-9a-fA-F-:]+)_handshake(?:_(full|partial|incomplete))?\.([A-Za-z0-9]+)$ ``` Fallback: any unparseable file still appears as a row with `mac:'--', client:'--', type:'full', source:'Recon'`, `in_db:false` — it keeps Download/Delete and shows "?" glyphs (matches Mark VII's file-without-DB-record presentation). ### 4.3 Record composition (per file) | field | source | |---|---| | `mac` | parsed AP MAC (colon, case as stored) | | `client` | parsed client MAC (or `--`) | | `source` | `"Recon"` | | `type` | `full` / `partial` / `incomplete` (from filename; default `full`) | | `timestamp` | epoch seconds (parsed `ts` prefix; fallback `st_mtime`; DB `time` wins if present) | | `in_db` | bool — matching `(ap,sta)` row in the correlation dict | | `part_mask` | `m1|m2|m3|m4` from DB row (0 when not in DB) | | `beacon` | bool from DB row (false when not in DB) | | `extension` | file extension | | `name` | bare filename (pager-webui convenience field; used for download/delete) | | `location` | `LOOT_HS_DIR + '/' + name` | | `file_exists` | true | ### 4.4 New routes - `GET /api/pineap/handshakes/location` → `{location: LOOT_HS_DIR}` (mirrors Mark VII; read-only). - `DELETE /api/pineap/handshakes/all` → remove every file in `LOOT_HS_DIR`, return updated `handshakes_data()`. Mirrors Mark VII "delete all". - Existing `GET /api/pineap/handshakes/{name}` (download) and `DELETE /api/pineap/handshakes` `{name}` (per-row delete) unchanged. Optionally set the download's `Content-Disposition` filename to the Mark VII `__.` form when the name parses. ## 5. Frontend (`www/`) ### 5.1 `views.recon_handshakes` (rewrite of `views.js:911`) Keep: page title "Recon", `RECON_TABS` tab bar, card title row "Captured WPA Handshakes" + gear, and the Pager-specific `Download all (zip)` / `Archive` / `Refresh` row (user decision: keep, don't match Mark VII pixel-for-pixel there). Replace the file table with the Mark VII table: - Custom table build (the generic `table()` helper only renders text; this needs glyphs + icon buttons), header row exactly: `BSSID, Client, Source, Type, Captured, Message 1, Message 2, Message 3, Message 4, Beacon Frame, ""`. - Cells: - **BSSID / Client**: text. - **Source**: `"Recon"`. - **Type**: `${title(type)} ${format}` via existing `hsType()` (PCAP / Hashcat / Unknown). - **Captured**: `fmtTime(timestamp)`. - **Message 1–4**: `!in_db` → `?` icon with `title` tooltip (Mark VII wording); else check icon when `part_mask & bit`, X icon when not. - **Beacon Frame**: `!in_db` → `?` icon; else check when `beacon`, X when not. - **Action**: `file_download` icon btn → `window.location = apiBase + '/api/pineap/handshakes/' + encodeURIComponent(name)`; `delete` icon btn (warn) → `PagerAPI.del('/api/pineap/handshakes', {name})` then reload — no confirm (matches Mark VII). - Delete / delete-all success → green check flash; failure → red X + error text flash (replaces the current silent `.catch`). - Empty state text: `"No Handshakes Available"`. - Settings gear opens the dialog (§5.2). The previous "toast" behavior is removed. ### 5.2 Settings dialog Simple modal overlay (Mark VII uses a 600px mat-dialog; replicate visually): - Title "Handshake Settings" (approximation of the Mark VII dialog). - **Handshake Location**: read-only value from `GET /api/pineap/handshakes/location`. - **Delete All Handshakes** button → `PagerAPI.del('/api/pineap/handshakes/all')` → success flash + reload table + close. - Close button / click-outside to dismiss. ### 5.3 `js/icons.js` Add inline Material SVG paths: `check`, `close`, `question_mark` (the Mark VII uses `assets/icons/question-mark.svg`; an inline `help` glyph is equivalent). `settings`, `file_download`, `delete`, `refresh` already exist. ### 5.4 `css/app.css` Add (light + `html.dark`): `.hs-cell-center` (M1–4/Beacon centering), `.hs-ok` (green check), `.hs-bad` (red X), `.hs-na` (grey `?`), `.hs-flash` / `.hs-flash-ok` / `.hs-flash-error` (title-row indicator), `.modal-overlay`, `.modal`, `.modal-title`, `.modal-actions`, `.modal-body`. Reuse existing `--surface`/`--border`/`--primary` tokens. ## 6. Compute-cost mitigation (user requirement) - No handshake files → **no** DB reads, no subprocess spawns. - Files exist → **exactly 1** `_db_rows` call, scoped by `WHERE h.time >= ` so a sparse capture never scans the full recon.db history. - Correlation is dict-based (O(1) per file); filename parsing is pure string work. - No polling added to the view — loads on mount + manual Refresh (unchanged). - Context: the recon scanning view already issues ~6 `_db_rows` calls every 10s; two extra on page load is negligible. ## 7. Verification notes (implementation-time, not blockers) - **File naming / quality suffix**: inferred from on-device payloads, not yet observed live (no handshakes captured). The tolerant parser + `--`/`?` fallbacks keep the UI correct for any naming variant; a real capture should be sanity-checked if one can be produced. - **hs1–hs4/beacon population**: recon.db schema includes them; if the daemon leaves them NULL the table still renders (all X for in-DB rows). - `handshake.time` epoch-seconds assumption matches the existing recon reads. ## 8. Testing - Python (`tests/test_recon.py`, same mock style as existing): - `handshakes_data()` returns `{files, handshakes}`; empty dir → no DB-helper calls (assert mocked `_db_rows` not invoked). - Filename parser: full/partial/incomplete × pcap/22000, colon and dash MACs, unparseable fallback. - Correlation: canned handshake/wifi_device rows → correct `part_mask`, `beacon`, `in_db`, `timestamp`. - `GET …/handshakes/location` returns the loot dir; `DELETE …/handshakes/all` removes all files and returns updated data. - Front-end: manual on-device pass — table renders, glyphs in both `in_db` states, download filename, delete flash, settings dialog, delete-all, empty state, dark theme, dashboard unchanged (still `files`). - Deploy via `scripts/deploy.ps1`; `curl` the changed JS/CSS for 200. ## 9. Out of scope - Evil WPA/2 Twin handshakes (`source:"Evil WPA/2 Twin"`) — no Pager mode. - Editable handshake location (`PUT /api/pineap/handshakes/location`). - PMKID / `hostap_handshake` rows in this table (Pager's `loghandshake` is EAPOL; PMKID handling is separate). - Changing the dashboard's handshake card/table (it keeps using `files`).