Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Worksheet Passwords #2197

Merged
merged 5 commits into from
Jul 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 0 additions & 30 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -4020,11 +4020,6 @@ parameters:
count: 1
path: src/PhpSpreadsheet/Shared/OLERead.php

-
message: "#^Argument of an invalid type array\\<int, string\\>\\|false supplied for foreach, only iterables are supported\\.$#"
count: 1
path: src/PhpSpreadsheet/Shared/PasswordHasher.php

-
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\StringHelper\\:\\:sanitizeUTF8\\(\\) should return string but returns string\\|false\\.$#"
count: 1
Expand Down Expand Up @@ -6635,11 +6630,6 @@ parameters:
count: 1
path: tests/PhpSpreadsheetTests/Worksheet/RowCellIterator2Test.php

-
message: "#^Parameter \\#1 \\$options of static method PhpOffice\\\\PhpSpreadsheet\\\\Settings\\:\\:setLibXmlLoaderOptions\\(\\) expects int, null given\\.$#"
count: 1
path: tests/PhpSpreadsheetTests/Worksheet/WorksheetNamedRangesTest.php

-
message: "#^Property PhpOffice\\\\PhpSpreadsheetTests\\\\Writer\\\\Csv\\\\CsvEnclosureTest\\:\\:\\$cellValues has no typehint specified\\.$#"
count: 1
Expand Down Expand Up @@ -6680,11 +6670,6 @@ parameters:
count: 3
path: tests/PhpSpreadsheetTests/Writer/Html/HtmlCommentsTest.php

-
message: "#^Parameter \\#1 \\$options of static method PhpOffice\\\\PhpSpreadsheet\\\\Settings\\:\\:setLibXmlLoaderOptions\\(\\) expects int, null given\\.$#"
count: 1
path: tests/PhpSpreadsheetTests/Writer/Xlsx/FloatsRetainedTest.php

-
message: "#^Parameter \\#2 \\$locale of function setlocale expects string\\|null, string\\|false given\\.$#"
count: 1
Expand All @@ -6695,16 +6680,6 @@ parameters:
count: 1
path: tests/PhpSpreadsheetTests/Writer/Xlsx/LocaleFloatsTest.php

-
message: "#^Parameter \\#1 \\$options of static method PhpOffice\\\\PhpSpreadsheet\\\\Settings\\:\\:setLibXmlLoaderOptions\\(\\) expects int, null given\\.$#"
count: 2
path: tests/PhpSpreadsheetTests/Writer/Xlsx/StartsWithHashTest.php

-
message: "#^Parameter \\#1 \\$options of static method PhpOffice\\\\PhpSpreadsheet\\\\Settings\\:\\:setLibXmlLoaderOptions\\(\\) expects int, null given\\.$#"
count: 2
path: tests/PhpSpreadsheetTests/Writer/Xlsx/UnparsedDataCloneTest.php

-
message: "#^Cannot call method getDrawingCollection\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Worksheet\\|null\\.$#"
count: 4
Expand All @@ -6715,11 +6690,6 @@ parameters:
count: 4
path: tests/PhpSpreadsheetTests/Writer/Xlsx/UnparsedDataCloneTest.php

-
message: "#^Parameter \\#1 \\$options of static method PhpOffice\\\\PhpSpreadsheet\\\\Settings\\:\\:setLibXmlLoaderOptions\\(\\) expects int, null given\\.$#"
count: 1
path: tests/PhpSpreadsheetTests/Writer/Xlsx/UnparsedDataTest.php

-
message: "#^Parameter \\#1 \\$data of function simplexml_load_string expects string, string\\|false given\\.$#"
count: 2
Expand Down
43 changes: 26 additions & 17 deletions src/PhpSpreadsheet/Shared/PasswordHasher.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

namespace PhpOffice\PhpSpreadsheet\Shared;

use PhpOffice\PhpSpreadsheet\Exception;
use PhpOffice\PhpSpreadsheet\Exception as SpException;
use PhpOffice\PhpSpreadsheet\Worksheet\Protection;

class PasswordHasher
{
const MAX_PASSWORD_LENGTH = 255;

/**
* Get algorithm name for PHP.
*/
Expand Down Expand Up @@ -34,36 +36,40 @@ private static function getAlgorithm(string $algorithmName): string
return $mapping[$algorithmName];
}

throw new Exception('Unsupported password algorithm: ' . $algorithmName);
throw new SpException('Unsupported password algorithm: ' . $algorithmName);
}

/**
* Create a password hash from a given string.
*
* This method is based on the algorithm provided by
* This method is based on the spec at:
* https://interoperability.blob.core.windows.net/files/MS-OFFCRYPTO/[MS-OFFCRYPTO].pdf
* 2.3.7.1 Binary Document Password Verifier Derivation Method 1
*
* It replaces a method based on the algorithm provided by
* Daniel Rentz of OpenOffice and the PEAR package
* Spreadsheet_Excel_Writer by Xavier Noguer <[email protected]>.
*
* Scrutinizer will squawk at the use of bitwise operations here,
* but it should ultimately pass.
*
* @param string $pPassword Password to hash
*/
private static function defaultHashPassword(string $pPassword): string
{
$password = 0x0000;
$charPos = 1; // char position

// split the plain text password in its component characters
$chars = preg_split('//', $pPassword, -1, PREG_SPLIT_NO_EMPTY);
foreach ($chars as $char) {
$value = ord($char) << $charPos++; // shifted ASCII value
$rotated_bits = $value >> 15; // rotated bits beyond bit 15
$value &= 0x7fff; // first 15 bits
$password ^= ($value | $rotated_bits);
$verifier = 0;
$pwlen = strlen($pPassword);
$passwordArray = pack('c', $pwlen) . $pPassword;
for ($i = $pwlen; $i >= 0; --$i) {
$intermediate1 = (($verifier & 0x4000) === 0) ? 0 : 1;
$intermediate2 = 2 * $verifier;
$intermediate2 = $intermediate2 & 0x7fff;
$intermediate3 = $intermediate1 | $intermediate2;
$verifier = $intermediate3 ^ ord($passwordArray[$i]);
}
$verifier ^= 0xCE4B;

$password ^= strlen($pPassword);
$password ^= 0xCE4B;

return strtoupper(dechex($password));
return strtoupper(dechex($verifier));
}

/**
Expand All @@ -82,6 +88,9 @@ private static function defaultHashPassword(string $pPassword): string
*/
public static function hashPassword(string $password, string $algorithm = '', string $salt = '', int $spinCount = 10000): string
{
if (strlen($password) > self::MAX_PASSWORD_LENGTH) {
throw new SpException('Password exceeds ' . self::MAX_PASSWORD_LENGTH . ' characters');
}
$phpAlgorithm = self::getAlgorithm($algorithm);
if (!$phpAlgorithm) {
return self::defaultHashPassword($password);
Expand Down
2 changes: 1 addition & 1 deletion src/PhpSpreadsheet/Writer/Xls/Worksheet.php
Original file line number Diff line number Diff line change
Expand Up @@ -2138,7 +2138,7 @@ private function writeObjectProtect(): void
private function writePassword(): void
{
// Exit unless sheet protection and password have been specified
if (!$this->phpSheet->getProtection()->getSheet() || !$this->phpSheet->getProtection()->getPassword()) {
if (!$this->phpSheet->getProtection()->getSheet() || !$this->phpSheet->getProtection()->getPassword() || $this->phpSheet->getProtection()->getAlgorithm() !== '') {
return;
}

Expand Down
2 changes: 2 additions & 0 deletions tests/PhpSpreadsheetTests/SettingsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,14 @@ public function testGetXMLSettings(): void

public function testSetXMLSettings(): void
{
$original = Settings::getLibXmlLoaderOptions();
Settings::setLibXmlLoaderOptions(LIBXML_DTDLOAD | LIBXML_DTDATTR | LIBXML_DTDVALID);
$result = Settings::getLibXmlLoaderOptions();
self::assertTrue((bool) ((LIBXML_DTDLOAD | LIBXML_DTDATTR | LIBXML_DTDVALID) & $result));
// php 8.+ deprecated libxml_disable_entity_loader() - It's on by default
if (\PHP_VERSION_ID < 80000) {
self::assertFalse(libxml_disable_entity_loader());
}
Settings::setLibXmlLoaderOptions($original);
}
}
4 changes: 4 additions & 0 deletions tests/PhpSpreadsheetTests/Shared/PasswordHasherTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace PhpOffice\PhpSpreadsheetTests\Shared;

use PhpOffice\PhpSpreadsheet\Exception as SpException;
use PhpOffice\PhpSpreadsheet\Shared\PasswordHasher;
use PHPUnit\Framework\TestCase;

Expand All @@ -14,6 +15,9 @@ class PasswordHasherTest extends TestCase
*/
public function testHashPassword($expectedResult, ...$args): void
{
if ($expectedResult === 'exception') {
$this->expectException(SpException::class);
}
$result = PasswordHasher::hashPassword(...$args);
self::assertEquals($expectedResult, $result);
}
Expand Down
51 changes: 51 additions & 0 deletions tests/PhpSpreadsheetTests/Shared/PasswordReloadTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

namespace PhpOffice\PhpSpreadsheetTests\Shared;

use PhpOffice\PhpSpreadsheet\Shared\PasswordHasher;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheetTests\Functional\AbstractFunctional;

class PasswordReloadTest extends AbstractFunctional
{
/**
* @dataProvider providerPasswords
*/
public function testPasswordReload(string $format, string $algorithm, bool $supported = true): void
{
$password = 'hello';
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->getCell('A1')->setValue(1);
$protection = $sheet->getProtection();
$protection->setAlgorithm($algorithm);
$protection->setPassword($password);
$protection->setSheet(true);

$reloadedSpreadsheet = $this->writeAndReload($spreadsheet, $format);
$resheet = $reloadedSpreadsheet->getActiveSheet();
$reprot = $resheet->getProtection();
$repassword = $reprot->getPassword();
$hash = '';
if ($supported) {
$readAlgorithm = $reprot->getAlgorithm();
self::assertSame($algorithm, $readAlgorithm);
$salt = $reprot->getSalt();
$spin = $reprot->getSpinCount();
$hash = PasswordHasher::hashPassword($password, $readAlgorithm, $salt, $spin);
}
self::assertSame($repassword, $hash);
$spreadsheet->disconnectWorksheets();
$reloadedSpreadsheet->disconnectWorksheets();
}

public function providerPasswords(): array
{
return [
'Xls basic algorithm' => ['Xls', ''],
'Xls cannot use SHA512' => ['Xls', 'SHA-512', false],
'Xlsx basic algorithm' => ['Xlsx', ''],
'Xlsx can use SHA512' => ['Xlsx', 'SHA-512'],
];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

use PhpOffice\PhpSpreadsheet\Exception;
use PhpOffice\PhpSpreadsheet\Reader\Xlsx;
use PhpOffice\PhpSpreadsheet\Settings;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PHPUnit\Framework\TestCase;

Expand All @@ -17,8 +16,6 @@ class WorksheetNamedRangesTest extends TestCase

protected function setUp(): void
{
Settings::setLibXmlLoaderOptions(null); // reset to default options

$reader = new Xlsx();
$this->spreadsheet = $reader->load('tests/data/Worksheet/namedRangeTest.xlsx');
}
Expand Down
19 changes: 0 additions & 19 deletions tests/PhpSpreadsheetTests/Writer/Xlsx/ConditionalTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

namespace PhpOffice\PhpSpreadsheetTests\Writer\Xlsx;

use PhpOffice\PhpSpreadsheet\Settings;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Style\Conditional;
use PhpOffice\PhpSpreadsheet\Style\Fill;
Expand All @@ -11,24 +10,6 @@

class ConditionalTest extends AbstractFunctional
{
/**
* @var int
*/
private $prevValue;

protected function setUp(): void
{
$this->prevValue = Settings::getLibXmlLoaderOptions();

// Disable validating XML with the DTD
Settings::setLibXmlLoaderOptions($this->prevValue & ~LIBXML_DTDVALID & ~LIBXML_DTDATTR & ~LIBXML_DTDLOAD);
}

protected function tearDown(): void
{
Settings::setLibXmlLoaderOptions($this->prevValue);
}

/**
* Test check if conditional style with type 'notContainsText' works on xlsx.
*/
Expand Down
19 changes: 0 additions & 19 deletions tests/PhpSpreadsheetTests/Writer/Xlsx/DrawingsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,11 @@

use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Reader\Xlsx;
use PhpOffice\PhpSpreadsheet\Settings;
use PhpOffice\PhpSpreadsheet\Shared\File;
use PhpOffice\PhpSpreadsheetTests\Functional\AbstractFunctional;

class DrawingsTest extends AbstractFunctional
{
/**
* @var int
*/
private $prevValue;

protected function setUp(): void
{
$this->prevValue = Settings::getLibXmlLoaderOptions();

// Disable validating XML with the DTD
Settings::setLibXmlLoaderOptions($this->prevValue & ~LIBXML_DTDVALID & ~LIBXML_DTDATTR & ~LIBXML_DTDLOAD);
}

protected function tearDown(): void
{
Settings::setLibXmlLoaderOptions($this->prevValue);
}

/**
* Test save and load XLSX file with drawing on 2nd worksheet.
*/
Expand Down
2 changes: 0 additions & 2 deletions tests/PhpSpreadsheetTests/Writer/Xlsx/FloatsRetainedTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
namespace PhpOffice\PhpSpreadsheetTests\Writer\Xlsx;

use PhpOffice\PhpSpreadsheet\Reader\Xlsx as Reader;
use PhpOffice\PhpSpreadsheet\Settings;
use PhpOffice\PhpSpreadsheet\Shared\File;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx as Writer;
Expand All @@ -19,7 +18,6 @@ class FloatsRetainedTest extends TestCase
public function testIntyFloatsRetainedByWriter($value): void
{
$outputFilename = File::temporaryFilename();
Settings::setLibXmlLoaderOptions(null);
$sheet = new Spreadsheet();
$sheet->getActiveSheet()->getCell('A1')->setValue($value);

Expand Down
3 changes: 0 additions & 3 deletions tests/PhpSpreadsheetTests/Writer/Xlsx/StartsWithHashTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

use PhpOffice\PhpSpreadsheet\Cell\DataType;
use PhpOffice\PhpSpreadsheet\Reader\Xlsx as Reader;
use PhpOffice\PhpSpreadsheet\Settings;
use PhpOffice\PhpSpreadsheet\Shared\File;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx as Writer;
Expand All @@ -16,7 +15,6 @@ class StartsWithHashTest extends TestCase
public function testStartWithHash(): void
{
$outputFilename = File::temporaryFilename();
Settings::setLibXmlLoaderOptions(null);
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->setCellValueExplicit('A1', '#define M', DataType::TYPE_STRING);
Expand All @@ -41,7 +39,6 @@ public function testStartWithHashReadRaw(): void
{
// Make sure raw data indicates A3 is an error, but A2 isn't.
$outputFilename = File::temporaryFilename();
Settings::setLibXmlLoaderOptions(null);
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->setCellValueExplicit('A1', '#define M', DataType::TYPE_STRING);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

namespace PhpOffice\PhpSpreadsheetTests\Writer\Xlsx;

use PhpOffice\PhpSpreadsheet\Settings;
use PhpOffice\PhpSpreadsheet\Shared\File;
use PHPUnit\Framework\TestCase;
use ZipArchive;
Expand All @@ -16,7 +15,6 @@ public function testLoadSaveXlsxWithUnparsedDataClone(): void
{
$sampleFilename = 'tests/data/Writer/XLSX/drawing_on_2nd_page.xlsx';
$resultFilename = File::temporaryFilename();
Settings::setLibXmlLoaderOptions(null); // reset to default options
$reader = new \PhpOffice\PhpSpreadsheet\Reader\Xlsx();
$spreadsheet = $reader->load($sampleFilename);
$spreadsheet->setActiveSheetIndex(1);
Expand Down Expand Up @@ -63,7 +61,6 @@ public function testSaveTwice(): void
$resultFilename1 = File::temporaryFilename();
$resultFilename2 = File::temporaryFilename();
self::assertNotEquals($resultFilename1, $resultFilename2);
Settings::setLibXmlLoaderOptions(null); // reset to default options
$reader = new \PhpOffice\PhpSpreadsheet\Reader\Xlsx();
$spreadsheet = $reader->load($sampleFilename);
$sheet = $spreadsheet->setActiveSheetIndex(1);
Expand Down
1 change: 0 additions & 1 deletion tests/PhpSpreadsheetTests/Writer/Xlsx/UnparsedDataTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ public function testLoadSaveXlsxWithUnparsedData(): void
{
$sampleFilename = 'tests/data/Writer/XLSX/form_pass_print.xlsm';
$resultFilename = File::temporaryFilename();
Settings::setLibXmlLoaderOptions(null); // reset to default options
$reader = new \PhpOffice\PhpSpreadsheet\Reader\Xlsx();
$excel = $reader->load($sampleFilename);

Expand Down
Loading