Skip to main content

lib/air_play/v2/pairing.ex

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