diff --git a/bech32/tweak.go b/bech32/tweak.go new file mode 100644 index 00000000..5b48d66d --- /dev/null +++ b/bech32/tweak.go @@ -0,0 +1,162 @@ +package bech32 + +import ( + "strings" +) + +// MaxLengthBIP173 is the maximum length of bech32-encoded address defined by +// BIP-173. +const MaxLengthBIP173 = 90 + +// VerifyChecksum verifies whether the bech32 string specified by the +// provided hrp and payload data (encoded as 5 bits per element byte slice) has +// the correct checksum suffix. The version of bech32 used (bech32 OG, or +// bech32m) is also returned to allow the caller to perform proper address +// validation (segwitv0 should use bech32, v1+ should use bech32m). +// +// For more details on the checksum verification, please refer to BIP 173. +func VerifyChecksum(hrp string, data []byte, checksum []byte) (Version, bool) { + polymod := bech32Polymod(hrp, data, checksum) + + // Before BIP-350, we'd always check this against a static constant of + // 1 to know if the checksum was computed properly. As we want to + // generically support decoding for bech32m as well as bech32, we'll + // look up the returned value and compare it to the set of defined + // constants. + bech32Version, ok := ConstsToVersion[ChecksumConst(polymod)] + if ok { + return bech32Version, true + } + + return VersionUnknown, false +} + +// Normalize converts the uppercase letters to lowercase in string, because +// Bech32 standard uses only the lowercase for of string for checksum calculation. +// If conversion occurs during function call, `true` will be returned. +// +// Mixed case is NOT allowed. +func Normalize(bech *string) (bool, error) { + // OnlyASCII characters between 33 and 126 are allowed. + var hasLower, hasUpper bool + for i := 0; i < len(*bech); i++ { + if (*bech)[i] < 33 || (*bech)[i] > 126 { + return false, ErrInvalidCharacter((*bech)[i]) + } + + // The characters must be either all lowercase or all uppercase. Testing + // directly with ascii codes is safe here, given the previous test. + hasLower = hasLower || ((*bech)[i] >= 97 && (*bech)[i] <= 122) + hasUpper = hasUpper || ((*bech)[i] >= 65 && (*bech)[i] <= 90) + if hasLower && hasUpper { + return false, ErrMixedCase{} + } + } + + // Bech32 standard uses only the lowercase for of strings for checksum + // calculation. + if hasUpper { + *bech = strings.ToLower(*bech) + return true, nil + } + + return false, nil +} + +// DecodeUnsafe decodes a bech32 encoded string, returning the human-readable +// part, the data part (excluding the checksum) and the checksum. This function +// does NOT validate against the BIP-173 maximum length allowed for bech32 strings +// and is meant for use in custom applications (such as lightning network payment +// requests), NOT on-chain addresses. This function assumes the given string +// includes lowercase letters only, so if not, you should call Normalize first. +// +// Note that the returned data is 5-bit (base32) encoded and the human-readable +// part will be lowercase. +func DecodeUnsafe(bech string) (string, []byte, []byte, error) { + // 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'). + one := strings.LastIndexByte(bech, '1') + if one < 1 || one+7 > len(bech) { + return "", nil, nil, ErrInvalidSeparatorIndex(one) + } + + // 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, nil, err + } + + return hrp, decoded[:len(decoded)-6], decoded[len(decoded)-6:], nil +} + +// decodeWithLimit is a bech32 checksum version aware bounded string length +// decoder. This function will return the version of the decoded checksum +// constant so higher level validation can be performed to ensure the correct +// version of bech32 was used when encoding. +func decodeWithLimit(bech string, limit int) (string, []byte, Version, error) { + // The length of the string should not exceed the given limit. + if len(bech) < 8 || len(bech) > limit { + return "", nil, VersionUnknown, ErrInvalidLength(len(bech)) + } + + _, err := Normalize(&bech) + if err != nil { + return "", nil, VersionUnknown, err + } + + hrp, data, checksum, err := DecodeUnsafe(bech) + if err != nil { + return "", nil, VersionUnknown, err + } + + // Verify if the checksum (stored inside decoded[:]) is valid, given the + // previously decoded hrp. + bech32Version, ok := VerifyChecksum(hrp, data, checksum) + if !ok { + // Invalid checksum. Calculate what it should have been, so that the + // error contains this information. + + // Extract the payload bytes and actual checksum in the string. + actual := bech[len(bech)-6:] + + // Calculate the expected checksum, given the hrp and payload + // data. We'll actually compute _both_ possibly valid checksum + // to further aide in debugging. + var expectedBldr strings.Builder + expectedBldr.Grow(6) + writeBech32Checksum(hrp, data, &expectedBldr, Version0) + expectedVersion0 := expectedBldr.String() + + var b strings.Builder + b.Grow(6) + writeBech32Checksum(hrp, data, &expectedBldr, VersionM) + expectedVersionM := expectedBldr.String() + + err = ErrInvalidChecksum{ + Expected: expectedVersion0, + ExpectedM: expectedVersionM, + Actual: actual, + } + return "", nil, VersionUnknown, err + } + + // We exclude the last 6 bytes, which is the checksum. + return hrp, data, bech32Version, nil + +} + +// DecodeWithLimit decodes a bech32 encoded string, returning the human-readable part and +// the data part excluding the checksum. +// +// Note that the returned data is 5-bit (base32) encoded and the human-readable +// part will be lowercase. +func DecodeWithLimit(bech string, limit int) (string, []byte, error) { + hrp, data, _, err := decodeWithLimit(bech, limit) + return hrp, data, err +} diff --git a/bech32/tweak_test.go b/bech32/tweak_test.go new file mode 100644 index 00000000..c0e27dd7 --- /dev/null +++ b/bech32/tweak_test.go @@ -0,0 +1,113 @@ +// Copyright (c) 2017-2020 The btcsuite developers +// Copyright (c) 2019 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package bech32 + +import ( + "strings" + "testing" +) + +// TestBech32Tweak tests whether decoding and re-encoding the valid BIP-173 test +// vectors works and if decoding invalid test vectors fails for the correct +// reason. +func TestBech32Tweak(t *testing.T) { + tests := []struct { + str string + expectedError error + }{ + {"A12UEL5L", nil}, + {"a12uel5l", nil}, + {"an83characterlonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1tt5tgs", nil}, + {"abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw", nil}, + {"11qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqc8247j", nil}, + {"split1checkupstagehandshakeupstreamerranterredcaperred2y9e3w", nil}, + {"split1checkupstagehandshakeupstreamerranterredcaperred2y9e2w", ErrInvalidChecksum{"2y9e3w", "2y9e3wlc445v", "2y9e2w"}}, // invalid checksum + {"s lit1checkupstagehandshakeupstreamerranterredcaperredp8hs2p", ErrInvalidCharacter(' ')}, // invalid character (space) in hrp + {"spl\x7Ft1checkupstagehandshakeupstreamerranterredcaperred2y9e3w", ErrInvalidCharacter(127)}, // invalid character (DEL) in hrp + {"split1cheo2y9e2w", ErrNonCharsetChar('o')}, // invalid character (o) in data part + {"split1a2y9w", ErrInvalidSeparatorIndex(5)}, // too short data part + {"1checkupstagehandshakeupstreamerranterredcaperred2y9e3w", ErrInvalidSeparatorIndex(0)}, // empty hrp + {"11qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqsqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqc8247j", ErrInvalidLength(91)}, // too long + + // Additional test vectors used in bitcoin core + {" 1nwldj5", ErrInvalidCharacter(' ')}, + {"\x7f" + "1axkwrx", ErrInvalidCharacter(0x7f)}, + {"\x801eym55h", ErrInvalidCharacter(0x80)}, + {"an84characterslonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1569pvx", ErrInvalidLength(91)}, + {"pzry9x0s0muk", ErrInvalidSeparatorIndex(-1)}, + {"1pzry9x0s0muk", ErrInvalidSeparatorIndex(0)}, + {"x1b4n0q5v", ErrNonCharsetChar(98)}, + {"li1dgmt3", ErrInvalidSeparatorIndex(2)}, + {"de1lg7wt\xff", ErrInvalidCharacter(0xff)}, + {"A1G7SGD8", ErrInvalidChecksum{"2uel5l", "2uel5llqfn3a", "g7sgd8"}}, + {"10a06t8", ErrInvalidLength(7)}, + {"1qzzfhee", ErrInvalidSeparatorIndex(0)}, + {"a12UEL5L", ErrMixedCase{}}, + {"A12uEL5L", ErrMixedCase{}}, + } + + for i, test := range tests { + str := test.str + hrp, decoded, err := DecodeWithLimit(str, MaxLengthBIP173) + if test.expectedError != err { + t.Errorf("%d: expected decoding error %v "+ + "instead got %v", i, test.expectedError, err) + continue + } + + if err != nil { + // End test case here if a decoding error was expected. + continue + } + + // Check that it encodes to the same string + encoded, err := Encode(hrp, decoded) + if err != nil { + t.Errorf("encoding failed: %v", err) + } + + if encoded != strings.ToLower(str) { + t.Errorf("expected data to encode to %v, but got %v", + str, encoded) + } + + // Flip a bit in the string an make sure it is caught. + pos := strings.LastIndexAny(str, "1") + flipped := str[:pos+1] + string((str[pos+1] ^ 1)) + str[pos+2:] + _, _, err = DecodeWithLimit(flipped, MaxLengthBIP173) + if err == nil { + t.Error("expected decoding to fail") + } + } +} + +// BenchmarkDecodeUnsafe performs a benchmark for a decode cycle of a bech32 +// string without normalization and checksum validation. +func BenchmarkDecodeUnsafe(b *testing.B) { + encoded := "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7" + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _, _, err := DecodeUnsafe(encoded) + if err != nil { + b.Fatalf("error converting bits: %v", err) + } + } +} + +// BenchmarkDecode performs a benchmark for a decode cycle of a bech32 string +// with normalization and checksum validation. +func BenchmarkDecode(b *testing.B) { + encoded := "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7" + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _, err := DecodeWithLimit(encoded, MaxLengthBIP173) + if err != nil { + b.Fatalf("error converting bits: %v", err) + } + } +}