mirror of
https://github.com/danog/endtoend-test-psl.git
synced 2024-11-30 04:39:48 +01:00
[Hash] introduce the Hash API (#90)
This commit is contained in:
parent
9939c3ee0b
commit
d169ee5f1d
103
src/Psl/Hash/Context.php
Normal file
103
src/Psl/Hash/Context.php
Normal 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);
|
||||
}
|
||||
}
|
20
src/Psl/Hash/Hmac/algorithms.php
Normal file
20
src/Psl/Hash/Hmac/algorithms.php
Normal 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();
|
||||
}
|
20
src/Psl/Hash/Hmac/hash.php
Normal file
20
src/Psl/Hash/Hmac/hash.php
Normal 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();
|
||||
}
|
20
src/Psl/Hash/algorithms.php
Normal file
20
src/Psl/Hash/algorithms.php
Normal 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
17
src/Psl/Hash/equals.php
Normal 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
19
src/Psl/Hash/hash.php
Normal 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();
|
||||
}
|
@ -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;
|
||||
|
18
tests/Psl/Hash/AlgorithmsTest.php
Normal file
18
tests/Psl/Hash/AlgorithmsTest.php
Normal 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());
|
||||
}
|
||||
}
|
79
tests/Psl/Hash/ContextTest.php
Normal file
79
tests/Psl/Hash/ContextTest.php
Normal 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());
|
||||
}
|
||||
}
|
30
tests/Psl/Hash/EqualsTest.php
Normal file
30
tests/Psl/Hash/EqualsTest.php
Normal 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'];
|
||||
}
|
||||
}
|
37
tests/Psl/Hash/HashTest.php
Normal file
37
tests/Psl/Hash/HashTest.php
Normal 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'];
|
||||
}
|
||||
}
|
18
tests/Psl/Hash/Hmac/AlgorithmsTest.php
Normal file
18
tests/Psl/Hash/Hmac/AlgorithmsTest.php
Normal 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());
|
||||
}
|
||||
}
|
44
tests/Psl/Hash/Hmac/HashTest.php
Normal file
44
tests/Psl/Hash/Hmac/HashTest.php
Normal 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'];
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user