Current section
Files
Jump to
Current section
Files
lib/device_check.ex
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
<<part1::32, part2::16, part3::16, part4::16, part5::48>> = :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