Skip to content

Speak Sendspin 1.0.0-rc1 on the audio input source, with pairing tokens - #322

Merged
bbangert merged 7 commits into
mainfrom
claude/sendspin-source-rc1
Nov 10, 2026
Merged

bbangert merged 7 commits into
mainfrom
claude/sendspin-source-rc1

Conversation

@bbangert

Copy link
Copy Markdown
Owner

Moves the USB audio input's Sendspin source client (source@v1, pure Elixir) to the 1.0.0-rc1 wire. Before this change, aiosendspin 10.3.0 flagged it as non-compliant and tolerated it only because allow_noncompliant_clients defaults to on; a strict server would reject it. The player already speaks rc1 (via sendspin-cpp), so this brings inputs in line.

Compatibility: the source is now rc1-only. It won't interoperate with aiosendspin ≤9.1.1 servers (Music Assistant ≤2.10.6), the same as the players already. MA 2.10.7 stable pins aiosendspin 10.2.0, which is verified below.

Wire

  • client/hello no longer sends the removed trust_level. supported_pair_methods is now an object offering pairing_psk (required by the spec) and dynamic_pairing_code.
  • client-stream/start and client-stream/end replace the underscore names.
  • Every client/state carries the full source object.
  • Fragments use single-ID framing (type 1 with first/last flags).
  • Unknown activities values follow the spec's unknown-value rule.

Pairing (rc1)

  • Method names are now dynamic_pairing_code/static_pairing_code, with a fixed 6-digit code under the sendspin-pairing-code-derive-v1 label.
  • client/pair-confirm carries wrapped_nonce_B; the round number is part of sid; client/pair-retry rounds and client/pair-pending are supported; pair/abort uses the rc1 reasons.
  • Noise msg 1 lookups honour psk_category (lt/pr/sn). A miss on the initial handshake takes the Sentinel Fallback.
  • A server_id mismatch now fails the handshake.
  • Pairing PSK: each input gets a CSPRNG pairing PSK stored in its DETS row (file mode 0600). Its token is SP:0 + base32(client key ‖ pairing PSK), checked against the spec vector. The full Pairing PSK flow is implemented, both re-keying a live session and dialing with the token.
  • Multiple pairings: up to 8 records per input, so it meets the spec's ≥5. A new pairing replaces the same server's record or adds one, evicting the least recently used but never the record backing the open connection.
  • server/unpair: honoured in a long-term session: the record is removed, then goodbye unpaired and close. Code pairing works again on an already-paired input; the consent hold stays.
  • Migration: single-record rows from the old wire are converted on first read, with psk_id recomputed.

UI (Audio tab, input cards)

  • A "Pairing" disclosure on each input card:
    • the token behind Show, Hide and Copy, mirroring the output cards from Show each output's Sendspin pairing token on the Audio tab #224 (never in the DOM before Show, never in logs or PubSub, hidden again after 60 s);
    • the list of paired servers;
    • Unpair (per server), Unpair all and Regenerate token, each needing a click to arm and a second to confirm.
  • Unpairing or regenerating closes a live session whose key is gone.
  • Residual risk is documented: the UI is plain HTTP and unauthenticated, as with the output tokens.

Hardening from review

  • Attacker-supplied strings are truncated in logs.
  • The consent timer is cancelled on a re-key.
  • client-stream/end is deferred while a re-key is in progress.
  • The re-handshake cap now counts every handshake outside a long-term session.
  • PSK comparisons are constant-time.

Verification

  • Local checks: mise run test (3851 passed), format, credo --strict, dialyzer, Argus: all clean.
  • Test coverage:
    • pairing-crypto test vectors computed with aiosendspin 10.3.0's own functions, plus the spec's token vector;
    • exact-payload tests for every changed message;
    • Store migration tests built from real old-shape DETS rows;
    • LiveView tests, including that the token is absent from the HTML before Show.
  • Live, aiosendspin 10.3.0 with allow_noncompliant_clients=False, 0 non-compliance lines in every run:
    • long-term PSK ×3;
    • Pairing PSK, both by re-keying and by dialing with the token;
    • dynamic code ×3, including a mistyped round and the consent hold;
    • two servers pairing the same input and both reconnecting long-term;
    • server/unpair followed by re-pairing by code;
    • Regenerate, after which the old token is refused and the new one pairs.
  • aiosendspin 10.2.0 (MA 2.10.7 stable): long-term PSK, Pairing PSK and dynamic code all stream, in both strict and lenient mode.
  • Not yet verified:
    • Music Assistant's own UI flows (whether it offers token paste for a source-only client);
    • hardware validation on a Pi.

🤖 Generated with Claude Code

bbangert and others added 5 commits November 10, 2026 01:49
The source@v1 client now uses the rc1 wire only: no trust_level, an object
of pair methods with pairing_psk always offered, the rc1 pairing code flow
(6-digit code, wrapped_nonce_B, rounds, pair-pending), psk_category lookup
with Sentinel Fallback, client-stream/*, full client/state, single-ID
fragmentation and hello-less re-handshakes. Each input gets a persisted
pairing PSK and token.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An input card can reveal its pairing token on demand for pasting into Music
Assistant's "Pairing token" option, mirroring the output cards: read only on
Show, never broadcast or logged, copied client-side.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each input now stores up to 8 pairing records keyed by server, converts
pre-rc1 rows on read, and pairs by code or token alongside existing records.
The source handles server/unpair and revoked records, and its re-handshake,
activation and logging edge cases follow the rc1 spec more closely.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An input card's Pairing disclosure lists the servers it is paired with and
offers Unpair, Unpair all and Regenerate pairing token behind a confirm
click; a revealed token hides after a minute or when the disclosure closes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Revocation failures are treated as success, potentially leaving credentials valid while hiding them, and socket tests can miss forbidden frames already buffered by the peer.

2 open issues
What changed in this PR

Updates the audio-input Sendspin client to 1.0.0-rc1, bringing source compatibility in line with players and extending the output-token UI pattern to inputs.

Changes:

  • Adopts rc1 messaging and pairing flows, with updated protocol and cryptographic tests.
  • Adds persistent pairing tokens, multiple server records, migration, and revocation APIs.
  • Adds input pairing controls with timed token disclosure and confirmation for destructive actions.
File Description
test/​universal_proxy/​sendspin/​wire_test.exs Verifies rc1 framing and message payloads.
test/​universal_proxy/​sendspin/​pairing_token_test.exs Checks token encoding against the reference vector.
test/​universal_proxy/​sendspin/​pairing_test.exs Covers rc1 cryptography, retries, and pairing methods.
test/​universal_proxy/​audio/​input/​store_test.exs Tests token persistence, record eviction, and migration.
test/​universal_proxy/​audio/​input/​source_test.exs Covers authentication, revocation, and re-handshake behavior.
test/​universal_proxy/​audio/​input/​source_pairing_test.exs Exercises code and token pairing flows.
test/​universal_proxy/​audio/​input/​server_test.exs Tests pairing APIs and secret-free summaries.
test/​universal_proxy_web/​live/​audio_live_test.exs Tests disclosure, expiry, and confirmation controls.
test/​support/​sendspin_source_peer.ex Updates the scripted peer for rc1 exchanges.
test/​support/​sendspin_pairing_server.ex Implements rc1 server-side pairing test flows.
lib/​universal_proxy/​sendspin/​pairing.ex Implements rc1 pairing methods and retry rounds.
lib/​universal_proxy/​sendspin/​pairing_token.ex Encodes version-0 pairing tokens.
lib/​universal_proxy/​audio/​input/​store.ex Persists credentials and manages multiple pairing records.
lib/​universal_proxy/​audio/​input/​server.ex Coordinates token access, revocation, and public summaries.
lib/​universal_proxy/​audio/​input.ex Exposes pairing-management APIs.
lib/​universal_proxy_web/​live/​audio_live.ex Adds input token disclosure and pairing-management controls.

Review effort: Balanced

bbangert and others added 2 commits November 10, 2026 03:05
A revocation that returns {:error, _} now keeps the prompt armed and the token shown, with an error on the card. Source tests assert silence via Peer.refute_frames!/2 so buffered frames are checked too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Security-critical authentication and credential-migration changes need independent protocol review, while Music Assistant’s native pairing flow and Pi execution still require human validation.

No open issues 🎉

2 resolved since last review

Review effort: Balanced

@bbangert
bbangert merged commit 68c4299 into main Nov 10, 2026
9 checks passed
@bbangert
bbangert deleted the claude/sendspin-source-rc1 branch November 10, 2026 03:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants