# Copyright (c) 2021 Łukasz Samosn
# Copyright (c) 2017 Adán Sánchez de Pedro Crespo
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
defmodule Bech32 do
use Bitwise
@moduledoc ~S"""
Encode and decode the Bech32 and Bech32m format, with checksums.
"""
# Encoding character set. Maps data value -> char
@charset 'qpzry9x8gf2tvdw0s3jn54khce6mua7l'
# Human-readable part and data part separator
@separator 0x31
# Generator coefficients
@generator [0x3B6A57B2, 0x26508E6D, 0x1EA119FA, 0x3D4233DD, 0x2A1462B3]
# Bech32m checksum constant
@bech32m_const 0x2BC830A3
@typedoc """
Base32 code point
"""
@type code_point_t :: 0..31
@typedoc """
Encoding type
:bech32 defined in BIP0173
:bech32m defined in BIP0350
"""
@type encoding_t :: :bech32 | :bech32m
@doc ~S"""
Encode a Bech32/Bech32m string.
## Examples
iex> Bech32.encode("bech32", [0, 1, 2], :bech32)
"bech321qpz4nc4pe"
iex> Bech32.encode("bech32", [0, 1, 2], :bech32m)
"bech321qpzq0geym"
iex> Bech32.encode("bc", [0, 14, 20, 15, 7, 13, 26, 0, 25, 18, 6, 11, 13,
...> 8, 21, 4, 20, 3, 17, 2, 29, 3, 12, 29, 3, 4, 15, 24, 20, 6, 14, 30, 22], :bech32)
"bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4"
"""
@spec encode(String.t(), list(code_point_t), encoding_t()) :: String.t()
def encode(hrp, data, encoding) when is_list(data) do
unless byte_size(hrp) in 1..83 do
raise ArgumentError, message: "invalid hrp length"
end
if hrp |> :binary.bin_to_list() |> Enum.any?(&(&1 < 33 || &1 > 126)) do
raise ArgumentError, message: "illegal character in hrp"
end
checksummed = data ++ create_checksum(hrp, data, encoding)
dp = for i <- checksummed, into: "", do: <<Enum.at(@charset, i)>>
<<hrp::binary, @separator, dp::binary>>
end
@spec encode(String.t(), String.t(), encoding_t()) :: String.t()
def encode(hrp, data, encoding) when is_binary(data) do
encode(hrp, :binary.bin_to_list(data), encoding)
end
@doc ~S"""
Decode a Bech32/Bech32m string.
## Examples
iex> Bech32.decode("bech321qpz4nc4pe")
{:ok, {"bech32", [0, 1, 2], :bech32}}
iex> Bech32.decode("bech321qpzq0geym")
{:ok, {"bech32", [0, 1, 2], :bech32m}}
iex> Bech32.decode("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4")
{:ok, {"bc", [0, 14, 20, 15, 7, 13, 26, 0, 25, 18, 6, 11, 13, 8, 21,
4, 20, 3, 17, 2, 29, 3, 12, 29, 3, 4, 15, 24, 20, 6, 14, 30, 22], :bech32}}
"""
@spec decode(String.t()) ::
{:ok, {String.t(), list(code_point_t), encoding_t()}} | {:error, String.t()}
def decode(bech) do
with {_, false} <-
{:mixed,
String.downcase(bech) != bech &&
String.upcase(bech) != bech},
bech_charlist = :binary.bin_to_list(bech),
{_, nil} <-
{:oor,
Enum.find(
bech_charlist,
fn c -> c < 33 || c > 126 end
)},
bech = String.downcase(bech),
len = Enum.count(bech_charlist),
pos =
Enum.find_index(Enum.reverse(bech_charlist), fn c ->
c == @separator
end),
{_, true} <- {:oor_sep, pos != nil},
pos = len - pos - 1,
{_, false} <- {:empty_hrp, pos < 1},
{_, false, _} <- {:short_cs, pos + 7 > len, len},
<<hrp::binary-size(pos), @separator, data::binary>> = bech,
data_charlist =
(for c <- :binary.bin_to_list(data) do
Enum.find_index(@charset, fn d -> c == d end)
end),
{_, nil} <-
{:oor_data,
Enum.find_index(
data_charlist,
fn c -> c < 0 || c > 31 end
)},
{_, {:ok, encoding}} <- {:cs, verify_checksum(hrp, data_charlist)},
data_len = Enum.count(data_charlist),
data = Enum.slice(data_charlist, 0, data_len - 6) do
{:ok, {hrp, data, encoding}}
else
{:mixed, _} -> {:error, "Mixed case"}
{:oor, c} -> {:error, "Character #{inspect(<<c>>)} out of range (#{c})"}
{:oor_sep, _} -> {:error, "No separator character"}
{:empty_hrp, _} -> {:error, "Empty HRP"}
{:short_cs, _, l} -> {:error, "Too short checksum (#{l})"}
{:oor_data, c} -> {:error, "Invalid data character #{inspect(<<c>>)} (#{c})}"}
{:cs, _} -> {:error, "Invalid checksum"}
_ -> {:error, "Unknown error"}
end
end
# Create a checksum.
defp create_checksum(hrp, data, encoding) do
values = expand_hrp(hrp) ++ data ++ [0, 0, 0, 0, 0, 0]
mod = bxor(polymod(values), get_encoding_const(encoding))
for p <- 0..5, do: mod >>> (5 * (5 - p)) &&& 31
end
# Verify a checksum.
defp verify_checksum(hrp, data) do
case polymod(expand_hrp(hrp) ++ data) do
1 -> {:ok, :bech32}
@bech32m_const -> {:ok, :bech32m}
_ -> :error
end
end
# Gets checksum constant
defp get_encoding_const(:bech32), do: 1
defp get_encoding_const(:bech32m), do: @bech32m_const
# Expand a HRP for use in checksum computation.
defp expand_hrp(hrp) do
hrp_charlist = :binary.bin_to_list(hrp)
a_values = for c <- hrp_charlist, do: c >>> 5
b_values = for c <- hrp_charlist, do: c &&& 31
a_values ++ [0] ++ b_values
end
# Find the polynomial with value coefficients mod the generator as 30-bit.
defp polymod(values) do
Enum.reduce(values, 1, fn v, chk ->
top = chk >>> 25
chk = bxor((chk &&& 0x1FFFFFF) <<< 5, v)
Enum.reduce(for(i <- 0..4, do: i), chk, fn i, chk ->
bxor(
chk,
if (top >>> i &&& 1) != 0 do
Enum.at(@generator, i)
else
0
end
)
end)
end)
end
end