diff --git a/src/applications/diffusion/panel/DiffusionSetPasswordPanel.php b/src/applications/diffusion/panel/DiffusionSetPasswordPanel.php
index c45acb7185..21e00c7076 100644
--- a/src/applications/diffusion/panel/DiffusionSetPasswordPanel.php
+++ b/src/applications/diffusion/panel/DiffusionSetPasswordPanel.php
@@ -1,252 +1,274 @@
 <?php
 
 final class DiffusionSetPasswordPanel extends PhabricatorSettingsPanel {
 
   public function isEditableByAdministrators() {
     return true;
   }
 
   public function getPanelKey() {
     return 'vcspassword';
   }
 
   public function getPanelName() {
     return pht('VCS Password');
   }
 
   public function getPanelGroup() {
     return pht('Authentication');
   }
 
   public function isEnabled() {
     return PhabricatorEnv::getEnvConfig('diffusion.allow-http-auth');
   }
 
   public function processRequest(AphrontRequest $request) {
     $viewer = $request->getUser();
     $user = $this->getUser();
 
     $token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession(
       $viewer,
       $request,
       '/settings/');
 
     $vcspassword = id(new PhabricatorRepositoryVCSPassword())
       ->loadOneWhere(
         'userPHID = %s',
         $user->getPHID());
     if (!$vcspassword) {
       $vcspassword = id(new PhabricatorRepositoryVCSPassword());
       $vcspassword->setUserPHID($user->getPHID());
     }
 
     $panel_uri = $this->getPanelURI('?saved=true');
 
     $errors = array();
 
     $e_password = true;
     $e_confirm = true;
 
     if ($request->isFormPost()) {
       if ($request->getBool('remove')) {
         if ($vcspassword->getID()) {
           $vcspassword->delete();
           return id(new AphrontRedirectResponse())->setURI($panel_uri);
         }
       }
 
       $new_password = $request->getStr('password');
       $confirm = $request->getStr('confirm');
       if (!strlen($new_password)) {
         $e_password = pht('Required');
         $errors[] = pht('Password is required.');
       } else {
         $e_password = null;
       }
 
       if (!strlen($confirm)) {
         $e_confirm = pht('Required');
         $errors[] = pht('You must confirm the new password.');
       } else {
         $e_confirm = null;
       }
 
       if (!$errors) {
         $envelope = new PhutilOpaqueEnvelope($new_password);
 
+        try {
+          // NOTE: This test is against $viewer (not $user), so that the error
+          // message below makes sense in the case that the two are different,
+          // and because an admin reusing their own password is bad, while
+          // system agents generally do not have passwords anyway.
+
+          $same_password = $viewer->comparePassword($envelope);
+        } catch (PhabricatorPasswordHasherUnavailableException $ex) {
+          // If we're missing the hasher, just let the user continue.
+          $same_password = false;
+        }
+
         if ($new_password !== $confirm) {
           $e_password = pht('Does Not Match');
           $e_confirm = pht('Does Not Match');
           $errors[] = pht('Password and confirmation do not match.');
-        } else if ($viewer->comparePassword($envelope)) {
-          // NOTE: The above test is against $viewer (not $user), so that the
-          // error message below makes sense in the case that the two are
-          // different, and because an admin reusing their own password is bad,
-          // while system agents generally do not have passwords anyway.
-
+        } else if ($same_password) {
           $e_password = pht('Not Unique');
           $e_confirm = pht('Not Unique');
           $errors[] = pht(
             'This password is the same as another password associated '.
             'with your account. You must use a unique password for '.
             'VCS access.');
         } else if (
           PhabricatorCommonPasswords::isCommonPassword($new_password)) {
           $e_password = pht('Very Weak');
           $e_confirm = pht('Very Weak');
           $errors[] = pht(
             'This password is extremely weak: it is one of the most common '.
             'passwords in use. Choose a stronger password.');
         }
 
 
         if (!$errors) {
           $vcspassword->setPassword($envelope, $user);
           $vcspassword->save();
 
           return id(new AphrontRedirectResponse())->setURI($panel_uri);
         }
       }
     }
 
     $title = pht('Set VCS Password');
 
     $form = id(new AphrontFormView())
       ->setUser($viewer)
       ->appendRemarkupInstructions(
         pht(
           'To access repositories hosted by Phabricator over HTTP, you must '.
           'set a version control password. This password should be unique.'.
           "\n\n".
           "This password applies to all repositories available over ".
           "HTTP."));
 
     if ($vcspassword->getID()) {
       $form
         ->appendChild(
           id(new AphrontFormPasswordControl())
             ->setDisableAutocomplete(true)
             ->setLabel(pht('Current Password'))
             ->setDisabled(true)
             ->setValue('********************'));
     } else {
       $form
         ->appendChild(
           id(new AphrontFormMarkupControl())
             ->setLabel(pht('Current Password'))
             ->setValue(phutil_tag('em', array(), pht('No Password Set'))));
     }
 
     $form
       ->appendChild(
         id(new AphrontFormPasswordControl())
           ->setDisableAutocomplete(true)
           ->setName('password')
           ->setLabel(pht('New VCS Password'))
           ->setError($e_password))
       ->appendChild(
         id(new AphrontFormPasswordControl())
           ->setDisableAutocomplete(true)
           ->setName('confirm')
           ->setLabel(pht('Confirm VCS Password'))
           ->setError($e_confirm))
       ->appendChild(
         id(new AphrontFormSubmitControl())
           ->setValue(pht('Change Password')));
 
 
     if (!$vcspassword->getID()) {
       $is_serious = PhabricatorEnv::getEnvConfig(
         'phabricator.serious-business');
 
       $suggest = Filesystem::readRandomBytes(128);
       $suggest = preg_replace('([^A-Za-z0-9/!().,;{}^&*%~])', '', $suggest);
       $suggest = substr($suggest, 0, 20);
 
       if ($is_serious) {
         $form->appendRemarkupInstructions(
           pht(
             'Having trouble coming up with a good password? Try this randomly '.
             'generated one, made by a computer:'.
             "\n\n".
             "`%s`",
             $suggest));
       } else {
         $form->appendRemarkupInstructions(
           pht(
             'Having trouble coming up with a good password? Try this '.
             'artisinal password, hand made in small batches by our expert '.
             'craftspeople: '.
             "\n\n".
             "`%s`",
             $suggest));
       }
     }
 
     $hash_envelope = new PhutilOpaqueEnvelope($vcspassword->getPasswordHash());
 
     $form->appendChild(
       id(new AphrontFormStaticControl())
         ->setLabel(pht('Current Algorithm'))
         ->setValue(
           PhabricatorPasswordHasher::getCurrentAlgorithmName($hash_envelope)));
 
     $form->appendChild(
       id(new AphrontFormStaticControl())
         ->setLabel(pht('Best Available Algorithm'))
         ->setValue(PhabricatorPasswordHasher::getBestAlgorithmName()));
 
     if (strlen($hash_envelope->openEnvelope())) {
-      if (PhabricatorPasswordHasher::canUpgradeHash($hash_envelope)) {
+      try {
+        $can_upgrade = PhabricatorPasswordHasher::canUpgradeHash(
+          $hash_envelope);
+      } catch (PhabricatorPasswordHasherUnavailableException $ex) {
+        $can_upgrade = false;
+        $errors[] = pht(
+          'Your VCS password is currently hashed using an algorithm which is '.
+          'no longer available on this install.');
+        $errors[] = pht(
+          'Because the algorithm implementation is missing, your password '.
+          'can not be used.');
+        $errors[] = pht(
+          'You can set a new password to replace the old password.');
+      }
+
+      if ($can_upgrade) {
         $errors[] = pht(
           'The strength of your stored VCS password hash can be upgraded. '.
           'To upgrade, either: use the password to authenticate with a '.
           'repository; or change your password.');
       }
     }
 
     $object_box = id(new PHUIObjectBoxView())
       ->setHeaderText($title)
       ->setForm($form)
       ->setFormErrors($errors);
 
     $remove_form = id(new AphrontFormView())
       ->setUser($viewer);
 
     if ($vcspassword->getID()) {
       $remove_form
         ->addHiddenInput('remove', true)
         ->appendRemarkupInstructions(
           pht(
             'You can remove your VCS password, which will prevent your '.
             'account from accessing repositories.'))
         ->appendChild(
           id(new AphrontFormSubmitControl())
             ->setValue(pht('Remove Password')));
     } else {
       $remove_form->appendRemarkupInstructions(
         pht(
           'You do not currently have a VCS password set. If you set one, you '.
           'can remove it here later.'));
     }
 
     $remove_box = id(new PHUIObjectBoxView())
       ->setHeaderText(pht('Remove VCS Password'))
       ->setForm($remove_form);
 
     $saved = null;
     if ($request->getBool('saved')) {
       $saved = id(new AphrontErrorView())
         ->setSeverity(AphrontErrorView::SEVERITY_NOTICE)
         ->setTitle(pht('Password Updated'))
         ->appendChild(pht('Your VCS password has been updated.'));
     }
 
     return array(
       $saved,
       $object_box,
       $remove_box,
     );
   }
 
 }
diff --git a/src/applications/settings/panel/PhabricatorSettingsPanelPassword.php b/src/applications/settings/panel/PhabricatorSettingsPanelPassword.php
index 2aef196b96..eeb9b3410e 100644
--- a/src/applications/settings/panel/PhabricatorSettingsPanelPassword.php
+++ b/src/applications/settings/panel/PhabricatorSettingsPanelPassword.php
@@ -1,203 +1,224 @@
 <?php
 
 final class PhabricatorSettingsPanelPassword
   extends PhabricatorSettingsPanel {
 
   public function getPanelKey() {
     return 'password';
   }
 
   public function getPanelName() {
     return pht('Password');
   }
 
   public function getPanelGroup() {
     return pht('Authentication');
   }
 
   public function isEnabled() {
     // There's no sense in showing a change password panel if the user
     // can't change their password...
 
     if (!PhabricatorEnv::getEnvConfig('account.editable')) {
       return false;
     }
 
     // ...or this install doesn't support password authentication at all.
 
     if (!PhabricatorPasswordAuthProvider::getPasswordProvider()) {
       return false;
     }
 
     return true;
   }
 
   public function processRequest(AphrontRequest $request) {
     $user = $request->getUser();
 
     $token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession(
       $user,
       $request,
       '/settings/');
 
     $min_len = PhabricatorEnv::getEnvConfig('account.minimum-password-length');
     $min_len = (int)$min_len;
 
     // NOTE: To change your password, you need to prove you own the account,
     // either by providing the old password or by carrying a token to
     // the workflow from a password reset email.
 
     $key = $request->getStr('key');
     $token = null;
     if ($key) {
       $token = id(new PhabricatorAuthTemporaryTokenQuery())
         ->setViewer($user)
         ->withObjectPHIDs(array($user->getPHID()))
         ->withTokenTypes(
           array(PhabricatorAuthSessionEngine::PASSWORD_TEMPORARY_TOKEN_TYPE))
         ->withTokenCodes(array(PhabricatorHash::digest($key)))
         ->withExpired(false)
         ->executeOne();
     }
 
     $e_old = true;
     $e_new = true;
     $e_conf = true;
 
     $errors = array();
     if ($request->isFormPost()) {
       if (!$token) {
         $envelope = new PhutilOpaqueEnvelope($request->getStr('old_pw'));
         if (!$user->comparePassword($envelope)) {
           $errors[] = pht('The old password you entered is incorrect.');
           $e_old = pht('Invalid');
         }
       }
 
       $pass = $request->getStr('new_pw');
       $conf = $request->getStr('conf_pw');
 
       if (strlen($pass) < $min_len) {
         $errors[] = pht('Your new password is too short.');
         $e_new = pht('Too Short');
       } else if ($pass !== $conf) {
         $errors[] = pht('New password and confirmation do not match.');
         $e_conf = pht('Invalid');
       } else if (PhabricatorCommonPasswords::isCommonPassword($pass)) {
         $e_new = pht('Very Weak');
         $e_conf = pht('Very Weak');
         $errors[] = pht(
           'Your new password is very weak: it is one of the most common '.
           'passwords in use. Choose a stronger password.');
       }
 
       if (!$errors) {
         // This write is unguarded because the CSRF token has already
         // been checked in the call to $request->isFormPost() and
         // the CSRF token depends on the password hash, so when it
         // is changed here the CSRF token check will fail.
         $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
 
           $envelope = new PhutilOpaqueEnvelope($pass);
           id(new PhabricatorUserEditor())
             ->setActor($user)
             ->changePassword($user, $envelope);
 
         unset($unguarded);
 
         if ($token) {
           // Destroy the token.
           $token->delete();
 
           // If this is a password set/reset, kick the user to the home page
           // after we update their account.
           $next = '/';
         } else {
           $next = $this->getPanelURI('?saved=true');
         }
 
         id(new PhabricatorAuthSessionEngine())->terminateLoginSessions(
           $user,
           $request->getCookie(PhabricatorCookies::COOKIE_SESSION));
 
         return id(new AphrontRedirectResponse())->setURI($next);
       }
     }
 
     $hash_envelope = new PhutilOpaqueEnvelope($user->getPasswordHash());
     if (strlen($hash_envelope->openEnvelope())) {
-      if (PhabricatorPasswordHasher::canUpgradeHash($hash_envelope)) {
+      try {
+        $can_upgrade = PhabricatorPasswordHasher::canUpgradeHash(
+          $hash_envelope);
+      } catch (PhabricatorPasswordHasherUnavailableException $ex) {
+        $can_upgrade = false;
+
+        // Only show this stuff if we aren't on the reset workflow. We can
+        // do resets regardless of the old hasher's availability.
+        if (!$token) {
+          $errors[] = pht(
+            'Your password is currently hashed using an algorithm which is '.
+            'no longer available on this install.');
+          $errors[] = pht(
+            'Because the algorithm implementation is missing, your password '.
+            'can not be used or updated.');
+          $errors[] = pht(
+            'To set a new password, request a password reset link from the '.
+            'login screen and then follow the instructions.');
+        }
+      }
+
+      if ($can_upgrade) {
         $errors[] = pht(
           'The strength of your stored password hash can be upgraded. '.
           'To upgrade, either: log out and log in using your password; or '.
           'change your password.');
       }
     }
 
     $len_caption = null;
     if ($min_len) {
       $len_caption = pht('Minimum password length: %d characters.', $min_len);
     }
 
     $form = new AphrontFormView();
     $form
       ->setUser($user)
       ->addHiddenInput('key', $key);
 
     if (!$token) {
       $form->appendChild(
         id(new AphrontFormPasswordControl())
           ->setLabel(pht('Old Password'))
           ->setError($e_old)
           ->setName('old_pw'));
     }
 
     $form
       ->appendChild(
         id(new AphrontFormPasswordControl())
           ->setDisableAutocomplete(true)
           ->setLabel(pht('New Password'))
           ->setError($e_new)
           ->setName('new_pw'));
     $form
       ->appendChild(
         id(new AphrontFormPasswordControl())
           ->setDisableAutocomplete(true)
           ->setLabel(pht('Confirm Password'))
           ->setCaption($len_caption)
           ->setError($e_conf)
           ->setName('conf_pw'));
     $form
       ->appendChild(
         id(new AphrontFormSubmitControl())
           ->setValue(pht('Change Password')));
 
     $form->appendChild(
       id(new AphrontFormStaticControl())
         ->setLabel(pht('Current Algorithm'))
         ->setValue(PhabricatorPasswordHasher::getCurrentAlgorithmName(
           new PhutilOpaqueEnvelope($user->getPasswordHash()))));
 
     $form->appendChild(
       id(new AphrontFormStaticControl())
         ->setLabel(pht('Best Available Algorithm'))
         ->setValue(PhabricatorPasswordHasher::getBestAlgorithmName()));
 
     $form->appendRemarkupInstructions(
       pht(
         'NOTE: Changing your password will terminate any other outstanding '.
         'login sessions.'));
 
     $form_box = id(new PHUIObjectBoxView())
       ->setHeaderText(pht('Change Password'))
       ->setFormSaved($request->getStr('saved'))
       ->setFormErrors($errors)
       ->setForm($form);
 
     return array(
       $form_box,
     );
   }
 
 
 }
diff --git a/src/infrastructure/util/password/PhabricatorPasswordHasher.php b/src/infrastructure/util/password/PhabricatorPasswordHasher.php
index 338a7615ee..4409163509 100644
--- a/src/infrastructure/util/password/PhabricatorPasswordHasher.php
+++ b/src/infrastructure/util/password/PhabricatorPasswordHasher.php
@@ -1,429 +1,431 @@
 <?php
 
 /**
  * Provides a mechanism for hashing passwords, like "iterated md5", "bcrypt",
  * "scrypt", etc.
  *
  * Hashers define suitability and strength, and the system automatically
  * chooses the strongest available hasher and can prompt users to upgrade as
  * soon as a stronger hasher is available.
  *
  * @task hasher   Implementing a Hasher
  * @task hashing  Using Hashers
  */
 abstract class PhabricatorPasswordHasher extends Phobject {
 
   const MAXIMUM_STORAGE_SIZE = 128;
 
 
 /* -(  Implementing a Hasher  )---------------------------------------------- */
 
 
   /**
    * Return a human-readable description of this hasher, like "Iterated MD5".
    *
    * @return string Human readable hash name.
    * @task hasher
    */
   abstract public function getHumanReadableName();
 
 
   /**
    * Return a short, unique, key identifying this hasher, like "md5" or
    * "bcrypt". This identifier should not be translated.
    *
    * @return string Short, unique hash name.
    * @task hasher
    */
   abstract public function getHashName();
 
 
   /**
    * Return the maximum byte length of hashes produced by this hasher. This is
    * used to prevent storage overflows.
    *
    * @return int  Maximum number of bytes in hashes this class produces.
    * @task hasher
    */
   abstract public function getHashLength();
 
 
   /**
    * Return `true` to indicate that any required extensions or dependencies
    * are available, and this hasher is able to perform hashing.
    *
    * @return bool True if this hasher can execute.
    * @task hasher
    */
   abstract public function canHashPasswords();
 
 
   /**
    * Return a human-readable string describing why this hasher is unable
    * to operate. For example, "To use bcrypt, upgrade to PHP 5.5.0 or newer.".
    *
    * @return string Human-readable description of how to enable this hasher.
    * @task hasher
    */
   abstract public function getInstallInstructions();
 
 
   /**
    * Return an indicator of this hasher's strength. When choosing to hash
    * new passwords, the strongest available hasher which is usuable for new
    * passwords will be used, and the presence of a stronger hasher will
    * prompt users to update their hashes.
    *
    * Generally, this method should return a larger number than hashers it is
    * preferable to, but a smaller number than hashers which are better than it
    * is. This number does not need to correspond directly with the actual hash
    * strength.
    *
    * @return float  Strength of this hasher.
    * @task hasher
    */
   abstract public function getStrength();
 
 
   /**
    * Return a short human-readable indicator of this hasher's strength, like
    * "Weak", "Okay", or "Good".
    *
    * This is only used to help administrators make decisions about
    * configuration.
    *
    * @return string Short human-readable description of hash strength.
    * @task hasher
    */
   abstract public function getHumanReadableStrength();
 
 
   /**
    * Produce a password hash.
    *
    * @param   PhutilOpaqueEnvelope  Text to be hashed.
    * @return  PhutilOpaqueEnvelope  Hashed text.
    * @task hasher
    */
   abstract protected function getPasswordHash(PhutilOpaqueEnvelope $envelope);
 
 
   /**
    * Verify that a password matches a hash.
    *
    * The default implementation checks for equality; if a hasher embeds salt in
    * hashes it should override this method and perform a salt-aware comparison.
    *
    * @param   PhutilOpaqueEnvelope  Password to compare.
    * @param   PhutilOpaqueEnvelope  Bare password hash.
    * @return  bool                  True if the passwords match.
    * @task hasher
    */
   protected function verifyPassword(
     PhutilOpaqueEnvelope $password,
     PhutilOpaqueEnvelope $hash) {
 
     $actual_hash = $this->getPasswordHash($password)->openEnvelope();
     $expect_hash = $hash->openEnvelope();
 
     return ($actual_hash === $expect_hash);
   }
 
 
   /**
    * Check if an existing hash created by this algorithm is upgradeable.
    *
    * The default implementation returns `false`. However, hash algorithms which
    * have (for example) an internal cost function may be able to upgrade an
    * existing hash to a stronger one with a higher cost.
    *
    * @param PhutilOpaqueEnvelope  Bare hash.
    * @return bool                 True if the hash can be upgraded without
    *                              changing the algorithm (for example, to a
    *                              higher cost).
    * @task hasher
    */
   protected function canUpgradeInternalHash(PhutilOpaqueEnvelope $hash) {
     return false;
   }
 
 
 /* -(  Using Hashers  )------------------------------------------------------ */
 
 
   /**
    * Get the hash of a password for storage.
    *
    * @param   PhutilOpaqueEnvelope  Password text.
    * @return  PhutilOpaqueEnvelope  Hashed text.
    * @task hashing
    */
   final public function getPasswordHashForStorage(
     PhutilOpaqueEnvelope $envelope) {
 
     $name = $this->getHashName();
     $hash = $this->getPasswordHash($envelope);
 
     $actual_len = strlen($hash->openEnvelope());
     $expect_len = $this->getHashLength();
     if ($actual_len > $expect_len) {
       throw new Exception(
         pht(
           "Password hash '%s' produced a hash of length %d, but a ".
           "maximum length of %d was expected.",
           $name,
           new PhutilNumber($actual_len),
           new PhutilNumber($expect_len)));
     }
 
     return new PhutilOpaqueEnvelope($name.':'.$hash->openEnvelope());
   }
 
 
   /**
    * Parse a storage hash into its components, like the hash type and hash
    * data.
    *
    * @return map  Dictionary of information about the hash.
    * @task hashing
    */
   private static function parseHashFromStorage(PhutilOpaqueEnvelope $hash) {
     $raw_hash = $hash->openEnvelope();
     if (strpos($raw_hash, ':') === false) {
       throw new Exception(
         pht(
           'Malformed password hash, expected "name:hash".'));
     }
 
     list($name, $hash) = explode(':', $raw_hash);
 
     return array(
       'name' => $name,
       'hash' => new PhutilOpaqueEnvelope($hash),
     );
   }
 
 
   /**
    * Get all available password hashers. This may include hashers which can not
    * actually be used (for example, a required extension is missing).
    *
    * @return list<PhabicatorPasswordHasher> Hasher objects.
    * @task hashing
    */
   public static function getAllHashers() {
     $objects = id(new PhutilSymbolLoader())
       ->setAncestorClass('PhabricatorPasswordHasher')
       ->loadObjects();
 
     $map = array();
     foreach ($objects as $object) {
       $name = $object->getHashName();
 
       $potential_length = strlen($name) + $object->getHashLength() + 1;
       $maximum_length = self::MAXIMUM_STORAGE_SIZE;
 
       if ($potential_length > $maximum_length) {
         throw new Exception(
           pht(
             'Hasher "%s" may produce hashes which are too long to fit in '.
             'storage. %d characters are available, but its hashes may be '.
             'up to %d characters in length.',
             $name,
             $maximum_length,
             $potential_length));
       }
 
       if (isset($map[$name])) {
         throw new Exception(
           pht(
             'Two hashers use the same hash name ("%s"), "%s" and "%s". Each '.
             'hasher must have a unique name.',
             $name,
             get_class($object),
             get_class($map[$name])));
       }
       $map[$name] = $object;
     }
 
     return $map;
   }
 
 
   /**
    * Get all usable password hashers. This may include hashers which are
    * not desirable or advisable.
    *
    * @return list<PhabicatorPasswordHasher> Hasher objects.
    * @task hashing
    */
   public static function getAllUsableHashers() {
     $hashers = self::getAllHashers();
     foreach ($hashers as $key => $hasher) {
       if (!$hasher->canHashPasswords()) {
         unset($hashers[$key]);
       }
     }
     return $hashers;
   }
 
 
   /**
    * Get the best (strongest) available hasher.
    *
    * @return PhabicatorPasswordHasher Best hasher.
    * @task hashing
    */
   public static function getBestHasher() {
     $hashers = self::getAllUsableHashers();
     $hashers = msort($hashers, 'getStrength');
 
     $hasher = last($hashers);
     if (!$hasher) {
       throw new PhabricatorPasswordHasherUnavailableException(
         pht(
           'There are no password hashers available which are usable for '.
           'new passwords.'));
     }
 
     return $hasher;
   }
 
 
   /**
    * Get the hashser for a given stored hash.
    *
    * @return PhabicatorPasswordHasher Corresponding hasher.
    * @task hashing
    */
   public static function getHasherForHash(PhutilOpaqueEnvelope $hash) {
     $info = self::parseHashFromStorage($hash);
     $name = $info['name'];
 
     $usable = self::getAllUsableHashers();
     if (isset($usable[$name])) {
       return $usable[$name];
     }
 
     $all = self::getAllHashers();
     if (isset($all[$name])) {
       throw new PhabricatorPasswordHasherUnavailableException(
         pht(
           'Attempting to compare a password saved with the "%s" hash. The '.
           'hasher exists, but is not currently usable. %s',
           $name,
           $all[$name]->getInstallInstructions()));
     }
 
     throw new PhabricatorPasswordHasherUnavailableException(
       pht(
         'Attempting to compare a password saved with the "%s" hash. No such '.
         'hasher is known to Phabricator.',
         $name));
   }
 
 
   /**
    * Test if a password is using an weaker hash than the strongest available
    * hash. This can be used to prompt users to upgrade, or automatically upgrade
    * on login.
    *
    * @return bool True to indicate that rehashing this password will improve
    *              the hash strength.
    * @task hashing
    */
   public static function canUpgradeHash(PhutilOpaqueEnvelope $hash) {
     if (!strlen($hash->openEnvelope())) {
       throw new Exception(
         pht('Expected a password hash, received nothing!'));
     }
 
     $current_hasher = self::getHasherForHash($hash);
     $best_hasher = self::getBestHasher();
 
     if ($current_hasher->getHashName() != $best_hasher->getHashName()) {
       // If the algorithm isn't the best one, we can upgrade.
       return true;
     }
 
     $info = self::parseHashFromStorage($hash);
     if ($current_hasher->canUpgradeInternalHash($info['hash'])) {
       // If the algorithm provides an internal upgrade, we can also upgrade.
       return true;
     }
 
     // Already on the best algorithm with the best settings.
     return false;
   }
 
 
   /**
    * Generate a new hash for a password, using the best available hasher.
    *
    * @param   PhutilOpaqueEnvelope  Password to hash.
    * @return  PhutilOpaqueEnvelope  Hashed password, using best available
    *                                hasher.
    * @task hashing
    */
   public static function generateNewPasswordHash(
     PhutilOpaqueEnvelope $password) {
     $hasher = self::getBestHasher();
     return $hasher->getPasswordHashForStorage($password);
   }
 
 
   /**
    * Compare a password to a stored hash.
    *
    * @param   PhutilOpaqueEnvelope  Password to compare.
    * @param   PhutilOpaqueEnvelope  Stored password hash.
    * @return  bool                  True if the passwords match.
    * @task hashing
    */
   public static function comparePassword(
     PhutilOpaqueEnvelope $password,
     PhutilOpaqueEnvelope $hash) {
 
     $hasher = self::getHasherForHash($hash);
     $parts = self::parseHashFromStorage($hash);
 
     return $hasher->verifyPassword($password, $parts['hash']);
   }
 
 
   /**
    * Get the human-readable algorithm name for a given hash.
    *
    * @param   PhutilOpaqueEnvelope  Storage hash.
    * @return  string                Human-readable algorithm name.
    */
   public static function getCurrentAlgorithmName(PhutilOpaqueEnvelope $hash) {
     $raw_hash = $hash->openEnvelope();
     if (!strlen($raw_hash)) {
       return pht('None');
     }
 
     try {
       $current_hasher = PhabricatorPasswordHasher::getHasherForHash($hash);
       return $current_hasher->getHumanReadableName();
     } catch (Exception $ex) {
-      return pht('Unknown');
+      $info = self::parseHashFromStorage($hash);
+      $name = $info['name'];
+      return pht('Unknown ("%s")', $name);
     }
   }
 
 
   /**
    * Get the human-readable algorithm name for the best available hash.
    *
    * @return  string                Human-readable name for best hash.
    */
   public static function getBestAlgorithmName() {
     try {
       $best_hasher = PhabricatorPasswordHasher::getBestHasher();
       return $best_hasher->getHumanReadableName();
     } catch (Exception $ex) {
       return pht('Unknown');
     }
   }
 
 }