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.nio.charset.StandardCharsets; 022 023/** 024 * Provides Base58 encoding and decoding as commonly used in cryptocurrency and blockchain applications. 025 * <p> 026 * 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 027 * commonly used in Bitcoin and other blockchain systems. 028 * </p> 029 * <p> 030 * This implementation accumulates data internally until EOF is signaled, at which point the entire input is converted using BigInteger arithmetic. This is 031 * necessary because Base58 encoding/decoding requires access to the complete data to properly handle leading zeros. 032 * </p> 033 * <p> 034 * This class is thread-safe for read operations but the Context object used during encoding/decoding should not be shared between threads. 035 * </p> 036 * <p> 037 * The Base58 alphabet is: 038 * </p> 039 * 040 * <pre> 041 * 123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz 042 * </pre> 043 * <p> 044 * This excludes: {@code 0}, {@code I}, {@code O}, and {@code l}. 045 * </p> 046 * 047 * @see Base58InputStream 048 * @see Base58OutputStream 049 * @see <a href="https://datatracker.ietf.org/doc/html/draft-msporny-base58-03">The Base58 Encoding Scheme draft-msporny-base58-03</a> 050 * @since 1.22.0 051 */ 052public class Base58 extends BaseNCodec { 053 054 /** 055 * Builds {@link Base58} instances with custom configuration. 056 */ 057 public static class Builder extends AbstractBuilder<Base58, Builder> { 058 059 /** 060 * Constructs a new Base58 builder. 061 */ 062 public Builder() { 063 super(ENCODE_TABLE); 064 setDecodeTable(DECODE_TABLE); 065 } 066 067 /** 068 * Builds a new Base58 instance with the configured settings. 069 * 070 * @return a new Base58 codec. 071 */ 072 @Override 073 public Base58 get() { 074 return new Base58(this); 075 } 076 077 /** 078 * Creates a new Base58 codec instance. 079 * 080 * @return a new Base58 codec. 081 */ 082 @Override 083 public Base58.Builder setEncodeTable(final byte... encodeTable) { 084 super.setDecodeTableRaw(DECODE_TABLE); 085 return super.setEncodeTable(encodeTable); 086 } 087 } 088 private static final BigInteger BASE = BigInteger.valueOf(58); 089 090 private static final byte[] EMPTY = new byte[0]; 091 092 /** 093 * Base58 alphabet: 123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz 094 * (excludes: 0, I, O, l). 095 */ 096 private static final byte[] ENCODE_TABLE = { 097 '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 098 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 099 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'm', 'n', 'o', 'p', 'q', 'r', 's', 100 't', 'u', 'v', 'w', 'x', 'y', 'z' 101 }; 102 /** 103 * This array is a lookup table that translates Unicode characters drawn from the "Base58 Alphabet" 104 * into their numeric equivalents (0-57). Characters that are not in the Base58 alphabet are marked 105 * with -1. 106 */ 107 // @formatter:off 108 private static final byte[] DECODE_TABLE = { 109 // 0 1 2 3 4 5 6 7 8 9 A B C D E F 110 -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 00-0f 111 -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 10-1f 112 -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 20-2f 113 -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, -1, -1, -1, -1, -1, -1, // 30-3f '1'-'9' -> 0-8 114 -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') 115 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, // 50-5a 'P'-'Z' 116 -1, -1, -1, -1, -1, // 5b-5f 117 -1, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, -1, 44, 45, 46, // 60-6f 'a'-'k', 'm'-'o' (skip 'l') 118 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, // 70-7a 'p'-'z' 119 }; 120 // @formatter:on 121 122 /** 123 * Creates a new Builder. 124 * 125 * <p> 126 * To configure a new instance, use a {@link Builder}. For example: 127 * </p> 128 * 129 * <pre> 130 * Base58 base58 = Base58.builder() 131 * .setEncode(true) 132 * .get() 133 * </pre> 134 * 135 * @return a new Builder. 136 */ 137 public static Builder builder() { 138 return new Builder(); 139 } 140 141 /** 142 * Constructs a Base58 codec used for encoding and decoding. 143 */ 144 public Base58() { 145 this(new Builder()); 146 } 147 148 /** 149 * Constructs a Base58 codec used for encoding and decoding with custom configuration. 150 * 151 * @param builder the builder with custom configuration. 152 */ 153 public Base58(final Builder builder) { 154 super(builder); 155 } 156 157 /** 158 * Converts Base58 encoded data to binary. 159 * <p> 160 * Uses BigInteger arithmetic to convert the Base58 string to binary data. Leading '1' characters in the Base58 encoding represent leading zero bytes in the 161 * binary data. 162 * </p> 163 * 164 * @param base58 the Base58 encoded data. 165 * @param context the context for this decoding operation. 166 * @throws IllegalArgumentException if the Base58 data contains invalid characters. 167 */ 168 private void convertFromBase58(final byte[] base58, final Context context) { 169 BigInteger value = BigInteger.ZERO; 170 int leadingOnes = 0; 171 for (final byte b : base58) { 172 if (b != '1') { 173 break; 174 } 175 leadingOnes++; 176 } 177 BigInteger power = BigInteger.ONE; 178 for (int i = base58.length - 1; i >= leadingOnes; i--) { 179 final byte b = base58[i]; 180 final int digit = b < DECODE_TABLE.length ? DECODE_TABLE[b] : -1; 181 if (digit < 0) { 182 throw new IllegalArgumentException(String.format("Invalid character in Base58 string: 0x%02x", b)); 183 } 184 value = value.add(BigInteger.valueOf(digit).multiply(power)); 185 power = power.multiply(BASE); 186 } 187 byte[] decoded = value.equals(BigInteger.ZERO) ? EMPTY : value.toByteArray(); 188 if (decoded.length > 1 && decoded[0] == 0) { 189 final byte[] tmp = new byte[decoded.length - 1]; 190 System.arraycopy(decoded, 1, tmp, 0, tmp.length); 191 decoded = tmp; 192 } 193 final byte[] result = new byte[leadingOnes + decoded.length]; 194 System.arraycopy(decoded, 0, result, leadingOnes, decoded.length); 195 final byte[] buffer = ensureBufferSize(result.length, context); 196 System.arraycopy(result, 0, buffer, context.pos, result.length); 197 context.pos += result.length; 198 } 199 200 /** 201 * Converts accumulated binary data to Base58 encoding. 202 * <p> 203 * Uses BigInteger arithmetic to convert the binary data to Base58. Leading zeros in the binary data are represented as '1' characters in the Base58 204 * encoding. 205 * </p> 206 * 207 * @param accumulate the binary data to encode. 208 * @param context the context for this encoding operation. 209 * @return the buffer containing the encoded data. 210 */ 211 private byte[] convertToBase58(final byte[] accumulate, final Context context) { 212 final StringBuilder base58 = getStringBuilder(accumulate); 213 final String encoded = base58.reverse().toString(); 214 final byte[] encodedBytes = encoded.getBytes(StandardCharsets.UTF_8); 215 final byte[] buffer = ensureBufferSize(encodedBytes.length, context); 216 System.arraycopy(encodedBytes, 0, buffer, context.pos, encodedBytes.length); 217 context.pos += encodedBytes.length; 218 return buffer; 219 } 220 221 /** 222 * Decodes the given Base58 encoded data. 223 * <p> 224 * This implementation accumulates data internally. When length is less than 0 (EOF), the accumulated data is converted from Base58 to binary. 225 * </p> 226 * 227 * @param array the byte array containing Base58 encoded data. 228 * @param offset the offset in the array to start from. 229 * @param length the number of bytes to decode, or negative to signal EOF. 230 * @param context the context for this decoding operation. 231 */ 232 @Override 233 void decode(final byte[] array, final int offset, final int length, final Context context) { 234 if (context.eof) { 235 return; 236 } 237 if (length < 0) { 238 context.eof = true; 239 final byte[] accumulate = context.buffer = context.buffer != null ? context.buffer : EMPTY; 240 if (accumulate.length > 0) { 241 convertFromBase58(accumulate, context); 242 } 243 return; 244 } 245 final byte[] accumulate = context.buffer = context.buffer != null ? context.buffer : EMPTY; 246 final byte[] newAccumulated = new byte[accumulate.length + length]; 247 if (accumulate.length > 0) { 248 System.arraycopy(accumulate, 0, newAccumulated, 0, accumulate.length); 249 } 250 System.arraycopy(array, offset, newAccumulated, accumulate.length, length); 251 context.buffer = newAccumulated; 252 } 253 254 /** 255 * Encodes the given binary data as Base58. 256 * <p> 257 * This implementation accumulates data internally. When length is less than 0 (EOF), the accumulated data is converted to Base58. 258 * </p> 259 * 260 * @param array the byte array containing binary data to encode. 261 * @param offset the offset in the array to start from. 262 * @param length the number of bytes to encode, or negative to signal EOF. 263 * @param context the context for this encoding operation. 264 */ 265 @Override 266 void encode(final byte[] array, final int offset, final int length, final Context context) { 267 if (context.eof) { 268 return; 269 } 270 if (length < 0) { 271 context.eof = true; 272 final byte[] accumulate = context.buffer = context.buffer != null ? context.buffer : EMPTY; 273 convertToBase58(accumulate, context); 274 return; 275 } 276 final byte[] accumulate = context.buffer = context.buffer != null ? context.buffer : EMPTY; 277 final byte[] newAccumulated = new byte[accumulate.length + length]; 278 if (accumulate.length > 0) { 279 System.arraycopy(accumulate, 0, newAccumulated, 0, accumulate.length); 280 } 281 System.arraycopy(array, offset, newAccumulated, accumulate.length, length); 282 context.buffer = newAccumulated; 283 } 284 285 /** 286 * Builds the Base58 string representation of the given binary data. 287 * <p> 288 * Converts binary data to a BigInteger and divides by 58 repeatedly to get the Base58 digits. Handles leading zeros by counting them and appending '1' for 289 * each leading zero byte. 290 * </p> 291 * 292 * @param accumulate the binary data to convert. 293 * @return a StringBuilder with the Base58 representation (not yet reversed). 294 */ 295 private StringBuilder getStringBuilder(final byte[] accumulate) { 296 BigInteger value = new BigInteger(1, accumulate); 297 int leadingZeros = 0; 298 for (final byte b : accumulate) { 299 if (b != 0) { 300 break; 301 } 302 leadingZeros++; 303 } 304 final StringBuilder base58 = new StringBuilder(); 305 while (value.signum() > 0) { 306 final BigInteger[] divRem = value.divideAndRemainder(BASE); 307 base58.append((char) ENCODE_TABLE[divRem[1].intValue()]); 308 value = divRem[0]; 309 } 310 for (int i = 0; i < leadingZeros; i++) { 311 base58.append('1'); 312 } 313 return base58; 314 } 315 316 /** 317 * Returns whether or not the {@code octet} is in the Base58 alphabet. 318 * 319 * @param value The value to test. 320 * @return {@code true} if the value is defined in the Base58 alphabet {@code false} otherwise. 321 */ 322 @Override 323 protected boolean isInAlphabet(final byte value) { 324 return isInAlphabet(value, DECODE_TABLE); 325 } 326}