Completed
Pull Request — master (#7)
by lee
02:06
created

CsrfGuard::generateToken()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2.0932

Importance

Changes 0
Metric Value
cc 2
eloc 7
nc 2
nop 0
dl 0
loc 13
ccs 5
cts 7
cp 0.7143
crap 2.0932
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * Linna Cross-site Request Forgery Guard
5
 *
6
 * @author Sebastian Rapetti <[email protected]>
7
 * @copyright (c) 2018, Sebastian Rapetti
8
 * @license http://opensource.org/licenses/MIT MIT License
9
 */
10
declare(strict_types=1);
11
12
namespace Linna;
13
14
use Ramsey\Uuid\Uuid;
15
use Ramsey\Uuid\Exception\UnsatisfiedDependencyException;
16
use RuntimeException;
17
18
/**
19
 * Cross-site Request Forgery Guard
20
 */
21
class CsrfGuard
22
{
23
    /**
24
     * @var array Php session data from superglobal.
25
     */
26
    private $session;
27
28
    /**
29
     * @var int Max number of tokens stored in session.
30
     */
31
    private $maxStorage;
32
33
    /**
34
     * @var int Rapresent the lenght of the token in bytes.
35
     */
36
    private $tokenStrength;
37
38
    /**
39
     * Class constructor.
40
     *
41
     * @param int $maxStorage    Max number of tokens stored in session, work as
42
     *                           FIFO data structure, when maximun capacity is
43
     *                           reached, oldest token be dequeued from storage.
44
     * @param int $tokenStrength Rapresent the lenght of the token in bytes.
45
     */
46 23
    public function __construct(int $maxStorage, int $tokenStrength = 16)
47
    {
48 23
        if (session_status() === 1) {
49 1
            throw new RuntimeException(__CLASS__.': Session must be started before create '.__CLASS__.' instance.');
50
        }
51
52 22
        if ($tokenStrength < 16) {
53 1
            throw new RuntimeException('The minimum CSRF token strength is 16.');
54
        }
55
56 21
        $_SESSION['CSRF'] = $_SESSION['CSRF'] ?? [];
57
58 21
        $this->session = &$_SESSION;
59 21
        $this->maxStorage = $maxStorage;
60 21
        $this->tokenStrength = $tokenStrength;
61 21
    }
62
63
    /**
64
     * Limit number of token stored in session.
65
     *
66
     * @param array $array
67
     */
68 20
    private function dequeue(array &$array): void
69
    {
70 20
        $size = count($array);
71
72 20
        while ($size > $this->maxStorage) {
73 14
            array_shift($array);
74 14
            $size--;
75
        }
76 20
    }
77
78
    /**
79
     * Return csrf token as array.
80
     *
81
     * @return array
82
     */
83 17
    public function getToken(): array
84
    {
85 17
        $token = $this->generateToken();
86
87 17
        $name = $token['name'];
88
89 17
        $this->session['CSRF'][$name] = $token;
90
91
        //storage cleaning!
92
        //warning!! if you get in a page more token of maximun storage,
93
        //will there a leak of token, the firsts generated
94
        //in future I think throw and exception.
95 17
        $this->dequeue($this->session['CSRF']);
96
97 17
        return $token;
98
    }
99
100
    /**
101
     * Return timed csrf token as array.
102
     *
103
     * @param int $ttl Time to live for the token.
104
     *
105
     * @return array
106
     */
107 3
    public function getTimedToken(int $ttl): array
108
    {
109 3
        $token = $this->generateToken();
110 3
        $token['time'] = time() + $ttl;
111
112 3
        $name = $token['name'];
113
114 3
        $this->session['CSRF'][$name] = $token;
115
116 3
        $this->dequeue($this->session['CSRF']);
117
118 3
        return $token;
119
    }
120
121
    /**
122
     * Generate a random token.
123
     *
124
     * @return array
125
     */
126 20
    private function generateToken(): array
127
    {
128 20
        $name = 'csrf_'.bin2hex(random_bytes(8));
0 ignored issues
show
Unused Code introduced by
The assignment to $name is dead and can be removed.
Loading history...
129
130
        try {
131 20
            $name = 'csrf_' . Uuid::uuid4();
0 ignored issues
show
Bug introduced by
Are you sure Ramsey\Uuid\Uuid::uuid4() of type Ramsey\Uuid\UuidInterface can be used in concatenation? Consider adding a __toString()-method. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

131
            $name = 'csrf_' . /** @scrutinizer ignore-type */ Uuid::uuid4();
Loading history...
132
        } catch (UnsatisfiedDependencyException $e) {
133
            echo 'Caught exception: ' . $e->getMessage();
134
        }
135
136 20
        $value = bin2hex(random_bytes($this->tokenStrength));
137
138 20
        return ['name' => $name, 'value' => $value];
139
    }
140
141
    /**
142
     * Validate a csrf token or a csrf timed token.
143
     *
144
     * @param array $requestData From request or from superglobal variables $_POST,
145
     *                           $_GET, $_REQUEST and $_COOKIE.
146
     *
147
     * @return bool
148
     */
149 4
    public function validate(array $requestData): bool
150
    {
151
        //apply matchToken method elements of passed data,
152
        //using this instead of forach for code shortness.
153 4
        $array = array_filter($requestData, array($this, 'doChecks'), ARRAY_FILTER_USE_BOTH);
154
155 4
        return (bool) count($array);
156
    }
157
158
    /**
159
     * Tests for valid token.
160
     *
161
     * @param string $value
162
     * @param string $key
163
     *
164
     * @return bool
165
     */
166 4
    private function doChecks(string $value, string $key): bool
167
    {
168 4
        $tokens = &$this->session['CSRF'];
169
170 4
        return $this->tokenIsValid($tokens, $value, $key) &&
171 4
               $this->tokenIsExiperd($tokens, $key)  &&
172 4
               $this->deleteToken($tokens, $key);
173
    }
174
175
    /**
176
     * Delete token after validation.
177
     *
178
     * @param array $tokens
179
     * @param string $key
180
     * @return bool
181
     */
182 3
    private function deleteToken(array &$tokens, string &$key): bool
183
    {
184 3
        unset($tokens[$key]);
185
186 3
        return true;
187
    }
188
189
    /**
190
     * Check if token is valid
191
     *
192
     * @param array $tokens
193
     * @param string $value
194
     * @param string $key
195
     *
196
     * @return bool
197
     */
198 4
    private function tokenIsValid(array &$tokens, string &$value, string &$key): bool
199
    {
200
        //if token is not existed
201 4
        if (empty($tokens[$key])) {
202 2
            return false;
203
        }
204
205
        //if the hash of token and value are not equal
206 4
        if (!hash_equals($tokens[$key]['value'], $value)) {
207 1
            return false;
208
        }
209
210 4
        return true;
211
    }
212
213
    /**
214
     * Check if timed token is expired.
215
     *
216
     * @param array $tokens
217
     * @param string $key
218
     *
219
     * @return bool
220
     */
221 4
    private function tokenIsExiperd(array &$tokens, string &$key): bool
222
    {
223
        //if timed and if time is valid
224 4
        if (isset($tokens[$key]['time']) && $tokens[$key]['time'] < time()) {
225 1
            return false;
226
        }
227
228 3
        return true;
229
    }
230
}
231