This patch adds macOS Keychain support to fill specific password option. If Keychain doesn't have a password entry, it will prompt it then save it in Keychain. This patch is squashed commit from https://github.com/niw/openconnect/tree/add_keychain_support Signed-off-by: Yoshimasa Niwa <niw at niw.at> --- Makefile.am | 2 +- configure.ac | 16 +++++++ main.c | 132 ++++++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 143 insertions(+), 7 deletions(-) diff --git a/Makefile.am b/Makefile.am index 522725eb..2e006a90 100644 --- a/Makefile.am +++ b/Makefile.am @@ -22,7 +22,7 @@ AM_CPPFLAGS = -DLOCALEDIR="\"$(localedir)\"" openconnect_SOURCES = xml.c main.c openconnect_CFLAGS = $(AM_CFLAGS) $(SSL_CFLAGS) $(DTLS_SSL_CFLAGS) $(LIBXML2_CFLAGS) $(LIBPROXY_CFLAGS) $(ZLIB_CFLAGS) $(LIBSTOKEN_CFLAGS) $(LIBPSKC_CFLAGS) $(GSSAPI_CFLAGS) $(INTL_CFLAGS) $(ICONV_CFLAGS) $(LIBPCSCLITE_CFLAGS) -openconnect_LDADD = libopenconnect.la $(SSL_LIBS) $(LIBXML2_LIBS) $(LIBPROXY_LIBS) $(INTL_LIBS) $(ICONV_LIBS) +openconnect_LDADD = libopenconnect.la $(SSL_LIBS) $(LIBXML2_LIBS) $(LIBPROXY_LIBS) $(INTL_LIBS) $(ICONV_LIBS) $(KEYCHAIN_LIBS) if OPENCONNECT_WIN32 openconnect_SOURCES += openconnect.rc diff --git a/configure.ac b/configure.ac index 5065a298..3c4cb83a 100644 --- a/configure.ac +++ b/configure.ac @@ -204,6 +204,21 @@ AC_CHECK_FUNC(__android_log_vprint, [], AC_CHECK_LIB(log, __android_log_vprint, AC_ENABLE_SHARED AC_DISABLE_STATIC +keychain_support=no +AC_ARG_ENABLE([keychain], + AS_HELP_STRING([--enable-keychain], [Enable Keychain support]), + [ENABLE_KEYCHAIN=$enableval], + [ENABLE_KEYCHAIN=no]) +if test "$ENABLE_KEYCHAIN" = "yes"; then + AC_CHECK_HEADER([CoreFoundation/CoreFoundation.h], + [], [AC_MSG_ERROR(Cannot find CoreFoundaation header.)]) + AC_CHECK_HEADER([Security/Security.h], + [], [AC_MSG_ERROR(Cannot find Security header.)]) + AC_DEFINE([ENABLE_KEYCHAIN], 1, [Enable Keychain support]) + keychain_support=yes + AC_SUBST(KEYCHAIN_LIBS, ["-framework Foundation -framework Security"]) +fi + AC_CHECK_FUNC(nl_langinfo, [AC_DEFINE(HAVE_NL_LANGINFO, 1, [Have nl_langinfo() function])], []) if test "$ac_cv_func_nl_langinfo" = "yes"; then @@ -1042,6 +1057,7 @@ SUMMARY([Java bindings], [$with_java]) SUMMARY([Build docs], [$build_www]) SUMMARY([Unit tests], [$have_cwrap]) SUMMARY([Net namespace tests], [$have_netns]) +SUMMARY([Keychain support], [$keychain_support]) if test "$ssl_library" = "OpenSSL"; then AC_MSG_WARN([[ diff --git a/main.c b/main.c index 2e9e3059..ef87a2a7 100644 --- a/main.c +++ b/main.c @@ -62,6 +62,11 @@ static const char *legacy_charset; #endif +#if ENABLE_KEYCHAIN +#include <CoreFoundation/CoreFoundation.h> +#include <Security/Security.h> +#endif + static int write_new_config(void *_vpninfo, const char *buf, int buflen); static void __attribute__ ((format(printf, 3, 4))) @@ -85,6 +90,7 @@ static int do_passphrase_from_fsid; static int non_inter; static int cookieonly; static int allow_stdin_read; +static char *keychain_opt_name = NULL; static char *token_filename; static char *server_cert = NULL; @@ -171,6 +177,7 @@ enum { OPT_NO_XMLPOST, OPT_PIDFILE, OPT_PASSWORD_ON_STDIN, + OPT_USE_KEYCHAIN, OPT_PRINTCOOKIE, OPT_RECONNECT_TIMEOUT, OPT_SERVERCERT, @@ -246,6 +253,9 @@ static const struct option long_options[] = { OPTION("xmlconfig", 1, 'x'), OPTION("cookie-on-stdin", 0, OPT_COOKIE_ON_STDIN), OPTION("passwd-on-stdin", 0, OPT_PASSWORD_ON_STDIN), +#if ENABLE_KEYCHAIN + OPTION("use-keychain", 1, OPT_USE_KEYCHAIN), +#endif OPTION("no-passwd", 0, OPT_NO_PASSWD), OPTION("reconnect-timeout", 1, OPT_RECONNECT_TIMEOUT), OPTION("dtls-ciphers", 1, OPT_DTLS_CIPHERS), @@ -813,6 +823,9 @@ static void usage(void) #ifndef HAVE_LIBPCSCLITE printf(" %s\n", _("(NOTE: Yubikey OATH disabled in this build)")); #endif +#if ENABLE_KEYCHAIN + printf(" --use-keychain=NAME %s\n", _("Name of password option to lookup Keychain")); +#endif printf("\n%s:\n", _("Server validation")); printf(" --servercert=FINGERPRINT %s\n", _("Server's certificate SHA1 fingerprint")); @@ -1284,6 +1297,12 @@ int main(int argc, char **argv) read_stdin(&password, 0, 0); allow_stdin_read = 1; break; +#if ENABLE_KEYCHAIN + case OPT_USE_KEYCHAIN: + free(keychain_opt_name); + keychain_opt_name = dup_config_arg(); + break; +#endif case OPT_NO_PASSWD: vpninfo->nopasswd = 1; break; @@ -1946,6 +1965,83 @@ retry: return 0; } +#if ENABLE_KEYCHAIN +static char *lookup_keychain_password(const char *user, const char *prompt, struct openconnect_info *vpninfo) +{ + OSStatus err = 0; + + CFMutableDictionaryRef query = NULL; + CFStringRef account = NULL, server = NULL, path = NULL; + CFTypeRef data = NULL; + char *result = NULL; + + if (verbose > PRG_ERR) { + fprintf(stderr, "Lookup keychain for user: %s url: https://%s%s\n", user, vpninfo->hostname, vpninfo->urlpath); + } + + query = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); + if (!query) goto end; + + account = CFStringCreateWithCString(kCFAllocatorDefault, user, kCFStringEncodingUTF8); + if (!account) goto end; + server = CFStringCreateWithCString(kCFAllocatorDefault, vpninfo->hostname, kCFStringEncodingUTF8); + if (!server) goto end; + path = CFStringCreateWithCString(kCFAllocatorDefault, vpninfo->urlpath, kCFStringEncodingUTF8); + if (!path) goto end; + + CFDictionaryAddValue(query, kSecClass, kSecClassInternetPassword); + CFDictionaryAddValue(query, kSecAttrAccount, account); + CFDictionaryAddValue(query, kSecAttrProtocol, kSecAttrProtocolHTTPS); + CFDictionaryAddValue(query, kSecAttrServer, server); + CFDictionaryAddValue(query, kSecAttrPath, path); + CFDictionaryAddValue(query, kSecMatchLimit, kSecMatchLimitOne); + CFDictionaryAddValue(query, kSecReturnData, kCFBooleanTrue); + + err = SecItemCopyMatching(query, &data); + if (err == errSecItemNotFound) { + if (data) CFRelease(data); + + fprintf(stderr, "Item not found in Keychain\n"); + + result = prompt_for_input(prompt, vpninfo, 1); + if (!result) goto end; + size_t len = strlen(result); + if (len == 0) goto end; + + data = CFDataCreate(kCFAllocatorDefault, (UInt8 *)result, len + 1); + if (!data) goto end; + + CFDictionaryAddValue(query, kSecValueData, data); + CFDictionaryRemoveValue(query, kSecReturnData); + + err = SecItemAdd(query, NULL); + if (err != errSecSuccess) { + if (verbose > PRG_ERR) { + fprintf(stderr, "Fail to add item to Keychain\n"); + } + } + goto end; + } + if (err != errSecSuccess) goto end; + if (!data || CFGetTypeID(data) != CFDataGetTypeID()) goto end; + + CFIndex size = CFDataGetLength(data); + result = malloc((size_t)size); + if (!result) goto end; + + CFDataGetBytes(data, CFRangeMake(0, size), (UInt8 *)result); + +end: + if (query) CFRelease(query); + if (account) CFRelease(account); + if (server) CFRelease(server); + if (path) CFRelease(path); + if (data) CFRelease(data); + + return result; +} +#endif + /* Return value: * < 0, on error * = 0, when form was parsed and POST required @@ -1955,8 +2051,9 @@ static int process_auth_form_cb(void *_vpninfo, struct oc_auth_form *form) { struct openconnect_info *vpninfo = _vpninfo; - struct oc_form_opt *opt; + struct oc_form_opt *opt, *prev_opt; int empty = 1; + char *user; if (form->banner && verbose > PRG_ERR) fprintf(stderr, "%s\n", form->banner); @@ -1981,6 +2078,18 @@ static int process_auth_form_cb(void *_vpninfo, } } + // Reorder `opts` to bring `user` first. + for (prev_opt = NULL, opt = form->opts; opt; prev_opt = opt, opt = opt->next) { + if ((opt->type == OC_FORM_OPT_TEXT) && !strncmp(opt->name, "user", 4)) { + if (prev_opt) { + prev_opt->next = opt->next; + opt->next = form->opts; + form->opts = opt; + } + break; + } + } + for (opt = form->opts; opt; opt = opt->next) { if (opt->flags & OC_FORM_OPT_IGNORE) @@ -1998,10 +2107,14 @@ static int process_auth_form_cb(void *_vpninfo, empty = 0; } else if (opt->type == OC_FORM_OPT_TEXT) { - if (username && - !strncmp(opt->name, "user", 4)) { - opt->_value = username; - username = NULL; + if (!strncmp(opt->name, "user", 4)) { + if (username) { + opt->_value = username; + username = NULL; + } else { + opt->_value = prompt_for_input(opt->label, vpninfo, 0); + } + user = opt->_value; } else { opt->_value = prompt_for_input(opt->label, vpninfo, 0); } @@ -2014,7 +2127,14 @@ static int process_auth_form_cb(void *_vpninfo, if (password) { opt->_value = password; password = NULL; - } else { + } +#if ENABLE_KEYCHAIN + else if (keychain_opt_name && user && !strcmp(opt->name, keychain_opt_name)) { + opt->_value = lookup_keychain_password(user, opt->label, vpninfo); + keychain_opt_name = NULL; + } +#endif + else { opt->_value = prompt_for_input(opt->label, vpninfo, 1); } -- Yoshimasa Niwa