Evidence
Build
Community
The mode 3 only badge is orange on purpose, and the hardware badge names the device count rather than saying yes. Badges on a cryptography project should carry the caveats, not hide them — see Limitations.
A from-scratch reimplementation of the FairPlay SAP authentication handshake an AirPlay 2 sender must complete to talk to a receiver — in six languages, replacing a ~1.07 MB Apple binary and an ARM64 emulator with ~500 KB of portable code.
This is an authentication handshake, not FairPlay Streaming DRM. It decrypts no content and extracts no content keys. It proves a sender is a legitimate AirPlay sender so a receiver will continue — nothing more.
- Just want it working? → Quickstart: install the
binary, run
fpsap verify, runfpsap exchange. - Want to know how it works? → Architecture and The handshake.
- Porting it to another language? → Porting guide — the one page with knowledge that exists nowhere else.
go install github.com/objevovat/fairplay-sap-core-airplay2-sender-authentication-handshake/cmd/fpsap@latest
fpsap verify # runs the 142 bundled golden vectors -> "142/142"
printf '%0256d' 0 | tr '0-9' '0' | fpsap exchange
# 6f627565f3e77f5b5ede91beee7baf92e4241e0bfpsap is hex-in/hex-out and never touches the network. verify makes any binary
self-proving. Full walkthrough: Quickstart.
sequenceDiagram
participant S as Sender (this code)
participant R as Receiver (Apple TV, speaker)
S->>R: m1 — "I am an AirPlay sender"
R->>S: m2 — 128-byte challenge (selects a mode)
S->>R: m3 — 20-byte response + local SAP
R->>S: m4 — acknowledgement, streaming may begin
An AirPlay 2 sender cannot open a stream until it answers a FairPlay SAP challenge
(the POST /fp-setup exchange). Historically the only way to compute that answer
was to run Apple's compiled binary inside an ARM64 emulator, because the algorithm
was unknown — only the machine code existed. This project recovers the algorithm
and computes the 20-byte response directly, in three stages:
flowchart LR
A[128-byte payload] --> P1[Phase 1<br/>white-box AES] --> GP[128-byte GP buffer]
GP --> BR[bridge<br/>fpsapcore] --> X9[20-byte digest]
X9 --> P2[Phase 2<br/>white-box MD5] --> R[20-byte response]
flowchart TD
subgraph Before
B1[Apple binary ~1.07 MB] --> B2[ARM64 emulator]
end
subgraph After
A1[~500 KB portable code] --- A2[6 languages]
end
Before -.replaced by.-> After
A full exchange is 5.19 µs with zero allocations — about 4.97× faster
than the emulator-based omarroth/doubletake, measured 24.96 µs in the same
interleaved session.
| time | |
|---|---|
| Full exchange | 5.19 µs |
| Phase 1 (white-box AES) | ~0.9 µs |
bridge (fpsapcore) |
~2.3 µs |
| Phase 2 (white-box MD5) | ~1.7 µs |
| doubletake, same session | 24.96 µs |
(Stage figures are measured separately; believe the full-exchange number — see How this was derived on why back-to-back builds mislead.)
The Go module (repo root) is the complete, drop-in implementation. The other five are single-file, dependency-free cores of the Phase-1 bridge, meant to be vendored. All six reproduce the shared corpora.
| Language | Role | Entry point | Also checked |
|---|---|---|---|
| Go | complete responder | fpbridge.FPExchangeBlobless |
142/142 golden + 8 emulator vectors, -race |
| C | bridge core | fp_bridge_x9_head_for_sap |
-Wpedantic -Wconversion, UBSan + ASan |
| Rust | bridge core | bridge_x9_head_for_sap |
debug build (overflow checks on) |
| C# | bridge core | BridgeX9HeadForSap |
CheckForOverflowUnderflow=true |
| Kotlin | bridge core | bridgeX9HeadForSap |
all arithmetic in Int |
| Python | bridge core | bridge_x9_head_for_sap |
asserts the corpus rejects 3 wrong ports |
See ports/ and the API reference.
Validated against 142 golden vectors, byte-for-byte agreement with two
independent emulator-based implementations (omarroth/doubletake,
nored/airfry), per-language conformance corpora (40 SAP-hash + 30 bridge
vectors = 70/70), and — since 2026-08-04 — three real HomePods, which accept
the computed response and reject deliberately corrupted ones. Twelve of those
accepted exchanges are kept as a replayable
hardware-attested corpus.
Three limits that do not get softened (full page):
- Hardware-validated on HomePods, and only HomePods. Three units
(firmware
23L471) accept the response and reject every corrupted one — a single flipped bit is refused. That is real evidence and also all of it: one product family, one firmware, one day. Every other FairPlay-capable device tested — two Apple TVs, two Macs — refuses before evaluating any response, so they give no verdict either way. Matrix in Pairing. - Mode 3 only. The receiver selects a FairPlay message mode; this answers mode 3 (the only one ever observed) and refuses the rest rather than returning bytes from the wrong key schedule.
- Use the session-aware m3. The frozen replay path is rejected by strict
receivers (
RTSP/1.0 466 Key Management Error);NewFPSAPSessionis not.
Full wiki in docs/:
| What is this? | Quickstart | The handshake |
| Architecture | White-box crypto | Porting guide |
| Conformance | API reference | Limitations |
| How this was derived | FAQ | Pairing |
Distributed under LGPL-3.0-or-later, because fpsapcore is derived from
omarroth/doubletake and the strongest
copyleft present sets the terms for the combined work.
Part of the tree is independent reverse engineering under Blue Oak 1.0.0 —
fairplayhash entirely, and fpbridge apart from six named files. The
ports/ cores are not: each fairplay_sapcore.* is a port of fpsapcore and
carries LGPL-3.0-or-later, so vendoring one carries copyleft with it. The
per-file breakdown is in NOTICE.md; every file states its own
licence in an SPDX-License-Identifier header, which is authoritative.
The white-box tables are Apple-derived data — the same recovered tables already public in the upstream project, no new exposure. No Apple source code is present, no Apple instruction is executed, and no Apple address survives anywhere.