MediaWiki REL1_39
Session.php
Go to the documentation of this file.
1<?php
24namespace MediaWiki\Session;
25
28use Psr\Log\LoggerInterface;
29use User;
30use WebRequest;
31
50class Session implements \Countable, \Iterator, \ArrayAccess {
52 private static $encryptionAlgorithm = null;
53
55 private $backend;
56
58 private $index;
59
61 private $logger;
62
68 public function __construct( SessionBackend $backend, $index, LoggerInterface $logger ) {
69 $this->backend = $backend;
70 $this->index = $index;
71 $this->logger = $logger;
72 }
73
74 public function __destruct() {
75 $this->backend->deregisterSession( $this->index );
76 }
77
82 public function getId() {
83 return $this->backend->getId();
84 }
85
91 public function getSessionId() {
92 return $this->backend->getSessionId();
93 }
94
99 public function resetId() {
100 return $this->backend->resetId();
101 }
102
107 public function getProvider() {
108 return $this->backend->getProvider();
109 }
110
118 public function isPersistent() {
119 return $this->backend->isPersistent();
120 }
121
128 public function persist() {
129 $this->backend->persist();
130 }
131
140 public function unpersist() {
141 $this->backend->unpersist();
142 }
143
149 public function shouldRememberUser() {
150 return $this->backend->shouldRememberUser();
151 }
152
158 public function setRememberUser( $remember ) {
159 $this->backend->setRememberUser( $remember );
160 }
161
166 public function getRequest() {
167 return $this->backend->getRequest( $this->index );
168 }
169
174 public function getUser() {
175 return $this->backend->getUser();
176 }
177
182 public function getAllowedUserRights() {
183 return $this->backend->getAllowedUserRights();
184 }
185
190 public function canSetUser() {
191 return $this->backend->canSetUser();
192 }
193
201 public function setUser( $user ) {
202 $this->backend->setUser( $user );
203 }
204
209 public function suggestLoginUsername() {
210 return $this->backend->suggestLoginUsername( $this->index );
211 }
212
220 public function shouldForceHTTPS() {
221 return $this->backend->shouldForceHTTPS();
222 }
223
231 public function setForceHTTPS( $force ) {
232 $this->backend->setForceHTTPS( $force );
233 }
234
239 public function getLoggedOutTimestamp() {
240 return $this->backend->getLoggedOutTimestamp();
241 }
242
246 public function setLoggedOutTimestamp( $ts ) {
247 $this->backend->setLoggedOutTimestamp( $ts );
248 }
249
255 public function getProviderMetadata() {
256 return $this->backend->getProviderMetadata();
257 }
258
262 public function clear() {
263 $data = &$this->backend->getData();
264 if ( $data ) {
265 $data = [];
266 $this->backend->dirty();
267 }
268 if ( $this->backend->canSetUser() ) {
269 $this->backend->setUser( new User );
270 }
271 $this->backend->save();
272 }
273
278 public function renew() {
279 $this->backend->renew();
280 }
281
291 public function sessionWithRequest( WebRequest $request ) {
292 $request->setSessionId( $this->backend->getSessionId() );
293 return $this->backend->getSession( $request );
294 }
295
302 public function get( $key, $default = null ) {
303 $data = &$this->backend->getData();
304 return array_key_exists( $key, $data ) ? $data[$key] : $default;
305 }
306
313 public function exists( $key ) {
314 $data = &$this->backend->getData();
315 return array_key_exists( $key, $data );
316 }
317
323 public function set( $key, $value ) {
324 $data = &$this->backend->getData();
325 if ( !array_key_exists( $key, $data ) || $data[$key] !== $value ) {
326 $data[$key] = $value;
327 $this->backend->dirty();
328 }
329 }
330
335 public function remove( $key ) {
336 $data = &$this->backend->getData();
337 if ( array_key_exists( $key, $data ) ) {
338 unset( $data[$key] );
339 $this->backend->dirty();
340 }
341 }
342
350 public function hasToken( string $key = 'default' ): bool {
351 $secrets = $this->get( 'wsTokenSecrets' );
352 if ( !is_array( $secrets ) ) {
353 return false;
354 }
355 return isset( $secrets[$key] ) && is_string( $secrets[$key] );
356 }
357
368 public function getToken( $salt = '', $key = 'default' ) {
369 $new = false;
370 $secrets = $this->get( 'wsTokenSecrets' );
371 if ( !is_array( $secrets ) ) {
372 $secrets = [];
373 }
374 if ( isset( $secrets[$key] ) && is_string( $secrets[$key] ) ) {
375 $secret = $secrets[$key];
376 } else {
377 $secret = \MWCryptRand::generateHex( 32 );
378 $secrets[$key] = $secret;
379 $this->set( 'wsTokenSecrets', $secrets );
380 $new = true;
381 }
382 if ( is_array( $salt ) ) {
383 $salt = implode( '|', $salt );
384 }
385 return new Token( $secret, (string)$salt, $new );
386 }
387
395 public function resetToken( $key = 'default' ) {
396 $secrets = $this->get( 'wsTokenSecrets' );
397 if ( is_array( $secrets ) && isset( $secrets[$key] ) ) {
398 unset( $secrets[$key] );
399 $this->set( 'wsTokenSecrets', $secrets );
400 }
401 }
402
406 public function resetAllTokens() {
407 $this->remove( 'wsTokenSecrets' );
408 }
409
414 private function getSecretKeys() {
415 $mainConfig = MediaWikiServices::getInstance()->getMainConfig();
416 $sessionSecret = $mainConfig->get( MainConfigNames::SessionSecret );
417 $secretKey = $mainConfig->get( MainConfigNames::SecretKey );
418 $sessionPbkdf2Iterations = $mainConfig->get( MainConfigNames::SessionPbkdf2Iterations );
419 $wikiSecret = $sessionSecret ?: $secretKey;
420 $userSecret = $this->get( 'wsSessionSecret', null );
421 if ( $userSecret === null ) {
422 $userSecret = \MWCryptRand::generateHex( 32 );
423 $this->set( 'wsSessionSecret', $userSecret );
424 }
425 $iterations = $this->get( 'wsSessionPbkdf2Iterations', null );
426 if ( $iterations === null ) {
427 $iterations = $sessionPbkdf2Iterations;
428 $this->set( 'wsSessionPbkdf2Iterations', $iterations );
429 }
430
431 $keymats = hash_pbkdf2( 'sha256', $wikiSecret, $userSecret, $iterations, 64, true );
432 return [
433 substr( $keymats, 0, 32 ),
434 substr( $keymats, 32, 32 ),
435 ];
436 }
437
442 private static function getEncryptionAlgorithm() {
443 $sessionInsecureSecrets = MediaWikiServices::getInstance()->getMainConfig()
445
446 if ( self::$encryptionAlgorithm === null ) {
447 if ( function_exists( 'openssl_encrypt' ) ) {
448 $methods = openssl_get_cipher_methods();
449 if ( in_array( 'aes-256-ctr', $methods, true ) ) {
450 self::$encryptionAlgorithm = [ 'openssl', 'aes-256-ctr' ];
451 return self::$encryptionAlgorithm;
452 }
453 if ( in_array( 'aes-256-cbc', $methods, true ) ) {
454 self::$encryptionAlgorithm = [ 'openssl', 'aes-256-cbc' ];
455 return self::$encryptionAlgorithm;
456 }
457 }
458
459 if ( $sessionInsecureSecrets ) {
460 // @todo: import a pure-PHP library for AES instead of this
461 self::$encryptionAlgorithm = [ 'insecure' ];
462 return self::$encryptionAlgorithm;
463 }
464
465 throw new \BadMethodCallException(
466 'Encryption is not available. You really should install the PHP OpenSSL extension. ' .
467 'But if you really can\'t and you\'re willing ' .
468 'to accept insecure storage of sensitive session data, set ' .
469 '$wgSessionInsecureSecrets = true in LocalSettings.php to make this exception go away.'
470 );
471 }
472
473 return self::$encryptionAlgorithm;
474 }
475
484 public function setSecret( $key, $value ) {
485 list( $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 // @todo: import a pure-PHP library for AES instead of doing $wgSessionInsecureSecrets
493 $iv = random_bytes( 16 );
494 $algorithm = self::getEncryptionAlgorithm();
495 switch ( $algorithm[0] ) {
496 case 'openssl':
497 $ciphertext = openssl_encrypt( $serialized, $algorithm[1], $encKey, OPENSSL_RAW_DATA, $iv );
498 if ( $ciphertext === false ) {
499 throw new \UnexpectedValueException( 'Encryption failed: ' . openssl_error_string() );
500 }
501 break;
502 case 'insecure':
503 $ex = new \Exception( 'No encryption is available, storing data as plain text' );
504 $this->logger->warning( $ex->getMessage(), [ 'exception' => $ex ] );
505 $ciphertext = $serialized;
506 break;
507 default:
508 throw new \LogicException( 'invalid algorithm' );
509 }
510
511 // Seal
512 $sealed = base64_encode( $iv ) . '.' . base64_encode( $ciphertext );
513 $hmac = hash_hmac( 'sha256', $sealed, $hmacKey, true );
514 $encrypted = base64_encode( $hmac ) . '.' . $sealed;
515
516 // Store
517 $this->set( $key, $encrypted );
518 }
519
526 public function getSecret( $key, $default = null ) {
527 // Fetch
528 $encrypted = $this->get( $key, null );
529 if ( $encrypted === null ) {
530 return $default;
531 }
532
533 // The code for unsealing, checking, and decrypting (with OpenSSL) is
534 // taken from Chris Steipp's OATHAuthUtils class in
535 // Extension::OATHAuth.
536
537 // Unseal and check
538 $pieces = explode( '.', $encrypted, 4 );
539 if ( count( $pieces ) !== 3 ) {
540 $ex = new \Exception( 'Invalid sealed-secret format' );
541 $this->logger->warning( $ex->getMessage(), [ 'exception' => $ex ] );
542 return $default;
543 }
544 list( $hmac, $iv, $ciphertext ) = $pieces;
545 list( $encKey, $hmacKey ) = $this->getSecretKeys();
546 $integCalc = hash_hmac( 'sha256', $iv . '.' . $ciphertext, $hmacKey, true );
547 if ( !hash_equals( $integCalc, base64_decode( $hmac ) ) ) {
548 $ex = new \Exception( 'Sealed secret has been tampered with, aborting.' );
549 $this->logger->warning( $ex->getMessage(), [ 'exception' => $ex ] );
550 return $default;
551 }
552
553 // Decrypt
554 $algorithm = self::getEncryptionAlgorithm();
555 switch ( $algorithm[0] ) {
556 case 'openssl':
557 $serialized = openssl_decrypt( base64_decode( $ciphertext ), $algorithm[1], $encKey,
558 OPENSSL_RAW_DATA, base64_decode( $iv ) );
559 if ( $serialized === false ) {
560 $ex = new \Exception( 'Decyption failed: ' . openssl_error_string() );
561 $this->logger->debug( $ex->getMessage(), [ 'exception' => $ex ] );
562 return $default;
563 }
564 break;
565 case 'insecure':
566 $ex = new \Exception(
567 'No encryption is available, retrieving data that was stored as plain text'
568 );
569 $this->logger->warning( $ex->getMessage(), [ 'exception' => $ex ] );
570 $serialized = base64_decode( $ciphertext );
571 break;
572 default:
573 throw new \LogicException( 'invalid algorithm' );
574 }
575
576 $value = unserialize( $serialized );
577 if ( $value === false && $serialized !== serialize( false ) ) {
578 $value = $default;
579 }
580 return $value;
581 }
582
590 public function delaySave() {
591 return $this->backend->delaySave();
592 }
593
598 public function save() {
599 $this->backend->save();
600 }
601
602 // region Interface methods
608 public function count(): int {
609 $data = &$this->backend->getData();
610 return count( $data );
611 }
612
614 #[\ReturnTypeWillChange]
615 public function current() {
616 $data = &$this->backend->getData();
617 return current( $data );
618 }
619
621 #[\ReturnTypeWillChange]
622 public function key() {
623 $data = &$this->backend->getData();
624 return key( $data );
625 }
626
628 public function next(): void {
629 $data = &$this->backend->getData();
630 next( $data );
631 }
632
634 public function rewind(): void {
635 $data = &$this->backend->getData();
636 reset( $data );
637 }
638
640 public function valid(): bool {
641 $data = &$this->backend->getData();
642 return key( $data ) !== null;
643 }
644
650 public function offsetExists( $offset ): bool {
651 $data = &$this->backend->getData();
652 return isset( $data[$offset] );
653 }
654
663 #[\ReturnTypeWillChange]
664 public function &offsetGet( $offset ) {
665 $data = &$this->backend->getData();
666 if ( !array_key_exists( $offset, $data ) ) {
667 $ex = new \Exception( "Undefined index (auto-adds to session with a null value): $offset" );
668 $this->logger->debug( $ex->getMessage(), [ 'exception' => $ex ] );
669 }
670 return $data[$offset];
671 }
672
674 public function offsetSet( $offset, $value ): void {
675 $this->set( $offset, $value );
676 }
677
679 public function offsetUnset( $offset ): void {
680 $this->remove( $offset );
681 }
682
684 // endregion -- end of Interface methods
685}
serialize()
unserialize( $serialized)
A class containing constants representing the names of configuration variables.
const SessionSecret
Name constant for the SessionSecret setting, for use with Config::get()
const SessionPbkdf2Iterations
Name constant for the SessionPbkdf2Iterations setting, for use with Config::get()
const SessionInsecureSecrets
Name constant for the SessionInsecureSecrets setting, for use with Config::get()
const SecretKey
Name constant for the SecretKey setting, for use with Config::get()
Service locator for MediaWiki core services.
static getInstance()
Returns the global default instance of the top level service locator.
This is the actual workhorse for Session.
Manages data for an authenticated session.
Definition Session.php:50
offsetSet( $offset, $value)
Definition Session.php:674
delaySave()
Delay automatic saving while multiple updates are being made.
Definition Session.php:590
suggestLoginUsername()
Get a suggested username for the login form.
Definition Session.php:209
setForceHTTPS( $force)
Set the value of the forceHTTPS cookie.
Definition Session.php:231
getToken( $salt='', $key='default')
Fetch a CSRF token from the session.
Definition Session.php:368
setSecret( $key, $value)
Set a value in the session, encrypted.
Definition Session.php:484
persist()
Make this session persisted across requests.
Definition Session.php:128
shouldForceHTTPS()
Get the expected value of the forceHTTPS cookie.
Definition Session.php:220
renew()
Resets the TTL in the backend store if the session is near expiring, and re-persists the session to a...
Definition Session.php:278
getSecret( $key, $default=null)
Fetch a value from the session that was set with self::setSecret()
Definition Session.php:526
resetId()
Changes the session ID.
Definition Session.php:99
getProvider()
Fetch the SessionProvider for this session.
Definition Session.php:107
isPersistent()
Indicate whether this session is persisted across requests.
Definition Session.php:118
getRequest()
Returns the request associated with this session.
Definition Session.php:166
getProviderMetadata()
Fetch provider metadata.
Definition Session.php:255
resetToken( $key='default')
Remove a CSRF token from the session.
Definition Session.php:395
sessionWithRequest(WebRequest $request)
Fetch a copy of this session attached to an alternative WebRequest.
Definition Session.php:291
save()
This will update the backend data and might re-persist the session if needed.
Definition Session.php:598
unpersist()
Make this session not be persisted across requests.
Definition Session.php:140
canSetUser()
Indicate whether the session user info can be changed.
Definition Session.php:190
exists( $key)
Test if a value exists in the session.
Definition Session.php:313
getLoggedOutTimestamp()
Fetch the "logged out" timestamp.
Definition Session.php:239
getUser()
Returns the authenticated user for this session.
Definition Session.php:174
getId()
Returns the session ID.
Definition Session.php:82
setRememberUser( $remember)
Set whether the user should be remembered independently of the session ID.
Definition Session.php:158
resetAllTokens()
Remove all CSRF tokens from the session.
Definition Session.php:406
get( $key, $default=null)
Fetch a value from the session.
Definition Session.php:302
hasToken(string $key='default')
Check if a CSRF token is set for the session.
Definition Session.php:350
clear()
Delete all session data and clear the user (if possible)
Definition Session.php:262
shouldRememberUser()
Indicate whether the user should be remembered independently of the session ID.
Definition Session.php:149
setUser( $user)
Set a new user for this session.
Definition Session.php:201
getSessionId()
Returns the SessionId object.
Definition Session.php:91
getAllowedUserRights()
Fetch the rights allowed the user when this session is active.
Definition Session.php:182
__construct(SessionBackend $backend, $index, LoggerInterface $logger)
Definition Session.php:68
Value object representing a CSRF token.
Definition Token.php:32
internal since 1.36
Definition User.php:70
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form stripping il...
setSessionId(SessionId $sessionId)
Set the session for this request.
foreach( $res as $row) $serialized