Skip to content

Commit 05387fa

Browse files
committed
enable password setting in word
1 parent 6da9d8a commit 05387fa

File tree

2 files changed

+244
-8
lines changed

2 files changed

+244
-8
lines changed

src/PhpWord/Metadata/Protection.php

Lines changed: 222 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,77 @@
2222
*
2323
* @since 0.12.0
2424
* @link http://www.datypic.com/sc/ooxml/t-w_CT_DocProtect.html
25-
* @todo Password!
2625
*/
2726
class Protection
2827
{
28+
static $algorithmMapping = [
29+
1 => 'md2',
30+
2 => 'md4',
31+
3 => 'md5',
32+
4 => 'sha1',
33+
5 => '', // 'mac' -> not possible with hash()
34+
6 => 'ripemd',
35+
7 => 'ripemd160',
36+
8 => '',
37+
9 => '', //'hmac' -> not possible with hash()
38+
10 => '',
39+
11 => '',
40+
12 => 'sha256',
41+
13 => 'sha384',
42+
14 => 'sha512',
43+
];
44+
static $initialCodeArray = [
45+
0xE1F0,
46+
0x1D0F,
47+
0xCC9C,
48+
0x84C0,
49+
0x110C,
50+
0x0E10,
51+
0xF1CE,
52+
0x313E,
53+
0x1872,
54+
0xE139,
55+
0xD40F,
56+
0x84F9,
57+
0x280C,
58+
0xA96A,
59+
0x4EC3
60+
];
61+
static $encryptionMatrix =
62+
[
63+
[0xAEFC, 0x4DD9, 0x9BB2, 0x2745, 0x4E8A, 0x9D14, 0x2A09],
64+
[0x7B61, 0xF6C2, 0xFDA5, 0xEB6B, 0xC6F7, 0x9DCF, 0x2BBF],
65+
[0x4563, 0x8AC6, 0x05AD, 0x0B5A, 0x16B4, 0x2D68, 0x5AD0],
66+
[0x0375, 0x06EA, 0x0DD4, 0x1BA8, 0x3750, 0x6EA0, 0xDD40],
67+
[0xD849, 0xA0B3, 0x5147, 0xA28E, 0x553D, 0xAA7A, 0x44D5],
68+
[0x6F45, 0xDE8A, 0xAD35, 0x4A4B, 0x9496, 0x390D, 0x721A],
69+
[0xEB23, 0xC667, 0x9CEF, 0x29FF, 0x53FE, 0xA7FC, 0x5FD9],
70+
[0x47D3, 0x8FA6, 0x0F6D, 0x1EDA, 0x3DB4, 0x7B68, 0xF6D0],
71+
[0xB861, 0x60E3, 0xC1C6, 0x93AD, 0x377B, 0x6EF6, 0xDDEC],
72+
[0x45A0, 0x8B40, 0x06A1, 0x0D42, 0x1A84, 0x3508, 0x6A10],
73+
[0xAA51, 0x4483, 0x8906, 0x022D, 0x045A, 0x08B4, 0x1168],
74+
[0x76B4, 0xED68, 0xCAF1, 0x85C3, 0x1BA7, 0x374E, 0x6E9C],
75+
[0x3730, 0x6E60, 0xDCC0, 0xA9A1, 0x4363, 0x86C6, 0x1DAD],
76+
[0x3331, 0x6662, 0xCCC4, 0x89A9, 0x0373, 0x06E6, 0x0DCC],
77+
[0x1021, 0x2042, 0x4084, 0x8108, 0x1231, 0x2462, 0x48C4]
78+
];
79+
2980
/**
30-
* Editing restriction readOnly|comments|trackedChanges|forms
81+
* Editing restriction none|readOnly|comments|trackedChanges|forms
3182
*
3283
* @var string
3384
* @link http://www.datypic.com/sc/ooxml/a-w_edit-1.html
3485
*/
3586
private $editing;
3687

88+
private $password;
89+
90+
private $spinCount = 100000;
91+
92+
private $algorithmSid = 4;
93+
94+
private $salt;
95+
3796
/**
3897
* Create a new instance
3998
*
@@ -66,4 +125,165 @@ public function setEditing($editing = null)
66125

67126
return $this;
68127
}
128+
129+
public function getPassword()
130+
{
131+
return $this->password;
132+
}
133+
134+
public function setPassword($password)
135+
{
136+
$this->password = $this->getPasswordHash($password);
137+
138+
return $this;
139+
}
140+
141+
public function getSpinCount()
142+
{
143+
return $this->spinCount;
144+
}
145+
146+
public function setSpinCount($spinCount)
147+
{
148+
$this->spinCount = $spinCount;
149+
150+
return $this;
151+
}
152+
153+
public function getAlgorithmSid()
154+
{
155+
return $this->algorithmSid;
156+
}
157+
158+
public function setAlgorithmSid($algorithmSid)
159+
{
160+
$this->algorithmSid = $algorithmSid;
161+
162+
return $this;
163+
}
164+
165+
public function setSalt($salt)
166+
{
167+
$this->salt = $salt;
168+
}
169+
170+
public function getSalt()
171+
{
172+
return $this->salt;
173+
}
174+
175+
private function getAlgorithm()
176+
{
177+
$algorithm = self::$algorithmMapping[$this->algorithmSid];
178+
if ($algorithm == '') {
179+
$algorithm = 'sha1';
180+
}
181+
182+
return $algorithm;
183+
}
184+
185+
private function getPasswordHash($password)
186+
{
187+
if (empty($password)) {
188+
return '';
189+
}
190+
$passwordMaxLength = 15;
191+
192+
// Truncate the password to $passwordMaxLength characters
193+
$password = mb_substr($password, 0, min($passwordMaxLength, mb_strlen($password)));
194+
195+
$byteChars = [];
196+
197+
echo "password: '{$password}'(".mb_strlen($password).")";
198+
199+
$pass_utf8 = mb_convert_encoding($password, 'UCS-2LE', 'UTF-8');
200+
for ($i = 0; $i < mb_strlen($password); $i++) {
201+
$byteChars[$i] = ord(substr($pass_utf8, $i*2, 1));
202+
if ($byteChars[$i] == 0) {
203+
echo "hi!$i";
204+
$byteChars[$i] = ord(substr($pass_utf8, $i*2+1, 1));
205+
}
206+
}
207+
208+
// Compute the high-order word
209+
$highOrderWord = self::$initialCodeArray[sizeof($byteChars) - 1];
210+
for ($i = 0; $i < sizeof($byteChars); $i++) {
211+
$tmp = $passwordMaxLength - sizeof($byteChars) + $i;
212+
$matrixRow = self::$encryptionMatrix[$tmp];
213+
214+
for ($intBit = 0; $intBit < 7; $intBit++) {
215+
if (($byteChars[$i] & (0x0001 << $intBit)) != 0) {
216+
$highOrderWord = ($highOrderWord ^ $matrixRow[$intBit]);
217+
}
218+
}
219+
}
220+
221+
// Compute low-order word
222+
$lowOrderWord = 0;
223+
for ($i = sizeof($byteChars) - 1; $i >= 0; $i--) {
224+
$lowOrderWord = (((($lowOrderWord >> 14) & 0x0001) | (($lowOrderWord << 1) & 0x7FFF)) ^ $byteChars[$i]);
225+
}
226+
$lowOrderWord = (((($lowOrderWord >> 14) & 0x0001) | (($lowOrderWord << 1) & 0x7FFF)) ^ sizeof($byteChars) ^ 0xCE4B);
227+
228+
$combinedKey = $this->int32(($highOrderWord << 16) + $lowOrderWord);
229+
$generatedKey = [
230+
0 => (($combinedKey & 0x000000FF) >> 0),
231+
1 => (($combinedKey & 0x0000FF00) >> 8),
232+
2 => (($combinedKey & 0x00FF0000) >> 16),
233+
3 => (($combinedKey & 0xFF000000) >> 24),
234+
];
235+
236+
$tmpStr = '';
237+
for ($i = 0; $i < 4; $i++) {
238+
$tmpStr .= strtoupper(dechex($generatedKey[$i]));
239+
}
240+
$generatedKey = [];
241+
$tmpStr = mb_convert_encoding($tmpStr, 'UCS-2LE', 'UTF-8');
242+
for ($i = 0; $i < strlen($tmpStr); $i++) {
243+
$generatedKey[] = ord(substr($tmpStr, $i, 1));
244+
}
245+
246+
$salt = unpack('C*', base64_decode($this->getSalt()));
247+
$algorithm = $this->getAlgorithm();
248+
249+
$tmpArray1 = $generatedKey;
250+
$tmpArray2 = $salt;
251+
$generatedKey = array_merge($tmpArray2, $tmpArray1);
252+
253+
$generatedKey = $this->hashByteArray($algorithm, $generatedKey);
254+
255+
for ($i = 0; $i < $this->getSpinCount(); $i++) {
256+
$iterator = [
257+
0 => (($i & 0x000000FF) >> 0),
258+
1 => (($i & 0x0000FF00) >> 8),
259+
2 => (($i & 0x00FF0000) >> 16),
260+
3 => (($i & 0xFF000000) >> 24),
261+
];
262+
$generatedKey = array_merge($generatedKey, $iterator);
263+
$generatedKey = $this->hashByteArray($algorithm, $generatedKey);
264+
}
265+
266+
$hash = implode(array_map("chr", $generatedKey));
267+
268+
return base64_encode($hash);
269+
}
270+
271+
private function int32($value)
272+
{
273+
$value = ($value & 0xFFFFFFFF);
274+
275+
if ($value & 0x80000000) {
276+
$value = -((~$value & 0xFFFFFFFF) + 1);
277+
}
278+
279+
return $value;
280+
}
281+
282+
private function hashByteArray($algorithm, $array)
283+
{
284+
$string = implode(array_map("chr", $array));
285+
$string = hash($algorithm, $string, true);
286+
287+
return unpack('C*', $string);
288+
}
69289
}

src/PhpWord/Writer/Word2007/Part/Settings.php

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -152,12 +152,28 @@ private function getProtection()
152152
{
153153
$protection = $this->getParentWriter()->getPhpWord()->getProtection();
154154
if ($protection->getEditing() !== null) {
155-
$this->settings['w:documentProtection'] = array(
156-
'@attributes' => array(
157-
'w:enforcement' => 1,
158-
'w:edit' => $protection->getEditing(),
159-
)
160-
);
155+
if (empty($protection->getPassword())) {
156+
$this->settings['w:documentProtection'] = array(
157+
'@attributes' => array(
158+
'w:enforcement' => 1,
159+
'w:edit' => $protection->getEditing(),
160+
)
161+
);
162+
} else {
163+
$this->settings['w:documentProtection'] = array(
164+
'@attributes' => array(
165+
'w:enforcement' => 1,
166+
'w:edit' => $protection->getEditing(),
167+
'w:cryptProviderType' => 'rsaFull',
168+
'w:cryptAlgorithmClass' => 'hash',
169+
'w:cryptAlgorithmType' => 'typeAny',
170+
'w:cryptAlgorithmSid' => $protection->getAlgorithmSid(),
171+
'w:cryptSpinCount' => $protection->getSpinCount(),
172+
'w:hash' => $protection->getPassword(),
173+
'w:salt' => $protection->getSalt(),
174+
)
175+
);
176+
}
161177
}
162178
}
163179

0 commit comments

Comments
 (0)