package info.frostfs.sdk; import org.apache.commons.lang3.StringUtils; import java.util.Arrays; import static info.frostfs.sdk.ArrayHelper.concat; import static info.frostfs.sdk.Helper.getSha256; import static java.util.Objects.isNull; public class Base58 { public static final char[] ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz".toCharArray(); public static final int BASE58_SYMBOL_COUNT = 58; public static final int BASE256_SYMBOL_COUNT = 256; private static final int BYTE_DIVISION = 0xFF; private static final char ENCODED_ZERO = ALPHABET[0]; private static final char BASE58_ASCII_MAX_VALUE = 128; private static final int[] INDEXES = new int[BASE58_ASCII_MAX_VALUE]; private static final String ERROR_VALUE_MISSING_MESSAGE = "Input value is missing"; private static final String ERROR_INVALID_CHARACTER_TEMPLATE = "Invalid character in Base58: 0x%04x"; static { Arrays.fill(INDEXES, -1); for (int i = 0; i < ALPHABET.length; i++) { INDEXES[ALPHABET[i]] = i; } } private Base58() { } public static byte[] base58CheckDecode(String input) { if (StringUtils.isEmpty(input)) { throw new IllegalArgumentException(ERROR_VALUE_MISSING_MESSAGE); } byte[] buffer = decode(input); if (buffer.length < 4) { throw new IllegalArgumentException(); } byte[] decode = Arrays.copyOfRange(buffer, 0, buffer.length - 4); byte[] checksum = getSha256(getSha256(decode)); var bufferEnd = Arrays.copyOfRange(buffer, buffer.length - 4, buffer.length); var checksumStart = Arrays.copyOfRange(checksum, 0, 4); if (!Arrays.equals(bufferEnd, checksumStart)) { throw new IllegalArgumentException(); } return decode; } public static String base58CheckEncode(byte[] data) { if (isNull(data)) { throw new IllegalArgumentException(ERROR_VALUE_MISSING_MESSAGE); } byte[] checksum = getSha256(getSha256(data)); var buffer = concat(data, Arrays.copyOfRange(checksum, 0, 4)); var ret = encode(buffer); return ret; } public static String encode(byte[] input) { if (input.length == 0) { return ""; } // Count leading zeros. int zeros = 0; while (zeros < input.length && input[zeros] == 0) { ++zeros; } // Convert base-256 digits to base-58 digits (plus conversion to ASCII characters) input = Arrays.copyOf(input, input.length); // since we modify it in-place char[] encoded = new char[input.length * 2]; // upper bound int outputStart = encoded.length; for (int inputStart = zeros; inputStart < input.length; ) { encoded[--outputStart] = ALPHABET[divmod(input, inputStart, BASE256_SYMBOL_COUNT, BASE58_SYMBOL_COUNT)]; if (input[inputStart] == 0) { ++inputStart; // optimization - skip leading zeros } } // Preserve exactly as many leading encoded zeros in output as there were leading zeros in input. while (outputStart < encoded.length && encoded[outputStart] == ENCODED_ZERO) { ++outputStart; } while (--zeros >= 0) { encoded[--outputStart] = ENCODED_ZERO; } // Return encoded string (including encoded leading zeros). return new String(encoded, outputStart, encoded.length - outputStart); } public static byte[] decode(String input) { if (input.isEmpty()) { return new byte[0]; } // Convert the base58-encoded ASCII chars to a base58 byte sequence (base58 digits). byte[] input58 = new byte[input.length()]; for (int i = 0; i < input.length(); ++i) { char c = input.charAt(i); int digit = c < BASE58_ASCII_MAX_VALUE ? INDEXES[c] : -1; if (digit < 0) { throw new IllegalArgumentException(String.format(ERROR_INVALID_CHARACTER_TEMPLATE, (int) c)); } input58[i] = (byte) digit; } // Count leading zeros. int zeros = 0; while (zeros < input58.length && input58[zeros] == 0) { ++zeros; } // Convert base-58 digits to base-256 digits. byte[] decoded = new byte[input.length()]; int outputStart = decoded.length; for (int inputStart = zeros; inputStart < input58.length; ) { decoded[--outputStart] = divmod(input58, inputStart, BASE58_SYMBOL_COUNT, BASE256_SYMBOL_COUNT); if (input58[inputStart] == 0) { ++inputStart; // optimization - skip leading zeros } } // Ignore extra leading zeroes that were added during the calculation. while (outputStart < decoded.length && decoded[outputStart] == 0) { ++outputStart; } // Return decoded data (including original number of leading zeros). return Arrays.copyOfRange(decoded, outputStart - zeros, decoded.length); } private static byte divmod(byte[] number, int firstDigit, int base, int divisor) { // this is just long division which accounts for the base of the input digits int remainder = 0; for (int i = firstDigit; i < number.length; i++) { int digit = (int) number[i] & BYTE_DIVISION; int temp = remainder * base + digit; number[i] = (byte) (temp / divisor); remainder = temp % divisor; } return (byte) remainder; } }