diff options
author | Simo Sorce <simo@redhat.com> | 2014-01-04 15:15:39 -0500 |
---|---|---|
committer | Simo Sorce <simo@redhat.com> | 2014-01-06 21:27:55 -0500 |
commit | d5f34554229b2e41dc4147547686a2c3f494d745 (patch) | |
tree | cc787351aae66ba7c79740eadb342533ee835703 | |
parent | 8a1f41674541eb0d1c5df9c090cec1674f2d9284 (diff) | |
download | gss-proxy-d5f34554229b2e41dc4147547686a2c3f494d745.tar.gz gss-proxy-d5f34554229b2e41dc4147547686a2c3f494d745.tar.xz gss-proxy-d5f34554229b2e41dc4147547686a2c3f494d745.zip |
Add facility to manage encrypted user credentials
Signed-off-by: Simo Sorce <simo@redhat.com>
-rw-r--r-- | proxy/Makefile.am | 22 | ||||
-rw-r--r-- | proxy/external/libkeyutils.m4 | 2 | ||||
-rw-r--r-- | proxy/src/gp_common.h | 17 | ||||
-rw-r--r-- | proxy/src/gp_secrets.c | 343 | ||||
-rw-r--r-- | proxy/src/gp_secrets.h | 34 | ||||
-rw-r--r-- | proxy/src/gpcc.c | 226 |
6 files changed, 637 insertions, 7 deletions
diff --git a/proxy/Makefile.am b/proxy/Makefile.am index 10f4e30..a6c64e3 100644 --- a/proxy/Makefile.am +++ b/proxy/Makefile.am @@ -29,6 +29,7 @@ pkgconfigdir = $(libdir)/pkgconfig gpstatedir = @gpstatedir@ gpclidir = @gpstatedir@/clients +gpprivdir = @gpstatedir@/private AM_CFLAGS = if WANT_AUX_INFO @@ -48,6 +49,9 @@ ACLOCAL_AMFLAGS = -I m4 -I . sbin_PROGRAMS = \ gssproxy +bin_PROGRAMS = \ + gpcc + check_PROGRAMS = \ cli_srv_comm interposetest @@ -81,7 +85,9 @@ AM_CPPFLAGS = \ EXTRA_DIST = build/config.rpath -GSS_PROXY_LIBS = $(POPT_LIBS) $(KRB5_LIBS) $(VERTO_LIBS) $(INI_LIBS) $(GSSAPI_LIBS) $(GSSRPC_LIBS) +GSS_PROXY_LIBS = $(POPT_LIBS) $(VERTO_LIBS) $(INI_LIBS) \ + $(KRB5_LIBS) $(GSSAPI_LIBS) $(GSSRPC_LIBS) \ + $(KEYUTILS_LIBS) if BUILD_SELINUX GSS_PROXY_LIBS += $(SELINUX_LIBS) @@ -137,6 +143,7 @@ dist_noinst_HEADERS = \ src/gp_rpc_creds.h \ src/gp_selinux.h \ src/gp_crypto.h \ + src/gp_secrets.h \ src/mechglue/gss_plugin.h @@ -160,6 +167,7 @@ gssproxy_SOURCES = \ src/gp_log.c \ src/gp_util.c \ src/gp_crypto.c \ + src/gp_secrets.c \ src/gp_rpc_accept_sec_context.c \ src/gp_rpc_release_handle.c \ src/gp_rpc_acquire_cred.c \ @@ -196,6 +204,14 @@ interposetest_SOURCES = \ src/gp_debug.c \ tests/interposetest.c +gpcc_SOURCES = \ + src/gp_debug.c \ + src/gp_log.c \ + src/gp_util.c \ + src/gp_crypto.c \ + src/gp_secrets.c \ + src/gpcc.c + gssproxy_LDADD = \ $(GSS_PROXY_LIBS) @@ -205,6 +221,9 @@ cli_srv_comm_LDADD = \ interposetest_LDADD = \ $(GSS_PROXY_LIBS) +gpcc_LDADD = \ + $(GSS_PROXY_LIBS) + dist_noinst_DATA += \ m4 @@ -241,6 +260,7 @@ installgsspdirs:: $(DESTDIR)$(logpath) \ $(DESTDIR)$(gpstatedir) \ $(DESTDIR)$(gpclidir) \ + $(DESTDIR)$(gpprivdir) \ $(DESTDIR)$(pubconfpath) if HAVE_DOXYGEN diff --git a/proxy/external/libkeyutils.m4 b/proxy/external/libkeyutils.m4 index 5753d77..1dee80b 100644 --- a/proxy/external/libkeyutils.m4 +++ b/proxy/external/libkeyutils.m4 @@ -1,7 +1,7 @@ AC_SUBST(KEYUTILS_LIBS) AC_CHECK_HEADERS([keyutils.h], - [AC_CHECK_LIB([keyutils], [add_key], + [AC_CHECK_LIB([keyutils], [keyctl_get_persistent], [AC_DEFINE(USE_KEYRING, 1, [Define if the keyring should be used]) KEYUTILS_LIBS="-lkeyutils" ], diff --git a/proxy/src/gp_common.h b/proxy/src/gp_common.h index 7b3e9ac..ceca481 100644 --- a/proxy/src/gp_common.h +++ b/proxy/src/gp_common.h @@ -57,11 +57,6 @@ elem->next = NULL; \ } while (0) -#define safefree(ptr) do { \ - free(no_const(ptr)); \ - ptr = NULL; \ -} while(0) - /* max out at 1MB for now */ #define MAX_RPC_SIZE 1024*1024 @@ -73,6 +68,18 @@ ssize_t gp_safe_read(int fd, void *buf, size_t count); ssize_t gp_safe_write(int fd, const void *buf, size_t count); void gp_safe_zero(void *buf, size_t len); +#define safefree(ptr) \ +if (ptr) { \ + free(no_const(ptr)); \ + ptr = NULL; \ +} + +#define strzerofree(str) \ +if (str) { \ + gp_safe_zero((str), strlen(str)); \ + safefree(str); \ +} + /* NOTE: read the note in gp_util.c before using gp_strerror() */ char *gp_strerror(int errnum); diff --git a/proxy/src/gp_secrets.c b/proxy/src/gp_secrets.c new file mode 100644 index 0000000..af63b3a --- /dev/null +++ b/proxy/src/gp_secrets.c @@ -0,0 +1,343 @@ +/* + GSS-PROXY + + Copyright (C) 2013 Simo Sorce <simo.sorce@redhat.com> + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#include "config.h" +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <keyutils.h> +#include <errno.h> +#include <string.h> +#include <stdbool.h> +#include "gp_common.h" +#include "gp_crypto.h" +#include "gp_secrets.h" + +#define PRIV_PATH GPSTATE_PATH"/private" + +#define USER_KEY "user" +#define CREDS_KEY_NAME "GSS-Proxy-Creds-Key" + +int gp_priv_init(void) +{ + struct gp_crypto_key *encryption_key; + key_serial_t keyring, keyid; + size_t len; + void *buf; + int ret; + + /* create private dir if necessary */ + ret = mkdir(PRIV_PATH, S_IRWXU); + if (ret == -1 && errno != EEXIST) { + ret = errno; + GPERROR("Failed to create %s directory.\n", PRIV_PATH); + return ret; + } + + keyring = keyctl_get_persistent(getuid(), KEY_SPEC_PROCESS_KEYRING); + if (keyring == -1) { + ret = errno; + GPERROR("Failed to get persistent keyring: (%d) %s\n", + ret, gp_strerror(ret)); + return ret; + } + + keyid = keyctl_search(keyring, USER_KEY, CREDS_KEY_NAME, 0); + if (keyid == -1) { + /* not found, generate a new key */ + encryption_key = gp_crypto_new_key(); + if (!encryption_key) { + GPERROR("Failed to generate key\n"); + return EINVAL; + } + ret = gp_crypto_export_key(encryption_key, &buf, &len); + if (ret) { + GPERROR("Failed to serialize key\n"); + return ret; + } + keyid = add_key(USER_KEY, CREDS_KEY_NAME, buf, len, keyring); + if (keyid == -1) { + ret = errno; + GPERROR("Failed to store random key: (%d) %s\n", + ret, gp_strerror(ret)); + return ret; + } + + gp_safe_zero(buf, len); + } + + return 0; +} + +static int get_crypto_key(struct gp_crypto_key **key) +{ + key_serial_t keyring, keyid; + void *kbuf = NULL; + size_t klen = 0; + int ret; + + keyring = keyctl_get_persistent(getuid(), KEY_SPEC_PROCESS_KEYRING); + if (keyring == -1) { + ret = errno; + GPERROR("Failed to get persistent keyring: (%d) %s\n", + ret, gp_strerror(ret)); + return ret; + } + + keyid = keyctl_search(keyring, USER_KEY, CREDS_KEY_NAME, 0); + if (keyid == -1) { + ret = errno; + GPERROR("Failed to get encryption key: (%d) %s\n", + ret, gp_strerror(ret)); + return ret; + } + + klen = keyctl_read_alloc(keyid, &kbuf); + if (klen == -1) { + ret = errno; + GPERROR("Failed to read encryption key: (%d) %s\n", + ret, gp_strerror(ret)); + return ret; + } + + ret = gp_crypto_import_key(kbuf, klen, key); + if (ret) { + GPERROR("Failed to serialize key: (%d) %s\n", + ret, gp_strerror(ret)); + } + + gp_safe_zero(kbuf, klen); + free(kbuf); + + return ret; +} + +static int decrypt_user_creds(char *buf, size_t len) +{ + struct gp_crypto_key *encryption_key; + void *tmpbuf; + size_t tmplen; + int ret; + + ret = get_crypto_key(&encryption_key); + if (ret) { + return ret; + } + + ret = gp_crypto_decrypt(encryption_key, buf, len, &tmpbuf, &tmplen); + if (ret) { + GPERROR("Failed to decrypt user creds: (%d) %s\n", + ret, gp_strerror(ret)); + goto done; + } + + /* copy plaintext over */ + memcpy(buf, tmpbuf, tmplen); + buf[tmplen] = '\0'; + + /* then zero and free temporary buffer */ + gp_safe_zero(tmpbuf, tmplen); + free(tmpbuf); + +done: + gp_crypto_free_key(&encryption_key); + return ret; +} + +static int encrypt_user_creds(void *in, size_t inlen, + void **out, size_t *outlen) +{ + struct gp_crypto_key *encryption_key; + int ret; + + ret = get_crypto_key(&encryption_key); + if (ret) { + return ret; + } + + ret = gp_crypto_encrypt(encryption_key, in, inlen, out, outlen); + if (ret) { + GPERROR("Failed to encrypt user creds: (%d) %s\n", + ret, gp_strerror(ret)); + } + + gp_crypto_free_key(&encryption_key); + return ret; +} + +#define MAX_CREDS_SIZE 1024 + +int gp_get_uid_creds(uid_t uid, char **domain, + char **username, char **password) +{ + char *filename; + char *s, *e; + char buf[MAX_CREDS_SIZE + 1]; + int len; + int ret; + int fd; + + ret = asprintf(&filename, "%s/creds_%d", PRIV_PATH, uid); + if (ret == -1) { + return ENOMEM; + } + + *domain = NULL; + *username = NULL; + *password = NULL; + + fd = open(filename, O_RDONLY | O_CLOEXEC); + if (fd == -1) { + ret = errno; + GPERROR("Failed to open creds file [%s]: (%d) %s\n", + filename, ret, gp_strerror(ret)); + goto done; + } + + len = gp_safe_read(fd, buf, MAX_CREDS_SIZE + 1); + if (len == -1) { + ret = errno; + GPERROR("Failed to read creds file [%s]: (%d) %s\n", + filename, ret, gp_strerror(ret)); + goto done; + } + if (len > MAX_CREDS_SIZE) { + GPERROR("Creds file %s is too big.\n", filename); + ret = E2BIG; + goto done; + } + + ret = decrypt_user_creds(buf, len); + if (ret) goto done; + + /* get domain */ + s = buf; + e = strchr(s, ':'); + if (!e) { + GPERROR("Invalid creds (malformed)\n"); + ret = EINVAL; + goto done; + } + ret = asprintf(domain, "%.*s", (int)(e - s), s); + if (ret == -1) { + ret = ENOMEM; + goto done; + } + + /* get username */ + s = e + 1; + e = strchr(s, ':'); + if (!e) { + GPERROR("Invalid creds (malformed)\n"); + ret = EINVAL; + goto done; + } + ret = asprintf(username, "%.*s", (int)(e - s), s); + if (ret == -1) { + ret = ENOMEM; + goto done; + } + + /* get password */ + s = e + 1; + *password = strdup(s); + if (*password == NULL) { + ret = ENOMEM; + goto done; + } + + ret = 0; + +done: + if (ret) { + strzerofree(*domain); + strzerofree(*username); + strzerofree(*password); + } + gp_safe_zero(buf, MAX_CREDS_SIZE); + free(filename); + close(fd); + return ret; +} + +int gp_save_uid_creds(uid_t uid, char *domain, + char *username, char *password) +{ + char *filename; + char buf[MAX_CREDS_SIZE + 1]; + void *ebuf = NULL; + size_t elen = 0; + int len; + int ret; + int fd; + + ret = asprintf(&filename, "%s/creds_%d", PRIV_PATH, uid); + if (ret == -1) { + return ENOMEM; + } + + fd = open(filename, O_CREAT | O_TRUNC | O_WRONLY | O_CLOEXEC, 0600); + if (fd == -1) { + ret = errno; + GPERROR("Failed to open creds file [%s]: (%d) %s\n", + filename, ret, gp_strerror(ret)); + goto done; + } + + len = snprintf(buf, MAX_CREDS_SIZE, "%s:%s:%s", + domain ? domain : "", username, password); + if (len < 0 || len > MAX_CREDS_SIZE) { + GPERROR("Failed to properly format creds buffer.\n"); + ret = EINVAL; + goto done; + } + + ret = encrypt_user_creds(buf, len, &ebuf, &elen); + if (ret) goto done; + + if (elen > MAX_CREDS_SIZE) { + GPERROR("Final creds buffer too long. Max is %d, got %lu.\n", + MAX_CREDS_SIZE, elen); + ret = EINVAL; + goto done; + } + + len = gp_safe_write(fd, ebuf, elen); + if (len != elen) { + ret = errno; + GPERROR("Failed to write creds file [%s]: (%d) %s\n", + filename, ret, gp_strerror(ret)); + goto done; + } + + ret = 0; + +done: + gp_safe_zero(buf, MAX_CREDS_SIZE); + gp_safe_zero(ebuf, elen); + free(filename); + free(ebuf); + close(fd); + return ret; +} diff --git a/proxy/src/gp_secrets.h b/proxy/src/gp_secrets.h new file mode 100644 index 0000000..1c384e2 --- /dev/null +++ b/proxy/src/gp_secrets.h @@ -0,0 +1,34 @@ +/* + GSS-PROXY + + Copyright (C) 2013 Simo Sorce <simo.sorce@redhat.com> + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#ifndef _GP_SECRETS_H_ +#define _GP_SECRETS_H_ + +int gp_priv_init(void); +int gp_get_uid_creds(uid_t uid, char **domain, + char **username, char **password); +int gp_save_uid_creds(uid_t uid, char *domain, + char *username, char *password); + +#endif /* _GP_SECRETS_H_ */ diff --git a/proxy/src/gpcc.c b/proxy/src/gpcc.c new file mode 100644 index 0000000..44a2690 --- /dev/null +++ b/proxy/src/gpcc.c @@ -0,0 +1,226 @@ +/* + GSS-PROXY + + Copyright (C) 2011 Red Hat, Inc. + Copyright (C) 2011 Simo Sorce <simo.sorce@redhat.com> + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#include "config.h" +#include <libintl.h> +#include <stdbool.h> +#include <stdint.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <sys/types.h> +#include <stdio.h> +#include <termios.h> +#include <errno.h> +#include "popt.h" +#include "gp_common.h" +#include "gp_secrets.h" + +#define _(STRING) gettext(STRING) + +char *fgets_password(void) +{ + struct termios old_flags, flags; + char buf[1024]; + char *pwd; + int ret; + + ret = tcgetattr(fileno(stdin), &old_flags); + flags = old_flags; + flags.c_lflag = (flags.c_lflag & ~ECHO) | ECHONL; + + ret = tcsetattr(fileno(stdin), TCSANOW, &flags); + if (ret) return NULL; + + fprintf(stdout, "Password: "); + pwd = fgets(buf, 1024, stdin); + + /* nothing we can do if this fails anyway */ + (void)tcsetattr(fileno(stdin), TCSANOW, &old_flags); + + if (pwd) { + buf[strlen(buf) - 1] = '\0'; + pwd = strdup(buf); + } + + gp_safe_zero(buf, 1024); + return pwd; +} + +int main(int argc, const char *argv[]) +{ + int opt; + poptContext pc; + const char *pc_arg; + const char *opt_type = "NTLMSSP"; + uid_t realuid = getuid(); + long opt_uid = realuid; + int opt_version = 0; + int opt_debug = 0; + char *domain = NULL; + char *username = NULL; + char *password = NULL; + const char *p; + int ret; + + struct poptOption long_options[] = { + POPT_AUTOHELP + { "debug", 'd', POPT_ARG_NONE | POPT_ARGFLAG_SHOW_DEFAULT, + &opt_debug, 0, _("Enable debugging"), NULL }, + { "version", '\0', POPT_ARG_NONE, + &opt_version, 0, _("Print version number and exit"), NULL }, + { "type", 't', POPT_ARG_STRING | POPT_ARGFLAG_SHOW_DEFAULT, + &opt_type, 0, _("Type of credentials"), NULL }, + { "uid", 'u', POPT_ARG_LONG | POPT_ARGFLAG_SHOW_DEFAULT, + &opt_uid, 0, _("UserID owning of the credentials"), NULL }, + POPT_TABLEEND + }; + + pc = poptGetContext(argv[0], argc, argv, long_options, 0); + poptSetOtherOptionHelp(pc, "[[domain:]username]"); + while((opt = poptGetNextOpt(pc)) != -1) { + switch(opt) { + default: + fprintf(stderr, "\nInvalid option %s: %s\n\n", + poptBadOption(pc, 0), poptStrerror(opt)); + poptPrintUsage(pc, stderr, 0); + ret = 1; + goto done; + } + } + pc_arg = poptGetArg(pc); + if (opt_version != 0 && pc_arg != NULL) { + fprintf(stderr, "\nThe version option does not take an argument\n"); + poptPrintUsage(pc, stderr, 0); + ret = 1; + goto done; + } + + if (opt_version) { + puts(VERSION""DISTRO_VERSION""PRERELEASE_VERSION); + ret = 0; + goto done; + } + + if (opt_debug) { + gp_debug_enable(); + } + + if (strcasecmp(opt_type, "NTLMSSP") != 0) { + fprintf(stderr, "\nUnsupported type %s\n", opt_type); + poptPrintUsage(pc, stderr, 0); + ret = 1; + goto done; + } + + /* FIXME: if gssproxy is configure with run_as_user, we'll not use the + * right keyring here. We need to drop privileges like gssproxy does, which + * means reading the gssproxy.conf file. */ + if (geteuid() != 0) { + if (opt_debug && opt_uid == 0) { + fprintf(stderr, "###### This is a test mode ######\n"); + fprintf(stderr, "Please do not use real credentials\n"); + realuid = 0; + } else { + fprintf(stderr, "This program requires root privileges\n"); + return -EPERM; + } + } + + /* only root can specify an arbitrary uid */ + if (realuid != 0) { + opt_uid = realuid; + } + + ret = gp_priv_init(); + if (ret) { + fprintf(stderr, "Failed to initialize security infrastructure\n"); + fprintf(stderr, "Error: (%d) %s\n", ret, gp_strerror(ret)); + return -1; + } + + if (pc_arg == NULL && realuid != 0) { + /* only root can read the credentials back */ + fprintf(stderr, "\nPlease provide a [domain:]username option\n"); + poptPrintUsage(pc, stderr, 0); + ret = 1; + goto done; + } + + if (pc_arg == NULL) { + ret = gp_get_uid_creds(opt_uid, &domain, &username, &password); + if (ret) { + fprintf(stderr, "\nCredentials not found: (%d) %s\n", + ret, gp_strerror(ret)); + ret = ENOENT; + goto done; + } + fprintf(stderr, "\nFound credentials for uid %ld\n", opt_uid); + fprintf(stderr, "Username: %s\n", username); + fprintf(stderr, "Domain: %s\n", domain); + + ret = 0; + goto done; + } + + p = strchr(pc_arg, ':'); + if (p) { + domain = strndup(pc_arg, p - pc_arg); + if (!domain) { + ret = -ENOMEM; + goto done; + } + p += 1; + } else { + p = pc_arg; + } + username = strdup(p); + if (!username) { + ret = -ENOMEM; + goto done; + } + + password = fgets_password(); + if (!password) { + ret = -ENOMEM; + goto done; + } + + ret = gp_save_uid_creds(opt_uid, domain, username, password); + if (ret) { + fprintf(stderr, "\nFailed to save credentials for uid %ld\n", opt_uid); + ret = 1; + goto done; + } + + ret = 0; + +done: + poptFreeContext(pc); + strzerofree(domain); + strzerofree(username); + strzerofree(password); + return ret; +} |