Skip to content

Commit b9a5966

Browse files
committed
Password and hash are exclusive
As specified in https://docs.microsoft.com/en-us/openspecs/office_standards/ms-xlsx/85f5567f-2599-41ad-ae26-8cfab23ce754 password and hashValue are exlusive and thus should be treated transparently with a single API in our model.
1 parent 1eaf40b commit b9a5966

File tree

6 files changed

+240
-192
lines changed

6 files changed

+240
-192
lines changed

docs/topics/recipes.md

Lines changed: 58 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -919,29 +919,53 @@ disallow inserting rows on a specific sheet, disallow sorting, ...
919919
- Cell: offers the option to lock/unlock a cell as well as show/hide
920920
the internal formula.
921921

922+
**Make sure you enable worksheet protection if you need any of the
923+
worksheet or cell protection features!** This can be done using the following
924+
code:
925+
926+
``` php
927+
$spreadsheet->getActiveSheet()->getProtection()->setSheet(true);
928+
```
929+
930+
### Document
931+
922932
An example on setting document security:
923933

924934
``` php
925-
$spreadsheet->getSecurity()->setLockWindows(true);
926-
$spreadsheet->getSecurity()->setLockStructure(true);
927-
$spreadsheet->getSecurity()->setWorkbookPassword("PhpSpreadsheet");
935+
$security = $spreadsheet->getSecurity();
936+
$security->setLockWindows(true);
937+
$security->setLockStructure(true);
938+
$security->setWorkbookPassword("PhpSpreadsheet");
928939
```
929940

941+
### Worksheet
942+
930943
An example on setting worksheet security:
931944

932945
``` php
933-
$spreadsheet->getActiveSheet()
934-
->getProtection()->setPassword('PhpSpreadsheet');
935-
$spreadsheet->getActiveSheet()
936-
->getProtection()->setSheet(true);
937-
$spreadsheet->getActiveSheet()
938-
->getProtection()->setSort(true);
939-
$spreadsheet->getActiveSheet()
940-
->getProtection()->setInsertRows(true);
941-
$spreadsheet->getActiveSheet()
942-
->getProtection()->setFormatCells(true);
946+
$protection = $spreadsheet->getActiveSheet()->getProtection();
947+
$protection->setPassword('PhpSpreadsheet');
948+
$protection->setSheet(true);
949+
$protection->setSort(true);
950+
$protection->setInsertRows(true);
951+
$protection->setFormatCells(true);
943952
```
944953

954+
If writing Xlsx files you can specify the algorithm used to hash the password
955+
before calling `setPassword()` like so:
956+
957+
```php
958+
$protection = $spreadsheet->getActiveSheet()->getProtection();
959+
$protection->setAlgorithm(Protection::ALGORITHM_SHA_512);
960+
$protection->setSpinCount(20000);
961+
$protection->setPassword('PhpSpreadsheet');
962+
```
963+
964+
The salt should **not** be set manually and will be automatically generated
965+
when setting a new password.
966+
967+
### Cell
968+
945969
An example on setting cell security:
946970

947971
``` php
@@ -950,14 +974,30 @@ $spreadsheet->getActiveSheet()->getStyle('B1')
950974
->setLocked(\PhpOffice\PhpSpreadsheet\Style\Protection::PROTECTION_UNPROTECTED);
951975
```
952976

953-
**Make sure you enable worksheet protection if you need any of the
954-
worksheet protection features!** This can be done using the following
955-
code:
977+
## Reading protected spreadsheet
956978

957-
``` php
958-
$spreadsheet->getActiveSheet()->getProtection()->setSheet(true);
979+
Spreadsheets that are protected the as described above can always be read by
980+
PhpSpreadsheet. There is no need to know the password or do anything special in
981+
order to read a protected file.
982+
983+
However if you need to implement a password verification mechanism, you can use the
984+
following helper method:
985+
986+
987+
```php
988+
$protection = $spreadsheet->getActiveSheet()->getProtection();
989+
$allowed = $protection->verify('my password');
990+
991+
if ($allowed) {
992+
doSomething();
993+
} else {
994+
throw new Exception('Incorrect password');
995+
}
959996
```
960997

998+
If you need to completely prevent reading a file by any tool, including PhpSpreadsheet,
999+
then you are looking for "encryption", not "protection".
1000+
9611001
## Setting data validation on a cell
9621002

9631003
Data validation is a powerful feature of Xlsx. It allows to specify an

src/PhpSpreadsheet/Reader/Xlsx.php

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -763,17 +763,8 @@ public function load($pFilename)
763763
}
764764
}
765765

766-
if (!$this->readDataOnly && $xmlSheet && $xmlSheet->sheetProtection) {
767-
$docSheet->getProtection()->setPassword((string) $xmlSheet->sheetProtection['password'], true);
768-
$docSheet->getProtection()->setAlgorithmName((string) $xmlSheet->sheetProtection['algorithmName']);
769-
$docSheet->getProtection()->setHashValue((string) $xmlSheet->sheetProtection['hashValue']);
770-
$docSheet->getProtection()->setSaltValue((string) $xmlSheet->sheetProtection['saltValue']);
771-
$docSheet->getProtection()->setSpinCount((int) $xmlSheet->sheetProtection['spinCount']);
772-
if ($xmlSheet->protectedRanges->protectedRange) {
773-
foreach ($xmlSheet->protectedRanges->protectedRange as $protectedRange) {
774-
$docSheet->protectCells((string) $protectedRange['sqref'], (string) $protectedRange['password'], true);
775-
}
776-
}
766+
if ($xmlSheet) {
767+
$this->readSheetProtection($docSheet, $xmlSheet);
777768
}
778769

779770
if ($xmlSheet && $xmlSheet->autoFilter && !$this->readDataOnly) {
@@ -2035,4 +2026,29 @@ private function getWorkbookBaseName(ZipArchive $zip)
20352026

20362027
return $workbookBasename;
20372028
}
2029+
2030+
private function readSheetProtection(Worksheet $docSheet, SimpleXMLElement $xmlSheet): void
2031+
{
2032+
if ($this->readDataOnly || !$xmlSheet->sheetProtection) {
2033+
return;
2034+
}
2035+
2036+
$algorithmName = (string) $xmlSheet->sheetProtection['algorithmName'];
2037+
$protection = $docSheet->getProtection();
2038+
$protection->setAlgorithm($algorithmName);
2039+
2040+
if ($algorithmName) {
2041+
$protection->setPassword((string) $xmlSheet->sheetProtection['hashValue'], true);
2042+
$protection->setSalt((string) $xmlSheet->sheetProtection['saltValue']);
2043+
$protection->setSpinCount((int) $xmlSheet->sheetProtection['spinCount']);
2044+
} else {
2045+
$protection->setPassword((string) $xmlSheet->sheetProtection['password'], true);
2046+
}
2047+
2048+
if ($xmlSheet->protectedRanges->protectedRange) {
2049+
foreach ($xmlSheet->protectedRanges->protectedRange as $protectedRange) {
2050+
$docSheet->protectCells((string) $protectedRange['sqref'], (string) $protectedRange['password'], true);
2051+
}
2052+
}
2053+
}
20382054
}

src/PhpSpreadsheet/Shared/PasswordHasher.php

Lines changed: 40 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -2,51 +2,39 @@
22

33
namespace PhpOffice\PhpSpreadsheet\Shared;
44

5+
use PhpOffice\PhpSpreadsheet\Exception;
6+
use PhpOffice\PhpSpreadsheet\Worksheet\Protection;
7+
58
class PasswordHasher
69
{
7-
const ALGORITHM_MD2 = 'MD2';
8-
const ALGORITHM_MD4 = 'MD4';
9-
const ALGORITHM_MD5 = 'MD5';
10-
const ALGORITHM_SHA_1 = 'SHA-1';
11-
const ALGORITHM_SHA_256 = 'SHA-256';
12-
const ALGORITHM_SHA_384 = 'SHA-384';
13-
const ALGORITHM_SHA_512 = 'SHA-512';
14-
const ALGORITHM_RIPEMD_128 = 'RIPEMD-128';
15-
const ALGORITHM_RIPEMD_160 = 'RIPEMD-160';
16-
const ALGORITHM_WHIRLPOOL = 'WHIRLPOOL';
17-
1810
/**
19-
* Mapping between algorithm name in Excel and algorithm name in PHP.
20-
*
21-
* @var array
11+
* Get algorithm name for PHP.
2212
*/
23-
private static $algorithmArray = [
24-
self::ALGORITHM_MD2 => 'md2',
25-
self::ALGORITHM_MD4 => 'md4',
26-
self::ALGORITHM_MD5 => 'md5',
27-
self::ALGORITHM_SHA_1 => 'sha1',
28-
self::ALGORITHM_SHA_256 => 'sha256',
29-
self::ALGORITHM_SHA_384 => 'sha384',
30-
self::ALGORITHM_SHA_512 => 'sha512',
31-
self::ALGORITHM_RIPEMD_128 => 'ripemd128',
32-
self::ALGORITHM_RIPEMD_160 => 'ripemd160',
33-
self::ALGORITHM_WHIRLPOOL => 'whirlpool',
34-
];
35-
36-
/**
37-
* Get algorithm from self::$algorithmArray.
38-
*
39-
* @param string $pAlgorithmName
40-
*
41-
* @return string
42-
*/
43-
private static function getAlgorithm($pAlgorithmName)
13+
private static function getAlgorithm(string $algorithmName): string
4414
{
45-
if (array_key_exists($pAlgorithmName, self::$algorithmArray)) {
46-
return self::$algorithmArray[$pAlgorithmName];
15+
if (!$algorithmName) {
16+
return '';
17+
}
18+
19+
// Mapping between algorithm name in Excel and algorithm name in PHP
20+
$mapping = [
21+
Protection::ALGORITHM_MD2 => 'md2',
22+
Protection::ALGORITHM_MD4 => 'md4',
23+
Protection::ALGORITHM_MD5 => 'md5',
24+
Protection::ALGORITHM_SHA_1 => 'sha1',
25+
Protection::ALGORITHM_SHA_256 => 'sha256',
26+
Protection::ALGORITHM_SHA_384 => 'sha384',
27+
Protection::ALGORITHM_SHA_512 => 'sha512',
28+
Protection::ALGORITHM_RIPEMD_128 => 'ripemd128',
29+
Protection::ALGORITHM_RIPEMD_160 => 'ripemd160',
30+
Protection::ALGORITHM_WHIRLPOOL => 'whirlpool',
31+
];
32+
33+
if (array_key_exists($algorithmName, $mapping)) {
34+
return $mapping[$algorithmName];
4735
}
4836

49-
return '';
37+
throw new Exception('Unsupported password algorithm: ' . $algorithmName);
5038
}
5139

5240
/**
@@ -57,10 +45,8 @@ private static function getAlgorithm($pAlgorithmName)
5745
* Spreadsheet_Excel_Writer by Xavier Noguer <[email protected]>.
5846
*
5947
* @param string $pPassword Password to hash
60-
*
61-
* @return string Hashed password
6248
*/
63-
public static function defaultHashPassword($pPassword)
49+
private static function defaultHashPassword(string $pPassword): string
6450
{
6551
$password = 0x0000;
6652
$charPos = 1; // char position
@@ -87,40 +73,28 @@ public static function defaultHashPassword($pPassword)
8773
*
8874
* @see https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-offcrypto/1357ea58-646e-4483-92ef-95d718079d6f
8975
*
90-
* @param string $pPassword Password to hash
91-
* @param string $pAlgorithmName Hash algorithm used to compute the password hash value
92-
* @param string $pSaltValue Pseudorandom string
93-
* @param string $pSpinCount Number of times to iterate on a hash of a password
76+
* @param string $password Password to hash
77+
* @param string $algorithm Hash algorithm used to compute the password hash value
78+
* @param string $salt Pseudorandom string
79+
* @param int $spinCount Number of times to iterate on a hash of a password
9480
*
9581
* @return string Hashed password
9682
*/
97-
public static function hashPassword($pPassword, $pAlgorithmName = '', $pSaltValue = '', $pSpinCount = 10000)
83+
public static function hashPassword(string $password, string $algorithm = '', string $salt = '', int $spinCount = 10000): string
9884
{
99-
$algorithmName = self::getAlgorithm($pAlgorithmName);
100-
if (!$pAlgorithmName) {
101-
return self::defaultHashPassword($pPassword);
85+
$phpAlgorithm = self::getAlgorithm($algorithm);
86+
if (!$phpAlgorithm) {
87+
return self::defaultHashPassword($password);
10288
}
10389

104-
$saltValue = base64_decode($pSaltValue);
105-
$password = mb_convert_encoding($pPassword, 'UCS-2LE', 'UTF-8');
90+
$saltValue = base64_decode($salt);
91+
$encodedPassword = mb_convert_encoding($password, 'UCS-2LE', 'UTF-8');
10692

107-
$hashValue = hash($algorithmName, $saltValue . $password, true);
108-
for ($i = 0; $i < $pSpinCount; ++$i) {
109-
$hashValue = hash($algorithmName, $hashValue . pack('L', $i), true);
93+
$hashValue = hash($phpAlgorithm, $saltValue . $encodedPassword, true);
94+
for ($i = 0; $i < $spinCount; ++$i) {
95+
$hashValue = hash($phpAlgorithm, $hashValue . pack('L', $i), true);
11096
}
11197

11298
return base64_encode($hashValue);
11399
}
114-
115-
/**
116-
* Create a pseudorandom string.
117-
*
118-
* @param int $pSize Length of the output string in bytes
119-
*
120-
* @return string Pseudorandom string
121-
*/
122-
public static function generateSalt($pSize = 16)
123-
{
124-
return base64_encode(random_bytes($pSize));
125-
}
126100
}

0 commit comments

Comments
 (0)