add Base58

This commit is contained in:
Looly 2022-02-25 03:18:28 +08:00
parent c3bf175b2b
commit 1c55e39832
9 changed files with 311 additions and 117 deletions

View File

@ -1,36 +1,37 @@
package cn.hutool.core.codec;
import java.math.BigInteger;
import cn.hutool.core.exceptions.UtilException;
import cn.hutool.core.exceptions.ValidateException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
/**
* @author lin
* Inspired from https://github.com/adamcaudill/Base58Check/blob/master/src/Base58Check/Base58CheckEncoding.cs
* Base58工具类提供Base58的编码和解码方案<br>
* 参考 https://github.com/Anujraval24/Base58Encoding<br>
* 规范见https://en.bitcoin.it/wiki/Base58Check_encoding
*
* @author lin looly
* @since 5.7.22
*/
public class Base58 {
private static final String ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
private static final char[] ALPHABET_ARRAY = ALPHABET.toCharArray();
private static final BigInteger BASE_SIZE = BigInteger.valueOf(ALPHABET_ARRAY.length);
private static final int CHECKSUM_SIZE = 4;
// -------------------------------------------------------------------- encode
/**
* Base58编码
* Base58编码<br>
* 包含版本位和校验位
*
* @param data 被编码的数组添加校验和
* @param version 编码版本{@code null}表示不包含版本位
* @param data 被编码的数组添加校验和
* @return 编码后的字符串
* @since 5.7.22
*/
public static String encode(byte[] data) throws NoSuchAlgorithmException {
return encodePlain(addChecksum(data));
public static String encodeChecked(Integer version, byte[] data) {
return encode(addChecksum(version, data));
}
/**
@ -38,114 +39,114 @@ public class Base58 {
*
* @param data 被编码的数据不带校验和
* @return 编码后的字符串
* @since 5.7.22
*/
public static String encodePlain(byte[] data) {
BigInteger intData;
try {
intData = new BigInteger(1, data);
} catch (NumberFormatException e) {
return "";
}
StringBuilder result = new StringBuilder();
while (intData.compareTo(BigInteger.ZERO) > 0) {
BigInteger[] quotientAndRemainder = intData.divideAndRemainder(BASE_SIZE);
BigInteger quotient = quotientAndRemainder[0];
BigInteger remainder = quotientAndRemainder[1];
intData = quotient;
result.insert(0, ALPHABET_ARRAY[remainder.intValue()]);
}
for (int i = 0; i < data.length && data[i] == 0; i++) {
result.insert(0, '1');
}
return result.toString();
public static String encode(byte[] data) {
return Base58Codec.INSTANCE.encode(data);
}
// -------------------------------------------------------------------- decode
/**
* Base58编码
* Base58解码<br>
* 解码包含标志位验证和版本呢位去除
*
* @param encoded 被解码的base58字符串
* @return 解码后的bytes
* @since 5.7.22
* @throws ValidateException 标志位验证错误抛出此异常
*/
public static byte[] decode(String encoded) throws NoSuchAlgorithmException {
byte[] valueWithChecksum = decodePlain(encoded);
byte[] value = verifyAndRemoveChecksum(valueWithChecksum);
if (value == null) {
throw new IllegalArgumentException("Base58 checksum is invalid");
public static byte[] decodeChecked(CharSequence encoded) throws ValidateException {
try {
return decodeChecked(encoded, true);
} catch (ValidateException ignore) {
return decodeChecked(encoded, false);
}
return value;
}
/**
* Base58编码
* Base58解码<br>
* 解码包含标志位验证和版本呢位去除
*
* @param encoded 被解码的base58字符串
* @param encoded 被解码的base58字符串
* @param withVersion 是否包含版本位
* @return 解码后的bytes
* @since 5.7.22
* @throws ValidateException 标志位验证错误抛出此异常
*/
public static byte[] decodePlain(String encoded) {
if (encoded.length() == 0) {
return new byte[0];
}
BigInteger intData = BigInteger.ZERO;
int leadingZeros = 0;
for (int i = 0; i < encoded.length(); i++) {
char current = encoded.charAt(i);
int digit = ALPHABET.indexOf(current);
if (digit == -1) {
throw new IllegalArgumentException(String.format("Invalid Base58 character `%c` at position %d", current, i));
}
intData = (intData.multiply(BASE_SIZE)).add(BigInteger.valueOf(digit));
}
public static byte[] decodeChecked(CharSequence encoded, boolean withVersion) throws ValidateException {
byte[] valueWithChecksum = decode(encoded);
return verifyAndRemoveChecksum(valueWithChecksum, withVersion);
}
for (int i = 0; i < encoded.length(); i++) {
char current = encoded.charAt(i);
if (current == '1') {
leadingZeros++;
} else {
break;
}
/**
* Base58解码
*
* @param encoded 被编码的base58字符串
* @return 解码后的bytes
*/
public static byte[] decode(CharSequence encoded) {
return Base58Codec.INSTANCE.decode(encoded);
}
/**
* 验证并去除验证位和版本位
*
* @param data 编码的数据
* @param withVersion 是否包含版本位
* @return 载荷数据
*/
private static byte[] verifyAndRemoveChecksum(byte[] data, boolean withVersion) {
final byte[] payload = Arrays.copyOfRange(data, withVersion ? 1 : 0, data.length - CHECKSUM_SIZE);
final byte[] checksum = Arrays.copyOfRange(data, data.length - CHECKSUM_SIZE, data.length);
final byte[] expectedChecksum = checksum(payload);
if (false == Arrays.equals(checksum, expectedChecksum)) {
throw new ValidateException("Base58 checksum is invalid");
}
byte[] bytesData;
if (intData.equals(BigInteger.ZERO)) {
bytesData = new byte[0];
return payload;
}
/**
* 数据 + 校验码
*
* @param version 版本{@code null}表示不添加版本位
* @param payload Base58数据不含校验码
* @return Base58数据
*/
private static byte[] addChecksum(Integer version, byte[] payload) {
final byte[] addressBytes;
if (null != version) {
addressBytes = new byte[1 + payload.length + CHECKSUM_SIZE];
addressBytes[0] = (byte) version.intValue();
System.arraycopy(payload, 0, addressBytes, 1, payload.length);
} else {
bytesData = intData.toByteArray();
addressBytes = new byte[payload.length + CHECKSUM_SIZE];
System.arraycopy(payload, 0, addressBytes, 0, payload.length);
}
//Should we cut the sign byte ? - https://bitcoinj.googlecode.com/git-history/216deb2d35d1a128a7f617b91f2ca35438aae546/lib/src/com/google/bitcoin/core/Base58.java
boolean stripSignByte = bytesData.length > 1 && bytesData[0] == 0 && bytesData[1] < 0;
byte[] decoded = new byte[bytesData.length - (stripSignByte ? 1 : 0) + leadingZeros];
System.arraycopy(bytesData, stripSignByte ? 1 : 0, decoded, leadingZeros, decoded.length - leadingZeros);
return decoded;
final byte[] checksum = checksum(payload);
System.arraycopy(checksum, 0, addressBytes, addressBytes.length - CHECKSUM_SIZE, CHECKSUM_SIZE);
return addressBytes;
}
private static byte[] verifyAndRemoveChecksum(byte[] data) throws NoSuchAlgorithmException {
byte[] value = Arrays.copyOfRange(data, 0, data.length - CHECKSUM_SIZE);
byte[] checksum = Arrays.copyOfRange(data, data.length - CHECKSUM_SIZE, data.length);
byte[] expectedChecksum = getChecksum(value);
return Arrays.equals(checksum, expectedChecksum) ? value : null;
}
private static byte[] addChecksum(byte[] data) throws NoSuchAlgorithmException {
byte[] checksum = getChecksum(data);
byte[] result = new byte[data.length + checksum.length];
System.arraycopy(data, 0, result, 0, data.length);
System.arraycopy(checksum, 0, result, data.length, checksum.length);
return result;
}
private static byte[] getChecksum(byte[] data) throws NoSuchAlgorithmException {
byte[] hash = hash256(data);
hash = hash256(hash);
/**
* 获取校验码<br>
* 计算规则为对数据进行两次sha256计算然后取{@link #CHECKSUM_SIZE}长度
*
* @param data 数据
* @return 校验码
*/
private static byte[] checksum(byte[] data) {
byte[] hash = hash256(hash256(data));
return Arrays.copyOfRange(hash, 0, CHECKSUM_SIZE);
}
private static byte[] hash256(byte[] data) throws NoSuchAlgorithmException {
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(data);
return md.digest();
/**
* 计算数据的SHA-256值
*
* @param data 数据
* @return sha-256值
*/
private static byte[] hash256(byte[] data) {
try {
return MessageDigest.getInstance("SHA-256").digest(data);
} catch (NoSuchAlgorithmException e) {
throw new UtilException(e);
}
}
}

View File

@ -0,0 +1,142 @@
package cn.hutool.core.codec;
import cn.hutool.core.util.StrUtil;
import java.util.Arrays;
/**
* Base58编码器<br>
* 此编码器不包括校验码版本等信息
*
* @author lin looly
* @since 5.7.22
*/
public class Base58Codec implements Encoder<byte[], String>, Decoder<CharSequence, byte[]> {
public static Base58Codec INSTANCE = new Base58Codec();
private final char[] alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz".toCharArray();
private final char ENCODED_ZERO = alphabet[0];
private final int[] lookup = initLookup();
/**
* Base58编码
*
* @param data 被编码的数据不带校验和
* @return 编码后的字符串
*/
@Override
public String encode(byte[] data) {
if (null == data) {
return null;
}
if (data.length == 0) {
return StrUtil.EMPTY;
}
// 计算开头0的个数
int zeroCount = 0;
while (zeroCount < data.length && data[zeroCount] == 0) {
++zeroCount;
}
// 将256位编码转换为58位编码
data = Arrays.copyOf(data, data.length); // since we modify it in-place
final char[] encoded = new char[data.length * 2]; // upper bound
int outputStart = encoded.length;
for (int inputStart = zeroCount; inputStart < data.length; ) {
encoded[--outputStart] = alphabet[divmod(data, inputStart, 256, 58)];
if (data[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 (--zeroCount >= 0) {
encoded[--outputStart] = ENCODED_ZERO;
}
// Return encoded string (including encoded leading zeros).
return new String(encoded, outputStart, encoded.length - outputStart);
}
/**
* 解码给定的Base58字符串
*
* @param encoded Base58编码字符串
* @return 解码后的bytes
* @throws IllegalArgumentException 非标准Base58字符串
*/
@Override
public byte[] decode(CharSequence encoded) throws IllegalArgumentException {
if (encoded.length() == 0) {
return new byte[0];
}
// Convert the base58-encoded ASCII chars to a base58 byte sequence (base58 digits).
final byte[] input58 = new byte[encoded.length()];
for (int i = 0; i < encoded.length(); ++i) {
char c = encoded.charAt(i);
int digit = c < 128 ? lookup[c] : -1;
if (digit < 0) {
throw new IllegalArgumentException(StrUtil.format("Invalid char '{}' at [{}]", c, i));
}
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[encoded.length()];
int outputStart = decoded.length;
for (int inputStart = zeros; inputStart < input58.length; ) {
decoded[--outputStart] = divmod(input58, inputStart, 58, 256);
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);
}
/**
* 初始化字符序号查找表
*
* @return 字符序号查找表
*/
private int[] initLookup() {
final int[] lookup = new int['z' + 1];
Arrays.fill(lookup, -1);
for (int i = 0; i < alphabet.length; i++)
lookup[alphabet[i]] = i;
return lookup;
}
/**
* Divides a number, represented as an array of bytes each containing a single digit
* in the specified base, by the given divisor. The given number is modified in-place
* to contain the quotient, and the return value is the remainder.
*
* @param number the number to divide
* @param firstDigit the index within the array of the first non-zero digit
* (this is used for optimization by skipping the leading zeros)
* @param base the base in which the number's digits are represented (up to 256)
* @param divisor the number to divide by (up to 256)
* @return the remainder of the division operation
*/
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] & 0xFF;
int temp = remainder * base + digit;
number[i] = (byte) (temp / divisor);
remainder = temp % divisor;
}
return (byte) remainder;
}
}

View File

@ -12,7 +12,7 @@ import java.io.Serializable;
* @author Looly, Sebastian Ruhleder, sebastian@seruco.io
* @since 4.5.9
*/
public class Base62Codec implements Serializable{
public class Base62Codec implements Encoder<byte[], byte[]>, Decoder<byte[], byte[]>, Serializable{
private static final long serialVersionUID = 1L;
private static final int STANDARD_BASE = 256;
@ -86,6 +86,7 @@ public class Base62Codec implements Serializable{
* @param message 被编码的消息
* @return Base62内容
*/
@Override
public byte[] encode(byte[] message) {
final byte[] indices = convert(message, STANDARD_BASE, TARGET_BASE);
return translate(indices, alphabet);
@ -97,6 +98,7 @@ public class Base62Codec implements Serializable{
* @param encoded Base62内容
* @return 消息
*/
@Override
public byte[] decode(byte[] encoded) {
final byte[] prepared = translate(encoded, lookup);
return convert(prepared, TARGET_BASE, STANDARD_BASE);
@ -177,4 +179,4 @@ public class Base62Codec implements Serializable{
return (int) Math.ceil((Math.log(sourceBase) / Math.log(targetBase)) * inputLength);
}
// --------------------------------------------------------------------------------------------------------------- Private method end
}
}

View File

@ -18,15 +18,6 @@ public class Base64Decoder {
private static final byte PADDING = -2;
/** Base64解码表共128位-1表示非base64字符-2表示padding */
// private static final byte[] DECODE_TABLE2 = {
// -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
// -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
// -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,
// 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1,
// -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
// 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,
// -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
// 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1 };
private static final byte[] DECODE_TABLE = {
// 0 1 2 3 4 5 6 7 8 9 A B C D E F
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 00-0f

View File

@ -11,7 +11,7 @@ import java.nio.charset.Charset;
* @author looly
* @since 3.2.0
*/
public class Base64Encoder {
public class Base64Encoder{
private static final Charset DEFAULT_CHARSET = CharsetUtil.CHARSET_UTF_8;
/** 标准编码表 */

View File

@ -0,0 +1,20 @@
package cn.hutool.core.codec;
/**
* 解码接口
*
* @param <T> 被解码的数据类型
* @param <R> 解码后的数据类型
* @author looly
* @since 5.7.22
*/
public interface Decoder<T, R> {
/**
* 执行解码
*
* @param data 被解码的数据
* @return 解码后的数据
*/
R decode(T data);
}

View File

@ -0,0 +1,20 @@
package cn.hutool.core.codec;
/**
* 编码接口
*
* @param <T> 被编码的数据类型
* @param <R> 编码后的数据类型
* @author looly
* @since 5.7.22
*/
public interface Encoder<T, R> {
/**
* 执行编码
*
* @param encoded 被编码的数据
* @return 编码后的数据
*/
R encode(T encoded);
}

View File

@ -4,31 +4,37 @@ import org.junit.Assert;
import org.junit.Test;
import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
public class Base58Test {
@Test
public void testEncode() throws NoSuchAlgorithmException {
public void encodeCheckedTest() {
String a = "hello world";
String encode = Base58.encode(a.getBytes(StandardCharsets.UTF_8));
String encode = Base58.encodeChecked(0, a.getBytes());
Assert.assertEquals(1 + "3vQB7B6MrGQZaxCuFg4oh", encode);
// 无版本位
encode = Base58.encodeChecked(null, a.getBytes());
Assert.assertEquals("3vQB7B6MrGQZaxCuFg4oh", encode);
}
@Test
public void testEncodePlain() {
public void encodeTest() {
String a = "hello world";
String encode = Base58.encodePlain(a.getBytes(StandardCharsets.UTF_8));
String encode = Base58.encode(a.getBytes(StandardCharsets.UTF_8));
Assert.assertEquals("StV1DL6CwTryKyV", encode);
}
@Test
public void testDecode() throws NoSuchAlgorithmException {
public void decodeCheckedTest() {
String a = "3vQB7B6MrGQZaxCuFg4oh";
byte[] decode = Base58.decode(a);
byte[] decode = Base58.decodeChecked(1 + a);
Assert.assertArrayEquals("hello world".getBytes(StandardCharsets.UTF_8),decode);
decode = Base58.decodeChecked(a);
Assert.assertArrayEquals("hello world".getBytes(StandardCharsets.UTF_8),decode);
}
@Test
public void testDecodePlain() {
public void testDecode() {
String a = "StV1DL6CwTryKyV";
byte[] decode = Base58.decodePlain(a);
byte[] decode = Base58.decode(a);
Assert.assertArrayEquals("hello world".getBytes(StandardCharsets.UTF_8),decode);
}
}

View File

@ -1,5 +1,6 @@
package cn.hutool.crypto;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.lang.Assert;
@ -22,6 +23,7 @@ import java.io.InputStream;
import java.math.BigInteger;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
@ -988,4 +990,14 @@ public class KeyUtil {
throw new CryptoException(e);
}
}
/**
* 将密钥编码为Base64格式
* @param key 密钥
* @return Base64格式密钥
* @since 5.7.22
*/
public static String toBase64(Key key){
return Base64.encode(key.getEncoded());
}
}