defmodule DeviceCheck do @moduledoc """ Elixir client for the [Apple DeviceCheck API](https://developer.apple.com/documentation/devicecheck). DeviceCheck lets your server: - Validate a device token generated by `DCDevice` - Query the two per-device fraud bits stored by Apple - Update those two bits to reflect device-level state The public surface is intentionally small: DeviceCheck.validate_token(device_token) DeviceCheck.query_bits(device_token) DeviceCheck.update_bits(device_token, bit0: true, bit1: false) DeviceCheck.token() ## Configuration config :device_check, team_id: System.get_env("APPLE_TEAM_ID"), key_id: System.get_env("DEVICE_CHECK_KEY_ID"), private_key: System.get_env("DEVICE_CHECK_PRIVATE_KEY"), development: true Every function also accepts per-call `opts` that override the application config. """ import Bitwise alias DeviceCheck.{Client, Token} @request_keys [:transaction_id, :timestamp, :bit0, :bit1] @type opts :: keyword() @type response :: {:ok, term()} | {:error, term()} @doc "Return a cached-per-call DeviceCheck access token after JWT generation." @spec token(opts) :: {:ok, String.t()} | {:error, term()} def token(opts \\ []), do: Token.access_token(opts) @doc "Validate a device token with Apple." @spec validate_token(String.t(), opts) :: response def validate_token(device_token, opts \\ []) when is_binary(device_token) do validate_device_token(device_token, opts) end @doc "Validate a device token with Apple." @spec validate_device_token(String.t(), opts) :: response def validate_device_token(device_token, opts \\ []) when is_binary(device_token) do {request_opts, config_opts} = split_request_opts(opts) Client.post( "/v1/validate_device_token", build_request_payload(device_token, request_opts), config_opts ) end @doc "Query the two DeviceCheck bits for a device token." @spec query_bits(String.t(), opts) :: {:ok, map() | :bit_state_not_found} | {:error, term()} def query_bits(device_token, opts \\ []) when is_binary(device_token) do query_two_bits(device_token, opts) end @doc "Query the two DeviceCheck bits for a device token." @spec query_two_bits(String.t(), opts) :: {:ok, map() | :bit_state_not_found} | {:error, term()} def query_two_bits(device_token, opts \\ []) when is_binary(device_token) do {request_opts, config_opts} = split_request_opts(opts) case Client.post( "/v1/query_two_bits", build_request_payload(device_token, request_opts), config_opts ) do {:ok, %{"bit0" => bit0, "bit1" => bit1} = body} -> {:ok, %{ bit0: bit0, bit1: bit1, last_update_time: Map.get(body, "last_update_time") }} {:ok, "Bit State Not Found"} -> {:ok, :bit_state_not_found} other -> other end end @doc "Update one or both DeviceCheck bits for a device token." @spec update_bits(String.t(), opts) :: response def update_bits(device_token, opts \\ []) when is_binary(device_token) do update_two_bits(device_token, opts) end @doc "Update one or both DeviceCheck bits for a device token." @spec update_two_bits(String.t(), opts) :: response def update_two_bits(device_token, opts \\ []) when is_binary(device_token) do {request_opts, config_opts} = split_request_opts(opts) with {:ok, body} <- build_update_payload(device_token, request_opts), {:ok, response} <- Client.post("/v1/update_two_bits", body, config_opts) do case response do nil -> {:ok, %{}} "" -> {:ok, %{}} other -> {:ok, other} end end end defp split_request_opts(opts) do Keyword.split(opts, @request_keys) end defp build_update_payload(device_token, request_opts) do body = build_request_payload(device_token, request_opts) bit0 = Keyword.get(request_opts, :bit0, :unset) bit1 = Keyword.get(request_opts, :bit1, :unset) body = body |> maybe_put_bit(:bit0, bit0) |> maybe_put_bit(:bit1, bit1) if Map.has_key?(body, :bit0) or Map.has_key?(body, :bit1) do {:ok, body} else {:error, {:missing_update_bits, [:bit0, :bit1]}} end end defp build_request_payload(device_token, request_opts) do %{ device_token: device_token, transaction_id: Keyword.get_lazy(request_opts, :transaction_id, &generate_transaction_id/0), timestamp: Keyword.get_lazy(request_opts, :timestamp, ¤t_timestamp_ms/0) } end defp maybe_put_bit(body, _key, :unset), do: body defp maybe_put_bit(body, key, value) when is_boolean(value), do: Map.put(body, key, value) defp maybe_put_bit(body, _key, _value), do: body defp current_timestamp_ms do System.system_time(:millisecond) end defp generate_transaction_id do <> = :crypto.strong_rand_bytes(16) part3 = bor(band(part3, 0x0FFF), 0x4000) part4 = bor(band(part4, 0x3FFF), 0x8000) :io_lib.format( ~c"~8.16.0b-~4.16.0b-~4.16.0b-~4.16.0b-~12.16.0b", [part1, part2, part3, part4, part5] ) |> IO.iodata_to_binary() end end