MediaWiki  master
Session.php
Go to the documentation of this file.
1 <?php
24 namespace MediaWiki\Session;
25 
27 use User;
29 
48 final 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' ];
428  return self::$encryptionAlgorithm;
429  }
430  if ( in_array( 'aes-256-cbc', $methods, true ) ) {
431  self::$encryptionAlgorithm = [ 'openssl', 'aes-256-cbc' ];
432  return self::$encryptionAlgorithm;
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' ];
442  return self::$encryptionAlgorithm;
443  }
444  if ( in_array( 'cbc', $modes, true ) ) {
445  self::$encryptionAlgorithm = [ 'mcrypt', 'rijndael-128', 'cbc' ];
446  return self::$encryptionAlgorithm;
447  }
448  }
449 
450  if ( $wgSessionInsecureSecrets ) {
451  // @todo: import a pure-PHP library for AES instead of this
452  self::$encryptionAlgorithm = [ 'insecure' ];
453  return self::$encryptionAlgorithm;
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 
464  return self::$encryptionAlgorithm;
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 = random_bytes( 16 );
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, 4 );
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 
625  public function count() {
626  $data = &$this->backend->getData();
627  return count( $data );
628  }
629 
631  public function current() {
632  $data = &$this->backend->getData();
633  return current( $data );
634  }
635 
637  public function key() {
638  $data = &$this->backend->getData();
639  return key( $data );
640  }
641 
643  public function next() {
644  $data = &$this->backend->getData();
645  next( $data );
646  }
647 
649  public function rewind() {
650  $data = &$this->backend->getData();
651  reset( $data );
652  }
653 
655  public function valid() {
656  $data = &$this->backend->getData();
657  return key( $data ) !== null;
658  }
659 
665  public function offsetExists( $offset ) {
666  $data = &$this->backend->getData();
667  return isset( $data[$offset] );
668  }
669 
678  public function &offsetGet( $offset ) {
679  $data = &$this->backend->getData();
680  if ( !array_key_exists( $offset, $data ) ) {
681  $ex = new \Exception( "Undefined index (auto-adds to session with a null value): $offset" );
682  $this->logger->debug( $ex->getMessage(), [ 'exception' => $ex ] );
683  }
684  return $data[$offset];
685  }
686 
688  public function offsetSet( $offset, $value ) {
689  $this->set( $offset, $value );
690  }
691 
693  public function offsetUnset( $offset ) {
694  $this->remove( $offset );
695  }
696 
699 }
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
This is the actual workhorse for Session.
$data
Utility to generate mapping file used in mw.Title (phpCharToUpper.json)
serialize()
canSetUser()
Indicate whether the session user info can be changed.
Definition: Session.php:188
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
setSecret( $key, $value)
Set a value in the session, encrypted.
Definition: Session.php:475
$wgSecretKey
This should always be customised in LocalSettings.php.
getLoggedOutTimestamp()
Fetch the "logged out" timestamp.
Definition: Session.php:231
resetAllTokens()
Remove all CSRF tokens from the session.
Definition: Session.php:386
setForceHTTPS( $force)
Set whether HTTPS should be forced.
Definition: Session.php:223
$value
$wgSessionPbkdf2Iterations
Number of internal PBKDF2 iterations to use when deriving session secrets.
$wgSessionSecret
Secret for session storage.
The User object encapsulates all of the user-specific settings (user_id, name, rights, email address, options, last login time).
Definition: User.php:48
getRequest()
Returns the request associated with this session.
Definition: Session.php:164
getAllowedUserRights()
Fetch the rights allowed the user when this session is active.
Definition: Session.php:180
offsetSet( $offset, $value)
Definition: Session.php:688
renew()
Renew the session.
Definition: Session.php:273
__construct(SessionBackend $backend, $index, LoggerInterface $logger)
Definition: Session.php:66
getSessionId()
Returns the SessionId object.
Definition: Session.php:89
Value object representing a CSRF token.
Definition: Token.php:32
static getEncryptionAlgorithm()
Decide what type of encryption to use, based on system capabilities.
Definition: Session.php:420
setLoggedOutTimestamp( $ts)
Set the "logged out" timestamp.
Definition: Session.php:239
delaySave()
Delay automatic saving while multiple updates are being made.
Definition: Session.php:605
shouldForceHTTPS()
Whether HTTPS should be forced.
Definition: Session.php:215
static null string [] $encryptionAlgorithm
Encryption algorithm to use.
Definition: Session.php:50
sessionWithRequest(WebRequest $request)
Fetch a copy of this session attached to an alternative WebRequest.
Definition: Session.php:286
getProviderMetadata()
Fetch provider metadata.
Definition: Session.php:248
unserialize( $serialized)
getProvider()
Fetch the SessionProvider for this session.
Definition: Session.php:105
static generateHex( $chars)
Generate a run of cryptographically random data and return it in hexadecimal string format...
Definition: MWCryptRand.php:36
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not null
Definition: hooks.txt:780
shouldRememberUser()
Indicate whether the user should be remembered independently of the session ID.
Definition: Session.php:147
Manages data for an an authenticated session.
Definition: Session.php:48
setUser( $user)
Set a new user for this session.
Definition: Session.php:199
LoggerInterface $logger
Definition: Session.php:59
getId()
Returns the session ID.
Definition: Session.php:80
setSessionId(SessionId $sessionId)
Set the session for this request.
Definition: WebRequest.php:759
resetToken( $key='default')
Remove a CSRF token from the session.
Definition: Session.php:375
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition: injection.txt:35
getSecretKeys()
Fetch the secret keys for self::setSecret() and self::getSecret().
Definition: Session.php:394
isPersistent()
Indicate whether this session is persisted across requests.
Definition: Session.php:116
resetId()
Changes the session ID.
Definition: Session.php:97
int $index
Session index.
Definition: Session.php:56
suggestLoginUsername()
Get a suggested username for the login form.
Definition: Session.php:207
setRememberUser( $remember)
Set whether the user should be remembered independently of the session ID.
Definition: Session.php:156
getToken( $salt='', $key='default')
Fetch a CSRF token from the session.
Definition: Session.php:348
save()
Save the session.
Definition: Session.php:615
clear()
Delete all session data and clear the user (if possible)
Definition: Session.php:255
SessionBackend $backend
Session backend.
Definition: Session.php:53
exists( $key)
Test if a value exists in the session.
Definition: Session.php:308
getUser()
Returns the authenticated user for this session.
Definition: Session.php:172
foreach( $res as $row) $serialized
$wgSessionInsecureSecrets
If for some reason you can&#39;t install the PHP OpenSSL or mcrypt extensions, you can set this to true t...
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:2633
unpersist()
Make this session not be persisted across requests.
Definition: Session.php:138
persist()
Make this session persisted across requests.
Definition: Session.php:126
return true to allow those checks to and false if checking is done & $user
Definition: hooks.txt:1473
getSecret( $key, $default=null)
Fetch a value from the session that was set with self::setSecret()
Definition: Session.php:528