Skip to content

Commit 483a167

Browse files
committed
refactoring of hash function
1 parent 05387fa commit 483a167

File tree

1 file changed

+141
-65
lines changed

1 file changed

+141
-65
lines changed

src/PhpWord/Metadata/Protection.php

Lines changed: 141 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ class Protection
7676
[0x3331, 0x6662, 0xCCC4, 0x89A9, 0x0373, 0x06E6, 0x0DCC],
7777
[0x1021, 0x2042, 0x4084, 0x8108, 0x1231, 0x2462, 0x48C4]
7878
];
79+
static $passwordMaxLength = 15;
7980

8081
/**
8182
* Editing restriction none|readOnly|comments|trackedChanges|forms
@@ -85,12 +86,32 @@ class Protection
8586
*/
8687
private $editing;
8788

89+
/**
90+
* Hashed password
91+
*
92+
* @var string
93+
*/
8894
private $password;
8995

96+
/**
97+
* Number of hashing iterations
98+
*
99+
* @var int
100+
*/
90101
private $spinCount = 100000;
91102

103+
/**
104+
* Algorithm-SID according to self::$algorithmMapping
105+
*
106+
* @var int
107+
*/
92108
private $algorithmSid = 4;
93109

110+
/**
111+
* Hashed salt
112+
*
113+
* @var string
114+
*/
94115
private $salt;
95116

96117
/**
@@ -126,52 +147,103 @@ public function setEditing($editing = null)
126147
return $this;
127148
}
128149

150+
/**
151+
* Get password hash
152+
*
153+
* @return string
154+
*/
129155
public function getPassword()
130156
{
131157
return $this->password;
132158
}
133159

160+
/**
161+
* Set password
162+
*
163+
* @param $password
164+
* @return self
165+
*/
134166
public function setPassword($password)
135167
{
136168
$this->password = $this->getPasswordHash($password);
137169

138170
return $this;
139171
}
140172

173+
/**
174+
* Get count for hash iterations
175+
*
176+
* @return int
177+
*/
141178
public function getSpinCount()
142179
{
143180
return $this->spinCount;
144181
}
145182

183+
/**
184+
* Set count for hash iterations
185+
*
186+
* @param $spinCount
187+
* @return self
188+
*/
146189
public function setSpinCount($spinCount)
147190
{
148191
$this->spinCount = $spinCount;
149192

150193
return $this;
151194
}
152195

196+
/**
197+
* Get algorithm-sid
198+
*
199+
* @return int
200+
*/
153201
public function getAlgorithmSid()
154202
{
155203
return $this->algorithmSid;
156204
}
157205

206+
/**
207+
* Set algorithm-sid (see self::$algorithmMapping)
208+
*
209+
* @param $algorithmSid
210+
* @return self
211+
*/
158212
public function setAlgorithmSid($algorithmSid)
159213
{
160214
$this->algorithmSid = $algorithmSid;
161215

162216
return $this;
163217
}
164218

165-
public function setSalt($salt)
219+
/**
220+
* Get salt hash
221+
*
222+
* @return string
223+
*/
224+
public function getSalt()
166225
{
167-
$this->salt = $salt;
226+
return $this->salt;
168227
}
169228

170-
public function getSalt()
229+
/**
230+
* Set salt hash
231+
*
232+
* @param $salt
233+
* @return self
234+
*/
235+
public function setSalt($salt)
171236
{
172-
return $this->salt;
237+
$this->salt = $salt;
238+
239+
return $this;
173240
}
174241

242+
/**
243+
* Get algorithm from self::$algorithmMapping
244+
*
245+
* @return string
246+
*/
175247
private function getAlgorithm()
176248
{
177249
$algorithm = self::$algorithmMapping[$this->algorithmSid];
@@ -182,35 +254,76 @@ private function getAlgorithm()
182254
return $algorithm;
183255
}
184256

257+
/**
258+
* Create a hashed password that MS Word will be able to work with
259+
*
260+
* @param string $password
261+
* @return string
262+
*/
185263
private function getPasswordHash($password)
186264
{
265+
$orig_encoding = mb_internal_encoding();
266+
mb_internal_encoding("UTF-8");
267+
187268
if (empty($password)) {
188269
return '';
189270
}
190-
$passwordMaxLength = 15;
191271

192-
// Truncate the password to $passwordMaxLength characters
193-
$password = mb_substr($password, 0, min($passwordMaxLength, mb_strlen($password)));
272+
$password = mb_substr($password, 0, min(self::$passwordMaxLength, mb_strlen($password)));
194273

274+
// Construct a new NULL-terminated string consisting of single-byte characters:
275+
// Get the single-byte values by iterating through the Unicode characters of the truncated password.
276+
// For each character, if the low byte is not equal to 0, take it. Otherwise, take the high byte.
277+
$pass_utf8 = mb_convert_encoding($password, 'UCS-2LE', 'UTF-8');
195278
$byteChars = [];
196-
197-
echo "password: '{$password}'(".mb_strlen($password).")";
198-
199-
$pass_utf8 = mb_convert_encoding($password, 'UCS-2LE', 'UTF-8');
200279
for ($i = 0; $i < mb_strlen($password); $i++) {
201-
$byteChars[$i] = ord(substr($pass_utf8, $i*2, 1));
280+
$byteChars[$i] = ord(substr($pass_utf8, $i * 2, 1));
202281
if ($byteChars[$i] == 0) {
203-
echo "hi!$i";
204-
$byteChars[$i] = ord(substr($pass_utf8, $i*2+1, 1));
282+
$byteChars[$i] = ord(substr($pass_utf8, $i * 2 + 1, 1));
205283
}
206284
}
207285

286+
// build low-order word and hig-order word and combine them
287+
$combinedKey = $this->buildCombinedKey($byteChars);
288+
// build reversed hexadecimal string
289+
$hex = strtoupper(dechex($combinedKey & 0xFFFFFFFF));
290+
$reversedHex = $hex[6].$hex[7].$hex[4].$hex[5].$hex[2].$hex[3].$hex[0].$hex[1];
291+
292+
$generatedKey = mb_convert_encoding($reversedHex, 'UCS-2LE', 'UTF-8');
293+
294+
// Implementation Notes List:
295+
// Word requires that the initial hash of the password with the salt not be considered in the count.
296+
// The initial hash of salt + key is not included in the iteration count.
297+
$generatedKey = hash($this->getAlgorithm(), base64_decode($this->getSalt()) . $generatedKey, true);
298+
for ($i = 0; $i < $this->getSpinCount(); $i++) {
299+
$generatedKey = hash($this->getAlgorithm(), $generatedKey . pack("CCCC", $i, $i>>8, $i>>16, $i>>24), true);
300+
}
301+
$generatedKey = base64_encode($generatedKey);
302+
303+
mb_internal_encoding($orig_encoding);
304+
305+
return $generatedKey;
306+
}
307+
308+
/**
309+
* Build combined key from low-order word and high-order word
310+
*
311+
* @param array $byteChars -> byte array representation of password
312+
* @return int
313+
*/
314+
private function buildCombinedKey($byteChars)
315+
{
208316
// Compute the high-order word
317+
// Initialize from the initial code array (see above), depending on the passwords length.
209318
$highOrderWord = self::$initialCodeArray[sizeof($byteChars) - 1];
319+
320+
// For each character in the password:
321+
// For every bit in the character, starting with the least significant and progressing to (but excluding)
322+
// the most significant, if the bit is set, XOR the key’s high-order word with the corresponding word from
323+
// the Encryption Matrix
210324
for ($i = 0; $i < sizeof($byteChars); $i++) {
211-
$tmp = $passwordMaxLength - sizeof($byteChars) + $i;
325+
$tmp = self::$passwordMaxLength - sizeof($byteChars) + $i;
212326
$matrixRow = self::$encryptionMatrix[$tmp];
213-
214327
for ($intBit = 0; $intBit < 7; $intBit++) {
215328
if (($byteChars[$i] & (0x0001 << $intBit)) != 0) {
216329
$highOrderWord = ($highOrderWord ^ $matrixRow[$intBit]);
@@ -219,55 +332,26 @@ private function getPasswordHash($password)
219332
}
220333

221334
// Compute low-order word
335+
// Initialize with 0
222336
$lowOrderWord = 0;
337+
// For each character in the password, going backwards
223338
for ($i = sizeof($byteChars) - 1; $i >= 0; $i--) {
339+
// low-order word = (((low-order word SHR 14) AND 0x0001) OR (low-order word SHL 1) AND 0x7FFF)) XOR character
224340
$lowOrderWord = (((($lowOrderWord >> 14) & 0x0001) | (($lowOrderWord << 1) & 0x7FFF)) ^ $byteChars[$i]);
225341
}
342+
// Lastly, low-order word = (((low-order word SHR 14) AND 0x0001) OR (low-order word SHL 1) AND 0x7FFF)) XOR strPassword length XOR 0xCE4B.
226343
$lowOrderWord = (((($lowOrderWord >> 14) & 0x0001) | (($lowOrderWord << 1) & 0x7FFF)) ^ sizeof($byteChars) ^ 0xCE4B);
227344

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);
345+
// Combine the Low and High Order Word
346+
return $this->int32(($highOrderWord << 16) + $lowOrderWord);
269347
}
270348

349+
/**
350+
* simulate behaviour of int32
351+
*
352+
* @param int $value
353+
* @return int
354+
*/
271355
private function int32($value)
272356
{
273357
$value = ($value & 0xFFFFFFFF);
@@ -278,12 +362,4 @@ private function int32($value)
278362

279363
return $value;
280364
}
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-
}
289365
}

0 commit comments

Comments
 (0)