diff --git a/src/main/java/com/sparrowwallet/hummingbird/BC32.java b/src/main/java/com/sparrowwallet/hummingbird/BC32.java new file mode 100644 index 0000000..f3c7248 --- /dev/null +++ b/src/main/java/com/sparrowwallet/hummingbird/BC32.java @@ -0,0 +1,171 @@ +package com.sparrowwallet.hummingbird; + +/* + * Copyright 2018 Coinomi Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class BC32 { + /** + * The BC32 character set for encoding. + */ + private static final String CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"; + + /** + * 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; + } + + /** + * Verify a checksum. + */ + private static boolean verifyChecksum(final byte[] values) { + byte[] combined = new byte[1 + values.length]; + System.arraycopy(new byte[]{0}, 0, combined, 0, 1); + System.arraycopy(values, 0, combined, 1, values.length); + return polymod(combined) == 0x3fffffff; + } + + /** + * Create a checksum. + */ + private static byte[] createChecksum(final byte[] values) { + byte[] enc = new byte[1 + values.length + 6]; + System.arraycopy(values, 0, enc, 1, values.length); + int mod = polymod(enc) ^ 0x3fffffff; + byte[] result = new byte[6]; + for (int i = 0; i < 6; ++i) { + result[i] = (byte) ((mod >>> (5 * (5 - i))) & 31); + } + return result; + } + + /** + * Encode a BC32 string. + */ + public static String encode(final byte[] values) { + List boxedList = new ArrayList<>(values.length); + for(byte value : values) { + boxedList.add(value); + } + + byte[] data = convertBits(boxedList, 8, 5, true); + + byte[] checksum = createChecksum(data); + byte[] combined = new byte[data.length + checksum.length]; + System.arraycopy(data, 0, combined, 0, data.length); + System.arraycopy(checksum, 0, combined, data.length, checksum.length); + + StringBuilder sb = new StringBuilder(combined.length); + for (byte b : combined) { + sb.append(CHARSET.charAt(b)); + } + return sb.toString(); + } + + /** + * Decode a BC32 string. + */ + public static byte[] decode(final String str) { + boolean lower = false, upper = false; + if (str.length() < 6) + throw new BC32DecoderException.InvalidDataLength("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 BC32DecoderException.InvalidCharacter(c, i); + if (c >= 'a' && c <= 'z') { + if (upper) + throw new BC32DecoderException.InvalidCharacter(c, i); + lower = true; + } + if (c >= 'A' && c <= 'Z') { + if (lower) + throw new BC32DecoderException.InvalidCharacter(c, i); + upper = true; + } + } + + final int dataPartLength = str.length(); + byte[] values = new byte[dataPartLength]; + for (int i = 0; i < dataPartLength; ++i) { + char c = str.charAt(i); + + if(CHARSET.indexOf(c) == -1) { + throw new IllegalArgumentException("BC32 characters out of range"); + } + values[i] = (byte) CHARSET.indexOf(c); + } + + if (!verifyChecksum(values)) throw new BC32DecoderException.InvalidChecksum(); + + byte[] data = Arrays.copyOfRange(values, 0, values.length - 6); + List valueList = new ArrayList<>(); + for (byte b : data) { + valueList.add(b); + } + return convertBits(valueList, 5, 8, false); + } + + private static byte[] convertBits(List data, int fromBits, int toBits, boolean pad) { + int acc = 0; + int bits = 0; + int maxv = (1 << toBits) - 1; + List ret = new ArrayList<>(); + + for (Byte value : data) { + short b = (short) (value & 0xff); + if ((b >> fromBits) > 0) { + throw new IllegalArgumentException("Illegal bytes for bc32 encoding"); + } + + 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) { + throw new IllegalArgumentException("Illegal bytes for bc32 encoding"); + } + + Object[] boxedArray = ret.toArray(); + int len = boxedArray.length; + byte[] array = new byte[len]; + for (int i = 0; i < len; i++) { + array[i] = ((Number)boxedArray[i]).byteValue(); + } + + return array; + } +} \ No newline at end of file diff --git a/src/main/java/com/sparrowwallet/hummingbird/BC32DecoderException.java b/src/main/java/com/sparrowwallet/hummingbird/BC32DecoderException.java new file mode 100644 index 0000000..4c61b44 --- /dev/null +++ b/src/main/java/com/sparrowwallet/hummingbird/BC32DecoderException.java @@ -0,0 +1,32 @@ +package com.sparrowwallet.hummingbird; + +public class BC32DecoderException extends IllegalArgumentException { + + public BC32DecoderException(String message) { + super(message); + } + + public static class InvalidCharacter extends BC32DecoderException { + public final char character; + public final int position; + + public InvalidCharacter(char character, int position) { + super("Invalid character '" + character + "' at position " + position); + this.character = character; + this.position = position; + } + } + + public static class InvalidDataLength extends BC32DecoderException { + + public InvalidDataLength(String message) { + super(message); + } + } + + public static class InvalidChecksum extends BC32DecoderException { + public InvalidChecksum() { + super("Checksum does not validate"); + } + } +} diff --git a/src/main/java/com/sparrowwallet/hummingbird/LegacyURDecoder.java b/src/main/java/com/sparrowwallet/hummingbird/LegacyURDecoder.java new file mode 100644 index 0000000..e99bc73 --- /dev/null +++ b/src/main/java/com/sparrowwallet/hummingbird/LegacyURDecoder.java @@ -0,0 +1,146 @@ +package com.sparrowwallet.hummingbird; + +import java.security.MessageDigest; +import java.util.*; + +public class LegacyURDecoder { + private final Set fragments = new LinkedHashSet<>(); + + public void receivePart(String fragment) { + fragments.add(fragment); + } + + public boolean isComplete() { + if(fragments.isEmpty()) { + return false; + } + + String fragment = fragments.iterator().next(); + String[] components = fragment.split("/"); + if(components.length > 3) { + int[] sequence = checkAndGetSequence(components[1]); + int total = sequence[1]; + return total == fragments.size(); + } + + return true; + } + + public static boolean isLegacyURFragment(String fragment) { + String[] components = fragment.split("/"); + + //Multi-part legacy encoding may include digest which adds an extra component + if(components.length > 3) { + return true; + } + + //Last component is always fragment payload in both legacy and current + String payload = components[components.length-1]; + + //BC32 will never contain the following characters + if(payload.indexOf('b') > -1 || payload.indexOf('i') > -1 || payload.indexOf('o') > -1) { + return false; + } + + //Bytewords and BC32 strings can usually be distinguished because the latter includes digits as permissible characters + return (payload.matches(".*\\d.*")); + } + + public UR decode() throws UR.InvalidTypeException { + return decode(fragments.toArray(new String[0])); + } + + public static UR decode(String[] fragments) throws UR.InvalidTypeException { + int length = fragments.length; + if(length == 1){ + return handleFragment(fragments[0]); + } + else { + return handleFragments(fragments); + } + } + + private static UR handleFragment(String fragment) throws UR.InvalidTypeException { + String[] components = fragment.split("/"); + + switch(components.length) { + case 2 -> { + return new UR(components[0], BC32.decode(components[1])); + } + case 3 -> { + String digest = components[1]; + String data = components[2]; + checkDigest(data, digest); + return new UR(components[0], BC32.decode(data)); + } + case 4 -> { + checkAndGetSequence(components[1]); + String digest = components[2]; + String data = components[3]; + checkDigest(digest, fragment); + return new UR(components[0], BC32.decode(data)); + } + default -> throw new IllegalArgumentException("Invalid number of fragments: expected 2 / 3 / 4 but got " + components.length); + } + } + + private static UR handleFragments(String[] fragments) throws UR.InvalidTypeException { + int length = fragments.length; + String[] parts = new String[length]; + Arrays.fill(parts, ""); + String type = null; + String digest = null; + for(String fragment : fragments) { + String[] components = fragment.split("/"); + if(components.length < 4) { + throw new IllegalArgumentException(String.format("Invalid fragment: %s, insufficient number of components (%d)", fragment, components.length)); + } + if(type != null && !type.equals(components[0])) { + throw new IllegalArgumentException(String.format("Invalid fragment: %s, checksum changed %s, %s", fragment, type, components[0])); + } + type = components[0]; + int[] sequence = checkAndGetSequence(components[1]); + int index = sequence[0]; + int total = sequence[1]; + if(total != length) { + throw new IllegalArgumentException(String.format("Invalid fragment: %s, total %d not equals to fragments length %d", fragment, total, length)); + } + if(digest != null && !digest.equals(components[2])) { + throw new IllegalArgumentException(String.format("Invalid fragment: %s, checksum changed %s, %s", fragment, digest, components[2])); + } + digest = components[2]; + if(parts[index - 1].length() > 0) { + throw new IllegalArgumentException(String.format("Invalid fragment: %s, index %d has already been set", fragment, index)); + } + parts[index - 1] = components[3]; + } + String payload = Arrays.stream(parts).reduce((cur, acc) -> cur+acc).orElse(""); + checkDigest(payload, digest); + + return new UR(type, BC32.decode(payload)); + } + + private static void checkDigest(String payload, String digest) { + MessageDigest sha256Digest = LegacyUREncoder.newDigest(); + sha256Digest.update(BC32.decode(payload)); + byte[] calculatedChecksum = sha256Digest.digest(); + byte[] checksum = BC32.decode(digest); + + if(!Arrays.equals(calculatedChecksum, checksum)) { + throw new IllegalArgumentException("Invalid digest: " + digest + " for payload: " + payload); + } + } + + public static int[] checkAndGetSequence(String payload) { + String[] pieces = payload.toLowerCase().split("of"); + if(pieces.length != 2) { + throw new IllegalArgumentException("Invalid sequence: " + payload); + } + int index = Integer.parseInt(pieces[0]); + int total = Integer.parseInt(pieces[1]); + if(index < 1 || index > total) { + throw new IllegalArgumentException("Invalid sequence: " + payload); + } + return new int[]{index, total}; + } +} diff --git a/src/main/java/com/sparrowwallet/hummingbird/LegacyUREncoder.java b/src/main/java/com/sparrowwallet/hummingbird/LegacyUREncoder.java new file mode 100644 index 0000000..888bcc3 --- /dev/null +++ b/src/main/java/com/sparrowwallet/hummingbird/LegacyUREncoder.java @@ -0,0 +1,84 @@ +package com.sparrowwallet.hummingbird; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.stream.IntStream; + +public class LegacyUREncoder { + public static final int DEFAULT_FRAGMENT_LENGTH = 200; + + private final UR ur; + private final int fragmentLen; + + public LegacyUREncoder(UR ur) { + this(ur, DEFAULT_FRAGMENT_LENGTH); + } + + public LegacyUREncoder(UR ur, int fragmentLen) { + this.ur = ur; + this.fragmentLen = fragmentLen; + } + + public String[] encode() { + String encoded = BC32.encode(ur.getCborBytes()); + + MessageDigest sha256Digest = newDigest(); + sha256Digest.update(ur.getCborBytes()); + byte[] checksum = sha256Digest.digest(); + String bc32Checksum = BC32.encode(checksum); + + String[] fragments = splitData(encoded, fragmentLen); + return composeHeadersToFragments(fragments, bc32Checksum); + } + + private String[] splitData(String s, int fragmentLen) { + int count = (int)Math.ceil(s.length() / (float)fragmentLen); + int partLength = (int)Math.ceil(s.length() / (float)count); + String[] fragments = new String[count]; + for(int i = 0; i < count; i++) { + fragments[i] = s.substring(partLength * i, Math.min(partLength * (i + 1), s.length())); + } + + return fragments; + } + + private String[] composeHeadersToFragments(String[] fragments, String checksum) { + int length = fragments.length; + if(length <= 1) { + return Arrays.stream(fragments).map(this::composeUR).toArray(String[]::new); + } else { + return IntStream.range(0, length) + .mapToObj(i -> composeHeadersToFragment(fragments[i], checksum, i, length)) + .toArray(String[]::new); + } + } + + public static MessageDigest newDigest() { + try { + return MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); // Can't happen. + } + } + + public String composeHeadersToFragment(String fragment, String checksum, int index, int total) { + return composeUR(composeSequencing(composeChecksum(fragment, checksum), index, total)); + } + + private String composeChecksum(String payload, String checksum) { + return String.format("%s/%s", checksum, payload); + } + + private String composeSequencing(String payload, int index, int total) { + return String.format("%dof%d/%s", index + 1, total, payload); + } + + private String composeUR(String payload) { + return composeUR(payload, ur.getType()); + } + + private String composeUR(String payload, String type) { + return String.format("ur:%s/%s", type, payload); + } +} diff --git a/src/test/java/com/sparrowwallet/hummingbird/BC32Test.java b/src/test/java/com/sparrowwallet/hummingbird/BC32Test.java new file mode 100644 index 0000000..3745df4 --- /dev/null +++ b/src/test/java/com/sparrowwallet/hummingbird/BC32Test.java @@ -0,0 +1,20 @@ +package com.sparrowwallet.hummingbird; + +import org.junit.Assert; +import org.junit.Test; + +import java.nio.charset.StandardCharsets; + +public class BC32Test { + @Test + public void testDecode() { + byte[] decode = BC32.decode("fpjkcmr0ypmk7unvvsh4ra4j"); + Assert.assertEquals("Hello world", new String(decode)); + } + + @Test + public void testEncode() { + String s = BC32.encode("Hello world".getBytes(StandardCharsets.UTF_8)); + Assert.assertEquals("fpjkcmr0ypmk7unvvsh4ra4j", s); + } +} diff --git a/src/test/java/com/sparrowwallet/hummingbird/URTest.java b/src/test/java/com/sparrowwallet/hummingbird/URTest.java index d427d6b..56ae0e4 100644 --- a/src/test/java/com/sparrowwallet/hummingbird/URTest.java +++ b/src/test/java/com/sparrowwallet/hummingbird/URTest.java @@ -7,6 +7,8 @@ import org.junit.Assert; import org.junit.Test; import java.io.ByteArrayOutputStream; +import java.util.Arrays; +import java.util.Iterator; import java.util.List; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -17,6 +19,7 @@ public class URTest { UR ur = makeMessageUR(50, "Wolf"); String encoded = UREncoder.encode(ur); Assert.assertEquals("ur:bytes/hdeymejtswhhylkepmykhhtsytsnoyoyaxaedsuttydmmhhpktpmsrjtgwdpfnsboxgwlbaawzuefywkdplrsrjynbvygabwjldapfcsdwkbrkch", encoded); + Assert.assertFalse(LegacyURDecoder.isLegacyURFragment(encoded)); } @Test @@ -47,6 +50,7 @@ public class URTest { "ur:bytes/20-9/lpbbascfadaxcywenbpljkhdcayapmrleeleaxpasfrtrdkncffwjyjzgyetdmlewtkpktgllepfrltataztksmhkbot" }; Assert.assertArrayEquals("", expectedParts, parts.toArray()); + parts.forEach(part -> Assert.assertFalse(LegacyURDecoder.isLegacyURFragment(part))); } @Test @@ -58,6 +62,7 @@ public class URTest { do { String part = urEncoder.nextPart(); + Assert.assertFalse(LegacyURDecoder.isLegacyURFragment(part)); urDecoder.receivePart(part); } while(urDecoder.getResult() == null); @@ -103,4 +108,102 @@ public class URTest { String encoded = UREncoder.encode(ur); Assert.assertEquals("ur:bytes/gdaebycpeofygoiyktlonlpkrksfutwyzmwmfyeozs", encoded); } + + @Test + public void testLegacyEncode() throws UR.InvalidTypeException, UR.InvalidCBORException { + String random = "7e4a61385e2550981b4b5633ab178eb077a30505fbd53f107ec1081e7cf0ca3c0dc0bfea5b8bfb5e6ffc91afd104c3aa756210b5dbc5118fd12c87ee04269815ba6a9968a0d0d3b7a9b631382a36bc70ab626d5670b4b48ff843f4d9a15631aa67c7aaf0ac6ce7e3bff03b2c9643e3375e47493c4e0f8635329d66fdec41b10ce74dcbf25fc15d829e7830c325643a98561f441b40a02e8353493e6afc16192fe99d90d8ca65539af77ddeaccc8943a37563a9ba83675bd5d4da7c60c9a172cf6940cbf0ec8fe04175a629932e3512c5d2aaea3cca3246f40a21ffdc33c3987dc7b880351230eb3759fe3c7dc7b2d3a20a95996ff0b7a0dba834f96beb64c14e3426fb051a936ba41569ab99c0066a6d9c0777a49e49e6cbad24d722a4c7da112432679264b9adc0a8cff9dd1fe0ee9ee2747f6a68537c389a7303a1af23c534ee6392bc17b04cf0fbce7689e66b673a440c04a9454005b0c76664639113458eb7d0902eff04d11138ce2a8ee16a9cd7c8926514efa9bd83ae7a4c139835f0fe0f68c628e0645c8524c30dfc314e825a7aa13224d98e2f7a9d12183a999bb1f28549c99a9072d99c05c24e0c84848c4fc147a094ab7b69e9cbea86952fccf15500fbb234ffe6ee6e6ded515c8016cb017ba36fb931ef276cec4ed22c1aed1495d2df3b3ce66c03f5b9ffa8434bf0e8fb149de94e050b3da178df1f76c00a366cb2801fabdf1a1e90cd3cd45ecb7a930a40b151455f76b726d552f31c21324992da257ff8bde2923dfd5d0d6b87233fae215ffacbecd96249099e7e3427d533db56cdb09c7475b4ce3314e33f43953a7370866cc11d85f00b71b15510b46c4b4fa490c660ddfeda0ceb1b8265995f7071c155ad1b57465fdc0fa81a73f9f19ac4872029d5844c1838f732e803043673e26cbc5b51297a324ff00a2d2d4222bad556b93d27c8e376e3ff8f9d37b3073410708ebb3d4dd7473d27212310b71a3c33a5c8f87f44824640e7f8970f4eda9364195c87a91172b8f085f1773641dde1ce21938746234055bc971ce2325f814e3eec60f781dd4faf52afd5be4a6b38656f7e9739f724cb7ccd4e4d01e802add3dc7b83191f894b3ee0ed752ee514d5ec55"; + + UR ur = UR.fromBytes(TestUtils.hexToBytes(random)); + LegacyUREncoder legacyUREncoder = new LegacyUREncoder(ur); + String[] result = legacyUREncoder.encode(); + Assert.assertArrayEquals(result, new String[]{ + "ur:bytes/1of7/0jsmw5retzcecxhpgz2e6pnzw9qy98m0frafzjuwn324w4yf8xdq0h0cmw/typjqlj2vyu9uf2snqd5k43n4vtcavrh5vzst7748ug8asggre70pj3uphqtl6jm30a4umlujxhazpxr4f6kyy94m0z3rr739jr7uppxnq2m565edzsdp5ah4xmrzwp2x678p2mzd4t8pd953luy8axe59trr2n8c740ptrvul3mlupm9jty8ceht", + "ur:bytes/2of7/0jsmw5retzcecxhpgz2e6pnzw9qy98m0frafzjuwn324w4yf8xdq0h0cmw/er5j0zwp7rr2v5avm77csd3pnn5mjljtlq4mq570qcvxfty82v9v86yrdq2qt5r2dynu6huzcvjl6vajrvv5e2nntmhmh4vejy58gm4vw5m4qm8t02afknuvry6zuk0d9qvhu8v3lsyzadx9xfjudgjchf2463uegeydaq2y8lacv7rnp7u0wyqx5", + "ur:bytes/3of7/0jsmw5retzcecxhpgz2e6pnzw9qy98m0frafzjuwn324w4yf8xdq0h0cmw/frp6eht8lrclw8ktf6yz54n9hlpdaqmw5rf7ttadjvzn35ymas2x5ndwjp26dtn8qqv6ndnsrh0fy7f8nvhtfy6u32f376zyjryeujvju6ms9gelua68lqa60wyarldf59xlpcnfes8gd0y0znfmnrj27p0vzv7rauua5fue4kwwjypsz2j32qqkc", + "ur:bytes/4of7/0jsmw5retzcecxhpgz2e6pnzw9qy98m0frafzjuwn324w4yf8xdq0h0cmw/vwenyvwg3x3vwklgfqthlqng3zwxw928wz65u6lyfyeg5a75mmqaw0fxp8xp47rlq76xx9rsxghy9ynpsmlp3f6p9574pxgjdnr3002w3yxp6nxdmru59f8ye4yrjmxwqtsjwpjzgfrz0c9r6p99t0d57njl2s62jln8325q0hv35llnwumnda4g4", + "ur:bytes/5of7/0jsmw5retzcecxhpgz2e6pnzw9qy98m0frafzjuwn324w4yf8xdq0h0cmw/eqqkevqhhgm0hyc77fmva38dytq6a52ft5kl8v7wvmqr7kull2zrf0cw37c5nh55upgt8ksh3hclwmqq5dnvk2qpl27lrg0fpnfu630vk75npfqtz529tamtwfk42te3cgfjfxfd5ftllz779y3al4ws66u8yvl6ug2llt97ektzfyyeul35yl2n8", + "ur:bytes/6of7/0jsmw5retzcecxhpgz2e6pnzw9qy98m0frafzjuwn324w4yf8xdq0h0cmw/k6kekcfcar4kn8rx98r8ape2wnnwzrxesgashcqkud325gtgmztf7jfp3nqmhld5r8trwpxtx2lwpcuz4ddrdt5vh7up75p5ule7xdvfpeq982cgnqc8rmn96qrqsm88cnvh3d4z2t6xf8lqz3d94pz9wk426un6f7gudmw8lu0n5mmxpe5zpcgaw", + "ur:bytes/7of7/0jsmw5retzcecxhpgz2e6pnzw9qy98m0frafzjuwn324w4yf8xdq0h0cmw/eafht5w0f8yy33pdc68se6tj8c0azgy3jqulufwr6wm2fkgx2us753zu4c7zzlzaekg8w7rn3pjwr5vg6q2k7fw88zxf0czn37a3s00qwaf7h49t74he9xkwr9dalfww0hyn9hen2wf5q7sq4d60w8hqcer7y5k0hqa46jaeg56hk92xd0vz4", + }); + + Assert.assertEquals(random, TestUtils.bytesToHex(LegacyURDecoder.decode(result).toBytes())); + Arrays.stream(result).forEach(part -> Assert.assertTrue(LegacyURDecoder.isLegacyURFragment(part))); + } + + @Test + public void testLegacyEncodeShort() throws Exception { + String random = "7e4a61385e2550981b4b5633ab178eb077a30505fb"; + + UR ur = UR.fromBytes(TestUtils.hexToBytes(random)); + LegacyUREncoder legacyUREncoder = new LegacyUREncoder(ur); + String[] result = legacyUREncoder.encode(); + + Assert.assertArrayEquals(result, new String[] { + "ur:bytes/24ly5cfctcj4pxqmfdtr82ch36c80gc9qhasmxdxs3" + }); + Assert.assertTrue(LegacyURDecoder.isLegacyURFragment(result[0])); + } + + @Test + public void testLegacyDecode() throws Exception{ + String[] fragments = new String[]{ + "ur:bytes/1of7/0jsmw5retzcecxhpgz2e6pnzw9qy98m0frafzjuwn324w4yf8xdq0h0cmw/typjqlj2vyu9uf2snqd5k43n4vtcavrh5vzst7748ug8asggre70pj3uphqtl6jm30a4umlujxhazpxr4f6kyy94m0z3rr739jr7uppxnq2m565edzsdp5ah4xmrzwp2x678p2mzd4t8pd953luy8axe59trr2n8c740ptrvul3mlupm9jty8cehter5j0zwp7rr2v5a", + "ur:bytes/2of7/0jsmw5retzcecxhpgz2e6pnzw9qy98m0frafzjuwn324w4yf8xdq0h0cmw/vm77csd3pnn5mjljtlq4mq570qcvxfty82v9v86yrdq2qt5r2dynu6huzcvjl6vajrvv5e2nntmhmh4vejy58gm4vw5m4qm8t02afknuvry6zuk0d9qvhu8v3lsyzadx9xfjudgjchf2463uegeydaq2y8lacv7rnp7u0wyqx5frp6eht8lrclw8ktf6yz54n9hlpdaq", + "ur:bytes/3of7/0jsmw5retzcecxhpgz2e6pnzw9qy98m0frafzjuwn324w4yf8xdq0h0cmw/mw5rf7ttadjvzn35ymas2x5ndwjp26dtn8qqv6ndnsrh0fy7f8nvhtfy6u32f376zyjryeujvju6ms9gelua68lqa60wyarldf59xlpcnfes8gd0y0znfmnrj27p0vzv7rauua5fue4kwwjypsz2j32qqkcvwenyvwg3x3vwklgfqthlqng3zwxw928wz65u6lyfyeg5", + "ur:bytes/4of7/0jsmw5retzcecxhpgz2e6pnzw9qy98m0frafzjuwn324w4yf8xdq0h0cmw/a75mmqaw0fxp8xp47rlq76xx9rsxghy9ynpsmlp3f6p9574pxgjdnr3002w3yxp6nxdmru59f8ye4yrjmxwqtsjwpjzgfrz0c9r6p99t0d57njl2s62jln8325q0hv35llnwumnda4g4eqqkevqhhgm0hyc77fmva38dytq6a52ft5kl8v7wvmqr7kull2zrf0cw37c5", + "ur:bytes/5of7/0jsmw5retzcecxhpgz2e6pnzw9qy98m0frafzjuwn324w4yf8xdq0h0cmw/nh55upgt8ksh3hclwmqq5dnvk2qpl27lrg0fpnfu630vk75npfqtz529tamtwfk42te3cgfjfxfd5ftllz779y3al4ws66u8yvl6ug2llt97ektzfyyeul35yl2n8k6kekcfcar4kn8rx98r8ape2wnnwzrxesgashcqkud325gtgmztf7jfp3nqmhld5r8trwpxtx2l", + "ur:bytes/6of7/0jsmw5retzcecxhpgz2e6pnzw9qy98m0frafzjuwn324w4yf8xdq0h0cmw/wpcuz4ddrdt5vh7up75p5ule7xdvfpeq982cgnqc8rmn96qrqsm88cnvh3d4z2t6xf8lqz3d94pz9wk426un6f7gudmw8lu0n5mmxpe5zpcgaweafht5w0f8yy33pdc68se6tj8c0azgy3jqulufwr6wm2fkgx2us753zu4c7zzlzaekg8w7rn3pjwr5vg6q2k7fw88z", + "ur:bytes/7of7/0jsmw5retzcecxhpgz2e6pnzw9qy98m0frafzjuwn324w4yf8xdq0h0cmw/xf0czn37a3s00qwaf7h49t74he9xkwr9dalfww0hyn9hen2wf5q7sq4d60w8hqcer7y5k0hqa46jaeg56hk92xd0vz4", + }; + + UR ur = LegacyURDecoder.decode(fragments); + Assert.assertEquals(TestUtils.bytesToHex(ur.toBytes()), "7e4a61385e2550981b4b5633ab178eb077a30505fbd53f107ec1081e7cf0ca3c0dc0bfea5b8bfb5e6ffc91afd104c3aa756210b5dbc5118fd12c87ee04269815ba6a9968a0d0d3b7a9b631382a36bc70ab626d5670b4b48ff843f4d9a15631aa67c7aaf0ac6ce7e3bff03b2c9643e3375e47493c4e0f8635329d66fdec41b10ce74dcbf25fc15d829e7830c325643a98561f441b40a02e8353493e6afc16192fe99d90d8ca65539af77ddeaccc8943a37563a9ba83675bd5d4da7c60c9a172cf6940cbf0ec8fe04175a629932e3512c5d2aaea3cca3246f40a21ffdc33c3987dc7b880351230eb3759fe3c7dc7b2d3a20a95996ff0b7a0dba834f96beb64c14e3426fb051a936ba41569ab99c0066a6d9c0777a49e49e6cbad24d722a4c7da112432679264b9adc0a8cff9dd1fe0ee9ee2747f6a68537c389a7303a1af23c534ee6392bc17b04cf0fbce7689e66b673a440c04a9454005b0c76664639113458eb7d0902eff04d11138ce2a8ee16a9cd7c8926514efa9bd83ae7a4c139835f0fe0f68c628e0645c8524c30dfc314e825a7aa13224d98e2f7a9d12183a999bb1f28549c99a9072d99c05c24e0c84848c4fc147a094ab7b69e9cbea86952fccf15500fbb234ffe6ee6e6ded515c8016cb017ba36fb931ef276cec4ed22c1aed1495d2df3b3ce66c03f5b9ffa8434bf0e8fb149de94e050b3da178df1f76c00a366cb2801fabdf1a1e90cd3cd45ecb7a930a40b151455f76b726d552f31c21324992da257ff8bde2923dfd5d0d6b87233fae215ffacbecd96249099e7e3427d533db56cdb09c7475b4ce3314e33f43953a7370866cc11d85f00b71b15510b46c4b4fa490c660ddfeda0ceb1b8265995f7071c155ad1b57465fdc0fa81a73f9f19ac4872029d5844c1838f732e803043673e26cbc5b51297a324ff00a2d2d4222bad556b93d27c8e376e3ff8f9d37b3073410708ebb3d4dd7473d27212310b71a3c33a5c8f87f44824640e7f8970f4eda9364195c87a91172b8f085f1773641dde1ce21938746234055bc971ce2325f814e3eec60f781dd4faf52afd5be4a6b38656f7e9739f724cb7ccd4e4d01e802add3dc7b83191f894b3ee0ed752ee514d5ec55"); + Arrays.stream(fragments).forEach(part -> Assert.assertTrue(LegacyURDecoder.isLegacyURFragment(part))); + } + + @Test + public void testLegacyDecodeParts() throws Exception{ + String[] fragments = new String[]{ + "ur:bytes/1of7/0jsmw5retzcecxhpgz2e6pnzw9qy98m0frafzjuwn324w4yf8xdq0h0cmw/typjqlj2vyu9uf2snqd5k43n4vtcavrh5vzst7748ug8asggre70pj3uphqtl6jm30a4umlujxhazpxr4f6kyy94m0z3rr739jr7uppxnq2m565edzsdp5ah4xmrzwp2x678p2mzd4t8pd953luy8axe59trr2n8c740ptrvul3mlupm9jty8cehter5j0zwp7rr2v5a", + "ur:bytes/2of7/0jsmw5retzcecxhpgz2e6pnzw9qy98m0frafzjuwn324w4yf8xdq0h0cmw/vm77csd3pnn5mjljtlq4mq570qcvxfty82v9v86yrdq2qt5r2dynu6huzcvjl6vajrvv5e2nntmhmh4vejy58gm4vw5m4qm8t02afknuvry6zuk0d9qvhu8v3lsyzadx9xfjudgjchf2463uegeydaq2y8lacv7rnp7u0wyqx5frp6eht8lrclw8ktf6yz54n9hlpdaq", + "ur:bytes/3of7/0jsmw5retzcecxhpgz2e6pnzw9qy98m0frafzjuwn324w4yf8xdq0h0cmw/mw5rf7ttadjvzn35ymas2x5ndwjp26dtn8qqv6ndnsrh0fy7f8nvhtfy6u32f376zyjryeujvju6ms9gelua68lqa60wyarldf59xlpcnfes8gd0y0znfmnrj27p0vzv7rauua5fue4kwwjypsz2j32qqkcvwenyvwg3x3vwklgfqthlqng3zwxw928wz65u6lyfyeg5", + "ur:bytes/4of7/0jsmw5retzcecxhpgz2e6pnzw9qy98m0frafzjuwn324w4yf8xdq0h0cmw/a75mmqaw0fxp8xp47rlq76xx9rsxghy9ynpsmlp3f6p9574pxgjdnr3002w3yxp6nxdmru59f8ye4yrjmxwqtsjwpjzgfrz0c9r6p99t0d57njl2s62jln8325q0hv35llnwumnda4g4eqqkevqhhgm0hyc77fmva38dytq6a52ft5kl8v7wvmqr7kull2zrf0cw37c5", + "ur:bytes/5of7/0jsmw5retzcecxhpgz2e6pnzw9qy98m0frafzjuwn324w4yf8xdq0h0cmw/nh55upgt8ksh3hclwmqq5dnvk2qpl27lrg0fpnfu630vk75npfqtz529tamtwfk42te3cgfjfxfd5ftllz779y3al4ws66u8yvl6ug2llt97ektzfyyeul35yl2n8k6kekcfcar4kn8rx98r8ape2wnnwzrxesgashcqkud325gtgmztf7jfp3nqmhld5r8trwpxtx2l", + "ur:bytes/6of7/0jsmw5retzcecxhpgz2e6pnzw9qy98m0frafzjuwn324w4yf8xdq0h0cmw/wpcuz4ddrdt5vh7up75p5ule7xdvfpeq982cgnqc8rmn96qrqsm88cnvh3d4z2t6xf8lqz3d94pz9wk426un6f7gudmw8lu0n5mmxpe5zpcgaweafht5w0f8yy33pdc68se6tj8c0azgy3jqulufwr6wm2fkgx2us753zu4c7zzlzaekg8w7rn3pjwr5vg6q2k7fw88z", + "ur:bytes/7of7/0jsmw5retzcecxhpgz2e6pnzw9qy98m0frafzjuwn324w4yf8xdq0h0cmw/xf0czn37a3s00qwaf7h49t74he9xkwr9dalfww0hyn9hen2wf5q7sq4d60w8hqcer7y5k0hqa46jaeg56hk92xd0vz4", + }; + + LegacyURDecoder legacyURDecoder = new LegacyURDecoder(); + + for(Iterator iter = Arrays.stream(fragments).iterator(); iter.hasNext(); ) { + String fragment = iter.next(); + Assert.assertTrue(LegacyURDecoder.isLegacyURFragment(fragment)); + legacyURDecoder.receivePart(fragment); + Assert.assertEquals(iter.hasNext(), !legacyURDecoder.isComplete()); + } + + UR ur = legacyURDecoder.decode(); + Assert.assertEquals(TestUtils.bytesToHex(ur.toBytes()), "7e4a61385e2550981b4b5633ab178eb077a30505fbd53f107ec1081e7cf0ca3c0dc0bfea5b8bfb5e6ffc91afd104c3aa756210b5dbc5118fd12c87ee04269815ba6a9968a0d0d3b7a9b631382a36bc70ab626d5670b4b48ff843f4d9a15631aa67c7aaf0ac6ce7e3bff03b2c9643e3375e47493c4e0f8635329d66fdec41b10ce74dcbf25fc15d829e7830c325643a98561f441b40a02e8353493e6afc16192fe99d90d8ca65539af77ddeaccc8943a37563a9ba83675bd5d4da7c60c9a172cf6940cbf0ec8fe04175a629932e3512c5d2aaea3cca3246f40a21ffdc33c3987dc7b880351230eb3759fe3c7dc7b2d3a20a95996ff0b7a0dba834f96beb64c14e3426fb051a936ba41569ab99c0066a6d9c0777a49e49e6cbad24d722a4c7da112432679264b9adc0a8cff9dd1fe0ee9ee2747f6a68537c389a7303a1af23c534ee6392bc17b04cf0fbce7689e66b673a440c04a9454005b0c76664639113458eb7d0902eff04d11138ce2a8ee16a9cd7c8926514efa9bd83ae7a4c139835f0fe0f68c628e0645c8524c30dfc314e825a7aa13224d98e2f7a9d12183a999bb1f28549c99a9072d99c05c24e0c84848c4fc147a094ab7b69e9cbea86952fccf15500fbb234ffe6ee6e6ded515c8016cb017ba36fb931ef276cec4ed22c1aed1495d2df3b3ce66c03f5b9ffa8434bf0e8fb149de94e050b3da178df1f76c00a366cb2801fabdf1a1e90cd3cd45ecb7a930a40b151455f76b726d552f31c21324992da257ff8bde2923dfd5d0d6b87233fae215ffacbecd96249099e7e3427d533db56cdb09c7475b4ce3314e33f43953a7370866cc11d85f00b71b15510b46c4b4fa490c660ddfeda0ceb1b8265995f7071c155ad1b57465fdc0fa81a73f9f19ac4872029d5844c1838f732e803043673e26cbc5b51297a324ff00a2d2d4222bad556b93d27c8e376e3ff8f9d37b3073410708ebb3d4dd7473d27212310b71a3c33a5c8f87f44824640e7f8970f4eda9364195c87a91172b8f085f1773641dde1ce21938746234055bc971ce2325f814e3eec60f781dd4faf52afd5be4a6b38656f7e9739f724cb7ccd4e4d01e802add3dc7b83191f894b3ee0ed752ee514d5ec55"); + } + + @Test + public void testLegacyDecodeShort() throws Exception { + String[] fragments = new String[]{ + "ur:bytes/24ly5cfctcj4pxqmfdtr82ch36c80gc9qhasmxdxs3" + }; + UR ur = LegacyURDecoder.decode(fragments); + Assert.assertEquals(TestUtils.bytesToHex(ur.toBytes()), "7e4a61385e2550981b4b5633ab178eb077a30505fb"); + Assert.assertTrue(LegacyURDecoder.isLegacyURFragment(fragments[0])); + } + + @Test + public void testDecodeShortUR1() throws Exception { + String[] fragments = new String[]{ + "UR:BYTES/1OF2/EFE6JTF4UM5NW43JVL95KX7NYQHX5V9HYGAAEQWN620XHSQ3ZNMS0PF2GR/TZAHQUMZWNLSZQZJQGQQQQQP4K6PXJYRYULEQDCUXER58CVPDHNSN80N39WME90TE5VMAWPJQRKQQQQQQQQ0LLLLLUQ7SQCQQQQQQQQQZCQPG4MKDDMGDJNQU53PZXVKD007R505KCSCZQQQQQQQQQGPRAVPKQQQQQQQQQQKQQ29MGDUNFESKL5AYZ03TTLECZT0DW7C".toLowerCase(), + "UR:BYTES/2OF2/EFE6JTF4UM5NW43JVL95KX7NYQHX5V9HYGAAEQWN620XHSQ3ZNMS0PF2GR/N5NZYPSREN29X2CN2RSYEWHLJYZKHKCGH5U80D8UHRXHP2HD553EECGJ23A3SQQQQQQ9GQQQSQQQQQYQQQQQPQQQQQQQQPSQQQQQQQQ0YTU7N".toLowerCase() + }; + UR ur = LegacyURDecoder.decode(fragments); + Assert.assertEquals(TestUtils.bytesToHex(ur.toBytes()), "70736274ff0100520200000001adb4134883273f90371c364743e1816de7099df3895dbc95ebcd19beb83200ec0000000000ffffffff01e80300000000000016001457766b7686ca60e5221119966bdfe1d1f4b62181000000000001011f581b0000000000001600145da1bc9a730b7e9d209f15aff9c096f6bbd89d26220603ccd4532b1350e04cbaff91056bdb08bd3877b4fcb8cd70aaeda5239ce112547b180000000054000080000000800000008000000000060000000000"); + Arrays.stream(fragments).forEach(part -> Assert.assertTrue(LegacyURDecoder.isLegacyURFragment(part))); + } }