@@ -76,6 +76,7 @@ class Protection
76
76
[0x3331 , 0x6662 , 0xCCC4 , 0x89A9 , 0x0373 , 0x06E6 , 0x0DCC ],
77
77
[0x1021 , 0x2042 , 0x4084 , 0x8108 , 0x1231 , 0x2462 , 0x48C4 ]
78
78
];
79
+ static $ passwordMaxLength = 15 ;
79
80
80
81
/**
81
82
* Editing restriction none|readOnly|comments|trackedChanges|forms
@@ -85,12 +86,32 @@ class Protection
85
86
*/
86
87
private $ editing ;
87
88
89
+ /**
90
+ * Hashed password
91
+ *
92
+ * @var string
93
+ */
88
94
private $ password ;
89
95
96
+ /**
97
+ * Number of hashing iterations
98
+ *
99
+ * @var int
100
+ */
90
101
private $ spinCount = 100000 ;
91
102
103
+ /**
104
+ * Algorithm-SID according to self::$algorithmMapping
105
+ *
106
+ * @var int
107
+ */
92
108
private $ algorithmSid = 4 ;
93
109
110
+ /**
111
+ * Hashed salt
112
+ *
113
+ * @var string
114
+ */
94
115
private $ salt ;
95
116
96
117
/**
@@ -126,52 +147,103 @@ public function setEditing($editing = null)
126
147
return $ this ;
127
148
}
128
149
150
+ /**
151
+ * Get password hash
152
+ *
153
+ * @return string
154
+ */
129
155
public function getPassword ()
130
156
{
131
157
return $ this ->password ;
132
158
}
133
159
160
+ /**
161
+ * Set password
162
+ *
163
+ * @param $password
164
+ * @return self
165
+ */
134
166
public function setPassword ($ password )
135
167
{
136
168
$ this ->password = $ this ->getPasswordHash ($ password );
137
169
138
170
return $ this ;
139
171
}
140
172
173
+ /**
174
+ * Get count for hash iterations
175
+ *
176
+ * @return int
177
+ */
141
178
public function getSpinCount ()
142
179
{
143
180
return $ this ->spinCount ;
144
181
}
145
182
183
+ /**
184
+ * Set count for hash iterations
185
+ *
186
+ * @param $spinCount
187
+ * @return self
188
+ */
146
189
public function setSpinCount ($ spinCount )
147
190
{
148
191
$ this ->spinCount = $ spinCount ;
149
192
150
193
return $ this ;
151
194
}
152
195
196
+ /**
197
+ * Get algorithm-sid
198
+ *
199
+ * @return int
200
+ */
153
201
public function getAlgorithmSid ()
154
202
{
155
203
return $ this ->algorithmSid ;
156
204
}
157
205
206
+ /**
207
+ * Set algorithm-sid (see self::$algorithmMapping)
208
+ *
209
+ * @param $algorithmSid
210
+ * @return self
211
+ */
158
212
public function setAlgorithmSid ($ algorithmSid )
159
213
{
160
214
$ this ->algorithmSid = $ algorithmSid ;
161
215
162
216
return $ this ;
163
217
}
164
218
165
- public function setSalt ($ salt )
219
+ /**
220
+ * Get salt hash
221
+ *
222
+ * @return string
223
+ */
224
+ public function getSalt ()
166
225
{
167
- $ this ->salt = $ salt ;
226
+ return $ this ->salt ;
168
227
}
169
228
170
- public function getSalt ()
229
+ /**
230
+ * Set salt hash
231
+ *
232
+ * @param $salt
233
+ * @return self
234
+ */
235
+ public function setSalt ($ salt )
171
236
{
172
- return $ this ->salt ;
237
+ $ this ->salt = $ salt ;
238
+
239
+ return $ this ;
173
240
}
174
241
242
+ /**
243
+ * Get algorithm from self::$algorithmMapping
244
+ *
245
+ * @return string
246
+ */
175
247
private function getAlgorithm ()
176
248
{
177
249
$ algorithm = self ::$ algorithmMapping [$ this ->algorithmSid ];
@@ -182,35 +254,76 @@ private function getAlgorithm()
182
254
return $ algorithm ;
183
255
}
184
256
257
+ /**
258
+ * Create a hashed password that MS Word will be able to work with
259
+ *
260
+ * @param string $password
261
+ * @return string
262
+ */
185
263
private function getPasswordHash ($ password )
186
264
{
265
+ $ orig_encoding = mb_internal_encoding ();
266
+ mb_internal_encoding ("UTF-8 " );
267
+
187
268
if (empty ($ password )) {
188
269
return '' ;
189
270
}
190
- $ passwordMaxLength = 15 ;
191
271
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 )));
194
273
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 ' );
195
278
$ byteChars = [];
196
-
197
- echo "password: ' {$ password }'( " .mb_strlen ($ password ).") " ;
198
-
199
- $ pass_utf8 = mb_convert_encoding ($ password , 'UCS-2LE ' , 'UTF-8 ' );
200
279
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 ));
202
281
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 ));
205
283
}
206
284
}
207
285
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
+ {
208
316
// Compute the high-order word
317
+ // Initialize from the initial code array (see above), depending on the passwords length.
209
318
$ 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
210
324
for ($ i = 0 ; $ i < sizeof ($ byteChars ); $ i ++) {
211
- $ tmp = $ passwordMaxLength - sizeof ($ byteChars ) + $ i ;
325
+ $ tmp = self :: $ passwordMaxLength - sizeof ($ byteChars ) + $ i ;
212
326
$ matrixRow = self ::$ encryptionMatrix [$ tmp ];
213
-
214
327
for ($ intBit = 0 ; $ intBit < 7 ; $ intBit ++) {
215
328
if (($ byteChars [$ i ] & (0x0001 << $ intBit )) != 0 ) {
216
329
$ highOrderWord = ($ highOrderWord ^ $ matrixRow [$ intBit ]);
@@ -219,55 +332,26 @@ private function getPasswordHash($password)
219
332
}
220
333
221
334
// Compute low-order word
335
+ // Initialize with 0
222
336
$ lowOrderWord = 0 ;
337
+ // For each character in the password, going backwards
223
338
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
224
340
$ lowOrderWord = (((($ lowOrderWord >> 14 ) & 0x0001 ) | (($ lowOrderWord << 1 ) & 0x7FFF )) ^ $ byteChars [$ i ]);
225
341
}
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.
226
343
$ lowOrderWord = (((($ lowOrderWord >> 14 ) & 0x0001 ) | (($ lowOrderWord << 1 ) & 0x7FFF )) ^ sizeof ($ byteChars ) ^ 0xCE4B );
227
344
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 );
269
347
}
270
348
349
+ /**
350
+ * simulate behaviour of int32
351
+ *
352
+ * @param int $value
353
+ * @return int
354
+ */
271
355
private function int32 ($ value )
272
356
{
273
357
$ value = ($ value & 0xFFFFFFFF );
@@ -278,12 +362,4 @@ private function int32($value)
278
362
279
363
return $ value ;
280
364
}
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
- }
289
365
}
0 commit comments