001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * https://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 018package org.apache.commons.codec.binary; 019 020import java.math.BigInteger; 021import java.util.Arrays; 022import java.util.function.BiConsumer; 023 024/** 025 * Provides Base58 encoding and decoding as commonly used in cryptocurrency and blockchain applications. 026 * <p> 027 * Base58 is a binary-to-text encoding scheme that uses a 58-character alphabet to encode data. It avoids characters that can be confused (0/O, I/l, +/) and is 028 * commonly used in Bitcoin and other blockchain systems. 029 * </p> 030 * <p> 031 * This implementation accumulates data internally until EOF is signaled, at which point the entire input is converted using BigInteger arithmetic. This is 032 * necessary because Base58 encoding/decoding requires access to the complete data to properly handle leading zeros. 033 * </p> 034 * <p> 035 * This class is thread-safe for read operations but the Context object used during encoding/decoding should not be shared between threads. 036 * </p> 037 * <p> 038 * The Base58 alphabet is: 039 * </p> 040 * 041 * <pre> 042 * 123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz 043 * </pre> 044 * <p> 045 * This excludes: {@code 0}, {@code I}, {@code O}, and {@code l}. 046 * </p> 047 * 048 * @see Base58InputStream 049 * @see Base58OutputStream 050 * @see <a href="https://datatracker.ietf.org/doc/html/draft-msporny-base58-03">The Base58 Encoding Scheme draft-msporny-base58-03</a> 051 * @since 1.22.0 052 */ 053public class Base58 extends BaseNCodec { 054 055 /** 056 * Builds {@link Base58} instances with custom configuration. 057 */ 058 public static class Builder extends AbstractBuilder<Base58, Builder> { 059 060 /** 061 * Constructs a new Base58 builder. 062 */ 063 public Builder() { 064 super(ENCODE_TABLE); 065 setDecodeTable(DECODE_TABLE); 066 } 067 068 /** 069 * Builds a new Base58 instance with the configured settings. 070 * 071 * @return A new Base58 codec. 072 */ 073 @Override 074 public Base58 get() { 075 return new Base58(this); 076 } 077 078 /** 079 * Sets the encode table and derives the matching decode table. 080 * 081 * @param encodeTable The encode table with exactly 58 unique entries, null resets to the default. 082 * @return {@code this} instance. 083 * @throws IllegalArgumentException if the encode table does not contain exactly 58 unique entries. 084 */ 085 @Override 086 public Base58.Builder setEncodeTable(final byte... encodeTable) { 087 super.setDecodeTableRaw(toDecodeTable(encodeTable)); 088 return super.setEncodeTable(encodeTable); 089 } 090 } 091 private static final BigInteger BASE = BigInteger.valueOf(58); 092 093 private static final int DECODING_TABLE_LENGTH = 256; 094 private static final int ENCODING_TABLE_LENGTH = 58; 095 096 /** 097 * Base58 alphabet: 123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz 098 * (excludes: 0, I, O, l). 099 */ 100 private static final byte[] ENCODE_TABLE = { 101 '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 102 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 103 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'm', 'n', 'o', 'p', 'q', 'r', 's', 104 't', 'u', 'v', 'w', 'x', 'y', 'z' 105 }; 106 /** 107 * This array is a lookup table that translates Unicode characters drawn from the "Base58 Alphabet" 108 * into their numeric equivalents (0-57). Characters that are not in the Base58 alphabet are marked 109 * with -1. 110 */ 111 // @formatter:off 112 private static final byte[] DECODE_TABLE = { 113 // 0 1 2 3 4 5 6 7 8 9 A B C D E F 114 -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 00-0f 115 -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 10-1f 116 -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 20-2f 117 -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, -1, -1, -1, -1, -1, -1, // 30-3f '1'-'9' -> 0-8 118 -1, 9, 10, 11, 12, 13, 14, 15, 16, -1, 17, 18, 19, 20, 21, -1, // 40-4f 'A'-'N', 'P'-'Z' (skip 'I' and 'O') 119 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, // 50-5a 'P'-'Z' 120 -1, -1, -1, -1, -1, // 5b-5f 121 -1, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, -1, 44, 45, 46, // 60-6f 'a'-'k', 'm'-'o' (skip 'l') 122 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, // 70-7a 'p'-'z' 123 }; 124 // @formatter:on 125 126 /** 127 * Creates a new Builder. 128 * 129 * <p> 130 * To configure a new instance, use a {@link Builder}. For example: 131 * </p> 132 * 133 * <pre> 134 * Base58 base58 = Base58.builder() 135 * .setEncode(true) 136 * .get() 137 * </pre> 138 * 139 * @return A new Builder. 140 */ 141 public static Builder builder() { 142 return new Builder(); 143 } 144 145 /** 146 * Calculates a decode table for a given encode table. 147 * 148 * @param encodeTable that is used to determine decode lookup table. 149 * @return A new decode table. 150 * @throws IllegalArgumentException if the encode table does not contain exactly 58 unique entries. 151 */ 152 private static byte[] calculateDecodeTable(final byte[] encodeTable) { 153 if (encodeTable.length != ENCODING_TABLE_LENGTH) { 154 throw new IllegalArgumentException("encodeTable must have exactly 58 entries."); 155 } 156 final byte[] decodeTable = new byte[DECODING_TABLE_LENGTH]; 157 Arrays.fill(decodeTable, (byte) -1); 158 for (int i = 0; i < encodeTable.length; i++) { 159 final int encodedByte = encodeTable[i] & 0xff; 160 if (decodeTable[encodedByte] != -1) { 161 throw new IllegalArgumentException("encodeTable must not contain duplicate entries."); 162 } 163 decodeTable[encodedByte] = (byte) i; 164 } 165 return decodeTable; 166 } 167 168 /** 169 * Gets the decode table that matches the given encode table. 170 * 171 * @param encodeTable that is used to determine decode lookup table. 172 * @return The matching decode table. 173 */ 174 private static byte[] toDecodeTable(final byte[] encodeTable) { 175 final byte[] table = encodeTable != null ? encodeTable : ENCODE_TABLE; 176 if (Arrays.equals(table, ENCODE_TABLE)) { 177 return DECODE_TABLE; 178 } 179 return calculateDecodeTable(table); 180 } 181 182 /** 183 * Constructs a Base58 codec used for encoding and decoding. 184 */ 185 public Base58() { 186 this(new Builder()); 187 } 188 189 /** 190 * Constructs a Base58 codec used for encoding and decoding with custom configuration. 191 * 192 * @param builder The builder with custom configuration. 193 */ 194 public Base58(final Builder builder) { 195 super(builder); 196 } 197 198 private void code(final byte[] array, final int offset, final int length, final Context context, final BiConsumer<byte[], Context> consumer) { 199 if (context.eof) { 200 return; 201 } 202 if (length < 0) { 203 context.eof = true; 204 final byte[] accumulate = context.buffer = context.buffer != null ? context.buffer : EMPTY_BYTE_ARRAY; 205 if (accumulate.length > 0) { 206 consumer.accept(accumulate, context); 207 } 208 return; 209 } 210 final byte[] accumulate = context.buffer = context.buffer != null ? context.buffer : EMPTY_BYTE_ARRAY; 211 final byte[] newAccumulated = new byte[accumulate.length + length]; 212 if (accumulate.length > 0) { 213 System.arraycopy(accumulate, 0, newAccumulated, 0, accumulate.length); 214 } 215 System.arraycopy(array, offset, newAccumulated, accumulate.length, length); 216 context.buffer = newAccumulated; 217 } 218 219 /** 220 * Converts Base58 encoded data to binary. 221 * <p> 222 * Uses BigInteger arithmetic to convert the Base58 string to binary data. Leading characters that match the first Base58 alphabet entry represent leading 223 * zero bytes in the binary data. 224 * </p> 225 * 226 * @param base58 The Base58 encoded data. 227 * @param context The context for this decoding operation. 228 * @throws IllegalArgumentException if the Base58 data contains invalid characters. 229 */ 230 private void convertFromBase58(final byte[] base58, final Context context) { 231 BigInteger value = BigInteger.ZERO; 232 int leadingZeros = 0; 233 final int zero = encodeTable[0] & 0xff; 234 for (final byte b : base58) { 235 if ((b & 0xff) != zero) { 236 break; 237 } 238 leadingZeros++; 239 } 240 BigInteger power = BigInteger.ONE; 241 for (int i = base58.length - 1; i >= leadingZeros; i--) { 242 final int b = base58[i] & 0xff; 243 final int digit = b < decodeTable.length ? decodeTable[b] : -1; 244 if (digit < 0) { 245 throw new IllegalArgumentException(String.format("Invalid character in Base58 string: 0x%02x", b)); 246 } 247 value = value.add(BigInteger.valueOf(digit).multiply(power)); 248 power = power.multiply(BASE); 249 } 250 final byte[] decoded = toUnsignedBytes(value); 251 final byte[] result = new byte[leadingZeros + decoded.length]; 252 System.arraycopy(decoded, 0, result, leadingZeros, decoded.length); 253 final byte[] buffer = ensureBufferSize(result.length, context); 254 System.arraycopy(result, 0, buffer, context.pos, result.length); 255 context.pos += result.length; 256 } 257 258 /** 259 * Converts accumulated binary data to Base58 encoding. 260 * <p> 261 * Uses BigInteger arithmetic to convert the binary data to Base58. Leading zeros in the binary data are represented as the first character in the Base58 262 * alphabet. 263 * </p> 264 * 265 * @param accumulate The binary data to encode. 266 * @param context The context for this encoding operation. 267 * @return The buffer containing the encoded data. 268 */ 269 private byte[] convertToBase58(final byte[] accumulate, final Context context) { 270 final StringBuilder base58 = getStringBuilder(accumulate); 271 final byte[] encodedBytes = new byte[base58.length()]; 272 for (int i = 0; i < encodedBytes.length; i++) { 273 encodedBytes[i] = (byte) base58.charAt(encodedBytes.length - 1 - i); 274 } 275 final byte[] buffer = ensureBufferSize(encodedBytes.length, context); 276 System.arraycopy(encodedBytes, 0, buffer, context.pos, encodedBytes.length); 277 context.pos += encodedBytes.length; 278 return buffer; 279 } 280 281 /** 282 * Decodes the given Base58 encoded data. 283 * <p> 284 * This implementation accumulates data internally. When length is less than 0 (EOF), the accumulated data is converted from Base58 to binary. 285 * </p> 286 * 287 * @param array The byte array containing Base58 encoded data. 288 * @param offset The offset in the array to start from. 289 * @param length The number of bytes to decode, or negative to signal EOF. 290 * @param context The context for this decoding operation. 291 */ 292 @Override 293 void decode(final byte[] array, final int offset, final int length, final Context context) { 294 code(array, offset, length, context, this::convertFromBase58); 295 } 296 297 /** 298 * Encodes the given binary data as Base58. 299 * <p> 300 * This implementation accumulates data internally. When length is less than 0 (EOF), the accumulated data is converted to Base58. 301 * </p> 302 * 303 * @param array The byte array containing binary data to encode. 304 * @param offset The offset in the array to start from. 305 * @param length The number of bytes to encode, or negative to signal EOF. 306 * @param context The context for this encoding operation. 307 */ 308 @Override 309 void encode(final byte[] array, final int offset, final int length, final Context context) { 310 code(array, offset, length, context, this::convertToBase58); 311 } 312 313 /** 314 * Builds the Base58 string representation of the given binary data. 315 * <p> 316 * Converts binary data to a BigInteger and divides by 58 repeatedly to get the Base58 digits. Handles leading zeros by counting them and appending the first 317 * character in the Base58 alphabet for each leading zero byte. 318 * </p> 319 * 320 * @param accumulate The binary data to convert. 321 * @return A StringBuilder with the Base58 representation (not yet reversed). 322 */ 323 private StringBuilder getStringBuilder(final byte[] accumulate) { 324 BigInteger value = new BigInteger(1, accumulate); 325 int leadingZeros = 0; 326 for (final byte b : accumulate) { 327 if (b != 0) { 328 break; 329 } 330 leadingZeros++; 331 } 332 final StringBuilder base58 = new StringBuilder(); 333 while (value.signum() > 0) { 334 final BigInteger[] divRem = value.divideAndRemainder(BASE); 335 base58.append((char) (encodeTable[divRem[1].intValue()] & 0xff)); 336 value = divRem[0]; 337 } 338 final char zero = (char) (encodeTable[0] & 0xff); 339 for (int i = 0; i < leadingZeros; i++) { 340 base58.append(zero); 341 } 342 return base58; 343 } 344 345 /** 346 * Returns whether or not the {@code octet} is in the Base58 alphabet. 347 * 348 * @param value The value to test. 349 * @return {@code true} if the value is defined in the Base58 alphabet {@code false} otherwise. 350 */ 351 @Override 352 protected boolean isInAlphabet(final byte value) { 353 final int octet = value & 0xff; 354 return octet < decodeTable.length && decodeTable[octet] != -1; 355 } 356}