Current section

Files

Jump to
tezex lib crypto utils.ex
Raw

lib/crypto/utils.ex

# Copied and modified from <https://github.com/starkbank/ecdsa-elixir>, Copyright (c) 2020 Stark Bank S.A, MIT License.
defmodule Tezex.Crypto.Utils do
  @moduledoc false
  import Bitwise

  @spec mod(integer(), integer()) :: non_neg_integer()
  def mod(x, n) do
    case rem(x, n) do
      r when r < 0 -> r + n
      r -> r
    end
  end

  @spec mod_add(integer(), integer(), integer()) :: non_neg_integer()
  def mod_add(left, right, modulus) do
    mod(left + right, modulus)
  end

  @spec mod_sub(integer(), integer(), integer()) :: non_neg_integer()
  def mod_sub(left, right, modulus) do
    mod(left - right, modulus)
  end

  @spec ipow(integer(), integer()) :: integer()
  def ipow(base, p) when is_integer(base) and is_integer(p) do
    ipow(base, p, 1)
  end

  defp ipow(base, p, acc) when p > 0 do
    ipow(base, p - 1, base * acc)
  end

  defp ipow(_base, _p, acc) do
    acc
  end

  @spec number_from_string(binary()) :: integer()
  def number_from_string(string) do
    {parsed_int, ""} =
      string
      |> Base.encode16()
      |> Integer.parse(16)

    parsed_int
  end

  @spec string_from_number(integer(), non_neg_integer()) :: binary()
  def string_from_number(number, string_length) do
    number
    |> Integer.to_string(16)
    |> fill_number_string(string_length)
    |> Base.decode16!()
  end

  defp fill_number_string(string, string_length) do
    String.duplicate("0", 2 * string_length - byte_size(string)) <> string
  end

  def rand_fun(bytes_needed) do
    :crypto.strong_rand_bytes(bytes_needed)
  end

  @spec between(number(), number()) :: number()
  def between(minimum, maximum) when minimum < maximum do
    range = maximum - minimum + 1
    {bytes_needed, mask} = calculate_parameters(range)

    # We apply the mask to reduce the amount of attempts we might need
    # to make to get a number that is in range. This is somewhat like
    # the commonly used 'modulo trick', but without the bias:
    #
    # > Let's say you invoke secure_rand(0, 60). When the other code
    # > generates a random integer, you might get 243. If you take
    # > (243 & 63)-- noting that the mask is 63-- you get 51. Since
    # > 51 is less than 60, we can return this without bias. If we
    # > got 255, then 255 & 63 is 63. 63 > 60, so we try again.
    #
    # > The purpose of the mask is to reduce the number of random
    # > numbers discarded for the sake of ensuring an unbiased
    # > distribution. In the example above, 243 would discard, but
    # > (243 & 63) is in the range of 0 and 60.
    #
    # (Source: Scott Arciszewski)

    random_number =
      rand_fun(bytes_needed)
      |> :binary.bin_to_list()
      |> bytes_to_number &&& mask

    if random_number < range do
      minimum + random_number
    else
      # Outside of the acceptable range, throw it away and try again.
      # We don't try any modulo tricks, as this would introduce bias.
      between(minimum, maximum)
    end
  end

  defp bytes_to_number(random_bytes, random_number \\ 0, i \\ 0)

  defp bytes_to_number([random_byte | other_random_bytes], random_number, i) do
    bytes_to_number(
      other_random_bytes,
      random_number ||| random_byte <<< (8 * i),
      i + 1
    )
  end

  defp bytes_to_number([], random_number, _i) do
    random_number
  end

  defp calculate_parameters(range) do
    calculate_parameters(range, 1, 0)
  end

  defp calculate_parameters(range, mask, bits_needed) when range > 0 do
    calculate_parameters(
      range >>> 1,
      mask <<< 1 ||| 1,
      bits_needed + 1
    )
  end

  defp calculate_parameters(_range, mask, bits_needed) do
    {div(bits_needed, 8) + 1, mask}
  end

  @spec pad(binary(), integer(), :leading | :trailing) :: binary()
  def pad(bin, byte_length, type) when is_binary(bin) do
    cond do
      byte_size(bin) == byte_length ->
        bin

      type == :leading ->
        pad_len = 8 * byte_length - byte_size(bin) * 8
        <<0::size(pad_len)>> <> bin

      type == :trailing ->
        pad_len = 8 * byte_length - byte_size(bin) * 8
        bin <> <<0::size(pad_len)>>
    end
  end

  def truncate_to_n(msg, n, trunc_only \\ false) do
    delta =
      byte_size(:binary.encode_unsigned(msg)) * 8 - byte_size(:binary.encode_unsigned(n)) * 8

    msg =
      if delta > 0 do
        msg >>> delta
      else
        msg
      end

    if not trunc_only and msg >= n do
      msg - n
    else
      msg
    end
  end
end