Path: blob/master/src/applications/auth/engine/PhabricatorAuthPasswordEngine.php
12256 views
<?php12final class PhabricatorAuthPasswordEngine3extends Phobject {45private $viewer;6private $contentSource;7private $object;8private $passwordType;9private $upgradeHashers = true;1011public function setViewer(PhabricatorUser $viewer) {12$this->viewer = $viewer;13return $this;14}1516public function getViewer() {17return $this->viewer;18}1920public function setContentSource(PhabricatorContentSource $content_source) {21$this->contentSource = $content_source;22return $this;23}2425public function getContentSource() {26return $this->contentSource;27}2829public function setObject(PhabricatorAuthPasswordHashInterface $object) {30$this->object = $object;31return $this;32}3334public function getObject() {35return $this->object;36}3738public function setPasswordType($password_type) {39$this->passwordType = $password_type;40return $this;41}4243public function getPasswordType() {44return $this->passwordType;45}4647public function setUpgradeHashers($upgrade_hashers) {48$this->upgradeHashers = $upgrade_hashers;49return $this;50}5152public function getUpgradeHashers() {53return $this->upgradeHashers;54}5556public function checkNewPassword(57PhutilOpaqueEnvelope $password,58PhutilOpaqueEnvelope $confirm,59$can_skip = false) {6061$raw_password = $password->openEnvelope();6263if (!strlen($raw_password)) {64if ($can_skip) {65throw new PhabricatorAuthPasswordException(66pht('You must choose a password or skip this step.'),67pht('Required'));68} else {69throw new PhabricatorAuthPasswordException(70pht('You must choose a password.'),71pht('Required'));72}73}7475$min_len = PhabricatorEnv::getEnvConfig('account.minimum-password-length');76$min_len = (int)$min_len;77if ($min_len) {78if (strlen($raw_password) < $min_len) {79throw new PhabricatorAuthPasswordException(80pht(81'The selected password is too short. Passwords must be a minimum '.82'of %s characters long.',83new PhutilNumber($min_len)),84pht('Too Short'));85}86}8788$raw_confirm = $confirm->openEnvelope();8990if (!strlen($raw_confirm)) {91throw new PhabricatorAuthPasswordException(92pht('You must confirm the selected password.'),93null,94pht('Required'));95}9697if ($raw_password !== $raw_confirm) {98throw new PhabricatorAuthPasswordException(99pht('The password and confirmation do not match.'),100pht('Invalid'),101pht('Invalid'));102}103104if (PhabricatorCommonPasswords::isCommonPassword($raw_password)) {105throw new PhabricatorAuthPasswordException(106pht(107'The selected password is very weak: it is one of the most common '.108'passwords in use. Choose a stronger password.'),109pht('Very Weak'));110}111112// If we're creating a brand new object (like registering a new user)113// and it does not have a PHID yet, it isn't possible for it to have any114// revoked passwords or colliding passwords either, so we can skip these115// checks.116117$object = $this->getObject();118119if ($object->getPHID()) {120if ($this->isRevokedPassword($password)) {121throw new PhabricatorAuthPasswordException(122pht(123'The password you entered has been revoked. You can not reuse '.124'a password which has been revoked. Choose a new password.'),125pht('Revoked'));126}127128if (!$this->isUniquePassword($password)) {129throw new PhabricatorAuthPasswordException(130pht(131'The password you entered is the same as another password '.132'associated with your account. Each password must be unique.'),133pht('Not Unique'));134}135}136137// Prevent use of passwords which are similar to any object identifier.138// For example, if your username is "alincoln", your password may not be139// "alincoln", "lincoln", or "alincoln1".140$viewer = $this->getViewer();141$blocklist = $object->newPasswordBlocklist($viewer, $this);142143// Smallest number of overlapping characters that we'll consider to be144// too similar.145$minimum_similarity = 4;146147// Add the domain name to the blocklist.148$base_uri = PhabricatorEnv::getAnyBaseURI();149$base_uri = new PhutilURI($base_uri);150$blocklist[] = $base_uri->getDomain();151152// Generate additional subterms by splitting the raw blocklist on153// characters like "@", " " (space), and "." to break up email addresses,154// readable names, and domain names into components.155$terms_map = array();156foreach ($blocklist as $term) {157$terms_map[$term] = $term;158foreach (preg_split('/[ @.]/', $term) as $subterm) {159$terms_map[$subterm] = $term;160}161}162163// Skip very short terms: it's okay if your password has the substring164// "com" in it somewhere even if the install is on "mycompany.com".165foreach ($terms_map as $term => $source) {166if (strlen($term) < $minimum_similarity) {167unset($terms_map[$term]);168}169}170171// Normalize terms for comparison.172$normal_map = array();173foreach ($terms_map as $term => $source) {174$term = phutil_utf8_strtolower($term);175$normal_map[$term] = $source;176}177178// Finally, make sure that none of the terms appear in the password,179// and that the password does not appear in any of the terms.180$normal_password = phutil_utf8_strtolower($raw_password);181if (strlen($normal_password) >= $minimum_similarity) {182foreach ($normal_map as $term => $source) {183184// See T2312. This may be required if the term list includes numeric185// strings like "12345", which will be cast to integers when used as186// array keys.187$term = phutil_string_cast($term);188189if (strpos($term, $normal_password) === false &&190strpos($normal_password, $term) === false) {191continue;192}193194throw new PhabricatorAuthPasswordException(195pht(196'The password you entered is very similar to a nonsecret account '.197'identifier (like a username or email address). Choose a more '.198'distinct password.'),199pht('Not Distinct'));200}201}202}203204public function isValidPassword(PhutilOpaqueEnvelope $envelope) {205$this->requireSetup();206207$password_type = $this->getPasswordType();208209$passwords = $this->newQuery()210->withPasswordTypes(array($password_type))211->withIsRevoked(false)212->execute();213214$matches = $this->getMatches($envelope, $passwords);215if (!$matches) {216return false;217}218219if ($this->shouldUpgradeHashers()) {220$this->upgradeHashers($envelope, $matches);221}222223return true;224}225226public function isUniquePassword(PhutilOpaqueEnvelope $envelope) {227$this->requireSetup();228229$password_type = $this->getPasswordType();230231// To test that the password is unique, we're loading all active and232// revoked passwords for all roles for the given user, then throwing out233// the active passwords for the current role (so a password can't234// collide with itself).235236// Note that two different objects can have the same password (say,237// users @alice and @bailey). We're only preventing @alice from using238// the same password for everything.239240$passwords = $this->newQuery()241->execute();242243foreach ($passwords as $key => $password) {244$same_type = ($password->getPasswordType() === $password_type);245$is_active = !$password->getIsRevoked();246247if ($same_type && $is_active) {248unset($passwords[$key]);249}250}251252$matches = $this->getMatches($envelope, $passwords);253254return !$matches;255}256257public function isRevokedPassword(PhutilOpaqueEnvelope $envelope) {258$this->requireSetup();259260// To test if a password is revoked, we're loading all revoked passwords261// across all roles for the given user. If a password was revoked in one262// role, you can't reuse it in a different role.263264$passwords = $this->newQuery()265->withIsRevoked(true)266->execute();267268$matches = $this->getMatches($envelope, $passwords);269270return (bool)$matches;271}272273private function requireSetup() {274if (!$this->getObject()) {275throw new PhutilInvalidStateException('setObject');276}277278if (!$this->getPasswordType()) {279throw new PhutilInvalidStateException('setPasswordType');280}281282if (!$this->getViewer()) {283throw new PhutilInvalidStateException('setViewer');284}285286if ($this->shouldUpgradeHashers()) {287if (!$this->getContentSource()) {288throw new PhutilInvalidStateException('setContentSource');289}290}291}292293private function shouldUpgradeHashers() {294if (!$this->getUpgradeHashers()) {295return false;296}297298if (PhabricatorEnv::isReadOnly()) {299// Don't try to upgrade hashers if we're in read-only mode, since we300// won't be able to write the new hash to the database.301return false;302}303304return true;305}306307private function newQuery() {308$viewer = $this->getViewer();309$object = $this->getObject();310$password_type = $this->getPasswordType();311312return id(new PhabricatorAuthPasswordQuery())313->setViewer($viewer)314->withObjectPHIDs(array($object->getPHID()));315}316317private function getMatches(318PhutilOpaqueEnvelope $envelope,319array $passwords) {320321$object = $this->getObject();322323$matches = array();324foreach ($passwords as $password) {325try {326$is_match = $password->comparePassword($envelope, $object);327} catch (PhabricatorPasswordHasherUnavailableException $ex) {328$is_match = false;329}330331if ($is_match) {332$matches[] = $password;333}334}335336return $matches;337}338339private function upgradeHashers(340PhutilOpaqueEnvelope $envelope,341array $passwords) {342343assert_instances_of($passwords, 'PhabricatorAuthPassword');344345$need_upgrade = array();346foreach ($passwords as $password) {347if (!$password->canUpgrade()) {348continue;349}350$need_upgrade[] = $password;351}352353if (!$need_upgrade) {354return;355}356357$upgrade_type = PhabricatorAuthPasswordUpgradeTransaction::TRANSACTIONTYPE;358$viewer = $this->getViewer();359$content_source = $this->getContentSource();360361$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();362foreach ($need_upgrade as $password) {363364// This does the actual upgrade. We then apply a transaction to make365// the upgrade more visible and auditable.366$old_hasher = $password->getHasher();367$password->upgradePasswordHasher($envelope, $this->getObject());368$new_hasher = $password->getHasher();369370// NOTE: We must save the change before applying transactions because371// the editor will reload the object to obtain a read lock.372$password->save();373374$xactions = array();375376$xactions[] = $password->getApplicationTransactionTemplate()377->setTransactionType($upgrade_type)378->setNewValue($new_hasher->getHashName());379380$editor = $password->getApplicationTransactionEditor()381->setActor($viewer)382->setContinueOnNoEffect(true)383->setContinueOnMissingFields(true)384->setContentSource($content_source)385->setOldHasher($old_hasher)386->applyTransactions($password, $xactions);387}388unset($unguarded);389}390391}392393394