defmodule StellarBase.StrKey do @moduledoc """ Allows encoding and decoding signatures used in the Stellar network. """ import Bitwise alias StellarBase.StrKeyError @type data :: String.t() | nil @type binary_data :: binary() | nil @type version_bytes :: integer() @type checksum :: integer() @type error :: {:error, atom()} @type validation :: :ok | error() @type decoded_components :: {:ok, {version_bytes(), binary(), checksum()}} | error() @type version :: :ed25519_public_key | :ed25519_secret_seed | :pre_auth_tx | :sha256_hash | :muxed_account | :signed_payload | :contract @version_bytes [ # Base32-encodes to 'G...' ed25519_public_key: 6 <<< 3, # Base32-encodes to 'S...' ed25519_secret_seed: 18 <<< 3, # Base32-encodes to 'T...' pre_auth_tx: 19 <<< 3, # Base32-encodes to 'X...' sha256_hash: 23 <<< 3, # Base32-encodes to 'M...' muxed_account: 12 <<< 3, # Base32-encodes to 'P...' signed_payload: 15 <<< 3, # Base32-encodes to 'C...' contract: 2 <<< 3 ] @spec encode(data :: binary_data(), version :: version()) :: {:ok, String.t()} | {:error, atom()} def encode(nil, _version), do: {:error, :invalid_binary} def encode(data, version) do version_bytes = Keyword.get(@version_bytes, version, :ed25519_public_key) payload = <> <> data payload |> (&CRC.crc(:crc_16_xmodem, &1)).() |> (&(payload <> <<&1::little-16>>)).() |> Base.encode32(padding: false) |> (&{:ok, &1}).() end @spec encode!(data :: binary_data(), version :: version()) :: String.t() | no_return() def encode!(data, version) do case encode(data, version) do {:ok, key} -> key {:error, reason} -> raise(StrKeyError, reason) end end @spec decode(data :: data(), version :: version()) :: {:ok, binary()} | error() def decode(nil, _version), do: {:error, :cant_decode_nil_data} def decode(data, version) do version_bytes = Keyword.get(@version_bytes, version, :ed25519_public_key) with {:ok, {decoded_version_bytes, decoded_data, decoded_checksum}} <- decode_components(data), :ok <- validate_version_bytes(version_bytes, decoded_version_bytes), :ok <- validate_checksum(decoded_data, version_bytes, decoded_checksum) do {:ok, decoded_data} end end @spec decode!(data :: data(), version :: atom()) :: binary() | no_return() def decode!(data, version) do case decode(data, version) do {:ok, key} -> key {:error, reason} -> raise(StrKeyError, reason) end end @spec decode_components(data :: binary()) :: decoded_components() defp decode_components(data) do data |> Base.decode32(padding: false) |> validate_decoded_binary() end @spec validate_decoded_binary(data :: {:ok, binary()} | :error) :: decoded_components() defp validate_decoded_binary( {:ok, <>} ), do: {:ok, {version_bytes, data, checksum}} defp validate_decoded_binary( {:ok, <>} ), do: {:ok, {version_bytes, data, checksum}} defp validate_decoded_binary({:ok, binary}) when byte_size(binary) >= 43 and byte_size(binary) <= 103 do data_size = byte_size(binary) - 3 <> = binary {:ok, {version_bytes, data, checksum}} end defp validate_decoded_binary(_decoded_data), do: {:error, :invalid_data_to_decode} @spec validate_version_bytes( version_bytes :: version_bytes(), decoded_version :: version_bytes() ) :: validation() defp validate_version_bytes(version_bytes, version_bytes), do: :ok defp validate_version_bytes(_version_bytes, _decoded_version_bytes), do: {:error, :unmatched_version_bytes} @spec validate_checksum( decoded_data :: binary(), version_bytes :: version_bytes(), decoded_checksum :: checksum() ) :: validation() defp validate_checksum(decoded_data, version_bytes, decoded_checksum) do checksum = CRC.crc(:crc_16_xmodem, <> <> decoded_data) if checksum == decoded_checksum, do: :ok, else: {:error, :invalid_checksum} end end