Current section
Files
Jump to
Current section
Files
lib/erps/packet.ex
defmodule Erps.Packet do
@moduledoc false
# this is the construction of the erps packet, which looks as follows:
#
# bytes
#
# | 1 | 2 4 | 5 16 | 17 31 | 32 64 | 65 96 | 97 ...
# |------|---------|------------|----------------|-----------|--------------|---------
# | type | version | identifier | HMAC key | signature | payload size | payload
defstruct [
type: :keepalive,
version: %Version{major: 0, minor: 0, patch: 0, pre: []},
identifier: "",
hmac_key: <<0::16 * 8>>,
signature: <<0::32 * 8>>,
payload_size: 0,
payload: "",
]
@type_to_code %{keepalive: 0, error: 1, call: 4, cast: 8, reply: 2, push: 3}
@code_to_type %{0 => :keepalive, 1 => :error, 4 => :call, 8 => :cast, 2 => :reply, 3 => :push}
@valid_codes Map.keys(@code_to_type)
@type type ::
:call | :cast | :push | :reply | :error | :keepalive
@type t :: %__MODULE__{
type: type,
version: Version.t,
identifier: String.t,
hmac_key: String.t,
signature: binary,
payload_size: non_neg_integer,
payload: term
}
@empty_key <<0::16 * 8>>
@empty_sig <<0::32 * 8>>
@type decode_option ::
{:verification, (binary, binary -> boolean)} |
{:identifier, String.t} |
{:versions, String.t} |
{:safe, boolean}
#############################################################################
## DECODING
@spec decode(binary, [decode_option]) :: {:error, term} | {:ok, t}
def decode(packet, opts \\ [])
def decode(<<0>>, _) do
{:ok, %__MODULE__{type: :keepalive}}
end
def decode(packet = <<
code, v1, v2, v3,
pkt_identifier::binary-size(12),
hmac_key::binary-size(16),
signature::binary-size(32),
payload_size::32>> <> payload, opts)
when (code in @valid_codes) and (:erlang.size(payload) == payload_size) do
# key options to use in our conditional checking pipeline
verification = opts[:verification]
srv_identifier = opts[:identifier]
version_req = opts[:versions]
# set the appropriate lambda to use for unpickling.
binary_to_term = if opts[:safe] do
&Plug.Crypto.non_executable_binary_to_term(&1, [:safe])
else
&:erlang.binary_to_term/1
end
cond do
# verify that we are using the same remote protocol.
identifier_mismatches?(srv_identifier, pkt_identifier) ->
{:error, "wrong identifier"}
# verify that we are using an acceptable version
version_mismatches?(version_req, %Version{major: v1, minor: v2, patch: v3, pre: []}) ->
{:error, "incompatible version"}
# if verified, go ahead and generate the packet.
verification ->
verify_and_convert(verification, binary_to_term, packet, hmac_key, signature)
# if we don't care about auth, fail if provided.
hmac_key != @empty_key ->
{:error, "authentication provided"}
signature != @empty_sig ->
{:error, "authentication provided"}
# we didn't care about auth, and everything else matches.
true ->
binary_to_packet(packet, binary_to_term)
end
end
def decode(<<code, _rest::binary>>, _) when code not in @valid_codes do
{:error, "invalid code"}
end
def decode(_, _), do: {:error, "malformed packet"}
defp identifier_mismatches?(nil, _), do: false
defp identifier_mismatches?(srv_identifier, pkt_identifier) do
srv_identifier != trim(pkt_identifier)
end
defp version_mismatches?(nil, _), do: false
defp version_mismatches?(version_req, version) do
not Version.match?(version, version_req)
end
# if we care about auth, and don't provide it, die.
defp verify_and_convert(_, _, _, @empty_key, _), do: {:error, "authentication required"}
defp verify_and_convert(_, _, _, _, @empty_sig), do: {:error, "authentication required"}
defp verify_and_convert(verification, binary_to_term, packet, hmac_key, signature) do
packet
|> empty_sig
|> verification.(hmac_key, signature)
|> if do
binary_to_packet(packet, binary_to_term)
else
{:error, "authentication failed"}
end
end
##############################################################################
@doc false
@spec empty_sig(binary) :: binary
def empty_sig(<<prefix::binary-size(32), _ :: binary-size(32), rest :: binary>>) do
<<prefix :: binary, @empty_sig :: binary, rest :: binary>>
end
@spec binary_to_packet(binary, (binary -> term)) :: {:ok, t} | {:error, :badarg}
defp binary_to_packet(
<<code, v1, v2, v3,
identifier::binary-size(12),
hmac_key::binary-size(16),
signature::binary-size(32),
payload_size::32, payload :: binary>>,
binary_to_term) do
{:ok, %__MODULE__{
type: @code_to_type[code],
version: %Version{major: v1, minor: v2, patch: v3, pre: []},
identifier: trim(identifier),
hmac_key: hmac_key,
signature: signature,
payload_size: payload_size,
payload: binary_to_term.(payload)
}}
catch
# return unsafe arguments as badarg.
:error, :badarg ->
{:error, :badarg}
end
#############################################################################
@spec encode(t, keyword) :: iodata
def encode(packet, opts \\ [])
def encode(%__MODULE__{type: :keepalive}, _), do: <<0>>
def encode(packet = %__MODULE__{}, opts) do
type_code = @type_to_code[packet.type]
padded_id = pad(packet.identifier)
hmac_key = packet.hmac_key || @empty_key
version = packet.version
payload_binary = if opts[:compressed] do
compression = if opts[:compressed] === true, do: :compressed, else: {:compressed, opts[:compressed]}
:erlang.term_to_binary(packet.payload, [compression, minor_version: 2])
else
:erlang.term_to_binary(packet.payload)
end
payload_size = :erlang.size(payload_binary)
assembled_binary =
<<type_code, version.major, version.minor, version.patch,
padded_id :: binary, hmac_key :: binary,
0::(32 * 8), payload_size :: 32,
payload_binary::binary>>
sign_func = opts[:sign_with]
if sign_func do
[type_code, version.major, version.minor, version.patch, padded_id,
packet.hmac_key, sign_func.(assembled_binary),
<<payload_size :: 32>>, payload_binary]
else
assembled_binary
end
end
defp pad(string) do
leftover = 12 - :erlang.size(string)
string <> <<0::(leftover * 8)>>
end
defp trim(binary) do
prefix_size = :erlang.size(binary) - 1
case binary do
<<prefix::binary-size(prefix_size), 0>> -> trim(prefix)
_ -> binary
end
end
end