Skip to content

Commit ad83196

Browse files
committed
move password encoding in separate class
fix PHPCS errors add documentation add sample
1 parent 7b30145 commit ad83196

File tree

8 files changed

+360
-209
lines changed

8 files changed

+360
-209
lines changed

docs/general.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,3 +271,14 @@ points to twips.
271271
$sectionStyle->setMarginLeft(\PhpOffice\PhpWord\Shared\Converter::inchToTwip(.5));
272272
// 2 cm right margin
273273
$sectionStyle->setMarginRight(\PhpOffice\PhpWord\Shared\Converter::cmToTwip(2));
274+
275+
Document protection
276+
-------------------
277+
278+
The document (or parts of it) can be password protected.
279+
280+
.. code-block:: php
281+
282+
$documentProtection = $phpWord->getSettings()->getDocumentProtection();
283+
$documentProtection->setEditing(DocProtect::READ_ONLY);
284+
$documentProtection->setPassword('myPassword');

samples/Sample_38_Protection.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
use PhpOffice\PhpWord\SimpleType\DocProtect;
3+
4+
include_once 'Sample_Header.php';
5+
6+
// New Word Document
7+
echo date('H:i:s') , ' Create new PhpWord object' , EOL;
8+
$phpWord = new \PhpOffice\PhpWord\PhpWord();
9+
10+
$documentProtection = $phpWord->getSettings()->getDocumentProtection();
11+
$documentProtection->setEditing(DocProtect::READ_ONLY);
12+
$documentProtection->setPassword('myPassword');
13+
14+
$section = $phpWord->addSection();
15+
$section->addText('this document is password protected');
16+
17+
// Save file
18+
echo write($phpWord, basename(__FILE__, '.php'), $writers);
19+
if (!CLI) {
20+
include_once 'Sample_Footer.php';
21+
}

src/PhpWord/Metadata/Protection.php

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717

1818
namespace PhpOffice\PhpWord\Metadata;
1919

20+
use PhpOffice\PhpWord\SimpleType\DocProtect;
21+
2022
/**
2123
* Document protection class
2224
*
@@ -38,28 +40,28 @@ class Protection
3840
*
3941
* @var string
4042
*/
41-
private $password = '';
43+
private $password;
4244

4345
/**
44-
* Number of hashing iterations
46+
* Iterations to Run Hashing Algorithm
4547
*
4648
* @var int
4749
*/
4850
private $spinCount = 100000;
4951

5052
/**
51-
* Algorithm-SID (see to \PhpOffice\PhpWord\Writer\Word2007\Part\Settings::$algorithmMapping)
53+
* Cryptographic Hashing Algorithm (see to \PhpOffice\PhpWord\Writer\Word2007\Part\Settings::$algorithmMapping)
5254
*
5355
* @var int
5456
*/
5557
private $mswordAlgorithmSid = 4;
5658

5759
/**
58-
* salt
60+
* Salt for Password Verifier
5961
*
6062
* @var string
6163
*/
62-
private $salt = '';
64+
private $salt;
6365

6466
/**
6567
* Create a new instance
@@ -68,7 +70,9 @@ class Protection
6870
*/
6971
public function __construct($editing = null)
7072
{
71-
$this->setEditing($editing);
73+
if ($editing != null) {
74+
$this->setEditing($editing);
75+
}
7276
}
7377

7478
/**
@@ -84,11 +88,12 @@ public function getEditing()
8488
/**
8589
* Set editing protection
8690
*
87-
* @param string $editing
91+
* @param string $editing Any value of \PhpOffice\PhpWord\SimpleType\DocProtect
8892
* @return self
8993
*/
9094
public function setEditing($editing = null)
9195
{
96+
DocProtect::validate($editing);
9297
$this->editing = $editing;
9398

9499
return $this;
@@ -177,12 +182,12 @@ public function getSalt()
177182
* Set salt. Salt HAS to be 16 characters, or an exception will be thrown.
178183
*
179184
* @param $salt
180-
* @return self
181185
* @throws \InvalidArgumentException
186+
* @return self
182187
*/
183188
public function setSalt($salt)
184189
{
185-
if ($salt !== null && strlen($salt) !== 16){
190+
if ($salt !== null && strlen($salt) !== 16) {
186191
throw new \InvalidArgumentException('salt has to be of exactly 16 bytes length');
187192
}
188193

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
<?php
2+
/**
3+
* This file is part of PHPWord - A pure PHP library for reading and writing
4+
* word processing documents.
5+
*
6+
* PHPWord is free software distributed under the terms of the GNU Lesser
7+
* General Public License version 3 as published by the Free Software Foundation.
8+
*
9+
* For the full copyright and license information, please read the LICENSE
10+
* file that was distributed with this source code. For the full list of
11+
* contributors, visit https://github.com/PHPOffice/PHPWord/contributors.
12+
*
13+
* @see https://github.com/PHPOffice/PHPWord
14+
* @copyright 2010-2017 PHPWord contributors
15+
* @license http://www.gnu.org/licenses/lgpl.txt LGPL version 3
16+
*/
17+
18+
namespace PhpOffice\PhpWord\Shared\Microsoft;
19+
20+
/**
21+
* Password encoder for microsoft office applications
22+
*/
23+
class PasswordEncoder
24+
{
25+
private static $algorithmMapping = array(
26+
1 => 'md2',
27+
2 => 'md4',
28+
3 => 'md5',
29+
4 => 'sha1',
30+
5 => '', // 'mac' -> not possible with hash()
31+
6 => 'ripemd',
32+
7 => 'ripemd160',
33+
8 => '',
34+
9 => '', //'hmac' -> not possible with hash()
35+
10 => '',
36+
11 => '',
37+
12 => 'sha256',
38+
13 => 'sha384',
39+
14 => 'sha512',
40+
);
41+
42+
private static $initialCodeArray = array(
43+
0xE1F0,
44+
0x1D0F,
45+
0xCC9C,
46+
0x84C0,
47+
0x110C,
48+
0x0E10,
49+
0xF1CE,
50+
0x313E,
51+
0x1872,
52+
0xE139,
53+
0xD40F,
54+
0x84F9,
55+
0x280C,
56+
0xA96A,
57+
0x4EC3,
58+
);
59+
60+
private static $encryptionMatrix = array(
61+
array(0xAEFC, 0x4DD9, 0x9BB2, 0x2745, 0x4E8A, 0x9D14, 0x2A09),
62+
array(0x7B61, 0xF6C2, 0xFDA5, 0xEB6B, 0xC6F7, 0x9DCF, 0x2BBF),
63+
array(0x4563, 0x8AC6, 0x05AD, 0x0B5A, 0x16B4, 0x2D68, 0x5AD0),
64+
array(0x0375, 0x06EA, 0x0DD4, 0x1BA8, 0x3750, 0x6EA0, 0xDD40),
65+
array(0xD849, 0xA0B3, 0x5147, 0xA28E, 0x553D, 0xAA7A, 0x44D5),
66+
array(0x6F45, 0xDE8A, 0xAD35, 0x4A4B, 0x9496, 0x390D, 0x721A),
67+
array(0xEB23, 0xC667, 0x9CEF, 0x29FF, 0x53FE, 0xA7FC, 0x5FD9),
68+
array(0x47D3, 0x8FA6, 0x0F6D, 0x1EDA, 0x3DB4, 0x7B68, 0xF6D0),
69+
array(0xB861, 0x60E3, 0xC1C6, 0x93AD, 0x377B, 0x6EF6, 0xDDEC),
70+
array(0x45A0, 0x8B40, 0x06A1, 0x0D42, 0x1A84, 0x3508, 0x6A10),
71+
array(0xAA51, 0x4483, 0x8906, 0x022D, 0x045A, 0x08B4, 0x1168),
72+
array(0x76B4, 0xED68, 0xCAF1, 0x85C3, 0x1BA7, 0x374E, 0x6E9C),
73+
array(0x3730, 0x6E60, 0xDCC0, 0xA9A1, 0x4363, 0x86C6, 0x1DAD),
74+
array(0x3331, 0x6662, 0xCCC4, 0x89A9, 0x0373, 0x06E6, 0x0DCC),
75+
array(0x1021, 0x2042, 0x4084, 0x8108, 0x1231, 0x2462, 0x48C4),
76+
);
77+
78+
private static $passwordMaxLength = 15;
79+
80+
/**
81+
* Create a hashed password that MS Word will be able to work with
82+
* @see https://blogs.msdn.microsoft.com/vsod/2010/04/05/how-to-set-the-editing-restrictions-in-word-using-open-xml-sdk-2-0/
83+
*
84+
* @param string $password
85+
* @param number $algorithmSid
86+
* @param string $salt
87+
* @param number $spinCount
88+
* @return string
89+
*/
90+
public static function hashPassword($password, $algorithmSid = 4, $salt = null, $spinCount = 10000)
91+
{
92+
$origEncoding = mb_internal_encoding();
93+
mb_internal_encoding('UTF-8');
94+
95+
$password = mb_substr($password, 0, min(self::$passwordMaxLength, mb_strlen($password)));
96+
97+
// Get the single-byte values by iterating through the Unicode characters of the truncated password.
98+
// For each character, if the low byte is not equal to 0, take it. Otherwise, take the high byte.
99+
$passUtf8 = mb_convert_encoding($password, 'UCS-2LE', 'UTF-8');
100+
$byteChars = array();
101+
for ($i = 0; $i < mb_strlen($password); $i++) {
102+
$byteChars[$i] = ord(substr($passUtf8, $i * 2, 1));
103+
if ($byteChars[$i] == 0) {
104+
$byteChars[$i] = ord(substr($passUtf8, $i * 2 + 1, 1));
105+
}
106+
}
107+
108+
// build low-order word and hig-order word and combine them
109+
$combinedKey = self::buildCombinedKey($byteChars);
110+
// build reversed hexadecimal string
111+
$hex = str_pad(strtoupper(dechex($combinedKey & 0xFFFFFFFF)), 8, '0', \STR_PAD_LEFT);
112+
$reversedHex = $hex[6] . $hex[7] . $hex[4] . $hex[5] . $hex[2] . $hex[3] . $hex[0] . $hex[1];
113+
114+
$generatedKey = mb_convert_encoding($reversedHex, 'UCS-2LE', 'UTF-8');
115+
116+
// Implementation Notes List:
117+
// Word requires that the initial hash of the password with the salt not be considered in the count.
118+
// The initial hash of salt + key is not included in the iteration count.
119+
$algorithm = self::getAlgorithm($algorithmSid);
120+
$generatedKey = hash($algorithm, $salt . $generatedKey, true);
121+
122+
for ($i = 0; $i < $spinCount; $i++) {
123+
$generatedKey = hash($algorithm, $generatedKey . pack('CCCC', $i, $i >> 8, $i >> 16, $i >> 24), true);
124+
}
125+
$generatedKey = base64_encode($generatedKey);
126+
127+
mb_internal_encoding($origEncoding);
128+
129+
return $generatedKey;
130+
}
131+
132+
/**
133+
* Get algorithm from self::$algorithmMapping
134+
*
135+
* @param int $sid
136+
* @return string
137+
*/
138+
private static function getAlgorithm($sid)
139+
{
140+
$algorithm = self::$algorithmMapping[$sid];
141+
if ($algorithm == '') {
142+
$algorithm = 'sha1';
143+
}
144+
145+
return $algorithm;
146+
}
147+
148+
/**
149+
* Build combined key from low-order word and high-order word
150+
*
151+
* @param array $byteChars byte array representation of password
152+
* @return int
153+
*/
154+
private static function buildCombinedKey($byteChars)
155+
{
156+
// Compute the high-order word
157+
// Initialize from the initial code array (see above), depending on the passwords length.
158+
$highOrderWord = self::$initialCodeArray[count($byteChars) - 1];
159+
160+
// For each character in the password:
161+
// For every bit in the character, starting with the least significant and progressing to (but excluding)
162+
// the most significant, if the bit is set, XOR the key’s high-order word with the corresponding word from
163+
// the Encryption Matrix
164+
for ($i = 0; $i < count($byteChars); $i++) {
165+
$tmp = self::$passwordMaxLength - count($byteChars) + $i;
166+
$matrixRow = self::$encryptionMatrix[$tmp];
167+
for ($intBit = 0; $intBit < 7; $intBit++) {
168+
if (($byteChars[$i] & (0x0001 << $intBit)) != 0) {
169+
$highOrderWord = ($highOrderWord ^ $matrixRow[$intBit]);
170+
}
171+
}
172+
}
173+
174+
// Compute low-order word
175+
// Initialize with 0
176+
$lowOrderWord = 0;
177+
// For each character in the password, going backwards
178+
for ($i = count($byteChars) - 1; $i >= 0; $i--) {
179+
// low-order word = (((low-order word SHR 14) AND 0x0001) OR (low-order word SHL 1) AND 0x7FFF)) XOR character
180+
$lowOrderWord = (((($lowOrderWord >> 14) & 0x0001) | (($lowOrderWord << 1) & 0x7FFF)) ^ $byteChars[$i]);
181+
}
182+
// Lastly, low-order word = (((low-order word SHR 14) AND 0x0001) OR (low-order word SHL 1) AND 0x7FFF)) XOR strPassword length XOR 0xCE4B.
183+
$lowOrderWord = (((($lowOrderWord >> 14) & 0x0001) | (($lowOrderWord << 1) & 0x7FFF)) ^ count($byteChars) ^ 0xCE4B);
184+
185+
// Combine the Low and High Order Word
186+
return self::int32(($highOrderWord << 16) + $lowOrderWord);
187+
}
188+
189+
/**
190+
* Simulate behaviour of (signed) int32
191+
*
192+
* @param int $value
193+
* @return int
194+
*/
195+
private static function int32($value)
196+
{
197+
$value = ($value & 0xFFFFFFFF);
198+
199+
if ($value & 0x80000000) {
200+
$value = -((~$value & 0xFFFFFFFF) + 1);
201+
}
202+
203+
return $value;
204+
}
205+
}

src/PhpWord/SimpleType/DocProtect.php

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
/**
3+
* This file is part of PHPWord - A pure PHP library for reading and writing
4+
* word processing documents.
5+
*
6+
* PHPWord is free software distributed under the terms of the GNU Lesser
7+
* General Public License version 3 as published by the Free Software Foundation.
8+
*
9+
* For the full copyright and license information, please read the LICENSE
10+
* file that was distributed with this source code. For the full list of
11+
* contributors, visit https://github.com/PHPOffice/PHPWord/contributors.
12+
*
13+
* @see https://github.com/PHPOffice/PHPWord
14+
* @copyright 2010-2017 PHPWord contributors
15+
* @license http://www.gnu.org/licenses/lgpl.txt LGPL version 3
16+
*/
17+
18+
namespace PhpOffice\PhpWord\SimpleType;
19+
20+
use PhpOffice\PhpWord\Shared\AbstractEnum;
21+
22+
/**
23+
* Document Protection Types
24+
*
25+
* @since 0.14.0
26+
*
27+
* @see http://www.datypic.com/sc/ooxml/t-w_ST_DocProtect.html
28+
*/
29+
final class DocProtect extends AbstractEnum
30+
{
31+
/**
32+
* No Editing Restrictions
33+
*/
34+
const NONE = 'none';
35+
36+
/**
37+
* Allow No Editing
38+
*/
39+
const READ_ONLY = 'readOnly';
40+
41+
/**
42+
* Allow Editing of Comments
43+
*/
44+
const COMMENTS = 'comments';
45+
46+
/**
47+
* Allow Editing With Revision Tracking
48+
*/
49+
const TRACKED_CHANGES = 'trackedChanges';
50+
51+
/**
52+
* Allow Editing of Form Fields
53+
*/
54+
const FORMS = 'forms';
55+
}

0 commit comments

Comments
 (0)