Add support protection of worksheet by a specific hash algorithm

This commit is contained in:
Reijn 2020-05-23 20:01:03 +03:00 committed by Adrien Crivelli
parent c434e9b137
commit dfa6f77178
No known key found for this signature in database
GPG Key ID: B182FD79DC6DE92E
6 changed files with 261 additions and 1 deletions

View File

@ -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

View File

@ -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);

View File

@ -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));
}
}

View File

@ -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.
*/

View File

@ -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'));

View File

@ -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,
],
];