Files
Mark-VIII/docs/superpowers/specs/2026-08-11-handshakes-markvii-parity-design.md
T
2026-08-11 20:24:24 -07:00

275 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 14, 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 14 / 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 `<mac>_<client>_<type>.<extension>`.
## 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: `<unix_ts>_<AP_MAC>_<CLIENT_MAC>_handshake.pcap|.22000`
- partial: `<unix_ts>_<AP_MAC>_<CLIENT_MAC>_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 >= <oldest parsed file ts>
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
`<mac>_<client>_<type>.<ext>` 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 14**: `!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` (M14/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 >=
<oldest file ts>` 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.
- **hs1hs4/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`).