MediaWiki  master
Session.php
Go to the documentation of this file.
1 <?php
24 namespace MediaWiki\Session;
25 
26 use Psr\Log\LoggerInterface;
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 
133  public function unpersist() {
134  $this->backend->unpersist();
135  }
136 
142  public function shouldRememberUser() {
143  return $this->backend->shouldRememberUser();
144  }
145 
151  public function setRememberUser( $remember ) {
152  $this->backend->setRememberUser( $remember );
153  }
154 
159  public function getRequest() {
160  return $this->backend->getRequest( $this->index );
161  }
162 
167  public function getUser() {
168  return $this->backend->getUser();
169  }
170 
175  public function getAllowedUserRights() {
176  return $this->backend->getAllowedUserRights();
177  }
178 
183  public function canSetUser() {
184  return $this->backend->canSetUser();
185  }
186 
194  public function setUser( $user ) {
195  $this->backend->setUser( $user );
196  }
197 
202  public function suggestLoginUsername() {
203  return $this->backend->suggestLoginUsername( $this->index );
204  }
205 
210  public function shouldForceHTTPS() {
211  return $this->backend->shouldForceHTTPS();
212  }
213 
218  public function setForceHTTPS( $force ) {
219  $this->backend->setForceHTTPS( $force );
220  }
221 
226  public function getLoggedOutTimestamp() {
227  return $this->backend->getLoggedOutTimestamp();
228  }
229 
234  public function setLoggedOutTimestamp( $ts ) {
235  $this->backend->setLoggedOutTimestamp( $ts );
236  }
237 
243  public function getProviderMetadata() {
244  return $this->backend->getProviderMetadata();
245  }
246 
250  public function clear() {
251  $data = &$this->backend->getData();
252  if ( $data ) {
253  $data = [];
254  $this->backend->dirty();
255  }
256  if ( $this->backend->canSetUser() ) {
257  $this->backend->setUser( new User );
258  }
259  $this->backend->save();
260  }
261 
268  public function renew() {
269  $this->backend->renew();
270  }
271 
282  $request->setSessionId( $this->backend->getSessionId() );
283  return $this->backend->getSession( $request );
284  }
285 
292  public function get( $key, $default = null ) {
293  $data = &$this->backend->getData();
294  return array_key_exists( $key, $data ) ? $data[$key] : $default;
295  }
296 
303  public function exists( $key ) {
304  $data = &$this->backend->getData();
305  return array_key_exists( $key, $data );
306  }
307 
313  public function set( $key, $value ) {
314  $data = &$this->backend->getData();
315  if ( !array_key_exists( $key, $data ) || $data[$key] !== $value ) {
316  $data[$key] = $value;
317  $this->backend->dirty();
318  }
319  }
320 
325  public function remove( $key ) {
326  $data = &$this->backend->getData();
327  if ( array_key_exists( $key, $data ) ) {
328  unset( $data[$key] );
329  $this->backend->dirty();
330  }
331  }
332 
343  public function getToken( $salt = '', $key = 'default' ) {
344  $new = false;
345  $secrets = $this->get( 'wsTokenSecrets' );
346  if ( !is_array( $secrets ) ) {
347  $secrets = [];
348  }
349  if ( isset( $secrets[$key] ) && is_string( $secrets[$key] ) ) {
350  $secret = $secrets[$key];
351  } else {
352  $secret = \MWCryptRand::generateHex( 32 );
353  $secrets[$key] = $secret;
354  $this->set( 'wsTokenSecrets', $secrets );
355  $new = true;
356  }
357  if ( is_array( $salt ) ) {
358  $salt = implode( '|', $salt );
359  }
360  return new Token( $secret, (string)$salt, $new );
361  }
362 
370  public function resetToken( $key = 'default' ) {
371  $secrets = $this->get( 'wsTokenSecrets' );
372  if ( is_array( $secrets ) && isset( $secrets[$key] ) ) {
373  unset( $secrets[$key] );
374  $this->set( 'wsTokenSecrets', $secrets );
375  }
376  }
377 
381  public function resetAllTokens() {
382  $this->remove( 'wsTokenSecrets' );
383  }
384 
389  private function getSecretKeys() {
391 
392  $wikiSecret = $wgSessionSecret ?: $wgSecretKey;
393  $userSecret = $this->get( 'wsSessionSecret', null );
394  if ( $userSecret === null ) {
395  $userSecret = \MWCryptRand::generateHex( 32 );
396  $this->set( 'wsSessionSecret', $userSecret );
397  }
398  $iterations = $this->get( 'wsSessionPbkdf2Iterations', null );
399  if ( $iterations === null ) {
400  $iterations = $wgSessionPbkdf2Iterations;
401  $this->set( 'wsSessionPbkdf2Iterations', $iterations );
402  }
403 
404  $keymats = hash_pbkdf2( 'sha256', $wikiSecret, $userSecret, $iterations, 64, true );
405  return [
406  substr( $keymats, 0, 32 ),
407  substr( $keymats, 32, 32 ),
408  ];
409  }
410 
415  private static function getEncryptionAlgorithm() {
417 
418  if ( self::$encryptionAlgorithm === null ) {
419  if ( function_exists( 'openssl_encrypt' ) ) {
420  $methods = openssl_get_cipher_methods();
421  if ( in_array( 'aes-256-ctr', $methods, true ) ) {
422  self::$encryptionAlgorithm = [ 'openssl', 'aes-256-ctr' ];
423  return self::$encryptionAlgorithm;
424  }
425  if ( in_array( 'aes-256-cbc', $methods, true ) ) {
426  self::$encryptionAlgorithm = [ 'openssl', 'aes-256-cbc' ];
427  return self::$encryptionAlgorithm;
428  }
429  }
430 
431  if ( function_exists( 'mcrypt_encrypt' )
432  && in_array( 'rijndael-128', mcrypt_list_algorithms(), true )
433  ) {
434  $modes = mcrypt_list_modes();
435  if ( in_array( 'ctr', $modes, true ) ) {
436  self::$encryptionAlgorithm = [ 'mcrypt', 'rijndael-128', 'ctr' ];
437  return self::$encryptionAlgorithm;
438  }
439  if ( in_array( 'cbc', $modes, true ) ) {
440  self::$encryptionAlgorithm = [ 'mcrypt', 'rijndael-128', 'cbc' ];
441  return self::$encryptionAlgorithm;
442  }
443  }
444 
445  if ( $wgSessionInsecureSecrets ) {
446  // @todo: import a pure-PHP library for AES instead of this
447  self::$encryptionAlgorithm = [ 'insecure' ];
448  return self::$encryptionAlgorithm;
449  }
450 
451  throw new \BadMethodCallException(
452  'Encryption is not available. You really should install the PHP OpenSSL extension, ' .
453  'or failing that the mcrypt extension. But if you really can\'t and you\'re willing ' .
454  'to accept insecure storage of sensitive session data, set ' .
455  '$wgSessionInsecureSecrets = true in LocalSettings.php to make this exception go away.'
456  );
457  }
458 
459  return self::$encryptionAlgorithm;
460  }
461 
470  public function setSecret( $key, $value ) {
471  list( $encKey, $hmacKey ) = $this->getSecretKeys();
473 
474  // The code for encryption (with OpenSSL) and sealing is taken from
475  // Chris Steipp's OATHAuthUtils class in Extension::OATHAuth.
476 
477  // Encrypt
478  // @todo: import a pure-PHP library for AES instead of doing $wgSessionInsecureSecrets
479  $iv = \MWCryptRand::generate( 16, true );
480  $algorithm = self::getEncryptionAlgorithm();
481  switch ( $algorithm[0] ) {
482  case 'openssl':
483  $ciphertext = openssl_encrypt( $serialized, $algorithm[1], $encKey, OPENSSL_RAW_DATA, $iv );
484  if ( $ciphertext === false ) {
485  throw new \UnexpectedValueException( 'Encryption failed: ' . openssl_error_string() );
486  }
487  break;
488  case 'mcrypt':
489  // PKCS7 padding
490  $blocksize = mcrypt_get_block_size( $algorithm[1], $algorithm[2] );
491  $pad = $blocksize - ( strlen( $serialized ) % $blocksize );
492  $serialized .= str_repeat( chr( $pad ), $pad );
493 
494  $ciphertext = mcrypt_encrypt( $algorithm[1], $encKey, $serialized, $algorithm[2], $iv );
495  if ( $ciphertext === false ) {
496  throw new \UnexpectedValueException( 'Encryption failed' );
497  }
498  break;
499  case 'insecure':
500  $ex = new \Exception( 'No encryption is available, storing data as plain text' );
501  $this->logger->warning( $ex->getMessage(), [ 'exception' => $ex ] );
502  $ciphertext = $serialized;
503  break;
504  default:
505  throw new \LogicException( 'invalid algorithm' );
506  }
507 
508  // Seal
509  $sealed = base64_encode( $iv ) . '.' . base64_encode( $ciphertext );
510  $hmac = hash_hmac( 'sha256', $sealed, $hmacKey, true );
511  $encrypted = base64_encode( $hmac ) . '.' . $sealed;
512 
513  // Store
514  $this->set( $key, $encrypted );
515  }
516 
523  public function getSecret( $key, $default = null ) {
524  // Fetch
525  $encrypted = $this->get( $key, null );
526  if ( $encrypted === null ) {
527  return $default;
528  }
529 
530  // The code for unsealing, checking, and decrypting (with OpenSSL) is
531  // taken from Chris Steipp's OATHAuthUtils class in
532  // Extension::OATHAuth.
533 
534  // Unseal and check
535  $pieces = explode( '.', $encrypted );
536  if ( count( $pieces ) !== 3 ) {
537  $ex = new \Exception( 'Invalid sealed-secret format' );
538  $this->logger->warning( $ex->getMessage(), [ 'exception' => $ex ] );
539  return $default;
540  }
541  list( $hmac, $iv, $ciphertext ) = $pieces;
542  list( $encKey, $hmacKey ) = $this->getSecretKeys();
543  $integCalc = hash_hmac( 'sha256', $iv . '.' . $ciphertext, $hmacKey, true );
544  if ( !hash_equals( $integCalc, base64_decode( $hmac ) ) ) {
545  $ex = new \Exception( 'Sealed secret has been tampered with, aborting.' );
546  $this->logger->warning( $ex->getMessage(), [ 'exception' => $ex ] );
547  return $default;
548  }
549 
550  // Decrypt
551  $algorithm = self::getEncryptionAlgorithm();
552  switch ( $algorithm[0] ) {
553  case 'openssl':
554  $serialized = openssl_decrypt( base64_decode( $ciphertext ), $algorithm[1], $encKey,
555  OPENSSL_RAW_DATA, base64_decode( $iv ) );
556  if ( $serialized === false ) {
557  $ex = new \Exception( 'Decyption failed: ' . openssl_error_string() );
558  $this->logger->debug( $ex->getMessage(), [ 'exception' => $ex ] );
559  return $default;
560  }
561  break;
562  case 'mcrypt':
563  $serialized = mcrypt_decrypt( $algorithm[1], $encKey, base64_decode( $ciphertext ),
564  $algorithm[2], base64_decode( $iv ) );
565  if ( $serialized === false ) {
566  $ex = new \Exception( 'Decyption failed' );
567  $this->logger->debug( $ex->getMessage(), [ 'exception' => $ex ] );
568  return $default;
569  }
570 
571  // Remove PKCS7 padding
572  $pad = ord( substr( $serialized, -1 ) );
573  $serialized = substr( $serialized, 0, -$pad );
574  break;
575  case 'insecure':
576  $ex = new \Exception(
577  'No encryption is available, retrieving data that was stored as plain text'
578  );
579  $this->logger->warning( $ex->getMessage(), [ 'exception' => $ex ] );
580  $serialized = base64_decode( $ciphertext );
581  break;
582  default:
583  throw new \LogicException( 'invalid algorithm' );
584  }
585 
587  if ( $value === false && $serialized !== serialize( false ) ) {
588  $value = $default;
589  }
590  return $value;
591  }
592 
600  public function delaySave() {
601  return $this->backend->delaySave();
602  }
603 
607  public function save() {
608  $this->backend->save();
609  }
610 
616  public function count() {
617  $data = &$this->backend->getData();
618  return count( $data );
619  }
620 
621  public function current() {
622  $data = &$this->backend->getData();
623  return current( $data );
624  }
625 
626  public function key() {
627  $data = &$this->backend->getData();
628  return key( $data );
629  }
630 
631  public function next() {
632  $data = &$this->backend->getData();
633  next( $data );
634  }
635 
636  public function rewind() {
637  $data = &$this->backend->getData();
638  reset( $data );
639  }
640 
641  public function valid() {
642  $data = &$this->backend->getData();
643  return key( $data ) !== null;
644  }
645 
650  public function offsetExists( $offset ) {
651  $data = &$this->backend->getData();
652  return isset( $data[$offset] );
653  }
654 
662  public function &offsetGet( $offset ) {
663  $data = &$this->backend->getData();
664  if ( !array_key_exists( $offset, $data ) ) {
665  $ex = new \Exception( "Undefined index (auto-adds to session with a null value): $offset" );
666  $this->logger->debug( $ex->getMessage(), [ 'exception' => $ex ] );
667  }
668  return $data[$offset];
669  }
670 
671  public function offsetSet( $offset, $value ) {
672  $this->set( $offset, $value );
673  }
674 
675  public function offsetUnset( $offset ) {
676  $this->remove( $offset );
677  }
678 
681 }
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:218
setUser($user)
Set a new user for this session.
Definition: Session.php:194
canSetUser()
Indicate whether the session user info can be changed.
Definition: Session.php:183
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form stripping il...
Definition: WebRequest.php:37
$wgSecretKey
This should always be customised in LocalSettings.php.
getLoggedOutTimestamp()
Fetch the "logged out" timestamp.
Definition: Session.php:226
resetAllTokens()
Remove all CSRF tokens from the session.
Definition: Session.php:381
$value
set($key, $value)
Set a value in the session.
Definition: Session.php:313
exists($key)
Test if a value exists in the session.
Definition: Session.php:303
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:234
$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:47
getRequest()
Returns the request associated with this session.
Definition: Session.php:159
getAllowedUserRights()
Fetch the rights allowed the user when this session is active.
Definition: Session.php:175
renew()
Renew the session.
Definition: Session.php:268
__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:415
unserialize($serialized)
Definition: ApiMessage.php:102
delaySave()
Delay automatic saving while multiple updates are being made.
Definition: Session.php:600
shouldForceHTTPS()
Whether HTTPS should be forced.
Definition: Session.php:210
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:281
getProviderMetadata()
Fetch provider metadata.
Definition: Session.php:243
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:142
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:523
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:710
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:389
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:2458
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:370
offsetSet($offset, $value)
Definition: Session.php:671
getToken($salt= '', $key= 'default')
Fetch a CSRF token from the session.
Definition: Session.php:343
static generateHex($chars, $forceStrong=false)
Generate a run of (ideally) cryptographically random data and return it in hexadecimal string format...
setRememberUser($remember)
Set whether the user should be remembered independently of the session ID.
Definition: Session.php:151
suggestLoginUsername()
Get a suggested username for the login form.
Definition: Session.php:202
save()
Save the session.
Definition: Session.php:607
clear()
Delete all session data and clear the user (if possible)
Definition: Session.php:250
SessionBackend $backend
Session backend.
Definition: Session.php:53
setSecret($key, $value)
Set a value in the session, encrypted.
Definition: Session.php:470
getUser()
Returns the authenticated user for this session.
Definition: Session.php:167
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...
foreach($res as $row) $serialized
$wgSessionInsecureSecrets
If for some reason you can't install the PHP OpenSSL or mcrypt extensions, you can set this to true t...
unpersist()
Make this session not be persisted across requests.
Definition: Session.php:133
persist()
Make this session persisted across requests.
Definition: Session.php:126