lib/lob.ex

defmodule Lob do
  require Chacha20

  @moduledoc """
  Length-Object-Binary (LOB) Packet Encoding

  Data serialization, primarily in use by the [Telehash Project](http://telehash.org)
  """

  @type maybe_binary :: binary | nil

  @doc """
  Decode a wire packet for consumption

  The parts are returned in a struct compliant with the specification.
  Errors reflecting improperly decoded JSON are stored in the `json` field.
  """
  @spec decode(binary) :: Lob.DecodedPacket.t() | no_return
  def decode(<<s::size(16), rest::binary>>), do: rest |> decode_rest(s)

  @spec encode(maybe_binary | map, maybe_binary) :: binary | no_return
  @doc """
  Encode a head and body into a packet

  The packet should be usable across any supported transport.  May raise an
  exception if the payload is too large or there are encoding errors.
  """
  def encode(head, body) when is_nil(head), do: encode("", body)
  def encode(head, body) when is_nil(body), do: encode(head, "")
  def encode(head, body) when is_binary(head), do: head_size(head) <> head <> body
  def encode(head, body) when is_map(head), do: encode(Jason.encode!(head), body)

  defp head_size(s) when byte_size(s) <= 0xFFFF, do: <<byte_size(s)::size(16)>>
  defp head_size(s) when byte_size(s) > 0xFFFF, do: raise("Head payload too large.")

  @spec decode_rest(binary, char) :: Lob.DecodedPacket.t()
  defp decode_rest(r, _s) when r == "", do: %Lob.DecodedPacket{}
  defp decode_rest(r, s) when s == 0, do: Lob.DecodedPacket.__build__(nil, nil, r)

  defp decode_rest(r, s) do
    <<head::binary-size(s), body::binary>> = r

    json =
      if s <= 6 do
        nil
      else
        case Jason.decode(head) do
          {:ok, j} -> j
          e -> e
        end
      end

    Lob.DecodedPacket.__build__(head, json, body)
  end

  # SHA256 of "telehash"
  defp cloak_key,
    do:
      <<215, 240, 229, 85, 84, 98, 65, 178, 169, 68, 236, 214, 208, 222, 102, 133, 106, 197, 11,
        11, 171, 167, 106, 111, 90, 71, 130, 149, 108, 169, 69, 154>>

  @spec cloak(binary) :: binary
  @doc """
  Cloak a packet to frustrate wire monitoring

  A random number (between 1 and 20) of rounds are applied.  This also
  serves to slightly obfuscate the message size.
  """
  def cloak(b), do: cloak_loop(b, :rand.uniform(20))
  defp cloak_loop(b, 0), do: b

  defp cloak_loop(b, rounds) do
    n = make_nonce()
    cloak_loop(n <> Chacha20.crypt(b, cloak_key(), n), rounds - 1)
  end

  defp make_nonce do
    n = :crypto.strong_rand_bytes(8)

    case binary_part(n, 0, 1) do
      <<0>> -> make_nonce()
      _ -> n
    end
  end

  @spec decloak(binary) :: Lob.DecodedPacket.t() | no_return
  @doc """
  De-cloak a cloaked packet.

  Upon success, the decoded packet will have the number of cloaking rounds unfurled
  in the `cloaked` field.
  """
  def decloak(b), do: decloak_loop(b, 0)
  defp decloak_loop(<<0, _rest::binary>> = b, r), do: %{decode(b) | cloaked: r}

  defp decloak_loop(<<nonce::binary-size(8), ct::binary>>, r),
    do: decloak_loop(Chacha20.crypt(ct, cloak_key(), nonce), r + 1)
end