From 21e415299f265e909eb18925cd5d68dd75c28266 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 21 Jan 2018 18:07:49 -0800 Subject: [PATCH] Mark all existing password hashes as "legacy" and start upgrading digest formats Summary: Depends on D18907. Ref T13043. Ref T12509. We have some weird old password digest behavior that isn't terribly concerning, but also isn't great. Specifically, old passwords were digested in weird ways before being hashed. Notably, account passwords were digested with usernames, so your password stops working if your username is chagned. Not the end of the world, but silly. Mark all existing hashes as "v1", and automatically upgrade then when they're used or changed. Some day, far in the future, we could stop supporting these legacy digests and delete the code and passwords and just issue upgrade advice ("Passwords which haven't been used in more than two years no longer work."). But at least get things on a path toward sane, modern behavior. Test Plan: Ran migration. Spot-checked that everthing in the database got marked as "v1". Used an existing password to login successfully. Verified that it was upgraded to a `null` (modern) digest. Logged in with it again. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13043, T12509 Differential Revision: https://secure.phabricator.com/D18908 --- .../20180121.auth.06.legacydigest.sql | 2 + .../20180121.auth.07.marklegacy.sql | 4 ++ .../auth/storage/PhabricatorAuthPassword.php | 11 +++ .../people/storage/PhabricatorUser.php | 67 ++++++++++--------- 4 files changed, 51 insertions(+), 33 deletions(-) create mode 100644 resources/sql/autopatches/20180121.auth.06.legacydigest.sql create mode 100644 resources/sql/autopatches/20180121.auth.07.marklegacy.sql diff --git a/resources/sql/autopatches/20180121.auth.06.legacydigest.sql b/resources/sql/autopatches/20180121.auth.06.legacydigest.sql new file mode 100644 index 0000000000..af9c7990d0 --- /dev/null +++ b/resources/sql/autopatches/20180121.auth.06.legacydigest.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_auth.auth_password + ADD legacyDigestFormat VARCHAR(32) COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20180121.auth.07.marklegacy.sql b/resources/sql/autopatches/20180121.auth.07.marklegacy.sql new file mode 100644 index 0000000000..798757d348 --- /dev/null +++ b/resources/sql/autopatches/20180121.auth.07.marklegacy.sql @@ -0,0 +1,4 @@ +UPDATE {$NAMESPACE}_auth.auth_password + SET legacyDigestFormat = 'v1' + WHERE passwordType IN ('vcs', 'account') + AND legacyDigestFormat IS NULL; diff --git a/src/applications/auth/storage/PhabricatorAuthPassword.php b/src/applications/auth/storage/PhabricatorAuthPassword.php index 3a88b990bf..62cca7f130 100644 --- a/src/applications/auth/storage/PhabricatorAuthPassword.php +++ b/src/applications/auth/storage/PhabricatorAuthPassword.php @@ -12,6 +12,7 @@ final class PhabricatorAuthPassword protected $passwordHash; protected $passwordSalt; protected $isRevoked; + protected $legacyDigestFormat; private $object = self::ATTACHABLE; @@ -38,6 +39,7 @@ final class PhabricatorAuthPassword 'passwordHash' => 'text128', 'passwordSalt' => 'text64', 'isRevoked' => 'bool', + 'legacyDigestFormat' => 'text32?', ), self::CONFIG_KEY_SCHEMA => array( 'key_role' => array( @@ -66,6 +68,12 @@ final class PhabricatorAuthPassword } public function canUpgrade() { + // If this password uses a legacy digest format, we can upgrade it to the + // new digest format even if a better hasher isn't available. + if ($this->getLegacyDigestFormat() !== null) { + return true; + } + $hash = $this->newPasswordEnvelope(); return PhabricatorPasswordHasher::canUpgradeHash($hash); } @@ -110,6 +118,9 @@ final class PhabricatorAuthPassword $new_salt = Filesystem::readRandomCharacters(64); $this->setPasswordSalt($new_salt); + // Clear any legacy digest format to force a modern digest. + $this->setLegacyDigestFormat(null); + $digest = $this->digestPassword($password, $object); $hash = $hasher->getPasswordHashForStorage($digest); $raw_hash = $hash->openEnvelope(); diff --git a/src/applications/people/storage/PhabricatorUser.php b/src/applications/people/storage/PhabricatorUser.php index 840bec7d50..7d7b50a4cb 100644 --- a/src/applications/people/storage/PhabricatorUser.php +++ b/src/applications/people/storage/PhabricatorUser.php @@ -1590,7 +1590,7 @@ final class PhabricatorUser // Applying salt while digesting passwords ensures that hashes are salted // whether we ultimately select a self-salting hasher or not. - // For legacy compatibility reasons, the VCS and Account password digest + // For legacy compatibility reasons, old VCS and Account password digest // algorithms are significantly more complicated than necessary to achieve // these goals. This is because they once used a different hashing and // salting process. When we upgraded to the modern modular hasher @@ -1602,43 +1602,44 @@ final class PhabricatorUser // everything that a digest callback should without any needless legacy // baggage on top. - switch ($password->getPasswordType()) { - case PhabricatorAuthPassword::PASSWORD_TYPE_VCS: - // VCS passwords use an iterated HMAC SHA1 as a digest algorithm. They - // originally used this as a hasher, but it became a digest alorithm - // once hashing was upgraded to include bcrypt. - $digest = $envelope->openEnvelope(); - $salt = $this->getPHID(); - for ($ii = 0; $ii < 1000; $ii++) { - $digest = PhabricatorHash::weakDigest($digest, $salt); - } - return new PhutilOpaqueEnvelope($digest); - case PhabricatorAuthPassword::PASSWORD_TYPE_ACCOUNT: - // Account passwords use this weird mess of salt and do not digest - // the input to a standard length. + if ($password->getLegacyDigestFormat() == 'v1') { + switch ($password->getPasswordType()) { + case PhabricatorAuthPassword::PASSWORD_TYPE_VCS: + // Old VCS passwords use an iterated HMAC SHA1 as a digest algorithm. + // They originally used this as a hasher, but it became a digest + // algorithm once hashing was upgraded to include bcrypt. + $digest = $envelope->openEnvelope(); + $salt = $this->getPHID(); + for ($ii = 0; $ii < 1000; $ii++) { + $digest = PhabricatorHash::weakDigest($digest, $salt); + } + return new PhutilOpaqueEnvelope($digest); + case PhabricatorAuthPassword::PASSWORD_TYPE_ACCOUNT: + // Account passwords previously used this weird mess of salt and did + // not digest the input to a standard length. - // TODO: We should build a migration pathway forward from this which - // uses a better (HMAC SHA256) digest algorithm. Beyond this being - // a weird special case, there are two actual problems with this, - // although neither are particularly severe: + // Beyond this being a weird special case, there are two actual + // problems with this, although neither are particularly severe: - // First, because we do not normalize the length of passwords, this - // algorithm may make us vulnerable to DOS attacks where attacker - // attempt to use very long inputs to slow down hashers. + // First, because we do not normalize the length of passwords, this + // algorithm may make us vulnerable to DOS attacks where an attacker + // attempts to use a very long input to slow down hashers. - // Second, because the username is part of the hash algorithm, renaming - // a user breaks their password. This isn't a huge deal but it's pretty - // silly. There's no security justification for this behavior, I just - // didn't think about the implication when I wrote it originally. + // Second, because the username is part of the hash algorithm, + // renaming a user breaks their password. This isn't a huge deal but + // it's pretty silly. There's no security justification for this + // behavior, I just didn't think about the implication when I wrote + // it originally. - $parts = array( - $this->getUsername(), - $envelope->openEnvelope(), - $this->getPHID(), - $password->getPasswordSalt(), - ); + $parts = array( + $this->getUsername(), + $envelope->openEnvelope(), + $this->getPHID(), + $password->getPasswordSalt(), + ); - return new PhutilOpaqueEnvelope(implode('', $parts)); + return new PhutilOpaqueEnvelope(implode('', $parts)); + } } // For passwords which do not have some crazy legacy reason to use some