1<?php 2 3namespace Mpdf\Pdf; 4 5use Mpdf\Pdf\Protection\UniqidGenerator; 6 7class Protection 8{ 9 10 /** 11 * @var string 12 */ 13 private $lastRc4Key; 14 15 /** 16 * @var string 17 */ 18 private $lastRc4KeyC; 19 20 /** 21 * @var bool 22 */ 23 private $useRC128Encryption; 24 25 /** 26 * @var string 27 */ 28 private $encryptionKey; 29 30 /** 31 * @var string 32 */ 33 private $padding; 34 35 /** 36 * @var string 37 */ 38 private $uniqid; 39 40 /** 41 * @var string 42 */ 43 private $oValue; 44 45 /** 46 * @var string 47 */ 48 private $uValue; 49 50 /** 51 * @var string 52 */ 53 private $pValue; 54 55 /** 56 * @var int[] Array of permission => byte representation 57 */ 58 private $options; 59 60 /** 61 * @var \Mpdf\Pdf\Protection\UniqidGenerator 62 */ 63 private $uniqidGenerator; 64 65 public function __construct(UniqidGenerator $uniqidGenerator) 66 { 67 if (!function_exists('random_int') || !function_exists('random_bytes')) { 68 throw new \Mpdf\MpdfException( 69 'Unable to set PDF file protection, CSPRNG Functions are not available. ' 70 . 'Use paragonie/random_compat polyfill or upgrade to PHP 7.' 71 ); 72 } 73 74 $this->uniqidGenerator = $uniqidGenerator; 75 76 $this->lastRc4Key = ''; 77 78 $this->padding = "\x28\xBF\x4E\x5E\x4E\x75\x8A\x41\x64\x00\x4E\x56\xFF\xFA\x01\x08" . 79 "\x2E\x2E\x00\xB6\xD0\x68\x3E\x80\x2F\x0C\xA9\xFE\x64\x53\x69\x7A"; 80 81 $this->useRC128Encryption = false; 82 83 $this->options = [ 84 'print' => 4, // bit 3 85 'modify' => 8, // bit 4 86 'copy' => 16, // bit 5 87 'annot-forms' => 32, // bit 6 88 'fill-forms' => 256, // bit 9 89 'extract' => 512, // bit 10 90 'assemble' => 1024, // bit 11 91 'print-highres' => 2048 // bit 12 92 ]; 93 } 94 95 /** 96 * @param array $permissions 97 * @param string $user_pass 98 * @param string $owner_pass 99 * @param int $length 100 * 101 * @return bool 102 */ 103 public function setProtection($permissions = [], $user_pass = '', $owner_pass = null, $length = 40) 104 { 105 if (is_string($permissions) && strlen($permissions) > 0) { 106 $permissions = [$permissions]; 107 } elseif (!is_array($permissions)) { 108 return false; 109 } 110 111 $protection = $this->getProtectionBitsFromOptions($permissions); 112 113 if ($length === 128) { 114 $this->useRC128Encryption = true; 115 } elseif ($length !== 40) { 116 throw new \Mpdf\MpdfException('PDF protection only allows lenghts of 40 or 128'); 117 } 118 119 if ($owner_pass === null) { 120 $owner_pass = bin2hex(random_bytes(23)); 121 } 122 123 $this->generateEncryptionKey($user_pass, $owner_pass, $protection); 124 125 return true; 126 } 127 128 /** 129 * Compute key depending on object number where the encrypted data is stored 130 * 131 * @param int $n 132 * 133 * @return string 134 */ 135 public function objectKey($n) 136 { 137 if ($this->useRC128Encryption) { 138 $len = 16; 139 } else { 140 $len = 10; 141 } 142 143 return substr($this->md5toBinary($this->encryptionKey . pack('VXxx', $n)), 0, $len); 144 } 145 146 /** 147 * RC4 is the standard encryption algorithm used in PDF format 148 * 149 * @param string $key 150 * @param string $text 151 * 152 * @return string 153 */ 154 public function rc4($key, $text) 155 { 156 if ($this->lastRc4Key != $key) { 157 $k = str_repeat($key, round(256 / strlen($key)) + 1); 158 $rc4 = range(0, 255); 159 $j = 0; 160 for ($i = 0; $i < 256; $i++) { 161 $t = $rc4[$i]; 162 $j = ($j + $t + ord($k[$i])) % 256; 163 $rc4[$i] = $rc4[$j]; 164 $rc4[$j] = $t; 165 } 166 $this->lastRc4Key = $key; 167 $this->lastRc4KeyC = $rc4; 168 } else { 169 $rc4 = $this->lastRc4KeyC; 170 } 171 172 $len = strlen($text); 173 $a = 0; 174 $b = 0; 175 $out = ''; 176 for ($i = 0; $i < $len; $i++) { 177 $a = ($a + 1) % 256; 178 $t = $rc4[$a]; 179 $b = ($b + $t) % 256; 180 $rc4[$a] = $rc4[$b]; 181 $rc4[$b] = $t; 182 $k = $rc4[($rc4[$a] + $rc4[$b]) % 256]; 183 $out .= chr(ord($text[$i]) ^ $k); 184 } 185 186 return $out; 187 } 188 189 /** 190 * @return mixed 191 */ 192 public function getUseRC128Encryption() 193 { 194 return $this->useRC128Encryption; 195 } 196 197 /** 198 * @return mixed 199 */ 200 public function getUniqid() 201 { 202 return $this->uniqid; 203 } 204 205 /** 206 * @return mixed 207 */ 208 public function getOValue() 209 { 210 return $this->oValue; 211 } 212 213 /** 214 * @return mixed 215 */ 216 public function getUValue() 217 { 218 return $this->uValue; 219 } 220 221 /** 222 * @return mixed 223 */ 224 public function getPValue() 225 { 226 return $this->pValue; 227 } 228 229 private function getProtectionBitsFromOptions($permissions) 230 { 231 // bit 31 = 1073741824 232 // bit 32 = 2147483648 233 // bits 13-31 = 2147479552 234 // bits 13-32 = 4294963200 + 192 = 4294963392 235 236 $protection = 4294963392; // bits 7, 8, 13-32 237 238 foreach ($permissions as $permission) { 239 if (!isset($this->options[$permission])) { 240 throw new \Mpdf\MpdfException(sprintf('Invalid permission type "%s"', $permission)); 241 } 242 if ($this->options[$permission] > 32) { 243 $this->useRC128Encryption = true; 244 } 245 if (isset($this->options[$permission])) { 246 $protection += $this->options[$permission]; 247 } 248 } 249 250 return $protection; 251 } 252 253 private function oValue($user_pass, $owner_pass) 254 { 255 $tmp = $this->md5toBinary($owner_pass); 256 if ($this->useRC128Encryption) { 257 for ($i = 0; $i < 50; ++$i) { 258 $tmp = $this->md5toBinary($tmp); 259 } 260 } 261 if ($this->useRC128Encryption) { 262 $keybytelen = (128 / 8); 263 } else { 264 $keybytelen = (40 / 8); 265 } 266 $owner_rc4_key = substr($tmp, 0, $keybytelen); 267 $enc = $this->rc4($owner_rc4_key, $user_pass); 268 if ($this->useRC128Encryption) { 269 $len = strlen($owner_rc4_key); 270 for ($i = 1; $i <= 19; ++$i) { 271 $key = ''; 272 for ($j = 0; $j < $len; ++$j) { 273 $key .= chr(ord($owner_rc4_key[$j]) ^ $i); 274 } 275 $enc = $this->rc4($key, $enc); 276 } 277 } 278 279 return $enc; 280 } 281 282 private function uValue() 283 { 284 if ($this->useRC128Encryption) { 285 $tmp = $this->md5toBinary($this->padding . $this->hexToString($this->uniqid)); 286 $enc = $this->rc4($this->encryptionKey, $tmp); 287 $len = strlen($tmp); 288 for ($i = 1; $i <= 19; ++$i) { 289 $key = ''; 290 for ($j = 0; $j < $len; ++$j) { 291 $key .= chr(ord($this->encryptionKey[$j]) ^ $i); 292 } 293 $enc = $this->rc4($key, $enc); 294 } 295 $enc .= str_repeat("\x00", 16); 296 297 return substr($enc, 0, 32); 298 } else { 299 return $this->rc4($this->encryptionKey, $this->padding); 300 } 301 } 302 303 private function generateEncryptionKey($user_pass, $owner_pass, $protection) 304 { 305 // Pad passwords 306 $user_pass = substr($user_pass . $this->padding, 0, 32); 307 $owner_pass = substr($owner_pass . $this->padding, 0, 32); 308 309 $this->oValue = $this->oValue($user_pass, $owner_pass); 310 311 $this->uniqid = $this->uniqidGenerator->generate(); 312 313 // Compute encyption key 314 if ($this->useRC128Encryption) { 315 $keybytelen = (128 / 8); 316 } else { 317 $keybytelen = (40 / 8); 318 } 319 320 $prot = sprintf('%032b', $protection); 321 322 $perms = chr(bindec(substr($prot, 24, 8))); 323 $perms .= chr(bindec(substr($prot, 16, 8))); 324 $perms .= chr(bindec(substr($prot, 8, 8))); 325 $perms .= chr(bindec(substr($prot, 0, 8))); 326 327 $tmp = $this->md5toBinary($user_pass . $this->oValue . $perms . $this->hexToString($this->uniqid)); 328 329 if ($this->useRC128Encryption) { 330 for ($i = 0; $i < 50; ++$i) { 331 $tmp = $this->md5toBinary(substr($tmp, 0, $keybytelen)); 332 } 333 } 334 335 $this->encryptionKey = substr($tmp, 0, $keybytelen); 336 337 $this->uValue = $this->uValue(); 338 $this->pValue = $protection; 339 } 340 341 private function md5toBinary($string) 342 { 343 return pack('H*', md5($string)); 344 } 345 346 private function hexToString($hs) 347 { 348 $s = ''; 349 $len = strlen($hs); 350 if (($len % 2) != 0) { 351 $hs .= '0'; 352 ++$len; 353 } 354 for ($i = 0; $i < $len; $i += 2) { 355 $s .= chr(hexdec($hs[$i] . $hs[($i + 1)])); 356 } 357 358 return $s; 359 } 360} 361