MediaWiki REL1_30
Session.php
Go to the documentation of this file.
1<?php
24namespace MediaWiki\Session;
25
26use Psr\Log\LoggerInterface;
27use User;
28use WebRequest;
29
48final class Session implements \Countable, \Iterator, \ArrayAccess {
50 private static $encryptionAlgorithm = null;
51
53 private $backend;
54
56 private $index;
57
59 private $logger;
60
66 public function __construct( SessionBackend $backend, $index, LoggerInterface $logger ) {
67 $this->backend = $backend;
68 $this->index = $index;
69 $this->logger = $logger;
70 }
71
72 public function __destruct() {
73 $this->backend->deregisterSession( $this->index );
74 }
75
80 public function getId() {
81 return $this->backend->getId();
82 }
83
89 public function getSessionId() {
90 return $this->backend->getSessionId();
91 }
92
97 public function resetId() {
98 return $this->backend->resetId();
99 }
100
105 public function getProvider() {
106 return $this->backend->getProvider();
107 }
108
116 public function isPersistent() {
117 return $this->backend->isPersistent();
118 }
119
126 public function persist() {
127 $this->backend->persist();
128 }
129
138 public function unpersist() {
139 $this->backend->unpersist();
140 }
141
147 public function shouldRememberUser() {
148 return $this->backend->shouldRememberUser();
149 }
150
156 public function setRememberUser( $remember ) {
157 $this->backend->setRememberUser( $remember );
158 }
159
164 public function getRequest() {
165 return $this->backend->getRequest( $this->index );
166 }
167
172 public function getUser() {
173 return $this->backend->getUser();
174 }
175
180 public function getAllowedUserRights() {
181 return $this->backend->getAllowedUserRights();
182 }
183
188 public function canSetUser() {
189 return $this->backend->canSetUser();
190 }
191
199 public function setUser( $user ) {
200 $this->backend->setUser( $user );
201 }
202
207 public function suggestLoginUsername() {
208 return $this->backend->suggestLoginUsername( $this->index );
209 }
210
215 public function shouldForceHTTPS() {
216 return $this->backend->shouldForceHTTPS();
217 }
218
223 public function setForceHTTPS( $force ) {
224 $this->backend->setForceHTTPS( $force );
225 }
226
231 public function getLoggedOutTimestamp() {
232 return $this->backend->getLoggedOutTimestamp();
233 }
234
239 public function setLoggedOutTimestamp( $ts ) {
240 $this->backend->setLoggedOutTimestamp( $ts );
241 }
242
248 public function getProviderMetadata() {
249 return $this->backend->getProviderMetadata();
250 }
251
255 public function clear() {
256 $data = &$this->backend->getData();
257 if ( $data ) {
258 $data = [];
259 $this->backend->dirty();
260 }
261 if ( $this->backend->canSetUser() ) {
262 $this->backend->setUser( new User );
263 }
264 $this->backend->save();
265 }
266
273 public function renew() {
274 $this->backend->renew();
275 }
276
287 $request->setSessionId( $this->backend->getSessionId() );
288 return $this->backend->getSession( $request );
289 }
290
297 public function get( $key, $default = null ) {
298 $data = &$this->backend->getData();
299 return array_key_exists( $key, $data ) ? $data[$key] : $default;
300 }
301
308 public function exists( $key ) {
309 $data = &$this->backend->getData();
310 return array_key_exists( $key, $data );
311 }
312
318 public function set( $key, $value ) {
319 $data = &$this->backend->getData();
320 if ( !array_key_exists( $key, $data ) || $data[$key] !== $value ) {
321 $data[$key] = $value;
322 $this->backend->dirty();
323 }
324 }
325
330 public function remove( $key ) {
331 $data = &$this->backend->getData();
332 if ( array_key_exists( $key, $data ) ) {
333 unset( $data[$key] );
334 $this->backend->dirty();
335 }
336 }
337
348 public function getToken( $salt = '', $key = 'default' ) {
349 $new = false;
350 $secrets = $this->get( 'wsTokenSecrets' );
351 if ( !is_array( $secrets ) ) {
352 $secrets = [];
353 }
354 if ( isset( $secrets[$key] ) && is_string( $secrets[$key] ) ) {
355 $secret = $secrets[$key];
356 } else {
357 $secret = \MWCryptRand::generateHex( 32 );
358 $secrets[$key] = $secret;
359 $this->set( 'wsTokenSecrets', $secrets );
360 $new = true;
361 }
362 if ( is_array( $salt ) ) {
363 $salt = implode( '|', $salt );
364 }
365 return new Token( $secret, (string)$salt, $new );
366 }
367
375 public function resetToken( $key = 'default' ) {
376 $secrets = $this->get( 'wsTokenSecrets' );
377 if ( is_array( $secrets ) && isset( $secrets[$key] ) ) {
378 unset( $secrets[$key] );
379 $this->set( 'wsTokenSecrets', $secrets );
380 }
381 }
382
386 public function resetAllTokens() {
387 $this->remove( 'wsTokenSecrets' );
388 }
389
394 private function getSecretKeys() {
396
397 $wikiSecret = $wgSessionSecret ?: $wgSecretKey;
398 $userSecret = $this->get( 'wsSessionSecret', null );
399 if ( $userSecret === null ) {
400 $userSecret = \MWCryptRand::generateHex( 32 );
401 $this->set( 'wsSessionSecret', $userSecret );
402 }
403 $iterations = $this->get( 'wsSessionPbkdf2Iterations', null );
404 if ( $iterations === null ) {
405 $iterations = $wgSessionPbkdf2Iterations;
406 $this->set( 'wsSessionPbkdf2Iterations', $iterations );
407 }
408
409 $keymats = hash_pbkdf2( 'sha256', $wikiSecret, $userSecret, $iterations, 64, true );
410 return [
411 substr( $keymats, 0, 32 ),
412 substr( $keymats, 32, 32 ),
413 ];
414 }
415
420 private static function getEncryptionAlgorithm() {
422
423 if ( self::$encryptionAlgorithm === null ) {
424 if ( function_exists( 'openssl_encrypt' ) ) {
425 $methods = openssl_get_cipher_methods();
426 if ( in_array( 'aes-256-ctr', $methods, true ) ) {
427 self::$encryptionAlgorithm = [ 'openssl', 'aes-256-ctr' ];
429 }
430 if ( in_array( 'aes-256-cbc', $methods, true ) ) {
431 self::$encryptionAlgorithm = [ 'openssl', 'aes-256-cbc' ];
433 }
434 }
435
436 if ( function_exists( 'mcrypt_encrypt' )
437 && in_array( 'rijndael-128', mcrypt_list_algorithms(), true )
438 ) {
439 $modes = mcrypt_list_modes();
440 if ( in_array( 'ctr', $modes, true ) ) {
441 self::$encryptionAlgorithm = [ 'mcrypt', 'rijndael-128', 'ctr' ];
443 }
444 if ( in_array( 'cbc', $modes, true ) ) {
445 self::$encryptionAlgorithm = [ 'mcrypt', 'rijndael-128', 'cbc' ];
447 }
448 }
449
451 // @todo: import a pure-PHP library for AES instead of this
452 self::$encryptionAlgorithm = [ 'insecure' ];
454 }
455
456 throw new \BadMethodCallException(
457 'Encryption is not available. You really should install the PHP OpenSSL extension, ' .
458 'or failing that the mcrypt extension. But if you really can\'t and you\'re willing ' .
459 'to accept insecure storage of sensitive session data, set ' .
460 '$wgSessionInsecureSecrets = true in LocalSettings.php to make this exception go away.'
461 );
462 }
463
465 }
466
475 public function setSecret( $key, $value ) {
476 list( $encKey, $hmacKey ) = $this->getSecretKeys();
478
479 // The code for encryption (with OpenSSL) and sealing is taken from
480 // Chris Steipp's OATHAuthUtils class in Extension::OATHAuth.
481
482 // Encrypt
483 // @todo: import a pure-PHP library for AES instead of doing $wgSessionInsecureSecrets
484 $iv = \MWCryptRand::generate( 16, true );
485 $algorithm = self::getEncryptionAlgorithm();
486 switch ( $algorithm[0] ) {
487 case 'openssl':
488 $ciphertext = openssl_encrypt( $serialized, $algorithm[1], $encKey, OPENSSL_RAW_DATA, $iv );
489 if ( $ciphertext === false ) {
490 throw new \UnexpectedValueException( 'Encryption failed: ' . openssl_error_string() );
491 }
492 break;
493 case 'mcrypt':
494 // PKCS7 padding
495 $blocksize = mcrypt_get_block_size( $algorithm[1], $algorithm[2] );
496 $pad = $blocksize - ( strlen( $serialized ) % $blocksize );
497 $serialized .= str_repeat( chr( $pad ), $pad );
498
499 $ciphertext = mcrypt_encrypt( $algorithm[1], $encKey, $serialized, $algorithm[2], $iv );
500 if ( $ciphertext === false ) {
501 throw new \UnexpectedValueException( 'Encryption failed' );
502 }
503 break;
504 case 'insecure':
505 $ex = new \Exception( 'No encryption is available, storing data as plain text' );
506 $this->logger->warning( $ex->getMessage(), [ 'exception' => $ex ] );
507 $ciphertext = $serialized;
508 break;
509 default:
510 throw new \LogicException( 'invalid algorithm' );
511 }
512
513 // Seal
514 $sealed = base64_encode( $iv ) . '.' . base64_encode( $ciphertext );
515 $hmac = hash_hmac( 'sha256', $sealed, $hmacKey, true );
516 $encrypted = base64_encode( $hmac ) . '.' . $sealed;
517
518 // Store
519 $this->set( $key, $encrypted );
520 }
521
528 public function getSecret( $key, $default = null ) {
529 // Fetch
530 $encrypted = $this->get( $key, null );
531 if ( $encrypted === null ) {
532 return $default;
533 }
534
535 // The code for unsealing, checking, and decrypting (with OpenSSL) is
536 // taken from Chris Steipp's OATHAuthUtils class in
537 // Extension::OATHAuth.
538
539 // Unseal and check
540 $pieces = explode( '.', $encrypted );
541 if ( count( $pieces ) !== 3 ) {
542 $ex = new \Exception( 'Invalid sealed-secret format' );
543 $this->logger->warning( $ex->getMessage(), [ 'exception' => $ex ] );
544 return $default;
545 }
546 list( $hmac, $iv, $ciphertext ) = $pieces;
547 list( $encKey, $hmacKey ) = $this->getSecretKeys();
548 $integCalc = hash_hmac( 'sha256', $iv . '.' . $ciphertext, $hmacKey, true );
549 if ( !hash_equals( $integCalc, base64_decode( $hmac ) ) ) {
550 $ex = new \Exception( 'Sealed secret has been tampered with, aborting.' );
551 $this->logger->warning( $ex->getMessage(), [ 'exception' => $ex ] );
552 return $default;
553 }
554
555 // Decrypt
556 $algorithm = self::getEncryptionAlgorithm();
557 switch ( $algorithm[0] ) {
558 case 'openssl':
559 $serialized = openssl_decrypt( base64_decode( $ciphertext ), $algorithm[1], $encKey,
560 OPENSSL_RAW_DATA, base64_decode( $iv ) );
561 if ( $serialized === false ) {
562 $ex = new \Exception( 'Decyption failed: ' . openssl_error_string() );
563 $this->logger->debug( $ex->getMessage(), [ 'exception' => $ex ] );
564 return $default;
565 }
566 break;
567 case 'mcrypt':
568 $serialized = mcrypt_decrypt( $algorithm[1], $encKey, base64_decode( $ciphertext ),
569 $algorithm[2], base64_decode( $iv ) );
570 if ( $serialized === false ) {
571 $ex = new \Exception( 'Decyption failed' );
572 $this->logger->debug( $ex->getMessage(), [ 'exception' => $ex ] );
573 return $default;
574 }
575
576 // Remove PKCS7 padding
577 $pad = ord( substr( $serialized, -1 ) );
578 $serialized = substr( $serialized, 0, -$pad );
579 break;
580 case 'insecure':
581 $ex = new \Exception(
582 'No encryption is available, retrieving data that was stored as plain text'
583 );
584 $this->logger->warning( $ex->getMessage(), [ 'exception' => $ex ] );
585 $serialized = base64_decode( $ciphertext );
586 break;
587 default:
588 throw new \LogicException( 'invalid algorithm' );
589 }
590
592 if ( $value === false && $serialized !== serialize( false ) ) {
593 $value = $default;
594 }
595 return $value;
596 }
597
605 public function delaySave() {
606 return $this->backend->delaySave();
607 }
608
615 public function save() {
616 $this->backend->save();
617 }
618
624 public function count() {
625 $data = &$this->backend->getData();
626 return count( $data );
627 }
628
629 public function current() {
630 $data = &$this->backend->getData();
631 return current( $data );
632 }
633
634 public function key() {
635 $data = &$this->backend->getData();
636 return key( $data );
637 }
638
639 public function next() {
640 $data = &$this->backend->getData();
641 next( $data );
642 }
643
644 public function rewind() {
645 $data = &$this->backend->getData();
646 reset( $data );
647 }
648
649 public function valid() {
650 $data = &$this->backend->getData();
651 return key( $data ) !== null;
652 }
653
659 public function offsetExists( $offset ) {
660 $data = &$this->backend->getData();
661 return isset( $data[$offset] );
662 }
663
672 public function &offsetGet( $offset ) {
673 $data = &$this->backend->getData();
674 if ( !array_key_exists( $offset, $data ) ) {
675 $ex = new \Exception( "Undefined index (auto-adds to session with a null value): $offset" );
676 $this->logger->debug( $ex->getMessage(), [ 'exception' => $ex ] );
677 }
678 return $data[$offset];
679 }
680
681 public function offsetSet( $offset, $value ) {
682 $this->set( $offset, $value );
683 }
684
685 public function offsetUnset( $offset ) {
686 $this->remove( $offset );
687 }
688
691}
serialize()
unserialize( $serialized)
$wgSecretKey
This should always be customised in LocalSettings.php.
$wgSessionPbkdf2Iterations
Number of internal PBKDF2 iterations to use when deriving session secrets.
$wgSessionSecret
Secret for session storage.
$wgSessionInsecureSecrets
If for some reason you can't install the PHP OpenSSL or mcrypt extensions, you can set this to true t...
This is the actual workhorse for Session.
Manages data for an an authenticated session.
Definition Session.php:48
offsetSet( $offset, $value)
Definition Session.php:681
delaySave()
Delay automatic saving while multiple updates are being made.
Definition Session.php:605
int $index
Session index.
Definition Session.php:56
suggestLoginUsername()
Get a suggested username for the login form.
Definition Session.php:207
static getEncryptionAlgorithm()
Decide what type of encryption to use, based on system capabilities.
Definition Session.php:420
setForceHTTPS( $force)
Set whether HTTPS should be forced.
Definition Session.php:223
getToken( $salt='', $key='default')
Fetch a CSRF token from the session.
Definition Session.php:348
setSecret( $key, $value)
Set a value in the session, encrypted.
Definition Session.php:475
persist()
Make this session persisted across requests.
Definition Session.php:126
shouldForceHTTPS()
Whether HTTPS should be forced.
Definition Session.php:215
renew()
Renew the session.
Definition Session.php:273
getSecret( $key, $default=null)
Fetch a value from the session that was set with self::setSecret()
Definition Session.php:528
resetId()
Changes the session ID.
Definition Session.php:97
getProvider()
Fetch the SessionProvider for this session.
Definition Session.php:105
isPersistent()
Indicate whether this session is persisted across requests.
Definition Session.php:116
getRequest()
Returns the request associated with this session.
Definition Session.php:164
getProviderMetadata()
Fetch provider metadata.
Definition Session.php:248
resetToken( $key='default')
Remove a CSRF token from the session.
Definition Session.php:375
sessionWithRequest(WebRequest $request)
Fetch a copy of this session attached to an alternative WebRequest.
Definition Session.php:286
save()
Save the session.
Definition Session.php:615
static null string[] $encryptionAlgorithm
Encryption algorithm to use.
Definition Session.php:50
unpersist()
Make this session not be persisted across requests.
Definition Session.php:138
canSetUser()
Indicate whether the session user info can be changed.
Definition Session.php:188
exists( $key)
Test if a value exists in the session.
Definition Session.php:308
getLoggedOutTimestamp()
Fetch the "logged out" timestamp.
Definition Session.php:231
getUser()
Returns the authenticated user for this session.
Definition Session.php:172
getId()
Returns the session ID.
Definition Session.php:80
getSecretKeys()
Fetch the secret keys for self::setSecret() and self::getSecret().
Definition Session.php:394
setRememberUser( $remember)
Set whether the user should be remembered independently of the session ID.
Definition Session.php:156
resetAllTokens()
Remove all CSRF tokens from the session.
Definition Session.php:386
clear()
Delete all session data and clear the user (if possible)
Definition Session.php:255
shouldRememberUser()
Indicate whether the user should be remembered independently of the session ID.
Definition Session.php:147
SessionBackend $backend
Session backend.
Definition Session.php:53
setLoggedOutTimestamp( $ts)
Set the "logged out" timestamp.
Definition Session.php:239
LoggerInterface $logger
Definition Session.php:59
setUser( $user)
Set a new user for this session.
Definition Session.php:199
getSessionId()
Returns the SessionId object.
Definition Session.php:89
getAllowedUserRights()
Fetch the rights allowed the user when this session is active.
Definition Session.php:180
__construct(SessionBackend $backend, $index, LoggerInterface $logger)
Definition Session.php:66
Value object representing a CSRF token.
Definition Token.php:32
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:51
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form stripping il...
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition deferred.txt:11
do that in ParserLimitReportFormat instead use this to modify the parameters of the image all existing parser cache entries will be invalid To avoid you ll need to handle that somehow(e.g. with the RejectParserCacheValue hook) because MediaWiki won 't do it for you. & $defaults also a ContextSource after deleting those rows but within the same transaction you ll probably need to make sure the header is varied on $request
Definition hooks.txt:2775
foreach( $res as $row) $serialized