Base64算法
Base64算法概述
Base64算法最早应用于解决电子邮件传输的问题。早期,由于“历史问题”,电子邮件只允许ASCII码字符,如果邮件中包含非ASCII码字符,当它通过有“历史问题”的网关时,这个网关会对该字符的二进制位进行调整,即将其8位二进制码的最高位置0,这样用户收到的邮件就会是一封乱码。为了解决这个问题,产生了Base64算法。
Base64主要用于将不可打印的字符转换成可打印字符,或者简单的说将二进制数据编码成ASCII字符(注:可打印的)。
将二进制数据编码成ASCII字符主要的目的是能在纯文本内容中插入二进制数据,常见的应用场景包括:
1. 电子邮件
这个可参考阮一峰的《MIME笔记》。
2. 微软的MHT格式
这是模仿邮件格式将多种资源打包在一个文件中的格式,所有二进制资源都采用 Base64 编码。
3. XML文件
这是一个纯文本文件,如果要基于 XML 格式设计可以保存图片或其它附件的数据格式,那就需要将这些二进制数据转码成 ASCII 字符。
4. DATA URL
最近流行起来的 Data URL,要在URL中使用二进制数据,当然也只能进行 ASCII 编码。
当然除了 Base64 之外,还有其它一些编码方式可以将二进制数据编码成 ASCII 字符,比如十六进制编码,除此之外还有 Quoted-printable 等。甚至 URL 中使用 %XX 来对非 ASCII 字符进行编码的方式也可以算在内。
当然一般非特定环境下,选用十六进制编码和 Base64 编码的情况比较多,主要是因为这两种编码易用,而且转换后的数据量相对较小。
十六进制编码是将 1
个字节编码成 2
个十六进制字符,比如 0x10110110
编码成 B6
,转换后数据量会增大 1 倍
。
Base64 编码是将 3
个字节共 24
位数据,以每 6
位一个 Base64 字符 [0-9a-zA-Z+/]
表示,24
位数据共需要 4
个 Base64 字符表示,编码后数据增长约 1/3
。为什么是“约”?因为如果原数据字节数不是 3 的倍数,需要补位,这样转换出来的数据量就会比原来的 4/3
略多一点。
从上面的数据增长比来看,Base64编码 比十六进制编码更节省磁盘容量,所以一般较大的数据需要进行 ASCII 编码多采用 Base64;而较小的数据,则使用易于人工识别十六进制(用纸笔就能解码出来)。
Base64算法基本原理
Base64算法的转换方式类似于古典加密算法里的单表置换算法 。RFC 2045中给出了Base64的字符映射表,如下图所示。
这张字符映射表中,Value是十进制编码,Encoding是字符,共映射了64个字符,这也是Base64算法命名的由来。映射表的最后一个字符“=”是用来补位的。
Base64算法的编码和解码操作可用作加密解密,但是Base64的字符映射表是公开的,因此并不能叫做加密算法。
Base64算法主要是将给定的字符以字符编码(如ASCII、UTF-8等)对应的十进制数为基准,做编码操作:
1. 将给定的字符串以字符为单位,转换为对应的字符编码。
2. 将获得的字符编码转换为二进制串。
3. 将获得的二进制串做分组转换操作,每3个8位的二进制串为一组,将这样的一组再转换为4个6位二进制串,不足6位时低位补0。
4. 对每组4个6位二进制串补位,即向6位二进制串的高位补两个0,生成4个8位二进制串。
5. 将获得的4-8二进制码转换为十进制码。
6. 将获得的十进制码用Base64字符映射表中对应的字符替换。
经过Base64编码后的数据会比原始数据略长,为原来的4/3倍,编码后的字符数是4的倍数。
编码后的字符串最多有2个补位的“=”,因为原始数据的二进制串的分组是以3个8位为一组的,余数 = 原始数据字节数 mod 3,余数只能为0、1、2。如果余数为0,3个8位转换为4个6位,高位补0之后是4个8位,则不需要补位符;如果余数为1,1个8位只能转换为2个6位,高位补0之后是2个8位,为了让编码之后的字符数是4的倍数,要补两个补位符;同理,如果余数为2,要补一个补位符。
ASCII码进行Base64编码的例子如下图,字符“A”编码之后的字符串为“QQ==”。
非ASCII码如GBK、UTF-8等编码,一个字符包含多个字节,如UTF-8用3个字节表示一个汉字,GBK用2个字节表示一个汉字。以字符串“密”为例,对应的UTF-8编码是-27、-81、-122,用Base64编码如下图,编码后的字符串为“5a+G”。
具体参考:Java加密与解密 - Base64算法 (这个好) java加密解密研究3、Base64算法
Base64算法的实现
Java API中没有Base64的实现,实际上Sun也有Base64算法的实现,但是没有公布。Bouncy Castle提供了一般Base64算法的实现,Commons Codec提供了基于RFC 2045相关定义的Base64算法实现。
Bouncy Castle遵循的是一般Base64算法,就是根据字符映射表做了编码转换。Commons Codec中既支持RFC 2045定义的Base64算法,也支持一般的Base64算法。这两种的差异是RFC 2045定义的算法要求在编码后的字符串中换行和末尾添加回车换行符。
注:RFC 2045中规定,在电子邮件中,每行为76个字符,每行末需添加一个回车换行符("\r\n"),不管每行是否够76个字符,都要添加一个回车换行符。不过在实际应用中,根据实际需要,这一要求往往被忽略。
kSOAP中也提供Base64的实现,具体如下:
package org.kobjects.base64; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; public class Base64 { static final char[] charTab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".toCharArray(); public static String encode(byte[] paramArrayOfByte) { return encode(paramArrayOfByte, 0, paramArrayOfByte.length, null).toString(); } public static StringBuffer encode(byte[] paramArrayOfByte, int paramInt1, int paramInt2, StringBuffer paramStringBuffer) { if (paramStringBuffer == null) paramStringBuffer = new StringBuffer(paramArrayOfByte.length * 3 / 2); int i = paramInt2 - 3; int j = paramInt1; int k = 0; int l; while (j <= i) { l = (paramArrayOfByte[j] & 0xFF) << 16 | (paramArrayOfByte[(j + 1)] & 0xFF) << 8 | paramArrayOfByte[(j + 2)] & 0xFF; paramStringBuffer.append(charTab[(l >> 18 & 0x3F)]); paramStringBuffer.append(charTab[(l >> 12 & 0x3F)]); paramStringBuffer.append(charTab[(l >> 6 & 0x3F)]); paramStringBuffer.append(charTab[(l & 0x3F)]); j += 3; if (k++ < 14) continue; k = 0; paramStringBuffer.append("\r\n"); } if (j == paramInt1 + paramInt2 - 2) { l = (paramArrayOfByte[j] & 0xFF) << 16 | (paramArrayOfByte[(j + 1)] & 0xFF) << 8; paramStringBuffer.append(charTab[(l >> 18 & 0x3F)]); paramStringBuffer.append(charTab[(l >> 12 & 0x3F)]); paramStringBuffer.append(charTab[(l >> 6 & 0x3F)]); paramStringBuffer.append("="); } else if (j == paramInt1 + paramInt2 - 1) { l = (paramArrayOfByte[j] & 0xFF) << 16; paramStringBuffer.append(charTab[(l >> 18 & 0x3F)]); paramStringBuffer.append(charTab[(l >> 12 & 0x3F)]); paramStringBuffer.append("=="); } return paramStringBuffer; } static int decode(char paramChar) { if ((paramChar >= 'A') && (paramChar <= 'Z')) return paramChar - 'A'; if ((paramChar >= 'a') && (paramChar <= 'z')) return paramChar - 'a' + 26; if ((paramChar >= '0') && (paramChar <= '9')) return paramChar - '0' + 26 + 26; switch (paramChar) { case '+': return 62; case '/': return 63; case '=': return 0; } throw new RuntimeException("unexpected code: " + paramChar); } public static byte[] decode(String paramString) { ByteArrayOutputStream localByteArrayOutputStream = new ByteArrayOutputStream(); try { decode(paramString, localByteArrayOutputStream); } catch (IOException localIOException) { throw new RuntimeException(); } return localByteArrayOutputStream.toByteArray(); } public static void decode(String paramString, OutputStream paramOutputStream) throws IOException { int i = 0; int j = paramString.length(); while (true) { if ((i < j) && (paramString.charAt(i) <= ' ')) ++i; if (i == j) return; int k = (decode(paramString.charAt(i)) << 18) + (decode(paramString.charAt(i + 1)) << 12) + (decode(paramString.charAt(i + 2)) << 6) + decode(paramString.charAt(i + 3)); paramOutputStream.write(k >> 16 & 0xFF); if (paramString.charAt(i + 2) == '=') return; paramOutputStream.write(k >> 8 & 0xFF); if (paramString.charAt(i + 3) == '=') return; paramOutputStream.write(k & 0xFF); i += 4; } } }