Skip to content

Commit 08a6e75

Browse files
committed
More Sophisticated Workbook Password Algorithms (Xlsx only)
Fix #4673. Our password hasher can handle different algorithms, but the workbook password (for maintaining the structure of the workbook, not for encrypting the entire workbook) currently supports only the single algorithm that was in place many years ago. Expand it, and Xlsx Reader and Writer, to be able to use, say, SHA-512, which is what Excel itself uses. The revisions password needs to be expanded in the same way as the workbook password. It is used for file sharing, but MS has deprecated it because it feels that modern technologies introduce better ways to accomplish what it was needed for. You'll need to look pretty hard to even find it in Excel - it's not, for example, on any of the ribbons. Nevertheless, the solution here is pretty much identical to the solution for the workbook password, so I am fixing it at the same time.
1 parent 0c459cb commit 08a6e75

File tree

6 files changed

+300
-29
lines changed

6 files changed

+300
-29
lines changed

.php-cs-fixer.dist.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
'method_chaining_indentation' => true,
8585
'modernize_strpos' => true,
8686
'modernize_types_casting' => true,
87+
'modifier_keywords' => ['elements' => ['property', 'method']], // not const
8788
'multiline_comment_opening_closing' => true,
8889
'multiline_whitespace_before_semicolons' => true,
8990
'native_constant_invocation' => false, // Micro optimization that look messy
@@ -236,7 +237,6 @@
236237
'types_spaces' => true,
237238
'unary_operator_spaces' => true,
238239
'use_arrow_functions' => true,
239-
'visibility_required' => ['elements' => ['property', 'method']], // not const
240240
'void_return' => true,
241241
'whitespace_after_comma_in_array' => true,
242242
'yoda_style' => false,

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
@@ -2201,23 +2201,79 @@ private function readProtection(Spreadsheet $excel, SimpleXMLElement $xmlWorkboo
22012201
return;
22022202
}
22032203

2204-
$excel->getSecurity()->setLockRevision(self::getLockValue($xmlWorkbook->workbookProtection, 'lockRevision'));
2205-
$excel->getSecurity()->setLockStructure(self::getLockValue($xmlWorkbook->workbookProtection, 'lockStructure'));
2206-
$excel->getSecurity()->setLockWindows(self::getLockValue($xmlWorkbook->workbookProtection, 'lockWindows'));
2204+
$security = $excel->getSecurity();
2205+
$security->setLockRevision(
2206+
self::getLockValue($xmlWorkbook->workbookProtection, 'lockRevision')
2207+
);
2208+
$security->setLockStructure(
2209+
self::getLockValue($xmlWorkbook->workbookProtection, 'lockStructure')
2210+
);
2211+
$security->setLockWindows(
2212+
self::getLockValue($xmlWorkbook->workbookProtection, 'lockWindows')
2213+
);
22072214

22082215
if ($xmlWorkbook->workbookProtection['revisionsPassword']) {
2209-
$excel->getSecurity()->setRevisionsPassword(
2216+
$security->setRevisionsPassword(
22102217
(string) $xmlWorkbook->workbookProtection['revisionsPassword'],
22112218
true
22122219
);
22132220
}
2221+
if ($xmlWorkbook->workbookProtection['revisionsAlgorithmName']) {
2222+
$security->setRevisionsAlgorithmName(
2223+
(string) $xmlWorkbook->workbookProtection['revisionsAlgorithmName']
2224+
);
2225+
}
2226+
if ($xmlWorkbook->workbookProtection['revisionsSaltValue']) {
2227+
$security->setRevisionsSaltValue(
2228+
(string) $xmlWorkbook->workbookProtection['revisionsSaltValue'],
2229+
false
2230+
);
2231+
}
2232+
if ($xmlWorkbook->workbookProtection['revisionsSpinCount']) {
2233+
$security->setRevisionsSpinCount(
2234+
(int) $xmlWorkbook->workbookProtection['revisionsSpinCount']
2235+
);
2236+
}
2237+
if ($xmlWorkbook->workbookProtection['revisionsHashValue']) {
2238+
if ($security->advancedRevisionsPassword()) {
2239+
$security->setRevisionsPassword(
2240+
(string) $xmlWorkbook->workbookProtection['revisionsHashValue'],
2241+
true
2242+
);
2243+
}
2244+
}
22142245

22152246
if ($xmlWorkbook->workbookProtection['workbookPassword']) {
2216-
$excel->getSecurity()->setWorkbookPassword(
2247+
$security->setWorkbookPassword(
22172248
(string) $xmlWorkbook->workbookProtection['workbookPassword'],
22182249
true
22192250
);
22202251
}
2252+
2253+
if ($xmlWorkbook->workbookProtection['workbookAlgorithmName']) {
2254+
$security->setWorkbookAlgorithmName(
2255+
(string) $xmlWorkbook->workbookProtection['workbookAlgorithmName']
2256+
);
2257+
}
2258+
if ($xmlWorkbook->workbookProtection['workbookSaltValue']) {
2259+
$security->setWorkbookSaltValue(
2260+
(string) $xmlWorkbook->workbookProtection['workbookSaltValue'],
2261+
false
2262+
);
2263+
}
2264+
if ($xmlWorkbook->workbookProtection['workbookSpinCount']) {
2265+
$security->setWorkbookSpinCount(
2266+
(int) $xmlWorkbook->workbookProtection['workbookSpinCount']
2267+
);
2268+
}
2269+
if ($xmlWorkbook->workbookProtection['workbookHashValue']) {
2270+
if ($security->advancedPassword()) {
2271+
$security->setWorkbookPassword(
2272+
(string) $xmlWorkbook->workbookProtection['workbookHashValue'],
2273+
true
2274+
);
2275+
}
2276+
}
22212277
}
22222278

22232279
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)