diff --git a/ref/java/README.md b/ref/java/README.md new file mode 100644 index 0000000..905eac1 --- /dev/null +++ b/ref/java/README.md @@ -0,0 +1,27 @@ +# Bech32 + +Bech32 implementation in Java. + +## Build Process + +Install Maven 3.2 or higher. + +### Build: + +mvn clean + +mvn package + +Two .jar files will be created in the directory ./target : + +Bech32.jar : Can be included in any Java project 'as is' but requires inclusion of dependencies. Main.java harness not included. + +Bech32-jar-with-dependencies.jar : includes all dependencies and can be run from the command line using the Main.java harness. + +### Run using Main.java harness: + +java -jar -ea target/Bech32-jar-with-dependencies.jar + +### Dev contact: + +[PGP](http://pgp.mit.edu/pks/lookup?op=get&search=0x72B5BACDFEDF39D7) diff --git a/ref/java/pom.xml b/ref/java/pom.xml new file mode 100644 index 0000000..caa6ad8 --- /dev/null +++ b/ref/java/pom.xml @@ -0,0 +1,92 @@ + + + + 4.0.0 + org.bech32 + Bech32 + jar + 1.0-SNAPSHOT + Bech32 + http://maven.apache.org + + + UTF-8 + + + + Bech32 + + + + + + org.apache.maven.plugins + maven-eclipse-plugin + 2.9 + + true + false + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 2.3.2 + + ${jdk.version} + ${jdk.version} + + + + + + org.apache.maven.plugins + maven-jar-plugin + 2.6 + + + **/Main* + + + + + + maven-assembly-plugin + + + + org.bech32.Main + + + + jar-with-dependencies + + + + + make-assembly + package + + single + + + + + + + + + + + + commons-codec + commons-codec + 1.9 + + + + 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..72f6cf8 --- /dev/null +++ b/ref/java/src/main/java/org/bech32/Bech32.java @@ -0,0 +1,154 @@ +package org.bech32; + +import java.util.Locale; + +public class Bech32 { + + private static final String CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"; + + public static String bech32Encode(String hrp, byte[] data) { + + byte[] chk = createChecksum(hrp.getBytes(), data); + byte[] combined = new byte[chk.length + data.length]; + + System.arraycopy(data, 0, combined, 0, data.length); + System.arraycopy(chk, 0, combined, data.length, chk.length); + + byte[] xlat = new byte[combined.length]; + for(int i = 0; i < combined.length; i++) { + xlat[i] = (byte)CHARSET.charAt(combined[i]); + } + + byte[] ret = new byte[hrp.getBytes().length + xlat.length + 1]; + System.arraycopy(hrp.getBytes(), 0, ret, 0, hrp.getBytes().length); + System.arraycopy(new byte[] { 0x31 }, 0, ret, hrp.getBytes().length, 1); + System.arraycopy(xlat, 0, ret, hrp.getBytes().length + 1, xlat.length); + + return new String(ret); + } + + public static Pair bech32Decode(String bech) throws Exception { + + byte[] buffer = bech.getBytes(); + for(byte b : buffer) { + if(b < 0x21 || b > 0x7e) { + throw new Exception("bech32 characters out of range"); + } + } + + if(!bech.equals(bech.toLowerCase(Locale.ROOT)) && !bech.equals(bech.toUpperCase(Locale.ROOT))) { + throw new Exception("bech32 cannot mix upper and lower case"); + } + + bech = bech.toLowerCase(); + int pos = bech.lastIndexOf("1"); + if(pos < 1) { + throw new Exception("bech32 missing separator"); + } + else if(pos + 7 > bech.length()) { + throw new Exception("bech32 separator misplaced"); + } + else if(bech.length() < 8) { + throw new Exception("bech32 input too short"); + } + else if(bech.length() > 90) { + throw new Exception("bech32 input too long"); + } + else { + ; + } + + String s = bech.substring(pos + 1); + for(int i = 0; i < s.length(); i++) { + if(CHARSET.indexOf(s.charAt(i)) == -1) { + throw new Exception("bech32 characters out of range"); + } + } + + byte[] hrp = bech.substring(0, pos).getBytes(); + + byte[] data = new byte[bech.length() - pos - 1]; + for(int j = 0, i = pos + 1; i < bech.length(); i++, j++) { + data[j] = (byte)CHARSET.indexOf(bech.charAt(i)); + } + + if (!verifyChecksum(hrp, data)) { + throw new Exception("invalid bech32 checksum"); + } + + byte[] ret = new byte[data.length - 6]; + System.arraycopy(data, 0, ret, 0, data.length - 6); + + return Pair.of(new String(hrp), ret); + } + + private static int polymod(byte[] values) { + + final int[] GENERATORS = { 0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3 }; + + int chk = 1; + + for(byte b : values) { + byte top = (byte)(chk >> 0x19); + chk = b ^ ((chk & 0x1ffffff) << 5); + for(int i = 0; i < 5; i++) { + chk ^= ((top >> i) & 1) == 1 ? GENERATORS[i] : 0; + } + } + + return chk; + } + + private static byte[] hrpExpand(byte[] hrp) { + + byte[] buf1 = new byte[hrp.length]; + byte[] buf2 = new byte[hrp.length]; + byte[] mid = new byte[1]; + + for (int i = 0; i < hrp.length; i++) { + buf1[i] = (byte)(hrp[i] >> 5); + } + mid[0] = 0x00; + for (int i = 0; i < hrp.length; i++) { + buf2[i] = (byte)(hrp[i] & 0x1f); + } + + byte[] ret = new byte[(hrp.length * 2) + 1]; + System.arraycopy(buf1, 0, ret, 0, buf1.length); + System.arraycopy(mid, 0, ret, buf1.length, mid.length); + System.arraycopy(buf2, 0, ret, buf1.length + mid.length, buf2.length); + + return ret; + } + + private static boolean verifyChecksum(byte[] hrp, byte[] data) { + + byte[] exp = hrpExpand(hrp); + + byte[] values = new byte[exp.length + data.length]; + System.arraycopy(exp, 0, values, 0, exp.length); + System.arraycopy(data, 0, values, exp.length, data.length); + + return (1 == polymod(values)); + } + + private static byte[] createChecksum(byte[] hrp, byte[] data) { + + final byte[] zeroes = new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; + byte[] expanded = hrpExpand(hrp); + byte[] values = new byte[zeroes.length + expanded.length + data.length]; + + System.arraycopy(expanded, 0, values, 0, expanded.length); + System.arraycopy(data, 0, values, expanded.length, data.length); + System.arraycopy(zeroes, 0, values, expanded.length + data.length, zeroes.length); + + int polymod = polymod(values) ^ 1; + byte[] ret = new byte[6]; + for(int i = 0; i < ret.length; i++) { + ret[i] = (byte)((polymod >> 5 * (5 - i)) & 0x1f); + } + + return ret; + } + +} diff --git a/ref/java/src/main/java/org/bech32/Main.java b/ref/java/src/main/java/org/bech32/Main.java new file mode 100644 index 0000000..c84ba13 --- /dev/null +++ b/ref/java/src/main/java/org/bech32/Main.java @@ -0,0 +1,216 @@ +package org.bech32; + +import java.util.Arrays; + +import org.apache.commons.codec.binary.Hex; + +public class Main { + + // test vectors + private static String[] VALID_CHECKSUM = { + "A12UEL5L", + "an83characterlonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1tt5tgs", + "abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw", + "11qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqc8247j", + "split1checkupstagehandshakeupstreamerranterredcaperred2y9e3w" + }; + + private static String[][] VALID_ADDRESS = { + // example provided in BIP + new String[] { "bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3", "00201863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262"}, + // test vectors + new String[] { "BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4", "0014751e76e8199196d454941c45d1b3a323f1433bd6"}, + new String[] { "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7","00201863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262"}, + new String[] { "bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7k7grplx", "5128751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd6"}, + new String[] { "BC1SW50QA3JX3S", "6002751e"}, + new String[] { "bc1zw508d6qejxtdg4y5r3zarvaryvg6kdaj", "5210751e76e8199196d454941c45d1b3a323"}, + new String[] { "tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy", "0020000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433"}, + // BIP49 test vector + new String[] { "tb1q8zt37uunpakpg8vh0tz06jnj0jz5jddn5mlts3", "001438971f73930f6c141d977ac4fd4a727c854935b3"}, + }; + + // test vectors + private static String[] INVALID_ADDRESS = { + "tc1qw508d6qejxtdg4y5r3zarvary0c5xw7kg3g4ty", + "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t5", // bad checksum + "BC13W508D6QEJXTDG4Y5R3ZARVARY0C5XW7KN40WF2", + "bc1rw5uspcuh", + "bc10w508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kw5rljs90", + "BC1QR508D6QEJXTDG4Y5R3ZARVARYV98GJ9P", + "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sL5k7", // mixed case + "bc1zw508d6qejxtdg4y5r3zarvaryvqyzf3du", + "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3pjxtptv", + "bc1gmk9yu" + }; + + private static String[] INVALID_CHECKSUM = { + " 1nwldj5", + new String(new char[] { 0x7f }) + "1axkwrx", + "an84characterslonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1569pvx", + "pzry9x0s0muk", + "1pzry9x0s0muk", + "x1b4n0q5v", + "li1dgmt3", + "de1lg7wt" + new String(new char[] { 0xff }), + }; + + public static void main(String[] args) { + + try { + + Pair p = null; + + System.out.println("valid checksum test"); + for(String s : VALID_CHECKSUM) { + + p = null; + + try{ + p = Bech32.bech32Decode(s); + assert(p.getLeft() != null); + } + catch(Exception e) { + System.out.println("Error:" + s + "," + e.getMessage()); + } + } + + System.out.println("invalid checksum test"); + for(String s : INVALID_CHECKSUM) { + + p = null; + + try{ + p = Bech32.bech32Decode(s); + assert(p.getLeft() == null); + } + catch(Exception e) { + ; + } + + assert(p == null); + + } + + System.out.println("valid address test"); + for(String[] s : VALID_ADDRESS) { + p = null; + + try{ + p = Bech32.bech32Decode(s[0]); + } + catch(Exception e) { + System.out.println("Error:" + s[0] + "," + e.getMessage()); + } + } + + System.out.println("invalid address test"); + for(String s : INVALID_ADDRESS) { + + p = null; + Pair pair = null; + + try { + p = Bech32.bech32Decode(s); + pair = SegwitAddress.decode(p.getLeft(), s); + } + catch(Exception e) { + ; + } + + assert(p == null || pair == null); + + } + + System.out.println("valid segwit address test"); + for(String[] s : VALID_ADDRESS) { + try { + byte witVer; + String hrp = new String(Bech32.bech32Decode(s[0]).getLeft()); + + byte[] witProg; + Pair segp = null; + segp = SegwitAddress.decode(hrp, s[0]); + assert(segp != null); + witVer = segp.getLeft(); + witProg = segp.getRight(); + assert(!(witVer < 0 || witVer > 16)); + + byte[] pubkey = SegwitAddress.getScriptPubkey(witVer, witProg); + assert(Hex.encodeHexString(pubkey).equalsIgnoreCase(s[1])); + + String address = SegwitAddress.encode(hrp, witVer, witProg); + assert(s[0].equalsIgnoreCase(address)); + + int idx = s[0].lastIndexOf("1"); + Pair testPair = null; + try{ + testPair = SegwitAddress.decode(hrp, s[0].substring(0, idx + 1) + new String(new char[] { (char)(s[0].charAt(idx + 1) ^ 1) }) + s[0].substring(idx + 2)); + assert(!Arrays.equals(witProg, testPair.getRight())); + } + catch(Exception e) { + ; + } + assert(testPair == null); + + } + catch(Exception e) { + System.out.println("Error:" + s[0] + "," + e.getMessage()); + } + + } + + System.out.println("invalid segwit address test"); + for(String s : INVALID_ADDRESS) { + + Pair segp = null; + + try { + byte witVer; + String hrp = new String(Bech32.bech32Decode(s).getLeft()); + + segp = SegwitAddress.decode(new String(hrp), s); + } + catch(Exception e) { + ; + } + + assert(segp == null); + + } + + } + catch(Exception e) { + e.printStackTrace(); + } + + System.out.println("encode BIP49 test vector"); + try { + + Hex hex = new Hex(); + + String address = SegwitAddress.encode("tb", (byte)0x00, hex.decode("38971f73930f6c141d977ac4fd4a727c854935b3".getBytes())); + System.out.println("BIP49 test vector:" + address); + + byte witVer; + String hrp = new String(Bech32.bech32Decode(address).getLeft()); + + byte[] witProg; + Pair segp = null; + segp = SegwitAddress.decode(hrp, address); + witVer = segp.getLeft(); + witProg = segp.getRight(); + System.out.println("decoded witVer:" + witVer); + System.out.println("decoded witProg:" + Hex.encodeHexString(witProg)); + + assert(!(witVer < 0 || witVer > 16)); + + byte[] pubkey = SegwitAddress.getScriptPubkey(witVer, witProg); + System.out.println("decoded pubkey:" + Hex.encodeHexString(pubkey)); + } + catch(Exception e) { + System.out.println("Error:" + e.getMessage()); + } + + } + +} diff --git a/ref/java/src/main/java/org/bech32/Pair.java b/ref/java/src/main/java/org/bech32/Pair.java new file mode 100644 index 0000000..82c4bfa --- /dev/null +++ b/ref/java/src/main/java/org/bech32/Pair.java @@ -0,0 +1,27 @@ +package org.bech32; + +// clone of org.apache.commons.lang3.tuple.Pair; + +public class Pair { + + private K elementLeft = null; + private V elementRight = null; + + public static Pair of(K elementLeft, V elementRight) { + return new Pair(elementLeft, elementRight); + } + + public Pair(K elementLeft, V elementRight) { + this.elementLeft = elementLeft; + this.elementRight = elementRight; + } + + public K getLeft() { + return elementLeft; + } + + public V getRight() { + return elementRight; + } + +} 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..789ff9f --- /dev/null +++ b/ref/java/src/main/java/org/bech32/SegwitAddress.java @@ -0,0 +1,123 @@ +package org.bech32; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class SegwitAddress { + + public static Pair decode(String hrp, String addr) throws Exception { + + Pair p = Bech32.bech32Decode(addr); + + String hrpgotStr = p.getLeft(); + if(hrpgotStr == null) { + return null; + } + if (!hrp.equalsIgnoreCase(hrpgotStr)) { + return null; + } + if (!hrpgotStr.equalsIgnoreCase("bc") && !hrpgotStr.equalsIgnoreCase("tb")) { + throw new Exception("invalid segwit human readable part"); + } + + byte[] data = p.getRight(); + List progBytes = new ArrayList(); + for(int i = 1; i < data.length; i++) { + progBytes.add(data[i]); + } + byte[] decoded = convertBits(progBytes, 5, 8, false); + if(decoded.length < 2 || decoded.length > 40) { + throw new Exception("invalid decoded data length"); + } + + byte witnessVersion = data[0]; + if (witnessVersion > 16) { + throw new Exception("invalid decoded witness version"); + } + + if (witnessVersion == 0 && decoded.length != 20 && decoded.length != 32) { + throw new Exception("decoded witness version 0 with unknown length"); + } + + return Pair.of(witnessVersion, decoded); + } + + public static String encode(String hrp, byte witnessVersion, byte[] witnessProgram) throws Exception { + + List progBytes = new ArrayList(); + for(int i = 0; i < witnessProgram.length; i++) { + progBytes.add(witnessProgram[i]); + } + + byte[] prog = convertBits(progBytes, 8, 5, true); + byte[] data = new byte[1 + prog.length]; + + System.arraycopy(new byte[] { witnessVersion }, 0, data, 0, 1); + System.arraycopy(prog, 0, data, 1, prog.length); + + String ret = Bech32.bech32Encode(hrp, data); + + return ret; + } + + private static byte[] convertBits(List data, int fromBits, int toBits, boolean pad) throws Exception { + + int acc = 0; + int bits = 0; + int maxv = (1 << toBits) - 1; + List ret = new ArrayList(); + + for(Byte value : data) { + + short b = (short)(value.byteValue() & 0xff); + + if (b < 0) { + throw new Exception(); + } + else if ((b >> fromBits) > 0) { + throw new Exception(); + } + else { + ; + } + + acc = (acc << fromBits) | b; + bits += fromBits; + while (bits >= toBits) { + bits -= toBits; + ret.add((byte)((acc >> bits) & maxv)); + } + } + + if(pad && (bits > 0)) { + ret.add((byte)((acc << (toBits - bits)) & maxv)); + } + else if (bits >= fromBits || (byte)(((acc << (toBits - bits)) & maxv)) != 0) { + return null; + } + else { + ; + } + + byte[] buf = new byte[ret.size()]; + for(int i = 0; i < ret.size(); i++) { + buf[i] = ret.get(i); + } + + return buf; + } + + public static byte[] getScriptPubkey(byte witver, byte[] witprog) { + + byte v = (witver > 0) ? (byte)(witver + 0x50) : (byte)0; + byte[] ver = new byte[] { v, (byte)witprog.length }; + + byte[] ret = new byte[witprog.length + ver.length]; + System.arraycopy(ver, 0, ret, 0, ver.length); + System.arraycopy(witprog, 0, ret, ver.length, witprog.length); + + return ret; + } + +}