From ad83196a052b4fcb0b00a278a16dc0ef53ec6c74 Mon Sep 17 00:00:00 2001 From: troosan Date: Thu, 23 Nov 2017 22:49:21 +0100 Subject: [PATCH] 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')); } /**