Skip to content
Open
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
75 changes: 24 additions & 51 deletions bolt12/bech32.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,8 @@ import (
)

var (
// ErrStringTooLong is returned when a raw string is longer than
// maxBolt12RawStringLen or a cleaned string is longer than
// maxBolt12StringLen. It is also returned when a payload is larger
// than maxBolt12DataLen.
// ErrStringTooLong is returned when a payload is larger than
// maxBolt12DataLen.
ErrStringTooLong = errors.New("input length exceeds limit")

// ErrEmptyString is returned when a string has no characters. It is
Expand Down Expand Up @@ -72,30 +70,19 @@ const (
maxPrintableASCII = 126

// bolt12HRPLen is the length of a BOLT 12 human-readable prefix. All
// three prefixes have it, so the limit below counts it as a fixed cost.
// three prefixes have it.
bolt12HRPLen = 3

// maxBolt12DataLen is the largest TLV stream that one BOLT 12 string
// can hold. The spec limits neither a field nor the stream, so the
// limit comes from this package: the P2P decoder rejects a record above
// tlv.MaxRecordSize. Eleven offer fields at that size give 704
// kibibytes, and one mebibyte leaves room for unknown odd fields. Only
// an offer needs the room, because an invoice travels in a smaller
// onion message.
// maxBolt12DataLen is the largest TLV stream encodeBech32 emits. It
// is a writer policy, not a protocol rule: the spec limits neither a
// field nor the stream, and no other implementation caps either. The
// P2P decoder rejects a record above tlv.MaxRecordSize, eleven offer
// fields at that size give 704 kibibytes, and one mebibyte leaves
// room for unknown odd fields. decodeBech32 enforces no length limit,
// because a decode allocates on the order of its input and the input
// already exists in the caller's memory, so a limit there would
// reject a spec-valid message without protecting anything.
maxBolt12DataLen = 1 << 20

// maxBolt12StringLen is the largest cleaned BOLT 12 bech32 string the
// codec accepts, once continuation markers and their whitespace are
// stripped. Each character of the data part holds 5 of the 8 bits of
// a payload byte. The limit therefore comes from maxBolt12DataLen. It
// counts the prefix, the separator, and one character for each group
// of 5 bits. Encode and Decode use the same limit, so every string
// that Encode makes is a string that Decode accepts.
maxBolt12StringLen = bolt12HRPLen + 1 + (maxBolt12DataLen*8+4)/5

// maxBolt12RawStringLen is the largest raw BOLT 12 string the codec
// accepts, continuation markers and whitespace included.
maxBolt12RawStringLen = 2 * maxBolt12StringLen
)

// validHRPs holds the prefixes the BOLT 12 codec accepts, in the order the
Expand All @@ -116,32 +103,18 @@ func unsupportedHRPError(hrp string) error {
)
}

// Decode reads a BOLT 12 bech32 string. It returns the human-readable prefix
// and the data bytes. A BOLT 12 string has no checksum. A '+' character can
// join two parts of the string, and whitespace can follow it. Decode rejects
// a raw string above maxBolt12RawStringLen and a cleaned string above
// maxBolt12StringLen, but the caller must set a smaller limit for its own
// medium. See the caller obligations in the package documentation.
func Decode(s string) (string, []byte, error) {
if len(s) > maxBolt12RawStringLen {
return "", nil, fmt.Errorf(
"bolt12: %w: input length %d exceeds limit %d",
ErrStringTooLong, len(s), maxBolt12RawStringLen,
)
}

// decodeBech32 reads a BOLT 12 bech32 string. It returns the human-readable
// prefix and the data bytes. A BOLT 12 string has no checksum. A '+' character
// can join two parts of the string, and whitespace can follow it. It enforces
// the BOLT 12 encoding rules and no length limit, so the caller bounds its own
// medium: the onion-message envelope bounds an invoice_request and an invoice,
// and the RPC or CLI bounds a pasted or scanned offer string.
func decodeBech32(s string) (string, []byte, error) {
cleaned, err := stripContinuation(s)
if err != nil {
return "", nil, err
}

if len(cleaned) > maxBolt12StringLen {
return "", nil, fmt.Errorf(
"bolt12: %w: cleaned length %d exceeds limit %d",
ErrStringTooLong, len(cleaned), maxBolt12StringLen,
)
}

if len(cleaned) == 0 {
return "", nil, fmt.Errorf("bolt12: %w", ErrEmptyString)
}
Expand Down Expand Up @@ -183,19 +156,19 @@ func Decode(s string) (string, []byte, error) {
return hrp, data8bit, nil
}

// Encode makes a BOLT 12 bech32 string from the data bytes and the given
// encodeBech32 makes a BOLT 12 bech32 string from the data bytes and the given
// human-readable prefix. It adds no checksum. It changes the prefix to
// lowercase and takes only lno, lnr, and lni. The payload size must be a size
// that Decode also takes, so a caller can make only strings that Decode reads.
func Encode(hrp string, data []byte) (string, error) {
// lowercase and takes only lno, lnr, and lni. It refuses a payload above
// maxBolt12DataLen, a writer policy the reader does not mirror.
func encodeBech32(hrp string, data []byte) (string, error) {
hrp = strings.ToLower(hrp)
if !isValidHRP(hrp) {
return "", unsupportedHRPError(hrp)
}

// A BOLT 12 string holds a TLV stream, and the stream must hold at
// least one record. An empty payload gives a string with only the
// prefix and the separator, which Decode rejects.
// prefix and the separator, which decodeBech32 rejects.
if len(data) == 0 {
return "", fmt.Errorf(
"bolt12: %w: nothing to encode", ErrEmptyString,
Expand Down
92 changes: 47 additions & 45 deletions bolt12/bech32_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func TestBech32FormatStringVectors(t *testing.T) {
t.Run(tc.Comment, func(t *testing.T) {
t.Parallel()

hrp, decoded, err := Decode(tc.String)
hrp, decoded, err := decodeBech32(tc.String)

if !tc.Valid {
require.Error(t, err, "expected error for: %s",
Expand All @@ -37,10 +37,10 @@ func TestBech32FormatStringVectors(t *testing.T) {
require.NotEmpty(t, decoded)

// Round-trip: re-encode and decode again.
encoded, err := Encode(hrp, decoded)
encoded, err := encodeBech32(hrp, decoded)
require.NoError(t, err)

hrp2, decoded2, err := Decode(encoded)
hrp2, decoded2, err := decodeBech32(encoded)
require.NoError(t, err)
require.Equal(t, hrp, hrp2)
require.Equal(t, decoded, decoded2)
Expand All @@ -59,11 +59,11 @@ func TestBech32RoundTrip(t *testing.T) {
t.Run(hrp, func(t *testing.T) {
t.Parallel()

encoded, err := Encode(hrp, testData)
encoded, err := encodeBech32(hrp, testData)
require.NoError(t, err)
require.True(t, len(encoded) > len(hrp)+1)

gotHRP, gotData, err := Decode(encoded)
gotHRP, gotData, err := decodeBech32(encoded)
require.NoError(t, err)
require.Equal(t, hrp, gotHRP)
require.Equal(t, testData, gotData)
Expand Down Expand Up @@ -105,7 +105,7 @@ func TestBech32DecodeErrors(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()

_, _, err := Decode(tc.input)
_, _, err := decodeBech32(tc.input)
require.Error(t, err)
})
}
Expand Down Expand Up @@ -239,13 +239,13 @@ func TestDecodeContinuationAnywhere(t *testing.T) {
t.Parallel()

payload := []byte{0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef}
encoded, err := Encode(HRPOffer, payload)
encoded, err := encodeBech32(HRPOffer, payload)
require.NoError(t, err)

for i := 1; i < len(encoded); i++ {
split := encoded[:i] + "+" + encoded[i:]

hrp, data, err := Decode(split)
hrp, data, err := decodeBech32(split)
require.NoError(t, err, "marker at position %d", i)
require.Equal(t, HRPOffer, hrp)
require.Equal(t, payload, data)
Expand All @@ -259,7 +259,7 @@ func TestDecodeContinuationAnywhere(t *testing.T) {
func TestEncodeUnknownHRP(t *testing.T) {
t.Parallel()

_, err := Encode("bogus", []byte{0x00})
_, err := encodeBech32("bogus", []byte{0x00})
require.ErrorIs(t, err, ErrUnsupportedHRP)

for _, hrp := range validHRPs {
Expand All @@ -272,7 +272,7 @@ func TestEncodeUnknownHRP(t *testing.T) {
func TestDecodeUnknownHRP(t *testing.T) {
t.Parallel()

_, _, err := Decode("bogus1pqps7sjq")
_, _, err := decodeBech32("bogus1pqps7sjq")
require.ErrorIs(t, err, ErrUnsupportedHRP)
}

Expand All @@ -293,51 +293,53 @@ func TestDecodeUnprintableCharacter(t *testing.T) {
}

for _, input := range unprintable {
_, _, err := Decode(input)
_, _, err := decodeBech32(input)
require.ErrorIs(t, err, ErrInvalidCharacter)
}
}

// TestDecodeOversizeInput asserts the input length cap fires before any
// allocation.
func TestDecodeOversizeInput(t *testing.T) {
// maxEncodedLen is the length of the string Encode makes from the largest
// payload it accepts: the prefix, the separator, and one character per group
// of five payload bits.
const maxEncodedLen = bolt12HRPLen + 1 + (maxBolt12DataLen*8+4)/5

// TestDecodeAcceptsAboveWriterLimit asserts Decode enforces no length limit of
// its own, so a string longer than anything Encode emits still decodes. The
// bound belongs to the caller's medium, not to the codec.
func TestDecodeAcceptsAboveWriterLimit(t *testing.T) {
t.Parallel()

// A raw string above the transport limit is rejected.
huge := strings.Repeat("a", maxBolt12RawStringLen+1)
_, _, err := Decode(huge)
require.ErrorIs(t, err, ErrStringTooLong)

// A string under the raw limit but over the cleaned limit is
// rejected after stripping.
oversize := strings.Repeat("a", maxBolt12StringLen+1)
_, _, err = Decode(oversize)
require.ErrorIs(t, err, ErrStringTooLong)

// A string at the cleaned limit is accepted, but here leads to a
// parsing error.
oversize = strings.Repeat("a", maxBolt12StringLen)
_, _, err = Decode(oversize)
require.ErrorIs(t, err, ErrInvalidSeparator)
payload := make([]byte, maxBolt12DataLen)
encoded, err := encodeBech32(HRPOffer, payload)
require.NoError(t, err)

// Eight more data characters carry five more payload bytes, so the
// string and its payload both exceed what Encode would emit.
oversize := encoded + strings.Repeat("q", 8)
require.Greater(t, len(oversize), maxEncodedLen)

hrp, data, err := decodeBech32(oversize)
require.NoError(t, err)
require.Equal(t, HRPOffer, hrp)
require.Greater(t, len(data), maxBolt12DataLen)
}

// TestDecodeWrappedMaxPayload asserts that a legal continuation wrapping of the
// longest string Encode can make still decodes. The cleaned limit governs the
// payload, and the raw limit leaves room for the wrapping.
// longest string Encode can make still decodes.
func TestDecodeWrappedMaxPayload(t *testing.T) {
t.Parallel()

payload := make([]byte, maxBolt12DataLen)
encoded, err := Encode(HRPOffer, payload)
encoded, err := encodeBech32(HRPOffer, payload)
require.NoError(t, err)
require.Len(t, encoded, maxBolt12StringLen)
require.Len(t, encoded, maxEncodedLen)

// Insert a marker and a whitespace run into the data part. The raw
// string grows past the cleaned limit but stays under the raw one.
// Insert a marker and a whitespace run into the data part, so the raw
// string grows past the string Encode made.
wrapped := encoded[:100] + "+ \n\t" + encoded[100:]
require.Greater(t, len(wrapped), maxBolt12StringLen)
require.Greater(t, len(wrapped), len(encoded))

hrp, data, err := Decode(wrapped)
hrp, data, err := decodeBech32(wrapped)
require.NoError(t, err)
require.Equal(t, HRPOffer, hrp)
require.Equal(t, payload, data)
Expand Down Expand Up @@ -397,7 +399,7 @@ func TestEncodePayloadSize(t *testing.T) {
{
name: "longest payload",
payload: make([]byte, maxBolt12DataLen),
wantLen: maxBolt12StringLen,
wantLen: maxEncodedLen,
},
{
name: "one byte above the longest payload",
Expand All @@ -410,7 +412,7 @@ func TestEncodePayloadSize(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()

encoded, err := Encode(HRPOffer, tc.payload)
encoded, err := encodeBech32(HRPOffer, tc.payload)
if tc.wantErr != nil {
require.ErrorIs(t, err, tc.wantErr)

Expand All @@ -423,7 +425,7 @@ func TestEncodePayloadSize(t *testing.T) {
}

// Decode takes each string that Encode makes.
hrp, data, err := Decode(encoded)
hrp, data, err := decodeBech32(encoded)
require.NoError(t, err)
require.Equal(t, HRPOffer, hrp)
require.Equal(t, tc.payload, data)
Expand All @@ -438,13 +440,13 @@ func TestDecodeUppercase(t *testing.T) {
t.Parallel()

payload := []byte{0x01, 0x23, 0x45, 0x67}
encoded, err := Encode(HRPOffer, payload)
encoded, err := encodeBech32(HRPOffer, payload)
require.NoError(t, err)

uppered := strings.ToUpper(encoded)
require.NotEqual(t, encoded, uppered)

hrp, data, err := Decode(uppered)
hrp, data, err := decodeBech32(uppered)
require.NoError(t, err)
require.Equal(t, HRPOffer, hrp)
require.Equal(t, payload, data)
Expand Down Expand Up @@ -472,10 +474,10 @@ func TestPropertyBech32RoundTrip(t *testing.T) {
rapid.Byte(), size, size,
).Draw(t, "data")

encoded, err := Encode(hrp, data)
encoded, err := encodeBech32(hrp, data)
require.NoError(t, err)

decodedHRP, decodedData, err := Decode(encoded)
decodedHRP, decodedData, err := decodeBech32(encoded)
require.NoError(t, err)
require.Equal(t, hrp, decodedHRP)
require.Equal(t, data, decodedData)
Expand Down
Loading
Loading