From 4bdacf15524bcdfbb35d7dfdcf2363266eeaaa5a Mon Sep 17 00:00:00 2001
From: Oliver Runge <oliver.runge@gmail.com>
Date: Sun, 10 Mar 2019 17:29:26 +0100
Subject: [PATCH] Add configuration to get password from macOS Keychain

This allows specifying a specific name and account to look up the
appropriate generic password in the macOS Keychain. If it isn't found,
then the Internet password item for the host, account, and
protocol (IMAP) is looked up. If none of those lookups succeed, then
the usual password retrieval strategy is resumed.
---
 configure.ac        | 30 +++++++++++++++++++++++
 src/drv_imap.c      | 60 +++++++++++++++++++++++++++++++++++++++++++--
 src/mbsync.1        | 10 ++++++++
 src/mbsyncrc.sample |  2 +-
 4 files changed, 99 insertions(+), 3 deletions(-)

diff --git a/configure.ac b/configure.ac
index f56ed81..a80a30a 100644
--- a/configure.ac
+++ b/configure.ac
@@ -117,6 +117,30 @@ if test "x$ob_cv_with_ssl" != xno; then
 fi
 AC_SUBST(SSL_LIBS)
 
+dnl macOS Keychain Services (Security Framework)
+AC_ARG_WITH([macos-keychain], [AS_HELP_STRING([--with-macos-keychain],
+    [Support macOS keychain])],
+    [have_macos_keychain=$withval],[have_macos_keychain=yes])
+if test "$have_macos_keychain" != "no"; then
+    AC_CACHE_CHECK([for SecKeychainGetVersion],
+    ac_cv_func_SecKeychainGetVersion,
+    [ac_save_LIBS="$LIBS"
+     LIBS="$LIBS -Wl,-framework -Wl,Security"
+     AC_TRY_LINK([#include <Security/Security.h>],
+        [SecKeychainGetVersion(NULL);],
+        [ac_cv_func_SecKeychainGetVersion=yes],
+        [ac_cv_func_SecKeychainGetVersion=no])
+     LIBS="$ac_save_LIBS"])
+    if test $ac_cv_func_SecKeychainGetVersion = yes; then
+    have_macos_keychain=yes
+    AC_DEFINE([HAVE_MACOS_KEYCHAIN], [1],
+        [Define to 1 if you have the macOS Keychain Services API.])
+    LIBS="$LIBS -Wl,-framework -Wl,Security"
+    else
+    have_macos_keychain=no
+    fi
+fi
+
 have_sasl_paths=
 AC_ARG_WITH(sasl,
   AS_HELP_STRING([--with-sasl[=PATH]], [where to look for SASL [detect]]),
@@ -215,6 +239,12 @@ if test -n "$have_zlib"; then
 else
     AC_MSG_RESULT([Not using zlib])
 fi
+if test -n "$have_macos_keychain"; then
+    AC_MSG_RESULT([Using macOS Keychain])
+else
+    AC_MSG_RESULT([Not using macOS Keychain])
+fi
+
 if test "x$ac_cv_berkdb4" = xyes; then
     AC_MSG_RESULT([Using Berkeley DB])
 else
diff --git a/src/drv_imap.c b/src/drv_imap.c
index 58fc9d3..05cd7ad 100644
--- a/src/drv_imap.c
+++ b/src/drv_imap.c
@@ -41,6 +41,10 @@
 # include <sasl/saslutil.h>
 #endif
 
+#ifdef HAVE_MACOS_KEYCHAIN
+# include <Security/Security.h>
+#endif
+
 #ifdef HAVE_LIBSSL
 enum { SSL_None, SSL_STARTTLS, SSL_IMAPS };
 #endif
@@ -52,6 +56,8 @@ typedef struct imap_server_conf {
 	char *user;
 	char *pass;
 	char *pass_cmd;
+	char *pass_keychain_name;
+	char *pass_keychain_account;
 	int max_in_progress;
 	int cap_mask;
 	string_list_t *auth_mechs;
@@ -1870,7 +1876,50 @@ ensure_password( imap_server_conf_t *srvc )
 {
 	char *cmd = srvc->pass_cmd;
 
-	if (cmd) {
+#ifdef HAVE_MACOS_KEYCHAIN
+	if (srvc->pass_keychain_name && srvc->pass_keychain_account) {
+		void *password_data;
+		UInt32 password_length;
+		if (SecKeychainFindGenericPassword(
+				NULL,
+				strlen(srvc->pass_keychain_name), srvc->pass_keychain_name,
+				strlen(srvc->pass_keychain_account), srvc->pass_keychain_account,
+				&password_length, &password_data,
+				NULL) == noErr) {
+			srvc->pass = nfmalloc((password_length + 1) * sizeof(char));
+			strncpy(srvc->pass, password_data, (size_t)password_length);
+			srvc->pass[password_length] = '\0';
+			SecKeychainItemFreeContent(NULL, password_data);
+		} else {
+			error( "Looking up Keychain item failed: name = %s, account = %s\n",
+				srvc->pass_keychain_name, srvc->pass_keychain_account );
+			return 0;
+		}
+	}
+
+	if (!srvc->pass) {
+		void *password_data;
+		UInt32 password_length;
+		if (SecKeychainFindInternetPassword(
+				NULL,
+				strlen(srvc->sconf.host), srvc->sconf.host,
+				0, NULL,
+				strlen(srvc->user), srvc->user,
+				0, (char *)NULL,
+				0,
+				kSecProtocolTypeIMAP,
+				kSecAuthenticationTypeDefault,
+				&password_length, &password_data,
+				NULL) == noErr) {
+			srvc->pass = nfmalloc((password_length + 1) * sizeof(char));
+			strncpy(srvc->pass, password_data, (size_t)password_length);
+			srvc->pass[password_length] = '\0';
+			SecKeychainItemFreeContent(NULL, password_data);
+		}
+	}
+#endif /* HAVE_MACOS_KEYCHAIN */
+
+	if (!srvc->pass && cmd) {
 		FILE *fp;
 		int ret;
 		char buffer[80];
@@ -1902,7 +1951,9 @@ ensure_password( imap_server_conf_t *srvc )
 		buffer[strcspn( buffer, "\n" )] = 0; /* Strip trailing newline */
 		free( srvc->pass ); /* From previous runs */
 		srvc->pass = nfstrdup( buffer );
-	} else if (!srvc->pass) {
+	}
+
+  if (!srvc->pass) {
 		char *pass, prompt[80];
 
 		flushn();
@@ -1919,6 +1970,7 @@ ensure_password( imap_server_conf_t *srvc )
 		/* getpass() returns a pointer to a static buffer. Make a copy for long term storage. */
 		srvc->pass = nfstrdup( pass );
 	}
+
 	return srvc->pass;
 }
 
@@ -3181,6 +3233,10 @@ imap_parse_store( conffile_t *cfg, store_conf_t **storep )
 			server->pass = nfstrdup( cfg->val );
 		else if (!strcasecmp( "PassCmd", cfg->cmd ))
 			server->pass_cmd = nfstrdup( cfg->val );
+		else if (!strcasecmp( "KeychainName", cfg->cmd ))
+			server->pass_keychain_name = nfstrdup( cfg->val );
+		else if (!strcasecmp( "KeychainAccount", cfg->cmd ))
+			server->pass_keychain_account = nfstrdup( cfg->val );
 		else if (!strcasecmp( "Port", cfg->cmd )) {
 			int port = parse_int( cfg );
 			if ((unsigned)port > 0xffff) {
diff --git a/src/mbsync.1 b/src/mbsync.1
index 4dcd5aa..b49e18c 100644
--- a/src/mbsync.1
+++ b/src/mbsync.1
@@ -333,6 +333,16 @@ Prepend \fB+\fR to the command to indicate that it produces TTY output
 messier output.
 ..
 .TP
+\fBKeychainName\fR \fIname\fR
+Specify a name to look up in the macOS Keychain, requires \fBKeychainAccount\fR
+to be set as well.
+..
+.TP
+\fBKeychainAccount\fR \fIaccount\fR
+Specify the account to look up in the macOS Keychain, requires \fBKeychainName\fR
+to be set as well.
+..
+.TP
 \fBTunnel\fR \fIcommand\fR
 Specify a command to run to establish a connection rather than opening a TCP
 socket.  This allows you to run an IMAP session over an SSH tunnel, for
diff --git a/src/mbsyncrc.sample b/src/mbsyncrc.sample
index ef842fe..abf74e5 100644
--- a/src/mbsyncrc.sample
+++ b/src/mbsyncrc.sample
@@ -21,7 +21,7 @@ Pass xxxxxxxx
 #PassCmd "gpg --quiet --for-your-eyes-only --decrypt $HOME/imappassword.gpg"
 # Fetch password from pwmd (http://pwmd.sourceforge.net/):
 #PassCmd "echo -ne 'GET myIsp\\tpassword' | pwmc datafile"
-# On Mac OS X, run "KeyChain Access" -- File->New Password Item. Fill out form using
+# On macOS, run "KeyChain Access" -- File->New Password Item. Fill out form using
 #  "Keychain Item Name" http://IMAPSERVER  (note: the "http://" is a hack)
 #  "Account Name" USERNAME
 #  "Password" PASSWORD
-- 
2.21.0

