defmodule BitcoinLib.Key.HD.SeedPhrase do @moduledoc """ A seed phrase can generate a private key. It is a human friendly way to store private keys for disaster recovery. """ @pbkdf2_opts rounds: 2048, digest: :sha512, length: 64, format: :hex alias BitcoinLib.Crypto.BitUtils alias BitcoinLib.Key.HD.SeedPhrase.{Checksum, Wordlist} alias BitcoinLib.Key.HD.Entropy @doc """ Create a seed phrase out of entropy ## Examples iex> 101_750_443_022_601_924_635_824_320_539_097_414_732 ...> |> BitcoinLib.Key.HD.SeedPhrase.wordlist_from_entropy() "erode gloom apart system broom lemon dismiss post artist slot humor occur" """ @spec wordlist_from_entropy(integer()) :: binary() def wordlist_from_entropy(entropy) do entropy |> Binary.from_integer() |> append_checksum |> split_indices |> get_word_indices |> Wordlist.get_words() |> Enum.join(" ") end @doc """ Convert a set of 50 or 99 dice rolls into a 12 or 24 word list ## Examples iex> "12345612345612345612345612345612345612345612345612" ...> |> BitcoinLib.Key.HD.SeedPhrase.from_dice_rolls() {:ok, "blue involve cook print twist crystal razor february caution private slim medal"} """ @spec from_dice_rolls(binary()) :: {:ok, binary()} | {:error, binary()} def from_dice_rolls(dice_rolls) do case Entropy.from_dice_rolls(dice_rolls) do {:ok, entropy} -> {:ok, wordlist_from_entropy(entropy)} {:error, message} -> {:error, message} end end @doc """ Convert a set of 50 or 99 dice rolls into a 12 or 24 word list ## Examples iex> "12345612345612345612345612345612345612345612345612" ...> |> BitcoinLib.Key.HD.SeedPhrase.from_dice_rolls!() "blue involve cook print twist crystal razor february caution private slim medal" """ @spec from_dice_rolls!(binary()) :: binary() def from_dice_rolls!(dice_rolls) do case from_dice_rolls(dice_rolls) do {:ok, wordlist} -> wordlist {:error, message} -> throw(message) end end @doc """ Convert a seed phrase into a seed, with a optional passphrase See https://learnmeabitcoin.com/technical/mnemonic#mnemonic-to-seed ## Examples iex> "brick giggle panic mammal document foam gym canvas wheel among room analyst" ...> |> BitcoinLib.Key.HD.SeedPhrase.to_seed() "7e4803bd0278e223532f5833d81605bedc5e16f39c49bdfff322ca83d444892ddb091969761ea406bee99d6ab613fad6a99a6d4beba66897b252f00c9dd7b364" """ @spec to_seed(binary(), binary()) :: binary() def to_seed(seed_phrase, passphrase \\ "") do Pbkdf2.Base.hash_password(seed_phrase, "mnemonic#{passphrase}", @pbkdf2_opts) end @doc """ Executes the checksum on a seed phrase, making sure it's valid ## Examples iex> "brick giggle panic mammal document foam gym canvas wheel among room analyst" ...> |> BitcoinLib.Key.HD.SeedPhrase.validate() true """ @spec validate(binary()) :: boolean() def validate(seed_phrase) do words = seed_phrase |> String.split(" ") decoded = words |> Wordlist.get_indices() |> combine_indices() decoded_size = bit_size(decoded) checksum_size = Enum.count(words) |> div(3) data_size = decoded_size - checksum_size <> = decoded append_checksum(data) == decoded end @doc """ Takes two parts of a seed phrase, with a missing word anywhere in between. Returns a list of words that would complete it with a valid checksum ## Examples iex> first_words = "work tenant tourist globe among cattle suggest fever begin boil undo" ...> last_words = "work tenant tourist globe among cattle suggest fever begin boil undo slogan" ...> BitcoinLib.Key.HD.SeedPhrase.find_possible_missing_words(first_words, last_words) ["arrange", "broom", "genius", "hurt", "melody", "repeat", "save", "setup", "strategy"] """ @spec find_possible_missing_words(binary(), binary()) :: list() def find_possible_missing_words(first_words, last_words) do Wordlist.all() |> Enum.map(fn missing_word -> valid? = "#{first_words |> String.trim()} #{missing_word} #{last_words |> String.trim()}" |> String.trim() |> validate() {missing_word, valid?} end) |> Enum.filter(fn {_, valid?} -> valid? end) |> Enum.map(fn {valid_word, true} -> valid_word end) end defp append_checksum(binary_seed) do binary_seed |> Checksum.compute_and_append_to_seed() end defp pad_leading_zeros(bs) when is_bitstring(bs) do pad_length = 8 - rem(bit_size(bs), 8) <<0::size(pad_length), bs::bitstring>> end defp split_indices(seed_with_checksum) do seed_with_checksum |> BitUtils.split(11) end defp combine_indices(indices) do indices |> Enum.reduce( <<>>, fn indice, acc -> <> end ) end defp get_word_indices(chunks) do chunks |> Enum.map(&(&1 |> pad_leading_zeros |> :binary.decode_unsigned())) end end