defmodule Mnemonic do @moduledoc """ BIP39 Implementation """ alias Mnemonic.{Crypto, Wordlist} @languages [ :english, :chinese_simplified, :chinese_traditional, :french, :italian, :japanese, :korean, :spanish ] @valid_entropy_length [128, 160, 192, 224, 256] @valid_mnemonic_length [12, 15, 18, 21, 24] # SEE: https://github.com/bitcoin/bips/blob/master/bip-0039/bip-0039-wordlists.md @ideographic_space "\u3000" defguardp valid_ent?(ent) when ent in @valid_entropy_length defguardp valid_entropy?(entropy) when valid_ent?(bit_size(entropy)) defguardp supported_lang?(lang) when lang in @languages @typedoc """ Supported mnemonic languages """ @type language :: :english | :chinese_simplified | :chinese_traditional | :french | :italian | :japanese | :korean | :spanish @doc ~S""" Generate mnemonic sentences with given entropy length(in bits) and mnemonic language. Allowed entropy length are 128, 160, 192, 224 and 256. Supported languages are English, Chinese(Simplified), Chinese(Tranditional), Japanese, Korean, Spanish, French and Italian. """ @spec generate(ent :: integer(), lang :: language()) :: String.t() | {:error, term()} def generate(ent, lang) when valid_ent?(ent) and supported_lang?(lang) do ent |> generate_entropy() |> from_entropy(lang) end def generate(_, _), do: {:error, :invalid_entropy_length_or_language} @doc ~S""" Generate mnemonic sentences with given entropy and mnemonic language. The bits size of entropy should be in 128, 160, 192, 224 and 256. Supported languages are English, Chinese(Simplified), Chinese(Tranditional), Japanese, Korean, Spanish, French and Italian. ## Examples iex> entropy = <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>> iex> Mnemonic.from_entropy(entropy, :english) "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" """ @spec from_entropy(entropy :: binary(), lang :: language()) :: String.t() | {:error, term()} def from_entropy(entropy, lang) when valid_entropy?(entropy) and supported_lang?(lang) do entropy |> append_checksum() |> generate_mnemonic(lang) end @doc ~S""" Generate seed by given mnemonic, passphrase and language. The seed is 64 bytes. ## Examples iex> mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" iex> Mnemonic.to_seed(mnemonic, "TREZOR", :english) <<197, 82, 87, 195, 96, 192, 124, 114, 2, 154, 235, 193, 181, 60, 5, 237, 3, 98, 173, 163, 142, 173, 62, 62, 158, 250, 55, 8, 229, 52, 149, 83, 31, 9, 166, 152, 117, 153, 209, 130, 100, 193, 225, 201, 47, 44, 241, 65, 99, 12, 122, 60, 74, 183, 200, 27, 47, 0, 22, 152, 231, 70, 59, 4>> """ @spec to_seed(mnemonic :: String.t(), passphrase :: String.t(), lang :: language()) :: binary | {:error, term()} def to_seed(mnemonic, passphrase \\ "", lang) when is_binary(mnemonic) do with mnemonic = normalize(mnemonic), {:ok, _entropy} <- validate(mnemonic, lang) do Crypto.pbkdf2(&Crypto.hmac_sha512/2, mnemonic, salt(passphrase), 2048) end end @doc ~S""" Check the given mnemonic is valid or not. ## Examples iex(17)> mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" iex(19)> Mnemonic.validate(mnemonic, :english) {:ok, <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>} """ @spec validate(mnemonic :: String.t(), lang :: language()) :: {:ok, binary()} | {:error, term()} def validate(mnemonic, lang) when is_binary(mnemonic) and supported_lang?(lang) do mnemonic |> mnemonic_to_words() |> words_to_checksummed_entropy(lang) |> checksummed_entropy_to_entropy() end defp salt(passphrase), do: normalize("mnemonic" <> passphrase) defp generate_entropy(ent) do ent |> div(8) |> :crypto.strong_rand_bytes() end defp append_checksum(entropy) do cs = entropy |> bit_size() |> div(32) <> = Crypto.sha256(entropy) <> end defp generate_mnemonic(entropy, lang) do joiner = case lang do :japanese -> @ideographic_space _otherwise -> " " end entropy |> split_to_group() |> Enum.map(&Wordlist.at(lang, &1)) |> Enum.join(joiner) end defp split_to_group(entropy) do do_split_to_group(entropy, []) end defp do_split_to_group(<<>>, groups), do: groups defp do_split_to_group(<>, groups) do do_split_to_group(rest, groups ++ [group]) end defp mnemonic_to_words(mnemonic) do mnemonic |> String.trim() |> normalize() |> String.split(" ") |> case do words when length(words) in @valid_mnemonic_length -> {:ok, words} _otherwise -> {:error, :invalid_words} end end defp words_to_checksummed_entropy({:error, error}, _lang), do: {:error, error} defp words_to_checksummed_entropy({:ok, words}, lang) when is_list(words) do indexes = Enum.map(words, &Wordlist.find_index(lang, &1)) case Enum.any?(indexes, &is_nil(&1)) do true -> {:error, :invalid_words} false -> checksummed_entropy = indexes |> Enum.reverse() |> Enum.reduce(<<>>, &<<&1::11, &2::bits>>) {:ok, checksummed_entropy} end end defp checksummed_entropy_to_entropy({:error, error}), do: {:error, error} defp checksummed_entropy_to_entropy({:ok, checksummed_entropy}) do checksummed_entropy |> extract_entropy() |> validate_checksum() end defp extract_entropy(checksummed_entropy) when is_bitstring(checksummed_entropy) do ent = bit_size(checksummed_entropy) |> Kernel.*(32) |> div(33) cs = div(ent, 32) with <> <- checksummed_entropy do {:ok, entropy, checksum} else _error -> {:error, :invalid_mnemonic} end end defp validate_checksum({:error, error}), do: {:error, error} defp validate_checksum({:ok, entropy, checksum}) do cs = bit_size(checksum) <> = Crypto.sha256(entropy) if valid_checksum == checksum do {:ok, entropy} else {:error, :invalid_mnemonic_checksum} end end defp normalize(string), do: :unicode.characters_to_nfkd_binary(string) end