| /* |
| * 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 <apr_strings.h> |
| #include <apr_user.h> |
| #include "svn_auth.h" |
| #include "svn_config.h" |
| #include "svn_error.h" |
| #include "svn_io.h" |
| #include "svn_pools.h" |
| #include "svn_cmdline.h" |
| #include "svn_checksum.h" |
| #include "svn_string.h" |
| #include "svn_hash.h" |
| #include "svn_user.h" |
| #include "svn_dirent_uri.h" |
| |
| #include "auth.h" |
| #include "private/svn_auth_private.h" |
| |
| #include "svn_private_config.h" |
| |
| #ifdef SVN_HAVE_GPG_AGENT |
| |
| #define BUFFER_SIZE 1024 |
| #define ATTEMPT_PARAMETER "svn.simple.gpg_agent.attempt" |
| |
| /* 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; |
| } |
| |
| #define is_hex(c) (((c) >= '0' && (c) <= '9') || ((c) >= 'A' && (c) <= 'F')) |
| #define hex_to_int(c) ((c) < '9' ? (c) - '0' : (c) - 'A' + 10) |
| |
| /* Modify STR in-place. '%', CR and LF are always percent escaped, |
| other characters may be percent escaped, always using uppercase |
| hex, see https://www.gnupg.org/documentation/manuals/assuan.pdf */ |
| static char * |
| unescape_assuan(char *str) |
| { |
| char *s = str; |
| |
| while (s[0]) |
| { |
| if (s[0] == '%' && is_hex(s[1]) && is_hex(s[2])) |
| { |
| char *s2 = s; |
| char val = hex_to_int(s[1]) * 16 + hex_to_int(s[2]); |
| |
| s2[0] = val; |
| ++s2; |
| |
| while (s2[2]) |
| { |
| s2[0] = s2[2]; |
| ++s2; |
| } |
| s2[0] = '\0'; |
| } |
| ++s; |
| } |
| |
| return str; |
| } |
| |
| /* Generate the string CACHE_ID_P based on the REALMSTRING allocated in |
| * RESULT_POOL using SCRATCH_POOL for temporary allocations. This is similar |
| * to other password caching mechanisms. */ |
| static svn_error_t * |
| get_cache_id(const char **cache_id_p, const char *realmstring, |
| apr_pool_t *result_pool, apr_pool_t *scratch_pool) |
| { |
| const char *cache_id = NULL; |
| svn_checksum_t *digest = NULL; |
| |
| SVN_ERR(svn_checksum(&digest, svn_checksum_md5, realmstring, |
| strlen(realmstring), scratch_pool)); |
| cache_id = svn_checksum_to_cstring(digest, result_pool); |
| *cache_id_p = cache_id; |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* 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); |
| } |
| |
| /* Send the BYE command and disconnect from the gpg-agent. Doing this avoids |
| * gpg-agent emitting a "Connection reset by peer" log message with some |
| * versions of gpg-agent. */ |
| static void |
| bye_gpg_agent(int sd) |
| { |
| /* don't bother to check the result of the write, it either worked or it |
| * didn't, but either way we're closing. */ |
| write(sd, "BYE\n", 4); |
| close(sd); |
| } |
| |
| /* This implements a method of finding the socket which is a mix of the |
| * description from GPG 1.x's gpg-agent man page under the |
| * --use-standard-socket option and the logic from GPG 2.x's socket discovery |
| * code in common/homedir.c. |
| * |
| * The man page says the standard socket is "named 'S.gpg-agent' located |
| * in the home directory." GPG's home directory is either the directory |
| * specified by $GNUPGHOME or ~/.gnupg. GPG >= 2.1.13 will check for a |
| * socket under (/var)/run/UID/gnupg before ~/.gnupg if no environment |
| * variables are set. |
| * |
| * $GPG_AGENT_INFO takes precedence, if set, otherwise $GNUPGHOME will be |
| * used. For GPG >= 2.1.13, $GNUPGHOME will be used directly only if it |
| * refers to the canonical home -- ~/.gnupg. Otherwise, the path specified |
| * by $GNUPGHOME is hashed (SHA1 + z-base-32) and the socket is expected to |
| * be present under (/var)/run/UID/gnupg/d.HASH. This last mechanism is not |
| * yet supported here. */ |
| static const char * |
| find_gpg_agent_socket(apr_pool_t *result_pool, apr_pool_t *scratch_pool) |
| { |
| char *gpg_agent_info = NULL; |
| char *gnupghome = NULL; |
| const char *socket_name = NULL; |
| |
| if ((gpg_agent_info = getenv("GPG_AGENT_INFO")) != NULL) |
| { |
| apr_array_header_t *socket_details; |
| |
| /* For reference GPG_AGENT_INFO consists of 3 : separated fields. |
| * The path to the socket, the pid of the gpg-agent process and |
| * finally the version of the protocol the agent talks. */ |
| socket_details = svn_cstring_split(gpg_agent_info, ":", TRUE, |
| scratch_pool); |
| socket_name = APR_ARRAY_IDX(socket_details, 0, const char *); |
| } |
| else if ((gnupghome = getenv("GNUPGHOME")) != NULL) |
| { |
| const char *homedir = svn_dirent_canonicalize(gnupghome, scratch_pool); |
| socket_name = svn_dirent_join(homedir, "S.gpg-agent", scratch_pool); |
| } |
| else |
| { |
| int i = 0; |
| const char *maybe_socket[] = {NULL, NULL, NULL, NULL}; |
| const char *homedir; |
| |
| #ifdef APR_HAS_USER |
| apr_uid_t uid; |
| apr_gid_t gid; |
| |
| if (apr_uid_current(&uid, &gid, scratch_pool) == APR_SUCCESS) |
| { |
| const char *uidbuf = apr_psprintf(scratch_pool, "%lu", |
| (unsigned long)uid); |
| maybe_socket[i++] = svn_dirent_join_many(scratch_pool, "/run/user", |
| uidbuf, "gnupg", |
| "S.gpg-agent", |
| SVN_VA_NULL); |
| maybe_socket[i++] = svn_dirent_join_many(scratch_pool, |
| "/var/run/user", |
| uidbuf, "gnupg", |
| "S.gpg-agent", |
| SVN_VA_NULL); |
| } |
| #endif |
| |
| homedir = svn_user_get_homedir(scratch_pool); |
| if (homedir) |
| maybe_socket[i++] = svn_dirent_join_many(scratch_pool, homedir, |
| ".gnupg", "S.gpg-agent", |
| SVN_VA_NULL); |
| |
| for (i = 0; !socket_name && maybe_socket[i]; i++) |
| { |
| apr_finfo_t finfo; |
| svn_error_t *err = svn_io_stat(&finfo, maybe_socket[i], |
| APR_FINFO_TYPE, scratch_pool); |
| if (!err && finfo.filetype == APR_SOCK) |
| socket_name = maybe_socket[i]; |
| svn_error_clear(err); |
| } |
| } |
| |
| if (socket_name) |
| socket_name = apr_pstrdup(result_pool, socket_name); |
| |
| return socket_name; |
| } |
| |
| /* Locate a running GPG Agent, and return an open file descriptor |
| * for communication with the agent in *NEW_SD. If no running agent |
| * can be found, set *NEW_SD to -1. */ |
| static svn_error_t * |
| find_running_gpg_agent(int *new_sd, apr_pool_t *pool) |
| { |
| char *buffer; |
| const char *socket_name = find_gpg_agent_socket(pool, pool); |
| const char *request = NULL; |
| const char *p = NULL; |
| char *ep = NULL; |
| int sd; |
| |
| *new_sd = -1; |
| |
| if (socket_name != NULL) |
| { |
| struct sockaddr_un addr; |
| |
| 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)) |
| { |
| bye_gpg_agent(sd); |
| return SVN_NO_ERROR; |
| } |
| |
| if (strncmp(buffer, "OK", 2) != 0) |
| { |
| bye_gpg_agent(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) |
| { |
| bye_gpg_agent(sd); |
| return SVN_NO_ERROR; |
| } |
| if (!receive_from_gpg_agent(sd, buffer, BUFFER_SIZE)) |
| { |
| bye_gpg_agent(sd); |
| return SVN_NO_ERROR; |
| } |
| if (strncmp(buffer, "D", 1) == 0) |
| p = &buffer[2]; |
| if (!p) |
| { |
| bye_gpg_agent(sd); |
| return SVN_NO_ERROR; |
| } |
| ep = strchr(p, '\n'); |
| if (ep != NULL) |
| *ep = '\0'; |
| if (strcmp(socket_name, p) != 0) |
| { |
| bye_gpg_agent(sd); |
| return SVN_NO_ERROR; |
| } |
| /* The agent will terminate its response with "OK". */ |
| if (!receive_from_gpg_agent(sd, buffer, BUFFER_SIZE)) |
| { |
| bye_gpg_agent(sd); |
| return SVN_NO_ERROR; |
| } |
| if (strncmp(buffer, "OK", 2) != 0) |
| { |
| bye_gpg_agent(sd); |
| return SVN_NO_ERROR; |
| } |
| |
| *new_sd = sd; |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_boolean_t |
| send_options(int sd, char *buf, size_t n, apr_pool_t *scratch_pool) |
| { |
| const char *tty_name; |
| const char *tty_type; |
| const char *lc_ctype; |
| const char *display; |
| |
| /* Send TTY_NAME to the gpg-agent daemon. */ |
| tty_name = getenv("GPG_TTY"); |
| if (tty_name != NULL) |
| { |
| if (!send_option(sd, buf, n, "ttyname", tty_name, scratch_pool)) |
| return FALSE; |
| } |
| |
| /* Send TTY_TYPE to the gpg-agent daemon. */ |
| tty_type = getenv("TERM"); |
| if (tty_type != NULL) |
| { |
| if (!send_option(sd, buf, n, "ttytype", tty_type, scratch_pool)) |
| return FALSE; |
| } |
| |
| /* 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, buf, n, "lc-ctype", lc_ctype, scratch_pool)) |
| return FALSE; |
| } |
| |
| /* Send DISPLAY to the gpg-agent daemon. */ |
| display = getenv("DISPLAY"); |
| if (display != NULL) |
| { |
| if (!send_option(sd, buf, n, "display", display, scratch_pool)) |
| return FALSE; |
| } |
| |
| return TRUE; |
| } |
| |
| /* 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 *p = NULL; |
| char *ep = NULL; |
| char *buffer; |
| const char *request = NULL; |
| const char *cache_id = NULL; |
| char *password_prompt; |
| char *realm_prompt; |
| char *error_prompt; |
| int *attempt; |
| |
| *done = FALSE; |
| |
| attempt = svn_hash_gets(parameters, ATTEMPT_PARAMETER); |
| |
| SVN_ERR(find_running_gpg_agent(&sd, pool)); |
| if (sd == -1) |
| return SVN_NO_ERROR; |
| |
| buffer = apr_palloc(pool, BUFFER_SIZE); |
| |
| if (!send_options(sd, buffer, BUFFER_SIZE, pool)) |
| { |
| bye_gpg_agent(sd); |
| return SVN_NO_ERROR; |
| } |
| |
| SVN_ERR(get_cache_id(&cache_id, realmstring, pool, pool)); |
| |
| password_prompt = apr_psprintf(pool, _("Password for '%s': "), username); |
| realm_prompt = apr_psprintf(pool, _("Enter your Subversion password for %s"), |
| realmstring); |
| if (*attempt == 1) |
| /* X means no error to the gpg-agent protocol */ |
| error_prompt = apr_pstrdup(pool, "X"); |
| else |
| error_prompt = apr_pstrdup(pool, _("Authentication failed")); |
| |
| request = apr_psprintf(pool, |
| "GET_PASSPHRASE --data %s" |
| "%s %s %s %s\n", |
| non_interactive ? "--no-ask " : "", |
| cache_id, |
| escape_blanks(error_prompt), |
| escape_blanks(password_prompt), |
| escape_blanks(realm_prompt)); |
| |
| if (write(sd, request, strlen(request)) == -1) |
| { |
| bye_gpg_agent(sd); |
| return SVN_NO_ERROR; |
| } |
| if (!receive_from_gpg_agent(sd, buffer, BUFFER_SIZE)) |
| { |
| bye_gpg_agent(sd); |
| return SVN_NO_ERROR; |
| } |
| |
| bye_gpg_agent(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 = unescape_assuan(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 we just need |
| to check if a running GPG Agent exists. */ |
| 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) |
| { |
| int sd; |
| |
| *done = FALSE; |
| |
| SVN_ERR(find_running_gpg_agent(&sd, pool)); |
| if (sd == -1) |
| return SVN_NO_ERROR; |
| |
| bye_gpg_agent(sd); |
| *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) |
| { |
| svn_error_t *err; |
| int *attempt = apr_palloc(pool, sizeof(*attempt)); |
| |
| *attempt = 1; |
| svn_hash_sets(parameters, ATTEMPT_PARAMETER, attempt); |
| err = svn_auth__simple_creds_cache_get(credentials, iter_baton, |
| provider_baton, parameters, |
| realmstring, password_get_gpg_agent, |
| SVN_AUTH__GPG_AGENT_PASSWORD_TYPE, |
| pool); |
| *iter_baton = attempt; |
| |
| return err; |
| } |
| |
| /* An implementation of svn_auth_provider_t::next_credentials() */ |
| static svn_error_t * |
| simple_gpg_agent_next_creds(void **credentials, |
| void *iter_baton, |
| void *provider_baton, |
| apr_hash_t *parameters, |
| const char *realmstring, |
| apr_pool_t *pool) |
| { |
| int *attempt = (int *)iter_baton; |
| int sd; |
| char *buffer; |
| const char *cache_id = NULL; |
| const char *request = NULL; |
| |
| *credentials = NULL; |
| |
| /* The users previous credentials failed so first remove the cached entry, |
| * before trying to retrieve them again. Because gpg-agent stores cached |
| * credentials immediately upon retrieving them, this gives us the |
| * opportunity to remove the invalid credentials and prompt the |
| * user again. While it's possible that server side issues could trigger |
| * this, this cache is ephemeral so at worst we're just speeding up |
| * when the user would need to re-enter their password. */ |
| |
| if (svn_hash_gets(parameters, SVN_AUTH_PARAM_NON_INTERACTIVE)) |
| { |
| /* In this case since we're running non-interactively we do not |
| * want to clear the cache since the user was never prompted by |
| * gpg-agent to set a password. */ |
| return SVN_NO_ERROR; |
| } |
| |
| *attempt = *attempt + 1; |
| |
| SVN_ERR(find_running_gpg_agent(&sd, pool)); |
| if (sd == -1) |
| return SVN_NO_ERROR; |
| |
| buffer = apr_palloc(pool, BUFFER_SIZE); |
| |
| if (!send_options(sd, buffer, BUFFER_SIZE, pool)) |
| { |
| bye_gpg_agent(sd); |
| return SVN_NO_ERROR; |
| } |
| |
| SVN_ERR(get_cache_id(&cache_id, realmstring, pool, pool)); |
| |
| request = apr_psprintf(pool, "CLEAR_PASSPHRASE %s\n", cache_id); |
| |
| if (write(sd, request, strlen(request)) == -1) |
| { |
| bye_gpg_agent(sd); |
| return SVN_NO_ERROR; |
| } |
| |
| if (!receive_from_gpg_agent(sd, buffer, BUFFER_SIZE)) |
| { |
| bye_gpg_agent(sd); |
| return SVN_NO_ERROR; |
| } |
| |
| bye_gpg_agent(sd); |
| |
| if (strncmp(buffer, "OK\n", 3) != 0) |
| return SVN_NO_ERROR; |
| |
| /* TODO: This attempt limit hard codes it at 3 attempts (or 2 retries) |
| * which matches svn command line client's retry_limit as set in |
| * svn_cmdline_create_auth_baton(). It would be nice to have that |
| * limit reflected here but that violates the boundary between the |
| * prompt provider and the cache provider. gpg-agent is acting as |
| * both here due to the peculiarities of their design so we'll have to |
| * live with this for now. Note that when these failures get exceeded |
| * it'll eventually fall back on the retry limits of whatever prompt |
| * provider is in effect, so this effectively doubles the limit. */ |
| if (*attempt < 4) |
| 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); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| |
| /* 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, |
| simple_gpg_agent_next_creds, |
| 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 */ |