Completed
Push — master ( de0c2e...06730c )
by Sam
01:56
created

UrlSigner::addParamKeys()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 8
ccs 5
cts 5
cp 1
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 1
crap 2
1
<?php
2
declare(strict_types=1);
3
4
5
namespace SamIT\Yii2\UrlSigner;
6
7
8
use yii\base\Component;
9
use yii\base\InvalidConfigException;
10
use yii\helpers\StringHelper;
11
12
class UrlSigner extends Component
13
{
14
    /**
15
     * @var string The name of the URL param for the HMAC
16
     */
17
    public $hmacParam = 'hmac';
18
19
    /**
20
     * @var string The name of the URL param for the parameters
21
     */
22
    public $paramsParam = 'params';
23
24
    /**
25
     * @var string The name of the URL param for the expiration date time
26
     */
27
    public $expirationParam = 'expires';
28
29
    /**
30
     * Note that expiration dates cannot be disabled. If you really need to you can set a longer duration for the links.
31
     * @var \DateInterval The default interval for link validity (default: 1 week)
32
     */
33
    private $_defaultExpirationInterval;
34
35
    /**
36
     * @var string
37
     */
38
    public $secret;
39
40 16
    public function init(): void
41
    {
42 16
        parent::init();
43 16
        $this->setDefaultExpirationInterval('P7D');
44 16
        if (empty($this->secret)
45 16
            || empty($this->hmacParam)
46 16
            || empty($this->paramsParam)
47 16
            || empty($this->expirationParam)
48
        ) {
49 2
            throw new InvalidConfigException('The following configuration params are required: secret, hmacParam, paramsParam and expirationParam');
50
        }
51
52
53
    }
54
55 16
    public function setDefaultExpirationInterval(string $interval): void
56
    {
57 16
        $this->_defaultExpirationInterval = new \DateInterval($interval);
58
59
    }
60
    /**
61
     * Calculates the HMAC for a URL.
62
     **/
63 10
    public function calculateHMAC(
64
        array $params,
65
        string $route
66
    ): string {
67 10
        if (isset($params[0])) {
68 10
            unset($params[0]);
69
        }
70
71 10
        \ksort($params);
72
73 10
        $hash = \hash_hmac('sha256',
74 10
            \trim($route, '/') . '|' . \implode('#', $params),
75 10
            $this->secret,
76 10
            true
77
        );
78
79 10
        return $this->urlEncode($hash);
80
    }
81
82
    /**
83
     * This adds an HMAC to a list of query params.
84
     * If
85
     * @param array $queryParams List of query parameters
86
     * @param bool $allowAddition Whether to allow extra parameters to be added.
87
     * @throws \Exception
88
     * @return void
89
     */
90 12
    public function signParams(
91
        array &$queryParams,
92
        $allowAddition = true,
93
        ?\DateTimeInterface $expiration = null
94
    ): void {
95 12
        if (isset($queryParams[$this->hmacParam])) {
96 2
            throw new \RuntimeException("HMAC param is already present");
97
        }
98
99 12
        $route = $queryParams[0];
100
101 12
        if (\strncmp($route, '/', 1) !== 0) {
102 2
            throw new \RuntimeException("Route must be absolute (start with /)");
103
        }
104
105 10
        $this->addExpiration($queryParams, $expiration);
106 10
        if ($allowAddition) {
107 4
            $this->addParamKeys($queryParams);
108
        }
109
110 10
        $queryParams[$this->hmacParam] = $this->calculateHMAC($queryParams, $route);
111
    }
112
113
    /**
114
     * Adds the expiration param if needed.
115
     */
116 10
    private function addExpiration(array &$params, ?\DateTimeInterface $expiration = null): void
117
    {
118 10
        if (!empty($this->expirationParam)) {
119 10
            if (!isset($expiration)) {
120 10
                $expiration = (new \DateTime())->add($this->_defaultExpirationInterval);
121
            }
122 10
            $params[$this->expirationParam] = $expiration->getTimestamp();
123
        }
124
    }
125
126 6
    private function checkExpiration(array $params): bool
127
    {
128
        // Check expiration date.
129 6
        return $params[$this->expirationParam] > \time();
130
    }
131
132
    /**
133
     * Adds the keys of all params to the param array so it is included for signing.
134
     * @param array $params
135
     */
136 4
    private function addParamKeys(array &$params): void
137
    {
138 4
        $keys = \array_keys($params);
139 4
        if ($keys[0] === 0) {
140 4
            unset($keys[0]);
141
        }
142 4
        $params[$this->paramsParam] = implode(',', $keys);
143
    }
144
145
    /**
146
     * Extracts the signed params from an array of params.
147
     * @param array $params
148
     * @return array
149
     */
150 8
    private function getSignedParams(array $params): array
151
    {
152 8
        if (empty($params[$this->paramsParam])) {
153
            // HMAC itself is never signed.
154 6
            unset($params[$this->hmacParam]);
155 6
            return $params;
156
        }
157
158 2
        $signedParams = [];
159 2
        $signedParams[$this->paramsParam] = $params[$this->paramsParam];
160
161 2
        foreach(\explode(',', $params[$this->paramsParam]) as $signedParam) {
162 2
            $signedParams[$signedParam] = $params[$signedParam] ?? null;
163
        }
164
165 2
        return $signedParams;
166
    }
167
168
    /**
169
     * Verifies the params for a specific route.
170
     * Checks that the HMAC is present and valid.
171
     * Checks that the HMAC is not expired.
172
     * @param array $params
173
     * @throws \Exception
174
     * @return bool
175
     */
176 8
    public function verify(array $params, string $route):bool
177
    {
178 8
        if (!isset($params[$this->hmacParam])) {
179 2
           return false;
180
        }
181 8
        $hmac = $params[$this->hmacParam];
182
183 8
        $signedParams = $this->getSignedParams($params);
184
185 8
        $calculated = $this->calculateHMAC($signedParams, $route);
186 8
        if (!\hash_equals($calculated, $hmac)) {
187 2
            return false;
188
        }
189
190 6
        return $this->checkExpiration($params);
191
    }
192
193 10
    private function urlEncode(string $bytes): string
194
    {
195 10
        return StringHelper::base64UrlEncode($bytes);
196
    }
197
}
198