[PATCH] Add Keychain support

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



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




[Index of Archives]     [Linux Samsung SoC]     [Linux Rockchip SoC]     [Linux Actions SoC]     [Linux for Synopsys ARC Processors]     [Linux NFS]     [Linux NILFS]     [Linux USB Devel]     [Video for Linux]     [Linux Audio Users]     [Yosemite News]     [Linux Kernel]     [Linux SCSI]


  Powered by Linux