public function crypt( $password ) {
$secret = $this->config['secrets'][$this->params['secret']];
+ // Clear error string
+ while ( openssl_error_string() !== false );
+
if ( $this->hash ) {
- $underlyingPassword = $this->factory->newFromCiphertext( openssl_decrypt(
- base64_decode( $this->hash ), $this->params['cipher'],
- $secret, 0, base64_decode( $this->args[0] )
- ) );
+ $decrypted = openssl_decrypt(
+ $this->hash, $this->params['cipher'],
+ $secret, 0, base64_decode( $this->args[0] ) );
+ if ( $decrypted === false ) {
+ throw new PasswordError( 'Error decrypting password: ' . openssl_error_string() );
+ }
+ $underlyingPassword = $this->factory->newFromCiphertext( $decrypted );
} else {
$underlyingPassword = $this->factory->newFromType( $this->config['underlying'] );
}
$underlyingPassword->crypt( $password );
- $iv = MWCryptRand::generate( openssl_cipher_iv_length( $this->params['cipher'] ), true );
+ if ( count( $this->args ) ) {
+ $iv = base64_decode( $this->args[0] );
+ } else {
+ $iv = MWCryptRand::generate( openssl_cipher_iv_length( $this->params['cipher'] ), true );
+ }
$this->hash = openssl_encrypt(
$underlyingPassword->toString(), $this->params['cipher'], $secret, 0, $iv );
+ if ( $this->hash === false ) {
+ throw new PasswordError( 'Error encrypting password: ' . openssl_error_string() );
+ }
$this->args = [ base64_encode( $iv ) ];
}
* @return bool True if the password was updated
*/
public function update() {
- if ( count( $this->args ) != 2 || $this->params == $this->getDefaultParams() ) {
+ if ( count( $this->args ) != 1 || $this->params == $this->getDefaultParams() ) {
// Hash does not need updating
return false;
}
+ // Clear error string
+ while ( openssl_error_string() !== false );
+
// Decrypt the underlying hash
$underlyingHash = openssl_decrypt(
- base64_decode( $this->args[1] ),
+ $this->hash,
$this->params['cipher'],
$this->config['secrets'][$this->params['secret']],
0,
base64_decode( $this->args[0] )
);
+ if ( $underlyingHash === false ) {
+ throw new PasswordError( 'Error decrypting password: ' . openssl_error_string() );
+ }
// Reset the params
$this->params = $this->getDefaultParams();
// Check the key size with the new params
$iv = MWCryptRand::generate( openssl_cipher_iv_length( $this->params['cipher'] ), true );
- $this->hash = base64_encode( openssl_encrypt(
+ $this->hash = openssl_encrypt(
$underlyingHash,
$this->params['cipher'],
$this->config['secrets'][$this->params['secret']],
0,
$iv
- ) );
+ );
+ if ( $this->hash === false ) {
+ throw new PasswordError( 'Error encrypting password: ' . openssl_error_string() );
+ }
+
$this->args = [ base64_encode( $iv ) ];
return true;
}
public function crypt( $plaintext ) {
- $this->args = [];
- $this->hash = md5( $plaintext );
+ if ( count( $this->args ) === 1 ) {
+ // Accept (but do not generate) salted passwords with :A: prefix.
+ // These are actually B-type passwords, but an error in a previous
+ // version of MediaWiki caused them to be written with an :A:
+ // prefix.
+ $this->hash = md5( $this->args[0] . '-' . md5( $plaintext ) );
+ } else {
+ $this->args = [];
+ $this->hash = md5( $plaintext );
+ }
if ( !is_string( $this->hash ) || strlen( $this->hash ) < 32 ) {
throw new PasswordError( 'Error when hashing password.' );
public function onSuccess() {
if ( $this->getUser()->isAllowed( 'passwordreset' ) && $this->passwords ) {
- // @todo Logging
-
if ( $this->result->isGood() ) {
$this->getOutput()->addWikiMsg( 'passwordreset-emailsent-capture2',
count( $this->passwords ) );
use MediaWiki\Auth\AuthManager;
use MediaWiki\Auth\TemporaryPasswordAuthenticationRequest;
+use Psr\Log\LoggerAwareInterface;
+use Psr\Log\LoggerInterface;
+use MediaWiki\Logger\LoggerFactory;
/**
* Helper class for the password reset functionality shared by the web UI and the API.
* EmailNotificationSecondaryAuthenticationProvider (or something providing equivalent
* functionality) to be enabled.
*/
-class PasswordReset {
+class PasswordReset implements LoggerAwareInterface {
/** @var Config */
protected $config;
/** @var AuthManager */
protected $authManager;
+ /** @var LoggerInterface */
+ protected $logger;
+
/**
* In-process cache for isAllowed lookups, by username. Contains pairs of StatusValue objects
* (for false and true value of $displayPassword, respectively).
$this->config = $config;
$this->authManager = $authManager;
$this->permissionCache = new HashBagOStuff( [ 'maxKeys' => 1 ] );
+ $this->logger = LoggerFactory::getInstance( 'authentication' );
+ }
+
+ /**
+ * Set the logger instance to use.
+ *
+ * @param LoggerInterface $logger
+ * @since 1.29
+ */
+ public function setLogger( LoggerInterface $logger ) {
+ $this->logger = $logger;
}
/**
if ( $resetRoutes['username'] && $username ) {
$method = 'username';
$users = [ User::newFromName( $username ) ];
+ $email = null;
} elseif ( $resetRoutes['email'] && $email ) {
if ( !Sanitizer::validateEmail( $email ) ) {
return StatusValue::newFatal( 'passwordreset-invalidemail' );
}
$method = 'email';
$users = $this->getUsersByEmail( $email );
+ $username = null;
} else {
// The user didn't supply any data
return StatusValue::newFatal( 'passwordreset-nodata' );
}
}
+ $logContext = [
+ 'requestingIp' => $ip,
+ 'requestingUser' => $performingUser->getName(),
+ 'targetUsername' => $username,
+ 'targetEmail' => $email,
+ 'actualUser' => $firstUser->getName(),
+ 'capture' => $displayPassword,
+ ];
+
if ( !$result->isGood() ) {
+ $this->logger->info(
+ "{requestingUser} attempted password reset of {actualUser} but failed",
+ $logContext + [ 'errors' => $result->getErrors() ]
+ );
return $result;
}
}
}
+ if ( $displayPassword ) {
+ // The password capture thing is scary, so log
+ // at a higher warning level.
+ $this->logger->warning(
+ "{requestingUser} did password reset of {actualUser} with password capturing!",
+ $logContext
+ );
+ } else {
+ $this->logger->info(
+ "{requestingUser} did password reset of {actualUser}",
+ $logContext
+ );
+ }
+
return StatusValue::newGood( $passwords );
}
"passwordreset-nocaller": "A caller must be provided",
"passwordreset-nosuchcaller": "Caller does not exist: $1",
"passwordreset-ignored": "The password reset was not handled. Maybe no provider was configured?",
- "passwordreset-invalideamil": "Invalid email address",
+ "passwordreset-invalidemail": "Invalid email address",
"passwordreset-nodata": "Neither a username nor an email address was supplied",
"changeemail": "Change or remove email address",
"changeemail-summary": "",
"passwordreset-nocaller": "Shown when a password reset was requested but the process failed due to an internal error related to missing details about the origin (caller) of the password reset request.",
"passwordreset-nosuchcaller": "Shown when a password reset was requested but the username of the caller could not be resolved to a user. This is an internal error.\n\nParameters:\n* $1 - username of the caller",
"passwordreset-ignored": "Shown when password reset was unsuccessful due to configuration problems.",
- "passwordreset-invalideamil": "Returned when the email address is syntatically invalid.",
+ "passwordreset-invalidemail": "Returned when the email address is syntatically invalid.",
"passwordreset-nodata": "Returned when no data was provided.",
"changeemail": "Title of [[Special:ChangeEmail|special page]]. This page also allows removing the user's email address.",
"changeemail-summary": "{{ignored}}",