diff --git a/hutool-core/src/main/java/cn/hutool/core/codec/Base58.java b/hutool-core/src/main/java/cn/hutool/core/codec/Base58.java index 90ce8df77..1a7a6444a 100644 --- a/hutool-core/src/main/java/cn/hutool/core/codec/Base58.java +++ b/hutool-core/src/main/java/cn/hutool/core/codec/Base58.java @@ -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的编码和解码方案
+ * 参考: https://github.com/Anujraval24/Base58Encoding
+ * 规范见: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编码
+ * 包含版本位和校验位 * - * @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解码
+ * 解码包含标志位验证和版本呢位去除 * * @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解码
+ * 解码包含标志位验证和版本呢位去除 * - * @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); + /** + * 获取校验码
+ * 计算规则为对数据进行两次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); + } } - } diff --git a/hutool-core/src/main/java/cn/hutool/core/codec/Base58Codec.java b/hutool-core/src/main/java/cn/hutool/core/codec/Base58Codec.java new file mode 100644 index 000000000..6e6d02d82 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/codec/Base58Codec.java @@ -0,0 +1,142 @@ +package cn.hutool.core.codec; + +import cn.hutool.core.util.StrUtil; + +import java.util.Arrays; + +/** + * Base58编码器
+ * 此编码器不包括校验码、版本等信息 + * + * @author lin, looly + * @since 5.7.22 + */ +public class Base58Codec implements Encoder, Decoder { + + 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; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/codec/Base62Codec.java b/hutool-core/src/main/java/cn/hutool/core/codec/Base62Codec.java index 0dbc1fadd..2386de005 100644 --- a/hutool-core/src/main/java/cn/hutool/core/codec/Base62Codec.java +++ b/hutool-core/src/main/java/cn/hutool/core/codec/Base62Codec.java @@ -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, Decoder, 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 -} \ No newline at end of file +} diff --git a/hutool-core/src/main/java/cn/hutool/core/codec/Base64Decoder.java b/hutool-core/src/main/java/cn/hutool/core/codec/Base64Decoder.java index 91dd741e8..20d0baad9 100644 --- a/hutool-core/src/main/java/cn/hutool/core/codec/Base64Decoder.java +++ b/hutool-core/src/main/java/cn/hutool/core/codec/Base64Decoder.java @@ -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 diff --git a/hutool-core/src/main/java/cn/hutool/core/codec/Base64Encoder.java b/hutool-core/src/main/java/cn/hutool/core/codec/Base64Encoder.java index 608dbc4bf..c1ec7a6d6 100644 --- a/hutool-core/src/main/java/cn/hutool/core/codec/Base64Encoder.java +++ b/hutool-core/src/main/java/cn/hutool/core/codec/Base64Encoder.java @@ -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; /** 标准编码表 */ diff --git a/hutool-core/src/main/java/cn/hutool/core/codec/Decoder.java b/hutool-core/src/main/java/cn/hutool/core/codec/Decoder.java new file mode 100644 index 000000000..b1f9e0a57 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/codec/Decoder.java @@ -0,0 +1,20 @@ +package cn.hutool.core.codec; + +/** + * 解码接口 + * + * @param 被解码的数据类型 + * @param 解码后的数据类型 + * @author looly + * @since 5.7.22 + */ +public interface Decoder { + + /** + * 执行解码 + * + * @param data 被解码的数据 + * @return 解码后的数据 + */ + R decode(T data); +} diff --git a/hutool-core/src/main/java/cn/hutool/core/codec/Encoder.java b/hutool-core/src/main/java/cn/hutool/core/codec/Encoder.java new file mode 100644 index 000000000..ca05d0ac3 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/codec/Encoder.java @@ -0,0 +1,20 @@ +package cn.hutool.core.codec; + +/** + * 编码接口 + * + * @param 被编码的数据类型 + * @param 编码后的数据类型 + * @author looly + * @since 5.7.22 + */ +public interface Encoder { + + /** + * 执行编码 + * + * @param encoded 被编码的数据 + * @return 编码后的数据 + */ + R encode(T encoded); +} diff --git a/hutool-core/src/test/java/cn/hutool/core/codec/Base58Test.java b/hutool-core/src/test/java/cn/hutool/core/codec/Base58Test.java index d27a43180..0dab5faf2 100644 --- a/hutool-core/src/test/java/cn/hutool/core/codec/Base58Test.java +++ b/hutool-core/src/test/java/cn/hutool/core/codec/Base58Test.java @@ -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); } } diff --git a/hutool-crypto/src/main/java/cn/hutool/crypto/KeyUtil.java b/hutool-crypto/src/main/java/cn/hutool/crypto/KeyUtil.java index 90ef70540..518f09917 100644 --- a/hutool-crypto/src/main/java/cn/hutool/crypto/KeyUtil.java +++ b/hutool-crypto/src/main/java/cn/hutool/crypto/KeyUtil.java @@ -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()); + } }