forked from btcsuite/btcutil
-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
bech32: Add DecodeUnsafe for bypassing checksum validation
- Loading branch information
Showing
2 changed files
with
275 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} | ||
} | ||
} |