Something went wrong. Try again.
@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
Something went wrong. Try again.
12 kB · 398 lines
PHP
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399<?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.
$object = $this->getObject();
if ($object->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')); } }
// Prevent use of passwords which are similar to any object identifier. // For example, if your username is "alincoln", your password may not be // "alincoln", "lincoln", or "alincoln1". $viewer = $this->getViewer(); $blocklist = $object->newPasswordBlocklist($viewer, $this);
// Smallest number of overlapping characters that we'll consider to be // too similar. $minimum_similarity = 4;
// Add the domain name to the blocklist. $base_uri = PhabricatorEnv::getAnyBaseURI(); $base_uri = new PhutilURI($base_uri); $blocklist[] = $base_uri->getDomain();
$blocklist = array_filter($blocklist);
// Generate additional subterms by splitting the raw blocklist on // characters like "@", " " (space), and "." to break up email addresses, // readable names, and domain names into components. $terms_map = array(); foreach ($blocklist as $term) { $terms_map[$term] = $term; foreach (preg_split('/[ @.]/', $term) as $subterm) { $terms_map[$subterm] = $term; } }
// Skip very short terms: it's okay if your password has the substring // "com" in it somewhere even if the install is on "mycompany.com". foreach ($terms_map as $term => $source) { if (strlen($term) < $minimum_similarity) { unset($terms_map[$term]); } }
// Normalize terms for comparison. $normal_map = array(); foreach ($terms_map as $term => $source) { $term = phutil_utf8_strtolower($term); $normal_map[$term] = $source; }
// Finally, make sure that none of the terms appear in the password, // and that the password does not appear in any of the terms. $normal_password = phutil_utf8_strtolower($raw_password); if (strlen($normal_password) >= $minimum_similarity) { foreach ($normal_map as $term => $source) {
// See T2312. This may be required if the term list includes numeric // strings like "12345", which will be cast to integers when used as // array keys. $term = phutil_string_cast($term);
if (strpos($term, $normal_password) === false && strpos($normal_password, $term) === false) { continue; }
throw new PhabricatorAuthPasswordException( pht( 'The password you entered is very similar to a nonsecret account '. 'identifier (like a username or email address). Choose a more '. 'distinct password.'), pht('Not Distinct')); } } }
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; }
/** * @param PhutilOpaqueEnvelope $envelope * @param array<PhabricatorAuthPassword> $passwords */ private function upgradeHashers( PhutilOpaqueEnvelope $envelope, array $passwords) {
assert_instances_of($passwords, PhabricatorAuthPassword::class);
$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); }
}