defmodule PgpWordlist do @words Application.get_env(:pgp_wordlist, :words) @moduledoc """ Toolset for working with the PGP word list. Allows converting hex to / from words. The word list itself belongs to PGP Corporation. """ @doc """ Converts a 2 digit hex string (with uppercase letters) and even / odd position into a word on the PGP word list. `pos` is based on whether the hex appears at an even or odd index `rem(INDEX, 2)` ## Examples iex> PgpWordlist.hex_to_word("00", 0) "aardvark" iex> PgpWordlist.hex_to_word("00", 1) "adroitness" iex> PgpWordlist.hex_to_word("FF", 0) "Zulu" iex> PgpWordlist.hex_to_word("FF", 1) "Yucatan" iex> PgpWordlist.hex_to_word("1", 1) ** (ArgumentError) Hex must be 2 bytes long ('01' instead of '1') """ @spec hex_to_word(binary, 0..1) :: binary def hex_to_word(hex, pos) def hex_to_word(hex, _pos) when is_binary(hex) and byte_size(hex) == 1 do raise(ArgumentError, "Hex must be 2 bytes long ('01' instead of '1')") end @doc """ Converts a word to a hex based on the PGP word list. Will handle different cases of words and return hex with uppercase letters. ## Examples iex> PgpWordlist.word_to_hex("AARDVARK") "00" iex> PgpWordlist.word_to_hex("aardvark") "00" iex> PgpWordlist.word_to_hex("AardVaRk") "00" iex> PgpWordlist.word_to_hex("woodlark") "FE" """ @spec word_to_hex(binary) :: binary def word_to_hex(word) def word_to_hex(word), do: lowered_word_to_hex(String.downcase(word)) for {{word_a, word_b}, number} <- Enum.with_index(@words) do hex = number |> Integer.to_string(16) |> String.pad_leading(2, "0") def hex_to_word(unquote(hex), unquote(0)), do: unquote(word_a) def hex_to_word(unquote(hex), unquote(1)), do: unquote(word_b) defp lowered_word_to_hex(unquote(String.downcase(word_a))), do: unquote(hex) defp lowered_word_to_hex(unquote(String.downcase(word_b))), do: unquote(hex) end @doc """ Converts an even length hex string to list of words ## Examples iex> PgpWordlist.hex_to_words("00") ["aardvark"] iex> PgpWordlist.hex_to_words("E582") ["topmost", "Istanbul"] iex> PgpWordlist.hex_to_words("82E5") ["miser", "travesty"] iex> PgpWordlist.hex_to_words("00F") ** (ArgumentError) Hex must be an even amount of bytes long """ @spec hex_to_words(binary) :: nonempty_list(binary()) def hex_to_words(hex) def hex_to_words(hex) when is_binary(hex) and rem(byte_size(hex), 2) == 1 do raise(ArgumentError, "Hex must be an even amount of bytes long") end def hex_to_words(hex) when is_binary(hex) and rem(byte_size(hex), 2) == 0 do hex |> String.split("", trim: true) |> Stream.chunk_every(2) |> Stream.map(&Enum.join/1) |> Stream.with_index() |> Enum.map(fn {h, p} -> hex_to_word(h, rem(p, 2)) end) end @doc """ Converts a list of words to hex based on the PGP word list ## Examples iex> PgpWordlist.words_to_hex(["topmost", "Istanbul", "Pluto", "vagabond"]) "E58294F2" iex> PgpWordlist.words_to_hex(["miser", "travesty"]) "82E5" """ @spec words_to_hex(nonempty_list(binary)) :: binary def words_to_hex(words) def words_to_hex(words) when is_list(words) do words |> Enum.map(&word_to_hex/1) |> Enum.join() end end