diff --git a/baked_in.go b/baked_in.go index 41f68a2..1dae5a9 100644 --- a/baked_in.go +++ b/baked_in.go @@ -109,6 +109,7 @@ var ( "isbn13": isISBN13, "eth_addr": isEthereumAddress, "btc_addr": isBitcoinAddress, + "btc_addr_bech32": isBitcoinBech32Address, "uuid": isUUID, "uuid3": isUUID3, "uuid4": isUUID4, @@ -393,58 +394,150 @@ func isISBN10(fl FieldLevel) bool { // IsEthereumAddress is the validation function for validating if the field's value is a valid ethereum address based currently only on the format func isEthereumAddress(fl FieldLevel) bool { - field := fl.Field() + address := fl.Field().String() + + if !ethAddressRegex.MatchString(address) { + return false + } + + if ethaddressRegexUpper.MatchString(address) || ethAddressRegexLower.MatchString(address) { + return true + } - return ethAddressRegex.MatchString(field.String()) + // checksum validation is blocked by https://github.com/golang/crypto/pull/28 + + return true } -// IsBitcoinAddress is the validation function for validating if the field's value is a valid btc address, currently only based on the format +// IsBitcoinAddress is the validation function for validating if the field's value is a valid btc address func isBitcoinAddress(fl FieldLevel) bool { address := fl.Field().String() - if btcAddressRegex.MatchString(address) { - - alphabet := []byte("123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz") + if !btcAddressRegex.MatchString(address) { + return false + } - decode := [25]byte{} + alphabet := []byte("123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz") - for _, n := range []byte(address) { - d := bytes.IndexByte(alphabet, n) + decode := [25]byte{} - if(d == -1){ - return false - } + for _, n := range []byte(address) { + d := bytes.IndexByte(alphabet, n) - for i := 24; i >= 0; i-- { - d += 58 * int(decode[i]) - decode[i] = byte(d % 256) - d /= 256 - } + for i := 24; i >= 0; i-- { + d += 58 * int(decode[i]) + decode[i] = byte(d % 256) + d /= 256 } + } - if !(decode[0] == 0 || decode[0] == 5) { + if !(decode[0] == 0 || decode[0] == 5) { + return false + } + + h := sha256.New() + h.Write(decode[:21]) + d := h.Sum([]byte{}) + h = sha256.New() + h.Write(d) + + validchecksum := [4]byte{} + computedchecksum := [4]byte{} + + copy(computedchecksum[:], h.Sum(d[:0])) + copy(validchecksum[:], decode[21:]) + + return validchecksum == computedchecksum +} + +// IsBitcoinAddress is the validation function for validating if the field's value is a valid bech32 btc address +func isBitcoinBech32Address(fl FieldLevel) bool { + address := fl.Field().String() + + if !btcAddressRegexBech32.MatchString(address){ + return false + } + + am := len(address) % 8 + + if am == 0 || am == 3 || am == 5{ + return false + } + + address = strings.ToLower(address) + + alphabet := "qpzry9x8gf2tvdw0s3jn54khce6mua7l" + + hr := []int{3, 3, 0, 2, 3} // the human readable part will always be bc + dp := []int{} + + for _, c := range []rune(address[3:]) { + dp = append(dp, strings.IndexRune(alphabet, c)) + } + + ver := dp[0] + + if ver < 0 || ver > 16{ + return false + } + + if ver == 0 { + if len(address) != 42 && len(address) != 62 { return false } + } + + values := append(hr, dp...) + + GEN := []int{ 0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3 } + + p := 1 + + for _, v := range values { + b := p >> 25 + p = (p & 0x1ffffff) << 5 ^ v + + for i := 0; i < 5; i++ { + if (b >> uint(i)) & 1 == 1 { + p ^= GEN[i] + } + } + } - h := sha256.New() - h.Write(decode[:21]) - d := h.Sum([]byte{}) - h = sha256.New() - h.Write(d) + if p != 1 { + return false + } - validchecksum := [4]byte{} - computedchecksum := [4]byte{} + b := uint(0) + acc := 0 + mv := (1 << 5) - 1 - copy(computedchecksum[:], h.Sum(d[:0])) - copy(validchecksum[:], decode[21:]) + sw := []int{} - println(address, "::", validchecksum[:], computedchecksum[:]) + dp = dp[:len(dp) - 6] - return validchecksum == computedchecksum + for _, v := range dp[1:]{ + if v < 0 || (v >> 5) != 0{ + return false + } + acc = (acc << 5) | v + b += 5 + for b >= 8{ + b -= 8 + sw = append(sw, (acc>>b)&mv) + } + } + + if len(sw) < 2 || len(sw) > 40{ + return false + } + + if ver == 0 && len(sw) != 20 && len(sw) != 32 { + return false } - return btcAddressRegexBech32.MatchString(address) + return true } // ExcludesRune is the validation function for validating that the field's value does not contain the rune specified within the param. diff --git a/doc.go b/doc.go index 5522ebb..fdb689a 100644 --- a/doc.go +++ b/doc.go @@ -613,14 +613,23 @@ Bitcoin Address This validates that a string value contains a valid bitcoin address. The format of the string is checked to ensure it matches one of the three formats -P2PKH, P2SH, or Bech32. Currently no further validation is performed. +P2PKH, P2SH and performs checksum validation. Usage: btc_addr +Bitcoin Bech32 Address (segwit) + +This validates that a string value contains a valid bitcoin Bech32 address as defined +by bip-0173 (https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki) +Special thanks to Pieter Wuille for providng reference implementations. + + Usage: btc_addr_bech32 + Ethereum Address This validates that a string value contains a valid ethereum address. The format of the string is checked to ensure it matches the standard Ethereum address format +Full validation is blocked by https://github.com/golang/crypto/pull/28 Usage: eth_addr diff --git a/regexes.go b/regexes.go index 82f7ec2..3492c6a 100644 --- a/regexes.go +++ b/regexes.go @@ -33,8 +33,10 @@ const ( hostnameRegexStringRFC952 = `^[a-zA-Z][a-zA-Z0-9\-\.]+[a-z-Az0-9]$` // https://tools.ietf.org/html/rfc952 hostnameRegexStringRFC1123 = `^[a-zA-Z0-9][a-zA-Z0-9\-\.]+[a-z-Az0-9]$` // accepts hostname starting with a digit https://tools.ietf.org/html/rfc1123 btcAddressRegexString = `^[13][a-km-zA-HJ-NP-Z1-9]{25,34}$` // bitcoin address - btcAddressRegexStringBech32 = `^(bc1|[13])[a-zA-HJ-NP-Z0-9]{25,39}$` // bitcoin bech32 address https://en.bitcoin.it/wiki/Bech32 + btcAddressRegexStringBech32 = `^([bB][cC]1)[02-9ac-hj-np-zAC-HJ-NP-Z]{7,76}$` // bitcoin bech32 address https://en.bitcoin.it/wiki/Bech32 ethAddressRegexString = `^0x[0-9a-fA-F]{40}$` + ethAddressUpperRegexString = `^0x[0-9A-F]{40}$` + ethAddressLowerRegexString = `^0x[0-9a-f]{40}$` ) var ( @@ -70,4 +72,6 @@ var ( btcAddressRegex = regexp.MustCompile(btcAddressRegexString) btcAddressRegexBech32 = regexp.MustCompile(btcAddressRegexStringBech32) ethAddressRegex = regexp.MustCompile(ethAddressRegexString) + ethaddressRegexUpper = regexp.MustCompile(ethAddressUpperRegexString) + ethAddressRegexLower = regexp.MustCompile(ethAddressLowerRegexString) ) diff --git a/validator_test.go b/validator_test.go index aaab196..a9ef1d4 100644 --- a/validator_test.go +++ b/validator_test.go @@ -4452,12 +4452,8 @@ func TestBitcoinAddressValidation(t *testing.T){ {"3P141597f3E4gFr7JterCCQh9QjiTjiZrG", false}, // invalid p2sh address with valid characters {"1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2", true}, // valid p2pkh address {"3P14159f73E4gFr7JterCCQh9QjiTjiZrG", true}, // valid p2sh address - {"bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq", true}, // valid bech32 address - } - - for i, test := range tests { errs := validate.Var(test.param, "btc_addr") @@ -4479,6 +4475,49 @@ func TestBitcoinAddressValidation(t *testing.T){ } } +func TestBitcoinBech32AddressValidation(t *testing.T){ + + validate := New() + + tests := []struct { + param string + expected bool + }{ + {"", false}, + {"bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t5", false}, // invalid bech32 address with invalid startingcharacters + {"BC13W508D6QEJXTDG4Y5R3ZARVARY0C5XW7KN40WF2", false}, + {"bc1rw5uspcuh", false}, + {"bc10w508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kw5rljs90", false}, + {"BC1QR508D6QEJXTDG4Y5R3ZARVARYV98GJ9P", false}, + {"bc10w508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kw5rljs90", false}, + {"bc1zw508d6qejxtdg4y5r3zarvaryvqyzf3du", false}, + {"bc1gmk9yu", false}, + {"BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4", true}, + {"bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7k7grplx", true}, + {"BC1SW50QA3JX3S", true}, + {"bc1zw508d6qejxtdg4y5r3zarvaryvg6kdaj", true}, + } + + for i, test := range tests { + + errs := validate.Var(test.param, "btc_addr_bech32") + + if test.expected { + if !IsEqual(errs, nil) { + t.Fatalf("Index: %d btc_addr_bech32 failed with Error: %s", i, errs) + } + } else { + if IsEqual(errs, nil) { + t.Fatalf("Index: %d btc_addr_bech32 failed with Error: %s", i, errs) + } else { + val := getError(errs, "", "") + if val.Tag() != "btc_addr_bech32" { + t.Fatalf("Index: %d Latitude failed with Error: %s", i, errs) + } + } + } + } +} func TestNoStructLevelValidation(t *testing.T) {