Yoshimasa Niwa
2018-10-21 16:19:15 UTC
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>
---
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);
}
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>
---
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
Yoshimasa Niwa