diff --git a/ref/java/.gitignore b/ref/java/.gitignore new file mode 100644 index 0000000..c41cc9e --- /dev/null +++ b/ref/java/.gitignore @@ -0,0 +1 @@ +/target \ No newline at end of file diff --git a/ref/java/README.md b/ref/java/README.md new file mode 100644 index 0000000..b8c2fca --- /dev/null +++ b/ref/java/README.md @@ -0,0 +1,11 @@ +# Bech32 Java + +Build and test the library with +``` +$ mvn test +``` + +Build the jar +``` +$ mvn package +``` diff --git a/ref/java/pom.xml b/ref/java/pom.xml new file mode 100644 index 0000000..12ac092 --- /dev/null +++ b/ref/java/pom.xml @@ -0,0 +1,22 @@ + + 4.0.0 + org.bech32 + bech32 + jar + 1.0-SNAPSHOT + bech32 + http://maven.apache.org + + 1.7 + 1.7 + + + + junit + junit + 4.11 + test + + + diff --git a/ref/java/src/main/java/org/bech32/Bech32.java b/ref/java/src/main/java/org/bech32/Bech32.java new file mode 100644 index 0000000..6dbfcbc --- /dev/null +++ b/ref/java/src/main/java/org/bech32/Bech32.java @@ -0,0 +1,158 @@ +/* Copyright (c) 2018 Coinomi Ltd + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.bech32; + +import java.util.Arrays; +import java.util.Locale; + +public class Bech32 { + /** The Bech32 character set for encoding. */ + private static final String CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"; + + /** The Bech32 character set for decoding. */ + private static final byte[] CHARSET_REV = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + 15, -1, 10, 17, 21, 20, 26, 30, 7, 5, -1, -1, -1, -1, -1, -1, + -1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1, + 1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1, + -1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1, + 1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1 + }; + + public static class Bech32Exception extends Exception { + Bech32Exception(final String s) { + super(s); + } + } + + public static class Bech32Data { + final String hrp; + final byte[] values; + + private Bech32Data(final String hrp, final byte[] values) { + this.hrp = hrp; + this.values = values; + } + } + + /** Find the polynomial with value coefficients mod the generator as 30-bit. */ + private static int polymod(final byte[] values) { + int c = 1; + for (byte v_i: values) { + int c0 = (c >>> 25) & 0xff; + c = ((c & 0x1ffffff) << 5) ^ (v_i & 0xff); + if ((c0 & 1) != 0) c ^= 0x3b6a57b2; + if ((c0 & 2) != 0) c ^= 0x26508e6d; + if ((c0 & 4) != 0) c ^= 0x1ea119fa; + if ((c0 & 8) != 0) c ^= 0x3d4233dd; + if ((c0 & 16) != 0) c ^= 0x2a1462b3; + } + return c; + } + + /** Expand a HRP for use in checksum computation. */ + private static byte[] expandHrp(final String hrp) { + int hrpLength = hrp.length(); + byte ret[] = new byte[hrpLength * 2 + 1]; + for (int i = 0; i < hrpLength; ++i) { + int c = hrp.charAt(i) & 0x7f; // Limit to standard 7-bit ASCII + ret[i] = (byte) ((c >>> 5) & 0x07); + ret[i + hrpLength + 1] = (byte) (c & 0x1f); + } + ret[hrpLength] = 0; + return ret; + } + + /** Verify a checksum. */ + private static boolean verifyChecksum(final String hrp, final byte[] values) { + byte[] hrpExpanded = expandHrp(hrp); + byte[] combined = new byte[hrpExpanded.length + values.length]; + System.arraycopy(hrpExpanded, 0, combined, 0, hrpExpanded.length); + System.arraycopy(values, 0, combined, hrpExpanded.length, values.length); + return polymod(combined) == 1; + } + + /** Create a checksum. */ + private static byte[] createChecksum(final String hrp, final byte[] values) { + byte[] hrpExpanded = expandHrp(hrp); + byte[] enc = new byte[hrpExpanded.length + values.length + 6]; + System.arraycopy(hrpExpanded, 0, enc, 0, hrpExpanded.length); + System.arraycopy(values, 0, enc, hrpExpanded.length, values.length); + int mod = polymod(enc) ^ 1; + byte[] ret = new byte[6]; + for (int i = 0; i < 6; ++i) { + ret[i] = (byte) ((mod >>> (5 * (5 - i))) & 31); + } + return ret; + } + + /** Encode a Bech32 string. */ + public static String encode(final Bech32Data bech32) throws Bech32Exception { + return encode(bech32.hrp, bech32.values); + } + + /** Encode a Bech32 string. */ + public static String encode(String hrp, final byte[] values) throws Bech32Exception { + if (hrp.length() < 1) throw new Bech32Exception("Human-readable part is too short"); + if (hrp.length() > 83) throw new Bech32Exception("Human-readable part is too long"); + hrp = hrp.toLowerCase(Locale.ROOT); + byte[] checksum = createChecksum(hrp, values); + byte[] combined = new byte[values.length + checksum.length]; + System.arraycopy(values, 0, combined, 0, values.length); + System.arraycopy(checksum, 0, combined, values.length, checksum.length); + StringBuilder sb = new StringBuilder(hrp.length() + 1 + combined.length); + sb.append(hrp); + sb.append('1'); + for (byte b : combined) { + sb.append(CHARSET.charAt(b)); + } + return sb.toString(); + } + + /** Decode a Bech32 string. */ + public static Bech32Data decode(final String str) throws Bech32Exception { + boolean lower = false, upper = false; + if (str.length() < 8) throw new Bech32Exception("Input too short"); + if (str.length() > 90) throw new Bech32Exception("Input too long"); + for (int i = 0; i < str.length(); ++i) { + char c = str.charAt(i); + if (c < 33 || c > 126) throw new Bech32Exception("Characters out of range"); + if (c >= 'a' && c <= 'z') lower = true; + if (c >= 'A' && c <= 'Z') upper = true; + } + if (lower && upper) throw new Bech32Exception("Cannot mix upper and lower cases"); + int pos = str.lastIndexOf('1'); + if (pos < 1) throw new Bech32Exception("Missing human-readable part"); + if (pos + 7 > str.length()) throw new Bech32Exception("Data part too short"); + byte[] values = new byte[str.length() - 1 - pos]; + for (int i = 0; i < str.length() - 1 - pos; ++i) { + char c = str.charAt(i + pos + 1); + if (CHARSET_REV[c] == -1) throw new Bech32Exception("Characters out of range"); + values[i] = CHARSET_REV[c]; + } + String hrp = str.substring(0, pos).toLowerCase(Locale.ROOT); + if (!verifyChecksum(hrp, values)) throw new Bech32Exception("Invalid checksum"); + return new Bech32Data(hrp, Arrays.copyOfRange(values, 0, values.length - 6)); + } +} diff --git a/ref/java/src/main/java/org/bech32/SegwitAddress.java b/ref/java/src/main/java/org/bech32/SegwitAddress.java new file mode 100644 index 0000000..91dd018 --- /dev/null +++ b/ref/java/src/main/java/org/bech32/SegwitAddress.java @@ -0,0 +1,143 @@ +/* Copyright (c) 2018 Coinomi Ltd + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.bech32; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +public class SegwitAddress { + public static class SegwitAddressException extends Exception { + SegwitAddressException(Exception e) { + super(e); + } + SegwitAddressException(String s) { + super(s); + } + } + + public static class SegwitAddressData { + public final byte version; + public final byte[] program; + + public SegwitAddressData(final int version, final byte[] program) + throws SegwitAddressException { + this.version = (byte) (version & 0xff); + this.program = program; + verify(this); + } + } + + private static byte[] convertBits(final byte[] in, final int inStart, final int inLen, + final int fromBits, final int toBits, final boolean pad) + throws SegwitAddressException { + int acc = 0; + int bits = 0; + ByteArrayOutputStream out = new ByteArrayOutputStream(64); + final int maxv = (1 << toBits) - 1; + final int max_acc = (1 << (fromBits + toBits - 1)) - 1; + for (int i = 0; i < inLen; i++) { + int value = in[i + inStart] & 0xff; + if ((value >>> fromBits) != 0) { + throw new SegwitAddressException(String.format( + "Input value '%X' exceeds '%d' bit size", value, fromBits)); + } + acc = ((acc << fromBits) | value) & max_acc; + bits += fromBits; + while (bits >= toBits) { + bits -= toBits; + out.write((acc >>> bits) & maxv); + } + } + if (pad) { + if (bits > 0) out.write((acc << (toBits - bits)) & maxv); + } else if (bits >= fromBits || ((acc << (toBits - bits)) & maxv) != 0) { + throw new SegwitAddressException("Could not convert bits, invalid padding"); + } + return out.toByteArray(); + } + + /** Decode a SegWit address. */ + public static SegwitAddressData decode(final String hrp, final String addr) + throws SegwitAddressException { + Bech32.Bech32Data dec; + try { + dec = Bech32.decode(addr); + } catch (Bech32.Bech32Exception e) { + throw new SegwitAddressException(e); + } + if (dec.hrp.compareToIgnoreCase(hrp) != 0) { + throw new SegwitAddressException(String.format( + "Human-readable part expected '%s' but found '%s'", hrp, dec.hrp)); + } + if (dec.values.length < 1) throw new SegwitAddressException("Zero data found"); + // Skip the version byte and convert the rest of the decoded bytes + byte[] conv = convertBits(dec.values, 1, dec.values.length - 1, 5, 8, false); + + return new SegwitAddressData(dec.values[0], conv); + } + + /** Encode a SegWit address. */ + public static String encode(final String hrp, final int witver, final byte[] witprog) + throws SegwitAddressException { + ByteArrayOutputStream enc = new ByteArrayOutputStream(64 + 1); + enc.write(witver); + String ret; + try { + enc.write(convertBits(witprog, 0, witprog.length, 8, 5, true)); + ret = Bech32.encode(hrp, enc.toByteArray()); + } catch (Bech32.Bech32Exception | IOException e) { + throw new SegwitAddressException(e); + } + decode(hrp, ret); + return ret; + } + + /** + * Runs the SegWit address verification + * @throws SegwitAddressException on error + */ + public static void verify(SegwitAddressData data) throws SegwitAddressException { + if (data.version > 16) { + throw new SegwitAddressException("Invalid script version"); + } + if (data.program.length < 2 || data.program.length > 40) { + throw new SegwitAddressException("Invalid length"); + } + // Check script length for version 0 + if (data.version == 0 && data.program.length != 20 && data.program.length != 32) { + throw new SegwitAddressException("Invalid length for address version 0"); + } + } + + public static byte[] toScriptpubkey(SegwitAddressData data) { + ByteArrayOutputStream pubkey = new ByteArrayOutputStream(40 + 1); + int v = data.version; + // OP_0 is encoded as 0x00, but OP_1 through OP_16 are encoded as 0x51 though 0x60 + if (v > 0) { + v += 0x50; + } + pubkey.write(v); + pubkey.write(data.program.length); + pubkey.write(data.program, 0, data.program.length); + return pubkey.toByteArray(); + } +} diff --git a/ref/java/src/test/java/org/bech32/Bech32Test.java b/ref/java/src/test/java/org/bech32/Bech32Test.java new file mode 100644 index 0000000..875ebc2 --- /dev/null +++ b/ref/java/src/test/java/org/bech32/Bech32Test.java @@ -0,0 +1,206 @@ +/* Copyright (c) 2018 Coinomi Ltd + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.bech32; + +import org.junit.Test; + +import java.util.Locale; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +public class Bech32Test { + @Test + public void validChecksum() throws Bech32.Bech32Exception { + for (String valid : VALID_CHECKSUM) { + Bech32.Bech32Data dec = Bech32.decode(valid); + String recode = Bech32.encode(dec); + assertEquals(String.format("Failed to roundtrip '%s' -> '%s'", valid, recode), + valid.toLowerCase(Locale.ROOT), recode.toLowerCase(Locale.ROOT)); + // Test encoding with an uppercase HRP + recode = Bech32.encode(dec.hrp.toUpperCase(Locale.ROOT), dec.values); + assertEquals(String.format("Failed to roundtrip '%s' -> '%s'", valid, recode), + valid.toLowerCase(Locale.ROOT), recode.toLowerCase(Locale.ROOT)); + } + } + + @Test + public void invalidChecksum() { + for (String invalid : INVALID_CHECKSUM) { + try { + Bech32.decode(invalid); + fail(String.format("Parsed an invalid code: '%s'", invalid)); + } catch (Bech32.Bech32Exception e) { + /* expected */ + } + } + } + + @Test + public void validAddress() throws Bech32.Bech32Exception, SegwitAddress.SegwitAddressException { + for (AddressData valid : VALID_ADDRESS) { + assertValidAddress(valid, false); + assertValidAddress(valid, true); + } + } + + private void assertValidAddress(AddressData valid, boolean hrpUppercase) + throws SegwitAddress.SegwitAddressException { + String hrp = hrpUppercase ? "BC" : "bc"; + SegwitAddress.SegwitAddressData dec; + try { + dec = SegwitAddress.decode(hrp, valid.address); + } catch (SegwitAddress.SegwitAddressException e) { + hrp = hrpUppercase ? "TB" : "tb"; + dec = SegwitAddress.decode(hrp, valid.address); + } + + byte[] spk = SegwitAddress.toScriptpubkey(dec); + assertArrayEquals(String.format("decode produces wrong result: '%s'", valid.address), + valid.scriptPubKey, spk); + + String recode = SegwitAddress.encode(hrp, dec.version, dec.program); + assertEquals(String.format("encode roundtrip fails: '%s' -> '%s'", + valid.address.toLowerCase(Locale.ROOT), recode), + valid.address.toLowerCase(Locale.ROOT), recode); + } + + @Test + public void invalidAddress() { + for (String invalid : INVALID_ADDRESS) { + try { + SegwitAddress.decode("bc", invalid); + fail(String.format("Parsed an invalid address: '%s'", invalid)); + } catch (SegwitAddress.SegwitAddressException e) { /* expected */ } + try { + SegwitAddress.decode("tb", invalid); + fail(String.format("Parsed an invalid address: '%s'", invalid)); + } catch (SegwitAddress.SegwitAddressException e) { /* expected */ } + } + } + + @Test + public void invalidAddressEncoding() { + for (InvalidAddressData invalid : INVALID_ADDRESS_ENC) { + try { + String code = SegwitAddress.encode(invalid.hrp, invalid.version, new byte[invalid.program_length]); + fail(String.format("Encode succeeds on invalid '%s'", code)); + } catch (SegwitAddress.SegwitAddressException e) { /* expected */ } + } + } + + @Test + public void invalidHrp() throws Bech32.Bech32Exception { + byte[] program = new byte[20]; + for (String invalidHrp : INVALID_HRP_ENC) { + try { + String code = SegwitAddress.encode(invalidHrp, 0, program); + fail(String.format("Encode succeeds on invalid '%s'", code)); + } catch (SegwitAddress.SegwitAddressException e) { /* expected */ } + } + } + + // test vectors + private static String[] VALID_CHECKSUM = { + "A12UEL5L", + "an83characterlonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1tt5tgs", + "abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw", + "11qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqc8247j", + "split1checkupstagehandshakeupstreamerranterredcaperred2y9e3w" + }; + + private static String[] INVALID_CHECKSUM = { + " 1nwldj5", + new String(new char[] { 0x7f }) + "1axkwrx", + "an84characterslonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1569pvx", + "pzry9x0s0muk", + "1pzry9x0s0muk", + "x1b4n0q5v", + "li1dgmt3", + "de1lg7wt" + new String(new char[] { 0xff }), + }; + + private static class AddressData { + final String address; + final byte scriptPubKey[]; + + AddressData(String address, String scriptPubKeyHex) { + this.address = address; + // Convert hex to bytes, does minimal error checking + int len = scriptPubKeyHex.length(); + scriptPubKey = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + scriptPubKey[i / 2] = (byte) ((Character.digit(scriptPubKeyHex.charAt(i), 16) << 4) + + Character.digit(scriptPubKeyHex.charAt(i+1), 16)); + } + } + } + + private static AddressData[] VALID_ADDRESS = { + new AddressData("BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4", "0014751e76e8199196d454941c45d1b3a323f1433bd6"), + new AddressData("tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7","00201863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262"), + new AddressData("bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7k7grplx", "5128751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd6"), + new AddressData("BC1SW50QA3JX3S", "6002751e"), + new AddressData("bc1zw508d6qejxtdg4y5r3zarvaryvg6kdaj", "5210751e76e8199196d454941c45d1b3a323"), + new AddressData("tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy", "0020000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433"), + }; + + private static String[] INVALID_ADDRESS = { + "tc1qw508d6qejxtdg4y5r3zarvary0c5xw7kg3g4ty", + "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t5", + "BC13W508D6QEJXTDG4Y5R3ZARVARY0C5XW7KN40WF2", + "bc1rw5uspcuh", + "bc10w508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kw5rljs90", + "BC1QR508D6QEJXTDG4Y5R3ZARVARYV98GJ9P", + "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sL5k7", + "bc1zw508d6qejxtdg4y5r3zarvaryvqyzf3du", + "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3pjxtptv", + "bc1gmk9yu", + }; + + private static class InvalidAddressData { + final String hrp; + final int version; + final int program_length; + + InvalidAddressData(String hrp, int version, int program_length) { + this.hrp = hrp; + this.version = version; + this.program_length = program_length; + } + } + + private static InvalidAddressData[] INVALID_ADDRESS_ENC = { + new InvalidAddressData("bc", 0, 21), + new InvalidAddressData("bc", 17, 32), + new InvalidAddressData("bc", 1, 1), + new InvalidAddressData("bc", 16, 41), + }; + + private static String[] INVALID_HRP_ENC = { + "café", + "μπίτκοιν", + "бит", + "コイン", + }; +}