MediaWiki fundraising/REL1_35
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
245 public function setLoggedOutTimestamp( $ts ) {
246 $this->backend->setLoggedOutTimestamp( $ts );
247 }
248
254 public function getProviderMetadata() {
255 return $this->backend->getProviderMetadata();
256 }
257
261 public function clear() {
262 $data = &$this->backend->getData();
263 if ( $data ) {
264 $data = [];
265 $this->backend->dirty();
266 }
267 if ( $this->backend->canSetUser() ) {
268 $this->backend->setUser( new User );
269 }
270 $this->backend->save();
271 }
272
279 public function renew() {
280 $this->backend->renew();
281 }
282
292 public function sessionWithRequest( WebRequest $request ) {
293 $request->setSessionId( $this->backend->getSessionId() );
294 return $this->backend->getSession( $request );
295 }
296
303 public function get( $key, $default = null ) {
304 $data = &$this->backend->getData();
305 return array_key_exists( $key, $data ) ? $data[$key] : $default;
306 }
307
314 public function exists( $key ) {
315 $data = &$this->backend->getData();
316 return array_key_exists( $key, $data );
317 }
318
324 public function set( $key, $value ) {
325 $data = &$this->backend->getData();
326 if ( !array_key_exists( $key, $data ) || $data[$key] !== $value ) {
327 $data[$key] = $value;
328 $this->backend->dirty();
329 }
330 }
331
336 public function remove( $key ) {
337 $data = &$this->backend->getData();
338 if ( array_key_exists( $key, $data ) ) {
339 unset( $data[$key] );
340 $this->backend->dirty();
341 }
342 }
343
354 public function getToken( $salt = '', $key = 'default' ) {
355 $new = false;
356 $secrets = $this->get( 'wsTokenSecrets' );
357 if ( !is_array( $secrets ) ) {
358 $secrets = [];
359 }
360 if ( isset( $secrets[$key] ) && is_string( $secrets[$key] ) ) {
361 $secret = $secrets[$key];
362 } else {
363 $secret = \MWCryptRand::generateHex( 32 );
364 $secrets[$key] = $secret;
365 $this->set( 'wsTokenSecrets', $secrets );
366 $new = true;
367 }
368 if ( is_array( $salt ) ) {
369 $salt = implode( '|', $salt );
370 }
371 return new Token( $secret, (string)$salt, $new );
372 }
373
381 public function resetToken( $key = 'default' ) {
382 $secrets = $this->get( 'wsTokenSecrets' );
383 if ( is_array( $secrets ) && isset( $secrets[$key] ) ) {
384 unset( $secrets[$key] );
385 $this->set( 'wsTokenSecrets', $secrets );
386 }
387 }
388
392 public function resetAllTokens() {
393 $this->remove( 'wsTokenSecrets' );
394 }
395
400 private function getSecretKeys() {
402
403 $wikiSecret = $wgSessionSecret ?: $wgSecretKey;
404 $userSecret = $this->get( 'wsSessionSecret', null );
405 if ( $userSecret === null ) {
406 $userSecret = \MWCryptRand::generateHex( 32 );
407 $this->set( 'wsSessionSecret', $userSecret );
408 }
409 $iterations = $this->get( 'wsSessionPbkdf2Iterations', null );
410 if ( $iterations === null ) {
411 $iterations = $wgSessionPbkdf2Iterations;
412 $this->set( 'wsSessionPbkdf2Iterations', $iterations );
413 }
414
415 $keymats = hash_pbkdf2( 'sha256', $wikiSecret, $userSecret, $iterations, 64, true );
416 return [
417 substr( $keymats, 0, 32 ),
418 substr( $keymats, 32, 32 ),
419 ];
420 }
421
426 private static function getEncryptionAlgorithm() {
428
429 if ( self::$encryptionAlgorithm === null ) {
430 if ( function_exists( 'openssl_encrypt' ) ) {
431 $methods = openssl_get_cipher_methods();
432 if ( in_array( 'aes-256-ctr', $methods, true ) ) {
433 self::$encryptionAlgorithm = [ 'openssl', 'aes-256-ctr' ];
435 }
436 if ( in_array( 'aes-256-cbc', $methods, true ) ) {
437 self::$encryptionAlgorithm = [ 'openssl', 'aes-256-cbc' ];
439 }
440 }
441
443 // @todo: import a pure-PHP library for AES instead of this
444 self::$encryptionAlgorithm = [ 'insecure' ];
446 }
447
448 throw new \BadMethodCallException(
449 'Encryption is not available. You really should install the PHP OpenSSL extension. ' .
450 'But if you really can\'t and you\'re willing ' .
451 'to accept insecure storage of sensitive session data, set ' .
452 '$wgSessionInsecureSecrets = true in LocalSettings.php to make this exception go away.'
453 );
454 }
455
457 }
458
467 public function setSecret( $key, $value ) {
468 list( $encKey, $hmacKey ) = $this->getSecretKeys();
469 $serialized = serialize( $value );
470
471 // The code for encryption (with OpenSSL) and sealing is taken from
472 // Chris Steipp's OATHAuthUtils class in Extension::OATHAuth.
473
474 // Encrypt
475 // @todo: import a pure-PHP library for AES instead of doing $wgSessionInsecureSecrets
476 $iv = random_bytes( 16 );
477 $algorithm = self::getEncryptionAlgorithm();
478 switch ( $algorithm[0] ) {
479 case 'openssl':
480 $ciphertext = openssl_encrypt( $serialized, $algorithm[1], $encKey, OPENSSL_RAW_DATA, $iv );
481 if ( $ciphertext === false ) {
482 throw new \UnexpectedValueException( 'Encryption failed: ' . openssl_error_string() );
483 }
484 break;
485 case 'insecure':
486 $ex = new \Exception( 'No encryption is available, storing data as plain text' );
487 $this->logger->warning( $ex->getMessage(), [ 'exception' => $ex ] );
488 $ciphertext = $serialized;
489 break;
490 default:
491 throw new \LogicException( 'invalid algorithm' );
492 }
493
494 // Seal
495 $sealed = base64_encode( $iv ) . '.' . base64_encode( $ciphertext );
496 $hmac = hash_hmac( 'sha256', $sealed, $hmacKey, true );
497 $encrypted = base64_encode( $hmac ) . '.' . $sealed;
498
499 // Store
500 $this->set( $key, $encrypted );
501 }
502
509 public function getSecret( $key, $default = null ) {
510 // Fetch
511 $encrypted = $this->get( $key, null );
512 if ( $encrypted === null ) {
513 return $default;
514 }
515
516 // The code for unsealing, checking, and decrypting (with OpenSSL) is
517 // taken from Chris Steipp's OATHAuthUtils class in
518 // Extension::OATHAuth.
519
520 // Unseal and check
521 $pieces = explode( '.', $encrypted, 4 );
522 if ( count( $pieces ) !== 3 ) {
523 $ex = new \Exception( 'Invalid sealed-secret format' );
524 $this->logger->warning( $ex->getMessage(), [ 'exception' => $ex ] );
525 return $default;
526 }
527 list( $hmac, $iv, $ciphertext ) = $pieces;
528 list( $encKey, $hmacKey ) = $this->getSecretKeys();
529 $integCalc = hash_hmac( 'sha256', $iv . '.' . $ciphertext, $hmacKey, true );
530 if ( !hash_equals( $integCalc, base64_decode( $hmac ) ) ) {
531 $ex = new \Exception( 'Sealed secret has been tampered with, aborting.' );
532 $this->logger->warning( $ex->getMessage(), [ 'exception' => $ex ] );
533 return $default;
534 }
535
536 // Decrypt
537 $algorithm = self::getEncryptionAlgorithm();
538 switch ( $algorithm[0] ) {
539 case 'openssl':
540 $serialized = openssl_decrypt( base64_decode( $ciphertext ), $algorithm[1], $encKey,
541 OPENSSL_RAW_DATA, base64_decode( $iv ) );
542 if ( $serialized === false ) {
543 $ex = new \Exception( 'Decyption failed: ' . openssl_error_string() );
544 $this->logger->debug( $ex->getMessage(), [ 'exception' => $ex ] );
545 return $default;
546 }
547 break;
548 case 'insecure':
549 $ex = new \Exception(
550 'No encryption is available, retrieving data that was stored as plain text'
551 );
552 $this->logger->warning( $ex->getMessage(), [ 'exception' => $ex ] );
553 $serialized = base64_decode( $ciphertext );
554 break;
555 default:
556 throw new \LogicException( 'invalid algorithm' );
557 }
558
559 $value = unserialize( $serialized );
560 if ( $value === false && $serialized !== serialize( false ) ) {
561 $value = $default;
562 }
563 return $value;
564 }
565
573 public function delaySave() {
574 return $this->backend->delaySave();
575 }
576
583 public function save() {
584 $this->backend->save();
585 }
586
593 public function count(): int {
594 $data = &$this->backend->getData();
595 return count( $data );
596 }
597
599 #[\ReturnTypeWillChange]
600 public function current() {
601 $data = &$this->backend->getData();
602 return current( $data );
603 }
604
606 #[\ReturnTypeWillChange]
607 public function key() {
608 $data = &$this->backend->getData();
609 return key( $data );
610 }
611
613 public function next(): void {
614 $data = &$this->backend->getData();
615 next( $data );
616 }
617
619 public function rewind(): void {
620 $data = &$this->backend->getData();
621 reset( $data );
622 }
623
625 public function valid(): bool {
626 $data = &$this->backend->getData();
627 return key( $data ) !== null;
628 }
629
635 public function offsetExists( $offset ): bool {
636 $data = &$this->backend->getData();
637 return isset( $data[$offset] );
638 }
639
648 #[\ReturnTypeWillChange]
649 public function &offsetGet( $offset ) {
650 $data = &$this->backend->getData();
651 if ( !array_key_exists( $offset, $data ) ) {
652 $ex = new \Exception( "Undefined index (auto-adds to session with a null value): $offset" );
653 $this->logger->debug( $ex->getMessage(), [ 'exception' => $ex ] );
654 }
655 return $data[$offset];
656 }
657
659 public function offsetSet( $offset, $value ): void {
660 $this->set( $offset, $value );
661 }
662
664 public function offsetUnset( $offset ): void {
665 $this->remove( $offset );
666 }
667
670}
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 an authenticated session.
Definition Session.php:48
offsetSet( $offset, $value)
Definition Session.php:659
delaySave()
Delay automatic saving while multiple updates are being made.
Definition Session.php:573
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:426
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:354
setSecret( $key, $value)
Set a value in the session, encrypted.
Definition Session.php:467
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()
Renew the session.
Definition Session.php:279
getSecret( $key, $default=null)
Fetch a value from the session that was set with self::setSecret()
Definition Session.php:509
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:254
resetToken( $key='default')
Remove a CSRF token from the session.
Definition Session.php:381
sessionWithRequest(WebRequest $request)
Fetch a copy of this session attached to an alternative WebRequest.
Definition Session.php:292
save()
Save the session.
Definition Session.php:583
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:314
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:400
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:392
clear()
Delete all session data and clear the user (if possible)
Definition Session.php:261
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
setLoggedOutTimestamp( $ts)
Set the "logged out" timestamp.
Definition Session.php:245
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:60
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