Skip to content

Commit

Permalink
[Hash] introduce the Hash API
Browse files Browse the repository at this point in the history
  • Loading branch information
azjezz committed Oct 18, 2020
1 parent 9939c3e commit 5480c9d
Show file tree
Hide file tree
Showing 13 changed files with 422 additions and 0 deletions.
94 changes: 94 additions & 0 deletions src/Psl/Hash/Context.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?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 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 changes: 20 additions & 0 deletions src/Psl/Hash/Hmac/algorithms.php
Original file line number Diff line number Diff line change
@@ -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 changes: 20 additions & 0 deletions src/Psl/Hash/Hmac/hash.php
Original file line number Diff line number Diff line change
@@ -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 changes: 20 additions & 0 deletions src/Psl/Hash/algorithms.php
Original file line number Diff line number Diff line change
@@ -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 changes: 17 additions & 0 deletions src/Psl/Hash/equals.php
Original file line number Diff line number Diff line change
@@ -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 changes: 19 additions & 0 deletions src/Psl/Hash/hash.php
Original file line number Diff line number Diff line change
@@ -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();
}
6 changes: 6 additions & 0 deletions src/Psl/Internal/Loader.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -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;
Expand Down
18 changes: 18 additions & 0 deletions tests/Psl/Hash/AlgorithmsTest.php
Original file line number Diff line number Diff line change
@@ -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 changes: 79 additions & 0 deletions tests/Psl/Hash/ContextTest.php
Original file line number Diff line number Diff line change
@@ -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 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 changes: 30 additions & 0 deletions tests/Psl/Hash/EqualsTest.php
Original file line number Diff line number Diff line change
@@ -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 changes: 37 additions & 0 deletions tests/Psl/Hash/HashTest.php
Original file line number Diff line number Diff line change
@@ -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'];
}
}
Loading

0 comments on commit 5480c9d

Please sign in to comment.