194 lines
6.5 KiB
Java
194 lines
6.5 KiB
Java
package love.distributedrebirth.no2all.octo;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.List;
|
|
import java.util.Locale;
|
|
|
|
public final class OctoBech32 {
|
|
|
|
private static final String CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
|
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 OctoBech32String toBech32(String hrp, byte[] hexKey) {
|
|
byte[] data = convertBits(hexKey, 8, 5, true);
|
|
return encode(new OctoBech32Bucket(OctoBech32Variant.BECH32, hrp, data));
|
|
}
|
|
|
|
public static OctoBech32Bucket fromBech32(OctoBech32String link) {
|
|
OctoBech32Bucket shareB32 = decode(link);
|
|
byte[] data = shareB32.getData();
|
|
data = convertBits(data, 5, 8, true);
|
|
data = Arrays.copyOfRange(data, 0, data.length - 1);
|
|
return new OctoBech32Bucket(shareB32.getVariant(), shareB32.getHrp(), data);
|
|
}
|
|
|
|
public static OctoBech32String encode(OctoBech32Bucket shareB32) {
|
|
if (shareB32.getHrp().length() < 1) {
|
|
throw new IllegalArgumentException("Human-readable part is too short");
|
|
}
|
|
String hrp = shareB32.getHrp().toLowerCase(Locale.ROOT);
|
|
byte[] values = shareB32.getData();
|
|
byte[] checksum = createChecksum(shareB32.getVariant(), 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 new OctoBech32String(sb.toString());
|
|
}
|
|
|
|
public static OctoBech32Bucket decode(final OctoBech32String link) {
|
|
final String str = link.getValue();
|
|
boolean lower = false;
|
|
boolean upper = false;
|
|
if (str.length() < 8) {
|
|
throw new IllegalArgumentException("Input too short: " + str.length());
|
|
}
|
|
for (int i = 0; i < str.length(); ++i) {
|
|
char c = str.charAt(i);
|
|
if (c < 33 || c > 126) {
|
|
throw new IllegalArgumentException(String.format("Invalid Character %c, %d", c, i));
|
|
}
|
|
if (c >= 'a' && c <= 'z') {
|
|
if (upper) {
|
|
throw new IllegalArgumentException(String.format("Invalid Character %c, %d", c, i));
|
|
}
|
|
lower = true;
|
|
|
|
}
|
|
if (c >= 'A' && c <= 'Z') {
|
|
if (lower) {
|
|
throw new IllegalArgumentException(String.format("Invalid Character %c, %d", c, i));
|
|
}
|
|
upper = true;
|
|
}
|
|
}
|
|
final int pos = str.lastIndexOf('1');
|
|
if (pos < 1) {
|
|
throw new IllegalArgumentException("Missing human-readable part");
|
|
}
|
|
final int dataPartLength = str.length() - 1 - pos;
|
|
if (dataPartLength < 6) {
|
|
throw new IllegalArgumentException(String.format("Data part too short: %d)", dataPartLength));
|
|
}
|
|
byte[] values = new byte[dataPartLength];
|
|
for (int i = 0; i < dataPartLength; ++i) {
|
|
char c = str.charAt(i + pos + 1);
|
|
if (CHARSET_REV[c] == -1) {
|
|
throw new IllegalArgumentException(String.format("Invalid Character %c, %d", c, i + pos + 1));
|
|
}
|
|
values[i] = CHARSET_REV[c];
|
|
}
|
|
String hrp = str.substring(0, pos).toLowerCase(Locale.ROOT);
|
|
OctoBech32Variant encoding = verifyChecksum(hrp, values);
|
|
if (encoding == null) {
|
|
throw new IllegalArgumentException("InvalidChecksum");
|
|
}
|
|
return new OctoBech32Bucket(encoding, hrp, Arrays.copyOfRange(values, 0, values.length - 6));
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
private static OctoBech32Variant 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);
|
|
int check = polymod(combined);
|
|
for (OctoBech32Variant type:OctoBech32Variant.values()) {
|
|
if (type.getMarker() == check) {
|
|
return type;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private static byte[] createChecksum(final OctoBech32Variant encoding, 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) ^ encoding.getMarker();
|
|
byte[] ret = new byte[6];
|
|
for (int i = 0; i < 6; ++i) {
|
|
ret[i] = (byte) ((mod >>> (5 * (5 - i))) & 31);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
private static byte[] convertBits(byte[] data, int fromWidth, int toWidth, boolean pad) {
|
|
int acc = 0;
|
|
int bits = 0;
|
|
List<Byte> result = new ArrayList<>();
|
|
for (int i = 0; i < data.length; i++) {
|
|
int value = (data[i] & 0xff) & ((1 << fromWidth) - 1);
|
|
acc = (acc << fromWidth) | value;
|
|
bits += fromWidth;
|
|
while (bits >= toWidth) {
|
|
bits -= toWidth;
|
|
result.add((byte) ((acc >> bits) & ((1 << toWidth) - 1)));
|
|
}
|
|
}
|
|
if (pad) {
|
|
if (bits > 0) {
|
|
result.add((byte) ((acc << (toWidth - bits)) & ((1 << toWidth) - 1)));
|
|
}
|
|
} else if (bits == fromWidth || ((acc << (toWidth - bits)) & ((1 << toWidth) - 1)) != 0) {
|
|
return null;
|
|
}
|
|
byte[] output = new byte[result.size()];
|
|
for (int i = 0; i < output.length; i++) {
|
|
output[i] = result.get(i);
|
|
}
|
|
return output;
|
|
}
|
|
}
|