Browse Source

add SASL support

patch initially by Jack Stone <jwjstone@fastmail.fm>,
cleaned up by Jan Synacek <jsynacek@redhat.com>,
... and then almost completely rewritten by me. ^^
Oswald Buddenhagen 11 years ago
parent
commit
eb1005151c
7 changed files with 287 additions and 15 deletions
  1. 1 1
      NEWS
  2. 1 1
      README
  3. 0 4
      TODO
  4. 48 6
      configure.ac
  5. 1 1
      src/Makefile.am
  6. 235 0
      src/drv_imap.c
  7. 1 2
      src/mbsync.1

+ 1 - 1
NEWS

@@ -6,7 +6,7 @@ The SSL/TLS configuration has been re-designed.
 SSL is now explicitly enabled or disabled - "use SSL if available" is gone.
 SSL is now explicitly enabled or disabled - "use SSL if available" is gone.
 Notice: Tunnels are assumed to be secure and thus default to no SSL.
 Notice: Tunnels are assumed to be secure and thus default to no SSL.
 
 
-More flexible configuration of the used authentication mechanism.
+Support for SASL (flexible authentication) has been added.
 
 
 [1.1.0]
 [1.1.0]
 
 

+ 1 - 1
README

@@ -32,7 +32,7 @@ isync executable still exists; it is a compatibility wrapper around mbsync.
     * Trash functionality: backup messages before removing them
     * Trash functionality: backup messages before removing them
     * IMAP features:
     * IMAP features:
 	* Supports TLS/SSL via imaps: (port 993) and STARTTLS (RFC2595)
 	* Supports TLS/SSL via imaps: (port 993) and STARTTLS (RFC2595)
-	* Supports CRAM-MD5 (RFC2195) for authentication
+	* Supports SASL (RFC4422) for authentication
 	* Supports NAMESPACE (RFC2342) for simplified configuration
 	* Supports NAMESPACE (RFC2342) for simplified configuration
 	* Pipelining for maximum speed
 	* Pipelining for maximum speed
 
 

+ 0 - 4
TODO

@@ -6,10 +6,6 @@ real transactions would be certainly not particularly useful ...
 make sync_chans() aware of servers, so a bad server (e.g., wrong password)
 make sync_chans() aware of servers, so a bad server (e.g., wrong password)
 won't cause the same error message for every attached store.
 won't cause the same error message for every attached store.
 
 
-add support for more authentication methods: oauth, ntlm, ... use SASL?
-possibly by calling an external command. that might be overkill, and
-wouldn't be very user-friendly, though.
-
 make SSL (connect) timeouts produce a bit more than "Unidentified socket error".
 make SSL (connect) timeouts produce a bit more than "Unidentified socket error".
 
 
 network timeout handling in general would be a good idea.
 network timeout handling in general would be a good idea.

+ 48 - 6
configure.ac

@@ -100,6 +100,45 @@ if test "x$ob_cv_with_ssl" != xno; then
 fi
 fi
 AC_SUBST(SSL_LIBS)
 AC_SUBST(SSL_LIBS)
 
 
+have_sasl_paths=
+AC_ARG_WITH(sasl,
+  AS_HELP_STRING([--with-sasl[=PATH]], [where to look for SASL [detect]]),
+  [ob_cv_with_sasl=$withval])
+if test "x$ob_cv_with_sasl" != xno; then
+  case $ob_cv_with_sasl in
+    ""|yes)
+      dnl FIXME: Try various possible paths here...
+      ;;
+    *)
+      SASL_LDFLAGS=-L$ob_cv_with_sasl/lib$libsuff
+      SASL_CPPFLAGS=-I$ob_cv_with_sasl/include
+      ;;
+  esac
+  if test -z "$have_sasl_paths"; then
+    sav_LDFLAGS=$LDFLAGS
+    LDFLAGS="$LDFLAGS $SASL_LDFLAGS"
+    AC_CHECK_LIB(sasl2, sasl_client_init,
+                 [SASL_LIBS="-lsasl2" have_sasl_paths=yes])
+    LDFLAGS=$sav_LDFLAGS
+  fi
+
+  sav_CPPFLAGS=$CPPFLAGS
+  CPPFLAGS="$CPPFLAGS $SASL_CPPFLAGS"
+  AC_CHECK_HEADER(sasl/sasl.h, , [have_sasl_paths=])
+  CPPFLAGS=$sav_CPPFLAGS
+
+  if test -z "$have_sasl_paths"; then
+    if test -n "$ob_cv_with_sasl"; then
+      AC_MSG_ERROR([SASL libs and/or includes were not found where specified])
+    fi
+  else
+    AC_DEFINE(HAVE_LIBSASL, 1, [if you have the SASL libraries])
+    CPPFLAGS="$CPPFLAGS $SASL_CPPFLAGS"
+    LDFLAGS="$LDFLAGS $SASL_LDFLAGS"
+  fi
+fi
+AC_SUBST(SASL_LIBS)
+
 AC_CACHE_CHECK([for Berkley DB >= 4.2], ac_cv_berkdb4,
 AC_CACHE_CHECK([for Berkley DB >= 4.2], ac_cv_berkdb4,
   [ac_cv_berkdb4=no
   [ac_cv_berkdb4=no
    AC_TRY_LINK([#include <db.h>],
    AC_TRY_LINK([#include <db.h>],
@@ -122,12 +161,15 @@ AM_CONDITIONAL(with_compat, test "x$ob_cv_enable_compat" != xno)
 AC_CONFIG_FILES([Makefile src/Makefile src/compat/Makefile isync.spec])
 AC_CONFIG_FILES([Makefile src/Makefile src/compat/Makefile isync.spec])
 AC_OUTPUT
 AC_OUTPUT
 
 
+AC_MSG_RESULT()
 if test -n "$have_ssl_paths"; then
 if test -n "$have_ssl_paths"; then
-    AC_MSG_RESULT([
-Using SSL
-])
+    AC_MSG_RESULT([Using SSL])
+else
+    AC_MSG_RESULT([Not using SSL])
+fi
+if test -n "$have_sasl_paths"; then
+    AC_MSG_RESULT([Using SASL])
 else
 else
-    AC_MSG_RESULT([
-Not using SSL
-])
+    AC_MSG_RESULT([Not using SASL])
 fi
 fi
+AC_MSG_RESULT()

+ 1 - 1
src/Makefile.am

@@ -6,7 +6,7 @@ SUBDIRS = $(compat_dir)
 bin_PROGRAMS = mbsync mdconvert
 bin_PROGRAMS = mbsync mdconvert
 
 
 mbsync_SOURCES = main.c sync.c config.c util.c socket.c driver.c drv_imap.c drv_maildir.c
 mbsync_SOURCES = main.c sync.c config.c util.c socket.c driver.c drv_imap.c drv_maildir.c
-mbsync_LDADD = -ldb $(SSL_LIBS) $(SOCK_LIBS)
+mbsync_LDADD = -ldb $(SSL_LIBS) $(SOCK_LIBS) $(SASL_LIBS)
 noinst_HEADERS = common.h config.h driver.h sync.h socket.h
 noinst_HEADERS = common.h config.h driver.h sync.h socket.h
 
 
 mdconvert_SOURCES = mdconvert.c
 mdconvert_SOURCES = mdconvert.c

+ 235 - 0
src/drv_imap.c

@@ -36,6 +36,11 @@
 #include <time.h>
 #include <time.h>
 #include <sys/wait.h>
 #include <sys/wait.h>
 
 
+#ifdef HAVE_LIBSASL
+# include <sasl/sasl.h>
+# include <sasl/saslutil.h>
+#endif
+
 #ifdef HAVE_LIBSSL
 #ifdef HAVE_LIBSSL
 enum { SSL_None, SSL_STARTTLS, SSL_IMAPS };
 enum { SSL_None, SSL_STARTTLS, SSL_IMAPS };
 #endif
 #endif
@@ -115,6 +120,10 @@ typedef struct imap_store {
 		void (*imap_cancel)( void *aux );
 		void (*imap_cancel)( void *aux );
 	} callbacks;
 	} callbacks;
 	void *callback_aux;
 	void *callback_aux;
+#ifdef HAVE_LIBSASL
+	sasl_conn_t *sasl;
+	int sasl_cont;
+#endif
 
 
 	conn_t conn; /* this is BIG, so put it last */
 	conn_t conn; /* this is BIG, so put it last */
 } imap_store_t;
 } imap_store_t;
@@ -173,6 +182,9 @@ struct imap_cmd_refcounted {
 
 
 enum CAPABILITY {
 enum CAPABILITY {
 	NOLOGIN = 0,
 	NOLOGIN = 0,
+#ifdef HAVE_LIBSASL
+	SASLIR,
+#endif
 #ifdef HAVE_LIBSSL
 #ifdef HAVE_LIBSSL
 	STARTTLS,
 	STARTTLS,
 #endif
 #endif
@@ -184,6 +196,9 @@ enum CAPABILITY {
 
 
 static const char *cap_list[] = {
 static const char *cap_list[] = {
 	"LOGINDISABLED",
 	"LOGINDISABLED",
+#ifdef HAVE_LIBSASL
+	"SASL-IR",
+#endif
 #ifdef HAVE_LIBSSL
 #ifdef HAVE_LIBSSL
 	"STARTTLS",
 	"STARTTLS",
 #endif
 #endif
@@ -1354,6 +1369,9 @@ imap_cancel_store( store_t *gctx )
 {
 {
 	imap_store_t *ctx = (imap_store_t *)gctx;
 	imap_store_t *ctx = (imap_store_t *)gctx;
 
 
+#ifdef HAVE_LIBSASL
+	sasl_dispose( &ctx->sasl );
+#endif
 	socket_close( &ctx->conn );
 	socket_close( &ctx->conn );
 	cancel_submitted_imap_cmds( ctx );
 	cancel_submitted_imap_cmds( ctx );
 	cancel_pending_imap_cmds( ctx );
 	cancel_pending_imap_cmds( ctx );
@@ -1708,6 +1726,162 @@ ensure_password( imap_server_conf_t *srvc )
 	return srvc->pass;
 	return srvc->pass;
 }
 }
 
 
+#ifdef HAVE_LIBSASL
+
+static sasl_callback_t sasl_callbacks[] = {
+	{ SASL_CB_USER,     NULL, NULL },
+	{ SASL_CB_PASS,     NULL, NULL },
+	{ SASL_CB_LIST_END, NULL, NULL }
+};
+
+static int
+process_sasl_interact( sasl_interact_t *interact, imap_server_conf_t *srvc )
+{
+	const char *val;
+
+	for (;; ++interact) {
+		switch (interact->id) {
+		case SASL_CB_LIST_END:
+			return 0;
+		case SASL_CB_USER:
+			val = ensure_user( srvc );
+			break;
+		case SASL_CB_PASS:
+			val = ensure_password( srvc );
+			break;
+		default:
+			error( "Error: Unknown SASL interaction ID\n" );
+			return -1;
+		}
+		if (!val)
+			return -1;
+		interact->result = val;
+		interact->len = strlen( val );
+	}
+}
+
+static int
+process_sasl_step( imap_store_t *ctx, int rc, const char *in, unsigned in_len,
+                   sasl_interact_t *interact, const char **out, unsigned *out_len )
+{
+	imap_server_conf_t *srvc = ((imap_store_conf_t *)ctx->gen.conf)->server;
+
+	while (rc == SASL_INTERACT) {
+		if (process_sasl_interact( interact, srvc ) < 0)
+			return -1;
+		rc = sasl_client_step( ctx->sasl, in, in_len, &interact, out, out_len );
+	}
+	if (rc == SASL_CONTINUE) {
+		ctx->sasl_cont = 1;
+	} else if (rc == SASL_OK) {
+		ctx->sasl_cont = 0;
+	} else {
+		error( "Error: %s\n", sasl_errdetail( ctx->sasl ) );
+		return -1;
+	}
+	return 0;
+}
+
+static int
+decode_sasl_data( const char *prompt, char **in, unsigned *in_len )
+{
+	if (prompt) {
+		int rc;
+		unsigned prompt_len = strlen( prompt );
+		/* We're decoding, the output will be shorter than prompt_len. */
+		*in = nfmalloc( prompt_len );
+		rc = sasl_decode64( prompt, prompt_len, *in, prompt_len, in_len );
+		if (rc != SASL_OK) {
+			free( *in );
+			error( "Error: SASL(%d): %s\n", rc, sasl_errstring( rc, NULL, NULL ) );
+			return -1;
+		}
+	} else {
+		*in = NULL;
+		*in_len = 0;
+	}
+	return 0;
+}
+
+static int
+encode_sasl_data( const char *out, unsigned out_len, char **enc, unsigned *enc_len )
+{
+	int rc;
+	unsigned enc_len_max = ((out_len + 2) / 3) * 4 + 1;
+	*enc = nfmalloc( enc_len_max );
+	rc = sasl_encode64( out, out_len, *enc, enc_len_max, enc_len );
+	if (rc != SASL_OK) {
+		free( *enc );
+		error( "Error: SASL(%d): %s\n", rc, sasl_errstring( rc, NULL, NULL ) );
+		return -1;
+	}
+	return 0;
+}
+
+static int
+do_sasl_auth( imap_store_t *ctx, struct imap_cmd *cmdp ATTR_UNUSED, const char *prompt )
+{
+	int rc, ret;
+	unsigned in_len, out_len, enc_len;
+	const char *out;
+	char *in, *enc;
+	sasl_interact_t *interact = NULL;
+
+	if (!ctx->sasl_cont) {
+		error( "Error: IMAP wants more steps despite successful SASL authentication.\n" );
+		goto bail;
+	}
+	if (decode_sasl_data( prompt, &in, &in_len ) < 0)
+		goto bail;
+	rc = sasl_client_step( ctx->sasl, in, in_len, &interact, &out, &out_len );
+	ret = process_sasl_step( ctx, rc, in, in_len, interact, &out, &out_len );
+	free( in );
+	if (ret < 0)
+		goto bail;
+
+	if (out) {
+		if (encode_sasl_data( out, out_len, &enc, &enc_len ) < 0)
+			goto bail;
+
+		if (DFlags & VERBOSE) {
+			printf( "%s>+> %s\n", ctx->label, enc );
+			fflush( stdout );
+		}
+
+		if (socket_write( &ctx->conn, enc, enc_len, GiveOwn ) < 0)
+			return -1;
+	} else {
+		if (DFlags & VERBOSE) {
+			printf( "%s>+>\n", ctx->label );
+			fflush( stdout );
+		}
+	}
+	return socket_write( &ctx->conn, "\r\n", 2, KeepOwn );
+
+  bail:
+	imap_open_store_bail( ctx );
+	return -1;
+}
+
+static void
+done_sasl_auth( imap_store_t *ctx, struct imap_cmd *cmd ATTR_UNUSED, int response )
+{
+	if (response == RESP_OK && ctx->sasl_cont) {
+		sasl_interact_t *interact = NULL;
+		const char *out;
+		unsigned out_len;
+		int rc = sasl_client_step( ctx->sasl, NULL, 0, &interact, &out, &out_len );
+		if (process_sasl_step( ctx, rc, NULL, 0, interact, &out, &out_len ) < 0)
+			warn( "Warning: SASL reported failure despite successful IMAP authentication. Ignoring...\n" );
+		else if (out)
+			warn( "Warning: SASL wants more steps despite successful IMAP authentication. Ignoring...\n" );
+	}
+
+	imap_open_store_authenticate2_p2( ctx, NULL, response );
+}
+
+#endif
+
 static void
 static void
 imap_open_store_authenticate2( imap_store_t *ctx )
 imap_open_store_authenticate2( imap_store_t *ctx )
 {
 {
@@ -1718,6 +1892,9 @@ imap_open_store_authenticate2( imap_store_t *ctx )
 	int auth_cram = 0;
 	int auth_cram = 0;
 #endif
 #endif
 	int auth_login = 0;
 	int auth_login = 0;
+#ifdef HAVE_LIBSASL
+	char saslmechs[1024], *saslend = saslmechs;
+#endif
 
 
 	info( "Logging in...\n" );
 	info( "Logging in...\n" );
 	for (mech = srvc->auth_mechs; mech; mech = mech->next) {
 	for (mech = srvc->auth_mechs; mech; mech = mech->next) {
@@ -1734,12 +1911,70 @@ imap_open_store_authenticate2( imap_store_t *ctx )
 					auth_cram = 1;
 					auth_cram = 1;
 #endif
 #endif
 				} else {
 				} else {
+#ifdef HAVE_LIBSASL
+					int len = strlen( cmech->string );
+					if (saslend + len + 2 > saslmechs + sizeof(saslmechs))
+						oob();
+					*saslend++ = ' ';
+					memcpy( saslend, cmech->string, len + 1 );
+					saslend += len;
+#else
 					error( "IMAP error: authentication mechanism %s is not supported\n", cmech->string );
 					error( "IMAP error: authentication mechanism %s is not supported\n", cmech->string );
 					goto bail;
 					goto bail;
+#endif
 				}
 				}
 			}
 			}
 		}
 		}
 	}
 	}
+#ifdef HAVE_LIBSASL
+	if (saslend != saslmechs) {
+		int rc;
+		unsigned out_len = 0;
+		char *enc = NULL;
+		const char *gotmech = NULL, *out = NULL;
+		sasl_interact_t *interact = NULL;
+		struct imap_cmd *cmd;
+		static int sasl_inited;
+
+		if (!sasl_inited) {
+			rc = sasl_client_init( sasl_callbacks );
+			if (rc != SASL_OK) {
+			  saslbail:
+				error( "Error: SASL(%d): %s\n", rc, sasl_errstring( rc, NULL, NULL ) );
+				goto bail;
+			}
+			sasl_inited = 1;
+		}
+
+		rc = sasl_client_new( "imap", srvc->sconf.host, NULL, NULL, NULL, 0, &ctx->sasl );
+		if (rc != SASL_OK) {
+			if (!ctx->sasl)
+				goto saslbail;
+			error( "Error: %s\n", sasl_errdetail( ctx->sasl ) );
+			goto bail;
+		}
+
+		rc = sasl_client_start( ctx->sasl, saslmechs + 1, &interact, CAP(SASLIR) ? &out : NULL, &out_len, &gotmech );
+		if (gotmech)
+			info( "Authenticating with SASL mechanism %s...\n", gotmech );
+		/* Technically, we are supposed to loop over sasl_client_start(),
+		 * but it just calls sasl_client_step() anyway. */
+		if (process_sasl_step( ctx, rc, NULL, 0, interact, CAP(SASLIR) ? &out : NULL, &out_len ) < 0)
+			goto bail;
+		if (out) {
+			if (!out_len)
+				enc = nfstrdup( "=" ); /* A zero-length initial response is encoded as padding. */
+			else if (encode_sasl_data( out, out_len, &enc, NULL ) < 0)
+				goto bail;
+		}
+
+		cmd = new_imap_cmd( sizeof(*cmd) );
+		cmd->param.cont = do_sasl_auth;
+		imap_exec( ctx, cmd, done_sasl_auth, enc ? "AUTHENTICATE %s %s" : "AUTHENTICATE %s", gotmech, enc );
+		free( enc );
+		return;
+	}
+#endif
 #ifdef HAVE_LIBSSL
 #ifdef HAVE_LIBSSL
 	if (auth_cram) {
 	if (auth_cram) {
 		struct imap_cmd *cmd = new_imap_cmd( sizeof(*cmd) );
 		struct imap_cmd *cmd = new_imap_cmd( sizeof(*cmd) );

+ 1 - 2
src/mbsync.1

@@ -281,8 +281,7 @@ the legacy IMAP \fBLOGIN\fR mechanism is known.
 The wildcard \fB*\fR represents all mechanisms that are deemed secure
 The wildcard \fB*\fR represents all mechanisms that are deemed secure
 enough for the current \fBSSLType\fR setting.
 enough for the current \fBSSLType\fR setting.
 The actually used mechanism is the most secure choice from the intersection
 The actually used mechanism is the most secure choice from the intersection
-of this list, the list supplied by the server, and the mechanisms actually
-supported by \fBmbsync\fR (currently only \fBCRAM-MD5\fR and \fBLOGIN\fR).
+of this list, the list supplied by the server, and the installed SASL modules.
 (Default: \fB*\fR)
 (Default: \fB*\fR)
 ..
 ..
 .TP
 .TP