PluginProbe
TablePress – Tables in WordPress made easy / 3.4
TablePress – Tables in WordPress made easy v3.4
3.4 3.3.4 3.3.3 3.3.2 3.3.1 trunk 1.12 1.14 1.9.2 2.0.4 2.1.7 2.1.8 2.2 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.3 2.3.1 2.3.2 2.4 2.4.1 2.4.2 2.4.3 All 45 releases
tablepress / libraries / vendor / PhpSpreadsheet / Shared / Xlsx / AgileEncryption.php

AgileEncryption.php in TablePress – Tables in WordPress made easy 3.4, at libraries/vendor/PhpSpreadsheet/Shared/Xlsx/AgileEncryption.php

863 lines 41.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace TablePress\PhpOffice\PhpSpreadsheet\Shared\Xlsx;
4
5 use TablePress\PhpOffice\PhpSpreadsheet\Reader\Exception;
6 use TablePress\PhpOffice\PhpSpreadsheet\Shared\File as SharedFile;
7 use TablePress\PhpOffice\PhpSpreadsheet\Shared\OLE;
8 use SimpleXMLElement;
9 use Stringable;
10 use Throwable;
11
12 /** ECMA-376 Agile Encryption (AES-256/SHA-512 profile). */
13 final class AgileEncryption
14 {
15 public const MAX_SPIN_COUNT = 10000000;
16
17 private const BLOCK_KEY_VERIFIER = "\xFE\xA7\xD2\x76\x3B\x4B\x9E\x79";
18
19 private const BLOCK_KEY_VERIFIER_HASH = "\xD7\xAA\x0F\x6D\x30\x61\x34\x4E";
20
21 private const BLOCK_KEY_ENCRYPTED_KEY = "\x14\x6E\x0B\xE7\xAB\xAC\xD0\xD6";
22
23 private const BLOCK_KEY_HMAC_KEY = "\x5F\xB2\xAD\x01\x0C\xB9\xE1\xF6";
24
25 private const BLOCK_KEY_HMAC_VALUE = "\xA0\x67\x7F\x02\xB2\x2C\x84\x33";
26
27 private const BLOCK_SIZE = 16;
28
29 private const SEGMENT_SIZE = 4096;
30
31 private const ENCRYPTION_NAMESPACE = 'http://schemas.microsoft.com/office/2006/encryption';
32
33 private const PASSWORD_NAMESPACE = 'http://schemas.microsoft.com/office/2006/keyEncryptor/password';
34
35 private const PASSWORD_KEY_ENCRYPTOR_URI = 'http://schemas.microsoft.com/office/2006/keyEncryptor/password';
36
37 private const CFB_SECTOR_SIZE = 512;
38
39 private const CFB_MINI_SECTOR_SIZE = 64;
40
41 private const CFB_MINI_STREAM_CUTOFF = 4096;
42
43 private const CFB_FAT_ENTRIES_PER_SECTOR = self::CFB_SECTOR_SIZE >> 2;
44
45 private const CFB_DIFAT_ENTRIES_IN_HEADER = 109;
46
47 private const CFB_DIFAT_ENTRIES_PER_SECTOR = self::CFB_FAT_ENTRIES_PER_SECTOR - 1;
48
49 private const CFB_DIRECTORY_ENTRIES_PER_SECTOR = self::CFB_SECTOR_SIZE >> 7;
50
51 private const CFB_MINI_SECTORS_PER_SECTOR = self::CFB_SECTOR_SIZE >> 6;
52
53 private const DIRECTORY_ENCRYPTED_PACKAGE = 1;
54
55 private const DIRECTORY_DATA_SPACES = 2;
56
57 private const DIRECTORY_VERSION = 3;
58
59 private const DIRECTORY_DATA_SPACE_MAP = 4;
60
61 private const DIRECTORY_DATA_SPACE_INFO = 5;
62
63 private const DIRECTORY_STRONG_ENCRYPTION_DATA_SPACE = 6;
64
65 private const DIRECTORY_TRANSFORM_INFO = 7;
66
67 private const DIRECTORY_STRONG_ENCRYPTION_TRANSFORM = 8;
68
69 private const DIRECTORY_PRIMARY = 9;
70
71 private const DIRECTORY_ENCRYPTION_INFO = 10;
72
73 private const CFB_DIRECTORY_ENTRY_COUNT = self::DIRECTORY_ENCRYPTION_INFO + 1;
74
75 private const STREAM_ENCRYPTED_PACKAGE = 'EncryptedPackage';
76
77 private const STORAGE_DATA_SPACES = "\x06DataSpaces";
78
79 private const STREAM_VERSION = 'Version';
80
81 private const STREAM_DATA_SPACE_MAP = 'DataSpaceMap';
82
83 private const STORAGE_DATA_SPACE_INFO = 'DataSpaceInfo';
84
85 private const STREAM_STRONG_ENCRYPTION_DATA_SPACE = 'StrongEncryptionDataSpace';
86
87 private const STORAGE_TRANSFORM_INFO = 'TransformInfo';
88
89 private const STORAGE_STRONG_ENCRYPTION_TRANSFORM = 'StrongEncryptionTransform';
90
91 private const STREAM_PRIMARY = "\x06Primary";
92
93 private const STREAM_ENCRYPTION_INFO = 'EncryptionInfo';
94
95 /** @var array<string, array{string, int}> */
96 private const HASH_ALGORITHMS = [
97 'SHA1' => ['sha1', 20],
98 'SHA256' => ['sha256', 32],
99 'SHA384' => ['sha384', 48],
100 'SHA512' => ['sha512', 64],
101 ];
102
103 /**
104 * @return array{keyDataSalt: string, passwordSalt: string, encryptedVerifier: string, encryptedVerifierHash: string, encryptedKey: string, encryptedHmacKey: string, encryptedHmacValue: string, spinCount: int, keyBits: int, hashAlgorithm: string, hashSize: int}
105 */
106 public static function parse(string $encryptionInfo, int $maxSpinCount = self::MAX_SPIN_COUNT): array
107 {
108 if (substr($encryptionInfo, 0, 8) !== "\x04\x00\x04\x00\x40\x00\x00\x00") {
109 throw new Exception('Unsupported XLSX encryption profile.');
110 }
111
112 $xml = @simplexml_load_string((string) substr($encryptionInfo, 8));
113 if (!$xml instanceof SimpleXMLElement) {
114 throw new Exception('Malformed XLSX encryption information.');
115 }
116 $namespaces = $xml->getDocNamespaces(true) ?: [];
117 if ($xml->getName() !== 'encryption' || !in_array(self::ENCRYPTION_NAMESPACE, $namespaces, true) || !in_array(self::PASSWORD_NAMESPACE, $namespaces, true)) {
118 throw new Exception('Unsupported XLSX encryption profile.');
119 }
120 $xml->registerXPathNamespace('e', self::ENCRYPTION_NAMESPACE);
121 $xml->registerXPathNamespace('p', self::PASSWORD_NAMESPACE);
122 $keyDataNodes = $xml->xpath('/e:encryption/e:keyData') ?: [];
123 $integrityNodes = $xml->xpath('/e:encryption/e:dataIntegrity') ?: [];
124 $keyEncryptorNodes = $xml->xpath('/e:encryption/e:keyEncryptors/e:keyEncryptor') ?: [];
125 $encryptedKeyNodes = $xml->xpath('/e:encryption/e:keyEncryptors/e:keyEncryptor/p:encryptedKey') ?: [];
126 $keyData = $keyDataNodes[0] ?? null;
127 $integrity = $integrityNodes[0] ?? null;
128 $keyEncryptor = $keyEncryptorNodes[0] ?? null;
129 $encryptedKey = $encryptedKeyNodes[0] ?? null;
130 if (!$keyData instanceof SimpleXMLElement || !$integrity instanceof SimpleXMLElement || !$keyEncryptor instanceof SimpleXMLElement || !$encryptedKey instanceof SimpleXMLElement || count($keyDataNodes) !== 1 || count($integrityNodes) !== 1 || count($keyEncryptorNodes) !== 1 || count($encryptedKeyNodes) !== 1) {
131 throw new Exception('Unsupported XLSX encryption profile.');
132 }
133 $keyBits = self::decimalAttribute($keyData, 'keyBits');
134 $hashAlgorithm = (string) $keyData['hashAlgorithm'];
135 $hashSize = self::decimalAttribute($keyData, 'hashSize');
136 $spinCount = self::decimalAttribute($encryptedKey, 'spinCount');
137 $keyDataSaltSize = self::decimalAttribute($keyData, 'saltSize');
138 $keyDataBlockSize = self::decimalAttribute($keyData, 'blockSize');
139 $encryptedKeySaltSize = self::decimalAttribute($encryptedKey, 'saltSize');
140 $encryptedKeyBlockSize = self::decimalAttribute($encryptedKey, 'blockSize');
141 $encryptedKeyBits = self::decimalAttribute($encryptedKey, 'keyBits');
142 $encryptedKeyHashSize = self::decimalAttribute($encryptedKey, 'hashSize');
143 if (
144 (string) $keyData['cipherAlgorithm'] !== 'AES' || (string) $keyData['cipherChaining'] !== 'ChainingModeCBC'
145 || !self::isSupportedProfile($keyBits, $hashAlgorithm, $hashSize) || $keyDataSaltSize !== 16 || $keyDataBlockSize !== 16
146 || (string) $encryptedKey['cipherAlgorithm'] !== 'AES'
147 || (string) $encryptedKey['cipherChaining'] !== 'ChainingModeCBC'
148 || (string) $encryptedKey['hashAlgorithm'] !== $hashAlgorithm || $encryptedKeySaltSize !== 16 || $encryptedKeyBlockSize !== 16 || $encryptedKeyBits !== $keyBits
149 || $encryptedKeyHashSize !== $hashSize || $spinCount > min($maxSpinCount, self::MAX_SPIN_COUNT)
150 || (string) $keyEncryptor['uri'] !== self::PASSWORD_KEY_ENCRYPTOR_URI
151 ) {
152 throw new Exception('Unsupported XLSX encryption profile.');
153 }
154
155 $result = [
156 'keyDataSalt' => self::decode($keyData['saltValue']),
157 'passwordSalt' => self::decode($encryptedKey['saltValue']),
158 'encryptedVerifier' => self::decode($encryptedKey['encryptedVerifierHashInput']),
159 'encryptedVerifierHash' => self::decode($encryptedKey['encryptedVerifierHashValue']),
160 'encryptedKey' => self::decode($encryptedKey['encryptedKeyValue']),
161 'encryptedHmacKey' => self::decode($integrity['encryptedHmacKey']),
162 'encryptedHmacValue' => self::decode($integrity['encryptedHmacValue']),
163 'spinCount' => $spinCount,
164 'keyBits' => $keyBits,
165 'hashAlgorithm' => $hashAlgorithm,
166 'hashSize' => $hashSize,
167 ];
168 if (
169 strlen($result['keyDataSalt']) !== 16 || strlen($result['passwordSalt']) !== 16
170 || strlen($result['encryptedVerifier']) !== 16 || strlen($result['encryptedVerifierHash']) !== self::paddedLength($hashSize)
171 || strlen($result['encryptedKey']) !== self::paddedLength(intdiv($keyBits, 8)) || strlen($result['encryptedHmacKey']) !== self::paddedLength($hashSize)
172 || strlen($result['encryptedHmacValue']) !== self::paddedLength($hashSize)
173 ) {
174 throw new Exception('Malformed XLSX encryption information.');
175 }
176
177 return $result;
178 }
179
180 /** @param array{keyDataSalt: string, passwordSalt: string, encryptedVerifier: string, encryptedVerifierHash: string, encryptedKey: string, encryptedHmacKey: string, encryptedHmacValue: string, spinCount: int, keyBits: int, hashAlgorithm: string, hashSize: int} $info */
181 public static function decrypt(array $info, string $encryptedPackage, string $password): string
182 {
183 if ($password === '') {
184 throw new Exception('XLSX encryption password required.');
185 }
186 if (strlen($encryptedPackage) < 8) {
187 throw new Exception('Malformed encrypted XLSX package.');
188 }
189 $hash = self::passwordHash($password, $info['passwordSalt'], $info['spinCount'], $info['hashAlgorithm']);
190 $verifierKey = self::deriveKey($hash, self::BLOCK_KEY_VERIFIER, $info['hashAlgorithm'], $info['keyBits']);
191 $verifierHashKey = self::deriveKey($hash, self::BLOCK_KEY_VERIFIER_HASH, $info['hashAlgorithm'], $info['keyBits']);
192 $verifier = self::aesDecrypt($info['encryptedVerifier'], $verifierKey, $info['passwordSalt'], $info['keyBits']);
193 $expectedHash = self::aesDecrypt($info['encryptedVerifierHash'], $verifierHashKey, $info['passwordSalt'], $info['keyBits']);
194 if (!hash_equals(self::hash($info['hashAlgorithm'], $verifier), (string) substr($expectedHash, 0, $info['hashSize']))) {
195 throw new Exception('XLSX encryption password is incorrect.');
196 }
197 $secretKey = self::aesDecrypt($info['encryptedKey'], self::deriveKey($hash, self::BLOCK_KEY_ENCRYPTED_KEY, $info['hashAlgorithm'], $info['keyBits']), $info['passwordSalt'], $info['keyBits']);
198 $secretKey = (string) substr($secretKey, 0, intdiv($info['keyBits'], 8));
199 if (strlen($secretKey) !== intdiv($info['keyBits'], 8)) {
200 throw new Exception('Malformed XLSX encryption information.');
201 }
202 self::verifyIntegrity($info, $secretKey, $encryptedPackage);
203 $size = self::unpackSize(substr($encryptedPackage, 0, 8));
204 $offset = 8;
205 $plain = '';
206 for ($block = 0; $size > 0; ++$block) {
207 $length = min(self::SEGMENT_SIZE, $size);
208 $cipherLength = (int) (ceil($length / self::BLOCK_SIZE) * self::BLOCK_SIZE);
209 $cipher = (string) substr($encryptedPackage, $offset, $cipherLength);
210 if (strlen($cipher) !== $cipherLength) {
211 throw new Exception('Malformed encrypted XLSX package.');
212 }
213 $iv = self::iv($info['keyDataSalt'], pack('V', $block), $info['hashAlgorithm']);
214 $plain .= substr(self::aesDecrypt($cipher, $secretKey, $iv, $info['keyBits']), 0, $length);
215 $offset += $cipherLength;
216 $size -= $length;
217 }
218 if ($offset !== strlen($encryptedPackage)) {
219 throw new Exception('Malformed encrypted XLSX package.');
220 }
221
222 return $plain;
223 }
224
225 /** @param array{keyDataSalt: string, passwordSalt: string, encryptedVerifier: string, encryptedVerifierHash: string, encryptedKey: string, encryptedHmacKey: string, encryptedHmacValue: string, spinCount: int, keyBits: int, hashAlgorithm: string, hashSize: int} $info */
226 public static function decryptFile(array $info, string $inputFilename, string $outputFilename, string $password): void
227 {
228 if ($password === '') {
229 throw new Exception('XLSX encryption password required.');
230 }
231 $input = @fopen($inputFilename, 'rb');
232 if ($input === false) {
233 throw new Exception('Could not open XLSX package for decryption.');
234 }
235
236 try {
237 $hash = self::passwordHash($password, $info['passwordSalt'], $info['spinCount'], $info['hashAlgorithm']);
238 $verifier = self::aesDecrypt($info['encryptedVerifier'], self::deriveKey($hash, self::BLOCK_KEY_VERIFIER, $info['hashAlgorithm'], $info['keyBits']), $info['passwordSalt'], $info['keyBits']);
239 $expected = self::aesDecrypt($info['encryptedVerifierHash'], self::deriveKey($hash, self::BLOCK_KEY_VERIFIER_HASH, $info['hashAlgorithm'], $info['keyBits']), $info['passwordSalt'], $info['keyBits']);
240 if (!hash_equals(self::hash($info['hashAlgorithm'], $verifier), (string) substr($expected, 0, $info['hashSize']))) {
241 throw new Exception('XLSX encryption password is incorrect.');
242 }
243 $secretKey = (string) substr(self::aesDecrypt($info['encryptedKey'], self::deriveKey($hash, self::BLOCK_KEY_ENCRYPTED_KEY, $info['hashAlgorithm'], $info['keyBits']), $info['passwordSalt'], $info['keyBits']), 0, intdiv($info['keyBits'], 8));
244 if (strlen($secretKey) !== intdiv($info['keyBits'], 8)) {
245 throw new Exception('Malformed XLSX encryption information.');
246 }
247 self::verifyIntegrityFile($info, $secretKey, $input);
248 $output = @fopen($outputFilename, 'wb');
249 if ($output === false) {
250 throw new Exception('Could not open XLSX package for decryption.');
251 }
252 rewind($input);
253
254 try {
255 $size = self::unpackSize(self::read($input, 8));
256 for ($block = 0; $size > 0; ++$block) {
257 $length = min(self::SEGMENT_SIZE, $size);
258 $cipher = self::read($input, self::paddedLength($length));
259 $iv = self::iv($info['keyDataSalt'], pack('V', $block), $info['hashAlgorithm']);
260 self::write($output, (string) substr(self::aesDecrypt($cipher, $secretKey, $iv, $info['keyBits']), 0, $length));
261 $size -= $length;
262 }
263 if (fread($input, 1) !== '') {
264 throw new Exception('Malformed encrypted XLSX package.');
265 }
266 } finally {
267 fclose($output);
268 }
269 } finally {
270 fclose($input);
271 }
272 }
273
274 /**
275 * Create the EncryptionInfo and EncryptedPackage streams for an Agile-encrypted XLSX.
276 *
277 * @return array{encryptionInfo: string, encryptedPackage: string}
278 */
279 public static function encrypt(string $plainPackage, string $password, int $keyBits = 256, string $hashAlgorithm = 'SHA512', int $spinCount = 100000): array
280 {
281 if ($password === '') {
282 throw new Exception('XLSX encryption password required.');
283 }
284 if (!self::isSupportedProfile($keyBits, $hashAlgorithm, self::HASH_ALGORITHMS[$hashAlgorithm][1] ?? 0) || $spinCount < 0 || $spinCount > self::MAX_SPIN_COUNT) {
285 throw new Exception('Unsupported XLSX encryption profile.');
286 }
287
288 $passwordSalt = random_bytes(self::BLOCK_SIZE);
289 $keyDataSalt = random_bytes(self::BLOCK_SIZE);
290 $hashSize = self::hashSize($hashAlgorithm);
291 $passwordHash = self::passwordHash($password, $passwordSalt, $spinCount, $hashAlgorithm);
292 $verifier = random_bytes(self::BLOCK_SIZE);
293 $secretKey = random_bytes(self::keyLength($keyBits));
294 $encryptedVerifier = self::aesEncrypt($verifier, self::deriveKey($passwordHash, self::BLOCK_KEY_VERIFIER, $hashAlgorithm, $keyBits), $passwordSalt, $keyBits);
295 $encryptedVerifierHash = self::aesEncrypt(str_pad(self::hash($hashAlgorithm, $verifier), self::paddedLength($hashSize), "\x00"), self::deriveKey($passwordHash, self::BLOCK_KEY_VERIFIER_HASH, $hashAlgorithm, $keyBits), $passwordSalt, $keyBits);
296 $encryptedKey = self::aesEncrypt(str_pad($secretKey, self::paddedLength(strlen($secretKey)), "\x00"), self::deriveKey($passwordHash, self::BLOCK_KEY_ENCRYPTED_KEY, $hashAlgorithm, $keyBits), $passwordSalt, $keyBits);
297
298 $encryptedPackage = self::packSize(strlen($plainPackage));
299 for ($block = 0, $offset = 0; $offset < strlen($plainPackage); ++$block, $offset += self::SEGMENT_SIZE) {
300 $segment = (string) substr($plainPackage, $offset, self::SEGMENT_SIZE);
301 $iv = self::iv($keyDataSalt, pack('V', $block), $hashAlgorithm);
302 $encryptedPackage .= self::aesEncrypt(str_pad($segment, self::paddedLength(strlen($segment)), "\x00"), $secretKey, $iv, $keyBits);
303 }
304
305 $hmacKey = random_bytes(self::hashSize($hashAlgorithm));
306 $hmacKeyIv = self::iv($keyDataSalt, self::BLOCK_KEY_HMAC_KEY, $hashAlgorithm);
307 $hmacValueIv = self::iv($keyDataSalt, self::BLOCK_KEY_HMAC_VALUE, $hashAlgorithm);
308 $encryptedHmacKey = self::aesEncrypt(str_pad($hmacKey, self::paddedLength($hashSize), "\x00"), $secretKey, $hmacKeyIv, $keyBits);
309 $encryptedHmacValue = self::aesEncrypt(str_pad(hash_hmac(self::HASH_ALGORITHMS[$hashAlgorithm][0], $encryptedPackage, $hmacKey, true), self::paddedLength($hashSize), "\x00"), $secretKey, $hmacValueIv, $keyBits);
310
311 $encryptionInfo = "\x04\x00\x04\x00\x40\x00\x00\x00" . self::encryptionInfoXml(
312 $keyDataSalt,
313 $passwordSalt,
314 $encryptedVerifier,
315 $encryptedVerifierHash,
316 $encryptedKey,
317 $encryptedHmacKey,
318 $encryptedHmacValue,
319 $keyBits,
320 $hashAlgorithm,
321 $hashSize,
322 $spinCount
323 );
324
325 return ['encryptionInfo' => $encryptionInfo, 'encryptedPackage' => $encryptedPackage];
326 }
327
328 /**
329 * Encrypt a ZIP package without loading either package into memory.
330 *
331 * @return array{encryptionInfo: string, encryptedPackageFilename: string}
332 */
333 public static function encryptFile(string $plainPackageFilename, string $password, int $keyBits = 256, string $hashAlgorithm = 'SHA512', int $spinCount = 100000): array
334 {
335 if ($password === '') {
336 throw new Exception('XLSX encryption password required.');
337 }
338 if (!self::isSupportedProfile($keyBits, $hashAlgorithm, self::HASH_ALGORITHMS[$hashAlgorithm][1] ?? 0) || $spinCount < 0 || $spinCount > self::MAX_SPIN_COUNT) {
339 throw new Exception('Unsupported XLSX encryption profile.');
340 }
341 $size = @filesize($plainPackageFilename);
342 if ($size === false) {
343 throw new Exception('Could not determine XLSX package size.');
344 }
345 $input = fopen($plainPackageFilename, 'rb');
346 if ($input === false) {
347 throw new Exception('Could not open XLSX package for encryption.');
348 }
349 $encryptedPackageFilename = SharedFile::temporaryFilename();
350 $output = fopen($encryptedPackageFilename, 'wb');
351 if ($output === false) {
352 fclose($input);
353 @unlink($encryptedPackageFilename);
354
355 throw new Exception('Could not create encrypted XLSX package.');
356 }
357
358 try {
359 $passwordSalt = random_bytes(self::BLOCK_SIZE);
360 $keyDataSalt = random_bytes(self::BLOCK_SIZE);
361 $hashSize = self::hashSize($hashAlgorithm);
362 $passwordHash = self::passwordHash($password, $passwordSalt, $spinCount, $hashAlgorithm);
363 $verifier = random_bytes(self::BLOCK_SIZE);
364 $secretKey = random_bytes(self::keyLength($keyBits));
365 $encryptedVerifier = self::aesEncrypt($verifier, self::deriveKey($passwordHash, self::BLOCK_KEY_VERIFIER, $hashAlgorithm, $keyBits), $passwordSalt, $keyBits);
366 $encryptedVerifierHash = self::aesEncrypt(str_pad(self::hash($hashAlgorithm, $verifier), self::paddedLength($hashSize), "\x00"), self::deriveKey($passwordHash, self::BLOCK_KEY_VERIFIER_HASH, $hashAlgorithm, $keyBits), $passwordSalt, $keyBits);
367 $encryptedKey = self::aesEncrypt(str_pad($secretKey, self::paddedLength(strlen($secretKey)), "\x00"), self::deriveKey($passwordHash, self::BLOCK_KEY_ENCRYPTED_KEY, $hashAlgorithm, $keyBits), $passwordSalt, $keyBits);
368 $hmacKey = random_bytes(self::hashSize($hashAlgorithm));
369 $hmac = hash_init(self::HASH_ALGORITHMS[$hashAlgorithm][0], HASH_HMAC, $hmacKey);
370 $header = self::packSize($size);
371 self::write($output, $header);
372 hash_update($hmac, $header);
373 $remaining = $size;
374 for ($block = 0; $remaining > 0; ++$block) {
375 $length = min(self::SEGMENT_SIZE, $remaining);
376 $segment = fread($input, $length);
377 if ($segment === false || strlen($segment) !== $length) {
378 throw new Exception('Could not read XLSX package for encryption.');
379 }
380 $iv = self::iv($keyDataSalt, pack('V', $block), $hashAlgorithm);
381 $cipher = self::aesEncrypt(str_pad($segment, self::paddedLength(strlen($segment)), "\x00"), $secretKey, $iv, $keyBits);
382 self::write($output, $cipher);
383 hash_update($hmac, $cipher);
384 $remaining -= $length;
385 }
386 $extra = fread($input, 1);
387 if ($extra === false || $extra !== '') {
388 throw new Exception('XLSX package changed while being encrypted.');
389 }
390 $hmacKeyIv = self::iv($keyDataSalt, self::BLOCK_KEY_HMAC_KEY, $hashAlgorithm);
391 $hmacValueIv = self::iv($keyDataSalt, self::BLOCK_KEY_HMAC_VALUE, $hashAlgorithm);
392 $encryptedHmacKey = self::aesEncrypt(str_pad($hmacKey, self::paddedLength($hashSize), "\x00"), $secretKey, $hmacKeyIv, $keyBits);
393 $encryptedHmacValue = self::aesEncrypt(str_pad(hash_final($hmac, true), self::paddedLength($hashSize), "\x00"), $secretKey, $hmacValueIv, $keyBits);
394 $encryptionInfo = "\x04\x00\x04\x00\x40\x00\x00\x00" . self::encryptionInfoXml($keyDataSalt, $passwordSalt, $encryptedVerifier, $encryptedVerifierHash, $encryptedKey, $encryptedHmacKey, $encryptedHmacValue, $keyBits, $hashAlgorithm, $hashSize, $spinCount);
395 } catch (Throwable $e) {
396 @unlink($encryptedPackageFilename);
397
398 throw $e;
399 } finally {
400 fclose($input);
401 fclose($output);
402 }
403
404 return ['encryptionInfo' => $encryptionInfo, 'encryptedPackageFilename' => $encryptedPackageFilename];
405 }
406
407 /**
408 * Write an Office encrypted package CFB container using a file-backed EncryptedPackage stream.
409 *
410 * @param resource $fileHandle
411 */
412 public static function writeContainerFromFile($fileHandle, string $encryptionInfo, string $encryptedPackageFilename): void
413 {
414 $containerFilename = SharedFile::temporaryFilename();
415 $container = fopen($containerFilename, 'w+b');
416 if ($container === false) {
417 @unlink($containerFilename);
418
419 throw new Exception('Could not create encrypted XLSX container.');
420 }
421
422 try {
423 self::writeEcma376Container($container, $encryptionInfo, $encryptedPackageFilename);
424 rewind($container);
425 while (!feof($container)) {
426 $data = fread($container, 8192);
427 if ($data === false) {
428 throw new Exception('Could not read encrypted XLSX container.');
429 }
430 if ($data !== '' && fwrite($fileHandle, $data) !== strlen($data)) {
431 throw new Exception('Could not write encrypted XLSX container.');
432 }
433 }
434 } finally {
435 fclose($container);
436 @unlink($containerFilename);
437 }
438 }
439
440 /**
441 * Serialize the ECMA-376 encrypted-package CFB layout. The directory tree
442 * is prescribed by the Data Spaces structure; sector allocation is dynamic
443 * so normal workbook size is not artificially constrained.
444 *
445 * @param resource $fileHandle
446 */
447 private static function writeEcma376Container($fileHandle, string $encryptionInfo, string $encryptedPackageFilename): void
448 {
449 $packageSize = filesize($encryptedPackageFilename);
450 if ($packageSize === false || $packageSize < self::CFB_MINI_STREAM_CUTOFF || $packageSize > 0xFFFFFFFF) {
451 throw new Exception('Malformed encrypted XLSX package.');
452 }
453 $smallStreams = [
454 self::DIRECTORY_VERSION => [self::STREAM_VERSION, self::dataSpacesVersion()],
455 self::DIRECTORY_DATA_SPACE_MAP => [self::STREAM_DATA_SPACE_MAP, self::dataSpaceMap()],
456 self::DIRECTORY_STRONG_ENCRYPTION_DATA_SPACE => [self::STREAM_STRONG_ENCRYPTION_DATA_SPACE, self::strongEncryptionDataSpace()],
457 self::DIRECTORY_PRIMARY => [self::STREAM_PRIMARY, self::primaryTransform()],
458 self::DIRECTORY_ENCRYPTION_INFO => [self::STREAM_ENCRYPTION_INFO, $encryptionInfo],
459 ];
460 $miniStarts = array_fill_keys(array_keys($smallStreams), 0);
461 $miniFat = [];
462 $miniCount = 0;
463 foreach ($smallStreams as $id => [, $content]) {
464 if (strlen($content) >= self::CFB_MINI_STREAM_CUTOFF) {
465 throw new Exception('Malformed XLSX encryption information.');
466 }
467 $count = (int) ceil(strlen($content) / self::CFB_MINI_SECTOR_SIZE);
468 $miniStarts[$id] = $miniCount;
469 for ($i = 0; $i < $count - 1; ++$i) {
470 $miniFat[$miniCount + $i] = $miniCount + $i + 1;
471 }
472 $miniFat[$miniCount + $count - 1] = 0xFFFFFFFE;
473 $miniCount += $count;
474 }
475 $miniFatSectors = (int) ceil($miniCount / self::CFB_FAT_ENTRIES_PER_SECTOR);
476 $miniFat = array_pad($miniFat, $miniFatSectors * self::CFB_FAT_ENTRIES_PER_SECTOR, 0xFFFFFFFF);
477 $miniDataSectors = (int) ceil($miniCount / self::CFB_MINI_SECTORS_PER_SECTOR);
478 $directorySectors = (int) ceil(self::CFB_DIRECTORY_ENTRY_COUNT / self::CFB_DIRECTORY_ENTRIES_PER_SECTOR);
479 $packageSectors = (int) ceil($packageSize / self::CFB_SECTOR_SIZE);
480 $contentSectors = $miniFatSectors + $directorySectors + $miniDataSectors + $packageSectors;
481 $fatSectors = 1;
482 $difatSectors = 0;
483 do {
484 $requiredFatSectors = (int) ceil(($contentSectors + $fatSectors + $difatSectors) / self::CFB_FAT_ENTRIES_PER_SECTOR);
485 $requiredDifatSectors = $requiredFatSectors <= self::CFB_DIFAT_ENTRIES_IN_HEADER ? 0 : (int) ceil(($requiredFatSectors - self::CFB_DIFAT_ENTRIES_IN_HEADER) / self::CFB_DIFAT_ENTRIES_PER_SECTOR);
486 $changed = $requiredFatSectors !== $fatSectors || $requiredDifatSectors !== $difatSectors;
487 $fatSectors = $requiredFatSectors;
488 $difatSectors = $requiredDifatSectors;
489 } while ($changed);
490 $miniFatSector = $fatSectors;
491 $directorySector = $miniFatSector + $miniFatSectors;
492 $miniDataSector = $directorySector + $directorySectors;
493 $packageSector = $miniDataSector + $miniDataSectors;
494 $difatSector = $packageSector + $packageSectors;
495
496 self::write($fileHandle, self::cfbHeader($fatSectors, $directorySector, $miniFatSector, $miniFatSectors, $difatSector, $difatSectors));
497 $fat = array_fill(0, $fatSectors * self::CFB_FAT_ENTRIES_PER_SECTOR, 0xFFFFFFFF);
498 for ($sector = 0; $sector < $fatSectors; ++$sector) {
499 $fat[$sector] = 0xFFFFFFFD;
500 }
501 self::chainFat($fat, $miniFatSector, $miniFatSectors);
502 self::chainFat($fat, $directorySector, $directorySectors);
503 self::chainFat($fat, $miniDataSector, $miniDataSectors);
504 self::chainFat($fat, $packageSector, $packageSectors);
505 for ($sector = 0; $sector < $difatSectors; ++$sector) {
506 $fat[$difatSector + $sector] = 0xFFFFFFFC;
507 }
508 self::write($fileHandle, self::packSectors($fat));
509 self::write($fileHandle, self::packSectors($miniFat));
510
511 $entries = [
512 // CFB sibling trees sort by name length, then case-folded UTF-16 code points.
513 self::directoryEntry('Root Entry', 5, 1, 0xFFFFFFFF, 0xFFFFFFFF, self::DIRECTORY_ENCRYPTION_INFO, $miniDataSector, $miniCount * self::CFB_MINI_SECTOR_SIZE),
514 self::directoryEntry(self::STREAM_ENCRYPTED_PACKAGE, 2, 0, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, $packageSector, $packageSize),
515 self::directoryEntry(self::STORAGE_DATA_SPACES, 1, 0, 0xFFFFFFFF, 0xFFFFFFFF, self::DIRECTORY_DATA_SPACE_MAP, 0, 0),
516 self::directoryEntry(self::STREAM_VERSION, 2, 0, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, $miniStarts[self::DIRECTORY_VERSION], strlen($smallStreams[self::DIRECTORY_VERSION][1])),
517 self::directoryEntry(self::STREAM_DATA_SPACE_MAP, 2, 1, self::DIRECTORY_VERSION, self::DIRECTORY_DATA_SPACE_INFO, 0xFFFFFFFF, $miniStarts[self::DIRECTORY_DATA_SPACE_MAP], strlen($smallStreams[self::DIRECTORY_DATA_SPACE_MAP][1])),
518 self::directoryEntry(self::STORAGE_DATA_SPACE_INFO, 1, 0, 0xFFFFFFFF, 0xFFFFFFFF, self::DIRECTORY_TRANSFORM_INFO, 0, 0),
519 self::directoryEntry(self::STREAM_STRONG_ENCRYPTION_DATA_SPACE, 2, 0, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, $miniStarts[self::DIRECTORY_STRONG_ENCRYPTION_DATA_SPACE], strlen($smallStreams[self::DIRECTORY_STRONG_ENCRYPTION_DATA_SPACE][1])),
520 self::directoryEntry(self::STORAGE_TRANSFORM_INFO, 1, 1, self::DIRECTORY_STRONG_ENCRYPTION_DATA_SPACE, 0xFFFFFFFF, self::DIRECTORY_STRONG_ENCRYPTION_TRANSFORM, 0, 0),
521 self::directoryEntry(self::STORAGE_STRONG_ENCRYPTION_TRANSFORM, 1, 1, 0xFFFFFFFF, 0xFFFFFFFF, self::DIRECTORY_PRIMARY, 0, 0),
522 self::directoryEntry(self::STREAM_PRIMARY, 2, 1, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, $miniStarts[self::DIRECTORY_PRIMARY], strlen($smallStreams[self::DIRECTORY_PRIMARY][1])),
523 self::directoryEntry(self::STREAM_ENCRYPTION_INFO, 2, 1, self::DIRECTORY_DATA_SPACES, self::DIRECTORY_ENCRYPTED_PACKAGE, 0xFFFFFFFF, $miniStarts[self::DIRECTORY_ENCRYPTION_INFO], strlen($smallStreams[self::DIRECTORY_ENCRYPTION_INFO][1])),
524 ];
525 self::write($fileHandle, str_pad(implode('', $entries), $directorySectors * self::CFB_SECTOR_SIZE, "\x00"));
526 $miniData = str_repeat("\x00", $miniDataSectors * self::CFB_SECTOR_SIZE);
527 foreach ($smallStreams as $id => [, $content]) {
528 $miniData = substr_replace($miniData, $content, $miniStarts[$id] * self::CFB_MINI_SECTOR_SIZE, strlen($content));
529 }
530 self::write($fileHandle, $miniData);
531 $package = fopen($encryptedPackageFilename, 'rb');
532 if ($package === false) {
533 throw new Exception('Could not open encrypted XLSX package.');
534 }
535
536 try {
537 while (!feof($package)) {
538 $data = fread($package, 8192);
539 if ($data === false) {
540 throw new Exception('Could not read encrypted XLSX package.');
541 }
542 if ($data !== '') {
543 self::write($fileHandle, $data);
544 }
545 }
546 } finally {
547 fclose($package);
548 }
549 $padding = $packageSectors * self::CFB_SECTOR_SIZE - $packageSize;
550 if ($padding > 0) {
551 self::write($fileHandle, str_repeat("\x00", $padding));
552 }
553 for ($sector = 0; $sector < $difatSectors; ++$sector) {
554 $firstFatSector = self::CFB_DIFAT_ENTRIES_IN_HEADER + $sector * self::CFB_DIFAT_ENTRIES_PER_SECTOR;
555 $fatReferences = [];
556 for ($i = 0; $i < self::CFB_DIFAT_ENTRIES_PER_SECTOR; ++$i) {
557 $fatReferences[] = $firstFatSector + $i < $fatSectors ? $firstFatSector + $i : 0xFFFFFFFF;
558 }
559 $fatReferences[] = $sector + 1 < $difatSectors ? $difatSector + $sector + 1 : 0xFFFFFFFE;
560 self::write($fileHandle, self::packSectors($fatReferences));
561 }
562 }
563
564 /** @param int[] $fat */
565 private static function chainFat(array &$fat, int $start, int $count): void
566 {
567 for ($i = 0; $i < $count - 1; ++$i) {
568 $fat[$start + $i] = $start + $i + 1;
569 }
570 $fat[$start + $count - 1] = 0xFFFFFFFE;
571 }
572
573 /** @param int[] $sectors */
574 private static function packSectors(array $sectors): string
575 {
576 return pack('V*', ...$sectors);
577 }
578
579 private static function cfbHeader(int $fatSectors, int $directorySector, int $miniFatSector, int $miniFatSectors, int $difatSector, int $difatSectors): string
580 {
581 $headerDifat = [];
582 for ($sector = 0; $sector < self::CFB_DIFAT_ENTRIES_IN_HEADER; ++$sector) {
583 $headerDifat[] = $sector < $fatSectors ? $sector : 0xFFFFFFFF;
584 }
585
586 return "\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1" . str_repeat("\x00", 16)
587 . pack('v8', 0x3E, 3, 0xFFFE, 9, 6, 0, 0, 0)
588 . pack('V8', 0, $fatSectors, $directorySector, 0, self::CFB_MINI_STREAM_CUTOFF, $miniFatSector, $miniFatSectors, $difatSectors === 0 ? 0xFFFFFFFE : $difatSector)
589 . pack('V', $difatSectors) . self::packSectors($headerDifat);
590 }
591
592 private static function directoryEntry(string $name, int $type, int $color, int $left, int $right, int $child, int $start, int $size): string
593 {
594 $name = OLE::ascToUcs($name);
595
596 return str_pad($name, 64, "\x00") . pack('vCCVVV', strlen($name) + 2, $type, $color, $left, $right, $child)
597 . str_repeat("\x00", 16) . pack('V', 0) . str_repeat("\x00", 16)
598 . pack('V2', $start, $size % 4294967296) . pack('V', intdiv($size, 4294967296));
599 }
600
601 /**
602 * @param mixed $value
603 */
604 private static function decode($value): string
605 {
606 if (!is_string($value) && !(is_object($value) && method_exists($value, '__toString'))) {
607 throw new Exception('Malformed XLSX encryption information.');
608 }
609 $result = base64_decode((string) $value, true);
610 if ($result === false) {
611 throw new Exception('Malformed XLSX encryption information.');
612 }
613
614 return $result;
615 }
616
617 private static function decimalAttribute(SimpleXMLElement $element, string $name): int
618 {
619 $value = (string) $element[$name];
620 $maximum = (string) PHP_INT_MAX;
621 if ($value === '' || !ctype_digit($value) || strlen($value) > strlen($maximum) || (strlen($value) === strlen($maximum) && $value > $maximum)) {
622 throw new Exception('Malformed XLSX encryption information.');
623 }
624
625 return (int) $value;
626 }
627
628 private static function passwordHash(string $password, string $salt, int $spinCount, string $algorithm = 'SHA512'): string
629 {
630 $hash = self::hash($algorithm, $salt . mb_convert_encoding($password, 'UTF-16LE', 'UTF-8'));
631 for ($i = 0; $i < $spinCount; ++$i) {
632 $hash = self::hash($algorithm, pack('V', $i) . $hash);
633 }
634
635 return $hash;
636 }
637
638 private static function deriveKey(string $hash, string $blockKey, string $algorithm = 'SHA512', int $keyBits = 256): string
639 {
640 return (string) substr(str_pad(self::hash($algorithm, $hash . $blockKey), intdiv($keyBits, 8), "\x36"), 0, intdiv($keyBits, 8));
641 }
642
643 private static function aesDecrypt(string $data, string $key, string $iv, int $keyBits = 256): string
644 {
645 self::assertOpenSslAvailable();
646 $result = openssl_decrypt($data, 'aes-' . $keyBits . '-cbc', $key, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, $iv);
647 if ($result === false) {
648 throw new Exception('Malformed XLSX encrypted data.');
649 }
650
651 return $result;
652 }
653
654 private static function aesEncrypt(string $data, string $key, string $iv, int $keyBits = 256): string
655 {
656 self::assertOpenSslAvailable();
657 $result = openssl_encrypt($data, 'aes-' . $keyBits . '-cbc', $key, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, $iv);
658 if ($result === false) {
659 throw new Exception('Could not encrypt XLSX data.');
660 }
661
662 return $result;
663 }
664
665 private static function isSupportedProfile(int $keyBits, string $hashAlgorithm, int $hashSize): bool
666 {
667 return in_array($keyBits, [128, 192, 256], true)
668 && isset(self::HASH_ALGORITHMS[$hashAlgorithm])
669 && self::HASH_ALGORITHMS[$hashAlgorithm][1] === $hashSize;
670 }
671
672 private static function hash(string $algorithm, string $data): string
673 {
674 return hash(self::HASH_ALGORITHMS[$algorithm][0], $data, true);
675 }
676
677 /** @return positive-int */
678 private static function hashSize(string $algorithm): int
679 {
680 switch ($algorithm) {
681 case 'SHA1':
682 return 20;
683 case 'SHA256':
684 return 32;
685 case 'SHA384':
686 return 48;
687 case 'SHA512':
688 return 64;
689 default:
690 throw new Exception('Unsupported XLSX encryption profile.');
691 }
692 }
693
694 /** @return positive-int */
695 private static function keyLength(int $keyBits): int
696 {
697 switch ($keyBits) {
698 case 128:
699 return 16;
700 case 192:
701 return 24;
702 case 256:
703 return 32;
704 default:
705 throw new Exception('Unsupported XLSX encryption profile.');
706 }
707 }
708
709 private static function iv(string $salt, string $blockKey, string $algorithm): string
710 {
711 return substr(str_pad(self::hash($algorithm, $salt . $blockKey), self::BLOCK_SIZE, "\x36"), 0, self::BLOCK_SIZE);
712 }
713
714 private static function paddedLength(int $length): int
715 {
716 return (int) (ceil($length / self::BLOCK_SIZE) * self::BLOCK_SIZE);
717 }
718
719 private static function assertOpenSslAvailable(): void
720 {
721 if (!function_exists('openssl_encrypt') || !function_exists('openssl_decrypt')) {
722 throw new Exception('XLSX encryption requires the OpenSSL extension.');
723 }
724 }
725
726 private static function packSize(int $size): string
727 {
728 if ($size < 0) {
729 throw new Exception('XLSX package is too large to encrypt.');
730 }
731
732 return pack('V2', $size % 4294967296, intdiv($size, 4294967296));
733 }
734
735 private static function unpackSize(string $data): int
736 {
737 $unpackedSize = unpack('Vlow/Vhigh', $data);
738 if ($unpackedSize === false || !isset($unpackedSize['low'], $unpackedSize['high']) || !is_int($unpackedSize['low']) || !is_int($unpackedSize['high'])) {
739 throw new Exception('Malformed encrypted XLSX package.');
740 }
741
742 return self::sizeFromWords($unpackedSize['low'], $unpackedSize['high'], PHP_INT_SIZE, PHP_INT_MAX);
743 }
744
745 private static function sizeFromWords(int $low, int $high, int $integerSize, int $integerMax): int
746 {
747 if ($integerSize < 8) {
748 if ($high !== 0 || $low > $integerMax) {
749 throw new Exception('Encrypted XLSX package is too large for this platform.');
750 }
751
752 return $low;
753 }
754 if ($high > 0x7FFFFFFF) {
755 throw new Exception('Encrypted XLSX package is too large for this platform.');
756 }
757
758 return $low + $high * 4294967296;
759 }
760
761 /** @param resource $fileHandle */
762 private static function write($fileHandle, string $data): void
763 {
764 if (fwrite($fileHandle, $data) !== strlen($data)) {
765 throw new Exception('Could not write encrypted XLSX package.');
766 }
767 }
768
769 /** @param resource $fileHandle */
770 private static function read($fileHandle, int $length): string
771 {
772 if ($length < 1) {
773 throw new Exception('Malformed encrypted XLSX package.');
774 }
775 $data = fread($fileHandle, $length);
776 if ($data === false || strlen($data) !== $length) {
777 throw new Exception('Malformed encrypted XLSX package.');
778 }
779
780 return $data;
781 }
782
783 private static function encryptionInfoXml(string $keyDataSalt, string $passwordSalt, string $encryptedVerifier, string $encryptedVerifierHash, string $encryptedKey, string $encryptedHmacKey, string $encryptedHmacValue, int $keyBits = 256, string $hashAlgorithm = 'SHA512', int $hashSize = 64, int $spinCount = 100000): string
784 {
785 $base64 = static fn (string $value): string => base64_encode($value);
786
787 return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
788 . '<encryption xmlns="http://schemas.microsoft.com/office/2006/encryption" xmlns:p="http://schemas.microsoft.com/office/2006/keyEncryptor/password">'
789 . '<keyData saltSize="16" blockSize="16" keyBits="' . $keyBits . '" hashSize="' . $hashSize . '" cipherAlgorithm="AES" cipherChaining="ChainingModeCBC" hashAlgorithm="' . $hashAlgorithm . '" saltValue="' . $base64($keyDataSalt) . '"/>'
790 . '<dataIntegrity encryptedHmacKey="' . $base64($encryptedHmacKey) . '" encryptedHmacValue="' . $base64($encryptedHmacValue) . '"/>'
791 . '<keyEncryptors><keyEncryptor uri="http://schemas.microsoft.com/office/2006/keyEncryptor/password"><p:encryptedKey spinCount="' . $spinCount . '" saltSize="16" blockSize="16" keyBits="' . $keyBits . '" hashSize="' . $hashSize . '" cipherAlgorithm="AES" cipherChaining="ChainingModeCBC" hashAlgorithm="' . $hashAlgorithm . '" saltValue="' . $base64($passwordSalt) . '" encryptedVerifierHashInput="' . $base64($encryptedVerifier) . '" encryptedVerifierHashValue="' . $base64($encryptedVerifierHash) . '" encryptedKeyValue="' . $base64($encryptedKey) . '"/></keyEncryptor></keyEncryptors></encryption>';
792 }
793
794 private static function dataSpacesVersion(): string
795 {
796 if (!hex2bin('3c0000004d006900630072006f0073006f00660074002e0043006f006e007400610069006e00650072002e004400610074006100530070006100630065007300010000000100000001000000')) {
797 throw new Exception('Could not generate XLSX encryption DataSpaces stream.');
798 }
799 return hex2bin('3c0000004d006900630072006f0073006f00660074002e0043006f006e007400610069006e00650072002e004400610074006100530070006100630065007300010000000100000001000000');
800 }
801
802 private static function primaryTransform(): string
803 {
804 if (!hex2bin('58000000010000004c0000007b00460046003900410033004600300033002d0035003600450046002d0034003600310033002d0042004400440035002d003500410034003100430031004400300037003200340036007d004e0000004d006900630072006f0073006f00660074002e0043006f006e007400610069006e00650072002e0045006e006300720079007000740069006f006e005400720061006e00730066006f0072006d00000001000000010000000100000000000000000000000000000004000000')) {
805 throw new Exception('Could not generate XLSX encryption DataSpaces stream.');
806 }
807 return hex2bin('58000000010000004c0000007b00460046003900410033004600300033002d0035003600450046002d0034003600310033002d0042004400440035002d003500410034003100430031004400300037003200340036007d004e0000004d006900630072006f0073006f00660074002e0043006f006e007400610069006e00650072002e0045006e006300720079007000740069006f006e005400720061006e00730066006f0072006d00000001000000010000000100000000000000000000000000000004000000');
808 }
809
810 private static function dataSpaceMap(): string
811 {
812 if (!hex2bin('08000000010000006800000001000000000000002000000045006e0063007200790070007400650064005000610063006b00610067006500320000005300740072006f006e00670045006e006300720079007000740069006f006e004400610074006100530070006100630065000000')) {
813 throw new Exception('Could not generate XLSX encryption DataSpaces stream.');
814 }
815 return hex2bin('08000000010000006800000001000000000000002000000045006e0063007200790070007400650064005000610063006b00610067006500320000005300740072006f006e00670045006e006300720079007000740069006f006e004400610074006100530070006100630065000000');
816 }
817
818 private static function strongEncryptionDataSpace(): string
819 {
820 if (!hex2bin('0800000001000000320000005300740072006f006e00670045006e006300720079007000740069006f006e005400720061006e00730066006f0072006d000000')) {
821 throw new Exception('Could not generate XLSX encryption DataSpaces stream.');
822 }
823 return hex2bin('0800000001000000320000005300740072006f006e00670045006e006300720079007000740069006f006e005400720061006e00730066006f0072006d000000');
824 }
825
826 /** @param array{keyDataSalt: string, passwordSalt: string, encryptedVerifier: string, encryptedVerifierHash: string, encryptedKey: string, encryptedHmacKey: string, encryptedHmacValue: string, spinCount: int, keyBits: int, hashAlgorithm: string, hashSize: int} $info */
827 private static function verifyIntegrity(array $info, string $secretKey, string $encryptedPackage): void
828 {
829 $hmacKeyIv = self::iv($info['keyDataSalt'], self::BLOCK_KEY_HMAC_KEY, $info['hashAlgorithm']);
830 $hmacValueIv = self::iv($info['keyDataSalt'], self::BLOCK_KEY_HMAC_VALUE, $info['hashAlgorithm']);
831 $hmacKey = (string) substr(self::aesDecrypt($info['encryptedHmacKey'], $secretKey, $hmacKeyIv, $info['keyBits']), 0, $info['hashSize']);
832 $expected = self::aesDecrypt($info['encryptedHmacValue'], $secretKey, $hmacValueIv, $info['keyBits']);
833 if (!hash_equals(hash_hmac(self::HASH_ALGORITHMS[$info['hashAlgorithm']][0], $encryptedPackage, $hmacKey, true), (string) substr($expected, 0, $info['hashSize']))) {
834 throw new Exception('Encrypted XLSX package integrity check failed.');
835 }
836 }
837
838 /**
839 * @param array{keyDataSalt: string, passwordSalt: string, encryptedVerifier: string, encryptedVerifierHash: string, encryptedKey: string, encryptedHmacKey: string, encryptedHmacValue: string, spinCount: int, keyBits: int, hashAlgorithm: string, hashSize: int} $info
840 * @param resource $input
841 */
842 private static function verifyIntegrityFile(array $info, string $secretKey, $input): void
843 {
844 $hmacKeyIv = self::iv($info['keyDataSalt'], self::BLOCK_KEY_HMAC_KEY, $info['hashAlgorithm']);
845 $hmacValueIv = self::iv($info['keyDataSalt'], self::BLOCK_KEY_HMAC_VALUE, $info['hashAlgorithm']);
846 $hmacKey = (string) substr(self::aesDecrypt($info['encryptedHmacKey'], $secretKey, $hmacKeyIv, $info['keyBits']), 0, $info['hashSize']);
847 $expected = self::aesDecrypt($info['encryptedHmacValue'], $secretKey, $hmacValueIv, $info['keyBits']);
848 $hmac = hash_init(self::HASH_ALGORITHMS[$info['hashAlgorithm']][0], HASH_HMAC, $hmacKey);
849 while (!feof($input)) {
850 $data = fread($input, 8192);
851 if ($data === false) {
852 throw new Exception('Could not read encrypted XLSX package.');
853 }
854 if ($data !== '') {
855 hash_update($hmac, $data);
856 }
857 }
858 if (!hash_equals(hash_final($hmac, true), (string) substr($expected, 0, $info['hashSize']))) {
859 throw new Exception('Encrypted XLSX package integrity check failed.');
860 }
861 }
862 }
863