From ff2501aa8f7dbf9422dca413d1a563abe40fcefc Mon Sep 17 00:00:00 2001 From: erikn69 Date: Mon, 4 May 2026 10:18:54 -0500 Subject: [PATCH 1/4] Re-arranging the code a bit to allow smoother upgrade to 128 bit encryption Added a link to the GitHub repository for PDF Protection. --- scripts/PDFProtection/PDFProtectionTrait.php | 98 ++++++++++++++------ 1 file changed, 68 insertions(+), 30 deletions(-) diff --git a/scripts/PDFProtection/PDFProtectionTrait.php b/scripts/PDFProtection/PDFProtectionTrait.php index fb6014a..172df6c 100644 --- a/scripts/PDFProtection/PDFProtectionTrait.php +++ b/scripts/PDFProtection/PDFProtectionTrait.php @@ -3,15 +3,19 @@ namespace FPDF\Scripts\PDFProtection; //http://www.fpdf.org/en/script/script37.php + //https://github.com/klemenv/FPDF_Protection trait PDFProtectionTrait { protected $encrypted = false; //whether document is protected - protected $padding; - protected $encryption_key; protected $Uvalue; //U entry in pdf document protected $Ovalue; //O entry in pdf document protected $Pvalue; //P entry in pdf document - protected $enc_obj_id; //encryption object id + protected $enc_obj_id; //encryption PDF object id + protected $enc_algorithm; //Encryption algorithm, RC4 or AES + protected $enc_security_handler;//Security handler version, 2 for basic, 3 for AES and 40+ bits + protected $enc_key; //Key used for encryption + protected $enc_key_len; //Number of bytes used for encryption key + protected $id; //Document ID /** * Set permissions as well as user and owner passwords @@ -36,13 +40,18 @@ public function SetProtection (array $permissions = [], ?string $user_pass = nul $protection += $options[$permission]; } + $this->enc_key_len = 5; + $this->id = uniqid().__FILE__.rand(); + if ($owner_pass === null) { $owner_pass = uniqid((string) rand()); } $this->encrypted = true; - $this->padding = "\x28\xBF\x4E\x5E\x4E\x75\x8A\x41\x64\x00\x4E\x56\xFF\xFA\x01\x08\x2E\x2E\x00\xB6\xD0\x68\x3E\x80\x2F\x0C\xA9\xFE\x64\x53\x69\x7A"; - $this->_generateencryptionkey((string) $user_pass, $owner_pass, $protection); + $this->_setOvalue($owner_pass, $user_pass); + $this->_setEncryptionKey($user_pass, $protection); + $this->_setUvalue(); + $this->_setPvalue($protection); } protected function RC4 (string $key, string $data) { @@ -112,7 +121,10 @@ protected function _textstring($s) { * Compute key depending on object number where the encrypted data is stored */ protected function _objectkey ($n) { - return substr($this->_md5_16($this->encryption_key.pack('VXxx',$n)), 0, 10); + $key = $this->enc_key.pack('VXxx', $n); + $len = $this->enc_key_len + 5; + + return substr($this->_md5_16($key), 0, $len); } protected function _putresources () { @@ -132,7 +144,8 @@ protected function _encrypresources () { } protected function _putencryption () { - $this->_put('/Filter /Standard'); + $this->_put('/Filter'); + $this->_put('/Standard'); $this->_put('/V 1'); $this->_put('/R 2'); $this->_put('/O (' . $this->_escape($this->Ovalue) . ')'); @@ -143,49 +156,74 @@ protected function _putencryption () { protected function _puttrailer () { parent::_puttrailer(); if ($this->encrypted) { + $id = md5($this->id); $this->_put('/Encrypt '.$this->enc_obj_id.' 0 R'); - $this->_put('/ID [()()]'); + $this->_put('/ID [ <'.$id.'> <'.$id.'> ]'); } } /** - * Get MD5 as binary string + * Get MD5 as 16 byte binary string */ protected function _md5_16 ($string) { return md5($string, true); } + private function _pad ($string, $len = 0) { + $padding = "\x28\xBF\x4E\x5E\x4E\x75\x8A\x41\x64\x00\x4E\x56\xFF\xFA\x01\x08". + "\x2E\x2E\x00\xB6\xD0\x68\x3E\x80\x2F\x0C\xA9\xFE\x64\x53\x69\x7A"; + if ($len == 0) { + $len = strlen($padding); + } + + return substr($string.$padding, 0, $len); + } + /** - * Compute O value + * Compute O (owner password) value + * + * Depends on following member variables: + * - enc_key_len */ - protected function _Ovalue ($user_pass, $owner_pass) { - $tmp = $this->_md5_16($owner_pass); - $owner_RC4_key = substr($tmp,0,5); - return $this->RC4($owner_RC4_key, $user_pass); + protected function _setOvalue ($owner_pass, $user_pass) { + $key = $this->_md5_16( $this->_pad($owner_pass, 32) ); + $key = substr($key,0,$this->enc_key_len); + $encrypted = $this->RC4($key, $this->_pad($user_pass, 32) ); + $this->Ovalue = $encrypted; } /** - * Compute U value + * Compute U (user password) value + * + * Depends on following member variables: + * - enc_key + * - enc_key_len */ - protected function _Uvalue () { - return $this->RC4($this->encryption_key, $this->padding); + protected function _setUvalue () { + $padding = $this->_pad('', 32); + $encrypted = $this->RC4($this->enc_key, $padding); + $this->Uvalue = $encrypted; } /** - * Compute encryption key + * Set Pvalue */ - protected function _generateencryptionkey ($user_pass, $owner_pass, $protection) { - // Pad passwords - $user_pass = substr($user_pass.$this->padding,0,32); - $owner_pass = substr($owner_pass.$this->padding,0,32); - // Compute O value - $this->Ovalue = $this->_Ovalue($user_pass,$owner_pass); - // Compute encyption key - $tmp = $this->_md5_16($user_pass.$this->Ovalue.chr($protection)."\xFF\xFF\xFF"); - $this->encryption_key = substr($tmp,0,5); - // Compute U value - $this->Uvalue = $this->_Uvalue(); - // Compute P value + protected function _setPvalue ($protection) { $this->Pvalue = -(($protection^255)+1); } + + /** + * Compute encryption key + * + * Depends on following member variables: + * - Ovalue + * - enc_key_len + */ + protected function _setEncryptionKey ($user_pass, $protection) { + $user_pass = $this->_pad($user_pass, 32); + $id = $this->_md5_16($this->id); + $hash = $this->_md5_16($user_pass.$this->Ovalue.pack("V", $protection | 0xFFFFFF00).$id); + + $this->enc_key = substr($hash, 0, $this->enc_key_len); + } } From cc2dff7a77146d69962224cddd39fd443f9e7729 Mon Sep 17 00:00:00 2001 From: erikn69 Date: Mon, 4 May 2026 11:45:11 -0500 Subject: [PATCH 2/4] Added RC4 encryption up to 128 bits --- scripts/PDFProtection/PDFProtectionTrait.php | 111 ++++++++++++++++--- 1 file changed, 98 insertions(+), 13 deletions(-) diff --git a/scripts/PDFProtection/PDFProtectionTrait.php b/scripts/PDFProtection/PDFProtectionTrait.php index 172df6c..56d74a2 100644 --- a/scripts/PDFProtection/PDFProtectionTrait.php +++ b/scripts/PDFProtection/PDFProtectionTrait.php @@ -27,21 +27,61 @@ trait PDFProtectionTrait { * - annot-forms * @param string $user_pass If a user password is set, user will be prompted before document is opened * @param null|string $owner_pass If an owner password is set, document can be opened in privilege mode with no restriction if that password is entered + * @param string $algorithm must be one of 'ARCFOUR' or 'AES' + * @param int $bits is number of bits used as encryption algorithm, must be a multiple of 8 in range between 40 and 128 + * + * Output PDF is version 1.3 when using RC4 40bit or 1.4 when using RC4 40+bit or AES encryption. + * * @return void */ - public function SetProtection (array $permissions = [], ?string $user_pass = null, ?string $owner_pass = null) : void { + public function SetProtection (array $permissions = [], ?string $user_pass = null, ?string $owner_pass = null, string $algorithm = "RC4", int $bits = 40) : void { + $this->id = uniqid().__FILE__.rand(); $options = ['print' => 4, 'modify' => 8, 'copy' => 16, 'annot-forms' => 32]; $protection = 192; foreach ($permissions as $permission) { if (!isset($options[$permission])) { $this->Error('Incorrect permission: ' . $permission); + } else { + $protection += $options[$permission]; } - $protection += $options[$permission]; } - $this->enc_key_len = 5; - $this->id = uniqid().__FILE__.rand(); + if (strncmp($algorithm, "RC4", 7) !== 0 && strncasecmp($algorithm, "AES", 3) !== 0) { + $this->Error('Invalid encryption algorithm '.$algorithm.', supported RC4 and AES'); + + return; + } + + if (strncasecmp($algorithm, "AES", 3) === 0) { + $this->Error('Encryption algorithm AES, bo supported yet'); + + return; + } + + $bits = intval($bits); + if ($bits < 40 || $bits > 128) { + $this->Error('Number of bits limited between 40 and 128'); + + return; + } + + if (($bits % 8 ) != 0) { + $this->Error('Number of bits not a multiple of 8'); + + return; + } + + $this->enc_key_len = $bits / 8; + $this->enc_algorithm = strtoupper(substr($algorithm,0,3)); + if ($bits == 40 && strcmp($this->enc_algorithm, "RC4") == 0) { + $this->enc_security_handler = 2; + } else { + $this->enc_security_handler = 3; + if ($this->PDFVersion<'1.4') { + $this->PDFVersion = '1.4'; + } + } if ($owner_pass === null) { $owner_pass = uniqid((string) rand()); @@ -54,7 +94,7 @@ public function SetProtection (array $permissions = [], ?string $user_pass = nul $this->_setPvalue($protection); } - protected function RC4 (string $key, string $data) { + protected function ARCFOUR (string $key, string $data) { static $last_key; static $last_state; @@ -100,7 +140,7 @@ protected function RC4 (string $key, string $data) { protected function _putstream ($s) { if ($this->encrypted) { - $s = $this->RC4($this->_objectkey($this->n), $s); + $s = $this->ARCFOUR($this->_objectkey($this->n), $s); } parent::_putstream($s); } @@ -111,7 +151,7 @@ protected function _textstring($s) { } if ($this->encrypted) { - $s = $this->RC4($this->_objectkey($this->n), $s); + $s = $this->ARCFOUR($this->_objectkey($this->n), $s); } return '(' . $this->_escape($s) . ')'; @@ -146,8 +186,14 @@ protected function _encrypresources () { protected function _putencryption () { $this->_put('/Filter'); $this->_put('/Standard'); - $this->_put('/V 1'); - $this->_put('/R 2'); + if ($this->enc_security_handler == 2) { + $this->_put('/V 1'); + $this->_put('/R 2'); + } else {// ($this->enc_security_handler == 3) + $this->_put('/V 2'); + $this->_put('/Length '.$this->enc_key_len*8); + $this->_put('/R 3'); + } $this->_put('/O (' . $this->_escape($this->Ovalue) . ')'); $this->_put('/U (' . $this->_escape($this->Uvalue) . ')'); $this->_put('/P ' . $this->Pvalue); @@ -183,12 +229,27 @@ private function _pad ($string, $len = 0) { * Compute O (owner password) value * * Depends on following member variables: + * - enc_security_handler * - enc_key_len */ protected function _setOvalue ($owner_pass, $user_pass) { - $key = $this->_md5_16( $this->_pad($owner_pass, 32) ); - $key = substr($key,0,$this->enc_key_len); - $encrypted = $this->RC4($key, $this->_pad($user_pass, 32) ); + $key = $this->_md5_16($this->_pad($owner_pass, 32)); + if ($this->enc_security_handler >= 3) { + for ($i=0; $i<50; $i++) { + $key = $this->_md5_16($key); + } + } + $key = substr($key, 0, $this->enc_key_len); + $encrypted = $this->ARCFOUR($key, $this->_pad($user_pass, 32)); + if ($this->enc_security_handler >= 3) { + for ($i=1; $i<=19; $i++) { + $loop_key = ''; + for ($j=0; $j<$this->enc_key_len; $j++) { + $loop_key .= chr(ord($key[$j]) ^ $i); + } + $encrypted = $this->ARCFOUR($loop_key, $encrypted ?: ''); + } + } $this->Ovalue = $encrypted; } @@ -196,12 +257,27 @@ protected function _setOvalue ($owner_pass, $user_pass) { * Compute U (user password) value * * Depends on following member variables: + * - enc_security_handler * - enc_key * - enc_key_len */ protected function _setUvalue () { $padding = $this->_pad('', 32); - $encrypted = $this->RC4($this->enc_key, $padding); + if ($this->enc_security_handler == 2) { + $encrypted = $this->ARCFOUR($this->enc_key, $padding); + } else { + $id = $this->_md5_16($this->id); + $hash = $this->_md5_16($padding.$id); + $encrypted = $this->ARCFOUR($this->enc_key, $hash); + for ($i=1; $i<=19; $i++) { + $key = ''; + for ($j=0; $j<$this->enc_key_len; $j++) { + $key .= chr( ord($this->enc_key[$j]) ^ $i ); + } + $encrypted = $this->ARCFOUR($key, $encrypted ?: ''); + } + $encrypted = $this->_pad($encrypted, 32); + } $this->Uvalue = $encrypted; } @@ -217,6 +293,7 @@ protected function _setPvalue ($protection) { * * Depends on following member variables: * - Ovalue + * - enc_security_handler * - enc_key_len */ protected function _setEncryptionKey ($user_pass, $protection) { @@ -224,6 +301,14 @@ protected function _setEncryptionKey ($user_pass, $protection) { $id = $this->_md5_16($this->id); $hash = $this->_md5_16($user_pass.$this->Ovalue.pack("V", $protection | 0xFFFFFF00).$id); + if ($this->enc_security_handler >= 3) { + for ($i=0; $i<50; $i++) { + $hash = $this->_md5_16(substr($hash, 0, $this->enc_key_len)); + } + } else { + $key_len = 5; + } + $this->enc_key = substr($hash, 0, $this->enc_key_len); } } From 979c83610317a46e3d1d7736ec26b076e97793cb Mon Sep 17 00:00:00 2001 From: erikn69 Date: Mon, 4 May 2026 12:23:01 -0500 Subject: [PATCH 3/4] Added AES-128 encryption --- scripts/PDFProtection/PDFProtectionTrait.php | 64 +++++++++++++++----- 1 file changed, 48 insertions(+), 16 deletions(-) diff --git a/scripts/PDFProtection/PDFProtectionTrait.php b/scripts/PDFProtection/PDFProtectionTrait.php index 56d74a2..404b998 100644 --- a/scripts/PDFProtection/PDFProtectionTrait.php +++ b/scripts/PDFProtection/PDFProtectionTrait.php @@ -53,12 +53,6 @@ public function SetProtection (array $permissions = [], ?string $user_pass = nul return; } - if (strncasecmp($algorithm, "AES", 3) === 0) { - $this->Error('Encryption algorithm AES, bo supported yet'); - - return; - } - $bits = intval($bits); if ($bits < 40 || $bits > 128) { $this->Error('Number of bits limited between 40 and 128'); @@ -72,8 +66,19 @@ public function SetProtection (array $permissions = [], ?string $user_pass = nul return; } - $this->enc_key_len = $bits / 8; $this->enc_algorithm = strtoupper(substr($algorithm,0,3)); + if ($this->enc_algorithm === 'AES') { + if (!function_exists('openssl_encrypt')) { // fallback + $this->enc_algorithm = 'RC4'; + } else { + $bits = 128; + if ($this->PDFVersion<'1.5') { + $this->PDFVersion = '1.5'; + } + } + } + + $this->enc_key_len = $bits / 8; if ($bits == 40 && strcmp($this->enc_algorithm, "RC4") == 0) { $this->enc_security_handler = 2; } else { @@ -94,6 +99,20 @@ public function SetProtection (array $permissions = [], ?string $user_pass = nul $this->_setPvalue($protection); } + protected function _encryptData($key, $data) { + if ($this->enc_algorithm === 'AES') { + return $this->AES($key, $data); + } + + return $this->ARCFOUR($key, $data); + } + protected function AES (string $key, string $data) { + $iv = random_bytes(16); + $cipher = openssl_encrypt($data, 'aes-128-cbc', $key, OPENSSL_RAW_DATA, $iv); + + return $iv . $cipher; + } + protected function ARCFOUR (string $key, string $data) { static $last_key; static $last_state; @@ -140,7 +159,7 @@ protected function ARCFOUR (string $key, string $data) { protected function _putstream ($s) { if ($this->encrypted) { - $s = $this->ARCFOUR($this->_objectkey($this->n), $s); + $s = $this->_encryptData($this->_objectkey($this->n), $s); } parent::_putstream($s); } @@ -151,7 +170,7 @@ protected function _textstring($s) { } if ($this->encrypted) { - $s = $this->ARCFOUR($this->_objectkey($this->n), $s); + $s = $this->_encryptData($this->_objectkey($this->n), $s); } return '(' . $this->_escape($s) . ')'; @@ -162,6 +181,9 @@ protected function _textstring($s) { */ protected function _objectkey ($n) { $key = $this->enc_key.pack('VXxx', $n); + if ($this->enc_algorithm === 'AES') { + $key .= "sAlT"; + } $len = $this->enc_key_len + 5; return substr($this->_md5_16($key), 0, $len); @@ -186,13 +208,23 @@ protected function _encrypresources () { protected function _putencryption () { $this->_put('/Filter'); $this->_put('/Standard'); - if ($this->enc_security_handler == 2) { - $this->_put('/V 1'); - $this->_put('/R 2'); - } else {// ($this->enc_security_handler == 3) - $this->_put('/V 2'); - $this->_put('/Length '.$this->enc_key_len*8); - $this->_put('/R 3'); + if ($this->enc_algorithm === 'AES') { + $this->_put('/V 4'); + $this->_put('/R 4'); + $this->_put('/Length 128'); + + $this->_put('/CF << /StdCF << /CFM /AESV2 /Length 16 >> >>'); + $this->_put('/StmF /StdCF'); + $this->_put('/StrF /StdCF'); + } else { + if ($this->enc_security_handler == 2) { + $this->_put('/V 1'); + $this->_put('/R 2'); + } else {// ($this->enc_security_handler == 3) + $this->_put('/V 2'); + $this->_put('/Length '.$this->enc_key_len*8); + $this->_put('/R 3'); + } } $this->_put('/O (' . $this->_escape($this->Ovalue) . ')'); $this->_put('/U (' . $this->_escape($this->Uvalue) . ')'); From 0e668138f865b563fcc47549df8d123228c09a7a Mon Sep 17 00:00:00 2001 From: erikn69 Date: Tue, 5 May 2026 10:53:10 -0500 Subject: [PATCH 4/4] fix: Check if algorithm exists --- scripts/PDFProtection/PDFProtectionTrait.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/PDFProtection/PDFProtectionTrait.php b/scripts/PDFProtection/PDFProtectionTrait.php index 404b998..5e4340e 100644 --- a/scripts/PDFProtection/PDFProtectionTrait.php +++ b/scripts/PDFProtection/PDFProtectionTrait.php @@ -68,7 +68,7 @@ public function SetProtection (array $permissions = [], ?string $user_pass = nul $this->enc_algorithm = strtoupper(substr($algorithm,0,3)); if ($this->enc_algorithm === 'AES') { - if (!function_exists('openssl_encrypt')) { // fallback + if (!function_exists('openssl_encrypt') || !in_array('aes-128-cbc', openssl_get_cipher_methods(), true)) { // fallback $this->enc_algorithm = 'RC4'; } else { $bits = 128; @@ -117,7 +117,7 @@ protected function ARCFOUR (string $key, string $data) { static $last_key; static $last_state; - if (function_exists('openssl_encrypt')) { + if (function_exists('openssl_encrypt') && in_array('RC4-40', openssl_get_cipher_methods(), true)) { return openssl_encrypt($data, 'RC4-40', $key, OPENSSL_RAW_DATA); }