正确理解Base64编码

1 什么是Base64

以前,email只支持ASCII编码的字符,而ASCII只能表示128个字符。若一封email存在非ASCII编码的字符,当它通过网关的时候,网关(以前)会对email的二进制位作调整,把非ASCII编码的8位二进制码的最高位设置为0,因此,接收方收到的email会出现乱码。

如何使一封包含非ASCII编码字符的email正确地发送到目的地呢?Base64应运而生,它定义了规则映射表发送方规则映射表把内容编码成只包含ASCII字符的字符串,接收方收到信息后,根据规则映射表对信息进行解码还原。

2 规则映射表

在RFC(Request For Comments) 2045文档中规定了Base64的映射规则:

十进制编码十进制编码
0A32g
1B33h
2C34i
3D35j
4E36k
5F37l
6G38m
7H39n
8I40o
9J41p
10K42q
11L43r
12M44s
13N45t
14O46u
15P47v
16Q48w
17R49x
18S50y
19T51z
20U520
21V531
22W542
23X553
24Y564
25Z575
26a586
27b597
28c608
29d619
30e62+
31f63/
(pad)=

3 Base64与加密

严格来说,Base64并不是一种加密方式,而是一种使用了表置换算法编码方式。由于Base64的规则映射表是公开的,因此,我们不能把一些私密的信息经Base64简单编码后就在网络上传播,例如密码、私钥。

Base64编码把二进制流编码成可见的字符,方便一些文件的保存和传输,例如:

x509证书,有derpem两种格式,der是二进制格式,pem则是der经Base64编码后的格式。假设,我们希望把一个证书拷贝到远程的服务器,正好,服务器没有提供sftp服务,对于pem,我们只需要打开文件,复制文件中的内容,并拷贝到服务器就可以了,对于der,我们则无法这样拷贝,因为,使用文本编辑器打开der后,会显示乱码。

4 Base64的编码过程

Base64的编码主要经过以下步骤:

(1) 把要进行Base64编码的二进制码进行分组,每3个字节,即每24位一组,若二进制码的字节数无法被3整除,则要低位补 (3 - 二进制码字节数 mod 3) * 8 位0,例如:如果要编码的内容为11个字节,那么则要在低位补8位0,根据 (3 - 11 mod 3) * 8计算得出。

(2) 经过第一步后,再把24位的分组继续拆成6位一个分组。

(3) 现在每个分组为6位,然后,每个分组的高位补2位0。

(4) 最后,把第三步处理后得到的二进制码,逐个字节从规则对照表找到对应的编码。

下面展示了把"great中国"进行Base64的编码过程:

下面的代码段可以获取"great中国"的UTF-8的二进制码

byte[] bytes = "great中国".getBytes(Charset.forName("UTF-8"));
for (int i = 0; i < bytes.length; i++) {
	if (i > 0) {
		System.out.print("_");
	}
	System.out.print(String.format(
			"%8s",
			Integer.toBinaryString(bytes[i] & 0xff)).replaceAll(" ", "0")
	);
}

运行上面的代码段后,我们获取字符串的二进制码为:

01100111_01110010_01100101_01100001_01110100_11100100_10111000_10101101_11100101_10011011_10111101

共11个字节。

按24位(3个字节)1组的规则对上面的二进制码进行分组

(01100111_01110010_01100101)_(01100001_01110100_11100100)_(10111000_10101101_11100101)_(10011011_10111101)

共分 Ceil(11字节 / 3个字节) = 4 组。

低位补0

上面进行分组后,最后的分组不足3个字节,所以低位需要补8位0。

(01100111_01110010_01100101)_(01100001_01110100_11100100)_(10111000_10101101_11100101)_(10011011_10111101_00000000)

按每6位为一组再分组

(011001)_(110111)_(001001)_(100101)_(011000)_(010111)_(010011)_(100100)_(101110)_(001010)_(110111)_(100101)_(100110)_(111011)_(110100)_(000000)

每个分组的高位补2位0

(00011001)_(00110111)_(00001001)_(00100101)_(00011000)_(00010111)_(00010011)_(00100100)_(00101110)_(00001010)_(00110111)_(00100101)_(00100110)_(00111011)_(00110100)_(00000000)

把分组转成十进制并在对照表找出对应的编码

二进制十进制规则对照表的编码
0001100125Z
00110111553
000010019J
0010010137l
0001100024Y
0001011123X
0001001119T
0010010036k
0010111046u
0000101010K
00110111553
0010010137l
0010011038m
00111011597
00110100520
00000000第一步分组,要求每个组为3个字节,由于最后一个分组不足3个字节,所以补了0用于填充=

最终得到"great中国"的Base64编码为Z3JlYXTkuK3lm70=

使用bouncycastle的Base64的工具类对"great中国"进行编码

在这里插入图片描述
我们可以看到结果跟我们上面手动编码的结果是一致的。

5 Base64编码的特点

(1) Base64编码的位长度一定能被32整除。
(2) 原文长度为编码后长度的3/4,因为,每6位的分组的高位补了2位0。
(3) Base64的编码结尾最多会有两个=号,因为,若二进制码的字节数无法被3整除,则要低位补 (3 - 二进制码字节数 mod 3) * 8 位0,即用=号填充。

6 用Java实现Base64编码

上面比较直观地描述了Base64的编码过程,下面将尝试使用Java实现Base64的编码过程。

6.1 规则映射表

使用一个长度为64的byte数组作为编码规则表,其中,数组的下标为十进制值,元素为编码

private final static byte[] encodingArray =
{
    (byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', (byte) 'G',
    (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', (byte) 'L', (byte) 'M', (byte) 'N',
    (byte) 'O', (byte) 'P', (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U',
    (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', (byte) 'a', (byte) 'b',
    (byte) 'c', (byte) 'd', (byte) 'e', (byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i',
    (byte) 'j', (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o', (byte) 'p',
    (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', (byte) 'u', (byte) 'v', (byte) 'w',
    (byte) 'x', (byte) 'y', (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3',
    (byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8', (byte) '9', (byte) '+',
    (byte) '/'
};

除了64个编码外,还需要定义用于填充的=号。

private final static byte padding = (byte)'=';

6.2 计算长度

开始编码前,需要把字节数组拆成每3个字节(24位)一组进,因此,按字节数组长度 mod 3需填充处理的字节长度,结果为12字节数组长度- 需填充处理的字节长度优先处理的字节长度

// 计算不足3个字节需要做填充的字节长度,例如,长度为11,则有2个字节要填充处理
int tailLength = bytes.length % 3;
// 计算能被3整除的长度,例如,长度为11,则先处理前9个字节
int processLength = bytes.length - tailLength;

6.3 处理优先字节

先对前processLength个字节进行分组处理。上面手动编码的过程是,每3个字节一组,然后再分为6位一小组,然后每个小组高位补2位0,最终,每个小组为1个字节,通过字节的数值在规则映射表中找出编码。

若在程序中,我们可以借助位运算的技巧简单实现这个过程,例如,分组如下:

01100111_01110010_01100101

任务是要把上面的3个字节分为4组。

先把这3个字节转int类型

我们先把组里的3个字节转换为int类型,分别把这三个字段与0xff做与运算。

int ele1 = bytes[i] & 0xff;
int ele2 = bytes[i + 1] & 0xff;
int ele3 = bytes[i + 2] & 0xff;

0xff做与运算的目的是,保证原来字节的8位在转换成int后不会被改变,因为,若字节的首位是1,即负数,那么转换int的时候,补码形式会使原来字节的8位发生改变,而与0xff做与运算则只是在原来8位前面补0

转换int后,

ele1为: 00000000_00000000_00000000_01100111
ele2为: 00000000_00000000_00000000_01110010
ele3为: 00000000_00000000_00000000_01100101

下面对这3个int分成4个小组。

第1小组

第1小组取ele1最后一个字节的前6位,因此,把ele1无符号右移2位

encodedBytes[encodedIdx++] = encodingArray[(ele1 >>> 2) & 0x3f];

上面的代码中,跟0x3f做按位与得目的是,只保留最后的6位,其他位都为0。

ele1无符号右移2位后的二进制码为:

00000000_00000000_00000000_0011001

(ele1 >>> 2) & 0x3f的数值作为下标立刻就从上面定义的规则映射数组encodingArray中获取第1小组的编码了。

第2小组

第1小组取ele1最后1个字节的后2位ele2最后1个字节的前4位组成,即由下面粗体的位组成:

ele1: 00000000_00000000_00000000_01100111
ele2: 00000000_00000000_00000000_01110010

(1) 先对ele1左移动4位

00000000_00000000_00000110_01110000

(2) 对ele2无符号右移4位

00000000_00000000_00000000_00000111

(3) 上面结果按位与

  00000000_00000000_00000110_01110000
&00000000_00000000_00000000_00000111

  00000000_00000000_00000110_01110111

(4) 跟0x3f按位与,只保留最后6位

  00000000_00000000_00000110_01110111
&00000000_00000000_00000000_00111111

  00000000_00000000_00000000_00110111

整个过程的代码如下:

encodedBytes[encodedIdx++] = encodingArray[((ele1 << 4) | (ele2 >>> 4)) & 0x3f];

以得出的数值作为下标立刻就从上面定义的规则映射数组encodingArray中获取第2小组的编码了。

第3个小组

第3小组取ele2最后1个字节的后4位ele3最后1个字节的前2位组成,即由下面粗体的位组成:

ele2: 00000000_00000000_00000000_01110010
ele3: 00000000_00000000_00000000_01100101

(1) 先对ele2左移动2位

00000000_00000000_00000001_11001000

(2) 对ele3无符号右移6位

00000000_00000000_00000000_00000001

(3) 上面结果按位与

  00000000_00000000_00000001_11001000
&00000000_00000000_00000000_00000001

  00000000_00000000_00000000_00001001

(4) 跟0x3f按位与,只保留最后6位

  00000000_00000000_00000000_00001001
&00000000_00000000_00000000_00111111

  00000000_00000000_00000000_00001001

整个过程的代码如下:

encodedBytes[encodedIdx++] = encodingArray[((ele2 << 2) | (ele3 >>> 6)) & 0x3f];

以得出的数值作为下标立刻就从上面定义的规则映射数组encodingArray中获取第3小组的编码了。

第4小组

第4小组取ele3最后1个字节的后6位

ele3: 00000000_00000000_00000000_01100101

直接与0x3f做按位与。

代码如下:

encodedBytes[encodedIdx++] = encodingArray[ele3 & 0x3f];

以得出的数值作为下标立刻就从上面定义的规则映射数组encodingArray中获取第4小组的编码了。

至此,该包含3个字节的小组经过处理变成了4个字节

6.4 填充

6.5 完整代码

public class Base64Encoder {
    // Base64的规则映射表
    // 数组 下标 对应 编码
    private final static byte[] encodingArray =
        {
            (byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', (byte) 'G',
            (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', (byte) 'L', (byte) 'M', (byte) 'N',
            (byte) 'O', (byte) 'P', (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U',
            (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', (byte) 'a', (byte) 'b',
            (byte) 'c', (byte) 'd', (byte) 'e', (byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i',
            (byte) 'j', (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o', (byte) 'p',
            (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', (byte) 'u', (byte) 'v', (byte) 'w',
            (byte) 'x', (byte) 'y', (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3',
            (byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8', (byte) '9', (byte) '+',
            (byte) '/'
        };

    // 填充用的=
    private final static byte padding = (byte)'=';

    public static byte[] encode(byte[] bytes) {

        // 计算尾数,后面填充用,例如,长度为11,则有2个字节要填充处理
        int tailLength = bytes.length % 3;
        // 计算能被3整除的长度,例如,长度为11,则先处理前9个字节
        int processLength = bytes.length - tailLength;

        int ele1, ele2, ele3;

        byte[] encodedBytes = new byte[4 * processLength / 3 + (tailLength == 0 ? 0 :4) ];
        int encodedIdx = 0;
        // 3个字节为一组,因此循环步长为3
        for (int i = 0; i < processLength; i += 3)
        {
            ele1 = bytes[i] & 0xff;
            ele2 = bytes[i + 1] & 0xff;
            ele3 = bytes[i + 2] & 0xff;

            // 分组第1个成员,为ele1的前6位
            encodedBytes[encodedIdx++] = encodingArray[(ele1 >>> 2) & 0x3f];
            // 第2个成员,为ele1的后2位和ele2的前4位
            encodedBytes[encodedIdx++] = encodingArray[((ele1 << 4) | (ele2 >>> 4)) & 0x3f];
            // 第3个成员,为ele2的前4位和ele3的前2位
            encodedBytes[encodedIdx++] = encodingArray[((ele2 << 2) | (ele3 >>> 6)) & 0x3f];
            // 第4个成员,为ele3的后6位
            encodedBytes[encodedIdx++] = encodingArray[ele3 & 0x3f];
        }

        /* 更新尾部,tailLength的值会是1或2,0则不处理 */
        int tailEle1, tailEle2;

        // 当剩下1个字节未处理
        if (tailLength == 1) {
            // 最后1个未处理的字节
            tailEle1 = bytes[processLength] & 0xff;
            // 组第1个成员为tailEle1的前6位
            encodedBytes[encodedIdx++] = encodingArray[(tailEle1 >>> 2) & 0x3f];
            // 组第2个成员为这样1个字节,前2位为tailEle1的后2,其余位为0
            encodedBytes[encodedIdx++] = encodingArray[(tailEle1 << 4) & 0x3f];
            // 补2个=作填充
            encodedBytes[encodedIdx++] = padding;
            encodedBytes[encodedIdx++] = padding;
        }
        // 剩余2个字节未处理
        else if (tailLength == 2) {
            tailEle1 = bytes[processLength] & 0xff;
            tailEle2 = bytes[processLength + 1] & 0xff;
            // 组第1个成员为tailEle1的前6位
            encodedBytes[encodedIdx++] = encodingArray[(tailEle1 >>> 2) & 0x3f];
            // 组第2个成员为这样1个字节,前2位为tailEle1的后2,后4位为tailEle2的前4位
            encodedBytes[encodedIdx++] = encodingArray[((tailEle1 << 4) | (tailEle2 >>> 4)) & 0x3f];
            // 组第3个成员为这样1个字节,前
            encodedBytes[encodedIdx++] = encodingArray[(tailEle2 << 2) & 0x3f];
            // 补1个=作填充
            encodedBytes[encodedIdx++] = padding;
        }
        return encodedBytes;
    }
}
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值