diff --git a/app/src/main/java/com/m2049r/xmrwallet/util/BitcoinAddressValidator.java b/app/src/main/java/com/m2049r/xmrwallet/util/BitcoinAddressValidator.java index 5d1dda91..76fa8979 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/util/BitcoinAddressValidator.java +++ b/app/src/main/java/com/m2049r/xmrwallet/util/BitcoinAddressValidator.java @@ -16,12 +16,13 @@ package com.m2049r.xmrwallet.util; -// based on https://rosettacode.org/wiki/Bitcoin/address_validation#Java +// mostly based on https://rosettacode.org/wiki/Bitcoin/address_validation#Java import com.m2049r.xmrwallet.model.NetworkType; import com.m2049r.xmrwallet.model.WalletManager; import java.math.BigInteger; +import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Arrays; @@ -31,8 +32,9 @@ public class BitcoinAddressValidator { private static final String ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; public static boolean validate(String addrress) { - return validate(addrress, - WalletManager.getInstance().getNetworkType() != NetworkType.NetworkType_Mainnet); + boolean testnet = WalletManager.getInstance().getNetworkType() != NetworkType.NetworkType_Mainnet; + if (validate(addrress, testnet)) return true; + return validateBech32Segwit(addrress, testnet); } public static boolean validate(String addrress, boolean testnet) { @@ -85,4 +87,112 @@ public class BitcoinAddressValidator { throw new IllegalStateException(e); } } + + // + // validate Bech32 segwit + // see https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki for spec + // + + private static final String DATA_CHARS = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"; + + public static boolean validateBech32Segwit(String bech32, boolean testnet) { + if (!bech32.equals(bech32.toLowerCase()) && !bech32.equals(bech32.toUpperCase())) { + return false; // mixing upper and lower case not allowed + } + bech32 = bech32.toLowerCase(); + + if (testnet && !bech32.startsWith("tb1")) return false; + if (!testnet && !bech32.startsWith("bc1")) return false; + + if ((bech32.length() < 14) || (bech32.length() > 74)) return false; + int mod = bech32.length() % 8; + if ((mod == 0) || (mod == 3) || (mod == 5)) return false; + + int sep = -1; + final byte[] bytes = bech32.getBytes(StandardCharsets.US_ASCII); + for (int i = 0; i < bytes.length; i++) { + if ((bytes[i] < 33) || (bytes[i] > 126)) { + return false; + } + if (bytes[i] == 49) sep = i; // 49 := '1' in ASCII + } + + if (sep != 2) return false; // bech32 always has len(hrp)==2 + if (sep > bytes.length - 7) { + return false; // min 6 bytes data + } + if (bytes.length < 8) { // hrp{min}=1 + sep=1 + data{min}=6 := 8 + return false; // too short + } + if (bytes.length > 90) { + return false; // too long + } + + final byte[] hrp = Arrays.copyOfRange(bytes, 0, sep); + + final byte[] data = Arrays.copyOfRange(bytes, sep + 1, bytes.length); + for (int i = 0; i < data.length; i++) { + int b = DATA_CHARS.indexOf(data[i]); + if (b < 0) return false; // invalid character + data[i] = (byte) b; + } + + if (!validateBech32Data(data)) return false; + + return verifyChecksum(hrp, data); + } + + private static int polymod(byte[] values) { + final int[] GEN = {0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3}; + int chk = 1; + for (byte v : values) { + byte b = (byte) (chk >> 25); + chk = ((chk & 0x1ffffff) << 5) ^ v; + for (int i = 0; i < 5; i++) { + chk ^= ((b >> i) & 1) == 1 ? GEN[i] : 0; + } + } + return chk; + } + + private static byte[] hrpExpand(byte[] hrp) { + final byte[] expanded = new byte[(2 * hrp.length) + 1]; + int i = 0; + for (int j = 0; j < hrp.length; j++) { + expanded[i++] = (byte) (hrp[j] >> 5); + } + expanded[i++] = 0; + for (int j = 0; j < hrp.length; j++) { + expanded[i++] = (byte) (hrp[j] & 0x1f); + } + return expanded; + } + + private static boolean verifyChecksum(byte[] hrp, byte[] data) { + final byte[] hrpExpanded = hrpExpand(hrp); + final byte[] values = new byte[hrpExpanded.length + data.length]; + System.arraycopy(hrpExpanded, 0, values, 0, hrpExpanded.length); + System.arraycopy(data, 0, values, hrpExpanded.length, data.length); + return (polymod(values) == 1); + } + + private static boolean validateBech32Data(final byte[] data) { + if ((data[0] < 0) || (data[0] > 16)) return false; // witness version + final int programLength = data.length - 1 - 6; // 1-byte version at beginning & 6-byte checksum at end + + // since we are coming from our own decoder, we don't need to verify data is 5-bit bytes + + final int convertedSize = programLength * 5 / 8; + final int remainderSize = programLength * 5 % 8; + + if ((convertedSize < 2) || (convertedSize > 40)) return false; + + if ((data[0] == 0) && (convertedSize != 20) && (convertedSize != 32)) return false; + + if (remainderSize >= 5) return false; + // ignore checksum at end and get last byte of program + if ((data[data.length - 1 - 6] & ((1 << remainderSize) - 1)) != 0) return false; + + return true; + } } \ No newline at end of file diff --git a/app/src/test/java/com/m2049r/xmrwallet/util/BitcoinAddressValidatorTest.java b/app/src/test/java/com/m2049r/xmrwallet/util/BitcoinAddressValidatorTest.java index 5882723f..c2502fb4 100644 --- a/app/src/test/java/com/m2049r/xmrwallet/util/BitcoinAddressValidatorTest.java +++ b/app/src/test/java/com/m2049r/xmrwallet/util/BitcoinAddressValidatorTest.java @@ -18,6 +18,7 @@ package com.m2049r.xmrwallet.util; import org.junit.Test; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -54,4 +55,38 @@ public class BitcoinAddressValidatorTest { assertTrue(!BitcoinAddressValidator.validate("3NagLCvw8fLwtoUrK7s2mJPy9k6hoyWvTU ", false)); assertTrue(!BitcoinAddressValidator.validate(" 3NagLCvw8fLwtoUrK7s2mJPy9k6hoyWvTU ", false)); } + + @Test + public void validSegwit() { + // see https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki + assertTrue(BitcoinAddressValidator.validateBech32Segwit("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4", false)); + assertTrue(BitcoinAddressValidator.validateBech32Segwit("tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx", true)); + assertTrue(BitcoinAddressValidator.validateBech32Segwit("bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3", false)); + assertTrue(BitcoinAddressValidator.validateBech32Segwit("tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7", true)); + + assertTrue(BitcoinAddressValidator.validateBech32Segwit("BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4", false)); + assertTrue(BitcoinAddressValidator.validateBech32Segwit("tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7", true)); + assertTrue(BitcoinAddressValidator.validateBech32Segwit("bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7k7grplx", false)); + assertTrue(BitcoinAddressValidator.validateBech32Segwit("BC1SW50QA3JX3S", false)); + assertTrue(BitcoinAddressValidator.validateBech32Segwit("bc1zw508d6qejxtdg4y5r3zarvaryvg6kdaj", false)); + assertTrue(BitcoinAddressValidator.validateBech32Segwit("tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy", true)); + + assertTrue(BitcoinAddressValidator.validateBech32Segwit("bc1q76awjp3nmklgnf0yyu0qncsekktf4e3qj248t4", false)); // electrum blog + + } + + @Test + public void invalidSegwit() { + // see https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki + assertFalse(BitcoinAddressValidator.validateBech32Segwit("tc1qw508d6qejxtdg4y5r3zarvary0c5xw7kg3g4ty", true)); // Invalid human-readable part + assertFalse(BitcoinAddressValidator.validateBech32Segwit("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t5", true)); // Invalid checksum + assertFalse(BitcoinAddressValidator.validateBech32Segwit("BC13W508D6QEJXTDG4Y5R3ZARVARY0C5XW7KN40WF2", true)); // Invalid witness version + assertFalse(BitcoinAddressValidator.validateBech32Segwit("bc1rw5uspcuh", true)); // Invalid program length + assertFalse(BitcoinAddressValidator.validateBech32Segwit("bc10w508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kw5rljs90", true)); // Invalid program length + assertFalse(BitcoinAddressValidator.validateBech32Segwit("BC1QR508D6QEJXTDG4Y5R3ZARVARYV98GJ9P", true)); // Invalid program length for witness version 0 (per BIP141) + assertFalse(BitcoinAddressValidator.validateBech32Segwit("tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sL5k7", true)); // Mixed case + assertFalse(BitcoinAddressValidator.validateBech32Segwit("bc1zw508d6qejxtdg4y5r3zarvaryvqyzf3du", true)); // zero padding of more than 4 bits + assertFalse(BitcoinAddressValidator.validateBech32Segwit("tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3pjxtptv", true)); // Non-zero padding in 8-to-5 conversion + assertFalse(BitcoinAddressValidator.validateBech32Segwit("bc1gmk9yu", true)); // Empty data section + } }