Commit graph

7 commits

Author SHA1 Message Date
c8a755513f add zebrad: pulseaudio→websocket introspector for chat.html
single C file (~620 LOC), no third-party deps beyond libpulse.
closes a receive loop for web/chat.html: a browser tab cannot read
another tab's PulseAudio state, so each peer runs zebrad locally
to forward decoded frames to ws://127.0.0.1:7777.

components inline:
  * SHA-1 (RFC 3174) + base64 encode — for the WebSocket handshake;
    verified against the canonical RFC 6455 example
    (key dGhlIHNhbXBsZSBub25jZQ== → accept s3pPLMBiTxaQ9kYGzzhZRbK+xOo=)
  * CRC-32 (IEEE 802.3) — verifies DATA + HELLO frame payloads
  * UART symbol decoder with adaptive threshold:
    running peak with 2-second half-life decay, threshold = 50% of peak,
    falling-edge start detect, sample at 1.5..8.5 × sps from edge
  * frame assembler: magic-sync ZB, type dispatch OFFER/READY/DATA/HELLO,
    overflow + corrupt-length guards, resync on bad checksum
  * embedded WebSocket server: TCP listener on 127.0.0.1:7777,
    handshake responder, binary frames only (opcode 0x82), no masking
    (server→client), any inbound data closes the client (browser
    auto-reconnects); max 16 concurrent clients
  * pulseaudio: pa_mainloop integration, record stream on @DEFAULT_MONITOR@
    by default, PA_SAMPLE_FLOAT32LE mono @ 8 kHz, 10 ms fragsize, peak
    energy extracted from |abs(sample)|

adaptive baud: starts at ZEBRA_BAUD_HANDSHAKE (50). On valid READY frame
locks in negotiated baud from payload. Matches the chat.html send path.

binds 127.0.0.1 only — never reachable from the LAN, even though the
chat.html page itself may be served over HTTPS from a public domain.

smoke tested: PA connect + stream ready + WS handshake (RFC example
verified) + clean shutdown. -Wall -Wextra clean.

Makefile: zebrad now part of `make all`; clean target updated.
README: new section documenting it.
2026-05-27 15:57:22 -04:00
5d76d713a6 web/chat.html: add ?loopback=1 dev loopback via BroadcastChannel
without zebrad running, the page is TX-only: tabs see their own
outbound frames but nothing crosses. for UX validation while the
introspector daemon doesn't exist yet, gate a BroadcastChannel
fallback on the ?loopback=1 query param.

behavior:
  * with ?loopback=1: after each txFrame completes its modulation,
    the raw frame bytes are postMessage'd on a same-origin
    BroadcastChannel('zebra-report-loopback'). other tabs of the
    same browser subscribe & feed received bytes into onRxFrame
    exactly as zebrad would. BroadcastChannel suppresses echo to
    sender by spec, so the originating tab does not see itself.
  * without ?loopback=1: behavior unchanged. real PA transport
    stays the production path.

a yellow banner at the top of the page warns when loopback is
active: "frames cross via BroadcastChannel between same-browser
tabs, NOT via PulseAudio". no risk of mistaking dev mode for
production.
2026-05-27 15:47:00 -04:00
d8607cb000 add README documenting Makefile targets and build workflow 2026-05-27 15:06:53 -04:00
d9b2499a2e web/chat.html: production multi-person volume-modem chatroom
single self-contained page implementing the recovered zebra-report
protocol as a peer-to-peer chatroom whose data path is PulseAudio
sink-input state — chat content never enters an IP packet.

architecture:
  page (TX)   modulates audio output via Web Audio GainNode
              between MARK (0.80) and SPACE (0.20). silent stereo
              carrier 440/441 Hz published to PulseAudio.
  zebrad      separate program (companion, not in this commit):
              polls PA sink-input volumes, decodes frames,
              forwards to ws://127.0.0.1:7777 — the page connects
              and renders received messages.
  RX path     the page is TX-only without zebrad running; with
              it, full bidirectional chat.

crypto modes (toggle in UI):
  passphrase  PBKDF2-SHA256 (600k iter) → AES-GCM-256 group key.
              everyone with the same phrase joins the room.
  pubkey      ECDH P-256 keypair per browser → AES-GCM-256
              pairwise. peers added by pasting their pubkey.
  both modes use Web Crypto API. no external crypto deps.

frame protocol (matches include/zebra.h):
  OFFER  magic(ZB) + 0x01 + baud_le(2) + xor(1)          — 6B
  READY  magic(ZB) + 0x02 + baud_le(2) + xor(1)          — 6B
  HELLO  magic + 0x04 + sid(4) + maxBaud_le(2)
         + handleLen(1) + handle(n) + crc32_le(4)
  DATA   magic + 0x03 + sid(4) + len_le(2)
         + payload(n) + crc32_le(4)

handshake:
  1. user clicks "benchmark self" — page schedules 200 gain
     events at 0.5ms intervals, measures avg, divides by 2 for
     safety, clamps to [ZEBRA_BAUD_MIN, ZEBRA_BAUD_MAX].
  2. user clicks "send OFFER" — sends OFFER frame at the
     fixed handshake baud (50). also broadcasts HELLO.
  3. peer's introspector decodes; their page replies with READY
     at their max baud, plus their HELLO.
  4. negotiated baud = min(my max, every peer's max).
  5. data frames at negotiated baud.

UI sections:
  identity   handle + mode toggle
  room       passphrase OR pubkey, depending on mode
  carrier    start audio, live gain meter
  handshake  benchmark, OFFER button, baud display
  receive    ws://127.0.0.1:7777 status + connect
  chat       peer list, log, message box
  about      threat model, mitigation, whitepaper link

defensive framing prominent: the protocol is documented as a
research artifact demonstrating PA-IPC's open same-UID volume
read access. "what this does not protect against" enumerated.
mitigation panel cites foxhop.net/linux-audio-ipc-attack-surface.

known v1 tradeoffs:
  - no CSMA: simultaneous TX from two peers will collide.
  - pubkey mode broadcasts one frame per peer per message.
  - GainNode scheduling has OS-level jitter — actual sustained
    baud will be lower than the optimistic benchmark.
  - base carrier is audible at 440/441 Hz; can be lowered or
    moved ultrasonic in a follow-up.

reuses visual identity from web/index.html (chunkfive font,
monospace, black/white, no framework, no build step).

file: 1062 lines, 40 KB, JS syntax-clean (node --check), HTML
balanced.
2026-05-27 14:54:26 -04:00
bf6427f279 add .gitignore for build artifacts
aligned with `make clean` — ignores compiled binaries (tx/rx/chat/bt/
carrier, test/{unit,integration,functional}), object files, and the
generated blog output under web/blog/.

recovered code verified at this commit:
  make all              : 5 binaries built, no -Wall/-Wextra warnings
  test/unit             : 84/84 passed
  test/integration      : 17/17 passed (benchmark: 836 baud measured)
  test/functional       : 8/10 passed
    - t_loopback_fixed_baud (50 baud) ok — "ZEBRA" tx->rx round-trip
    - t_autoneg_pipeline FAIL: autoneg handshake succeeds; data
      transmission at benchmarked baud (1994) times out. cause is
      environmental — RX polling under loadavg 2.0 can't sustain
      sub-millisecond symbol timing on a non-RT kernel. not a defect
      in the recovered code; same path passes at fixed 50 baud.
2026-05-27 14:03:06 -04:00
4fadfca5b1 phase 2: gap analysis — cat>> append + binary asset recovery
quality verification:
  - confirmed all 8 unapplied edits in phase 1 also failed in their
    original sessions (tool-result is_error=1, "String to replace
    not found"). Our reconstruction is faithful to live execution.

gaps closed:
  - include/modem.h: re-ran replay with cat>>heredoc handling in
    chronological position. BATTLE TOADS dual-channel stereo UART
    block (4495 bytes, 102 lines) now appended at correct point in
    timeline. Two previously-failing edits now apply against the
    post-append baseline. unapplied edits dropped 8 -> 6.
  - web/fonts/: chunkfive-regular-webfont.{woff,woff2} restored
    from live source /home/fox/git/www.unturf.com/css/chunkfive/.
    HTML+CSS @font-face references now resolve.

remaining unapplied edits (6) confirmed legitimate live-session
failures, not reconstruction artifacts.

internal references audit:
  - all #include directives resolve within recovered tree
  - all font url() references resolve to recovered web/fonts/
  - no other Bash file-creation ops target zebra-report

final tree: 22 files, ~160KB.
2026-05-27 13:54:19 -04:00
b77da42bbe phase 1: unfirehose reconstruction from session JSONL ingest
Source: ~/.unfirehose/unfirehose.db (project_id=81, 4 sessions covering
2026-03-29 through 2026-04-05). Reconstructed via chronological replay
of Write/Edit tool_input on file_paths under /home/fox/zebra-report/.

stats:
  files reconstructed:    20
  writes baselined:       all (zero missing)
  edits applied:          68
  edits unapplied:        8 (1 SKIP pre-baseline, 6 FAIL old_string drift, 1 AMBIGUOUS)

unapplied edits represent small drift in 6 files; baseline content for
each is intact. quality verification deferred to phase 2.

recovered tree:
  CLAUDE.md, Makefile
  src/{tx,rx,pulse,carrier,chat,bt}.c
  include/{modem,zebra}.h
  test/{functional,integration,unit}.c, test/test.h
  web/{index,kernel}.html, web/blog/style.css
  blog/build.py, blog/posts/{001-volume-modem,002-sse-chatroom}.md

report: /tmp/zebra_recover_report.txt
script: /tmp/zebra_recover.py
2026-05-27 13:51:14 -04:00