我曾经在很多场合多次反对过将MD5作为一种“加密”过程的提法。因为MD5是散列函数,其输入是任意长度而输出是固定长度的,这就意味着如果它是加密,则怎么可能通过一个固定大小的空间(2的128次方)表示无限多的内容呢?

但是,这不意味着MD5不能用作加密。实际上,不但能,还可以构成强度不错的加密形式(绝不是维吉尼亚密码那种坑爹的形式)。请听我细细道来。

其实评价一个加密算法的好坏,就是看算法对输入的数据作了变换之后,输出的东西到底有多么随机。假如不知道密钥,我们希望输出的数据和随机的数据相比,对于第三方是无法辨识的。

如果我们能借助MD5之类的散列函数,能达到这样一种效果,即在给定一个密钥之后,一段程序可以(在实用范围内)无限制产生随机的比特流,并且看上去和随机数发生器产生的结果差不多都无法区分,则这种方法就很有意义。

理论上最完美的加密是一次一密,就是让密钥和明文一样长,而每次加密密钥都变化。假如密钥是随机的,那么结果也是。因为这种情况下,你光截获密文,而不知道密钥,实际上截获了的信息除了明文的长度没有其他。

但是和明文一样长的密钥实际上不可能使用得了,这样,如果我们能用相对短的密钥,在任何我们需要的时候构建一个伪随机的数据流,并且还保证不知道密钥的话数据流的可能足够多以至于不能穷举——则我们仍然实现了上述目标。

构建随机数据流的发生器的方法这里就简单了。因为我们的函数——无论是AES还是MD5——在一定程度上说都可以是相当满足如下条件:在输入数据哪怕变化只有一个比特时,也会产生完全不同的输出。

那么我们就构建一系列字符串,根据一个规则:前1-16字节是密钥,后17-32字节是进行本次加密开始随机找到的一个16字节的字符串,最后4字节是计数器,比如:

MyUltraSecretKeySOMErandomSTRING0001

MyUltraSecretKeySOMErandomSTRING0002

MyUltraSecretKeySOMErandomSTRING0003

我们对每个字符串进行MD5,得到一系列结果:

28f96f2ba91124373149c5c4295e5e77
3accb1e071b9ffdf503c575a57085916
da43cd70d8032b3783f41adce53fb483
…

把这些结果拼接起来,在最前面附上随机的16字节:

SOMErandomSTRING28f96f2ba91124373149c5c4295e5e773accb1e0
71b9ffdf503c575a57085916da43cd70d8032b3783f41adce53fb483

因为上述计数器不断变化,这就得到了足够长的伪随机数据流了。下一步,将输入的明文和这个数据流从第17字节开始按位异或即得到密文。

解密的时候,过程几乎相同,首先取出随机的字节SOMErandomSTRING,然后和密钥重新构建伪随机数据流,再次按位异或即可。

值得一提的是,SOMErandomSTRING一定要每次加密更换。毕竟这不是太麻烦的事情,而且有安全的优势。如果密钥和这个随机参数都不更换,则当数据流发生器用得太久,一定会产生重复的128比特结果(虽然工程上可能性不大)。这就可以区分出数据流不是随机数发生器产生的,也就理论上意味着不安全了。

还有一个问题,就是这种数据流的加密,不能保证密文的完整。密文中翻转一个比特,则明文也受到影响(而解密可以继续)。所以既然手头正好有一个散列函数,务必记得同时传送明文的散列,以资解密之后校验。

最后,本文是以MD5为例的,但很多不错的散列函数都可以应用这一结果。在实现环境中只有几种有限的散列函数时(比如有些虚拟主机,或者带有硬件散列模块的芯片上),这是一种可以考虑的替代方案。但是要指出,MD5的速度和专职的加密算法比起来优势并不明显。