lib/grizzly/zwave/security.ex

defmodule Grizzly.ZWave.Security do
  @moduledoc """
  Helpers for security
  """
  import Bitwise

  @type key :: :s2_unauthenticated | :s2_authenticated | :s2_access_control | :s0

  @type key_byte :: 0x01 | 0x02 | 0x04 | 0x80

  @typedoc """
  Possible key exchange failures

  - `:none` - Bootstrapping was successful
  - `:key` - No match between requested and granted keys
  - `:scheme` - no scheme is supported by the controller or joining node
  - `:decrypt` - joining node failed to decrypt the input pin from the value. Wrong input value/DSK from user
  - `:cancel` - user has canceled the S2 bootstrapping
  - `:auth` - the echo kex change frame does not match the earlier exchanged frame
  - `:get` - the joining node requested a key that was not granted by the controller at an earlier stage
  - `:verify` - the joining node cannot verify and decrypt the exchanged key
  - `:report` - the including node transmitted a frame containing a different key than what was currently being exchanged
  """
  @type key_exchange_fail_type ::
          :none | :key | :scheme | :curves | :decrypt | :cancel | :auth | :get | :verify | :report

  @spec byte_to_keys(byte) :: [key]
  def byte_to_keys(granted_keys_byte) do
    <<s0::size(1), _::size(4), ac::size(1), auth::size(1), unauth::size(1)>> =
      <<granted_keys_byte>>

    keys = [s0: s0, ac: ac, auth: auth, unauth: unauth]

    Enum.reduce(keys, [], fn
      {:s0, 1}, acc -> acc ++ [:s0]
      {:ac, 1}, acc -> acc ++ [:s2_access_control]
      {:auth, 1}, acc -> acc ++ [:s2_authenticated]
      {:unauth, 1}, acc -> acc ++ [:s2_unauthenticated]
      {_, 0}, acc -> acc
    end)
  end

  @doc """
  Get the list of available security keys
  """
  @spec keys() :: [key()]
  def keys() do
    [:s2_unauthenticated, :s2_authenticated, :s2_access_control, :s0]
  end

  @spec keys_to_byte([key]) :: byte
  def keys_to_byte(keys) do
    Enum.reduce(keys, 0, fn key, byte -> byte ||| key_byte(key) end)
  end

  @doc """
  Validate the user input pin length, should be a 16 bit number
  """
  @spec validate_user_input_pin_length(non_neg_integer()) :: :valid | :invalid
  def validate_user_input_pin_length(n) when n >= 0 and n <= 65535, do: :valid
  def validate_user_input_pin_length(_), do: :invalid

  @doc """
  Decode a byte representation of the key exchanged failed type
  """
  @spec failed_type_from_byte(byte()) :: key_exchange_fail_type()
  def failed_type_from_byte(0x00), do: :none
  def failed_type_from_byte(0x01), do: :key
  def failed_type_from_byte(0x02), do: :scheme
  def failed_type_from_byte(0x03), do: :curves
  def failed_type_from_byte(0x05), do: :decrypt
  def failed_type_from_byte(0x06), do: :cancel
  def failed_type_from_byte(0x07), do: :auth
  def failed_type_from_byte(0x08), do: :get
  def failed_type_from_byte(0x09), do: :verify
  def failed_type_from_byte(0x0A), do: :report

  @spec failed_type_to_byte(key_exchange_fail_type()) :: byte()
  def failed_type_to_byte(:none), do: 0x00
  def failed_type_to_byte(:key), do: 0x01
  def failed_type_to_byte(:scheme), do: 0x02
  def failed_type_to_byte(:curves), do: 0x03
  def failed_type_to_byte(:decrypt), do: 0x05
  def failed_type_to_byte(:cancel), do: 0x06
  def failed_type_to_byte(:auth), do: 0x07
  def failed_type_to_byte(:get), do: 0x08
  def failed_type_to_byte(:verify), do: 0x09
  def failed_type_to_byte(:report), do: 0x0A

  @doc """
  Get the byte representation of a key.

  The key `:none` is an invalid key to encode to,
  so this function does not support encoding to that
  key.
  """
  @spec key_byte(key) :: key_byte()
  def key_byte(:s0), do: 0x80
  def key_byte(:s2_access_control), do: 0x04
  def key_byte(:s2_authenticated), do: 0x02
  def key_byte(:s2_unauthenticated), do: 0x01

  @doc """
  Gets the highest security level key from a key list

  Since Z-Wave will work at the highest S2 security group
  available on a node, if multiple groups are in a list of keys
  it will assume that highest level is the security level of the node
  who provided this list.

  If the node S0 security Z-Wave will response with granted keys
  with the lone key being S0.
  """
  @spec get_highest_level([key]) :: key | :none
  def get_highest_level([]), do: :none
  def get_highest_level([:s0]), do: :s0

  def get_highest_level(keys) do
    Enum.reduce(keys, fn
      :s2_access_control, _ ->
        :s2_access_control

      :s2_authenticated, last_highest when last_highest != :s2_access_control ->
        :s2_authenticated

      :s2_unauthenticated, last_highest
      when last_highest not in [:s2_authenticated, :s2_access_control] ->
        :s2_unauthenticated

      :s2_unauthenticated, :s2_authenticated ->
        :s2_authenticated

      :s2_unauthenticated, :s2_access_control ->
        :s2_access_control

      _, last_highest ->
        last_highest
    end)
  end
end