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

13 KiB
Raw Blame History

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:

{ "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 (pcapPCAP, 22000Hashcat, 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 extensionformat 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
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).