Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
88.69% |
149 / 168 |
|
90.00% |
45 / 50 |
CRAP | |
0.00% |
0 / 1 |
Session | |
88.69% |
149 / 168 |
|
90.00% |
45 / 50 |
92.97 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
__destruct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getId | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getSessionId | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
resetId | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getProvider | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isPersistent | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
persist | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
unpersist | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
shouldRememberUser | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setRememberUser | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getRequest | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getUser | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getAllowedUserRights | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getRestrictions | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
canSetUser | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setUser | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
suggestLoginUsername | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
shouldForceHTTPS | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setForceHTTPS | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getLoggedOutTimestamp | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setLoggedOutTimestamp | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getProviderMetadata | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
clear | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
renew | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
sessionWithRequest | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
get | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
exists | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
set | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
remove | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
hasToken | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
getToken | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
5 | |||
resetToken | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
resetAllTokens | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getSecretKeys | |
100.00% |
18 / 18 |
|
100.00% |
1 / 1 |
4 | |||
getEncryptionAlgorithm | |
15.38% |
2 / 13 |
|
0.00% |
0 / 1 |
20.15 | |||
setSecret | |
86.67% |
13 / 15 |
|
0.00% |
0 / 1 |
4.04 | |||
getSecret | |
86.67% |
26 / 30 |
|
0.00% |
0 / 1 |
9.19 | |||
delaySave | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
save | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
count | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
current | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
key | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
next | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
rewind | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
valid | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
offsetExists | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
offsetGet | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
offsetSet | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
offsetUnset | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | /** |
3 | * MediaWiki session |
4 | * |
5 | * This program is free software; you can redistribute it and/or modify |
6 | * it under the terms of the GNU General Public License as published by |
7 | * the Free Software Foundation; either version 2 of the License, or |
8 | * (at your option) any later version. |
9 | * |
10 | * This program is distributed in the hope that it will be useful, |
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
13 | * GNU General Public License for more details. |
14 | * |
15 | * You should have received a copy of the GNU General Public License along |
16 | * with this program; if not, write to the Free Software Foundation, Inc., |
17 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
18 | * http://www.gnu.org/copyleft/gpl.html |
19 | * |
20 | * @file |
21 | * @ingroup Session |
22 | */ |
23 | |
24 | namespace MediaWiki\Session; |
25 | |
26 | use BadMethodCallException; |
27 | use LogicException; |
28 | use MediaWiki\MainConfigNames; |
29 | use MediaWiki\MediaWikiServices; |
30 | use MediaWiki\Request\WebRequest; |
31 | use MediaWiki\User\User; |
32 | use MWRestrictions; |
33 | use Psr\Log\LoggerInterface; |
34 | use RuntimeException; |
35 | |
36 | /** |
37 | * Manages data for an authenticated session |
38 | * |
39 | * A Session represents the fact that the current HTTP request is part of a |
40 | * session. There are two broad types of Sessions, based on whether they |
41 | * return true or false from self::canSetUser(): |
42 | * * When true (mutable), the Session identifies multiple requests as part of |
43 | * a session generically, with no tie to a particular user. |
44 | * * When false (immutable), the Session identifies multiple requests as part |
45 | * of a session by identifying and authenticating the request itself as |
46 | * belonging to a particular user. |
47 | * |
48 | * The Session object also serves as a replacement for PHP's $_SESSION, |
49 | * managing access to per-session data. |
50 | * |
51 | * @ingroup Session |
52 | * @since 1.27 |
53 | */ |
54 | class Session implements \Countable, \Iterator, \ArrayAccess { |
55 | /** @var null|string[] Encryption algorithm to use */ |
56 | private static $encryptionAlgorithm = null; |
57 | |
58 | /** @var SessionBackend Session backend (can't be type-hinted, see DummySessionBackend in tests) */ |
59 | private $backend; |
60 | |
61 | /** @var int Session index */ |
62 | private $index; |
63 | |
64 | private LoggerInterface $logger; |
65 | |
66 | /** |
67 | * @param SessionBackend $backend |
68 | * @param int $index |
69 | * @param LoggerInterface $logger |
70 | */ |
71 | public function __construct( SessionBackend $backend, $index, LoggerInterface $logger ) { |
72 | $this->backend = $backend; |
73 | $this->index = $index; |
74 | $this->logger = $logger; |
75 | } |
76 | |
77 | public function __destruct() { |
78 | $this->backend->deregisterSession( $this->index ); |
79 | } |
80 | |
81 | /** |
82 | * Returns the session ID |
83 | * @return string |
84 | */ |
85 | public function getId() { |
86 | return $this->backend->getId(); |
87 | } |
88 | |
89 | /** |
90 | * Returns the SessionId object |
91 | * @internal For internal use by WebRequest |
92 | * @return SessionId |
93 | */ |
94 | public function getSessionId() { |
95 | return $this->backend->getSessionId(); |
96 | } |
97 | |
98 | /** |
99 | * Changes the session ID |
100 | * @return string New ID (might be the same as the old) |
101 | */ |
102 | public function resetId() { |
103 | return $this->backend->resetId(); |
104 | } |
105 | |
106 | /** |
107 | * Fetch the SessionProvider for this session |
108 | * @return SessionProviderInterface |
109 | */ |
110 | public function getProvider() { |
111 | return $this->backend->getProvider(); |
112 | } |
113 | |
114 | /** |
115 | * Indicate whether this session is persisted across requests |
116 | * |
117 | * For example, if cookies are set. |
118 | * |
119 | * @return bool |
120 | */ |
121 | public function isPersistent() { |
122 | return $this->backend->isPersistent(); |
123 | } |
124 | |
125 | /** |
126 | * Make this session persisted across requests |
127 | * |
128 | * If the session is already persistent, equivalent to calling |
129 | * $this->renew(). |
130 | */ |
131 | public function persist() { |
132 | $this->backend->persist(); |
133 | } |
134 | |
135 | /** |
136 | * Make this session not be persisted across requests |
137 | * |
138 | * This will remove persistence information (e.g. delete cookies) |
139 | * from the associated WebRequest(s), and delete session data in the |
140 | * backend. The session data will still be available via get() until |
141 | * the end of the request. |
142 | */ |
143 | public function unpersist() { |
144 | $this->backend->unpersist(); |
145 | } |
146 | |
147 | /** |
148 | * Indicate whether the user should be remembered independently of the |
149 | * session ID. |
150 | * @return bool |
151 | */ |
152 | public function shouldRememberUser() { |
153 | return $this->backend->shouldRememberUser(); |
154 | } |
155 | |
156 | /** |
157 | * Set whether the user should be remembered independently of the session |
158 | * ID. |
159 | * @param bool $remember |
160 | */ |
161 | public function setRememberUser( $remember ) { |
162 | $this->backend->setRememberUser( $remember ); |
163 | } |
164 | |
165 | /** |
166 | * Returns the request associated with this session |
167 | * @return WebRequest |
168 | */ |
169 | public function getRequest() { |
170 | return $this->backend->getRequest( $this->index ); |
171 | } |
172 | |
173 | /** |
174 | * Returns the authenticated user for this session |
175 | * @return User |
176 | */ |
177 | public function getUser(): User { |
178 | return $this->backend->getUser(); |
179 | } |
180 | |
181 | /** |
182 | * Fetch the rights allowed the user when this session is active. |
183 | * @return null|string[] Allowed user rights, or null to allow all. |
184 | */ |
185 | public function getAllowedUserRights() { |
186 | return $this->backend->getAllowedUserRights(); |
187 | } |
188 | |
189 | /** |
190 | * Fetch any restrictions imposed on logins or actions when this |
191 | * session is active. |
192 | * @return MWRestrictions|null |
193 | */ |
194 | public function getRestrictions(): ?MWRestrictions { |
195 | return $this->backend->getRestrictions(); |
196 | } |
197 | |
198 | /** |
199 | * Indicate whether the session user info can be changed |
200 | * @return bool |
201 | */ |
202 | public function canSetUser() { |
203 | return $this->backend->canSetUser(); |
204 | } |
205 | |
206 | /** |
207 | * Set a new user for this session |
208 | * @note This should only be called when the user has been authenticated |
209 | * @param User $user User to set on the session. |
210 | * This may become a "UserValue" in the future, or User may be refactored |
211 | * into such. |
212 | */ |
213 | public function setUser( $user ) { |
214 | $this->backend->setUser( $user ); |
215 | } |
216 | |
217 | /** |
218 | * Get a suggested username for the login form |
219 | * @return string|null |
220 | */ |
221 | public function suggestLoginUsername() { |
222 | return $this->backend->suggestLoginUsername( $this->index ); |
223 | } |
224 | |
225 | /** |
226 | * Get the expected value of the forceHTTPS cookie. This reflects whether |
227 | * session cookies were sent with the Secure attribute. If $wgForceHTTPS |
228 | * is true, the forceHTTPS cookie is not sent and this value is ignored. |
229 | * |
230 | * @return bool |
231 | */ |
232 | public function shouldForceHTTPS() { |
233 | return $this->backend->shouldForceHTTPS(); |
234 | } |
235 | |
236 | /** |
237 | * Set the value of the forceHTTPS cookie. This reflects whether session |
238 | * cookies were sent with the Secure attribute. If $wgForceHTTPS is true, |
239 | * the forceHTTPS cookie is not sent, and this value is ignored. |
240 | * |
241 | * @param bool $force |
242 | */ |
243 | public function setForceHTTPS( $force ) { |
244 | $this->backend->setForceHTTPS( $force ); |
245 | } |
246 | |
247 | /** |
248 | * Fetch the "logged out" timestamp |
249 | * @return int |
250 | */ |
251 | public function getLoggedOutTimestamp() { |
252 | return $this->backend->getLoggedOutTimestamp(); |
253 | } |
254 | |
255 | /** |
256 | * @param int $ts |
257 | */ |
258 | public function setLoggedOutTimestamp( $ts ) { |
259 | $this->backend->setLoggedOutTimestamp( $ts ); |
260 | } |
261 | |
262 | /** |
263 | * Fetch provider metadata |
264 | * @note For use by SessionProvider subclasses only |
265 | * @return mixed |
266 | */ |
267 | public function getProviderMetadata() { |
268 | return $this->backend->getProviderMetadata(); |
269 | } |
270 | |
271 | /** |
272 | * Delete all session data and clear the user (if possible) |
273 | */ |
274 | public function clear() { |
275 | $data = &$this->backend->getData(); |
276 | if ( $data ) { |
277 | $data = []; |
278 | $this->backend->dirty(); |
279 | } |
280 | if ( $this->backend->canSetUser() ) { |
281 | $this->backend->setUser( MediaWikiServices::getInstance()->getUserFactory()->newAnonymous() ); |
282 | } |
283 | $this->backend->save(); |
284 | } |
285 | |
286 | /** |
287 | * Resets the TTL in the backend store if the session is near expiring, and |
288 | * re-persists the session to any active WebRequests if persistent. |
289 | */ |
290 | public function renew() { |
291 | $this->backend->renew(); |
292 | } |
293 | |
294 | /** |
295 | * Fetch a copy of this session attached to an alternative WebRequest |
296 | * |
297 | * Actions on the copy will affect this session too, and vice versa. |
298 | * |
299 | * @param WebRequest $request Any existing session associated with this |
300 | * WebRequest object will be overwritten. |
301 | * @return Session |
302 | */ |
303 | public function sessionWithRequest( WebRequest $request ) { |
304 | $request->setSessionId( $this->backend->getSessionId() ); |
305 | return $this->backend->getSession( $request ); |
306 | } |
307 | |
308 | /** |
309 | * Fetch a value from the session |
310 | * @param string|int $key |
311 | * @param mixed|null $default Returned if $this->exists( $key ) would be false |
312 | * @return mixed |
313 | */ |
314 | public function get( $key, $default = null ) { |
315 | $data = &$this->backend->getData(); |
316 | return array_key_exists( $key, $data ) ? $data[$key] : $default; |
317 | } |
318 | |
319 | /** |
320 | * Test if a value exists in the session |
321 | * @note Unlike isset(), null values are considered to exist. |
322 | * @param string|int $key |
323 | * @return bool |
324 | */ |
325 | public function exists( $key ) { |
326 | $data = &$this->backend->getData(); |
327 | return array_key_exists( $key, $data ); |
328 | } |
329 | |
330 | /** |
331 | * Set a value in the session |
332 | * @param string|int $key |
333 | * @param mixed $value |
334 | */ |
335 | public function set( $key, $value ) { |
336 | $data = &$this->backend->getData(); |
337 | if ( !array_key_exists( $key, $data ) || $data[$key] !== $value ) { |
338 | $data[$key] = $value; |
339 | $this->backend->dirty(); |
340 | } |
341 | } |
342 | |
343 | /** |
344 | * Remove a value from the session |
345 | * @param string|int $key |
346 | */ |
347 | public function remove( $key ) { |
348 | $data = &$this->backend->getData(); |
349 | if ( array_key_exists( $key, $data ) ) { |
350 | unset( $data[$key] ); |
351 | $this->backend->dirty(); |
352 | } |
353 | } |
354 | |
355 | /** |
356 | * Check if a CSRF token is set for the session |
357 | * |
358 | * @since 1.37 |
359 | * @param string $key Token key |
360 | * @return bool |
361 | */ |
362 | public function hasToken( string $key = 'default' ): bool { |
363 | $secrets = $this->get( 'wsTokenSecrets' ); |
364 | if ( !is_array( $secrets ) ) { |
365 | return false; |
366 | } |
367 | return isset( $secrets[$key] ) && is_string( $secrets[$key] ); |
368 | } |
369 | |
370 | /** |
371 | * Fetch a CSRF token from the session |
372 | * |
373 | * Note that this does not persist the session, which you'll probably want |
374 | * to do if you want the token to actually be useful. |
375 | * |
376 | * @param string|string[] $salt Token salt |
377 | * @param string $key Token key |
378 | * @return Token |
379 | */ |
380 | public function getToken( $salt = '', $key = 'default' ) { |
381 | $new = false; |
382 | $secrets = $this->get( 'wsTokenSecrets' ); |
383 | if ( !is_array( $secrets ) ) { |
384 | $secrets = []; |
385 | } |
386 | if ( isset( $secrets[$key] ) && is_string( $secrets[$key] ) ) { |
387 | $secret = $secrets[$key]; |
388 | } else { |
389 | $secret = \MWCryptRand::generateHex( 32 ); |
390 | $secrets[$key] = $secret; |
391 | $this->set( 'wsTokenSecrets', $secrets ); |
392 | $new = true; |
393 | } |
394 | if ( is_array( $salt ) ) { |
395 | $salt = implode( '|', $salt ); |
396 | } |
397 | return new Token( $secret, (string)$salt, $new ); |
398 | } |
399 | |
400 | /** |
401 | * Remove a CSRF token from the session |
402 | * |
403 | * The next call to self::getToken() with $key will generate a new secret. |
404 | * |
405 | * @param string $key Token key |
406 | */ |
407 | public function resetToken( $key = 'default' ) { |
408 | $secrets = $this->get( 'wsTokenSecrets' ); |
409 | if ( is_array( $secrets ) && isset( $secrets[$key] ) ) { |
410 | unset( $secrets[$key] ); |
411 | $this->set( 'wsTokenSecrets', $secrets ); |
412 | } |
413 | } |
414 | |
415 | /** |
416 | * Remove all CSRF tokens from the session |
417 | */ |
418 | public function resetAllTokens() { |
419 | $this->remove( 'wsTokenSecrets' ); |
420 | } |
421 | |
422 | /** |
423 | * Fetch the secret keys for self::setSecret() and self::getSecret(). |
424 | * @return string[] Encryption key, HMAC key |
425 | */ |
426 | private function getSecretKeys() { |
427 | $mainConfig = MediaWikiServices::getInstance()->getMainConfig(); |
428 | $sessionSecret = $mainConfig->get( MainConfigNames::SessionSecret ); |
429 | $secretKey = $mainConfig->get( MainConfigNames::SecretKey ); |
430 | $sessionPbkdf2Iterations = $mainConfig->get( MainConfigNames::SessionPbkdf2Iterations ); |
431 | $wikiSecret = $sessionSecret ?: $secretKey; |
432 | $userSecret = $this->get( 'wsSessionSecret', null ); |
433 | if ( $userSecret === null ) { |
434 | $userSecret = \MWCryptRand::generateHex( 32 ); |
435 | $this->set( 'wsSessionSecret', $userSecret ); |
436 | } |
437 | $iterations = $this->get( 'wsSessionPbkdf2Iterations', null ); |
438 | if ( $iterations === null ) { |
439 | $iterations = $sessionPbkdf2Iterations; |
440 | $this->set( 'wsSessionPbkdf2Iterations', $iterations ); |
441 | } |
442 | |
443 | $keymats = openssl_pbkdf2( $wikiSecret, $userSecret, 64, $iterations, 'sha256' ); |
444 | return [ |
445 | substr( $keymats, 0, 32 ), |
446 | substr( $keymats, 32, 32 ), |
447 | ]; |
448 | } |
449 | |
450 | /** |
451 | * Decide what type of encryption to use, based on system capabilities. |
452 | * @return array |
453 | */ |
454 | private static function getEncryptionAlgorithm() { |
455 | if ( self::$encryptionAlgorithm === null ) { |
456 | if ( function_exists( 'openssl_encrypt' ) ) { |
457 | $methods = openssl_get_cipher_methods(); |
458 | if ( in_array( 'aes-256-ctr', $methods, true ) ) { |
459 | self::$encryptionAlgorithm = [ 'openssl', 'aes-256-ctr' ]; |
460 | return self::$encryptionAlgorithm; |
461 | } |
462 | if ( in_array( 'aes-256-cbc', $methods, true ) ) { |
463 | self::$encryptionAlgorithm = [ 'openssl', 'aes-256-cbc' ]; |
464 | return self::$encryptionAlgorithm; |
465 | } |
466 | } |
467 | |
468 | throw new BadMethodCallException( |
469 | 'Encryption is not available. You need to install the PHP OpenSSL extension.' |
470 | ); |
471 | } |
472 | |
473 | return self::$encryptionAlgorithm; |
474 | } |
475 | |
476 | /** |
477 | * Set a value in the session, encrypted |
478 | * |
479 | * This relies on the secrecy of $wgSecretKey (by default), or $wgSessionSecret. |
480 | * |
481 | * @param string|int $key |
482 | * @param mixed $value |
483 | */ |
484 | public function setSecret( $key, $value ) { |
485 | [ $encKey, $hmacKey ] = $this->getSecretKeys(); |
486 | $serialized = serialize( $value ); |
487 | |
488 | // The code for encryption (with OpenSSL) and sealing is taken from |
489 | // Chris Steipp's OATHAuthUtils class in Extension::OATHAuth. |
490 | |
491 | // Encrypt |
492 | $iv = random_bytes( 16 ); |
493 | $algorithm = self::getEncryptionAlgorithm(); |
494 | switch ( $algorithm[0] ) { |
495 | case 'openssl': |
496 | $ciphertext = openssl_encrypt( $serialized, $algorithm[1], $encKey, OPENSSL_RAW_DATA, $iv ); |
497 | if ( $ciphertext === false ) { |
498 | throw new \UnexpectedValueException( 'Encryption failed: ' . openssl_error_string() ); |
499 | } |
500 | break; |
501 | default: |
502 | throw new LogicException( 'invalid algorithm' ); |
503 | } |
504 | |
505 | // Seal |
506 | $sealed = base64_encode( $iv ) . '.' . base64_encode( $ciphertext ); |
507 | $hmac = hash_hmac( 'sha256', $sealed, $hmacKey, true ); |
508 | $encrypted = base64_encode( $hmac ) . '.' . $sealed; |
509 | |
510 | // Store |
511 | $this->set( $key, $encrypted ); |
512 | } |
513 | |
514 | /** |
515 | * Fetch a value from the session that was set with self::setSecret() |
516 | * @param string|int $key |
517 | * @param mixed|null $default Returned if $this->exists( $key ) would be false or decryption fails |
518 | * @return mixed |
519 | */ |
520 | public function getSecret( $key, $default = null ) { |
521 | // Fetch |
522 | $encrypted = $this->get( $key, null ); |
523 | if ( $encrypted === null ) { |
524 | return $default; |
525 | } |
526 | |
527 | // The code for unsealing, checking, and decrypting (with OpenSSL) is |
528 | // taken from Chris Steipp's OATHAuthUtils class in |
529 | // Extension::OATHAuth. |
530 | |
531 | // Unseal and check |
532 | $pieces = explode( '.', $encrypted, 4 ); |
533 | if ( count( $pieces ) !== 3 ) { |
534 | $ex = new RuntimeException( 'Invalid sealed-secret format' ); |
535 | $this->logger->warning( $ex->getMessage(), [ 'exception' => $ex ] ); |
536 | return $default; |
537 | } |
538 | [ $hmac, $iv, $ciphertext ] = $pieces; |
539 | [ $encKey, $hmacKey ] = $this->getSecretKeys(); |
540 | $integCalc = hash_hmac( 'sha256', $iv . '.' . $ciphertext, $hmacKey, true ); |
541 | if ( !hash_equals( $integCalc, base64_decode( $hmac ) ) ) { |
542 | $ex = new RuntimeException( 'Sealed secret has been tampered with, aborting.' ); |
543 | $this->logger->warning( $ex->getMessage(), [ 'exception' => $ex ] ); |
544 | return $default; |
545 | } |
546 | |
547 | // Decrypt |
548 | $algorithm = self::getEncryptionAlgorithm(); |
549 | switch ( $algorithm[0] ) { |
550 | case 'openssl': |
551 | $serialized = openssl_decrypt( base64_decode( $ciphertext ), $algorithm[1], $encKey, |
552 | OPENSSL_RAW_DATA, base64_decode( $iv ) ); |
553 | if ( $serialized === false ) { |
554 | $ex = new RuntimeException( 'Decyption failed: ' . openssl_error_string() ); |
555 | $this->logger->debug( $ex->getMessage(), [ 'exception' => $ex ] ); |
556 | return $default; |
557 | } |
558 | break; |
559 | default: |
560 | throw new \LogicException( 'invalid algorithm' ); |
561 | } |
562 | |
563 | $value = unserialize( $serialized ); |
564 | if ( $value === false && $serialized !== serialize( false ) ) { |
565 | $value = $default; |
566 | } |
567 | return $value; |
568 | } |
569 | |
570 | /** |
571 | * Delay automatic saving while multiple updates are being made |
572 | * |
573 | * Calls to save() or clear() will not be delayed. |
574 | * |
575 | * @return \Wikimedia\ScopedCallback When this goes out of scope, a save will be triggered |
576 | */ |
577 | public function delaySave() { |
578 | return $this->backend->delaySave(); |
579 | } |
580 | |
581 | /** |
582 | * This will update the backend data and might re-persist the session |
583 | * if needed. |
584 | */ |
585 | public function save() { |
586 | $this->backend->save(); |
587 | } |
588 | |
589 | // region Interface methods |
590 | /** @name Interface methods |
591 | * @{ |
592 | */ |
593 | |
594 | /** @inheritDoc */ |
595 | public function count(): int { |
596 | $data = &$this->backend->getData(); |
597 | return count( $data ); |
598 | } |
599 | |
600 | /** @inheritDoc */ |
601 | #[\ReturnTypeWillChange] |
602 | public function current() { |
603 | $data = &$this->backend->getData(); |
604 | return current( $data ); |
605 | } |
606 | |
607 | /** @inheritDoc */ |
608 | #[\ReturnTypeWillChange] |
609 | public function key() { |
610 | $data = &$this->backend->getData(); |
611 | return key( $data ); |
612 | } |
613 | |
614 | /** @inheritDoc */ |
615 | public function next(): void { |
616 | $data = &$this->backend->getData(); |
617 | next( $data ); |
618 | } |
619 | |
620 | /** @inheritDoc */ |
621 | public function rewind(): void { |
622 | $data = &$this->backend->getData(); |
623 | reset( $data ); |
624 | } |
625 | |
626 | /** @inheritDoc */ |
627 | public function valid(): bool { |
628 | $data = &$this->backend->getData(); |
629 | return key( $data ) !== null; |
630 | } |
631 | |
632 | /** |
633 | * @note Despite the name, this seems to be intended to implement isset() |
634 | * rather than array_key_exists(). So do that. |
635 | * @inheritDoc |
636 | */ |
637 | public function offsetExists( $offset ): bool { |
638 | $data = &$this->backend->getData(); |
639 | return isset( $data[$offset] ); |
640 | } |
641 | |
642 | /** |
643 | * @note This supports indirect modifications but can't mark the session |
644 | * dirty when those happen. SessionBackend::save() checks the hash of the |
645 | * data to detect such changes. |
646 | * @note Accessing a nonexistent key via this mechanism causes that key to |
647 | * be created with a null value, and does not raise a PHP warning. |
648 | * @inheritDoc |
649 | */ |
650 | #[\ReturnTypeWillChange] |
651 | public function &offsetGet( $offset ) { |
652 | $data = &$this->backend->getData(); |
653 | if ( !array_key_exists( $offset, $data ) ) { |
654 | $ex = new LogicException( "Undefined index (auto-adds to session with a null value): $offset" ); |
655 | $this->logger->debug( $ex->getMessage(), [ 'exception' => $ex ] ); |
656 | } |
657 | return $data[$offset]; |
658 | } |
659 | |
660 | /** @inheritDoc */ |
661 | public function offsetSet( $offset, $value ): void { |
662 | $this->set( $offset, $value ); |
663 | } |
664 | |
665 | /** @inheritDoc */ |
666 | public function offsetUnset( $offset ): void { |
667 | $this->remove( $offset ); |
668 | } |
669 | |
670 | /** @} */ |
671 | // endregion -- end of Interface methods |
672 | } |