From dfa6f7717801e6738bb266ef3fac25fd8ad5294d Mon Sep 17 00:00:00 2001 From: Reijn Date: Sat, 23 May 2020 20:01:03 +0300 Subject: [PATCH] Add support protection of worksheet by a specific hash algorithm --- CHANGELOG.md | 1 + src/PhpSpreadsheet/Reader/Xlsx.php | 4 + src/PhpSpreadsheet/Shared/PasswordHasher.php | 91 +++++++++++++- src/PhpSpreadsheet/Worksheet/Protection.php | 124 +++++++++++++++++++ src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php | 16 +++ tests/data/Shared/PasswordHashes.php | 26 ++++ 6 files changed, 261 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75eb393c..dd24e146 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Support writing to streams in all writers [#1292](https://github.com/PHPOffice/PhpSpreadsheet/issues/1292) - Support CSV files with data wrapping a lot of lines [#1468](https://github.com/PHPOffice/PhpSpreadsheet/pull/1468) +- Support protection of worksheet by a specific hash algorithm ### Fixed diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index 797e59ea..f9f20bdd 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -765,6 +765,10 @@ class Xlsx extends BaseReader if (!$this->readDataOnly && $xmlSheet && $xmlSheet->sheetProtection) { $docSheet->getProtection()->setPassword((string) $xmlSheet->sheetProtection['password'], true); + $docSheet->getProtection()->setAlgorithmName((string) $xmlSheet->sheetProtection['algorithmName']); + $docSheet->getProtection()->setHashValue((string) $xmlSheet->sheetProtection['hashValue']); + $docSheet->getProtection()->setSaltValue((string) $xmlSheet->sheetProtection['saltValue']); + $docSheet->getProtection()->setSpinCount((int) $xmlSheet->sheetProtection['spinCount']); if ($xmlSheet->protectedRanges->protectedRange) { foreach ($xmlSheet->protectedRanges->protectedRange as $protectedRange) { $docSheet->protectCells((string) $protectedRange['sqref'], (string) $protectedRange['password'], true); diff --git a/src/PhpSpreadsheet/Shared/PasswordHasher.php b/src/PhpSpreadsheet/Shared/PasswordHasher.php index 9b0080b9..f3c2d3e1 100644 --- a/src/PhpSpreadsheet/Shared/PasswordHasher.php +++ b/src/PhpSpreadsheet/Shared/PasswordHasher.php @@ -4,6 +4,51 @@ namespace PhpOffice\PhpSpreadsheet\Shared; class PasswordHasher { + const ALGORITHM_MD2 = 'MD2'; + const ALGORITHM_MD4 = 'MD4'; + const ALGORITHM_MD5 = 'MD5'; + const ALGORITHM_SHA_1 = 'SHA-1'; + const ALGORITHM_SHA_256 = 'SHA-256'; + const ALGORITHM_SHA_384 = 'SHA-384'; + const ALGORITHM_SHA_512 = 'SHA-512'; + const ALGORITHM_RIPEMD_128 = 'RIPEMD-128'; + const ALGORITHM_RIPEMD_160 = 'RIPEMD-160'; + const ALGORITHM_WHIRLPOOL = 'WHIRLPOOL'; + + /** + * Mapping between algorithm name in Excel and algorithm name in PHP. + * + * @var array + */ + private static $algorithmArray = [ + self::ALGORITHM_MD2 => 'md2', + self::ALGORITHM_MD4 => 'md4', + self::ALGORITHM_MD5 => 'md5', + self::ALGORITHM_SHA_1 => 'sha1', + self::ALGORITHM_SHA_256 => 'sha256', + self::ALGORITHM_SHA_384 => 'sha384', + self::ALGORITHM_SHA_512 => 'sha512', + self::ALGORITHM_RIPEMD_128 => 'ripemd128', + self::ALGORITHM_RIPEMD_160 => 'ripemd160', + self::ALGORITHM_WHIRLPOOL => 'whirlpool', + ]; + + /** + * Get algorithm from self::$algorithmArray. + * + * @param string $pAlgorithmName + * + * @return string + */ + private static function getAlgorithm($pAlgorithmName) + { + if (array_key_exists($pAlgorithmName, self::$algorithmArray)) { + return self::$algorithmArray[$pAlgorithmName]; + } + + return ''; + } + /** * Create a password hash from a given string. * @@ -15,7 +60,7 @@ class PasswordHasher * * @return string Hashed password */ - public static function hashPassword($pPassword) + public static function defaultHashPassword($pPassword) { $password = 0x0000; $charPos = 1; // char position @@ -34,4 +79,48 @@ class PasswordHasher return strtoupper(dechex($password)); } + + /** + * Create a password hash from a given string by a specific algorithm. + * + * 2.4.2.4 ISO Write Protection Method + * + * @see https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-offcrypto/1357ea58-646e-4483-92ef-95d718079d6f + * + * @param string $pPassword Password to hash + * @param string $pAlgorithmName Hash algorithm used to compute the password hash value + * @param string $pSaltValue Pseudorandom string + * @param string $pSpinCount Number of times to iterate on a hash of a password + * + * @return string Hashed password + */ + public static function hashPassword($pPassword, $pAlgorithmName = '', $pSaltValue = '', $pSpinCount = 10000) + { + $algorithmName = self::getAlgorithm($pAlgorithmName); + if (!$pAlgorithmName) { + return self::defaultHashPassword($pPassword); + } + + $saltValue = base64_decode($pSaltValue); + $password = mb_convert_encoding($pPassword, 'UCS-2LE', 'UTF-8'); + + $hashValue = hash($algorithmName, $saltValue . $password, true); + for ($i = 0; $i < $pSpinCount; ++$i) { + $hashValue = hash($algorithmName, $hashValue . pack('L', $i), true); + } + + return base64_encode($hashValue); + } + + /** + * Create a pseudorandom string. + * + * @param int $pSize Length of the output string in bytes + * + * @return string Pseudorandom string + */ + public static function generateSalt($pSize = 16) + { + return base64_encode(random_bytes($pSize)); + } } diff --git a/src/PhpSpreadsheet/Worksheet/Protection.php b/src/PhpSpreadsheet/Worksheet/Protection.php index 2fd3e919..3566e255 100644 --- a/src/PhpSpreadsheet/Worksheet/Protection.php +++ b/src/PhpSpreadsheet/Worksheet/Protection.php @@ -125,6 +125,34 @@ class Protection */ private $password = ''; + /** + * Algorithm name. + * + * @var string + */ + private $algorithmName = ''; + + /** + * Hash value. + * + * @var string + */ + private $hashValue = ''; + + /** + * Salt value. + * + * @var string + */ + private $saltValue = ''; + + /** + * Spin count. + * + * @var int + */ + private $spinCount = ''; + /** * Create a new Protection. */ @@ -569,6 +597,102 @@ class Protection return $this; } + /** + * Get AlgorithmName. + * + * @return string + */ + public function getAlgorithmName() + { + return $this->algorithmName; + } + + /** + * Set AlgorithmName. + * + * @param string $pValue + * + * @return $this + */ + public function setAlgorithmName($pValue) + { + $this->algorithmName = $pValue; + + return $this; + } + + /** + * Get HashValue. + * + * @return string + */ + public function getHashValue() + { + return $this->hashValue; + } + + /** + * Set HashValue. + * + * @param string $pValue + * + * @return $this + */ + public function setHashValue($pValue) + { + $this->hashValue = $pValue; + + return $this; + } + + /** + * Get SaltValue. + * + * @return string + */ + public function getSaltValue() + { + return $this->saltValue; + } + + /** + * Set SaltValue. + * + * @param string $pValue + * + * @return $this + */ + public function setSaltValue($pValue) + { + $this->saltValue = $pValue; + + return $this; + } + + /** + * Get SpinCount. + * + * @return int + */ + public function getSpinCount() + { + return $this->spinCount; + } + + /** + * Set SpinCount. + * + * @param int $pValue + * + * @return $this + */ + public function setSpinCount($pValue) + { + $this->spinCount = $pValue; + + return $this; + } + /** * Implement PHP __clone to create a deep clone, not just a shallow copy. */ diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php index 3d47eeaa..803ade8a 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php @@ -424,6 +424,22 @@ class Worksheet extends WriterPart $objWriter->writeAttribute('password', $pSheet->getProtection()->getPassword()); } + if ($pSheet->getProtection()->getHashValue() !== '') { + $objWriter->writeAttribute('hashValue', $pSheet->getProtection()->getHashValue()); + } + + if ($pSheet->getProtection()->getAlgorithmName() !== '') { + $objWriter->writeAttribute('algorithmName', $pSheet->getProtection()->getAlgorithmName()); + } + + if ($pSheet->getProtection()->getSaltValue() !== '') { + $objWriter->writeAttribute('saltValue', $pSheet->getProtection()->getSaltValue()); + } + + if ($pSheet->getProtection()->getSpinCount() !== '') { + $objWriter->writeAttribute('spinCount', $pSheet->getProtection()->getSpinCount()); + } + $objWriter->writeAttribute('sheet', ($pSheet->getProtection()->getSheet() ? 'true' : 'false')); $objWriter->writeAttribute('objects', ($pSheet->getProtection()->getObjects() ? 'true' : 'false')); $objWriter->writeAttribute('scenarios', ($pSheet->getProtection()->getScenarios() ? 'true' : 'false')); diff --git a/tests/data/Shared/PasswordHashes.php b/tests/data/Shared/PasswordHashes.php index b4f348ca..34c25cef 100644 --- a/tests/data/Shared/PasswordHashes.php +++ b/tests/data/Shared/PasswordHashes.php @@ -25,4 +25,30 @@ return [ 'CE4B', '', ], + [ + 'O6EXRLpLEDNJDL/AzYtnnA4O4bY=', + '', + 'SHA-1', + ], + [ + 'GYvlIMljDI1Czc4jfWrGaxU5pxl9n5Og0KUzyAfYxwk=', + 'PhpSpreadsheet', + 'SHA-256', + 'Php_salt', + 1000, + ], + [ + 'sSHdxQv9qgpkr4LDT0bYQxM9hOQJFRhJ4D752/NHQtDDR1EVy67NCEW9cPd6oWvCoBGd96MqKpuma1A7pN1nEA==', + 'Mark Baker', + 'SHA-512', + 'Mark_salt', + 10000, + ], + [ + 'r9KVLLCKIYOILvE2rcby+g==', + '!+&=()~§±æþ', + 'MD5', + 'Symbols_salt', + 100000, + ], ];