defmodule Mnemoniac do @moduledoc """ Mnemoniac is an implementation of BIP-39 which describes generation of mnemonic codes or mnemonic sentences - a group of easy to remember words - for the generation of deterministic wallets. See https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki """ @word_numbers_to_entropy_bits %{12 => 128, 15 => 160, 18 => 192, 21 => 224, 24 => 256} @word_numbers Map.keys(@word_numbers_to_entropy_bits) @entropy_bits_sizes Map.values(@word_numbers_to_entropy_bits) @words :mnemoniac |> :code.priv_dir() |> Path.join("words") |> File.stream!() |> Stream.map(&String.trim/1) |> Enum.to_list() @doc """ Create a random mnemonic with the provided number of words. By default, the number of words is 24. Allowed numbers of words are 12, 15, 18, 24 ## Examples iex> {:ok, mnemonic} = Mnemoniac.create_mnemonic() iex> mnemonic |> String.split(" ") |> Enum.count() 24 iex> {:ok, mnemonic} = Mnemoniac.create_mnemonic(12) iex> mnemonic |> String.split(" ") |> Enum.count() 12 iex> Mnemoniac.create_mnemonic(10) {:error, :invalid_number} """ @spec create_mnemonic(non_neg_integer()) :: {:ok, String.t()} | {:error, :invalid_number} def create_mnemonic(word_number \\ Enum.max(@word_numbers)) def create_mnemonic(word_number) when word_number not in @word_numbers do {:error, :invalid_number} end def create_mnemonic(word_number) do entropy_bits = Map.fetch!(@word_numbers_to_entropy_bits, word_number) mnemonic = entropy_bits |> create_entropy() |> do_create_from_entropy(entropy_bits) {:ok, mnemonic} end @doc """ Similar to `create_mnemonic/1`, but fails the number of words is not supported ## Examples iex> mnemonic = Mnemoniac.create_mnemonic!() iex> mnemonic |> String.split(" ") |> Enum.count() 24 iex> mnemonic = Mnemoniac.create_mnemonic!(12) iex> mnemonic |> String.split(" ") |> Enum.count() 12 iex> Mnemoniac.create_mnemonic!(10) ** (ArgumentError) Number of words 10 is not supported, please use one of the [12, 15, 18, 21, 24] """ @spec create_mnemonic!(non_neg_integer()) :: String.t() | no_return() def create_mnemonic!(word_number \\ Enum.max(@word_numbers)) do case create_mnemonic(word_number) do {:ok, mnemonic} -> mnemonic _ -> raise ArgumentError, message: "Number of words #{inspect(word_number)} is not supported, please use one of the #{inspect(@word_numbers)}" end end @doc """ Create a mnemonic from entropy. The supported byte sizes are 16, 20, 24, 32 ## Examples iex> Mnemoniac.create_mnemonic_from_entropy(<<6, 197, 169, 93, 98, 210, 82, 216, 148, 177, 1, 251, 142, 15, 154, 85, 140, 0, 13, 202,234, 160, 129, 218>>) {:ok, "almost coil firm shield cement hobby fan cage wine idea track prison scale alone close favorite limb still"} iex> Mnemoniac.create_mnemonic_from_entropy(<<6, 197, 169, 93, 98, 210, 82, 216, 148, 177, 1, 251, 142, 15, 154, 85, 140, 0, 13, 202,234, 160, 129, 218, 6, 197, 169, 93, 98, 210, 82, 216>>) {:ok, "almost coil firm shield cement hobby fan cage wine idea track prison scale alone close favorite limb south ramp famous stomach hard enter author"} iex> Mnemoniac.create_mnemonic_from_entropy(<<1>>) {:error, :invalid_entropy} """ @spec create_mnemonic_from_entropy(binary()) :: {:ok, String.t()} | {:error, :invalid_entropy} def create_mnemonic_from_entropy(entropy) do found_entropy_bits = Enum.find(@word_numbers_to_entropy_bits, fn {_number, bits} -> div(bits, 8) == byte_size(entropy) end) case found_entropy_bits do {_, entropy_bits} -> mnemonic = do_create_from_entropy(entropy, entropy_bits) {:ok, mnemonic} _ -> {:error, :invalid_entropy} end end @doc """ Similar to `create_mnemonic_from_entropy/1`, but fails the entropy has unsupported byte size ## Examples iex> Mnemoniac.create_mnemonic_from_entropy!(<<6, 197, 169, 93, 98, 210, 82, 216, 148, 177, 1, 251, 142, 15, 154, 85, 140, 0, 13, 202,234, 160, 129, 218>>) "almost coil firm shield cement hobby fan cage wine idea track prison scale alone close favorite limb still" iex> Mnemoniac.create_mnemonic_from_entropy!(<<1>>) ** (ArgumentError) Entropy size is invalid """ @spec create_mnemonic_from_entropy!(binary()) :: String.t() | no_return def create_mnemonic_from_entropy!(entropy) do case create_mnemonic_from_entropy(entropy) do {:ok, mnemonic} -> mnemonic _ -> raise ArgumentError, message: "Entropy size is invalid" end end @doc """ Return all 2048 words used for mnemonic generation """ @spec words() :: [String.t()] def words, do: @words @doc """ Return a map of word numbers to entropy bits. """ @spec word_numbers_to_entropy_bits() :: %{non_neg_integer() => non_neg_integer()} def word_numbers_to_entropy_bits, do: @word_numbers_to_entropy_bits @doc """ Return supported numbers of words that can be used for mnemonic generation """ @spec word_numbers() :: [non_neg_integer()] def word_numbers, do: @word_numbers @doc """ Return supported entopy bit sizes """ @spec entropy_bit_sizes() :: [non_neg_integer()] def entropy_bit_sizes, do: @entropy_bits_sizes defp do_create_from_entropy(entropy, entropy_bits) do entropy |> append_checksum(entropy_bits) |> to_mnemonic() end defp create_entropy(entropy_bits) do entropy_bits |> div(8) |> :crypto.strong_rand_bytes() end defp append_checksum(entropy, entropy_bits) do checksum_size = div(entropy_bits, 32) <> = :crypto.hash(:sha256, entropy) <> end defp to_mnemonic(bytes) do words = for <> do Enum.at(@words, chunk) end Enum.join(words, " ") end end