1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-12-18 03:20:59 +01:00

Add a more modern object for storing password hashes

Summary:
Ref T13043. Currently:

  - Passwords are stored separately in the "VCS Passwords" and "User" tables and don't share as much code as they could.
  - Because User objects are all over the place in the code, password hashes are all over the place too (i.e., often somewhere in process memory). This is a very low-severity, theoretical sort of issue, but it could make leaving a stray `var_dump()` in the code somewhere a lot more dangerous than it otherwise is. Even if we never do this, third-party developers might. So it "feels nice" to imagine separating this data into a different table that we rarely load.
  - Passwords can not be //revoked//. They can be //deleted//, but users can set the same password again. If you believe or suspect that a password may have been compromised, you might reasonably prefer to revoke it and force the user to select a //different// password.

This change prepares to remedy these issues by adding a new, more modern dedicated password storage table which supports storing multiple password types (account vs VCS), gives passwords real PHIDs and transactions, supports DestructionEngine, supports revocation, and supports `bin/auth revoke`.

It doesn't actually make anything use this new table yet. Future changes will migrate VCS passwords and account passwords to this table.

(This also gives third party applications a reasonable place to store password hashes in a consistent way if they have some need for it.)

Test Plan: Added some basic unit tests to cover general behavior. This is just skeleton code for now and will get more thorough testing when applications move.

Reviewers: amckinley

Reviewed By: amckinley

Maniphest Tasks: T13043

Differential Revision: https://secure.phabricator.com/D18894
This commit is contained in:
epriestley 2018-01-20 11:42:35 -08:00
parent fa1ecb7f66
commit 9c00a43784
14 changed files with 563 additions and 1 deletions

View file

@ -0,0 +1,10 @@
CREATE TABLE {$NAMESPACE}_auth.auth_password (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
phid VARBINARY(64) NOT NULL,
objectPHID VARBINARY(64) NOT NULL,
passwordType VARCHAR(64) NOT NULL COLLATE {$COLLATE_TEXT},
passwordHash VARCHAR(128) NOT NULL COLLATE {$COLLATE_TEXT},
isRevoked BOOL NOT NULL,
dateCreated INT UNSIGNED NOT NULL,
dateModified INT UNSIGNED NOT NULL
) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};

View file

@ -0,0 +1,19 @@
CREATE TABLE {$NAMESPACE}_auth.auth_passwordtransaction (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
phid VARBINARY(64) NOT NULL,
authorPHID VARBINARY(64) NOT NULL,
objectPHID VARBINARY(64) NOT NULL,
viewPolicy VARBINARY(64) NOT NULL,
editPolicy VARBINARY(64) NOT NULL,
commentPHID VARBINARY(64) DEFAULT NULL,
commentVersion INT UNSIGNED NOT NULL,
transactionType VARCHAR(32) COLLATE {$COLLATE_TEXT} NOT NULL,
oldValue LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL,
newValue LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL,
contentSource LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL,
metadata LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL,
dateCreated INT UNSIGNED NOT NULL,
dateModified INT UNSIGNED NOT NULL,
UNIQUE KEY `key_phid` (`phid`),
KEY `key_object` (`objectPHID`)
) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};

View file

@ -2089,7 +2089,16 @@ phutil_register_library_map(array(
'PhabricatorAuthOldOAuthRedirectController' => 'applications/auth/controller/PhabricatorAuthOldOAuthRedirectController.php',
'PhabricatorAuthOneTimeLoginController' => 'applications/auth/controller/PhabricatorAuthOneTimeLoginController.php',
'PhabricatorAuthOneTimeLoginTemporaryTokenType' => 'applications/auth/tokentype/PhabricatorAuthOneTimeLoginTemporaryTokenType.php',
'PhabricatorAuthPassword' => 'applications/auth/storage/PhabricatorAuthPassword.php',
'PhabricatorAuthPasswordEditor' => 'applications/auth/editor/PhabricatorAuthPasswordEditor.php',
'PhabricatorAuthPasswordPHIDType' => 'applications/auth/phid/PhabricatorAuthPasswordPHIDType.php',
'PhabricatorAuthPasswordQuery' => 'applications/auth/query/PhabricatorAuthPasswordQuery.php',
'PhabricatorAuthPasswordResetTemporaryTokenType' => 'applications/auth/tokentype/PhabricatorAuthPasswordResetTemporaryTokenType.php',
'PhabricatorAuthPasswordRevokeTransaction' => 'applications/auth/xaction/PhabricatorAuthPasswordRevokeTransaction.php',
'PhabricatorAuthPasswordRevoker' => 'applications/auth/revoker/PhabricatorAuthPasswordRevoker.php',
'PhabricatorAuthPasswordTestCase' => 'applications/auth/__tests__/PhabricatorAuthPasswordTestCase.php',
'PhabricatorAuthPasswordTransaction' => 'applications/auth/storage/PhabricatorAuthPasswordTransaction.php',
'PhabricatorAuthPasswordTransactionType' => 'applications/auth/xaction/PhabricatorAuthPasswordTransactionType.php',
'PhabricatorAuthProvider' => 'applications/auth/provider/PhabricatorAuthProvider.php',
'PhabricatorAuthProviderConfig' => 'applications/auth/storage/PhabricatorAuthProviderConfig.php',
'PhabricatorAuthProviderConfigController' => 'applications/auth/controller/config/PhabricatorAuthProviderConfigController.php',
@ -3481,6 +3490,7 @@ phutil_register_library_map(array(
'PhabricatorPagerUIExample' => 'applications/uiexample/examples/PhabricatorPagerUIExample.php',
'PhabricatorPassphraseApplication' => 'applications/passphrase/application/PhabricatorPassphraseApplication.php',
'PhabricatorPasswordAuthProvider' => 'applications/auth/provider/PhabricatorPasswordAuthProvider.php',
'PhabricatorPasswordDestructionEngineExtension' => 'applications/auth/extension/PhabricatorPasswordDestructionEngineExtension.php',
'PhabricatorPasswordHasher' => 'infrastructure/util/password/PhabricatorPasswordHasher.php',
'PhabricatorPasswordHasherTestCase' => 'infrastructure/util/password/__tests__/PhabricatorPasswordHasherTestCase.php',
'PhabricatorPasswordHasherUnavailableException' => 'infrastructure/util/password/PhabricatorPasswordHasherUnavailableException.php',
@ -7366,7 +7376,21 @@ phutil_register_library_map(array(
'PhabricatorAuthOldOAuthRedirectController' => 'PhabricatorAuthController',
'PhabricatorAuthOneTimeLoginController' => 'PhabricatorAuthController',
'PhabricatorAuthOneTimeLoginTemporaryTokenType' => 'PhabricatorAuthTemporaryTokenType',
'PhabricatorAuthPassword' => array(
'PhabricatorAuthDAO',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
'PhabricatorApplicationTransactionInterface',
),
'PhabricatorAuthPasswordEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorAuthPasswordPHIDType' => 'PhabricatorPHIDType',
'PhabricatorAuthPasswordQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorAuthPasswordResetTemporaryTokenType' => 'PhabricatorAuthTemporaryTokenType',
'PhabricatorAuthPasswordRevokeTransaction' => 'PhabricatorAuthPasswordTransactionType',
'PhabricatorAuthPasswordRevoker' => 'PhabricatorAuthRevoker',
'PhabricatorAuthPasswordTestCase' => 'PhabricatorTestCase',
'PhabricatorAuthPasswordTransaction' => 'PhabricatorApplicationTransaction',
'PhabricatorAuthPasswordTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorAuthProvider' => 'Phobject',
'PhabricatorAuthProviderConfig' => array(
'PhabricatorAuthDAO',
@ -8984,6 +9008,7 @@ phutil_register_library_map(array(
'PhabricatorPagerUIExample' => 'PhabricatorUIExample',
'PhabricatorPassphraseApplication' => 'PhabricatorApplication',
'PhabricatorPasswordAuthProvider' => 'PhabricatorAuthProvider',
'PhabricatorPasswordDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension',
'PhabricatorPasswordHasher' => 'Phobject',
'PhabricatorPasswordHasherTestCase' => 'PhabricatorTestCase',
'PhabricatorPasswordHasherUnavailableException' => 'Exception',

View file

@ -0,0 +1,31 @@
<?php
final class PhabricatorAuthPasswordTestCase extends PhabricatorTestCase {
protected function getPhabricatorTestCaseConfiguration() {
return array(
self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES => true,
);
}
public function testCompare() {
$password1 = new PhutilOpaqueEnvelope('hunter2');
$password2 = new PhutilOpaqueEnvelope('hunter3');
$user = $this->generateNewTestUser();
$type = PhabricatorAuthPassword::PASSWORD_TYPE_TEST;
$pass = PhabricatorAuthPassword::initializeNewPassword($user, $type)
->setPassword($password1, $user)
->save();
$this->assertTrue(
$pass->comparePassword($password1, $user),
pht('Good password should match.'));
$this->assertFalse(
$pass->comparePassword($password2, $user),
pht('Bad password should not match.'));
}
}

View file

@ -0,0 +1,22 @@
<?php
final class PhabricatorAuthPasswordEditor
extends PhabricatorApplicationTransactionEditor {
public function getEditorApplicationClass() {
return 'PhabricatorAuthApplication';
}
public function getEditorObjectsDescription() {
return pht('Passwords');
}
public function getCreateObjectTitle($author, $object) {
return pht('%s created this password.', $author);
}
public function getCreateObjectTitleForFeed($author, $object) {
return pht('%s created %s.', $author, $object);
}
}

View file

@ -0,0 +1,29 @@
<?php
final class PhabricatorPasswordDestructionEngineExtension
extends PhabricatorDestructionEngineExtension {
const EXTENSIONKEY = 'passwords';
public function getExtensionName() {
return pht('Passwords');
}
public function destroyObject(
PhabricatorDestructionEngine $engine,
$object) {
$viewer = $engine->getViewer();
$object_phid = $object->getPHID();
$passwords = id(new PhabricatorAuthPasswordQuery())
->setViewer($viewer)
->withObjectPHIDs(array($object_phid))
->execute();
foreach ($passwords as $password) {
$engine->destroyObject($password);
}
}
}

View file

@ -0,0 +1,36 @@
<?php
final class PhabricatorAuthPasswordPHIDType extends PhabricatorPHIDType {
const TYPECONST = 'APAS';
public function getTypeName() {
return pht('Auth Password');
}
public function newObject() {
return new PhabricatorAuthPassword();
}
public function getPHIDTypeApplicationClass() {
return 'PhabricatorAuthApplication';
}
protected function buildQueryForObjects(
PhabricatorObjectQuery $query,
array $phids) {
return id(new PhabricatorAuthPasswordQuery())
->withPHIDs($phids);
}
public function loadHandles(
PhabricatorHandleQuery $query,
array $handles,
array $objects) {
foreach ($handles as $phid => $handle) {
$password = $objects[$phid];
}
}
}

View file

@ -0,0 +1,114 @@
<?php
final class PhabricatorAuthPasswordQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $objectPHIDs;
private $passwordTypes;
private $isRevoked;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withObjectPHIDs(array $object_phids) {
$this->objectPHIDs = $object_phids;
return $this;
}
public function withPasswordTypes(array $types) {
$this->passwordTypes = $types;
return $this;
}
public function withIsRevoked($is_revoked) {
$this->isRevoked = $is_revoked;
return $this;
}
public function newResultObject() {
return new PhabricatorAuthPassword();
}
protected function loadPage() {
return $this->loadStandardPage($this->newResultObject());
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'phid IN (%Ls)',
$this->phids);
}
if ($this->objectPHIDs !== null) {
$where[] = qsprintf(
$conn,
'objectPHID IN (%Ls)',
$this->objectPHIDs);
}
if ($this->passwordTypes !== null) {
$where[] = qsprintf(
$conn,
'passwordType IN (%Ls)',
$this->passwordTypes);
}
if ($this->isRevoked !== null) {
$where[] = qsprintf(
$conn,
'isRevoked = %d',
(int)$this->isRevoked);
}
return $where;
}
protected function willFilterPage(array $passwords) {
$object_phids = mpull($passwords, 'getObjectPHID');
$objects = id(new PhabricatorObjectQuery())
->setViewer($this->getViewer())
->setParentQuery($this)
->withPHIDs($object_phids)
->execute();
$objects = mpull($objects, null, 'getPHID');
foreach ($passwords as $key => $password) {
$object = idx($objects, $password->getObjectPHID());
if (!$object) {
unset($passwords[$key]);
$this->didRejectResult($password);
continue;
}
$password->attachObject($object);
}
return $passwords;
}
public function getQueryApplicationClass() {
return 'PhabricatorAuthApplication';
}
}

View file

@ -0,0 +1,52 @@
<?php
final class PhabricatorAuthPasswordRevoker
extends PhabricatorAuthRevoker {
const REVOKERKEY = 'password';
public function revokeAllCredentials() {
$query = new PhabricatorAuthPasswordQuery();
return $this->revokeWithQuery($query);
}
public function revokeCredentialsFrom($object) {
$query = id(new PhabricatorAuthPasswordQuery())
->withObjectPHIDs(array($object->getPHID()));
return $this->revokeWithQuery($query);
}
private function revokeWithQuery(PhabricatorAuthPasswordQuery $query) {
$viewer = $this->getViewer();
$passwords = $query
->setViewer($viewer)
->withIsRevoked(false)
->execute();
$content_source = PhabricatorContentSource::newForSource(
PhabricatorDaemonContentSource::SOURCECONST);
$revoke_type = PhabricatorAuthPasswordRevokeTransaction::TRANSACTIONTYPE;
$auth_phid = id(new PhabricatorAuthApplication())->getPHID();
foreach ($passwords as $password) {
$xactions = array();
$xactions[] = $password->getApplicationTransactionTemplate()
->setTransactionType($revoke_type)
->setNewValue(true);
$editor = $password->getApplicationTransactionEditor()
->setActor($viewer)
->setActingAsPHID($auth_phid)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->setContentSource($content_source)
->applyTransactions($password, $xactions);
}
return count($passwords);
}
}

View file

@ -37,7 +37,7 @@ final class PhabricatorAuthSSHRevoker
->setTransactionType(PhabricatorAuthSSHKeyTransaction::TYPE_DEACTIVATE)
->setNewValue(1);
$editor = id(new PhabricatorAuthSSHKeyEditor())
$editor = $ssh_key->getApplicationTransactionEditor()
->setActor($viewer)
->setActingAsPHID($auth_phid)
->setContinueOnNoEffect(true)

View file

@ -0,0 +1,167 @@
<?php
final class PhabricatorAuthPassword
extends PhabricatorAuthDAO
implements
PhabricatorPolicyInterface,
PhabricatorDestructibleInterface,
PhabricatorApplicationTransactionInterface {
protected $objectPHID;
protected $passwordType;
protected $passwordHash;
protected $isRevoked;
private $object = self::ATTACHABLE;
const PASSWORD_TYPE_ACCOUNT = 'account';
const PASSWORD_TYPE_VCS = 'vcs';
const PASSWORD_TYPE_TEST = 'test';
public static function initializeNewPassword(
PhabricatorUser $object,
$type) {
return id(new self())
->setObjectPHID($object->getPHID())
->attachObject($object)
->setPasswordType($type)
->setIsRevoked(0);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'passwordType' => 'text64',
'passwordHash' => 'text128',
'isRevoked' => 'bool',
),
self::CONFIG_KEY_SCHEMA => array(
'key_role' => array(
'columns' => array('objectPHID', 'passwordType'),
),
),
) + parent::getConfiguration();
}
public function getPHIDType() {
return PhabricatorAuthPasswordPHIDType::TYPECONST;
}
public function getObject() {
return $this->assertAttached($this->object);
}
public function attachObject($object) {
$this->object = $object;
return $this;
}
public function setPassword(
PhutilOpaqueEnvelope $password,
PhabricatorUser $object) {
$hasher = PhabricatorPasswordHasher::getBestHasher();
$digest = $this->digestPassword($password, $object);
$hash = $hasher->getPasswordHashForStorage($digest);
$raw_hash = $hash->openEnvelope();
return $this->setPasswordHash($raw_hash);
}
public function comparePassword(
PhutilOpaqueEnvelope $password,
PhabricatorUser $object) {
$digest = $this->digestPassword($password, $object);
$raw_hash = $this->getPasswordHash();
$hash = new PhutilOpaqueEnvelope($raw_hash);
return PhabricatorPasswordHasher::comparePassword($digest, $hash);
}
private function digestPassword(
PhutilOpaqueEnvelope $password,
PhabricatorUser $object) {
$object_phid = $object->getPHID();
if ($this->getObjectPHID() !== $object->getPHID()) {
throw new Exception(
pht(
'This password is associated with an object PHID ("%s") for '.
'a different object than the provided one ("%s").',
$this->getObjectPHID(),
$object->getPHID()));
}
$raw_input = PhabricatorHash::digestPassword($password, $object_phid);
return new PhutilOpaqueEnvelope($raw_input);
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
);
}
public function getPolicy($capability) {
return PhabricatorPolicies::getMostOpenPolicy();
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
/* -( PhabricatorExtendedPolicyInterface )--------------------------------- */
public function getExtendedPolicy($capability, PhabricatorUser $viewer) {
return array(
array($this->getObject(), PhabricatorPolicyCapability::CAN_VIEW),
);
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->delete();
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorAuthPasswordEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhabricatorAuthPasswordTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
}

View file

@ -0,0 +1,21 @@
<?php
final class PhabricatorAuthPasswordTransaction
extends PhabricatorApplicationTransaction {
public function getApplicationName() {
return 'auth';
}
public function getApplicationTransactionType() {
return PhabricatorAuthPasswordPHIDType::TYPECONST;
}
public function getApplicationTransactionCommentObject() {
return null;
}
public function getBaseTransactionClass() {
return 'PhabricatorAuthPasswordTransactionType';
}
}

View file

@ -0,0 +1,32 @@
<?php
final class PhabricatorAuthPasswordRevokeTransaction
extends PhabricatorAuthPasswordTransactionType {
const TRANSACTIONTYPE = 'password.revoke';
public function generateOldValue($object) {
return (bool)$object->getIsRevoked();
}
public function generateNewValue($object, $value) {
return (bool)$value;
}
public function applyInternalEffects($object, $value) {
$object->setIsRevoked((int)$value);
}
public function getTitle() {
if ($this->getNewValue()) {
return pht(
'%s revoked this password.',
$this->renderAuthor());
} else {
return pht(
'%s removed this password from the revocation list.',
$this->renderAuthor());
}
}
}

View file

@ -0,0 +1,4 @@
<?php
abstract class PhabricatorAuthPasswordTransactionType
extends PhabricatorModularTransactionType {}