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 %{
3 => 32,
6 => 64,
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 3, 6, 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 [3, 6, 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)
<<checksum::bits-size(checksum_size), _::bits>> = :crypto.hash(:sha256, entropy)
<<entropy::bits, checksum::bits>>
end
defp to_mnemonic(bytes) do
words =
for <<chunk::size(11) <- bytes>> do
Enum.at(@words, chunk)
end
Enum.join(words, " ")
end
end