返回

解决FPDF SetProtection在Amazon Linux 2023上需密码问题

php

FPDF SetProtection 在 Amazon Linux 2023 上需要密码的问题解决

最近把系统从 Amazon Linux 2 迁移到 Amazon Linux 2023,结果发现 FPDF 的 SetProtection 脚本出了点问题。原本只是用来加密 PDF 文件、限制只允许打印的,现在打开时却要输密码,可我们压根没设置过密码!

问题根源:加密函数 RC4 的差异

深挖了下,发现问题出在 RC4 函数上。这函数调用了 openssl_encrypt 来实现加密。那个 SetProtection 脚本 (http://www.fpdf.org/en/script/script37.php) 提供了一个纯 PHP 的备用方案,在 openssl_encrypt 不可用时使用,这个备用方案在新系统上倒是能正常工作。 这让我觉得,多半是 OpenSSL 的什么地方不对劲。 也有可能是新版 PHP 改变了啥行为。

PDF 生成过程中,日志里也没看到报错信息。

更具体一点, return openssl_encrypt($data, 'RC4-40', $key, OPENSSL_RAW_DATA); 这行代码在新系统上啥也没返回, 旧系统上则有“东西”返回。 尝试过修改不同的加密方法,都没用(也正常,毕竟我对这块不太懂…)。

解决方案

下面提供几种解决思路,大家可以根据实际情况选用:

1. 使用纯 PHP 的 RC4 实现 (最简单粗暴)

这是最简单的方法,直接用脚本提供的备用方案,绕过 openssl_encrypt

原理: FPDF SetProtection 脚本自带了一个纯 PHP 实现的 RC4 加密算法,用于在没有 OpenSSL 扩展的情况下进行加密。

操作步骤:

  1. 找到 FPDF 类文件中的 RC4 函数 (通常在 fpdf.php 或你包含 SetProtection 脚本的文件里)。

  2. 注释掉使用 openssl_encrypt 的那一行,解除注释使用纯 PHP 实现的那一行。

    function RC4($key, $data)
    {
    	// Use the PHP-only version.
    	//if(function_exists('openssl_encrypt'))
    	//{
    	//	return openssl_encrypt($data, 'RC4-40', $key, OPENSSL_RAW_DATA);
    	//}
    	$s = array();
    	for($i=0;$i<256;$i++)
    	{
    		$s[$i] = $i;
    	}
    	$j = 0;
    	for($i=0;$i<256;$i++)
    	{
    		$j = ($j + $s[$i] + ord($key[$i%strlen($key)])) % 256;
    		$t = $s[$i];
    		$s[$i] = $s[$j];
    		$s[$j] = $t;
    	}
    	$a = 0;
    	$j = 0;
    	$r = '';
    	for($i=0;$i<strlen($data);$i++)
    	{
    		$a = ($a + 1) % 256;
    		$j = ($j + $s[$a]) % 256;
    		$t = $s[$a];
    		$s[$a] = $s[$j];
    		$s[$j] = $t;
    		$k = $s[($s[$a] + $s[$j]) % 256];
    		$r .= chr(ord($data[$i]) ^ $k);
    	}
    	return $r;
    }
    

安全性: 因为这个纯 PHP 实现用的是 RC4-40,密钥长度只有 40 位。这种加密强度较低,易被破解,如果对安全性要求高,慎用。

2. 检查并修复 OpenSSL 环境

可能系统上的 OpenSSL 环境配置有问题,或者某些必要的库没装。

原理: openssl_encrypt 函数依赖于正确配置的 OpenSSL 环境。如果环境有问题,函数可能无法正常工作。

操作步骤:

  1. 确认 OpenSSL 扩展已启用:phpinfo() 查看 openssl 扩展是否加载。没加载的话,需要在 php.ini 中启用。

    # 找到 php.ini (可以用 php --ini 命令查找)
    # 取消 extension=openssl 的注释
    

    重启php-fpm 或者web服务器。

  2. 检查 OpenSSL 版本兼容性: 虽然可能性小, 但也可以检查一下 PHP 和 OpenSSL 的版本兼容性,看官方文档有没有相关说明。

  3. 确认系统库依赖:

    # 检查OpenSSL库文件是否存在并完整
    sudo yum reinstall openssl
    ldd $(which php) | grep ssl #查看php链接的ssl库。
    
  4. 查看详细的错误信息(进阶): 可以用更底层的调试方法, 比如 strace 去追踪 PHP 进程, 看看调用 openssl_encrypt 时具体发生了什么错误:

    # 找到你的PHP进程ID(假设为1234)
    strace -p 1234 -e trace=network,file,process,signal -f -o /tmp/phptrace.log
    
    # 重现PDF生成的操作. 然后查看 /tmp/phptrace.log
    

分析日志可能需要一些系统编程知识。

3. 使用其他加密方法 (进阶, 如果您了解加密相关)

如果对安全性要求高,可以考虑换个更强的加密算法。但改动会大一些。

原理: FPDF 的 SetProtection 支持多种加密算法,你可以通过修改脚本,用更安全的算法 (比如 AES) 来替代 RC4-40。

操作步骤 (以 AES-128 为例):

  1. 修改 _putencryption 函数: FPDF 内部用 _putencryption 函数生成加密字典。找到它,修改里面的算法和密钥长度参数。

  2. 修改 RC4 函数 (如果其他地方也用到了): 将所有调用 RC4 的地方,都换成新算法对应的函数 (比如 openssl_encrypt 加上正确的 AES 参数)。

  3. 重新计算加密密钥等 : 因为算法和参数变了, 你要根据 PDF 规范,重新计算 UserPasswordOwnerPassword 这些值.

    //在_putencryption中修改:
    //$filter = '/Standard';  //保留此项,因为它表示使用的是标准安全处理器。
      ...
    	//$v = 1;  //或者 $v =2,但是这里设置了P权限,如果这里使用v=2会要求v=45$v = 4;     // 必须与算法匹配 (12 表示 RC4, 4/5 表示 AES)
    	$m = ($this->ProtectionParams['P'] >> 2) + 1; //计算加密方法的数值。根据允许的权限推导出的数字
    
    	//If $v = 4 and using AES128:
        $this->enc_obj_num = $n + 1; // Store the encryption object number
        // $Length 对应 KeyLength  长度必须修改,单位是bit
        $Length=128;
       ...
    
        //_hexmarker的定义为
        //$this->_hexmarker=chr(0xFD).chr(0xFC).chr(0xFB).chr(0xFA);
    	...
    

    再在RC4函数那里,将加密算法更换为:

     function RC4($key, $data)
    {
      //根据需求修改:   $cipher = 'aes-128-cbc'; // 或其他
         $cipher = 'aes-128-cbc'; //例如
      if(function_exists('openssl_encrypt'))
        {
            return openssl_encrypt($data, $cipher, $key, OPENSSL_RAW_DATA);
         }
    	//  ...  这里原来的RC4代码不需要了,当然,你需要一个PHP的aes的加密函数来实现同样的操作。
    }
    

安全性: AES-128 或更强的算法安全性更高, 可以有效保护 PDF 内容。 但代码修改量较大, 风险较高,务必仔细测试。

重要提示 : 对PDF底层结构以及加密有了解才进行此修改! 修改时要格外小心!

4.使用低版本的php或旧的openssl库(不推荐)

除非您已经用完了上面的所有选项,否则您再来选择这条。
尝试回退 PHP 或 OpenSSL 到旧版本, 看能不能解决. (只建议在测试环境尝试!)

原理是降低版本使其重新可以被RC4-40正常加密,虽然简单,但是不可避免的引入了安全风险!

步骤

降级php或者openssl到兼容版本,具体的yum 指令为 yum downgrade <package_name>,您可以查找旧版本进行尝试,操作之前请做好充分备份
安全性 : 回退到旧版本,意味着放弃了新版本中的安全补丁和改进。系统可能面临已知的安全漏洞。非常不推荐长期使用此方案。