Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LNURL Package #151

Merged
merged 2 commits into from
Aug 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
236 changes: 236 additions & 0 deletions pkg/openvasp/lnurl/bech32.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
package lnurl

import (
"fmt"
"strings"
)

// Set of characters used in the data of bech32 strings. Note that this string is
// ordered, such that for a given charset[i], i is the binary value of the character.
const charset = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"

// gen encodes the generator polynomial for the bech32 BCH checksum.
var gen = []int{0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3}

// decode decodes a bech32 encoded string, returning the human-readable
// part and the data part excluding the checksum.
func decode(bech string) (string, []byte, error) {
// Only ASCII characters between 33 and 126 are allowed.
for i := 0; i < len(bech); i++ {
if bech[i] < 33 || bech[i] > 126 {
return "", nil, ErrInvalidCharacter(bech[i])
}
}

// The characters must be either all lowercase or all uppercase.
lower := strings.ToLower(bech)
upper := strings.ToUpper(bech)
if bech != lower && bech != upper {
return "", nil, ErrMixedCase
}

// We'll work with the lowercase string from now on.
bech = lower

// The string is invalid if the last '1' is non-existent, it is the
// first character of the string (no human-readable part) or one of the
// last 6 characters of the string (since checksum cannot contain '1'),
// or if the string is more than 90 characters in total.
one := strings.LastIndexByte(bech, '1')
if one < 1 || one+7 > len(bech) {
return "", nil, ErrInvalidSeparatorIndex(1)
}

// The human-readable part is everything before the last '1'.
hrp := bech[:one]
data := bech[one+1:]

// Each character corresponds to the byte with value of the index in
// 'charset'.
decoded, err := toBytes(data)
if err != nil {
return "", nil, fmt.Errorf("failed converting data to bytes: %w", err)
}

if !bech32VerifyChecksum(hrp, decoded) {
checksum := bech[len(bech)-6:]
expected, err := toChars(bech32Checksum(hrp, decoded[:len(decoded)-6]))
if err == nil {
err = ErrInvalidChecksum{expected, checksum}
}
return "", nil, fmt.Errorf("checksum failed: %w", err)
}

// We exclude the last 6 bytes, which is the checksum.
return hrp, decoded[:len(decoded)-6], nil
}

// encode encodes a byte slice into a bech32 string with the
// human-readable part hrb. Note that the bytes must each encode 5 bits
// (base32).
func encode(hrp string, data []byte) (string, error) {
// Calculate the checksum of the data and append it at the end.
checksum := bech32Checksum(hrp, data)
combined := append(data, checksum...)

// The resulting bech32 string is the concatenation of the hrp, the
// separator 1, data and checksum. Everything after the separator is
// represented using the specified charset.
dataChars, err := toChars(combined)
if err != nil {
return "", fmt.Errorf("unable to convert data bytes to chars: %w", err)
}
return hrp + "1" + dataChars, nil
}

// toBytes converts each character in the string 'chars' to the value of the
// index of the correspoding character in 'charset'.
func toBytes(chars string) ([]byte, error) {
decoded := make([]byte, 0, len(chars))
for i := 0; i < len(chars); i++ {
index := strings.IndexByte(charset, chars[i])
if index < 0 {
return nil, ErrNonCharsetChar(chars[i])
}
decoded = append(decoded, byte(index))
}
return decoded, nil
}

// toChars converts the byte slice 'data' to a string where each byte in 'data'
// encodes the index of a character in 'charset'.
func toChars(data []byte) (string, error) {
result := make([]byte, 0, len(data))
for _, b := range data {
if int(b) >= len(charset) {
return "", ErrInvalidDataByte(b)
}
result = append(result, charset[b])
}
return string(result), nil
}

// convertBits converts a byte slice where each byte is encoding fromBits bits,
// to a byte slice where each byte is encoding toBits bits.
func convertBits(data []byte, fromBits, toBits uint8, pad bool) ([]byte, error) {
if fromBits < 1 || fromBits > 8 || toBits < 1 || toBits > 8 {
return nil, ErrInvalidBitGroups
}

// The final bytes, each byte encoding toBits bits.
var regrouped []byte

// Keep track of the next byte we create and how many bits we have
// added to it out of the toBits goal.
nextByte := byte(0)
filledBits := uint8(0)

for _, b := range data {

// Discard unused bits.
b = b << (8 - fromBits)

// How many bits remaining to extract from the input data.
remFromBits := fromBits
for remFromBits > 0 {
// How many bits remaining to be added to the next byte.
remToBits := toBits - filledBits

// The number of bytes to next extract is the minimum of
// remFromBits and remToBits.
toExtract := remFromBits
if remToBits < toExtract {
toExtract = remToBits
}

// Add the next bits to nextByte, shifting the already
// added bits to the left.
nextByte = (nextByte << toExtract) | (b >> (8 - toExtract))

// Discard the bits we just extracted and get ready for
// next iteration.
b = b << toExtract
remFromBits -= toExtract
filledBits += toExtract

// If the nextByte is completely filled, we add it to
// our regrouped bytes and start on the next byte.
if filledBits == toBits {
regrouped = append(regrouped, nextByte)
filledBits = 0
nextByte = 0
}
}
}

// We pad any unfinished group if specified.
if pad && filledBits > 0 {
nextByte = nextByte << (toBits - filledBits)
regrouped = append(regrouped, nextByte)
filledBits = 0
nextByte = 0
}

// Any incomplete group must be <= 4 bits, and all zeroes.
if filledBits > 0 && (filledBits > 4 || nextByte != 0) {
return nil, ErrInvalidIncompleteGroup
}

return regrouped, nil
}

// For more details on the checksum calculation, please refer to BIP 173.
func bech32Checksum(hrp string, data []byte) []byte {
// Convert the bytes to list of integers, as this is needed for the
// checksum calculation.
integers := make([]int, len(data))
for i, b := range data {
integers[i] = int(b)
}
values := append(bech32HrpExpand(hrp), integers...)
values = append(values, []int{0, 0, 0, 0, 0, 0}...)
polymod := bech32Polymod(values) ^ 1
var res []byte
for i := 0; i < 6; i++ {
res = append(res, byte((polymod>>uint(5*(5-i)))&31))
}
return res
}

// For more details on the polymod calculation, please refer to BIP 173.
func bech32Polymod(values []int) int {
chk := 1
for _, v := range values {
b := chk >> 25
chk = (chk&0x1ffffff)<<5 ^ v
for i := 0; i < 5; i++ {
if (b>>uint(i))&1 == 1 {
chk ^= gen[i]
}
}
}
return chk
}

// For more details on HRP expansion, please refer to BIP 173.
func bech32HrpExpand(hrp string) []int {
v := make([]int, 0, len(hrp)*2+1)
for i := 0; i < len(hrp); i++ {
v = append(v, int(hrp[i]>>5))
}
v = append(v, 0)
for i := 0; i < len(hrp); i++ {
v = append(v, int(hrp[i]&31))
}
return v
}

// For more details on the checksum verification, please refer to BIP 173.
func bech32VerifyChecksum(hrp string, data []byte) bool {
integers := make([]int, len(data))
for i, b := range data {
integers[i] = int(b)
}
concat := append(bech32HrpExpand(hrp), integers...)
return bech32Polymod(concat) == 1
}
56 changes: 56 additions & 0 deletions pkg/openvasp/lnurl/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package lnurl

import (
"errors"
"fmt"
)

var (
ErrUnhandledScheme = errors.New("unhandled lnurl scheme")
ErrMixedCase = errors.New("string is not all lowercase or all uppercase")
ErrInvalidBitGroups = errors.New("only bit groups between 1 and 8 allowed")
ErrInvalidIncompleteGroup = errors.New("invalid incomplete group")
)

// ErrNonCharsetChar is returned when a character outside of the specific
// bech32 charset is used in the string.
type ErrNonCharsetChar rune

func (e ErrNonCharsetChar) Error() string {
return fmt.Sprintf("invalid character not part of charset: %v", int(e))
}

// ErrInvalidDataByte is returned when a byte outside the range required for
// conversion into a string was found.
type ErrInvalidDataByte byte

func (e ErrInvalidDataByte) Error() string {
return fmt.Sprintf("invalid data byte: %v", byte(e))
}

// ErrInvalidChecksum is returned when the extracted checksum of the string
// is different than what was expected.
type ErrInvalidChecksum struct {
Expected string
Actual string
}

func (e ErrInvalidChecksum) Error() string {
return fmt.Sprintf("expected %v, got %v", e.Expected, e.Actual)
}

// ErrInvalidCharacter is returned when the bech32 string has a character
// outside the range of the supported charset.
type ErrInvalidCharacter rune

func (e ErrInvalidCharacter) Error() string {
return fmt.Sprintf("invalid character in string: '%c'", rune(e))
}

// ErrInvalidSeparatorIndex is returned when the separator character '1' is
// in an invalid position in the bech32 string.
type ErrInvalidSeparatorIndex int

func (e ErrInvalidSeparatorIndex) Error() string {
return fmt.Sprintf("invalid separator index %d", int(e))
}
57 changes: 57 additions & 0 deletions pkg/openvasp/lnurl/lnurl.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
Helper functions for encoding and decoding (LNURLs) which are used to specify which
VASP controls a specific virtual asset address. OpenVASP and TRP both use LNURLs to
facilitate travel rule transfers and the TRISA protocol recommends its use.

https://www.21analytics.ch/blog/lnurl-for-fatf-travel-rule-software-solution/

The bech32 implementation of the bech32 format specified in BIP 173 was ported from
https://github.com/fiatjaf/go-lnurl per their MIT license and the test cases from the
BIP were ported from the https://github.com/btcsuite/btcd repository per their ISC
license.
*/
package lnurl

import (
"errors"
"strings"
)

// Encode a plain-text https URL into a bech32-encoded uppercased lnurl string.
func Encode(url string) (lnurl string, err error) {
var converted []byte
if converted, err = convertBits([]byte(url), 8, 5, true); err != nil {
return "", err
}

if lnurl, err = encode("lnurl", converted); err != nil {
return "", err
}
return strings.ToUpper(lnurl), nil
}

// Decode a bech32 encoded lnurl string and returns a plain-text https URL.
func Decode(lnurl string) (url string, err error) {
lnurl = strings.ToLower(lnurl)

if !strings.HasPrefix(lnurl, "lnurl1") {
return "", ErrUnhandledScheme
}

// bech32
tag, data, err := decode(lnurl)
if err != nil {
return "", err
}

if tag != "lnurl" {
return "", errors.New("tag is not 'lnurl', but '" + tag + "'")
}

converted, err := convertBits(data, 5, 8, false)
if err != nil {
return "", err
}

return string(converted), nil
}
Loading