defmodule AirPlay.V2.Pairing do
@moduledoc """
AirPlay 2 transient pairing.
Transient pairing is the no-PIN AP2 path used by many speakers. It performs
SRP-6a over `/pair-setup`, then upgrades the RTSP control connection to the AP2
encrypted channel. This is the first required AP2 milestone before SETUP/audio.
"""
alias AirPlay.V2.{Rtsp2, Srp, TLV}
defstruct [:rtsp, :shared_secret, :audio_key, type: :transient]
@type t :: %__MODULE__{
rtsp: Rtsp2.t(),
shared_secret: binary(),
audio_key: binary(),
type: :transient
}
@doc "Pair transiently with an AirPlay 2 receiver and return an encrypted RTSP state."
@spec transient(String.t(), keyword()) :: {:ok, t()} | {:error, term()}
def transient(host, opts \\ []) do
port = Keyword.get(opts, :port, 7000)
with {:ok, rtsp} <- Rtsp2.connect(host, port),
{:ok, rtsp} <- maybe_get_info(rtsp, opts),
{:ok, pair1, rtsp} <- pair_setup1(rtsp),
{:ok, srp, rtsp} <- pair_setup2(rtsp, pair1) do
rtsp = Rtsp2.enable_encryption(rtsp, srp.session_key)
audio_key = Keyword.get(opts, :audio_key) || :crypto.strong_rand_bytes(32)
{:ok, %__MODULE__{rtsp: rtsp, shared_secret: srp.session_key, audio_key: audio_key}}
end
end
defp maybe_get_info(rtsp, opts) do
if Keyword.get(opts, :preflight_info, true) do
case Rtsp2.request(rtsp, "GET", "/info", [], <<>>) do
{:ok, status, _headers, _body, rtsp} when status in 200..299 -> {:ok, rtsp}
{:ok, status, _headers, body, _rtsp} -> {:error, {:info, status, body}}
error -> error
end
else
{:ok, rtsp}
end
end
defp pair_setup1(rtsp) do
body = TLV.encode(state: 1, method: 0, flags: <<0x10, 0x00, 0x00, 0x00>>)
headers = [
{"X-Apple-HKP", "4"},
{"X-Apple-Client-Name", "AirPlay"},
{"Content-Type", "application/octet-stream"}
]
case Rtsp2.request(rtsp, "POST", "/pair-setup", headers, body) do
{:ok, 200, _headers, response, rtsp} ->
case TLV.decode(response) do
tlv when is_map(tlv) -> {:ok, tlv, rtsp}
error -> error
end
{:ok, status, _headers, body, _rtsp} ->
{:error, {:pair_setup1, status, body}}
error ->
error
end
end
defp pair_setup2(rtsp, %{public_key: server_public_key, salt: salt}) do
srp = Srp.transient(server_public_key, salt)
body =
TLV.encode(
state: 3,
public_key: srp.client_public_key,
proof: srp.client_proof
)
headers = [
{"X-Apple-HKP", "4"},
{"X-Apple-Client-Name", "AirPlay"},
{"Content-Type", "application/octet-stream"}
]
case Rtsp2.request(rtsp, "POST", "/pair-setup", headers, body) do
{:ok, 200, _headers, response, rtsp} -> verify_pair_setup2(response, srp, rtsp)
{:ok, status, _headers, body, _rtsp} -> {:error, {:pair_setup2, status, body}}
error -> error
end
end
defp pair_setup2(_rtsp, tlv), do: {:error, {:pair_setup1_missing_fields, Map.keys(tlv)}}
defp verify_pair_setup2(response, srp, rtsp) do
case TLV.decode(response) do
%{error: error} ->
{:error, {:pair_setup2, {:device_error, error}}}
%{proof: proof} when proof == srp.server_proof ->
{:ok, srp, rtsp}
%{proof: _proof} ->
{:error, {:pair_setup2, :server_proof_mismatch}}
tlv when is_map(tlv) ->
{:error, {:pair_setup2, {:missing_server_proof, Map.keys(tlv)}}}
error ->
error
end
end
end