Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,13 @@ sections below describe the design, some of which is not yet connected.
- **AirPlay sending** — discovery, the HTTP/plist session layer, TLV8, NTP, the
mirror stream and HAP pairing are implemented. Pairing previously used a
fabricated SRP group and could never succeed; it now uses the real RFC 5054
3072-bit group, but is unconfirmed against physical hardware
([#27](https://github.com/Developer1010x/openplay/issues/27)). FairPlay will
3072-bit group, and transient pair-setup is **confirmed against hardware**:
`pair_probe` completes SRP-6a against a Mac running AirTunes/950.7.1, and an
encrypted `GET /info` over the post-pairing control channel returns 200
([#27](https://github.com/Developer1010x/openplay/issues/27)). **Mirroring
is not confirmed** — a modern Mac gates it behind FairPlay, and the mirror
stream cannot yet write video into the encrypted post-pairing connection
anyway, so the authenticated path stops with an explicit error. FairPlay will
not be implemented here, so Apple TV 2nd/3rd generation are refused up front
with an explicit error — see the decision in
[docs/crypto.md](docs/crypto.md).
Expand Down Expand Up @@ -64,7 +69,7 @@ sections below describe the design, some of which is not yet connected.

| Protocol | Direction | Notes |
|---|---|---|
| AirPlay | Sender only, **untested against hardware** | Discovery, HTTP/plist session layer, TLV8, NTP, the mirror stream and HAP pairing are implemented. FairPlay will not be implemented, so receivers that require it (Apple TV 2nd/3rd gen) are rejected by design. Target: Apple TV, AirPlay 2 TVs, and compatible displays. See [#27](https://github.com/Developer1010x/openplay/issues/27) |
| AirPlay | Sender only — **pairing confirmed against hardware, mirroring unconfirmed** | Discovery, HTTP/plist session layer, TLV8, NTP, the mirror stream, HAP transient pairing and the encrypted control channel are implemented. Video cannot yet be sent over the encrypted post-pairing connection. FairPlay will not be implemented, so receivers that require it (Apple TV 2nd/3rd gen) are rejected by design. Target: Apple TV, AirPlay 2 TVs, and compatible displays. See [#27](https://github.com/Developer1010x/openplay/issues/27) |
| Miracast / Wi-Fi Display | Sender only | Cast to Miracast adapters and smart TVs; Wi-Fi Direct P2P supported on Linux |
| OpenPlay (WebRTC) | Sender and receiver, **not yet wired up** | Native protocol between two OpenPlay instances. The signaling, pipeline and discovery libraries are implemented; connecting them to the two binaries is in progress. See [#11](https://github.com/Developer1010x/openplay/issues/11) |

Expand Down
17 changes: 10 additions & 7 deletions crates/openplay-airplay/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ AirPlay mirroring protocol implementation. Handles the sender-side flow from
device discovery through stream setup to per-frame video delivery.

> **Interoperability status.** HAP pairing used a fabricated SRP group and could
> never succeed; that is fixed (see `srp.rs`) but is **unconfirmed against physical
> Apple hardware**. `fairplay.rs` uses an invented key derivation and **has no
> callers** — Apple TV 2nd/3rd generation are refused by model string instead.
> Read [docs/crypto.md](../../docs/crypto.md) before debugging a receiver that
> rejects a connection.
> never succeed; that is fixed (see `srp.rs`) and transient pair-setup is
> **confirmed against physical Apple hardware** (issue #27). Mirroring is not:
> `mirror_stream.rs` cannot yet write into the encrypted post-pairing connection
> that `control_channel.rs` provides. `fairplay.rs` uses an invented key
> derivation and **has no callers** — Apple TV 2nd/3rd generation are refused by
> model string instead. Read [docs/crypto.md](../../docs/crypto.md) before
> debugging a receiver that rejects a connection.

## Protocol phases

Expand All @@ -17,8 +19,8 @@ The AirPlay mirroring handshake proceeds in four phases, all handled by `AirPlay
1. **Feature negotiation** (`http_session.rs`) — GET `/info` to read server capabilities, POST `/stream` with SDP-like parameters. Checks `AirPlayFeatures` for mirroring support.

2. **HAP pairing** (`hap_pairing.rs`) — HomeKit Accessory Protocol pairing over TLV8-encoded HTTP POST requests to `/pair-setup` and `/pair-verify`. Two modes:
- Transient pairing (PIN `3939`) — no stored credentials.
- Full SRP-6a pairing with a 4-digit PIN, followed by Ed25519 signing and X25519 ECDH. Credentials are stored in a SQLite database via `rusqlite`.
- Transient pairing (PIN `3939`) — no stored credentials. Ends at M4 and hands the connection to the encrypted **control channel** (`control_channel.rs`), which frames every later request in ChaCha20-Poly1305. This is the only mode the session uses.
- Full SRP-6a pairing with a 4-digit PIN, followed by Ed25519 signing and X25519 ECDH. Credentials are stored in a SQLite database via `rusqlite`. Reachable from the `pair_probe` example only.

3. **FairPlay** (`fairplay.rs`) — AES-CTR content protection negotiation.

Expand All @@ -29,6 +31,7 @@ The AirPlay mirroring handshake proceeds in four phases, all handled by `AirPlay
| Type | File | Purpose |
|---|---|---|
| `AirPlaySession` | session.rs | High-level orchestration; emits `SessionEvent::Ready` / `SessionEvent::Ended` |
| `ControlChannel` | control_channel.rs | Post-M4 connection wrapper: HKDF-SHA512 keys, ChaCha20-Poly1305 frames, per-direction counter nonces |
| `AirPlayFeatures` | features.rs | Bitfield parsed from `0xHEX` or `0xLO,0xHI` mDNS TXT |
| `MirrorHeader` | mirror_header.rs | 128-byte fixed header: payload size (LE u32), packet type, NTP timestamp (BE u64) |
| `MirrorStream` | mirror_stream.rs | Writes codec data, video frames, and heartbeats over TCP |
Expand Down
7 changes: 4 additions & 3 deletions crates/openplay-airplay/src/hap_pairing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@
//! [`crate::srp`], whose tests re-derive it from RFC 3526's formula and check
//! the client against an independent implementation of the server side.
//!
//! That removes the known blocker to pairing with real hardware. It has not
//! been confirmed against physical Apple hardware, so treat pairing as
//! untested-in-the-field rather than proven.
//! Transient pair-setup is confirmed against physical Apple hardware: a Mac
//! running AirTunes/950.7.1 completes SRP-6a verification (issue #27). That
//! flow ends at M4 and hands the connection to [`crate::control_channel`];
//! there is no pair-verify in it.
//!
//! Note that a receiver requiring FairPlay will still fail later in the
//! session, for an unrelated reason — see [`crate::fairplay`].
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ no consumer in either binary; the WebRTC path that would use them is unwired.
|---|---|
| `openplay-sender` | Binary. egui UI, receiver list, `casting.rs` orchestration |
| `openplay-receiver` | Binary. egui window showing a static "waiting" page |
| `openplay-airplay` | AirPlay: HAP pairing, SRP, NTP, mirror stream, TLV8. Also `fairplay.rs`, which has no callers |
| `openplay-airplay` | AirPlay: HAP pairing, SRP, encrypted control channel, NTP, mirror stream, TLV8. Also `fairplay.rs`, which has no callers |
| `openplay-miracast` | Miracast/WFD: RTSP, WFD params, Wi-Fi Direct (Linux) |
| `openplay-pipeline` | GStreamer pipeline construction, encoder probing |
| `openplay-signaling` | WebSocket signaling client and server. Never constructed |
Expand Down
12 changes: 7 additions & 5 deletions docs/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,12 +137,14 @@ implementation, do not invent one. Leave it unimplemented with a warning.
[airplay-receiver-design.md](airplay-receiver-design.md)
- macOS and Windows screen capture backends
- AirPlay and Miracast receiver support
- Confirming AirPlay HAP pairing against real hardware — one `pair_probe` run,
see [#27](https://github.com/Developer1010x/openplay/issues/27)
- Confirming AirPlay mirroring end to end. Pairing itself is confirmed
([#27](https://github.com/Developer1010x/openplay/issues/27)); what is missing
is a mirror stream that can write into the encrypted post-pairing connection,
and a receiver that mirrors without FairPlay to test it against
- Testing against real AirPlay and Miracast hardware — genuinely valuable, since
no test in this repo can substitute
- Packaging (Flatpak, Homebrew, Winget, AUR)

**Not** useful: porting Apple's FairPlay key tables. That was considered and
declined — see [crypto.md](crypto.md#fairplay--not-fixed). A PR adding them will
be closed, so please do not spend time on it.
- Testing against real AirPlay and Miracast hardware — genuinely valuable, since
no test in this repo can substitute
- Packaging (Flatpak, Homebrew, Winget, AUR)
81 changes: 63 additions & 18 deletions docs/crypto.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ Original report: issue #8 (closed). Hardware confirmation is tracked in issue #2

| Component | Where | Status |
|---|---|---|
| HAP pair-setup (SRP-6a) | `airplay/srp.rs`, `hap_pairing.rs` | **Fixed**, untested against hardware |
| HAP pair-verify | `airplay/hap_pairing.rs` | Implemented, unreachable until pairing is confirmed |
| HAP transient pair-setup (SRP-6a) | `airplay/srp.rs`, `hap_pairing.rs` | **Fixed and confirmed against hardware** — M1–M4 against AirTunes/950.7.1 |
| Encrypted control channel | `airplay/control_channel.rs` | Implemented and confirmed against hardware — an encrypted `GET /info` returns 200. The mirror stream cannot write into it yet, so mirroring is unconfirmed |
| HAP pair-verify | `airplay/hap_pairing.rs` | Implemented, no callers — the transient flow ends at M4 and keys the control channel directly; pair-verify belongs to the PIN flow, which the session path never uses |
| FairPlay | `airplay/fairplay.rs` | **Will not be implemented** (decision below), and not wired in — `fp_setup` has no callers |
| TLS certificates | `openplay-crypto/certs.rs` | Implemented, never constructed anywhere |
| Signaling TLS config | `openplay-crypto/tls.rs` | Implemented, no callers — fingerprint pinning, which is **not** peer authentication |
Expand Down Expand Up @@ -88,20 +89,64 @@ and continuing would derive a session key an attacker can predict.
The private exponent was reduced from 2048 bits to 256. The old code called
`random_bigint(256)` against a parameter named `bytes`.

### Caveat: still unconfirmed against hardware

**This has not been confirmed against physical Apple hardware.** The known
blocker is removed; that is not the same as proven working. No test in this
repository can substitute for a real receiver.

There is a probe for exactly this:
### Confirmed against hardware

Transient pair-setup **has been confirmed against physical Apple hardware**
([#27](https://github.com/Developer1010x/openplay/issues/27), 2026-08-26): a
Mac running AirTunes/950.7.1 with AirPlay Receiver set to *Everyone* runs the
SRP exchange to completion and the probe reports
`SRP-6a verification successful`.

Two facts from that run are worth more than the bare result:

- **The receiver's public key is 384 bytes** — 3072 bits. A receiver on a
different modulus would not produce a `B` of that width, so the group is now
corroborated from the far side of the wire, not only by the self-checks in
`srp.rs`.
- **It did not work as shipped.** Reaching M4 took two fixes, both real bugs
rather than environment problems. `POST /pair-setup` needs an
`X-Apple-HKP: 4` header, or the receiver answers 400 before reading the body
(#41). And the M1 proof must hash `g`'s minimal encoding — the single byte
`0x05` — not `PAD(g)`; with padding the receiver answers HAP error `2` at M4
(#42, recovered in #43). The neighbouring `k = H(N | PAD(g))` genuinely does
need padding, which is how the bug got written.

Transient pairing **ends at M4**. There is no M5/M6, no long-term keys and no
pair-verify. Running M5 anyway makes the receiver close the connection right
after an otherwise successful M4 — which reads exactly like a crypto failure
and is not one. From M4 on the connection is encrypted: HKDF-SHA512 under
`Control-Salt` with the `Control-Write-Encryption-Key` /
`Control-Read-Encryption-Key` info strings, then ChaCha20-Poly1305 frames with
a 2-byte little-endian length as AEAD associated data and a per-direction
64-bit counter nonce. `control_channel.rs` implements that, and an encrypted
`GET /info` over it returns `HTTP/1.1 200 OK`.

**A trap when re-testing.** The receiver backs off hard after a failed
pair-setup, answering HAP error `0x03` with a retry delay. Attempts closer
together than roughly two minutes return backoff rather than a real answer, and
backoff looks nothing like an authentication failure. Treat any `0x03` as "no
result", wait, and re-run.

**What this does not establish: mirroring.** `MirrorStream` writes NAL units
to a raw `TcpStream`, but every byte after M4 must be wrapped in
control-channel frames, so `negotiate_with_auth` deliberately stops after
`POST /stream` with an explicit error rather than emit plaintext into an
encrypted connection. Making the mirror stream encryption-aware is the
remaining work. Confirming it end to end also needs a receiver that mirrors
without FairPlay: a modern Mac gates mirroring behind it (`/fp-setup` answers
400, RTSP `SETUP` 455, `/stream` 404), and FairPlay stays out of scope by the
decision below. A software receiver such as `uxplay` is probably the cheapest
way to get one.

The probes that produced all of this:

```console
cargo run -p openplay-airplay --example pair_probe -- <ip>:7000 # transient
cargo run -p openplay-airplay --example pair_probe -- <ip>:7000 # transient, stops after M4
cargo run -p openplay-airplay --example pair_probe -- <ip>:7000 1234 # with PIN
cargo run -p openplay-airplay --example control_probe -- <ip>:7000 # M4, then the encrypted channel
```

### What a real attempt actually produced
### What the first attempt produced

Run against a MacBook Air (`Mac16,12`, AirTunes/950.7.1), `GET /info` succeeded —
1157-byte plist, features `0x38174FDE4A7FCFD5`, mirroring, video and audio all
Expand All @@ -117,14 +162,14 @@ same Apple ID, and it is enforced before any crypto runs. To test the SRP path
the receiver must be set to "Anyone on the same network" in
**System Settings → General → AirDrop & Handoff → AirPlay Receiver**.

So the SRP question is still open. The attempt was not wasted, though: a 403 was
being reported as `Missing state TLV`, because `recv_response` never looked at
the HTTP status line and an empty body failed TLV8 decoding. That is precisely
the misleading-diagnostic problem this issue was filed about, one layer up. It
now reports the status and names the setting to change (`check_http_status` in
`hap_pairing.rs`, with four tests).
That attempt was not wasted: a 403 was being reported as `Missing state TLV`,
because `recv_response` never looked at the HTTP status line and an empty body
failed TLV8 decoding. That is precisely the misleading-diagnostic problem this
issue was filed about, one layer up. It now reports the status and names the
setting to change (`check_http_status` in `hap_pairing.rs`, with four tests).

If you get further against real hardware, please add the result to issue #27
With the setting changed, the second attempt is the one described above. If you
get a different result against other hardware, please add it to issue #27
either way.

## FairPlay — not fixed
Expand Down
36 changes: 23 additions & 13 deletions docs/protocols.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ audio, even where the protocol layer advertises it — see
| Protocol | Discovery | Session setup | Transport | State |
|---|---|---|---|---|
| Miracast | mDNS + Wi-Fi Direct | RTSP M1–M7 | RTP/MPEG2-TS over UDP | Works |
| AirPlay | mDNS `_airplay._tcp` | HTTP/plist + HAP | Mirror stream (TCP) | Pairing unconfirmed; FairPlay not wired in |
| AirPlay | mDNS `_airplay._tcp` | HTTP/plist + HAP | Mirror stream (TCP) | Pairing confirmed against hardware; mirroring unconfirmed; FairPlay not wired in |
| OpenPlay | mDNS `_openplay._tcp` | WebSocket signaling | WebRTC | Sender browses; everything else is libraries only |

---
Expand Down Expand Up @@ -85,10 +85,13 @@ flow at all** — see [crypto.md](crypto.md).
`POST /stream`
- if that fails with **501 or 403**, falls back to `negotiate_with_auth`:
`GET /info` to identify the model, then HAP **transient** pair-setup
followed by pair-verify (`hap_pairing.rs`), then `POST /stream` on the
verified connection
- wraps the resulting connection in a `MirrorStream` and starts a 2-second
heartbeat
(`hap_pairing.rs`), which ends at M4 — there is no pair-verify — then
wraps the same connection in the encrypted control channel
(`control_channel.rs`) and sends `POST /stream` through it. The path
currently **stops there with an explicit error**: `MirrorStream` writes
to a raw socket and cannot yet frame video for the encrypted connection
- on the unauthenticated path, wraps the resulting connection in a
`MirrorStream` and starts a 2-second heartbeat
4. `AirPlaySenderPipeline` captures and encodes, emitting H.264 NAL units to an
appsink
5. The casting loop copies the SPS/PPS out of the first frame and sends them once
Expand Down Expand Up @@ -121,14 +124,21 @@ frames video, codec data and heartbeats onto it.
### Pairing modes

- `pair_setup_transient(addr)` — no PIN. Used when the receiver is set to
"Everyone on the Same Network". Sends flags `0x02` and uses the standard
transient PIN `3939`. **This is the only mode the session path uses.**
- `pair_setup(addr, pin)` — first-time pairing with a 4-digit PIN. Reachable via
the `pair_probe` example, not from the session flow.
- `pair_verify(...)` — subsequent connections, using stored Ed25519 keys.

Ed25519 signing and ChaCha20-Poly1305 are used in pair-setup M5/M6; X25519 ECDH
is used separately in `pair_verify`.
"Everyone on the Same Network". Sends `X-Apple-HKP: 4`, flags `0x02` (which
the measured receiver ignores) and the standard transient PIN `3939`, and
**ends at M4**: no long-term keys, nothing persisted, no pair-verify. It hands
back the connection together with the SRP session key, because from here on
the receiver only accepts control-channel frames on it. **This is the only
mode the session path uses**, and it is confirmed against hardware
([#27](https://github.com/Developer1010x/openplay/issues/27)).
- `pair_setup(addr, pin)` — first-time pairing with a 4-digit PIN, running
M1–M6. Reachable via the `pair_probe` example, not from the session flow.
- `pair_verify(...)` — subsequent connections, using stored Ed25519 keys. No
callers.

Ed25519 signing and ChaCha20-Poly1305 are used in pair-setup M5/M6, which only
the PIN flow reaches; X25519 ECDH is used separately in `pair_verify`.
ChaCha20-Poly1305 also frames the control channel after transient M4.

`hap_pairing.rs` provides SQLite helpers for paired devices (`init_paired_db`,
`store_paired_device`, `load_paired_device`), but nothing outside the module's
Expand Down
23 changes: 20 additions & 3 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,26 @@ the device and OpenPlay does not, that is a bug worth reporting.
### Rejected during pairing

Pairing previously could never succeed — the SRP group was fabricated. That is
fixed, but **has not been confirmed against physical hardware**. If you hit a
pairing failure, `RUST_LOG=openplay_airplay=debug` will show which message it
died on, and that result is worth adding to issue #27 either way.
fixed, and transient pair-setup is **confirmed against a Mac running
AirTunes/950.7.1** (issue #27). If you hit a pairing failure anyway,
`RUST_LOG=openplay_airplay=debug` will show which message it died on, and that
result is worth adding to #27 either way.

Two failures are not what they look like:

- **HAP error `0x03` with a retry delay.** The receiver backs off hard after a
failed pair-setup and keeps answering with backoff for roughly two minutes.
It is not an authentication failure and not a result. Wait, then re-run.
- **The connection closes right after a successful M4.** Transient pairing ends
at M4. Anything sent in plaintext after it — an M5 included — makes the
receiver drop the connection.

### "mirror stream cannot yet send video over an encrypted connection"

Pairing and the encrypted control channel both succeeded, and the cast stopped
because the mirror stream cannot yet frame video for the encrypted post-pairing
connection. This is the current end of the authenticated path, not a network
problem. See [crypto.md](crypto.md#confirmed-against-hardware).

### "requires FairPlay authentication which is not supported"

Expand Down
Loading