Skip to content

Add java ref implementation #40

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ref/java/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/target
11 changes: 11 additions & 0 deletions ref/java/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Bech32 Java

Build and test the library with
```
$ mvn test
```

Build the jar
```
$ mvn package
```
22 changes: 22 additions & 0 deletions ref/java/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.bech32</groupId>
<artifactId>bech32</artifactId>
<packaging>jar</packaging>
<version>1.0-SNAPSHOT</version>
<name>bech32</name>
<url>http://maven.apache.org</url>
<properties>
<maven.compiler.source>1.7</maven.compiler.source>
<maven.compiler.target>1.7</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
158 changes: 158 additions & 0 deletions ref/java/src/main/java/org/bech32/Bech32.java
Original file line number Diff line number Diff line change
@@ -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));
}
}
143 changes: 143 additions & 0 deletions ref/java/src/main/java/org/bech32/SegwitAddress.java
Original file line number Diff line number Diff line change
@@ -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();
}
}
Loading