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;
28 use WebRequest;
29 
48 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 
218  public function shouldForceHTTPS() {
219  return $this->backend->shouldForceHTTPS();
220  }
221 
229  public function setForceHTTPS( $force ) {
230  $this->backend->setForceHTTPS( $force );
231  }
232 
237  public function getLoggedOutTimestamp() {
238  return $this->backend->getLoggedOutTimestamp();
239  }
240 
244  public function setLoggedOutTimestamp( $ts ) {
245  $this->backend->setLoggedOutTimestamp( $ts );
246  }
247 
253  public function getProviderMetadata() {
254  return $this->backend->getProviderMetadata();
255  }
256 
260  public function clear() {
261  $data = &$this->backend->getData();
262  if ( $data ) {
263  $data = [];
264  $this->backend->dirty();
265  }
266  if ( $this->backend->canSetUser() ) {
267  $this->backend->setUser( new User );
268  }
269  $this->backend->save();
270  }
271 
276  public function renew() {
277  $this->backend->renew();
278  }
279 
289  public function sessionWithRequest( WebRequest $request ) {
290  $request->setSessionId( $this->backend->getSessionId() );
291  return $this->backend->getSession( $request );
292  }
293 
300  public function get( $key, $default = null ) {
301  $data = &$this->backend->getData();
302  return array_key_exists( $key, $data ) ? $data[$key] : $default;
303  }
304 
311  public function exists( $key ) {
312  $data = &$this->backend->getData();
313  return array_key_exists( $key, $data );
314  }
315 
321  public function set( $key, $value ) {
322  $data = &$this->backend->getData();
323  if ( !array_key_exists( $key, $data ) || $data[$key] !== $value ) {
324  $data[$key] = $value;
325  $this->backend->dirty();
326  }
327  }
328 
333  public function remove( $key ) {
334  $data = &$this->backend->getData();
335  if ( array_key_exists( $key, $data ) ) {
336  unset( $data[$key] );
337  $this->backend->dirty();
338  }
339  }
340 
348  public function hasToken( string $key = 'default' ): bool {
349  $secrets = $this->get( 'wsTokenSecrets' );
350  if ( !is_array( $secrets ) ) {
351  return false;
352  }
353  return isset( $secrets[$key] ) && is_string( $secrets[$key] );
354  }
355 
366  public function getToken( $salt = '', $key = 'default' ) {
367  $new = false;
368  $secrets = $this->get( 'wsTokenSecrets' );
369  if ( !is_array( $secrets ) ) {
370  $secrets = [];
371  }
372  if ( isset( $secrets[$key] ) && is_string( $secrets[$key] ) ) {
373  $secret = $secrets[$key];
374  } else {
375  $secret = \MWCryptRand::generateHex( 32 );
376  $secrets[$key] = $secret;
377  $this->set( 'wsTokenSecrets', $secrets );
378  $new = true;
379  }
380  if ( is_array( $salt ) ) {
381  $salt = implode( '|', $salt );
382  }
383  return new Token( $secret, (string)$salt, $new );
384  }
385 
393  public function resetToken( $key = 'default' ) {
394  $secrets = $this->get( 'wsTokenSecrets' );
395  if ( is_array( $secrets ) && isset( $secrets[$key] ) ) {
396  unset( $secrets[$key] );
397  $this->set( 'wsTokenSecrets', $secrets );
398  }
399  }
400 
404  public function resetAllTokens() {
405  $this->remove( 'wsTokenSecrets' );
406  }
407 
412  private function getSecretKeys() {
414 
415  $wikiSecret = $wgSessionSecret ?: $wgSecretKey;
416  $userSecret = $this->get( 'wsSessionSecret', null );
417  if ( $userSecret === null ) {
418  $userSecret = \MWCryptRand::generateHex( 32 );
419  $this->set( 'wsSessionSecret', $userSecret );
420  }
421  $iterations = $this->get( 'wsSessionPbkdf2Iterations', null );
422  if ( $iterations === null ) {
423  $iterations = $wgSessionPbkdf2Iterations;
424  $this->set( 'wsSessionPbkdf2Iterations', $iterations );
425  }
426 
427  $keymats = hash_pbkdf2( 'sha256', $wikiSecret, $userSecret, $iterations, 64, true );
428  return [
429  substr( $keymats, 0, 32 ),
430  substr( $keymats, 32, 32 ),
431  ];
432  }
433 
438  private static function getEncryptionAlgorithm() {
440 
441  if ( self::$encryptionAlgorithm === null ) {
442  if ( function_exists( 'openssl_encrypt' ) ) {
443  $methods = openssl_get_cipher_methods();
444  if ( in_array( 'aes-256-ctr', $methods, true ) ) {
445  self::$encryptionAlgorithm = [ 'openssl', 'aes-256-ctr' ];
447  }
448  if ( in_array( 'aes-256-cbc', $methods, true ) ) {
449  self::$encryptionAlgorithm = [ 'openssl', 'aes-256-cbc' ];
451  }
452  }
453 
455  // @todo: import a pure-PHP library for AES instead of this
456  self::$encryptionAlgorithm = [ 'insecure' ];
458  }
459 
460  throw new \BadMethodCallException(
461  'Encryption is not available. You really should install the PHP OpenSSL extension. ' .
462  'But if you really can\'t and you\'re willing ' .
463  'to accept insecure storage of sensitive session data, set ' .
464  '$wgSessionInsecureSecrets = true in LocalSettings.php to make this exception go away.'
465  );
466  }
467 
469  }
470 
479  public function setSecret( $key, $value ) {
480  list( $encKey, $hmacKey ) = $this->getSecretKeys();
481  $serialized = serialize( $value );
482 
483  // The code for encryption (with OpenSSL) and sealing is taken from
484  // Chris Steipp's OATHAuthUtils class in Extension::OATHAuth.
485 
486  // Encrypt
487  // @todo: import a pure-PHP library for AES instead of doing $wgSessionInsecureSecrets
488  $iv = random_bytes( 16 );
489  $algorithm = self::getEncryptionAlgorithm();
490  switch ( $algorithm[0] ) {
491  case 'openssl':
492  $ciphertext = openssl_encrypt( $serialized, $algorithm[1], $encKey, OPENSSL_RAW_DATA, $iv );
493  if ( $ciphertext === false ) {
494  throw new \UnexpectedValueException( 'Encryption failed: ' . openssl_error_string() );
495  }
496  break;
497  case 'insecure':
498  $ex = new \Exception( 'No encryption is available, storing data as plain text' );
499  $this->logger->warning( $ex->getMessage(), [ 'exception' => $ex ] );
500  $ciphertext = $serialized;
501  break;
502  default:
503  throw new \LogicException( 'invalid algorithm' );
504  }
505 
506  // Seal
507  $sealed = base64_encode( $iv ) . '.' . base64_encode( $ciphertext );
508  $hmac = hash_hmac( 'sha256', $sealed, $hmacKey, true );
509  $encrypted = base64_encode( $hmac ) . '.' . $sealed;
510 
511  // Store
512  $this->set( $key, $encrypted );
513  }
514 
521  public function getSecret( $key, $default = null ) {
522  // Fetch
523  $encrypted = $this->get( $key, null );
524  if ( $encrypted === null ) {
525  return $default;
526  }
527 
528  // The code for unsealing, checking, and decrypting (with OpenSSL) is
529  // taken from Chris Steipp's OATHAuthUtils class in
530  // Extension::OATHAuth.
531 
532  // Unseal and check
533  $pieces = explode( '.', $encrypted, 4 );
534  if ( count( $pieces ) !== 3 ) {
535  $ex = new \Exception( 'Invalid sealed-secret format' );
536  $this->logger->warning( $ex->getMessage(), [ 'exception' => $ex ] );
537  return $default;
538  }
539  list( $hmac, $iv, $ciphertext ) = $pieces;
540  list( $encKey, $hmacKey ) = $this->getSecretKeys();
541  $integCalc = hash_hmac( 'sha256', $iv . '.' . $ciphertext, $hmacKey, true );
542  if ( !hash_equals( $integCalc, base64_decode( $hmac ) ) ) {
543  $ex = new \Exception( 'Sealed secret has been tampered with, aborting.' );
544  $this->logger->warning( $ex->getMessage(), [ 'exception' => $ex ] );
545  return $default;
546  }
547 
548  // Decrypt
549  $algorithm = self::getEncryptionAlgorithm();
550  switch ( $algorithm[0] ) {
551  case 'openssl':
552  $serialized = openssl_decrypt( base64_decode( $ciphertext ), $algorithm[1], $encKey,
553  OPENSSL_RAW_DATA, base64_decode( $iv ) );
554  if ( $serialized === false ) {
555  $ex = new \Exception( 'Decyption failed: ' . openssl_error_string() );
556  $this->logger->debug( $ex->getMessage(), [ 'exception' => $ex ] );
557  return $default;
558  }
559  break;
560  case 'insecure':
561  $ex = new \Exception(
562  'No encryption is available, retrieving data that was stored as plain text'
563  );
564  $this->logger->warning( $ex->getMessage(), [ 'exception' => $ex ] );
565  $serialized = base64_decode( $ciphertext );
566  break;
567  default:
568  throw new \LogicException( 'invalid algorithm' );
569  }
570 
571  $value = unserialize( $serialized );
572  if ( $value === false && $serialized !== serialize( false ) ) {
573  $value = $default;
574  }
575  return $value;
576  }
577 
585  public function delaySave() {
586  return $this->backend->delaySave();
587  }
588 
593  public function save() {
594  $this->backend->save();
595  }
596 
597  // region Interface methods
603  public function count() {
604  $data = &$this->backend->getData();
605  return count( $data );
606  }
607 
609  public function current() {
610  $data = &$this->backend->getData();
611  return current( $data );
612  }
613 
615  public function key() {
616  $data = &$this->backend->getData();
617  return key( $data );
618  }
619 
621  public function next() {
622  $data = &$this->backend->getData();
623  next( $data );
624  }
625 
627  public function rewind() {
628  $data = &$this->backend->getData();
629  reset( $data );
630  }
631 
633  public function valid() {
634  $data = &$this->backend->getData();
635  return key( $data ) !== null;
636  }
637 
643  public function offsetExists( $offset ) {
644  $data = &$this->backend->getData();
645  return isset( $data[$offset] );
646  }
647 
656  public function &offsetGet( $offset ) {
657  $data = &$this->backend->getData();
658  if ( !array_key_exists( $offset, $data ) ) {
659  $ex = new \Exception( "Undefined index (auto-adds to session with a null value): $offset" );
660  $this->logger->debug( $ex->getMessage(), [ 'exception' => $ex ] );
661  }
662  return $data[$offset];
663  }
664 
666  public function offsetSet( $offset, $value ) {
667  $this->set( $offset, $value );
668  }
669 
671  public function offsetUnset( $offset ) {
672  $this->remove( $offset );
673  }
674 
676  // endregion -- end of Interface methods
677 }
MediaWiki\Session\Session\key
key()
Definition: Session.php:615
MediaWiki\Session\Session\getSecret
getSecret( $key, $default=null)
Fetch a value from the session that was set with self::setSecret()
Definition: Session.php:521
MediaWiki\Session\Session\clear
clear()
Delete all session data and clear the user (if possible)
Definition: Session.php:260
MediaWiki\Session\Session\sessionWithRequest
sessionWithRequest(WebRequest $request)
Fetch a copy of this session attached to an alternative WebRequest.
Definition: Session.php:289
MediaWiki\Session\Session\getProvider
getProvider()
Fetch the SessionProvider for this session.
Definition: Session.php:105
MediaWiki\Session\Session\__construct
__construct(SessionBackend $backend, $index, LoggerInterface $logger)
Definition: Session.php:66
MediaWiki\Session\Session\shouldForceHTTPS
shouldForceHTTPS()
Get the expected value of the forceHTTPS cookie.
Definition: Session.php:218
WebRequest\setSessionId
setSessionId(SessionId $sessionId)
Set the session for this request.
Definition: WebRequest.php:841
$wgSessionInsecureSecrets
$wgSessionInsecureSecrets
If for some reason you can't install the PHP OpenSSL extension, you can set this to true to make Medi...
Definition: DefaultSettings.php:7030
MediaWiki\Session\Session\persist
persist()
Make this session persisted across requests.
Definition: Session.php:126
MediaWiki\Session\Session\delaySave
delaySave()
Delay automatic saving while multiple updates are being made.
Definition: Session.php:585
MediaWiki\Session\Session\isPersistent
isPersistent()
Indicate whether this session is persisted across requests.
Definition: Session.php:116
$serialized
foreach( $res as $row) $serialized
Definition: testCompression.php:88
MediaWiki\Session\Session\getToken
getToken( $salt='', $key='default')
Fetch a CSRF token from the session.
Definition: Session.php:366
MediaWiki\Session\Session\getLoggedOutTimestamp
getLoggedOutTimestamp()
Fetch the "logged out" timestamp.
Definition: Session.php:237
MediaWiki\Session\Session\getAllowedUserRights
getAllowedUserRights()
Fetch the rights allowed the user when this session is active.
Definition: Session.php:180
MediaWiki\Session\Session\shouldRememberUser
shouldRememberUser()
Indicate whether the user should be remembered independently of the session ID.
Definition: Session.php:147
MediaWiki\Session\Session\exists
exists( $key)
Test if a value exists in the session.
Definition: Session.php:311
MediaWiki\Session\Session\getSecretKeys
getSecretKeys()
Fetch the secret keys for self::setSecret() and self::getSecret().
Definition: Session.php:412
serialize
serialize()
Definition: ApiMessageTrait.php:138
MediaWiki\Session\Session\getProviderMetadata
getProviderMetadata()
Fetch provider metadata.
Definition: Session.php:253
MediaWiki\Session\Session\resetAllTokens
resetAllTokens()
Remove all CSRF tokens from the session.
Definition: Session.php:404
MediaWiki\Session\Session\$logger
LoggerInterface $logger
Definition: Session.php:59
MediaWiki\Session\Session\next
next()
Definition: Session.php:621
MediaWiki\Session\Session\offsetExists
offsetExists( $offset)
Definition: Session.php:643
MediaWiki\Session\Session
Manages data for an authenticated session.
Definition: Session.php:48
MediaWiki\Session\Session\renew
renew()
Resets the TTL in the backend store if the session is near expiring, and re-persists the session to a...
Definition: Session.php:276
MediaWiki\Session\Session\getUser
getUser()
Returns the authenticated user for this session.
Definition: Session.php:172
MediaWiki\Session\Session\count
count()
Definition: Session.php:603
MediaWiki\Session\Session\getRequest
getRequest()
Returns the request associated with this session.
Definition: Session.php:164
MediaWiki\Session\Session\current
current()
Definition: Session.php:609
MediaWiki\Session
Definition: BotPasswordSessionProvider.php:24
MediaWiki\Session\Session\canSetUser
canSetUser()
Indicate whether the session user info can be changed.
Definition: Session.php:188
MediaWiki\Session\Session\getSessionId
getSessionId()
Returns the SessionId object.
Definition: Session.php:89
MediaWiki\Session\Session\unpersist
unpersist()
Make this session not be persisted across requests.
Definition: Session.php:138
MediaWiki\Session\Session\setLoggedOutTimestamp
setLoggedOutTimestamp( $ts)
Definition: Session.php:244
MediaWiki\Session\Session\resetId
resetId()
Changes the session ID.
Definition: Session.php:97
$wgSessionPbkdf2Iterations
$wgSessionPbkdf2Iterations
Number of internal PBKDF2 iterations to use when deriving session secrets.
Definition: DefaultSettings.php:3011
MediaWiki\Session\Session\offsetGet
& offsetGet( $offset)
Definition: Session.php:656
MediaWiki\Session\Session\$backend
SessionBackend $backend
Session backend.
Definition: Session.php:53
MediaWiki\Session\Session\valid
valid()
Definition: Session.php:633
MediaWiki\Session\Session\setRememberUser
setRememberUser( $remember)
Set whether the user should be remembered independently of the session ID.
Definition: Session.php:156
MediaWiki\Session\Session\$index
int $index
Session index.
Definition: Session.php:56
MediaWiki\Session\Session\offsetUnset
offsetUnset( $offset)
Definition: Session.php:671
MediaWiki\Session\Session\resetToken
resetToken( $key='default')
Remove a CSRF token from the session.
Definition: Session.php:393
MWCryptRand\generateHex
static generateHex( $chars)
Generate a run of cryptographically random data and return it in hexadecimal string format.
Definition: MWCryptRand.php:36
MediaWiki\Session\Session\__destruct
__destruct()
Definition: Session.php:72
MediaWiki\Session\Session\offsetSet
offsetSet( $offset, $value)
Definition: Session.php:666
MediaWiki\Session\Session\rewind
rewind()
Definition: Session.php:627
WebRequest
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form stripping il...
Definition: WebRequest.php:43
unserialize
unserialize( $serialized)
Definition: ApiMessageTrait.php:146
MediaWiki\Session\Session\setUser
setUser( $user)
Set a new user for this session.
Definition: Session.php:199
MediaWiki\Session\Session\hasToken
hasToken(string $key='default')
Check if a CSRF token is set for the session.
Definition: Session.php:348
MediaWiki\Session\Session\$encryptionAlgorithm
static null string[] $encryptionAlgorithm
Encryption algorithm to use.
Definition: Session.php:50
MediaWiki\Session\Session\setSecret
setSecret( $key, $value)
Set a value in the session, encrypted.
Definition: Session.php:479
MediaWiki\Session\Token
Value object representing a CSRF token.
Definition: Token.php:32
MediaWiki\Session\Session\suggestLoginUsername
suggestLoginUsername()
Get a suggested username for the login form.
Definition: Session.php:207
$wgSecretKey
$wgSecretKey
This should always be customised in LocalSettings.php.
Definition: DefaultSettings.php:6858
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:68
MediaWiki\Session\Session\getId
getId()
Returns the session ID.
Definition: Session.php:80
$wgSessionSecret
$wgSessionSecret
Secret for session storage.
Definition: DefaultSettings.php:7021
MediaWiki\Session\Session\save
save()
This will update the backend data and might re-persist the session if needed.
Definition: Session.php:593
MediaWiki\Session\Session\setForceHTTPS
setForceHTTPS( $force)
Set the value of the forceHTTPS cookie.
Definition: Session.php:229
MediaWiki\Session\Session\getEncryptionAlgorithm
static getEncryptionAlgorithm()
Decide what type of encryption to use, based on system capabilities.
Definition: Session.php:438
MediaWiki\Session\SessionBackend
This is the actual workhorse for Session.
Definition: SessionBackend.php:53