Skip to content

Commit dfa6f77

Browse files
reijnnnPowerKiKi
authored andcommitted
Add support protection of worksheet by a specific hash algorithm
1 parent c434e9b commit dfa6f77

File tree

6 files changed

+261
-1
lines changed

6 files changed

+261
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).
1111

1212
- Support writing to streams in all writers [#1292](https://github.com/PHPOffice/PhpSpreadsheet/issues/1292)
1313
- Support CSV files with data wrapping a lot of lines [#1468](https://github.com/PHPOffice/PhpSpreadsheet/pull/1468)
14+
- Support protection of worksheet by a specific hash algorithm
1415

1516
### Fixed
1617

src/PhpSpreadsheet/Reader/Xlsx.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -765,6 +765,10 @@ public function load($pFilename)
765765

766766
if (!$this->readDataOnly && $xmlSheet && $xmlSheet->sheetProtection) {
767767
$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']);
768772
if ($xmlSheet->protectedRanges->protectedRange) {
769773
foreach ($xmlSheet->protectedRanges->protectedRange as $protectedRange) {
770774
$docSheet->protectCells((string) $protectedRange['sqref'], (string) $protectedRange['password'], true);

src/PhpSpreadsheet/Shared/PasswordHasher.php

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,51 @@
44

55
class PasswordHasher
66
{
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+
18+
/**
19+
* Mapping between algorithm name in Excel and algorithm name in PHP.
20+
*
21+
* @var array
22+
*/
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)
44+
{
45+
if (array_key_exists($pAlgorithmName, self::$algorithmArray)) {
46+
return self::$algorithmArray[$pAlgorithmName];
47+
}
48+
49+
return '';
50+
}
51+
752
/**
853
* Create a password hash from a given string.
954
*
@@ -15,7 +60,7 @@ class PasswordHasher
1560
*
1661
* @return string Hashed password
1762
*/
18-
public static function hashPassword($pPassword)
63+
public static function defaultHashPassword($pPassword)
1964
{
2065
$password = 0x0000;
2166
$charPos = 1; // char position
@@ -34,4 +79,48 @@ public static function hashPassword($pPassword)
3479

3580
return strtoupper(dechex($password));
3681
}
82+
83+
/**
84+
* Create a password hash from a given string by a specific algorithm.
85+
*
86+
* 2.4.2.4 ISO Write Protection Method
87+
*
88+
* @see https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-offcrypto/1357ea58-646e-4483-92ef-95d718079d6f
89+
*
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
94+
*
95+
* @return string Hashed password
96+
*/
97+
public static function hashPassword($pPassword, $pAlgorithmName = '', $pSaltValue = '', $pSpinCount = 10000)
98+
{
99+
$algorithmName = self::getAlgorithm($pAlgorithmName);
100+
if (!$pAlgorithmName) {
101+
return self::defaultHashPassword($pPassword);
102+
}
103+
104+
$saltValue = base64_decode($pSaltValue);
105+
$password = mb_convert_encoding($pPassword, 'UCS-2LE', 'UTF-8');
106+
107+
$hashValue = hash($algorithmName, $saltValue . $password, true);
108+
for ($i = 0; $i < $pSpinCount; ++$i) {
109+
$hashValue = hash($algorithmName, $hashValue . pack('L', $i), true);
110+
}
111+
112+
return base64_encode($hashValue);
113+
}
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+
}
37126
}

src/PhpSpreadsheet/Worksheet/Protection.php

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,34 @@ class Protection
125125
*/
126126
private $password = '';
127127

128+
/**
129+
* Algorithm name.
130+
*
131+
* @var string
132+
*/
133+
private $algorithmName = '';
134+
135+
/**
136+
* Hash value.
137+
*
138+
* @var string
139+
*/
140+
private $hashValue = '';
141+
142+
/**
143+
* Salt value.
144+
*
145+
* @var string
146+
*/
147+
private $saltValue = '';
148+
149+
/**
150+
* Spin count.
151+
*
152+
* @var int
153+
*/
154+
private $spinCount = '';
155+
128156
/**
129157
* Create a new Protection.
130158
*/
@@ -569,6 +597,102 @@ public function setPassword($pValue, $pAlreadyHashed = false)
569597
return $this;
570598
}
571599

600+
/**
601+
* Get AlgorithmName.
602+
*
603+
* @return string
604+
*/
605+
public function getAlgorithmName()
606+
{
607+
return $this->algorithmName;
608+
}
609+
610+
/**
611+
* Set AlgorithmName.
612+
*
613+
* @param string $pValue
614+
*
615+
* @return $this
616+
*/
617+
public function setAlgorithmName($pValue)
618+
{
619+
$this->algorithmName = $pValue;
620+
621+
return $this;
622+
}
623+
624+
/**
625+
* Get HashValue.
626+
*
627+
* @return string
628+
*/
629+
public function getHashValue()
630+
{
631+
return $this->hashValue;
632+
}
633+
634+
/**
635+
* Set HashValue.
636+
*
637+
* @param string $pValue
638+
*
639+
* @return $this
640+
*/
641+
public function setHashValue($pValue)
642+
{
643+
$this->hashValue = $pValue;
644+
645+
return $this;
646+
}
647+
648+
/**
649+
* Get SaltValue.
650+
*
651+
* @return string
652+
*/
653+
public function getSaltValue()
654+
{
655+
return $this->saltValue;
656+
}
657+
658+
/**
659+
* Set SaltValue.
660+
*
661+
* @param string $pValue
662+
*
663+
* @return $this
664+
*/
665+
public function setSaltValue($pValue)
666+
{
667+
$this->saltValue = $pValue;
668+
669+
return $this;
670+
}
671+
672+
/**
673+
* Get SpinCount.
674+
*
675+
* @return int
676+
*/
677+
public function getSpinCount()
678+
{
679+
return $this->spinCount;
680+
}
681+
682+
/**
683+
* Set SpinCount.
684+
*
685+
* @param int $pValue
686+
*
687+
* @return $this
688+
*/
689+
public function setSpinCount($pValue)
690+
{
691+
$this->spinCount = $pValue;
692+
693+
return $this;
694+
}
695+
572696
/**
573697
* Implement PHP __clone to create a deep clone, not just a shallow copy.
574698
*/

src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,22 @@ private function writeSheetProtection(XMLWriter $objWriter, PhpspreadsheetWorksh
424424
$objWriter->writeAttribute('password', $pSheet->getProtection()->getPassword());
425425
}
426426

427+
if ($pSheet->getProtection()->getHashValue() !== '') {
428+
$objWriter->writeAttribute('hashValue', $pSheet->getProtection()->getHashValue());
429+
}
430+
431+
if ($pSheet->getProtection()->getAlgorithmName() !== '') {
432+
$objWriter->writeAttribute('algorithmName', $pSheet->getProtection()->getAlgorithmName());
433+
}
434+
435+
if ($pSheet->getProtection()->getSaltValue() !== '') {
436+
$objWriter->writeAttribute('saltValue', $pSheet->getProtection()->getSaltValue());
437+
}
438+
439+
if ($pSheet->getProtection()->getSpinCount() !== '') {
440+
$objWriter->writeAttribute('spinCount', $pSheet->getProtection()->getSpinCount());
441+
}
442+
427443
$objWriter->writeAttribute('sheet', ($pSheet->getProtection()->getSheet() ? 'true' : 'false'));
428444
$objWriter->writeAttribute('objects', ($pSheet->getProtection()->getObjects() ? 'true' : 'false'));
429445
$objWriter->writeAttribute('scenarios', ($pSheet->getProtection()->getScenarios() ? 'true' : 'false'));

tests/data/Shared/PasswordHashes.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,30 @@
2525
'CE4B',
2626
'',
2727
],
28+
[
29+
'O6EXRLpLEDNJDL/AzYtnnA4O4bY=',
30+
'',
31+
'SHA-1',
32+
],
33+
[
34+
'GYvlIMljDI1Czc4jfWrGaxU5pxl9n5Og0KUzyAfYxwk=',
35+
'PhpSpreadsheet',
36+
'SHA-256',
37+
'Php_salt',
38+
1000,
39+
],
40+
[
41+
'sSHdxQv9qgpkr4LDT0bYQxM9hOQJFRhJ4D752/NHQtDDR1EVy67NCEW9cPd6oWvCoBGd96MqKpuma1A7pN1nEA==',
42+
'Mark Baker',
43+
'SHA-512',
44+
'Mark_salt',
45+
10000,
46+
],
47+
[
48+
'r9KVLLCKIYOILvE2rcby+g==',
49+
'!+&=()~§±æþ',
50+
'MD5',
51+
'Symbols_salt',
52+
100000,
53+
],
2854
];

0 commit comments

Comments
 (0)