From 05387fac0903969fb88c832042991bd34f0154b0 Mon Sep 17 00:00:00 2001 From: Maria Haubner Date: Fri, 10 Mar 2017 13:28:32 +0100 Subject: [PATCH 01/14] enable password setting in word --- src/PhpWord/Metadata/Protection.php | 224 +++++++++++++++++- src/PhpWord/Writer/Word2007/Part/Settings.php | 28 ++- 2 files changed, 244 insertions(+), 8 deletions(-) diff --git a/src/PhpWord/Metadata/Protection.php b/src/PhpWord/Metadata/Protection.php index 0e2ee7c1..bcc0d652 100644 --- a/src/PhpWord/Metadata/Protection.php +++ b/src/PhpWord/Metadata/Protection.php @@ -22,18 +22,77 @@ namespace PhpOffice\PhpWord\Metadata; * * @since 0.12.0 * @link http://www.datypic.com/sc/ooxml/t-w_CT_DocProtect.html - * @todo Password! */ class Protection { + static $algorithmMapping = [ + 1 => 'md2', + 2 => 'md4', + 3 => 'md5', + 4 => 'sha1', + 5 => '', // 'mac' -> not possible with hash() + 6 => 'ripemd', + 7 => 'ripemd160', + 8 => '', + 9 => '', //'hmac' -> not possible with hash() + 10 => '', + 11 => '', + 12 => 'sha256', + 13 => 'sha384', + 14 => 'sha512', + ]; + static $initialCodeArray = [ + 0xE1F0, + 0x1D0F, + 0xCC9C, + 0x84C0, + 0x110C, + 0x0E10, + 0xF1CE, + 0x313E, + 0x1872, + 0xE139, + 0xD40F, + 0x84F9, + 0x280C, + 0xA96A, + 0x4EC3 + ]; + static $encryptionMatrix = + [ + [0xAEFC, 0x4DD9, 0x9BB2, 0x2745, 0x4E8A, 0x9D14, 0x2A09], + [0x7B61, 0xF6C2, 0xFDA5, 0xEB6B, 0xC6F7, 0x9DCF, 0x2BBF], + [0x4563, 0x8AC6, 0x05AD, 0x0B5A, 0x16B4, 0x2D68, 0x5AD0], + [0x0375, 0x06EA, 0x0DD4, 0x1BA8, 0x3750, 0x6EA0, 0xDD40], + [0xD849, 0xA0B3, 0x5147, 0xA28E, 0x553D, 0xAA7A, 0x44D5], + [0x6F45, 0xDE8A, 0xAD35, 0x4A4B, 0x9496, 0x390D, 0x721A], + [0xEB23, 0xC667, 0x9CEF, 0x29FF, 0x53FE, 0xA7FC, 0x5FD9], + [0x47D3, 0x8FA6, 0x0F6D, 0x1EDA, 0x3DB4, 0x7B68, 0xF6D0], + [0xB861, 0x60E3, 0xC1C6, 0x93AD, 0x377B, 0x6EF6, 0xDDEC], + [0x45A0, 0x8B40, 0x06A1, 0x0D42, 0x1A84, 0x3508, 0x6A10], + [0xAA51, 0x4483, 0x8906, 0x022D, 0x045A, 0x08B4, 0x1168], + [0x76B4, 0xED68, 0xCAF1, 0x85C3, 0x1BA7, 0x374E, 0x6E9C], + [0x3730, 0x6E60, 0xDCC0, 0xA9A1, 0x4363, 0x86C6, 0x1DAD], + [0x3331, 0x6662, 0xCCC4, 0x89A9, 0x0373, 0x06E6, 0x0DCC], + [0x1021, 0x2042, 0x4084, 0x8108, 0x1231, 0x2462, 0x48C4] + ]; + /** - * Editing restriction readOnly|comments|trackedChanges|forms + * Editing restriction none|readOnly|comments|trackedChanges|forms * * @var string * @link http://www.datypic.com/sc/ooxml/a-w_edit-1.html */ private $editing; + private $password; + + private $spinCount = 100000; + + private $algorithmSid = 4; + + private $salt; + /** * Create a new instance * @@ -66,4 +125,165 @@ class Protection return $this; } + + public function getPassword() + { + return $this->password; + } + + public function setPassword($password) + { + $this->password = $this->getPasswordHash($password); + + return $this; + } + + public function getSpinCount() + { + return $this->spinCount; + } + + public function setSpinCount($spinCount) + { + $this->spinCount = $spinCount; + + return $this; + } + + public function getAlgorithmSid() + { + return $this->algorithmSid; + } + + public function setAlgorithmSid($algorithmSid) + { + $this->algorithmSid = $algorithmSid; + + return $this; + } + + public function setSalt($salt) + { + $this->salt = $salt; + } + + public function getSalt() + { + return $this->salt; + } + + private function getAlgorithm() + { + $algorithm = self::$algorithmMapping[$this->algorithmSid]; + if ($algorithm == '') { + $algorithm = 'sha1'; + } + + return $algorithm; + } + + private function getPasswordHash($password) + { + if (empty($password)) { + return ''; + } + $passwordMaxLength = 15; + + // Truncate the password to $passwordMaxLength characters + $password = mb_substr($password, 0, min($passwordMaxLength, mb_strlen($password))); + + $byteChars = []; + + echo "password: '{$password}'(".mb_strlen($password).")"; + + $pass_utf8 = mb_convert_encoding($password, 'UCS-2LE', 'UTF-8'); + for ($i = 0; $i < mb_strlen($password); $i++) { + $byteChars[$i] = ord(substr($pass_utf8, $i*2, 1)); + if ($byteChars[$i] == 0) { + echo "hi!$i"; + $byteChars[$i] = ord(substr($pass_utf8, $i*2+1, 1)); + } + } + + // Compute the high-order word + $highOrderWord = self::$initialCodeArray[sizeof($byteChars) - 1]; + for ($i = 0; $i < sizeof($byteChars); $i++) { + $tmp = $passwordMaxLength - sizeof($byteChars) + $i; + $matrixRow = self::$encryptionMatrix[$tmp]; + + for ($intBit = 0; $intBit < 7; $intBit++) { + if (($byteChars[$i] & (0x0001 << $intBit)) != 0) { + $highOrderWord = ($highOrderWord ^ $matrixRow[$intBit]); + } + } + } + + // Compute low-order word + $lowOrderWord = 0; + for ($i = sizeof($byteChars) - 1; $i >= 0; $i--) { + $lowOrderWord = (((($lowOrderWord >> 14) & 0x0001) | (($lowOrderWord << 1) & 0x7FFF)) ^ $byteChars[$i]); + } + $lowOrderWord = (((($lowOrderWord >> 14) & 0x0001) | (($lowOrderWord << 1) & 0x7FFF)) ^ sizeof($byteChars) ^ 0xCE4B); + + $combinedKey = $this->int32(($highOrderWord << 16) + $lowOrderWord); + $generatedKey = [ + 0 => (($combinedKey & 0x000000FF) >> 0), + 1 => (($combinedKey & 0x0000FF00) >> 8), + 2 => (($combinedKey & 0x00FF0000) >> 16), + 3 => (($combinedKey & 0xFF000000) >> 24), + ]; + + $tmpStr = ''; + for ($i = 0; $i < 4; $i++) { + $tmpStr .= strtoupper(dechex($generatedKey[$i])); + } + $generatedKey = []; + $tmpStr = mb_convert_encoding($tmpStr, 'UCS-2LE', 'UTF-8'); + for ($i = 0; $i < strlen($tmpStr); $i++) { + $generatedKey[] = ord(substr($tmpStr, $i, 1)); + } + + $salt = unpack('C*', base64_decode($this->getSalt())); + $algorithm = $this->getAlgorithm(); + + $tmpArray1 = $generatedKey; + $tmpArray2 = $salt; + $generatedKey = array_merge($tmpArray2, $tmpArray1); + + $generatedKey = $this->hashByteArray($algorithm, $generatedKey); + + for ($i = 0; $i < $this->getSpinCount(); $i++) { + $iterator = [ + 0 => (($i & 0x000000FF) >> 0), + 1 => (($i & 0x0000FF00) >> 8), + 2 => (($i & 0x00FF0000) >> 16), + 3 => (($i & 0xFF000000) >> 24), + ]; + $generatedKey = array_merge($generatedKey, $iterator); + $generatedKey = $this->hashByteArray($algorithm, $generatedKey); + } + + $hash = implode(array_map("chr", $generatedKey)); + + return base64_encode($hash); + } + + private function int32($value) + { + $value = ($value & 0xFFFFFFFF); + + if ($value & 0x80000000) { + $value = -((~$value & 0xFFFFFFFF) + 1); + } + + return $value; + } + + private function hashByteArray($algorithm, $array) + { + $string = implode(array_map("chr", $array)); + $string = hash($algorithm, $string, true); + + return unpack('C*', $string); + } } diff --git a/src/PhpWord/Writer/Word2007/Part/Settings.php b/src/PhpWord/Writer/Word2007/Part/Settings.php index d881e13a..11549e08 100644 --- a/src/PhpWord/Writer/Word2007/Part/Settings.php +++ b/src/PhpWord/Writer/Word2007/Part/Settings.php @@ -152,12 +152,28 @@ class Settings extends AbstractPart { $protection = $this->getParentWriter()->getPhpWord()->getProtection(); if ($protection->getEditing() !== null) { - $this->settings['w:documentProtection'] = array( - '@attributes' => array( - 'w:enforcement' => 1, - 'w:edit' => $protection->getEditing(), - ) - ); + if (empty($protection->getPassword())) { + $this->settings['w:documentProtection'] = array( + '@attributes' => array( + 'w:enforcement' => 1, + 'w:edit' => $protection->getEditing(), + ) + ); + } else { + $this->settings['w:documentProtection'] = array( + '@attributes' => array( + 'w:enforcement' => 1, + 'w:edit' => $protection->getEditing(), + 'w:cryptProviderType' => 'rsaFull', + 'w:cryptAlgorithmClass' => 'hash', + 'w:cryptAlgorithmType' => 'typeAny', + 'w:cryptAlgorithmSid' => $protection->getAlgorithmSid(), + 'w:cryptSpinCount' => $protection->getSpinCount(), + 'w:hash' => $protection->getPassword(), + 'w:salt' => $protection->getSalt(), + ) + ); + } } } From 483a167500a008d4895a79f594658dc3b5fc2769 Mon Sep 17 00:00:00 2001 From: Maria Haubner Date: Fri, 10 Mar 2017 15:44:13 +0100 Subject: [PATCH 02/14] refactoring of hash function --- src/PhpWord/Metadata/Protection.php | 214 +++++++++++++++++++--------- 1 file changed, 145 insertions(+), 69 deletions(-) diff --git a/src/PhpWord/Metadata/Protection.php b/src/PhpWord/Metadata/Protection.php index bcc0d652..5427f570 100644 --- a/src/PhpWord/Metadata/Protection.php +++ b/src/PhpWord/Metadata/Protection.php @@ -76,6 +76,7 @@ class Protection [0x3331, 0x6662, 0xCCC4, 0x89A9, 0x0373, 0x06E6, 0x0DCC], [0x1021, 0x2042, 0x4084, 0x8108, 0x1231, 0x2462, 0x48C4] ]; + static $passwordMaxLength = 15; /** * Editing restriction none|readOnly|comments|trackedChanges|forms @@ -85,12 +86,32 @@ class Protection */ private $editing; + /** + * Hashed password + * + * @var string + */ private $password; + /** + * Number of hashing iterations + * + * @var int + */ private $spinCount = 100000; + /** + * Algorithm-SID according to self::$algorithmMapping + * + * @var int + */ private $algorithmSid = 4; + /** + * Hashed salt + * + * @var string + */ private $salt; /** @@ -126,11 +147,22 @@ class Protection return $this; } + /** + * Get password hash + * + * @return string + */ public function getPassword() { return $this->password; } + /** + * Set password + * + * @param $password + * @return self + */ public function setPassword($password) { $this->password = $this->getPasswordHash($password); @@ -138,11 +170,22 @@ class Protection return $this; } + /** + * Get count for hash iterations + * + * @return int + */ public function getSpinCount() { return $this->spinCount; } + /** + * Set count for hash iterations + * + * @param $spinCount + * @return self + */ public function setSpinCount($spinCount) { $this->spinCount = $spinCount; @@ -150,11 +193,22 @@ class Protection return $this; } + /** + * Get algorithm-sid + * + * @return int + */ public function getAlgorithmSid() { return $this->algorithmSid; } + /** + * Set algorithm-sid (see self::$algorithmMapping) + * + * @param $algorithmSid + * @return self + */ public function setAlgorithmSid($algorithmSid) { $this->algorithmSid = $algorithmSid; @@ -162,16 +216,34 @@ class Protection return $this; } - public function setSalt($salt) - { - $this->salt = $salt; - } - + /** + * Get salt hash + * + * @return string + */ public function getSalt() { return $this->salt; } + /** + * Set salt hash + * + * @param $salt + * @return self + */ + public function setSalt($salt) + { + $this->salt = $salt; + + return $this; + } + + /** + * Get algorithm from self::$algorithmMapping + * + * @return string + */ private function getAlgorithm() { $algorithm = self::$algorithmMapping[$this->algorithmSid]; @@ -182,35 +254,76 @@ class Protection return $algorithm; } + /** + * Create a hashed password that MS Word will be able to work with + * + * @param string $password + * @return string + */ private function getPasswordHash($password) { + $orig_encoding = mb_internal_encoding(); + mb_internal_encoding("UTF-8"); + if (empty($password)) { return ''; } - $passwordMaxLength = 15; - // Truncate the password to $passwordMaxLength characters - $password = mb_substr($password, 0, min($passwordMaxLength, mb_strlen($password))); + $password = mb_substr($password, 0, min(self::$passwordMaxLength, mb_strlen($password))); + // Construct a new NULL-terminated string consisting of single-byte characters: + // Get the single-byte values by iterating through the Unicode characters of the truncated password. + // For each character, if the low byte is not equal to 0, take it. Otherwise, take the high byte. + $pass_utf8 = mb_convert_encoding($password, 'UCS-2LE', 'UTF-8'); $byteChars = []; - - echo "password: '{$password}'(".mb_strlen($password).")"; - - $pass_utf8 = mb_convert_encoding($password, 'UCS-2LE', 'UTF-8'); for ($i = 0; $i < mb_strlen($password); $i++) { - $byteChars[$i] = ord(substr($pass_utf8, $i*2, 1)); + $byteChars[$i] = ord(substr($pass_utf8, $i * 2, 1)); if ($byteChars[$i] == 0) { - echo "hi!$i"; - $byteChars[$i] = ord(substr($pass_utf8, $i*2+1, 1)); + $byteChars[$i] = ord(substr($pass_utf8, $i * 2 + 1, 1)); } } - // Compute the high-order word - $highOrderWord = self::$initialCodeArray[sizeof($byteChars) - 1]; - for ($i = 0; $i < sizeof($byteChars); $i++) { - $tmp = $passwordMaxLength - sizeof($byteChars) + $i; - $matrixRow = self::$encryptionMatrix[$tmp]; + // build low-order word and hig-order word and combine them + $combinedKey = $this->buildCombinedKey($byteChars); + // build reversed hexadecimal string + $hex = strtoupper(dechex($combinedKey & 0xFFFFFFFF)); + $reversedHex = $hex[6].$hex[7].$hex[4].$hex[5].$hex[2].$hex[3].$hex[0].$hex[1]; + $generatedKey = mb_convert_encoding($reversedHex, 'UCS-2LE', 'UTF-8'); + + // Implementation Notes List: + // Word requires that the initial hash of the password with the salt not be considered in the count. + // The initial hash of salt + key is not included in the iteration count. + $generatedKey = hash($this->getAlgorithm(), base64_decode($this->getSalt()) . $generatedKey, true); + for ($i = 0; $i < $this->getSpinCount(); $i++) { + $generatedKey = hash($this->getAlgorithm(), $generatedKey . pack("CCCC", $i, $i>>8, $i>>16, $i>>24), true); + } + $generatedKey = base64_encode($generatedKey); + + mb_internal_encoding($orig_encoding); + + return $generatedKey; + } + + /** + * Build combined key from low-order word and high-order word + * + * @param array $byteChars -> byte array representation of password + * @return int + */ + private function buildCombinedKey($byteChars) + { + // Compute the high-order word + // Initialize from the initial code array (see above), depending on the passwords length. + $highOrderWord = self::$initialCodeArray[sizeof($byteChars) - 1]; + + // For each character in the password: + // For every bit in the character, starting with the least significant and progressing to (but excluding) + // the most significant, if the bit is set, XOR the key’s high-order word with the corresponding word from + // the Encryption Matrix + for ($i = 0; $i < sizeof($byteChars); $i++) { + $tmp = self::$passwordMaxLength - sizeof($byteChars) + $i; + $matrixRow = self::$encryptionMatrix[$tmp]; for ($intBit = 0; $intBit < 7; $intBit++) { if (($byteChars[$i] & (0x0001 << $intBit)) != 0) { $highOrderWord = ($highOrderWord ^ $matrixRow[$intBit]); @@ -219,55 +332,26 @@ class Protection } // Compute low-order word + // Initialize with 0 $lowOrderWord = 0; + // For each character in the password, going backwards for ($i = sizeof($byteChars) - 1; $i >= 0; $i--) { + // low-order word = (((low-order word SHR 14) AND 0x0001) OR (low-order word SHL 1) AND 0x7FFF)) XOR character $lowOrderWord = (((($lowOrderWord >> 14) & 0x0001) | (($lowOrderWord << 1) & 0x7FFF)) ^ $byteChars[$i]); } + // Lastly, low-order word = (((low-order word SHR 14) AND 0x0001) OR (low-order word SHL 1) AND 0x7FFF)) XOR strPassword length XOR 0xCE4B. $lowOrderWord = (((($lowOrderWord >> 14) & 0x0001) | (($lowOrderWord << 1) & 0x7FFF)) ^ sizeof($byteChars) ^ 0xCE4B); - $combinedKey = $this->int32(($highOrderWord << 16) + $lowOrderWord); - $generatedKey = [ - 0 => (($combinedKey & 0x000000FF) >> 0), - 1 => (($combinedKey & 0x0000FF00) >> 8), - 2 => (($combinedKey & 0x00FF0000) >> 16), - 3 => (($combinedKey & 0xFF000000) >> 24), - ]; - - $tmpStr = ''; - for ($i = 0; $i < 4; $i++) { - $tmpStr .= strtoupper(dechex($generatedKey[$i])); - } - $generatedKey = []; - $tmpStr = mb_convert_encoding($tmpStr, 'UCS-2LE', 'UTF-8'); - for ($i = 0; $i < strlen($tmpStr); $i++) { - $generatedKey[] = ord(substr($tmpStr, $i, 1)); - } - - $salt = unpack('C*', base64_decode($this->getSalt())); - $algorithm = $this->getAlgorithm(); - - $tmpArray1 = $generatedKey; - $tmpArray2 = $salt; - $generatedKey = array_merge($tmpArray2, $tmpArray1); - - $generatedKey = $this->hashByteArray($algorithm, $generatedKey); - - for ($i = 0; $i < $this->getSpinCount(); $i++) { - $iterator = [ - 0 => (($i & 0x000000FF) >> 0), - 1 => (($i & 0x0000FF00) >> 8), - 2 => (($i & 0x00FF0000) >> 16), - 3 => (($i & 0xFF000000) >> 24), - ]; - $generatedKey = array_merge($generatedKey, $iterator); - $generatedKey = $this->hashByteArray($algorithm, $generatedKey); - } - - $hash = implode(array_map("chr", $generatedKey)); - - return base64_encode($hash); + // Combine the Low and High Order Word + return $this->int32(($highOrderWord << 16) + $lowOrderWord); } + /** + * simulate behaviour of int32 + * + * @param int $value + * @return int + */ private function int32($value) { $value = ($value & 0xFFFFFFFF); @@ -278,12 +362,4 @@ class Protection return $value; } - - private function hashByteArray($algorithm, $array) - { - $string = implode(array_map("chr", $array)); - $string = hash($algorithm, $string, true); - - return unpack('C*', $string); - } } From 703e34137b4fa9147d864fbef09442aa4370509c Mon Sep 17 00:00:00 2001 From: Maria Haubner Date: Fri, 10 Mar 2017 16:24:52 +0100 Subject: [PATCH 03/14] refactored hash function to word settings --- src/PhpWord/Metadata/Protection.php | 191 +---------------- src/PhpWord/Writer/Word2007/Part/Settings.php | 199 +++++++++++++++++- 2 files changed, 204 insertions(+), 186 deletions(-) diff --git a/src/PhpWord/Metadata/Protection.php b/src/PhpWord/Metadata/Protection.php index 5427f570..511503e4 100644 --- a/src/PhpWord/Metadata/Protection.php +++ b/src/PhpWord/Metadata/Protection.php @@ -25,59 +25,6 @@ namespace PhpOffice\PhpWord\Metadata; */ class Protection { - static $algorithmMapping = [ - 1 => 'md2', - 2 => 'md4', - 3 => 'md5', - 4 => 'sha1', - 5 => '', // 'mac' -> not possible with hash() - 6 => 'ripemd', - 7 => 'ripemd160', - 8 => '', - 9 => '', //'hmac' -> not possible with hash() - 10 => '', - 11 => '', - 12 => 'sha256', - 13 => 'sha384', - 14 => 'sha512', - ]; - static $initialCodeArray = [ - 0xE1F0, - 0x1D0F, - 0xCC9C, - 0x84C0, - 0x110C, - 0x0E10, - 0xF1CE, - 0x313E, - 0x1872, - 0xE139, - 0xD40F, - 0x84F9, - 0x280C, - 0xA96A, - 0x4EC3 - ]; - static $encryptionMatrix = - [ - [0xAEFC, 0x4DD9, 0x9BB2, 0x2745, 0x4E8A, 0x9D14, 0x2A09], - [0x7B61, 0xF6C2, 0xFDA5, 0xEB6B, 0xC6F7, 0x9DCF, 0x2BBF], - [0x4563, 0x8AC6, 0x05AD, 0x0B5A, 0x16B4, 0x2D68, 0x5AD0], - [0x0375, 0x06EA, 0x0DD4, 0x1BA8, 0x3750, 0x6EA0, 0xDD40], - [0xD849, 0xA0B3, 0x5147, 0xA28E, 0x553D, 0xAA7A, 0x44D5], - [0x6F45, 0xDE8A, 0xAD35, 0x4A4B, 0x9496, 0x390D, 0x721A], - [0xEB23, 0xC667, 0x9CEF, 0x29FF, 0x53FE, 0xA7FC, 0x5FD9], - [0x47D3, 0x8FA6, 0x0F6D, 0x1EDA, 0x3DB4, 0x7B68, 0xF6D0], - [0xB861, 0x60E3, 0xC1C6, 0x93AD, 0x377B, 0x6EF6, 0xDDEC], - [0x45A0, 0x8B40, 0x06A1, 0x0D42, 0x1A84, 0x3508, 0x6A10], - [0xAA51, 0x4483, 0x8906, 0x022D, 0x045A, 0x08B4, 0x1168], - [0x76B4, 0xED68, 0xCAF1, 0x85C3, 0x1BA7, 0x374E, 0x6E9C], - [0x3730, 0x6E60, 0xDCC0, 0xA9A1, 0x4363, 0x86C6, 0x1DAD], - [0x3331, 0x6662, 0xCCC4, 0x89A9, 0x0373, 0x06E6, 0x0DCC], - [0x1021, 0x2042, 0x4084, 0x8108, 0x1231, 0x2462, 0x48C4] - ]; - static $passwordMaxLength = 15; - /** * Editing restriction none|readOnly|comments|trackedChanges|forms * @@ -91,28 +38,28 @@ class Protection * * @var string */ - private $password; + private $password = ''; /** * Number of hashing iterations * * @var int */ - private $spinCount = 100000; + private $spinCount = 0; /** - * Algorithm-SID according to self::$algorithmMapping + * Algorithm-SID (see to \PhpOffice\PhpWord\Writer\Word2007\Part\Settings::$algorithmMapping) * * @var int */ - private $algorithmSid = 4; + private $algorithmSid = 0; /** * Hashed salt * * @var string */ - private $salt; + private $salt = ''; /** * Create a new instance @@ -165,7 +112,7 @@ class Protection */ public function setPassword($password) { - $this->password = $this->getPasswordHash($password); + $this->password = $password; return $this; } @@ -204,7 +151,7 @@ class Protection } /** - * Set algorithm-sid (see self::$algorithmMapping) + * Set algorithm-sid (see \PhpOffice\PhpWord\Writer\Word2007\Part\Settings::$algorithmMapping) * * @param $algorithmSid * @return self @@ -238,128 +185,4 @@ class Protection return $this; } - - /** - * Get algorithm from self::$algorithmMapping - * - * @return string - */ - private function getAlgorithm() - { - $algorithm = self::$algorithmMapping[$this->algorithmSid]; - if ($algorithm == '') { - $algorithm = 'sha1'; - } - - return $algorithm; - } - - /** - * Create a hashed password that MS Word will be able to work with - * - * @param string $password - * @return string - */ - private function getPasswordHash($password) - { - $orig_encoding = mb_internal_encoding(); - mb_internal_encoding("UTF-8"); - - if (empty($password)) { - return ''; - } - - $password = mb_substr($password, 0, min(self::$passwordMaxLength, mb_strlen($password))); - - // Construct a new NULL-terminated string consisting of single-byte characters: - // Get the single-byte values by iterating through the Unicode characters of the truncated password. - // For each character, if the low byte is not equal to 0, take it. Otherwise, take the high byte. - $pass_utf8 = mb_convert_encoding($password, 'UCS-2LE', 'UTF-8'); - $byteChars = []; - for ($i = 0; $i < mb_strlen($password); $i++) { - $byteChars[$i] = ord(substr($pass_utf8, $i * 2, 1)); - if ($byteChars[$i] == 0) { - $byteChars[$i] = ord(substr($pass_utf8, $i * 2 + 1, 1)); - } - } - - // build low-order word and hig-order word and combine them - $combinedKey = $this->buildCombinedKey($byteChars); - // build reversed hexadecimal string - $hex = strtoupper(dechex($combinedKey & 0xFFFFFFFF)); - $reversedHex = $hex[6].$hex[7].$hex[4].$hex[5].$hex[2].$hex[3].$hex[0].$hex[1]; - - $generatedKey = mb_convert_encoding($reversedHex, 'UCS-2LE', 'UTF-8'); - - // Implementation Notes List: - // Word requires that the initial hash of the password with the salt not be considered in the count. - // The initial hash of salt + key is not included in the iteration count. - $generatedKey = hash($this->getAlgorithm(), base64_decode($this->getSalt()) . $generatedKey, true); - for ($i = 0; $i < $this->getSpinCount(); $i++) { - $generatedKey = hash($this->getAlgorithm(), $generatedKey . pack("CCCC", $i, $i>>8, $i>>16, $i>>24), true); - } - $generatedKey = base64_encode($generatedKey); - - mb_internal_encoding($orig_encoding); - - return $generatedKey; - } - - /** - * Build combined key from low-order word and high-order word - * - * @param array $byteChars -> byte array representation of password - * @return int - */ - private function buildCombinedKey($byteChars) - { - // Compute the high-order word - // Initialize from the initial code array (see above), depending on the passwords length. - $highOrderWord = self::$initialCodeArray[sizeof($byteChars) - 1]; - - // For each character in the password: - // For every bit in the character, starting with the least significant and progressing to (but excluding) - // the most significant, if the bit is set, XOR the key’s high-order word with the corresponding word from - // the Encryption Matrix - for ($i = 0; $i < sizeof($byteChars); $i++) { - $tmp = self::$passwordMaxLength - sizeof($byteChars) + $i; - $matrixRow = self::$encryptionMatrix[$tmp]; - for ($intBit = 0; $intBit < 7; $intBit++) { - if (($byteChars[$i] & (0x0001 << $intBit)) != 0) { - $highOrderWord = ($highOrderWord ^ $matrixRow[$intBit]); - } - } - } - - // Compute low-order word - // Initialize with 0 - $lowOrderWord = 0; - // For each character in the password, going backwards - for ($i = sizeof($byteChars) - 1; $i >= 0; $i--) { - // low-order word = (((low-order word SHR 14) AND 0x0001) OR (low-order word SHL 1) AND 0x7FFF)) XOR character - $lowOrderWord = (((($lowOrderWord >> 14) & 0x0001) | (($lowOrderWord << 1) & 0x7FFF)) ^ $byteChars[$i]); - } - // Lastly, low-order word = (((low-order word SHR 14) AND 0x0001) OR (low-order word SHL 1) AND 0x7FFF)) XOR strPassword length XOR 0xCE4B. - $lowOrderWord = (((($lowOrderWord >> 14) & 0x0001) | (($lowOrderWord << 1) & 0x7FFF)) ^ sizeof($byteChars) ^ 0xCE4B); - - // Combine the Low and High Order Word - return $this->int32(($highOrderWord << 16) + $lowOrderWord); - } - - /** - * simulate behaviour of int32 - * - * @param int $value - * @return int - */ - private function int32($value) - { - $value = ($value & 0xFFFFFFFF); - - if ($value & 0x80000000) { - $value = -((~$value & 0xFFFFFFFF) + 1); - } - - return $value; - } } diff --git a/src/PhpWord/Writer/Word2007/Part/Settings.php b/src/PhpWord/Writer/Word2007/Part/Settings.php index 11549e08..ed9c07d3 100644 --- a/src/PhpWord/Writer/Word2007/Part/Settings.php +++ b/src/PhpWord/Writer/Word2007/Part/Settings.php @@ -24,6 +24,59 @@ namespace PhpOffice\PhpWord\Writer\Word2007\Part; */ class Settings extends AbstractPart { + static $algorithmMapping = [ + 1 => 'md2', + 2 => 'md4', + 3 => 'md5', + 4 => 'sha1', + 5 => '', // 'mac' -> not possible with hash() + 6 => 'ripemd', + 7 => 'ripemd160', + 8 => '', + 9 => '', //'hmac' -> not possible with hash() + 10 => '', + 11 => '', + 12 => 'sha256', + 13 => 'sha384', + 14 => 'sha512', + ]; + static $initialCodeArray = [ + 0xE1F0, + 0x1D0F, + 0xCC9C, + 0x84C0, + 0x110C, + 0x0E10, + 0xF1CE, + 0x313E, + 0x1872, + 0xE139, + 0xD40F, + 0x84F9, + 0x280C, + 0xA96A, + 0x4EC3 + ]; + static $encryptionMatrix = + [ + [0xAEFC, 0x4DD9, 0x9BB2, 0x2745, 0x4E8A, 0x9D14, 0x2A09], + [0x7B61, 0xF6C2, 0xFDA5, 0xEB6B, 0xC6F7, 0x9DCF, 0x2BBF], + [0x4563, 0x8AC6, 0x05AD, 0x0B5A, 0x16B4, 0x2D68, 0x5AD0], + [0x0375, 0x06EA, 0x0DD4, 0x1BA8, 0x3750, 0x6EA0, 0xDD40], + [0xD849, 0xA0B3, 0x5147, 0xA28E, 0x553D, 0xAA7A, 0x44D5], + [0x6F45, 0xDE8A, 0xAD35, 0x4A4B, 0x9496, 0x390D, 0x721A], + [0xEB23, 0xC667, 0x9CEF, 0x29FF, 0x53FE, 0xA7FC, 0x5FD9], + [0x47D3, 0x8FA6, 0x0F6D, 0x1EDA, 0x3DB4, 0x7B68, 0xF6D0], + [0xB861, 0x60E3, 0xC1C6, 0x93AD, 0x377B, 0x6EF6, 0xDDEC], + [0x45A0, 0x8B40, 0x06A1, 0x0D42, 0x1A84, 0x3508, 0x6A10], + [0xAA51, 0x4483, 0x8906, 0x022D, 0x045A, 0x08B4, 0x1168], + [0x76B4, 0xED68, 0xCAF1, 0x85C3, 0x1BA7, 0x374E, 0x6E9C], + [0x3730, 0x6E60, 0xDCC0, 0xA9A1, 0x4363, 0x86C6, 0x1DAD], + [0x3331, 0x6662, 0xCCC4, 0x89A9, 0x0373, 0x06E6, 0x0DCC], + [0x1021, 0x2042, 0x4084, 0x8108, 0x1231, 0x2462, 0x48C4] + ]; + static $passwordMaxLength = 15; + /** * Settings value * @@ -169,8 +222,8 @@ class Settings extends AbstractPart 'w:cryptAlgorithmType' => 'typeAny', 'w:cryptAlgorithmSid' => $protection->getAlgorithmSid(), 'w:cryptSpinCount' => $protection->getSpinCount(), - 'w:hash' => $protection->getPassword(), - 'w:salt' => $protection->getSalt(), + 'w:hash' => $this->getPasswordHash($protection), + 'w:salt' => $this->getSaltHash($protection->getSalt()), ) ); } @@ -193,4 +246,146 @@ class Settings extends AbstractPart )); } } + + + /** + * Create a hashed password that MS Word will be able to work with + * + * @param \PhpOffice\PhpWord\Metadata\Protection $protection + * @return string + */ + private function getPasswordHash($protection) + { + $orig_encoding = mb_internal_encoding(); + mb_internal_encoding("UTF-8"); + + $password = $protection->getPassword(); + $password = mb_substr($password, 0, min(self::$passwordMaxLength, mb_strlen($password))); + + // Construct a new NULL-terminated string consisting of single-byte characters: + // Get the single-byte values by iterating through the Unicode characters of the truncated password. + // For each character, if the low byte is not equal to 0, take it. Otherwise, take the high byte. + $pass_utf8 = mb_convert_encoding($password, 'UCS-2LE', 'UTF-8'); + $byteChars = []; + for ($i = 0; $i < mb_strlen($password); $i++) { + $byteChars[$i] = ord(substr($pass_utf8, $i * 2, 1)); + if ($byteChars[$i] == 0) { + $byteChars[$i] = ord(substr($pass_utf8, $i * 2 + 1, 1)); + } + } + + // build low-order word and hig-order word and combine them + $combinedKey = $this->buildCombinedKey($byteChars); + // build reversed hexadecimal string + $hex = strtoupper(dechex($combinedKey & 0xFFFFFFFF)); + $reversedHex = $hex[6].$hex[7].$hex[4].$hex[5].$hex[2].$hex[3].$hex[0].$hex[1]; + + $generatedKey = mb_convert_encoding($reversedHex, 'UCS-2LE', 'UTF-8'); + + // Implementation Notes List: + // Word requires that the initial hash of the password with the salt not be considered in the count. + // The initial hash of salt + key is not included in the iteration count. + $algorithm = $this->getAlgorithm($protection->getAlgorithmSid()); + $generatedKey = hash($algorithm, base64_decode($this->getSaltHash($protection->getSalt())) . $generatedKey, true); + + $spinCount = (!empty($protection->getSpinCount())) ? $protection->getSpinCount() : 100000; + + for ($i = 0; $i < $spinCount; $i++) { + $generatedKey = hash($algorithm, $generatedKey . pack("CCCC", $i, $i>>8, $i>>16, $i>>24), true); + } + $generatedKey = base64_encode($generatedKey); + + mb_internal_encoding($orig_encoding); + + return $generatedKey; + } + + /** + * Get algorithm from self::$algorithmMapping + * + * @param int $sid + * @return string + */ + private function getAlgorithm($sid) + { + if (empty($sid)) { + $sid = 4; + } + + $algorithm = self::$algorithmMapping[$sid]; + if ($algorithm == '') { + $algorithm = 'sha1'; + } + + return $algorithm; + } + + /** + * Get salt hash + * + * @param string $salt + * @return string + */ + private function getSaltHash($salt) + { + return $salt; + } + + /** + * Build combined key from low-order word and high-order word + * + * @param array $byteChars -> byte array representation of password + * @return int + */ + private function buildCombinedKey($byteChars) + { + // Compute the high-order word + // Initialize from the initial code array (see above), depending on the passwords length. + $highOrderWord = self::$initialCodeArray[sizeof($byteChars) - 1]; + + // For each character in the password: + // For every bit in the character, starting with the least significant and progressing to (but excluding) + // the most significant, if the bit is set, XOR the key’s high-order word with the corresponding word from + // the Encryption Matrix + for ($i = 0; $i < sizeof($byteChars); $i++) { + $tmp = self::$passwordMaxLength - sizeof($byteChars) + $i; + $matrixRow = self::$encryptionMatrix[$tmp]; + for ($intBit = 0; $intBit < 7; $intBit++) { + if (($byteChars[$i] & (0x0001 << $intBit)) != 0) { + $highOrderWord = ($highOrderWord ^ $matrixRow[$intBit]); + } + } + } + + // Compute low-order word + // Initialize with 0 + $lowOrderWord = 0; + // For each character in the password, going backwards + for ($i = sizeof($byteChars) - 1; $i >= 0; $i--) { + // low-order word = (((low-order word SHR 14) AND 0x0001) OR (low-order word SHL 1) AND 0x7FFF)) XOR character + $lowOrderWord = (((($lowOrderWord >> 14) & 0x0001) | (($lowOrderWord << 1) & 0x7FFF)) ^ $byteChars[$i]); + } + // Lastly, low-order word = (((low-order word SHR 14) AND 0x0001) OR (low-order word SHL 1) AND 0x7FFF)) XOR strPassword length XOR 0xCE4B. + $lowOrderWord = (((($lowOrderWord >> 14) & 0x0001) | (($lowOrderWord << 1) & 0x7FFF)) ^ sizeof($byteChars) ^ 0xCE4B); + + // Combine the Low and High Order Word + return $this->int32(($highOrderWord << 16) + $lowOrderWord); + } + + /** + * Simulate behaviour of int32 + * + * @param int $value + * @return int + */ + private function int32($value) + { + $value = ($value & 0xFFFFFFFF); + + if ($value & 0x80000000) { + $value = -((~$value & 0xFFFFFFFF) + 1); + } + + return $value; + } } From 0221414ee0855612b6a5f09412790fb2bf2dfd1e Mon Sep 17 00:00:00 2001 From: Maria Haubner Date: Fri, 10 Mar 2017 16:57:42 +0100 Subject: [PATCH 04/14] randomly genereate salt for word password protection --- src/PhpWord/Metadata/Protection.php | 14 ++++---- src/PhpWord/Writer/Word2007/Part/Settings.php | 35 +++++++++---------- 2 files changed, 24 insertions(+), 25 deletions(-) diff --git a/src/PhpWord/Metadata/Protection.php b/src/PhpWord/Metadata/Protection.php index 511503e4..a25a8f31 100644 --- a/src/PhpWord/Metadata/Protection.php +++ b/src/PhpWord/Metadata/Protection.php @@ -45,14 +45,14 @@ class Protection * * @var int */ - private $spinCount = 0; + private $spinCount = 100000; /** * Algorithm-SID (see to \PhpOffice\PhpWord\Writer\Word2007\Part\Settings::$algorithmMapping) * * @var int */ - private $algorithmSid = 0; + private $mswordAlgorithmSid = 4; /** * Hashed salt @@ -145,20 +145,20 @@ class Protection * * @return int */ - public function getAlgorithmSid() + public function getMswordAlgorithmSid() { - return $this->algorithmSid; + return $this->mswordAlgorithmSid; } /** * Set algorithm-sid (see \PhpOffice\PhpWord\Writer\Word2007\Part\Settings::$algorithmMapping) * - * @param $algorithmSid + * @param $mswordAlgorithmSid * @return self */ - public function setAlgorithmSid($algorithmSid) + public function setMswordAlgorithmSid($mswordAlgorithmSid) { - $this->algorithmSid = $algorithmSid; + $this->mswordAlgorithmSid = $mswordAlgorithmSid; return $this; } diff --git a/src/PhpWord/Writer/Word2007/Part/Settings.php b/src/PhpWord/Writer/Word2007/Part/Settings.php index ed9c07d3..c709ee62 100644 --- a/src/PhpWord/Writer/Word2007/Part/Settings.php +++ b/src/PhpWord/Writer/Word2007/Part/Settings.php @@ -213,6 +213,9 @@ class Settings extends AbstractPart ) ); } else { + if ($protection->getSalt() == null) { + $protection->setSalt(openssl_random_pseudo_bytes(16)); + } $this->settings['w:documentProtection'] = array( '@attributes' => array( 'w:enforcement' => 1, @@ -220,7 +223,7 @@ class Settings extends AbstractPart 'w:cryptProviderType' => 'rsaFull', 'w:cryptAlgorithmClass' => 'hash', 'w:cryptAlgorithmType' => 'typeAny', - 'w:cryptAlgorithmSid' => $protection->getAlgorithmSid(), + 'w:cryptAlgorithmSid' => $protection->getMswordAlgorithmSid(), 'w:cryptSpinCount' => $protection->getSpinCount(), 'w:hash' => $this->getPasswordHash($protection), 'w:salt' => $this->getSaltHash($protection->getSalt()), @@ -239,11 +242,13 @@ class Settings extends AbstractPart { $compatibility = $this->getParentWriter()->getPhpWord()->getCompatibility(); if ($compatibility->getOoxmlVersion() !== null) { - $this->settings['w:compat']['w:compatSetting'] = array('@attributes' => array( - 'w:name' => 'compatibilityMode', - 'w:uri' => 'http://schemas.microsoft.com/office/word', - 'w:val' => $compatibility->getOoxmlVersion(), - )); + $this->settings['w:compat']['w:compatSetting'] = array( + '@attributes' => array( + 'w:name' => 'compatibilityMode', + 'w:uri' => 'http://schemas.microsoft.com/office/word', + 'w:val' => $compatibility->getOoxmlVersion(), + ) + ); } } @@ -277,21 +282,19 @@ class Settings extends AbstractPart // build low-order word and hig-order word and combine them $combinedKey = $this->buildCombinedKey($byteChars); // build reversed hexadecimal string - $hex = strtoupper(dechex($combinedKey & 0xFFFFFFFF)); - $reversedHex = $hex[6].$hex[7].$hex[4].$hex[5].$hex[2].$hex[3].$hex[0].$hex[1]; + $hex = strtoupper(dechex($combinedKey & 0xFFFFFFFF)); + $reversedHex = $hex[6] . $hex[7] . $hex[4] . $hex[5] . $hex[2] . $hex[3] . $hex[0] . $hex[1]; $generatedKey = mb_convert_encoding($reversedHex, 'UCS-2LE', 'UTF-8'); // Implementation Notes List: // Word requires that the initial hash of the password with the salt not be considered in the count. // The initial hash of salt + key is not included in the iteration count. - $algorithm = $this->getAlgorithm($protection->getAlgorithmSid()); + $algorithm = $this->getAlgorithm($protection->getMswordAlgorithmSid()); $generatedKey = hash($algorithm, base64_decode($this->getSaltHash($protection->getSalt())) . $generatedKey, true); - $spinCount = (!empty($protection->getSpinCount())) ? $protection->getSpinCount() : 100000; - - for ($i = 0; $i < $spinCount; $i++) { - $generatedKey = hash($algorithm, $generatedKey . pack("CCCC", $i, $i>>8, $i>>16, $i>>24), true); + for ($i = 0; $i < $protection->getSpinCount(); $i++) { + $generatedKey = hash($algorithm, $generatedKey . pack("CCCC", $i, $i >> 8, $i >> 16, $i >> 24), true); } $generatedKey = base64_encode($generatedKey); @@ -308,10 +311,6 @@ class Settings extends AbstractPart */ private function getAlgorithm($sid) { - if (empty($sid)) { - $sid = 4; - } - $algorithm = self::$algorithmMapping[$sid]; if ($algorithm == '') { $algorithm = 'sha1'; @@ -328,7 +327,7 @@ class Settings extends AbstractPart */ private function getSaltHash($salt) { - return $salt; + return base64_encode(str_pad(substr($salt, 0, 16), 16, '1')); } /** From 76246630ce4eac8ba9aac13f8b19cb4b657ca947 Mon Sep 17 00:00:00 2001 From: Maria Haubner Date: Fri, 10 Mar 2017 17:30:51 +0100 Subject: [PATCH 05/14] add test --- src/PhpWord/Writer/Word2007/Part/Settings.php | 2 +- .../Writer/Word2007/Part/SettingsTest.php | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/PhpWord/Writer/Word2007/Part/Settings.php b/src/PhpWord/Writer/Word2007/Part/Settings.php index c709ee62..07d7a90c 100644 --- a/src/PhpWord/Writer/Word2007/Part/Settings.php +++ b/src/PhpWord/Writer/Word2007/Part/Settings.php @@ -282,7 +282,7 @@ class Settings extends AbstractPart // build low-order word and hig-order word and combine them $combinedKey = $this->buildCombinedKey($byteChars); // build reversed hexadecimal string - $hex = strtoupper(dechex($combinedKey & 0xFFFFFFFF)); + $hex = str_pad(strtoupper(dechex($combinedKey & 0xFFFFFFFF)), 8, '0', \STR_PAD_LEFT); $reversedHex = $hex[6] . $hex[7] . $hex[4] . $hex[5] . $hex[2] . $hex[3] . $hex[0] . $hex[1]; $generatedKey = mb_convert_encoding($reversedHex, 'UCS-2LE', 'UTF-8'); diff --git a/tests/PhpWord/Writer/Word2007/Part/SettingsTest.php b/tests/PhpWord/Writer/Word2007/Part/SettingsTest.php index 6ed23e44..110d2aff 100644 --- a/tests/PhpWord/Writer/Word2007/Part/SettingsTest.php +++ b/tests/PhpWord/Writer/Word2007/Part/SettingsTest.php @@ -50,6 +50,27 @@ class SettingsTest extends \PHPUnit_Framework_TestCase $this->assertTrue($doc->elementExists($path, $file)); } + /** + * Test document protection with password + * + * Note: to get comparison values, a docx was generated in Word2010 and the values taken from the settings.xml + */ + public function testDocumentProtectionWithPassword() + { + $phpWord = new PhpWord(); + $phpWord->getProtection()->setEditing('readOnly'); + $phpWord->getProtection()->setPassword('testÄö@€!$&'); + $phpWord->getProtection()->setSalt(base64_decode("uq81pJRRGFIY5U+E9gt8tA==")); + + $doc = TestHelperDOCX::getDocument($phpWord); + + $file = 'word/settings.xml'; + + $path = '/w:settings/w:documentProtection'; + $this->assertTrue($doc->elementExists($path, $file)); + $this->assertEquals($doc->getElement($path, $file)->getAttribute('w:hash'), "RA9jfY/u3DX114PMcl+uSekxsYk="); + } + /** * Test compatibility */ From 71574d1fe2a3638e4bb3f4a888c9a8d4cd815821 Mon Sep 17 00:00:00 2001 From: Lenz Weber Date: Mon, 13 Mar 2017 16:22:04 +0100 Subject: [PATCH 06/14] Code Review; minor changes to salt handling, corrected some comments --- src/PhpWord/Metadata/Protection.php | 15 ++++++++---- src/PhpWord/Writer/Word2007/Part/Settings.php | 23 +++++-------------- 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/src/PhpWord/Metadata/Protection.php b/src/PhpWord/Metadata/Protection.php index a25a8f31..88cfa99e 100644 --- a/src/PhpWord/Metadata/Protection.php +++ b/src/PhpWord/Metadata/Protection.php @@ -34,7 +34,7 @@ class Protection private $editing; /** - * Hashed password + * password * * @var string */ @@ -55,7 +55,7 @@ class Protection private $mswordAlgorithmSid = 4; /** - * Hashed salt + * salt * * @var string */ @@ -95,7 +95,7 @@ class Protection } /** - * Get password hash + * Get password * * @return string */ @@ -164,7 +164,7 @@ class Protection } /** - * Get salt hash + * Get salt * * @return string */ @@ -174,13 +174,18 @@ class Protection } /** - * Set salt hash + * Set salt. Salt HAS to be 16 characters, or an exception will be thrown. * * @param $salt * @return self + * @throws \InvalidArgumentException */ public function setSalt($salt) { + if ($salt !== null && strlen($salt) !== 16){ + throw new \InvalidArgumentException('salt has to be of exactly 16 bytes length'); + } + $this->salt = $salt; return $this; diff --git a/src/PhpWord/Writer/Word2007/Part/Settings.php b/src/PhpWord/Writer/Word2007/Part/Settings.php index 07d7a90c..82f8192a 100644 --- a/src/PhpWord/Writer/Word2007/Part/Settings.php +++ b/src/PhpWord/Writer/Word2007/Part/Settings.php @@ -225,8 +225,8 @@ class Settings extends AbstractPart 'w:cryptAlgorithmType' => 'typeAny', 'w:cryptAlgorithmSid' => $protection->getMswordAlgorithmSid(), 'w:cryptSpinCount' => $protection->getSpinCount(), - 'w:hash' => $this->getPasswordHash($protection), - 'w:salt' => $this->getSaltHash($protection->getSalt()), + 'w:hash' => $this->getEncodedPasswordHash($protection), + 'w:salt' => base64_encode($protection->getSalt()), ) ); } @@ -255,11 +255,12 @@ class Settings extends AbstractPart /** * Create a hashed password that MS Word will be able to work with + * @link https://blogs.msdn.microsoft.com/vsod/2010/04/05/how-to-set-the-editing-restrictions-in-word-using-open-xml-sdk-2-0/ * * @param \PhpOffice\PhpWord\Metadata\Protection $protection * @return string */ - private function getPasswordHash($protection) + private function getEncodedPasswordHash($protection) { $orig_encoding = mb_internal_encoding(); mb_internal_encoding("UTF-8"); @@ -267,7 +268,6 @@ class Settings extends AbstractPart $password = $protection->getPassword(); $password = mb_substr($password, 0, min(self::$passwordMaxLength, mb_strlen($password))); - // Construct a new NULL-terminated string consisting of single-byte characters: // Get the single-byte values by iterating through the Unicode characters of the truncated password. // For each character, if the low byte is not equal to 0, take it. Otherwise, take the high byte. $pass_utf8 = mb_convert_encoding($password, 'UCS-2LE', 'UTF-8'); @@ -291,7 +291,7 @@ class Settings extends AbstractPart // Word requires that the initial hash of the password with the salt not be considered in the count. // The initial hash of salt + key is not included in the iteration count. $algorithm = $this->getAlgorithm($protection->getMswordAlgorithmSid()); - $generatedKey = hash($algorithm, base64_decode($this->getSaltHash($protection->getSalt())) . $generatedKey, true); + $generatedKey = hash($algorithm, $protection->getSalt() . $generatedKey, true); for ($i = 0; $i < $protection->getSpinCount(); $i++) { $generatedKey = hash($algorithm, $generatedKey . pack("CCCC", $i, $i >> 8, $i >> 16, $i >> 24), true); @@ -319,17 +319,6 @@ class Settings extends AbstractPart return $algorithm; } - /** - * Get salt hash - * - * @param string $salt - * @return string - */ - private function getSaltHash($salt) - { - return base64_encode(str_pad(substr($salt, 0, 16), 16, '1')); - } - /** * Build combined key from low-order word and high-order word * @@ -372,7 +361,7 @@ class Settings extends AbstractPart } /** - * Simulate behaviour of int32 + * Simulate behaviour of (signed) int32 * * @param int $value * @return int From ad83196a052b4fcb0b00a278a16dc0ef53ec6c74 Mon Sep 17 00:00:00 2001 From: troosan Date: Thu, 23 Nov 2017 22:49:21 +0100 Subject: [PATCH 07/14] move password encoding in separate class fix PHPCS errors add documentation add sample --- docs/general.rst | 11 + samples/Sample_38_Protection.php | 21 ++ src/PhpWord/Metadata/Protection.php | 23 +- .../Shared/Microsoft/PasswordEncoder.php | 205 +++++++++++++++++ src/PhpWord/SimpleType/DocProtect.php | 55 +++++ src/PhpWord/Writer/Word2007/Part/Settings.php | 208 ++---------------- tests/PhpWord/Metadata/SettingsTest.php | 13 +- .../Writer/Word2007/Part/SettingsTest.php | 33 ++- 8 files changed, 360 insertions(+), 209 deletions(-) create mode 100644 samples/Sample_38_Protection.php create mode 100644 src/PhpWord/Shared/Microsoft/PasswordEncoder.php create mode 100644 src/PhpWord/SimpleType/DocProtect.php diff --git a/docs/general.rst b/docs/general.rst index b11734b1..f6c8df1c 100644 --- a/docs/general.rst +++ b/docs/general.rst @@ -271,3 +271,14 @@ points to twips. $sectionStyle->setMarginLeft(\PhpOffice\PhpWord\Shared\Converter::inchToTwip(.5)); // 2 cm right margin $sectionStyle->setMarginRight(\PhpOffice\PhpWord\Shared\Converter::cmToTwip(2)); + +Document protection +------------------- + +The document (or parts of it) can be password protected. + +.. code-block:: php + + $documentProtection = $phpWord->getSettings()->getDocumentProtection(); + $documentProtection->setEditing(DocProtect::READ_ONLY); + $documentProtection->setPassword('myPassword'); diff --git a/samples/Sample_38_Protection.php b/samples/Sample_38_Protection.php new file mode 100644 index 00000000..ee2b460b --- /dev/null +++ b/samples/Sample_38_Protection.php @@ -0,0 +1,21 @@ +getSettings()->getDocumentProtection(); +$documentProtection->setEditing(DocProtect::READ_ONLY); +$documentProtection->setPassword('myPassword'); + +$section = $phpWord->addSection(); +$section->addText('this document is password protected'); + +// Save file +echo write($phpWord, basename(__FILE__, '.php'), $writers); +if (!CLI) { + include_once 'Sample_Footer.php'; +} diff --git a/src/PhpWord/Metadata/Protection.php b/src/PhpWord/Metadata/Protection.php index be78c055..09d08aac 100644 --- a/src/PhpWord/Metadata/Protection.php +++ b/src/PhpWord/Metadata/Protection.php @@ -17,6 +17,8 @@ namespace PhpOffice\PhpWord\Metadata; +use PhpOffice\PhpWord\SimpleType\DocProtect; + /** * Document protection class * @@ -38,28 +40,28 @@ class Protection * * @var string */ - private $password = ''; + private $password; /** - * Number of hashing iterations + * Iterations to Run Hashing Algorithm * * @var int */ private $spinCount = 100000; /** - * Algorithm-SID (see to \PhpOffice\PhpWord\Writer\Word2007\Part\Settings::$algorithmMapping) + * Cryptographic Hashing Algorithm (see to \PhpOffice\PhpWord\Writer\Word2007\Part\Settings::$algorithmMapping) * * @var int */ private $mswordAlgorithmSid = 4; /** - * salt + * Salt for Password Verifier * * @var string */ - private $salt = ''; + private $salt; /** * Create a new instance @@ -68,7 +70,9 @@ class Protection */ public function __construct($editing = null) { - $this->setEditing($editing); + if ($editing != null) { + $this->setEditing($editing); + } } /** @@ -84,11 +88,12 @@ class Protection /** * Set editing protection * - * @param string $editing + * @param string $editing Any value of \PhpOffice\PhpWord\SimpleType\DocProtect * @return self */ public function setEditing($editing = null) { + DocProtect::validate($editing); $this->editing = $editing; return $this; @@ -177,12 +182,12 @@ class Protection * Set salt. Salt HAS to be 16 characters, or an exception will be thrown. * * @param $salt - * @return self * @throws \InvalidArgumentException + * @return self */ public function setSalt($salt) { - if ($salt !== null && strlen($salt) !== 16){ + if ($salt !== null && strlen($salt) !== 16) { throw new \InvalidArgumentException('salt has to be of exactly 16 bytes length'); } diff --git a/src/PhpWord/Shared/Microsoft/PasswordEncoder.php b/src/PhpWord/Shared/Microsoft/PasswordEncoder.php new file mode 100644 index 00000000..40a3ea12 --- /dev/null +++ b/src/PhpWord/Shared/Microsoft/PasswordEncoder.php @@ -0,0 +1,205 @@ + 'md2', + 2 => 'md4', + 3 => 'md5', + 4 => 'sha1', + 5 => '', // 'mac' -> not possible with hash() + 6 => 'ripemd', + 7 => 'ripemd160', + 8 => '', + 9 => '', //'hmac' -> not possible with hash() + 10 => '', + 11 => '', + 12 => 'sha256', + 13 => 'sha384', + 14 => 'sha512', + ); + + private static $initialCodeArray = array( + 0xE1F0, + 0x1D0F, + 0xCC9C, + 0x84C0, + 0x110C, + 0x0E10, + 0xF1CE, + 0x313E, + 0x1872, + 0xE139, + 0xD40F, + 0x84F9, + 0x280C, + 0xA96A, + 0x4EC3, + ); + + private static $encryptionMatrix = array( + array(0xAEFC, 0x4DD9, 0x9BB2, 0x2745, 0x4E8A, 0x9D14, 0x2A09), + array(0x7B61, 0xF6C2, 0xFDA5, 0xEB6B, 0xC6F7, 0x9DCF, 0x2BBF), + array(0x4563, 0x8AC6, 0x05AD, 0x0B5A, 0x16B4, 0x2D68, 0x5AD0), + array(0x0375, 0x06EA, 0x0DD4, 0x1BA8, 0x3750, 0x6EA0, 0xDD40), + array(0xD849, 0xA0B3, 0x5147, 0xA28E, 0x553D, 0xAA7A, 0x44D5), + array(0x6F45, 0xDE8A, 0xAD35, 0x4A4B, 0x9496, 0x390D, 0x721A), + array(0xEB23, 0xC667, 0x9CEF, 0x29FF, 0x53FE, 0xA7FC, 0x5FD9), + array(0x47D3, 0x8FA6, 0x0F6D, 0x1EDA, 0x3DB4, 0x7B68, 0xF6D0), + array(0xB861, 0x60E3, 0xC1C6, 0x93AD, 0x377B, 0x6EF6, 0xDDEC), + array(0x45A0, 0x8B40, 0x06A1, 0x0D42, 0x1A84, 0x3508, 0x6A10), + array(0xAA51, 0x4483, 0x8906, 0x022D, 0x045A, 0x08B4, 0x1168), + array(0x76B4, 0xED68, 0xCAF1, 0x85C3, 0x1BA7, 0x374E, 0x6E9C), + array(0x3730, 0x6E60, 0xDCC0, 0xA9A1, 0x4363, 0x86C6, 0x1DAD), + array(0x3331, 0x6662, 0xCCC4, 0x89A9, 0x0373, 0x06E6, 0x0DCC), + array(0x1021, 0x2042, 0x4084, 0x8108, 0x1231, 0x2462, 0x48C4), + ); + + private static $passwordMaxLength = 15; + + /** + * Create a hashed password that MS Word will be able to work with + * @see https://blogs.msdn.microsoft.com/vsod/2010/04/05/how-to-set-the-editing-restrictions-in-word-using-open-xml-sdk-2-0/ + * + * @param string $password + * @param number $algorithmSid + * @param string $salt + * @param number $spinCount + * @return string + */ + public static function hashPassword($password, $algorithmSid = 4, $salt = null, $spinCount = 10000) + { + $origEncoding = mb_internal_encoding(); + mb_internal_encoding('UTF-8'); + + $password = mb_substr($password, 0, min(self::$passwordMaxLength, mb_strlen($password))); + + // Get the single-byte values by iterating through the Unicode characters of the truncated password. + // For each character, if the low byte is not equal to 0, take it. Otherwise, take the high byte. + $passUtf8 = mb_convert_encoding($password, 'UCS-2LE', 'UTF-8'); + $byteChars = array(); + for ($i = 0; $i < mb_strlen($password); $i++) { + $byteChars[$i] = ord(substr($passUtf8, $i * 2, 1)); + if ($byteChars[$i] == 0) { + $byteChars[$i] = ord(substr($passUtf8, $i * 2 + 1, 1)); + } + } + + // build low-order word and hig-order word and combine them + $combinedKey = self::buildCombinedKey($byteChars); + // build reversed hexadecimal string + $hex = str_pad(strtoupper(dechex($combinedKey & 0xFFFFFFFF)), 8, '0', \STR_PAD_LEFT); + $reversedHex = $hex[6] . $hex[7] . $hex[4] . $hex[5] . $hex[2] . $hex[3] . $hex[0] . $hex[1]; + + $generatedKey = mb_convert_encoding($reversedHex, 'UCS-2LE', 'UTF-8'); + + // Implementation Notes List: + // Word requires that the initial hash of the password with the salt not be considered in the count. + // The initial hash of salt + key is not included in the iteration count. + $algorithm = self::getAlgorithm($algorithmSid); + $generatedKey = hash($algorithm, $salt . $generatedKey, true); + + for ($i = 0; $i < $spinCount; $i++) { + $generatedKey = hash($algorithm, $generatedKey . pack('CCCC', $i, $i >> 8, $i >> 16, $i >> 24), true); + } + $generatedKey = base64_encode($generatedKey); + + mb_internal_encoding($origEncoding); + + return $generatedKey; + } + + /** + * Get algorithm from self::$algorithmMapping + * + * @param int $sid + * @return string + */ + private static function getAlgorithm($sid) + { + $algorithm = self::$algorithmMapping[$sid]; + if ($algorithm == '') { + $algorithm = 'sha1'; + } + + return $algorithm; + } + + /** + * Build combined key from low-order word and high-order word + * + * @param array $byteChars byte array representation of password + * @return int + */ + private static function buildCombinedKey($byteChars) + { + // Compute the high-order word + // Initialize from the initial code array (see above), depending on the passwords length. + $highOrderWord = self::$initialCodeArray[count($byteChars) - 1]; + + // For each character in the password: + // For every bit in the character, starting with the least significant and progressing to (but excluding) + // the most significant, if the bit is set, XOR the key’s high-order word with the corresponding word from + // the Encryption Matrix + for ($i = 0; $i < count($byteChars); $i++) { + $tmp = self::$passwordMaxLength - count($byteChars) + $i; + $matrixRow = self::$encryptionMatrix[$tmp]; + for ($intBit = 0; $intBit < 7; $intBit++) { + if (($byteChars[$i] & (0x0001 << $intBit)) != 0) { + $highOrderWord = ($highOrderWord ^ $matrixRow[$intBit]); + } + } + } + + // Compute low-order word + // Initialize with 0 + $lowOrderWord = 0; + // For each character in the password, going backwards + for ($i = count($byteChars) - 1; $i >= 0; $i--) { + // low-order word = (((low-order word SHR 14) AND 0x0001) OR (low-order word SHL 1) AND 0x7FFF)) XOR character + $lowOrderWord = (((($lowOrderWord >> 14) & 0x0001) | (($lowOrderWord << 1) & 0x7FFF)) ^ $byteChars[$i]); + } + // Lastly, low-order word = (((low-order word SHR 14) AND 0x0001) OR (low-order word SHL 1) AND 0x7FFF)) XOR strPassword length XOR 0xCE4B. + $lowOrderWord = (((($lowOrderWord >> 14) & 0x0001) | (($lowOrderWord << 1) & 0x7FFF)) ^ count($byteChars) ^ 0xCE4B); + + // Combine the Low and High Order Word + return self::int32(($highOrderWord << 16) + $lowOrderWord); + } + + /** + * Simulate behaviour of (signed) int32 + * + * @param int $value + * @return int + */ + private static function int32($value) + { + $value = ($value & 0xFFFFFFFF); + + if ($value & 0x80000000) { + $value = -((~$value & 0xFFFFFFFF) + 1); + } + + return $value; + } +} diff --git a/src/PhpWord/SimpleType/DocProtect.php b/src/PhpWord/SimpleType/DocProtect.php new file mode 100644 index 00000000..cffa0003 --- /dev/null +++ b/src/PhpWord/SimpleType/DocProtect.php @@ -0,0 +1,55 @@ + 'md2', - 2 => 'md4', - 3 => 'md5', - 4 => 'sha1', - 5 => '', // 'mac' -> not possible with hash() - 6 => 'ripemd', - 7 => 'ripemd160', - 8 => '', - 9 => '', //'hmac' -> not possible with hash() - 10 => '', - 11 => '', - 12 => 'sha256', - 13 => 'sha384', - 14 => 'sha512', - ]; - static $initialCodeArray = [ - 0xE1F0, - 0x1D0F, - 0xCC9C, - 0x84C0, - 0x110C, - 0x0E10, - 0xF1CE, - 0x313E, - 0x1872, - 0xE139, - 0xD40F, - 0x84F9, - 0x280C, - 0xA96A, - 0x4EC3 - ]; - static $encryptionMatrix = - [ - [0xAEFC, 0x4DD9, 0x9BB2, 0x2745, 0x4E8A, 0x9D14, 0x2A09], - [0x7B61, 0xF6C2, 0xFDA5, 0xEB6B, 0xC6F7, 0x9DCF, 0x2BBF], - [0x4563, 0x8AC6, 0x05AD, 0x0B5A, 0x16B4, 0x2D68, 0x5AD0], - [0x0375, 0x06EA, 0x0DD4, 0x1BA8, 0x3750, 0x6EA0, 0xDD40], - [0xD849, 0xA0B3, 0x5147, 0xA28E, 0x553D, 0xAA7A, 0x44D5], - [0x6F45, 0xDE8A, 0xAD35, 0x4A4B, 0x9496, 0x390D, 0x721A], - [0xEB23, 0xC667, 0x9CEF, 0x29FF, 0x53FE, 0xA7FC, 0x5FD9], - [0x47D3, 0x8FA6, 0x0F6D, 0x1EDA, 0x3DB4, 0x7B68, 0xF6D0], - [0xB861, 0x60E3, 0xC1C6, 0x93AD, 0x377B, 0x6EF6, 0xDDEC], - [0x45A0, 0x8B40, 0x06A1, 0x0D42, 0x1A84, 0x3508, 0x6A10], - [0xAA51, 0x4483, 0x8906, 0x022D, 0x045A, 0x08B4, 0x1168], - [0x76B4, 0xED68, 0xCAF1, 0x85C3, 0x1BA7, 0x374E, 0x6E9C], - [0x3730, 0x6E60, 0xDCC0, 0xA9A1, 0x4363, 0x86C6, 0x1DAD], - [0x3331, 0x6662, 0xCCC4, 0x89A9, 0x0373, 0x06E6, 0x0DCC], - [0x1021, 0x2042, 0x4084, 0x8108, 0x1231, 0x2462, 0x48C4] - ]; - static $passwordMaxLength = 15; - /** * Settings value * @@ -238,25 +186,26 @@ class Settings extends AbstractPart $this->settings['w:documentProtection'] = array( '@attributes' => array( 'w:enforcement' => 1, - 'w:edit' => $documentProtection->getEditing(), - ) + 'w:edit' => $documentProtection->getEditing(), + ), ); } else { if ($documentProtection->getSalt() == null) { $documentProtection->setSalt(openssl_random_pseudo_bytes(16)); } + $passwordHash = PasswordEncoder::hashPassword($documentProtection->getPassword(), $documentProtection->getMswordAlgorithmSid(), $documentProtection->getSalt(), $documentProtection->getSpinCount()); $this->settings['w:documentProtection'] = array( '@attributes' => array( - 'w:enforcement' => 1, - 'w:edit' => $documentProtection->getEditing(), - 'w:cryptProviderType' => 'rsaFull', + 'w:enforcement' => 1, + 'w:edit' => $documentProtection->getEditing(), + 'w:cryptProviderType' => 'rsaFull', 'w:cryptAlgorithmClass' => 'hash', - 'w:cryptAlgorithmType' => 'typeAny', - 'w:cryptAlgorithmSid' => $documentProtection->getMswordAlgorithmSid(), - 'w:cryptSpinCount' => $documentProtection->getSpinCount(), - 'w:hash' => $this->getEncodedPasswordHash($documentProtection), - 'w:salt' => base64_encode($documentProtection->getSalt()), - ) + 'w:cryptAlgorithmType' => 'typeAny', + 'w:cryptAlgorithmSid' => $documentProtection->getMswordAlgorithmSid(), + 'w:cryptSpinCount' => $documentProtection->getSpinCount(), + 'w:hash' => $passwordHash, + 'w:salt' => base64_encode($documentProtection->getSalt()), + ), ); } } @@ -337,135 +286,10 @@ class Settings extends AbstractPart $this->settings['w:compat']['w:compatSetting'] = array( '@attributes' => array( 'w:name' => 'compatibilityMode', - 'w:uri' => 'http://schemas.microsoft.com/office/word', - 'w:val' => $compatibility->getOoxmlVersion(), - ) + 'w:uri' => 'http://schemas.microsoft.com/office/word', + 'w:val' => $compatibility->getOoxmlVersion(), + ), ); } } - - - /** - * Create a hashed password that MS Word will be able to work with - * @link https://blogs.msdn.microsoft.com/vsod/2010/04/05/how-to-set-the-editing-restrictions-in-word-using-open-xml-sdk-2-0/ - * - * @param \PhpOffice\PhpWord\Metadata\Protection $protection - * @return string - */ - private function getEncodedPasswordHash($protection) - { - $orig_encoding = mb_internal_encoding(); - mb_internal_encoding("UTF-8"); - - $password = $protection->getPassword(); - $password = mb_substr($password, 0, min(self::$passwordMaxLength, mb_strlen($password))); - - // Get the single-byte values by iterating through the Unicode characters of the truncated password. - // For each character, if the low byte is not equal to 0, take it. Otherwise, take the high byte. - $pass_utf8 = mb_convert_encoding($password, 'UCS-2LE', 'UTF-8'); - $byteChars = []; - for ($i = 0; $i < mb_strlen($password); $i++) { - $byteChars[$i] = ord(substr($pass_utf8, $i * 2, 1)); - if ($byteChars[$i] == 0) { - $byteChars[$i] = ord(substr($pass_utf8, $i * 2 + 1, 1)); - } - } - - // build low-order word and hig-order word and combine them - $combinedKey = $this->buildCombinedKey($byteChars); - // build reversed hexadecimal string - $hex = str_pad(strtoupper(dechex($combinedKey & 0xFFFFFFFF)), 8, '0', \STR_PAD_LEFT); - $reversedHex = $hex[6] . $hex[7] . $hex[4] . $hex[5] . $hex[2] . $hex[3] . $hex[0] . $hex[1]; - - $generatedKey = mb_convert_encoding($reversedHex, 'UCS-2LE', 'UTF-8'); - - // Implementation Notes List: - // Word requires that the initial hash of the password with the salt not be considered in the count. - // The initial hash of salt + key is not included in the iteration count. - $algorithm = $this->getAlgorithm($protection->getMswordAlgorithmSid()); - $generatedKey = hash($algorithm, $protection->getSalt() . $generatedKey, true); - - for ($i = 0; $i < $protection->getSpinCount(); $i++) { - $generatedKey = hash($algorithm, $generatedKey . pack("CCCC", $i, $i >> 8, $i >> 16, $i >> 24), true); - } - $generatedKey = base64_encode($generatedKey); - - mb_internal_encoding($orig_encoding); - - return $generatedKey; - } - - /** - * Get algorithm from self::$algorithmMapping - * - * @param int $sid - * @return string - */ - private function getAlgorithm($sid) - { - $algorithm = self::$algorithmMapping[$sid]; - if ($algorithm == '') { - $algorithm = 'sha1'; - } - - return $algorithm; - } - - /** - * Build combined key from low-order word and high-order word - * - * @param array $byteChars -> byte array representation of password - * @return int - */ - private function buildCombinedKey($byteChars) - { - // Compute the high-order word - // Initialize from the initial code array (see above), depending on the passwords length. - $highOrderWord = self::$initialCodeArray[sizeof($byteChars) - 1]; - - // For each character in the password: - // For every bit in the character, starting with the least significant and progressing to (but excluding) - // the most significant, if the bit is set, XOR the key’s high-order word with the corresponding word from - // the Encryption Matrix - for ($i = 0; $i < sizeof($byteChars); $i++) { - $tmp = self::$passwordMaxLength - sizeof($byteChars) + $i; - $matrixRow = self::$encryptionMatrix[$tmp]; - for ($intBit = 0; $intBit < 7; $intBit++) { - if (($byteChars[$i] & (0x0001 << $intBit)) != 0) { - $highOrderWord = ($highOrderWord ^ $matrixRow[$intBit]); - } - } - } - - // Compute low-order word - // Initialize with 0 - $lowOrderWord = 0; - // For each character in the password, going backwards - for ($i = sizeof($byteChars) - 1; $i >= 0; $i--) { - // low-order word = (((low-order word SHR 14) AND 0x0001) OR (low-order word SHL 1) AND 0x7FFF)) XOR character - $lowOrderWord = (((($lowOrderWord >> 14) & 0x0001) | (($lowOrderWord << 1) & 0x7FFF)) ^ $byteChars[$i]); - } - // Lastly, low-order word = (((low-order word SHR 14) AND 0x0001) OR (low-order word SHL 1) AND 0x7FFF)) XOR strPassword length XOR 0xCE4B. - $lowOrderWord = (((($lowOrderWord >> 14) & 0x0001) | (($lowOrderWord << 1) & 0x7FFF)) ^ sizeof($byteChars) ^ 0xCE4B); - - // Combine the Low and High Order Word - return $this->int32(($highOrderWord << 16) + $lowOrderWord); - } - - /** - * Simulate behaviour of (signed) int32 - * - * @param int $value - * @return int - */ - private function int32($value) - { - $value = ($value & 0xFFFFFFFF); - - if ($value & 0x80000000) { - $value = -((~$value & 0xFFFFFFFF) + 1); - } - - return $value; - } } diff --git a/tests/PhpWord/Metadata/SettingsTest.php b/tests/PhpWord/Metadata/SettingsTest.php index bee8d0ca..a2a80b12 100644 --- a/tests/PhpWord/Metadata/SettingsTest.php +++ b/tests/PhpWord/Metadata/SettingsTest.php @@ -63,13 +63,22 @@ class SettingsTest extends \PHPUnit\Framework\TestCase public function testDocumentProtection() { $oSettings = new Settings(); - $oSettings->setDocumentProtection(new Protection()); + $oSettings->setDocumentProtection(new Protection('trackedChanges')); $this->assertNotNull($oSettings->getDocumentProtection()); - $oSettings->getDocumentProtection()->setEditing('trackedChanges'); $this->assertEquals('trackedChanges', $oSettings->getDocumentProtection()->getEditing()); } + /** + * Test setting an invalid salt + * @expectedException \InvalidArgumentException + */ + public function testInvalidSalt() + { + $p = new Protection(); + $p->setSalt('123'); + } + /** * TrackRevistions */ diff --git a/tests/PhpWord/Writer/Word2007/Part/SettingsTest.php b/tests/PhpWord/Writer/Word2007/Part/SettingsTest.php index 8c47cb52..7d4ef491 100644 --- a/tests/PhpWord/Writer/Word2007/Part/SettingsTest.php +++ b/tests/PhpWord/Writer/Word2007/Part/SettingsTest.php @@ -58,15 +58,15 @@ class SettingsTest extends \PHPUnit\Framework\TestCase /** * Test document protection with password - * - * Note: to get comparison values, a docx was generated in Word2010 and the values taken from the settings.xml */ public function testDocumentProtectionWithPassword() { $phpWord = new PhpWord(); - $phpWord->getProtection()->setEditing('readOnly'); - $phpWord->getProtection()->setPassword('testÄö@€!$&'); - $phpWord->getProtection()->setSalt(base64_decode("uq81pJRRGFIY5U+E9gt8tA==")); + $phpWord->getSettings()->getDocumentProtection()->setEditing('readOnly'); + $phpWord->getSettings()->getDocumentProtection()->setPassword('testÄö@€!$&'); + $phpWord->getSettings()->getDocumentProtection()->setSalt(base64_decode('uq81pJRRGFIY5U+E9gt8tA==')); + $phpWord->getSettings()->getDocumentProtection()->setMswordAlgorithmSid(1); + $phpWord->getSettings()->getDocumentProtection()->setSpinCount(10); $doc = TestHelperDOCX::getDocument($phpWord); @@ -74,7 +74,28 @@ class SettingsTest extends \PHPUnit\Framework\TestCase $path = '/w:settings/w:documentProtection'; $this->assertTrue($doc->elementExists($path, $file)); - $this->assertEquals($doc->getElement($path, $file)->getAttribute('w:hash'), "RA9jfY/u3DX114PMcl+uSekxsYk="); + $this->assertEquals('rUuJbk6LuN2/qFyp7IUPQA==', $doc->getElement($path, $file)->getAttribute('w:hash')); + $this->assertEquals('1', $doc->getElement($path, $file)->getAttribute('w:cryptAlgorithmSid')); + $this->assertEquals('10', $doc->getElement($path, $file)->getAttribute('w:cryptSpinCount')); + } + + /** + * Test document protection with password only + */ + public function testDocumentProtectionWithPasswordOnly() + { + $phpWord = new PhpWord(); + $phpWord->getSettings()->getDocumentProtection()->setEditing('readOnly'); + $phpWord->getSettings()->getDocumentProtection()->setPassword('testÄö@€!$&'); + + $doc = TestHelperDOCX::getDocument($phpWord); + + $file = 'word/settings.xml'; + + $path = '/w:settings/w:documentProtection'; + $this->assertTrue($doc->elementExists($path, $file)); + $this->assertEquals('4', $doc->getElement($path, $file)->getAttribute('w:cryptAlgorithmSid')); + $this->assertEquals('100000', $doc->getElement($path, $file)->getAttribute('w:cryptSpinCount')); } /** From 2e562512f4d969edfd500252b9fe7107c1d45f74 Mon Sep 17 00:00:00 2001 From: troosan Date: Fri, 24 Nov 2017 14:45:05 +0100 Subject: [PATCH 08/14] Add unit tests for PasswordEncoder --- .../Shared/Microsoft/PasswordEncoder.php | 3 + tests/PhpWord/Metadata/SettingsTest.php | 4 +- .../Shared/Microsoft/PasswordEncoderTest.php | 91 +++++++++++++++++++ .../Writer/Word2007/Part/SettingsTest.php | 19 ---- 4 files changed, 96 insertions(+), 21 deletions(-) create mode 100644 tests/PhpWord/Shared/Microsoft/PasswordEncoderTest.php diff --git a/src/PhpWord/Shared/Microsoft/PasswordEncoder.php b/src/PhpWord/Shared/Microsoft/PasswordEncoder.php index 40a3ea12..cddcfcd3 100644 --- a/src/PhpWord/Shared/Microsoft/PasswordEncoder.php +++ b/src/PhpWord/Shared/Microsoft/PasswordEncoder.php @@ -98,8 +98,10 @@ class PasswordEncoder // For each character, if the low byte is not equal to 0, take it. Otherwise, take the high byte. $passUtf8 = mb_convert_encoding($password, 'UCS-2LE', 'UTF-8'); $byteChars = array(); + for ($i = 0; $i < mb_strlen($password); $i++) { $byteChars[$i] = ord(substr($passUtf8, $i * 2, 1)); + if ($byteChars[$i] == 0) { $byteChars[$i] = ord(substr($passUtf8, $i * 2 + 1, 1)); } @@ -189,6 +191,7 @@ class PasswordEncoder /** * Simulate behaviour of (signed) int32 * + * @codeCoverageIgnore * @param int $value * @return int */ diff --git a/tests/PhpWord/Metadata/SettingsTest.php b/tests/PhpWord/Metadata/SettingsTest.php index a2a80b12..9830fd28 100644 --- a/tests/PhpWord/Metadata/SettingsTest.php +++ b/tests/PhpWord/Metadata/SettingsTest.php @@ -75,8 +75,8 @@ class SettingsTest extends \PHPUnit\Framework\TestCase */ public function testInvalidSalt() { - $p = new Protection(); - $p->setSalt('123'); + $protection = new Protection(); + $protection->setSalt('123'); } /** diff --git a/tests/PhpWord/Shared/Microsoft/PasswordEncoderTest.php b/tests/PhpWord/Shared/Microsoft/PasswordEncoderTest.php new file mode 100644 index 00000000..7b2bd3e7 --- /dev/null +++ b/tests/PhpWord/Shared/Microsoft/PasswordEncoderTest.php @@ -0,0 +1,91 @@ +assertEquals('10', $doc->getElement($path, $file)->getAttribute('w:cryptSpinCount')); } - /** - * Test document protection with password only - */ - public function testDocumentProtectionWithPasswordOnly() - { - $phpWord = new PhpWord(); - $phpWord->getSettings()->getDocumentProtection()->setEditing('readOnly'); - $phpWord->getSettings()->getDocumentProtection()->setPassword('testÄö@€!$&'); - - $doc = TestHelperDOCX::getDocument($phpWord); - - $file = 'word/settings.xml'; - - $path = '/w:settings/w:documentProtection'; - $this->assertTrue($doc->elementExists($path, $file)); - $this->assertEquals('4', $doc->getElement($path, $file)->getAttribute('w:cryptAlgorithmSid')); - $this->assertEquals('100000', $doc->getElement($path, $file)->getAttribute('w:cryptSpinCount')); - } - /** * Test compatibility */ From 9e029415cc3eafad7581346b8ed84e05d7674b08 Mon Sep 17 00:00:00 2001 From: troosan Date: Wed, 13 Dec 2017 23:17:01 +0100 Subject: [PATCH 09/14] align with pull request submitted in PHPOffice/Commom --- src/PhpWord/Metadata/Protection.php | 23 ++++--- .../Shared/Microsoft/PasswordEncoder.php | 68 ++++++++++++------- src/PhpWord/Writer/Word2007/Part/Settings.php | 2 +- .../Shared/Microsoft/PasswordEncoderTest.php | 6 +- 4 files changed, 58 insertions(+), 41 deletions(-) diff --git a/src/PhpWord/Metadata/Protection.php b/src/PhpWord/Metadata/Protection.php index 09d08aac..bb1cc1ad 100644 --- a/src/PhpWord/Metadata/Protection.php +++ b/src/PhpWord/Metadata/Protection.php @@ -18,6 +18,7 @@ namespace PhpOffice\PhpWord\Metadata; use PhpOffice\PhpWord\SimpleType\DocProtect; +use PhpOffice\PhpWord\Shared\Microsoft\PasswordEncoder; /** * Document protection class @@ -50,11 +51,11 @@ class Protection private $spinCount = 100000; /** - * Cryptographic Hashing Algorithm (see to \PhpOffice\PhpWord\Writer\Word2007\Part\Settings::$algorithmMapping) + * Cryptographic Hashing Algorithm (see constants defined in \PhpOffice\PhpWord\Shared\Microsoft\PasswordEncoder) * - * @var int + * @var string */ - private $mswordAlgorithmSid = 4; + private $algorithm = PasswordEncoder::ALGORITHM_SHA_1; /** * Salt for Password Verifier @@ -146,24 +147,24 @@ class Protection } /** - * Get algorithm-sid + * Get algorithm * - * @return int + * @return string */ - public function getMswordAlgorithmSid() + public function getAlgorithm() { - return $this->mswordAlgorithmSid; + return $this->algorithm; } /** - * Set algorithm-sid (see \PhpOffice\PhpWord\Writer\Word2007\Part\Settings::$algorithmMapping) + * Set algorithm * - * @param $mswordAlgorithmSid + * @param $algorithm * @return self */ - public function setMswordAlgorithmSid($mswordAlgorithmSid) + public function setMswordAlgorithmSid($algorithm) { - $this->mswordAlgorithmSid = $mswordAlgorithmSid; + $this->algorithm = $algorithm; return $this; } diff --git a/src/PhpWord/Shared/Microsoft/PasswordEncoder.php b/src/PhpWord/Shared/Microsoft/PasswordEncoder.php index cddcfcd3..a3ba345c 100644 --- a/src/PhpWord/Shared/Microsoft/PasswordEncoder.php +++ b/src/PhpWord/Shared/Microsoft/PasswordEncoder.php @@ -22,21 +22,36 @@ namespace PhpOffice\PhpWord\Shared\Microsoft; */ class PasswordEncoder { + const ALGORITHM_MD2 = 'MD2'; + const ALGORITHM_MD4 = 'MD4'; + const ALGORITHM_MD5 = 'MD5'; + const ALGORITHM_SHA_1 = 'SHA-1'; + const ALGORITHM_SHA_256 = 'SHA-256'; + const ALGORITHM_SHA_384 = 'SHA-384'; + const ALGORITHM_SHA_512 = 'SHA-512'; + const ALGORITHM_RIPEMD = 'RIPEMD'; + const ALGORITHM_RIPEMD_160 = 'RIPEMD-160'; + const ALGORITHM_MAC = 'MAC'; + const ALGORITHM_HMAC= 'HMAC'; + + /** + * Mapping between algorithm name and algorithm ID + * + * @var array + * @see https://msdn.microsoft.com/en-us/library/documentformat.openxml.wordprocessing.writeprotection.cryptographicalgorithmsid(v=office.14).aspx + */ private static $algorithmMapping = array( - 1 => 'md2', - 2 => 'md4', - 3 => 'md5', - 4 => 'sha1', - 5 => '', // 'mac' -> not possible with hash() - 6 => 'ripemd', - 7 => 'ripemd160', - 8 => '', - 9 => '', //'hmac' -> not possible with hash() - 10 => '', - 11 => '', - 12 => 'sha256', - 13 => 'sha384', - 14 => 'sha512', + self::ALGORITHM_MD2 => array(1, 'md2'), + self::ALGORITHM_MD4 => array(2, 'md4'), + self::ALGORITHM_MD5 => array(3, 'md5'), + self::ALGORITHM_SHA_1 => array(4, 'sha1'), + self::ALGORITHM_MAC => array(5, ''), // 'mac' -> not possible with hash() + self::ALGORITHM_RIPEMD => array(6, 'ripemd'), + self::ALGORITHM_RIPEMD_160 => array(7, 'ripemd160'), + self::ALGORITHM_HMAC => array(9, ''), //'hmac' -> not possible with hash() + self::ALGORITHM_SHA_256 => array(12, 'sha256'), + self::ALGORITHM_SHA_384 => array(13, 'sha384'), + self::ALGORITHM_SHA_512 => array(14, 'sha512'), ); private static $initialCodeArray = array( @@ -82,12 +97,12 @@ class PasswordEncoder * @see https://blogs.msdn.microsoft.com/vsod/2010/04/05/how-to-set-the-editing-restrictions-in-word-using-open-xml-sdk-2-0/ * * @param string $password - * @param number $algorithmSid + * @param string $algorithmName * @param string $salt - * @param number $spinCount + * @param integer $spinCount * @return string */ - public static function hashPassword($password, $algorithmSid = 4, $salt = null, $spinCount = 10000) + public static function hashPassword($password, $algorithmName = PasswordEncoder::ALGORITHM_SHA_1, $salt = null, $spinCount = 10000) { $origEncoding = mb_internal_encoding(); mb_internal_encoding('UTF-8'); @@ -118,7 +133,7 @@ class PasswordEncoder // Implementation Notes List: // Word requires that the initial hash of the password with the salt not be considered in the count. // The initial hash of salt + key is not included in the iteration count. - $algorithm = self::getAlgorithm($algorithmSid); + $algorithm = self::getAlgorithm($algorithmName); $generatedKey = hash($algorithm, $salt . $generatedKey, true); for ($i = 0; $i < $spinCount; $i++) { @@ -134,12 +149,12 @@ class PasswordEncoder /** * Get algorithm from self::$algorithmMapping * - * @param int $sid + * @param string $algorithmName * @return string */ - private static function getAlgorithm($sid) + private static function getAlgorithm($algorithmName) { - $algorithm = self::$algorithmMapping[$sid]; + $algorithm = self::$algorithmMapping[$algorithmName][1]; if ($algorithm == '') { $algorithm = 'sha1'; } @@ -155,16 +170,17 @@ class PasswordEncoder */ private static function buildCombinedKey($byteChars) { + $byteCharsLength = count($byteChars); // Compute the high-order word // Initialize from the initial code array (see above), depending on the passwords length. - $highOrderWord = self::$initialCodeArray[count($byteChars) - 1]; + $highOrderWord = self::$initialCodeArray[$byteCharsLength - 1]; // For each character in the password: // For every bit in the character, starting with the least significant and progressing to (but excluding) // the most significant, if the bit is set, XOR the key’s high-order word with the corresponding word from // the Encryption Matrix - for ($i = 0; $i < count($byteChars); $i++) { - $tmp = self::$passwordMaxLength - count($byteChars) + $i; + for ($i = 0; $i < $byteCharsLength; $i++) { + $tmp = self::$passwordMaxLength - $byteCharsLength + $i; $matrixRow = self::$encryptionMatrix[$tmp]; for ($intBit = 0; $intBit < 7; $intBit++) { if (($byteChars[$i] & (0x0001 << $intBit)) != 0) { @@ -177,12 +193,12 @@ class PasswordEncoder // Initialize with 0 $lowOrderWord = 0; // For each character in the password, going backwards - for ($i = count($byteChars) - 1; $i >= 0; $i--) { + for ($i = $byteCharsLength - 1; $i >= 0; $i--) { // low-order word = (((low-order word SHR 14) AND 0x0001) OR (low-order word SHL 1) AND 0x7FFF)) XOR character $lowOrderWord = (((($lowOrderWord >> 14) & 0x0001) | (($lowOrderWord << 1) & 0x7FFF)) ^ $byteChars[$i]); } // Lastly, low-order word = (((low-order word SHR 14) AND 0x0001) OR (low-order word SHL 1) AND 0x7FFF)) XOR strPassword length XOR 0xCE4B. - $lowOrderWord = (((($lowOrderWord >> 14) & 0x0001) | (($lowOrderWord << 1) & 0x7FFF)) ^ count($byteChars) ^ 0xCE4B); + $lowOrderWord = (((($lowOrderWord >> 14) & 0x0001) | (($lowOrderWord << 1) & 0x7FFF)) ^ $byteCharsLength ^ 0xCE4B); // Combine the Low and High Order Word return self::int32(($highOrderWord << 16) + $lowOrderWord); diff --git a/src/PhpWord/Writer/Word2007/Part/Settings.php b/src/PhpWord/Writer/Word2007/Part/Settings.php index 565aab2c..f292583e 100644 --- a/src/PhpWord/Writer/Word2007/Part/Settings.php +++ b/src/PhpWord/Writer/Word2007/Part/Settings.php @@ -193,7 +193,7 @@ class Settings extends AbstractPart if ($documentProtection->getSalt() == null) { $documentProtection->setSalt(openssl_random_pseudo_bytes(16)); } - $passwordHash = PasswordEncoder::hashPassword($documentProtection->getPassword(), $documentProtection->getMswordAlgorithmSid(), $documentProtection->getSalt(), $documentProtection->getSpinCount()); + $passwordHash = PasswordEncoder::hashPassword($documentProtection->getPassword(), $documentProtection->getAlgorithm(), $documentProtection->getSalt(), $documentProtection->getSpinCount()); $this->settings['w:documentProtection'] = array( '@attributes' => array( 'w:enforcement' => 1, diff --git a/tests/PhpWord/Shared/Microsoft/PasswordEncoderTest.php b/tests/PhpWord/Shared/Microsoft/PasswordEncoderTest.php index 7b2bd3e7..c42a6eb4 100644 --- a/tests/PhpWord/Shared/Microsoft/PasswordEncoderTest.php +++ b/tests/PhpWord/Shared/Microsoft/PasswordEncoderTest.php @@ -51,7 +51,7 @@ class PasswordEncoderTest extends \PHPUnit\Framework\TestCase $salt = base64_decode('uq81pJRRGFIY5U+E9gt8tA=='); //when - $hashPassword = PasswordEncoder::hashPassword($password, 4, $salt); + $hashPassword = PasswordEncoder::hashPassword($password, PasswordEncoder::ALGORITHM_SHA_1, $salt); //then TestCase::assertEquals('QiDOcpia1YzSVJPiKPwWebl9p/0=', $hashPassword); @@ -67,7 +67,7 @@ class PasswordEncoderTest extends \PHPUnit\Framework\TestCase $salt = base64_decode('uq81pJRRGFIY5U+E9gt8tA=='); //when - $hashPassword = PasswordEncoder::hashPassword($password, 5, $salt); + $hashPassword = PasswordEncoder::hashPassword($password, PasswordEncoder::ALGORITHM_MAC, $salt); //then TestCase::assertEquals('QiDOcpia1YzSVJPiKPwWebl9p/0=', $hashPassword); @@ -83,7 +83,7 @@ class PasswordEncoderTest extends \PHPUnit\Framework\TestCase $salt = base64_decode('uq81pJRRGFIY5U+E9gt8tA=='); //when - $hashPassword = PasswordEncoder::hashPassword($password, 5, $salt, 1); + $hashPassword = PasswordEncoder::hashPassword($password, PasswordEncoder::ALGORITHM_MAC, $salt, 1); //then TestCase::assertEquals('rDV9sgdDsztoCQlvRCb1lF2wxNg=', $hashPassword); From f7d2ad7201bc91f79253a6e0fdbcdaf0154679d5 Mon Sep 17 00:00:00 2001 From: troosan Date: Wed, 13 Dec 2017 23:24:37 +0100 Subject: [PATCH 10/14] formatting --- src/PhpWord/Metadata/Protection.php | 2 +- .../Shared/Microsoft/PasswordEncoder.php | 26 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/PhpWord/Metadata/Protection.php b/src/PhpWord/Metadata/Protection.php index bb1cc1ad..634751fb 100644 --- a/src/PhpWord/Metadata/Protection.php +++ b/src/PhpWord/Metadata/Protection.php @@ -17,8 +17,8 @@ namespace PhpOffice\PhpWord\Metadata; -use PhpOffice\PhpWord\SimpleType\DocProtect; use PhpOffice\PhpWord\Shared\Microsoft\PasswordEncoder; +use PhpOffice\PhpWord\SimpleType\DocProtect; /** * Document protection class diff --git a/src/PhpWord/Shared/Microsoft/PasswordEncoder.php b/src/PhpWord/Shared/Microsoft/PasswordEncoder.php index a3ba345c..a6a607a1 100644 --- a/src/PhpWord/Shared/Microsoft/PasswordEncoder.php +++ b/src/PhpWord/Shared/Microsoft/PasswordEncoder.php @@ -32,7 +32,7 @@ class PasswordEncoder const ALGORITHM_RIPEMD = 'RIPEMD'; const ALGORITHM_RIPEMD_160 = 'RIPEMD-160'; const ALGORITHM_MAC = 'MAC'; - const ALGORITHM_HMAC= 'HMAC'; + const ALGORITHM_HMAC = 'HMAC'; /** * Mapping between algorithm name and algorithm ID @@ -41,17 +41,17 @@ class PasswordEncoder * @see https://msdn.microsoft.com/en-us/library/documentformat.openxml.wordprocessing.writeprotection.cryptographicalgorithmsid(v=office.14).aspx */ private static $algorithmMapping = array( - self::ALGORITHM_MD2 => array(1, 'md2'), - self::ALGORITHM_MD4 => array(2, 'md4'), - self::ALGORITHM_MD5 => array(3, 'md5'), - self::ALGORITHM_SHA_1 => array(4, 'sha1'), - self::ALGORITHM_MAC => array(5, ''), // 'mac' -> not possible with hash() - self::ALGORITHM_RIPEMD => array(6, 'ripemd'), + self::ALGORITHM_MD2 => array(1, 'md2'), + self::ALGORITHM_MD4 => array(2, 'md4'), + self::ALGORITHM_MD5 => array(3, 'md5'), + self::ALGORITHM_SHA_1 => array(4, 'sha1'), + self::ALGORITHM_MAC => array(5, ''), // 'mac' -> not possible with hash() + self::ALGORITHM_RIPEMD => array(6, 'ripemd'), self::ALGORITHM_RIPEMD_160 => array(7, 'ripemd160'), - self::ALGORITHM_HMAC => array(9, ''), //'hmac' -> not possible with hash() - self::ALGORITHM_SHA_256 => array(12, 'sha256'), - self::ALGORITHM_SHA_384 => array(13, 'sha384'), - self::ALGORITHM_SHA_512 => array(14, 'sha512'), + self::ALGORITHM_HMAC => array(9, ''), //'hmac' -> not possible with hash() + self::ALGORITHM_SHA_256 => array(12, 'sha256'), + self::ALGORITHM_SHA_384 => array(13, 'sha384'), + self::ALGORITHM_SHA_512 => array(14, 'sha512'), ); private static $initialCodeArray = array( @@ -99,10 +99,10 @@ class PasswordEncoder * @param string $password * @param string $algorithmName * @param string $salt - * @param integer $spinCount + * @param int $spinCount * @return string */ - public static function hashPassword($password, $algorithmName = PasswordEncoder::ALGORITHM_SHA_1, $salt = null, $spinCount = 10000) + public static function hashPassword($password, $algorithmName = self::ALGORITHM_SHA_1, $salt = null, $spinCount = 10000) { $origEncoding = mb_internal_encoding(); mb_internal_encoding('UTF-8'); From 5a57409df028bb609f9f180424c0a0f489334b6f Mon Sep 17 00:00:00 2001 From: troosan Date: Wed, 13 Dec 2017 23:55:48 +0100 Subject: [PATCH 11/14] fix tests --- src/PhpWord/Metadata/Protection.php | 2 +- src/PhpWord/Shared/Microsoft/PasswordEncoder.php | 11 +++++++++++ src/PhpWord/Writer/Word2007/Part/Settings.php | 4 ++-- tests/PhpWord/Writer/Word2007/Part/SettingsTest.php | 3 ++- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/PhpWord/Metadata/Protection.php b/src/PhpWord/Metadata/Protection.php index 634751fb..35391cb2 100644 --- a/src/PhpWord/Metadata/Protection.php +++ b/src/PhpWord/Metadata/Protection.php @@ -162,7 +162,7 @@ class Protection * @param $algorithm * @return self */ - public function setMswordAlgorithmSid($algorithm) + public function setAlgorithm($algorithm) { $this->algorithm = $algorithm; diff --git a/src/PhpWord/Shared/Microsoft/PasswordEncoder.php b/src/PhpWord/Shared/Microsoft/PasswordEncoder.php index a6a607a1..d3a03d97 100644 --- a/src/PhpWord/Shared/Microsoft/PasswordEncoder.php +++ b/src/PhpWord/Shared/Microsoft/PasswordEncoder.php @@ -162,6 +162,17 @@ class PasswordEncoder return $algorithm; } + /** + * Returns the algorithm ID + * + * @param sting $algorithmName + * @return int + */ + public static function getAlgorithmId($algorithmName) + { + return self::$algorithmMapping[$algorithmName][0]; + } + /** * Build combined key from low-order word and high-order word * diff --git a/src/PhpWord/Writer/Word2007/Part/Settings.php b/src/PhpWord/Writer/Word2007/Part/Settings.php index 6ac5ec4a..e56e2612 100644 --- a/src/PhpWord/Writer/Word2007/Part/Settings.php +++ b/src/PhpWord/Writer/Word2007/Part/Settings.php @@ -183,7 +183,7 @@ class Settings extends AbstractPart private function setDocumentProtection($documentProtection) { if ($documentProtection->getEditing() !== null) { - if (empty($documentProtection->getPassword())) { + if ($documentProtection->getPassword() == null) { $this->settings['w:documentProtection'] = array( '@attributes' => array( 'w:enforcement' => 1, @@ -202,7 +202,7 @@ class Settings extends AbstractPart 'w:cryptProviderType' => 'rsaFull', 'w:cryptAlgorithmClass' => 'hash', 'w:cryptAlgorithmType' => 'typeAny', - 'w:cryptAlgorithmSid' => $documentProtection->getMswordAlgorithmSid(), + 'w:cryptAlgorithmSid' => PasswordEncoder::getAlgorithmId($documentProtection->getAlgorithm()), 'w:cryptSpinCount' => $documentProtection->getSpinCount(), 'w:hash' => $passwordHash, 'w:salt' => base64_encode($documentProtection->getSalt()), diff --git a/tests/PhpWord/Writer/Word2007/Part/SettingsTest.php b/tests/PhpWord/Writer/Word2007/Part/SettingsTest.php index 7a355042..1e6af567 100644 --- a/tests/PhpWord/Writer/Word2007/Part/SettingsTest.php +++ b/tests/PhpWord/Writer/Word2007/Part/SettingsTest.php @@ -24,6 +24,7 @@ use PhpOffice\PhpWord\Settings; use PhpOffice\PhpWord\SimpleType\Zoom; use PhpOffice\PhpWord\Style\Language; use PhpOffice\PhpWord\TestHelperDOCX; +use PhpOffice\PhpWord\Shared\Microsoft\PasswordEncoder; /** * Test class for PhpOffice\PhpWord\Writer\Word2007\Part\Settings @@ -65,7 +66,7 @@ class SettingsTest extends \PHPUnit\Framework\TestCase $phpWord->getSettings()->getDocumentProtection()->setEditing('readOnly'); $phpWord->getSettings()->getDocumentProtection()->setPassword('testÄö@€!$&'); $phpWord->getSettings()->getDocumentProtection()->setSalt(base64_decode('uq81pJRRGFIY5U+E9gt8tA==')); - $phpWord->getSettings()->getDocumentProtection()->setMswordAlgorithmSid(1); + $phpWord->getSettings()->getDocumentProtection()->setAlgorithm(PasswordEncoder::ALGORITHM_MD2); $phpWord->getSettings()->getDocumentProtection()->setSpinCount(10); $doc = TestHelperDOCX::getDocument($phpWord); From 5d5362a3fda20d3e79c089510a19e696d836cf63 Mon Sep 17 00:00:00 2001 From: troosan Date: Thu, 14 Dec 2017 00:15:23 +0100 Subject: [PATCH 12/14] sort imports --- tests/PhpWord/Writer/Word2007/Part/SettingsTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PhpWord/Writer/Word2007/Part/SettingsTest.php b/tests/PhpWord/Writer/Word2007/Part/SettingsTest.php index 1e6af567..50b444b8 100644 --- a/tests/PhpWord/Writer/Word2007/Part/SettingsTest.php +++ b/tests/PhpWord/Writer/Word2007/Part/SettingsTest.php @@ -21,10 +21,10 @@ use PhpOffice\PhpWord\ComplexType\ProofState; use PhpOffice\PhpWord\ComplexType\TrackChangesView; use PhpOffice\PhpWord\PhpWord; use PhpOffice\PhpWord\Settings; +use PhpOffice\PhpWord\Shared\Microsoft\PasswordEncoder; use PhpOffice\PhpWord\SimpleType\Zoom; use PhpOffice\PhpWord\Style\Language; use PhpOffice\PhpWord\TestHelperDOCX; -use PhpOffice\PhpWord\Shared\Microsoft\PasswordEncoder; /** * Test class for PhpOffice\PhpWord\Writer\Word2007\Part\Settings From 46a037ebd0ace99f10052d19aa6511a85b471f69 Mon Sep 17 00:00:00 2001 From: troosan Date: Mon, 18 Dec 2017 17:02:34 +0100 Subject: [PATCH 13/14] add composer scripts --- composer.json | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 70b60b46..41730374 100644 --- a/composer.json +++ b/composer.json @@ -34,12 +34,22 @@ "name": "Antoine de Troostembergh" } ], + "scripts": { + "check": [ + "./vendor/bin/php-cs-fixer fix --ansi --dry-run --diff", + "./vendor/bin/phpcs --report-width=200 --report-summary --report-full samples/ src/ tests/ --ignore=src/PhpWord/Shared/PCLZip --standard=PSR2 -n", + "./vendor/bin/phpmd src/,tests/ text ./phpmd.xml.dist --exclude pclzip.lib.php", + "./vendor/bin/phpunit --color=always" + ], + "fix": [ + "./vendor/bin/php-cs-fixer fix --ansi" + ] + }, "require": { "php": ">=5.3.3", "ext-xml": "*", "zendframework/zend-escaper": "^2.2", - "zendframework/zend-stdlib": "^2.2 || ^3.0", - "phpoffice/common": "^0.2" + "zendframework/zend-stdlib": "^2.2 || ^3.0" }, "require-dev": { "phpunit/phpunit": "^4.8.36", From 7908491ba3e24f170da5aea61fbf9b85c6c21abb Mon Sep 17 00:00:00 2001 From: troosan Date: Tue, 19 Dec 2017 22:14:52 +0100 Subject: [PATCH 14/14] revert mistakenly deleted line --- composer.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 41730374..2774ad98 100644 --- a/composer.json +++ b/composer.json @@ -49,7 +49,8 @@ "php": ">=5.3.3", "ext-xml": "*", "zendframework/zend-escaper": "^2.2", - "zendframework/zend-stdlib": "^2.2 || ^3.0" + "zendframework/zend-stdlib": "^2.2 || ^3.0", + "phpoffice/common": "^0.2" }, "require-dev": { "phpunit/phpunit": "^4.8.36",