Commit graph

10 commits

Author SHA1 Message Date
0d1236e030 chat.html: bump QR canvas + render size 180px → 320px 2026-05-27 19:02:46 -04:00
9e67b05f2f chat.html: share-link + QR for SDP exchange (near-automated handshake)
manual copy-paste of raw SDP was v1. this adds:

1. share-link encoding: SDP description → JSON → deflate-raw (native
   CompressionStream) → base64url → URL hash. typical 2 KB SDP fits
   in ~700 chars after compression. fits in any text channel.
   URL format:  .../zebra-report/#o=<deflated-base64>    (offer)
                .../zebra-report/#a=<deflated-base64>    (answer)

2. QR rendering: same URL rendered as 180x180 QR using inlined
   davidshimjs qrcodejs (MIT, 28 KB). offers a visual scan path
   (phone scanners) without an in-browser decoder library yet.

3. auto-fill on link open: page reads location.hash on load.
     #o=... → autofills remote-offer textarea, prompts user to
              complete identity + audio + click "create answer"
     #a=... → autofills remote-answer textarea, prompts "accept"
   hash is cleared from address bar after parse so a reload doesn't
   double-trigger.

4. parsers tolerate either input form: full URL with #o=/#a= hash,
   or raw JSON SDP. peer A can paste back a URL or a textarea dump,
   same handler.

5. tucked the raw SDP textareas behind a "show raw SDP" toggle so
   the default UI is just the URL + QR. expert users still get the
   raw bytes when needed.

UX flow (cross-internet, two peers):
  A: enter room → start audio → "create offer" → "copy link"
     → send link to B via Signal/SMS/anywhere
  B: open link → page auto-fills offer → enter room → start audio
     → "create answer" → "copy link" → send back to A
  A: paste link into the answer textarea → "accept answer"
  → SRTP candidates pair → chat begins.

deferred: in-browser camera scanning of QR (needs jsQR or similar
~60 KB inlined; not on disk currently). v3.

file: 1509 lines, ~85 KB. JS syntax-clean (qrcode lib + main IIFE),
HTML balanced. CompressionStream + DecompressionStream available
in all current browsers (Chrome 80+, Firefox 113+, Safari 16.4+).
2026-05-27 18:37:18 -04:00
0ff2c788d6 chat.html: add proxy.uncloseai.com TURN to iceServers
with coturn live on proxy.uncloseai.com (proxy.unturf.com:22ccf93),
ICE now offers a relay candidate path on every connection. peers
behind symmetric NAT or strict firewalls fall back to TURN instead
of failing the connection.

static creds zebra:7a4a2b1c8d9e6f5a — public by design (a static
page can't hide them); rate-limited by coturn quotas, not secrecy.
google stun stays as fallback if our stun is unreachable.
2026-05-27 17:10:29 -04:00
202dd706f9 chat.html: add reset button + ICE candidate logging + mDNS hint
addresses two issues in the WebRTC same-LAN test:

1. "Cannot set remote answer in state stable" on retry
   the offerer's pc enters stable after the first successful accept-
   answer; subsequent clicks fail. add a reset-connection button that
   close()s the current pc, clears all SDP textareas + status lines,
   detaches inbound rx decoders, & restores the disconnected UI.
   user can now retry without reloading the page.

2. silent NAT/mDNS diagnostic gap
   browsers anonymize host IPs as <uuid>.local mDNS names. routers
   with multicast-blocking client isolation (common on guest Wi-Fi)
   silently prevent host-candidate pairing on the same LAN. add:
     * a details/summary section under the rtc dot documenting the
       Firefox + Chromium config knobs to disable host obfuscation;
     * pc.onicecandidate logging that prints candidate type
       (host/srflx/relay) + an explicit '(mdns-anonymized)' tag
       for .local foundations, so the user can see WHAT is being
       gathered without opening devtools;
     * an err logLine on connectionState=failed pointing at the
       reset button.

unchanged: protocol, frame codec, crypto, audio path, signaling
flow. UI surface gains one button + one collapsed details panel.
2026-05-27 16:38:07 -04:00
0dede3d6ca chat.html: WebRTC P2P audio + in-browser AudioWorklet decode
major architectural rework. previously chat.html modulated its own
GainNode and depended on a native zebrad daemon polling local PA
to close the receive loop. that worked single-machine multi-tab
but had no cross-laptop path without bridging external audio (voice
call, virtual sink, etc.).

new architecture: each peer's modulated audio is the outbound track
of a WebRTC peer connection. each peer decodes the inbound RTC
track in-page via an AudioWorklet energy detector + the same UART
state machine zebrad uses. cross-laptop works because WebRTC carries
the modulated audio between machines. native daemon not required.

  outbound:  oscillators (440/441 Hz) → modulation gain (MARK/SPACE)
             → channel merger → MediaStreamDestination → RTCPeer-
             Connection.addTrack().  also → optional local monitor
             gain (muted by default; toggleable for debugging).

  inbound:   RTCPeerConnection.ontrack → MediaStreamSource
             → AudioWorklet (per-quantum peak detection, 128 samples)
             → main thread postMessage → UartDecoder → FrameAssembler
             → onRxFrame (same handler as before).

  signaling: v1 uses manual SDP paste via two textareas. role A
             ("start a new connection") creates an offer, waits for
             gathered ICE, exposes JSON-serialized localDescription
             for copy. role B ("join") pastes that, generates an
             answer, gets pasted back to A.  STUN: stun.l.google.com.
             v2 will add a tiny signaling server for auto-pair.

  threat:    SRTP carries the modulated audio. Wireshark sees only
             encrypted RTP packets; chat content lives in audio
             amplitude transitions inside the encrypted payload.
             additional app-layer AES-GCM (passphrase mode PBKDF2,
             pubkey mode ECDH P-256) preserved unchanged.

zebrad daemon (src/zebrad.c) stays in the repo as the V2 disclosure
demo (a same-UID host process polling PulseAudio sink-input volumes
of an arbitrary same-UID tab) but is no longer required for chat.

preserved unchanged from prior chat.html:
  * crypto modes (passphrase + pubkey), mode toggle
  * sender id (random per-tab in passphrase mode; SHA-256(pubkey)[0:4] in pubkey mode)
  * self-echo filter via sid match
  * frame codec (OFFER/READY at 50 baud, DATA/HELLO with CRC-32)
  * benchmark + handshake state machine
  * peer list, chat log
  * threat model + mitigation panel
  * BroadcastChannel dev loopback via ?loopback=1

file size: 1258 lines, ~46 KB. JS syntax-clean (node --check).
HTML balanced.
2026-05-27 16:14:51 -04:00
dcb1c8e5a1 chat.html: drop self-echo frames at onRxFrame entry
zebrad reads @DEFAULT_MONITOR@ which mixes every PA stream on
a machine, so each browser tab hears its own carrier echoed
back. without a filter, every outbound message would render
twice in the chat log (once as 'me' at send time, once as
'peer' on echo arrival).

filter: compare frame.sid (carried in DATA + HELLO) against
our own senderIdBytes(). if equal, drop frame before any UI
work. loopback mode unaffected since BroadcastChannel never
echoes to sender by spec.

senderIdBytes returns a stable per-tab id:
  * passphrase mode: random 4 bytes cached for tab lifetime
  * pubkey mode: first 4 of SHA-256(pubkey), stable across reloads
2026-05-27 16:00:28 -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
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
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