@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.

Detect and prompt for passwords on SSH private keys, then strip them

Summary:
Fixes T4356. Currently, if users add a passworded private key to the Passphrase application, we never ask for the password and can not use it later. This makes several changes:

- Prompt for the password.
- Detect passworded private keys, and don't accept them until we can decrypt them.
- Try to decrypt passworded private keys, and tell the user if the password is missing or incorrect.
- Stop further creation of path-based private keys, which are really just for compatibility. We can't do anything reasonable about passwords with these, since users can change the files.

Test Plan: Created a private key with a password, was prompted to provide it, tried empty/bad passwords, provided the correct password and had the key decrypted for use.

Reviewers: btrahan

Reviewed By: btrahan

CC: aran

Maniphest Tasks: T4356

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

+241 -63
+1 -1
src/applications/passphrase/controller/PassphraseCredentialCreateController.php
··· 6 6 $request = $this->getRequest(); 7 7 $viewer = $request->getUser(); 8 8 9 - $types = PassphraseCredentialType::getAllTypes(); 9 + $types = PassphraseCredentialType::getAllCreateableTypes(); 10 10 $types = mpull($types, null, 'getCredentialType'); 11 11 $types = msort($types, 'getCredentialTypeName'); 12 12
+109 -62
src/applications/passphrase/controller/PassphraseCredentialEditController.php
··· 32 32 throw new Exception(pht('Credential has invalid type "%s"!', $type)); 33 33 } 34 34 35 + if (!$type->isCreateable()) { 36 + throw new Exception( 37 + pht('Credential has noncreateable type "%s"!', $type)); 38 + } 39 + 35 40 $is_new = false; 36 41 } else { 37 42 $type_const = $request->getStr('type'); ··· 65 70 $v_secret = $credential->getSecretID() ? str_repeat($bullet, 32) : null; 66 71 67 72 $validation_exception = null; 73 + $errors = array(); 74 + $e_password = null; 68 75 if ($request->isFormPost()) { 76 + 69 77 $v_name = $request->getStr('name'); 70 78 $v_desc = $request->getStr('description'); 71 79 $v_username = $request->getStr('username'); 72 - $v_secret = $request->getStr('secret'); 73 80 $v_view_policy = $request->getStr('viewPolicy'); 74 81 $v_edit_policy = $request->getStr('editPolicy'); 75 82 76 - $type_name = PassphraseCredentialTransaction::TYPE_NAME; 77 - $type_desc = PassphraseCredentialTransaction::TYPE_DESCRIPTION; 78 - $type_username = PassphraseCredentialTransaction::TYPE_USERNAME; 79 - $type_destroy = PassphraseCredentialTransaction::TYPE_DESTROY; 80 - $type_secret_id = PassphraseCredentialTransaction::TYPE_SECRET_ID; 81 - $type_view_policy = PhabricatorTransactions::TYPE_VIEW_POLICY; 82 - $type_edit_policy = PhabricatorTransactions::TYPE_EDIT_POLICY; 83 + $v_secret = $request->getStr('secret'); 84 + $v_password = $request->getStr('password'); 85 + $v_decrypt = $v_secret; 83 86 84 - $xactions = array(); 87 + $env_secret = new PhutilOpaqueEnvelope($v_secret); 88 + $env_password = new PhutilOpaqueEnvelope($v_password); 85 89 86 - $xactions[] = id(new PassphraseCredentialTransaction()) 87 - ->setTransactionType($type_name) 88 - ->setNewValue($v_name); 90 + if ($type->requiresPassword($env_secret)) { 91 + if (strlen($v_password)) { 92 + $v_decrypt = $type->decryptSecret($env_secret, $env_password); 93 + if ($v_decrypt === null) { 94 + $e_password = pht('Incorrect'); 95 + $errors[] = pht( 96 + 'This key requires a password, but the password you provided '. 97 + 'is incorrect.'); 98 + } else { 99 + $v_decrypt = $v_decrypt->openEnvelope(); 100 + } 101 + } else { 102 + $e_password = pht('Required'); 103 + $errors[] = pht( 104 + 'This key requires a password. You must provide the password '. 105 + 'for the key.'); 106 + } 107 + } 89 108 90 - $xactions[] = id(new PassphraseCredentialTransaction()) 91 - ->setTransactionType($type_desc) 92 - ->setNewValue($v_desc); 109 + if (!$errors) { 110 + $type_name = PassphraseCredentialTransaction::TYPE_NAME; 111 + $type_desc = PassphraseCredentialTransaction::TYPE_DESCRIPTION; 112 + $type_username = PassphraseCredentialTransaction::TYPE_USERNAME; 113 + $type_destroy = PassphraseCredentialTransaction::TYPE_DESTROY; 114 + $type_secret_id = PassphraseCredentialTransaction::TYPE_SECRET_ID; 115 + $type_view_policy = PhabricatorTransactions::TYPE_VIEW_POLICY; 116 + $type_edit_policy = PhabricatorTransactions::TYPE_EDIT_POLICY; 93 117 94 - $xactions[] = id(new PassphraseCredentialTransaction()) 95 - ->setTransactionType($type_username) 96 - ->setNewValue($v_username); 118 + $xactions = array(); 97 119 98 - $xactions[] = id(new PassphraseCredentialTransaction()) 99 - ->setTransactionType($type_view_policy) 100 - ->setNewValue($v_view_policy); 120 + $xactions[] = id(new PassphraseCredentialTransaction()) 121 + ->setTransactionType($type_name) 122 + ->setNewValue($v_name); 101 123 102 - $xactions[] = id(new PassphraseCredentialTransaction()) 103 - ->setTransactionType($type_edit_policy) 104 - ->setNewValue($v_edit_policy); 124 + $xactions[] = id(new PassphraseCredentialTransaction()) 125 + ->setTransactionType($type_desc) 126 + ->setNewValue($v_desc); 105 127 106 - // Open a transaction in case we're writing a new secret; this limits 107 - // the amount of code which handles secret plaintexts. 108 - $credential->openTransaction(); 128 + $xactions[] = id(new PassphraseCredentialTransaction()) 129 + ->setTransactionType($type_username) 130 + ->setNewValue($v_username); 109 131 110 - $min_secret = str_replace($bullet, '', trim($v_secret)); 111 - if (strlen($min_secret)) { 112 - // If the credential was previously destroyed, restore it when it is 113 - // edited if a secret is provided. 114 132 $xactions[] = id(new PassphraseCredentialTransaction()) 115 - ->setTransactionType($type_destroy) 116 - ->setNewValue(0); 133 + ->setTransactionType($type_view_policy) 134 + ->setNewValue($v_view_policy); 117 135 118 - $new_secret = id(new PassphraseSecret()) 119 - ->setSecretData($v_secret) 120 - ->save(); 121 136 $xactions[] = id(new PassphraseCredentialTransaction()) 122 - ->setTransactionType($type_secret_id) 123 - ->setNewValue($new_secret->getID()); 124 - } 137 + ->setTransactionType($type_edit_policy) 138 + ->setNewValue($v_edit_policy); 125 139 126 - try { 127 - $editor = id(new PassphraseCredentialTransactionEditor()) 128 - ->setActor($viewer) 129 - ->setContinueOnNoEffect(true) 130 - ->setContentSourceFromRequest($request) 131 - ->applyTransactions($credential, $xactions); 140 + // Open a transaction in case we're writing a new secret; this limits 141 + // the amount of code which handles secret plaintexts. 142 + $credential->openTransaction(); 132 143 133 - $credential->saveTransaction(); 144 + $min_secret = str_replace($bullet, '', trim($v_decrypt)); 145 + if (strlen($min_secret)) { 146 + // If the credential was previously destroyed, restore it when it is 147 + // edited if a secret is provided. 148 + $xactions[] = id(new PassphraseCredentialTransaction()) 149 + ->setTransactionType($type_destroy) 150 + ->setNewValue(0); 134 151 135 - if ($request->isAjax()) { 136 - return id(new AphrontAjaxResponse())->setContent( 137 - array( 138 - 'phid' => $credential->getPHID(), 139 - 'name' => 'K'.$credential->getID().' '.$credential->getName(), 140 - )); 141 - } else { 142 - return id(new AphrontRedirectResponse()) 143 - ->setURI('/K'.$credential->getID()); 152 + $new_secret = id(new PassphraseSecret()) 153 + ->setSecretData($v_decrypt) 154 + ->save(); 155 + $xactions[] = id(new PassphraseCredentialTransaction()) 156 + ->setTransactionType($type_secret_id) 157 + ->setNewValue($new_secret->getID()); 144 158 } 145 - } catch (PhabricatorApplicationTransactionValidationException $ex) { 146 - $credential->killTransaction(); 159 + 160 + try { 161 + $editor = id(new PassphraseCredentialTransactionEditor()) 162 + ->setActor($viewer) 163 + ->setContinueOnNoEffect(true) 164 + ->setContentSourceFromRequest($request) 165 + ->applyTransactions($credential, $xactions); 147 166 148 - $validation_exception = $ex; 167 + $credential->saveTransaction(); 149 168 150 - $e_name = $ex->getShortMessage($type_name); 151 - $e_username = $ex->getShortMessage($type_username); 169 + if ($request->isAjax()) { 170 + return id(new AphrontAjaxResponse())->setContent( 171 + array( 172 + 'phid' => $credential->getPHID(), 173 + 'name' => 'K'.$credential->getID().' '.$credential->getName(), 174 + )); 175 + } else { 176 + return id(new AphrontRedirectResponse()) 177 + ->setURI('/K'.$credential->getID()); 178 + } 179 + } catch (PhabricatorApplicationTransactionValidationException $ex) { 180 + $credential->killTransaction(); 152 181 153 - $credential->setViewPolicy($v_view_policy); 154 - $credential->setEditPolicy($v_edit_policy); 182 + $validation_exception = $ex; 183 + 184 + $e_name = $ex->getShortMessage($type_name); 185 + $e_username = $ex->getShortMessage($type_username); 186 + 187 + $credential->setViewPolicy($v_view_policy); 188 + $credential->setEditPolicy($v_edit_policy); 189 + } 155 190 } 156 191 } 157 192 ··· 214 249 ->setLabel($type->getSecretLabel()) 215 250 ->setValue($v_secret)); 216 251 252 + if ($type->shouldShowPasswordField()) { 253 + $form->appendChild( 254 + id(new AphrontFormPasswordControl()) 255 + ->setName('password') 256 + ->setLabel($type->getPasswordLabel()) 257 + ->setError($e_password)); 258 + } 259 + 217 260 $crumbs = $this->buildApplicationCrumbs(); 218 261 219 262 if ($is_new) { ··· 230 273 } 231 274 232 275 if ($request->isAjax()) { 276 + $errors = id(new AphrontErrorView())->setErrors($errors); 277 + 233 278 $dialog = id(new AphrontDialogView()) 234 279 ->setUser($viewer) 235 280 ->setWidth(AphrontDialogView::WIDTH_FORM) 236 281 ->setTitle($title) 282 + ->appendChild($errors) 237 283 ->appendChild($form) 238 284 ->addSubmitButton(pht('Create Credential')) 239 285 ->addCancelButton($this->getApplicationURI()); ··· 248 294 249 295 $box = id(new PHUIObjectBoxView()) 250 296 ->setHeaderText($header) 297 + ->setFormErrors($errors) 251 298 ->setValidationException($validation_exception) 252 299 ->setForm($form); 253 300
+81
src/applications/passphrase/credentialtype/PassphraseCredentialType.php
··· 1 1 <?php 2 2 3 + /** 4 + * @task password Managing Encryption Passwords 5 + */ 3 6 abstract class PassphraseCredentialType extends Phobject { 4 7 5 8 abstract public function getCredentialType(); ··· 19 22 return $types; 20 23 } 21 24 25 + public static function getAllCreateableTypes() { 26 + $types = self::getAllTypes(); 27 + foreach ($types as $key => $type) { 28 + if (!$type->isCreateable()) { 29 + unset($types[$key]); 30 + } 31 + } 32 + 33 + return $types; 34 + } 35 + 22 36 public static function getTypeByConstant($constant) { 23 37 $all = self::getAllTypes(); 24 38 $all = mpull($all, null, 'getCredentialType'); 25 39 return idx($all, $constant); 40 + } 41 + 42 + 43 + /** 44 + * Can users create new credentials of this type? 45 + * 46 + * @return bool True if new credentials of this type can be created. 47 + */ 48 + public function isCreateable() { 49 + return true; 50 + } 51 + 52 + 53 + /* -( Passwords )---------------------------------------------------------- */ 54 + 55 + 56 + /** 57 + * Return true to show an additional "Password" field. This is used by 58 + * SSH credentials to strip passwords off private keys. 59 + * 60 + * @return bool True if a password field should be shown to the user. 61 + * 62 + * @task password 63 + */ 64 + public function shouldShowPasswordField() { 65 + return false; 66 + } 67 + 68 + 69 + /** 70 + * Return the label for the password field, if one is shown. 71 + * 72 + * @return string Human-readable field label. 73 + * 74 + * @task password 75 + */ 76 + public function getPasswordLabel() { 77 + return pht('Password'); 78 + } 79 + 80 + 81 + /** 82 + * Return true if the provided credental requires a password to decrypt. 83 + * 84 + * @param PhutilOpaqueEnvelope Credential secret value. 85 + * @return bool True if the credential needs a password. 86 + * 87 + * @task password 88 + */ 89 + public function requiresPassword(PhutilOpaqueEnvelope $secret) { 90 + return false; 91 + } 92 + 93 + 94 + /** 95 + * Return the decrypted credential secret, or `null` if the password does 96 + * not decrypt the credential. 97 + * 98 + * @param PhutilOpaqueEnvelope Credential secret value. 99 + * @param PhutilOpaqueEnvelope Credential password. 100 + * @return 101 + * @task password 102 + */ 103 + public function decryptSecret( 104 + PhutilOpaqueEnvelope $secret, 105 + PhutilOpaqueEnvelope $password) { 106 + return $secret; 26 107 } 27 108 28 109 }
+8
src/applications/passphrase/credentialtype/PassphraseCredentialTypeSSHPrivateKeyFile.php
··· 25 25 return new AphrontFormTextControl(); 26 26 } 27 27 28 + public function isCreateable() { 29 + // This credential type exists to support historic repository configuration. 30 + // We don't support creating new credentials with this type, since it does 31 + // not scale and managing passwords is much more difficult than if we have 32 + // the key text. 33 + return false; 34 + } 35 + 28 36 }
+42
src/applications/passphrase/credentialtype/PassphraseCredentialTypeSSHPrivateKeyText.php
··· 21 21 return pht('Private Key'); 22 22 } 23 23 24 + public function shouldShowPasswordField() { 25 + return true; 26 + } 27 + 28 + public function getPasswordLabel() { 29 + return pht('Password for Key'); 30 + } 31 + 32 + public function requiresPassword(PhutilOpaqueEnvelope $secret) { 33 + // According to the internet, this is the canonical test for an SSH private 34 + // key with a password. 35 + return preg_match('/ENCRYPTED/', $secret->openEnvelope()); 36 + } 37 + 38 + public function decryptSecret( 39 + PhutilOpaqueEnvelope $secret, 40 + PhutilOpaqueEnvelope $password) { 41 + 42 + $tmp = new TempFile(); 43 + Filesystem::writeFile($tmp, $secret->openEnvelope()); 44 + 45 + if (!Filesystem::binaryExists('ssh-keygen')) { 46 + throw new Exception( 47 + pht( 48 + 'Decrypting SSH keys requires the `ssh-keygen` binary, but it '. 49 + 'is not available in PATH. Either make it available or strip the '. 50 + 'password fromt his SSH key manually before uploading it.')); 51 + } 52 + 53 + list($err, $stdout, $stderr) = exec_manual( 54 + 'ssh-keygen -p -P %P -N %s -f %s', 55 + $password, 56 + '', 57 + (string)$tmp); 58 + 59 + if ($err) { 60 + return null; 61 + } else { 62 + return new PhutilOpaqueEnvelope(Filesystem::readFile($tmp)); 63 + } 64 + } 65 + 24 66 }