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, 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