PDF文档的加解密及数字签名技术(二)
在对PDF文档加密之前的准备工作
既然加密需要密钥,那么密钥是怎么来的?首先我们要有两个口令,一个用户口令,一个权限口令,或者至少有其中之一,另一个为空口令。神马?你两个口令都不提供行不行?那还加个啥子密嘛。两个口令分别作什么用的,请自行复习前文。
让我们翻到PDF Reference 1.7的126页,从算法3.3开始。
算法3.3
第一步,如果没有权限口令,则用用户口令替代权限口令。如果权限口令长度超过32字节,则截断为32字节,如果不足32字节,则填充至32字节。用什么填充呢?有一个标准的填充串,定义如下:
const BYTE bytePadding[] = {0x28, 0xBF, 0x4E, 0x5E, 0x4E, 0x75, 0x8A, 0x41,
0x64, 0x00, 0x4E, 0x56, 0xFF, 0xFA, 0x01, 0x08,
0x2E, 0x2E, 0x00, 0xB6, 0xD0, 0x68, 0x3E, 0x80,
0x2F, 0x0C, 0xA9, 0xFE, 0x64, 0x53, 0x69, 0x7A};
如何填充呢,往权限口令后追加嘛,nLen就是得出的权限口令原长度。
memcpy(byteOwnerPassword+nLen, bytePadding, 32 - nLen);
第二步,将第一步的结果作为MD5函数的输入计算出HASH值。
第三步(仅在R为3或更高时,R是哪来的,自行复习前文),将输出作为输入再用MD5做HASH,且连做50遍。
第四步,将上一步的输出的前n字节作为RC4算法的密钥。n是前文中加密字典Length项/8得出的,根据V项不同,n可能会不同。
第五步,将用户口令按照第一步的方法做截断或填充。
第六步,用第四步得出的密钥对第五步的结果进行加密。
第七步(仅在R为3或更高时),做19遍以下操作:将第四步得出的密钥,每位分别与循环计数器做异或(XOR)操作,然后对前一步得出的结果用RC4算法进行加密。说起来头昏脑胀,还是免费送代码吧,再理解不了我也没办法了:
for (i = 1; i <=19; i++)
{
for (j = 0; j < 16; j++)
{
byteTempKey[j] = byteHashPassword[j] ^ i;
}
rc4.SetKey(byteTempKey, 16);
rc4.GetRC4(byteOwnerPassword, 32);
}
第八步,最终得出的就是加密字典的O项,我们得把这O项记在小本本上,生成加密字典对象或者后续操作都需要用到。
然后就是计算全局密钥了,啥叫全局密钥?因为PDF对每个对象加密的密钥都不同,但是都是在全局密钥的基础上生成,所以我们首先得有这么个玩意。这次我们要用的是算法3.2,请同学们翻到PDF Reference 1.7的125页。
算法3.2
第一步,对用户口令做截断或填充,方法前面已经提过了。
第二步,将第一步的结果输入到MD5函数中。
第三步,将计算出的O项输入到MD5函数中。(你看我们记在小本本上的O项有用了吧?)
第四步,将用户权限输入到MD5函数中。(记得我们说加密字典对象中提到过的奇怪负数吧,控制用户能干啥不能干啥那个。)
第五步,将文档ID输入到MD5函数中。(文档ID是文档的身份证,就是出现在交叉引用表部分的ID项的值。一般这东西是随机生成的,当然你可以在其中加点啥小彩蛋神马的。)
第六步(仅在R为4或更高时),如果你决定不加密Metadata,将4字节的0xFFFFFFFF输入到MD5函数中,这跟加密字典中的EncryptMetadata项取值有关。
第七步,将那么那么那么多输入用MD5做HASH。
第八步(仅在R为3或更高时),将输出作为输入再用MD5做HASH,且连做50遍。
第九步,最终结果的前n位就是全局加密密钥了,这个n请复习上文算法3.3的第四步的说明。
全局加密密钥我们也得拿小本本记起来。
有了全局加密密钥并不意味着我们工作的结束,我们还需要计算出加密字典的U项,以供验证用户口令之用。对于R项为2时,使用算法3.4,R为3或更高时,使用算法3.5。算法3.4我们这里不做介绍,想了解的读者自行翻看PDF手册。我们这里仅介绍算法3.5,这次在PDF Reference 1.7的127页。
算法3.5
第一步,就是算法3.2的结果,我们记在小本本上的全局密钥。
第二步,将那个固定的填充串(看写在算法3.3第一步处的那个。)输入到MD5函数中。
第三步,将文档ID输入到MD5函数中并将两个输入做HASH。
第四步,用全局密钥用RC4对第三步的结果进行加密。
第五步,做19遍以下操作:将全局密钥,每位分别与循环计数器做异或(XOR)操作,然后对前一步得出的结果用RC4算法进行加密。与算法3.3第七步类似。
最后得出的结果就是加密字典的U项。
PDF文档的加密
对PDF文档进行加密,其实只对其中的字符串(用括号“()”或者尖括号“<>”括起来的内容)以及流(Stream)进行加密,字典啦,交叉引用表啦,都还是文本形态老老实实摆在那里。加密的算法呢,只有AES和RC4两种,其中RC4支持40-128位的密钥,而AES密钥长度为128位,当然最新的PDF1.9已经支持256位AES密钥,这些都在前文中提到过。不过我们的算法基于PDF1.7,密钥最长为128位。1.9版本的密钥只要对提到的算法稍作变动即可。
有了全局密钥,并不能直接用它对PDF文档进行加密,前文提到过,每个对象的加密密钥都是不同的,具体到一个对象的密钥,算法是PDF Reference 1.7的119页的算法3.1。
算法3.1
第一步,取得要加密的字符串或者流所在的对象编号和对象代次号(9 0 obj的对象,9是对象编号,0是代次号)。
第二步,将对象编号附加到全局密钥之后,对象编号在前,代次号在后,对象编号占第n、n+1、n+2三字节,代次号占n+3、n+4两字节。说明一下,这里提到的位置是Base 0的以免引起混乱。如果使用AES算法,则还需要做“加盐”操作,将4字节的“盐”再附加在对象编号和代次号之后。“盐”的声明如下:
const BYTE AES_Salt[] = {0x73, 0x41, 0x6c, 0x54};
聪明的同学看出来了,就是"sAlT"四个字母的ASCII码。真够“盐”的。
第三步,将第二步的结果用MD5做HASH。
第四步,用第三步结果的前n+5字节(最长为16字节)作为对象加密密钥。
既然有了加密密钥,就可以用其对PDF文档内容(字符串及流)进行加密了,然后再把加密字典加入PDF文档,这个PDF的加密就完成了。
一个字符串,加密前是这样的:
(involute@sina.com)
RC4加密后变成这样:
<3CF63984481EAF626081105E2D4D32B070>
AES加密后就变这样了:
<2DA128BE0D2E1375070D756E797A3FB948E4366E5ED42F62C3322044F84268C9D1074D2CD8D3EA9D7C047A87B348DA02>
关于加密需要一提的问题
只设置了权限口令的PDF,打开时并不需要密码也能看见内容,是不是文档内容就没加密?错!当然加密了。只不过阅读软件在打开该PDF时,可以将密钥根据加密字典O项按照算法3.2计算出来。所以只设置了权限口令,用来限制用户复制内容和打印等操作的PDF,实际上是使用一种防君子不防小人的手段。虽然如果一个程序宣称自己遵守PDF规范的话,是会按照权限的规定限制用户操作的。但是呢,不愿意遵守PDF规范的程序大把,比如据说QQ邮箱内置了PDF转文本的功能,就无视加密PDF文档的权限规定。
既然只加了权限口令的PDF文件不安全,那加上用户口令,是不是就比较安全了?当然,加上了用户口令的PDF至少可以避免不必要的扩散,按照算法3.2,不提供用户口令就无法根据O项计算出全局密钥,也就没法解密内容阅读文档了。
但是现在有暴力法破解PDF,就是尝试用不同的用户口令使用算法3.5去计算,如果结果与加密字典U项相同,那么该口令就是用户口令。所以要加用户口令,还是加得长点吧。理论上超过16字节的复杂口令(含有大小写字符,数字,甚至乱七八糟的其他字符的最好)是无法破解的,所耗时间太长了,更别提口令可以支持到32字节呢。但是自己千万不要忘了这个口令,一旦记不起来,神仙也帮不了你。
总的来说,用口令加密的方法加密的PDF,并不是太安全,我绝对不推荐用这种方法加密非常敏感的PDF文档。
安全性较好的加密,是用数字证书进行加密,有一定PKI体系知识基础的同学都清楚,用一份证书的公钥加密的数据,只有证书持有人的私钥才能解密,安全性大大的好。不过本文不探讨这种加密方式。具体的实现,PDF手册中有详细说明,有了口令加密的基础,理解证书加密方式也不难了。
关于AES算法再啰嗦几句。PDF中的AES算法,是CBC的Padding模式。AES算法需要一个初始向量(IV),可以随机生成,但是要将随机向量放在密文的最前,拿128位AES算法来说,要将密文的128/8=16字节用来存放IV。不熟悉AES算法,听不懂我在说什么的同学,请自行寻找AES算法的相关知识阅读。用AES加密后的数据,长度会有变化,因此流对象字典中的Length要变成新的长度,其它项(包括编码方式)都不用动。
本来想今天把解密也写上的,啰嗦半天有点累了,解密留到下篇说。