From f950985cfd7c0cb00408adf76fd5b738d749ef33 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 12 Mar 2014 18:58:25 -0700 Subject: [PATCH] Allow Passphrase to generate SSH keypairs and extact public keys from private keys Summary: Ref T4587. - Add an option to generate a keypair. - Add an option to view the public keys for existing keypairs. Test Plan: - Generated keypairs. - Viewed public keys. Reviewers: btrahan Reviewed By: btrahan Subscribers: aran, epriestley Maniphest Tasks: T4587 Differential Revision: https://secure.phabricator.com/D8515 --- src/__phutil_library_map__.php | 4 ++ .../PhabricatorApplicationPassphrase.php | 1 + .../PassphraseCredentialEditController.php | 15 +++++ .../PassphraseCredentialPublicController.php | 59 +++++++++++++++++++ .../PassphraseCredentialRevealController.php | 8 ++- .../PassphraseCredentialViewController.php | 15 ++++- .../PassphraseCredentialType.php | 17 ++++++ ...assphraseCredentialTypeSSHGeneratedKey.php | 36 +++++++++++ .../PassphraseCredentialTypeSSHPrivateKey.php | 16 +++++ ...sphraseCredentialTypeSSHPrivateKeyFile.php | 5 ++ .../passphrase/keys/PassphraseSSHKey.php | 8 +-- .../storage/PassphraseCredential.php | 4 ++ 12 files changed, 181 insertions(+), 7 deletions(-) create mode 100644 src/applications/passphrase/controller/PassphraseCredentialPublicController.php create mode 100644 src/applications/passphrase/credentialtype/PassphraseCredentialTypeSSHGeneratedKey.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index c651baad1c..2bce7672d5 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1024,6 +1024,7 @@ phutil_register_library_map(array( 'PassphraseCredentialDestroyController' => 'applications/passphrase/controller/PassphraseCredentialDestroyController.php', 'PassphraseCredentialEditController' => 'applications/passphrase/controller/PassphraseCredentialEditController.php', 'PassphraseCredentialListController' => 'applications/passphrase/controller/PassphraseCredentialListController.php', + 'PassphraseCredentialPublicController' => 'applications/passphrase/controller/PassphraseCredentialPublicController.php', 'PassphraseCredentialQuery' => 'applications/passphrase/query/PassphraseCredentialQuery.php', 'PassphraseCredentialRevealController' => 'applications/passphrase/controller/PassphraseCredentialRevealController.php', 'PassphraseCredentialSearchEngine' => 'applications/passphrase/query/PassphraseCredentialSearchEngine.php', @@ -1032,6 +1033,7 @@ phutil_register_library_map(array( 'PassphraseCredentialTransactionQuery' => 'applications/passphrase/query/PassphraseCredentialTransactionQuery.php', 'PassphraseCredentialType' => 'applications/passphrase/credentialtype/PassphraseCredentialType.php', 'PassphraseCredentialTypePassword' => 'applications/passphrase/credentialtype/PassphraseCredentialTypePassword.php', + 'PassphraseCredentialTypeSSHGeneratedKey' => 'applications/passphrase/credentialtype/PassphraseCredentialTypeSSHGeneratedKey.php', 'PassphraseCredentialTypeSSHPrivateKey' => 'applications/passphrase/credentialtype/PassphraseCredentialTypeSSHPrivateKey.php', 'PassphraseCredentialTypeSSHPrivateKeyFile' => 'applications/passphrase/credentialtype/PassphraseCredentialTypeSSHPrivateKeyFile.php', 'PassphraseCredentialTypeSSHPrivateKeyText' => 'applications/passphrase/credentialtype/PassphraseCredentialTypeSSHPrivateKeyText.php', @@ -3676,6 +3678,7 @@ phutil_register_library_map(array( 0 => 'PassphraseController', 1 => 'PhabricatorApplicationSearchResultsControllerInterface', ), + 'PassphraseCredentialPublicController' => 'PassphraseController', 'PassphraseCredentialQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PassphraseCredentialRevealController' => 'PassphraseController', 'PassphraseCredentialSearchEngine' => 'PhabricatorApplicationSearchEngine', @@ -3684,6 +3687,7 @@ phutil_register_library_map(array( 'PassphraseCredentialTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PassphraseCredentialType' => 'Phobject', 'PassphraseCredentialTypePassword' => 'PassphraseCredentialType', + 'PassphraseCredentialTypeSSHGeneratedKey' => 'PassphraseCredentialTypeSSHPrivateKey', 'PassphraseCredentialTypeSSHPrivateKey' => 'PassphraseCredentialType', 'PassphraseCredentialTypeSSHPrivateKeyFile' => 'PassphraseCredentialTypeSSHPrivateKey', 'PassphraseCredentialTypeSSHPrivateKeyText' => 'PassphraseCredentialTypeSSHPrivateKey', diff --git a/src/applications/passphrase/application/PhabricatorApplicationPassphrase.php b/src/applications/passphrase/application/PhabricatorApplicationPassphrase.php index f2951353ed..f62314b6a7 100644 --- a/src/applications/passphrase/application/PhabricatorApplicationPassphrase.php +++ b/src/applications/passphrase/application/PhabricatorApplicationPassphrase.php @@ -40,6 +40,7 @@ final class PhabricatorApplicationPassphrase extends PhabricatorApplication { 'edit/(?:(?P\d+)/)?' => 'PassphraseCredentialEditController', 'destroy/(?P\d+)/' => 'PassphraseCredentialDestroyController', 'reveal/(?P\d+)/' => 'PassphraseCredentialRevealController', + 'public/(?P\d+)/' => 'PassphraseCredentialPublicController', )); } diff --git a/src/applications/passphrase/controller/PassphraseCredentialEditController.php b/src/applications/passphrase/controller/PassphraseCredentialEditController.php index 917b045c6f..9e3d363d57 100644 --- a/src/applications/passphrase/controller/PassphraseCredentialEditController.php +++ b/src/applications/passphrase/controller/PassphraseCredentialEditController.php @@ -53,6 +53,10 @@ final class PassphraseCredentialEditController extends PassphraseController { // Prefill username if provided. $credential->setUsername($request->getStr('username')); + + if (!$request->getStr('isInitialized')) { + $type->didInitializeNewCredential($viewer, $credential); + } } $errors = array(); @@ -68,6 +72,16 @@ final class PassphraseCredentialEditController extends PassphraseController { $bullet = "\xE2\x80\xA2"; $v_secret = $credential->getSecretID() ? str_repeat($bullet, 32) : null; + if ($is_new && ($v_secret === null)) { + // If we're creating a new credential, the credential type may have + // populated the secret for us (for example, generated an SSH key). In + // this case, + try { + $v_secret = $credential->getSecret()->openEnvelope(); + } catch (Exception $ex) { + // Ignore this. + } + } $validation_exception = null; $errors = array(); @@ -205,6 +219,7 @@ final class PassphraseCredentialEditController extends PassphraseController { } $form + ->addHiddenInput('isInitialized', true) ->appendChild( id(new AphrontFormTextControl()) ->setName('name') diff --git a/src/applications/passphrase/controller/PassphraseCredentialPublicController.php b/src/applications/passphrase/controller/PassphraseCredentialPublicController.php new file mode 100644 index 0000000000..dc129a327d --- /dev/null +++ b/src/applications/passphrase/controller/PassphraseCredentialPublicController.php @@ -0,0 +1,59 @@ +id = $data['id']; + } + + public function processRequest() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + $credential = id(new PassphraseCredentialQuery()) + ->setViewer($viewer) + ->withIDs(array($this->id)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + )) + ->executeOne(); + if (!$credential) { + return new Aphront404Response(); + } + + $type = PassphraseCredentialType::getTypeByConstant( + $credential->getCredentialType()); + if (!$type) { + throw new Exception(pht('Credential has invalid type "%s"!', $type)); + } + + if (!$type->hasPublicKey()) { + throw new Exception(pht('Credential has no public key!')); + } + + $view_uri = '/'.$credential->getMonogram(); + + $public_key = $type->getPublicKey($viewer, $credential); + + $body = id(new PHUIFormLayoutView()) + ->appendChild( + id(new AphrontFormTextAreaControl()) + ->setLabel(pht('Public Key')) + ->setReadOnly(true) + ->setValue($public_key)); + + $dialog = id(new AphrontDialogView()) + ->setUser($viewer) + ->setWidth(AphrontDialogView::WIDTH_FORM) + ->setTitle(pht('Public Key (%s)', $credential->getMonogram())) + ->appendChild($body) + ->addCancelButton($view_uri, pht('Done')); + + return id(new AphrontDialogResponse())->setDialog($dialog); + } + +} diff --git a/src/applications/passphrase/controller/PassphraseCredentialRevealController.php b/src/applications/passphrase/controller/PassphraseCredentialRevealController.php index 7599337f25..b11b370bbc 100644 --- a/src/applications/passphrase/controller/PassphraseCredentialRevealController.php +++ b/src/applications/passphrase/controller/PassphraseCredentialRevealController.php @@ -36,15 +36,21 @@ final class PassphraseCredentialRevealController id(new AphrontFormTextAreaControl()) ->setLabel(pht('Plaintext')) ->setReadOnly(true) + ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL) ->setValue($credential->getSecret()->openEnvelope())); } else { $body = pht('This credential has no associated secret.'); } + // NOTE: Disable workflow on the cancel button to reload the page so + // the viewer can see that their view was logged. + $dialog = id(new AphrontDialogView()) ->setUser($viewer) - ->setTitle(pht('Credential Secret')) + ->setWidth(AphrontDialogView::WIDTH_FORM) + ->setTitle(pht('Credential Secret (%s)', $credential->getMonogram())) ->appendChild($body) + ->setDisableWorkflowOnCancel(true) ->addCancelButton($view_uri, pht('Done')); $type_secret = PassphraseCredentialTransaction::TYPE_LOOKEDATSECRET; diff --git a/src/applications/passphrase/controller/PassphraseCredentialViewController.php b/src/applications/passphrase/controller/PassphraseCredentialViewController.php index 5ff0181bde..fcd87c83df 100644 --- a/src/applications/passphrase/controller/PassphraseCredentialViewController.php +++ b/src/applications/passphrase/controller/PassphraseCredentialViewController.php @@ -44,7 +44,7 @@ final class PassphraseCredentialViewController extends PassphraseController { $crumbs->addTextCrumb('K'.$credential->getID()); $header = $this->buildHeaderView($credential); - $actions = $this->buildActionView($credential); + $actions = $this->buildActionView($credential, $type); $properties = $this->buildPropertyView($credential, $type, $actions); $box = id(new PHUIObjectBoxView()) @@ -78,7 +78,9 @@ final class PassphraseCredentialViewController extends PassphraseController { return $header; } - private function buildActionView(PassphraseCredential $credential) { + private function buildActionView( + PassphraseCredential $credential, + PassphraseCredentialType $type) { $viewer = $this->getRequest()->getUser(); $id = $credential->getID(); @@ -116,6 +118,15 @@ final class PassphraseCredentialViewController extends PassphraseController { ->setHref($this->getApplicationURI("reveal/{$id}/")) ->setDisabled(!$can_edit) ->setWorkflow(true)); + + if ($type->hasPublicKey()) { + $actions->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Show Public Key')) + ->setIcon('download-alt') + ->setHref($this->getApplicationURI("public/{$id}/")) + ->setWorkflow(true)); + } } diff --git a/src/applications/passphrase/credentialtype/PassphraseCredentialType.php b/src/applications/passphrase/credentialtype/PassphraseCredentialType.php index 5d94a58f0d..2171c40cdc 100644 --- a/src/applications/passphrase/credentialtype/PassphraseCredentialType.php +++ b/src/applications/passphrase/credentialtype/PassphraseCredentialType.php @@ -50,6 +50,23 @@ abstract class PassphraseCredentialType extends Phobject { } + public function didInitializeNewCredential( + PhabricatorUser $actor, + PassphraseCredential $credential) { + return $credential; + } + + public function hasPublicKey() { + return false; + } + + public function getPublicKey( + PhabricatorUser $viewer, + PassphraseCredential $credential) { + return null; + } + + /* -( Passwords )---------------------------------------------------------- */ diff --git a/src/applications/passphrase/credentialtype/PassphraseCredentialTypeSSHGeneratedKey.php b/src/applications/passphrase/credentialtype/PassphraseCredentialTypeSSHGeneratedKey.php new file mode 100644 index 0000000000..4deff95f56 --- /dev/null +++ b/src/applications/passphrase/credentialtype/PassphraseCredentialTypeSSHGeneratedKey.php @@ -0,0 +1,36 @@ +attachSecret(new PhutilOpaqueEnvelope($private_key)); + + return $credential; + } + +} diff --git a/src/applications/passphrase/credentialtype/PassphraseCredentialTypeSSHPrivateKey.php b/src/applications/passphrase/credentialtype/PassphraseCredentialTypeSSHPrivateKey.php index 5c7c684eb8..1c874be453 100644 --- a/src/applications/passphrase/credentialtype/PassphraseCredentialTypeSSHPrivateKey.php +++ b/src/applications/passphrase/credentialtype/PassphraseCredentialTypeSSHPrivateKey.php @@ -9,4 +9,20 @@ abstract class PassphraseCredentialTypeSSHPrivateKey return self::PROVIDES_TYPE; } + public function hasPublicKey() { + return true; + } + + public function getPublicKey( + PhabricatorUser $viewer, + PassphraseCredential $credential) { + + $key = PassphraseSSHKey::loadFromPHID($credential->getPHID(), $viewer); + $file = $key->getKeyfileEnvelope(); + + list($stdout) = execx('ssh-keygen -y -f %P', $file); + + return $stdout; + } + } diff --git a/src/applications/passphrase/credentialtype/PassphraseCredentialTypeSSHPrivateKeyFile.php b/src/applications/passphrase/credentialtype/PassphraseCredentialTypeSSHPrivateKeyFile.php index ae856cc94f..66dd36e6cf 100644 --- a/src/applications/passphrase/credentialtype/PassphraseCredentialTypeSSHPrivateKeyFile.php +++ b/src/applications/passphrase/credentialtype/PassphraseCredentialTypeSSHPrivateKeyFile.php @@ -33,4 +33,9 @@ final class PassphraseCredentialTypeSSHPrivateKeyFile return false; } + public function hasPublicKey() { + // These have public keys, but they'd be cumbersome to extract. + return true; + } + } diff --git a/src/applications/passphrase/keys/PassphraseSSHKey.php b/src/applications/passphrase/keys/PassphraseSSHKey.php index 4ba5025b98..1c646704cb 100644 --- a/src/applications/passphrase/keys/PassphraseSSHKey.php +++ b/src/applications/passphrase/keys/PassphraseSSHKey.php @@ -15,10 +15,10 @@ final class PassphraseSSHKey extends PassphraseAbstractKey { public function getKeyfileEnvelope() { $credential = $this->requireCredential(); - $text_type = PassphraseCredentialTypeSSHPrivateKeyText::CREDENTIAL_TYPE; - if ($credential->getCredentialType() == $text_type) { - // If the credential stores key text, write it out to a temporary file - // so we can pass it to `ssh`. + $file_type = PassphraseCredentialTypeSSHPrivateKeyFile::CREDENTIAL_TYPE; + if ($credential->getCredentialType() != $file_type) { + // If the credential does not store a file, write the key txt out to a + // temporary file so we can pass it to `ssh`. if (!$this->keyFile) { $temporary_file = new TempFile('passphrase-ssh-key'); diff --git a/src/applications/passphrase/storage/PassphraseCredential.php b/src/applications/passphrase/storage/PassphraseCredential.php index 75c68766c0..a4a9de5a9b 100644 --- a/src/applications/passphrase/storage/PassphraseCredential.php +++ b/src/applications/passphrase/storage/PassphraseCredential.php @@ -25,6 +25,10 @@ final class PassphraseCredential extends PassphraseDAO ->setEditPolicy($actor->getPHID()); } + public function getMonogram() { + return 'K'.$this->getID(); + } + public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true,