@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
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Manage object mailKeys automatically in Mail instead of storing them on objects

Summary:
Ref T13065. `mailKey`s are a private secret for each object. In some mail configurations, they help us ensure that inbound mail is authentic: when we send you mail, the "Reply-To" is "T123+456+abcdef".

- The `T123` is the object you're actually replying to.
- The `456` is your user ID.
- The `abcdef` is a hash of your user account with the `mailKey`.

Knowing this hash effectively proves that Phabricator has sent you mail about the object before, i.e. that you legitimately control the account you're sending from. Without this, anyone could send mail to any object "From" someone else, and have comments post under their username.

To generate this hash, we need a stable secret per object. (We can't use properties like the PHID because the secret has to be legitimately secret.)

Today, we store these in `mailKey` properties on the actual objects, and manually generate them. This results in tons and tons and tons of copies of this same ~10 lines of code.

Instead, just store them in the Mail application and generate them on demand. This change also anticipates possibly adding flags like "must encrypt" and "original subject", which are other "durable metadata about mail transmission" properties we may have use cases for eventually.

Test Plan:
- See next change for additional testing and context.
- Sent mail about Herald rules (next change); saw mail keys generate cleanly.
- Destroyed a Herald rule with a mail key, saw the mail properties get nuked.
- Grepped for `getMailKey()` and converted all callsites I could which aren't the copy/pasted boilerplate present in 50 places.
- Used `bin/mail receive-test --to T123` to test normal mail receipt of older-style objects and make sure that wasn't broken.

Reviewers: amckinley

Reviewed By: amckinley

Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam

Maniphest Tasks: T13065

Differential Revision: https://secure.phabricator.com/D19399

+204 -11
+8
resources/sql/autopatches/20180423.mail.01.properties.sql
··· 1 + CREATE TABLE {$NAMESPACE}_metamta.metamta_mailproperties ( 2 + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, 3 + objectPHID VARBINARY(64) NOT NULL, 4 + mailProperties LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT}, 5 + dateCreated INT UNSIGNED NOT NULL, 6 + dateModified INT UNSIGNED NOT NULL, 7 + UNIQUE KEY `key_object` (objectPHID) 8 + ) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};
+9
src/__phutil_library_map__.php
··· 3346 3346 'PhabricatorMailOutboundRoutingSelfEmailHeraldAction' => 'applications/metamta/herald/PhabricatorMailOutboundRoutingSelfEmailHeraldAction.php', 3347 3347 'PhabricatorMailOutboundRoutingSelfNotificationHeraldAction' => 'applications/metamta/herald/PhabricatorMailOutboundRoutingSelfNotificationHeraldAction.php', 3348 3348 'PhabricatorMailOutboundStatus' => 'applications/metamta/constants/PhabricatorMailOutboundStatus.php', 3349 + 'PhabricatorMailPropertiesDestructionEngineExtension' => 'applications/metamta/engineextension/PhabricatorMailPropertiesDestructionEngineExtension.php', 3349 3350 'PhabricatorMailReceiver' => 'applications/metamta/receiver/PhabricatorMailReceiver.php', 3350 3351 'PhabricatorMailReceiverTestCase' => 'applications/metamta/receiver/__tests__/PhabricatorMailReceiverTestCase.php', 3351 3352 'PhabricatorMailReplyHandler' => 'applications/metamta/replyhandler/PhabricatorMailReplyHandler.php', ··· 3403 3404 'PhabricatorMetaMTAMailHasRecipientEdgeType' => 'applications/metamta/edge/PhabricatorMetaMTAMailHasRecipientEdgeType.php', 3404 3405 'PhabricatorMetaMTAMailListController' => 'applications/metamta/controller/PhabricatorMetaMTAMailListController.php', 3405 3406 'PhabricatorMetaMTAMailPHIDType' => 'applications/metamta/phid/PhabricatorMetaMTAMailPHIDType.php', 3407 + 'PhabricatorMetaMTAMailProperties' => 'applications/metamta/storage/PhabricatorMetaMTAMailProperties.php', 3408 + 'PhabricatorMetaMTAMailPropertiesQuery' => 'applications/metamta/query/PhabricatorMetaMTAMailPropertiesQuery.php', 3406 3409 'PhabricatorMetaMTAMailQuery' => 'applications/metamta/query/PhabricatorMetaMTAMailQuery.php', 3407 3410 'PhabricatorMetaMTAMailSearchEngine' => 'applications/metamta/query/PhabricatorMetaMTAMailSearchEngine.php', 3408 3411 'PhabricatorMetaMTAMailSection' => 'applications/metamta/view/PhabricatorMetaMTAMailSection.php', ··· 9038 9041 'PhabricatorMailOutboundRoutingSelfEmailHeraldAction' => 'PhabricatorMailOutboundRoutingHeraldAction', 9039 9042 'PhabricatorMailOutboundRoutingSelfNotificationHeraldAction' => 'PhabricatorMailOutboundRoutingHeraldAction', 9040 9043 'PhabricatorMailOutboundStatus' => 'Phobject', 9044 + 'PhabricatorMailPropertiesDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension', 9041 9045 'PhabricatorMailReceiver' => 'Phobject', 9042 9046 'PhabricatorMailReceiverTestCase' => 'PhabricatorTestCase', 9043 9047 'PhabricatorMailReplyHandler' => 'Phobject', ··· 9106 9110 'PhabricatorMetaMTAMailHasRecipientEdgeType' => 'PhabricatorEdgeType', 9107 9111 'PhabricatorMetaMTAMailListController' => 'PhabricatorMetaMTAController', 9108 9112 'PhabricatorMetaMTAMailPHIDType' => 'PhabricatorPHIDType', 9113 + 'PhabricatorMetaMTAMailProperties' => array( 9114 + 'PhabricatorMetaMTADAO', 9115 + 'PhabricatorPolicyInterface', 9116 + ), 9117 + 'PhabricatorMetaMTAMailPropertiesQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 9109 9118 'PhabricatorMetaMTAMailQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 9110 9119 'PhabricatorMetaMTAMailSearchEngine' => 'PhabricatorApplicationSearchEngine', 9111 9120 'PhabricatorMetaMTAMailSection' => 'Phobject',
-6
src/applications/auth/storage/PhabricatorAuthSSHKey.php
··· 70 70 return parent::save(); 71 71 } 72 72 73 - public function getMailKey() { 74 - // NOTE: We don't actually receive mail for these objects. It's OK for 75 - // the mail key to be predictable until we do. 76 - return PhabricatorHash::digestForIndex($this->getPHID()); 77 - } 78 - 79 73 public function toPublicKey() { 80 74 return PhabricatorAuthSSHPublicKey::newFromStoredKey($this); 81 75 }
+28
src/applications/metamta/engineextension/PhabricatorMailPropertiesDestructionEngineExtension.php
··· 1 + <?php 2 + 3 + final class PhabricatorMailPropertiesDestructionEngineExtension 4 + extends PhabricatorDestructionEngineExtension { 5 + 6 + const EXTENSIONKEY = 'mail.properties'; 7 + 8 + public function getExtensionName() { 9 + return pht('Mail Properties'); 10 + } 11 + 12 + public function destroyObject( 13 + PhabricatorDestructionEngine $engine, 14 + $object) { 15 + 16 + $object_phid = $object->getPHID(); 17 + $viewer = $engine->getViewer(); 18 + 19 + $properties = id(new PhabricatorMetaMTAMailPropertiesQuery()) 20 + ->setViewer($viewer) 21 + ->withObjectPHIDs(array($object_phid)) 22 + ->executeOne(); 23 + if ($properties) { 24 + $properties->delete(); 25 + } 26 + } 27 + 28 + }
+3 -1
src/applications/metamta/management/PhabricatorMailManagementReceiveTestWorkflow.php
··· 139 139 throw new Exception(pht("No such object '%s'!", $to)); 140 140 } 141 141 142 + $mail_key = PhabricatorMetaMTAMailProperties::loadMailKey($object); 143 + 142 144 $hash = PhabricatorObjectMailReceiver::computeMailHash( 143 - $object->getMailKey(), 145 + $mail_key, 144 146 $user->getPHID()); 145 147 146 148 $header_content['to'] = $to.'+'.$user->getID().'+'.$hash.'@test.com';
+51
src/applications/metamta/query/PhabricatorMetaMTAMailPropertiesQuery.php
··· 1 + <?php 2 + 3 + final class PhabricatorMetaMTAMailPropertiesQuery 4 + extends PhabricatorCursorPagedPolicyAwareQuery { 5 + 6 + private $ids; 7 + private $objectPHIDs; 8 + 9 + public function withIDs(array $ids) { 10 + $this->ids = $ids; 11 + return $this; 12 + } 13 + 14 + public function withObjectPHIDs(array $object_phids) { 15 + $this->objectPHIDs = $object_phids; 16 + return $this; 17 + } 18 + 19 + public function newResultObject() { 20 + return new PhabricatorMetaMTAMailProperties(); 21 + } 22 + 23 + protected function loadPage() { 24 + return $this->loadStandardPage($this->newResultObject()); 25 + } 26 + 27 + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { 28 + $where = parent::buildWhereClauseParts($conn); 29 + 30 + if ($this->ids !== null) { 31 + $where[] = qsprintf( 32 + $conn, 33 + 'id IN (%Ld)', 34 + $this->ids); 35 + } 36 + 37 + if ($this->objectPHIDs !== null) { 38 + $where[] = qsprintf( 39 + $conn, 40 + 'objectPHID IN (%Ls)', 41 + $this->objectPHIDs); 42 + } 43 + 44 + return $where; 45 + } 46 + 47 + public function getQueryApplicationClass() { 48 + return 'PhabricatorMetaMTAApplication'; 49 + } 50 + 51 + }
+2 -1
src/applications/metamta/receiver/PhabricatorObjectMailReceiver.php
··· 124 124 $check_phid = $sender->getPHID(); 125 125 } 126 126 127 - $expect_hash = self::computeMailHash($object->getMailKey(), $check_phid); 127 + $mail_key = PhabricatorMetaMTAMailProperties::loadMailKey($object); 128 + $expect_hash = self::computeMailHash($mail_key, $check_phid); 128 129 129 130 if (!phutil_hashes_are_identical($expect_hash, $parts['hash'])) { 130 131 throw new PhabricatorMetaMTAReceivedMailProcessingException(
+4 -1
src/applications/metamta/receiver/__tests__/PhabricatorObjectMailReceiverTestCase.php
··· 98 98 if ($is_bad_hash) { 99 99 $hash = PhabricatorObjectMailReceiver::computeMailHash('x', 'y'); 100 100 } else { 101 + 102 + $mail_key = PhabricatorMetaMTAMailProperties::loadMailKey($task); 103 + 101 104 $hash = PhabricatorObjectMailReceiver::computeMailHash( 102 - $task->getMailKey(), 105 + $mail_key, 103 106 $is_public ? $task->getPHID() : $user->getPHID()); 104 107 } 105 108
+8 -2
src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php
··· 136 136 // We compute a hash using the object's own PHID to prevent an attacker 137 137 // from blindly interacting with objects that they haven't ever received 138 138 // mail about by just sending to D1@, D2@, etc... 139 + 140 + $mail_key = PhabricatorMetaMTAMailProperties::loadMailKey($receiver); 141 + 139 142 $hash = PhabricatorObjectMailReceiver::computeMailHash( 140 - $receiver->getMailKey(), 143 + $mail_key, 141 144 $receiver->getPHID()); 142 145 143 146 $address = "{$prefix}{$receiver_id}+public+{$hash}@{$domain}"; ··· 159 162 $receiver = $this->getMailReceiver(); 160 163 $receiver_id = $receiver->getID(); 161 164 $user_id = $user->getID(); 165 + 166 + $mail_key = PhabricatorMetaMTAMailProperties::loadMailKey($receiver); 167 + 162 168 $hash = PhabricatorObjectMailReceiver::computeMailHash( 163 - $receiver->getMailKey(), 169 + $mail_key, 164 170 $user->getPHID()); 165 171 $domain = $this->getReplyHandlerDomain(); 166 172
+91
src/applications/metamta/storage/PhabricatorMetaMTAMailProperties.php
··· 1 + <?php 2 + 3 + final class PhabricatorMetaMTAMailProperties 4 + extends PhabricatorMetaMTADAO 5 + implements PhabricatorPolicyInterface { 6 + 7 + protected $objectPHID; 8 + protected $mailProperties = array(); 9 + 10 + protected function getConfiguration() { 11 + return array( 12 + self::CONFIG_SERIALIZATION => array( 13 + 'mailProperties' => self::SERIALIZATION_JSON, 14 + ), 15 + self::CONFIG_COLUMN_SCHEMA => array(), 16 + self::CONFIG_KEY_SCHEMA => array( 17 + 'key_object' => array( 18 + 'columns' => array('objectPHID'), 19 + 'unique' => true, 20 + ), 21 + ), 22 + ) + parent::getConfiguration(); 23 + } 24 + 25 + public function getMailProperty($key, $default = null) { 26 + return idx($this->mailProperties, $key, $default); 27 + } 28 + 29 + public function setMailProperty($key, $value) { 30 + $this->mailProperties[$key] = $value; 31 + return $this; 32 + } 33 + 34 + public static function loadMailKey($object) { 35 + // If this is an older object with an onboard "mailKey" property, just 36 + // use it. 37 + // TODO: We should eventually get rid of these and get rid of this 38 + // piece of code. 39 + if ($object->hasProperty('mailKey')) { 40 + return $object->getMailKey(); 41 + } 42 + 43 + $viewer = PhabricatorUser::getOmnipotentUser(); 44 + $object_phid = $object->getPHID(); 45 + 46 + $properties = id(new PhabricatorMetaMTAMailPropertiesQuery()) 47 + ->setViewer($viewer) 48 + ->withObjectPHIDs(array($object_phid)) 49 + ->executeOne(); 50 + if (!$properties) { 51 + $properties = id(new self()) 52 + ->setObjectPHID($object_phid); 53 + } 54 + 55 + $mail_key = $properties->getMailProperty('mailKey'); 56 + if ($mail_key !== null) { 57 + return $mail_key; 58 + } 59 + 60 + $mail_key = Filesystem::readRandomCharacters(20); 61 + $properties->setMailProperty('mailKey', $mail_key); 62 + 63 + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 64 + $properties->save(); 65 + unset($unguarded); 66 + 67 + return $mail_key; 68 + } 69 + 70 + 71 + /* -( PhabricatorPolicyInterface )----------------------------------------- */ 72 + 73 + 74 + public function getCapabilities() { 75 + return array( 76 + PhabricatorPolicyCapability::CAN_VIEW, 77 + ); 78 + } 79 + 80 + public function getPolicy($capability) { 81 + switch ($capability) { 82 + case PhabricatorPolicyCapability::CAN_VIEW: 83 + return PhabricatorPolicies::POLICY_NOONE; 84 + } 85 + } 86 + 87 + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { 88 + return false; 89 + } 90 + 91 + }