Base58 编码和解码

Base58 是一种基于 58 个字符的编码方式,常用于比特币和其他加密货币中,主要目的是将任意二进制数据转换为人类可读的字符串形式,同时避免一些容易混淆的字符和潜在的错误。

与 Base64 不同,Base58 编码中通常会排除一些容易混淆的字符,例如0(零)、O(大写字母O)、I(大写字母I)和 l(小写字母L)等,以减少人为错误。

Base58 编码表

8f0348d02770f6cc8c5584fb1a746f7d_1709016970242-1b1aac8c-53c4-4bd6-80f5-4b999f0bd8ad_x-oss-process=image%2Fformat%2Cwebp%2Fresize%2Cw_750%2Climit_0.png

Base58 设计目的

Base58 编码是为比特币钱包地址设计的,主要考虑了一下几点:

(1)避免混淆,在某些字体下,数字 0 和字母大写 O,以及字母大写 I 和字母小写 l 会非常相似。

(2)Base64 编码中包含 "+" 和 "/",非字母或数字的字符串作为帐号较难被接受。

(3) 在邮件系统中,使用字符和数字的组合,不容易换行。

(4)双击可以选中整个字符串。

Base58 编码特点

字符集

Base58 使用的字符集包括数字 0-9 和大写字母 A-Z(去除了容易混淆的数字 0、大写字母 O、大写字母 I 和小写字母 l)以及一些特殊字符如 “+” 和 “/”。

可读性

相比十六进制编码或直接的二进制表示,Base58 编码的字符串更易于人类阅读和手动输入,减少了输入错误的可能性。

错误检测

虽然 Base58 编码本身不提供错误检测机制,但由于其字符集的选择,使得编码后的字符串在视觉上更容易发现错误。例如,如果一个字符被错误地替换为一个相似的字符,通常会很明显。

示例:

5Lq65Lq657yW56iL572RIGh0dHA6Ly93d3cuaHhzdHJpdmUuY29t

5Lq65Lq657yW56iL572RIGhOdHA6Ly93d3cuaHhzdHJpdmUuY29t

找一找上面两个字符串中不同的字符,是不是很不好找,特别是 I,他们的不同如下图:

682b31f1650191fc6f922b1055482443_1727340169387-a757b5fd-8e61-4c32-b982-bd6119ae3b12_x-oss-process=image%2Fformat%2Cwebp.png

Base58 编码原理

编码过程

  1. 初始化变量

    • 初始化一个整数变量value为 0。这个变量将用于存储输入数据转换后的整数值。

  1. 转换为整数

    • 遍历输入的二进制数据中的每个字节。

    • 将当前的value乘以 256(因为一个字节的取值范围是 0 到 255),然后加上当前字节的值(将字节视为无符号整数,即范围是 0 到 255)。

    • 这样,经过遍历所有字节后,value将存储输入数据转换后的整数值。

  1. 生成编码结果

    • 创建一个空的字符列表来存储编码后的结果。

    • 当value大于 0 时,执行以下操作:

      • 计算value对 58 取模的结果,这个结果将对应于 Base58 字符集中的一个字符。

      • 将对应的字符添加到字符列表中。

      • 将value除以 58,继续下一次循环。

    • 处理前导零

      • 遍历输入的二进制数据,从前到后检查每个字节是否为 0。

      • 如果遇到一个字节不为 0,则停止检查。

      • 对于每个前导零字节,在编码结果的字符列表开头添加字符 '1'。这是因为 Base58 编码中,前导零用字符 '1' 表示。

  1. 构建最终结果

    • 创建一个字符串构建器。

    • 从字符列表的末尾开始,依次将字符添加到字符串构建器中。

    • 最后,返回构建好的字符串作为 Base58 编码的结果。

例如,假设有输入数据为二进制形式的整数12345。

  1. 首先将其转换为整数值value = 12345。

  2. 然后,不断地将value除以 58,并将余数转换为对应的 Base58 字符。

    • value = 12345 % 58 = 27,对应字符集里的字符是某个特定字符(假设是 'J')。

    • value = 12345 / 58 = 212

    • value = 212 % 58 = 48,对应字符是另一个特定字符(假设是 '6')。

    • value = 212 / 58 = 3

    • value = 3 % 58 = 3,对应字符是 '3'

    • value = 3 / 58 = 0,结束循环。

  1. 检查输入数据没有前导零。

  2. 最终的 Base58 编码结果为 “36J”。

解码过程

  1. 初始化变量

    • 初始化一个长整型变量value为 0。这个变量将用于存储解码过程中的中间值。

  1. 遍历输入字符串

    • 对于输入的 Base58 编码字符串中的每个字符:

      • 在 Base58 字符集中查找该字符的位置。如果字符不在字符集中,说明输入不合法,抛出异常。

      • 将当前的value乘以 58,然后加上找到的字符位置值。

  1. 转换为字节数组

    • 创建一个空的字节列表来存储解码后的结果。

    • 当value大于 0 时,执行以下操作:

      • 将value对 256 取模,得到一个字节值,添加到字节列表中。

      • value除以 256,继续下一次循环。

  1. 处理前导零

    • 遍历输入的 Base58 编码字符串,从前到后检查每个字符是否为 '1'。

    • 对于每个前导 '1' 字符,在解码结果的字节列表开头添加一个字节值 0。

  1. 构建最终结果

    • 将字节列表转换为字节数组,并返回作为解码结果。

例如,假设有 Base58 编码字符串 “36J”。

  1. 首先确定字符集,然后遍历这个字符串。

    • 对于字符 “3”,在字符集中找到其位置为 3,此时value = 3。

    • 对于字符 “6”,在字符集中找到其位置为 6,此时value = 3 * 58 + 6 = 180。

    • 对于字符 “J”,在字符集中找到其位置为 27,此时value = 180 * 58 + 27 = 10467。

  1. 转换为字节数组。

    • value = 10467,value % 256 = 119,添加到字节列表中,此时字节列表为 [119]。

    • value = 10467 / 256 = 40,value % 256 = 40,添加到字节列表中,此时字节列表为 [40, 119]。

    • value = 40 / 256 = 0,结束循环。

  1. 输入字符串没有前导零,无需处理前导零。

  2. 将字节列表转换为字节数组 [40, 119] 作为解码结果。

Base58 编码实现

下面使用 Java 简单实现 Base58 编码的编码和解码,代码如下:

package com.hxstrive.encryption_decryption.base58;

import java.math.BigInteger;

/**
 * Base58 编码&解码
 * @author hxstrive.com
 */
public class Base58Demo {
    /** 定义 Base58 可用字符集 */
    private static final String ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";

    /**
     * 编码
     * @param input 字节数组
     * @return 编码后的字符串
     */
    public static String encode(byte[] input) {
        // signum 参数表示为整数值:-1表示负数,0表示零,或 1表示正数
        BigInteger value = new BigInteger(1, input);
        StringBuilder result = new StringBuilder();

        // 循环直到 value 变为 0
        while (value.compareTo(BigInteger.ZERO) > 0) {
            // divideAndRemainder 方法将返回两个 BigInteger 的数组 [商,余数]
            // 返回值为:[(this / val), (this % val)]
            BigInteger[] quotientAndRemainder = value.divideAndRemainder(BigInteger.valueOf(58));
            // 余数添加到结果中
            result.insert(0, ALPHABET.charAt(quotientAndRemainder[1].intValue()));
            value = quotientAndRemainder[0]; // 商
        }

        //  添加前导 0
        for (byte b : input) {
            if (b == 0) {
                result.insert(0, ALPHABET.charAt(0));
            } else {
                break;
            }
        }

        return result.toString();
    }

    /**
     * 解码
     * @param input 编码后的字符串
     * @return 原文
     */
    public static byte[] decode(String input) {
        BigInteger value = BigInteger.ZERO;

        for (int i = 0; i < input.length(); i++) {
            char c = input.charAt(i);
            int digit = ALPHABET.indexOf(c);
            if (digit == -1) {
                throw new IllegalArgumentException("Invalid Base58 character: " + c);
            }
            value = value.multiply(BigInteger.valueOf(58)).add(BigInteger.valueOf(digit));
        }

        return value.toByteArray();
    }

    public static void main(String[] args) {
        String originalData = "Hello, Base58!";
        String encodedData = encode(originalData.getBytes());
        System.out.println("Encoded: " + new String(encodedData));

        byte[] decodedData = decode(new String(encodedData));
        System.out.println("Decoded: " + new String(decodedData));
    }
}

运行示例,输出如下:

Encoded: TcgsE5e9XJSrakNTEQQ
Decoded: Hello, Base58!


说说我的看法
全部评论(
没有评论
关于
本网站专注于 Java、数据库(MySQL、Oracle)、Linux、软件架构及大数据等多领域技术知识分享。涵盖丰富的原创与精选技术文章,助力技术传播与交流。无论是技术新手渴望入门,还是资深开发者寻求进阶,这里都能为您提供深度见解与实用经验,让复杂编码变得轻松易懂,携手共赴技术提升新高度。如有侵权,请来信告知:hxstrive@outlook.com
公众号