Page MenuHomec4science

PhabricatorAuthPasswordEngine.php
No OneTemporary

File Metadata

Created
Wed, Feb 12, 09:08

PhabricatorAuthPasswordEngine.php

<?php
final class PhabricatorAuthPasswordEngine
extends Phobject {
private $viewer;
private $contentSource;
private $object;
private $passwordType;
private $upgradeHashers = true;
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
public function getViewer() {
return $this->viewer;
}
public function setContentSource(PhabricatorContentSource $content_source) {
$this->contentSource = $content_source;
return $this;
}
public function getContentSource() {
return $this->contentSource;
}
public function setObject(PhabricatorAuthPasswordHashInterface $object) {
$this->object = $object;
return $this;
}
public function getObject() {
return $this->object;
}
public function setPasswordType($password_type) {
$this->passwordType = $password_type;
return $this;
}
public function getPasswordType() {
return $this->passwordType;
}
public function setUpgradeHashers($upgrade_hashers) {
$this->upgradeHashers = $upgrade_hashers;
return $this;
}
public function getUpgradeHashers() {
return $this->upgradeHashers;
}
public function checkNewPassword(
PhutilOpaqueEnvelope $password,
PhutilOpaqueEnvelope $confirm,
$can_skip = false) {
$raw_password = $password->openEnvelope();
if (!strlen($raw_password)) {
if ($can_skip) {
throw new PhabricatorAuthPasswordException(
pht('You must choose a password or skip this step.'),
pht('Required'));
} else {
throw new PhabricatorAuthPasswordException(
pht('You must choose a password.'),
pht('Required'));
}
}
$min_len = PhabricatorEnv::getEnvConfig('account.minimum-password-length');
$min_len = (int)$min_len;
if ($min_len) {
if (strlen($raw_password) < $min_len) {
throw new PhabricatorAuthPasswordException(
pht(
'The selected password is too short. Passwords must be a minimum '.
'of %s characters long.',
new PhutilNumber($min_len)),
pht('Too Short'));
}
}
$raw_confirm = $confirm->openEnvelope();
if (!strlen($raw_confirm)) {
throw new PhabricatorAuthPasswordException(
pht('You must confirm the selected password.'),
null,
pht('Required'));
}
if ($raw_password !== $raw_confirm) {
throw new PhabricatorAuthPasswordException(
pht('The password and confirmation do not match.'),
pht('Invalid'),
pht('Invalid'));
}
if (PhabricatorCommonPasswords::isCommonPassword($raw_password)) {
throw new PhabricatorAuthPasswordException(
pht(
'The selected password is very weak: it is one of the most common '.
'passwords in use. Choose a stronger password.'),
pht('Very Weak'));
}
// If we're creating a brand new object (like registering a new user)
// and it does not have a PHID yet, it isn't possible for it to have any
// revoked passwords or colliding passwords either, so we can skip these
// checks.
if ($this->getObject()->getPHID()) {
if ($this->isRevokedPassword($password)) {
throw new PhabricatorAuthPasswordException(
pht(
'The password you entered has been revoked. You can not reuse '.
'a password which has been revoked. Choose a new password.'),
pht('Revoked'));
}
if (!$this->isUniquePassword($password)) {
throw new PhabricatorAuthPasswordException(
pht(
'The password you entered is the same as another password '.
'associated with your account. Each password must be unique.'),
pht('Not Unique'));
}
}
}
public function isValidPassword(PhutilOpaqueEnvelope $envelope) {
$this->requireSetup();
$password_type = $this->getPasswordType();
$passwords = $this->newQuery()
->withPasswordTypes(array($password_type))
->withIsRevoked(false)
->execute();
$matches = $this->getMatches($envelope, $passwords);
if (!$matches) {
return false;
}
if ($this->shouldUpgradeHashers()) {
$this->upgradeHashers($envelope, $matches);
}
return true;
}
public function isUniquePassword(PhutilOpaqueEnvelope $envelope) {
$this->requireSetup();
$password_type = $this->getPasswordType();
// To test that the password is unique, we're loading all active and
// revoked passwords for all roles for the given user, then throwing out
// the active passwords for the current role (so a password can't
// collide with itself).
// Note that two different objects can have the same password (say,
// users @alice and @bailey). We're only preventing @alice from using
// the same password for everything.
$passwords = $this->newQuery()
->execute();
foreach ($passwords as $key => $password) {
$same_type = ($password->getPasswordType() === $password_type);
$is_active = !$password->getIsRevoked();
if ($same_type && $is_active) {
unset($passwords[$key]);
}
}
$matches = $this->getMatches($envelope, $passwords);
return !$matches;
}
public function isRevokedPassword(PhutilOpaqueEnvelope $envelope) {
$this->requireSetup();
// To test if a password is revoked, we're loading all revoked passwords
// across all roles for the given user. If a password was revoked in one
// role, you can't reuse it in a different role.
$passwords = $this->newQuery()
->withIsRevoked(true)
->execute();
$matches = $this->getMatches($envelope, $passwords);
return (bool)$matches;
}
private function requireSetup() {
if (!$this->getObject()) {
throw new PhutilInvalidStateException('setObject');
}
if (!$this->getPasswordType()) {
throw new PhutilInvalidStateException('setPasswordType');
}
if (!$this->getViewer()) {
throw new PhutilInvalidStateException('setViewer');
}
if ($this->shouldUpgradeHashers()) {
if (!$this->getContentSource()) {
throw new PhutilInvalidStateException('setContentSource');
}
}
}
private function shouldUpgradeHashers() {
if (!$this->getUpgradeHashers()) {
return false;
}
if (PhabricatorEnv::isReadOnly()) {
// Don't try to upgrade hashers if we're in read-only mode, since we
// won't be able to write the new hash to the database.
return false;
}
return true;
}
private function newQuery() {
$viewer = $this->getViewer();
$object = $this->getObject();
$password_type = $this->getPasswordType();
return id(new PhabricatorAuthPasswordQuery())
->setViewer($viewer)
->withObjectPHIDs(array($object->getPHID()));
}
private function getMatches(
PhutilOpaqueEnvelope $envelope,
array $passwords) {
$object = $this->getObject();
$matches = array();
foreach ($passwords as $password) {
try {
$is_match = $password->comparePassword($envelope, $object);
} catch (PhabricatorPasswordHasherUnavailableException $ex) {
$is_match = false;
}
if ($is_match) {
$matches[] = $password;
}
}
return $matches;
}
private function upgradeHashers(
PhutilOpaqueEnvelope $envelope,
array $passwords) {
assert_instances_of($passwords, 'PhabricatorAuthPassword');
$need_upgrade = array();
foreach ($passwords as $password) {
if (!$password->canUpgrade()) {
continue;
}
$need_upgrade[] = $password;
}
if (!$need_upgrade) {
return;
}
$upgrade_type = PhabricatorAuthPasswordUpgradeTransaction::TRANSACTIONTYPE;
$viewer = $this->getViewer();
$content_source = $this->getContentSource();
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
foreach ($need_upgrade as $password) {
// This does the actual upgrade. We then apply a transaction to make
// the upgrade more visible and auditable.
$old_hasher = $password->getHasher();
$password->upgradePasswordHasher($envelope, $this->getObject());
$new_hasher = $password->getHasher();
// NOTE: We must save the change before applying transactions because
// the editor will reload the object to obtain a read lock.
$password->save();
$xactions = array();
$xactions[] = $password->getApplicationTransactionTemplate()
->setTransactionType($upgrade_type)
->setNewValue($new_hasher->getHashName());
$editor = $password->getApplicationTransactionEditor()
->setActor($viewer)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->setContentSource($content_source)
->setOldHasher($old_hasher)
->applyTransactions($password, $xactions);
}
unset($unguarded);
}
}

Event Timeline