Search This Blog

2013-05-01

Binary-to-text编码格式

目录  [+]

1 简介

Binary-to-text就是把任意一段二进制数据(字节数组)转换为可打印的ASCII字符串。8-bit(octet/byte)可以表示256个字符,ASCII包含128个字符(相当于7-bit),有95个可打印ASCII字符(ASCII码32到126),所以要用更少的bit来表示字符,从而全部转换为可打印ASCII字符。

Binary-to-text编码格式列表在http://en.wikipedia.org/wiki/Category:Binary-to-text_encoding_formats
只介绍其中几种,我把它们分为两类。

2 所有字符都用更少的bit来表示

可以是2/3/4/5/6-bit,那个列表里只发现3种,4/5/6-bit,即Base16, Base32, Base64。对应的RFC文档是The Base16, Base32, and Base64 Data Encodings - tools.ietf.org

2.1 Base64

Base 64 Encoding - tools.ietf.org
  • A 65-character subset of US-ASCII is used, enabling 6 bits to be represented per printable character. (The extra 65th character, "=", is used to signify a special processing function.) 总共使用65个ASCII字符,每个可打印字符使用6个bit来表示。第65个字符"="有特殊用途。
  • The encoding process represents 24-bit groups of input bits as output strings of 4 encoded characters. Proceeding from left to right, a 24-bit input group is formed by concatenating 3 8-bit input groups. These 24 bits are then treated as 4 concatenated 6-bit groups, each of which is translated into a single character in the base 64 alphabet. 8跟6的最小公倍数是24,每3个字节一组作为输入数据,每6个bit输出一个编码后的字符,固定输出4个字符。
  • Special processing is performed if fewer than 24 bits are available at the end of the data being encoded. 如果最后一组输入数据不足3个字节,那会有特殊处理。
  • The final quantum of encoding input is exactly 8 bits; here, the final unit of encoded output will be two characters followed by two "=" padding characters. 如果最后一组输入数据只有1个字节,那输出的字符串是2个字符加2个"="。
  • The final quantum of encoding input is exactly 16 bits; here, the final unit of encoded output will be three characters followed by one "=" padding character. 如果最后一组输入数据只有2个字节,那输出的字符串是3个字符加1个"="。
  • 允许的65个字符:[A-Za-z0-9+/=]

Base 64 Encoding with URL and Filename Safe Alphabet - tools.ietf.org
由于URL跟Filename不支持某些字符,所以这里改了一点字符映射,原理不变。
允许的65个字符:[A-Za-z0-9-_=]

)说明
  • 没有专门的小节提到解码,反过来处理就行了。
  • Base64#Implementations_and_history来看,Base64有不同的变种。编码跟解码可能在不同的机器上进行,可能会用不同的实现,一定要确认清楚。
  • 这个设计应该是针对实现优化过的,包括输入数据的长度、"="填充,既方便实现,又不拖累性能。
  • 在这一类里边,Base64编码后的数据是最节约空间的。
  • 应用广泛,我所了解的Base64在业界的使用情况:Jabber协议,通过Jabber服务器传送文件的时候,客户端1把文件分段编码为Base64并放入XML,然后发送到服务器;作为MIME的Content-Transfer-Encoding;作为HTTP Basic Auth编码;相似的应用场景可以考虑Base64。
  • Base64编码后的字符串很容易解码,最好不要直接在网络上传输用Base64编码的敏感数据。比如,使用HTTP Basic Auth作为认证方法的时候,最好使用HTTPS协议。
  • org.apache.commons.codec.binary.Base64.java并不验证用于解码的字符串是否合法。比如:new String(Base64.encodeBase64(Base64.decodeBase64("abcde".getBytes())))的结果是"abcd","abcde"是不合法的Base64字符串,提示非法会更好;如果在合法的Base64字符串中加入非法的字符,比如ASCII码为0的字符,还是不会报错,这也太纵容了。sun.misc.CharacterDecoder.decodeBuffer方法也不够严格。org.jivesoftware.smack.util.StringUtils.decodeBase64/org.jivesoftware.spark.util.Base64.decode方法就很严格。

2.2 Base32

原理跟Base64一样。

Base 32 Encoding - tools.ietf.org
  • 8跟5的最小公倍数是40,每5个字节一组作为输入数据,每5个bit输出一个编码后的字符,固定输出8个字符,最后一组输出数据中不足8个的部分用"="填充。
  • 允许的33个字符:[A-Z2-7=]。

)Base 32 Encoding with Extended Hex Alphabet
* 允许的33个字符是:[0-9A-V=]

)说明
跟Base64/Base16相比,Base32没有优势。没见过什么应用。

2.3 Base16

Base 16 Encoding - tools.ietf.org
  • Base 16 encoding is the standard case-insensitive hex encoding and may be referred to as "base16" or "hex". 每1个字节一组作为输入数据,每4个bit输出一个编码后的字符,固定输出2个字符。
  • 允许的字符:[0-9A-F],不区分大小写。

)说明
Base16应用广泛,下面一种类型的编码方法就用到了。

3 部分字符用更少的bit来表示

3.1 Quoted-printable

)定义
  • 任意字节可以被编码为3个字符,格式为"=byte.hex"。比如,ASCII字符"="必须被编码为"=3D"。
  • ASCII码在33到60或者62到126之间的ASCII字符不转换。
  • 如果ASCII字符tab跟space不出现在行末,那可以不转换;如果在行末,那需要分别编码为=09跟=20,也可以通过在后面添加"=\r\n"(软换行符)来让它们不被转换。
  • 如果即将被编码的数据包含有意义的换行符(比如在文本里边,可能是[\r\n]+),那它们必须被替换为"\r\n"(解码的时候会直接保留,可能导致编解码后的数据跟原来的数据不一样,但是对文本来说可能没问题),而不是"=byte.hex"格式。相反地,如果\r跟\n有行结束以外的含义,那它们必须分别被编码为=0D和=0A(比如在媒体文件里边,\r跟\n都可能存在,但是它们代表的是数据,而不是换行)。
  • 如果被编码后的数据多于76个字符,那必须换行,每行不多于76个字符(CRLF不计数)。为了满足这个需求,并且不改变被编码后的文本,需要加入软换行符。软换行符是"=\r\n",解码的时候需要忽略。这些软换行符使得没有换行符的文本或者包含很长的行的文本可以在行大小受限的环境下使用,比如,有些SMTP软件限制每行最多1000个字符,这在RFC 2821是允许的。

)参考资料

)说明
  • 定义一节括号里的内容是我额外加的,其他内容主要是按照wikipedia翻译。
  • 这个设计不够严谨,这决定了它的用途有限,只在MIME的Content-Transfer-Encoding里见过。不推荐使用,在相似的编码里边,URL encoding是更好的选择。

3.2 Percent-encoding

http://en.wikipedia.org/wiki/Percent-encoding
也叫做URL encoding,是一种URI编码机制。应用于URI、application/x-www-form-urlencoded形式的HTML表单数据。

)URI
URI包括URL跟URN,样式很多,RFC文档给了例子:Uniform Resource Identifier (URI): Generic Syntax # Examples - tools.ietf.org
URI允许两类字符:
  • 保留字符:!*'();:@&=+$,/?#[]。这些字符有特殊含义,具体含义在不同版本的规范里面略有不同。如果用户输入这些字符并且会破坏URI格式,那需要编码这些字符。
  • 非保留字符:符合正则表达式[A-Za-z0-9_.~-]的字符。这些字符可以编码,也可以不编码,比如,"%41"跟"A"是等价的。其他字符必须编码,成为"%HH"形式,包括'%'。

)Percent-encoding编码
把需要转换的ASCII字符转换为一个字节,非ASCII字符转换为UTF-8字节序列,再把每一个字节值(ASCII码)转换为"%byte.hex"。比如"%3F"代表"?","%E4%B8%AD"代表"中"。

)application/x-www-form-urlencoded形式的表单(不包含file元素的表单)
HTML4 # 17.13.4 Form content types - www.w3.org
这是默认的表单类型,提交的表单必须按如下方式编码:
    name跟value都需要编码。空格被替换为'+',保留字符按照RFC1738替换为"%HH"。换行符用CRLF表示。
    name/value按照表单里的顺序排列。name跟value用'='分隔,不同的name/value组合用'&'分隔。

)escape编码
支持不够广泛,ECMAScript有支持(有很多方言,常见的有JavaScript、ActionScript)。

4 实现的安全性

如果自行实现,那需要注意安全性,特别要注意不规范数据的处理,尤其是使用C/C++实现的时候,解析代码要预防缓冲区溢出之类的攻击。
RFC文档通常会包括安全注意事项(Security Considerations),可以参考一下。比如,The Base16, Base32, and Base64 Data Encodings # Security Considerations - tools.ietf.org

5 第三方实现的选择

谨慎地选择第三方实现,找广泛使用并测试过的实现,有时间的话就验证一下这个实现,哪怕这个实现很可信。如果是博客或论坛上给的源码,那必须对照规范验证这个实现。

)一次不谨慎选择引发的错误
刚工作不久的时候,我在网上找过一个Base64的实现,原因是我写的JavaME客户端需要用HTTP Basic Auth(要求的格式是:base64(bytes_of_username:password))。
测试账号没有问题,上线很久以后,有用户说无法登陆,这才发现问题。其实那段代码的出错概率很高,很长时间都没人发现问题,很可能是之前的服务器兼容性太强而没出错。
解决办法是使用org.apache.commons.codec.binary.Base64,没测试出问题。

)代码版权
在Google搜索"base64 cvtTable"可以很容易地找到这段代码,是一个叫做BasicAuth的Java类。我原来是在博客或论坛上摘取的代码,没有版权信息。修复问题的时候我没验证错误的那段代码,后来在写这篇文章的时候,搜索到了这段代码可能的原始出处,是有版权的:Copyright (c) 2000-2001 Sun Microsystems, Inc. All Rights Reserved.。
要注意网络代码的版权。

)这个BasicAuth类很简洁,只是可能出问题。这个类包含两个潜在的错误:
  • 如果input的长度是3的倍数,那解码的结果会多出4个ASCII码为0的字符。
    • 出错代码是byte[] output = new byte[((input.length / 3) + 1) * 4];,改为byte[] output = new byte[(input.length % 3 == 0 ? input.length / 3 : input.length / 3 + 1) * 4];。
  • 如果username:password包含非ASCII字符,那编码可能出错。
    • 这是HTTP协议允许的,只是没有规定如何处理这样的情况。出错代码是chunk = (input[i] << 16) | (input[i + 1] << 8) | input[i + 2]; chunk = (input[i] << 16) | (input[i + 1] << 8); chunk = input[i] << 16;,非ASCII字符通常被编码为多个扩展ASCII字符,也就是最高位为1的字节,这样的字节在做OR操作的时候会被扩展为负整数,这不符合要求。把chunk = input[i] << 16;改为chunk = (input[i] & 0xFF) << 16;,其他两个语句类似。另外,最好增加一个charset参数,getBytes(charset),便于跟Web服务器保持一致。

)测试代码
我是直接加到BasicAuth这个类的源码里面的。
Pre[-]
private void testBase64() throws UnsupportedEncodingException {
    String[][] npPairs = new String[][] { { "Aladdin", "open sesame" }, { "test1", "123456" }, { "中文名", "中文密码" } };
    for (String[] npPair : npPairs) {
        String np, auth, npDecoded = null;
        np = npPair[0] + ":" + npPair[1];
        auth = encode(npPair[0], npPair[1]);
        try {
            //TODO 3 ways to do base64 decoding, switch the comment to use it
            // non-strict decode
//                npDecoded = new String(org.apache.commons.codec.binary.Base64.decodeBase64(auth.getBytes()), "UTF-8");
            // strict decode
//                npDecoded = new String(org.jivesoftware.smack.util.StringUtils.decodeBase64(auth), "UTF-8");
            // non-strict decode
            npDecoded = new String(new sun.misc.BASE64Decoder().decodeBuffer(auth), "UTF-8");
        }
        catch (Exception e) {
            System.out.println(e);
        }
        System.out.println(String.format("np=%s, auth=%s, npDecoded=%s, equals=%b", np, auth, npDecoded,
                np.equals(npDecoded)));
    }
}

public static void main(String[] args) throws Exception {
    new BasicAuth().testBase64();
}

=文章版本=

201303
20130501 修改一下格式,顺便增减一部分内容
20130506 改为html格式

No comments:

Post a Comment