summaryrefslogtreecommitdiffstats
path: root/subversion/libsvn_subr/gpg_agent.c
diff options
context:
space:
mode:
Diffstat (limited to 'subversion/libsvn_subr/gpg_agent.c')
-rw-r--r--subversion/libsvn_subr/gpg_agent.c463
1 files changed, 463 insertions, 0 deletions
diff --git a/subversion/libsvn_subr/gpg_agent.c b/subversion/libsvn_subr/gpg_agent.c
new file mode 100644
index 0000000..f0395c0
--- /dev/null
+++ b/subversion/libsvn_subr/gpg_agent.c
@@ -0,0 +1,463 @@
+/*
+ * gpg_agent.c: GPG Agent provider for SVN_AUTH_CRED_*
+ *
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ */
+
+/* ==================================================================== */
+
+/* This auth provider stores a plaintext password in memory managed by
+ * a running gpg-agent. In contrast to other password store providers
+ * it does not save the password to disk.
+ *
+ * Prompting is performed by the gpg-agent using a "pinentry" program
+ * which needs to be installed separately. There are several pinentry
+ * implementations with different front-ends (e.g. qt, gtk, ncurses).
+ *
+ * The gpg-agent will let the password time out after a while,
+ * or immediately when it receives the SIGHUP signal.
+ * When the password has timed out it will automatically prompt the
+ * user for the password again. This is transparent to Subversion.
+ *
+ * SECURITY CONSIDERATIONS:
+ *
+ * Communication to the agent happens over a UNIX socket, which is located
+ * in a directory which only the user running Subversion can access.
+ * However, any program the user runs could access this socket and get
+ * the Subversion password if the program knows the "cache ID" Subversion
+ * uses for the password.
+ * The cache ID is very easy to obtain for programs running as the same user.
+ * Subversion uses the MD5 of the realmstring as cache ID, and these checksums
+ * are also used as filenames within ~/.subversion/auth/svn.simple.
+ * Unlike GNOME Keyring or KDE Wallet, the user is not prompted for
+ * permission if another program attempts to access the password.
+ *
+ * Therefore, while the gpg-agent is running and has the password cached,
+ * this provider is no more secure than a file storing the password in
+ * plaintext.
+ */
+
+
+/*** Includes. ***/
+
+#ifndef WIN32
+
+#include <unistd.h>
+
+#include <sys/socket.h>
+#include <sys/un.h>
+
+#include <apr_pools.h>
+#include "svn_auth.h"
+#include "svn_config.h"
+#include "svn_error.h"
+#include "svn_pools.h"
+#include "svn_cmdline.h"
+#include "svn_checksum.h"
+#include "svn_string.h"
+
+#include "private/svn_auth_private.h"
+
+#include "svn_private_config.h"
+
+#ifdef SVN_HAVE_GPG_AGENT
+
+#define BUFFER_SIZE 1024
+
+/* Modify STR in-place such that blanks are escaped as required by the
+ * gpg-agent protocol. Return a pointer to STR. */
+static char *
+escape_blanks(char *str)
+{
+ char *s = str;
+
+ while (*s)
+ {
+ if (*s == ' ')
+ *s = '+';
+ s++;
+ }
+
+ return str;
+}
+
+/* Attempt to read a gpg-agent response message from the socket SD into
+ * buffer BUF. Buf is assumed to be N bytes large. Return TRUE if a response
+ * message could be read that fits into the buffer. Else return FALSE.
+ * If a message could be read it will always be NUL-terminated and the
+ * trailing newline is retained. */
+static svn_boolean_t
+receive_from_gpg_agent(int sd, char *buf, size_t n)
+{
+ int i = 0;
+ size_t recvd;
+ char c;
+
+ /* Clear existing buffer content before reading response. */
+ if (n > 0)
+ *buf = '\0';
+
+ /* Require the message to fit into the buffer and be terminated
+ * with a newline. */
+ while (i < n)
+ {
+ recvd = read(sd, &c, 1);
+ if (recvd == -1)
+ return FALSE;
+ buf[i] = c;
+ i++;
+ if (i < n && c == '\n')
+ {
+ buf[i] = '\0';
+ return TRUE;
+ }
+ }
+
+ return FALSE;
+}
+
+/* Using socket SD, send the option OPTION with the specified VALUE
+ * to the gpg agent. Store the response in BUF, assumed to be N bytes
+ * in size, and evaluate the response. Return TRUE if the agent liked
+ * the smell of the option, if there is such a thing, and doesn't feel
+ * saturated by it. Else return FALSE.
+ * Do temporary allocations in scratch_pool. */
+static svn_boolean_t
+send_option(int sd, char *buf, size_t n, const char *option, const char *value,
+ apr_pool_t *scratch_pool)
+{
+ const char *request;
+
+ request = apr_psprintf(scratch_pool, "OPTION %s=%s\n", option, value);
+
+ if (write(sd, request, strlen(request)) == -1)
+ return FALSE;
+
+ if (!receive_from_gpg_agent(sd, buf, n))
+ return FALSE;
+
+ return (strncmp(buf, "OK", 2) == 0);
+}
+
+/* Implementation of svn_auth__password_get_t that retrieves the password
+ from gpg-agent */
+static svn_error_t *
+password_get_gpg_agent(svn_boolean_t *done,
+ const char **password,
+ apr_hash_t *creds,
+ const char *realmstring,
+ const char *username,
+ apr_hash_t *parameters,
+ svn_boolean_t non_interactive,
+ apr_pool_t *pool)
+{
+ int sd;
+ char *gpg_agent_info = NULL;
+ const char *p = NULL;
+ char *ep = NULL;
+ char *buffer;
+
+ apr_array_header_t *socket_details;
+ const char *request = NULL;
+ const char *cache_id = NULL;
+ struct sockaddr_un addr;
+ const char *tty_name;
+ const char *tty_type;
+ const char *lc_ctype;
+ const char *display;
+ const char *socket_name = NULL;
+ svn_checksum_t *digest = NULL;
+ char *password_prompt;
+ char *realm_prompt;
+
+ *done = FALSE;
+
+ gpg_agent_info = getenv("GPG_AGENT_INFO");
+ if (gpg_agent_info != NULL)
+ {
+ socket_details = svn_cstring_split(gpg_agent_info, ":", TRUE,
+ pool);
+ socket_name = APR_ARRAY_IDX(socket_details, 0, const char *);
+ }
+ else
+ return SVN_NO_ERROR;
+
+ if (socket_name != NULL)
+ {
+ addr.sun_family = AF_UNIX;
+ strncpy(addr.sun_path, socket_name, sizeof(addr.sun_path) - 1);
+ addr.sun_path[sizeof(addr.sun_path) - 1] = '\0';
+
+ sd = socket(AF_UNIX, SOCK_STREAM, 0);
+ if (sd == -1)
+ return SVN_NO_ERROR;
+
+ if (connect(sd, (struct sockaddr *)&addr, sizeof(addr)) == -1)
+ {
+ close(sd);
+ return SVN_NO_ERROR;
+ }
+ }
+ else
+ return SVN_NO_ERROR;
+
+ /* Receive the connection status from the gpg-agent daemon. */
+ buffer = apr_palloc(pool, BUFFER_SIZE);
+ if (!receive_from_gpg_agent(sd, buffer, BUFFER_SIZE))
+ {
+ close(sd);
+ return SVN_NO_ERROR;
+ }
+
+ if (strncmp(buffer, "OK", 2) != 0)
+ {
+ close(sd);
+ return SVN_NO_ERROR;
+ }
+
+ /* The GPG-Agent documentation says:
+ * "Clients should deny to access an agent with a socket name which does
+ * not match its own configuration". */
+ request = "GETINFO socket_name\n";
+ if (write(sd, request, strlen(request)) == -1)
+ {
+ close(sd);
+ return SVN_NO_ERROR;
+ }
+ if (!receive_from_gpg_agent(sd, buffer, BUFFER_SIZE))
+ {
+ close(sd);
+ return SVN_NO_ERROR;
+ }
+ if (strncmp(buffer, "D", 1) == 0)
+ p = &buffer[2];
+ if (!p)
+ {
+ close(sd);
+ return SVN_NO_ERROR;
+ }
+ ep = strchr(p, '\n');
+ if (ep != NULL)
+ *ep = '\0';
+ if (strcmp(socket_name, p) != 0)
+ {
+ close(sd);
+ return SVN_NO_ERROR;
+ }
+ /* The agent will terminate its response with "OK". */
+ if (!receive_from_gpg_agent(sd, buffer, BUFFER_SIZE))
+ {
+ close(sd);
+ return SVN_NO_ERROR;
+ }
+ if (strncmp(buffer, "OK", 2) != 0)
+ {
+ close(sd);
+ return SVN_NO_ERROR;
+ }
+
+ /* Send TTY_NAME to the gpg-agent daemon. */
+ tty_name = getenv("GPG_TTY");
+ if (tty_name != NULL)
+ {
+ if (!send_option(sd, buffer, BUFFER_SIZE, "ttyname", tty_name, pool))
+ {
+ close(sd);
+ return SVN_NO_ERROR;
+ }
+ }
+ else
+ {
+ close(sd);
+ return SVN_NO_ERROR;
+ }
+
+ /* Send TTY_TYPE to the gpg-agent daemon. */
+ tty_type = getenv("TERM");
+ if (tty_type != NULL)
+ {
+ if (!send_option(sd, buffer, BUFFER_SIZE, "ttytype", tty_type, pool))
+ {
+ close(sd);
+ return SVN_NO_ERROR;
+ }
+ }
+ else
+ {
+ close(sd);
+ return SVN_NO_ERROR;
+ }
+
+ /* Compute LC_CTYPE. */
+ lc_ctype = getenv("LC_ALL");
+ if (lc_ctype == NULL)
+ lc_ctype = getenv("LC_CTYPE");
+ if (lc_ctype == NULL)
+ lc_ctype = getenv("LANG");
+
+ /* Send LC_CTYPE to the gpg-agent daemon. */
+ if (lc_ctype != NULL)
+ {
+ if (!send_option(sd, buffer, BUFFER_SIZE, "lc-ctype", lc_ctype, pool))
+ {
+ close(sd);
+ return SVN_NO_ERROR;
+ }
+ }
+
+ /* Send DISPLAY to the gpg-agent daemon. */
+ display = getenv("DISPLAY");
+ if (display != NULL)
+ {
+ if (!send_option(sd, buffer, BUFFER_SIZE, "display", display, pool))
+ {
+ close(sd);
+ return SVN_NO_ERROR;
+ }
+ }
+
+ /* Create the CACHE_ID which will be generated based on REALMSTRING similar
+ to other password caching mechanisms. */
+ SVN_ERR(svn_checksum(&digest, svn_checksum_md5, realmstring,
+ strlen(realmstring), pool));
+ cache_id = svn_checksum_to_cstring(digest, pool);
+
+ password_prompt = apr_psprintf(pool, _("Password for '%s': "), username);
+ realm_prompt = apr_psprintf(pool, _("Enter your Subversion password for %s"),
+ realmstring);
+ request = apr_psprintf(pool,
+ "GET_PASSPHRASE --data %s--repeat=1 "
+ "%s X %s %s\n",
+ non_interactive ? "--no-ask " : "",
+ cache_id,
+ escape_blanks(password_prompt),
+ escape_blanks(realm_prompt));
+
+ if (write(sd, request, strlen(request)) == -1)
+ {
+ close(sd);
+ return SVN_NO_ERROR;
+ }
+ if (!receive_from_gpg_agent(sd, buffer, BUFFER_SIZE))
+ {
+ close(sd);
+ return SVN_NO_ERROR;
+ }
+
+ close(sd);
+
+ if (strncmp(buffer, "ERR", 3) == 0)
+ return SVN_NO_ERROR;
+
+ p = NULL;
+ if (strncmp(buffer, "D", 1) == 0)
+ p = &buffer[2];
+
+ if (!p)
+ return SVN_NO_ERROR;
+
+ ep = strchr(p, '\n');
+ if (ep != NULL)
+ *ep = '\0';
+
+ *password = p;
+
+ *done = TRUE;
+ return SVN_NO_ERROR;
+}
+
+
+/* Implementation of svn_auth__password_set_t that would store the
+ password in GPG Agent if that's how this particular integration
+ worked. But it isn't. GPG Agent stores the password provided by
+ the user via the pinentry program immediately upon its provision
+ (and regardless of its accuracy as passwords go), so there's
+ nothing really to do here. */
+static svn_error_t *
+password_set_gpg_agent(svn_boolean_t *done,
+ apr_hash_t *creds,
+ const char *realmstring,
+ const char *username,
+ const char *password,
+ apr_hash_t *parameters,
+ svn_boolean_t non_interactive,
+ apr_pool_t *pool)
+{
+ *done = TRUE;
+
+ return SVN_NO_ERROR;
+}
+
+
+/* An implementation of svn_auth_provider_t::first_credentials() */
+static svn_error_t *
+simple_gpg_agent_first_creds(void **credentials,
+ void **iter_baton,
+ void *provider_baton,
+ apr_hash_t *parameters,
+ const char *realmstring,
+ apr_pool_t *pool)
+{
+ return svn_auth__simple_creds_cache_get(credentials, iter_baton,
+ provider_baton, parameters,
+ realmstring, password_get_gpg_agent,
+ SVN_AUTH__GPG_AGENT_PASSWORD_TYPE,
+ pool);
+}
+
+
+/* An implementation of svn_auth_provider_t::save_credentials() */
+static svn_error_t *
+simple_gpg_agent_save_creds(svn_boolean_t *saved,
+ void *credentials,
+ void *provider_baton,
+ apr_hash_t *parameters,
+ const char *realmstring,
+ apr_pool_t *pool)
+{
+ return svn_auth__simple_creds_cache_set(saved, credentials,
+ provider_baton, parameters,
+ realmstring, password_set_gpg_agent,
+ SVN_AUTH__GPG_AGENT_PASSWORD_TYPE,
+ pool);
+}
+
+
+static const svn_auth_provider_t gpg_agent_simple_provider = {
+ SVN_AUTH_CRED_SIMPLE,
+ simple_gpg_agent_first_creds,
+ NULL,
+ simple_gpg_agent_save_creds
+};
+
+
+/* Public API */
+void
+svn_auth_get_gpg_agent_simple_provider(svn_auth_provider_object_t **provider,
+ apr_pool_t *pool)
+{
+ svn_auth_provider_object_t *po = apr_pcalloc(pool, sizeof(*po));
+
+ po->vtable = &gpg_agent_simple_provider;
+ *provider = po;
+}
+
+#endif /* SVN_HAVE_GPG_AGENT */
+#endif /* !WIN32 */
OpenPOWER on IntegriCloud