<?php
namespace Mpdf\Pdf;
use Mpdf\Pdf\Protection\UniqidGenerator;
class Protection
{
/**
* @var string
*/
private $lastRc4Key;
/**
* @var string
*/
private $lastRc4KeyC;
/**
* @var bool
*/
private $useRC128Encryption;
/**
* @var string
*/
private $encryptionKey;
/**
* @var string
*/
private $padding;
/**
* @var string
*/
private $uniqid;
/**
* @var string
*/
private $oValue;
/**
* @var string
*/
private $uValue;
/**
* @var string
*/
private $pValue;
/**
* @var int[] Array of permission => 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;
}
}
|