MediaWiki  master
Session.php
Go to the documentation of this file.
1 <?php
24 namespace MediaWiki\Session;
25 
27 use User;
28 use WebRequest;
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 
286  public function sessionWithRequest( WebRequest $request ) {
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 ( $wgSessionInsecureSecrets ) {
437  // @todo: import a pure-PHP library for AES instead of this
438  self::$encryptionAlgorithm = [ 'insecure' ];
439  return self::$encryptionAlgorithm;
440  }
441 
442  throw new \BadMethodCallException(
443  'Encryption is not available. You really should install the PHP OpenSSL extension. ' .
444  'But if you really can\'t and you\'re willing ' .
445  'to accept insecure storage of sensitive session data, set ' .
446  '$wgSessionInsecureSecrets = true in LocalSettings.php to make this exception go away.'
447  );
448  }
449 
450  return self::$encryptionAlgorithm;
451  }
452 
461  public function setSecret( $key, $value ) {
462  list( $encKey, $hmacKey ) = $this->getSecretKeys();
463  $serialized = serialize( $value );
464 
465  // The code for encryption (with OpenSSL) and sealing is taken from
466  // Chris Steipp's OATHAuthUtils class in Extension::OATHAuth.
467 
468  // Encrypt
469  // @todo: import a pure-PHP library for AES instead of doing $wgSessionInsecureSecrets
470  $iv = random_bytes( 16 );
471  $algorithm = self::getEncryptionAlgorithm();
472  switch ( $algorithm[0] ) {
473  case 'openssl':
474  $ciphertext = openssl_encrypt( $serialized, $algorithm[1], $encKey, OPENSSL_RAW_DATA, $iv );
475  if ( $ciphertext === false ) {
476  throw new \UnexpectedValueException( 'Encryption failed: ' . openssl_error_string() );
477  }
478  break;
479  case 'insecure':
480  $ex = new \Exception( 'No encryption is available, storing data as plain text' );
481  $this->logger->warning( $ex->getMessage(), [ 'exception' => $ex ] );
482  $ciphertext = $serialized;
483  break;
484  default:
485  throw new \LogicException( 'invalid algorithm' );
486  }
487 
488  // Seal
489  $sealed = base64_encode( $iv ) . '.' . base64_encode( $ciphertext );
490  $hmac = hash_hmac( 'sha256', $sealed, $hmacKey, true );
491  $encrypted = base64_encode( $hmac ) . '.' . $sealed;
492 
493  // Store
494  $this->set( $key, $encrypted );
495  }
496 
503  public function getSecret( $key, $default = null ) {
504  // Fetch
505  $encrypted = $this->get( $key, null );
506  if ( $encrypted === null ) {
507  return $default;
508  }
509 
510  // The code for unsealing, checking, and decrypting (with OpenSSL) is
511  // taken from Chris Steipp's OATHAuthUtils class in
512  // Extension::OATHAuth.
513 
514  // Unseal and check
515  $pieces = explode( '.', $encrypted, 4 );
516  if ( count( $pieces ) !== 3 ) {
517  $ex = new \Exception( 'Invalid sealed-secret format' );
518  $this->logger->warning( $ex->getMessage(), [ 'exception' => $ex ] );
519  return $default;
520  }
521  list( $hmac, $iv, $ciphertext ) = $pieces;
522  list( $encKey, $hmacKey ) = $this->getSecretKeys();
523  $integCalc = hash_hmac( 'sha256', $iv . '.' . $ciphertext, $hmacKey, true );
524  if ( !hash_equals( $integCalc, base64_decode( $hmac ) ) ) {
525  $ex = new \Exception( 'Sealed secret has been tampered with, aborting.' );
526  $this->logger->warning( $ex->getMessage(), [ 'exception' => $ex ] );
527  return $default;
528  }
529 
530  // Decrypt
531  $algorithm = self::getEncryptionAlgorithm();
532  switch ( $algorithm[0] ) {
533  case 'openssl':
534  $serialized = openssl_decrypt( base64_decode( $ciphertext ), $algorithm[1], $encKey,
535  OPENSSL_RAW_DATA, base64_decode( $iv ) );
536  if ( $serialized === false ) {
537  $ex = new \Exception( 'Decyption failed: ' . openssl_error_string() );
538  $this->logger->debug( $ex->getMessage(), [ 'exception' => $ex ] );
539  return $default;
540  }
541  break;
542  case 'insecure':
543  $ex = new \Exception(
544  'No encryption is available, retrieving data that was stored as plain text'
545  );
546  $this->logger->warning( $ex->getMessage(), [ 'exception' => $ex ] );
547  $serialized = base64_decode( $ciphertext );
548  break;
549  default:
550  throw new \LogicException( 'invalid algorithm' );
551  }
552 
553  $value = unserialize( $serialized );
554  if ( $value === false && $serialized !== serialize( false ) ) {
555  $value = $default;
556  }
557  return $value;
558  }
559 
567  public function delaySave() {
568  return $this->backend->delaySave();
569  }
570 
577  public function save() {
578  $this->backend->save();
579  }
580 
587  public function count() {
588  $data = &$this->backend->getData();
589  return count( $data );
590  }
591 
593  public function current() {
594  $data = &$this->backend->getData();
595  return current( $data );
596  }
597 
599  public function key() {
600  $data = &$this->backend->getData();
601  return key( $data );
602  }
603 
605  public function next() {
606  $data = &$this->backend->getData();
607  next( $data );
608  }
609 
611  public function rewind() {
612  $data = &$this->backend->getData();
613  reset( $data );
614  }
615 
617  public function valid() {
618  $data = &$this->backend->getData();
619  return key( $data ) !== null;
620  }
621 
627  public function offsetExists( $offset ) {
628  $data = &$this->backend->getData();
629  return isset( $data[$offset] );
630  }
631 
640  public function &offsetGet( $offset ) {
641  $data = &$this->backend->getData();
642  if ( !array_key_exists( $offset, $data ) ) {
643  $ex = new \Exception( "Undefined index (auto-adds to session with a null value): $offset" );
644  $this->logger->debug( $ex->getMessage(), [ 'exception' => $ex ] );
645  }
646  return $data[$offset];
647  }
648 
650  public function offsetSet( $offset, $value ) {
651  $this->set( $offset, $value );
652  }
653 
655  public function offsetUnset( $offset ) {
656  $this->remove( $offset );
657  }
658 
661 }
This is the actual workhorse for Session.
serialize()
canSetUser()
Indicate whether the session user info can be changed.
Definition: Session.php:188
setSecret( $key, $value)
Set a value in the session, encrypted.
Definition: Session.php:461
$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
$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:51
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:650
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:567
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
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:823
resetToken( $key='default')
Remove a CSRF token from the session.
Definition: Session.php:375
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:577
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 extension, you can set this to true to make Medi...
unpersist()
Make this session not be persisted across requests.
Definition: Session.php:138
persist()
Make this session persisted across requests.
Definition: Session.php:126
getSecret( $key, $default=null)
Fetch a value from the session that was set with self::setSecret()
Definition: Session.php:503