release: Mark VIII 1.0
This commit is contained in:
@@ -0,0 +1,274 @@
|
||||
# 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 `<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 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 >=
|
||||
<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.
|
||||
- **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`).
|
||||
Reference in New Issue
Block a user