MediaWiki  master
Session.php
Go to the documentation of this file.
1 <?php
24 namespace MediaWiki\Session;
25 
28 use Psr\Log\LoggerInterface;
29 use User;
30 use WebRequest;
31 
50 class Session implements \Countable, \Iterator, \ArrayAccess {
52  private static $encryptionAlgorithm = null;
53 
55  private $backend;
56 
58  private $index;
59 
61  private $logger;
62 
68  public function __construct( SessionBackend $backend, $index, LoggerInterface $logger ) {
69  $this->backend = $backend;
70  $this->index = $index;
71  $this->logger = $logger;
72  }
73 
74  public function __destruct() {
75  $this->backend->deregisterSession( $this->index );
76  }
77 
82  public function getId() {
83  return $this->backend->getId();
84  }
85 
91  public function getSessionId() {
92  return $this->backend->getSessionId();
93  }
94 
99  public function resetId() {
100  return $this->backend->resetId();
101  }
102 
107  public function getProvider() {
108  return $this->backend->getProvider();
109  }
110 
118  public function isPersistent() {
119  return $this->backend->isPersistent();
120  }
121 
128  public function persist() {
129  $this->backend->persist();
130  }
131 
140  public function unpersist() {
141  $this->backend->unpersist();
142  }
143 
149  public function shouldRememberUser() {
150  return $this->backend->shouldRememberUser();
151  }
152 
158  public function setRememberUser( $remember ) {
159  $this->backend->setRememberUser( $remember );
160  }
161 
166  public function getRequest() {
167  return $this->backend->getRequest( $this->index );
168  }
169 
174  public function getUser() {
175  return $this->backend->getUser();
176  }
177 
182  public function getAllowedUserRights() {
183  return $this->backend->getAllowedUserRights();
184  }
185 
190  public function canSetUser() {
191  return $this->backend->canSetUser();
192  }
193 
201  public function setUser( $user ) {
202  $this->backend->setUser( $user );
203  }
204 
209  public function suggestLoginUsername() {
210  return $this->backend->suggestLoginUsername( $this->index );
211  }
212 
220  public function shouldForceHTTPS() {
221  return $this->backend->shouldForceHTTPS();
222  }
223 
231  public function setForceHTTPS( $force ) {
232  $this->backend->setForceHTTPS( $force );
233  }
234 
239  public function getLoggedOutTimestamp() {
240  return $this->backend->getLoggedOutTimestamp();
241  }
242 
246  public function setLoggedOutTimestamp( $ts ) {
247  $this->backend->setLoggedOutTimestamp( $ts );
248  }
249 
255  public function getProviderMetadata() {
256  return $this->backend->getProviderMetadata();
257  }
258 
262  public function clear() {
263  $data = &$this->backend->getData();
264  if ( $data ) {
265  $data = [];
266  $this->backend->dirty();
267  }
268  if ( $this->backend->canSetUser() ) {
269  $this->backend->setUser( new User );
270  }
271  $this->backend->save();
272  }
273 
278  public function renew() {
279  $this->backend->renew();
280  }
281 
291  public function sessionWithRequest( WebRequest $request ) {
292  $request->setSessionId( $this->backend->getSessionId() );
293  return $this->backend->getSession( $request );
294  }
295 
302  public function get( $key, $default = null ) {
303  $data = &$this->backend->getData();
304  return array_key_exists( $key, $data ) ? $data[$key] : $default;
305  }
306 
313  public function exists( $key ) {
314  $data = &$this->backend->getData();
315  return array_key_exists( $key, $data );
316  }
317 
323  public function set( $key, $value ) {
324  $data = &$this->backend->getData();
325  if ( !array_key_exists( $key, $data ) || $data[$key] !== $value ) {
326  $data[$key] = $value;
327  $this->backend->dirty();
328  }
329  }
330 
335  public function remove( $key ) {
336  $data = &$this->backend->getData();
337  if ( array_key_exists( $key, $data ) ) {
338  unset( $data[$key] );
339  $this->backend->dirty();
340  }
341  }
342 
350  public function hasToken( string $key = 'default' ): bool {
351  $secrets = $this->get( 'wsTokenSecrets' );
352  if ( !is_array( $secrets ) ) {
353  return false;
354  }
355  return isset( $secrets[$key] ) && is_string( $secrets[$key] );
356  }
357 
368  public function getToken( $salt = '', $key = 'default' ) {
369  $new = false;
370  $secrets = $this->get( 'wsTokenSecrets' );
371  if ( !is_array( $secrets ) ) {
372  $secrets = [];
373  }
374  if ( isset( $secrets[$key] ) && is_string( $secrets[$key] ) ) {
375  $secret = $secrets[$key];
376  } else {
377  $secret = \MWCryptRand::generateHex( 32 );
378  $secrets[$key] = $secret;
379  $this->set( 'wsTokenSecrets', $secrets );
380  $new = true;
381  }
382  if ( is_array( $salt ) ) {
383  $salt = implode( '|', $salt );
384  }
385  return new Token( $secret, (string)$salt, $new );
386  }
387 
395  public function resetToken( $key = 'default' ) {
396  $secrets = $this->get( 'wsTokenSecrets' );
397  if ( is_array( $secrets ) && isset( $secrets[$key] ) ) {
398  unset( $secrets[$key] );
399  $this->set( 'wsTokenSecrets', $secrets );
400  }
401  }
402 
406  public function resetAllTokens() {
407  $this->remove( 'wsTokenSecrets' );
408  }
409 
414  private function getSecretKeys() {
415  $mainConfig = MediaWikiServices::getInstance()->getMainConfig();
416  $sessionSecret = $mainConfig->get( MainConfigNames::SessionSecret );
417  $secretKey = $mainConfig->get( MainConfigNames::SecretKey );
418  $sessionPbkdf2Iterations = $mainConfig->get( MainConfigNames::SessionPbkdf2Iterations );
419  $wikiSecret = $sessionSecret ?: $secretKey;
420  $userSecret = $this->get( 'wsSessionSecret', null );
421  if ( $userSecret === null ) {
422  $userSecret = \MWCryptRand::generateHex( 32 );
423  $this->set( 'wsSessionSecret', $userSecret );
424  }
425  $iterations = $this->get( 'wsSessionPbkdf2Iterations', null );
426  if ( $iterations === null ) {
427  $iterations = $sessionPbkdf2Iterations;
428  $this->set( 'wsSessionPbkdf2Iterations', $iterations );
429  }
430 
431  $keymats = hash_pbkdf2( 'sha256', $wikiSecret, $userSecret, $iterations, 64, true );
432  return [
433  substr( $keymats, 0, 32 ),
434  substr( $keymats, 32, 32 ),
435  ];
436  }
437 
442  private static function getEncryptionAlgorithm() {
443  $sessionInsecureSecrets = MediaWikiServices::getInstance()->getMainConfig()
445 
446  if ( self::$encryptionAlgorithm === null ) {
447  if ( function_exists( 'openssl_encrypt' ) ) {
448  $methods = openssl_get_cipher_methods();
449  if ( in_array( 'aes-256-ctr', $methods, true ) ) {
450  self::$encryptionAlgorithm = [ 'openssl', 'aes-256-ctr' ];
451  return self::$encryptionAlgorithm;
452  }
453  if ( in_array( 'aes-256-cbc', $methods, true ) ) {
454  self::$encryptionAlgorithm = [ 'openssl', 'aes-256-cbc' ];
455  return self::$encryptionAlgorithm;
456  }
457  }
458 
459  if ( $sessionInsecureSecrets ) {
460  // @todo: import a pure-PHP library for AES instead of this
461  self::$encryptionAlgorithm = [ 'insecure' ];
462  return self::$encryptionAlgorithm;
463  }
464 
465  throw new \BadMethodCallException(
466  'Encryption is not available. You really should install the PHP OpenSSL extension. ' .
467  'But if you really can\'t and you\'re willing ' .
468  'to accept insecure storage of sensitive session data, set ' .
469  '$wgSessionInsecureSecrets = true in LocalSettings.php to make this exception go away.'
470  );
471  }
472 
473  return self::$encryptionAlgorithm;
474  }
475 
484  public function setSecret( $key, $value ) {
485  list( $encKey, $hmacKey ) = $this->getSecretKeys();
486  $serialized = serialize( $value );
487 
488  // The code for encryption (with OpenSSL) and sealing is taken from
489  // Chris Steipp's OATHAuthUtils class in Extension::OATHAuth.
490 
491  // Encrypt
492  // @todo: import a pure-PHP library for AES instead of doing $wgSessionInsecureSecrets
493  $iv = random_bytes( 16 );
494  $algorithm = self::getEncryptionAlgorithm();
495  switch ( $algorithm[0] ) {
496  case 'openssl':
497  $ciphertext = openssl_encrypt( $serialized, $algorithm[1], $encKey, OPENSSL_RAW_DATA, $iv );
498  if ( $ciphertext === false ) {
499  throw new \UnexpectedValueException( 'Encryption failed: ' . openssl_error_string() );
500  }
501  break;
502  case 'insecure':
503  $ex = new \Exception( 'No encryption is available, storing data as plain text' );
504  $this->logger->warning( $ex->getMessage(), [ 'exception' => $ex ] );
505  $ciphertext = $serialized;
506  break;
507  default:
508  throw new \LogicException( 'invalid algorithm' );
509  }
510 
511  // Seal
512  $sealed = base64_encode( $iv ) . '.' . base64_encode( $ciphertext );
513  $hmac = hash_hmac( 'sha256', $sealed, $hmacKey, true );
514  $encrypted = base64_encode( $hmac ) . '.' . $sealed;
515 
516  // Store
517  $this->set( $key, $encrypted );
518  }
519 
526  public function getSecret( $key, $default = null ) {
527  // Fetch
528  $encrypted = $this->get( $key, null );
529  if ( $encrypted === null ) {
530  return $default;
531  }
532 
533  // The code for unsealing, checking, and decrypting (with OpenSSL) is
534  // taken from Chris Steipp's OATHAuthUtils class in
535  // Extension::OATHAuth.
536 
537  // Unseal and check
538  $pieces = explode( '.', $encrypted, 4 );
539  if ( count( $pieces ) !== 3 ) {
540  $ex = new \Exception( 'Invalid sealed-secret format' );
541  $this->logger->warning( $ex->getMessage(), [ 'exception' => $ex ] );
542  return $default;
543  }
544  list( $hmac, $iv, $ciphertext ) = $pieces;
545  list( $encKey, $hmacKey ) = $this->getSecretKeys();
546  $integCalc = hash_hmac( 'sha256', $iv . '.' . $ciphertext, $hmacKey, true );
547  if ( !hash_equals( $integCalc, base64_decode( $hmac ) ) ) {
548  $ex = new \Exception( 'Sealed secret has been tampered with, aborting.' );
549  $this->logger->warning( $ex->getMessage(), [ 'exception' => $ex ] );
550  return $default;
551  }
552 
553  // Decrypt
554  $algorithm = self::getEncryptionAlgorithm();
555  switch ( $algorithm[0] ) {
556  case 'openssl':
557  $serialized = openssl_decrypt( base64_decode( $ciphertext ), $algorithm[1], $encKey,
558  OPENSSL_RAW_DATA, base64_decode( $iv ) );
559  if ( $serialized === false ) {
560  $ex = new \Exception( 'Decyption failed: ' . openssl_error_string() );
561  $this->logger->debug( $ex->getMessage(), [ 'exception' => $ex ] );
562  return $default;
563  }
564  break;
565  case 'insecure':
566  $ex = new \Exception(
567  'No encryption is available, retrieving data that was stored as plain text'
568  );
569  $this->logger->warning( $ex->getMessage(), [ 'exception' => $ex ] );
570  $serialized = base64_decode( $ciphertext );
571  break;
572  default:
573  throw new \LogicException( 'invalid algorithm' );
574  }
575 
576  $value = unserialize( $serialized );
577  if ( $value === false && $serialized !== serialize( false ) ) {
578  $value = $default;
579  }
580  return $value;
581  }
582 
590  public function delaySave() {
591  return $this->backend->delaySave();
592  }
593 
598  public function save() {
599  $this->backend->save();
600  }
601 
602  // region Interface methods
608  #[\ReturnTypeWillChange]
609  public function count() {
610  $data = &$this->backend->getData();
611  return count( $data );
612  }
613 
615  #[\ReturnTypeWillChange]
616  public function current() {
617  $data = &$this->backend->getData();
618  return current( $data );
619  }
620 
622  #[\ReturnTypeWillChange]
623  public function key() {
624  $data = &$this->backend->getData();
625  return key( $data );
626  }
627 
629  #[\ReturnTypeWillChange]
630  public function next() {
631  $data = &$this->backend->getData();
632  next( $data );
633  }
634 
636  #[\ReturnTypeWillChange]
637  public function rewind() {
638  $data = &$this->backend->getData();
639  reset( $data );
640  }
641 
643  #[\ReturnTypeWillChange]
644  public function valid() {
645  $data = &$this->backend->getData();
646  return key( $data ) !== null;
647  }
648 
654  public function offsetExists( $offset ): bool {
655  $data = &$this->backend->getData();
656  return isset( $data[$offset] );
657  }
658 
667  #[\ReturnTypeWillChange]
668  public function &offsetGet( $offset ) {
669  $data = &$this->backend->getData();
670  if ( !array_key_exists( $offset, $data ) ) {
671  $ex = new \Exception( "Undefined index (auto-adds to session with a null value): $offset" );
672  $this->logger->debug( $ex->getMessage(), [ 'exception' => $ex ] );
673  }
674  return $data[$offset];
675  }
676 
678  public function offsetSet( $offset, $value ): void {
679  $this->set( $offset, $value );
680  }
681 
683  public function offsetUnset( $offset ): void {
684  $this->remove( $offset );
685  }
686 
688  // endregion -- end of Interface methods
689 }
serialize()
unserialize( $serialized)
static generateHex( $chars)
Generate a run of cryptographically random data and return it in hexadecimal string format.
Definition: MWCryptRand.php:36
A class containing constants representing the names of configuration variables.
const SessionSecret
Name constant for the SessionSecret setting, for use with Config::get()
const SessionPbkdf2Iterations
Name constant for the SessionPbkdf2Iterations setting, for use with Config::get()
const SessionInsecureSecrets
Name constant for the SessionInsecureSecrets setting, for use with Config::get()
const SecretKey
Name constant for the SecretKey setting, for use with Config::get()
MediaWikiServices is the service locator for the application scope of MediaWiki.
static getInstance()
Returns the global default instance of the top level service locator.
This is the actual workhorse for Session.
Manages data for an authenticated session.
Definition: Session.php:50
offsetSet( $offset, $value)
Definition: Session.php:678
delaySave()
Delay automatic saving while multiple updates are being made.
Definition: Session.php:590
int $index
Session index.
Definition: Session.php:58
suggestLoginUsername()
Get a suggested username for the login form.
Definition: Session.php:209
static getEncryptionAlgorithm()
Decide what type of encryption to use, based on system capabilities.
Definition: Session.php:442
setForceHTTPS( $force)
Set the value of the forceHTTPS cookie.
Definition: Session.php:231
getToken( $salt='', $key='default')
Fetch a CSRF token from the session.
Definition: Session.php:368
setSecret( $key, $value)
Set a value in the session, encrypted.
Definition: Session.php:484
persist()
Make this session persisted across requests.
Definition: Session.php:128
shouldForceHTTPS()
Get the expected value of the forceHTTPS cookie.
Definition: Session.php:220
renew()
Resets the TTL in the backend store if the session is near expiring, and re-persists the session to a...
Definition: Session.php:278
getSecret( $key, $default=null)
Fetch a value from the session that was set with self::setSecret()
Definition: Session.php:526
resetId()
Changes the session ID.
Definition: Session.php:99
getProvider()
Fetch the SessionProvider for this session.
Definition: Session.php:107
isPersistent()
Indicate whether this session is persisted across requests.
Definition: Session.php:118
getRequest()
Returns the request associated with this session.
Definition: Session.php:166
getProviderMetadata()
Fetch provider metadata.
Definition: Session.php:255
resetToken( $key='default')
Remove a CSRF token from the session.
Definition: Session.php:395
sessionWithRequest(WebRequest $request)
Fetch a copy of this session attached to an alternative WebRequest.
Definition: Session.php:291
save()
This will update the backend data and might re-persist the session if needed.
Definition: Session.php:598
static null string[] $encryptionAlgorithm
Encryption algorithm to use.
Definition: Session.php:52
unpersist()
Make this session not be persisted across requests.
Definition: Session.php:140
canSetUser()
Indicate whether the session user info can be changed.
Definition: Session.php:190
exists( $key)
Test if a value exists in the session.
Definition: Session.php:313
getLoggedOutTimestamp()
Fetch the "logged out" timestamp.
Definition: Session.php:239
getUser()
Returns the authenticated user for this session.
Definition: Session.php:174
getId()
Returns the session ID.
Definition: Session.php:82
getSecretKeys()
Fetch the secret keys for self::setSecret() and self::getSecret().
Definition: Session.php:414
setRememberUser( $remember)
Set whether the user should be remembered independently of the session ID.
Definition: Session.php:158
resetAllTokens()
Remove all CSRF tokens from the session.
Definition: Session.php:406
get( $key, $default=null)
Fetch a value from the session.
Definition: Session.php:302
hasToken(string $key='default')
Check if a CSRF token is set for the session.
Definition: Session.php:350
clear()
Delete all session data and clear the user (if possible)
Definition: Session.php:262
shouldRememberUser()
Indicate whether the user should be remembered independently of the session ID.
Definition: Session.php:149
SessionBackend $backend
Session backend.
Definition: Session.php:55
LoggerInterface $logger
Definition: Session.php:61
setUser( $user)
Set a new user for this session.
Definition: Session.php:201
getSessionId()
Returns the SessionId object.
Definition: Session.php:91
getAllowedUserRights()
Fetch the rights allowed the user when this session is active.
Definition: Session.php:182
__construct(SessionBackend $backend, $index, LoggerInterface $logger)
Definition: Session.php:68
Value object representing a CSRF token.
Definition: Token.php:32
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:68
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form stripping il...
Definition: WebRequest.php:43
setSessionId(SessionId $sessionId)
Set the session for this request.
Definition: WebRequest.php:853
foreach( $res as $row) $serialized