Skip to content

Commit f0abfe3

Browse files
committed
Mockable Hasher implementation
1 parent 2236b7a commit f0abfe3

File tree

8 files changed

+192
-21
lines changed

8 files changed

+192
-21
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Symfony\Component\Security\Core\Signature;
15+
16+
final readonly class HashContext implements HashContextInterface
17+
{
18+
public function __construct(
19+
private \HashContext $hashContext,
20+
) {
21+
}
22+
23+
public function update(string $data): void
24+
{
25+
hash_update($this->hashContext, ':' . base64_encode($data));
26+
}
27+
28+
public function final(): string
29+
{
30+
return strtr(base64_encode(hash_final($this->hashContext, true)), '+/=', '-_~');
31+
}
32+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Symfony\Component\Security\Core\Signature;
15+
16+
interface HashContextInterface
17+
{
18+
public function update(string $data): void;
19+
20+
public function final(): string;
21+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Symfony\Component\Security\Core\Signature;
15+
16+
final readonly class Hasher implements HasherInterface
17+
{
18+
private const ALGO = 'sha256';
19+
20+
public function init(): HashContextInterface
21+
{
22+
return new HashContext(hash_init(self::ALGO));
23+
}
24+
25+
public function hmac(string $data, string $key): string
26+
{
27+
return strtr(base64_encode(hash_hmac(self::ALGO, $data, $key, true)), '+/=', '-_~');
28+
}
29+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Symfony\Component\Security\Core\Signature;
15+
16+
interface HasherInterface
17+
{
18+
public function init(): HashContextInterface;
19+
20+
public function hmac(string $data, string $key): string;
21+
}

src/Symfony/Component/Security/Core/Signature/SignatureHasher.php

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ public function __construct(
3636
#[\SensitiveParameter] private string $secret,
3737
private ?ExpiredSignatureStorage $expiredSignaturesStorage = null,
3838
private ?int $maxUses = null,
39+
private HasherInterface $hasher = new Hasher(),
3940
) {
4041
if (!$secret) {
4142
throw new InvalidArgumentException('A non-empty secret is required.');
@@ -102,7 +103,7 @@ public function verifySignatureHash(UserInterface $user, int $expires, string $h
102103
public function computeSignatureHash(UserInterface $user, int $expires): string
103104
{
104105
$userIdentifier = $user->getUserIdentifier();
105-
$fieldsHash = hash_init('sha256');
106+
$fieldsHashContext = $this->hasher->init();
106107

107108
foreach ($this->signatureProperties as $property) {
108109
$value = $this->propertyAccessor->getValue($user, $property) ?? '';
@@ -115,16 +116,16 @@ public function computeSignatureHash(UserInterface $user, int $expires): string
115116
throw new \InvalidArgumentException(\sprintf('The property path "%s" on the user object "%s" must return a value that can be cast to a string, but "%s" was returned.', $property, $user::class, get_debug_type($value)));
116117
}
117118

118-
hash_update($fieldsHash, ':'.base64_encode($value));
119+
$fieldsHashContext->update($value);
119120
}
120121

121-
$fieldsHash = strtr(base64_encode(hash_final($fieldsHash, true)), '+/=', '-_~');
122+
$fieldsHash = $fieldsHashContext->final();
122123

123124
return $this->generateHash($fieldsHash.':'.$expires.':'.$userIdentifier).$fieldsHash;
124125
}
125126

126127
private function generateHash(string $tokenValue): string
127128
{
128-
return strtr(base64_encode(hash_hmac('sha256', $tokenValue, $this->secret, true)), '+/=', '-_~');
129+
return $this->hasher->hmac($tokenValue, $this->secret);
129130
}
130131
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Symfony\Component\Security\Core\Tests\Fixtures;
15+
16+
use Symfony\Component\Security\Core\Signature\HashContextInterface;
17+
18+
class DummyHashContext implements HashContextInterface
19+
{
20+
private string $data = '';
21+
22+
public function update(string $data): void
23+
{
24+
$this->data .= ':' . $data;
25+
}
26+
27+
public function final(): string
28+
{
29+
return sprintf('HASH(%s)', $this->data);
30+
}
31+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Symfony\Component\Security\Core\Tests\Fixtures;
15+
16+
use Symfony\Component\Security\Core\Signature\HashContextInterface;
17+
use Symfony\Component\Security\Core\Signature\HasherInterface;
18+
19+
class DummyHasher implements HasherInterface
20+
{
21+
public function init(): HashContextInterface
22+
{
23+
return new DummyHashContext();
24+
}
25+
26+
public function hmac(string $data, string $key): string
27+
{
28+
return sprintf('HMAC(%s,%s)', $data, $key);
29+
}
30+
}

src/Symfony/Component/Security/Core/Tests/Signature/SignatureHasherTest.php

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
1919
use Symfony\Component\PropertyAccess\PropertyAccessor;
2020
use Symfony\Component\Security\Core\Signature\SignatureHasher;
21+
use Symfony\Component\Security\Core\Tests\Fixtures\DummyHasher;
2122
use Symfony\Component\Security\Core\Tests\Fixtures\DummyUserWithProperties;
2223
use Symfony\Component\Security\Core\Tests\Fixtures\Enum\IntBackedEnum;
2324
use Symfony\Component\Security\Core\Tests\Fixtures\Enum\NonBackedEnum;
@@ -32,14 +33,15 @@ class SignatureHasherTest extends TestCase
3233
/**
3334
* @dataProvider providerComputeSignatureHash
3435
*/
35-
public function testComputeSignatureHash(mixed $arbitraryValue, array $signatureProperties, string $expectedHash)
36+
public function testComputeSignatureHash(mixed $arbitraryValue, array $signatureProperties, bool $useDummyHasher, string $expectedHash)
3637
{
3738
$user = new DummyUserWithProperties(self::USER_IDENTIFIER, $arbitraryValue);
3839

3940
$signatureHasher = new SignatureHasher(
4041
new PropertyAccessor(),
4142
$signatureProperties,
4243
self::SECRET,
44+
...($useDummyHasher ? ['hasher' => new DummyHasher()] : []),
4345
);
4446

4547
$actualHash = $signatureHasher->computeSignatureHash($user, self::EXPIRES);
@@ -49,22 +51,26 @@ public function testComputeSignatureHash(mixed $arbitraryValue, array $signature
4951
public function providerComputeSignatureHash(): array
5052
{
5153
return [
52-
['someValue', [], 'G8FxuQ7xlU0L132MkzxZu5KRob7AQBcxzpaDAUC6b54~47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU~'],
53-
['someValue', ['identifier'], 'a5YdlgWCmg7usTIoClr2uFFOEfO_XY1f7rCAoswmstE~blXCO2vRdOtJA_aCJPBaNpRpsaS957uvosMktnrI6wY~'],
54-
['someValue', ['arbitraryValue'], 'kfMzZgYYD1oeqSSW7m0k94VuRvS7LeHcKq-PKU8WD7k~0nuV8X2IlHqxDdPRNOLP-wp_v2KdVL9dNYJ0_557fGc~'],
55-
['someValue', ['identifier', 'arbitraryValue'], 'myxvvho8WkMuOcMMeuRlZQFe58TNDQFgDrVFb8SZ50g~iJ4d_Agaa0AaCHZinVr_zZCgR2nSZgokvXIkv7ne1b4~'],
56-
[null, ['arbitraryValue'], 'RMzJFvIb5BMTbyJb_VZwuKEchdxH8bA00ci1kYVJgEc~56wHhmaOD_DwK2K9BPRf9jb9gttjsRBGAcl13ABfOmc~'],
57-
[false, ['arbitraryValue'], 'RMzJFvIb5BMTbyJb_VZwuKEchdxH8bA00ci1kYVJgEc~56wHhmaOD_DwK2K9BPRf9jb9gttjsRBGAcl13ABfOmc~'],
58-
[true, ['arbitraryValue'], 'otQtMUGvEkdfOynddQ5WvoRldq8honHbEb1HcM8UR8I~D60nAF03Qti0aU2B2Z5nVMOl_evP1uYHUVXRtHzgea0~'],
59-
[123, ['arbitraryValue'], 'FTEV8ag4ndPfukNSOgsQtT7M7V0_Ab0Q6xnpqbWNhZ0~wWwB3p8Bp_5t2VDeCwTuCFHI-3gDxIPgP-C9ZZHEzaY~'],
60-
[123.456, ['arbitraryValue'], 'bmSC3nku_rZA6KjVLJgEZhfx7GOhrQDfxaAubuncdII~nriT5yCE-wjOnuk-yycrgCtch4raCAhuuVeja7X6N7k~'],
61-
[['foo', 'bar', 'baz'], ['arbitraryValue[0]', 'arbitraryValue[1]'], 'RRujHUR7iidZDEMkSHXEGvyaTCA5C4m0n5H200gqLxw~Zt46jI-2GYxtNTzeTcOoq2_jxow7h7PuI2C0qp7-H28~'],
62-
[['a' => 'foo', 'b' => 'bar', 'c' => 'baz'], ['arbitraryValue[b]', 'arbitraryValue[c]'], 'J6hgo51Cax5NBrtIH1JpZSuLgNXZ0G24dN1v7WGFyqg~fePV3ZmKYu5tz49IF6nlmAwhchNOkkGMCEIFapsOVYw~'],
63-
[(object) ['a' => 'foo', 'b' => 'bar', 'c' => 'baz'], ['arbitraryValue.c', 'arbitraryValue.a', 'arbitraryValue.b'], 'sXS_9yKjlog_OhI6oI5I0oG-M-A8TCaHhE7yhuUfhQU~6bdXP2SGmxK_WmYcg_mBySv40I_aKpbySb78NfJLQVA~'],
64-
[IntBackedEnum::FOO, ['arbitraryValue'], '3CtZMZJ-YGRX6xUInO9pn3Re0oyojM57L-7CDWzY51k~BtLamVEszxpZWe7HwvgW9MB2XJfepVW7yEWydNHdr2k~'],
65-
[IntBackedEnum::BAR, ['arbitraryValue'], 'otQtMUGvEkdfOynddQ5WvoRldq8honHbEb1HcM8UR8I~D60nAF03Qti0aU2B2Z5nVMOl_evP1uYHUVXRtHzgea0~'],
66-
[StringBackedEnum::FOO, ['arbitraryValue'], 'H0kSG0c8UDJswEoMdkvpPvksK5yL7XO-UOPT93H-1Xo~JFWxxE-9gSaxRbu2nBFRqYShfEIp87D6nFTv3Qcidas~'],
67-
[StringBackedEnum::BAR, ['arbitraryValue'], '5UzmukUROyA6-v0VEac9Tc2Wz1HiV_nbqbWraFXbIu0~ZYCW1TBi_AyktUQPgz9tP3utfjTx-lv2Ea45T-0o4w8~'],
54+
// test with dummy hasher
55+
['someValue', [], true, 'HMAC(HASH():1234567890:username,s3cr3t)HASH()'],
56+
['someValue', ['identifier'], true, 'HMAC(HASH(:username):1234567890:username,s3cr3t)HASH(:username)'],
57+
['someValue', ['arbitraryValue'], true, 'HMAC(HASH(:someValue):1234567890:username,s3cr3t)HASH(:someValue)'],
58+
['someValue', ['identifier', 'arbitraryValue'], true, 'HMAC(HASH(:username:someValue):1234567890:username,s3cr3t)HASH(:username:someValue)'],
59+
[null, ['arbitraryValue'], true, 'HMAC(HASH(:):1234567890:username,s3cr3t)HASH(:)'],
60+
[false, ['arbitraryValue'], true, 'HMAC(HASH(:):1234567890:username,s3cr3t)HASH(:)'],
61+
[true, ['arbitraryValue'], true, 'HMAC(HASH(:1):1234567890:username,s3cr3t)HASH(:1)'],
62+
[123, ['arbitraryValue'], true, 'HMAC(HASH(:123):1234567890:username,s3cr3t)HASH(:123)'],
63+
[123.456, ['arbitraryValue'], true, 'HMAC(HASH(:123.456):1234567890:username,s3cr3t)HASH(:123.456)'],
64+
[['foo', 'bar', 'baz'], ['arbitraryValue[0]', 'arbitraryValue[1]'], true, 'HMAC(HASH(:foo:bar):1234567890:username,s3cr3t)HASH(:foo:bar)'],
65+
[['a' => 'foo', 'b' => 'bar', 'c' => 'baz'], ['arbitraryValue[b]', 'arbitraryValue[c]'], true, 'HMAC(HASH(:bar:baz):1234567890:username,s3cr3t)HASH(:bar:baz)'],
66+
[(object) ['a' => 'foo', 'b' => 'bar', 'c' => 'baz'], ['arbitraryValue.c', 'arbitraryValue.a', 'arbitraryValue.b'], true, 'HMAC(HASH(:baz:foo:bar):1234567890:username,s3cr3t)HASH(:baz:foo:bar)'],
67+
[IntBackedEnum::FOO, ['arbitraryValue'], true, 'HMAC(HASH(:0):1234567890:username,s3cr3t)HASH(:0)'],
68+
[IntBackedEnum::BAR, ['arbitraryValue'], true, 'HMAC(HASH(:1):1234567890:username,s3cr3t)HASH(:1)'],
69+
[StringBackedEnum::FOO, ['arbitraryValue'], true, 'HMAC(HASH(:Foo):1234567890:username,s3cr3t)HASH(:Foo)'],
70+
[StringBackedEnum::BAR, ['arbitraryValue'], true, 'HMAC(HASH(:Bar):1234567890:username,s3cr3t)HASH(:Bar)'],
71+
// test with actual hasher
72+
['someValue', ['identifier'], false, 'a5YdlgWCmg7usTIoClr2uFFOEfO_XY1f7rCAoswmstE~blXCO2vRdOtJA_aCJPBaNpRpsaS957uvosMktnrI6wY~'],
73+
['someValue', ['identifier', 'arbitraryValue'], false, 'myxvvho8WkMuOcMMeuRlZQFe58TNDQFgDrVFb8SZ50g~iJ4d_Agaa0AaCHZinVr_zZCgR2nSZgokvXIkv7ne1b4~'],
6874
];
6975
}
7076

0 commit comments

Comments
 (0)