MediaWiki REL1_37
Session.php
Go to the documentation of this file.
1<?php
24namespace MediaWiki\Session;
25
26use Psr\Log\LoggerInterface;
27use User;
28use WebRequest;
29
48class 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' ];
446 return self::$encryptionAlgorithm;
447 }
448 if ( in_array( 'aes-256-cbc', $methods, true ) ) {
449 self::$encryptionAlgorithm = [ 'openssl', 'aes-256-cbc' ];
450 return self::$encryptionAlgorithm;
451 }
452 }
453
455 // @todo: import a pure-PHP library for AES instead of this
456 self::$encryptionAlgorithm = [ 'insecure' ];
457 return self::$encryptionAlgorithm;
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
468 return self::$encryptionAlgorithm;
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(): int {
604 $data = &$this->backend->getData();
605 return count( $data );
606 }
607
609 #[\ReturnTypeWillChange]
610 public function current() {
611 $data = &$this->backend->getData();
612 return current( $data );
613 }
614
616 #[\ReturnTypeWillChange]
617 public function key() {
618 $data = &$this->backend->getData();
619 return key( $data );
620 }
621
623 public function next(): void {
624 $data = &$this->backend->getData();
625 next( $data );
626 }
627
629 public function rewind(): void {
630 $data = &$this->backend->getData();
631 reset( $data );
632 }
633
635 public function valid(): bool {
636 $data = &$this->backend->getData();
637 return key( $data ) !== null;
638 }
639
645 public function offsetExists( $offset ): bool {
646 $data = &$this->backend->getData();
647 return isset( $data[$offset] );
648 }
649
658 #[\ReturnTypeWillChange]
659 public function &offsetGet( $offset ) {
660 $data = &$this->backend->getData();
661 if ( !array_key_exists( $offset, $data ) ) {
662 $ex = new \Exception( "Undefined index (auto-adds to session with a null value): $offset" );
663 $this->logger->debug( $ex->getMessage(), [ 'exception' => $ex ] );
664 }
665 return $data[$offset];
666 }
667
669 public function offsetSet( $offset, $value ): void {
670 $this->set( $offset, $value );
671 }
672
674 public function offsetUnset( $offset ): void {
675 $this->remove( $offset );
676 }
677
679 // endregion -- end of Interface methods
680}
serialize()
unserialize( $serialized)
$wgSecretKey
This should always be customised in LocalSettings.php.
$wgSessionPbkdf2Iterations
Number of internal PBKDF2 iterations to use when deriving session secrets.
$wgSessionSecret
Secret for session storage.
$wgSessionInsecureSecrets
If for some reason you can't install the PHP OpenSSL extension, you can set this to true to make Medi...
This is the actual workhorse for Session.
Manages data for an authenticated session.
Definition Session.php:48
offsetSet( $offset, $value)
Definition Session.php:669
delaySave()
Delay automatic saving while multiple updates are being made.
Definition Session.php:585
int $index
Session index.
Definition Session.php:56
suggestLoginUsername()
Get a suggested username for the login form.
Definition Session.php:207
static getEncryptionAlgorithm()
Decide what type of encryption to use, based on system capabilities.
Definition Session.php:438
setForceHTTPS( $force)
Set the value of the forceHTTPS cookie.
Definition Session.php:229
getToken( $salt='', $key='default')
Fetch a CSRF token from the session.
Definition Session.php:366
setSecret( $key, $value)
Set a value in the session, encrypted.
Definition Session.php:479
persist()
Make this session persisted across requests.
Definition Session.php:126
shouldForceHTTPS()
Get the expected value of the forceHTTPS cookie.
Definition Session.php:218
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
getSecret( $key, $default=null)
Fetch a value from the session that was set with self::setSecret()
Definition Session.php:521
resetId()
Changes the session ID.
Definition Session.php:97
getProvider()
Fetch the SessionProvider for this session.
Definition Session.php:105
isPersistent()
Indicate whether this session is persisted across requests.
Definition Session.php:116
getRequest()
Returns the request associated with this session.
Definition Session.php:164
getProviderMetadata()
Fetch provider metadata.
Definition Session.php:253
resetToken( $key='default')
Remove a CSRF token from the session.
Definition Session.php:393
sessionWithRequest(WebRequest $request)
Fetch a copy of this session attached to an alternative WebRequest.
Definition Session.php:289
save()
This will update the backend data and might re-persist the session if needed.
Definition Session.php:593
static null string[] $encryptionAlgorithm
Encryption algorithm to use.
Definition Session.php:50
unpersist()
Make this session not be persisted across requests.
Definition Session.php:138
canSetUser()
Indicate whether the session user info can be changed.
Definition Session.php:188
exists( $key)
Test if a value exists in the session.
Definition Session.php:311
getLoggedOutTimestamp()
Fetch the "logged out" timestamp.
Definition Session.php:237
getUser()
Returns the authenticated user for this session.
Definition Session.php:172
getId()
Returns the session ID.
Definition Session.php:80
getSecretKeys()
Fetch the secret keys for self::setSecret() and self::getSecret().
Definition Session.php:412
setRememberUser( $remember)
Set whether the user should be remembered independently of the session ID.
Definition Session.php:156
resetAllTokens()
Remove all CSRF tokens from the session.
Definition Session.php:404
get( $key, $default=null)
Fetch a value from the session.
Definition Session.php:300
hasToken(string $key='default')
Check if a CSRF token is set for the session.
Definition Session.php:348
clear()
Delete all session data and clear the user (if possible)
Definition Session.php:260
shouldRememberUser()
Indicate whether the user should be remembered independently of the session ID.
Definition Session.php:147
SessionBackend $backend
Session backend.
Definition Session.php:53
LoggerInterface $logger
Definition Session.php:59
setUser( $user)
Set a new user for this session.
Definition Session.php:199
getSessionId()
Returns the SessionId object.
Definition Session.php:89
getAllowedUserRights()
Fetch the rights allowed the user when this session is active.
Definition Session.php:180
__construct(SessionBackend $backend, $index, LoggerInterface $logger)
Definition Session.php:66
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:69
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form stripping il...
setSessionId(SessionId $sessionId)
Set the session for this request.
foreach( $res as $row) $serialized