Skip to content

Commit 093d7ee

Browse files
authored
Merge pull request #69 from swiyu-admin-ch/feat/base58-encoder-decoder-implementation
Base58 encoder/decode implemented (to replace com.github.multiformats:java-multibase:x.y.z)
2 parents 7974901 + aeac21e commit 093d7ee

10 files changed

Lines changed: 243 additions & 28 deletions

File tree

THIRD-PARTY-LICENSES.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ This is the list of all third-party dependencies grouped by their license type.
88
## Apache License, Version 2.0:
99

1010
* **Jackson-annotations** (com.fasterxml.jackson.core:jackson-annotations:2.21 - https://github.com/FasterXML/jackson)
11-
* **Jackson-core** (com.fasterxml.jackson.core:jackson-core:2.21.1 - https://github.com/FasterXML/jackson-core)
12-
* **jackson-databind** (com.fasterxml.jackson.core:jackson-databind:2.21.1 - https://github.com/FasterXML/jackson)
11+
* **Jackson-core** (com.fasterxml.jackson.core:jackson-core:2.21.2 - https://github.com/FasterXML/jackson-core)
12+
* **jackson-databind** (com.fasterxml.jackson.core:jackson-databind:2.21.2 - https://github.com/FasterXML/jackson)
1313
* **FindBugs-jsr305** (com.google.code.findbugs:jsr305:3.0.2 - http://findbugs.sourceforge.net/)
1414
* **Gson** (com.google.code.gson:gson:2.13.2 - https://github.com/google/gson)
1515
* **Tink Cryptography API** (com.google.crypto.tink:tink:1.20.0 - http://github.com/tink-crypto/tink-java)
@@ -49,6 +49,5 @@ This is the list of all third-party dependencies grouped by their license type.
4949

5050
## MIT License:
5151

52-
* **multibase** (com.github.multiformats:java-multibase:1.3.0 - https://github.com/multiformats/java-multibase)
53-
* **DID Resolver** (ch.admin.swiyu:didresolver:2.6.0 - https://github.com/swiyu-admin-ch/didresolver-kotlin)
52+
* **DID Resolver** (ch.admin.swiyu:didresolver:2.7.0 - https://github.com/swiyu-admin-ch/didresolver-kotlin)
5453
* **Project Lombok** (org.projectlombok:lombok:1.18.44 - https://projectlombok.org)

pom.xml

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,9 @@
6262
<!--project.dependencies.directory>lib/</project.dependencies.directory-->
6363
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
6464

65-
<didresolver.version>2.6.0</didresolver.version>
65+
<didresolver.version>2.7.0</didresolver.version>
6666
<jna.version>5.18.1</jna.version>
6767
<gson.version>2.13.2</gson.version>
68-
<java-multibase.version>1.3.0</java-multibase.version>
6968
<lombok.version>1.18.44</lombok.version>
7069
<jcommander.version>3.0</jcommander.version>
7170
<bouncycastle.version>1.83</bouncycastle.version>
@@ -505,15 +504,6 @@
505504
</snapshotRepository>
506505
</distributionManagement-->
507506

508-
<repositories>
509-
<!-- required for https://github.com/multiformats/java-multibase
510-
as described by https://jitpack.io/#multiformats/java-multibase/ -->
511-
<repository>
512-
<id>jitpack.io</id>
513-
<url>https://jitpack.io</url>
514-
</repository>
515-
</repositories>
516-
517507
<dependencies>
518508
<dependency>
519509
<!-- CAUTION Until 2.0.1 (GitHub packages), the "groupId" was set to "ch.admin.eid".
@@ -535,12 +525,6 @@
535525
<artifactId>gson</artifactId>
536526
<version>${gson.version}</version>
537527
</dependency>
538-
<!-- https://mvnrepository.com/artifact/com.github.multiformats/java-multibase -->
539-
<dependency>
540-
<groupId>com.github.multiformats</groupId>
541-
<artifactId>java-multibase</artifactId>
542-
<version>${java-multibase.version}</version>
543-
</dependency>
544528
<!-- https://mvnrepository.com/artifact/org.bouncycastle/bcprov-jdk18on -->
545529
<!-- https://www.bouncycastle.org/download/bouncy-castle-java/?filter=java%3Drelease-1-81 -->
546530
<dependency>
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
package ch.admin.bj.swiyu.didtoolbox;
2+
3+
import java.util.Arrays;
4+
5+
/**
6+
* Yet another Java implementation of <a href="https://bitcoinwiki.org/wiki/base58#Base58_Encode_and_Decode">base58btc</a> encoder/decoder.
7+
* <p>
8+
* Base58 is a way to encode Bitcoin addresses (or arbitrary data) as alphanumeric strings.
9+
*
10+
* <p>Note that this is different from the base58 as used by Flickr, which you may find referenced around
11+
* the Internet.
12+
*
13+
* <p>Satoshi explains: why base-58 instead of standard base-64 encoding?
14+
*
15+
* <ul>
16+
* <li>Don't want 0OIl characters that look the same in some fonts and could be used to create
17+
* visually identical looking account numbers.
18+
* <li>A string with non-alphanumeric characters is not as easily accepted as an account number.
19+
* <li>E-mail usually won't line-break if there's no punctuation to break at.
20+
* <li>Doubleclicking selects the whole number as one word if it's all alphanumeric.
21+
* </ul>
22+
*
23+
* <p>However, note that the encoding/decoding runs in O(n&sup2;) time, so it is not useful for
24+
* large data.
25+
*
26+
* <p>The basic idea of the encoding is to treat the data bytes as a large number represented using
27+
* base-256 digits, convert the number to be represented using base-58 digits, preserve the exact
28+
* number of leading zeros (which are otherwise lost during the mathematical operations on the
29+
* numbers), and finally represent the resulting base-58 digits as alphanumeric ASCII characters.
30+
*/
31+
public final class Base58 {
32+
33+
public static final char[] ALPHABET =
34+
"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz".toCharArray();
35+
private static final char ENCODED_ZERO = ALPHABET[0];
36+
private static final int[] INDEXES = new int[128];
37+
38+
static {
39+
Arrays.fill(INDEXES, -1);
40+
for (int i = 0; i < ALPHABET.length; i++) {
41+
INDEXES[ALPHABET[i]] = i;
42+
}
43+
}
44+
45+
private Base58() {
46+
}
47+
48+
/**
49+
* Encodes the given bytes as a base58 string (no checksum is appended).
50+
*
51+
* @param input the bytes to encode
52+
* @return the base58-encoded string
53+
*/
54+
public static String encode(byte[] input) {
55+
if (input.length == 0) {
56+
return "";
57+
}
58+
59+
// Count leading zeros.
60+
int zeros = 0;
61+
while (zeros < input.length && input[zeros] == 0) {
62+
++zeros;
63+
}
64+
65+
// Convert base-256 digits to base-58 digits (plus conversion to ASCII characters)
66+
var in = Arrays.copyOf(input, input.length); // since we modify it in-place
67+
char[] encoded = new char[in.length * 2]; // upper bound
68+
int outputStart = encoded.length;
69+
for (int inputStart = zeros; inputStart < in.length; ) {
70+
encoded[--outputStart] = ALPHABET[divmod(in, inputStart, 256, 58)];
71+
if (in[inputStart] == 0) {
72+
++inputStart; // optimization - skip leading zeros
73+
}
74+
}
75+
76+
// Preserve exactly as many leading encoded zeros in output as there were leading zeros in input.
77+
while (outputStart < encoded.length && encoded[outputStart] == ENCODED_ZERO) {
78+
++outputStart;
79+
}
80+
81+
while (--zeros >= 0) {
82+
encoded[--outputStart] = ENCODED_ZERO;
83+
}
84+
85+
// Return encoded string (including encoded leading zeros).
86+
return new String(encoded, outputStart, encoded.length - outputStart);
87+
}
88+
89+
/**
90+
* Decodes the given base58 string into the original data bytes.
91+
*
92+
* @param input the base58-encoded string to decode
93+
* @return the decoded data bytes
94+
*/
95+
@SuppressWarnings({"PMD.CyclomaticComplexity"})
96+
public static byte[] decode(String input) {
97+
if (input.isEmpty()) {
98+
return new byte[0];
99+
}
100+
101+
// Convert the base58-encoded ASCII chars to a base58 byte sequence (base58 digits).
102+
byte[] input58 = new byte[input.length()];
103+
for (int i = 0; i < input.length(); ++i) {
104+
char c = input.charAt(i);
105+
int digit = c < 128 ? INDEXES[c] : -1;
106+
if (digit < 0) {
107+
throw new IllegalArgumentException(
108+
String.format("Invalid character in Base58: 0x%04x", (int) c));
109+
}
110+
input58[i] = (byte) digit;
111+
}
112+
113+
// Count leading zeros.
114+
int zeros = 0;
115+
while (zeros < input58.length && input58[zeros] == 0) {
116+
++zeros;
117+
}
118+
119+
// Convert base-58 digits to base-256 digits.
120+
byte[] decoded = new byte[input.length()];
121+
int outputStart = decoded.length;
122+
for (int inputStart = zeros; inputStart < input58.length; ) {
123+
decoded[--outputStart] = divmod(input58, inputStart, 58, 256);
124+
if (input58[inputStart] == 0) {
125+
++inputStart; // optimization - skip leading zeros
126+
}
127+
}
128+
129+
// Ignore extra leading zeroes that were added during the calculation.
130+
while (outputStart < decoded.length && decoded[outputStart] == 0) {
131+
++outputStart;
132+
}
133+
134+
// Return decoded data (including original number of leading zeros).
135+
return Arrays.copyOfRange(decoded, outputStart - zeros, decoded.length);
136+
}
137+
138+
/**
139+
* Divides a number, represented as an array of bytes each containing a single digit in the
140+
* specified base, by the given divisor. The given number is modified in-place to contain the
141+
* quotient, and the return value is the remainder.
142+
*
143+
* @param number the number to divide
144+
* @param firstDigit the index within the array of the first non-zero digit (this is used for
145+
* optimization by skipping the leading zeros)
146+
* @param base the base in which the number's digits are represented (up to 256)
147+
* @param divisor the number to divide by (up to 256)
148+
* @return the remainder of the division operation
149+
*/
150+
private static byte divmod(byte[] number, int firstDigit, int base, int divisor) {
151+
// this is just long division which accounts for the base of the input digits
152+
int remainder = 0;
153+
for (int i = firstDigit; i < number.length; i++) {
154+
int digit = (int) number[i] & 0xFF;
155+
int temp = remainder * base + digit;
156+
number[i] = (byte) (temp / divisor);
157+
remainder = temp % divisor;
158+
}
159+
return (byte) remainder;
160+
}
161+
}

src/main/java/ch/admin/bj/swiyu/didtoolbox/Ed25519Utils.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
package ch.admin.bj.swiyu.didtoolbox;
22

3-
import io.ipfs.multibase.Base58;
4-
53
import java.math.BigInteger;
64
import java.nio.ByteBuffer;
75
import java.security.*;

src/main/java/ch/admin/bj/swiyu/didtoolbox/Ed25519VerificationMethodKeyProviderImpl.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import com.google.gson.JsonObject;
99
import com.google.gson.JsonParser;
1010
import com.google.gson.JsonSyntaxException;
11-
import io.ipfs.multibase.Base58;
1211
import org.bouncycastle.jce.provider.BouncyCastleProvider;
1312
import org.bouncycastle.util.io.pem.PemObject;
1413
import org.bouncycastle.util.io.pem.PemWriter;

src/main/java/ch/admin/bj/swiyu/didtoolbox/JCSHasher.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import ch.admin.eid.did_sidekicks.DidSidekicksException;
55
import ch.admin.eid.did_sidekicks.JcsSha256Hasher;
66
import com.google.gson.JsonObject;
7-
import io.ipfs.multibase.Base58;
87

98
import java.nio.ByteBuffer;
109
import java.nio.charset.StandardCharsets;

src/main/java/ch/admin/bj/swiyu/didtoolbox/model/NextKeyHashesDidMethodParameter.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
package ch.admin.bj.swiyu.didtoolbox.model;
22

3+
import ch.admin.bj.swiyu.didtoolbox.Base58;
34
import ch.admin.bj.swiyu.didtoolbox.Ed25519Utils;
45
import ch.admin.bj.swiyu.didtoolbox.JCSHasher;
56
import ch.admin.bj.swiyu.didtoolbox.PemUtils;
67
import ch.admin.eid.did_sidekicks.DidSidekicksException;
78
import com.google.gson.JsonArray;
89
import com.google.gson.JsonPrimitive;
9-
import io.ipfs.multibase.Base58;
1010

1111
import java.io.File;
1212
import java.nio.file.Path;

src/main/java/ch/admin/bj/swiyu/didtoolbox/vc_data_integrity/EdDsaJcs2022VcDataIntegrityCryptographicSuite.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package ch.admin.bj.swiyu.didtoolbox.vc_data_integrity;
22

3+
import ch.admin.bj.swiyu.didtoolbox.Base58;
34
import ch.admin.bj.swiyu.didtoolbox.Ed25519Utils;
45
import ch.admin.bj.swiyu.didtoolbox.VerificationMethodKeyProvider;
56
import ch.admin.bj.swiyu.didtoolbox.context.DidLogCreatorContext;
@@ -8,7 +9,6 @@
89
import com.google.gson.JsonObject;
910
import com.google.gson.JsonParser;
1011
import com.google.gson.JsonSyntaxException;
11-
import io.ipfs.multibase.Base58;
1212

1313
import java.io.IOException;
1414
import java.io.InputStream;
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package ch.admin.bj.swiyu.didtoolbox;
2+
3+
import org.junit.jupiter.params.ParameterizedTest;
4+
import org.junit.jupiter.params.provider.MethodSource;
5+
6+
import java.util.Arrays;
7+
import java.util.Collection;
8+
9+
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
10+
import static org.junit.jupiter.api.Assertions.assertEquals;
11+
12+
class Base58Test {
13+
14+
private static Collection<Object[]> data() {
15+
return Arrays.asList(
16+
new Object[][]{
17+
{
18+
hexToBytes("1220120F6AF601D46E10B2D2E11ED71C55D25F3042C22501E41D1246E7A1E9D3D8EC"),
19+
"QmPZ9gcCEpqKTo6aq61g2nXGUhM4iCL3ewB6LDXZCtioEB" // w/out multibase ('z') prefix
20+
},
21+
{
22+
hexToBytes("1220BA8632EF1A07986B171B3C8FAF0F79B3EE01B6C30BBE15A13261AD6CB0D02E3A"),
23+
"QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy" // w/out multibase ('z') prefix
24+
},
25+
{new byte[1], "1"}, // w/out multibase ('z') prefix
26+
{new byte[2], "11"}, // w/out multibase ('z') prefix
27+
{new byte[4], "1111"}, // w/out multibase ('z') prefix
28+
{new byte[8], "11111111"}, // w/out multibase ('z') prefix
29+
{new byte[16], "1111111111111111"}, // w/out multibase ('z') prefix
30+
{new byte[32], "11111111111111111111111111111111"}, // w/out multibase ('z') prefix
31+
{
32+
hexToBytes("446563656e7472616c697a652065766572797468696e67212121"),
33+
"36UQrhJq9fNDS7DiAHM9YXqDHMPfr4EMArvt" // w/out multibase ('z') prefix
34+
},
35+
});
36+
}
37+
38+
@MethodSource("data")
39+
@ParameterizedTest(name = "{index}: {0}, {2}")
40+
void testEncode(byte[] raw, String encoded) {
41+
String output = Base58.encode(raw);
42+
assertEquals(encoded, output, String.format("Expected %s, but got %s", bytesToHex(raw), output));
43+
}
44+
45+
@MethodSource("data")
46+
@ParameterizedTest(name = "{index}: {0}, {2}")
47+
void testDecode(byte[] raw, String encoded) {
48+
byte[] output = Base58.decode(encoded);
49+
assertArrayEquals(
50+
raw, output, String.format("Expected %s, but got %s", bytesToHex(raw), bytesToHex(output)));
51+
}
52+
53+
// Copied from https://stackoverflow.com/a/140861
54+
private static byte[] hexToBytes(String s) {
55+
int len = s.length();
56+
byte[] data = new byte[len / 2];
57+
for (int i = 0; i < len; i += 2) {
58+
data[i / 2] =
59+
(byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i + 1), 16));
60+
}
61+
return data;
62+
}
63+
64+
// Copied from https://stackoverflow.com/a/9855338
65+
private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();
66+
67+
private static String bytesToHex(byte[] bytes) {
68+
char[] hexChars = new char[bytes.length * 2];
69+
for (int j = 0; j < bytes.length; j++) {
70+
int v = bytes[j] & 0xFF;
71+
hexChars[j * 2] = HEX_ARRAY[v >>> 4];
72+
hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
73+
}
74+
return new String(hexChars);
75+
}
76+
}

src/test/java/ch/admin/bj/swiyu/didtoolbox/Ed25519UtilsTest.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package ch.admin.bj.swiyu.didtoolbox;
22

3-
import io.ipfs.multibase.Base58;
43
import org.junit.jupiter.api.DisplayName;
54
import org.junit.jupiter.api.Test;
65
import org.junit.jupiter.params.ParameterizedTest;

0 commit comments

Comments
 (0)