[Hash] introduce the Hash API (#90)

This commit is contained in:
Saif Eddin G 2020-10-20 08:45:30 +02:00 committed by GitHub
parent 9939c3ee0b
commit d169ee5f1d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 431 additions and 0 deletions

103
src/Psl/Hash/Context.php Normal file
View File

@ -0,0 +1,103 @@
<?php
declare(strict_types=1);
namespace Psl\Hash;
use HashContext;
use Psl;
use Psl\Arr;
use Psl\Str;
use function hash_final;
use function hash_init;
use function hash_update;
use const HASH_HMAC;
/**
* Incremental hashing context.
*
* Example:
*
* Hash\Context::forAlgorithm('md5')
* ->update('The quick brown fox ')
* ->update('jumped over the lazy dog.')
* ->finalize()
* => Str("5c6ffbdd40d9556b73a21e63c3e0e904")
*
* @psalm-immutable
*/
final class Context
{
private HashContext $internalContext;
private function __construct(HashContext $internal_context)
{
$this->internalContext = $internal_context;
}
/**
* Initialize an incremental hashing context.
*
* @throws Psl\Exception\InvariantViolationException If the given algorithm is unsupported.
*
* @psalm-pure
*/
public static function forAlgorithm(string $algorithm): Context
{
Psl\invariant(
Arr\contains(algorithms(), $algorithm),
'Expected a valid hashing algorithm, "%s" given.',
$algorithm,
);
$internal_context = hash_init($algorithm);
return new self($internal_context);
}
/**
* Initialize an incremental HMAC hashing context.
*
* @throws Psl\Exception\InvariantViolationException If the given algorithm is unsupported.
*
* @psalm-pure
*/
public static function hmac(string $algorithm, string $key): Context
{
Psl\invariant(
Arr\contains(Hmac\algorithms(), $algorithm),
'Expected a hashing algorithms suitable for HMAC, "%s" given.',
$algorithm
);
Psl\invariant(!Str\is_empty($key), 'Expected a non-empty shared secret key.');
$internal_context = hash_init($algorithm, HASH_HMAC, $key);
return new self($internal_context);
}
/**
* Pump data into an active hashing context.
*/
public function update(string $data): Context
{
/** @var HashContext $internal_context */
$internal_context = hash_copy($this->internalContext);
hash_update($internal_context, $data);
return new self($internal_context);
}
/**
* Finalize an incremental hash and return resulting digest.
*/
public function finalize(): string
{
/** @var HashContext $internal_context */
$internal_context = hash_copy($this->internalContext);
return hash_final($internal_context, false);
}
}

View File

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Psl\Hash\Hmac;
use function hash_hmac_algos;
/**
* Return a list of registered hashing algorithms suitable for `Psl\Hash\Hmac\hash()`
*
* @psalm-return list<string>
*
* @psalm-pure
*/
function algorithms(): array
{
/** @psalm-suppress ImpureFunctionCall - hash_hmac_algos is pure. */
return hash_hmac_algos();
}

View File

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Psl\Hash\Hmac;
use Psl;
use Psl\Hash;
/**
* Generate a keyed hash value using the HMAC method.
*
* @throws Psl\Exception\InvariantViolationException If the given algorithm is unsupported.
*
* @psalm-pure
*/
function hash(string $data, string $algorithm, string $key): string
{
return Hash\Context::hmac($algorithm, $key)->update($data)->finalize();
}

View File

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Psl\Hash;
use function hash_algos;
/**
* Return a list of registered hashing algorithms.
*
* @psalm-return list<string>
*
* @psalm-pure
*/
function algorithms(): array
{
/** @psalm-suppress ImpureFunctionCall - hash_algos is pure. */
return hash_algos();
}

17
src/Psl/Hash/equals.php Normal file
View File

@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Psl\Hash;
use function hash_equals;
/**
* Timing attack safe string comparison.
*
* @psalm-pure
*/
function equals(string $known_string, string $user_string): bool
{
return hash_equals($known_string, $user_string);
}

19
src/Psl/Hash/hash.php Normal file
View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Psl\Hash;
use Psl;
/**
* Generate a hash value (message digest).
*
* @throws Psl\Exception\InvariantViolationException If the given algorithm is unsupported.
*
* @psalm-pure
*/
function hash(string $data, string $algorithm): string
{
return Context::forAlgorithm($algorithm)->update($data)->finalize();
}

View File

@ -331,6 +331,11 @@ final class Loader
'Psl\Password\hash',
'Psl\Password\needs_rehash',
'Psl\Password\verify',
'Psl\Hash\hash',
'Psl\Hash\algorithms',
'Psl\Hash\equals',
'Psl\Hash\Hmac\hash',
'Psl\Hash\Hmac\algorithms',
];
public const INTERFACES = [
@ -387,6 +392,7 @@ final class Loader
'Psl\Type\Type',
'Psl\Json\Exception\DecodeException',
'Psl\Json\Exception\EncodeException',
'Psl\Hash\Context',
];
private const TYPE_CONSTANTS = 1;

View File

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Psl\Tests\Hash;
use PHPUnit\Framework\TestCase;
use Psl\Hash;
use function hash_algos;
final class AlgorithmsTest extends TestCase
{
public function testAlgorithms(): void
{
static::assertSame(hash_algos(), Hash\algorithms());
}
}

View File

@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace Psl\Tests\Hash;
use PHPUnit\Framework\TestCase;
use Psl\Exception\InvariantViolationException;
use Psl\Hash;
final class ContextTest extends TestCase
{
public function testForAlgorithm(): void
{
$context = Hash\Context::forAlgorithm('md5')
->update('The quick brown fox ')
->update('jumped over the lazy dog.');
static::assertSame('5c6ffbdd40d9556b73a21e63c3e0e904', $context->finalize());
}
public function testForAlgorithmThrowsForInvalidAlgorithm(): void
{
$this->expectException(InvariantViolationException::class);
$this->expectExceptionMessage('Expected a valid hashing algorithm, "base64" given.');
Hash\Context::forAlgorithm('base64');
}
public function testHmac(): void
{
$context = Hash\Context::hmac('md5', 'secret')
->update('The quick brown fox ')
->update('jumped over the lazy dog.');
static::assertSame('7eb2b5c37443418fc77c136dd20e859c', $context->finalize());
}
public function testHmacThrowsForInvalidAlgorithm(): void
{
$this->expectException(InvariantViolationException::class);
$this->expectExceptionMessage('Expected a hashing algorithms suitable for HMAC, "base64" given.');
Hash\Context::hmac('base64', 'secret');
}
public function testHmacThrowsForEmptySecretKey(): void
{
$this->expectException(InvariantViolationException::class);
$this->expectExceptionMessage('Expected a non-empty shared secret key.');
Hash\Context::hmac('sha1', '');
}
public function testContextIsImmutable(): void
{
$first = Hash\Context::forAlgorithm('md5');
$second = $first->update('The quick brown fox ');
$third = $second->update('jumped over the lazy dog.');
static::assertNotSame($first, $second);
static::assertNotSame($second, $third);
static::assertNotSame($third, $first);
static::assertSame('d41d8cd98f00b204e9800998ecf8427e', $first->finalize());
static::assertSame('c4314972a672ded8759cafdca9af3238', $second->finalize());
static::assertSame('5c6ffbdd40d9556b73a21e63c3e0e904', $third->finalize());
}
public function testContextIsStillValidAfterFinalization(): void
{
$context = Hash\Context::forAlgorithm('md5')
->update('The quick brown fox ')
->update('jumped over the lazy dog.');
static::assertSame('5c6ffbdd40d9556b73a21e63c3e0e904', $context->finalize());
static::assertSame('5983132dd3e26f51fa8611a94c8e05ac', $context->update(' cool!')->finalize());
}
}

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Psl\Tests\Hash;
use Generator;
use PHPUnit\Framework\TestCase;
use Psl\Hash;
final class EqualsTest extends TestCase
{
/**
* @dataProvider provideEqualsData
*/
public function testEquals(bool $expected, string $known_string, string $user_string): void
{
static::assertSame($expected, Hash\equals($known_string, $user_string));
}
/**
* @psalm-return Generator<int, array{0: bool, 1: string, 2: string}, mixed, void>
*/
public function provideEqualsData(): Generator
{
yield [true, 'hello', 'hello'];
yield [false, 'hey', 'hello'];
yield [false, 'hello', 'hey'];
}
}

View File

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Psl\Tests\Hash;
use Generator;
use PHPUnit\Framework\TestCase;
use Psl\Exception\InvariantViolationException;
use Psl\Hash;
final class HashTest extends TestCase
{
/**
* @dataProvider provideHashData
*/
public function testHash(string $expected, string $data, string $algorithm): void
{
static::assertSame($expected, Hash\hash($data, $algorithm));
}
public function testHashThrowsForUnsupportedAlgorithm(): void
{
$this->expectException(InvariantViolationException::class);
Hash\hash('Hello', 'base64');
}
/**
* @psalm-return Generator<int, array{0: string, 1: string, 2: string}, mixed, void>
*/
public function provideHashData(): Generator
{
yield ['2aae6c35c94fcfb415dbe95f408b9ce91ee846ed', 'hello world', 'sha1'];
yield ['5eb63bbbe01eeed093cb22bb8f5acdc3', 'hello world', 'md5'];
}
}

View File

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Psl\Tests\Hash\Hmac;
use PHPUnit\Framework\TestCase;
use Psl\Hash\Hmac;
use function hash_hmac_algos;
final class AlgorithmsTest extends TestCase
{
public function testAlgorithms(): void
{
static::assertSame(hash_hmac_algos(), Hmac\algorithms());
}
}

View File

@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace Psl\Tests\Hash\Hmac;
use Generator;
use PHPUnit\Framework\TestCase;
use Psl\Exception\InvariantViolationException;
use Psl\Hash\Hmac;
final class HashTest extends TestCase
{
/**
* @dataProvider provideHashData
*/
public function testHash(string $expected, string $data, string $algorithm, string $key): void
{
static::assertSame($expected, Hmac\hash($data, $algorithm, $key));
}
public function testHashThrowsForInvalidAlgorithm(): void
{
$this->expectException(InvariantViolationException::class);
Hmac\hash('Hello', 'base64', 'real-secret');
}
public function testHashThrowsForEmptySharedSecret(): void
{
$this->expectException(InvariantViolationException::class);
Hmac\hash('Hello', 'sha1', '');
}
/**
* @psalm-return Generator<int, array{0: string, 1: string, 2: string, 3: string}, mixed, void>
*/
public function provideHashData(): Generator
{
yield ['03376ee7ad7bbfceee98660439a4d8b125122a5a', 'hello world', 'sha1', 'secret'];
yield ['78d6997b1230f38e59b6d1642dfaa3a4', 'hello world', 'md5', 'secret'];
}
}