MediaWiki  1.28.0
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() {
395  global $wgSessionSecret, $wgSecretKey, $wgSessionPbkdf2Iterations;
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() {
421  global $wgSessionInsecureSecrets;
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 = \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 
658  public function offsetExists( $offset ) {
659  $data = &$this->backend->getData();
660  return isset( $data[$offset] );
661  }
662 
670  public function &offsetGet( $offset ) {
671  $data = &$this->backend->getData();
672  if ( !array_key_exists( $offset, $data ) ) {
673  $ex = new \Exception( "Undefined index (auto-adds to session with a null value): $offset" );
674  $this->logger->debug( $ex->getMessage(), [ 'exception' => $ex ] );
675  }
676  return $data[$offset];
677  }
678 
679  public function offsetSet( $offset, $value ) {
680  $this->set( $offset, $value );
681  }
682 
683  public function offsetUnset( $offset ) {
684  $this->remove( $offset );
685  }
686 
689 }
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.
setForceHTTPS($force)
Set whether HTTPS should be forced.
Definition: Session.php:223
setUser($user)
Set a new user for this session.
Definition: Session.php:199
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
getLoggedOutTimestamp()
Fetch the "logged out" timestamp.
Definition: Session.php:231
resetAllTokens()
Remove all CSRF tokens from the session.
Definition: Session.php:386
$value
set($key, $value)
Set a value in the session.
Definition: Session.php:318
exists($key)
Test if a value exists in the session.
Definition: Session.php:308
when a variable name is used in a it is silently declared as a new local masking the global
Definition: design.txt:93
setLoggedOutTimestamp($ts)
Set the "logged out" timestamp.
Definition: Session.php:239
$wgSessionPbkdf2Iterations
Number of internal PBKDF2 iterations to use when deriving session secrets.
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
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
unserialize($serialized)
Definition: ApiMessage.php:102
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
getProvider()
Fetch the SessionProvider for this session.
Definition: Session.php:105
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
LoggerInterface $logger
Definition: Session.php:59
getSecret($key, $default=null)
Fetch a value from the session that was set with self::setSecret()
Definition: Session.php:528
getId()
Returns the session ID.
Definition: Session.php:80
please add to it if you re going to add events to the MediaWiki code where normally authentication against an external auth plugin would be creating a local account $user
Definition: hooks.txt:242
setSessionId(SessionId $sessionId)
Set the session for this request.
Definition: WebRequest.php:746
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
error also a ContextSource you ll probably need to make sure the header is varied on $request
Definition: hooks.txt:2573
resetId()
Changes the session ID.
Definition: Session.php:97
int $index
Session index.
Definition: Session.php:56
resetToken($key= 'default')
Remove a CSRF token from the session.
Definition: Session.php:375
offsetSet($offset, $value)
Definition: Session.php:679
getToken($salt= '', $key= 'default')
Fetch a CSRF token from the session.
Definition: Session.php:348
static generateHex($chars, $forceStrong=false)
Generate a run of (ideally) cryptographically random data and return it in hexadecimal string format...
Definition: MWCryptRand.php:76
setRememberUser($remember)
Set whether the user should be remembered independently of the session ID.
Definition: Session.php:156
suggestLoginUsername()
Get a suggested username for the login form.
Definition: Session.php:207
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
setSecret($key, $value)
Set a value in the session, encrypted.
Definition: Session.php:475
getUser()
Returns the authenticated user for this session.
Definition: Session.php:172
serialize()
Definition: ApiMessage.php:94
static generate($bytes, $forceStrong=false)
Generate a run of (ideally) cryptographically random data and return it in raw binary form...
Definition: MWCryptRand.php:60
foreach($res as $row) $serialized
unpersist()
Make this session not be persisted across requests.
Definition: Session.php:138
persist()
Make this session persisted across requests.
Definition: Session.php:126