/
SignatureHasher.php
135 lines (117 loc) · 5.55 KB
/
SignatureHasher.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Core\Signature;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\Security\Core\Exception\InvalidArgumentException;
use Symfony\Component\Security\Core\Signature\Exception\ExpiredSignatureException;
use Symfony\Component\Security\Core\Signature\Exception\InvalidSignatureException;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* Creates and validates secure hashes used in login links and remember-me cookies.
*
* @author Wouter de Jong <wouter@wouterj.nl>
* @author Ryan Weaver <ryan@symfonycasts.com>
*/
class SignatureHasher
{
private PropertyAccessorInterface $propertyAccessor;
private array $signatureProperties;
private string $secret;
private ?ExpiredSignatureStorage $expiredSignaturesStorage;
private ?int $maxUses;
/**
* @param array $signatureProperties Properties of the User; the hash is invalidated if these properties change
* @param ExpiredSignatureStorage|null $expiredSignaturesStorage If provided, secures a sequence of hashes that are expired
* @param int|null $maxUses Used together with $expiredSignatureStorage to allow a maximum usage of a hash
*/
public function __construct(PropertyAccessorInterface $propertyAccessor, array $signatureProperties, #[\SensitiveParameter] string $secret, ?ExpiredSignatureStorage $expiredSignaturesStorage = null, ?int $maxUses = null)
{
if (!$secret) {
throw new InvalidArgumentException('A non-empty secret is required.');
}
$this->propertyAccessor = $propertyAccessor;
$this->signatureProperties = $signatureProperties;
$this->secret = $secret;
$this->expiredSignaturesStorage = $expiredSignaturesStorage;
$this->maxUses = $maxUses;
}
/**
* Verifies the hash using the provided user identifier and expire time.
*
* This method must be called before the user object is loaded from a provider.
*
* @param int $expires The expiry time as a unix timestamp
* @param string $hash The plaintext hash provided by the request
*
* @throws InvalidSignatureException If the signature does not match the provided parameters
* @throws ExpiredSignatureException If the signature is no longer valid
*/
public function acceptSignatureHash(string $userIdentifier, int $expires, string $hash): void
{
if ($expires < time()) {
throw new ExpiredSignatureException('Signature has expired.');
}
$hmac = substr($hash, 0, 44);
$payload = substr($hash, 44).':'.$expires.':'.$userIdentifier;
if (!hash_equals($hmac, $this->generateHash($payload))) {
throw new InvalidSignatureException('Invalid or expired signature.');
}
}
/**
* Verifies the hash using the provided user and expire time.
*
* @param int $expires The expiry time as a unix timestamp
* @param string $hash The plaintext hash provided by the request
*
* @throws InvalidSignatureException If the signature does not match the provided parameters
* @throws ExpiredSignatureException If the signature is no longer valid
*/
public function verifySignatureHash(UserInterface $user, int $expires, string $hash): void
{
if ($expires < time()) {
throw new ExpiredSignatureException('Signature has expired.');
}
if (!hash_equals($hash, $this->computeSignatureHash($user, $expires))) {
throw new InvalidSignatureException('Invalid or expired signature.');
}
if ($this->expiredSignaturesStorage && $this->maxUses) {
if ($this->expiredSignaturesStorage->countUsages($hash) >= $this->maxUses) {
throw new ExpiredSignatureException(sprintf('Signature can only be used "%d" times.', $this->maxUses));
}
$this->expiredSignaturesStorage->incrementUsages($hash);
}
}
/**
* Computes the secure hash for the provided user and expire time.
*
* @param int $expires The expiry time as a unix timestamp
*/
public function computeSignatureHash(UserInterface $user, int $expires): string
{
$userIdentifier = $user->getUserIdentifier();
$fieldsHash = hash_init('sha256');
foreach ($this->signatureProperties as $property) {
$value = $this->propertyAccessor->getValue($user, $property) ?? '';
if ($value instanceof \DateTimeInterface) {
$value = $value->format('c');
}
if (!\is_scalar($value) && !$value instanceof \Stringable) {
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)));
}
hash_update($fieldsHash, ':'.base64_encode($value));
}
$fieldsHash = strtr(base64_encode(hash_final($fieldsHash, true)), '+/=', '-_~');
return $this->generateHash($fieldsHash.':'.$expires.':'.$userIdentifier).$fieldsHash;
}
private function generateHash(string $tokenValue): string
{
return strtr(base64_encode(hash_hmac('sha256', $tokenValue, $this->secret, true)), '+/=', '-_~');
}
}