Skip to content

Commit bfb1666

Browse files
committed
bech32: Add DecodeUnsafe for bypassing checksum validation
1 parent 32fa4f6 commit bfb1666

File tree

2 files changed

+275
-0
lines changed

2 files changed

+275
-0
lines changed

bech32/tweak.go

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
package bech32
2+
3+
import (
4+
"strings"
5+
)
6+
7+
// MaxLengthBIP173 is the maximum length of bech32-encoded address defined by
8+
// BIP-173.
9+
const MaxLengthBIP173 = 90
10+
11+
// VerifyChecksum verifies whether the bech32 string specified by the
12+
// provided hrp and payload data (encoded as 5 bits per element byte slice) has
13+
// the correct checksum suffix. The version of bech32 used (bech32 OG, or
14+
// bech32m) is also returned to allow the caller to perform proper address
15+
// validation (segwitv0 should use bech32, v1+ should use bech32m).
16+
//
17+
// For more details on the checksum verification, please refer to BIP 173.
18+
func VerifyChecksum(hrp string, data []byte, checksum []byte) (Version, bool) {
19+
polymod := bech32Polymod(hrp, data, checksum)
20+
21+
// Before BIP-350, we'd always check this against a static constant of
22+
// 1 to know if the checksum was computed properly. As we want to
23+
// generically support decoding for bech32m as well as bech32, we'll
24+
// look up the returned value and compare it to the set of defined
25+
// constants.
26+
bech32Version, ok := ConstsToVersion[ChecksumConst(polymod)]
27+
if ok {
28+
return bech32Version, true
29+
}
30+
31+
return VersionUnknown, false
32+
}
33+
34+
// Normalize converts the uppercase letters to lowercase in string, because
35+
// Bech32 standard uses only the lowercase for of string for checksum calculation.
36+
// If conversion occurs during function call, `true` will be returned.
37+
//
38+
// Mixed case is NOT allowed.
39+
func Normalize(bech *string) (bool, error) {
40+
// OnlyASCII characters between 33 and 126 are allowed.
41+
var hasLower, hasUpper bool
42+
for i := 0; i < len(*bech); i++ {
43+
if (*bech)[i] < 33 || (*bech)[i] > 126 {
44+
return false, ErrInvalidCharacter((*bech)[i])
45+
}
46+
47+
// The characters must be either all lowercase or all uppercase. Testing
48+
// directly with ascii codes is safe here, given the previous test.
49+
hasLower = hasLower || ((*bech)[i] >= 97 && (*bech)[i] <= 122)
50+
hasUpper = hasUpper || ((*bech)[i] >= 65 && (*bech)[i] <= 90)
51+
if hasLower && hasUpper {
52+
return false, ErrMixedCase{}
53+
}
54+
}
55+
56+
// Bech32 standard uses only the lowercase for of strings for checksum
57+
// calculation.
58+
if hasUpper {
59+
*bech = strings.ToLower(*bech)
60+
return true, nil
61+
}
62+
63+
return false, nil
64+
}
65+
66+
// DecodeUnsafe decodes a bech32 encoded string, returning the human-readable
67+
// part, the data part (excluding the checksum) and the checksum. This function
68+
// does NOT validate against the BIP-173 maximum length allowed for bech32 strings
69+
// and is meant for use in custom applications (such as lightning network payment
70+
// requests), NOT on-chain addresses. This function assumes the given string
71+
// includes lowercase letters only, so if not, you should call Normalize first.
72+
//
73+
// Note that the returned data is 5-bit (base32) encoded and the human-readable
74+
// part will be lowercase.
75+
func DecodeUnsafe(bech string) (string, []byte, []byte, error) {
76+
// The string is invalid if the last '1' is non-existent, it is the
77+
// first character of the string (no human-readable part) or one of the
78+
// last 6 characters of the string (since checksum cannot contain '1').
79+
one := strings.LastIndexByte(bech, '1')
80+
if one < 1 || one+7 > len(bech) {
81+
return "", nil, nil, ErrInvalidSeparatorIndex(one)
82+
}
83+
84+
// The human-readable part is everything before the last '1'.
85+
hrp := bech[:one]
86+
data := bech[one+1:]
87+
88+
// Each character corresponds to the byte with value of the index in
89+
// 'charset'.
90+
decoded, err := toBytes(data)
91+
if err != nil {
92+
return "", nil, nil, err
93+
}
94+
95+
return hrp, decoded[:len(decoded)-6], decoded[len(decoded)-6:], nil
96+
}
97+
98+
// decodeWithLimit is a bech32 checksum version aware bounded string length
99+
// decoder. This function will return the version of the decoded checksum
100+
// constant so higher level validation can be performed to ensure the correct
101+
// version of bech32 was used when encoding.
102+
func decodeWithLimit(bech string, limit int) (string, []byte, Version, error) {
103+
// The length of the string should not exceed the given limit.
104+
if len(bech) < 8 || len(bech) > limit {
105+
return "", nil, VersionUnknown, ErrInvalidLength(len(bech))
106+
}
107+
108+
_, err := Normalize(&bech)
109+
if err != nil {
110+
return "", nil, VersionUnknown, err
111+
}
112+
113+
hrp, data, checksum, err := DecodeUnsafe(bech)
114+
if err != nil {
115+
return "", nil, VersionUnknown, err
116+
}
117+
118+
// Verify if the checksum (stored inside decoded[:]) is valid, given the
119+
// previously decoded hrp.
120+
bech32Version, ok := VerifyChecksum(hrp, data, checksum)
121+
if !ok {
122+
// Invalid checksum. Calculate what it should have been, so that the
123+
// error contains this information.
124+
125+
// Extract the payload bytes and actual checksum in the string.
126+
actual := bech[len(bech)-6:]
127+
128+
// Calculate the expected checksum, given the hrp and payload
129+
// data. We'll actually compute _both_ possibly valid checksum
130+
// to further aide in debugging.
131+
var expectedBldr strings.Builder
132+
expectedBldr.Grow(6)
133+
writeBech32Checksum(hrp, data, &expectedBldr, Version0)
134+
expectedVersion0 := expectedBldr.String()
135+
136+
var b strings.Builder
137+
b.Grow(6)
138+
writeBech32Checksum(hrp, data, &expectedBldr, VersionM)
139+
expectedVersionM := expectedBldr.String()
140+
141+
err = ErrInvalidChecksum{
142+
Expected: expectedVersion0,
143+
ExpectedM: expectedVersionM,
144+
Actual: actual,
145+
}
146+
return "", nil, VersionUnknown, err
147+
}
148+
149+
// We exclude the last 6 bytes, which is the checksum.
150+
return hrp, data, bech32Version, nil
151+
152+
}
153+
154+
// DecodeWithLimit decodes a bech32 encoded string, returning the human-readable part and
155+
// the data part excluding the checksum.
156+
//
157+
// Note that the returned data is 5-bit (base32) encoded and the human-readable
158+
// part will be lowercase.
159+
func DecodeWithLimit(bech string, limit int) (string, []byte, error) {
160+
hrp, data, _, err := decodeWithLimit(bech, limit)
161+
return hrp, data, err
162+
}

bech32/tweak_test.go

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
// Copyright (c) 2017-2020 The btcsuite developers
2+
// Copyright (c) 2019 The Decred developers
3+
// Use of this source code is governed by an ISC
4+
// license that can be found in the LICENSE file.
5+
6+
package bech32
7+
8+
import (
9+
"strings"
10+
"testing"
11+
)
12+
13+
// TestBech32Tweak tests whether decoding and re-encoding the valid BIP-173 test
14+
// vectors works and if decoding invalid test vectors fails for the correct
15+
// reason.
16+
func TestBech32Tweak(t *testing.T) {
17+
tests := []struct {
18+
str string
19+
expectedError error
20+
}{
21+
{"A12UEL5L", nil},
22+
{"a12uel5l", nil},
23+
{"an83characterlonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1tt5tgs", nil},
24+
{"abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw", nil},
25+
{"11qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqc8247j", nil},
26+
{"split1checkupstagehandshakeupstreamerranterredcaperred2y9e3w", nil},
27+
{"split1checkupstagehandshakeupstreamerranterredcaperred2y9e2w", ErrInvalidChecksum{"2y9e3w", "2y9e3wlc445v", "2y9e2w"}}, // invalid checksum
28+
{"s lit1checkupstagehandshakeupstreamerranterredcaperredp8hs2p", ErrInvalidCharacter(' ')}, // invalid character (space) in hrp
29+
{"spl\x7Ft1checkupstagehandshakeupstreamerranterredcaperred2y9e3w", ErrInvalidCharacter(127)}, // invalid character (DEL) in hrp
30+
{"split1cheo2y9e2w", ErrNonCharsetChar('o')}, // invalid character (o) in data part
31+
{"split1a2y9w", ErrInvalidSeparatorIndex(5)}, // too short data part
32+
{"1checkupstagehandshakeupstreamerranterredcaperred2y9e3w", ErrInvalidSeparatorIndex(0)}, // empty hrp
33+
{"11qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqsqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqc8247j", ErrInvalidLength(91)}, // too long
34+
35+
// Additional test vectors used in bitcoin core
36+
{" 1nwldj5", ErrInvalidCharacter(' ')},
37+
{"\x7f" + "1axkwrx", ErrInvalidCharacter(0x7f)},
38+
{"\x801eym55h", ErrInvalidCharacter(0x80)},
39+
{"an84characterslonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1569pvx", ErrInvalidLength(91)},
40+
{"pzry9x0s0muk", ErrInvalidSeparatorIndex(-1)},
41+
{"1pzry9x0s0muk", ErrInvalidSeparatorIndex(0)},
42+
{"x1b4n0q5v", ErrNonCharsetChar(98)},
43+
{"li1dgmt3", ErrInvalidSeparatorIndex(2)},
44+
{"de1lg7wt\xff", ErrInvalidCharacter(0xff)},
45+
{"A1G7SGD8", ErrInvalidChecksum{"2uel5l", "2uel5llqfn3a", "g7sgd8"}},
46+
{"10a06t8", ErrInvalidLength(7)},
47+
{"1qzzfhee", ErrInvalidSeparatorIndex(0)},
48+
{"a12UEL5L", ErrMixedCase{}},
49+
{"A12uEL5L", ErrMixedCase{}},
50+
}
51+
52+
for i, test := range tests {
53+
str := test.str
54+
hrp, decoded, err := DecodeWithLimit(str, MaxLengthBIP173)
55+
if test.expectedError != err {
56+
t.Errorf("%d: expected decoding error %v "+
57+
"instead got %v", i, test.expectedError, err)
58+
continue
59+
}
60+
61+
if err != nil {
62+
// End test case here if a decoding error was expected.
63+
continue
64+
}
65+
66+
// Check that it encodes to the same string
67+
encoded, err := Encode(hrp, decoded)
68+
if err != nil {
69+
t.Errorf("encoding failed: %v", err)
70+
}
71+
72+
if encoded != strings.ToLower(str) {
73+
t.Errorf("expected data to encode to %v, but got %v",
74+
str, encoded)
75+
}
76+
77+
// Flip a bit in the string an make sure it is caught.
78+
pos := strings.LastIndexAny(str, "1")
79+
flipped := str[:pos+1] + string((str[pos+1] ^ 1)) + str[pos+2:]
80+
_, _, err = DecodeWithLimit(flipped, MaxLengthBIP173)
81+
if err == nil {
82+
t.Error("expected decoding to fail")
83+
}
84+
}
85+
}
86+
87+
// BenchmarkDecodeUnsafe performs a benchmark for a decode cycle of a bech32
88+
// string without normalization and checksum validation.
89+
func BenchmarkDecodeUnsafe(b *testing.B) {
90+
encoded := "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7"
91+
92+
b.ResetTimer()
93+
for i := 0; i < b.N; i++ {
94+
_, _, _, err := DecodeUnsafe(encoded)
95+
if err != nil {
96+
b.Fatalf("error converting bits: %v", err)
97+
}
98+
}
99+
}
100+
101+
// BenchmarkDecode performs a benchmark for a decode cycle of a bech32 string
102+
// with normalization and checksum validation.
103+
func BenchmarkDecode(b *testing.B) {
104+
encoded := "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7"
105+
106+
b.ResetTimer()
107+
for i := 0; i < b.N; i++ {
108+
_, _, err := DecodeWithLimit(encoded, MaxLengthBIP173)
109+
if err != nil {
110+
b.Fatalf("error converting bits: %v", err)
111+
}
112+
}
113+
}

0 commit comments

Comments
 (0)