Skip to content

Commit b100d21

Browse files
authored
Merge pull request #4675 from oleibman/issue4673
More Sophisticated Workbook Password Algorithms (Xlsx only)
2 parents b5ba4ff + 12ac952 commit b100d21

File tree

7 files changed

+300
-29
lines changed

7 files changed

+300
-29
lines changed

.php-cs-fixer.dist.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,6 @@
8585
'modernize_strpos' => true,
8686
'modernize_types_casting' => true,
8787
'modifier_keywords' => ['elements' => ['property', 'method']], // not const
88-
8988
'multiline_comment_opening_closing' => true,
9089
'multiline_whitespace_before_semicolons' => true,
9190
'native_constant_invocation' => false, // Micro optimization that look messy

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). Thia is a
4343
- Missing array keys x,o,v for Xml Reader. [Issue #4668](https://github.com/PHPOffice/PhpSpreadsheet/issues/4668) [PR #4669](https://github.com/PHPOffice/PhpSpreadsheet/pull/4669)
4444
- Missing array key x for Xlsx Reader VML. [Issue #4505](https://github.com/PHPOffice/PhpSpreadsheet/issues/4505) [PR #4676](https://github.com/PHPOffice/PhpSpreadsheet/pull/4676)
4545
- Better support for Style Alignment Read Order. [Issue #850](https://github.com/PHPOffice/PhpSpreadsheet/issues/850) [PR #4655](https://github.com/PHPOffice/PhpSpreadsheet/pull/4655)
46+
- More sophisticated workbook password algorithms (Xlsx only). [Issue #4673](https://github.com/PHPOffice/PhpSpreadsheet/issues/4673) [PR #4675](https://github.com/PHPOffice/PhpSpreadsheet/pull/4675)
4647

4748
## 2025-09-03 - 5.1.0
4849

src/PhpSpreadsheet/Document/Security.php

Lines changed: 129 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,21 @@ class Security
3131
*/
3232
private string $workbookPassword = '';
3333

34-
/**
35-
* Create a new Document Security instance.
36-
*/
37-
public function __construct()
38-
{
39-
}
34+
private string $workbookAlgorithmName = '';
35+
36+
private string $workbookHashValue = '';
37+
38+
private string $workbookSaltValue = '';
39+
40+
private int $workbookSpinCount = 0;
41+
42+
private string $revisionsAlgorithmName = '';
43+
44+
private string $revisionsHashValue = '';
45+
46+
private string $revisionsSaltValue = '';
47+
48+
private int $revisionsSpinCount = 0;
4049

4150
/**
4251
* Is some sort of document security enabled?
@@ -105,10 +114,18 @@ public function getRevisionsPassword(): string
105114
public function setRevisionsPassword(?string $password, bool $alreadyHashed = false): static
106115
{
107116
if ($password !== null) {
108-
if (!$alreadyHashed) {
109-
$password = PasswordHasher::hashPassword($password);
117+
if ($this->advancedRevisionsPassword()) {
118+
if (!$alreadyHashed) {
119+
$password = PasswordHasher::hashPassword($password, $this->revisionsAlgorithmName, $this->revisionsSaltValue, $this->revisionsSpinCount);
120+
}
121+
$this->revisionsHashValue = $password;
122+
$this->revisionsPassword = '';
123+
} else {
124+
if (!$alreadyHashed) {
125+
$password = PasswordHasher::hashPassword($password);
126+
}
127+
$this->revisionsPassword = $password;
110128
}
111-
$this->revisionsPassword = $password;
112129
}
113130

114131
return $this;
@@ -129,12 +146,112 @@ public function getWorkbookPassword(): string
129146
public function setWorkbookPassword(?string $password, bool $alreadyHashed = false): static
130147
{
131148
if ($password !== null) {
132-
if (!$alreadyHashed) {
133-
$password = PasswordHasher::hashPassword($password);
149+
if ($this->advancedPassword()) {
150+
if (!$alreadyHashed) {
151+
$password = PasswordHasher::hashPassword($password, $this->workbookAlgorithmName, $this->workbookSaltValue, $this->workbookSpinCount);
152+
}
153+
$this->workbookHashValue = $password;
154+
$this->workbookPassword = '';
155+
} else {
156+
if (!$alreadyHashed) {
157+
$password = PasswordHasher::hashPassword($password);
158+
}
159+
$this->workbookPassword = $password;
134160
}
135-
$this->workbookPassword = $password;
136161
}
137162

138163
return $this;
139164
}
165+
166+
public function getWorkbookHashValue(): string
167+
{
168+
return $this->advancedPassword() ? $this->workbookHashValue : '';
169+
}
170+
171+
public function advancedPassword(): bool
172+
{
173+
return $this->workbookAlgorithmName !== '' && $this->workbookSaltValue !== '' && $this->workbookSpinCount > 0;
174+
}
175+
176+
public function getWorkbookAlgorithmName(): string
177+
{
178+
return $this->workbookAlgorithmName;
179+
}
180+
181+
public function setWorkbookAlgorithmName(string $workbookAlgorithmName): static
182+
{
183+
$this->workbookAlgorithmName = $workbookAlgorithmName;
184+
185+
return $this;
186+
}
187+
188+
public function getWorkbookSpinCount(): int
189+
{
190+
return $this->workbookSpinCount;
191+
}
192+
193+
public function setWorkbookSpinCount(int $workbookSpinCount): static
194+
{
195+
$this->workbookSpinCount = $workbookSpinCount;
196+
197+
return $this;
198+
}
199+
200+
public function getWorkbookSaltValue(): string
201+
{
202+
return $this->workbookSaltValue;
203+
}
204+
205+
public function setWorkbookSaltValue(string $workbookSaltValue, bool $base64Required): static
206+
{
207+
$this->workbookSaltValue = $base64Required ? base64_encode($workbookSaltValue) : $workbookSaltValue;
208+
209+
return $this;
210+
}
211+
212+
public function getRevisionsHashValue(): string
213+
{
214+
return $this->advancedRevisionsPassword() ? $this->revisionsHashValue : '';
215+
}
216+
217+
public function advancedRevisionsPassword(): bool
218+
{
219+
return $this->revisionsAlgorithmName !== '' && $this->revisionsSaltValue !== '' && $this->revisionsSpinCount > 0;
220+
}
221+
222+
public function getRevisionsAlgorithmName(): string
223+
{
224+
return $this->revisionsAlgorithmName;
225+
}
226+
227+
public function setRevisionsAlgorithmName(string $revisionsAlgorithmName): static
228+
{
229+
$this->revisionsAlgorithmName = $revisionsAlgorithmName;
230+
231+
return $this;
232+
}
233+
234+
public function getRevisionsSpinCount(): int
235+
{
236+
return $this->revisionsSpinCount;
237+
}
238+
239+
public function setRevisionsSpinCount(int $revisionsSpinCount): static
240+
{
241+
$this->revisionsSpinCount = $revisionsSpinCount;
242+
243+
return $this;
244+
}
245+
246+
public function getRevisionsSaltValue(): string
247+
{
248+
return $this->revisionsSaltValue;
249+
}
250+
251+
public function setRevisionsSaltValue(string $revisionsSaltValue, bool $base64Required): static
252+
{
253+
$this->revisionsSaltValue = $base64Required ? base64_encode($revisionsSaltValue) : $revisionsSaltValue;
254+
255+
return $this;
256+
}
140257
}

src/PhpSpreadsheet/Reader/Xlsx.php

Lines changed: 61 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2205,23 +2205,79 @@ private function readProtection(Spreadsheet $excel, SimpleXMLElement $xmlWorkboo
22052205
return;
22062206
}
22072207

2208-
$excel->getSecurity()->setLockRevision(self::getLockValue($xmlWorkbook->workbookProtection, 'lockRevision'));
2209-
$excel->getSecurity()->setLockStructure(self::getLockValue($xmlWorkbook->workbookProtection, 'lockStructure'));
2210-
$excel->getSecurity()->setLockWindows(self::getLockValue($xmlWorkbook->workbookProtection, 'lockWindows'));
2208+
$security = $excel->getSecurity();
2209+
$security->setLockRevision(
2210+
self::getLockValue($xmlWorkbook->workbookProtection, 'lockRevision')
2211+
);
2212+
$security->setLockStructure(
2213+
self::getLockValue($xmlWorkbook->workbookProtection, 'lockStructure')
2214+
);
2215+
$security->setLockWindows(
2216+
self::getLockValue($xmlWorkbook->workbookProtection, 'lockWindows')
2217+
);
22112218

22122219
if ($xmlWorkbook->workbookProtection['revisionsPassword']) {
2213-
$excel->getSecurity()->setRevisionsPassword(
2220+
$security->setRevisionsPassword(
22142221
(string) $xmlWorkbook->workbookProtection['revisionsPassword'],
22152222
true
22162223
);
22172224
}
2225+
if ($xmlWorkbook->workbookProtection['revisionsAlgorithmName']) {
2226+
$security->setRevisionsAlgorithmName(
2227+
(string) $xmlWorkbook->workbookProtection['revisionsAlgorithmName']
2228+
);
2229+
}
2230+
if ($xmlWorkbook->workbookProtection['revisionsSaltValue']) {
2231+
$security->setRevisionsSaltValue(
2232+
(string) $xmlWorkbook->workbookProtection['revisionsSaltValue'],
2233+
false
2234+
);
2235+
}
2236+
if ($xmlWorkbook->workbookProtection['revisionsSpinCount']) {
2237+
$security->setRevisionsSpinCount(
2238+
(int) $xmlWorkbook->workbookProtection['revisionsSpinCount']
2239+
);
2240+
}
2241+
if ($xmlWorkbook->workbookProtection['revisionsHashValue']) {
2242+
if ($security->advancedRevisionsPassword()) {
2243+
$security->setRevisionsPassword(
2244+
(string) $xmlWorkbook->workbookProtection['revisionsHashValue'],
2245+
true
2246+
);
2247+
}
2248+
}
22182249

22192250
if ($xmlWorkbook->workbookProtection['workbookPassword']) {
2220-
$excel->getSecurity()->setWorkbookPassword(
2251+
$security->setWorkbookPassword(
22212252
(string) $xmlWorkbook->workbookProtection['workbookPassword'],
22222253
true
22232254
);
22242255
}
2256+
2257+
if ($xmlWorkbook->workbookProtection['workbookAlgorithmName']) {
2258+
$security->setWorkbookAlgorithmName(
2259+
(string) $xmlWorkbook->workbookProtection['workbookAlgorithmName']
2260+
);
2261+
}
2262+
if ($xmlWorkbook->workbookProtection['workbookSaltValue']) {
2263+
$security->setWorkbookSaltValue(
2264+
(string) $xmlWorkbook->workbookProtection['workbookSaltValue'],
2265+
false
2266+
);
2267+
}
2268+
if ($xmlWorkbook->workbookProtection['workbookSpinCount']) {
2269+
$security->setWorkbookSpinCount(
2270+
(int) $xmlWorkbook->workbookProtection['workbookSpinCount']
2271+
);
2272+
}
2273+
if ($xmlWorkbook->workbookProtection['workbookHashValue']) {
2274+
if ($security->advancedPassword()) {
2275+
$security->setWorkbookPassword(
2276+
(string) $xmlWorkbook->workbookProtection['workbookHashValue'],
2277+
true
2278+
);
2279+
}
2280+
}
22252281
}
22262282

22272283
private static function getLockValue(SimpleXMLElement $protection, string $key): ?bool

src/PhpSpreadsheet/Shared/PasswordHasher.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ private static function defaultHashPassword(string $password): string
7878
*
7979
* @param string $password Password to hash
8080
* @param string $algorithm Hash algorithm used to compute the password hash value
81-
* @param string $salt Pseudorandom string
81+
* @param string $salt Pseudorandom base64-encoded string
8282
* @param int $spinCount Number of times to iterate on a hash of a password
8383
*
8484
* @return string Hashed password

src/PhpSpreadsheet/Writer/Xlsx/Workbook.php

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -125,18 +125,35 @@ private function writeBookViews(XMLWriter $objWriter, Spreadsheet $spreadsheet):
125125
*/
126126
private function writeWorkbookProtection(XMLWriter $objWriter, Spreadsheet $spreadsheet): void
127127
{
128-
if ($spreadsheet->getSecurity()->isSecurityEnabled()) {
128+
$security = $spreadsheet->getSecurity();
129+
if ($security->isSecurityEnabled()) {
129130
$objWriter->startElement('workbookProtection');
130-
$objWriter->writeAttribute('lockRevision', ($spreadsheet->getSecurity()->getLockRevision() ? 'true' : 'false'));
131-
$objWriter->writeAttribute('lockStructure', ($spreadsheet->getSecurity()->getLockStructure() ? 'true' : 'false'));
132-
$objWriter->writeAttribute('lockWindows', ($spreadsheet->getSecurity()->getLockWindows() ? 'true' : 'false'));
133-
134-
if ($spreadsheet->getSecurity()->getRevisionsPassword() != '') {
135-
$objWriter->writeAttribute('revisionsPassword', $spreadsheet->getSecurity()->getRevisionsPassword());
131+
$objWriter->writeAttribute('lockRevision', ($security->getLockRevision() ? 'true' : 'false'));
132+
$objWriter->writeAttribute('lockStructure', ($security->getLockStructure() ? 'true' : 'false'));
133+
$objWriter->writeAttribute('lockWindows', ($security->getLockWindows() ? 'true' : 'false'));
134+
135+
if ($security->getRevisionsPassword() !== '') {
136+
$objWriter->writeAttribute('revisionsPassword', $security->getRevisionsPassword());
137+
} else {
138+
$hashValue = $security->getRevisionsHashValue();
139+
if ($hashValue !== '') {
140+
$objWriter->writeAttribute('revisionsAlgorithmName', $security->getRevisionsAlgorithmName());
141+
$objWriter->writeAttribute('revisionsHashValue', $hashValue);
142+
$objWriter->writeAttribute('revisionsSaltValue', $security->getRevisionsSaltValue());
143+
$objWriter->writeAttribute('revisionsSpinCount', (string) $security->getRevisionsSpinCount());
144+
}
136145
}
137146

138-
if ($spreadsheet->getSecurity()->getWorkbookPassword() != '') {
139-
$objWriter->writeAttribute('workbookPassword', $spreadsheet->getSecurity()->getWorkbookPassword());
147+
if ($security->getWorkbookPassword() !== '') {
148+
$objWriter->writeAttribute('workbookPassword', $security->getWorkbookPassword());
149+
} else {
150+
$hashValue = $security->getWorkbookHashValue();
151+
if ($hashValue !== '') {
152+
$objWriter->writeAttribute('workbookAlgorithmName', $security->getWorkbookAlgorithmName());
153+
$objWriter->writeAttribute('workbookHashValue', $hashValue);
154+
$objWriter->writeAttribute('workbookSaltValue', $security->getWorkbookSaltValue());
155+
$objWriter->writeAttribute('workbookSpinCount', (string) $security->getWorkbookSpinCount());
156+
}
140157
}
141158

142159
$objWriter->endElement();

0 commit comments

Comments
 (0)