Updated a patch. * Change `--use-keychain` behavior as I suggested in previous email. * Update help texts and man page * Add `--save-to-keychain` option to specify which fields are saved to Keychain. -- >8 -- Subject: [PATCH] Add Keychain support This patch adds macOS Keychain support to fill specific password fields. If Keychain doesn't have a password entry, it will prompt it then save it to Keychain if needed. 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 | 129 ++++++++++++++++++++++++++++++++++++++++- openconnect-internal.h | 5 ++ openconnect.8.in | 23 ++++++++ 5 files changed, 173 insertions(+), 2 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..195cae71 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,8 @@ static int do_passphrase_from_fsid; static int non_inter; static int cookieonly; static int allow_stdin_read; +static char *keychain_account = NULL; +static struct oc_text_list_item *keychain_saving_fields = NULL; static char *token_filename; static char *server_cert = NULL; @@ -171,6 +178,8 @@ enum { OPT_NO_XMLPOST, OPT_PIDFILE, OPT_PASSWORD_ON_STDIN, + OPT_USE_KEYCHAIN, + OPT_SAVE_TO_KEYCHAIN, OPT_PRINTCOOKIE, OPT_RECONNECT_TIMEOUT, OPT_SERVERCERT, @@ -246,6 +255,10 @@ 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), + OPTION("save-to-keychain", 1, OPT_SAVE_TO_KEYCHAIN), +#endif OPTION("no-passwd", 0, OPT_NO_PASSWD), OPTION("reconnect-timeout", 1, OPT_RECONNECT_TIMEOUT), OPTION("dtls-ciphers", 1, OPT_DTLS_CIPHERS), @@ -798,6 +811,10 @@ static void usage(void) printf(" --no-passwd %s\n", _("Disable password/SecurID authentication")); printf(" --non-inter %s\n", _("Do not expect user input; exit if it is required")); printf(" --passwd-on-stdin %s\n", _("Read password from standard input")); +#if ENABLE_KEYCHAIN + printf(" --use-keychain=ACCOUNT %s\n", _("Look up Keychain to fill password form fields")); + printf(" --save-to-keychain=NAME %s\n", _("Name of password form field to be saved to Keychain")); +#endif printf(" --authgroup=GROUP %s\n", _("Choose authentication login selection")); printf(" -c, --certificate=CERT %s\n", _("Use SSL client certificate CERT")); printf(" -k, --sslkey=KEY %s\n", _("Use SSL private key file KEY")); @@ -1284,6 +1301,18 @@ int main(int argc, char **argv) read_stdin(&password, 0, 0); allow_stdin_read = 1; break; +#if ENABLE_KEYCHAIN + case OPT_USE_KEYCHAIN: + keychain_account = keep_config_arg(); + break; + case OPT_SAVE_TO_KEYCHAIN: { + struct oc_text_list_item *field = malloc(sizeof(*field)); + field->data = keep_config_arg(); + field->next = keychain_saving_fields; + keychain_saving_fields = field; + break; + } +#endif case OPT_NO_PASSWD: vpninfo->nopasswd = 1; break; @@ -1946,6 +1975,98 @@ retry: return 0; } +#if ENABLE_KEYCHAIN +static char *lookup_keychain_password(const char *acc, + struct oc_form_opt *opt, + struct openconnect_info *vpninfo) +{ + OSStatus err = 0; + + CFMutableDictionaryRef query = NULL; + CFStringRef account = NULL, name = NULL, key = NULL, label = NULL; + CFTypeRef data = NULL; + char *result = NULL; + + if (verbose > PRG_INFO) + fprintf(stderr, "Lookup keychain for account: %s name: %s\n", acc, opt->name); + + query = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); + if (!query) goto end; + + account = CFStringCreateWithCString(kCFAllocatorDefault, acc, kCFStringEncodingUTF8); + if (!account) goto end; + name = CFStringCreateWithCString(kCFAllocatorDefault, opt->name, kCFStringEncodingUTF8); + if (!name) goto end; + key = CFStringCreateWithFormat(kCFAllocatorDefault, NULL, CFSTR("%@:%@"), account, name); + if (!key) goto end; + + CFDictionaryAddValue(query, kSecClass, kSecClassGenericPassword); + CFDictionaryAddValue(query, kSecAttrService, CFSTR("openconnect")); + CFDictionaryAddValue(query, kSecAttrAccount, key); + CFDictionaryAddValue(query, kSecMatchLimit, kSecMatchLimitOne); + CFDictionaryAddValue(query, kSecReturnData, kCFBooleanTrue); + + err = SecItemCopyMatching(query, &data); + if (err == errSecItemNotFound) { + if (data) CFRelease(data); + + if (verbose > PRG_ERR) + fprintf(stderr, "Item not found in Keychain\n"); + + result = prompt_for_input(opt->label, vpninfo, 1); + if (!result) goto end; + size_t len = strlen(result); + if (len == 0) goto end; + + for (struct oc_text_list_item *field = keychain_saving_fields; field; field = field->next) { + if (strcmp(opt->name, field->data)) + continue; + + label = CFStringCreateWithFormat(kCFAllocatorDefault, NULL, CFSTR("openconnect: %@ (%@)"), account, name); + if (!label) goto end; + data = CFDataCreate(kCFAllocatorDefault, (UInt8 *)result, len + 1); + if (!data) goto end; + + CFDictionaryAddValue(query, kSecAttrLabel, label); + CFDictionaryAddValue(query, kSecValueData, data); + CFDictionaryRemoveValue(query, kSecReturnData); + + err = SecItemAdd(query, NULL); + if (err != errSecSuccess) { + if (verbose > PRG_ERR) + fprintf(stderr, "Failed to add item to Keychain error: %d\n", err); + } else { + if (verbose > PRG_INFO) + fprintf(stderr, "Item saved in Keychain\n"); + } + goto end; + } + goto end; + } else if (err != errSecSuccess) { + if (verbose > PRG_ERR) + fprintf(stderr, "Failed to find item in Keychain error: %d\n", err); + 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 (name) CFRelease(name); + if (key) CFRelease(key); + if (label) CFRelease(label); + if (data) CFRelease(data); + + return result; +} +#endif + /* Return value: * < 0, on error * = 0, when form was parsed and POST required @@ -2014,7 +2135,13 @@ static int process_auth_form_cb(void *_vpninfo, if (password) { opt->_value = password; password = NULL; - } else { + } +#if ENABLE_KEYCHAIN + else if (keychain_account) { + opt->_value = lookup_keychain_password(keychain_account, opt, vpninfo); + } +#endif + else { opt->_value = prompt_for_input(opt->label, vpninfo, 1); } diff --git a/openconnect-internal.h b/openconnect-internal.h index 8aa8fc89..c6b8686e 100644 --- a/openconnect-internal.h +++ b/openconnect-internal.h @@ -197,6 +197,11 @@ struct oc_text_buf { int error; }; +struct oc_text_list_item { + char *data; + struct oc_text_list_item *next; +}; + #define TLS_MASTER_KEY_SIZE 48 #define RECONNECT_INTERVAL_MIN 10 diff --git a/openconnect.8.in b/openconnect.8.in index 37a33d0c..437d374f 100644 --- a/openconnect.8.in +++ b/openconnect.8.in @@ -57,6 +57,8 @@ openconnect \- Multi-protocol VPN client, for Cisco AnyConnect VPNs and others .OP \-\-no\-xmlpost .OP \-\-non\-inter .OP \-\-passwd\-on\-stdin +.OP \-\-use\-keychain string +.OP \-\-save\-to\-keychain string .OP \-\-protocol proto .OP \-\-token\-mode mode .OP \-\-token\-secret {secret\fR[\fI,counter\fR]|@\fIfile\fR} @@ -426,6 +428,27 @@ Do not expect user input; exit if it is required. .B \-\-passwd\-on\-stdin Read password from standard input .TP +.B \-\-use\-keychain=ACCOUNT +Look up Keychain to fill password form field. +.I ACCOUNT +is a base name of Keychain items. For example, if +.I ACCOUNT +is "companyvpn", it looks up Keychain item named "companyvpn:token" for +"token" password form field. +.TP +.B \-\-save\-to\-keychain=NAME +Name of password form field to be saved to Keychain. +.I \-\-use\-keychain +option is required. +For example, if +.I \-\-use\-keychain +options's +.I ACCOUNT +is "companyvpn" and +.I NAME +is "token", it saves input value to Keychain item named "companyvpn:token" for +"token" password form field. +.TP .B \-\-protocol=PROTO Select VPN protocol .I PROTO -- Yoshimasa Niwa