byte representation */ private $options; /** * @var \Mpdf\Pdf\Protection\UniqidGenerator */ private $uniqidGenerator; public function __construct(UniqidGenerator $uniqidGenerator) { if (!function_exists('random_int') || !function_exists('random_bytes')) { throw new \Mpdf\MpdfException( 'Unable to set PDF file protection, CSPRNG Functions are not available. ' . 'Use paragonie/random_compat polyfill or upgrade to PHP 7.' ); } $this->uniqidGenerator = $uniqidGenerator; $this->lastRc4Key = ''; $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->useRC128Encryption = false; $this->options = [ 'print' => 4, // bit 3 'modify' => 8, // bit 4 'copy' => 16, // bit 5 'annot-forms' => 32, // bit 6 'fill-forms' => 256, // bit 9 'extract' => 512, // bit 10 'assemble' => 1024, // bit 11 'print-highres' => 2048 // bit 12 ]; } /** * @param array $permissions * @param string $user_pass * @param string $owner_pass * @param int $length * * @return bool */ public function setProtection($permissions = [], $user_pass = '', $owner_pass = null, $length = 40) { if (is_string($permissions) && strlen($permissions) > 0) { $permissions = [$permissions]; } elseif (!is_array($permissions)) { return false; } $protection = $this->getProtectionBitsFromOptions($permissions); if ($length === 128) { $this->useRC128Encryption = true; } elseif ($length !== 40) { throw new \Mpdf\MpdfException('PDF protection only allows lenghts of 40 or 128'); } if ($owner_pass === null) { $owner_pass = bin2hex(random_bytes(23)); } $this->generateEncryptionKey($user_pass, $owner_pass, $protection); return true; } /** * Compute key depending on object number where the encrypted data is stored * * @param int $n * * @return string */ public function objectKey($n) { if ($this->useRC128Encryption) { $len = 16; } else { $len = 10; } return substr($this->md5toBinary($this->encryptionKey . pack('VXxx', $n)), 0, $len); } /** * RC4 is the standard encryption algorithm used in PDF format * * @param string $key * @param string $text * * @return string */ public function rc4($key, $text) { if ($this->lastRc4Key != $key) { $k = str_repeat($key, round(256 / strlen($key)) + 1); $rc4 = range(0, 255); $j = 0; for ($i = 0; $i < 256; $i++) { $t = $rc4[$i]; $j = ($j + $t + ord($k[$i])) % 256; $rc4[$i] = $rc4[$j]; $rc4[$j] = $t; } $this->lastRc4Key = $key; $this->lastRc4KeyC = $rc4; } else { $rc4 = $this->lastRc4KeyC; } $len = strlen($text); $a = 0; $b = 0; $out = ''; for ($i = 0; $i < $len; $i++) { $a = ($a + 1) % 256; $t = $rc4[$a]; $b = ($b + $t) % 256; $rc4[$a] = $rc4[$b]; $rc4[$b] = $t; $k = $rc4[($rc4[$a] + $rc4[$b]) % 256]; $out .= chr(ord($text[$i]) ^ $k); } return $out; } /** * @return mixed */ public function getUseRC128Encryption() { return $this->useRC128Encryption; } /** * @return mixed */ public function getUniqid() { return $this->uniqid; } /** * @return mixed */ public function getOValue() { return $this->oValue; } /** * @return mixed */ public function getUValue() { return $this->uValue; } /** * @return mixed */ public function getPValue() { return $this->pValue; } private function getProtectionBitsFromOptions($permissions) { // bit 31 = 1073741824 // bit 32 = 2147483648 // bits 13-31 = 2147479552 // bits 13-32 = 4294963200 + 192 = 4294963392 $protection = 4294963392; // bits 7, 8, 13-32 foreach ($permissions as $permission) { if (!isset($this->options[$permission])) { throw new \Mpdf\MpdfException(sprintf('Invalid permission type "%s"', $permission)); } if ($this->options[$permission] > 32) { $this->useRC128Encryption = true; } if (isset($this->options[$permission])) { $protection += $this->options[$permission]; } } return $protection; } private function oValue($user_pass, $owner_pass) { $tmp = $this->md5toBinary($owner_pass); if ($this->useRC128Encryption) { for ($i = 0; $i < 50; ++$i) { $tmp = $this->md5toBinary($tmp); } } if ($this->useRC128Encryption) { $keybytelen = (128 / 8); } else { $keybytelen = (40 / 8); } $owner_rc4_key = substr($tmp, 0, $keybytelen); $enc = $this->rc4($owner_rc4_key, $user_pass); if ($this->useRC128Encryption) { $len = strlen($owner_rc4_key); for ($i = 1; $i <= 19; ++$i) { $key = ''; for ($j = 0; $j < $len; ++$j) { $key .= chr(ord($owner_rc4_key[$j]) ^ $i); } $enc = $this->rc4($key, $enc); } } return $enc; } private function uValue() { if ($this->useRC128Encryption) { $tmp = $this->md5toBinary($this->padding . $this->hexToString($this->uniqid)); $enc = $this->rc4($this->encryptionKey, $tmp); $len = strlen($tmp); for ($i = 1; $i <= 19; ++$i) { $key = ''; for ($j = 0; $j < $len; ++$j) { $key .= chr(ord($this->encryptionKey[$j]) ^ $i); } $enc = $this->rc4($key, $enc); } $enc .= str_repeat("\x00", 16); return substr($enc, 0, 32); } else { return $this->rc4($this->encryptionKey, $this->padding); } } private 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); $this->oValue = $this->oValue($user_pass, $owner_pass); $this->uniqid = $this->uniqidGenerator->generate(); // Compute encyption key if ($this->useRC128Encryption) { $keybytelen = (128 / 8); } else { $keybytelen = (40 / 8); } $prot = sprintf('%032b', $protection); $perms = chr(bindec(substr($prot, 24, 8))); $perms .= chr(bindec(substr($prot, 16, 8))); $perms .= chr(bindec(substr($prot, 8, 8))); $perms .= chr(bindec(substr($prot, 0, 8))); $tmp = $this->md5toBinary($user_pass . $this->oValue . $perms . $this->hexToString($this->uniqid)); if ($this->useRC128Encryption) { for ($i = 0; $i < 50; ++$i) { $tmp = $this->md5toBinary(substr($tmp, 0, $keybytelen)); } } $this->encryptionKey = substr($tmp, 0, $keybytelen); $this->uValue = $this->uValue(); $this->pValue = $protection; } private function md5toBinary($string) { return pack('H*', md5($string)); } private function hexToString($hs) { $s = ''; $len = strlen($hs); if (($len % 2) != 0) { $hs .= '0'; ++$len; } for ($i = 0; $i < $len; $i += 2) { $s .= chr(hexdec($hs[$i] . $hs[($i + 1)])); } return $s; } }