diff options
Diffstat (limited to 'src/auth')
134 files changed, 34501 insertions, 0 deletions
diff --git a/src/auth/Makefile.am b/src/auth/Makefile.am new file mode 100644 index 0000000..9e6200b --- /dev/null +++ b/src/auth/Makefile.am @@ -0,0 +1,301 @@ +noinst_LTLIBRARIES = libpassword.la libauth.la +auth_moduledir = $(moduledir)/auth + +# automake seems to force making this unconditional.. +NOPLUGIN_LDFLAGS = + +if GSSAPI_PLUGIN +GSSAPI_LIB = libmech_gssapi.la +endif + +if LDAP_PLUGIN +LDAP_LIB = libauthdb_ldap.la +endif + +LUA_LIB = +AUTH_LUA_LIBS = +AUTH_LUA_LDADD = + +if HAVE_LUA +if AUTH_LUA_PLUGIN +LUA_LIB += libauthdb_lua.la +else +AUTH_LUA_LIBS += $(LIBDOVECOT_LUA) +AUTH_LUA_LDADD += $(LUA_LIBS) +endif +endif + +auth_module_LTLIBRARIES = \ + $(GSSAPI_LIB) \ + $(LDAP_LIB) \ + $(LUA_LIB) \ + libauthdb_imap.la + +pkglibexecdir = $(libexecdir)/dovecot + +pkglibexec_PROGRAMS = auth checkpassword-reply + +AM_CPPFLAGS = \ + -I$(top_srcdir)/src/lib \ + -I$(top_srcdir)/src/lib-auth \ + -I$(top_srcdir)/src/lib-test \ + -I$(top_srcdir)/src/lib-dict \ + -I$(top_srcdir)/src/lib-dns \ + -I$(top_srcdir)/src/lib-http \ + -I$(top_srcdir)/src/lib-sql \ + -I$(top_srcdir)/src/lib-settings \ + -I$(top_srcdir)/src/lib-old-stats \ + -I$(top_srcdir)/src/lib-otp \ + -I$(top_srcdir)/src/lib-master \ + -I$(top_srcdir)/src/lib-oauth2 \ + -I$(top_srcdir)/src/lib-ssl-iostream \ + -I$(top_srcdir)/src/lib-lua \ + -I$(top_srcdir)/src/lib-dcrypt \ + -DAUTH_MODULE_DIR=\""$(auth_moduledir)"\" \ + -DPKG_LIBEXECDIR=\""$(pkglibexecdir)"\" \ + -DPKG_RUNDIR=\""$(rundir)"\" \ + -DSYSCONFDIR=\""$(sysconfdir)/dovecot"\" \ + $(LUA_CFLAGS) \ + $(AUTH_CFLAGS) + +auth_LDFLAGS = -export-dynamic + +libpassword_la_SOURCES = \ + crypt-blowfish.c \ + mycrypt.c \ + password-scheme.c \ + password-scheme-crypt.c \ + password-scheme-md5crypt.c \ + password-scheme-scram.c \ + password-scheme-otp.c \ + password-scheme-pbkdf2.c \ + password-scheme-sodium.c +libpassword_la_CFLAGS = $(AM_CPPFLAGS) $(LIBSODIUM_CFLAGS) + +auth_libs = \ + libauth.la \ + libstats_auth.la \ + libpassword.la \ + ../lib-otp/libotp.la \ + $(AUTH_LUA_LIBS) \ + $(LIBDOVECOT_SQL) + +auth_CPPFLAGS = $(AM_CPPFLAGS) $(BINARY_CFLAGS) +auth_LDADD = $(auth_libs) $(LIBDOVECOT) $(AUTH_LIBS) $(BINARY_LDFLAGS) $(AUTH_LUA_LDADD) +auth_DEPENDENCIES = $(auth_libs) $(LIBDOVECOT_DEPS) +auth_SOURCES = main.c + +ldap_sources = db-ldap.c passdb-ldap.c userdb-ldap.c +lua_sources = db-lua.c passdb-lua.c userdb-lua.c + +libauth_la_DEPENDENCIES = $(LIBDOVECOT_DEPS) +libauth_la_SOURCES = \ + auth.c \ + auth-cache.c \ + auth-client-connection.c \ + auth-master-connection.c \ + auth-policy.c \ + mech-otp-common.c \ + mech-plain-common.c \ + auth-penalty.c \ + auth-request.c \ + auth-request-fields.c \ + auth-request-handler.c \ + auth-request-stats.c \ + auth-request-var-expand.c \ + auth-settings.c \ + auth-fields.c \ + auth-token.c \ + auth-worker-client.c \ + auth-worker-server.c \ + db-checkpassword.c \ + db-dict.c \ + db-dict-cache-key.c \ + db-oauth2.c \ + db-sql.c \ + db-passwd-file.c \ + mech.c \ + mech-anonymous.c \ + mech-plain.c \ + mech-login.c \ + mech-cram-md5.c \ + mech-digest-md5.c \ + mech-external.c \ + mech-gssapi.c \ + mech-otp.c \ + mech-scram.c \ + mech-apop.c \ + mech-winbind.c \ + mech-dovecot-token.c \ + mech-oauth2.c \ + passdb.c \ + passdb-blocking.c \ + passdb-bsdauth.c \ + passdb-cache.c \ + passdb-checkpassword.c \ + passdb-dict.c \ + passdb-oauth2.c \ + passdb-passwd.c \ + passdb-passwd-file.c \ + passdb-pam.c \ + passdb-shadow.c \ + passdb-sql.c \ + passdb-static.c \ + passdb-template.c \ + userdb.c \ + userdb-blocking.c \ + userdb-checkpassword.c \ + userdb-dict.c \ + userdb-passwd.c \ + userdb-passwd-file.c \ + userdb-prefetch.c \ + userdb-static.c \ + userdb-sql.c \ + userdb-template.c \ + $(ldap_sources) \ + $(lua_sources) + +headers = \ + auth.h \ + auth-cache.h \ + auth-client-connection.h \ + auth-common.h \ + auth-master-connection.h \ + mech-otp-common.h \ + mech-plain-common.h \ + mech-digest-md5-private.h \ + mech-scram.h \ + auth-penalty.h \ + auth-policy.h \ + auth-request.h \ + auth-request-handler.h \ + auth-request-handler-private.h \ + auth-request-stats.h \ + auth-request-var-expand.h \ + auth-settings.h \ + auth-stats.h \ + auth-fields.h \ + auth-token.h \ + auth-worker-client.h \ + auth-worker-server.h \ + db-dict.h \ + db-ldap.h \ + db-sql.h \ + db-passwd-file.h \ + db-checkpassword.h \ + db-oauth2.h \ + mech.h \ + mycrypt.h \ + passdb.h \ + passdb-blocking.h \ + passdb-cache.h \ + passdb-template.h \ + password-scheme.h \ + userdb.h \ + userdb-blocking.h \ + userdb-template.h + +if GSSAPI_PLUGIN +libmech_gssapi_la_LDFLAGS = -module -avoid-version +libmech_gssapi_la_LIBADD = $(KRB5_LIBS) +libmech_gssapi_la_CPPFLAGS = $(AM_CPPFLAGS) $(KRB5_CFLAGS) -DPLUGIN_BUILD +libmech_gssapi_la_SOURCES = mech-gssapi.c +endif + +if LDAP_PLUGIN +libauthdb_ldap_la_LDFLAGS = -module -avoid-version +libauthdb_ldap_la_LIBADD = $(LDAP_LIBS) +libauthdb_ldap_la_CPPFLAGS = $(AM_CPPFLAGS) -DPLUGIN_BUILD +libauthdb_ldap_la_SOURCES = $(ldap_sources) +endif + +if AUTH_LUA_PLUGIN +libauthdb_lua_la_LDFLAGS = -module -avoid-version +libauthdb_lua_la_LIBADD = $(LIBDOVECOT_LUA) +libauthdb_lua_la_CPPFLAGS = $(AM_CPPFLAGS) -DPLUGIN_BUILD +libauthdb_lua_la_SOURCES = $(lua_sources) +endif + +libauthdb_imap_la_LDFLAGS = -module -avoid-version +libauthdb_imap_la_LIBADD = \ + ../lib-imap-client/libimap_client.la \ + $(LIBDOVECOT) +libauthdb_imap_la_CPPFLAGS = \ + $(AM_CPPFLAGS) \ + -I$(top_srcdir)/src/lib-imap \ + -I$(top_srcdir)/src/lib-imap-client +libauthdb_imap_la_SOURCES = passdb-imap.c + +pkginc_libdir=$(pkgincludedir) +pkginc_lib_HEADERS = $(headers) + +checkpassword_reply_CPPFLAGS = $(AM_CPPFLAGS) $(BINARY_CFLAGS) +checkpassword_reply_LDADD = $(LIBDOVECOT) $(BINARY_LDFLAGS) +checkpassword_reply_DEPENDENCIES = $(LIBDOVECOT_DEPS) + +checkpassword_reply_sources = \ + checkpassword-reply.c + +stats_moduledir = $(moduledir)/old-stats +stats_module_LTLIBRARIES = libstats_auth.la + +libstats_auth_la_LDFLAGS = -module -avoid-version +libstats_auth_la_LIBADD = $(LIBDOVECOT) +libstats_auth_la_DEPENDENCIES = $(LIBDOVECOT_DEPS) +libstats_auth_la_SOURCES = auth-stats.c + +test_programs = \ + test-libpassword \ + test-auth-cache \ + test-auth \ + test-mech + +noinst_PROGRAMS = $(test_programs) + +noinst_HEADERS = test-auth.h crypt-blowfish.h db-lua.h + +test_libs = \ + ../lib-dovecot/libdovecot.la + +test_libpassword_SOURCES = test-libpassword.c +test_libpassword_LDADD = \ + libpassword.la \ + ../lib-otp/libotp.la \ + $(CRYPT_LIBS) \ + $(LIBDOVECOT_SQL) \ + $(LIBSODIUM_LIBS) \ + $(test_libs) \ + $(BINARY_LDFLAGS) + +test_libpassword_DEPENDENCIES = libpassword.la +test_libpassword_CPPFLAGS = $(AM_CPPFLAGS) $(BINARY_CFLAGS) + +test_auth_cache_SOURCES = auth-cache.c test-auth-cache.c +test_auth_cache_LDADD = $(test_libs) +test_auth_cache_DEPENDENCIES = $(pkglibexec_PROGRAMS) $(test_libs) +# this is needed to force auth-cache.c recompilation +test_auth_cache_CPPFLAGS = $(AM_CPPFLAGS) + +test_auth_SOURCES = \ + test-auth-request-var-expand.c \ + test-auth-request-fields.c \ + test-username-filter.c \ + test-db-dict.c \ + test-lua.c \ + test-mock.c \ + test-main.c + +test_auth_LDADD = $(test_libs) $(auth_libs) $(AUTH_LIBS) $(LUA_LIBS) +test_auth_DEPENDENCIES = $(pkglibexec_PROGRAMS) $(test_libs) + +test_mech_SOURCES = \ + test-mock.c \ + test-mech.c + +test_mech_LDADD = $(test_libs) $(auth_libs) $(AUTH_LIBS) $(LUA_LIBS) +test_mech_DEPENDENCIES = $(pkglibexec_PROGRAMS) $(test_libs) + +check-local: + for bin in $(test_programs); do \ + if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \ + done diff --git a/src/auth/Makefile.in b/src/auth/Makefile.in new file mode 100644 index 0000000..aa40de5 --- /dev/null +++ b/src/auth/Makefile.in @@ -0,0 +1,1928 @@ +# Makefile.in generated by automake 1.16.1 from Makefile.am. +# @configure_input@ + +# Copyright (C) 1994-2018 Free Software Foundation, Inc. + +# This Makefile.in is free software; the Free Software Foundation +# gives unlimited permission to copy and/or distribute it, +# with or without modifications, as long as this notice is preserved. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY, to the extent permitted by law; without +# even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. + +@SET_MAKE@ + + + +VPATH = @srcdir@ +am__is_gnu_make = { \ + if test -z '$(MAKELEVEL)'; then \ + false; \ + elif test -n '$(MAKE_HOST)'; then \ + true; \ + elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \ + true; \ + else \ + false; \ + fi; \ +} +am__make_running_with_option = \ + case $${target_option-} in \ + ?) ;; \ + *) echo "am__make_running_with_option: internal error: invalid" \ + "target option '$${target_option-}' specified" >&2; \ + exit 1;; \ + esac; \ + has_opt=no; \ + sane_makeflags=$$MAKEFLAGS; \ + if $(am__is_gnu_make); then \ + sane_makeflags=$$MFLAGS; \ + else \ + case $$MAKEFLAGS in \ + *\\[\ \ ]*) \ + bs=\\; \ + sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \ + | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \ + esac; \ + fi; \ + skip_next=no; \ + strip_trailopt () \ + { \ + flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \ + }; \ + for flg in $$sane_makeflags; do \ + test $$skip_next = yes && { skip_next=no; continue; }; \ + case $$flg in \ + *=*|--*) continue;; \ + -*I) strip_trailopt 'I'; skip_next=yes;; \ + -*I?*) strip_trailopt 'I';; \ + -*O) strip_trailopt 'O'; skip_next=yes;; \ + -*O?*) strip_trailopt 'O';; \ + -*l) strip_trailopt 'l'; skip_next=yes;; \ + -*l?*) strip_trailopt 'l';; \ + -[dEDm]) skip_next=yes;; \ + -[JT]) skip_next=yes;; \ + esac; \ + case $$flg in \ + *$$target_option*) has_opt=yes; break;; \ + esac; \ + done; \ + test $$has_opt = yes +am__make_dryrun = (target_option=n; $(am__make_running_with_option)) +am__make_keepgoing = (target_option=k; $(am__make_running_with_option)) +pkgdatadir = $(datadir)/@PACKAGE@ +pkgincludedir = $(includedir)/@PACKAGE@ +pkglibdir = $(libdir)/@PACKAGE@ +am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd +install_sh_DATA = $(install_sh) -c -m 644 +install_sh_PROGRAM = $(install_sh) -c +install_sh_SCRIPT = $(install_sh) -c +INSTALL_HEADER = $(INSTALL_DATA) +transform = $(program_transform_name) +NORMAL_INSTALL = : +PRE_INSTALL = : +POST_INSTALL = : +NORMAL_UNINSTALL = : +PRE_UNINSTALL = : +POST_UNINSTALL = : +build_triplet = @build@ +host_triplet = @host@ +@AUTH_LUA_PLUGIN_TRUE@@HAVE_LUA_TRUE@am__append_1 = libauthdb_lua.la +@AUTH_LUA_PLUGIN_FALSE@@HAVE_LUA_TRUE@am__append_2 = $(LIBDOVECOT_LUA) +@AUTH_LUA_PLUGIN_FALSE@@HAVE_LUA_TRUE@am__append_3 = $(LUA_LIBS) +pkglibexec_PROGRAMS = auth$(EXEEXT) checkpassword-reply$(EXEEXT) +noinst_PROGRAMS = $(am__EXEEXT_1) +subdir = src/auth +ACLOCAL_M4 = $(top_srcdir)/aclocal.m4 +am__aclocal_m4_deps = $(top_srcdir)/m4/ac_checktype2.m4 \ + $(top_srcdir)/m4/ac_typeof.m4 $(top_srcdir)/m4/arc4random.m4 \ + $(top_srcdir)/m4/blockdev.m4 $(top_srcdir)/m4/c99_vsnprintf.m4 \ + $(top_srcdir)/m4/clock_gettime.m4 $(top_srcdir)/m4/crypt.m4 \ + $(top_srcdir)/m4/crypt_xpg6.m4 $(top_srcdir)/m4/dbqlk.m4 \ + $(top_srcdir)/m4/dirent_dtype.m4 $(top_srcdir)/m4/dovecot.m4 \ + $(top_srcdir)/m4/fd_passing.m4 $(top_srcdir)/m4/fdatasync.m4 \ + $(top_srcdir)/m4/flexible_array_member.m4 \ + $(top_srcdir)/m4/glibc.m4 $(top_srcdir)/m4/gmtime_max.m4 \ + $(top_srcdir)/m4/gmtime_tm_gmtoff.m4 \ + $(top_srcdir)/m4/ioloop.m4 $(top_srcdir)/m4/iovec.m4 \ + $(top_srcdir)/m4/ipv6.m4 $(top_srcdir)/m4/libcap.m4 \ + $(top_srcdir)/m4/libtool.m4 $(top_srcdir)/m4/libwrap.m4 \ + $(top_srcdir)/m4/linux_mremap.m4 $(top_srcdir)/m4/ltoptions.m4 \ + $(top_srcdir)/m4/ltsugar.m4 $(top_srcdir)/m4/ltversion.m4 \ + $(top_srcdir)/m4/lt~obsolete.m4 $(top_srcdir)/m4/mmap_write.m4 \ + $(top_srcdir)/m4/mntctl.m4 $(top_srcdir)/m4/modules.m4 \ + $(top_srcdir)/m4/notify.m4 $(top_srcdir)/m4/nsl.m4 \ + $(top_srcdir)/m4/off_t_max.m4 $(top_srcdir)/m4/pkg.m4 \ + $(top_srcdir)/m4/pr_set_dumpable.m4 \ + $(top_srcdir)/m4/q_quotactl.m4 $(top_srcdir)/m4/quota.m4 \ + $(top_srcdir)/m4/random.m4 $(top_srcdir)/m4/rlimit.m4 \ + $(top_srcdir)/m4/sendfile.m4 $(top_srcdir)/m4/size_t_signed.m4 \ + $(top_srcdir)/m4/sockpeercred.m4 $(top_srcdir)/m4/sql.m4 \ + $(top_srcdir)/m4/ssl.m4 $(top_srcdir)/m4/st_tim.m4 \ + $(top_srcdir)/m4/static_array.m4 $(top_srcdir)/m4/test_with.m4 \ + $(top_srcdir)/m4/time_t.m4 $(top_srcdir)/m4/typeof.m4 \ + $(top_srcdir)/m4/typeof_dev_t.m4 \ + $(top_srcdir)/m4/uoff_t_max.m4 $(top_srcdir)/m4/vararg.m4 \ + $(top_srcdir)/m4/want_apparmor.m4 \ + $(top_srcdir)/m4/want_bsdauth.m4 \ + $(top_srcdir)/m4/want_bzlib.m4 \ + $(top_srcdir)/m4/want_cassandra.m4 \ + $(top_srcdir)/m4/want_cdb.m4 \ + $(top_srcdir)/m4/want_checkpassword.m4 \ + $(top_srcdir)/m4/want_clucene.m4 $(top_srcdir)/m4/want_db.m4 \ + $(top_srcdir)/m4/want_gssapi.m4 $(top_srcdir)/m4/want_icu.m4 \ + $(top_srcdir)/m4/want_ldap.m4 $(top_srcdir)/m4/want_lua.m4 \ + $(top_srcdir)/m4/want_lz4.m4 $(top_srcdir)/m4/want_lzma.m4 \ + $(top_srcdir)/m4/want_mysql.m4 $(top_srcdir)/m4/want_pam.m4 \ + $(top_srcdir)/m4/want_passwd.m4 $(top_srcdir)/m4/want_pgsql.m4 \ + $(top_srcdir)/m4/want_prefetch.m4 \ + $(top_srcdir)/m4/want_shadow.m4 \ + $(top_srcdir)/m4/want_sodium.m4 $(top_srcdir)/m4/want_solr.m4 \ + $(top_srcdir)/m4/want_sqlite.m4 \ + $(top_srcdir)/m4/want_stemmer.m4 \ + $(top_srcdir)/m4/want_systemd.m4 \ + $(top_srcdir)/m4/want_textcat.m4 \ + $(top_srcdir)/m4/want_unwind.m4 $(top_srcdir)/m4/want_zlib.m4 \ + $(top_srcdir)/m4/want_zstd.m4 $(top_srcdir)/configure.ac +am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \ + $(ACLOCAL_M4) +DIST_COMMON = $(srcdir)/Makefile.am $(noinst_HEADERS) \ + $(pkginc_lib_HEADERS) $(am__DIST_COMMON) +mkinstalldirs = $(install_sh) -d +CONFIG_HEADER = $(top_builddir)/config.h +CONFIG_CLEAN_FILES = +CONFIG_CLEAN_VPATH_FILES = +am__EXEEXT_1 = test-libpassword$(EXEEXT) test-auth-cache$(EXEEXT) \ + test-auth$(EXEEXT) test-mech$(EXEEXT) +am__installdirs = "$(DESTDIR)$(pkglibexecdir)" \ + "$(DESTDIR)$(auth_moduledir)" "$(DESTDIR)$(stats_moduledir)" \ + "$(DESTDIR)$(pkginc_libdir)" +PROGRAMS = $(noinst_PROGRAMS) $(pkglibexec_PROGRAMS) +am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`; +am__vpath_adj = case $$p in \ + $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \ + *) f=$$p;; \ + esac; +am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`; +am__install_max = 40 +am__nobase_strip_setup = \ + srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'` +am__nobase_strip = \ + for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||" +am__nobase_list = $(am__nobase_strip_setup); \ + for p in $$list; do echo "$$p $$p"; done | \ + sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \ + $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \ + if (++n[$$2] == $(am__install_max)) \ + { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \ + END { for (dir in files) print dir, files[dir] }' +am__base_list = \ + sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \ + sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g' +am__uninstall_files_from_dir = { \ + test -z "$$files" \ + || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \ + || { echo " ( cd '$$dir' && rm -f" $$files ")"; \ + $(am__cd) "$$dir" && rm -f $$files; }; \ + } +LTLIBRARIES = $(auth_module_LTLIBRARIES) $(noinst_LTLIBRARIES) \ + $(stats_module_LTLIBRARIES) +libauth_la_LIBADD = +am__objects_1 = db-ldap.lo passdb-ldap.lo userdb-ldap.lo +am__objects_2 = db-lua.lo passdb-lua.lo userdb-lua.lo +am_libauth_la_OBJECTS = auth.lo auth-cache.lo \ + auth-client-connection.lo auth-master-connection.lo \ + auth-policy.lo mech-otp-common.lo mech-plain-common.lo \ + auth-penalty.lo auth-request.lo auth-request-fields.lo \ + auth-request-handler.lo auth-request-stats.lo \ + auth-request-var-expand.lo auth-settings.lo auth-fields.lo \ + auth-token.lo auth-worker-client.lo auth-worker-server.lo \ + db-checkpassword.lo db-dict.lo db-dict-cache-key.lo \ + db-oauth2.lo db-sql.lo db-passwd-file.lo mech.lo \ + mech-anonymous.lo mech-plain.lo mech-login.lo mech-cram-md5.lo \ + mech-digest-md5.lo mech-external.lo mech-gssapi.lo mech-otp.lo \ + mech-scram.lo mech-apop.lo mech-winbind.lo \ + mech-dovecot-token.lo mech-oauth2.lo passdb.lo \ + passdb-blocking.lo passdb-bsdauth.lo passdb-cache.lo \ + passdb-checkpassword.lo passdb-dict.lo passdb-oauth2.lo \ + passdb-passwd.lo passdb-passwd-file.lo passdb-pam.lo \ + passdb-shadow.lo passdb-sql.lo passdb-static.lo \ + passdb-template.lo userdb.lo userdb-blocking.lo \ + userdb-checkpassword.lo userdb-dict.lo userdb-passwd.lo \ + userdb-passwd-file.lo userdb-prefetch.lo userdb-static.lo \ + userdb-sql.lo userdb-template.lo $(am__objects_1) \ + $(am__objects_2) +libauth_la_OBJECTS = $(am_libauth_la_OBJECTS) +AM_V_lt = $(am__v_lt_@AM_V@) +am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@) +am__v_lt_0 = --silent +am__v_lt_1 = +am__DEPENDENCIES_1 = +libauthdb_imap_la_DEPENDENCIES = ../lib-imap-client/libimap_client.la \ + $(am__DEPENDENCIES_1) +am_libauthdb_imap_la_OBJECTS = libauthdb_imap_la-passdb-imap.lo +libauthdb_imap_la_OBJECTS = $(am_libauthdb_imap_la_OBJECTS) +libauthdb_imap_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \ + $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \ + $(AM_CFLAGS) $(CFLAGS) $(libauthdb_imap_la_LDFLAGS) $(LDFLAGS) \ + -o $@ +@LDAP_PLUGIN_TRUE@libauthdb_ldap_la_DEPENDENCIES = \ +@LDAP_PLUGIN_TRUE@ $(am__DEPENDENCIES_1) +am__libauthdb_ldap_la_SOURCES_DIST = db-ldap.c passdb-ldap.c \ + userdb-ldap.c +am__objects_3 = libauthdb_ldap_la-db-ldap.lo \ + libauthdb_ldap_la-passdb-ldap.lo \ + libauthdb_ldap_la-userdb-ldap.lo +@LDAP_PLUGIN_TRUE@am_libauthdb_ldap_la_OBJECTS = $(am__objects_3) +libauthdb_ldap_la_OBJECTS = $(am_libauthdb_ldap_la_OBJECTS) +libauthdb_ldap_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \ + $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \ + $(AM_CFLAGS) $(CFLAGS) $(libauthdb_ldap_la_LDFLAGS) $(LDFLAGS) \ + -o $@ +@LDAP_PLUGIN_TRUE@am_libauthdb_ldap_la_rpath = -rpath \ +@LDAP_PLUGIN_TRUE@ $(auth_moduledir) +@AUTH_LUA_PLUGIN_TRUE@libauthdb_lua_la_DEPENDENCIES = \ +@AUTH_LUA_PLUGIN_TRUE@ $(am__DEPENDENCIES_1) +am__libauthdb_lua_la_SOURCES_DIST = db-lua.c passdb-lua.c userdb-lua.c +am__objects_4 = libauthdb_lua_la-db-lua.lo \ + libauthdb_lua_la-passdb-lua.lo libauthdb_lua_la-userdb-lua.lo +@AUTH_LUA_PLUGIN_TRUE@am_libauthdb_lua_la_OBJECTS = $(am__objects_4) +libauthdb_lua_la_OBJECTS = $(am_libauthdb_lua_la_OBJECTS) +libauthdb_lua_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \ + $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \ + $(AM_CFLAGS) $(CFLAGS) $(libauthdb_lua_la_LDFLAGS) $(LDFLAGS) \ + -o $@ +@AUTH_LUA_PLUGIN_TRUE@@HAVE_LUA_TRUE@am_libauthdb_lua_la_rpath = \ +@AUTH_LUA_PLUGIN_TRUE@@HAVE_LUA_TRUE@ -rpath $(auth_moduledir) +@GSSAPI_PLUGIN_TRUE@libmech_gssapi_la_DEPENDENCIES = \ +@GSSAPI_PLUGIN_TRUE@ $(am__DEPENDENCIES_1) +am__libmech_gssapi_la_SOURCES_DIST = mech-gssapi.c +@GSSAPI_PLUGIN_TRUE@am_libmech_gssapi_la_OBJECTS = \ +@GSSAPI_PLUGIN_TRUE@ libmech_gssapi_la-mech-gssapi.lo +libmech_gssapi_la_OBJECTS = $(am_libmech_gssapi_la_OBJECTS) +libmech_gssapi_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \ + $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \ + $(AM_CFLAGS) $(CFLAGS) $(libmech_gssapi_la_LDFLAGS) $(LDFLAGS) \ + -o $@ +@GSSAPI_PLUGIN_TRUE@am_libmech_gssapi_la_rpath = -rpath \ +@GSSAPI_PLUGIN_TRUE@ $(auth_moduledir) +libpassword_la_LIBADD = +am_libpassword_la_OBJECTS = libpassword_la-crypt-blowfish.lo \ + libpassword_la-mycrypt.lo libpassword_la-password-scheme.lo \ + libpassword_la-password-scheme-crypt.lo \ + libpassword_la-password-scheme-md5crypt.lo \ + libpassword_la-password-scheme-scram.lo \ + libpassword_la-password-scheme-otp.lo \ + libpassword_la-password-scheme-pbkdf2.lo \ + libpassword_la-password-scheme-sodium.lo +libpassword_la_OBJECTS = $(am_libpassword_la_OBJECTS) +libpassword_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \ + $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \ + $(libpassword_la_CFLAGS) $(CFLAGS) $(AM_LDFLAGS) $(LDFLAGS) -o \ + $@ +am_libstats_auth_la_OBJECTS = auth-stats.lo +libstats_auth_la_OBJECTS = $(am_libstats_auth_la_OBJECTS) +libstats_auth_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \ + $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \ + $(AM_CFLAGS) $(CFLAGS) $(libstats_auth_la_LDFLAGS) $(LDFLAGS) \ + -o $@ +am_auth_OBJECTS = auth-main.$(OBJEXT) +auth_OBJECTS = $(am_auth_OBJECTS) +@AUTH_LUA_PLUGIN_FALSE@@HAVE_LUA_TRUE@am__DEPENDENCIES_2 = \ +@AUTH_LUA_PLUGIN_FALSE@@HAVE_LUA_TRUE@ $(am__DEPENDENCIES_1) +am__DEPENDENCIES_3 = $(am__DEPENDENCIES_2) +am__DEPENDENCIES_4 = libauth.la libstats_auth.la libpassword.la \ + ../lib-otp/libotp.la $(am__DEPENDENCIES_3) \ + $(am__DEPENDENCIES_1) +auth_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \ + $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \ + $(auth_LDFLAGS) $(LDFLAGS) -o $@ +checkpassword_reply_SOURCES = checkpassword-reply.c +checkpassword_reply_OBJECTS = \ + checkpassword_reply-checkpassword-reply.$(OBJEXT) +am_test_auth_OBJECTS = test-auth-request-var-expand.$(OBJEXT) \ + test-auth-request-fields.$(OBJEXT) \ + test-username-filter.$(OBJEXT) test-db-dict.$(OBJEXT) \ + test-lua.$(OBJEXT) test-mock.$(OBJEXT) test-main.$(OBJEXT) +test_auth_OBJECTS = $(am_test_auth_OBJECTS) +am_test_auth_cache_OBJECTS = test_auth_cache-auth-cache.$(OBJEXT) \ + test_auth_cache-test-auth-cache.$(OBJEXT) +test_auth_cache_OBJECTS = $(am_test_auth_cache_OBJECTS) +am_test_libpassword_OBJECTS = \ + test_libpassword-test-libpassword.$(OBJEXT) +test_libpassword_OBJECTS = $(am_test_libpassword_OBJECTS) +am_test_mech_OBJECTS = test-mock.$(OBJEXT) test-mech.$(OBJEXT) +test_mech_OBJECTS = $(am_test_mech_OBJECTS) +AM_V_P = $(am__v_P_@AM_V@) +am__v_P_ = $(am__v_P_@AM_DEFAULT_V@) +am__v_P_0 = false +am__v_P_1 = : +AM_V_GEN = $(am__v_GEN_@AM_V@) +am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@) +am__v_GEN_0 = @echo " GEN " $@; +am__v_GEN_1 = +AM_V_at = $(am__v_at_@AM_V@) +am__v_at_ = $(am__v_at_@AM_DEFAULT_V@) +am__v_at_0 = @ +am__v_at_1 = +DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir) +depcomp = $(SHELL) $(top_srcdir)/depcomp +am__maybe_remake_depfiles = depfiles +am__depfiles_remade = ./$(DEPDIR)/auth-cache.Plo \ + ./$(DEPDIR)/auth-client-connection.Plo \ + ./$(DEPDIR)/auth-fields.Plo ./$(DEPDIR)/auth-main.Po \ + ./$(DEPDIR)/auth-master-connection.Plo \ + ./$(DEPDIR)/auth-penalty.Plo ./$(DEPDIR)/auth-policy.Plo \ + ./$(DEPDIR)/auth-request-fields.Plo \ + ./$(DEPDIR)/auth-request-handler.Plo \ + ./$(DEPDIR)/auth-request-stats.Plo \ + ./$(DEPDIR)/auth-request-var-expand.Plo \ + ./$(DEPDIR)/auth-request.Plo ./$(DEPDIR)/auth-settings.Plo \ + ./$(DEPDIR)/auth-stats.Plo ./$(DEPDIR)/auth-token.Plo \ + ./$(DEPDIR)/auth-worker-client.Plo \ + ./$(DEPDIR)/auth-worker-server.Plo ./$(DEPDIR)/auth.Plo \ + ./$(DEPDIR)/checkpassword_reply-checkpassword-reply.Po \ + ./$(DEPDIR)/db-checkpassword.Plo \ + ./$(DEPDIR)/db-dict-cache-key.Plo ./$(DEPDIR)/db-dict.Plo \ + ./$(DEPDIR)/db-ldap.Plo ./$(DEPDIR)/db-lua.Plo \ + ./$(DEPDIR)/db-oauth2.Plo ./$(DEPDIR)/db-passwd-file.Plo \ + ./$(DEPDIR)/db-sql.Plo \ + ./$(DEPDIR)/libauthdb_imap_la-passdb-imap.Plo \ + ./$(DEPDIR)/libauthdb_ldap_la-db-ldap.Plo \ + ./$(DEPDIR)/libauthdb_ldap_la-passdb-ldap.Plo \ + ./$(DEPDIR)/libauthdb_ldap_la-userdb-ldap.Plo \ + ./$(DEPDIR)/libauthdb_lua_la-db-lua.Plo \ + ./$(DEPDIR)/libauthdb_lua_la-passdb-lua.Plo \ + ./$(DEPDIR)/libauthdb_lua_la-userdb-lua.Plo \ + ./$(DEPDIR)/libmech_gssapi_la-mech-gssapi.Plo \ + ./$(DEPDIR)/libpassword_la-crypt-blowfish.Plo \ + ./$(DEPDIR)/libpassword_la-mycrypt.Plo \ + ./$(DEPDIR)/libpassword_la-password-scheme-crypt.Plo \ + ./$(DEPDIR)/libpassword_la-password-scheme-md5crypt.Plo \ + ./$(DEPDIR)/libpassword_la-password-scheme-otp.Plo \ + ./$(DEPDIR)/libpassword_la-password-scheme-pbkdf2.Plo \ + ./$(DEPDIR)/libpassword_la-password-scheme-scram.Plo \ + ./$(DEPDIR)/libpassword_la-password-scheme-sodium.Plo \ + ./$(DEPDIR)/libpassword_la-password-scheme.Plo \ + ./$(DEPDIR)/mech-anonymous.Plo ./$(DEPDIR)/mech-apop.Plo \ + ./$(DEPDIR)/mech-cram-md5.Plo ./$(DEPDIR)/mech-digest-md5.Plo \ + ./$(DEPDIR)/mech-dovecot-token.Plo \ + ./$(DEPDIR)/mech-external.Plo ./$(DEPDIR)/mech-gssapi.Plo \ + ./$(DEPDIR)/mech-login.Plo ./$(DEPDIR)/mech-oauth2.Plo \ + ./$(DEPDIR)/mech-otp-common.Plo ./$(DEPDIR)/mech-otp.Plo \ + ./$(DEPDIR)/mech-plain-common.Plo ./$(DEPDIR)/mech-plain.Plo \ + ./$(DEPDIR)/mech-scram.Plo ./$(DEPDIR)/mech-winbind.Plo \ + ./$(DEPDIR)/mech.Plo ./$(DEPDIR)/passdb-blocking.Plo \ + ./$(DEPDIR)/passdb-bsdauth.Plo ./$(DEPDIR)/passdb-cache.Plo \ + ./$(DEPDIR)/passdb-checkpassword.Plo \ + ./$(DEPDIR)/passdb-dict.Plo ./$(DEPDIR)/passdb-ldap.Plo \ + ./$(DEPDIR)/passdb-lua.Plo ./$(DEPDIR)/passdb-oauth2.Plo \ + ./$(DEPDIR)/passdb-pam.Plo ./$(DEPDIR)/passdb-passwd-file.Plo \ + ./$(DEPDIR)/passdb-passwd.Plo ./$(DEPDIR)/passdb-shadow.Plo \ + ./$(DEPDIR)/passdb-sql.Plo ./$(DEPDIR)/passdb-static.Plo \ + ./$(DEPDIR)/passdb-template.Plo ./$(DEPDIR)/passdb.Plo \ + ./$(DEPDIR)/test-auth-request-fields.Po \ + ./$(DEPDIR)/test-auth-request-var-expand.Po \ + ./$(DEPDIR)/test-db-dict.Po ./$(DEPDIR)/test-lua.Po \ + ./$(DEPDIR)/test-main.Po ./$(DEPDIR)/test-mech.Po \ + ./$(DEPDIR)/test-mock.Po ./$(DEPDIR)/test-username-filter.Po \ + ./$(DEPDIR)/test_auth_cache-auth-cache.Po \ + ./$(DEPDIR)/test_auth_cache-test-auth-cache.Po \ + ./$(DEPDIR)/test_libpassword-test-libpassword.Po \ + ./$(DEPDIR)/userdb-blocking.Plo \ + ./$(DEPDIR)/userdb-checkpassword.Plo \ + ./$(DEPDIR)/userdb-dict.Plo ./$(DEPDIR)/userdb-ldap.Plo \ + ./$(DEPDIR)/userdb-lua.Plo ./$(DEPDIR)/userdb-passwd-file.Plo \ + ./$(DEPDIR)/userdb-passwd.Plo ./$(DEPDIR)/userdb-prefetch.Plo \ + ./$(DEPDIR)/userdb-sql.Plo ./$(DEPDIR)/userdb-static.Plo \ + ./$(DEPDIR)/userdb-template.Plo ./$(DEPDIR)/userdb.Plo +am__mv = mv -f +COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \ + $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) +LTCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \ + $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) \ + $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \ + $(AM_CFLAGS) $(CFLAGS) +AM_V_CC = $(am__v_CC_@AM_V@) +am__v_CC_ = $(am__v_CC_@AM_DEFAULT_V@) +am__v_CC_0 = @echo " CC " $@; +am__v_CC_1 = +CCLD = $(CC) +LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \ + $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \ + $(AM_LDFLAGS) $(LDFLAGS) -o $@ +AM_V_CCLD = $(am__v_CCLD_@AM_V@) +am__v_CCLD_ = $(am__v_CCLD_@AM_DEFAULT_V@) +am__v_CCLD_0 = @echo " CCLD " $@; +am__v_CCLD_1 = +SOURCES = $(libauth_la_SOURCES) $(libauthdb_imap_la_SOURCES) \ + $(libauthdb_ldap_la_SOURCES) $(libauthdb_lua_la_SOURCES) \ + $(libmech_gssapi_la_SOURCES) $(libpassword_la_SOURCES) \ + $(libstats_auth_la_SOURCES) $(auth_SOURCES) \ + checkpassword-reply.c $(test_auth_SOURCES) \ + $(test_auth_cache_SOURCES) $(test_libpassword_SOURCES) \ + $(test_mech_SOURCES) +DIST_SOURCES = $(libauth_la_SOURCES) $(libauthdb_imap_la_SOURCES) \ + $(am__libauthdb_ldap_la_SOURCES_DIST) \ + $(am__libauthdb_lua_la_SOURCES_DIST) \ + $(am__libmech_gssapi_la_SOURCES_DIST) \ + $(libpassword_la_SOURCES) $(libstats_auth_la_SOURCES) \ + $(auth_SOURCES) checkpassword-reply.c $(test_auth_SOURCES) \ + $(test_auth_cache_SOURCES) $(test_libpassword_SOURCES) \ + $(test_mech_SOURCES) +am__can_run_installinfo = \ + case $$AM_UPDATE_INFO_DIR in \ + n|no|NO) false;; \ + *) (install-info --version) >/dev/null 2>&1;; \ + esac +HEADERS = $(noinst_HEADERS) $(pkginc_lib_HEADERS) +am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP) +# Read a list of newline-separated strings from the standard input, +# and print each of them once, without duplicates. Input order is +# *not* preserved. +am__uniquify_input = $(AWK) '\ + BEGIN { nonempty = 0; } \ + { items[$$0] = 1; nonempty = 1; } \ + END { if (nonempty) { for (i in items) print i; }; } \ +' +# Make sure the list of sources is unique. This is necessary because, +# e.g., the same source file might be shared among _SOURCES variables +# for different programs/libraries. +am__define_uniq_tagged_files = \ + list='$(am__tagged_files)'; \ + unique=`for i in $$list; do \ + if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \ + done | $(am__uniquify_input)` +ETAGS = etags +CTAGS = ctags +am__DIST_COMMON = $(srcdir)/Makefile.in $(top_srcdir)/depcomp +DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST) +pkglibexecdir = $(libexecdir)/dovecot +ACLOCAL = @ACLOCAL@ +ACLOCAL_AMFLAGS = @ACLOCAL_AMFLAGS@ +AMTAR = @AMTAR@ +AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@ +APPARMOR_LIBS = @APPARMOR_LIBS@ +AR = @AR@ +AUTH_CFLAGS = @AUTH_CFLAGS@ +AUTH_LIBS = @AUTH_LIBS@ +AUTOCONF = @AUTOCONF@ +AUTOHEADER = @AUTOHEADER@ +AUTOMAKE = @AUTOMAKE@ +AWK = @AWK@ +BINARY_CFLAGS = @BINARY_CFLAGS@ +BINARY_LDFLAGS = @BINARY_LDFLAGS@ +BISON = @BISON@ +CASSANDRA_CFLAGS = @CASSANDRA_CFLAGS@ +CASSANDRA_LIBS = @CASSANDRA_LIBS@ +CC = @CC@ +CCDEPMODE = @CCDEPMODE@ +CDB_LIBS = @CDB_LIBS@ +CFLAGS = @CFLAGS@ +CLUCENE_CFLAGS = @CLUCENE_CFLAGS@ +CLUCENE_LIBS = @CLUCENE_LIBS@ +COMPRESS_LIBS = @COMPRESS_LIBS@ +CPP = @CPP@ +CPPFLAGS = @CPPFLAGS@ +CRYPT_LIBS = @CRYPT_LIBS@ +CXX = @CXX@ +CXXCPP = @CXXCPP@ +CXXDEPMODE = @CXXDEPMODE@ +CXXFLAGS = @CXXFLAGS@ +CYGPATH_W = @CYGPATH_W@ +DEFS = @DEFS@ +DEPDIR = @DEPDIR@ +DICT_LIBS = @DICT_LIBS@ +DLLIB = @DLLIB@ +DLLTOOL = @DLLTOOL@ +DSYMUTIL = @DSYMUTIL@ +DUMPBIN = @DUMPBIN@ +ECHO_C = @ECHO_C@ +ECHO_N = @ECHO_N@ +ECHO_T = @ECHO_T@ +EGREP = @EGREP@ +EXEEXT = @EXEEXT@ +FGREP = @FGREP@ +FLEX = @FLEX@ +FUZZER_CPPFLAGS = @FUZZER_CPPFLAGS@ +FUZZER_LDFLAGS = @FUZZER_LDFLAGS@ +GREP = @GREP@ +INSTALL = @INSTALL@ +INSTALL_DATA = @INSTALL_DATA@ +INSTALL_PROGRAM = @INSTALL_PROGRAM@ +INSTALL_SCRIPT = @INSTALL_SCRIPT@ +INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@ +KRB5CONFIG = @KRB5CONFIG@ +KRB5_CFLAGS = @KRB5_CFLAGS@ +KRB5_LIBS = @KRB5_LIBS@ +LD = @LD@ +LDAP_LIBS = @LDAP_LIBS@ +LDFLAGS = @LDFLAGS@ +LD_NO_WHOLE_ARCHIVE = @LD_NO_WHOLE_ARCHIVE@ +LD_WHOLE_ARCHIVE = @LD_WHOLE_ARCHIVE@ +LIBCAP = @LIBCAP@ +LIBDOVECOT = @LIBDOVECOT@ +LIBDOVECOT_COMPRESS = @LIBDOVECOT_COMPRESS@ +LIBDOVECOT_DEPS = @LIBDOVECOT_DEPS@ +LIBDOVECOT_DSYNC = @LIBDOVECOT_DSYNC@ +LIBDOVECOT_LA_LIBS = @LIBDOVECOT_LA_LIBS@ +LIBDOVECOT_LDA = @LIBDOVECOT_LDA@ +LIBDOVECOT_LDAP = @LIBDOVECOT_LDAP@ +LIBDOVECOT_LIBFTS = @LIBDOVECOT_LIBFTS@ +LIBDOVECOT_LIBFTS_DEPS = @LIBDOVECOT_LIBFTS_DEPS@ +LIBDOVECOT_LOGIN = @LIBDOVECOT_LOGIN@ +LIBDOVECOT_LUA = @LIBDOVECOT_LUA@ +LIBDOVECOT_LUA_DEPS = @LIBDOVECOT_LUA_DEPS@ +LIBDOVECOT_SQL = @LIBDOVECOT_SQL@ +LIBDOVECOT_STORAGE = @LIBDOVECOT_STORAGE@ +LIBDOVECOT_STORAGE_DEPS = @LIBDOVECOT_STORAGE_DEPS@ +LIBEXTTEXTCAT_CFLAGS = @LIBEXTTEXTCAT_CFLAGS@ +LIBEXTTEXTCAT_LIBS = @LIBEXTTEXTCAT_LIBS@ +LIBICONV = @LIBICONV@ +LIBICU_CFLAGS = @LIBICU_CFLAGS@ +LIBICU_LIBS = @LIBICU_LIBS@ +LIBOBJS = @LIBOBJS@ +LIBS = @LIBS@ +LIBSODIUM_CFLAGS = @LIBSODIUM_CFLAGS@ +LIBSODIUM_LIBS = @LIBSODIUM_LIBS@ +LIBTIRPC_CFLAGS = @LIBTIRPC_CFLAGS@ +LIBTIRPC_LIBS = @LIBTIRPC_LIBS@ +LIBTOOL = @LIBTOOL@ +LIBUNWIND_CFLAGS = @LIBUNWIND_CFLAGS@ +LIBUNWIND_LIBS = @LIBUNWIND_LIBS@ +LIBWRAP_LIBS = @LIBWRAP_LIBS@ +LINKED_STORAGE_LDADD = @LINKED_STORAGE_LDADD@ +LIPO = @LIPO@ +LN_S = @LN_S@ +LTLIBICONV = @LTLIBICONV@ +LTLIBOBJS = @LTLIBOBJS@ +LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@ +LUA_CFLAGS = @LUA_CFLAGS@ +LUA_LIBS = @LUA_LIBS@ +MAINT = @MAINT@ +MAKEINFO = @MAKEINFO@ +MANIFEST_TOOL = @MANIFEST_TOOL@ +MKDIR_P = @MKDIR_P@ +MODULE_LIBS = @MODULE_LIBS@ +MODULE_SUFFIX = @MODULE_SUFFIX@ +MYSQL_CFLAGS = @MYSQL_CFLAGS@ +MYSQL_CONFIG = @MYSQL_CONFIG@ +MYSQL_LIBS = @MYSQL_LIBS@ +NM = @NM@ +NMEDIT = @NMEDIT@ + +# automake seems to force making this unconditional.. +NOPLUGIN_LDFLAGS = +OBJDUMP = @OBJDUMP@ +OBJEXT = @OBJEXT@ +OTOOL = @OTOOL@ +OTOOL64 = @OTOOL64@ +PACKAGE = @PACKAGE@ +PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@ +PACKAGE_NAME = @PACKAGE_NAME@ +PACKAGE_STRING = @PACKAGE_STRING@ +PACKAGE_TARNAME = @PACKAGE_TARNAME@ +PACKAGE_URL = @PACKAGE_URL@ +PACKAGE_VERSION = @PACKAGE_VERSION@ +PANDOC = @PANDOC@ +PATH_SEPARATOR = @PATH_SEPARATOR@ +PGSQL_CFLAGS = @PGSQL_CFLAGS@ +PGSQL_LIBS = @PGSQL_LIBS@ +PG_CONFIG = @PG_CONFIG@ +PIE_CFLAGS = @PIE_CFLAGS@ +PIE_LDFLAGS = @PIE_LDFLAGS@ +PKG_CONFIG = @PKG_CONFIG@ +PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@ +PKG_CONFIG_PATH = @PKG_CONFIG_PATH@ +QUOTA_LIBS = @QUOTA_LIBS@ +RANLIB = @RANLIB@ +RELRO_LDFLAGS = @RELRO_LDFLAGS@ +RPCGEN = @RPCGEN@ +RUN_TEST = @RUN_TEST@ +SED = @SED@ +SETTING_FILES = @SETTING_FILES@ +SET_MAKE = @SET_MAKE@ +SHELL = @SHELL@ +SQLITE_CFLAGS = @SQLITE_CFLAGS@ +SQLITE_LIBS = @SQLITE_LIBS@ +SQL_CFLAGS = @SQL_CFLAGS@ +SQL_LIBS = @SQL_LIBS@ +SSL_CFLAGS = @SSL_CFLAGS@ +SSL_LIBS = @SSL_LIBS@ +STRIP = @STRIP@ +SYSTEMD_CFLAGS = @SYSTEMD_CFLAGS@ +SYSTEMD_LIBS = @SYSTEMD_LIBS@ +VALGRIND = @VALGRIND@ +VERSION = @VERSION@ +ZSTD_CFLAGS = @ZSTD_CFLAGS@ +ZSTD_LIBS = @ZSTD_LIBS@ +abs_builddir = @abs_builddir@ +abs_srcdir = @abs_srcdir@ +abs_top_builddir = @abs_top_builddir@ +abs_top_srcdir = @abs_top_srcdir@ +ac_ct_AR = @ac_ct_AR@ +ac_ct_CC = @ac_ct_CC@ +ac_ct_CXX = @ac_ct_CXX@ +ac_ct_DUMPBIN = @ac_ct_DUMPBIN@ +am__include = @am__include@ +am__leading_dot = @am__leading_dot@ +am__quote = @am__quote@ +am__tar = @am__tar@ +am__untar = @am__untar@ +bindir = @bindir@ +build = @build@ +build_alias = @build_alias@ +build_cpu = @build_cpu@ +build_os = @build_os@ +build_vendor = @build_vendor@ +builddir = @builddir@ +datadir = @datadir@ +datarootdir = @datarootdir@ +dict_drivers = @dict_drivers@ +docdir = @docdir@ +dvidir = @dvidir@ +exec_prefix = @exec_prefix@ +host = @host@ +host_alias = @host_alias@ +host_cpu = @host_cpu@ +host_os = @host_os@ +host_vendor = @host_vendor@ +htmldir = @htmldir@ +includedir = @includedir@ +infodir = @infodir@ +install_sh = @install_sh@ +libdir = @libdir@ +libexecdir = @libexecdir@ +localedir = @localedir@ +localstatedir = @localstatedir@ +mandir = @mandir@ +mkdir_p = @mkdir_p@ +moduledir = @moduledir@ +oldincludedir = @oldincludedir@ +pdfdir = @pdfdir@ +prefix = @prefix@ +program_transform_name = @program_transform_name@ +psdir = @psdir@ +rundir = @rundir@ +runstatedir = @runstatedir@ +sbindir = @sbindir@ +sharedstatedir = @sharedstatedir@ +sql_drivers = @sql_drivers@ +srcdir = @srcdir@ +ssldir = @ssldir@ +statedir = @statedir@ +sysconfdir = @sysconfdir@ +systemdservicetype = @systemdservicetype@ +systemdsystemunitdir = @systemdsystemunitdir@ +target_alias = @target_alias@ +top_build_prefix = @top_build_prefix@ +top_builddir = @top_builddir@ +top_srcdir = @top_srcdir@ +noinst_LTLIBRARIES = libpassword.la libauth.la +auth_moduledir = $(moduledir)/auth +@GSSAPI_PLUGIN_TRUE@GSSAPI_LIB = libmech_gssapi.la +@LDAP_PLUGIN_TRUE@LDAP_LIB = libauthdb_ldap.la +LUA_LIB = $(am__append_1) +AUTH_LUA_LIBS = $(am__append_2) +AUTH_LUA_LDADD = $(am__append_3) +auth_module_LTLIBRARIES = \ + $(GSSAPI_LIB) \ + $(LDAP_LIB) \ + $(LUA_LIB) \ + libauthdb_imap.la + +AM_CPPFLAGS = \ + -I$(top_srcdir)/src/lib \ + -I$(top_srcdir)/src/lib-auth \ + -I$(top_srcdir)/src/lib-test \ + -I$(top_srcdir)/src/lib-dict \ + -I$(top_srcdir)/src/lib-dns \ + -I$(top_srcdir)/src/lib-http \ + -I$(top_srcdir)/src/lib-sql \ + -I$(top_srcdir)/src/lib-settings \ + -I$(top_srcdir)/src/lib-old-stats \ + -I$(top_srcdir)/src/lib-otp \ + -I$(top_srcdir)/src/lib-master \ + -I$(top_srcdir)/src/lib-oauth2 \ + -I$(top_srcdir)/src/lib-ssl-iostream \ + -I$(top_srcdir)/src/lib-lua \ + -I$(top_srcdir)/src/lib-dcrypt \ + -DAUTH_MODULE_DIR=\""$(auth_moduledir)"\" \ + -DPKG_LIBEXECDIR=\""$(pkglibexecdir)"\" \ + -DPKG_RUNDIR=\""$(rundir)"\" \ + -DSYSCONFDIR=\""$(sysconfdir)/dovecot"\" \ + $(LUA_CFLAGS) \ + $(AUTH_CFLAGS) + +auth_LDFLAGS = -export-dynamic +libpassword_la_SOURCES = \ + crypt-blowfish.c \ + mycrypt.c \ + password-scheme.c \ + password-scheme-crypt.c \ + password-scheme-md5crypt.c \ + password-scheme-scram.c \ + password-scheme-otp.c \ + password-scheme-pbkdf2.c \ + password-scheme-sodium.c + +libpassword_la_CFLAGS = $(AM_CPPFLAGS) $(LIBSODIUM_CFLAGS) +auth_libs = \ + libauth.la \ + libstats_auth.la \ + libpassword.la \ + ../lib-otp/libotp.la \ + $(AUTH_LUA_LIBS) \ + $(LIBDOVECOT_SQL) + +auth_CPPFLAGS = $(AM_CPPFLAGS) $(BINARY_CFLAGS) +auth_LDADD = $(auth_libs) $(LIBDOVECOT) $(AUTH_LIBS) $(BINARY_LDFLAGS) $(AUTH_LUA_LDADD) +auth_DEPENDENCIES = $(auth_libs) $(LIBDOVECOT_DEPS) +auth_SOURCES = main.c +ldap_sources = db-ldap.c passdb-ldap.c userdb-ldap.c +lua_sources = db-lua.c passdb-lua.c userdb-lua.c +libauth_la_DEPENDENCIES = $(LIBDOVECOT_DEPS) +libauth_la_SOURCES = \ + auth.c \ + auth-cache.c \ + auth-client-connection.c \ + auth-master-connection.c \ + auth-policy.c \ + mech-otp-common.c \ + mech-plain-common.c \ + auth-penalty.c \ + auth-request.c \ + auth-request-fields.c \ + auth-request-handler.c \ + auth-request-stats.c \ + auth-request-var-expand.c \ + auth-settings.c \ + auth-fields.c \ + auth-token.c \ + auth-worker-client.c \ + auth-worker-server.c \ + db-checkpassword.c \ + db-dict.c \ + db-dict-cache-key.c \ + db-oauth2.c \ + db-sql.c \ + db-passwd-file.c \ + mech.c \ + mech-anonymous.c \ + mech-plain.c \ + mech-login.c \ + mech-cram-md5.c \ + mech-digest-md5.c \ + mech-external.c \ + mech-gssapi.c \ + mech-otp.c \ + mech-scram.c \ + mech-apop.c \ + mech-winbind.c \ + mech-dovecot-token.c \ + mech-oauth2.c \ + passdb.c \ + passdb-blocking.c \ + passdb-bsdauth.c \ + passdb-cache.c \ + passdb-checkpassword.c \ + passdb-dict.c \ + passdb-oauth2.c \ + passdb-passwd.c \ + passdb-passwd-file.c \ + passdb-pam.c \ + passdb-shadow.c \ + passdb-sql.c \ + passdb-static.c \ + passdb-template.c \ + userdb.c \ + userdb-blocking.c \ + userdb-checkpassword.c \ + userdb-dict.c \ + userdb-passwd.c \ + userdb-passwd-file.c \ + userdb-prefetch.c \ + userdb-static.c \ + userdb-sql.c \ + userdb-template.c \ + $(ldap_sources) \ + $(lua_sources) + +headers = \ + auth.h \ + auth-cache.h \ + auth-client-connection.h \ + auth-common.h \ + auth-master-connection.h \ + mech-otp-common.h \ + mech-plain-common.h \ + mech-digest-md5-private.h \ + mech-scram.h \ + auth-penalty.h \ + auth-policy.h \ + auth-request.h \ + auth-request-handler.h \ + auth-request-handler-private.h \ + auth-request-stats.h \ + auth-request-var-expand.h \ + auth-settings.h \ + auth-stats.h \ + auth-fields.h \ + auth-token.h \ + auth-worker-client.h \ + auth-worker-server.h \ + db-dict.h \ + db-ldap.h \ + db-sql.h \ + db-passwd-file.h \ + db-checkpassword.h \ + db-oauth2.h \ + mech.h \ + mycrypt.h \ + passdb.h \ + passdb-blocking.h \ + passdb-cache.h \ + passdb-template.h \ + password-scheme.h \ + userdb.h \ + userdb-blocking.h \ + userdb-template.h + +@GSSAPI_PLUGIN_TRUE@libmech_gssapi_la_LDFLAGS = -module -avoid-version +@GSSAPI_PLUGIN_TRUE@libmech_gssapi_la_LIBADD = $(KRB5_LIBS) +@GSSAPI_PLUGIN_TRUE@libmech_gssapi_la_CPPFLAGS = $(AM_CPPFLAGS) $(KRB5_CFLAGS) -DPLUGIN_BUILD +@GSSAPI_PLUGIN_TRUE@libmech_gssapi_la_SOURCES = mech-gssapi.c +@LDAP_PLUGIN_TRUE@libauthdb_ldap_la_LDFLAGS = -module -avoid-version +@LDAP_PLUGIN_TRUE@libauthdb_ldap_la_LIBADD = $(LDAP_LIBS) +@LDAP_PLUGIN_TRUE@libauthdb_ldap_la_CPPFLAGS = $(AM_CPPFLAGS) -DPLUGIN_BUILD +@LDAP_PLUGIN_TRUE@libauthdb_ldap_la_SOURCES = $(ldap_sources) +@AUTH_LUA_PLUGIN_TRUE@libauthdb_lua_la_LDFLAGS = -module -avoid-version +@AUTH_LUA_PLUGIN_TRUE@libauthdb_lua_la_LIBADD = $(LIBDOVECOT_LUA) +@AUTH_LUA_PLUGIN_TRUE@libauthdb_lua_la_CPPFLAGS = $(AM_CPPFLAGS) -DPLUGIN_BUILD +@AUTH_LUA_PLUGIN_TRUE@libauthdb_lua_la_SOURCES = $(lua_sources) +libauthdb_imap_la_LDFLAGS = -module -avoid-version +libauthdb_imap_la_LIBADD = \ + ../lib-imap-client/libimap_client.la \ + $(LIBDOVECOT) + +libauthdb_imap_la_CPPFLAGS = \ + $(AM_CPPFLAGS) \ + -I$(top_srcdir)/src/lib-imap \ + -I$(top_srcdir)/src/lib-imap-client + +libauthdb_imap_la_SOURCES = passdb-imap.c +pkginc_libdir = $(pkgincludedir) +pkginc_lib_HEADERS = $(headers) +checkpassword_reply_CPPFLAGS = $(AM_CPPFLAGS) $(BINARY_CFLAGS) +checkpassword_reply_LDADD = $(LIBDOVECOT) $(BINARY_LDFLAGS) +checkpassword_reply_DEPENDENCIES = $(LIBDOVECOT_DEPS) +checkpassword_reply_sources = \ + checkpassword-reply.c + +stats_moduledir = $(moduledir)/old-stats +stats_module_LTLIBRARIES = libstats_auth.la +libstats_auth_la_LDFLAGS = -module -avoid-version +libstats_auth_la_LIBADD = $(LIBDOVECOT) +libstats_auth_la_DEPENDENCIES = $(LIBDOVECOT_DEPS) +libstats_auth_la_SOURCES = auth-stats.c +test_programs = \ + test-libpassword \ + test-auth-cache \ + test-auth \ + test-mech + +noinst_HEADERS = test-auth.h crypt-blowfish.h db-lua.h +test_libs = \ + ../lib-dovecot/libdovecot.la + +test_libpassword_SOURCES = test-libpassword.c +test_libpassword_LDADD = \ + libpassword.la \ + ../lib-otp/libotp.la \ + $(CRYPT_LIBS) \ + $(LIBDOVECOT_SQL) \ + $(LIBSODIUM_LIBS) \ + $(test_libs) \ + $(BINARY_LDFLAGS) + +test_libpassword_DEPENDENCIES = libpassword.la +test_libpassword_CPPFLAGS = $(AM_CPPFLAGS) $(BINARY_CFLAGS) +test_auth_cache_SOURCES = auth-cache.c test-auth-cache.c +test_auth_cache_LDADD = $(test_libs) +test_auth_cache_DEPENDENCIES = $(pkglibexec_PROGRAMS) $(test_libs) +# this is needed to force auth-cache.c recompilation +test_auth_cache_CPPFLAGS = $(AM_CPPFLAGS) +test_auth_SOURCES = \ + test-auth-request-var-expand.c \ + test-auth-request-fields.c \ + test-username-filter.c \ + test-db-dict.c \ + test-lua.c \ + test-mock.c \ + test-main.c + +test_auth_LDADD = $(test_libs) $(auth_libs) $(AUTH_LIBS) $(LUA_LIBS) +test_auth_DEPENDENCIES = $(pkglibexec_PROGRAMS) $(test_libs) +test_mech_SOURCES = \ + test-mock.c \ + test-mech.c + +test_mech_LDADD = $(test_libs) $(auth_libs) $(AUTH_LIBS) $(LUA_LIBS) +test_mech_DEPENDENCIES = $(pkglibexec_PROGRAMS) $(test_libs) +all: all-am + +.SUFFIXES: +.SUFFIXES: .c .lo .o .obj +$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps) + @for dep in $?; do \ + case '$(am__configure_deps)' in \ + *$$dep*) \ + ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \ + && { if test -f $@; then exit 0; else break; fi; }; \ + exit 1;; \ + esac; \ + done; \ + echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign src/auth/Makefile'; \ + $(am__cd) $(top_srcdir) && \ + $(AUTOMAKE) --foreign src/auth/Makefile +Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status + @case '$?' in \ + *config.status*) \ + cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \ + *) \ + echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \ + cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \ + esac; + +$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES) + cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh + +$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps) + cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh +$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps) + cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh +$(am__aclocal_m4_deps): + +clean-noinstPROGRAMS: + @list='$(noinst_PROGRAMS)'; test -n "$$list" || exit 0; \ + echo " rm -f" $$list; \ + rm -f $$list || exit $$?; \ + test -n "$(EXEEXT)" || exit 0; \ + list=`for p in $$list; do echo "$$p"; done | sed 's/$(EXEEXT)$$//'`; \ + echo " rm -f" $$list; \ + rm -f $$list +install-pkglibexecPROGRAMS: $(pkglibexec_PROGRAMS) + @$(NORMAL_INSTALL) + @list='$(pkglibexec_PROGRAMS)'; test -n "$(pkglibexecdir)" || list=; \ + if test -n "$$list"; then \ + echo " $(MKDIR_P) '$(DESTDIR)$(pkglibexecdir)'"; \ + $(MKDIR_P) "$(DESTDIR)$(pkglibexecdir)" || exit 1; \ + fi; \ + for p in $$list; do echo "$$p $$p"; done | \ + sed 's/$(EXEEXT)$$//' | \ + while read p p1; do if test -f $$p \ + || test -f $$p1 \ + ; then echo "$$p"; echo "$$p"; else :; fi; \ + done | \ + sed -e 'p;s,.*/,,;n;h' \ + -e 's|.*|.|' \ + -e 'p;x;s,.*/,,;s/$(EXEEXT)$$//;$(transform);s/$$/$(EXEEXT)/' | \ + sed 'N;N;N;s,\n, ,g' | \ + $(AWK) 'BEGIN { files["."] = ""; dirs["."] = 1 } \ + { d=$$3; if (dirs[d] != 1) { print "d", d; dirs[d] = 1 } \ + if ($$2 == $$4) files[d] = files[d] " " $$1; \ + else { print "f", $$3 "/" $$4, $$1; } } \ + END { for (d in files) print "f", d, files[d] }' | \ + while read type dir files; do \ + if test "$$dir" = .; then dir=; else dir=/$$dir; fi; \ + test -z "$$files" || { \ + echo " $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files '$(DESTDIR)$(pkglibexecdir)$$dir'"; \ + $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files "$(DESTDIR)$(pkglibexecdir)$$dir" || exit $$?; \ + } \ + ; done + +uninstall-pkglibexecPROGRAMS: + @$(NORMAL_UNINSTALL) + @list='$(pkglibexec_PROGRAMS)'; test -n "$(pkglibexecdir)" || list=; \ + files=`for p in $$list; do echo "$$p"; done | \ + sed -e 'h;s,^.*/,,;s/$(EXEEXT)$$//;$(transform)' \ + -e 's/$$/$(EXEEXT)/' \ + `; \ + test -n "$$list" || exit 0; \ + echo " ( cd '$(DESTDIR)$(pkglibexecdir)' && rm -f" $$files ")"; \ + cd "$(DESTDIR)$(pkglibexecdir)" && rm -f $$files + +clean-pkglibexecPROGRAMS: + @list='$(pkglibexec_PROGRAMS)'; test -n "$$list" || exit 0; \ + echo " rm -f" $$list; \ + rm -f $$list || exit $$?; \ + test -n "$(EXEEXT)" || exit 0; \ + list=`for p in $$list; do echo "$$p"; done | sed 's/$(EXEEXT)$$//'`; \ + echo " rm -f" $$list; \ + rm -f $$list + +install-auth_moduleLTLIBRARIES: $(auth_module_LTLIBRARIES) + @$(NORMAL_INSTALL) + @list='$(auth_module_LTLIBRARIES)'; test -n "$(auth_moduledir)" || list=; \ + list2=; for p in $$list; do \ + if test -f $$p; then \ + list2="$$list2 $$p"; \ + else :; fi; \ + done; \ + test -z "$$list2" || { \ + echo " $(MKDIR_P) '$(DESTDIR)$(auth_moduledir)'"; \ + $(MKDIR_P) "$(DESTDIR)$(auth_moduledir)" || exit 1; \ + echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 '$(DESTDIR)$(auth_moduledir)'"; \ + $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 "$(DESTDIR)$(auth_moduledir)"; \ + } + +uninstall-auth_moduleLTLIBRARIES: + @$(NORMAL_UNINSTALL) + @list='$(auth_module_LTLIBRARIES)'; test -n "$(auth_moduledir)" || list=; \ + for p in $$list; do \ + $(am__strip_dir) \ + echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f '$(DESTDIR)$(auth_moduledir)/$$f'"; \ + $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f "$(DESTDIR)$(auth_moduledir)/$$f"; \ + done + +clean-auth_moduleLTLIBRARIES: + -test -z "$(auth_module_LTLIBRARIES)" || rm -f $(auth_module_LTLIBRARIES) + @list='$(auth_module_LTLIBRARIES)'; \ + locs=`for p in $$list; do echo $$p; done | \ + sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \ + sort -u`; \ + test -z "$$locs" || { \ + echo rm -f $${locs}; \ + rm -f $${locs}; \ + } + +clean-noinstLTLIBRARIES: + -test -z "$(noinst_LTLIBRARIES)" || rm -f $(noinst_LTLIBRARIES) + @list='$(noinst_LTLIBRARIES)'; \ + locs=`for p in $$list; do echo $$p; done | \ + sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \ + sort -u`; \ + test -z "$$locs" || { \ + echo rm -f $${locs}; \ + rm -f $${locs}; \ + } + +install-stats_moduleLTLIBRARIES: $(stats_module_LTLIBRARIES) + @$(NORMAL_INSTALL) + @list='$(stats_module_LTLIBRARIES)'; test -n "$(stats_moduledir)" || list=; \ + list2=; for p in $$list; do \ + if test -f $$p; then \ + list2="$$list2 $$p"; \ + else :; fi; \ + done; \ + test -z "$$list2" || { \ + echo " $(MKDIR_P) '$(DESTDIR)$(stats_moduledir)'"; \ + $(MKDIR_P) "$(DESTDIR)$(stats_moduledir)" || exit 1; \ + echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 '$(DESTDIR)$(stats_moduledir)'"; \ + $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 "$(DESTDIR)$(stats_moduledir)"; \ + } + +uninstall-stats_moduleLTLIBRARIES: + @$(NORMAL_UNINSTALL) + @list='$(stats_module_LTLIBRARIES)'; test -n "$(stats_moduledir)" || list=; \ + for p in $$list; do \ + $(am__strip_dir) \ + echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f '$(DESTDIR)$(stats_moduledir)/$$f'"; \ + $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f "$(DESTDIR)$(stats_moduledir)/$$f"; \ + done + +clean-stats_moduleLTLIBRARIES: + -test -z "$(stats_module_LTLIBRARIES)" || rm -f $(stats_module_LTLIBRARIES) + @list='$(stats_module_LTLIBRARIES)'; \ + locs=`for p in $$list; do echo $$p; done | \ + sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \ + sort -u`; \ + test -z "$$locs" || { \ + echo rm -f $${locs}; \ + rm -f $${locs}; \ + } + +libauth.la: $(libauth_la_OBJECTS) $(libauth_la_DEPENDENCIES) $(EXTRA_libauth_la_DEPENDENCIES) + $(AM_V_CCLD)$(LINK) $(libauth_la_OBJECTS) $(libauth_la_LIBADD) $(LIBS) + +libauthdb_imap.la: $(libauthdb_imap_la_OBJECTS) $(libauthdb_imap_la_DEPENDENCIES) $(EXTRA_libauthdb_imap_la_DEPENDENCIES) + $(AM_V_CCLD)$(libauthdb_imap_la_LINK) -rpath $(auth_moduledir) $(libauthdb_imap_la_OBJECTS) $(libauthdb_imap_la_LIBADD) $(LIBS) + +libauthdb_ldap.la: $(libauthdb_ldap_la_OBJECTS) $(libauthdb_ldap_la_DEPENDENCIES) $(EXTRA_libauthdb_ldap_la_DEPENDENCIES) + $(AM_V_CCLD)$(libauthdb_ldap_la_LINK) $(am_libauthdb_ldap_la_rpath) $(libauthdb_ldap_la_OBJECTS) $(libauthdb_ldap_la_LIBADD) $(LIBS) + +libauthdb_lua.la: $(libauthdb_lua_la_OBJECTS) $(libauthdb_lua_la_DEPENDENCIES) $(EXTRA_libauthdb_lua_la_DEPENDENCIES) + $(AM_V_CCLD)$(libauthdb_lua_la_LINK) $(am_libauthdb_lua_la_rpath) $(libauthdb_lua_la_OBJECTS) $(libauthdb_lua_la_LIBADD) $(LIBS) + +libmech_gssapi.la: $(libmech_gssapi_la_OBJECTS) $(libmech_gssapi_la_DEPENDENCIES) $(EXTRA_libmech_gssapi_la_DEPENDENCIES) + $(AM_V_CCLD)$(libmech_gssapi_la_LINK) $(am_libmech_gssapi_la_rpath) $(libmech_gssapi_la_OBJECTS) $(libmech_gssapi_la_LIBADD) $(LIBS) + +libpassword.la: $(libpassword_la_OBJECTS) $(libpassword_la_DEPENDENCIES) $(EXTRA_libpassword_la_DEPENDENCIES) + $(AM_V_CCLD)$(libpassword_la_LINK) $(libpassword_la_OBJECTS) $(libpassword_la_LIBADD) $(LIBS) + +libstats_auth.la: $(libstats_auth_la_OBJECTS) $(libstats_auth_la_DEPENDENCIES) $(EXTRA_libstats_auth_la_DEPENDENCIES) + $(AM_V_CCLD)$(libstats_auth_la_LINK) -rpath $(stats_moduledir) $(libstats_auth_la_OBJECTS) $(libstats_auth_la_LIBADD) $(LIBS) + +auth$(EXEEXT): $(auth_OBJECTS) $(auth_DEPENDENCIES) $(EXTRA_auth_DEPENDENCIES) + @rm -f auth$(EXEEXT) + $(AM_V_CCLD)$(auth_LINK) $(auth_OBJECTS) $(auth_LDADD) $(LIBS) + +checkpassword-reply$(EXEEXT): $(checkpassword_reply_OBJECTS) $(checkpassword_reply_DEPENDENCIES) $(EXTRA_checkpassword_reply_DEPENDENCIES) + @rm -f checkpassword-reply$(EXEEXT) + $(AM_V_CCLD)$(LINK) $(checkpassword_reply_OBJECTS) $(checkpassword_reply_LDADD) $(LIBS) + +test-auth$(EXEEXT): $(test_auth_OBJECTS) $(test_auth_DEPENDENCIES) $(EXTRA_test_auth_DEPENDENCIES) + @rm -f test-auth$(EXEEXT) + $(AM_V_CCLD)$(LINK) $(test_auth_OBJECTS) $(test_auth_LDADD) $(LIBS) + +test-auth-cache$(EXEEXT): $(test_auth_cache_OBJECTS) $(test_auth_cache_DEPENDENCIES) $(EXTRA_test_auth_cache_DEPENDENCIES) + @rm -f test-auth-cache$(EXEEXT) + $(AM_V_CCLD)$(LINK) $(test_auth_cache_OBJECTS) $(test_auth_cache_LDADD) $(LIBS) + +test-libpassword$(EXEEXT): $(test_libpassword_OBJECTS) $(test_libpassword_DEPENDENCIES) $(EXTRA_test_libpassword_DEPENDENCIES) + @rm -f test-libpassword$(EXEEXT) + $(AM_V_CCLD)$(LINK) $(test_libpassword_OBJECTS) $(test_libpassword_LDADD) $(LIBS) + +test-mech$(EXEEXT): $(test_mech_OBJECTS) $(test_mech_DEPENDENCIES) $(EXTRA_test_mech_DEPENDENCIES) + @rm -f test-mech$(EXEEXT) + $(AM_V_CCLD)$(LINK) $(test_mech_OBJECTS) $(test_mech_LDADD) $(LIBS) + +mostlyclean-compile: + -rm -f *.$(OBJEXT) + +distclean-compile: + -rm -f *.tab.c + +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/auth-cache.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/auth-client-connection.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/auth-fields.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/auth-main.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/auth-master-connection.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/auth-penalty.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/auth-policy.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/auth-request-fields.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/auth-request-handler.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/auth-request-stats.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/auth-request-var-expand.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/auth-request.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/auth-settings.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/auth-stats.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/auth-token.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/auth-worker-client.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/auth-worker-server.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/auth.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/checkpassword_reply-checkpassword-reply.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/db-checkpassword.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/db-dict-cache-key.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/db-dict.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/db-ldap.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/db-lua.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/db-oauth2.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/db-passwd-file.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/db-sql.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libauthdb_imap_la-passdb-imap.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libauthdb_ldap_la-db-ldap.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libauthdb_ldap_la-passdb-ldap.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libauthdb_ldap_la-userdb-ldap.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libauthdb_lua_la-db-lua.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libauthdb_lua_la-passdb-lua.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libauthdb_lua_la-userdb-lua.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libmech_gssapi_la-mech-gssapi.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libpassword_la-crypt-blowfish.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libpassword_la-mycrypt.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libpassword_la-password-scheme-crypt.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libpassword_la-password-scheme-md5crypt.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libpassword_la-password-scheme-otp.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libpassword_la-password-scheme-pbkdf2.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libpassword_la-password-scheme-scram.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libpassword_la-password-scheme-sodium.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libpassword_la-password-scheme.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mech-anonymous.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mech-apop.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mech-cram-md5.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mech-digest-md5.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mech-dovecot-token.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mech-external.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mech-gssapi.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mech-login.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mech-oauth2.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mech-otp-common.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mech-otp.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mech-plain-common.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mech-plain.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mech-scram.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mech-winbind.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mech.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/passdb-blocking.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/passdb-bsdauth.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/passdb-cache.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/passdb-checkpassword.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/passdb-dict.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/passdb-ldap.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/passdb-lua.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/passdb-oauth2.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/passdb-pam.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/passdb-passwd-file.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/passdb-passwd.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/passdb-shadow.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/passdb-sql.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/passdb-static.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/passdb-template.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/passdb.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-auth-request-fields.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-auth-request-var-expand.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-db-dict.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-lua.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-main.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-mech.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-mock.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-username-filter.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_auth_cache-auth-cache.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_auth_cache-test-auth-cache.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_libpassword-test-libpassword.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/userdb-blocking.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/userdb-checkpassword.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/userdb-dict.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/userdb-ldap.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/userdb-lua.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/userdb-passwd-file.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/userdb-passwd.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/userdb-prefetch.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/userdb-sql.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/userdb-static.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/userdb-template.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/userdb.Plo@am__quote@ # am--include-marker + +$(am__depfiles_remade): + @$(MKDIR_P) $(@D) + @echo '# dummy' >$@-t && $(am__mv) $@-t $@ + +am--depfiles: $(am__depfiles_remade) + +.c.o: +@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $< +@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po +@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ $< + +.c.obj: +@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ `$(CYGPATH_W) '$<'` +@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po +@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ `$(CYGPATH_W) '$<'` + +.c.lo: +@am__fastdepCC_TRUE@ $(AM_V_CC)$(LTCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $< +@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Plo +@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=yes @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LTCOMPILE) -c -o $@ $< + +libauthdb_imap_la-passdb-imap.lo: passdb-imap.c +@am__fastdepCC_TRUE@ $(AM_V_CC)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libauthdb_imap_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT libauthdb_imap_la-passdb-imap.lo -MD -MP -MF $(DEPDIR)/libauthdb_imap_la-passdb-imap.Tpo -c -o libauthdb_imap_la-passdb-imap.lo `test -f 'passdb-imap.c' || echo '$(srcdir)/'`passdb-imap.c +@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libauthdb_imap_la-passdb-imap.Tpo $(DEPDIR)/libauthdb_imap_la-passdb-imap.Plo +@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='passdb-imap.c' object='libauthdb_imap_la-passdb-imap.lo' libtool=yes @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libauthdb_imap_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o libauthdb_imap_la-passdb-imap.lo `test -f 'passdb-imap.c' || echo '$(srcdir)/'`passdb-imap.c + +libauthdb_ldap_la-db-ldap.lo: db-ldap.c +@am__fastdepCC_TRUE@ $(AM_V_CC)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libauthdb_ldap_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT libauthdb_ldap_la-db-ldap.lo -MD -MP -MF $(DEPDIR)/libauthdb_ldap_la-db-ldap.Tpo -c -o libauthdb_ldap_la-db-ldap.lo `test -f 'db-ldap.c' || echo '$(srcdir)/'`db-ldap.c +@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libauthdb_ldap_la-db-ldap.Tpo $(DEPDIR)/libauthdb_ldap_la-db-ldap.Plo +@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='db-ldap.c' object='libauthdb_ldap_la-db-ldap.lo' libtool=yes @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libauthdb_ldap_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o libauthdb_ldap_la-db-ldap.lo `test -f 'db-ldap.c' || echo '$(srcdir)/'`db-ldap.c + +libauthdb_ldap_la-passdb-ldap.lo: passdb-ldap.c +@am__fastdepCC_TRUE@ $(AM_V_CC)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libauthdb_ldap_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT libauthdb_ldap_la-passdb-ldap.lo -MD -MP -MF $(DEPDIR)/libauthdb_ldap_la-passdb-ldap.Tpo -c -o libauthdb_ldap_la-passdb-ldap.lo `test -f 'passdb-ldap.c' || echo '$(srcdir)/'`passdb-ldap.c +@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libauthdb_ldap_la-passdb-ldap.Tpo $(DEPDIR)/libauthdb_ldap_la-passdb-ldap.Plo +@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='passdb-ldap.c' object='libauthdb_ldap_la-passdb-ldap.lo' libtool=yes @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libauthdb_ldap_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o libauthdb_ldap_la-passdb-ldap.lo `test -f 'passdb-ldap.c' || echo '$(srcdir)/'`passdb-ldap.c + +libauthdb_ldap_la-userdb-ldap.lo: userdb-ldap.c +@am__fastdepCC_TRUE@ $(AM_V_CC)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libauthdb_ldap_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT libauthdb_ldap_la-userdb-ldap.lo -MD -MP -MF $(DEPDIR)/libauthdb_ldap_la-userdb-ldap.Tpo -c -o libauthdb_ldap_la-userdb-ldap.lo `test -f 'userdb-ldap.c' || echo '$(srcdir)/'`userdb-ldap.c +@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libauthdb_ldap_la-userdb-ldap.Tpo $(DEPDIR)/libauthdb_ldap_la-userdb-ldap.Plo +@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='userdb-ldap.c' object='libauthdb_ldap_la-userdb-ldap.lo' libtool=yes @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libauthdb_ldap_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o libauthdb_ldap_la-userdb-ldap.lo `test -f 'userdb-ldap.c' || echo '$(srcdir)/'`userdb-ldap.c + +libauthdb_lua_la-db-lua.lo: db-lua.c +@am__fastdepCC_TRUE@ $(AM_V_CC)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libauthdb_lua_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT libauthdb_lua_la-db-lua.lo -MD -MP -MF $(DEPDIR)/libauthdb_lua_la-db-lua.Tpo -c -o libauthdb_lua_la-db-lua.lo `test -f 'db-lua.c' || echo '$(srcdir)/'`db-lua.c +@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libauthdb_lua_la-db-lua.Tpo $(DEPDIR)/libauthdb_lua_la-db-lua.Plo +@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='db-lua.c' object='libauthdb_lua_la-db-lua.lo' libtool=yes @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libauthdb_lua_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o libauthdb_lua_la-db-lua.lo `test -f 'db-lua.c' || echo '$(srcdir)/'`db-lua.c + +libauthdb_lua_la-passdb-lua.lo: passdb-lua.c +@am__fastdepCC_TRUE@ $(AM_V_CC)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libauthdb_lua_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT libauthdb_lua_la-passdb-lua.lo -MD -MP -MF $(DEPDIR)/libauthdb_lua_la-passdb-lua.Tpo -c -o libauthdb_lua_la-passdb-lua.lo `test -f 'passdb-lua.c' || echo '$(srcdir)/'`passdb-lua.c +@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libauthdb_lua_la-passdb-lua.Tpo $(DEPDIR)/libauthdb_lua_la-passdb-lua.Plo +@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='passdb-lua.c' object='libauthdb_lua_la-passdb-lua.lo' libtool=yes @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libauthdb_lua_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o libauthdb_lua_la-passdb-lua.lo `test -f 'passdb-lua.c' || echo '$(srcdir)/'`passdb-lua.c + +libauthdb_lua_la-userdb-lua.lo: userdb-lua.c +@am__fastdepCC_TRUE@ $(AM_V_CC)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libauthdb_lua_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT libauthdb_lua_la-userdb-lua.lo -MD -MP -MF $(DEPDIR)/libauthdb_lua_la-userdb-lua.Tpo -c -o libauthdb_lua_la-userdb-lua.lo `test -f 'userdb-lua.c' || echo '$(srcdir)/'`userdb-lua.c +@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libauthdb_lua_la-userdb-lua.Tpo $(DEPDIR)/libauthdb_lua_la-userdb-lua.Plo +@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='userdb-lua.c' object='libauthdb_lua_la-userdb-lua.lo' libtool=yes @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libauthdb_lua_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o libauthdb_lua_la-userdb-lua.lo `test -f 'userdb-lua.c' || echo '$(srcdir)/'`userdb-lua.c + +libmech_gssapi_la-mech-gssapi.lo: mech-gssapi.c +@am__fastdepCC_TRUE@ $(AM_V_CC)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libmech_gssapi_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT libmech_gssapi_la-mech-gssapi.lo -MD -MP -MF $(DEPDIR)/libmech_gssapi_la-mech-gssapi.Tpo -c -o libmech_gssapi_la-mech-gssapi.lo `test -f 'mech-gssapi.c' || echo '$(srcdir)/'`mech-gssapi.c +@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libmech_gssapi_la-mech-gssapi.Tpo $(DEPDIR)/libmech_gssapi_la-mech-gssapi.Plo +@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='mech-gssapi.c' object='libmech_gssapi_la-mech-gssapi.lo' libtool=yes @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libmech_gssapi_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o libmech_gssapi_la-mech-gssapi.lo `test -f 'mech-gssapi.c' || echo '$(srcdir)/'`mech-gssapi.c + +libpassword_la-crypt-blowfish.lo: crypt-blowfish.c +@am__fastdepCC_TRUE@ $(AM_V_CC)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(libpassword_la_CFLAGS) $(CFLAGS) -MT libpassword_la-crypt-blowfish.lo -MD -MP -MF $(DEPDIR)/libpassword_la-crypt-blowfish.Tpo -c -o libpassword_la-crypt-blowfish.lo `test -f 'crypt-blowfish.c' || echo '$(srcdir)/'`crypt-blowfish.c +@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libpassword_la-crypt-blowfish.Tpo $(DEPDIR)/libpassword_la-crypt-blowfish.Plo +@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='crypt-blowfish.c' object='libpassword_la-crypt-blowfish.lo' libtool=yes @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(libpassword_la_CFLAGS) $(CFLAGS) -c -o libpassword_la-crypt-blowfish.lo `test -f 'crypt-blowfish.c' || echo '$(srcdir)/'`crypt-blowfish.c + +libpassword_la-mycrypt.lo: mycrypt.c +@am__fastdepCC_TRUE@ $(AM_V_CC)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(libpassword_la_CFLAGS) $(CFLAGS) -MT libpassword_la-mycrypt.lo -MD -MP -MF $(DEPDIR)/libpassword_la-mycrypt.Tpo -c -o libpassword_la-mycrypt.lo `test -f 'mycrypt.c' || echo '$(srcdir)/'`mycrypt.c +@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libpassword_la-mycrypt.Tpo $(DEPDIR)/libpassword_la-mycrypt.Plo +@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='mycrypt.c' object='libpassword_la-mycrypt.lo' libtool=yes @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(libpassword_la_CFLAGS) $(CFLAGS) -c -o libpassword_la-mycrypt.lo `test -f 'mycrypt.c' || echo '$(srcdir)/'`mycrypt.c + +libpassword_la-password-scheme.lo: password-scheme.c +@am__fastdepCC_TRUE@ $(AM_V_CC)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(libpassword_la_CFLAGS) $(CFLAGS) -MT libpassword_la-password-scheme.lo -MD -MP -MF $(DEPDIR)/libpassword_la-password-scheme.Tpo -c -o libpassword_la-password-scheme.lo `test -f 'password-scheme.c' || echo '$(srcdir)/'`password-scheme.c +@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libpassword_la-password-scheme.Tpo $(DEPDIR)/libpassword_la-password-scheme.Plo +@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='password-scheme.c' object='libpassword_la-password-scheme.lo' libtool=yes @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(libpassword_la_CFLAGS) $(CFLAGS) -c -o libpassword_la-password-scheme.lo `test -f 'password-scheme.c' || echo '$(srcdir)/'`password-scheme.c + +libpassword_la-password-scheme-crypt.lo: password-scheme-crypt.c +@am__fastdepCC_TRUE@ $(AM_V_CC)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(libpassword_la_CFLAGS) $(CFLAGS) -MT libpassword_la-password-scheme-crypt.lo -MD -MP -MF $(DEPDIR)/libpassword_la-password-scheme-crypt.Tpo -c -o libpassword_la-password-scheme-crypt.lo `test -f 'password-scheme-crypt.c' || echo '$(srcdir)/'`password-scheme-crypt.c +@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libpassword_la-password-scheme-crypt.Tpo $(DEPDIR)/libpassword_la-password-scheme-crypt.Plo +@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='password-scheme-crypt.c' object='libpassword_la-password-scheme-crypt.lo' libtool=yes @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(libpassword_la_CFLAGS) $(CFLAGS) -c -o libpassword_la-password-scheme-crypt.lo `test -f 'password-scheme-crypt.c' || echo '$(srcdir)/'`password-scheme-crypt.c + +libpassword_la-password-scheme-md5crypt.lo: password-scheme-md5crypt.c +@am__fastdepCC_TRUE@ $(AM_V_CC)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(libpassword_la_CFLAGS) $(CFLAGS) -MT libpassword_la-password-scheme-md5crypt.lo -MD -MP -MF $(DEPDIR)/libpassword_la-password-scheme-md5crypt.Tpo -c -o libpassword_la-password-scheme-md5crypt.lo `test -f 'password-scheme-md5crypt.c' || echo '$(srcdir)/'`password-scheme-md5crypt.c +@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libpassword_la-password-scheme-md5crypt.Tpo $(DEPDIR)/libpassword_la-password-scheme-md5crypt.Plo +@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='password-scheme-md5crypt.c' object='libpassword_la-password-scheme-md5crypt.lo' libtool=yes @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(libpassword_la_CFLAGS) $(CFLAGS) -c -o libpassword_la-password-scheme-md5crypt.lo `test -f 'password-scheme-md5crypt.c' || echo '$(srcdir)/'`password-scheme-md5crypt.c + +libpassword_la-password-scheme-scram.lo: password-scheme-scram.c +@am__fastdepCC_TRUE@ $(AM_V_CC)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(libpassword_la_CFLAGS) $(CFLAGS) -MT libpassword_la-password-scheme-scram.lo -MD -MP -MF $(DEPDIR)/libpassword_la-password-scheme-scram.Tpo -c -o libpassword_la-password-scheme-scram.lo `test -f 'password-scheme-scram.c' || echo '$(srcdir)/'`password-scheme-scram.c +@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libpassword_la-password-scheme-scram.Tpo $(DEPDIR)/libpassword_la-password-scheme-scram.Plo +@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='password-scheme-scram.c' object='libpassword_la-password-scheme-scram.lo' libtool=yes @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(libpassword_la_CFLAGS) $(CFLAGS) -c -o libpassword_la-password-scheme-scram.lo `test -f 'password-scheme-scram.c' || echo '$(srcdir)/'`password-scheme-scram.c + +libpassword_la-password-scheme-otp.lo: password-scheme-otp.c +@am__fastdepCC_TRUE@ $(AM_V_CC)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(libpassword_la_CFLAGS) $(CFLAGS) -MT libpassword_la-password-scheme-otp.lo -MD -MP -MF $(DEPDIR)/libpassword_la-password-scheme-otp.Tpo -c -o libpassword_la-password-scheme-otp.lo `test -f 'password-scheme-otp.c' || echo '$(srcdir)/'`password-scheme-otp.c +@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libpassword_la-password-scheme-otp.Tpo $(DEPDIR)/libpassword_la-password-scheme-otp.Plo +@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='password-scheme-otp.c' object='libpassword_la-password-scheme-otp.lo' libtool=yes @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(libpassword_la_CFLAGS) $(CFLAGS) -c -o libpassword_la-password-scheme-otp.lo `test -f 'password-scheme-otp.c' || echo '$(srcdir)/'`password-scheme-otp.c + +libpassword_la-password-scheme-pbkdf2.lo: password-scheme-pbkdf2.c +@am__fastdepCC_TRUE@ $(AM_V_CC)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(libpassword_la_CFLAGS) $(CFLAGS) -MT libpassword_la-password-scheme-pbkdf2.lo -MD -MP -MF $(DEPDIR)/libpassword_la-password-scheme-pbkdf2.Tpo -c -o libpassword_la-password-scheme-pbkdf2.lo `test -f 'password-scheme-pbkdf2.c' || echo '$(srcdir)/'`password-scheme-pbkdf2.c +@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libpassword_la-password-scheme-pbkdf2.Tpo $(DEPDIR)/libpassword_la-password-scheme-pbkdf2.Plo +@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='password-scheme-pbkdf2.c' object='libpassword_la-password-scheme-pbkdf2.lo' libtool=yes @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(libpassword_la_CFLAGS) $(CFLAGS) -c -o libpassword_la-password-scheme-pbkdf2.lo `test -f 'password-scheme-pbkdf2.c' || echo '$(srcdir)/'`password-scheme-pbkdf2.c + +libpassword_la-password-scheme-sodium.lo: password-scheme-sodium.c +@am__fastdepCC_TRUE@ $(AM_V_CC)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(libpassword_la_CFLAGS) $(CFLAGS) -MT libpassword_la-password-scheme-sodium.lo -MD -MP -MF $(DEPDIR)/libpassword_la-password-scheme-sodium.Tpo -c -o libpassword_la-password-scheme-sodium.lo `test -f 'password-scheme-sodium.c' || echo '$(srcdir)/'`password-scheme-sodium.c +@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libpassword_la-password-scheme-sodium.Tpo $(DEPDIR)/libpassword_la-password-scheme-sodium.Plo +@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='password-scheme-sodium.c' object='libpassword_la-password-scheme-sodium.lo' libtool=yes @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(libpassword_la_CFLAGS) $(CFLAGS) -c -o libpassword_la-password-scheme-sodium.lo `test -f 'password-scheme-sodium.c' || echo '$(srcdir)/'`password-scheme-sodium.c + +auth-main.o: main.c +@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(auth_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT auth-main.o -MD -MP -MF $(DEPDIR)/auth-main.Tpo -c -o auth-main.o `test -f 'main.c' || echo '$(srcdir)/'`main.c +@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/auth-main.Tpo $(DEPDIR)/auth-main.Po +@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='main.c' object='auth-main.o' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(auth_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o auth-main.o `test -f 'main.c' || echo '$(srcdir)/'`main.c + +auth-main.obj: main.c +@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(auth_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT auth-main.obj -MD -MP -MF $(DEPDIR)/auth-main.Tpo -c -o auth-main.obj `if test -f 'main.c'; then $(CYGPATH_W) 'main.c'; else $(CYGPATH_W) '$(srcdir)/main.c'; fi` +@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/auth-main.Tpo $(DEPDIR)/auth-main.Po +@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='main.c' object='auth-main.obj' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(auth_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o auth-main.obj `if test -f 'main.c'; then $(CYGPATH_W) 'main.c'; else $(CYGPATH_W) '$(srcdir)/main.c'; fi` + +checkpassword_reply-checkpassword-reply.o: checkpassword-reply.c +@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(checkpassword_reply_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT checkpassword_reply-checkpassword-reply.o -MD -MP -MF $(DEPDIR)/checkpassword_reply-checkpassword-reply.Tpo -c -o checkpassword_reply-checkpassword-reply.o `test -f 'checkpassword-reply.c' || echo '$(srcdir)/'`checkpassword-reply.c +@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/checkpassword_reply-checkpassword-reply.Tpo $(DEPDIR)/checkpassword_reply-checkpassword-reply.Po +@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='checkpassword-reply.c' object='checkpassword_reply-checkpassword-reply.o' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(checkpassword_reply_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o checkpassword_reply-checkpassword-reply.o `test -f 'checkpassword-reply.c' || echo '$(srcdir)/'`checkpassword-reply.c + +checkpassword_reply-checkpassword-reply.obj: checkpassword-reply.c +@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(checkpassword_reply_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT checkpassword_reply-checkpassword-reply.obj -MD -MP -MF $(DEPDIR)/checkpassword_reply-checkpassword-reply.Tpo -c -o checkpassword_reply-checkpassword-reply.obj `if test -f 'checkpassword-reply.c'; then $(CYGPATH_W) 'checkpassword-reply.c'; else $(CYGPATH_W) '$(srcdir)/checkpassword-reply.c'; fi` +@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/checkpassword_reply-checkpassword-reply.Tpo $(DEPDIR)/checkpassword_reply-checkpassword-reply.Po +@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='checkpassword-reply.c' object='checkpassword_reply-checkpassword-reply.obj' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(checkpassword_reply_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o checkpassword_reply-checkpassword-reply.obj `if test -f 'checkpassword-reply.c'; then $(CYGPATH_W) 'checkpassword-reply.c'; else $(CYGPATH_W) '$(srcdir)/checkpassword-reply.c'; fi` + +test_auth_cache-auth-cache.o: auth-cache.c +@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_auth_cache_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_auth_cache-auth-cache.o -MD -MP -MF $(DEPDIR)/test_auth_cache-auth-cache.Tpo -c -o test_auth_cache-auth-cache.o `test -f 'auth-cache.c' || echo '$(srcdir)/'`auth-cache.c +@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_auth_cache-auth-cache.Tpo $(DEPDIR)/test_auth_cache-auth-cache.Po +@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='auth-cache.c' object='test_auth_cache-auth-cache.o' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_auth_cache_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_auth_cache-auth-cache.o `test -f 'auth-cache.c' || echo '$(srcdir)/'`auth-cache.c + +test_auth_cache-auth-cache.obj: auth-cache.c +@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_auth_cache_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_auth_cache-auth-cache.obj -MD -MP -MF $(DEPDIR)/test_auth_cache-auth-cache.Tpo -c -o test_auth_cache-auth-cache.obj `if test -f 'auth-cache.c'; then $(CYGPATH_W) 'auth-cache.c'; else $(CYGPATH_W) '$(srcdir)/auth-cache.c'; fi` +@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_auth_cache-auth-cache.Tpo $(DEPDIR)/test_auth_cache-auth-cache.Po +@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='auth-cache.c' object='test_auth_cache-auth-cache.obj' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_auth_cache_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_auth_cache-auth-cache.obj `if test -f 'auth-cache.c'; then $(CYGPATH_W) 'auth-cache.c'; else $(CYGPATH_W) '$(srcdir)/auth-cache.c'; fi` + +test_auth_cache-test-auth-cache.o: test-auth-cache.c +@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_auth_cache_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_auth_cache-test-auth-cache.o -MD -MP -MF $(DEPDIR)/test_auth_cache-test-auth-cache.Tpo -c -o test_auth_cache-test-auth-cache.o `test -f 'test-auth-cache.c' || echo '$(srcdir)/'`test-auth-cache.c +@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_auth_cache-test-auth-cache.Tpo $(DEPDIR)/test_auth_cache-test-auth-cache.Po +@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-auth-cache.c' object='test_auth_cache-test-auth-cache.o' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_auth_cache_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_auth_cache-test-auth-cache.o `test -f 'test-auth-cache.c' || echo '$(srcdir)/'`test-auth-cache.c + +test_auth_cache-test-auth-cache.obj: test-auth-cache.c +@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_auth_cache_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_auth_cache-test-auth-cache.obj -MD -MP -MF $(DEPDIR)/test_auth_cache-test-auth-cache.Tpo -c -o test_auth_cache-test-auth-cache.obj `if test -f 'test-auth-cache.c'; then $(CYGPATH_W) 'test-auth-cache.c'; else $(CYGPATH_W) '$(srcdir)/test-auth-cache.c'; fi` +@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_auth_cache-test-auth-cache.Tpo $(DEPDIR)/test_auth_cache-test-auth-cache.Po +@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-auth-cache.c' object='test_auth_cache-test-auth-cache.obj' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_auth_cache_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_auth_cache-test-auth-cache.obj `if test -f 'test-auth-cache.c'; then $(CYGPATH_W) 'test-auth-cache.c'; else $(CYGPATH_W) '$(srcdir)/test-auth-cache.c'; fi` + +test_libpassword-test-libpassword.o: test-libpassword.c +@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_libpassword_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_libpassword-test-libpassword.o -MD -MP -MF $(DEPDIR)/test_libpassword-test-libpassword.Tpo -c -o test_libpassword-test-libpassword.o `test -f 'test-libpassword.c' || echo '$(srcdir)/'`test-libpassword.c +@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_libpassword-test-libpassword.Tpo $(DEPDIR)/test_libpassword-test-libpassword.Po +@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-libpassword.c' object='test_libpassword-test-libpassword.o' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_libpassword_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_libpassword-test-libpassword.o `test -f 'test-libpassword.c' || echo '$(srcdir)/'`test-libpassword.c + +test_libpassword-test-libpassword.obj: test-libpassword.c +@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_libpassword_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_libpassword-test-libpassword.obj -MD -MP -MF $(DEPDIR)/test_libpassword-test-libpassword.Tpo -c -o test_libpassword-test-libpassword.obj `if test -f 'test-libpassword.c'; then $(CYGPATH_W) 'test-libpassword.c'; else $(CYGPATH_W) '$(srcdir)/test-libpassword.c'; fi` +@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_libpassword-test-libpassword.Tpo $(DEPDIR)/test_libpassword-test-libpassword.Po +@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-libpassword.c' object='test_libpassword-test-libpassword.obj' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_libpassword_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_libpassword-test-libpassword.obj `if test -f 'test-libpassword.c'; then $(CYGPATH_W) 'test-libpassword.c'; else $(CYGPATH_W) '$(srcdir)/test-libpassword.c'; fi` + +mostlyclean-libtool: + -rm -f *.lo + +clean-libtool: + -rm -rf .libs _libs +install-pkginc_libHEADERS: $(pkginc_lib_HEADERS) + @$(NORMAL_INSTALL) + @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \ + if test -n "$$list"; then \ + echo " $(MKDIR_P) '$(DESTDIR)$(pkginc_libdir)'"; \ + $(MKDIR_P) "$(DESTDIR)$(pkginc_libdir)" || exit 1; \ + fi; \ + for p in $$list; do \ + if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \ + echo "$$d$$p"; \ + done | $(am__base_list) | \ + while read files; do \ + echo " $(INSTALL_HEADER) $$files '$(DESTDIR)$(pkginc_libdir)'"; \ + $(INSTALL_HEADER) $$files "$(DESTDIR)$(pkginc_libdir)" || exit $$?; \ + done + +uninstall-pkginc_libHEADERS: + @$(NORMAL_UNINSTALL) + @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \ + files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \ + dir='$(DESTDIR)$(pkginc_libdir)'; $(am__uninstall_files_from_dir) + +ID: $(am__tagged_files) + $(am__define_uniq_tagged_files); mkid -fID $$unique +tags: tags-am +TAGS: tags + +tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files) + set x; \ + here=`pwd`; \ + $(am__define_uniq_tagged_files); \ + shift; \ + if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \ + test -n "$$unique" || unique=$$empty_fix; \ + if test $$# -gt 0; then \ + $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \ + "$$@" $$unique; \ + else \ + $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \ + $$unique; \ + fi; \ + fi +ctags: ctags-am + +CTAGS: ctags +ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files) + $(am__define_uniq_tagged_files); \ + test -z "$(CTAGS_ARGS)$$unique" \ + || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \ + $$unique + +GTAGS: + here=`$(am__cd) $(top_builddir) && pwd` \ + && $(am__cd) $(top_srcdir) \ + && gtags -i $(GTAGS_ARGS) "$$here" +cscopelist: cscopelist-am + +cscopelist-am: $(am__tagged_files) + list='$(am__tagged_files)'; \ + case "$(srcdir)" in \ + [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \ + *) sdir=$(subdir)/$(srcdir) ;; \ + esac; \ + for i in $$list; do \ + if test -f "$$i"; then \ + echo "$(subdir)/$$i"; \ + else \ + echo "$$sdir/$$i"; \ + fi; \ + done >> $(top_builddir)/cscope.files + +distclean-tags: + -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags + +distdir: $(BUILT_SOURCES) + $(MAKE) $(AM_MAKEFLAGS) distdir-am + +distdir-am: $(DISTFILES) + @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \ + topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \ + list='$(DISTFILES)'; \ + dist_files=`for file in $$list; do echo $$file; done | \ + sed -e "s|^$$srcdirstrip/||;t" \ + -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \ + case $$dist_files in \ + */*) $(MKDIR_P) `echo "$$dist_files" | \ + sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \ + sort -u` ;; \ + esac; \ + for file in $$dist_files; do \ + if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \ + if test -d $$d/$$file; then \ + dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \ + if test -d "$(distdir)/$$file"; then \ + find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \ + fi; \ + if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \ + cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \ + find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \ + fi; \ + cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \ + else \ + test -f "$(distdir)/$$file" \ + || cp -p $$d/$$file "$(distdir)/$$file" \ + || exit 1; \ + fi; \ + done +check-am: all-am + $(MAKE) $(AM_MAKEFLAGS) check-local +check: check-am +all-am: Makefile $(PROGRAMS) $(LTLIBRARIES) $(HEADERS) +installdirs: + for dir in "$(DESTDIR)$(pkglibexecdir)" "$(DESTDIR)$(auth_moduledir)" "$(DESTDIR)$(stats_moduledir)" "$(DESTDIR)$(pkginc_libdir)"; do \ + test -z "$$dir" || $(MKDIR_P) "$$dir"; \ + done +install: install-am +install-exec: install-exec-am +install-data: install-data-am +uninstall: uninstall-am + +install-am: all-am + @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am + +installcheck: installcheck-am +install-strip: + if test -z '$(STRIP)'; then \ + $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \ + install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \ + install; \ + else \ + $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \ + install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \ + "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \ + fi +mostlyclean-generic: + +clean-generic: + +distclean-generic: + -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES) + -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES) + +maintainer-clean-generic: + @echo "This command is intended for maintainers to use" + @echo "it deletes files that may require special tools to rebuild." +clean: clean-am + +clean-am: clean-auth_moduleLTLIBRARIES clean-generic clean-libtool \ + clean-noinstLTLIBRARIES clean-noinstPROGRAMS \ + clean-pkglibexecPROGRAMS clean-stats_moduleLTLIBRARIES \ + mostlyclean-am + +distclean: distclean-am + -rm -f ./$(DEPDIR)/auth-cache.Plo + -rm -f ./$(DEPDIR)/auth-client-connection.Plo + -rm -f ./$(DEPDIR)/auth-fields.Plo + -rm -f ./$(DEPDIR)/auth-main.Po + -rm -f ./$(DEPDIR)/auth-master-connection.Plo + -rm -f ./$(DEPDIR)/auth-penalty.Plo + -rm -f ./$(DEPDIR)/auth-policy.Plo + -rm -f ./$(DEPDIR)/auth-request-fields.Plo + -rm -f ./$(DEPDIR)/auth-request-handler.Plo + -rm -f ./$(DEPDIR)/auth-request-stats.Plo + -rm -f ./$(DEPDIR)/auth-request-var-expand.Plo + -rm -f ./$(DEPDIR)/auth-request.Plo + -rm -f ./$(DEPDIR)/auth-settings.Plo + -rm -f ./$(DEPDIR)/auth-stats.Plo + -rm -f ./$(DEPDIR)/auth-token.Plo + -rm -f ./$(DEPDIR)/auth-worker-client.Plo + -rm -f ./$(DEPDIR)/auth-worker-server.Plo + -rm -f ./$(DEPDIR)/auth.Plo + -rm -f ./$(DEPDIR)/checkpassword_reply-checkpassword-reply.Po + -rm -f ./$(DEPDIR)/db-checkpassword.Plo + -rm -f ./$(DEPDIR)/db-dict-cache-key.Plo + -rm -f ./$(DEPDIR)/db-dict.Plo + -rm -f ./$(DEPDIR)/db-ldap.Plo + -rm -f ./$(DEPDIR)/db-lua.Plo + -rm -f ./$(DEPDIR)/db-oauth2.Plo + -rm -f ./$(DEPDIR)/db-passwd-file.Plo + -rm -f ./$(DEPDIR)/db-sql.Plo + -rm -f ./$(DEPDIR)/libauthdb_imap_la-passdb-imap.Plo + -rm -f ./$(DEPDIR)/libauthdb_ldap_la-db-ldap.Plo + -rm -f ./$(DEPDIR)/libauthdb_ldap_la-passdb-ldap.Plo + -rm -f ./$(DEPDIR)/libauthdb_ldap_la-userdb-ldap.Plo + -rm -f ./$(DEPDIR)/libauthdb_lua_la-db-lua.Plo + -rm -f ./$(DEPDIR)/libauthdb_lua_la-passdb-lua.Plo + -rm -f ./$(DEPDIR)/libauthdb_lua_la-userdb-lua.Plo + -rm -f ./$(DEPDIR)/libmech_gssapi_la-mech-gssapi.Plo + -rm -f ./$(DEPDIR)/libpassword_la-crypt-blowfish.Plo + -rm -f ./$(DEPDIR)/libpassword_la-mycrypt.Plo + -rm -f ./$(DEPDIR)/libpassword_la-password-scheme-crypt.Plo + -rm -f ./$(DEPDIR)/libpassword_la-password-scheme-md5crypt.Plo + -rm -f ./$(DEPDIR)/libpassword_la-password-scheme-otp.Plo + -rm -f ./$(DEPDIR)/libpassword_la-password-scheme-pbkdf2.Plo + -rm -f ./$(DEPDIR)/libpassword_la-password-scheme-scram.Plo + -rm -f ./$(DEPDIR)/libpassword_la-password-scheme-sodium.Plo + -rm -f ./$(DEPDIR)/libpassword_la-password-scheme.Plo + -rm -f ./$(DEPDIR)/mech-anonymous.Plo + -rm -f ./$(DEPDIR)/mech-apop.Plo + -rm -f ./$(DEPDIR)/mech-cram-md5.Plo + -rm -f ./$(DEPDIR)/mech-digest-md5.Plo + -rm -f ./$(DEPDIR)/mech-dovecot-token.Plo + -rm -f ./$(DEPDIR)/mech-external.Plo + -rm -f ./$(DEPDIR)/mech-gssapi.Plo + -rm -f ./$(DEPDIR)/mech-login.Plo + -rm -f ./$(DEPDIR)/mech-oauth2.Plo + -rm -f ./$(DEPDIR)/mech-otp-common.Plo + -rm -f ./$(DEPDIR)/mech-otp.Plo + -rm -f ./$(DEPDIR)/mech-plain-common.Plo + -rm -f ./$(DEPDIR)/mech-plain.Plo + -rm -f ./$(DEPDIR)/mech-scram.Plo + -rm -f ./$(DEPDIR)/mech-winbind.Plo + -rm -f ./$(DEPDIR)/mech.Plo + -rm -f ./$(DEPDIR)/passdb-blocking.Plo + -rm -f ./$(DEPDIR)/passdb-bsdauth.Plo + -rm -f ./$(DEPDIR)/passdb-cache.Plo + -rm -f ./$(DEPDIR)/passdb-checkpassword.Plo + -rm -f ./$(DEPDIR)/passdb-dict.Plo + -rm -f ./$(DEPDIR)/passdb-ldap.Plo + -rm -f ./$(DEPDIR)/passdb-lua.Plo + -rm -f ./$(DEPDIR)/passdb-oauth2.Plo + -rm -f ./$(DEPDIR)/passdb-pam.Plo + -rm -f ./$(DEPDIR)/passdb-passwd-file.Plo + -rm -f ./$(DEPDIR)/passdb-passwd.Plo + -rm -f ./$(DEPDIR)/passdb-shadow.Plo + -rm -f ./$(DEPDIR)/passdb-sql.Plo + -rm -f ./$(DEPDIR)/passdb-static.Plo + -rm -f ./$(DEPDIR)/passdb-template.Plo + -rm -f ./$(DEPDIR)/passdb.Plo + -rm -f ./$(DEPDIR)/test-auth-request-fields.Po + -rm -f ./$(DEPDIR)/test-auth-request-var-expand.Po + -rm -f ./$(DEPDIR)/test-db-dict.Po + -rm -f ./$(DEPDIR)/test-lua.Po + -rm -f ./$(DEPDIR)/test-main.Po + -rm -f ./$(DEPDIR)/test-mech.Po + -rm -f ./$(DEPDIR)/test-mock.Po + -rm -f ./$(DEPDIR)/test-username-filter.Po + -rm -f ./$(DEPDIR)/test_auth_cache-auth-cache.Po + -rm -f ./$(DEPDIR)/test_auth_cache-test-auth-cache.Po + -rm -f ./$(DEPDIR)/test_libpassword-test-libpassword.Po + -rm -f ./$(DEPDIR)/userdb-blocking.Plo + -rm -f ./$(DEPDIR)/userdb-checkpassword.Plo + -rm -f ./$(DEPDIR)/userdb-dict.Plo + -rm -f ./$(DEPDIR)/userdb-ldap.Plo + -rm -f ./$(DEPDIR)/userdb-lua.Plo + -rm -f ./$(DEPDIR)/userdb-passwd-file.Plo + -rm -f ./$(DEPDIR)/userdb-passwd.Plo + -rm -f ./$(DEPDIR)/userdb-prefetch.Plo + -rm -f ./$(DEPDIR)/userdb-sql.Plo + -rm -f ./$(DEPDIR)/userdb-static.Plo + -rm -f ./$(DEPDIR)/userdb-template.Plo + -rm -f ./$(DEPDIR)/userdb.Plo + -rm -f Makefile +distclean-am: clean-am distclean-compile distclean-generic \ + distclean-tags + +dvi: dvi-am + +dvi-am: + +html: html-am + +html-am: + +info: info-am + +info-am: + +install-data-am: install-auth_moduleLTLIBRARIES \ + install-pkginc_libHEADERS install-stats_moduleLTLIBRARIES + +install-dvi: install-dvi-am + +install-dvi-am: + +install-exec-am: install-pkglibexecPROGRAMS + +install-html: install-html-am + +install-html-am: + +install-info: install-info-am + +install-info-am: + +install-man: + +install-pdf: install-pdf-am + +install-pdf-am: + +install-ps: install-ps-am + +install-ps-am: + +installcheck-am: + +maintainer-clean: maintainer-clean-am + -rm -f ./$(DEPDIR)/auth-cache.Plo + -rm -f ./$(DEPDIR)/auth-client-connection.Plo + -rm -f ./$(DEPDIR)/auth-fields.Plo + -rm -f ./$(DEPDIR)/auth-main.Po + -rm -f ./$(DEPDIR)/auth-master-connection.Plo + -rm -f ./$(DEPDIR)/auth-penalty.Plo + -rm -f ./$(DEPDIR)/auth-policy.Plo + -rm -f ./$(DEPDIR)/auth-request-fields.Plo + -rm -f ./$(DEPDIR)/auth-request-handler.Plo + -rm -f ./$(DEPDIR)/auth-request-stats.Plo + -rm -f ./$(DEPDIR)/auth-request-var-expand.Plo + -rm -f ./$(DEPDIR)/auth-request.Plo + -rm -f ./$(DEPDIR)/auth-settings.Plo + -rm -f ./$(DEPDIR)/auth-stats.Plo + -rm -f ./$(DEPDIR)/auth-token.Plo + -rm -f ./$(DEPDIR)/auth-worker-client.Plo + -rm -f ./$(DEPDIR)/auth-worker-server.Plo + -rm -f ./$(DEPDIR)/auth.Plo + -rm -f ./$(DEPDIR)/checkpassword_reply-checkpassword-reply.Po + -rm -f ./$(DEPDIR)/db-checkpassword.Plo + -rm -f ./$(DEPDIR)/db-dict-cache-key.Plo + -rm -f ./$(DEPDIR)/db-dict.Plo + -rm -f ./$(DEPDIR)/db-ldap.Plo + -rm -f ./$(DEPDIR)/db-lua.Plo + -rm -f ./$(DEPDIR)/db-oauth2.Plo + -rm -f ./$(DEPDIR)/db-passwd-file.Plo + -rm -f ./$(DEPDIR)/db-sql.Plo + -rm -f ./$(DEPDIR)/libauthdb_imap_la-passdb-imap.Plo + -rm -f ./$(DEPDIR)/libauthdb_ldap_la-db-ldap.Plo + -rm -f ./$(DEPDIR)/libauthdb_ldap_la-passdb-ldap.Plo + -rm -f ./$(DEPDIR)/libauthdb_ldap_la-userdb-ldap.Plo + -rm -f ./$(DEPDIR)/libauthdb_lua_la-db-lua.Plo + -rm -f ./$(DEPDIR)/libauthdb_lua_la-passdb-lua.Plo + -rm -f ./$(DEPDIR)/libauthdb_lua_la-userdb-lua.Plo + -rm -f ./$(DEPDIR)/libmech_gssapi_la-mech-gssapi.Plo + -rm -f ./$(DEPDIR)/libpassword_la-crypt-blowfish.Plo + -rm -f ./$(DEPDIR)/libpassword_la-mycrypt.Plo + -rm -f ./$(DEPDIR)/libpassword_la-password-scheme-crypt.Plo + -rm -f ./$(DEPDIR)/libpassword_la-password-scheme-md5crypt.Plo + -rm -f ./$(DEPDIR)/libpassword_la-password-scheme-otp.Plo + -rm -f ./$(DEPDIR)/libpassword_la-password-scheme-pbkdf2.Plo + -rm -f ./$(DEPDIR)/libpassword_la-password-scheme-scram.Plo + -rm -f ./$(DEPDIR)/libpassword_la-password-scheme-sodium.Plo + -rm -f ./$(DEPDIR)/libpassword_la-password-scheme.Plo + -rm -f ./$(DEPDIR)/mech-anonymous.Plo + -rm -f ./$(DEPDIR)/mech-apop.Plo + -rm -f ./$(DEPDIR)/mech-cram-md5.Plo + -rm -f ./$(DEPDIR)/mech-digest-md5.Plo + -rm -f ./$(DEPDIR)/mech-dovecot-token.Plo + -rm -f ./$(DEPDIR)/mech-external.Plo + -rm -f ./$(DEPDIR)/mech-gssapi.Plo + -rm -f ./$(DEPDIR)/mech-login.Plo + -rm -f ./$(DEPDIR)/mech-oauth2.Plo + -rm -f ./$(DEPDIR)/mech-otp-common.Plo + -rm -f ./$(DEPDIR)/mech-otp.Plo + -rm -f ./$(DEPDIR)/mech-plain-common.Plo + -rm -f ./$(DEPDIR)/mech-plain.Plo + -rm -f ./$(DEPDIR)/mech-scram.Plo + -rm -f ./$(DEPDIR)/mech-winbind.Plo + -rm -f ./$(DEPDIR)/mech.Plo + -rm -f ./$(DEPDIR)/passdb-blocking.Plo + -rm -f ./$(DEPDIR)/passdb-bsdauth.Plo + -rm -f ./$(DEPDIR)/passdb-cache.Plo + -rm -f ./$(DEPDIR)/passdb-checkpassword.Plo + -rm -f ./$(DEPDIR)/passdb-dict.Plo + -rm -f ./$(DEPDIR)/passdb-ldap.Plo + -rm -f ./$(DEPDIR)/passdb-lua.Plo + -rm -f ./$(DEPDIR)/passdb-oauth2.Plo + -rm -f ./$(DEPDIR)/passdb-pam.Plo + -rm -f ./$(DEPDIR)/passdb-passwd-file.Plo + -rm -f ./$(DEPDIR)/passdb-passwd.Plo + -rm -f ./$(DEPDIR)/passdb-shadow.Plo + -rm -f ./$(DEPDIR)/passdb-sql.Plo + -rm -f ./$(DEPDIR)/passdb-static.Plo + -rm -f ./$(DEPDIR)/passdb-template.Plo + -rm -f ./$(DEPDIR)/passdb.Plo + -rm -f ./$(DEPDIR)/test-auth-request-fields.Po + -rm -f ./$(DEPDIR)/test-auth-request-var-expand.Po + -rm -f ./$(DEPDIR)/test-db-dict.Po + -rm -f ./$(DEPDIR)/test-lua.Po + -rm -f ./$(DEPDIR)/test-main.Po + -rm -f ./$(DEPDIR)/test-mech.Po + -rm -f ./$(DEPDIR)/test-mock.Po + -rm -f ./$(DEPDIR)/test-username-filter.Po + -rm -f ./$(DEPDIR)/test_auth_cache-auth-cache.Po + -rm -f ./$(DEPDIR)/test_auth_cache-test-auth-cache.Po + -rm -f ./$(DEPDIR)/test_libpassword-test-libpassword.Po + -rm -f ./$(DEPDIR)/userdb-blocking.Plo + -rm -f ./$(DEPDIR)/userdb-checkpassword.Plo + -rm -f ./$(DEPDIR)/userdb-dict.Plo + -rm -f ./$(DEPDIR)/userdb-ldap.Plo + -rm -f ./$(DEPDIR)/userdb-lua.Plo + -rm -f ./$(DEPDIR)/userdb-passwd-file.Plo + -rm -f ./$(DEPDIR)/userdb-passwd.Plo + -rm -f ./$(DEPDIR)/userdb-prefetch.Plo + -rm -f ./$(DEPDIR)/userdb-sql.Plo + -rm -f ./$(DEPDIR)/userdb-static.Plo + -rm -f ./$(DEPDIR)/userdb-template.Plo + -rm -f ./$(DEPDIR)/userdb.Plo + -rm -f Makefile +maintainer-clean-am: distclean-am maintainer-clean-generic + +mostlyclean: mostlyclean-am + +mostlyclean-am: mostlyclean-compile mostlyclean-generic \ + mostlyclean-libtool + +pdf: pdf-am + +pdf-am: + +ps: ps-am + +ps-am: + +uninstall-am: uninstall-auth_moduleLTLIBRARIES \ + uninstall-pkginc_libHEADERS uninstall-pkglibexecPROGRAMS \ + uninstall-stats_moduleLTLIBRARIES + +.MAKE: check-am install-am install-strip + +.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am \ + check-local clean clean-auth_moduleLTLIBRARIES clean-generic \ + clean-libtool clean-noinstLTLIBRARIES clean-noinstPROGRAMS \ + clean-pkglibexecPROGRAMS clean-stats_moduleLTLIBRARIES \ + cscopelist-am ctags ctags-am distclean distclean-compile \ + distclean-generic distclean-libtool distclean-tags distdir dvi \ + dvi-am html html-am info info-am install install-am \ + install-auth_moduleLTLIBRARIES install-data install-data-am \ + install-dvi install-dvi-am install-exec install-exec-am \ + install-html install-html-am install-info install-info-am \ + install-man install-pdf install-pdf-am \ + install-pkginc_libHEADERS install-pkglibexecPROGRAMS \ + install-ps install-ps-am install-stats_moduleLTLIBRARIES \ + install-strip installcheck installcheck-am installdirs \ + maintainer-clean maintainer-clean-generic mostlyclean \ + mostlyclean-compile mostlyclean-generic mostlyclean-libtool \ + pdf pdf-am ps ps-am tags tags-am uninstall uninstall-am \ + uninstall-auth_moduleLTLIBRARIES uninstall-pkginc_libHEADERS \ + uninstall-pkglibexecPROGRAMS uninstall-stats_moduleLTLIBRARIES + +.PRECIOUS: Makefile + + +check-local: + for bin in $(test_programs); do \ + if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \ + done + +# Tell versions [3.59,3.63) of GNU make to not export all variables. +# Otherwise a system limit (for SysV at least) may be exceeded. +.NOEXPORT: diff --git a/src/auth/auth-cache.c b/src/auth/auth-cache.c new file mode 100644 index 0000000..e8aa105 --- /dev/null +++ b/src/auth/auth-cache.c @@ -0,0 +1,481 @@ +/* Copyright (c) 2004-2018 Dovecot authors, see the included COPYING file */ + +#include "auth-common.h" +#include "lib-signals.h" +#include "hash.h" +#include "str.h" +#include "strescape.h" +#include "var-expand.h" +#include "auth-request.h" +#include "auth-cache.h" + +#include <time.h> + +struct auth_cache { + HASH_TABLE(char *, struct auth_cache_node *) hash; + struct auth_cache_node *head, *tail; + + size_t max_size, size_left; + unsigned int ttl_secs, neg_ttl_secs; + + unsigned int hit_count, miss_count; + unsigned int pos_entries, neg_entries; + unsigned long long pos_size, neg_size; +}; + +static bool +auth_request_var_expand_tab_find(const char *key, unsigned int size, + unsigned int *idx_r) +{ + const struct var_expand_table *tab = auth_request_var_expand_static_tab; + unsigned int i; + + for (i = 0; tab[i].key != '\0' || tab[i].long_key != NULL; i++) { + if (size == 1) { + if (key[0] == tab[i].key) { + *idx_r = i; + return TRUE; + } + } else if (tab[i].long_key != NULL) { + if (strncmp(key, tab[i].long_key, size) == 0 && + tab[i].long_key[size] == '\0') { + *idx_r = i; + return TRUE; + } + } + } + return FALSE; +} + +static void +auth_cache_key_add_var(string_t *str, const char *data, unsigned int len) +{ + if (str_len(str) > 0) + str_append_c(str, '\t'); + str_append_c(str, '%'); + if (len == 1) + str_append_c(str, data[0]); + else { + str_append_c(str, '{'); + str_append_data(str, data, len); + str_append_c(str, '}'); + } +} + +static void auth_cache_key_add_tab_idx(string_t *str, unsigned int i) +{ + const struct var_expand_table *tab = + &auth_request_var_expand_static_tab[i]; + + if (str_len(str) > 0) + str_append_c(str, '\t'); + str_append_c(str, '%'); + if (tab->key != '\0') + str_append_c(str, tab->key); + else { + str_append_c(str, '{'); + str_append(str, tab->long_key); + str_append_c(str, '}'); + } +} + +char *auth_cache_parse_key(pool_t pool, const char *query) +{ + string_t *str; + bool key_seen[AUTH_REQUEST_VAR_TAB_COUNT]; + const char *extra_vars; + unsigned int i, idx, size, tab_idx; + + memset(key_seen, 0, sizeof(key_seen)); + + str = t_str_new(32); + for (; *query != '\0'; ) { + if (*query != '%') { + query++; + continue; + } + + var_get_key_range(++query, &idx, &size); + if (size == 0) { + /* broken %variable ending too early */ + break; + } + query += idx; + + if (!auth_request_var_expand_tab_find(query, size, &tab_idx)) { + /* just add the key. it would be nice to prevent + duplicates here as well, but that's just too + much trouble and probably very rare. */ + auth_cache_key_add_var(str, query, size); + } else { + i_assert(tab_idx < N_ELEMENTS(key_seen)); + key_seen[tab_idx] = TRUE; + } + query += size; + } + + if (key_seen[AUTH_REQUEST_VAR_TAB_USERNAME_IDX] && + key_seen[AUTH_REQUEST_VAR_TAB_DOMAIN_IDX]) { + /* %n and %d both used -> replace with %u */ + key_seen[AUTH_REQUEST_VAR_TAB_USER_IDX] = TRUE; + key_seen[AUTH_REQUEST_VAR_TAB_USERNAME_IDX] = FALSE; + key_seen[AUTH_REQUEST_VAR_TAB_DOMAIN_IDX] = FALSE; + } + + /* we rely on these being at the beginning */ + i_assert(AUTH_REQUEST_VAR_TAB_USER_IDX == 0); + i_assert(AUTH_REQUEST_VAR_TAB_USERNAME_IDX == 1); + i_assert(AUTH_REQUEST_VAR_TAB_DOMAIN_IDX == 2); + + extra_vars = t_strdup(str_c(str)); + str_truncate(str, 0); + for (i = 0; i < N_ELEMENTS(key_seen); i++) { + if (key_seen[i]) + auth_cache_key_add_tab_idx(str, i); + } + + if (*extra_vars != '\0') { + if (str_len(str) > 0) + str_append_c(str, '\t'); + str_append(str, extra_vars); + } + + return p_strdup(pool, str_c(str)); +} + +static void +auth_cache_node_unlink(struct auth_cache *cache, struct auth_cache_node *node) +{ + if (node->prev != NULL) + node->prev->next = node->next; + else { + /* unlinking tail */ + cache->tail = node->next; + } + + if (node->next != NULL) + node->next->prev = node->prev; + else { + /* unlinking head */ + cache->head = node->prev; + } +} + +static void +auth_cache_node_link_head(struct auth_cache *cache, + struct auth_cache_node *node) +{ + node->prev = cache->head; + node->next = NULL; + + cache->head = node; + if (node->prev != NULL) + node->prev->next = node; + else + cache->tail = node; +} + +static void +auth_cache_node_destroy(struct auth_cache *cache, struct auth_cache_node *node) +{ + char *key = node->data; + + auth_cache_node_unlink(cache, node); + + cache->size_left += node->alloc_size; + hash_table_remove(cache->hash, key); + i_free(node); +} + +static void sig_auth_cache_clear(const siginfo_t *si ATTR_UNUSED, void *context) +{ + struct auth_cache *cache = context; + + i_info("SIGHUP received, %u cache entries flushed", + auth_cache_clear(cache)); +} + +static void sig_auth_cache_stats(const siginfo_t *si ATTR_UNUSED, void *context) +{ + struct auth_cache *cache = context; + unsigned int total_count; + size_t cache_used; + + total_count = cache->hit_count + cache->miss_count; + i_info("Authentication cache hits %u/%u (%u%%)", + cache->hit_count, total_count, + total_count == 0 ? 100 : (cache->hit_count * 100 / total_count)); + + i_info("Authentication cache inserts: " + "positive: %u entries %llu bytes, " + "negative: %u entries %llu bytes", + cache->pos_entries, cache->pos_size, + cache->neg_entries, cache->neg_size); + + cache_used = cache->max_size - cache->size_left; + i_info("Authentication cache current size: " + "%zu bytes used of %zu bytes (%u%%)", + cache_used, cache->max_size, + (unsigned int)(cache_used * 100ULL / cache->max_size)); + + /* reset counters */ + cache->hit_count = cache->miss_count = 0; + cache->pos_entries = cache->neg_entries = 0; + cache->pos_size = cache->neg_size = 0; +} + +struct auth_cache *auth_cache_new(size_t max_size, unsigned int ttl_secs, + unsigned int neg_ttl_secs +) +{ + struct auth_cache *cache; + + cache = i_new(struct auth_cache, 1); + hash_table_create(&cache->hash, default_pool, 0, str_hash, strcmp); + cache->max_size = max_size; + cache->size_left = max_size; + cache->ttl_secs = ttl_secs; + cache->neg_ttl_secs = neg_ttl_secs; + + lib_signals_set_handler(SIGHUP, LIBSIG_FLAGS_SAFE, + sig_auth_cache_clear, cache); + lib_signals_set_handler(SIGUSR2, LIBSIG_FLAGS_SAFE, + sig_auth_cache_stats, cache); + return cache; +} + +void auth_cache_free(struct auth_cache **_cache) +{ + struct auth_cache *cache = *_cache; + + *_cache = NULL; + lib_signals_unset_handler(SIGHUP, sig_auth_cache_clear, cache); + lib_signals_unset_handler(SIGUSR2, sig_auth_cache_stats, cache); + + auth_cache_clear(cache); + hash_table_destroy(&cache->hash); + i_free(cache); +} + +unsigned int auth_cache_clear(struct auth_cache *cache) +{ + unsigned int ret = hash_table_count(cache->hash); + + while (cache->tail != NULL) + auth_cache_node_destroy(cache, cache->tail); + hash_table_clear(cache->hash, FALSE); + return ret; +} + +static bool auth_cache_node_is_user(struct auth_cache_node *node, + const char *username) +{ + const char *data = node->data; + size_t username_len; + + /* The cache nodes begin with "P"/"U", passdb/userdb ID, optional + "+" master user, "\t" and then usually followed by the username. + It's too much trouble to keep track of all the cache keys, so we'll + just match it as if it was the username. If e.g. '%n' is used in the + cache key instead of '%u', it means that cache entries can be + removed only when @domain isn't in the username parameter. */ + if (*data != 'P' && *data != 'U') + return FALSE; + data++; + + while (*data >= '0' && *data <= '9') + data++; + if (*data == '+') { + /* skip over +master_user */ + while (*data != '\t' && *data != '\0') + data++; + } + if (*data != '\t') + return FALSE; + data++; + + username_len = strlen(username); + return str_begins(data, username) && + (data[username_len] == '\t' || data[username_len] == '\0'); +} + +static bool auth_cache_node_is_one_of_users(struct auth_cache_node *node, + const char *const *usernames) +{ + unsigned int i; + + for (i = 0; usernames[i] != NULL; i++) { + if (auth_cache_node_is_user(node, usernames[i])) + return TRUE; + } + return FALSE; +} + +unsigned int auth_cache_clear_users(struct auth_cache *cache, + const char *const *usernames) +{ + struct auth_cache_node *node, *next; + unsigned int ret = 0; + + for (node = cache->tail; node != NULL; node = next) { + next = node->next; + if (auth_cache_node_is_one_of_users(node, usernames)) { + auth_cache_node_destroy(cache, node); + ret++; + } + } + return ret; +} + +static const char * +auth_cache_escape(const char *string, + const struct auth_request *auth_request ATTR_UNUSED) +{ + /* cache key %variables are separated by tabs, make sure that there + are no tabs in the string */ + return str_tabescape(string); +} + +static const char * +auth_request_expand_cache_key(const struct auth_request *request, + const char *key, const char *username) +{ + static bool error_logged = FALSE; + const char *error; + + /* Uniquely identify the request's passdb/userdb with the P/U prefix + and by "%!", which expands to the passdb/userdb ID number. */ + key = t_strconcat(request->userdb_lookup ? "U" : "P", "%!", + request->fields.master_user == NULL ? "" : "+%{master_user}", + "\t", key, NULL); + + /* It's fine to have unknown %variables in the cache key. + For example db-ldap can have pass_attrs containing + %{ldap:fields} which are used for output, not as part of + the input needed for cache_key. Those could in theory be + filtered out early in the cache_key, but that gets more + problematic when it needs to support also filtering out + e.g. %{sha256:ldap:fields}. */ + string_t *value = t_str_new(128); + unsigned int count = 0; + const struct var_expand_table *table = + auth_request_get_var_expand_table_full(request, + username, auth_cache_escape, &count); + if (auth_request_var_expand_with_table(value, key, request, table, + auth_cache_escape, &error) < 0 && + !error_logged) { + error_logged = TRUE; + e_error(authdb_event(request), + "Failed to expand auth cache key %s: %s", key, error); + } + return str_c(value); +} + +const char * +auth_cache_lookup(struct auth_cache *cache, const struct auth_request *request, + const char *key, struct auth_cache_node **node_r, + bool *expired_r, bool *neg_expired_r) +{ + struct auth_cache_node *node; + const char *value; + unsigned int ttl_secs; + time_t now; + + *expired_r = FALSE; + *neg_expired_r = FALSE; + + key = auth_request_expand_cache_key(request, key, request->fields.translated_username); + node = hash_table_lookup(cache->hash, key); + if (node == NULL) { + cache->miss_count++; + return NULL; + } + + value = node->data + strlen(node->data) + 1; + ttl_secs = *value == '\0' ? cache->neg_ttl_secs : cache->ttl_secs; + + now = time(NULL); + if (node->created < now - (time_t)ttl_secs) { + /* TTL expired */ + cache->miss_count++; + *expired_r = TRUE; + } else { + /* move to head */ + if (node != cache->head) { + auth_cache_node_unlink(cache, node); + auth_cache_node_link_head(cache, node); + } + cache->hit_count++; + } + if (node->created < now - (time_t)cache->neg_ttl_secs) + *neg_expired_r = TRUE; + + if (node_r != NULL) + *node_r = node; + + return value; +} + +void auth_cache_insert(struct auth_cache *cache, struct auth_request *request, + const char *key, const char *value, bool last_success) +{ + struct auth_cache_node *node; + size_t data_size, alloc_size, key_len, value_len = strlen(value); + char *hash_key; + + if (*value == '\0' && cache->neg_ttl_secs == 0) { + /* we're not caching negative entries */ + return; + } + + key = auth_request_expand_cache_key(request, key, request->fields.translated_username); + key_len = strlen(key); + + data_size = key_len + 1 + value_len + 1; + alloc_size = sizeof(struct auth_cache_node) + data_size; + + /* make sure we have enough space */ + while (cache->size_left < alloc_size && cache->tail != NULL) + auth_cache_node_destroy(cache, cache->tail); + + node = hash_table_lookup(cache->hash, key); + if (node != NULL) { + /* key is already in cache (probably expired), remove it */ + auth_cache_node_destroy(cache, node); + } + + /* @UNSAFE */ + node = i_malloc(alloc_size); + node->created = time(NULL); + node->alloc_size = alloc_size; + node->last_success = last_success; + memcpy(node->data, key, key_len); + memcpy(node->data + key_len + 1, value, value_len); + + auth_cache_node_link_head(cache, node); + + cache->size_left -= alloc_size; + hash_key = node->data; + hash_table_insert(cache->hash, hash_key, node); + + if (*value != '\0') { + cache->pos_entries++; + cache->pos_size += alloc_size; + } else { + cache->neg_entries++; + cache->neg_size += alloc_size; + } +} + +void auth_cache_remove(struct auth_cache *cache, + const struct auth_request *request, const char *key) +{ + struct auth_cache_node *node; + + key = auth_request_expand_cache_key(request, key, request->fields.user); + node = hash_table_lookup(cache->hash, key); + if (node == NULL) + return; + + auth_cache_node_destroy(cache, node); +} diff --git a/src/auth/auth-cache.h b/src/auth/auth-cache.h new file mode 100644 index 0000000..ab02fe5 --- /dev/null +++ b/src/auth/auth-cache.h @@ -0,0 +1,53 @@ +#ifndef AUTH_CACHE_H +#define AUTH_CACHE_H + +struct auth_cache_node { + struct auth_cache_node *prev, *next; + + time_t created; + /* Total number of bytes used by this node */ + uint32_t alloc_size:31; + /* TRUE if the user gave the correct password the last time. */ + bool last_success:1; + + char data[]; /* key \0 value \0 */ +}; + +struct auth_cache; +struct auth_request; + +/* Parses all %x variables from query and compresses them into tab-separated + list, so it can be used as a cache key. */ +char *auth_cache_parse_key(pool_t pool, const char *query); + +/* Create a new cache. max_size specifies the maximum amount of memory in + bytes to use for cache (it's not fully exact). ttl_secs specifies time to + live for cache record, requests older than that are not used. + neg_ttl_secs specifies the TTL for negative entries. */ +struct auth_cache *auth_cache_new(size_t max_size, unsigned int ttl_secs, + unsigned int neg_ttl_secs); +void auth_cache_free(struct auth_cache **cache); + +/* Clear the cache. Returns how many entries were removed. */ +unsigned int ATTR_NOWARN_UNUSED_RESULT +auth_cache_clear(struct auth_cache *cache); +unsigned int auth_cache_clear_users(struct auth_cache *cache, + const char *const *usernames); + +/* Look key from cache. key should be the same string as returned by + auth_cache_parse_key(). Returned node can't be used after any other + auth_cache_*() calls. */ +const char * +auth_cache_lookup(struct auth_cache *cache, const struct auth_request *request, + const char *key, struct auth_cache_node **node_r, + bool *expired_r, bool *neg_expired_r); +/* Insert key => value into cache. "" value means negative cache entry. */ +void auth_cache_insert(struct auth_cache *cache, struct auth_request *request, + const char *key, const char *value, bool last_success); + +/* Remove key from cache */ +void auth_cache_remove(struct auth_cache *cache, + const struct auth_request *request, + const char *key); + +#endif diff --git a/src/auth/auth-client-connection.c b/src/auth/auth-client-connection.c new file mode 100644 index 0000000..961480e --- /dev/null +++ b/src/auth/auth-client-connection.c @@ -0,0 +1,456 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +#include "auth-common.h" +#include "ioloop.h" +#include "istream.h" +#include "ostream.h" +#include "net.h" +#include "hex-binary.h" +#include "hostpid.h" +#include "llist.h" +#include "str.h" +#include "str-sanitize.h" +#include "randgen.h" +#include "safe-memset.h" +#include "master-service.h" +#include "mech.h" +#include "auth-fields.h" +#include "auth-request-handler.h" +#include "auth-client-interface.h" +#include "auth-client-connection.h" +#include "auth-master-connection.h" + + +#define OUTBUF_THROTTLE_SIZE (1024*50) + +#define AUTH_DEBUG_SENSITIVE_SUFFIX \ + " (previous base64 data may contain sensitive data)" + +static void auth_client_disconnected(struct auth_client_connection **_conn); +static void auth_client_connection_unref(struct auth_client_connection **_conn); +static void auth_client_input(struct auth_client_connection *conn); + +static struct auth_client_connection *auth_client_connections; + +static const char *reply_line_hide_pass(const char *line) +{ + string_t *newline; + const char *p, *p2; + + if (strstr(line, "pass") == NULL) + return line; + + newline = t_str_new(strlen(line)); + + const char *const *fields = t_strsplit(line, "\t"); + + while(*fields != NULL) { + p = strstr(*fields, "pass"); + p2 = strchr(*fields, '='); + if (p == NULL || p2 == NULL || p2 < p) { + str_append(newline, *fields); + } else { + /* include = */ + str_append_data(newline, *fields, (p2 - *fields)+1); + str_append(newline, PASSWORD_HIDDEN_STR); + } + str_append_c(newline, '\t'); + fields++; + } + + return str_c(newline); +} + +static void auth_client_send(struct auth_client_connection *conn, + const char *cmd) +{ + struct const_iovec iov[2]; + + iov[0].iov_base = cmd; + iov[0].iov_len = strlen(cmd); + iov[1].iov_base = "\n"; + iov[1].iov_len = 1; + o_stream_nsendv(conn->output, iov, 2); + + if (o_stream_get_buffer_used_size(conn->output) >= + OUTBUF_THROTTLE_SIZE) { + /* stop reading new requests until client has read the pending + replies. */ + io_remove(&conn->io); + } + + e_debug(conn->event, "client passdb out: %s", + conn->auth->set->debug_passwords ? + cmd : reply_line_hide_pass(cmd)); +} + +static void auth_callback(const char *reply, + struct auth_client_connection *conn) +{ + if (reply == NULL) { + /* handler destroyed */ + auth_client_connection_unref(&conn); + } else { + auth_client_send(conn, reply); + } +} + +static bool +auth_client_input_cpid(struct auth_client_connection *conn, const char *args) +{ + struct auth_client_connection *old; + unsigned int pid; + + i_assert(conn->pid == 0); + + if (str_to_uint(args, &pid) < 0 || pid == 0) { + e_error(conn->event, "BUG: Authentication client said it's PID 0"); + return FALSE; + } + + if (conn->login_requests) + old = auth_client_connection_lookup(pid); + else { + /* the client is only authenticating, not logging in. + the PID isn't necessary, and since we allow authentication + via TCP sockets the PIDs may conflict, so ignore them. */ + old = NULL; + pid = 0; + } + + if (old != NULL) { + /* already exists. it's possible that it just reconnected, + see if the old connection is still there. */ + i_assert(old != conn); + if (i_stream_read(old->input) == -1) { + auth_client_disconnected(&old); + old = NULL; + } + } + + if (old != NULL) { + e_error(conn->event, "BUG: Authentication client gave a PID " + "%u of existing connection", pid); + return FALSE; + } + + /* handshake complete, we can now actually start serving requests */ + conn->refcount++; + conn->request_handler = + auth_request_handler_create(conn->token_auth, auth_callback, conn, + !conn->login_requests ? NULL : + auth_master_request_callback); + auth_request_handler_set(conn->request_handler, conn->connect_uid, pid); + + conn->pid = pid; + e_debug(conn->event, "auth client connected (pid=%u)", conn->pid); + return TRUE; +} + +static int auth_client_output(struct auth_client_connection *conn) +{ + if (o_stream_flush(conn->output) < 0) { + auth_client_disconnected(&conn); + return 1; + } + + if (o_stream_get_buffer_used_size(conn->output) <= + OUTBUF_THROTTLE_SIZE/3 && conn->io == NULL) { + /* allow input again */ + conn->io = io_add(conn->fd, IO_READ, auth_client_input, conn); + } + return 1; +} + +static const char * +auth_line_hide_pass(struct auth_client_connection *conn, const char *line) +{ + const char *p, *p2; + + p = strstr(line, "\tresp="); + if (p == NULL) + return line; + p += 6; + + if (conn->auth->set->debug_passwords) + return t_strconcat(line, AUTH_DEBUG_SENSITIVE_SUFFIX, NULL); + + p2 = strchr(p, '\t'); + return t_strconcat(t_strdup_until(line, p), PASSWORD_HIDDEN_STR, + p2, NULL); +} + +static const char * +cont_line_hide_pass(struct auth_client_connection *conn, const char *line) +{ + const char *p; + + if (conn->auth->set->debug_passwords) + return t_strconcat(line, AUTH_DEBUG_SENSITIVE_SUFFIX, NULL); + + p = strchr(line, '\t'); + if (p == NULL) + return line; + + return t_strconcat(t_strdup_until(line, p), PASSWORD_HIDDEN_STR, NULL); +} + +static bool +auth_client_cancel(struct auth_client_connection *conn, const char *line) +{ + unsigned int client_id; + + if (str_to_uint(line, &client_id) < 0) { + e_error(conn->event, "BUG: Authentication client sent broken CANCEL"); + return FALSE; + } + + auth_request_handler_cancel_request(conn->request_handler, client_id); + return TRUE; +} + +static bool +auth_client_handle_line(struct auth_client_connection *conn, const char *line) +{ + if (str_begins(line, "AUTH\t")) { + if (conn->auth->set->debug) { + e_debug(conn->event, "client in: %s", + auth_line_hide_pass(conn, line)); + } + return auth_request_handler_auth_begin(conn->request_handler, + line + 5); + } + if (str_begins(line, "CONT\t")) { + if (conn->auth->set->debug) { + e_debug(conn->event, "client in: %s", + cont_line_hide_pass(conn, line)); + } + return auth_request_handler_auth_continue(conn->request_handler, + line + 5); + } + if (str_begins(line, "CANCEL\t")) { + if (conn->auth->set->debug) + e_debug(conn->event, "client in: %s", line); + return auth_client_cancel(conn, line + 7); + } + + e_error(conn->event, "BUG: Authentication client sent unknown command: %s", + str_sanitize(line, 80)); + return FALSE; +} + +static void auth_client_input(struct auth_client_connection *conn) +{ + char *line; + bool ret; + + switch (i_stream_read(conn->input)) { + case 0: + return; + case -1: + /* disconnected */ + auth_client_disconnected(&conn); + return; + case -2: + /* buffer full */ + e_error(conn->event, "BUG: Auth client %u sent us more than %d bytes", + conn->pid, (int)AUTH_CLIENT_MAX_LINE_LENGTH); + auth_client_connection_destroy(&conn); + return; + } + + while (conn->request_handler == NULL) { + /* still handshaking */ + line = i_stream_next_line(conn->input); + if (line == NULL) + return; + + if (!conn->version_received) { + unsigned int vmajor, vminor; + const char *p; + + /* split the version line */ + if (!str_begins(line, "VERSION\t") || + str_parse_uint(line + 8, &vmajor, &p) < 0 || + *(p++) != '\t' || str_to_uint(p, &vminor) < 0) { + e_error(conn->event, "Authentication client " + "sent invalid VERSION line: %s", line); + auth_client_connection_destroy(&conn); + return; + } + /* make sure the major version matches */ + if (vmajor != AUTH_MASTER_PROTOCOL_MAJOR_VERSION) { + e_error(conn->event, "Authentication client " + "not compatible with this server " + "(mixed old and new binaries?)"); + auth_client_connection_destroy(&conn); + return; + } + conn->version_minor = vminor; + conn->version_received = TRUE; + continue; + } + + if (str_begins(line, "CPID\t")) { + if (!auth_client_input_cpid(conn, line + 5)) { + auth_client_connection_destroy(&conn); + return; + } + } else { + e_error(conn->event, "BUG: Authentication client sent " + "unknown handshake command: %s", + str_sanitize(line, 80)); + auth_client_connection_destroy(&conn); + return; + } + } + + conn->refcount++; + while ((line = i_stream_next_line(conn->input)) != NULL) { + T_BEGIN { + ret = auth_client_handle_line(conn, line); + safe_memset(line, 0, strlen(line)); + } T_END; + + if (!ret) { + struct auth_client_connection *tmp_conn = conn; + auth_client_connection_destroy(&tmp_conn); + break; + } + } + auth_client_connection_unref(&conn); +} + +void auth_client_connection_create(struct auth *auth, int fd, + bool login_requests, bool token_auth) +{ + static unsigned int connect_uid_counter = 0; + struct auth_client_connection *conn; + const char *mechanisms; + string_t *str; + + conn = i_new(struct auth_client_connection, 1); + conn->auth = auth; + conn->refcount = 1; + conn->connect_uid = ++connect_uid_counter; + conn->login_requests = login_requests; + conn->token_auth = token_auth; + conn->event = event_create(auth_event); + event_set_forced_debug(conn->event, auth->set->debug); + random_fill(conn->cookie, sizeof(conn->cookie)); + + conn->fd = fd; + conn->input = i_stream_create_fd(fd, AUTH_CLIENT_MAX_LINE_LENGTH); + conn->output = o_stream_create_fd(fd, SIZE_MAX); + o_stream_set_no_error_handling(conn->output, TRUE); + o_stream_set_flush_callback(conn->output, auth_client_output, conn); + conn->io = io_add(fd, IO_READ, auth_client_input, conn); + + DLLIST_PREPEND(&auth_client_connections, conn); + + if (token_auth) { + mechanisms = t_strconcat("MECH\t", + mech_dovecot_token.mech_name, "\n", NULL); + } else { + mechanisms = str_c(auth->reg->handshake); + } + + str = t_str_new(128); + str_printfa(str, "VERSION\t%u\t%u\n%sSPID\t%s\nCUID\t%u\nCOOKIE\t", + AUTH_CLIENT_PROTOCOL_MAJOR_VERSION, + AUTH_CLIENT_PROTOCOL_MINOR_VERSION, + mechanisms, my_pid, conn->connect_uid); + binary_to_hex_append(str, conn->cookie, sizeof(conn->cookie)); + str_append(str, "\nDONE\n"); + + if (o_stream_send(conn->output, str_data(str), str_len(str)) < 0) + auth_client_disconnected(&conn); +} + +void auth_client_connection_destroy(struct auth_client_connection **_conn) +{ + struct auth_client_connection *conn = *_conn; + + *_conn = NULL; + if (conn->fd == -1) + return; + + DLLIST_REMOVE(&auth_client_connections, conn); + + i_stream_close(conn->input); + o_stream_close(conn->output); + + io_remove(&conn->io); + + net_disconnect(conn->fd); + conn->fd = -1; + + if (conn->request_handler != NULL) { + auth_request_handler_abort_requests(conn->request_handler); + auth_request_handler_destroy(&conn->request_handler); + } + + master_service_client_connection_destroyed(master_service); + auth_client_connection_unref(&conn); +} + +static void auth_client_disconnected(struct auth_client_connection **_conn) +{ + struct auth_client_connection *conn = *_conn; + unsigned int request_count; + int err; + + *_conn = NULL; + + if (conn->input->stream_errno != 0) + err = conn->input->stream_errno; + else if (conn->output->stream_errno != 0) + err = conn->output->stream_errno; + else + err = 0; + + request_count = conn->request_handler == NULL ? 0 : + auth_request_handler_get_request_count(conn->request_handler); + if (request_count > 0) { + e_error(conn->event, "auth client %u disconnected with %u " + "pending requests: %s", conn->pid, request_count, + err == 0 ? "EOF" : strerror(err)); + } + auth_client_connection_destroy(&conn); +} + +static void auth_client_connection_unref(struct auth_client_connection **_conn) +{ + struct auth_client_connection *conn = *_conn; + + *_conn = NULL; + if (--conn->refcount > 0) + return; + + event_unref(&conn->event); + i_stream_unref(&conn->input); + o_stream_unref(&conn->output); + i_free(conn); +} + +struct auth_client_connection * +auth_client_connection_lookup(unsigned int pid) +{ + struct auth_client_connection *conn; + + for (conn = auth_client_connections; conn != NULL; conn = conn->next) { + if (conn->pid == pid) + return conn; + } + return NULL; +} + +void auth_client_connections_destroy_all(void) +{ + struct auth_client_connection *conn; + + while (auth_client_connections != NULL) { + conn = auth_client_connections; + auth_client_connection_destroy(&conn); + } +} diff --git a/src/auth/auth-client-connection.h b/src/auth/auth-client-connection.h new file mode 100644 index 0000000..7b10f76 --- /dev/null +++ b/src/auth/auth-client-connection.h @@ -0,0 +1,37 @@ +#ifndef AUTH_CLIENT_CONNECTION_H +#define AUTH_CLIENT_CONNECTION_H + +#include "master-auth.h" + +struct auth_client_connection { + struct auth_client_connection *prev, *next; + struct auth *auth; + struct event *event; + int refcount; + + int fd; + struct io *io; + struct istream *input; + struct ostream *output; + + unsigned int version_minor; + unsigned int pid; + unsigned int connect_uid; + uint8_t cookie[MASTER_AUTH_COOKIE_SIZE]; + struct auth_request_handler *request_handler; + + bool login_requests:1; + bool version_received:1; + bool token_auth:1; +}; + +void auth_client_connection_create(struct auth *auth, int fd, + bool login_requests, bool token_auth); +void auth_client_connection_destroy(struct auth_client_connection **conn); + +struct auth_client_connection * +auth_client_connection_lookup(unsigned int pid); + +void auth_client_connections_destroy_all(void); + +#endif diff --git a/src/auth/auth-common.h b/src/auth/auth-common.h new file mode 100644 index 0000000..5ebe8c4 --- /dev/null +++ b/src/auth/auth-common.h @@ -0,0 +1,17 @@ +#ifndef AUTH_COMMON_H +#define AUTH_COMMON_H + +#include "lib.h" +#include "auth.h" + +extern bool worker, worker_restart_request; +extern time_t process_start_time; +extern struct auth_penalty *auth_penalty; +extern struct event_category event_category_auth; +extern struct event *auth_event; + +void auth_refresh_proctitle(void); +void auth_worker_refresh_proctitle(const char *state); +void auth_module_load(const char *names); + +#endif diff --git a/src/auth/auth-fields.c b/src/auth/auth-fields.c new file mode 100644 index 0000000..0771390 --- /dev/null +++ b/src/auth/auth-fields.c @@ -0,0 +1,226 @@ +/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */ + +#include "auth-common.h" +#include "array.h" +#include "str.h" +#include "strescape.h" +#include "ostream.h" +#include "auth-request.h" +#include "auth-fields.h" + +struct auth_fields { + pool_t pool; + ARRAY_TYPE(auth_field) fields, snapshot_fields; + unsigned int snapshot_idx; + bool snapshotted; +}; + +struct auth_fields *auth_fields_init(pool_t pool) +{ + struct auth_fields *fields; + + fields = p_new(pool, struct auth_fields, 1); + fields->pool = pool; + return fields; +} + +static void auth_fields_snapshot_preserve(struct auth_fields *fields) +{ + if (!fields->snapshotted || array_is_created(&fields->snapshot_fields)) + return; + + p_array_init(&fields->snapshot_fields, fields->pool, + array_count(&fields->fields)); + array_append_array(&fields->snapshot_fields, &fields->fields); +} + +static bool +auth_fields_find_idx(struct auth_fields *fields, const char *key, + unsigned int *idx_r) +{ + const struct auth_field *f; + unsigned int i, count; + + if (!array_is_created(&fields->fields)) + return FALSE; + + f = array_get(&fields->fields, &count); + for (i = 0; i < count; i++) { + if (strcmp(f[i].key, key) == 0) { + *idx_r = i; + return TRUE; + } + } + return FALSE; +} + +void auth_fields_add(struct auth_fields *fields, + const char *key, const char *value, + enum auth_field_flags flags) +{ + struct auth_field *field; + unsigned int idx; + + i_assert(*key != '\0'); + i_assert(strchr(key, '\t') == NULL && + strchr(key, '\n') == NULL); + + if (!auth_fields_find_idx(fields, key, &idx)) { + if (!array_is_created(&fields->fields)) + p_array_init(&fields->fields, fields->pool, 16); + + field = array_append_space(&fields->fields); + field->key = p_strdup(fields->pool, key); + } else { + auth_fields_snapshot_preserve(fields); + field = array_idx_modifiable(&fields->fields, idx); + } + field->value = p_strdup_empty(fields->pool, value); + field->flags = flags | AUTH_FIELD_FLAG_CHANGED; +} + +void auth_fields_remove(struct auth_fields *fields, const char *key) +{ + unsigned int idx; + + if (auth_fields_find_idx(fields, key, &idx)) { + auth_fields_snapshot_preserve(fields); + array_delete(&fields->fields, idx, 1); + } +} + +const char *auth_fields_find(struct auth_fields *fields, const char *key) +{ + const struct auth_field *field; + unsigned int idx; + + if (!auth_fields_find_idx(fields, key, &idx)) + return NULL; + + field = array_idx(&fields->fields, idx); + return field->value == NULL ? "" : field->value; +} + +bool auth_fields_exists(struct auth_fields *fields, const char *key) +{ + return auth_fields_find(fields, key) != NULL; +} + +void auth_fields_reset(struct auth_fields *fields) +{ + if (array_is_created(&fields->fields)) { + auth_fields_snapshot_preserve(fields); + array_clear(&fields->fields); + } +} + +void auth_fields_import_prefixed(struct auth_fields *fields, const char *prefix, + const char *str, enum auth_field_flags flags) +{ + T_BEGIN { + const char *const *arg = t_strsplit_tabescaped(str); + const char *key, *value; + + for (; *arg != NULL; arg++) { + value = strchr(*arg, '='); + if (value == NULL) { + key = *arg; + value = NULL; + } else { + key = t_strdup_until(*arg, value++); + if (*prefix != '\0') + key = t_strconcat(prefix, key, NULL); + } + auth_fields_add(fields, key, value, flags); + } + } T_END; +} + +void auth_fields_import(struct auth_fields *fields, const char *str, + enum auth_field_flags flags) +{ + auth_fields_import_prefixed(fields, "", str, flags); +} + +const ARRAY_TYPE(auth_field) *auth_fields_export(struct auth_fields *fields) +{ + if (!array_is_created(&fields->fields)) + p_array_init(&fields->fields, fields->pool, 1); + return &fields->fields; +} + +void auth_fields_append(struct auth_fields *fields, string_t *dest, + enum auth_field_flags flags_mask, + enum auth_field_flags flags_result) +{ + const struct auth_field *f; + unsigned int i, count; + bool first = TRUE; + + if (!array_is_created(&fields->fields)) + return; + + f = array_get(&fields->fields, &count); + for (i = 0; i < count; i++) { + if ((f[i].flags & flags_mask) != flags_result) + continue; + + if (first) + first = FALSE; + else + str_append_c(dest, '\t'); + str_append(dest, f[i].key); + if (f[i].value != NULL) { + str_append_c(dest, '='); + str_append_tabescaped(dest, f[i].value); + } + } +} + +bool auth_fields_is_empty(struct auth_fields *fields) +{ + return fields == NULL || !array_is_created(&fields->fields) || + array_count(&fields->fields) == 0; +} + +void auth_fields_booleanize(struct auth_fields *fields, const char *key) +{ + struct auth_field *field; + unsigned int idx; + + if (auth_fields_find_idx(fields, key, &idx)) { + field = array_idx_modifiable(&fields->fields, idx); + field->value = NULL; + } +} + +void auth_fields_snapshot(struct auth_fields *fields) +{ + struct auth_field *field; + + fields->snapshotted = TRUE; + if (!array_is_created(&fields->fields)) + return; + + if (!array_is_created(&fields->snapshot_fields)) { + /* try to avoid creating this array */ + fields->snapshot_idx = array_count(&fields->fields); + } else { + array_clear(&fields->snapshot_fields); + array_append_array(&fields->snapshot_fields, &fields->fields); + } + array_foreach_modifiable(&fields->fields, field) + field->flags &= ENUM_NEGATE(AUTH_FIELD_FLAG_CHANGED); +} + +void auth_fields_rollback(struct auth_fields *fields) +{ + if (array_is_created(&fields->snapshot_fields)) { + array_clear(&fields->fields); + array_append_array(&fields->fields, &fields->snapshot_fields); + } else if (array_is_created(&fields->fields)) { + array_delete(&fields->fields, fields->snapshot_idx, + array_count(&fields->fields) - + fields->snapshot_idx); + } +} diff --git a/src/auth/auth-fields.h b/src/auth/auth-fields.h new file mode 100644 index 0000000..25c91c7 --- /dev/null +++ b/src/auth/auth-fields.h @@ -0,0 +1,48 @@ +#ifndef AUTH_FIELDS_H +#define AUTH_FIELDS_H + +struct auth_request; + +enum auth_field_flags { + /* This field is internal to auth process and won't be sent to client */ + AUTH_FIELD_FLAG_HIDDEN = 0x01, + /* Changed since last snapshot. Set/cleared automatically. */ + AUTH_FIELD_FLAG_CHANGED = 0x02 +}; + +struct auth_field { + const char *key, *value; + enum auth_field_flags flags; +}; +ARRAY_DEFINE_TYPE(auth_field, struct auth_field); + +struct auth_fields *auth_fields_init(pool_t pool); +void auth_fields_add(struct auth_fields *fields, + const char *key, const char *value, + enum auth_field_flags flags) ATTR_NULL(3); +void auth_fields_reset(struct auth_fields *fields); +void auth_fields_remove(struct auth_fields *fields, const char *key); + +const char *auth_fields_find(struct auth_fields *fields, const char *key); +bool auth_fields_exists(struct auth_fields *fields, const char *key); + +void auth_fields_import(struct auth_fields *fields, const char *str, + enum auth_field_flags flags); +void auth_fields_import_prefixed(struct auth_fields *fields, const char *prefix, + const char *str, enum auth_field_flags flags); +const ARRAY_TYPE(auth_field) *auth_fields_export(struct auth_fields *fields); +/* Append fields where (flag & flags_mask) == flags_result. */ +void auth_fields_append(struct auth_fields *fields, string_t *dest, + enum auth_field_flags flags_mask, + enum auth_field_flags flags_result); +bool auth_fields_is_empty(struct auth_fields *fields); +/* If the field exists, clear its value (so the exported string will be "key" + instead of e.g. "key=y"). */ +void auth_fields_booleanize(struct auth_fields *fields, const char *key); + +/* Remember the current fields. */ +void auth_fields_snapshot(struct auth_fields *fields); +/* Rollback to previous snapshot, or clear the fields if there isn't any. */ +void auth_fields_rollback(struct auth_fields *fields); + +#endif diff --git a/src/auth/auth-master-connection.c b/src/auth/auth-master-connection.c new file mode 100644 index 0000000..3f439b8 --- /dev/null +++ b/src/auth/auth-master-connection.c @@ -0,0 +1,855 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +#include "auth-common.h" +#include "buffer.h" +#include "hash.h" +#include "llist.h" +#include "str.h" +#include "strescape.h" +#include "str-sanitize.h" +#include "time-util.h" +#include "hostpid.h" +#include "hex-binary.h" +#include "ioloop.h" +#include "net.h" +#include "istream.h" +#include "ostream.h" +#include "ipwd.h" +#include "master-service.h" +#include "userdb.h" +#include "userdb-blocking.h" +#include "master-interface.h" +#include "passdb-cache.h" +#include "auth-request-handler.h" +#include "auth-client-connection.h" +#include "auth-master-connection.h" + +#include <unistd.h> + +#define MAX_INBUF_SIZE 1024 +#define MAX_OUTBUF_SIZE (1024*50) + +struct master_userdb_request { + struct auth_master_connection *conn; + struct auth_request *auth_request; +}; + +struct master_list_iter_ctx { + struct auth_master_connection *conn; + struct userdb_iterate_context *iter; + struct auth_request *auth_request; + bool failed; +}; + +static void master_input(struct auth_master_connection *conn); + +static struct auth_master_connection *auth_master_connections; + +static const char * +auth_master_reply_hide_passwords(struct auth_master_connection *conn, + const char *str) +{ + char **args, *p, *p2; + unsigned int i; + + if (conn->auth->set->debug_passwords) + return str; + + /* hide all parameters that have "pass" in their key */ + args = p_strsplit(pool_datastack_create(), str, "\t"); + for (i = 0; args[i] != NULL; i++) { + p = strstr(args[i], "pass"); + p2 = strchr(args[i], '='); + if (p != NULL && p < p2) { + *p2 = '\0'; + args[i] = p_strconcat(pool_datastack_create(), + args[i], "=<hidden>", NULL); + } + } + return t_strarray_join((void *)args, "\t"); +} + +void auth_master_request_callback(const char *reply, struct auth_master_connection *conn) +{ + struct const_iovec iov[2]; + + e_debug(auth_event, "master userdb out: %s", + auth_master_reply_hide_passwords(conn, reply)); + + iov[0].iov_base = reply; + iov[0].iov_len = strlen(reply); + iov[1].iov_base = "\n"; + iov[1].iov_len = 1; + + o_stream_nsendv(conn->output, iov, 2); +} + +static const char * +auth_master_event_log_callback(struct auth_master_connection *conn, + enum log_type log_type ATTR_UNUSED, + const char *message) +{ + string_t *str = t_str_new(128); + + str_printfa(str, "auth-master client: %s (created %d msecs ago", message, + timeval_diff_msecs(&ioloop_timeval, &conn->create_time)); + if (conn->handshake_time.tv_sec != 0) { + str_printfa(str, ", handshake %d msecs ago", + timeval_diff_msecs(&ioloop_timeval, &conn->create_time)); + } + str_append_c(str, ')'); + return str_c(str); +} + +static bool +master_input_request(struct auth_master_connection *conn, const char *args) +{ + struct auth_client_connection *client_conn; + const char *const *list, *const *params; + unsigned int id, client_pid, client_id; + uint8_t cookie[MASTER_AUTH_COOKIE_SIZE]; + buffer_t buf; + + /* <id> <client-pid> <client-id> <cookie> [<parameters>] */ + list = t_strsplit_tabescaped(args); + if (str_array_length(list) < 4 || + str_to_uint(list[0], &id) < 0 || + str_to_uint(list[1], &client_pid) < 0 || + str_to_uint(list[2], &client_id) < 0) { + e_error(conn->event, "BUG: Master sent broken REQUEST"); + return FALSE; + } + + buffer_create_from_data(&buf, cookie, sizeof(cookie)); + if (hex_to_binary(list[3], &buf) < 0) { + e_error(conn->event, "BUG: Master sent broken REQUEST cookie"); + return FALSE; + } + params = list + 4; + + client_conn = auth_client_connection_lookup(client_pid); + if (client_conn == NULL) { + e_error(conn->event, + "Master requested auth for nonexistent client %u", + client_pid); + o_stream_nsend_str(conn->output, + t_strdup_printf("FAIL\t%u\n", id)); + } else if (!mem_equals_timing_safe(client_conn->cookie, cookie, sizeof(cookie))) { + e_error(conn->event, + "Master requested auth for client %u with invalid cookie", + client_pid); + o_stream_nsend_str(conn->output, + t_strdup_printf("FAIL\t%u\n", id)); + } else if (!auth_request_handler_master_request( + client_conn->request_handler, conn, id, client_id, params)) { + e_error(conn->event, + "Master requested auth for non-login client %u", + client_pid); + o_stream_nsend_str(conn->output, + t_strdup_printf("FAIL\t%u\n", id)); + } + return TRUE; +} + +static bool +master_input_cache_flush(struct auth_master_connection *conn, const char *args) +{ + const char *const *list; + unsigned int count; + + /* <id> [<user> [<user> [..]] */ + list = t_strsplit_tabescaped(args); + if (list[0] == NULL) { + e_error(conn->event, "BUG: doveadm sent broken CACHE-FLUSH"); + return FALSE; + } + + if (passdb_cache == NULL) { + /* cache disabled */ + count = 0; + } else if (list[1] == NULL) { + /* flush the whole cache */ + count = auth_cache_clear(passdb_cache); + } else { + count = auth_cache_clear_users(passdb_cache, list+1); + } + o_stream_nsend_str(conn->output, + t_strdup_printf("OK\t%s\t%u\n", list[0], count)); + return TRUE; +} + +static int +master_input_auth_request(struct auth_master_connection *conn, const char *args, + const char *cmd, struct auth_request **request_r, + const char **error_r) +{ + struct auth_request *auth_request; + const char *const *list, *name, *arg, *username; + unsigned int id; + + /* <id> <userid> [<parameters>] */ + list = t_strsplit_tabescaped(args); + if (list[0] == NULL || list[1] == NULL || + str_to_uint(list[0], &id) < 0) { + e_error(conn->event, "BUG: Master sent broken %s", cmd); + return -1; + } + + auth_request = auth_request_new_dummy(auth_event); + auth_request->id = id; + auth_request->master = conn; + auth_master_connection_ref(conn); + username = list[1]; + + for (list += 2; *list != NULL; list++) { + arg = strchr(*list, '='); + if (arg == NULL) { + name = *list; + arg = ""; + } else { + name = t_strdup_until(*list, arg); + arg++; + } + + (void)auth_request_import_info(auth_request, name, arg); + } + + if (auth_request->fields.service == NULL) { + e_error(conn->event, + "BUG: Master sent %s request without service", cmd); + auth_request_unref(&auth_request); + auth_master_connection_unref(&conn); + return -1; + } + + auth_request_init(auth_request); + + if (!auth_request_set_username(auth_request, username, error_r)) { + *request_r = auth_request; + return 0; + } + *request_r = auth_request; + return 1; +} + +static int +user_verify_restricted_uid(struct auth_request *auth_request) +{ + struct auth_master_connection *conn = auth_request->master; + struct auth_fields *reply = auth_request->fields.userdb_reply; + const char *value, *reason; + uid_t uid; + + if (conn->userdb_restricted_uid == 0) + return 0; + + value = auth_fields_find(reply, "uid"); + if (value == NULL) + reason = "userdb reply doesn't contain uid"; + else if (str_to_uid(value, &uid) < 0) + reason = "userdb reply contains invalid uid"; + else if (uid != conn->userdb_restricted_uid) { + reason = t_strdup_printf( + "userdb uid (%s) doesn't match peer uid (%s)", + dec2str(uid), dec2str(conn->userdb_restricted_uid)); + } else { + return 0; + } + + auth_request_log_error(auth_request, "userdb", + "client doesn't have lookup permissions for this user: %s " + "(to bypass this check, set: service auth { unix_listener %s { mode=0777 } })", + reason, conn->path); + return -1; +} + +static void +user_callback(enum userdb_result result, + struct auth_request *auth_request) +{ + struct auth_master_connection *conn = auth_request->master; + string_t *str; + const char *value; + + if (auth_request->userdb_lookup_tempfailed) + result = USERDB_RESULT_INTERNAL_FAILURE; + + if (result == USERDB_RESULT_OK) { + if (user_verify_restricted_uid(auth_request) < 0) + result = USERDB_RESULT_INTERNAL_FAILURE; + } + + str = t_str_new(128); + switch (result) { + case USERDB_RESULT_INTERNAL_FAILURE: + str_printfa(str, "FAIL\t%u", auth_request->id); + if (auth_request->userdb_lookup_tempfailed) { + value = auth_fields_find(auth_request->fields.userdb_reply, + "reason"); + if (value != NULL) + str_printfa(str, "\treason=%s", value); + } + break; + case USERDB_RESULT_USER_UNKNOWN: + str_printfa(str, "NOTFOUND\t%u", auth_request->id); + break; + case USERDB_RESULT_OK: + str_printfa(str, "USER\t%u\t", auth_request->id); + str_append_tabescaped(str, auth_request->fields.user); + str_append_c(str, '\t'); + auth_fields_append(auth_request->fields.userdb_reply, str, + AUTH_FIELD_FLAG_HIDDEN, 0); + break; + } + + e_debug(auth_event, "userdb out: %s", + auth_master_reply_hide_passwords(conn, str_c(str))); + + str_append_c(str, '\n'); + o_stream_nsend(conn->output, str_data(str), str_len(str)); + + auth_request_unref(&auth_request); + auth_master_connection_unref(&conn); +} + +static bool +master_input_user(struct auth_master_connection *conn, const char *args) +{ + struct auth_request *auth_request; + const char *error; + int ret; + + ret = master_input_auth_request(conn, args, "USER", + &auth_request, &error); + if (ret <= 0) { + if (ret < 0) + return FALSE; + auth_request_log_info(auth_request, "userdb", "%s", error); + user_callback(USERDB_RESULT_USER_UNKNOWN, auth_request); + } else { + auth_request_set_state(auth_request, AUTH_REQUEST_STATE_USERDB); + auth_request_lookup_user(auth_request, user_callback); + } + return TRUE; +} + +static void pass_callback_finish(struct auth_request *auth_request, + enum passdb_result result) +{ + struct auth_master_connection *conn = auth_request->master; + string_t *str; + + str = t_str_new(128); + switch (result) { + case PASSDB_RESULT_OK: + if (auth_request->failed || !auth_request->passdb_success) { + str_printfa(str, "FAIL\t%u", auth_request->id); + break; + } + str_printfa(str, "PASS\t%u\tuser=", auth_request->id); + str_append_tabescaped(str, auth_request->fields.user); + if (!auth_fields_is_empty(auth_request->fields.extra_fields)) { + str_append_c(str, '\t'); + auth_fields_append(auth_request->fields.extra_fields, + str, AUTH_FIELD_FLAG_HIDDEN, 0); + } + break; + case PASSDB_RESULT_USER_UNKNOWN: + case PASSDB_RESULT_USER_DISABLED: + case PASSDB_RESULT_PASS_EXPIRED: + str_printfa(str, "NOTFOUND\t%u", auth_request->id); + break; + case PASSDB_RESULT_NEXT: + case PASSDB_RESULT_PASSWORD_MISMATCH: + case PASSDB_RESULT_INTERNAL_FAILURE: + str_printfa(str, "FAIL\t%u", auth_request->id); + break; + case PASSDB_RESULT_SCHEME_NOT_AVAILABLE: + str_printfa(str, "FAIL\t%u\treason=Configured passdbs don't support credentials lookups", + auth_request->id); + break; + } + + e_debug(auth_event, "passdb out: %s", str_c(str)); + + str_append_c(str, '\n'); + o_stream_nsend(conn->output, str_data(str), str_len(str)); + + auth_request_unref(&auth_request); + auth_master_connection_unref(&conn); +} + +static void +auth_master_pass_proxy_finish(bool success, struct auth_request *auth_request) +{ + pass_callback_finish(auth_request, success ? PASSDB_RESULT_OK : + PASSDB_RESULT_INTERNAL_FAILURE); +} + +static void +pass_callback(enum passdb_result result, + const unsigned char *credentials ATTR_UNUSED, + size_t size ATTR_UNUSED, + struct auth_request *auth_request) +{ + int ret; + + if (result != PASSDB_RESULT_OK) + auth_request_proxy_finish_failure(auth_request); + else { + ret = auth_request_proxy_finish(auth_request, + auth_master_pass_proxy_finish); + if (ret == 0) + return; + if (ret < 0) + result = PASSDB_RESULT_INTERNAL_FAILURE; + } + pass_callback_finish(auth_request, result); +} + +static const char *auth_restricted_reason(struct auth_master_connection *conn) +{ + struct passwd pw; + const char *namestr; + + if (i_getpwuid(conn->userdb_restricted_uid, &pw) <= 0) + namestr = ""; + else + namestr = t_strdup_printf("(%s)", pw.pw_name); + return t_strdup_printf("%s mode=0666, but not owned by UID %lu%s", + conn->path, + (unsigned long)conn->userdb_restricted_uid, + namestr); +} + +static bool +master_input_pass(struct auth_master_connection *conn, const char *args) +{ + struct auth_request *auth_request; + const char *error; + int ret; + + ret = master_input_auth_request(conn, args, "PASS", + &auth_request, &error); + if (ret <= 0) { + if (ret < 0) + return FALSE; + auth_request_log_info(auth_request, "passdb", "%s", error); + pass_callback(PASSDB_RESULT_USER_UNKNOWN, + uchar_empty_ptr, 0, auth_request); + } else if (conn->userdb_restricted_uid != 0) { + /* no permissions to do this lookup */ + auth_request_log_error(auth_request, "passdb", + "Auth client doesn't have permissions to do " + "a PASS lookup: %s", auth_restricted_reason(conn)); + pass_callback(PASSDB_RESULT_INTERNAL_FAILURE, + uchar_empty_ptr, 0, auth_request); + } else { + auth_request_set_state(auth_request, + AUTH_REQUEST_STATE_MECH_CONTINUE); + auth_request_lookup_credentials(auth_request, "", + pass_callback); + } + return TRUE; +} + +static void master_input_list_finish(struct master_list_iter_ctx *ctx) +{ + i_assert(ctx->conn->iter_ctx == ctx); + + ctx->conn->iter_ctx = NULL; + ctx->conn->io = io_add(ctx->conn->fd, IO_READ, master_input, ctx->conn); + + if (ctx->iter != NULL) + (void)userdb_blocking_iter_deinit(&ctx->iter); + o_stream_uncork(ctx->conn->output); + o_stream_unset_flush_callback(ctx->conn->output); + auth_request_unref(&ctx->auth_request); + auth_master_connection_unref(&ctx->conn); + i_free(ctx); +} + +static int master_output_list(struct master_list_iter_ctx *ctx) +{ + int ret; + + if ((ret = o_stream_flush(ctx->conn->output)) < 0) { + master_input_list_finish(ctx); + return 1; + } + if (ret > 0) { + o_stream_cork(ctx->conn->output); + userdb_blocking_iter_next(ctx->iter); + } + return 1; +} + +static void master_input_list_callback(const char *user, void *context) +{ + struct master_list_iter_ctx *ctx = context; + struct auth_userdb *userdb = ctx->auth_request->userdb; + int ret; + + if (user == NULL) { + if (userdb_blocking_iter_deinit(&ctx->iter) < 0) + ctx->failed = TRUE; + + do { + userdb = userdb->next; + } while (userdb != NULL && + userdb->userdb->iface->iterate_init == NULL); + if (userdb == NULL) { + /* iteration is finished */ + const char *str; + + str = t_strdup_printf("DONE\t%u\t%s\n", + ctx->auth_request->id, + ctx->failed ? "fail" : ""); + o_stream_nsend_str(ctx->conn->output, str); + master_input_list_finish(ctx); + return; + } + + /* continue iterating next userdb */ + ctx->auth_request->userdb = userdb; + ctx->iter = userdb_blocking_iter_init(ctx->auth_request, + master_input_list_callback, ctx); + return; + } + + T_BEGIN { + const char *str; + + str = t_strdup_printf("LIST\t%u\t%s\n", ctx->auth_request->id, + str_tabescape(user)); + ret = o_stream_send_str(ctx->conn->output, str); + } T_END; + if (o_stream_get_buffer_used_size(ctx->conn->output) >= MAX_OUTBUF_SIZE) + ret = o_stream_flush(ctx->conn->output); + if (ret < 0) { + /* disconnected, don't bother finishing */ + master_input_list_finish(ctx); + return; + } + if (o_stream_get_buffer_used_size(ctx->conn->output) < MAX_OUTBUF_SIZE) + userdb_blocking_iter_next(ctx->iter); + else + o_stream_uncork(ctx->conn->output); +} + +static bool +master_input_list(struct auth_master_connection *conn, const char *args) +{ + struct auth_userdb *userdb = conn->auth->userdbs; + struct auth_request *auth_request; + struct master_list_iter_ctx *ctx; + const char *str, *name, *arg, *const *list; + unsigned int id; + + /* <id> [<parameters>] */ + list = t_strsplit_tabescaped(args); + if (list[0] == NULL || str_to_uint(list[0], &id) < 0) { + e_error(conn->event, "BUG: Master sent broken LIST"); + return FALSE; + } + list++; + + if (conn->iter_ctx != NULL) { + e_error(conn->event, + "Auth client is already iterating users"); + str = t_strdup_printf("DONE\t%u\tfail\n", id); + o_stream_nsend_str(conn->output, str); + return TRUE; + } + + if (conn->userdb_restricted_uid != 0) { + e_error(conn->event, + "Auth client doesn't have permissions to list users: %s", + auth_restricted_reason(conn)); + str = t_strdup_printf("DONE\t%u\tfail\n", id); + o_stream_nsend_str(conn->output, str); + return TRUE; + } + + while (userdb != NULL && userdb->userdb->iface->iterate_init == NULL) + userdb = userdb->next; + if (userdb == NULL) { + e_error(conn->event, + "Trying to iterate users, but userdbs don't support it"); + str = t_strdup_printf("DONE\t%u\tfail\n", id); + o_stream_nsend_str(conn->output, str); + return TRUE; + } + + auth_request = auth_request_new_dummy(auth_event); + auth_request->id = id; + auth_request->master = conn; + auth_master_connection_ref(conn); + + for (; *list != NULL; list++) { + arg = strchr(*list, '='); + if (arg == NULL) { + name = *list; + arg = ""; + } else { + name = t_strdup_until(*list, arg); + arg++; + } + + if (!auth_request_import_info(auth_request, name, arg) && + strcmp(name, "user") == 0) { + /* username mask */ + auth_request_set_username_forced(auth_request, arg); + } + } + + /* rest of the code doesn't like NULL user or service */ + if (auth_request->fields.user == NULL) + auth_request_set_username_forced(auth_request, ""); + if (auth_request->fields.service == NULL) { + if (!auth_request_import(auth_request, "service", "")) + i_unreached(); + i_assert(auth_request->fields.service != NULL); + } + + ctx = i_new(struct master_list_iter_ctx, 1); + ctx->conn = conn; + ctx->auth_request = auth_request; + ctx->auth_request->userdb = userdb; + + io_remove(&conn->io); + o_stream_cork(conn->output); + o_stream_set_flush_callback(conn->output, master_output_list, ctx); + ctx->iter = userdb_blocking_iter_init(auth_request, + master_input_list_callback, ctx); + conn->iter_ctx = ctx; + return TRUE; +} + +static bool +auth_master_input_line(struct auth_master_connection *conn, const char *line) +{ + e_debug(auth_event, "master in: %s", line); + + if (str_begins(line, "USER\t")) + return master_input_user(conn, line + 5); + if (str_begins(line, "LIST\t")) + return master_input_list(conn, line + 5); + if (str_begins(line, "PASS\t")) + return master_input_pass(conn, line + 5); + + if (!conn->userdb_only) { + i_assert(conn->userdb_restricted_uid == 0); + if (str_begins(line, "REQUEST\t")) + return master_input_request(conn, line + 8); + if (str_begins(line, "CACHE-FLUSH\t")) + return master_input_cache_flush(conn, line + 12); + if (str_begins(line, "CPID\t")) { + e_error(conn->event, + "Authentication client trying to connect to " + "master socket"); + return FALSE; + } + } + + e_error(conn->event, "BUG: Unknown command in %s socket: %s", + conn->userdb_only ? "userdb" : "master", + str_sanitize(line, 80)); + return FALSE; +} + +static void master_input(struct auth_master_connection *conn) +{ + char *line; + bool ret; + + switch (i_stream_read(conn->input)) { + case 0: + return; + case -1: + /* disconnected */ + auth_master_connection_destroy(&conn); + return; + case -2: + /* buffer full */ + e_error(conn->event, "BUG: Master sent us more than %d bytes", + (int)MAX_INBUF_SIZE); + auth_master_connection_destroy(&conn); + return; + } + + if (!conn->version_received) { + line = i_stream_next_line(conn->input); + if (line == NULL) + return; + + /* make sure the major version matches */ + if (!str_begins(line, "VERSION\t") || + !str_uint_equals(t_strcut(line + 8, '\t'), + AUTH_MASTER_PROTOCOL_MAJOR_VERSION)) { + e_error(conn->event, + "Master not compatible with this server " + "(mixed old and new binaries?)"); + auth_master_connection_destroy(&conn); + return; + } + conn->version_received = TRUE; + conn->handshake_time = ioloop_timeval; + } + + while ((line = i_stream_next_line(conn->input)) != NULL) { + T_BEGIN { + ret = auth_master_input_line(conn, line); + } T_END; + if (!ret) { + auth_master_connection_destroy(&conn); + return; + } + } +} + +static int master_output(struct auth_master_connection *conn) +{ + if (o_stream_flush(conn->output) < 0) { + /* transmit error, probably master died */ + auth_master_connection_destroy(&conn); + return 1; + } + + if (conn->io == NULL && + o_stream_get_buffer_used_size(conn->output) <= MAX_OUTBUF_SIZE/2) { + /* allow input again */ + conn->io = io_add(conn->fd, IO_READ, master_input, conn); + } + return 1; +} + +static int +auth_master_connection_set_permissions(struct auth_master_connection *conn, + const struct stat *st) +{ + struct net_unix_cred cred; + + if (st == NULL) + return 0; + + /* figure out what permissions we want to give to this client */ + if ((st->st_mode & 0777) != 0666) { + /* permissions were already restricted by the socket + permissions. also +x bit indicates that we shouldn't do + any permission checks. */ + return 0; + } + + if (net_getunixcred(conn->fd, &cred) < 0) { + e_error(conn->event, + "userdb connection: Failed to get peer's credentials"); + return -1; + } + + if (cred.uid == st->st_uid || cred.gid == st->st_gid) { + /* full permissions */ + return 0; + } else { + /* restrict permissions: return only lookups whose returned + uid matches the peer's uid */ + conn->userdb_restricted_uid = cred.uid; + return 0; + } +} + +struct auth_master_connection * +auth_master_connection_create(struct auth *auth, int fd, + const char *path, const struct stat *socket_st, + bool userdb_only) +{ + struct auth_master_connection *conn; + const char *line; + + i_assert(path != NULL); + + conn = i_new(struct auth_master_connection, 1); + conn->refcount = 1; + conn->fd = fd; + conn->create_time = ioloop_timeval; + conn->path = i_strdup(path); + conn->auth = auth; + conn->input = i_stream_create_fd(fd, MAX_INBUF_SIZE); + conn->output = o_stream_create_fd(fd, SIZE_MAX); + o_stream_set_no_error_handling(conn->output, TRUE); + o_stream_set_flush_callback(conn->output, master_output, conn); + conn->io = io_add(fd, IO_READ, master_input, conn); + conn->userdb_only = userdb_only; + conn->event = event_create(auth_event); + event_set_log_message_callback(conn->event, auth_master_event_log_callback, conn); + + line = t_strdup_printf("VERSION\t%u\t%u\nSPID\t%s\n", + AUTH_MASTER_PROTOCOL_MAJOR_VERSION, + AUTH_MASTER_PROTOCOL_MINOR_VERSION, + my_pid); + o_stream_nsend_str(conn->output, line); + DLLIST_PREPEND(&auth_master_connections, conn); + + if (auth_master_connection_set_permissions(conn, socket_st) < 0) { + auth_master_connection_destroy(&conn); + return NULL; + } + return conn; +} + +void auth_master_connection_destroy(struct auth_master_connection **_conn) +{ + struct auth_master_connection *conn = *_conn; + + *_conn = NULL; + if (conn->destroyed) + return; + conn->destroyed = TRUE; + + DLLIST_REMOVE(&auth_master_connections, conn); + + if (conn->iter_ctx != NULL) + master_input_list_finish(conn->iter_ctx); + i_stream_close(conn->input); + o_stream_close(conn->output); + io_remove(&conn->io); + i_close_fd_path(&conn->fd, conn->path); + + master_service_client_connection_destroyed(master_service); + auth_master_connection_unref(&conn); +} + +void auth_master_connection_ref(struct auth_master_connection *conn) +{ + i_assert(conn->refcount > 0); + + conn->refcount++; +} + +void auth_master_connection_unref(struct auth_master_connection **_conn) +{ + struct auth_master_connection *conn = *_conn; + + *_conn = NULL; + i_assert(conn->refcount > 0); + + if (--conn->refcount > 0) + return; + + i_stream_unref(&conn->input); + o_stream_unref(&conn->output); + + event_unref(&conn->event); + i_free(conn->path); + i_free(conn); +} + +void auth_master_connections_destroy_all(void) +{ + struct auth_master_connection *conn; + + while (auth_master_connections != NULL) { + conn = auth_master_connections; + auth_master_connection_destroy(&conn); + } +} diff --git a/src/auth/auth-master-connection.h b/src/auth/auth-master-connection.h new file mode 100644 index 0000000..c2a5dd5 --- /dev/null +++ b/src/auth/auth-master-connection.h @@ -0,0 +1,44 @@ +#ifndef AUTH_MASTER_CONNECTION_H +#define AUTH_MASTER_CONNECTION_H + +struct stat; +struct auth_stream_reply; + +struct auth_master_connection { + struct auth_master_connection *prev, *next; + struct auth *auth; + struct event *event; + int refcount; + + struct timeval create_time, handshake_time; + + int fd; + char *path; + struct istream *input; + struct ostream *output; + struct io *io; + + struct master_list_iter_ctx *iter_ctx; + /* If non-zero, allow only USER lookups whose returned uid matches + this uid. Don't allow LIST/PASS lookups. */ + uid_t userdb_restricted_uid; + + bool version_received:1; + bool destroyed:1; + bool userdb_only:1; +}; + +struct auth_master_connection * +auth_master_connection_create(struct auth *auth, int fd, + const char *path, const struct stat *socket_st, + bool userdb_only) ATTR_NULL(4); +void auth_master_connection_destroy(struct auth_master_connection **conn); + +void auth_master_connection_ref(struct auth_master_connection *conn); +void auth_master_connection_unref(struct auth_master_connection **conn); + +void auth_master_request_callback(const char *reply, struct auth_master_connection *conn); + +void auth_master_connections_destroy_all(void); + +#endif diff --git a/src/auth/auth-penalty.c b/src/auth/auth-penalty.c new file mode 100644 index 0000000..3816902 --- /dev/null +++ b/src/auth/auth-penalty.c @@ -0,0 +1,176 @@ +/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "ioloop.h" +#include "net.h" +#include "crc32.h" +#include "master-service.h" +#include "anvil-client.h" +#include "auth-request.h" +#include "auth-penalty.h" + +#include <stdio.h> + +/* We don't want IPv6 hosts being able to flood our penalty + tracking with tons of different IPs. */ +#define PENALTY_IPV6_MASK_BITS 48 + +struct auth_penalty_request { + struct auth_request *auth_request; + struct anvil_client *client; + auth_penalty_callback_t *callback; +}; + +struct auth_penalty { + struct anvil_client *client; + + bool disabled:1; +}; + +struct auth_penalty *auth_penalty_init(const char *path) +{ + struct auth_penalty *penalty; + + penalty = i_new(struct auth_penalty, 1); + penalty->client = anvil_client_init(path, NULL, + ANVIL_CLIENT_FLAG_HIDE_ENOENT); + if (anvil_client_connect(penalty->client, TRUE) < 0) + penalty->disabled = TRUE; + else { + anvil_client_cmd(penalty->client, t_strdup_printf( + "PENALTY-SET-EXPIRE-SECS\t%u", AUTH_PENALTY_TIMEOUT)); + } + return penalty; +} + +void auth_penalty_deinit(struct auth_penalty **_penalty) +{ + struct auth_penalty *penalty = *_penalty; + + *_penalty = NULL; + anvil_client_deinit(&penalty->client); + i_free(penalty); +} + +unsigned int auth_penalty_to_secs(unsigned int penalty) +{ + unsigned int i, secs = AUTH_PENALTY_INIT_SECS; + + for (i = 0; i < penalty; i++) + secs *= 2; + return secs < AUTH_PENALTY_MAX_SECS ? secs : AUTH_PENALTY_MAX_SECS; +} + +static void auth_penalty_anvil_callback(const char *reply, void *context) +{ + struct auth_penalty_request *request = context; + unsigned int penalty = 0; + unsigned long last_penalty = 0; + unsigned int secs, drop_penalty; + + if (reply == NULL) { + /* internal failure. */ + if (!anvil_client_is_connected(request->client)) { + /* we probably didn't have permissions to reconnect + back to anvil. need to restart ourself. */ + master_service_stop(master_service); + } + } else if (sscanf(reply, "%u %lu", &penalty, &last_penalty) != 2) { + e_error(request->auth_request->event, + "Invalid PENALTY-GET reply: %s", reply); + } else { + if ((time_t)last_penalty > ioloop_time) { + /* time moved backwards? */ + last_penalty = ioloop_time; + } + + /* update penalty. */ + drop_penalty = AUTH_PENALTY_MAX_PENALTY; + while (penalty > 0) { + secs = auth_penalty_to_secs(drop_penalty); + if (ioloop_time - last_penalty < secs) + break; + drop_penalty--; + penalty--; + } + } + + request->callback(penalty, request->auth_request); + auth_request_unref(&request->auth_request); + i_free(request); +} + +static const char * +auth_penalty_get_ident(struct auth_request *auth_request) +{ + struct ip_addr ip; + + ip = auth_request->fields.remote_ip; + if (IPADDR_IS_V6(&ip)) { + memset(ip.u.ip6.s6_addr + PENALTY_IPV6_MASK_BITS/CHAR_BIT, 0, + sizeof(ip.u.ip6.s6_addr) - + PENALTY_IPV6_MASK_BITS/CHAR_BIT); + } + return net_ip2addr(&ip); +} + +void auth_penalty_lookup(struct auth_penalty *penalty, + struct auth_request *auth_request, + auth_penalty_callback_t *callback) +{ + struct auth_penalty_request *request; + const char *ident; + + ident = auth_penalty_get_ident(auth_request); + if (penalty->disabled || ident == NULL || + auth_request->fields.no_penalty) { + callback(0, auth_request); + return; + } + + request = i_new(struct auth_penalty_request, 1); + request->auth_request = auth_request; + request->client = penalty->client; + request->callback = callback; + auth_request_ref(auth_request); + + T_BEGIN { + anvil_client_query(penalty->client, + t_strdup_printf("PENALTY-GET\t%s", ident), + auth_penalty_anvil_callback, request); + } T_END; +} + +static unsigned int +get_userpass_checksum(struct auth_request *auth_request) +{ + return auth_request->mech_password == NULL ? 0 : + crc32_str_more(crc32_str(auth_request->mech_password), + auth_request->fields.user); +} + +void auth_penalty_update(struct auth_penalty *penalty, + struct auth_request *auth_request, unsigned int value) +{ + const char *ident; + + ident = auth_penalty_get_ident(auth_request); + if (penalty->disabled || ident == NULL || + auth_request->fields.no_penalty) + return; + + if (value > AUTH_PENALTY_MAX_PENALTY) { + /* even if the actual value doesn't change, the last_change + timestamp does. */ + value = AUTH_PENALTY_MAX_PENALTY; + } + T_BEGIN { + const char *cmd; + unsigned int checksum; + + checksum = value == 0 ? 0 : get_userpass_checksum(auth_request); + cmd = t_strdup_printf("PENALTY-INC\t%s\t%u\t%u", + ident, checksum, value); + anvil_client_cmd(penalty->client, cmd); + } T_END; +} diff --git a/src/auth/auth-penalty.h b/src/auth/auth-penalty.h new file mode 100644 index 0000000..96783e4 --- /dev/null +++ b/src/auth/auth-penalty.h @@ -0,0 +1,28 @@ +#ifndef AUTH_PENALTY_H +#define AUTH_PENALTY_H + +struct auth_request; + +#define AUTH_PENALTY_INIT_SECS 2 +#define AUTH_PENALTY_MAX_SECS 15 +/* timeout specifies how long it takes for penalty to be irrelevant. */ +#define AUTH_PENALTY_TIMEOUT \ + (AUTH_PENALTY_INIT_SECS + 4 + 8 + AUTH_PENALTY_MAX_SECS) +#define AUTH_PENALTY_MAX_PENALTY 4 + +/* If lookup failed, penalty and last_update are both zero */ +typedef void auth_penalty_callback_t(unsigned int penalty, + struct auth_request *request); + +struct auth_penalty *auth_penalty_init(const char *path); +void auth_penalty_deinit(struct auth_penalty **penalty); + +unsigned int auth_penalty_to_secs(unsigned int penalty); + +void auth_penalty_lookup(struct auth_penalty *penalty, + struct auth_request *auth_request, + auth_penalty_callback_t *callback); +void auth_penalty_update(struct auth_penalty *penalty, + struct auth_request *auth_request, unsigned int value); + +#endif diff --git a/src/auth/auth-policy.c b/src/auth/auth-policy.c new file mode 100644 index 0000000..951f85e --- /dev/null +++ b/src/auth/auth-policy.c @@ -0,0 +1,620 @@ +/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "net.h" +#include "str.h" +#include "istream.h" +#include "ioloop.h" +#include "base64.h" +#include "hex-binary.h" +#include "hash-method.h" +#include "http-url.h" +#include "http-client.h" +#include "json-parser.h" +#include "master-service.h" +#include "master-service-ssl-settings.h" +#include "auth-request.h" +#include "auth-penalty.h" +#include "auth-settings.h" +#include "auth-policy.h" +#include "auth-common.h" +#include "iostream-ssl.h" + +#define AUTH_POLICY_DNS_SOCKET_PATH "dns-client" + +static struct http_client_settings http_client_set = { + .dns_client_socket_path = AUTH_POLICY_DNS_SOCKET_PATH, + .max_connect_attempts = 1, + .max_idle_time_msecs = 10000, + .max_parallel_connections = 100, + .debug = 0, + .user_agent = "dovecot/auth-policy-client" +}; + +static char *auth_policy_json_template; + +static struct http_client *http_client; + +struct policy_lookup_ctx { + pool_t pool; + string_t *json; + struct auth_request *request; + struct http_client_request *http_request; + struct json_parser *parser; + const struct auth_settings *set; + const char *url; + bool expect_result; + int result; + const char *message; + auth_policy_callback_t callback; + void *callback_context; + + struct istream *payload; + struct io *io; + struct event *event; + + enum { + POLICY_RESULT = 0, + POLICY_RESULT_VALUE_STATUS, + POLICY_RESULT_VALUE_MESSAGE + } parse_state; + + bool parse_error; +}; + +struct policy_template_keyvalue { + const char *key; + const char *value; +}; + +static +int auth_policy_attribute_comparator(const struct policy_template_keyvalue *a, + const struct policy_template_keyvalue *b) +{ + return strcmp(a->key, b->key); +} + +static +int auth_policy_strptrcmp(const char *a0, const char *a1, + const char *b0, const char *b1) +{ + i_assert(a0 <= a1 && b0 <= b1); + return memcmp(a0, b0, I_MIN((a1-a0),(b1-b0))); +} + +static +void auth_policy_open_key(const char *key, string_t *template) +{ + const char *ptr; + while((ptr = strchr(key, '/')) != NULL) { + str_append_c(template,'"'); + json_append_escaped(template, t_strndup(key, (ptr-key))); + str_append_c(template,'"'); + str_append_c(template,':'); + str_append_c(template,'{'); + key = ptr+1; + } +} + +static +void auth_policy_close_key(const char *key, string_t *template) +{ + while((key = strchr(key, '/')) != NULL) { str_append_c(template,'}'); key++; } +} + +static +void auth_policy_open_and_close_to_key(const char *fromkey, const char *tokey, string_t *template) +{ + const char *fptr,*tptr,*fdash,*tdash; + + fptr = strrchr(fromkey, '/'); + tptr = strrchr(tokey, '/'); + + if (fptr == NULL && tptr == NULL) return; /* nothing to do */ + + if (fptr == NULL && tptr != NULL) { + auth_policy_open_key(tokey, template); + return; + } + + if (fptr != NULL && tptr == NULL) { + str_truncate(template, str_len(template)-1); + + auth_policy_close_key(fromkey, template); + str_append_c(template, ','); + return; + } + + if (auth_policy_strptrcmp(fromkey, fptr, tokey, tptr) == 0) { + /* nothing to do, again */ + return; + } + + fptr = fromkey; + tptr = tokey; + + while (fptr != NULL && tptr != NULL) { + fdash = strchr(fptr, '/'); + tdash = strchr(tptr, '/'); + + if (fdash == NULL) { + auth_policy_open_key(tptr, template); + break; + } + if (tdash == NULL) { + str_truncate(template, str_len(template)-1); + auth_policy_close_key(fptr, template); + str_append_c(template, ','); + break; + } + if (auth_policy_strptrcmp(fptr, fdash, tptr, tdash) != 0) { + str_truncate(template, str_len(template)-1); + auth_policy_close_key(fptr, template); + str_append_c(template, ','); + auth_policy_open_key(tptr, template); + break; + } + fptr = fdash+1; + tptr = tdash+1; + } +} + +void auth_policy_init(void) +{ + const struct master_service_ssl_settings *master_ssl_set = + master_service_ssl_settings_get(master_service); + struct ssl_iostream_settings ssl_set; + i_zero(&ssl_set); + + http_client_set.request_absolute_timeout_msecs = global_auth_settings->policy_server_timeout_msecs; + if (global_auth_settings->debug) + http_client_set.debug = 1; + + master_service_ssl_client_settings_to_iostream_set(master_ssl_set, + pool_datastack_create(), &ssl_set); + http_client_set.ssl = &ssl_set; + http_client_set.event_parent = auth_event; + http_client = http_client_init(&http_client_set); + + /* prepare template */ + + ARRAY(struct policy_template_keyvalue) attribute_pairs; + const struct policy_template_keyvalue *kvptr; + string_t *template = t_str_new(64); + const char **ptr; + const char *key = NULL; + const char **list = t_strsplit_spaces(global_auth_settings->policy_request_attributes, "= "); + + t_array_init(&attribute_pairs, 8); + for(ptr = list; *ptr != NULL; ptr++) { + struct policy_template_keyvalue pair; + if (key == NULL) { + key = *ptr; + } else { + pair.key = key; + pair.value = *ptr; + key = NULL; + array_push_back(&attribute_pairs, &pair); + } + } + if (key != NULL) { + i_fatal("auth_policy_request_attributes contains invalid value"); + } + + /* then we sort it */ + array_sort(&attribute_pairs, auth_policy_attribute_comparator); + + /* and build a template string */ + const char *prevkey = ""; + + array_foreach(&attribute_pairs, kvptr) { + const char *kptr = strchr(kvptr->key, '/'); + auth_policy_open_and_close_to_key(prevkey, kvptr->key, template); + str_append_c(template,'"'); + json_append_escaped(template, (kptr != NULL?kptr+1:kvptr->key)); + str_append_c(template,'"'); + str_append_c(template,':'); + str_append_c(template,'"'); + str_append(template,kvptr->value); + str_append_c(template,'"'); + str_append_c(template,','); + prevkey = kvptr->key; + } + + auth_policy_open_and_close_to_key(prevkey, "", template); + str_truncate(template, str_len(template)-1); + auth_policy_json_template = i_strdup(str_c(template)); + + if (global_auth_settings->policy_log_only) + i_warning("auth-policy: Currently in log-only mode. Ignoring " + "tarpit and disconnect instructions from policy server"); +} + +void auth_policy_deinit(void) +{ + if (http_client != NULL) + http_client_deinit(&http_client); + i_free(auth_policy_json_template); +} + +static +void auth_policy_log_result(struct policy_lookup_ctx *context) +{ + const char *action; + struct event_passthrough *e = event_create_passthrough(context->event)-> + set_name("auth_policy_request_finished"); + if (!context->expect_result) { + e_debug(e->event(), "Policy report action finished"); + return; + } + int result = context->result; + e->add_int("policy_response", context->result); + if (result < 0) + action = "drop connection"; + else if (context->result == 0) + action = "continue"; + else + action = t_strdup_printf("tarpit %d second(s)", context->result); + if (context->request->set->policy_log_only && result != 0) + e_info(e->event(), "Policy check action '%s' ignored", + action); + else if (result != 0) + e_info(e->event(), "Policy check action is %s", + action); + else + e_debug(e->event(), "Policy check action is %s", + action); +} + +static +void auth_policy_finish(struct policy_lookup_ctx *context) +{ + if (context->parser != NULL) { + const char *error ATTR_UNUSED; + (void)json_parser_deinit(&context->parser, &error); + } + http_client_request_abort(&context->http_request); + if (context->request != NULL) + auth_request_unref(&context->request); + event_unref(&context->event); + pool_unref(&context->pool); +} + +static +void auth_policy_callback(struct policy_lookup_ctx *context) +{ + if (context->callback != NULL) + context->callback(context->result, context->callback_context); + if (context->event != NULL) + auth_policy_log_result(context); +} + +static +void auth_policy_parse_response(struct policy_lookup_ctx *context) +{ + enum json_type type; + const char *value; + int ret; + + while((ret = json_parse_next(context->parser, &type, &value)) == 1) { + if (context->parse_state == POLICY_RESULT) { + if (type != JSON_TYPE_OBJECT_KEY) + continue; + else if (strcmp(value, "status") == 0) + context->parse_state = POLICY_RESULT_VALUE_STATUS; + else if (strcmp(value, "msg") == 0) + context->parse_state = POLICY_RESULT_VALUE_MESSAGE; + else + continue; + } else if (context->parse_state == POLICY_RESULT_VALUE_STATUS) { + if (type != JSON_TYPE_NUMBER || str_to_int(value, &context->result) != 0) + break; + context->parse_state = POLICY_RESULT; + } else if (context->parse_state == POLICY_RESULT_VALUE_MESSAGE) { + if (type != JSON_TYPE_STRING) + break; + if (*value != '\0') + context->message = p_strdup(context->pool, value); + context->parse_state = POLICY_RESULT; + } else { + break; + } + } + + if (ret == 0 && !context->payload->eof) + return; + + context->parse_error = TRUE; + + io_remove(&context->io); + + if (context->payload->stream_errno != 0) { + e_error(context->event, + "Error reading policy server result: %s", + i_stream_get_error(context->payload)); + } else if (ret == 0 && context->payload->eof) { + e_error(context->event, + "Policy server result was too short"); + } else if (ret == 1) { + e_error(context->event, + "Policy server response was malformed"); + } else { + const char *error = "unknown"; + if (json_parser_deinit(&context->parser, &error) != 0) + e_error(context->event, + "Policy server response JSON parse error: %s", error); + else if (context->parse_state == POLICY_RESULT) + context->parse_error = FALSE; + } + + if (context->parse_error) { + context->result = (context->set->policy_reject_on_fail ? -1 : 0); + } + + context->request->policy_refusal = FALSE; + + if (context->result < 0) { + if (context->message != NULL) { + /* set message here */ + e_debug(context->event, + "Policy response %d with message: %s", + context->result, context->message); + auth_request_set_field(context->request, "reason", context->message, NULL); + } + context->request->policy_refusal = TRUE; + } else { + e_debug(context->event, + "Policy response %d", context->result); + } + + if (context->request->policy_refusal == TRUE && context->set->verbose == TRUE) { + e_info(context->event, "Authentication failure due to policy server refusal%s%s", + (context->message!=NULL?": ":""), + (context->message!=NULL?context->message:"")); + } + + auth_policy_callback(context); + i_stream_unref(&context->payload); +} + +static +void auth_policy_process_response(const struct http_response *response, + void *ctx) +{ + struct policy_lookup_ctx *context = ctx; + + context->payload = response->payload; + + if ((response->status / 10) != 20) { + e_error(context->event, + "Policy server HTTP error: %s", + http_response_get_message(response)); + auth_policy_callback(context); + return; + } + + if (response->payload == NULL) { + if (context->expect_result) + e_error(context->event, + "Policy server result was empty"); + auth_policy_callback(context); + return; + } + + if (context->expect_result) { + i_stream_ref(response->payload); + context->io = io_add_istream(response->payload, auth_policy_parse_response, context); + context->parser = json_parser_init(response->payload); + auth_policy_parse_response(ctx); + } else { + auth_policy_callback(context); + } +} + +static +void auth_policy_send_request(struct policy_lookup_ctx *context) +{ + const char *error; + struct http_url *url; + + auth_request_ref(context->request); + if (http_url_parse(context->url, NULL, HTTP_URL_ALLOW_USERINFO_PART, + context->pool, &url, &error) != 0) { + e_error(context->event, + "Could not parse url %s: %s", context->url, error); + auth_policy_callback(context); + auth_policy_finish(context); + return; + } + context->http_request = http_client_request_url(http_client, + "POST", url, auth_policy_process_response, (void*)context); + http_client_request_set_destroy_callback(context->http_request, auth_policy_finish, context); + http_client_request_add_header(context->http_request, "Content-Type", "application/json"); + if (*context->set->policy_server_api_header != 0) { + const char *ptr; + if ((ptr = strstr(context->set->policy_server_api_header, ":")) != NULL) { + const char *header = t_strcut(context->set->policy_server_api_header, ':'); + http_client_request_add_header(context->http_request, header, ptr + 1); + } else { + http_client_request_add_header(context->http_request, + "X-API-Key", context->set->policy_server_api_header); + } + } + if (url->user != NULL) { + /* allow empty password */ + http_client_request_set_auth_simple(context->http_request, url->user, + (url->password != NULL ? url->password : "")); + } + struct istream *is = i_stream_create_from_buffer(context->json); + http_client_request_set_payload(context->http_request, is, FALSE); + i_stream_unref(&is); + http_client_request_submit(context->http_request); +} + +static +const char *auth_policy_escape_function(const char *string, + const struct auth_request *auth_request ATTR_UNUSED) +{ + string_t *tmp = t_str_new(64); + json_append_escaped(tmp, string); + return str_c(tmp); +} + +static +const struct var_expand_table *policy_get_var_expand_table(struct auth_request *auth_request, + const char *hashed_password, const char *requested_username) +{ + struct var_expand_table *table; + unsigned int count = 2; + + table = auth_request_get_var_expand_table_full(auth_request, + auth_request->fields.user, auth_policy_escape_function, &count); + table[0].key = '\0'; + table[0].long_key = "hashed_password"; + table[0].value = hashed_password; + table[1].key = '\0'; + table[1].long_key = "requested_username"; + table[1].value = requested_username; + if (table[0].value != NULL) + table[0].value = auth_policy_escape_function(table[0].value, auth_request); + if (table[1].value != NULL) + table[1].value = auth_policy_escape_function(table[1].value, auth_request); + + return table; +} + +static +void auth_policy_create_json(struct policy_lookup_ctx *context, + const char *password, bool include_success) +{ + const struct var_expand_table *var_table; + context->json = str_new(context->pool, 64); + unsigned char *ptr; + const char *requested_username; + const struct hash_method *digest = hash_method_lookup(context->set->policy_hash_mech); + + i_assert(digest != NULL); + + void *ctx = t_malloc_no0(digest->context_size); + buffer_t *buffer = t_buffer_create(64); + + digest->init(ctx); + digest->loop(ctx, + context->set->policy_hash_nonce, + strlen(context->set->policy_hash_nonce)); + if (context->request->fields.requested_login_user != NULL) + requested_username = context->request->fields.requested_login_user; + else if (context->request->fields.user != NULL) + requested_username = context->request->fields.user; + else + requested_username = ""; + /* use +1 to make sure \0 gets included */ + digest->loop(ctx, requested_username, strlen(requested_username)+1); + if (password != NULL) + digest->loop(ctx, password, strlen(password)); + ptr = buffer_get_modifiable_data(buffer, NULL); + digest->result(ctx, ptr); + buffer_set_used_size(buffer, digest->digest_size); + if (context->set->policy_hash_truncate > 0) { + buffer_truncate_rshift_bits(buffer, context->set->policy_hash_truncate); + } + const char *hashed_password = binary_to_hex(buffer->data, buffer->used); + str_append_c(context->json, '{'); + var_table = policy_get_var_expand_table(context->request, hashed_password, requested_username); + const char *error; + if (auth_request_var_expand_with_table(context->json, auth_policy_json_template, + context->request, var_table, + auth_policy_escape_function, &error) <= 0) { + e_error(context->event, + "Failed to expand auth policy template: %s", error); + } + if (include_success) { + str_append(context->json, ",\"success\":"); + if (!context->request->failed && + context->request->fields.successful && + !context->request->internal_failure) + str_append(context->json, "true"); + else + str_append(context->json, "false"); + str_append(context->json, ",\"policy_reject\":"); + str_append(context->json, context->request->policy_refusal ? "true" : "false"); + } + str_append(context->json, ",\"tls\":"); + if (context->request->fields.secured == AUTH_REQUEST_SECURED_TLS) + str_append(context->json, "true"); + else + str_append(context->json, "false"); + str_append_c(context->json, '}'); + e_debug(context->event, + "Policy server request JSON: %s", str_c(context->json)); +} + +static +void auth_policy_url(struct policy_lookup_ctx *context, const char *command) +{ + size_t len = strlen(context->set->policy_server_url); + if (context->set->policy_server_url[len-1] == '&') + context->url = p_strdup_printf(context->pool, "%scommand=%s", + context->set->policy_server_url, command); + else + context->url = p_strdup_printf(context->pool, "%s?command=%s", + context->set->policy_server_url, command); +} + +static const char *auth_policy_get_prefix(struct auth_request *request) +{ + string_t *str = t_str_new(256); + auth_request_get_log_prefix(str, request, "policy"); + return str_c(str); +} + +void auth_policy_check(struct auth_request *request, const char *password, + auth_policy_callback_t cb, void *context) +{ + if (request->master != NULL || *(request->set->policy_server_url) == '\0') { + cb(0, context); + return; + } + pool_t pool = pool_alloconly_create("auth policy", 512); + struct policy_lookup_ctx *ctx = p_new(pool, struct policy_lookup_ctx, 1); + ctx->pool = pool; + ctx->request = request; + ctx->expect_result = TRUE; + ctx->callback = cb; + ctx->callback_context = context; + ctx->set = request->set; + ctx->event = event_create(request->event); + event_add_str(ctx->event, "mode", "allow"); + event_set_append_log_prefix(ctx->event, auth_policy_get_prefix(request)); + auth_policy_url(ctx, "allow"); + ctx->result = (ctx->set->policy_reject_on_fail ? -1 : 0); + e_debug(ctx->event, "Policy request %s", ctx->url); + T_BEGIN { + auth_policy_create_json(ctx, password, FALSE); + } T_END; + auth_policy_send_request(ctx); +} + +void auth_policy_report(struct auth_request *request) +{ + if (request->master != NULL) + return; + + if (*(request->set->policy_server_url) == '\0') + return; + pool_t pool = pool_alloconly_create("auth policy", 512); + struct policy_lookup_ctx *ctx = p_new(pool, struct policy_lookup_ctx, 1); + ctx->pool = pool; + ctx->request = request; + ctx->expect_result = FALSE; + ctx->set = request->set; + ctx->event = event_create(request->event); + event_add_str(ctx->event, "mode", "report"); + event_set_append_log_prefix(ctx->event, auth_policy_get_prefix(request)); + auth_policy_url(ctx, "report"); + e_debug(ctx->event, "Policy request %s", ctx->url); + T_BEGIN { + auth_policy_create_json(ctx, request->mech_password, TRUE); + } T_END; + auth_policy_send_request(ctx); +} diff --git a/src/auth/auth-policy.h b/src/auth/auth-policy.h new file mode 100644 index 0000000..1c81945 --- /dev/null +++ b/src/auth/auth-policy.h @@ -0,0 +1,11 @@ +#ifndef AUTH_POLICY_H +#define AUTH_POLICY_H + +typedef void (*auth_policy_callback_t)(int, void *); + +void auth_policy_check(struct auth_request *request, const char *password, auth_policy_callback_t cb, void *context); +void auth_policy_report(struct auth_request *request); +void auth_policy_init(void); +void auth_policy_deinit(void); + +#endif diff --git a/src/auth/auth-request-fields.c b/src/auth/auth-request-fields.c new file mode 100644 index 0000000..590e671 --- /dev/null +++ b/src/auth/auth-request-fields.c @@ -0,0 +1,525 @@ +/* Copyright (c) 2002-2020 Dovecot authors, see the included COPYING file */ + +#define AUTH_REQUEST_FIELDS_CONST + +#include "auth-common.h" +#include "str.h" +#include "strescape.h" +#include "str-sanitize.h" +#include "auth-request.h" +#include "userdb-template.h" + +void auth_request_fields_init(struct auth_request *request) +{ + request->fields.extra_fields = auth_fields_init(request->pool); + if (request->mech != NULL) { + request->fields.mech_name = request->mech->mech_name; + event_add_str(request->event, "mechanism", + request->mech->mech_name); + } + /* Default to "insecure" until it's changed later */ + event_add_str(request->event, "transport", "insecure"); +} + +static void +auth_str_add_keyvalue(string_t *dest, const char *key, const char *value) +{ + str_append_c(dest, '\t'); + str_append(dest, key); + if (value != NULL) { + str_append_c(dest, '='); + str_append_tabescaped(dest, value); + } +} + +static void +auth_request_export_fields(string_t *dest, struct auth_fields *auth_fields, + const char *prefix) +{ + const ARRAY_TYPE(auth_field) *fields = auth_fields_export(auth_fields); + const struct auth_field *field; + + array_foreach(fields, field) { + str_printfa(dest, "\t%s%s", prefix, field->key); + if (field->value != NULL) { + str_append_c(dest, '='); + str_append_tabescaped(dest, field->value); + } + } +} + +void auth_request_export(struct auth_request *request, string_t *dest) +{ + const struct auth_request_fields *fields = &request->fields; + + str_append(dest, "user="); + str_append_tabescaped(dest, fields->user); + + auth_str_add_keyvalue(dest, "service", fields->service); + + if (fields->master_user != NULL) + auth_str_add_keyvalue(dest, "master-user", fields->master_user); + auth_str_add_keyvalue(dest, "original-username", + fields->original_username); + if (fields->requested_login_user != NULL) { + auth_str_add_keyvalue(dest, "requested-login-user", + fields->requested_login_user); + } + + if (fields->local_ip.family != 0) { + auth_str_add_keyvalue(dest, "lip", + net_ip2addr(&fields->local_ip)); + } + if (fields->remote_ip.family != 0) { + auth_str_add_keyvalue(dest, "rip", + net_ip2addr(&fields->remote_ip)); + } + if (fields->local_port != 0) + str_printfa(dest, "\tlport=%u", fields->local_port); + if (fields->remote_port != 0) + str_printfa(dest, "\trport=%u", fields->remote_port); + if (fields->real_local_ip.family != 0) { + auth_str_add_keyvalue(dest, "real_lip", + net_ip2addr(&fields->real_local_ip)); + } + if (fields->real_remote_ip.family != 0) { + auth_str_add_keyvalue(dest, "real_rip", + net_ip2addr(&fields->real_remote_ip)); + } + if (fields->real_local_port != 0) + str_printfa(dest, "\treal_lport=%u", fields->real_local_port); + if (fields->real_remote_port != 0) + str_printfa(dest, "\treal_rport=%u", fields->real_remote_port); + if (fields->local_name != 0) { + str_append(dest, "\tlocal_name="); + str_append_tabescaped(dest, fields->local_name); + } + if (fields->session_id != NULL) { + str_append(dest, "\tsession="); + str_append_tabescaped(dest, fields->session_id); + } + if (event_want_debug(request->event)) + str_append(dest, "\tdebug"); + switch (fields->secured) { + case AUTH_REQUEST_SECURED_NONE: break; + case AUTH_REQUEST_SECURED: str_append(dest, "\tsecured"); break; + case AUTH_REQUEST_SECURED_TLS: str_append(dest, "\tsecured=tls"); break; + default: break; + } + if (fields->skip_password_check) + str_append(dest, "\tskip-password-check"); + if (fields->delayed_credentials != NULL) + str_append(dest, "\tdelayed-credentials"); + if (fields->valid_client_cert) + str_append(dest, "\tvalid-client-cert"); + if (fields->no_penalty) + str_append(dest, "\tno-penalty"); + if (fields->successful) + str_append(dest, "\tsuccessful"); + if (fields->mech_name != NULL) + auth_str_add_keyvalue(dest, "mech", fields->mech_name); + if (fields->client_id != NULL) + auth_str_add_keyvalue(dest, "client_id", fields->client_id); + /* export passdb extra fields */ + auth_request_export_fields(dest, fields->extra_fields, "passdb_"); + /* export any userdb fields */ + if (fields->userdb_reply != NULL) + auth_request_export_fields(dest, fields->userdb_reply, "userdb_"); +} + +bool auth_request_import_info(struct auth_request *request, + const char *key, const char *value) +{ + struct auth_request_fields *fields = &request->fields; + struct event *event = request->event; + + i_assert(value != NULL); + + /* authentication and user lookups may set these */ + if (strcmp(key, "service") == 0) { + fields->service = p_strdup(request->pool, value); + event_add_str(event, "service", value); + } else if (strcmp(key, "lip") == 0) { + if (net_addr2ip(value, &fields->local_ip) < 0) + return TRUE; + event_add_str(event, "local_ip", value); + if (fields->real_local_ip.family == 0) + auth_request_import_info(request, "real_lip", value); + } else if (strcmp(key, "rip") == 0) { + if (net_addr2ip(value, &fields->remote_ip) < 0) + return TRUE; + event_add_str(event, "remote_ip", value); + if (fields->real_remote_ip.family == 0) + auth_request_import_info(request, "real_rip", value); + } else if (strcmp(key, "lport") == 0) { + if (net_str2port(value, &fields->local_port) < 0) + return TRUE; + event_add_int(event, "local_port", fields->local_port); + if (fields->real_local_port == 0) + auth_request_import_info(request, "real_lport", value); + } else if (strcmp(key, "rport") == 0) { + if (net_str2port(value, &fields->remote_port) < 0) + return TRUE; + event_add_int(event, "remote_port", fields->remote_port); + if (fields->real_remote_port == 0) + auth_request_import_info(request, "real_rport", value); + } else if (strcmp(key, "real_lip") == 0) { + if (net_addr2ip(value, &fields->real_local_ip) == 0) + event_add_str(event, "real_local_ip", value); + } else if (strcmp(key, "real_rip") == 0) { + if (net_addr2ip(value, &fields->real_remote_ip) == 0) + event_add_str(event, "real_remote_ip", value); + } else if (strcmp(key, "real_lport") == 0) { + if (net_str2port(value, &fields->real_local_port) == 0) + event_add_int(event, "real_local_port", + fields->real_local_port); + } else if (strcmp(key, "real_rport") == 0) { + if (net_str2port(value, &fields->real_remote_port) == 0) + event_add_int(event, "real_remote_port", + fields->real_remote_port); + } else if (strcmp(key, "local_name") == 0) { + fields->local_name = p_strdup(request->pool, value); + event_add_str(event, "local_name", value); + } else if (strcmp(key, "session") == 0) { + fields->session_id = p_strdup(request->pool, value); + event_add_str(event, "session", value); + } else if (strcmp(key, "debug") == 0) + event_set_forced_debug(request->event, TRUE); + else if (strcmp(key, "client_id") == 0) { + fields->client_id = p_strdup(request->pool, value); + event_add_str(event, "client_id", value); + } else if (strcmp(key, "forward_fields") == 0) { + auth_fields_import_prefixed(fields->extra_fields, + "forward_", value, 0); + /* make sure the forward_ fields aren't deleted by + auth_fields_rollback() if the first passdb lookup fails. */ + auth_fields_snapshot(fields->extra_fields); + } else + return FALSE; + /* NOTE: keep in sync with auth_request_export() */ + return TRUE; +} + +bool auth_request_import_auth(struct auth_request *request, + const char *key, const char *value) +{ + struct auth_request_fields *fields = &request->fields; + + i_assert(value != NULL); + + if (auth_request_import_info(request, key, value)) + return TRUE; + + /* auth client may set these */ + if (strcmp(key, "secured") == 0) { + if (strcmp(value, "tls") == 0) { + fields->secured = AUTH_REQUEST_SECURED_TLS; + event_add_str(request->event, "transport", "TLS"); + } else { + fields->secured = AUTH_REQUEST_SECURED; + event_add_str(request->event, "transport", "trusted"); + } + } + else if (strcmp(key, "final-resp-ok") == 0) + fields->final_resp_ok = TRUE; + else if (strcmp(key, "no-penalty") == 0) + fields->no_penalty = TRUE; + else if (strcmp(key, "valid-client-cert") == 0) + fields->valid_client_cert = TRUE; + else if (strcmp(key, "cert_username") == 0) { + if (request->set->ssl_username_from_cert && *value != '\0') { + /* get username from SSL certificate. it overrides + the username given by the auth mechanism. */ + auth_request_set_username_forced(request, value); + fields->cert_username = TRUE; + } + } else { + return FALSE; + } + return TRUE; +} + +bool auth_request_import(struct auth_request *request, + const char *key, const char *value) +{ + struct auth_request_fields *fields = &request->fields; + + i_assert(value != NULL); + + if (auth_request_import_auth(request, key, value)) + return TRUE; + + /* for communication between auth master and worker processes */ + if (strcmp(key, "user") == 0) + auth_request_set_username_forced(request, value); + else if (strcmp(key, "master-user") == 0) { + fields->master_user = p_strdup(request->pool, value); + event_add_str(request->event, "master_user", value); + } else if (strcmp(key, "original-username") == 0) { + fields->original_username = p_strdup(request->pool, value); + event_add_str(request->event, "original_user", value); + } else if (strcmp(key, "requested-login-user") == 0) + auth_request_set_login_username_forced(request, value); + else if (strcmp(key, "successful") == 0) + auth_request_set_auth_successful(request); + else if (strcmp(key, "skip-password-check") == 0) + auth_request_set_password_verified(request); + else if (strcmp(key, "delayed-credentials") == 0) { + /* just make passdb_handle_credentials() work identically in + auth-worker as it does in auth-master. the worker shouldn't + care about the actual contents of the credentials. */ + fields->delayed_credentials = &uchar_nul; + fields->delayed_credentials_size = 1; + } else if (strcmp(key, "mech") == 0) { + fields->mech_name = p_strdup(request->pool, value); + event_add_str(request->event, "mechanism", value); + } else if (str_begins(key, "passdb_")) + auth_fields_add(fields->extra_fields, key+7, value, 0); + else if (str_begins(key, "userdb_")) { + if (fields->userdb_reply == NULL) + auth_request_init_userdb_reply(request, FALSE); + auth_fields_add(fields->userdb_reply, key+7, value, 0); + } else + return FALSE; + + return TRUE; +} + +static int +auth_request_fix_username(struct auth_request *request, const char **username, + const char **error_r) +{ + const struct auth_settings *set = request->set; + unsigned char *p; + char *user; + + if (*set->default_realm != '\0' && + strchr(*username, '@') == NULL) { + user = p_strconcat(unsafe_data_stack_pool, *username, "@", + set->default_realm, NULL); + } else { + user = t_strdup_noconst(*username); + } + + for (p = (unsigned char *)user; *p != '\0'; p++) { + if (set->username_translation_map[*p & 0xff] != 0) + *p = set->username_translation_map[*p & 0xff]; + if (set->username_chars_map[*p & 0xff] == 0) { + *error_r = t_strdup_printf( + "Username character disallowed by auth_username_chars: " + "0x%02x (username: %s)", *p, + str_sanitize(*username, 128)); + return -1; + } + } + + if (*set->username_format != '\0') { + /* username format given, put it through variable expansion. + we'll have to temporarily replace request->user to get + %u to be the wanted username */ + const char *error; + string_t *dest; + + dest = t_str_new(256); + unsigned int count = 0; + const struct var_expand_table *table = + auth_request_get_var_expand_table_full(request, + user, NULL, &count); + if (auth_request_var_expand_with_table(dest, + set->username_format, request, + table, NULL, &error) <= 0) { + *error_r = t_strdup_printf( + "Failed to expand username_format=%s: %s", + set->username_format, error); + } + user = str_c_modifiable(dest); + } + + if (user[0] == '\0') { + /* Some PAM plugins go nuts with empty usernames */ + *error_r = "Empty username"; + return -1; + } + *username = user; + return 0; +} + +bool auth_request_set_username(struct auth_request *request, + const char *username, const char **error_r) +{ + const struct auth_settings *set = request->set; + const char *p, *login_username = NULL; + + if (*set->master_user_separator != '\0' && !request->userdb_lookup) { + /* check if the username contains a master user */ + p = strchr(username, *set->master_user_separator); + if (p != NULL) { + /* it does, set it. */ + login_username = t_strdup_until(username, p); + + /* username is the master user */ + username = p + 1; + } + } + + if (request->fields.original_username == NULL) { + /* the username may change later, but we need to use this + username when verifying at least DIGEST-MD5 password. */ + request->fields.original_username = + p_strdup(request->pool, username); + event_add_str(request->event, "original_user", + request->fields.original_username); + } + if (request->fields.cert_username) { + /* cert_username overrides the username given by + authentication mechanism. but still do checks and + translations to it. */ + username = request->fields.user; + } + + if (auth_request_fix_username(request, &username, error_r) < 0) { + request->fields.user = NULL; + event_field_clear(request->event, "user"); + return FALSE; + } + auth_request_set_username_forced(request, username); + if (request->fields.translated_username == NULL) { + /* similar to original_username, but after translations */ + request->fields.translated_username = request->fields.user; + event_add_str(request->event, "translated_user", + request->fields.translated_username); + } + request->user_changed_by_lookup = TRUE; + + if (login_username != NULL) { + if (!auth_request_set_login_username(request, + login_username, + error_r)) + return FALSE; + } + return TRUE; +} + +void auth_request_set_username_forced(struct auth_request *request, + const char *username) +{ + i_assert(username != NULL); + + request->fields.user = p_strdup(request->pool, username); + event_add_str(request->event, "user", request->fields.user); +} + +void auth_request_set_login_username_forced(struct auth_request *request, + const char *username) +{ + i_assert(username != NULL); + + request->fields.requested_login_user = + p_strdup(request->pool, username); + event_add_str(request->event, "login_user", + request->fields.requested_login_user); +} + +bool auth_request_set_login_username(struct auth_request *request, + const char *username, + const char **error_r) +{ + struct auth_passdb *master_passdb; + + if (username[0] == '\0') { + *error_r = "Master user login attempted to use empty login username"; + return FALSE; + } + + if (strcmp(username, request->fields.user) == 0) { + /* The usernames are the same, we don't really wish to log + in as someone else */ + return TRUE; + } + + /* lookup request->user from masterdb first */ + master_passdb = auth_request_get_auth(request)->masterdbs; + if (master_passdb == NULL) { + *error_r = "Master user login attempted without master passdbs"; + return FALSE; + } + request->passdb = master_passdb; + + if (auth_request_fix_username(request, &username, error_r) < 0) { + request->fields.requested_login_user = NULL; + event_field_clear(request->event, "login_user"); + return FALSE; + } + auth_request_set_login_username_forced(request, username); + + e_debug(request->event, + "%sMaster user lookup for login: %s", + auth_request_get_log_prefix_db(request), + request->fields.requested_login_user); + return TRUE; +} + +void auth_request_master_user_login_finish(struct auth_request *request) +{ + if (request->failed) + return; + + /* master login successful. update user and master_user variables. */ + e_info(authdb_event(request), + "Master user logging in as %s", + request->fields.requested_login_user); + + request->fields.master_user = request->fields.user; + event_add_str(request->event, "master_user", + request->fields.master_user); + + auth_request_set_username_forced(request, + request->fields.requested_login_user); + request->fields.translated_username = request->fields.requested_login_user; + event_add_str(request->event, "translated_user", + request->fields.translated_username); + request->fields.requested_login_user = NULL; + event_field_clear(request->event, "login_user"); +} + +void auth_request_set_realm(struct auth_request *request, const char *realm) +{ + i_assert(realm != NULL); + + request->fields.realm = p_strdup(request->pool, realm); + event_add_str(request->event, "realm", request->fields.realm); +} + +void auth_request_set_auth_successful(struct auth_request *request) +{ + request->fields.successful = TRUE; +} + +void auth_request_set_password_verified(struct auth_request *request) +{ + request->fields.skip_password_check = TRUE; +} + +void auth_request_init_userdb_reply(struct auth_request *request, + bool add_default_fields) +{ + const char *error; + + request->fields.userdb_reply = auth_fields_init(request->pool); + if (add_default_fields) { + if (userdb_template_export(request->userdb->default_fields_tmpl, + request, &error) < 0) { + e_error(authdb_event(request), + "Failed to expand default_fields: %s", error); + } + } +} + +void auth_request_set_delayed_credentials(struct auth_request *request, + const unsigned char *credentials, + size_t size) +{ + request->fields.delayed_credentials = + p_memdup(request->pool, credentials, size); + request->fields.delayed_credentials_size = size; +} diff --git a/src/auth/auth-request-handler-private.h b/src/auth/auth-request-handler-private.h new file mode 100644 index 0000000..4d733df --- /dev/null +++ b/src/auth/auth-request-handler-private.h @@ -0,0 +1,27 @@ +#ifndef AUTH_REQUEST_HANDLER_PRIVATE_H +#define AUTH_REQUEST_HANDLER_PRIVATE_H + +struct auth_request; +struct auth_client_connection; + +struct auth_request_handler { + int refcount; + pool_t pool; + HASH_TABLE(void *, struct auth_request *) requests; + + unsigned int connect_uid, client_pid; + + auth_client_request_callback_t *callback; + struct auth_client_connection *conn; + + auth_master_request_callback_t *master_callback; + auth_request_handler_reply_callback_t *reply_callback; + auth_request_handler_reply_continue_callback_t *reply_continue_callback; + verify_plain_continue_callback_t *verify_plain_continue_callback; + + bool destroyed:1; + bool token_auth:1; +}; + + +#endif diff --git a/src/auth/auth-request-handler.c b/src/auth/auth-request-handler.c new file mode 100644 index 0000000..d4bf53c --- /dev/null +++ b/src/auth/auth-request-handler.c @@ -0,0 +1,985 @@ +/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */ + +#include "auth-common.h" +#include "ioloop.h" +#include "array.h" +#include "aqueue.h" +#include "base64.h" +#include "hash.h" +#include "net.h" +#include "str.h" +#include "strescape.h" +#include "str-sanitize.h" +#include "master-interface.h" +#include "auth-penalty.h" +#include "auth-request.h" +#include "auth-token.h" +#include "auth-client-connection.h" +#include "auth-master-connection.h" +#include "auth-request-handler.h" +#include "auth-request-handler-private.h" +#include "auth-policy.h" + +#define AUTH_FAILURE_DELAY_CHECK_MSECS 500 +static ARRAY(struct auth_request *) auth_failures_arr; +static struct aqueue *auth_failures; +static struct timeout *to_auth_failures; + +static void auth_failure_timeout(void *context) ATTR_NULL(1); + + +static void +auth_request_handler_default_reply_callback(struct auth_request *request, + enum auth_client_result result, + const void *auth_reply, + size_t reply_size); + +static void +auth_request_handler_default_reply_continue(struct auth_request *request, + const void *reply, + size_t reply_size); + +struct auth_request_handler * +auth_request_handler_create(bool token_auth, auth_client_request_callback_t *callback, + struct auth_client_connection *conn, + auth_master_request_callback_t *master_callback) +{ + struct auth_request_handler *handler; + pool_t pool; + + pool = pool_alloconly_create("auth request handler", 4096); + + handler = p_new(pool, struct auth_request_handler, 1); + handler->refcount = 1; + handler->pool = pool; + hash_table_create_direct(&handler->requests, pool, 0); + handler->callback = callback; + handler->conn = conn; + handler->master_callback = master_callback; + handler->token_auth = token_auth; + handler->reply_callback = + auth_request_handler_default_reply_callback; + handler->reply_continue_callback = + auth_request_handler_default_reply_continue; + handler->verify_plain_continue_callback = + auth_request_default_verify_plain_continue; + return handler; +} + +unsigned int +auth_request_handler_get_request_count(struct auth_request_handler *handler) +{ + return hash_table_count(handler->requests); +} + +void auth_request_handler_abort_requests(struct auth_request_handler *handler) +{ + struct hash_iterate_context *iter; + void *key; + struct auth_request *auth_request; + + iter = hash_table_iterate_init(handler->requests); + while (hash_table_iterate(iter, handler->requests, &key, &auth_request)) { + switch (auth_request->state) { + case AUTH_REQUEST_STATE_NEW: + case AUTH_REQUEST_STATE_MECH_CONTINUE: + case AUTH_REQUEST_STATE_FINISHED: + auth_request->removed_from_handler = TRUE; + auth_request_unref(&auth_request); + hash_table_remove(handler->requests, key); + break; + case AUTH_REQUEST_STATE_PASSDB: + case AUTH_REQUEST_STATE_USERDB: + /* can't abort a pending passdb/userdb lookup */ + break; + case AUTH_REQUEST_STATE_MAX: + i_unreached(); + } + } + hash_table_iterate_deinit(&iter); +} + +void auth_request_handler_unref(struct auth_request_handler **_handler) +{ + struct auth_request_handler *handler = *_handler; + + *_handler = NULL; + + i_assert(handler->refcount > 0); + if (--handler->refcount > 0) + return; + + i_assert(hash_table_count(handler->requests) == 0); + + /* notify parent that we're done with all requests */ + handler->callback(NULL, handler->conn); + + hash_table_destroy(&handler->requests); + pool_unref(&handler->pool); +} + +void auth_request_handler_destroy(struct auth_request_handler **_handler) +{ + struct auth_request_handler *handler = *_handler; + + *_handler = NULL; + + i_assert(!handler->destroyed); + + handler->destroyed = TRUE; + auth_request_handler_unref(&handler); +} + +void auth_request_handler_set(struct auth_request_handler *handler, + unsigned int connect_uid, + unsigned int client_pid) +{ + handler->connect_uid = connect_uid; + handler->client_pid = client_pid; +} + +static void auth_request_handler_remove(struct auth_request_handler *handler, + struct auth_request *request) +{ + i_assert(request->handler == handler); + + if (request->removed_from_handler) { + /* already removed it */ + return; + } + request->removed_from_handler = TRUE; + + /* if db lookup is stuck, this call doesn't actually free the auth + request, so make sure we don't get back here. */ + timeout_remove(&request->to_abort); + + hash_table_remove(handler->requests, POINTER_CAST(request->id)); + auth_request_unref(&request); +} + +static void +auth_str_add_keyvalue(string_t *dest, const char *key, const char *value) +{ + str_append_c(dest, '\t'); + str_append(dest, key); + str_append_c(dest, '='); + str_append_tabescaped(dest, value); +} + +static void +auth_str_append_extra_fields(struct auth_request *request, string_t *dest) +{ + const struct auth_request_fields *fields = &request->fields; + + if (!auth_fields_is_empty(fields->extra_fields)) { + str_append_c(dest, '\t'); + auth_fields_append(fields->extra_fields, dest, + AUTH_FIELD_FLAG_HIDDEN, 0); + } + + if (fields->original_username != NULL && + null_strcmp(fields->original_username, fields->user) != 0 && + !auth_fields_exists(fields->extra_fields, "original_user")) { + auth_str_add_keyvalue(dest, "original_user", + fields->original_username); + } + if (fields->master_user != NULL && + !auth_fields_exists(fields->extra_fields, "auth_user")) + auth_str_add_keyvalue(dest, "auth_user", fields->master_user); + if (*request->set->anonymous_username != '\0' && + null_strcmp(fields->user, request->set->anonymous_username) == 0) { + /* this is an anonymous login, either via ANONYMOUS + SASL mechanism or simply logging in as the anonymous + user via another mechanism */ + str_append(dest, "\tanonymous"); + } + if (!request->auth_only && + auth_fields_exists(fields->extra_fields, "proxy")) { + /* we're proxying */ + if (!auth_fields_exists(fields->extra_fields, "pass") && + request->mech_password != NULL) { + /* send back the password that was sent by user + (not the password in passdb). */ + auth_str_add_keyvalue(dest, "pass", + request->mech_password); + } + if (fields->master_user != NULL && + !auth_fields_exists(fields->extra_fields, "master") && + *fields->master_user != '\0') { + /* the master username needs to be forwarded */ + auth_str_add_keyvalue(dest, "master", + fields->master_user); + } + } +} + +static void +auth_request_handle_failure(struct auth_request *request, const char *reply) +{ + struct auth_request_handler *handler = request->handler; + + /* handle failure here */ + auth_request_log_finished(request); + + if (request->in_delayed_failure_queue) { + /* we came here from flush_failures() */ + handler->callback(reply, handler->conn); + return; + } + + /* remove the request from requests-list */ + auth_request_ref(request); + auth_request_handler_remove(handler, request); + + if (request->set->policy_report_after_auth) + auth_policy_report(request); + + if (auth_fields_exists(request->fields.extra_fields, "nodelay")) { + /* passdb specifically requested not to delay the reply. */ + handler->callback(reply, handler->conn); + auth_request_unref(&request); + return; + } + + /* failure. don't announce it immediately to avoid + a) timing attacks, b) flooding */ + request->in_delayed_failure_queue = TRUE; + handler->refcount++; + + if (auth_penalty != NULL) { + auth_penalty_update(auth_penalty, request, + request->last_penalty + 1); + } + + auth_request_refresh_last_access(request); + aqueue_append(auth_failures, &request); + if (to_auth_failures == NULL) { + to_auth_failures = + timeout_add_short(AUTH_FAILURE_DELAY_CHECK_MSECS, + auth_failure_timeout, NULL); + } +} + +static void +auth_request_handler_reply_success_finish(struct auth_request *request) +{ + struct auth_request_handler *handler = request->handler; + string_t *str = t_str_new(128); + + auth_request_log_finished(request); + + if (request->last_penalty != 0 && auth_penalty != NULL) { + /* reset penalty */ + auth_penalty_update(auth_penalty, request, 0); + } + + /* sanitize these fields, since the login code currently assumes they + are exactly in this format. */ + auth_fields_booleanize(request->fields.extra_fields, "nologin"); + auth_fields_booleanize(request->fields.extra_fields, "proxy"); + + str_printfa(str, "OK\t%u\tuser=", request->id); + str_append_tabescaped(str, request->fields.user); + auth_str_append_extra_fields(request, str); + + if (request->set->policy_report_after_auth) + auth_policy_report(request); + + if (handler->master_callback == NULL || + auth_fields_exists(request->fields.extra_fields, "nologin") || + auth_fields_exists(request->fields.extra_fields, "proxy")) { + /* this request doesn't have to wait for master + process to pick it up. delete it */ + auth_request_handler_remove(handler, request); + } + + handler->callback(str_c(str), handler->conn); +} + +static void +auth_request_handler_reply_failure_finish(struct auth_request *request) +{ + const char *code = NULL; + string_t *str = t_str_new(128); + + auth_fields_remove(request->fields.extra_fields, "nologin"); + + str_printfa(str, "FAIL\t%u", request->id); + if (request->fields.user != NULL) + auth_str_add_keyvalue(str, "user", request->fields.user); + else if (request->fields.original_username != NULL) { + auth_str_add_keyvalue(str, "user", + request->fields.original_username); + } + + if (request->internal_failure) { + code = AUTH_CLIENT_FAIL_CODE_TEMPFAIL; + } else if (request->fields.master_user != NULL) { + /* authentication succeeded, but we can't log in + as the wanted user */ + code = AUTH_CLIENT_FAIL_CODE_AUTHZFAILED; + } else { + switch (request->passdb_result) { + case PASSDB_RESULT_NEXT: + case PASSDB_RESULT_INTERNAL_FAILURE: + case PASSDB_RESULT_SCHEME_NOT_AVAILABLE: + case PASSDB_RESULT_USER_UNKNOWN: + case PASSDB_RESULT_PASSWORD_MISMATCH: + case PASSDB_RESULT_OK: + break; + case PASSDB_RESULT_USER_DISABLED: + code = AUTH_CLIENT_FAIL_CODE_USER_DISABLED; + break; + case PASSDB_RESULT_PASS_EXPIRED: + code = AUTH_CLIENT_FAIL_CODE_PASS_EXPIRED; + break; + } + } + + if (auth_fields_exists(request->fields.extra_fields, "nodelay")) { + /* this is normally a hidden field, need to add it explicitly */ + str_append(str, "\tnodelay"); + } + + if (code != NULL) { + str_append(str, "\tcode="); + str_append(str, code); + } + auth_str_append_extra_fields(request, str); + + auth_request_handle_failure(request, str_c(str)); +} + +static void +auth_request_handler_proxy_callback(bool success, struct auth_request *request) +{ + struct auth_request_handler *handler = request->handler; + + if (success) + auth_request_handler_reply_success_finish(request); + else + auth_request_handler_reply_failure_finish(request); + auth_request_handler_unref(&handler); +} + +void auth_request_handler_reply(struct auth_request *request, + enum auth_client_result result, + const void *auth_reply, size_t reply_size) +{ + struct auth_request_handler *handler = request->handler; + + request->handler_pending_reply = FALSE; + handler->reply_callback(request, result, auth_reply, reply_size); +} + +static void +auth_request_handler_default_reply_callback(struct auth_request *request, + enum auth_client_result result, + const void *auth_reply, + size_t reply_size) +{ + struct auth_request_handler *handler = request->handler; + string_t *str; + int ret; + + if (handler->destroyed) { + /* the client connection was already closed. we can't do + anything but abort this request */ + request->internal_failure = TRUE; + result = AUTH_CLIENT_RESULT_FAILURE; + /* make sure this request is set to finished state + (it's not with result=continue) */ + auth_request_set_state(request, AUTH_REQUEST_STATE_FINISHED); + } + + switch (result) { + case AUTH_CLIENT_RESULT_CONTINUE: + str = t_str_new(16 + MAX_BASE64_ENCODED_SIZE(reply_size)); + str_printfa(str, "CONT\t%u\t", request->id); + base64_encode(auth_reply, reply_size, str); + + request->accept_cont_input = TRUE; + handler->callback(str_c(str), handler->conn); + break; + case AUTH_CLIENT_RESULT_SUCCESS: + if (reply_size > 0) { + str = t_str_new(MAX_BASE64_ENCODED_SIZE(reply_size)); + base64_encode(auth_reply, reply_size, str); + auth_fields_add(request->fields.extra_fields, "resp", + str_c(str), 0); + } + ret = auth_request_proxy_finish(request, + auth_request_handler_proxy_callback); + if (ret < 0) + auth_request_handler_reply_failure_finish(request); + else if (ret > 0) + auth_request_handler_reply_success_finish(request); + else + return; + break; + case AUTH_CLIENT_RESULT_FAILURE: + auth_request_proxy_finish_failure(request); + auth_request_handler_reply_failure_finish(request); + break; + } + /* NOTE: request may be destroyed now */ + + auth_request_handler_unref(&handler); +} + +void auth_request_handler_reply_continue(struct auth_request *request, + const void *reply, size_t reply_size) +{ + request->handler->reply_continue_callback(request, reply, reply_size); +} + +static void +auth_request_handler_default_reply_continue(struct auth_request *request, + const void *reply, + size_t reply_size) +{ + auth_request_handler_reply(request, AUTH_CLIENT_RESULT_CONTINUE, + reply, reply_size); +} + +void auth_request_handler_abort(struct auth_request *request) +{ + i_assert(request->handler_pending_reply); + + /* request destroyed while waiting for auth_request_penalty_finish() + to be called. */ + auth_request_handler_unref(&request->handler); +} + +static void +auth_request_handler_auth_fail_code(struct auth_request_handler *handler, + struct auth_request *request, + const char *fail_code, const char *reason) +{ + string_t *str = t_str_new(128); + + e_info(request->mech_event, "%s", reason); + + str_printfa(str, "FAIL\t%u", request->id); + if (*fail_code != '\0') { + str_append(str, "\tcode="); + str_append(str, fail_code); + } + str_append(str, "\treason="); + str_append_tabescaped(str, reason); + + handler->callback(str_c(str), handler->conn); + auth_request_handler_remove(handler, request); +} + +static void auth_request_handler_auth_fail +(struct auth_request_handler *handler, struct auth_request *request, + const char *reason) +{ + auth_request_handler_auth_fail_code(handler, request, "", reason); +} + +static void auth_request_timeout(struct auth_request *request) +{ + unsigned int secs = (unsigned int)(time(NULL) - request->last_access); + + if (request->state != AUTH_REQUEST_STATE_MECH_CONTINUE) { + /* client's fault */ + e_error(request->mech_event, + "Request %u.%u timed out after %u secs, state=%d", + request->handler->client_pid, request->id, + secs, request->state); + } else if (request->set->verbose) { + e_info(request->mech_event, + "Request timed out waiting for client to continue authentication " + "(%u secs)", secs); + } + auth_request_handler_remove(request->handler, request); +} + +static void auth_request_penalty_finish(struct auth_request *request) +{ + timeout_remove(&request->to_penalty); + auth_request_initial(request); +} + +static void +auth_penalty_callback(unsigned int penalty, struct auth_request *request) +{ + unsigned int secs; + + request->last_penalty = penalty; + + if (penalty == 0) + auth_request_initial(request); + else { + secs = auth_penalty_to_secs(penalty); + request->to_penalty = timeout_add(secs * 1000, + auth_request_penalty_finish, + request); + } +} + +bool auth_request_handler_auth_begin(struct auth_request_handler *handler, + const char *args) +{ + const struct mech_module *mech; + struct auth_request *request; + const char *const *list, *name, *arg, *initial_resp; + void *initial_resp_data; + unsigned int id; + buffer_t *buf; + + i_assert(!handler->destroyed); + + /* <id> <mechanism> [...] */ + list = t_strsplit_tabescaped(args); + if (list[0] == NULL || list[1] == NULL || + str_to_uint(list[0], &id) < 0 || id == 0) { + e_error(handler->conn->event, + "BUG: Authentication client %u " + "sent broken AUTH request", handler->client_pid); + return FALSE; + } + + if (handler->token_auth) { + mech = &mech_dovecot_token; + if (strcmp(list[1], mech->mech_name) != 0) { + /* unsupported mechanism */ + e_error(handler->conn->event, + "BUG: Authentication client %u requested invalid " + "authentication mechanism %s (DOVECOT-TOKEN required)", + handler->client_pid, str_sanitize(list[1], MAX_MECH_NAME_LEN)); + return FALSE; + } + } else { + struct auth *auth_default = auth_default_service(); + mech = mech_register_find(auth_default->reg, list[1]); + if (mech == NULL) { + /* unsupported mechanism */ + e_error(handler->conn->event, + "BUG: Authentication client %u requested unsupported " + "authentication mechanism %s", handler->client_pid, + str_sanitize(list[1], MAX_MECH_NAME_LEN)); + return FALSE; + } + } + + request = auth_request_new(mech, handler->conn->event); + request->handler = handler; + request->connect_uid = handler->connect_uid; + request->client_pid = handler->client_pid; + request->id = id; + request->auth_only = handler->master_callback == NULL; + + /* parse optional parameters */ + initial_resp = NULL; + for (list += 2; *list != NULL; list++) { + arg = strchr(*list, '='); + if (arg == NULL) { + name = *list; + arg = ""; + } else { + name = t_strdup_until(*list, arg); + arg++; + } + + if (auth_request_import_auth(request, name, arg)) + ; + else if (strcmp(name, "resp") == 0) { + initial_resp = arg; + /* this must be the last parameter */ + list++; + break; + } + } + + if (*list != NULL) { + e_error(handler->conn->event, + "BUG: Authentication client %u " + "sent AUTH parameters after 'resp'", + handler->client_pid); + auth_request_unref(&request); + return FALSE; + } + + if (request->fields.service == NULL) { + e_error(handler->conn->event, + "BUG: Authentication client %u " + "didn't specify service in request", + handler->client_pid); + auth_request_unref(&request); + return FALSE; + } + if (hash_table_lookup(handler->requests, POINTER_CAST(id)) != NULL) { + e_error(handler->conn->event, + "BUG: Authentication client %u " + "sent a duplicate ID %u", handler->client_pid, id); + auth_request_unref(&request); + return FALSE; + } + auth_request_init(request); + + request->to_abort = timeout_add(MASTER_AUTH_SERVER_TIMEOUT_SECS * 1000, + auth_request_timeout, request); + hash_table_insert(handler->requests, POINTER_CAST(id), request); + + if (request->set->ssl_require_client_cert && + !request->fields.valid_client_cert) { + /* we fail without valid certificate */ + auth_request_handler_auth_fail(handler, request, + "Client didn't present valid SSL certificate"); + return TRUE; + } + + if (request->set->ssl_require_client_cert && + request->set->ssl_username_from_cert && + !request->fields.cert_username) { + auth_request_handler_auth_fail(handler, request, + "SSL certificate didn't contain username"); + return TRUE; + } + + /* Handle initial respose */ + if (initial_resp == NULL) { + /* No initial response */ + request->initial_response = NULL; + request->initial_response_len = 0; + } else if (handler->conn->version_minor < 2 && *initial_resp == '\0') { + /* Some authentication clients like Exim send and empty initial + response field when it is in fact absent in the + authentication command. This was allowed for older versions + of the Dovecot authentication protocol. */ + request->initial_response = NULL; + request->initial_response_len = 0; + } else if (*initial_resp == '\0' || strcmp(initial_resp, "=") == 0 ) { + /* Empty initial response - Protocols that use SASL often + use '=' to indicate an empty initial response; i.e., to + distinguish it from an absent initial response. However, that + should not be conveyed to the SASL layer (it is not even + valid Base64); only the empty string should be passed on. + Still, we recognize it here anyway, because we used to make + the same mistake. */ + request->initial_response = uchar_empty_ptr; + request->initial_response_len = 0; + } else { + size_t len = strlen(initial_resp); + + /* Initial response encoded in Bas64 */ + buf = t_buffer_create(MAX_BASE64_DECODED_SIZE(len)); + if (base64_decode(initial_resp, len, NULL, buf) < 0) { + auth_request_handler_auth_fail_code(handler, request, + AUTH_CLIENT_FAIL_CODE_INVALID_BASE64, + "Invalid base64 data in initial response"); + return TRUE; + } + initial_resp_data = + p_malloc(request->pool, I_MAX(buf->used, 1)); + memcpy(initial_resp_data, buf->data, buf->used); + request->initial_response = initial_resp_data; + request->initial_response_len = buf->used; + } + + /* handler is referenced until auth_request_handler_reply() + is called. */ + handler->refcount++; + request->handler_pending_reply = TRUE; + + /* before we start authenticating, see if we need to wait first */ + auth_penalty_lookup(auth_penalty, request, auth_penalty_callback); + return TRUE; +} + +bool auth_request_handler_auth_continue(struct auth_request_handler *handler, + const char *args) +{ + struct auth_request *request; + const char *data; + size_t data_len; + buffer_t *buf; + unsigned int id; + + data = strchr(args, '\t'); + if (data == NULL || str_to_uint(t_strdup_until(args, data), &id) < 0) { + e_error(handler->conn->event, + "BUG: Authentication client sent broken CONT request"); + return FALSE; + } + data++; + + request = hash_table_lookup(handler->requests, POINTER_CAST(id)); + if (request == NULL) { + const char *reply = t_strdup_printf( + "FAIL\t%u\treason=Authentication request timed out", id); + handler->callback(reply, handler->conn); + return TRUE; + } + + /* accept input only once after mechanism has sent a CONT reply */ + if (!request->accept_cont_input) { + auth_request_handler_auth_fail(handler, request, + "Unexpected continuation"); + return TRUE; + } + request->accept_cont_input = FALSE; + + data_len = strlen(data); + buf = t_buffer_create(MAX_BASE64_DECODED_SIZE(data_len)); + if (base64_decode(data, data_len, NULL, buf) < 0) { + auth_request_handler_auth_fail_code(handler, request, + AUTH_CLIENT_FAIL_CODE_INVALID_BASE64, + "Invalid base64 data in continued response"); + return TRUE; + } + + /* handler is referenced until auth_request_handler_reply() + is called. */ + handler->refcount++; + auth_request_continue(request, buf->data, buf->used); + return TRUE; +} + +static void auth_str_append_userdb_extra_fields(struct auth_request *request, + string_t *dest) +{ + str_append_c(dest, '\t'); + auth_fields_append(request->fields.userdb_reply, dest, + AUTH_FIELD_FLAG_HIDDEN, 0); + + if (request->fields.master_user != NULL && + !auth_fields_exists(request->fields.userdb_reply, "master_user")) { + auth_str_add_keyvalue(dest, "master_user", + request->fields.master_user); + } + auth_str_add_keyvalue(dest, "auth_mech", request->mech->mech_name); + if (*request->set->anonymous_username != '\0' && + strcmp(request->fields.user, request->set->anonymous_username) == 0) { + /* this is an anonymous login, either via ANONYMOUS + SASL mechanism or simply logging in as the anonymous + user via another mechanism */ + str_append(dest, "\tanonymous"); + } + /* generate auth_token when master service provided session_pid */ + if (request->request_auth_token && + request->session_pid != (pid_t)-1) { + const char *auth_token = + auth_token_get(request->fields.service, + dec2str(request->session_pid), + request->fields.user, + request->fields.session_id); + auth_str_add_keyvalue(dest, "auth_token", auth_token); + } + if (request->fields.master_user != NULL) { + auth_str_add_keyvalue(dest, "auth_user", + request->fields.master_user); + } else if (request->fields.original_username != NULL && + strcmp(request->fields.original_username, + request->fields.user) != 0) { + auth_str_add_keyvalue(dest, "auth_user", + request->fields.original_username); + } +} + +static void userdb_callback(enum userdb_result result, + struct auth_request *request) +{ + struct auth_request_handler *handler = request->handler; + string_t *str; + const char *value; + + i_assert(request->state == AUTH_REQUEST_STATE_USERDB); + + auth_request_set_state(request, AUTH_REQUEST_STATE_FINISHED); + + if (request->userdb_lookup_tempfailed) + result = USERDB_RESULT_INTERNAL_FAILURE; + + str = t_str_new(128); + switch (result) { + case USERDB_RESULT_INTERNAL_FAILURE: + str_printfa(str, "FAIL\t%u", request->id); + if (request->userdb_lookup_tempfailed) { + value = auth_fields_find(request->fields.userdb_reply, + "reason"); + if (value != NULL) + auth_str_add_keyvalue(str, "reason", value); + } + break; + case USERDB_RESULT_USER_UNKNOWN: + str_printfa(str, "NOTFOUND\t%u", request->id); + break; + case USERDB_RESULT_OK: + str_printfa(str, "USER\t%u\t", request->id); + str_append_tabescaped(str, request->fields.user); + auth_str_append_userdb_extra_fields(request, str); + break; + } + handler->master_callback(str_c(str), request->master); + + auth_master_connection_unref(&request->master); + auth_request_unref(&request); + auth_request_handler_unref(&handler); +} + +static bool +auth_master_request_failed(struct auth_request_handler *handler, + struct auth_master_connection *master, + unsigned int id) +{ + if (handler->master_callback == NULL) + return FALSE; + handler->master_callback(t_strdup_printf("FAIL\t%u", id), master); + return TRUE; +} + +bool auth_request_handler_master_request(struct auth_request_handler *handler, + struct auth_master_connection *master, + unsigned int id, unsigned int client_id, + const char *const *params) +{ + struct auth_request *request; + struct net_unix_cred cred; + + request = hash_table_lookup(handler->requests, POINTER_CAST(client_id)); + if (request == NULL) { + e_error(master->event, "Master request %u.%u not found", + handler->client_pid, client_id); + return auth_master_request_failed(handler, master, id); + } + + auth_request_ref(request); + auth_request_handler_remove(handler, request); + + for (; *params != NULL; params++) { + const char *name, *param = strchr(*params, '='); + + if (param == NULL) { + name = *params; + param = ""; + } else { + name = t_strdup_until(*params, param); + param++; + } + + (void)auth_request_import_master(request, name, param); + } + + /* verify session pid if specified and possible */ + if (request->session_pid != (pid_t)-1 && + net_getunixcred(master->fd, &cred) == 0 && + cred.pid != (pid_t)-1 && request->session_pid != cred.pid) { + e_error(master->event, + "Session pid %ld provided by master for request %u.%u " + "did not match peer credentials (pid=%ld, uid=%ld)", + (long)request->session_pid, + handler->client_pid, client_id, + (long)cred.pid, (long)cred.uid); + return auth_master_request_failed(handler, master, id); + } + + if (request->state != AUTH_REQUEST_STATE_FINISHED || + !request->fields.successful) { + e_error(master->event, + "Master requested unfinished authentication request " + "%u.%u", handler->client_pid, client_id); + handler->master_callback(t_strdup_printf("FAIL\t%u", id), + master); + auth_request_unref(&request); + } else { + /* the request isn't being referenced anywhere anymore, + so we can do a bit of kludging.. replace the request's + old client_id with master's id. */ + auth_request_set_state(request, AUTH_REQUEST_STATE_USERDB); + request->id = id; + request->master = master; + + /* master and handler are referenced until userdb_callback i + s called. */ + auth_master_connection_ref(master); + handler->refcount++; + auth_request_lookup_user(request, userdb_callback); + } + return TRUE; +} + +void auth_request_handler_cancel_request(struct auth_request_handler *handler, + unsigned int client_id) +{ + struct auth_request *request; + + request = hash_table_lookup(handler->requests, POINTER_CAST(client_id)); + if (request != NULL) + auth_request_handler_remove(handler, request); +} + +void auth_request_handler_flush_failures(bool flush_all) +{ + struct auth_request **auth_requests, *auth_request; + unsigned int i, j, count; + time_t diff; + + count = aqueue_count(auth_failures); + if (count == 0) { + timeout_remove(&to_auth_failures); + return; + } + + auth_requests = array_front_modifiable(&auth_failures_arr); + /* count the number of requests that we need to flush */ + for (i = 0; i < count; i++) { + auth_request = auth_requests[aqueue_idx(auth_failures, i)]; + + /* FIXME: assumes that failure_delay is always the same. */ + diff = ioloop_time - auth_request->last_access; + if (diff < (time_t)auth_request->set->failure_delay && + !flush_all) + break; + } + + /* shuffle these requests to try to prevent any kind of timing attacks + where attacker performs multiple requests in parallel and attempts + to figure out results based on the order of replies. */ + count = i; + for (i = 0; i < count; i++) { + j = random() % (count - i) + i; + auth_request = auth_requests[aqueue_idx(auth_failures, i)]; + + /* swap i & j */ + auth_requests[aqueue_idx(auth_failures, i)] = + auth_requests[aqueue_idx(auth_failures, j)]; + auth_requests[aqueue_idx(auth_failures, j)] = auth_request; + } + + /* flush the requests */ + for (i = 0; i < count; i++) { + auth_request = auth_requests[aqueue_idx(auth_failures, 0)]; + aqueue_delete_tail(auth_failures); + + i_assert(auth_request != NULL); + i_assert(auth_request->state == AUTH_REQUEST_STATE_FINISHED); + auth_request_handler_reply(auth_request, + AUTH_CLIENT_RESULT_FAILURE, + uchar_empty_ptr, 0); + auth_request_unref(&auth_request); + } +} + +static void auth_failure_timeout(void *context ATTR_UNUSED) +{ + auth_request_handler_flush_failures(FALSE); +} + +void auth_request_handler_init(void) +{ + i_array_init(&auth_failures_arr, 128); + auth_failures = aqueue_init(&auth_failures_arr.arr); +} + +void auth_request_handler_deinit(void) +{ + auth_request_handler_flush_failures(TRUE); + array_free(&auth_failures_arr); + aqueue_deinit(&auth_failures); + + timeout_remove(&to_auth_failures); +} diff --git a/src/auth/auth-request-handler.h b/src/auth/auth-request-handler.h new file mode 100644 index 0000000..ceba935 --- /dev/null +++ b/src/auth/auth-request-handler.h @@ -0,0 +1,69 @@ +#ifndef AUTH_REQUEST_HANDLER_H +#define AUTH_REQUEST_HANDLER_H + +struct auth_request; +struct auth_client_connection; +struct auth_master_connection; +struct auth_stream_reply; + +enum auth_client_result { + AUTH_CLIENT_RESULT_CONTINUE = 1, + AUTH_CLIENT_RESULT_SUCCESS, + AUTH_CLIENT_RESULT_FAILURE +}; + +typedef void +auth_client_request_callback_t(const char *reply, struct auth_client_connection *conn); +typedef void +auth_master_request_callback_t(const char *reply, struct auth_master_connection *conn); + +typedef void +auth_request_handler_reply_callback_t(struct auth_request *request, + enum auth_client_result result, + const void *auth_reply, + size_t reply_size); +typedef void +auth_request_handler_reply_continue_callback_t(struct auth_request *request, + const void *reply, + size_t reply_size); + + +struct auth_request_handler * +auth_request_handler_create(bool token_auth, auth_client_request_callback_t *callback, + struct auth_client_connection *conn, + auth_master_request_callback_t *master_callback); + +void auth_request_handler_destroy(struct auth_request_handler **handler); +void auth_request_handler_unref(struct auth_request_handler **handler); +void auth_request_handler_abort_requests(struct auth_request_handler *handler); + +void auth_request_handler_set(struct auth_request_handler *handler, + unsigned int connect_uid, + unsigned int client_pid); + +bool auth_request_handler_auth_begin(struct auth_request_handler *handler, + const char *args); +bool auth_request_handler_auth_continue(struct auth_request_handler *handler, + const char *args); +void auth_request_handler_reply(struct auth_request *request, + enum auth_client_result result, + const void *reply, size_t reply_size); +void auth_request_handler_reply_continue(struct auth_request *request, + const void *reply, size_t reply_size); +void auth_request_handler_abort(struct auth_request *request); + +unsigned int +auth_request_handler_get_request_count(struct auth_request_handler *handler); +bool auth_request_handler_master_request(struct auth_request_handler *handler, + struct auth_master_connection *master, + unsigned int id, unsigned int client_id, + const char *const *params); +void auth_request_handler_cancel_request(struct auth_request_handler *handler, + unsigned int client_id); + +void auth_request_handler_flush_failures(bool flush_all); + +void auth_request_handler_init(void); +void auth_request_handler_deinit(void); + +#endif diff --git a/src/auth/auth-request-stats.c b/src/auth/auth-request-stats.c new file mode 100644 index 0000000..7af4e9a --- /dev/null +++ b/src/auth/auth-request-stats.c @@ -0,0 +1,77 @@ +/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */ + +#include "auth-common.h" +#include "str.h" +#include "strescape.h" +#include "buffer.h" +#include "base64.h" +#include "stats.h" +#include "stats-connection.h" +#include "auth-stats.h" +#include "auth-request.h" +#include "auth-request-stats.h" + +#define USER_STATS_SOCKET_NAME "old-stats-user" + +static struct stats_connection *auth_stats_conn = NULL; +static struct stats_item *auth_stats_item; + +struct auth_stats *auth_request_stats_get(struct auth_request *request) +{ + if (request->stats == NULL) + request->stats = stats_alloc(request->pool); + return stats_fill_ptr(request->stats, auth_stats_item); +} + +void auth_request_stats_add_tempfail(struct auth_request *request) +{ + struct auth_stats *stats = auth_request_stats_get(request); + + stats->auth_db_tempfail_count++; +} + +void auth_request_stats_send(struct auth_request *request) +{ + string_t *str; + buffer_t *buf; + + /* we'll send stats only when the request is finished. this reduces + memory usage and is a bit simpler. auth requests are typically + pretty short lived anyway. */ + i_assert(!request->stats_sent); + request->stats_sent = TRUE; + + if (request->stats == NULL) { + /* nothing happened in this request - don't send it */ + return; + } + if (!request->set->stats) + return; + + buf = t_buffer_create(128); + stats_export(buf, request->stats); + + str = t_str_new(256); + str_append(str, "ADD-USER\t"); + if (request->fields.user != NULL) + str_append_tabescaped(str, request->fields.user); + str_append_c(str, '\t'); + str_append_tabescaped(str, request->fields.service); + str_append_c(str, '\t'); + base64_encode(buf->data, buf->used, str); + + str_append_c(str, '\n'); + stats_connection_send(auth_stats_conn, str); +} + +void auth_request_stats_init(void) +{ + auth_stats_conn = stats_connection_create(USER_STATS_SOCKET_NAME); + auth_stats_item = stats_register(&auth_stats_vfuncs); +} + +void auth_request_stats_deinit(void) +{ + stats_connection_unref(&auth_stats_conn); + stats_unregister(&auth_stats_item); +} diff --git a/src/auth/auth-request-stats.h b/src/auth/auth-request-stats.h new file mode 100644 index 0000000..5095c2e --- /dev/null +++ b/src/auth/auth-request-stats.h @@ -0,0 +1,15 @@ +#ifndef AUTH_REQUEST_STATS_H +#define AUTH_REQUEST_STATS_H + +#include "auth-stats.h" + +struct auth_request; + +struct auth_stats *auth_request_stats_get(struct auth_request *request); +void auth_request_stats_add_tempfail(struct auth_request *request); +void auth_request_stats_send(struct auth_request *request); + +void auth_request_stats_init(void); +void auth_request_stats_deinit(void); + +#endif diff --git a/src/auth/auth-request-var-expand.c b/src/auth/auth-request-var-expand.c new file mode 100644 index 0000000..04d774e --- /dev/null +++ b/src/auth/auth-request-var-expand.c @@ -0,0 +1,303 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +#include "auth-common.h" +#include "str.h" +#include "strescape.h" +#include "auth-request.h" + +struct auth_request_var_expand_ctx { + const struct auth_request *auth_request; + auth_request_escape_func_t *escape_func; +}; + +const struct var_expand_table +auth_request_var_expand_static_tab[AUTH_REQUEST_VAR_TAB_COUNT+1] = { + { 'u', NULL, "user" }, + { 'n', NULL, "username" }, + { 'd', NULL, "domain" }, + { 's', NULL, "service" }, + { 'h', NULL, "home" }, + { 'l', NULL, "lip" }, + { 'r', NULL, "rip" }, + { 'p', NULL, "pid" }, + { 'w', NULL, "password" }, + { '!', NULL, NULL }, + { 'm', NULL, "mech" }, + { 'c', NULL, "secured" }, + { 'a', NULL, "lport" }, + { 'b', NULL, "rport" }, + { 'k', NULL, "cert" }, + { '\0', NULL, "login_user" }, + { '\0', NULL, "login_username" }, + { '\0', NULL, "login_domain" }, + { '\0', NULL, "session" }, + { '\0', NULL, "real_lip" }, + { '\0', NULL, "real_rip" }, + { '\0', NULL, "real_lport" }, + { '\0', NULL, "real_rport" }, + { '\0', NULL, "domain_first" }, + { '\0', NULL, "domain_last" }, + { '\0', NULL, "master_user" }, + { '\0', NULL, "session_pid" }, + { '\0', NULL, "orig_user" }, + { '\0', NULL, "orig_username" }, + { '\0', NULL, "orig_domain" }, + { '\0', NULL, "auth_user" }, + { '\0', NULL, "auth_username" }, + { '\0', NULL, "auth_domain" }, + { '\0', NULL, "local_name" }, + { '\0', NULL, "client_id" }, + + /* aliases: */ + { '\0', NULL, "local_ip" }, + { '\0', NULL, "remote_ip" }, + { '\0', NULL, "local_port" }, + { '\0', NULL, "remote_port" }, + { '\0', NULL, "real_local_ip" }, + { '\0', NULL, "real_remote_ip" }, + { '\0', NULL, "real_local_port" }, + { '\0', NULL, "real_remote_port" }, + { '\0', NULL, "mechanism" }, + { '\0', NULL, "original_user" }, + { '\0', NULL, "original_username" }, + { '\0', NULL, "original_domain" }, + + /* be sure to update AUTH_REQUEST_VAR_TAB_COUNT */ + { '\0', NULL, NULL } +}; + +static const char * +escape_none(const char *string, + const struct auth_request *request ATTR_UNUSED) +{ + return string; +} + +const char * +auth_request_str_escape(const char *string, + const struct auth_request *request ATTR_UNUSED) +{ + return str_escape(string); +} + +struct var_expand_table * +auth_request_get_var_expand_table_full(const struct auth_request *auth_request, + const char *username, + auth_request_escape_func_t *escape_func, + unsigned int *count) +{ + const struct auth_request_fields *fields = &auth_request->fields; + const unsigned int auth_count = + N_ELEMENTS(auth_request_var_expand_static_tab); + struct var_expand_table *tab, *ret_tab; + const char *orig_user, *auth_user; + + if (escape_func == NULL) + escape_func = escape_none; + + /* keep the extra fields at the beginning. the last static_tab field + contains the ending NULL-fields. */ + tab = ret_tab = t_new(struct var_expand_table, + MALLOC_ADD(*count, auth_count)); + tab += *count; + *count += auth_count; + + memcpy(tab, auth_request_var_expand_static_tab, + auth_count * sizeof(*tab)); + + if (username == NULL) + username = ""; + tab[0].value = escape_func(username, auth_request); + tab[1].value = escape_func(t_strcut(username, '@'), + auth_request); + tab[2].value = i_strchr_to_next(username, '@'); + if (tab[2].value != NULL) + tab[2].value = escape_func(tab[2].value, auth_request); + tab[3].value = escape_func(fields->service, auth_request); + /* tab[4] = we have no home dir */ + if (fields->local_ip.family != 0) + tab[5].value = tab[35].value = + net_ip2addr(&fields->local_ip); + if (fields->remote_ip.family != 0) + tab[6].value = tab[36].value = + net_ip2addr(&fields->remote_ip); + tab[7].value = dec2str(auth_request->client_pid); + if (auth_request->mech_password != NULL) { + tab[8].value = escape_func(auth_request->mech_password, + auth_request); + } + if (auth_request->userdb_lookup) { + tab[9].value = auth_request->userdb == NULL ? "" : + dec2str(auth_request->userdb->userdb->id); + } else { + tab[9].value = auth_request->passdb == NULL ? "" : + dec2str(auth_request->passdb->passdb->id); + } + tab[10].value = tab[43].value = fields->mech_name == NULL ? "" : + escape_func(fields->mech_name, auth_request); + switch (fields->secured) { + case AUTH_REQUEST_SECURED_NONE: tab[11].value = ""; break; + case AUTH_REQUEST_SECURED: tab[11].value = "secured"; break; + case AUTH_REQUEST_SECURED_TLS: tab[11].value = "TLS"; break; + default: tab[11].value = ""; break; + }; + tab[12].value = tab[37].value = dec2str(fields->local_port); + tab[13].value = tab[38].value = dec2str(fields->remote_port); + tab[14].value = fields->valid_client_cert ? "valid" : ""; + + if (fields->requested_login_user != NULL) { + const char *login_user = fields->requested_login_user; + + tab[15].value = escape_func(login_user, auth_request); + tab[16].value = escape_func(t_strcut(login_user, '@'), + auth_request); + tab[17].value = i_strchr_to_next(login_user, '@'); + if (tab[17].value != NULL) { + tab[17].value = escape_func(tab[17].value, + auth_request); + } + } + tab[18].value = fields->session_id == NULL ? NULL : + escape_func(fields->session_id, auth_request); + if (fields->real_local_ip.family != 0) + tab[19].value = tab[39].value = + net_ip2addr(&fields->real_local_ip); + if (fields->real_remote_ip.family != 0) + tab[20].value = tab[40].value = + net_ip2addr(&fields->real_remote_ip); + tab[21].value = tab[41].value = dec2str(fields->real_local_port); + tab[22].value = tab[42].value = dec2str(fields->real_remote_port); + tab[23].value = i_strchr_to_next(username, '@'); + if (tab[23].value != NULL) { + tab[23].value = escape_func(t_strcut(tab[23].value, '@'), + auth_request); + } + tab[24].value = strrchr(username, '@'); + if (tab[24].value != NULL) + tab[24].value = escape_func(tab[24].value+1, auth_request); + tab[25].value = fields->master_user == NULL ? NULL : + escape_func(fields->master_user, auth_request); + tab[26].value = auth_request->session_pid == (pid_t)-1 ? NULL : + dec2str(auth_request->session_pid); + + orig_user = fields->original_username != NULL ? + fields->original_username : username; + tab[27].value = tab[44].value = escape_func(orig_user, auth_request); + tab[28].value = tab[45].value = escape_func(t_strcut(orig_user, '@'), auth_request); + tab[29].value = tab[46].value = i_strchr_to_next(orig_user, '@'); + if (tab[29].value != NULL) + tab[29].value = tab[46].value = + escape_func(tab[29].value, auth_request); + + if (fields->master_user != NULL) + auth_user = fields->master_user; + else + auth_user = orig_user; + tab[30].value = escape_func(auth_user, auth_request); + tab[31].value = escape_func(t_strcut(auth_user, '@'), auth_request); + tab[32].value = i_strchr_to_next(auth_user, '@'); + if (tab[32].value != NULL) + tab[32].value = escape_func(tab[32].value, auth_request); + if (fields->local_name != NULL) + tab[33].value = escape_func(fields->local_name, auth_request); + if (fields->client_id != NULL) + tab[34].value = escape_func(fields->client_id, auth_request); + return ret_tab; +} + +const struct var_expand_table * +auth_request_get_var_expand_table(const struct auth_request *auth_request, + auth_request_escape_func_t *escape_func) +{ + unsigned int count = 0; + + return auth_request_get_var_expand_table_full(auth_request, + auth_request->fields.user, escape_func, &count); +} + +static const char *field_get_default(const char *data) +{ + const char *p; + + p = strchr(data, ':'); + if (p == NULL) + return ""; + else { + /* default value given */ + return p+1; + } +} + +static int +auth_request_var_expand_func_passdb(const char *data, void *context, + const char **value_r, + const char **error_r ATTR_UNUSED) +{ + struct auth_request_var_expand_ctx *ctx = context; + const char *field_name = t_strcut(data, ':'); + const char *value; + + value = auth_fields_find(ctx->auth_request->fields.extra_fields, field_name); + *value_r = ctx->escape_func(value != NULL ? value : field_get_default(data), + ctx->auth_request); + return 1; +} + +static int +auth_request_var_expand_func_userdb(const char *data, void *context, + const char **value_r, + const char **error_r ATTR_UNUSED) +{ + struct auth_request_var_expand_ctx *ctx = context; + const char *field_name = t_strcut(data, ':'); + const char *value; + + value = ctx->auth_request->fields.userdb_reply == NULL ? NULL : + auth_fields_find(ctx->auth_request->fields.userdb_reply, field_name); + *value_r = ctx->escape_func(value != NULL ? value : field_get_default(data), + ctx->auth_request); + return 1; +} + +const struct var_expand_func_table auth_request_var_funcs_table[] = { + { "passdb", auth_request_var_expand_func_passdb }, + { "userdb", auth_request_var_expand_func_userdb }, + { NULL, NULL } +}; + +int auth_request_var_expand(string_t *dest, const char *str, + const struct auth_request *auth_request, + auth_request_escape_func_t *escape_func, + const char **error_r) +{ + return auth_request_var_expand_with_table(dest, str, auth_request, + auth_request_get_var_expand_table(auth_request, escape_func), + escape_func, error_r); +} + +int auth_request_var_expand_with_table(string_t *dest, const char *str, + const struct auth_request *auth_request, + const struct var_expand_table *table, + auth_request_escape_func_t *escape_func, + const char **error_r) +{ + struct auth_request_var_expand_ctx ctx; + + i_zero(&ctx); + ctx.auth_request = auth_request; + ctx.escape_func = escape_func == NULL ? escape_none : escape_func; + return var_expand_with_funcs(dest, str, table, + auth_request_var_funcs_table, &ctx, error_r); +} + +int t_auth_request_var_expand(const char *str, + const struct auth_request *auth_request ATTR_UNUSED, + auth_request_escape_func_t *escape_func ATTR_UNUSED, + const char **value_r, const char **error_r) +{ + string_t *dest = t_str_new(128); + int ret = auth_request_var_expand(dest, str, auth_request, + escape_func, error_r); + *value_r = str_c(dest); + return ret; +} diff --git a/src/auth/auth-request-var-expand.h b/src/auth/auth-request-var-expand.h new file mode 100644 index 0000000..57e18a8 --- /dev/null +++ b/src/auth/auth-request-var-expand.h @@ -0,0 +1,44 @@ +#ifndef AUTH_REQUEST_VAR_EXPAND_H +#define AUTH_REQUEST_VAR_EXPAND_H + +typedef const char * +auth_request_escape_func_t(const char *string, + const struct auth_request *auth_request); + +#define AUTH_REQUEST_VAR_TAB_USER_IDX 0 +#define AUTH_REQUEST_VAR_TAB_USERNAME_IDX 1 +#define AUTH_REQUEST_VAR_TAB_DOMAIN_IDX 2 +#define AUTH_REQUEST_VAR_TAB_COUNT 47 +extern const struct var_expand_table +auth_request_var_expand_static_tab[AUTH_REQUEST_VAR_TAB_COUNT+1]; + +extern const struct var_expand_func_table auth_request_var_funcs_table[]; + +const struct var_expand_table * +auth_request_get_var_expand_table(const struct auth_request *auth_request, + auth_request_escape_func_t *escape_func) + ATTR_NULL(2); +struct var_expand_table * +auth_request_get_var_expand_table_full(const struct auth_request *auth_request, + const char *username, + auth_request_escape_func_t *escape_func, + unsigned int *count) ATTR_NULL(3); + +int auth_request_var_expand(string_t *dest, const char *str, + const struct auth_request *auth_request, + auth_request_escape_func_t *escape_func, + const char **error_r); +int auth_request_var_expand_with_table(string_t *dest, const char *str, + const struct auth_request *auth_request, + const struct var_expand_table *table, + auth_request_escape_func_t *escape_func, + const char **error_r); +int t_auth_request_var_expand(const char *str, + const struct auth_request *auth_request, + auth_request_escape_func_t *escape_func, + const char **value_r, const char **error_r); + +const char *auth_request_str_escape(const char *string, + const struct auth_request *request); + +#endif diff --git a/src/auth/auth-request.c b/src/auth/auth-request.c new file mode 100644 index 0000000..ee89e75 --- /dev/null +++ b/src/auth/auth-request.c @@ -0,0 +1,2580 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +#include "auth-common.h" +#include "ioloop.h" +#include "buffer.h" +#include "hash.h" +#include "sha1.h" +#include "hex-binary.h" +#include "str.h" +#include "array.h" +#include "safe-memset.h" +#include "str-sanitize.h" +#include "strescape.h" +#include "var-expand.h" +#include "dns-lookup.h" +#include "auth-cache.h" +#include "auth-request.h" +#include "auth-request-handler.h" +#include "auth-request-handler-private.h" +#include "auth-request-stats.h" +#include "auth-client-connection.h" +#include "auth-master-connection.h" +#include "auth-policy.h" +#include "passdb.h" +#include "passdb-blocking.h" +#include "passdb-cache.h" +#include "passdb-template.h" +#include "userdb-blocking.h" +#include "userdb-template.h" +#include "password-scheme.h" +#include "wildcard-match.h" + +#include <sys/stat.h> + +#define AUTH_SUBSYS_PROXY "proxy" +#define AUTH_DNS_SOCKET_PATH "dns-client" +#define AUTH_DNS_DEFAULT_TIMEOUT_MSECS (1000*10) +#define AUTH_DNS_WARN_MSECS 500 +#define AUTH_REQUEST_MAX_DELAY_SECS (60*5) +#define CACHED_PASSWORD_SCHEME "SHA1" + +struct auth_request_proxy_dns_lookup_ctx { + struct auth_request *request; + auth_request_proxy_cb_t *callback; + struct dns_lookup *dns_lookup; +}; + +struct auth_policy_check_ctx { + enum { + AUTH_POLICY_CHECK_TYPE_PLAIN, + AUTH_POLICY_CHECK_TYPE_LOOKUP, + AUTH_POLICY_CHECK_TYPE_SUCCESS, + } type; + struct auth_request *request; + + buffer_t *success_data; + + verify_plain_callback_t *callback_plain; + lookup_credentials_callback_t *callback_lookup; +}; + +const char auth_default_subsystems[2]; + +unsigned int auth_request_state_count[AUTH_REQUEST_STATE_MAX]; + +static void get_log_identifier(string_t *str, struct auth_request *auth_request); +static void +auth_request_userdb_import(struct auth_request *request, const char *args); + +static +void auth_request_lookup_credentials_policy_continue(struct auth_request *request, + lookup_credentials_callback_t *callback); +static +void auth_request_policy_check_callback(int result, void *context); + +static const char *get_log_prefix_mech(struct auth_request *auth_request) +{ + string_t *str = t_str_new(64); + auth_request_get_log_prefix(str, auth_request, AUTH_SUBSYS_MECH); + return str_c(str); +} + +const char *auth_request_get_log_prefix_db(struct auth_request *auth_request) +{ + string_t *str = t_str_new(64); + auth_request_get_log_prefix(str, auth_request, AUTH_SUBSYS_DB); + return str_c(str); +} + +static struct event *get_request_event(struct auth_request *request, + const char *subsystem) +{ + if (subsystem == AUTH_SUBSYS_DB) + return authdb_event(request); + else if (subsystem == AUTH_SUBSYS_MECH) + return request->mech_event; + else + return request->event; +} + +static void auth_request_post_alloc_init(struct auth_request *request, struct event *parent_event) +{ + enum log_type level; + request->state = AUTH_REQUEST_STATE_NEW; + auth_request_state_count[AUTH_REQUEST_STATE_NEW]++; + request->refcount = 1; + request->last_access = ioloop_time; + request->session_pid = (pid_t)-1; + request->set = global_auth_settings; + request->event = event_create(parent_event); + request->mech_event = event_create(request->event); + auth_request_fields_init(request); + + level = request->set->verbose ? LOG_TYPE_INFO : LOG_TYPE_WARNING; + event_set_min_log_level(request->event, level); + event_set_min_log_level(request->mech_event, level); + + p_array_init(&request->authdb_event, request->pool, 2); + event_set_log_prefix_callback(request->mech_event, FALSE, get_log_prefix_mech, + request); + event_set_forced_debug(request->event, request->set->debug); + event_add_category(request->event, &event_category_auth); +} + +struct auth_request * +auth_request_new(const struct mech_module *mech, struct event *parent_event) +{ + struct auth_request *request; + + request = mech->auth_new(); + request->mech = mech; + auth_request_post_alloc_init(request, parent_event); + + return request; +} + +struct auth_request *auth_request_new_dummy(struct event *parent_event) +{ + struct auth_request *request; + pool_t pool; + + pool = pool_alloconly_create(MEMPOOL_GROWING"auth_request", 1024); + request = p_new(pool, struct auth_request, 1); + request->pool = pool; + + auth_request_post_alloc_init(request, parent_event); + return request; +} + +void auth_request_set_state(struct auth_request *request, + enum auth_request_state state) +{ + if (request->state == state) + return; + + i_assert(request->to_penalty == NULL); + + i_assert(auth_request_state_count[request->state] > 0); + auth_request_state_count[request->state]--; + auth_request_state_count[state]++; + + request->state = state; + auth_refresh_proctitle(); +} + +void auth_request_init(struct auth_request *request) +{ + struct auth *auth; + + auth = auth_request_get_auth(request); + request->set = auth->set; + request->passdb = auth->passdbs; + request->userdb = auth->userdbs; +} + +struct auth *auth_request_get_auth(struct auth_request *request) +{ + return auth_find_service(request->fields.service); +} + +void auth_request_success(struct auth_request *request, + const void *data, size_t data_size) +{ + i_assert(request->state == AUTH_REQUEST_STATE_MECH_CONTINUE); + + if (!request->set->policy_check_after_auth) { + struct auth_policy_check_ctx *ctx = + p_new(request->pool, struct auth_policy_check_ctx, 1); + ctx->success_data = buffer_create_dynamic(request->pool, 1); + ctx->request = request; + ctx->type = AUTH_POLICY_CHECK_TYPE_SUCCESS; + auth_request_policy_check_callback(0, ctx); + return; + } + + /* perform second policy lookup here */ + struct auth_policy_check_ctx *ctx = p_new(request->pool, struct auth_policy_check_ctx, 1); + ctx->request = request; + ctx->success_data = buffer_create_dynamic(request->pool, data_size); + buffer_append(ctx->success_data, data, data_size); + ctx->type = AUTH_POLICY_CHECK_TYPE_SUCCESS; + auth_policy_check(request, request->mech_password, auth_request_policy_check_callback, ctx); +} + +struct event_passthrough * +auth_request_finished_event(struct auth_request *request, struct event *event) +{ + struct event_passthrough *e = event_create_passthrough(event); + + if (request->failed) { + if (request->internal_failure) { + e->add_str("error", "internal failure"); + } else { + e->add_str("error", "authentication failed"); + } + } else if (request->fields.successful) { + e->add_str("success", "yes"); + } + if (request->userdb_lookup) { + return e; + } + if (request->policy_penalty > 0) + e->add_int("policy_penalty", request->policy_penalty); + if (request->policy_refusal) { + e->add_str("policy_result", "refused"); + } else if (request->policy_processed && request->policy_penalty > 0) { + e->add_str("policy_result", "delayed"); + e->add_int("policy_penalty", request->policy_penalty); + } else if (request->policy_processed) { + e->add_str("policy_result", "ok"); + } + return e; +} + +void auth_request_log_finished(struct auth_request *request) +{ + if (request->event_finished_sent) + return; + request->event_finished_sent = TRUE; + string_t *str = t_str_new(64); + auth_request_get_log_prefix(str, request, "auth"); + struct event_passthrough *e = + auth_request_finished_event(request, request->event)-> + set_name("auth_request_finished"); + e_debug(e->event(), "%sAuth request finished", str_c(str)); +} + +static +void auth_request_success_continue(struct auth_policy_check_ctx *ctx) +{ + struct auth_request *request = ctx->request; + struct auth_stats *stats; + i_assert(request->state == AUTH_REQUEST_STATE_MECH_CONTINUE); + + timeout_remove(&request->to_penalty); + + if (request->failed || !request->passdb_success) { + /* password was valid, but some other check failed. */ + auth_request_fail(request); + return; + } + auth_request_set_auth_successful(request); + + /* log before delay */ + auth_request_log_finished(request); + + if (request->delay_until > ioloop_time) { + unsigned int delay_secs = request->delay_until - ioloop_time; + request->to_penalty = timeout_add(delay_secs * 1000, + auth_request_success_continue, ctx); + return; + } + + if (ctx->success_data->used > 0 && !request->fields.final_resp_ok) { + /* we'll need one more SASL round, since client doesn't support + the final SASL response */ + auth_request_handler_reply_continue(request, + ctx->success_data->data, ctx->success_data->used); + return; + } + + if (request->set->stats) { + stats = auth_request_stats_get(request); + stats->auth_success_count++; + if (request->fields.master_user != NULL) + stats->auth_master_success_count++; + } + + auth_request_set_state(request, AUTH_REQUEST_STATE_FINISHED); + auth_request_refresh_last_access(request); + auth_request_handler_reply(request, AUTH_CLIENT_RESULT_SUCCESS, + ctx->success_data->data, ctx->success_data->used); +} + +void auth_request_fail(struct auth_request *request) +{ + struct auth_stats *stats; + + i_assert(request->state == AUTH_REQUEST_STATE_MECH_CONTINUE); + + if (request->set->stats) { + stats = auth_request_stats_get(request); + stats->auth_failure_count++; + } + + auth_request_set_state(request, AUTH_REQUEST_STATE_FINISHED); + auth_request_refresh_last_access(request); + auth_request_log_finished(request); + auth_request_handler_reply(request, AUTH_CLIENT_RESULT_FAILURE, "", 0); +} + +void auth_request_internal_failure(struct auth_request *request) +{ + request->internal_failure = TRUE; + auth_request_fail(request); +} + +void auth_request_ref(struct auth_request *request) +{ + request->refcount++; +} + +void auth_request_unref(struct auth_request **_request) +{ + struct auth_request *request = *_request; + + *_request = NULL; + i_assert(request->refcount > 0); + if (--request->refcount > 0) + return; + + i_assert(array_count(&request->authdb_event) == 0); + + if (request->handler_pending_reply) + auth_request_handler_abort(request); + + event_unref(&request->mech_event); + event_unref(&request->event); + auth_request_stats_send(request); + auth_request_state_count[request->state]--; + auth_refresh_proctitle(); + + if (request->mech_password != NULL) { + safe_memset(request->mech_password, 0, + strlen(request->mech_password)); + } + + if (request->dns_lookup_ctx != NULL) + dns_lookup_abort(&request->dns_lookup_ctx->dns_lookup); + timeout_remove(&request->to_abort); + timeout_remove(&request->to_penalty); + + if (request->mech != NULL) + request->mech->auth_free(request); + else + pool_unref(&request->pool); +} + +bool auth_request_import_master(struct auth_request *request, + const char *key, const char *value) +{ + pid_t pid; + + i_assert(value != NULL); + + /* master request lookups may set these */ + if (strcmp(key, "session_pid") == 0) { + if (str_to_pid(value, &pid) == 0) + request->session_pid = pid; + } else if (strcmp(key, "request_auth_token") == 0) + request->request_auth_token = TRUE; + else + return FALSE; + return TRUE; +} + +static bool auth_request_fail_on_nuls(struct auth_request *request, + const unsigned char *data, size_t data_size) +{ + if ((request->mech->flags & MECH_SEC_ALLOW_NULS) != 0) + return FALSE; + if (memchr(data, '\0', data_size) != NULL) { + e_debug(request->mech_event, "Unexpected NUL in auth data"); + auth_request_fail(request); + return TRUE; + } + return FALSE; +} + +void auth_request_initial(struct auth_request *request) +{ + i_assert(request->state == AUTH_REQUEST_STATE_NEW); + + auth_request_set_state(request, AUTH_REQUEST_STATE_MECH_CONTINUE); + + if (auth_request_fail_on_nuls(request, request->initial_response, + request->initial_response_len)) + return; + + request->mech->auth_initial(request, request->initial_response, + request->initial_response_len); +} + +void auth_request_continue(struct auth_request *request, + const unsigned char *data, size_t data_size) +{ + i_assert(request->state == AUTH_REQUEST_STATE_MECH_CONTINUE); + + if (request->fields.successful) { + auth_request_success(request, "", 0); + return; + } + + if (auth_request_fail_on_nuls(request, data, data_size)) + return; + + auth_request_refresh_last_access(request); + request->mech->auth_continue(request, data, data_size); +} + +static void auth_request_save_cache(struct auth_request *request, + enum passdb_result result) +{ + struct auth_passdb *passdb = request->passdb; + const char *encoded_password; + string_t *str; + struct password_generate_params gen_params = { + .user = request->fields.user, + .rounds = 0 + }; + + switch (result) { + case PASSDB_RESULT_USER_UNKNOWN: + case PASSDB_RESULT_PASSWORD_MISMATCH: + case PASSDB_RESULT_OK: + case PASSDB_RESULT_SCHEME_NOT_AVAILABLE: + /* can be cached */ + break; + case PASSDB_RESULT_NEXT: + case PASSDB_RESULT_USER_DISABLED: + case PASSDB_RESULT_PASS_EXPIRED: + /* FIXME: we can't cache this now, or cache lookup would + return success. */ + return; + case PASSDB_RESULT_INTERNAL_FAILURE: + i_unreached(); + } + + if (passdb_cache == NULL || passdb->cache_key == NULL) + return; + + if (result < 0) { + /* lookup failed. */ + if (result == PASSDB_RESULT_USER_UNKNOWN) { + auth_cache_insert(passdb_cache, request, + passdb->cache_key, "", FALSE); + } + return; + } + + if (request->passdb_password == NULL && + !auth_fields_exists(request->fields.extra_fields, "nopassword")) { + /* passdb didn't provide the correct password */ + if (result != PASSDB_RESULT_OK || + request->mech_password == NULL) + return; + + /* we can still cache valid password lookups though. + strdup() it so that mech_password doesn't get + cleared too early. */ + if (!password_generate_encoded(request->mech_password, + &gen_params, + CACHED_PASSWORD_SCHEME, + &encoded_password)) + i_unreached(); + request->passdb_password = + p_strconcat(request->pool, "{"CACHED_PASSWORD_SCHEME"}", + encoded_password, NULL); + } + + /* save all except the currently given password in cache */ + str = t_str_new(256); + if (request->passdb_password != NULL) { + if (*request->passdb_password != '{') { + /* cached passwords must have a known scheme */ + str_append_c(str, '{'); + str_append(str, passdb->passdb->default_pass_scheme); + str_append_c(str, '}'); + } + str_append_tabescaped(str, request->passdb_password); + } + + if (!auth_fields_is_empty(request->fields.extra_fields)) { + str_append_c(str, '\t'); + /* add only those extra fields to cache that were set by this + passdb lookup. the CHANGED flag does this, because we + snapshotted the extra_fields before the current passdb + lookup. */ + auth_fields_append(request->fields.extra_fields, str, + AUTH_FIELD_FLAG_CHANGED, + AUTH_FIELD_FLAG_CHANGED); + } + auth_cache_insert(passdb_cache, request, passdb->cache_key, str_c(str), + result == PASSDB_RESULT_OK); +} + +static bool +auth_request_mechanism_accepted(const char *const *mechs, + const struct mech_module *mech) +{ + /* no filter specified, anything goes */ + if (mechs == NULL) return TRUE; + /* request has no mechanism, see if none is accepted */ + if (mech == NULL) + return str_array_icase_find(mechs, "none"); + /* check if request mechanism is accepted */ + return str_array_icase_find(mechs, mech->mech_name); +} + +/** + +Check if username is included in the filter. Logic is that if the username +is not excluded by anything, and is included by something, it will be accepted. +By default, all usernames are included, unless there is a inclusion item, when +username will be excluded if there is no inclusion for it. + +Exclusions are denoted with a ! in front of the pattern. +*/ +bool auth_request_username_accepted(const char *const *filter, const char *username) +{ + bool have_includes = FALSE; + bool matched_inc = FALSE; + + for(;*filter != NULL; filter++) { + /* if filter has ! it means the pattern will be refused */ + bool exclude = (**filter == '!'); + if (!exclude) + have_includes = TRUE; + if (wildcard_match(username, (*filter)+(exclude?1:0))) { + if (exclude) { + return FALSE; + } else { + matched_inc = TRUE; + } + } + } + + return matched_inc || !have_includes; +} + +static bool +auth_request_want_skip_passdb(struct auth_request *request, + struct auth_passdb *passdb) +{ + /* if mechanism is not supported, skip */ + const char *const *mechs = passdb->passdb->mechanisms; + const char *const *username_filter = passdb->passdb->username_filter; + const char *username; + + username = request->fields.user; + + if (!auth_request_mechanism_accepted(mechs, request->mech)) { + auth_request_log_debug(request, + request->mech != NULL ? AUTH_SUBSYS_MECH + : "none", + "skipping passdb: mechanism filtered"); + return TRUE; + } + + if (passdb->passdb->username_filter != NULL && + !auth_request_username_accepted(username_filter, username)) { + auth_request_log_debug(request, + request->mech != NULL ? AUTH_SUBSYS_MECH + : "none", + "skipping passdb: username filtered"); + return TRUE; + } + + /* skip_password_check basically specifies if authentication is + finished */ + bool authenticated = request->fields.skip_password_check; + + switch (passdb->skip) { + case AUTH_PASSDB_SKIP_NEVER: + return FALSE; + case AUTH_PASSDB_SKIP_AUTHENTICATED: + return authenticated; + case AUTH_PASSDB_SKIP_UNAUTHENTICATED: + return !authenticated; + } + i_unreached(); +} + +static bool +auth_request_want_skip_userdb(struct auth_request *request, + struct auth_userdb *userdb) +{ + switch (userdb->skip) { + case AUTH_USERDB_SKIP_NEVER: + return FALSE; + case AUTH_USERDB_SKIP_FOUND: + return request->userdb_success; + case AUTH_USERDB_SKIP_NOTFOUND: + return !request->userdb_success; + } + i_unreached(); +} + +static const char * +auth_request_cache_result_to_str(enum auth_request_cache_result result) +{ + switch(result) { + case AUTH_REQUEST_CACHE_NONE: + return "none"; + case AUTH_REQUEST_CACHE_HIT: + return "hit"; + case AUTH_REQUEST_CACHE_MISS: + return "miss"; + default: + i_unreached(); + } +} + +void auth_request_passdb_lookup_begin(struct auth_request *request) +{ + struct event *event; + const char *name; + + i_assert(request->passdb != NULL); + i_assert(!request->userdb_lookup); + + request->passdb_cache_result = AUTH_REQUEST_CACHE_NONE; + + name = (request->passdb->set->name[0] != '\0' ? + request->passdb->set->name : + request->passdb->passdb->iface.name); + + event = event_create(request->event); + event_add_str(event, "passdb_id", dec2str(request->passdb->passdb->id)); + event_add_str(event, "passdb_name", name); + event_add_str(event, "passdb", request->passdb->passdb->iface.name); + event_set_log_prefix_callback(event, FALSE, + auth_request_get_log_prefix_db, request); + + /* check if we should enable verbose logging here */ + if (*request->passdb->set->auth_verbose == 'y') + event_set_min_log_level(event, LOG_TYPE_INFO); + else if (*request->passdb->set->auth_verbose == 'n') + event_set_min_log_level(event, LOG_TYPE_WARNING); + + e_debug(event_create_passthrough(event)-> + set_name("auth_passdb_request_started")-> + event(), + "Performing passdb lookup"); + array_push_back(&request->authdb_event, &event); +} + +void auth_request_passdb_lookup_end(struct auth_request *request, + enum passdb_result result) +{ + i_assert(array_count(&request->authdb_event) > 0); + struct event *event = authdb_event(request); + struct event_passthrough *e = + event_create_passthrough(event)-> + set_name("auth_passdb_request_finished")-> + add_str("result", passdb_result_to_string(result)); + if (request->passdb_cache_result != AUTH_REQUEST_CACHE_NONE && + request->set->cache_ttl != 0 && request->set->cache_size != 0) + e->add_str("cache", auth_request_cache_result_to_str(request->passdb_cache_result)); + e_debug(e->event(), "Finished passdb lookup"); + event_unref(&event); + array_pop_back(&request->authdb_event); +} + +void auth_request_userdb_lookup_begin(struct auth_request *request) +{ + struct event *event; + const char *name; + + i_assert(request->userdb != NULL); + i_assert(request->userdb_lookup); + + request->userdb_cache_result = AUTH_REQUEST_CACHE_NONE; + + name = (request->userdb->set->name[0] != '\0' ? + request->userdb->set->name : + request->userdb->userdb->iface->name); + + event = event_create(request->event); + event_add_str(event, "userdb_id", dec2str(request->userdb->userdb->id)); + event_add_str(event, "userdb_name", name); + event_add_str(event, "userdb", request->userdb->userdb->iface->name); + event_set_log_prefix_callback(event, FALSE, + auth_request_get_log_prefix_db, request); + + /* check if we should enable verbose logging here*/ + if (*request->userdb->set->auth_verbose == 'y') + event_set_min_log_level(event, LOG_TYPE_INFO); + else if (*request->userdb->set->auth_verbose == 'n') + event_set_min_log_level(event, LOG_TYPE_WARNING); + + e_debug(event_create_passthrough(event)-> + set_name("auth_userdb_request_started")-> + event(), + "Performing userdb lookup"); + array_push_back(&request->authdb_event, &event); +} + +void auth_request_userdb_lookup_end(struct auth_request *request, + enum userdb_result result) +{ + i_assert(array_count(&request->authdb_event) > 0); + struct event *event = authdb_event(request); + struct event_passthrough *e = + event_create_passthrough(event)-> + set_name("auth_userdb_request_finished")-> + add_str("result", userdb_result_to_string(result)); + if (request->userdb_cache_result != AUTH_REQUEST_CACHE_NONE && + request->set->cache_ttl != 0 && request->set->cache_size != 0) + e->add_str("cache", auth_request_cache_result_to_str(request->userdb_cache_result)); + e_debug(e->event(), "Finished userdb lookup"); + event_unref(&event); + array_pop_back(&request->authdb_event); +} + +static bool +auth_request_handle_passdb_callback(enum passdb_result *result, + struct auth_request *request) +{ + struct auth_passdb *next_passdb; + enum auth_db_rule result_rule; + bool passdb_continue = FALSE; + + if (request->passdb_password != NULL) { + safe_memset(request->passdb_password, 0, + strlen(request->passdb_password)); + } + + auth_request_passdb_lookup_end(request, *result); + + if (request->passdb->set->deny && + *result != PASSDB_RESULT_USER_UNKNOWN) { + /* deny passdb. we can get through this step only if the + lookup returned that user doesn't exist in it. internal + errors are fatal here. */ + if (*result != PASSDB_RESULT_INTERNAL_FAILURE) { + e_info(authdb_event(request), + "User found from deny passdb"); + *result = PASSDB_RESULT_USER_DISABLED; + } + return TRUE; + } + if (request->failed) { + /* The passdb didn't fail, but something inside it failed + (e.g. allow_nets mismatch). Make sure we'll fail this + lookup, but reset the failure so the next passdb can + succeed. */ + if (*result == PASSDB_RESULT_OK) + *result = PASSDB_RESULT_USER_UNKNOWN; + request->failed = FALSE; + } + + /* users that exist but can't log in are special. we don't try to match + any of the success/failure rules to them. they'll always fail. */ + switch (*result) { + case PASSDB_RESULT_USER_DISABLED: + return TRUE; + case PASSDB_RESULT_PASS_EXPIRED: + auth_request_set_field(request, "reason", + "Password expired", NULL); + return TRUE; + + case PASSDB_RESULT_OK: + result_rule = request->passdb->result_success; + break; + case PASSDB_RESULT_INTERNAL_FAILURE: + result_rule = request->passdb->result_internalfail; + break; + case PASSDB_RESULT_NEXT: + e_debug(authdb_event(request), + "Not performing authentication (noauthenticate set)"); + result_rule = AUTH_DB_RULE_CONTINUE; + break; + case PASSDB_RESULT_SCHEME_NOT_AVAILABLE: + case PASSDB_RESULT_USER_UNKNOWN: + case PASSDB_RESULT_PASSWORD_MISMATCH: + default: + result_rule = request->passdb->result_failure; + break; + } + + switch (result_rule) { + case AUTH_DB_RULE_RETURN: + break; + case AUTH_DB_RULE_RETURN_OK: + request->passdb_success = TRUE; + break; + case AUTH_DB_RULE_RETURN_FAIL: + request->passdb_success = FALSE; + break; + case AUTH_DB_RULE_CONTINUE: + passdb_continue = TRUE; + if (*result == PASSDB_RESULT_OK) { + /* password was successfully verified. don't bother + checking it again. */ + auth_request_set_password_verified(request); + } + break; + case AUTH_DB_RULE_CONTINUE_OK: + passdb_continue = TRUE; + request->passdb_success = TRUE; + /* password was successfully verified. don't bother + checking it again. */ + auth_request_set_password_verified(request); + break; + case AUTH_DB_RULE_CONTINUE_FAIL: + passdb_continue = TRUE; + request->passdb_success = FALSE; + break; + } + /* nopassword check is specific to a single passdb and shouldn't leak + to the next one. we already added it to cache. */ + auth_fields_remove(request->fields.extra_fields, "nopassword"); + auth_fields_remove(request->fields.extra_fields, "noauthenticate"); + + if (request->fields.requested_login_user != NULL && + *result == PASSDB_RESULT_OK) { + auth_request_master_user_login_finish(request); + /* if the passdb lookup continues, it continues with non-master + passdbs for the requested_login_user. */ + next_passdb = auth_request_get_auth(request)->passdbs; + } else { + next_passdb = request->passdb->next; + } + + while (next_passdb != NULL && + auth_request_want_skip_passdb(request, next_passdb)) + next_passdb = next_passdb->next; + + if (*result == PASSDB_RESULT_OK || *result == PASSDB_RESULT_NEXT) { + /* this passdb lookup succeeded, preserve its extra fields */ + auth_fields_snapshot(request->fields.extra_fields); + request->snapshot_have_userdb_prefetch_set = + request->userdb_prefetch_set; + if (request->fields.userdb_reply != NULL) + auth_fields_snapshot(request->fields.userdb_reply); + } else { + /* this passdb lookup failed, remove any extra fields it set */ + auth_fields_rollback(request->fields.extra_fields); + if (request->fields.userdb_reply != NULL) { + auth_fields_rollback(request->fields.userdb_reply); + request->userdb_prefetch_set = + request->snapshot_have_userdb_prefetch_set; + } + } + + if (passdb_continue && next_passdb != NULL) { + /* try next passdb. */ + request->passdb = next_passdb; + request->passdb_password = NULL; + + if (*result == PASSDB_RESULT_USER_UNKNOWN) { + /* remember that we did at least one successful + passdb lookup */ + request->passdbs_seen_user_unknown = TRUE; + } else if (*result == PASSDB_RESULT_INTERNAL_FAILURE) { + /* remember that we have had an internal failure. at + the end return internal failure if we couldn't + successfully login. */ + request->passdbs_seen_internal_failure = TRUE; + } + return FALSE; + } else if (*result == PASSDB_RESULT_NEXT) { + /* admin forgot to put proper passdb last */ + e_error(request->event, + "%sLast passdb had noauthenticate field, cannot authenticate user", + auth_request_get_log_prefix_db(request)); + *result = PASSDB_RESULT_INTERNAL_FAILURE; + } else if (request->passdb_success) { + /* either this or a previous passdb lookup succeeded. */ + *result = PASSDB_RESULT_OK; + } else if (request->passdbs_seen_internal_failure) { + /* last passdb lookup returned internal failure. it may have + had the correct password, so return internal failure + instead of plain failure. */ + *result = PASSDB_RESULT_INTERNAL_FAILURE; + } + return TRUE; +} + +void +auth_request_verify_plain_callback_finish(enum passdb_result result, + struct auth_request *request) +{ + const char *error; + + if (passdb_template_export(request->passdb->override_fields_tmpl, + request, &error) < 0) { + e_error(authdb_event(request), + "Failed to expand override_fields: %s", error); + result = PASSDB_RESULT_INTERNAL_FAILURE; + } + if (!auth_request_handle_passdb_callback(&result, request)) { + /* try next passdb */ + auth_request_verify_plain(request, request->mech_password, + request->private_callback.verify_plain); + } else { + auth_request_ref(request); + request->passdb_result = result; + request->private_callback.verify_plain(request->passdb_result, request); + auth_request_unref(&request); + } +} + +void auth_request_verify_plain_callback(enum passdb_result result, + struct auth_request *request) +{ + struct auth_passdb *passdb = request->passdb; + + i_assert(request->state == AUTH_REQUEST_STATE_PASSDB); + + auth_request_set_state(request, AUTH_REQUEST_STATE_MECH_CONTINUE); + + if (result == PASSDB_RESULT_OK && + auth_fields_exists(request->fields.extra_fields, "noauthenticate")) + result = PASSDB_RESULT_NEXT; + + if (result != PASSDB_RESULT_INTERNAL_FAILURE) + auth_request_save_cache(request, result); + else { + /* lookup failed. if we're looking here only because the + request was expired in cache, fallback to using cached + expired record. */ + const char *cache_key = passdb->cache_key; + + auth_request_stats_add_tempfail(request); + if (passdb_cache_verify_plain(request, cache_key, + request->mech_password, + &result, TRUE)) { + e_info(authdb_event(request), + "Falling back to expired data from cache"); + return; + } + } + + auth_request_verify_plain_callback_finish(result, request); +} + +static bool password_has_illegal_chars(const char *password) +{ + for (; *password != '\0'; password++) { + switch (*password) { + case '\001': + case '\t': + case '\r': + case '\n': + /* these characters have a special meaning in internal + protocols, make sure the password doesn't + accidentally get there unescaped. */ + return TRUE; + } + } + return FALSE; +} + +static bool auth_request_is_disabled_master_user(struct auth_request *request) +{ + if (request->fields.requested_login_user == NULL || + request->passdb != NULL) + return FALSE; + + /* no masterdbs, master logins not supported */ + e_info(request->mech_event, + "Attempted master login with no master passdbs " + "(trying to log in as user: %s)", + request->fields.requested_login_user); + return TRUE; +} + +static +void auth_request_policy_penalty_finish(void *context) +{ + struct auth_policy_check_ctx *ctx = context; + + timeout_remove(&ctx->request->to_penalty); + + i_assert(ctx->request->state == AUTH_REQUEST_STATE_MECH_CONTINUE); + + switch(ctx->type) { + case AUTH_POLICY_CHECK_TYPE_PLAIN: + ctx->request->handler->verify_plain_continue_callback(ctx->request, ctx->callback_plain); + return; + case AUTH_POLICY_CHECK_TYPE_LOOKUP: + auth_request_lookup_credentials_policy_continue(ctx->request, ctx->callback_lookup); + return; + case AUTH_POLICY_CHECK_TYPE_SUCCESS: + auth_request_success_continue(ctx); + return; + default: + i_unreached(); + } +} + +static +void auth_request_policy_check_callback(int result, void *context) +{ + struct auth_policy_check_ctx *ctx = context; + + ctx->request->policy_processed = TRUE; + /* It's possible that multiple policy lookups return a penalty. + Sum them all up to the event. */ + ctx->request->policy_penalty += result < 0 ? 0 : result; + + if (ctx->request->set->policy_log_only && result != 0) { + auth_request_policy_penalty_finish(context); + return; + } + if (result < 0) { + /* fail it right here and now */ + auth_request_fail(ctx->request); + } else if (ctx->type != AUTH_POLICY_CHECK_TYPE_SUCCESS && result > 0 && + !ctx->request->fields.no_penalty) { + ctx->request->to_penalty = timeout_add(result * 1000, + auth_request_policy_penalty_finish, + context); + } else { + auth_request_policy_penalty_finish(context); + } +} + +void auth_request_verify_plain(struct auth_request *request, + const char *password, + verify_plain_callback_t *callback) +{ + struct auth_policy_check_ctx *ctx; + + i_assert(request->state == AUTH_REQUEST_STATE_MECH_CONTINUE); + + if (request->mech_password == NULL) + request->mech_password = p_strdup(request->pool, password); + else + i_assert(request->mech_password == password); + request->user_changed_by_lookup = FALSE; + + if (request->policy_processed || !request->set->policy_check_before_auth) { + request->handler->verify_plain_continue_callback(request, + callback); + } else { + ctx = p_new(request->pool, struct auth_policy_check_ctx, 1); + ctx->request = request; + ctx->callback_plain = callback; + ctx->type = AUTH_POLICY_CHECK_TYPE_PLAIN; + auth_policy_check(request, request->mech_password, auth_request_policy_check_callback, ctx); + } +} + +void auth_request_default_verify_plain_continue(struct auth_request *request, + verify_plain_callback_t *callback) +{ + struct auth_passdb *passdb; + enum passdb_result result; + const char *cache_key, *error; + const char *password = request->mech_password; + + i_assert(request->state == AUTH_REQUEST_STATE_MECH_CONTINUE); + + if (auth_request_is_disabled_master_user(request)) { + callback(PASSDB_RESULT_USER_UNKNOWN, request); + return; + } + + if (password_has_illegal_chars(password)) { + e_info(authdb_event(request), + "Attempted login with password having illegal chars"); + callback(PASSDB_RESULT_USER_UNKNOWN, request); + return; + } + + passdb = request->passdb; + + while (passdb != NULL && auth_request_want_skip_passdb(request, passdb)) + passdb = passdb->next; + + request->passdb = passdb; + + if (passdb == NULL) { + auth_request_log_error(request, + request->mech != NULL ? AUTH_SUBSYS_MECH : "none", + "All password databases were skipped"); + callback(PASSDB_RESULT_INTERNAL_FAILURE, request); + return; + } + + auth_request_passdb_lookup_begin(request); + request->private_callback.verify_plain = callback; + + cache_key = passdb_cache == NULL ? NULL : passdb->cache_key; + if (passdb_cache_verify_plain(request, cache_key, password, + &result, FALSE)) { + return; + } + + auth_request_set_state(request, AUTH_REQUEST_STATE_PASSDB); + /* In case this request had already done a credentials lookup (is it + even possible?), make sure wanted_credentials_scheme is cleared + so passdbs don't think we're doing a credentials lookup. */ + request->wanted_credentials_scheme = NULL; + + if (passdb->passdb->iface.verify_plain == NULL) { + /* we're deinitializing and just want to get rid of this + request */ + auth_request_verify_plain_callback( + PASSDB_RESULT_INTERNAL_FAILURE, request); + } else if (passdb->passdb->blocking) { + passdb_blocking_verify_plain(request); + } else if (passdb_template_export(passdb->default_fields_tmpl, + request, &error) < 0) { + e_error(authdb_event(request), + "Failed to expand default_fields: %s", error); + auth_request_verify_plain_callback( + PASSDB_RESULT_INTERNAL_FAILURE, request); + } else { + passdb->passdb->iface.verify_plain(request, password, + auth_request_verify_plain_callback); + } +} + +static void +auth_request_lookup_credentials_finish(enum passdb_result result, + const unsigned char *credentials, + size_t size, + struct auth_request *request) +{ + const char *error; + + if (passdb_template_export(request->passdb->override_fields_tmpl, + request, &error) < 0) { + e_error(authdb_event(request), + "Failed to expand override_fields: %s", error); + result = PASSDB_RESULT_INTERNAL_FAILURE; + } + if (!auth_request_handle_passdb_callback(&result, request)) { + /* try next passdb */ + if (request->fields.skip_password_check && + request->fields.delayed_credentials == NULL && size > 0) { + /* passdb continue* rule after a successful lookup. + remember these credentials and use them later on. */ + auth_request_set_delayed_credentials(request, + credentials, size); + } + auth_request_lookup_credentials(request, + request->wanted_credentials_scheme, + request->private_callback.lookup_credentials); + } else { + if (request->fields.delayed_credentials != NULL && size == 0) { + /* we did multiple passdb lookups, but the last one + didn't provide any credentials (e.g. just wanted to + add some extra fields). so use the first passdb's + credentials instead. */ + credentials = request->fields.delayed_credentials; + size = request->fields.delayed_credentials_size; + } + if (request->set->debug_passwords && + result == PASSDB_RESULT_OK) { + e_debug(authdb_event(request), + "Credentials: %s", + binary_to_hex(credentials, size)); + } + if (result == PASSDB_RESULT_SCHEME_NOT_AVAILABLE && + request->passdbs_seen_user_unknown) { + /* one of the passdbs accepted the scheme, + but the user was unknown there */ + result = PASSDB_RESULT_USER_UNKNOWN; + } + request->passdb_result = result; + request->private_callback. + lookup_credentials(result, credentials, size, request); + } +} + +void auth_request_lookup_credentials_callback(enum passdb_result result, + const unsigned char *credentials, + size_t size, + struct auth_request *request) +{ + struct auth_passdb *passdb = request->passdb; + const char *cache_cred, *cache_scheme; + + i_assert(request->state == AUTH_REQUEST_STATE_PASSDB); + + auth_request_set_state(request, AUTH_REQUEST_STATE_MECH_CONTINUE); + + if (result == PASSDB_RESULT_OK && + auth_fields_exists(request->fields.extra_fields, "noauthenticate")) + result = PASSDB_RESULT_NEXT; + + if (result != PASSDB_RESULT_INTERNAL_FAILURE) + auth_request_save_cache(request, result); + else { + /* lookup failed. if we're looking here only because the + request was expired in cache, fallback to using cached + expired record. */ + const char *cache_key = passdb->cache_key; + + auth_request_stats_add_tempfail(request); + if (passdb_cache_lookup_credentials(request, cache_key, + &cache_cred, &cache_scheme, + &result, TRUE)) { + e_info(authdb_event(request), + "Falling back to expired data from cache"); + passdb_handle_credentials( + result, cache_cred, cache_scheme, + auth_request_lookup_credentials_finish, + request); + return; + } + } + + auth_request_lookup_credentials_finish(result, credentials, size, + request); +} + +void auth_request_lookup_credentials(struct auth_request *request, + const char *scheme, + lookup_credentials_callback_t *callback) +{ + struct auth_policy_check_ctx *ctx; + + i_assert(request->state == AUTH_REQUEST_STATE_MECH_CONTINUE); + + if (request->wanted_credentials_scheme == NULL) + request->wanted_credentials_scheme = + p_strdup(request->pool, scheme); + request->user_changed_by_lookup = FALSE; + + if (request->policy_processed || !request->set->policy_check_before_auth) + auth_request_lookup_credentials_policy_continue(request, callback); + else { + ctx = p_new(request->pool, struct auth_policy_check_ctx, 1); + ctx->request = request; + ctx->callback_lookup = callback; + ctx->type = AUTH_POLICY_CHECK_TYPE_LOOKUP; + auth_policy_check(request, ctx->request->mech_password, auth_request_policy_check_callback, ctx); + } +} + +static +void auth_request_lookup_credentials_policy_continue(struct auth_request *request, + lookup_credentials_callback_t *callback) +{ + struct auth_passdb *passdb; + const char *cache_key, *cache_cred, *cache_scheme, *error; + enum passdb_result result; + + i_assert(request->state == AUTH_REQUEST_STATE_MECH_CONTINUE); + if (auth_request_is_disabled_master_user(request)) { + callback(PASSDB_RESULT_USER_UNKNOWN, NULL, 0, request); + return; + } + passdb = request->passdb; + while (passdb != NULL && auth_request_want_skip_passdb(request, passdb)) + passdb = passdb->next; + request->passdb = passdb; + + if (passdb == NULL) { + auth_request_log_error(request, + request->mech != NULL ? AUTH_SUBSYS_MECH : "none", + "All password databases were skipped"); + callback(PASSDB_RESULT_INTERNAL_FAILURE, NULL, 0, request); + return; + } + + auth_request_passdb_lookup_begin(request); + request->private_callback.lookup_credentials = callback; + + cache_key = passdb_cache == NULL ? NULL : passdb->cache_key; + if (cache_key != NULL) { + if (passdb_cache_lookup_credentials(request, cache_key, + &cache_cred, &cache_scheme, + &result, FALSE)) { + request->passdb_cache_result = AUTH_REQUEST_CACHE_HIT; + passdb_handle_credentials( + result, cache_cred, cache_scheme, + auth_request_lookup_credentials_finish, + request); + return; + } else { + request->passdb_cache_result = AUTH_REQUEST_CACHE_MISS; + } + } + + auth_request_set_state(request, AUTH_REQUEST_STATE_PASSDB); + + if (passdb->passdb->iface.lookup_credentials == NULL) { + /* this passdb doesn't support credentials */ + e_debug(authdb_event(request), + "passdb doesn't support credential lookups"); + auth_request_lookup_credentials_callback( + PASSDB_RESULT_SCHEME_NOT_AVAILABLE, + uchar_empty_ptr, 0, request); + } else if (passdb->passdb->blocking) { + passdb_blocking_lookup_credentials(request); + } else if (passdb_template_export(passdb->default_fields_tmpl, + request, &error) < 0) { + e_error(authdb_event(request), + "Failed to expand default_fields: %s", error); + auth_request_lookup_credentials_callback( + PASSDB_RESULT_INTERNAL_FAILURE, + uchar_empty_ptr, 0, request); + } else { + passdb->passdb->iface.lookup_credentials(request, + auth_request_lookup_credentials_callback); + } +} + +void auth_request_set_credentials(struct auth_request *request, + const char *scheme, const char *data, + set_credentials_callback_t *callback) +{ + struct auth_passdb *passdb = request->passdb; + const char *cache_key, *new_credentials; + + cache_key = passdb_cache == NULL ? NULL : passdb->cache_key; + if (cache_key != NULL) + auth_cache_remove(passdb_cache, request, cache_key); + + request->private_callback.set_credentials = callback; + + new_credentials = t_strdup_printf("{%s}%s", scheme, data); + if (passdb->passdb->blocking) + passdb_blocking_set_credentials(request, new_credentials); + else if (passdb->passdb->iface.set_credentials != NULL) { + passdb->passdb->iface.set_credentials(request, new_credentials, + callback); + } else { + /* this passdb doesn't support credentials update */ + callback(FALSE, request); + } +} + +static void auth_request_userdb_save_cache(struct auth_request *request, + enum userdb_result result) +{ + struct auth_userdb *userdb = request->userdb; + string_t *str; + const char *cache_value; + + if (passdb_cache == NULL || userdb->cache_key == NULL) + return; + + if (result == USERDB_RESULT_USER_UNKNOWN) + cache_value = ""; + else { + str = t_str_new(128); + auth_fields_append(request->fields.userdb_reply, str, + AUTH_FIELD_FLAG_CHANGED, + AUTH_FIELD_FLAG_CHANGED); + if (request->user_changed_by_lookup) { + /* username was changed by passdb or userdb */ + if (str_len(str) > 0) + str_append_c(str, '\t'); + str_append(str, "user="); + str_append_tabescaped(str, request->fields.user); + } + if (str_len(str) == 0) { + /* no userdb fields. but we can't save an empty string, + since that means "user unknown". */ + str_append(str, AUTH_REQUEST_USER_KEY_IGNORE); + } + cache_value = str_c(str); + } + /* last_success has no meaning with userdb */ + auth_cache_insert(passdb_cache, request, userdb->cache_key, + cache_value, FALSE); +} + +static bool auth_request_lookup_user_cache(struct auth_request *request, + const char *key, + enum userdb_result *result_r, + bool use_expired) +{ + struct auth_stats *stats = auth_request_stats_get(request); + const char *value; + struct auth_cache_node *node; + bool expired, neg_expired; + + value = auth_cache_lookup(passdb_cache, request, key, &node, + &expired, &neg_expired); + if (value == NULL || (expired && !use_expired)) { + stats->auth_cache_miss_count++; + request->userdb_cache_result = AUTH_REQUEST_CACHE_MISS; + e_debug(request->event, + value == NULL ? "%suserdb cache miss" : + "%suserdb cache expired", + auth_request_get_log_prefix_db(request)); + return FALSE; + } + stats->auth_cache_hit_count++; + request->userdb_cache_result = AUTH_REQUEST_CACHE_HIT; + e_debug(request->event, + "%suserdb cache hit: %s", + auth_request_get_log_prefix_db(request), value); + + if (*value == '\0') { + /* negative cache entry */ + *result_r = USERDB_RESULT_USER_UNKNOWN; + auth_request_init_userdb_reply(request, FALSE); + return TRUE; + } + + /* We want to preserve any userdb fields set by the earlier passdb + lookup, so initialize userdb_reply only if it doesn't exist. + Don't add userdb's default_fields, because the entire userdb part of + the result comes from the cache. */ + if (request->fields.userdb_reply == NULL) + auth_request_init_userdb_reply(request, FALSE); + auth_request_userdb_import(request, value); + *result_r = USERDB_RESULT_OK; + return TRUE; +} + +void auth_request_userdb_callback(enum userdb_result result, + struct auth_request *request) +{ + struct auth_userdb *userdb = request->userdb; + struct auth_userdb *next_userdb; + enum auth_db_rule result_rule; + const char *error; + bool userdb_continue = FALSE; + + switch (result) { + case USERDB_RESULT_OK: + result_rule = userdb->result_success; + break; + case USERDB_RESULT_INTERNAL_FAILURE: + auth_request_stats_add_tempfail(request); + result_rule = userdb->result_internalfail; + break; + case USERDB_RESULT_USER_UNKNOWN: + default: + result_rule = userdb->result_failure; + break; + } + + switch (result_rule) { + case AUTH_DB_RULE_RETURN: + break; + case AUTH_DB_RULE_RETURN_OK: + request->userdb_success = TRUE; + break; + case AUTH_DB_RULE_RETURN_FAIL: + request->userdb_success = FALSE; + break; + case AUTH_DB_RULE_CONTINUE: + userdb_continue = TRUE; + break; + case AUTH_DB_RULE_CONTINUE_OK: + userdb_continue = TRUE; + request->userdb_success = TRUE; + break; + case AUTH_DB_RULE_CONTINUE_FAIL: + userdb_continue = TRUE; + request->userdb_success = FALSE; + break; + } + + auth_request_userdb_lookup_end(request, result); + + next_userdb = userdb->next; + while (next_userdb != NULL && + auth_request_want_skip_userdb(request, next_userdb)) + next_userdb = next_userdb->next; + + if (userdb_continue && next_userdb != NULL) { + /* try next userdb. */ + if (result == USERDB_RESULT_INTERNAL_FAILURE) + request->userdbs_seen_internal_failure = TRUE; + + if (result == USERDB_RESULT_OK) { + /* this userdb lookup succeeded, preserve its extra + fields */ + if (userdb_template_export(userdb->override_fields_tmpl, + request, &error) < 0) { + e_error(request->event, + "%sFailed to expand override_fields: %s", + auth_request_get_log_prefix_db(request), error); + request->private_callback.userdb( + USERDB_RESULT_INTERNAL_FAILURE, request); + return; + } + auth_fields_snapshot(request->fields.userdb_reply); + } else { + /* this userdb lookup failed, remove any extra fields + it set */ + auth_fields_rollback(request->fields.userdb_reply); + } + request->user_changed_by_lookup = FALSE; + + request->userdb = next_userdb; + auth_request_lookup_user(request, + request->private_callback.userdb); + return; + } + + if (request->userdb_success) { + if (userdb_template_export(userdb->override_fields_tmpl, + request, &error) < 0) { + e_error(request->event, + "%sFailed to expand override_fields: %s", + auth_request_get_log_prefix_db(request), error); + result = USERDB_RESULT_INTERNAL_FAILURE; + } else { + result = USERDB_RESULT_OK; + } + } else if (request->userdbs_seen_internal_failure || + result == USERDB_RESULT_INTERNAL_FAILURE) { + /* one of the userdb lookups failed. the user might have been + in there, so this is an internal failure */ + result = USERDB_RESULT_INTERNAL_FAILURE; + } else if (request->client_pid != 0) { + /* this was an actual login attempt, the user should + have been found. */ + if (auth_request_get_auth(request)->userdbs->next == NULL) { + e_error(request->event, + "%suser not found from userdb", + auth_request_get_log_prefix_db(request)); + } else { + e_error(request->mech_event, + "user not found from any userdbs"); + } + result = USERDB_RESULT_USER_UNKNOWN; + } else { + result = USERDB_RESULT_USER_UNKNOWN; + } + + if (request->userdb_lookup_tempfailed) { + /* no caching */ + } else if (result != USERDB_RESULT_INTERNAL_FAILURE) { + if (request->userdb_cache_result != AUTH_REQUEST_CACHE_HIT) + auth_request_userdb_save_cache(request, result); + } else if (passdb_cache != NULL && userdb->cache_key != NULL) { + /* lookup failed. if we're looking here only because the + request was expired in cache, fallback to using cached + expired record. */ + const char *cache_key = userdb->cache_key; + + if (auth_request_lookup_user_cache(request, cache_key, + &result, TRUE)) { + e_info(request->event, + "%sFalling back to expired data from cache", + auth_request_get_log_prefix_db(request)); + } + } + + request->private_callback.userdb(result, request); +} + +void auth_request_lookup_user(struct auth_request *request, + userdb_callback_t *callback) +{ + struct auth_userdb *userdb = request->userdb; + const char *cache_key, *error; + + request->private_callback.userdb = callback; + request->user_changed_by_lookup = FALSE; + request->userdb_lookup = TRUE; + request->userdb_cache_result = AUTH_REQUEST_CACHE_NONE; + if (request->fields.userdb_reply == NULL) + auth_request_init_userdb_reply(request, TRUE); + else { + /* we still want to set default_fields. these override any + existing fields set by previous userdbs (because if that is + unwanted, ":protected" can be used). */ + if (userdb_template_export(userdb->default_fields_tmpl, + request, &error) < 0) { + e_error(authdb_event(request), + "Failed to expand default_fields: %s", error); + auth_request_userdb_callback( + USERDB_RESULT_INTERNAL_FAILURE, request); + return; + } + } + + auth_request_userdb_lookup_begin(request); + + /* (for now) auth_cache is shared between passdb and userdb */ + cache_key = passdb_cache == NULL ? NULL : userdb->cache_key; + if (cache_key != NULL) { + enum userdb_result result; + + if (auth_request_lookup_user_cache(request, cache_key, + &result, FALSE)) { + request->userdb_cache_result = AUTH_REQUEST_CACHE_HIT; + auth_request_userdb_callback(result, request); + return; + } else { + request->userdb_cache_result = AUTH_REQUEST_CACHE_MISS; + } + } + + if (userdb->userdb->iface->lookup == NULL) { + /* we are deinitializing */ + auth_request_userdb_callback(USERDB_RESULT_INTERNAL_FAILURE, + request); + } else if (userdb->userdb->blocking) + userdb_blocking_lookup(request); + else + userdb->userdb->iface->lookup(request, auth_request_userdb_callback); +} + +static void +auth_request_validate_networks(struct auth_request *request, + const char *name, const char *networks, + const struct ip_addr *remote_ip) +{ + const char *const *net; + struct ip_addr net_ip; + unsigned int bits; + bool found = FALSE; + + for (net = t_strsplit_spaces(networks, ", "); *net != NULL; net++) { + e_debug(authdb_event(request), + "%s: Matching for network %s", name, *net); + + if (strcmp(*net, "local") == 0) { + if (remote_ip->family == 0) { + found = TRUE; + break; + } + } else if (net_parse_range(*net, &net_ip, &bits) < 0) { + e_info(authdb_event(request), + "%s: Invalid network '%s'", name, *net); + } else if (remote_ip->family != 0 && + net_is_in_network(remote_ip, &net_ip, bits)) { + found = TRUE; + break; + } + } + + if (found) + ; + else if (remote_ip->family == 0) { + e_info(authdb_event(request), + "%s check failed: Remote IP not known and 'local' missing", name); + } else { + e_info(authdb_event(request), + "%s check failed: IP %s not in allowed networks", + name, net_ip2addr(remote_ip)); + } + if (!found) + request->failed = TRUE; +} + +static void +auth_request_set_password(struct auth_request *request, const char *value, + const char *default_scheme, bool noscheme) +{ + if (request->passdb_password != NULL) { + e_error(authdb_event(request), + "Multiple password values not supported"); + return; + } + + /* if the password starts with '{' it most likely contains + also '}'. check it anyway to make sure, because we + assert-crash later if it doesn't exist. this could happen + if plaintext passwords are used. */ + if (*value == '{' && !noscheme && strchr(value, '}') != NULL) + request->passdb_password = p_strdup(request->pool, value); + else { + i_assert(default_scheme != NULL); + request->passdb_password = + p_strdup_printf(request->pool, "{%s}%s", + default_scheme, value); + } +} + +static const char * +get_updated_username(const char *old_username, + const char *name, const char *value) +{ + const char *p; + + if (strcmp(name, "user") == 0) { + /* replace the whole username */ + return value; + } + + p = strchr(old_username, '@'); + if (strcmp(name, "username") == 0) { + if (strchr(value, '@') != NULL) + return value; + + /* preserve the current @domain */ + return t_strconcat(value, p, NULL); + } + + if (strcmp(name, "domain") == 0) { + if (p == NULL) { + /* add the domain */ + return t_strconcat(old_username, "@", value, NULL); + } else { + /* replace the existing domain */ + p = t_strdup_until(old_username, p + 1); + return t_strconcat(p, value, NULL); + } + } + return NULL; +} + +static bool +auth_request_try_update_username(struct auth_request *request, + const char *name, const char *value) +{ + const char *new_value; + + new_value = get_updated_username(request->fields.user, name, value); + if (new_value == NULL) + return FALSE; + if (new_value[0] == '\0') { + e_error(authdb_event(request), + "username attempted to be changed to empty"); + request->failed = TRUE; + return TRUE; + } + + if (strcmp(request->fields.user, new_value) != 0) { + e_debug(authdb_event(request), + "username changed %s -> %s", + request->fields.user, new_value); + auth_request_set_username_forced(request, new_value); + request->user_changed_by_lookup = TRUE; + } + return TRUE; +} + +static void +auth_request_passdb_import(struct auth_request *request, const char *args, + const char *key_prefix, const char *default_scheme) +{ + const char *const *arg, *field; + + for (arg = t_strsplit(args, "\t"); *arg != NULL; arg++) { + field = t_strconcat(key_prefix, *arg, NULL); + auth_request_set_field_keyvalue(request, field, default_scheme); + } +} + +void auth_request_set_field(struct auth_request *request, + const char *name, const char *value, + const char *default_scheme) +{ + size_t name_len = strlen(name); + + i_assert(*name != '\0'); + i_assert(value != NULL); + + i_assert(request->passdb != NULL); + + if (name_len > 10 && strcmp(name+name_len-10, ":protected") == 0) { + /* set this field only if it hasn't been set before */ + name = t_strndup(name, name_len-10); + if (auth_fields_exists(request->fields.extra_fields, name)) + return; + } else if (name_len > 7 && strcmp(name+name_len-7, ":remove") == 0) { + /* remove this field entirely */ + name = t_strndup(name, name_len-7); + auth_fields_remove(request->fields.extra_fields, name); + return; + } + + if (strcmp(name, "password") == 0) { + auth_request_set_password(request, value, + default_scheme, FALSE); + return; + } + if (strcmp(name, "password_noscheme") == 0) { + auth_request_set_password(request, value, default_scheme, TRUE); + return; + } + + if (auth_request_try_update_username(request, name, value)) { + /* don't change the original value so it gets saved correctly + to cache. */ + } else if (strcmp(name, "login_user") == 0) { + auth_request_set_login_username_forced(request, value); + } else if (strcmp(name, "allow_nets") == 0) { + auth_request_validate_networks(request, name, value, + &request->fields.remote_ip); + } else if (strcmp(name, "fail") == 0) { + request->failed = TRUE; + } else if (strcmp(name, "delay_until") == 0) { + time_t timestamp; + unsigned int extra_secs = 0; + const char *p; + + p = strchr(value, '+'); + if (p != NULL) { + value = t_strdup_until(value, p++); + if (str_to_uint(p, &extra_secs) < 0) { + e_error(authdb_event(request), + "Invalid delay_until randomness number '%s'", p); + request->failed = TRUE; + } else { + extra_secs = i_rand_limit(extra_secs); + } + } + if (str_to_time(value, ×tamp) < 0) { + e_error(authdb_event(request), + "Invalid delay_until timestamp '%s'", value); + request->failed = TRUE; + } else if (timestamp <= ioloop_time) { + /* no more delays */ + } else if (timestamp - ioloop_time > AUTH_REQUEST_MAX_DELAY_SECS) { + e_error(authdb_event(request), + "delay_until timestamp %s is too much in the future, failing", value); + request->failed = TRUE; + } else { + /* add randomness, but not too much of it */ + timestamp += extra_secs; + if (timestamp - ioloop_time > AUTH_REQUEST_MAX_DELAY_SECS) + timestamp = ioloop_time + AUTH_REQUEST_MAX_DELAY_SECS; + request->delay_until = timestamp; + } + } else if (strcmp(name, "allow_real_nets") == 0) { + auth_request_validate_networks(request, name, value, + &request->fields.real_remote_ip); + } else if (str_begins(name, "userdb_")) { + /* for prefetch userdb */ + request->userdb_prefetch_set = TRUE; + if (request->fields.userdb_reply == NULL) + auth_request_init_userdb_reply(request, TRUE); + if (strcmp(name, "userdb_userdb_import") == 0) { + /* we can't put the whole userdb_userdb_import + value to extra_cache_fields or it doesn't work + properly. so handle this explicitly. */ + auth_request_passdb_import(request, value, + "userdb_", default_scheme); + return; + } + auth_request_set_userdb_field(request, name + 7, value); + } else if (strcmp(name, "noauthenticate") == 0) { + /* add "nopassword" also so that passdbs won't try to verify + the password. */ + auth_fields_add(request->fields.extra_fields, name, value, 0); + auth_fields_add(request->fields.extra_fields, "nopassword", NULL, 0); + } else if (strcmp(name, "nopassword") == 0) { + /* NULL password - anything goes */ + const char *password = request->passdb_password; + + if (password != NULL && + !auth_fields_exists(request->fields.extra_fields, "noauthenticate")) { + (void)password_get_scheme(&password); + if (*password != '\0') { + e_error(authdb_event(request), + "nopassword set but password is " + "non-empty"); + return; + } + } + request->passdb_password = NULL; + auth_fields_add(request->fields.extra_fields, name, value, 0); + return; + } else if (strcmp(name, "passdb_import") == 0) { + auth_request_passdb_import(request, value, "", default_scheme); + return; + } else { + /* these fields are returned to client */ + auth_fields_add(request->fields.extra_fields, name, value, 0); + return; + } + + /* add the field unconditionally to extra_fields. this is required if + a) auth cache is used, b) if we're a worker and we'll need to send + this to the main auth process that can store it in the cache, + c) for easily checking :protected fields' existence. */ + auth_fields_add(request->fields.extra_fields, name, value, + AUTH_FIELD_FLAG_HIDDEN); +} + +void auth_request_set_null_field(struct auth_request *request, const char *name) +{ + if (str_begins(name, "userdb_")) { + /* make sure userdb prefetch is used even if all the fields + were returned as NULL. */ + request->userdb_prefetch_set = TRUE; + } +} + +void auth_request_set_field_keyvalue(struct auth_request *request, + const char *field, + const char *default_scheme) +{ + const char *key, *value; + + value = strchr(field, '='); + if (value == NULL) { + key = field; + value = ""; + } else { + key = t_strdup_until(field, value); + value++; + } + auth_request_set_field(request, key, value, default_scheme); +} + +void auth_request_set_fields(struct auth_request *request, + const char *const *fields, + const char *default_scheme) +{ + for (; *fields != NULL; fields++) { + if (**fields == '\0') + continue; + auth_request_set_field_keyvalue(request, *fields, default_scheme); + } +} + +static void auth_request_set_uidgid_file(struct auth_request *request, + const char *path_template) +{ + string_t *path; + struct stat st; + const char *error; + + path = t_str_new(256); + if (auth_request_var_expand(path, path_template, request, + NULL, &error) <= 0) { + e_error(authdb_event(request), + "Failed to expand uidgid_file=%s: %s", path_template, error); + request->userdb_lookup_tempfailed = TRUE; + } else if (stat(str_c(path), &st) < 0) { + e_error(authdb_event(request), + "stat(%s) failed: %m", str_c(path)); + request->userdb_lookup_tempfailed = TRUE; + } else { + auth_fields_add(request->fields.userdb_reply, + "uid", dec2str(st.st_uid), 0); + auth_fields_add(request->fields.userdb_reply, + "gid", dec2str(st.st_gid), 0); + } +} + +static void +auth_request_userdb_import(struct auth_request *request, const char *args) +{ + const char *key, *value, *const *arg; + + for (arg = t_strsplit(args, "\t"); *arg != NULL; arg++) { + value = strchr(*arg, '='); + if (value == NULL) { + key = *arg; + value = ""; + } else { + key = t_strdup_until(*arg, value); + value++; + } + auth_request_set_userdb_field(request, key, value); + } +} + +void auth_request_set_userdb_field(struct auth_request *request, + const char *name, const char *value) +{ + size_t name_len = strlen(name); + uid_t uid; + gid_t gid; + + i_assert(value != NULL); + + if (name_len > 10 && strcmp(name+name_len-10, ":protected") == 0) { + /* set this field only if it hasn't been set before */ + name = t_strndup(name, name_len-10); + if (auth_fields_exists(request->fields.userdb_reply, name)) + return; + } else if (name_len > 7 && strcmp(name+name_len-7, ":remove") == 0) { + /* remove this field entirely */ + name = t_strndup(name, name_len-7); + auth_fields_remove(request->fields.userdb_reply, name); + return; + } + + if (strcmp(name, "uid") == 0) { + uid = userdb_parse_uid(request, value); + if (uid == (uid_t)-1) { + request->userdb_lookup_tempfailed = TRUE; + return; + } + value = dec2str(uid); + } else if (strcmp(name, "gid") == 0) { + gid = userdb_parse_gid(request, value); + if (gid == (gid_t)-1) { + request->userdb_lookup_tempfailed = TRUE; + return; + } + value = dec2str(gid); + } else if (strcmp(name, "tempfail") == 0) { + request->userdb_lookup_tempfailed = TRUE; + return; + } else if (auth_request_try_update_username(request, name, value)) { + return; + } else if (strcmp(name, "uidgid_file") == 0) { + auth_request_set_uidgid_file(request, value); + return; + } else if (strcmp(name, "userdb_import") == 0) { + auth_request_userdb_import(request, value); + return; + } else if (strcmp(name, "system_user") == 0) { + /* FIXME: the system_user is for backwards compatibility */ + static bool warned = FALSE; + if (!warned) { + e_warning(authdb_event(request), + "Replace system_user with system_groups_user"); + warned = TRUE; + } + name = "system_groups_user"; + } else if (strcmp(name, AUTH_REQUEST_USER_KEY_IGNORE) == 0) { + return; + } + + auth_fields_add(request->fields.userdb_reply, name, value, 0); +} + +void auth_request_set_userdb_field_values(struct auth_request *request, + const char *name, + const char *const *values) +{ + if (*values == NULL) + return; + + if (strcmp(name, "gid") == 0) { + /* convert gids to comma separated list */ + string_t *value; + gid_t gid; + + value = t_str_new(128); + for (; *values != NULL; values++) { + gid = userdb_parse_gid(request, *values); + if (gid == (gid_t)-1) { + request->userdb_lookup_tempfailed = TRUE; + return; + } + + if (str_len(value) > 0) + str_append_c(value, ','); + str_append(value, dec2str(gid)); + } + auth_fields_add(request->fields.userdb_reply, name, str_c(value), 0); + } else { + /* add only one */ + if (values[1] != NULL) { + e_warning(authdb_event(request), + "Multiple values found for '%s', " + "using value '%s'", name, *values); + } + auth_request_set_userdb_field(request, name, *values); + } +} + +static bool auth_request_proxy_is_self(struct auth_request *request) +{ + const char *port = NULL; + + /* check if the port is the same */ + port = auth_fields_find(request->fields.extra_fields, "port"); + if (port != NULL && !str_uint_equals(port, request->fields.local_port)) + return FALSE; + /* don't check destuser. in some systems destuser is intentionally + changed to proxied connections, but that shouldn't affect the + proxying decision. + + it's unlikely any systems would actually want to proxy a connection + to itself only to change the username, since it can already be done + without proxying by changing the "user" field. */ + return TRUE; +} + +static bool +auth_request_proxy_ip_is_self(struct auth_request *request, + const struct ip_addr *ip) +{ + unsigned int i; + + if (net_ip_compare(ip, &request->fields.real_local_ip)) + return TRUE; + + for (i = 0; request->set->proxy_self_ips[i].family != 0; i++) { + if (net_ip_compare(ip, &request->set->proxy_self_ips[i])) + return TRUE; + } + return FALSE; +} + +static void +auth_request_proxy_finish_ip(struct auth_request *request, + bool proxy_host_is_self) +{ + const struct auth_request_fields *fields = &request->fields; + + if (!auth_fields_exists(fields->extra_fields, "proxy_maybe")) { + /* proxying */ + } else if (!proxy_host_is_self || + !auth_request_proxy_is_self(request)) { + /* proxy destination isn't ourself - proxy */ + auth_fields_remove(fields->extra_fields, "proxy_maybe"); + auth_fields_add(fields->extra_fields, "proxy", NULL, 0); + } else { + /* proxying to ourself - log in without proxying by dropping + all the proxying fields. */ + bool proxy_always = auth_fields_exists(fields->extra_fields, + "proxy_always"); + + auth_request_proxy_finish_failure(request); + if (proxy_always) { + /* setup where "self" refers to the local director + cluster, while "non-self" refers to remote clusters. + + we've matched self here, so add proxy field and + let director fill the host. */ + auth_fields_add(request->fields.extra_fields, + "proxy", NULL, 0); + } + } +} + +static void +auth_request_proxy_dns_callback(const struct dns_lookup_result *result, + struct auth_request_proxy_dns_lookup_ctx *ctx) +{ + struct auth_request *request = ctx->request; + const char *host; + unsigned int i; + bool proxy_host_is_self; + + request->dns_lookup_ctx = NULL; + ctx->dns_lookup = NULL; + + host = auth_fields_find(request->fields.extra_fields, "host"); + i_assert(host != NULL); + + if (result->ret != 0) { + auth_request_log_error(request, AUTH_SUBSYS_PROXY, + "DNS lookup for %s failed: %s", host, result->error); + request->internal_failure = TRUE; + auth_request_proxy_finish_failure(request); + } else { + if (result->msecs > AUTH_DNS_WARN_MSECS) { + auth_request_log_warning(request, AUTH_SUBSYS_PROXY, + "DNS lookup for %s took %u.%03u s", + host, result->msecs/1000, result->msecs % 1000); + } + auth_fields_add(request->fields.extra_fields, "hostip", + net_ip2addr(&result->ips[0]), 0); + proxy_host_is_self = FALSE; + for (i = 0; i < result->ips_count; i++) { + if (auth_request_proxy_ip_is_self(request, + &result->ips[i])) { + proxy_host_is_self = TRUE; + break; + } + } + auth_request_proxy_finish_ip(request, proxy_host_is_self); + } + if (ctx->callback != NULL) + ctx->callback(result->ret == 0, request); + auth_request_unref(&request); +} + +static int auth_request_proxy_host_lookup(struct auth_request *request, + const char *host, + auth_request_proxy_cb_t *callback) +{ + struct auth_request_proxy_dns_lookup_ctx *ctx; + struct dns_lookup_settings dns_set; + const char *value; + unsigned int secs; + + /* need to do dns lookup for the host */ + i_zero(&dns_set); + dns_set.dns_client_socket_path = AUTH_DNS_SOCKET_PATH; + dns_set.timeout_msecs = AUTH_DNS_DEFAULT_TIMEOUT_MSECS; + dns_set.event_parent = request->event; + value = auth_fields_find(request->fields.extra_fields, "proxy_timeout"); + if (value != NULL) { + if (str_to_uint(value, &secs) < 0) { + auth_request_log_error(request, AUTH_SUBSYS_PROXY, + "Invalid proxy_timeout value: %s", value); + } else { + dns_set.timeout_msecs = secs*1000; + } + } + + ctx = p_new(request->pool, struct auth_request_proxy_dns_lookup_ctx, 1); + ctx->request = request; + auth_request_ref(request); + request->dns_lookup_ctx = ctx; + + if (dns_lookup(host, &dns_set, auth_request_proxy_dns_callback, ctx, + &ctx->dns_lookup) < 0) { + /* failed early */ + return -1; + } + ctx->callback = callback; + return 0; +} + +int auth_request_proxy_finish(struct auth_request *request, + auth_request_proxy_cb_t *callback) +{ + const char *host, *hostip; + struct ip_addr ip; + bool proxy_host_is_self; + + if (request->auth_only) + return 1; + if (!auth_fields_exists(request->fields.extra_fields, "proxy") && + !auth_fields_exists(request->fields.extra_fields, "proxy_maybe")) + return 1; + + host = auth_fields_find(request->fields.extra_fields, "host"); + if (host == NULL) { + /* director can set the host. give it access to lip and lport + so it can also perform proxy_maybe internally */ + proxy_host_is_self = FALSE; + if (request->fields.local_ip.family != 0) { + auth_fields_add(request->fields.extra_fields, "lip", + net_ip2addr(&request->fields.local_ip), 0); + } + if (request->fields.local_port != 0) { + auth_fields_add(request->fields.extra_fields, "lport", + dec2str(request->fields.local_port), 0); + } + } else if (net_addr2ip(host, &ip) == 0) { + proxy_host_is_self = + auth_request_proxy_ip_is_self(request, &ip); + } else { + hostip = auth_fields_find(request->fields.extra_fields, "hostip"); + if (hostip != NULL && net_addr2ip(hostip, &ip) < 0) { + auth_request_log_error(request, AUTH_SUBSYS_PROXY, + "Invalid hostip in passdb: %s", hostip); + return -1; + } + if (hostip == NULL) { + /* asynchronous host lookup */ + return auth_request_proxy_host_lookup(request, host, callback); + } + proxy_host_is_self = + auth_request_proxy_ip_is_self(request, &ip); + } + + auth_request_proxy_finish_ip(request, proxy_host_is_self); + return 1; +} + +void auth_request_proxy_finish_failure(struct auth_request *request) +{ + /* drop all proxying fields */ + auth_fields_remove(request->fields.extra_fields, "proxy"); + auth_fields_remove(request->fields.extra_fields, "proxy_maybe"); + auth_fields_remove(request->fields.extra_fields, "proxy_always"); + auth_fields_remove(request->fields.extra_fields, "host"); + auth_fields_remove(request->fields.extra_fields, "port"); + auth_fields_remove(request->fields.extra_fields, "destuser"); +} + +static void log_password_failure(struct auth_request *request, + const char *plain_password, + const char *crypted_password, + const char *scheme, + const struct password_generate_params *params, + const char *subsystem) +{ + struct event *event = get_request_event(request, subsystem); + static bool scheme_ok = FALSE; + string_t *str = t_str_new(256); + const char *working_scheme; + + str_printfa(str, "%s(%s) != '%s'", scheme, + plain_password, crypted_password); + + if (!scheme_ok) { + /* perhaps the scheme is wrong - see if we can find + a working one */ + working_scheme = password_scheme_detect(plain_password, + crypted_password, params); + if (working_scheme != NULL) { + str_printfa(str, ", try %s scheme instead", + working_scheme); + } + } + + e_debug(event, "%s", str_c(str)); +} + +static void +auth_request_append_password(struct auth_request *request, string_t *str) +{ + const char *p, *log_type = request->set->verbose_passwords; + unsigned int max_len = 1024; + + if (request->mech_password == NULL) + return; + + p = strchr(log_type, ':'); + if (p != NULL) { + if (str_to_uint(p+1, &max_len) < 0) + i_unreached(); + log_type = t_strdup_until(log_type, p); + } + + if (strcmp(log_type, "plain") == 0) { + str_printfa(str, "(given password: %s)", + t_strndup(request->mech_password, max_len)); + } else if (strcmp(log_type, "sha1") == 0) { + unsigned char sha1[SHA1_RESULTLEN]; + + sha1_get_digest(request->mech_password, + strlen(request->mech_password), sha1); + str_printfa(str, "(SHA1 of given password: %s)", + t_strndup(binary_to_hex(sha1, sizeof(sha1)), + max_len)); + } else { + i_unreached(); + } +} + +void auth_request_log_password_mismatch(struct auth_request *request, + const char *subsystem) +{ + auth_request_log_login_failure(request, subsystem, AUTH_LOG_MSG_PASSWORD_MISMATCH); +} + +void auth_request_log_unknown_user(struct auth_request *request, + const char *subsystem) +{ + auth_request_log_login_failure(request, subsystem, "unknown user"); +} + +void auth_request_log_login_failure(struct auth_request *request, + const char *subsystem, + const char *message) +{ + struct event *event = get_request_event(request, subsystem); + string_t *str; + + if (strcmp(request->set->verbose_passwords, "no") == 0) { + e_info(event, "%s", message); + return; + } + + /* make sure this gets logged */ + enum log_type orig_level = event_get_min_log_level(event); + event_set_min_log_level(event, LOG_TYPE_INFO); + + str = t_str_new(128); + str_append(str, message); + str_append(str, " "); + + auth_request_append_password(request, str); + + if (request->userdb_lookup) { + if (request->userdb->next != NULL) + str_append(str, " - trying the next userdb"); + } else { + if (request->passdb->next != NULL) + str_append(str, " - trying the next passdb"); + } + e_info(event, "%s", str_c(str)); + event_set_min_log_level(event, orig_level); +} + +int auth_request_password_verify(struct auth_request *request, + const char *plain_password, + const char *crypted_password, + const char *scheme, const char *subsystem) +{ + return auth_request_password_verify_log(request, plain_password, + crypted_password, scheme, subsystem, TRUE); +} + +int auth_request_password_verify_log(struct auth_request *request, + const char *plain_password, + const char *crypted_password, + const char *scheme, const char *subsystem, + bool log_password_mismatch) +{ + const unsigned char *raw_password; + size_t raw_password_size; + const char *error; + int ret; + struct password_generate_params gen_params = { + .user = request->fields.original_username, + .rounds = 0 + }; + + if (request->fields.skip_password_check) { + /* passdb continue* rule after a successful authentication */ + return 1; + } + + if (request->passdb->set->deny) { + /* this is a deny database, we don't care about the password */ + return 0; + } + + if (auth_fields_exists(request->fields.extra_fields, "nopassword")) { + auth_request_log_debug(request, subsystem, + "Allowing any password"); + return 1; + } + + ret = password_decode(crypted_password, scheme, + &raw_password, &raw_password_size, &error); + if (ret <= 0) { + if (ret < 0) { + auth_request_log_error(request, subsystem, + "Password data is not valid for scheme %s: %s", + scheme, error); + } else { + auth_request_log_error(request, subsystem, + "Unknown scheme %s", scheme); + } + return -1; + } + + /* Use original_username since it may be important for some + password schemes (eg. digest-md5). Otherwise the username is used + only for logging purposes. */ + ret = password_verify(plain_password, &gen_params, + scheme, raw_password, raw_password_size, &error); + if (ret < 0) { + const char *password_str = request->set->debug_passwords ? + t_strdup_printf(" '%s'", crypted_password) : ""; + auth_request_log_error(request, subsystem, + "Invalid password%s in passdb: %s", + password_str, error); + } else if (ret == 0) { + if (log_password_mismatch) + auth_request_log_password_mismatch(request, subsystem); + } + if (ret <= 0 && request->set->debug_passwords) T_BEGIN { + log_password_failure(request, plain_password, + crypted_password, scheme, + &gen_params, + subsystem); + } T_END; + return ret; +} + +enum passdb_result auth_request_password_missing(struct auth_request *request) +{ + if (request->fields.skip_password_check) { + /* This passdb wasn't used for authentication */ + return PASSDB_RESULT_OK; + } + e_info(authdb_event(request), + "No password returned (and no nopassword)"); + return PASSDB_RESULT_PASSWORD_MISMATCH; +} + +void auth_request_get_log_prefix(string_t *str, struct auth_request *auth_request, + const char *subsystem) +{ + const char *name; + + if (subsystem == AUTH_SUBSYS_DB) { + if (!auth_request->userdb_lookup) { + i_assert(auth_request->passdb != NULL); + name = auth_request->passdb->set->name[0] != '\0' ? + auth_request->passdb->set->name : + auth_request->passdb->passdb->iface.name; + } else { + i_assert(auth_request->userdb != NULL); + name = auth_request->userdb->set->name[0] != '\0' ? + auth_request->userdb->set->name : + auth_request->userdb->userdb->iface->name; + } + } else if (subsystem == AUTH_SUBSYS_MECH) { + i_assert(auth_request->mech != NULL); + name = t_str_lcase(auth_request->mech->mech_name); + } else { + name = subsystem; + } + str_append(str, name); + str_append_c(str, '('); + get_log_identifier(str, auth_request); + str_append(str, "): "); +} + +#define MAX_LOG_USERNAME_LEN 64 +static void get_log_identifier(string_t *str, struct auth_request *auth_request) +{ + const char *ip; + + if (auth_request->fields.user == NULL) + str_append(str, "?"); + else + str_sanitize_append(str, auth_request->fields.user, + MAX_LOG_USERNAME_LEN); + + ip = net_ip2addr(&auth_request->fields.remote_ip); + if (ip[0] != '\0') { + str_append_c(str, ','); + str_append(str, ip); + } + if (auth_request->fields.requested_login_user != NULL) + str_append(str, ",master"); + if (auth_request->fields.session_id != NULL) + str_printfa(str, ",<%s>", auth_request->fields.session_id); +} + +void auth_request_log_debug(struct auth_request *auth_request, + const char *subsystem, + const char *format, ...) +{ + struct event *event = get_request_event(auth_request, subsystem); + va_list va; + + va_start(va, format); + T_BEGIN { + string_t *str = t_str_new(128); + str_vprintfa(str, format, va); + e_debug(event, "%s", str_c(str)); + } T_END; + va_end(va); +} + +void auth_request_log_info(struct auth_request *auth_request, + const char *subsystem, + const char *format, ...) +{ + struct event *event = get_request_event(auth_request, subsystem); + va_list va; + + va_start(va, format); + T_BEGIN { + string_t *str = t_str_new(128); + str_vprintfa(str, format, va); + e_info(event, "%s", str_c(str)); + } T_END; + va_end(va); +} + +void auth_request_log_warning(struct auth_request *auth_request, + const char *subsystem, + const char *format, ...) +{ + struct event *event = get_request_event(auth_request, subsystem); + va_list va; + + va_start(va, format); + T_BEGIN { + string_t *str = t_str_new(128); + str_vprintfa(str, format, va); + e_warning(event, "%s", str_c(str)); + } T_END; + va_end(va); +} + +void auth_request_log_error(struct auth_request *auth_request, + const char *subsystem, + const char *format, ...) +{ + struct event *event = get_request_event(auth_request, subsystem); + va_list va; + + va_start(va, format); + T_BEGIN { + string_t *str = t_str_new(128); + str_vprintfa(str, format, va); + e_error(event, "%s", str_c(str)); + } T_END; + va_end(va); +} + +void auth_request_refresh_last_access(struct auth_request *request) +{ + request->last_access = ioloop_time; + if (request->to_abort != NULL) + timeout_reset(request->to_abort); +} diff --git a/src/auth/auth-request.h b/src/auth/auth-request.h new file mode 100644 index 0000000..6a458d9 --- /dev/null +++ b/src/auth/auth-request.h @@ -0,0 +1,394 @@ +#ifndef AUTH_REQUEST_H +#define AUTH_REQUEST_H + +#ifndef AUTH_REQUEST_FIELDS_CONST +# define AUTH_REQUEST_FIELDS_CONST const +#endif + +#include "array.h" +#include "net.h" +#include "var-expand.h" +#include "mech.h" +#include "userdb.h" +#include "passdb.h" +#include "auth-request-var-expand.h" +#include "password-scheme.h" + +#define AUTH_REQUEST_USER_KEY_IGNORE " " + +struct auth_client_connection; + +enum auth_request_state { + AUTH_REQUEST_STATE_NEW, + AUTH_REQUEST_STATE_PASSDB, + AUTH_REQUEST_STATE_MECH_CONTINUE, + AUTH_REQUEST_STATE_FINISHED, + AUTH_REQUEST_STATE_USERDB, + + AUTH_REQUEST_STATE_MAX +}; + +enum auth_request_secured { + AUTH_REQUEST_SECURED_NONE, + AUTH_REQUEST_SECURED, + AUTH_REQUEST_SECURED_TLS, +}; + +enum auth_request_cache_result { + AUTH_REQUEST_CACHE_NONE, + AUTH_REQUEST_CACHE_MISS, + AUTH_REQUEST_CACHE_HIT, +}; + +/* All auth request fields are exported to auth-worker process. */ +struct auth_request_fields { + /* user contains the user who is being authenticated. + When master user is logging in as someone else, it gets more + complicated. Initially user is set to master's username and the + requested_login_user is set to destination username. After masterdb + has validated user as a valid master user, master_user is set to + user and user is set to requested_login_user. */ + char *user, *requested_login_user, *master_user; + /* original_username contains the username exactly as given by the + client. this is needed at least with DIGEST-MD5 for password + verification. however with master logins the master username has + been dropped from it. */ + const char *original_username; + /* the username after doing all internal translations, but before + being changed by a db lookup */ + const char *translated_username; + /* realm for the request, may be specified by some auth mechanisms */ + const char *realm; + + const char *service, *mech_name, *session_id, *local_name, *client_id; + struct ip_addr local_ip, remote_ip, real_local_ip, real_remote_ip; + in_port_t local_port, remote_port, real_local_port, real_remote_port; + + /* extra_fields are returned in authentication reply. Fields prefixed + with "userdb_" are automatically placed to userdb_reply instead. */ + struct auth_fields *extra_fields; + /* the whole userdb result reply */ + struct auth_fields *userdb_reply; + + /* Credentials from the first successful passdb lookup. These are used + as the final credentials, unless overridden by later passdb + lookups. Note that the requests in auth-worker processes see these + only as 1 byte sized \0 strings. */ + const unsigned char *delayed_credentials; + size_t delayed_credentials_size; + + enum auth_request_secured secured; + + /* Authentication was successfully finished, including policy checks + and such. There may still be some final delay or final SASL + response. */ + bool successful:1; + /* Password was verified successfully by a passdb. The following + passdbs shouldn't attempt to verify the password again. Note that + this differs from passdb_success, which may be set to FALSE due to + the result_* rules. */ + bool skip_password_check:1; + + /* flags received from auth client: */ + bool final_resp_ok:1; + bool no_penalty:1; + bool valid_client_cert:1; + bool cert_username:1; +}; + +struct auth_request { + int refcount; + + pool_t pool; + + struct event *event; + struct event *mech_event; + ARRAY(struct event *) authdb_event; + + enum auth_request_state state; + char *mech_password; /* set if verify_plain() is called */ + char *passdb_password; /* set after password lookup if successful */ + struct auth_request_proxy_dns_lookup_ctx *dns_lookup_ctx; + /* The final result of passdb lookup (delayed due to asynchronous + proxy DNS lookups) */ + enum passdb_result passdb_result; + + const struct mech_module *mech; + const struct auth_settings *set; + struct auth_passdb *passdb; + struct auth_userdb *userdb; + + struct stats *stats; + + /* passdb lookups have a handler, userdb lookups don't */ + struct auth_request_handler *handler; + struct auth_master_connection *master; + + /* FIXME: Remove this once mech-oauth2 correctly does the processing */ + const char *openid_config_url; + + unsigned int connect_uid; + unsigned int client_pid; + unsigned int id; + time_t last_access; + time_t delay_until; + pid_t session_pid; + + /* These are const for most of the code, so they don't try to modify + the fields directly. Only auth-request-fields.c and unit tests have + the fields writable. This way it's more difficult to make them + out-of-sync with events. */ + AUTH_REQUEST_FIELDS_CONST struct auth_request_fields fields; + + struct timeout *to_abort, *to_penalty; + unsigned int policy_penalty; + unsigned int last_penalty; + size_t initial_response_len; + const unsigned char *initial_response; + + union { + verify_plain_callback_t *verify_plain; + lookup_credentials_callback_t *lookup_credentials; + set_credentials_callback_t *set_credentials; + userdb_callback_t *userdb; + } private_callback; + /* Used by passdb's credentials lookup to determine which scheme is + wanted by the caller. For example CRAM-MD5 SASL mechanism wants + CRAM-MD5 scheme for passwords. + + When doing a PASS lookup (without authenticating), this is set to "" + to imply that caller accepts any kind of credentials. After the + credentials lookup is finished, this is set to the scheme that was + actually received. + + Otherwise, this is kept as NULL. */ + const char *wanted_credentials_scheme; + + void *context; + + enum auth_request_cache_result passdb_cache_result; + enum auth_request_cache_result userdb_cache_result; + + /* this is a lookup on auth socket (not login socket). + skip any proxying stuff if enabled. */ + bool auth_only:1; + /* we're doing a userdb lookup now (we may have done passdb lookup + earlier) */ + bool userdb_lookup:1; + /* DIGEST-MD5 kludge */ + bool domain_is_realm:1; + + bool request_auth_token:1; + + /* success/failure states: */ + bool failed:1; /* overrides any other success */ + bool internal_failure:1; + bool passdbs_seen_user_unknown:1; + bool passdbs_seen_internal_failure:1; + bool userdbs_seen_internal_failure:1; + + /* current state: */ + bool handler_pending_reply:1; + bool accept_cont_input:1; + bool prefer_plain_credentials:1; + bool in_delayed_failure_queue:1; + bool removed_from_handler:1; + bool snapshot_have_userdb_prefetch_set:1; + /* username was changed by this passdb/userdb lookup. Used by + auth-workers to determine whether to send back a changed username. */ + bool user_changed_by_lookup:1; + /* each passdb lookup can update the current success-status using the + result_* rules. the authentication succeeds only if this is TRUE + at the end. mechanisms that don't require passdb, but do a passdb + lookup anyway (e.g. GSSAPI) need to set this to TRUE by default. */ + bool passdb_success:1; + /* userdb equivalent of passdb_success */ + bool userdb_success:1; + /* the last userdb lookup failed either due to "tempfail" extra field + or because one of the returned uid/gid fields couldn't be translated + to a number */ + bool userdb_lookup_tempfailed:1; + /* userdb_* fields have been set by the passdb lookup, userdb prefetch + will work. */ + bool userdb_prefetch_set:1; + bool stats_sent:1; + bool policy_refusal:1; + bool policy_processed:1; + + bool event_finished_sent:1; + + /* ... mechanism specific data ... */ +}; + +typedef void auth_request_proxy_cb_t(bool success, struct auth_request *); + +extern unsigned int auth_request_state_count[AUTH_REQUEST_STATE_MAX]; + +extern const char auth_default_subsystems[2]; +#define AUTH_SUBSYS_DB &auth_default_subsystems[0] +#define AUTH_SUBSYS_MECH &auth_default_subsystems[1] + +struct auth_request * +auth_request_new(const struct mech_module *mech, struct event *parent_event); +struct auth_request *auth_request_new_dummy(struct event *parent_event); +void auth_request_init(struct auth_request *request); +struct auth *auth_request_get_auth(struct auth_request *request); + +void auth_request_set_state(struct auth_request *request, + enum auth_request_state state); + +void auth_request_ref(struct auth_request *request); +void auth_request_unref(struct auth_request **request); + +void auth_request_success(struct auth_request *request, + const void *data, size_t data_size); +void auth_request_fail(struct auth_request *request); +void auth_request_internal_failure(struct auth_request *request); + +void auth_request_export(struct auth_request *request, string_t *dest); +bool auth_request_import(struct auth_request *request, + const char *key, const char *value); +bool auth_request_import_info(struct auth_request *request, + const char *key, const char *value); +bool auth_request_import_auth(struct auth_request *request, + const char *key, const char *value); +bool auth_request_import_master(struct auth_request *request, + const char *key, const char *value); + +void auth_request_initial(struct auth_request *request); +void auth_request_continue(struct auth_request *request, + const unsigned char *data, size_t data_size); + +void auth_request_verify_plain(struct auth_request *request, + const char *password, + verify_plain_callback_t *callback); +void auth_request_lookup_credentials(struct auth_request *request, + const char *scheme, + lookup_credentials_callback_t *callback); +void auth_request_lookup_user(struct auth_request *request, + userdb_callback_t *callback); + +bool auth_request_set_username(struct auth_request *request, + const char *username, const char **error_r); +/* Change the username without any translations or checks. */ +void auth_request_set_username_forced(struct auth_request *request, + const char *username); +bool auth_request_set_login_username(struct auth_request *request, + const char *username, + const char **error_r); +/* Change the login username without any translations or checks. */ +void auth_request_set_login_username_forced(struct auth_request *request, + const char *username); +void auth_request_set_realm(struct auth_request *request, const char *realm); +/* Request was fully successfully authenticated, including policy checks etc. */ +void auth_request_set_auth_successful(struct auth_request *request); +/* Password was successfully verified by a passdb. */ +void auth_request_set_password_verified(struct auth_request *request); +/* Save credentials from a successful passdb lookup. */ +void auth_request_set_delayed_credentials(struct auth_request *request, + const unsigned char *credentials, + size_t size); + +void auth_request_set_field(struct auth_request *request, + const char *name, const char *value, + const char *default_scheme) ATTR_NULL(4); +void auth_request_set_null_field(struct auth_request *request, const char *name); +void auth_request_set_field_keyvalue(struct auth_request *request, + const char *field, + const char *default_scheme) ATTR_NULL(3); +void auth_request_set_fields(struct auth_request *request, + const char *const *fields, + const char *default_scheme) ATTR_NULL(3); + +void auth_request_init_userdb_reply(struct auth_request *request, + bool add_default_fields); +void auth_request_set_userdb_field(struct auth_request *request, + const char *name, const char *value); +void auth_request_set_userdb_field_values(struct auth_request *request, + const char *name, + const char *const *values); +/* returns -1 = failed, 0 = callback is called later, 1 = finished */ +int auth_request_proxy_finish(struct auth_request *request, + auth_request_proxy_cb_t *callback); +void auth_request_proxy_finish_failure(struct auth_request *request); + +void auth_request_log_password_mismatch(struct auth_request *request, + const char *subsystem); +int auth_request_password_verify(struct auth_request *request, + const char *plain_password, + const char *crypted_password, + const char *scheme, const char *subsystem); +int auth_request_password_verify_log(struct auth_request *request, + const char *plain_password, + const char *crypted_password, + const char *scheme, const char *subsystem, + bool log_password_mismatch); +enum passdb_result auth_request_password_missing(struct auth_request *request); + +void auth_request_get_log_prefix(string_t *str, struct auth_request *auth_request, + const char *subsystem); + +void auth_request_log_debug(struct auth_request *auth_request, + const char *subsystem, + const char *format, ...) ATTR_FORMAT(3, 4); +void auth_request_log_info(struct auth_request *auth_request, + const char *subsystem, + const char *format, ...) ATTR_FORMAT(3, 4); +void auth_request_log_warning(struct auth_request *auth_request, + const char *subsystem, + const char *format, ...) ATTR_FORMAT(3, 4); +void auth_request_log_error(struct auth_request *auth_request, + const char *subsystem, + const char *format, ...) ATTR_FORMAT(3, 4); +void auth_request_log_unknown_user(struct auth_request *auth_request, + const char *subsystem); + +void auth_request_log_login_failure(struct auth_request *request, + const char *subsystem, + const char *message); +void +auth_request_verify_plain_callback_finish(enum passdb_result result, + struct auth_request *request); +void auth_request_verify_plain_callback(enum passdb_result result, + struct auth_request *request); +void auth_request_lookup_credentials_callback(enum passdb_result result, + const unsigned char *credentials, + size_t size, + struct auth_request *request); +void auth_request_set_credentials(struct auth_request *request, + const char *scheme, const char *data, + set_credentials_callback_t *callback); +void auth_request_userdb_callback(enum userdb_result result, + struct auth_request *request); +void auth_request_default_verify_plain_continue(struct auth_request *request, + verify_plain_callback_t *callback); + +void auth_request_refresh_last_access(struct auth_request *request); +void auth_str_append(string_t *dest, const char *key, const char *value); +bool auth_request_username_accepted(const char *const *filter, const char *username); +struct event_passthrough * +auth_request_finished_event(struct auth_request *request, struct event *event); +void auth_request_log_finished(struct auth_request *request); +void auth_request_master_user_login_finish(struct auth_request *request); +const char *auth_request_get_log_prefix_db(struct auth_request *auth_request); +void auth_request_fields_init(struct auth_request *request); + +void auth_request_passdb_lookup_begin(struct auth_request *request); +void auth_request_passdb_lookup_end(struct auth_request *request, + enum passdb_result result); +void auth_request_userdb_lookup_begin(struct auth_request *request); +void auth_request_userdb_lookup_end(struct auth_request *request, + enum userdb_result result); + +/* Fetches the current authdb event, this is done because + some lookups can recurse into new lookups, requiring new event, + which will be returned here. */ +static inline struct event *authdb_event(const struct auth_request *request) +{ + if (array_count(&request->authdb_event) == 0) + return request->event; + struct event **e = array_back_modifiable(&request->authdb_event); + return *e; +} + +#endif diff --git a/src/auth/auth-settings.c b/src/auth/auth-settings.c new file mode 100644 index 0000000..4268b9a --- /dev/null +++ b/src/auth/auth-settings.c @@ -0,0 +1,553 @@ +/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "array.h" +#include "hash-method.h" +#include "settings-parser.h" +#include "master-service-private.h" +#include "master-service-settings.h" +#include "service-settings.h" +#include "auth-settings.h" + +#include <stddef.h> + +static bool auth_settings_check(void *_set, pool_t pool, const char **error_r); +static bool auth_passdb_settings_check(void *_set, pool_t pool, const char **error_r); +static bool auth_userdb_settings_check(void *_set, pool_t pool, const char **error_r); + +/* <settings checks> */ +static struct file_listener_settings auth_unix_listeners_array[] = { + { "login/login", 0666, "", "" }, + { "token-login/tokenlogin", 0666, "", "" }, + { "auth-login", 0600, "$default_internal_user", "" }, + { "auth-client", 0600, "$default_internal_user", "" }, + { "auth-userdb", 0666, "$default_internal_user", "" }, + { "auth-master", 0600, "", "" } +}; +static struct file_listener_settings *auth_unix_listeners[] = { + &auth_unix_listeners_array[0], + &auth_unix_listeners_array[1], + &auth_unix_listeners_array[2], + &auth_unix_listeners_array[3], + &auth_unix_listeners_array[4], + &auth_unix_listeners_array[5] +}; +static buffer_t auth_unix_listeners_buf = { + { { auth_unix_listeners, sizeof(auth_unix_listeners) } } +}; +/* </settings checks> */ + +struct service_settings auth_service_settings = { + .name = "auth", + .protocol = "", + .type = "", + .executable = "auth", + .user = "$default_internal_user", + .group = "", + .privileged_group = "", + .extra_groups = "", + .chroot = "", + + .drop_priv_before_exec = FALSE, + + .process_min_avail = 0, + .process_limit = 1, + .client_limit = 0, + .service_count = 0, + .idle_kill = 0, + .vsz_limit = UOFF_T_MAX, + + .unix_listeners = { { &auth_unix_listeners_buf, + sizeof(auth_unix_listeners[0]) } }, + .fifo_listeners = ARRAY_INIT, + .inet_listeners = ARRAY_INIT, + + .process_limit_1 = TRUE +}; + +/* <settings checks> */ +static struct file_listener_settings auth_worker_unix_listeners_array[] = { + { "auth-worker", 0600, "$default_internal_user", "" } +}; +static struct file_listener_settings *auth_worker_unix_listeners[] = { + &auth_worker_unix_listeners_array[0] +}; +static buffer_t auth_worker_unix_listeners_buf = { + { { auth_worker_unix_listeners, sizeof(auth_worker_unix_listeners) } } +}; +/* </settings checks> */ + +struct service_settings auth_worker_service_settings = { + .name = "auth-worker", + .protocol = "", + .type = "worker", + .executable = "auth -w", + .user = "", + .group = "", + .privileged_group = "", + .extra_groups = "", + .chroot = "", + + .drop_priv_before_exec = FALSE, + + .process_min_avail = 0, + .process_limit = 0, + .client_limit = 1, + .service_count = 0, + .idle_kill = 0, + .vsz_limit = UOFF_T_MAX, + + .unix_listeners = { { &auth_worker_unix_listeners_buf, + sizeof(auth_worker_unix_listeners[0]) } }, + .fifo_listeners = ARRAY_INIT, + .inet_listeners = ARRAY_INIT +}; + +#undef DEF +#define DEF(type, name) \ + SETTING_DEFINE_STRUCT_##type(#name, name, struct auth_passdb_settings) + +static const struct setting_define auth_passdb_setting_defines[] = { + DEF(STR, name), + DEF(STR, driver), + DEF(STR, args), + DEF(STR, default_fields), + DEF(STR, override_fields), + DEF(STR, mechanisms), + DEF(STR, username_filter), + + DEF(ENUM, skip), + DEF(ENUM, result_success), + DEF(ENUM, result_failure), + DEF(ENUM, result_internalfail), + + DEF(BOOL, deny), + DEF(BOOL, pass), + DEF(BOOL, master), + DEF(ENUM, auth_verbose), + + SETTING_DEFINE_LIST_END +}; + +static const struct auth_passdb_settings auth_passdb_default_settings = { + .name = "", + .driver = "", + .args = "", + .default_fields = "", + .override_fields = "", + .mechanisms = "", + .username_filter = "", + + .skip = "never:authenticated:unauthenticated", + .result_success = "return-ok:return:return-fail:continue:continue-ok:continue-fail", + .result_failure = "continue:return:return-ok:return-fail:continue-ok:continue-fail", + .result_internalfail = "continue:return:return-ok:return-fail:continue-ok:continue-fail", + + .deny = FALSE, + .pass = FALSE, + .master = FALSE, + .auth_verbose = "default:yes:no" +}; + +const struct setting_parser_info auth_passdb_setting_parser_info = { + .defines = auth_passdb_setting_defines, + .defaults = &auth_passdb_default_settings, + + .type_offset = offsetof(struct auth_passdb_settings, name), + .struct_size = sizeof(struct auth_passdb_settings), + + .parent_offset = SIZE_MAX, + .parent = &auth_setting_parser_info, + + .check_func = auth_passdb_settings_check +}; + +#undef DEF +#define DEF(type, name) \ + SETTING_DEFINE_STRUCT_##type(#name, name, struct auth_userdb_settings) + +static const struct setting_define auth_userdb_setting_defines[] = { + DEF(STR, name), + DEF(STR, driver), + DEF(STR, args), + DEF(STR, default_fields), + DEF(STR, override_fields), + + DEF(ENUM, skip), + DEF(ENUM, result_success), + DEF(ENUM, result_failure), + DEF(ENUM, result_internalfail), + + DEF(ENUM, auth_verbose), + + SETTING_DEFINE_LIST_END +}; + +static const struct auth_userdb_settings auth_userdb_default_settings = { + /* NOTE: when adding fields, update also auth.c:userdb_dummy_set */ + .name = "", + .driver = "", + .args = "", + .default_fields = "", + .override_fields = "", + + .skip = "never:found:notfound", + .result_success = "return-ok:return:return-fail:continue:continue-ok:continue-fail", + .result_failure = "continue:return:return-ok:return-fail:continue-ok:continue-fail", + .result_internalfail = "continue:return:return-ok:return-fail:continue-ok:continue-fail", + + .auth_verbose = "default:yes:no" +}; + +const struct setting_parser_info auth_userdb_setting_parser_info = { + .defines = auth_userdb_setting_defines, + .defaults = &auth_userdb_default_settings, + + .type_offset = offsetof(struct auth_userdb_settings, name), + .struct_size = sizeof(struct auth_userdb_settings), + + .parent_offset = SIZE_MAX, + .parent = &auth_setting_parser_info, + + .check_func = auth_userdb_settings_check +}; + +/* we're kind of kludging here to avoid "auth_" prefix in the struct fields */ +#undef DEF +#undef DEF_NOPREFIX +#undef DEFLIST +#define DEF(type, name) \ + SETTING_DEFINE_STRUCT_##type("auth_"#name, name, struct auth_settings) +#define DEF_NOPREFIX(type, name) \ + SETTING_DEFINE_STRUCT_##type(#name, name, struct auth_settings) +#define DEFLIST(field, name, defines) \ + { .type = SET_DEFLIST, .key = name, \ + .offset = offsetof(struct auth_settings, field), \ + .list_info = defines } + +static const struct setting_define auth_setting_defines[] = { + DEF(STR, mechanisms), + DEF(STR, realms), + DEF(STR, default_realm), + DEF(SIZE, cache_size), + DEF(TIME, cache_ttl), + DEF(TIME, cache_negative_ttl), + DEF(BOOL, cache_verify_password_with_worker), + DEF(STR, username_chars), + DEF(STR, username_translation), + DEF(STR, username_format), + DEF(STR, master_user_separator), + DEF(STR, anonymous_username), + DEF(STR, krb5_keytab), + DEF(STR, gssapi_hostname), + DEF(STR, winbind_helper_path), + DEF(STR, proxy_self), + DEF(TIME, failure_delay), + + DEF(STR, policy_server_url), + DEF(STR, policy_server_api_header), + DEF(UINT, policy_server_timeout_msecs), + DEF(STR, policy_hash_mech), + DEF(STR, policy_hash_nonce), + DEF(STR, policy_request_attributes), + DEF(BOOL, policy_reject_on_fail), + DEF(BOOL, policy_check_before_auth), + DEF(BOOL, policy_check_after_auth), + DEF(BOOL, policy_report_after_auth), + DEF(BOOL, policy_log_only), + DEF(UINT, policy_hash_truncate), + + DEF(BOOL, stats), + DEF(BOOL, verbose), + DEF(BOOL, debug), + DEF(BOOL, debug_passwords), + DEF(STR, verbose_passwords), + DEF(BOOL, ssl_require_client_cert), + DEF(BOOL, ssl_username_from_cert), + DEF(BOOL, use_winbind), + + DEF(UINT, worker_max_count), + + DEFLIST(passdbs, "passdb", &auth_passdb_setting_parser_info), + DEFLIST(userdbs, "userdb", &auth_userdb_setting_parser_info), + + DEF_NOPREFIX(STR, base_dir), + DEF_NOPREFIX(BOOL, verbose_proctitle), + DEF_NOPREFIX(UINT, first_valid_uid), + DEF_NOPREFIX(UINT, last_valid_uid), + DEF_NOPREFIX(UINT, first_valid_gid), + DEF_NOPREFIX(UINT, last_valid_gid), + + DEF_NOPREFIX(STR, ssl_client_ca_dir), + DEF_NOPREFIX(STR, ssl_client_ca_file), + + SETTING_DEFINE_LIST_END +}; + +static const struct auth_settings auth_default_settings = { + .mechanisms = "plain", + .realms = "", + .default_realm = "", + .cache_size = 0, + .cache_ttl = 60*60, + .cache_negative_ttl = 60*60, + .cache_verify_password_with_worker = FALSE, + .username_chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890.-_@", + .username_translation = "", + .username_format = "%Lu", + .master_user_separator = "", + .anonymous_username = "anonymous", + .krb5_keytab = "", + .gssapi_hostname = "", + .winbind_helper_path = "/usr/bin/ntlm_auth", + .proxy_self = "", + .failure_delay = 2, + + .policy_server_url = "", + .policy_server_api_header = "", + .policy_server_timeout_msecs = 2000, + .policy_hash_mech = "sha256", + .policy_hash_nonce = "", + .policy_request_attributes = "login=%{requested_username} pwhash=%{hashed_password} remote=%{rip} device_id=%{client_id} protocol=%s session_id=%{session}", + .policy_reject_on_fail = FALSE, + .policy_check_before_auth = TRUE, + .policy_check_after_auth = TRUE, + .policy_report_after_auth = TRUE, + .policy_log_only = FALSE, + .policy_hash_truncate = 12, + + .stats = FALSE, + .verbose = FALSE, + .debug = FALSE, + .debug_passwords = FALSE, + .verbose_passwords = "no", + .ssl_require_client_cert = FALSE, + .ssl_username_from_cert = FALSE, + .ssl_client_ca_dir = "", + .ssl_client_ca_file = "", + + .use_winbind = FALSE, + + .worker_max_count = 30, + + .passdbs = ARRAY_INIT, + .userdbs = ARRAY_INIT, + + .base_dir = PKG_RUNDIR, + .verbose_proctitle = FALSE, + .first_valid_uid = 500, + .last_valid_uid = 0, + .first_valid_gid = 1, + .last_valid_gid = 0, +}; + +const struct setting_parser_info auth_setting_parser_info = { + .module_name = "auth", + .defines = auth_setting_defines, + .defaults = &auth_default_settings, + + .type_offset = SIZE_MAX, + .struct_size = sizeof(struct auth_settings), + + .parent_offset = SIZE_MAX, + + .check_func = auth_settings_check +}; + +/* <settings checks> */ +static bool +auth_settings_set_self_ips(struct auth_settings *set, pool_t pool, + const char **error_r) +{ + const char *const *tmp; + ARRAY(struct ip_addr) ips_array; + struct ip_addr *ips; + unsigned int ips_count; + int ret; + + if (*set->proxy_self == '\0') { + set->proxy_self_ips = p_new(pool, struct ip_addr, 1); + return TRUE; + } + + p_array_init(&ips_array, pool, 4); + tmp = t_strsplit_spaces(set->proxy_self, " "); + for (; *tmp != NULL; tmp++) { + ret = net_gethostbyname(*tmp, &ips, &ips_count); + if (ret != 0) { + *error_r = t_strdup_printf("auth_proxy_self_ips: " + "gethostbyname(%s) failed: %s", + *tmp, net_gethosterror(ret)); + } + array_append(&ips_array, ips, ips_count); + } + array_append_zero(&ips_array); + set->proxy_self_ips = array_front(&ips_array); + return TRUE; +} + +static bool +auth_verify_verbose_password(struct auth_settings *set, + const char **error_r) +{ + const char *p, *value = set->verbose_passwords; + unsigned int num; + + p = strchr(value, ':'); + if (p != NULL) { + if (str_to_uint(p+1, &num) < 0 || num == 0) { + *error_r = t_strdup_printf("auth_verbose_passwords: " + "Invalid truncation number: '%s'", p+1); + return FALSE; + } + value = t_strdup_until(value, p); + } + if (strcmp(value, "no") == 0) + return TRUE; + else if (strcmp(value, "plain") == 0) + return TRUE; + else if (strcmp(value, "sha1") == 0) + return TRUE; + else if (strcmp(value, "yes") == 0) { + /* just use it as alias for "plain" */ + set->verbose_passwords = "plain"; + return TRUE; + } else { + *error_r = "auth_verbose_passwords: Invalid value"; + return FALSE; + } +} + +static bool auth_settings_check(void *_set, pool_t pool, + const char **error_r) +{ + struct auth_settings *set = _set; + const char *p; + + if (set->debug_passwords) + set->debug = TRUE; + if (set->debug) + set->verbose = TRUE; + + if (set->worker_max_count == 0) { + *error_r = "auth_worker_max_count must be above zero"; + return FALSE; + } + + if (set->cache_size > 0 && set->cache_size < 1024) { + /* probably a configuration error. + older versions used megabyte numbers */ + *error_r = t_strdup_printf("auth_cache_size value is too small " + "(%"PRIuUOFF_T" bytes)", + set->cache_size); + return FALSE; + } + + if (!auth_verify_verbose_password(set, error_r)) + return FALSE; + + if (*set->username_chars == '\0') { + /* all chars are allowed */ + memset(set->username_chars_map, 1, + sizeof(set->username_chars_map)); + } else { + for (p = set->username_chars; *p != '\0'; p++) + set->username_chars_map[(int)(uint8_t)*p] = 1; + } + + if (*set->username_translation != '\0') { + p = set->username_translation; + for (; *p != '\0' && p[1] != '\0'; p += 2) + set->username_translation_map[(int)(uint8_t)*p] = p[1]; + } + set->realms_arr = + (const char *const *)p_strsplit_spaces(pool, set->realms, " "); + + if (*set->policy_server_url != '\0') { + if (*set->policy_hash_nonce == '\0') { + + *error_r = "auth_policy_hash_nonce must be set when policy server is used"; + return FALSE; + } + const struct hash_method *digest = hash_method_lookup(set->policy_hash_mech); + if (digest == NULL) { + *error_r = "invalid auth_policy_hash_mech given"; + return FALSE; + } + if (set->policy_hash_truncate > 0 && set->policy_hash_truncate >= digest->digest_size*8) { + *error_r = t_strdup_printf("policy_hash_truncate is not smaller than digest size (%u >= %u)", + set->policy_hash_truncate, + digest->digest_size*8); + return FALSE; + } + } + + if (!auth_settings_set_self_ips(set, pool, error_r)) + return FALSE; + return TRUE; +} + +static bool +auth_passdb_settings_check(void *_set, pool_t pool ATTR_UNUSED, + const char **error_r) +{ + struct auth_passdb_settings *set = _set; + + if (set->driver == NULL || *set->driver == '\0') { + *error_r = "passdb is missing driver"; + return FALSE; + } + if (set->pass && strcmp(set->result_success, "return-ok") != 0) { + *error_r = "Obsolete pass=yes setting mixed with non-default result_success"; + return FALSE; + } + return TRUE; +} + +static bool +auth_userdb_settings_check(void *_set, pool_t pool ATTR_UNUSED, + const char **error_r) +{ + struct auth_userdb_settings *set = _set; + + if (set->driver == NULL || *set->driver == '\0') { + *error_r = "userdb is missing driver"; + return FALSE; + } + return TRUE; +} +/* </settings checks> */ + +struct auth_settings *global_auth_settings; + +struct auth_settings * +auth_settings_read(const char *service, pool_t pool, + struct master_service_settings_output *output_r) +{ + static const struct setting_parser_info *set_roots[] = { + &auth_setting_parser_info, + NULL + }; + struct master_service_settings_input input; + struct setting_parser_context *set_parser; + const char *error; + void **sets; + + i_zero(&input); + input.roots = set_roots; + input.module = "auth"; + input.service = service; + if (master_service_settings_read(master_service, &input, + output_r, &error) < 0) + i_fatal("Error reading configuration: %s", error); + + pool_ref(pool); + set_parser = settings_parser_dup(master_service->set_parser, pool); + if (!settings_parser_check(set_parser, pool, &error)) + i_unreached(); + + sets = master_service_settings_parser_get_others(master_service, + set_parser); + settings_parser_deinit(&set_parser); + return sets[0]; +} diff --git a/src/auth/auth-settings.h b/src/auth/auth-settings.h new file mode 100644 index 0000000..10ac379 --- /dev/null +++ b/src/auth/auth-settings.h @@ -0,0 +1,110 @@ +#ifndef AUTH_SETTINGS_H +#define AUTH_SETTINGS_H + +struct master_service; +struct master_service_settings_output; + +struct auth_passdb_settings { + const char *name; + const char *driver; + const char *args; + const char *default_fields; + const char *override_fields; + const char *mechanisms; + const char *username_filter; + + const char *skip; + const char *result_success; + const char *result_failure; + const char *result_internalfail; + bool deny; + bool pass; /* deprecated, use result_success=continue instead */ + bool master; + const char *auth_verbose; +}; + +struct auth_userdb_settings { + const char *name; + const char *driver; + const char *args; + const char *default_fields; + const char *override_fields; + + const char *skip; + const char *result_success; + const char *result_failure; + const char *result_internalfail; + const char *auth_verbose; +}; + +struct auth_settings { + const char *mechanisms; + const char *realms; + const char *default_realm; + uoff_t cache_size; + unsigned int cache_ttl; + unsigned int cache_negative_ttl; + bool cache_verify_password_with_worker; + const char *username_chars; + const char *username_translation; + const char *username_format; + const char *master_user_separator; + const char *anonymous_username; + const char *krb5_keytab; + const char *gssapi_hostname; + const char *winbind_helper_path; + const char *proxy_self; + unsigned int failure_delay; + + const char *policy_server_url; + const char *policy_server_api_header; + unsigned int policy_server_timeout_msecs; + const char *policy_hash_mech; + const char *policy_hash_nonce; + const char *policy_request_attributes; + bool policy_reject_on_fail; + bool policy_check_before_auth; + bool policy_check_after_auth; + bool policy_report_after_auth; + bool policy_log_only; + unsigned int policy_hash_truncate; + + bool stats; + bool verbose, debug, debug_passwords; + const char *verbose_passwords; + bool ssl_require_client_cert; + bool ssl_username_from_cert; + bool use_winbind; + + unsigned int worker_max_count; + + /* settings that don't have auth_ prefix: */ + ARRAY(struct auth_passdb_settings *) passdbs; + ARRAY(struct auth_userdb_settings *) userdbs; + + const char *base_dir; + const char *ssl_client_ca_dir; + const char *ssl_client_ca_file; + + bool verbose_proctitle; + unsigned int first_valid_uid; + unsigned int last_valid_uid; + unsigned int first_valid_gid; + unsigned int last_valid_gid; + + /* generated: */ + char username_chars_map[256]; + char username_translation_map[256]; + const char *const *realms_arr; + const struct ip_addr *proxy_self_ips; +}; + +extern const struct setting_parser_info auth_setting_parser_info; +extern struct auth_settings *global_auth_settings; + +struct auth_settings * +auth_settings_read(const char *service, pool_t pool, + struct master_service_settings_output *output_r) + ATTR_NULL(1); + +#endif diff --git a/src/auth/auth-stats.c b/src/auth/auth-stats.c new file mode 100644 index 0000000..71264a4 --- /dev/null +++ b/src/auth/auth-stats.c @@ -0,0 +1,116 @@ +/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "buffer.h" +#include "stats.h" +#include "stats-parser.h" +#include "auth-stats.h" + +static struct stats_parser_field auth_stats_fields[] = { +#define E(parsename, name, type) { parsename, offsetof(struct auth_stats, name), sizeof(((struct auth_stats *)0)->name), type } +#define EN(parsename, name) E(parsename, name, STATS_PARSER_TYPE_UINT) + EN("auth_successes", auth_success_count), + EN("auth_master_successes", auth_master_success_count), + EN("auth_failures", auth_failure_count), + EN("auth_db_tempfails", auth_db_tempfail_count), + + EN("auth_cache_hits", auth_cache_hit_count), + EN("auth_cache_misses", auth_cache_miss_count) +}; + +static size_t auth_stats_alloc_size(void) +{ + return sizeof(struct auth_stats); +} + +static unsigned int auth_stats_field_count(void) +{ + return N_ELEMENTS(auth_stats_fields); +} + +static const char *auth_stats_field_name(unsigned int n) +{ + i_assert(n < N_ELEMENTS(auth_stats_fields)); + + return auth_stats_fields[n].name; +} + +static void +auth_stats_field_value(string_t *str, const struct stats *stats, + unsigned int n) +{ + i_assert(n < N_ELEMENTS(auth_stats_fields)); + + stats_parser_value(str, &auth_stats_fields[n], stats); +} + +static bool +auth_stats_diff(const struct stats *stats1, const struct stats *stats2, + struct stats *diff_stats_r, const char **error_r) +{ + return stats_parser_diff(auth_stats_fields, N_ELEMENTS(auth_stats_fields), + stats1, stats2, diff_stats_r, error_r); +} + +static void auth_stats_add(struct stats *dest, const struct stats *src) +{ + stats_parser_add(auth_stats_fields, N_ELEMENTS(auth_stats_fields), + dest, src); +} + +static bool +auth_stats_have_changed(const struct stats *_prev, const struct stats *_cur) +{ + return memcmp(_prev, _cur, sizeof(struct auth_stats)) != 0; +} + +static void auth_stats_export(buffer_t *buf, const struct stats *_stats) +{ + const struct auth_stats *stats = (const struct auth_stats *)_stats; + + buffer_append(buf, stats, sizeof(*stats)); +} + +static bool +auth_stats_import(const unsigned char *data, size_t size, size_t *pos_r, + struct stats *_stats, const char **error_r) +{ + struct auth_stats *stats = (struct auth_stats *)_stats; + + if (size < sizeof(*stats)) { + *error_r = "auth_stats too small"; + return FALSE; + } + memcpy(stats, data, sizeof(*stats)); + *pos_r = sizeof(*stats); + return TRUE; +} + +const struct stats_vfuncs auth_stats_vfuncs = { + "auth", + auth_stats_alloc_size, + auth_stats_field_count, + auth_stats_field_name, + auth_stats_field_value, + auth_stats_diff, + auth_stats_add, + auth_stats_have_changed, + auth_stats_export, + auth_stats_import +}; + +/* for the stats_auth plugin: */ +void stats_auth_init(void); +void stats_auth_deinit(void); + +static struct stats_item *auth_stats_item; + +void stats_auth_init(void) +{ + auth_stats_item = stats_register(&auth_stats_vfuncs); +} + +void stats_auth_deinit(void) +{ + stats_unregister(&auth_stats_item); +} diff --git a/src/auth/auth-stats.h b/src/auth/auth-stats.h new file mode 100644 index 0000000..a3431bd --- /dev/null +++ b/src/auth/auth-stats.h @@ -0,0 +1,16 @@ +#ifndef AUTH_STATS_H +#define AUTH_STATS_H + +struct auth_stats { + uint32_t auth_success_count; + uint32_t auth_master_success_count; + uint32_t auth_failure_count; + uint32_t auth_db_tempfail_count; + + uint32_t auth_cache_hit_count; + uint32_t auth_cache_miss_count; +}; + +extern const struct stats_vfuncs auth_stats_vfuncs; + +#endif diff --git a/src/auth/auth-token.c b/src/auth/auth-token.c new file mode 100644 index 0000000..74a1020 --- /dev/null +++ b/src/auth/auth-token.c @@ -0,0 +1,177 @@ +/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */ + +/* Auth process maintains a random secret. Once a user authenticates the + response to the REQUEST command from a master service is augmented with an + auth_token value. This token is the SHA1 hash of the secret, the service + name and the username of the user that just logged in. Using this token the + service (e.g. imap) can login to another service (e.g. imap-urlauth) to + gain access to resources that require additional privileges (e.g. another + user's e-mail). +*/ + +#include "auth-common.h" +#include "hex-binary.h" +#include "hmac.h" +#include "sha1.h" +#include "randgen.h" +#include "read-full.h" +#include "write-full.h" +#include "safe-memset.h" +#include "auth-settings.h" +#include "auth-token.h" + +#include <stdio.h> +#include <fcntl.h> +#include <unistd.h> +#include <sys/types.h> +#include <sys/stat.h> + +#define AUTH_TOKEN_SECRET_LEN 32 + +#define AUTH_TOKEN_SECRET_FNAME "auth-token-secret.dat" + +static unsigned char auth_token_secret[AUTH_TOKEN_SECRET_LEN]; + +static int +auth_token_read_secret(const char *path, + unsigned char secret_r[AUTH_TOKEN_SECRET_LEN]) +{ + struct stat st, lst; + int fd, ret; + + fd = open(path, O_RDONLY); + if (fd == -1) { + if (errno != ENOENT) + i_error("open(%s) failed: %m", path); + return -1; + } + + if (fstat(fd, &st) < 0) { + i_error("fstat(%s) failed: %m", path); + i_close_fd(&fd); + return -1; + } + + /* check secret len and file type */ + if (st.st_size != AUTH_TOKEN_SECRET_LEN || !S_ISREG(st.st_mode)) { + i_error("Corrupted token secret file: %s", path); + i_close_fd(&fd); + i_unlink(path); + return -1; + } + + /* verify that we're not dealing with a symbolic link */ + if (lstat(path, &lst) < 0) { + i_error("lstat(%s) failed: %m", path); + i_close_fd(&fd); + return -1; + } + + /* check security parameters for compromise */ + if ((st.st_mode & 07777) != 0600 || + st.st_uid != geteuid() || st.st_nlink > 1 || + !S_ISREG(lst.st_mode) || st.st_ino != lst.st_ino || + !CMP_DEV_T(st.st_dev, lst.st_dev)) { + i_error("Compromised token secret file: %s", path); + i_close_fd(&fd); + i_unlink(path); + return -1; + } + + /* FIXME: fail here to generate new secret if stored one is too old */ + + ret = read_full(fd, secret_r, AUTH_TOKEN_SECRET_LEN); + if (ret < 0) + i_error("read(%s) failed: %m", path); + else if (ret == 0) { + i_error("Token secret file unexpectedly shrank: %s", path); + ret = -1; + } + if (close(fd) < 0) + i_error("close(%s) failed: %m", path); + + e_debug(auth_event, "Read auth token secret from %s", path); + return ret; +} + +static int +auth_token_write_secret(const char *path, + const unsigned char secret[AUTH_TOKEN_SECRET_LEN]) +{ + const char *temp_path; + mode_t old_mask; + int fd, ret; + + temp_path = t_strconcat(path, ".tmp", NULL); + + old_mask = umask(0); + fd = open(temp_path, O_WRONLY | O_CREAT | O_TRUNC, 0600); + umask(old_mask); + + if (fd == -1) { + i_error("open(%s) failed: %m", temp_path); + return -1; + } + + ret = write_full(fd, secret, AUTH_TOKEN_SECRET_LEN); + if (ret < 0) + i_error("write(%s) failed: %m", temp_path); + if (close(fd) < 0) { + i_error("close(%s) failed: %m", temp_path); + ret = -1; + } + + if (ret < 0) { + i_unlink(temp_path); + return -1; + } + + if (rename(temp_path, path) < 0) { + i_error("rename(%s, %s) failed: %m", temp_path, path); + i_unlink(temp_path); + return -1; + } + + e_debug(auth_event, "Wrote new auth token secret to %s", path); + return 0; +} + +void auth_token_init(void) +{ + const char *secret_path = + t_strconcat(global_auth_settings->base_dir, "/", + AUTH_TOKEN_SECRET_FNAME, NULL); + + if (auth_token_read_secret(secret_path, auth_token_secret) < 0) { + random_fill(auth_token_secret, sizeof(auth_token_secret)); + + if (auth_token_write_secret(secret_path, auth_token_secret) < 0) { + i_error("Failed to write auth token secret file; " + "returned tokens will be invalid once auth restarts"); + } + } +} + +void auth_token_deinit(void) +{ + /* not very useful, but we do it anyway */ + safe_memset(auth_token_secret, 0, sizeof(auth_token_secret)); +} + +const char *auth_token_get(const char *service, const char *session_pid, + const char *username, const char *session_id) +{ + struct hmac_context ctx; + unsigned char result[SHA1_RESULTLEN]; + + hmac_init(&ctx, (const unsigned char*)username, strlen(username), + &hash_method_sha1); + hmac_update(&ctx, session_pid, strlen(session_pid)); + if (session_id != NULL && *session_id != '\0') + hmac_update(&ctx, session_id, strlen(session_id)); + hmac_update(&ctx, service, strlen(service)); + hmac_update(&ctx, auth_token_secret, sizeof(auth_token_secret)); + hmac_final(&ctx, result); + + return binary_to_hex(result, sizeof(result)); +} diff --git a/src/auth/auth-token.h b/src/auth/auth-token.h new file mode 100644 index 0000000..3e427c0 --- /dev/null +++ b/src/auth/auth-token.h @@ -0,0 +1,11 @@ +#ifndef AUTH_TOKEN_H +#define AUTH_TOKEN_H + +void auth_token_init(void); +void auth_token_deinit(void); + +const char *auth_token_get(const char *service, const char *session_pid, + const char *username, const char *session_id); + +#endif + diff --git a/src/auth/auth-worker-client.c b/src/auth/auth-worker-client.c new file mode 100644 index 0000000..ff88bc2 --- /dev/null +++ b/src/auth/auth-worker-client.c @@ -0,0 +1,975 @@ +/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */ + +#include "auth-common.h" +#include "base64.h" +#include "connection.h" +#include "ioloop.h" +#include "net.h" +#include "istream.h" +#include "ostream.h" +#include "hex-binary.h" +#include "str.h" +#include "strescape.h" +#include "process-title.h" +#include "master-service.h" +#include "auth-request.h" +#include "auth-worker-client.h" + + +#define AUTH_WORKER_WARN_DISCONNECTED_LONG_CMD_SECS 30 +#define OUTBUF_THROTTLE_SIZE (1024*10) + +#define CLIENT_STATE_HANDSHAKE "handshaking" +#define CLIENT_STATE_ITER "iterating users" +#define CLIENT_STATE_IDLE "idling" +#define CLIENT_STATE_STOP "waiting for shutdown" + +static unsigned int auth_worker_max_service_count = 0; +static unsigned int auth_worker_service_count = 0; + +struct auth_worker_client { + struct connection conn; + int refcount; + + struct auth *auth; + struct event *event; + time_t cmd_start; + + bool error_sent:1; + bool destroyed:1; +}; + +struct auth_worker_command { + struct auth_worker_client *client; + struct event *event; +}; + +struct auth_worker_list_context { + struct auth_worker_command *cmd; + struct auth_worker_client *client; + struct auth_request *auth_request; + struct userdb_iterate_context *iter; + bool sending, sent, done; +}; + +static struct connection_list *clients = NULL; +static bool auth_worker_client_error = FALSE; + +static int auth_worker_output(struct auth_worker_client *client); +static void auth_worker_client_destroy(struct connection *conn); +static void auth_worker_client_unref(struct auth_worker_client **_client); + +void auth_worker_set_max_service_count(unsigned int count) +{ + auth_worker_max_service_count = count; +} + +static struct auth_worker_client *auth_worker_get_client(void) +{ + if (!auth_worker_has_client()) + return NULL; + struct auth_worker_client *client = + container_of(clients->connections, struct auth_worker_client, conn); + return client; +} + +void auth_worker_refresh_proctitle(const char *state) +{ + if (!global_auth_settings->verbose_proctitle || !worker) + return; + + if (auth_worker_client_error) + state = "error"; + else if (!auth_worker_has_client()) + state = "waiting for connection"; + process_title_set(t_strdup_printf("worker: %s", state)); +} + +static void +auth_worker_client_check_throttle(struct auth_worker_client *client) +{ + if (o_stream_get_buffer_used_size(client->conn.output) >= + OUTBUF_THROTTLE_SIZE) { + /* stop reading new requests until client has read the pending + replies. */ + connection_input_halt(&client->conn); + } +} + +static void +auth_worker_request_finished_full(struct auth_worker_command *cmd, + const char *error, bool log_as_error) +{ + event_set_name(cmd->event, "auth_worker_request_finished"); + if (error != NULL) { + event_add_str(cmd->event, "error", error); + if (log_as_error) + e_error(cmd->event, "Finished: %s", error); + else + e_debug(cmd->event, "Finished: %s", error); + } else { + e_debug(cmd->event, "Finished"); + } + auth_worker_client_check_throttle(cmd->client); + auth_worker_client_unref(&cmd->client); + event_unref(&cmd->event); + i_free(cmd); + + auth_worker_refresh_proctitle(CLIENT_STATE_IDLE); +} + +static void auth_worker_request_finished(struct auth_worker_command *cmd, + const char *error) +{ + auth_worker_request_finished_full(cmd, error, FALSE); +} + +static void auth_worker_request_finished_bug(struct auth_worker_command *cmd, + const char *error) +{ + auth_worker_request_finished_full(cmd, error, TRUE); +} + +bool auth_worker_auth_request_new(struct auth_worker_command *cmd, unsigned int id, + const char *const *args, struct auth_request **request_r) +{ + struct auth_request *auth_request; + const char *key, *value; + + auth_request = auth_request_new_dummy(cmd->event); + + cmd->client->refcount++; + auth_request->context = cmd; + auth_request->id = id; + + for (; *args != NULL; args++) { + value = strchr(*args, '='); + if (value == NULL) + (void)auth_request_import(auth_request, *args, ""); + else { + key = t_strdup_until(*args, value++); + (void)auth_request_import(auth_request, key, value); + } + } + if (auth_request->fields.user == NULL || + auth_request->fields.service == NULL) { + auth_request_unref(&auth_request); + return FALSE; + } + + /* reset changed-fields, so we'll export only the ones that were + changed by this lookup. */ + auth_fields_snapshot(auth_request->fields.extra_fields); + if (auth_request->fields.userdb_reply != NULL) + auth_fields_snapshot(auth_request->fields.userdb_reply); + + auth_request_init(auth_request); + *request_r = auth_request; + + return TRUE; +} + +static void auth_worker_send_reply(struct auth_worker_client *client, + struct auth_request *request, + string_t *str) +{ + time_t cmd_duration = time(NULL) - client->cmd_start; + const char *p; + + if (worker_restart_request) + o_stream_nsend_str(client->conn.output, "RESTART\n"); + o_stream_nsend(client->conn.output, str_data(str), str_len(str)); + if (o_stream_flush(client->conn.output) < 0 && request != NULL && + cmd_duration > AUTH_WORKER_WARN_DISCONNECTED_LONG_CMD_SECS) { + p = i_strchr_to_next(str_c(str), '\t'); + p = p == NULL ? "BUG" : t_strcut(p, '\t'); + + e_warning(client->conn.event, "Auth master disconnected us while handling " + "request for %s for %ld secs (result=%s)", + request->fields.user, (long)cmd_duration, p); + } +} + +static void +reply_append_extra_fields(string_t *str, struct auth_request *request) +{ + if (!auth_fields_is_empty(request->fields.extra_fields)) { + str_append_c(str, '\t'); + /* export only the fields changed by this lookup, so the + changed-flag gets preserved correctly on the master side as + well. */ + auth_fields_append(request->fields.extra_fields, str, + AUTH_FIELD_FLAG_CHANGED, + AUTH_FIELD_FLAG_CHANGED); + } + if (request->fields.userdb_reply != NULL && + auth_fields_is_empty(request->fields.userdb_reply)) { + /* all userdb_* fields had NULL values. we'll still + need to tell this to the master */ + str_append(str, "\tuserdb_"AUTH_REQUEST_USER_KEY_IGNORE); + } +} + +static void verify_plain_callback(enum passdb_result result, + struct auth_request *request) +{ + struct auth_worker_command *cmd = request->context; + struct auth_worker_client *client = cmd->client; + const char *error = NULL; + string_t *str; + + if (request->failed && result == PASSDB_RESULT_OK) + result = PASSDB_RESULT_PASSWORD_MISMATCH; + + str = t_str_new(128); + str_printfa(str, "%u\t", request->id); + + if (result == PASSDB_RESULT_OK) + if (auth_fields_exists(request->fields.extra_fields, "noauthenticate")) + str_append(str, "NEXT"); + else + str_append(str, "OK"); + else { + str_printfa(str, "FAIL\t%d", result); + error = passdb_result_to_string(result); + } + if (result != PASSDB_RESULT_INTERNAL_FAILURE) { + str_append_c(str, '\t'); + if (request->user_changed_by_lookup) + str_append_tabescaped(str, request->fields.user); + str_append_c(str, '\t'); + if (request->passdb_password != NULL) + str_append_tabescaped(str, request->passdb_password); + reply_append_extra_fields(str, request); + } + str_append_c(str, '\n'); + auth_worker_send_reply(client, request, str); + + auth_request_passdb_lookup_end(request, result); + auth_worker_request_finished(cmd, error); + auth_request_unref(&request); +} + +static bool +auth_worker_handle_passv(struct auth_worker_command *cmd, + unsigned int id, const char *const *args, + const char **error_r) +{ + /* verify plaintext password */ + struct auth_request *auth_request; + struct auth_passdb *passdb; + const char *password; + unsigned int passdb_id; + + /* <passdb id> <password> [<args>] */ + if (str_to_uint(args[0], &passdb_id) < 0 || args[1] == NULL) { + *error_r = "BUG: Auth worker server sent us invalid PASSV"; + return FALSE; + } + password = args[1]; + + if (!auth_worker_auth_request_new(cmd, id, args + 2, &auth_request)) { + *error_r = "BUG: Auth worker server sent us invalid PASSV"; + return FALSE; + } + auth_request->mech_password = + p_strdup(auth_request->pool, password); + + passdb = auth_request->passdb; + while (passdb != NULL && passdb->passdb->id != passdb_id) + passdb = passdb->next; + + if (passdb == NULL) { + /* could be a masterdb */ + passdb = auth_request_get_auth(auth_request)->masterdbs; + while (passdb != NULL && passdb->passdb->id != passdb_id) + passdb = passdb->next; + + if (passdb == NULL) { + *error_r = "BUG: PASSV had invalid passdb ID"; + auth_request_unref(&auth_request); + return FALSE; + } + } + + auth_request->passdb = passdb; + auth_request_passdb_lookup_begin(auth_request); + passdb->passdb->iface. + verify_plain(auth_request, password, verify_plain_callback); + return TRUE; +} + +static bool +auth_worker_handle_passw(struct auth_worker_command *cmd, + unsigned int id, const char *const *args, + const char **error_r) +{ + struct auth_worker_client *client = cmd->client; + struct auth_request *request; + string_t *str; + const char *password; + const char *crypted, *scheme, *error; + unsigned int passdb_id; + int ret; + + if (str_to_uint(args[0], &passdb_id) < 0 || args[1] == NULL || + args[2] == NULL) { + *error_r = "BUG: Auth worker server sent us invalid PASSW"; + return FALSE; + } + password = args[1]; + crypted = args[2]; + scheme = password_get_scheme(&crypted); + if (scheme == NULL) { + *error_r = "BUG: Auth worker server sent us invalid PASSW (scheme is NULL)"; + return FALSE; + } + + if (!auth_worker_auth_request_new(cmd, id, args + 3, &request)) { + *error_r = "BUG: PASSW had missing parameters"; + return FALSE; + } + request->mech_password = + p_strdup(request->pool, password); + + ret = auth_request_password_verify(request, password, + crypted, scheme, "cache"); + str = t_str_new(128); + str_printfa(str, "%u\t", request->id); + + if (ret == 1) { + str_printfa(str, "OK\t\t"); + error = NULL; + } else if (ret == 0) { + str_printfa(str, "FAIL\t%d", PASSDB_RESULT_PASSWORD_MISMATCH); + error = passdb_result_to_string(PASSDB_RESULT_PASSWORD_MISMATCH); + } else { + str_printfa(str, "FAIL\t%d", PASSDB_RESULT_INTERNAL_FAILURE); + error = passdb_result_to_string(PASSDB_RESULT_INTERNAL_FAILURE); + } + + str_append_c(str, '\n'); + auth_worker_send_reply(client, request, str); + + auth_worker_request_finished(cmd, error); + auth_request_unref(&request); + return TRUE; +} + +static void +lookup_credentials_callback(enum passdb_result result, + const unsigned char *credentials, size_t size, + struct auth_request *request) +{ + struct auth_worker_command *cmd = request->context; + struct auth_worker_client *client = cmd->client; + string_t *str; + + if (request->failed && result == PASSDB_RESULT_OK) + result = PASSDB_RESULT_PASSWORD_MISMATCH; + + str = t_str_new(128); + str_printfa(str, "%u\t", request->id); + + if (result != PASSDB_RESULT_OK && result != PASSDB_RESULT_NEXT) + str_printfa(str, "FAIL\t%d", result); + else { + if (result == PASSDB_RESULT_NEXT) + str_append(str, "NEXT\t"); + else + str_append(str, "OK\t"); + if (request->user_changed_by_lookup) + str_append_tabescaped(str, request->fields.user); + str_append_c(str, '\t'); + if (request->wanted_credentials_scheme[0] != '\0') { + str_printfa(str, "{%s.b64}", request->wanted_credentials_scheme); + base64_encode(credentials, size, str); + } else { + i_assert(size == 0); + } + reply_append_extra_fields(str, request); + } + str_append_c(str, '\n'); + auth_worker_send_reply(client, request, str); + + auth_request_passdb_lookup_end(request, result); + auth_request_unref(&request); + auth_worker_request_finished(cmd, NULL); +} + +static bool +auth_worker_handle_passl(struct auth_worker_command *cmd, + unsigned int id, const char *const *args, + const char **error_r) +{ + /* lookup credentials */ + struct auth_request *auth_request; + const char *scheme; + unsigned int passdb_id; + + /* <passdb id> <scheme> [<args>] */ + if (str_to_uint(args[0], &passdb_id) < 0 || args[1] == NULL) { + *error_r = "BUG: Auth worker server sent us invalid PASSL"; + return FALSE; + } + scheme = args[1]; + + if (!auth_worker_auth_request_new(cmd, id, args + 2, &auth_request)) { + *error_r = "BUG: PASSL had missing parameters"; + return FALSE; + } + auth_request->wanted_credentials_scheme = + p_strdup(auth_request->pool, scheme); + + while (auth_request->passdb->passdb->id != passdb_id) { + auth_request->passdb = auth_request->passdb->next; + if (auth_request->passdb == NULL) { + *error_r = "BUG: PASSL had invalid passdb ID"; + auth_request_unref(&auth_request); + return FALSE; + } + } + + if (auth_request->passdb->passdb->iface.lookup_credentials == NULL) { + *error_r = "BUG: PASSL lookup not supported by given passdb"; + auth_request_unref(&auth_request); + return FALSE; + } + + auth_request->prefer_plain_credentials = TRUE; + auth_request_passdb_lookup_begin(auth_request); + auth_request->passdb->passdb->iface. + lookup_credentials(auth_request, lookup_credentials_callback); + return TRUE; +} + +static void +set_credentials_callback(bool success, struct auth_request *request) +{ + struct auth_worker_command *cmd = request->context; + struct auth_worker_client *client = cmd->client; + + string_t *str; + + str = t_str_new(64); + str_printfa(str, "%u\t%s\n", request->id, success ? "OK" : "FAIL"); + auth_worker_send_reply(client, request, str); + + auth_worker_request_finished(cmd, success ? NULL : + "Failed to set credentials"); + auth_request_unref(&request); +} + +static bool +auth_worker_handle_setcred(struct auth_worker_command *cmd, + unsigned int id, const char *const *args, + const char **error_r) +{ + struct auth_request *auth_request; + unsigned int passdb_id; + const char *creds; + + /* <passdb id> <credentials> [<args>] */ + if (str_to_uint(args[0], &passdb_id) < 0 || args[1] == NULL) { + *error_r = "BUG: Auth worker server sent us invalid SETCRED"; + return FALSE; + } + creds = args[1]; + + if (!auth_worker_auth_request_new(cmd, id, args + 2, &auth_request)) { + *error_r = "BUG: SETCRED had missing parameters"; + return FALSE; + } + + while (auth_request->passdb->passdb->id != passdb_id) { + auth_request->passdb = auth_request->passdb->next; + if (auth_request->passdb == NULL) { + *error_r = "BUG: SETCRED had invalid passdb ID"; + auth_request_unref(&auth_request); + return FALSE; + } + } + + auth_request->passdb->passdb->iface. + set_credentials(auth_request, creds, set_credentials_callback); + return TRUE; +} + +static void +lookup_user_callback(enum userdb_result result, + struct auth_request *auth_request) +{ + struct auth_worker_command *cmd = auth_request->context; + struct auth_worker_client *client = cmd->client; + const char *error; + string_t *str; + + str = t_str_new(128); + str_printfa(str, "%u\t", auth_request->id); + switch (result) { + case USERDB_RESULT_INTERNAL_FAILURE: + str_append(str, "FAIL\t"); + break; + case USERDB_RESULT_USER_UNKNOWN: + str_append(str, "NOTFOUND\t"); + break; + case USERDB_RESULT_OK: + str_append(str, "OK\t"); + if (auth_request->user_changed_by_lookup) + str_append_tabescaped(str, auth_request->fields.user); + str_append_c(str, '\t'); + /* export only the fields changed by this lookup */ + auth_fields_append(auth_request->fields.userdb_reply, str, + AUTH_FIELD_FLAG_CHANGED, + AUTH_FIELD_FLAG_CHANGED); + if (auth_request->userdb_lookup_tempfailed) + str_append(str, "\ttempfail"); + break; + } + str_append_c(str, '\n'); + + auth_worker_send_reply(client, auth_request, str); + + auth_request_userdb_lookup_end(auth_request, result); + error = result == USERDB_RESULT_OK ? NULL : + userdb_result_to_string(result); + auth_worker_request_finished(cmd, error); + auth_request_unref(&auth_request); +} + +static struct auth_userdb * +auth_userdb_find_by_id(struct auth_userdb *userdbs, unsigned int id) +{ + struct auth_userdb *db; + + for (db = userdbs; db != NULL; db = db->next) { + if (db->userdb->id == id) + return db; + } + return NULL; +} + +static bool +auth_worker_handle_user(struct auth_worker_command *cmd, + unsigned int id, const char *const *args, + const char **error_r) +{ + /* lookup user */ + struct auth_request *auth_request; + unsigned int userdb_id; + + /* <userdb id> [<args>] */ + if (str_to_uint(args[0], &userdb_id) < 0) { + *error_r = "BUG: Auth worker server sent us invalid USER"; + return FALSE; + } + + if (!auth_worker_auth_request_new(cmd, id, args + 1, &auth_request)) { + *error_r = "BUG: USER had missing parameters"; + return FALSE; + } + + auth_request->userdb_lookup = TRUE; + auth_request->userdb = + auth_userdb_find_by_id(auth_request->userdb, userdb_id); + if (auth_request->userdb == NULL) { + *error_r = "BUG: USER had invalid userdb ID"; + auth_request_unref(&auth_request); + return FALSE; + } + + if (auth_request->fields.userdb_reply == NULL) + auth_request_init_userdb_reply(auth_request, TRUE); + auth_request_userdb_lookup_begin(auth_request); + auth_request->userdb->userdb->iface-> + lookup(auth_request, lookup_user_callback); + return TRUE; +} + +static void +auth_worker_client_idle_kill(struct connection *conn ATTR_UNUSED) +{ + auth_worker_client_send_shutdown(); +} + +static void list_iter_deinit(struct auth_worker_list_context *ctx) +{ + struct auth_worker_command *cmd = ctx->cmd; + struct auth_worker_client *client = ctx->client; + const char *error = NULL; + string_t *str; + + i_assert(client->conn.io == NULL); + + str = t_str_new(32); + if (ctx->auth_request->userdb->userdb->iface-> + iterate_deinit(ctx->iter) < 0) { + error = "Iteration failed"; + str_printfa(str, "%u\tFAIL\n", ctx->auth_request->id); + } else + str_printfa(str, "%u\tOK\n", ctx->auth_request->id); + auth_worker_send_reply(client, NULL, str); + + connection_input_resume(&client->conn); + o_stream_set_flush_callback(client->conn.output, auth_worker_output, + client); + auth_request_userdb_lookup_end(ctx->auth_request, USERDB_RESULT_OK); + auth_worker_request_finished(cmd, error); + auth_request_unref(&ctx->auth_request); + i_free(ctx); +} + +static void list_iter_callback(const char *user, void *context) +{ + struct auth_worker_list_context *ctx = context; + string_t *str; + + if (user == NULL) { + if (ctx->sending) + ctx->done = TRUE; + else + list_iter_deinit(ctx); + return; + } + + if (!ctx->sending) + o_stream_cork(ctx->client->conn.output); + T_BEGIN { + str = t_str_new(128); + str_printfa(str, "%u\t*\t%s\n", ctx->auth_request->id, user); + o_stream_nsend(ctx->client->conn.output, str_data(str), str_len(str)); + } T_END; + + if (ctx->sending) { + /* avoid recursively looping to this same function */ + ctx->sent = TRUE; + return; + } + + do { + ctx->sending = TRUE; + ctx->sent = FALSE; + T_BEGIN { + ctx->auth_request->userdb->userdb->iface-> + iterate_next(ctx->iter); + } T_END; + if (o_stream_get_buffer_used_size(ctx->client->conn.output) > OUTBUF_THROTTLE_SIZE) { + if (o_stream_flush(ctx->client->conn.output) < 0) { + ctx->done = TRUE; + break; + } + } + } while (ctx->sent && + o_stream_get_buffer_used_size(ctx->client->conn.output) <= OUTBUF_THROTTLE_SIZE); + o_stream_uncork(ctx->client->conn.output); + ctx->sending = FALSE; + if (ctx->done) + list_iter_deinit(ctx); + else + o_stream_set_flush_pending(ctx->client->conn.output, TRUE); +} + +static int auth_worker_list_output(struct auth_worker_list_context *ctx) +{ + int ret; + + if ((ret = o_stream_flush(ctx->client->conn.output)) < 0) { + list_iter_deinit(ctx); + return 1; + } + if (ret > 0) T_BEGIN { + ctx->auth_request->userdb->userdb->iface-> + iterate_next(ctx->iter); + } T_END; + return 1; +} + +static bool +auth_worker_handle_list(struct auth_worker_command *cmd, + unsigned int id, const char *const *args, + const char **error_r) +{ + struct auth_worker_client *client = cmd->client; + struct auth_worker_list_context *ctx; + struct auth_userdb *userdb; + unsigned int userdb_id; + + if (str_to_uint(args[0], &userdb_id) < 0) { + *error_r = "BUG: Auth worker server sent us invalid LIST"; + return FALSE; + } + + userdb = auth_userdb_find_by_id(client->auth->userdbs, userdb_id); + if (userdb == NULL) { + *error_r = "BUG: LIST had invalid userdb ID"; + return FALSE; + } + + ctx = i_new(struct auth_worker_list_context, 1); + ctx->cmd = cmd; + ctx->client = client; + if (!auth_worker_auth_request_new(cmd, id, args + 1, &ctx->auth_request)) { + *error_r = "BUG: LIST had missing parameters"; + i_free(ctx); + return FALSE; + } + ctx->auth_request->userdb = userdb; + + connection_input_halt(&ctx->client->conn); + + o_stream_set_flush_callback(ctx->client->conn.output, + auth_worker_list_output, ctx); + ctx->auth_request->userdb_lookup = TRUE; + auth_request_userdb_lookup_begin(ctx->auth_request); + ctx->iter = ctx->auth_request->userdb->userdb->iface-> + iterate_init(ctx->auth_request, list_iter_callback, ctx); + ctx->auth_request->userdb->userdb->iface->iterate_next(ctx->iter); + return TRUE; +} + +static bool auth_worker_verify_db_hash(const char *passdb_hash, const char *userdb_hash) +{ + string_t *str = t_str_new(MD5_RESULTLEN*2); + unsigned char passdb_md5[MD5_RESULTLEN]; + unsigned char userdb_md5[MD5_RESULTLEN]; + + passdbs_generate_md5(passdb_md5); + userdbs_generate_md5(userdb_md5); + + binary_to_hex_append(str, passdb_md5, sizeof(passdb_md5)); + if (strcmp(str_c(str), passdb_hash) != 0) + return FALSE; + str_truncate(str, 0); + binary_to_hex_append(str, userdb_md5, sizeof(userdb_md5)); + return strcmp(str_c(str), userdb_hash) == 0; +}; + +static int auth_worker_client_handshake_args(struct connection *conn, const char *const *args) +{ + if (!conn->version_received) { + if (connection_handshake_args_default(conn, args) < 0) + return -1; + return 0; + } + + if (str_array_length(args) < 3 || + strcmp(args[0], "DBHASH") != 0) { + e_error(conn->event, "BUG: Invalid input: %s", + t_strarray_join(args, "\t")); + return -1; + } + + if (!auth_worker_verify_db_hash(args[1], args[2])) { + e_error(conn->event, + "Auth worker sees different passdbs/userdbs " + "than auth server. Maybe config just changed " + "and this goes away automatically?"); + return -1; + } + return 1; +} + +static int +auth_worker_client_input_args(struct connection *conn, const char *const *args) +{ + unsigned int id; + bool ret = FALSE; + const char *error = NULL; + struct auth_worker_command *cmd; + struct auth_worker_client *client = + container_of(conn, struct auth_worker_client, conn); + + if (str_array_length(args) < 3 || + str_to_uint(args[0], &id) < 0) { + e_error(conn->event, "BUG: Invalid input: %s", + t_strarray_join(args, "\t")); + return -1; + } + + io_loop_time_refresh(); + + cmd = i_new(struct auth_worker_command, 1); + cmd->client = client; + cmd->event = event_create(client->conn.event); + event_add_category(cmd->event, &event_category_auth); + event_add_str(cmd->event, "command", args[1]); + event_add_int(cmd->event, "command_id", id); + event_set_append_log_prefix(cmd->event, t_strdup_printf("auth-worker<%u>: ", id)); + client->cmd_start = ioloop_time; + client->refcount++; + e_debug(cmd->event, "Handling %s request", args[1]); + + /* Check if we have reached service_count */ + if (auth_worker_max_service_count > 0) { + auth_worker_service_count++; + if (auth_worker_service_count >= auth_worker_max_service_count) + worker_restart_request = TRUE; + } + + auth_worker_refresh_proctitle(args[1]); + if (strcmp(args[1], "PASSV") == 0) + ret = auth_worker_handle_passv(cmd, id, args + 2, &error); + else if (strcmp(args[1], "PASSL") == 0) + ret = auth_worker_handle_passl(cmd, id, args + 2, &error); + else if (strcmp(args[1], "PASSW") == 0) + ret = auth_worker_handle_passw(cmd, id, args + 2, &error); + else if (strcmp(args[1], "SETCRED") == 0) + ret = auth_worker_handle_setcred(cmd, id, args + 2, &error); + else if (strcmp(args[1], "USER") == 0) + ret = auth_worker_handle_user(cmd, id, args + 2, &error); + else if (strcmp(args[1], "LIST") == 0) + ret = auth_worker_handle_list(cmd, id, args + 2, &error); + else { + error = t_strdup_printf("BUG: Auth-worker received unknown command: %s", + args[1]); + } + + i_assert(ret || error != NULL); + + if (!ret) { + auth_worker_request_finished_bug(cmd, error); + return -1; + } + auth_worker_client_unref(&client); + return 1; +} + +static int auth_worker_output(struct auth_worker_client *client) +{ + if (o_stream_flush(client->conn.output) < 0) { + auth_worker_client_destroy(&client->conn); + return 1; + } + + if (o_stream_get_buffer_used_size(client->conn.output) <= + OUTBUF_THROTTLE_SIZE/3 && client->conn.io == NULL) { + /* allow input again */ + connection_input_resume(&client->conn); + } + return 1; +} + +static void auth_worker_client_unref(struct auth_worker_client **_client) +{ + struct auth_worker_client *client = *_client; + if (client == NULL) + return; + if (--client->refcount > 0) + return; + + /* the connection should've been destroyed before getting here */ + i_assert(client->destroyed); + connection_deinit(&client->conn); + i_free(client); +} + +static void auth_worker_client_destroy(struct connection *conn) +{ + struct auth_worker_client *client = + container_of(conn, struct auth_worker_client, conn); + + i_assert(!client->destroyed); + client->destroyed = TRUE; + connection_input_halt(conn); + i_stream_close(conn->input); + o_stream_close(conn->output); + net_disconnect(conn->fd_in); + conn->fd_out = conn->fd_in = -1; + auth_worker_client_unref(&client); + master_service_client_connection_destroyed(master_service); +} + +static const struct connection_vfuncs auth_worker_client_v = +{ + .input_args = auth_worker_client_input_args, + .handshake_args = auth_worker_client_handshake_args, + .destroy = auth_worker_client_destroy, + .idle_timeout = auth_worker_client_idle_kill, +}; + +static const struct connection_settings auth_worker_client_set = +{ + .service_name_in = "auth-worker", + .service_name_out = "auth-worker", + .major_version = AUTH_WORKER_PROTOCOL_MAJOR_VERSION, + .minor_version = AUTH_WORKER_PROTOCOL_MINOR_VERSION, + .input_max_size = SIZE_MAX, + .output_max_size = SIZE_MAX, /* we use throttling */ +}; + +struct auth_worker_client * +auth_worker_client_create(struct auth *auth, + const struct master_service_connection *master_conn) +{ + struct auth_worker_client *client; + + if (clients == NULL) + clients = connection_list_init(&auth_worker_client_set, &auth_worker_client_v); + + client = i_new(struct auth_worker_client, 1); + client->refcount = 1; + client->auth = auth; + client->conn.event_parent = auth_event; + client->conn.input_idle_timeout_secs = master_service_get_idle_kill_secs(master_service); + connection_init_server(clients, &client->conn, master_conn->name, + master_conn->fd, master_conn->fd); + + auth_worker_refresh_proctitle(CLIENT_STATE_HANDSHAKE); + + if (auth_worker_client_error) + auth_worker_client_send_error(); + return client; +} + +void auth_worker_client_send_error(void) +{ + struct auth_worker_client *auth_worker_client = + auth_worker_get_client(); + auth_worker_client_error = TRUE; + if (auth_worker_client != NULL && + !auth_worker_client->error_sent) { + o_stream_nsend_str(auth_worker_client->conn.output, "ERROR\n"); + auth_worker_client->error_sent = TRUE; + } + auth_worker_refresh_proctitle(""); +} + +void auth_worker_client_send_success(void) +{ + struct auth_worker_client *auth_worker_client = + auth_worker_get_client(); + auth_worker_client_error = FALSE; + if (auth_worker_client == NULL) + return; + if (auth_worker_client->error_sent) { + o_stream_nsend_str(auth_worker_client->conn.output, + "SUCCESS\n"); + auth_worker_client->error_sent = FALSE; + } + if (auth_worker_client->conn.io != NULL) + auth_worker_refresh_proctitle(CLIENT_STATE_IDLE); +} + +void auth_worker_client_send_shutdown(void) +{ + struct auth_worker_client *auth_worker_client = + auth_worker_get_client(); + if (auth_worker_client != NULL) + o_stream_nsend_str(auth_worker_client->conn.output, + "SHUTDOWN\n"); + auth_worker_refresh_proctitle(CLIENT_STATE_STOP); +} + +void auth_worker_connections_destroy_all(void) +{ + if (clients == NULL) + return; + while (clients->connections != NULL) + connection_deinit(clients->connections); + connection_list_deinit(&clients); +} + +bool auth_worker_has_client(void) +{ + return clients != NULL && clients->connections_count > 0; +} diff --git a/src/auth/auth-worker-client.h b/src/auth/auth-worker-client.h new file mode 100644 index 0000000..e41ee83 --- /dev/null +++ b/src/auth/auth-worker-client.h @@ -0,0 +1,27 @@ +#ifndef AUTH_WORKER_CLIENT_H +#define AUTH_WORKER_CLIENT_H + +#define AUTH_WORKER_PROTOCOL_MAJOR_VERSION 1 +#define AUTH_WORKER_PROTOCOL_MINOR_VERSION 0 +#define AUTH_WORKER_MAX_LINE_LENGTH 8192 + +struct master_service_connection; +struct auth_worker_command; + +struct auth_worker_client * +auth_worker_client_create(struct auth *auth, + const struct master_service_connection *master_conn); +bool auth_worker_auth_request_new(struct auth_worker_command *cmd, unsigned int id, + const char *const *args, struct auth_request **request_r); + +bool auth_worker_has_client(void); +void auth_worker_client_send_error(void); +void auth_worker_client_send_success(void); +void auth_worker_client_send_shutdown(void); + +void auth_worker_connections_destroy_all(void); + +/* Stop master service after this many requests. 0 is unlimited. */ +void auth_worker_set_max_service_count(unsigned int count); + +#endif diff --git a/src/auth/auth-worker-server.c b/src/auth/auth-worker-server.c new file mode 100644 index 0000000..41b2b2b --- /dev/null +++ b/src/auth/auth-worker-server.c @@ -0,0 +1,519 @@ +/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */ + +#include "auth-common.h" +#include "ioloop.h" +#include "array.h" +#include "aqueue.h" +#include "net.h" +#include "istream.h" +#include "ostream.h" +#include "hex-binary.h" +#include "str.h" +#include "eacces-error.h" +#include "auth-request.h" +#include "auth-worker-client.h" +#include "auth-worker-server.h" + +#include <unistd.h> + +/* Initial lookup timeout */ +#define AUTH_WORKER_LOOKUP_TIMEOUT_SECS 60 +/* Timeout for multi-line replies, e.g. listing users. This should be a much + higher value, because e.g. doveadm could be doing some long-running commands + for the users. And because of buffering this timeout is for handling + multiple users, not just one. */ +#define AUTH_WORKER_RESUME_TIMEOUT_SECS (30*60) +#define AUTH_WORKER_MAX_IDLE_SECS (60*5) +#define AUTH_WORKER_ABORT_SECS 60 +#define AUTH_WORKER_DELAY_WARN_SECS 3 +#define AUTH_WORKER_DELAY_WARN_MIN_INTERVAL_SECS 300 + +struct auth_worker_request { + unsigned int id; + time_t created; + const char *username; + const char *data; + auth_worker_callback_t *callback; + void *context; +}; + +struct auth_worker_connection { + int fd; + + struct event *event; + struct io *io; + struct istream *input; + struct ostream *output; + struct timeout *to; + + struct auth_worker_request *request; + unsigned int id_counter; + + bool received_error:1; + bool restart:1; + bool shutdown:1; + bool timeout_pending_resume:1; + bool resuming:1; +}; + +static ARRAY(struct auth_worker_connection *) connections = ARRAY_INIT; +static unsigned int idle_count = 0, auth_workers_with_errors = 0; +static ARRAY(struct auth_worker_request *) worker_request_array; +static struct aqueue *worker_request_queue; +static time_t auth_worker_last_warn; +static unsigned int auth_workers_throttle_count; + +static const char *worker_socket_path; + +static void worker_input(struct auth_worker_connection *conn); +static void auth_worker_destroy(struct auth_worker_connection **conn, + const char *reason, bool restart) ATTR_NULL(2); + +static void auth_worker_idle_timeout(struct auth_worker_connection *conn) +{ + i_assert(conn->request == NULL); + + if (idle_count > 1) + auth_worker_destroy(&conn, NULL, FALSE); + else + timeout_reset(conn->to); +} + +static void auth_worker_call_timeout(struct auth_worker_connection *conn) +{ + i_assert(conn->request != NULL); + + auth_worker_destroy(&conn, "Lookup timed out", TRUE); +} + +static bool auth_worker_request_send(struct auth_worker_connection *conn, + struct auth_worker_request *request) +{ + struct const_iovec iov[3]; + unsigned int age_secs = ioloop_time - request->created; + + i_assert(conn->to != NULL); + + if (age_secs >= AUTH_WORKER_ABORT_SECS) { + e_error(conn->event, + "Aborting auth request that was queued for %d secs, " + "%d left in queue", + age_secs, aqueue_count(worker_request_queue)); + request->callback(conn, t_strdup_printf( + "FAIL\t%d", PASSDB_RESULT_INTERNAL_FAILURE), + request->context); + return FALSE; + } + if (age_secs >= AUTH_WORKER_DELAY_WARN_SECS && + ioloop_time - auth_worker_last_warn > + AUTH_WORKER_DELAY_WARN_MIN_INTERVAL_SECS) { + auth_worker_last_warn = ioloop_time; + e_error(conn->event, "Auth request was queued for %d " + "seconds, %d left in queue " + "(see auth_worker_max_count)", + age_secs, aqueue_count(worker_request_queue)); + } + + request->id = ++conn->id_counter; + + iov[0].iov_base = t_strdup_printf("%d\t", request->id); + iov[0].iov_len = strlen(iov[0].iov_base); + iov[1].iov_base = request->data; + iov[1].iov_len = strlen(request->data); + iov[2].iov_base = "\n"; + iov[2].iov_len = 1; + + o_stream_nsendv(conn->output, iov, 3); + + i_assert(conn->request == NULL); + conn->request = request; + + timeout_remove(&conn->to); + conn->to = timeout_add(AUTH_WORKER_LOOKUP_TIMEOUT_SECS * 1000, + auth_worker_call_timeout, conn); + idle_count--; + return TRUE; +} + +static void auth_worker_request_send_next(struct auth_worker_connection *conn) +{ + struct auth_worker_request *request; + + do { + if (aqueue_count(worker_request_queue) == 0) + return; + + request = array_idx_elem(&worker_request_array, + aqueue_idx(worker_request_queue, 0)); + aqueue_delete_tail(worker_request_queue); + } while (!auth_worker_request_send(conn, request)); +} + +static void auth_worker_send_handshake(struct auth_worker_connection *conn) +{ + string_t *str; + unsigned char passdb_md5[MD5_RESULTLEN]; + unsigned char userdb_md5[MD5_RESULTLEN]; + + str = t_str_new(128); + str_printfa(str, "VERSION\tauth-worker\t%u\t%u\n", + AUTH_WORKER_PROTOCOL_MAJOR_VERSION, + AUTH_WORKER_PROTOCOL_MINOR_VERSION); + + passdbs_generate_md5(passdb_md5); + userdbs_generate_md5(userdb_md5); + str_append(str, "DBHASH\t"); + binary_to_hex_append(str, passdb_md5, sizeof(passdb_md5)); + str_append_c(str, '\t'); + binary_to_hex_append(str, userdb_md5, sizeof(userdb_md5)); + str_append_c(str, '\n'); + + o_stream_nsend(conn->output, str_data(str), str_len(str)); +} + +static struct auth_worker_connection *auth_worker_create(void) +{ + struct auth_worker_connection *conn; + struct event *event; + int fd; + + if (array_count(&connections) >= auth_workers_throttle_count) + return NULL; + + event = event_create(auth_event); + event_set_append_log_prefix(event, "auth-worker: "); + + fd = net_connect_unix_with_retries(worker_socket_path, 5000); + if (fd == -1) { + if (errno == EACCES) { + e_error(event, "%s", + eacces_error_get("net_connect_unix", + worker_socket_path)); + } else { + e_error(event, "net_connect_unix(%s) failed: %m", + worker_socket_path); + } + event_unref(&event); + return NULL; + } + + conn = i_new(struct auth_worker_connection, 1); + conn->fd = fd; + conn->input = i_stream_create_fd(fd, AUTH_WORKER_MAX_LINE_LENGTH); + conn->output = o_stream_create_fd(fd, SIZE_MAX); + o_stream_set_no_error_handling(conn->output, TRUE); + conn->io = io_add(fd, IO_READ, worker_input, conn); + conn->to = timeout_add(AUTH_WORKER_MAX_IDLE_SECS * 1000, + auth_worker_idle_timeout, conn); + conn->event = event; + auth_worker_send_handshake(conn); + + idle_count++; + array_push_back(&connections, &conn); + return conn; +} + +static void auth_worker_destroy(struct auth_worker_connection **_conn, + const char *reason, bool restart) +{ + struct auth_worker_connection *conn = *_conn; + struct auth_worker_connection *const *conns; + unsigned int idx; + + *_conn = NULL; + + if (conn->received_error) { + i_assert(auth_workers_with_errors > 0); + i_assert(auth_workers_with_errors <= array_count(&connections)); + auth_workers_with_errors--; + } + + array_foreach(&connections, conns) { + if (*conns == conn) { + idx = array_foreach_idx(&connections, conns); + array_delete(&connections, idx, 1); + break; + } + } + + if (conn->request == NULL) + idle_count--; + + if (conn->request != NULL) { + e_error(conn->event, "Aborted %s request for %s: %s", + t_strcut(conn->request->data, '\t'), + conn->request->username, reason); + conn->request->callback(conn, t_strdup_printf( + "FAIL\t%d", PASSDB_RESULT_INTERNAL_FAILURE), + conn->request->context); + } + + io_remove(&conn->io); + i_stream_destroy(&conn->input); + o_stream_destroy(&conn->output); + timeout_remove(&conn->to); + + if (close(conn->fd) < 0) + e_error(conn->event, "close() failed: %m"); + event_unref(&conn->event); + i_free(conn); + + if (idle_count == 0 && restart) { + conn = auth_worker_create(); + if (conn != NULL) + auth_worker_request_send_next(conn); + } +} + +static struct auth_worker_connection *auth_worker_find_free(void) +{ + struct auth_worker_connection *conn; + + if (idle_count == 0) + return NULL; + + array_foreach_elem(&connections, conn) { + if (conn->request == NULL) + return conn; + } + i_unreached(); + return NULL; +} + +static bool auth_worker_request_handle(struct auth_worker_connection *conn, + struct auth_worker_request *request, + const char *line) +{ + if (str_begins(line, "*\t")) { + /* multi-line reply, not finished yet */ + if (conn->resuming) + timeout_reset(conn->to); + else { + conn->resuming = TRUE; + timeout_remove(&conn->to); + conn->to = timeout_add(AUTH_WORKER_RESUME_TIMEOUT_SECS * 1000, + auth_worker_call_timeout, conn); + } + } else { + conn->resuming = FALSE; + conn->request = NULL; + conn->timeout_pending_resume = FALSE; + timeout_remove(&conn->to); + conn->to = timeout_add(AUTH_WORKER_MAX_IDLE_SECS * 1000, + auth_worker_idle_timeout, conn); + idle_count++; + } + + if (!request->callback(conn, line, request->context) && conn->io != NULL) { + conn->timeout_pending_resume = FALSE; + timeout_remove(&conn->to); + io_remove(&conn->io); + return FALSE; + } + return TRUE; +} + +static bool auth_worker_error(struct auth_worker_connection *conn) +{ + if (conn->received_error) + return TRUE; + conn->received_error = TRUE; + auth_workers_with_errors++; + i_assert(auth_workers_with_errors <= array_count(&connections)); + + if (auth_workers_with_errors == 1) { + /* this is the only failing auth worker connection. + don't create new ones until this one sends SUCCESS. */ + auth_workers_throttle_count = array_count(&connections); + return TRUE; + } + + /* too many auth workers, reduce them */ + i_assert(array_count(&connections) > 1); + if (auth_workers_throttle_count >= array_count(&connections)) + auth_workers_throttle_count = array_count(&connections)-1; + else if (auth_workers_throttle_count > 1) + auth_workers_throttle_count--; + auth_worker_destroy(&conn, "Internal auth worker failure", FALSE); + return FALSE; +} + +static void auth_worker_success(struct auth_worker_connection *conn) +{ + unsigned int max_count = global_auth_settings->worker_max_count; + + if (!conn->received_error) + return; + + i_assert(auth_workers_with_errors > 0); + i_assert(auth_workers_with_errors <= array_count(&connections)); + auth_workers_with_errors--; + + if (auth_workers_with_errors == 0) { + /* all workers are succeeding now, set the limit back to + original. */ + auth_workers_throttle_count = max_count; + } else if (auth_workers_throttle_count < max_count) + auth_workers_throttle_count++; + conn->received_error = FALSE; +} + +static void worker_input(struct auth_worker_connection *conn) +{ + const char *line, *id_str; + unsigned int id; + + switch (i_stream_read(conn->input)) { + case 0: + return; + case -1: + /* disconnected */ + auth_worker_destroy(&conn, "Worker process died unexpectedly", + TRUE); + return; + case -2: + /* buffer full */ + e_error(conn->event, + "BUG: Auth worker sent us more than %d bytes", + (int)AUTH_WORKER_MAX_LINE_LENGTH); + auth_worker_destroy(&conn, "Worker is buggy", TRUE); + return; + } + + while ((line = i_stream_next_line(conn->input)) != NULL) { + if (strcmp(line, "RESTART") == 0) { + conn->restart = TRUE; + continue; + } + if (strcmp(line, "SHUTDOWN") == 0) { + conn->shutdown = TRUE; + continue; + } + if (strcmp(line, "ERROR") == 0) { + if (!auth_worker_error(conn)) + return; + continue; + } + if (strcmp(line, "SUCCESS") == 0) { + auth_worker_success(conn); + continue; + } + id_str = line; + line = strchr(line, '\t'); + if (line == NULL || + str_to_uint(t_strdup_until(id_str, line), &id) < 0) + continue; + + if (conn->request != NULL && id == conn->request->id) { + if (!auth_worker_request_handle(conn, conn->request, + line + 1)) + break; + } else { + if (conn->request != NULL) { + e_error(conn->event, + "BUG: Worker sent reply with id %u, " + "expected %u", id, conn->request->id); + } else { + e_error(conn->event, + "BUG: Worker sent reply with id %u, " + "none was expected", id); + } + auth_worker_destroy(&conn, "Worker is buggy", TRUE); + return; + } + } + + if (conn->request != NULL) { + /* there's still a pending request */ + } else if (conn->restart) + auth_worker_destroy(&conn, "Max requests limit", TRUE); + else if (conn->shutdown) + auth_worker_destroy(&conn, "Idle kill", FALSE); + else + auth_worker_request_send_next(conn); +} + +static void worker_input_resume(struct auth_worker_connection *conn) +{ + conn->timeout_pending_resume = FALSE; + timeout_remove(&conn->to); + conn->to = timeout_add(AUTH_WORKER_RESUME_TIMEOUT_SECS * 1000, + auth_worker_call_timeout, conn); + worker_input(conn); +} + +void auth_worker_call(pool_t pool, const char *username, const char *data, + auth_worker_callback_t *callback, void *context) +{ + struct auth_worker_connection *conn; + struct auth_worker_request *request; + + request = p_new(pool, struct auth_worker_request, 1); + request->created = ioloop_time; + request->username = p_strdup(pool, username); + request->data = p_strdup(pool, data); + request->callback = callback; + request->context = context; + + if (aqueue_count(worker_request_queue) > 0) { + /* requests are already being queued, no chance of + finding/creating a worker */ + conn = NULL; + } else { + conn = auth_worker_find_free(); + if (conn == NULL) { + /* no free connections, create a new one */ + conn = auth_worker_create(); + } + } + if (conn != NULL) { + if (!auth_worker_request_send(conn, request)) + i_unreached(); + } else { + /* reached the limit, queue the request */ + aqueue_append(worker_request_queue, &request); + } +} + +void auth_worker_server_resume_input(struct auth_worker_connection *conn) +{ + if (conn->request == NULL) { + /* request was just finished, don't try to resume it */ + return; + } + + if (conn->io == NULL) + conn->io = io_add(conn->fd, IO_READ, worker_input, conn); + if (!conn->timeout_pending_resume) { + conn->timeout_pending_resume = TRUE; + timeout_remove(&conn->to); + conn->to = timeout_add_short(0, worker_input_resume, conn); + } +} + +void auth_worker_server_init(void) +{ + worker_socket_path = "auth-worker"; + auth_workers_throttle_count = global_auth_settings->worker_max_count; + i_assert(auth_workers_throttle_count > 0); + + i_array_init(&worker_request_array, 128); + worker_request_queue = aqueue_init(&worker_request_array.arr); + + i_array_init(&connections, 16); +} + +void auth_worker_server_deinit(void) +{ + struct auth_worker_connection **connp, *conn; + + while (array_count(&connections) > 0) { + connp = array_front_modifiable(&connections); + conn = *connp; + auth_worker_destroy(&conn, "Shutting down", FALSE); + } + array_free(&connections); + + aqueue_deinit(&worker_request_queue); + array_free(&worker_request_array); +} diff --git a/src/auth/auth-worker-server.h b/src/auth/auth-worker-server.h new file mode 100644 index 0000000..d2a3063 --- /dev/null +++ b/src/auth/auth-worker-server.h @@ -0,0 +1,18 @@ +#ifndef AUTH_WORKER_SERVER_H +#define AUTH_WORKER_SERVER_H + +struct auth_request; +struct auth_stream_reply; +struct auth_worker_connection; + +typedef bool auth_worker_callback_t(struct auth_worker_connection *conn, + const char *reply, void *context); + +void auth_worker_call(pool_t pool, const char *username, const char *data, + auth_worker_callback_t *callback, void *context); +void auth_worker_server_resume_input(struct auth_worker_connection *conn); + +void auth_worker_server_init(void); +void auth_worker_server_deinit(void); + +#endif diff --git a/src/auth/auth.c b/src/auth/auth.c new file mode 100644 index 0000000..845c43c --- /dev/null +++ b/src/auth/auth.c @@ -0,0 +1,450 @@ +/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */ + +#include "auth-common.h" +#include "array.h" +#include "settings-parser.h" +#include "master-service-settings.h" +#include "mech.h" +#include "userdb.h" +#include "passdb.h" +#include "passdb-template.h" +#include "userdb-template.h" +#include "auth.h" + +struct event *auth_event; +struct event_category event_category_auth = { + .name = "auth", +}; + +static const struct auth_userdb_settings userdb_dummy_set = { + .name = "", + .driver = "static", + .args = "", + .default_fields = "", + .override_fields = "", + + .skip = "never", + .result_success = "return-ok", + .result_failure = "continue", + .result_internalfail = "continue", + + .auth_verbose = "default", +}; + +ARRAY_TYPE(auth) auths; + +static enum auth_passdb_skip auth_passdb_skip_parse(const char *str) +{ + if (strcmp(str, "never") == 0) + return AUTH_PASSDB_SKIP_NEVER; + if (strcmp(str, "authenticated") == 0) + return AUTH_PASSDB_SKIP_AUTHENTICATED; + if (strcmp(str, "unauthenticated") == 0) + return AUTH_PASSDB_SKIP_UNAUTHENTICATED; + i_unreached(); +} + +static enum auth_userdb_skip auth_userdb_skip_parse(const char *str) +{ + if (strcmp(str, "never") == 0) + return AUTH_USERDB_SKIP_NEVER; + if (strcmp(str, "found") == 0) + return AUTH_USERDB_SKIP_FOUND; + if (strcmp(str, "notfound") == 0) + return AUTH_USERDB_SKIP_NOTFOUND; + i_unreached(); +} + +static enum auth_db_rule auth_db_rule_parse(const char *str) +{ + if (strcmp(str, "return") == 0) + return AUTH_DB_RULE_RETURN; + if (strcmp(str, "return-ok") == 0) + return AUTH_DB_RULE_RETURN_OK; + if (strcmp(str, "return-fail") == 0) + return AUTH_DB_RULE_RETURN_FAIL; + if (strcmp(str, "continue") == 0) + return AUTH_DB_RULE_CONTINUE; + if (strcmp(str, "continue-ok") == 0) + return AUTH_DB_RULE_CONTINUE_OK; + if (strcmp(str, "continue-fail") == 0) + return AUTH_DB_RULE_CONTINUE_FAIL; + i_unreached(); +} + +static void +auth_passdb_preinit(struct auth *auth, const struct auth_passdb_settings *set, + struct auth_passdb **passdbs) +{ + struct auth_passdb *auth_passdb, **dest; + + auth_passdb = p_new(auth->pool, struct auth_passdb, 1); + auth_passdb->set = set; + auth_passdb->skip = auth_passdb_skip_parse(set->skip); + auth_passdb->result_success = + auth_db_rule_parse(set->result_success); + auth_passdb->result_failure = + auth_db_rule_parse(set->result_failure); + auth_passdb->result_internalfail = + auth_db_rule_parse(set->result_internalfail); + + auth_passdb->default_fields_tmpl = + passdb_template_build(auth->pool, set->default_fields); + auth_passdb->override_fields_tmpl = + passdb_template_build(auth->pool, set->override_fields); + + /* for backwards compatibility: */ + if (set->pass) + auth_passdb->result_success = AUTH_DB_RULE_CONTINUE; + + for (dest = passdbs; *dest != NULL; dest = &(*dest)->next) ; + *dest = auth_passdb; + + auth_passdb->passdb = passdb_preinit(auth->pool, set); + /* make sure any %variables in default_fields exist in cache_key */ + if (auth_passdb->passdb->default_cache_key != NULL) { + auth_passdb->cache_key = + p_strconcat(auth->pool, auth_passdb->passdb->default_cache_key, + set->default_fields, NULL); + } + else { + auth_passdb->cache_key = NULL; + } +} + +static void +auth_userdb_preinit(struct auth *auth, const struct auth_userdb_settings *set) +{ + struct auth_userdb *auth_userdb, **dest; + + auth_userdb = p_new(auth->pool, struct auth_userdb, 1); + auth_userdb->set = set; + auth_userdb->skip = auth_userdb_skip_parse(set->skip); + auth_userdb->result_success = + auth_db_rule_parse(set->result_success); + auth_userdb->result_failure = + auth_db_rule_parse(set->result_failure); + auth_userdb->result_internalfail = + auth_db_rule_parse(set->result_internalfail); + + auth_userdb->default_fields_tmpl = + userdb_template_build(auth->pool, set->driver, + set->default_fields); + auth_userdb->override_fields_tmpl = + userdb_template_build(auth->pool, set->driver, + set->override_fields); + + for (dest = &auth->userdbs; *dest != NULL; dest = &(*dest)->next) ; + *dest = auth_userdb; + + auth_userdb->userdb = userdb_preinit(auth->pool, set); + /* make sure any %variables in default_fields exist in cache_key */ + if (auth_userdb->userdb->default_cache_key != NULL) { + auth_userdb->cache_key = + p_strconcat(auth->pool, auth_userdb->userdb->default_cache_key, + set->default_fields, NULL); + } + else { + auth_userdb->cache_key = NULL; + } +} + +static bool auth_passdb_list_have_verify_plain(const struct auth *auth) +{ + const struct auth_passdb *passdb; + + for (passdb = auth->passdbs; passdb != NULL; passdb = passdb->next) { + if (passdb->passdb->iface.verify_plain != NULL) + return TRUE; + } + return FALSE; +} + +static bool auth_passdb_list_have_lookup_credentials(const struct auth *auth) +{ + const struct auth_passdb *passdb; + + for (passdb = auth->passdbs; passdb != NULL; passdb = passdb->next) { + if (passdb->passdb->iface.lookup_credentials != NULL) + return TRUE; + } + return FALSE; +} + +static bool auth_passdb_list_have_set_credentials(const struct auth *auth) +{ + const struct auth_passdb *passdb; + + for (passdb = auth->masterdbs; passdb != NULL; passdb = passdb->next) { + if (passdb->passdb->iface.set_credentials != NULL) + return TRUE; + } + for (passdb = auth->passdbs; passdb != NULL; passdb = passdb->next) { + if (passdb->passdb->iface.set_credentials != NULL) + return TRUE; + } + return FALSE; +} + +static bool +auth_mech_verify_passdb(const struct auth *auth, const struct mech_module_list *list) +{ + switch (list->module.passdb_need) { + case MECH_PASSDB_NEED_NOTHING: + break; + case MECH_PASSDB_NEED_VERIFY_PLAIN: + if (!auth_passdb_list_have_verify_plain(auth)) + return FALSE; + break; + case MECH_PASSDB_NEED_VERIFY_RESPONSE: + case MECH_PASSDB_NEED_LOOKUP_CREDENTIALS: + if (!auth_passdb_list_have_lookup_credentials(auth)) + return FALSE; + break; + case MECH_PASSDB_NEED_SET_CREDENTIALS: + if (!auth_passdb_list_have_lookup_credentials(auth)) + return FALSE; + if (!auth_passdb_list_have_set_credentials(auth)) + return FALSE; + break; + } + return TRUE; +} + +static void auth_mech_list_verify_passdb(const struct auth *auth) +{ + const struct mech_module_list *list; + + for (list = auth->reg->modules; list != NULL; list = list->next) { + if (!auth_mech_verify_passdb(auth, list)) + break; + } + + if (list != NULL) { + if (auth->passdbs == NULL) { + i_fatal("No passdbs specified in configuration file. " + "%s mechanism needs one", + list->module.mech_name); + } + i_fatal("%s mechanism can't be supported with given passdbs", + list->module.mech_name); + } +} + +static struct auth * ATTR_NULL(2) +auth_preinit(const struct auth_settings *set, const char *service, pool_t pool, + const struct mechanisms_register *reg) +{ + struct auth_passdb_settings *const *passdbs; + struct auth_userdb_settings *const *userdbs; + struct auth *auth; + unsigned int i, count, db_count, passdb_count, last_passdb = 0; + + auth = p_new(pool, struct auth, 1); + auth->pool = pool; + auth->service = p_strdup(pool, service); + auth->set = set; + auth->reg = reg; + + if (array_is_created(&set->passdbs)) + passdbs = array_get(&set->passdbs, &db_count); + else { + passdbs = NULL; + db_count = 0; + } + + /* initialize passdbs first and count them */ + for (passdb_count = 0, i = 0; i < db_count; i++) { + if (passdbs[i]->master) + continue; + + /* passdb { skip=unauthenticated } as the first passdb doesn't + make sense, since user is never authenticated at that point. + skip over them silently. */ + if (auth->passdbs == NULL && + auth_passdb_skip_parse(passdbs[i]->skip) == AUTH_PASSDB_SKIP_UNAUTHENTICATED) + continue; + + auth_passdb_preinit(auth, passdbs[i], &auth->passdbs); + passdb_count++; + last_passdb = i; + } + if (passdb_count != 0 && passdbs[last_passdb]->pass) + i_fatal("Last passdb can't have pass=yes"); + + for (i = 0; i < db_count; i++) { + if (!passdbs[i]->master) + continue; + + /* skip skip=unauthenticated, as explained above */ + if (auth->masterdbs == NULL && + auth_passdb_skip_parse(passdbs[i]->skip) == AUTH_PASSDB_SKIP_UNAUTHENTICATED) + continue; + + if (passdbs[i]->deny) + i_fatal("Master passdb can't have deny=yes"); + if (passdbs[i]->pass && passdb_count == 0) { + i_fatal("Master passdb can't have pass=yes " + "if there are no passdbs"); + } + auth_passdb_preinit(auth, passdbs[i], &auth->masterdbs); + } + + if (array_is_created(&set->userdbs)) { + userdbs = array_get(&set->userdbs, &count); + for (i = 0; i < count; i++) + auth_userdb_preinit(auth, userdbs[i]); + } + + if (auth->userdbs == NULL) { + /* use a dummy userdb static. */ + auth_userdb_preinit(auth, &userdb_dummy_set); + } + return auth; +} + +static void auth_passdb_init(struct auth_passdb *passdb) +{ + passdb_init(passdb->passdb); + + i_assert(passdb->passdb->default_pass_scheme != NULL || + passdb->cache_key == NULL); +} + +static void auth_init(struct auth *auth) +{ + struct auth_passdb *passdb; + struct auth_userdb *userdb; + + for (passdb = auth->masterdbs; passdb != NULL; passdb = passdb->next) + auth_passdb_init(passdb); + for (passdb = auth->passdbs; passdb != NULL; passdb = passdb->next) + auth_passdb_init(passdb); + for (userdb = auth->userdbs; userdb != NULL; userdb = userdb->next) + userdb_init(userdb->userdb); +} + +static void auth_deinit(struct auth *auth) +{ + struct auth_passdb *passdb; + struct auth_userdb *userdb; + + for (passdb = auth->masterdbs; passdb != NULL; passdb = passdb->next) + passdb_deinit(passdb->passdb); + for (passdb = auth->passdbs; passdb != NULL; passdb = passdb->next) + passdb_deinit(passdb->passdb); + for (userdb = auth->userdbs; userdb != NULL; userdb = userdb->next) + userdb_deinit(userdb->userdb); +} + +struct auth *auth_find_service(const char *name) +{ + struct auth *const *a; + unsigned int i, count; + + a = array_get(&auths, &count); + if (name != NULL) { + for (i = 1; i < count; i++) { + if (strcmp(a[i]->service, name) == 0) + return a[i]; + } + /* not found. maybe we can instead find a !service */ + for (i = 1; i < count; i++) { + if (a[i]->service[0] == '!' && + strcmp(a[i]->service + 1, name) != 0) + return a[i]; + } + } + return a[0]; +} + +struct auth *auth_default_service(void) +{ + struct auth *const *a; + unsigned int count; + + a = array_get(&auths, &count); + return a[0]; +} + +void auths_preinit(const struct auth_settings *set, pool_t pool, + const struct mechanisms_register *reg, + const char *const *services) +{ + struct master_service_settings_output set_output; + const struct auth_settings *service_set; + struct auth *auth; + unsigned int i; + const char *not_service = NULL; + bool check_default = TRUE; + + auth_event = event_create(NULL); + event_set_forced_debug(auth_event, set->debug); + event_add_category(auth_event, &event_category_auth); + i_array_init(&auths, 8); + + auth = auth_preinit(set, NULL, pool, reg); + array_push_back(&auths, &auth); + + for (i = 0; services[i] != NULL; i++) { + if (services[i][0] == '!') { + if (not_service != NULL) { + i_fatal("Can't have multiple protocol " + "!services (seen %s and %s)", + not_service, services[i]); + } + not_service = services[i]; + } + service_set = auth_settings_read(services[i], pool, + &set_output); + auth = auth_preinit(service_set, services[i], pool, reg); + array_push_back(&auths, &auth); + } + + if (not_service != NULL && str_array_find(services, not_service+1)) + check_default = FALSE; + + array_foreach_elem(&auths, auth) { + if (auth->service != NULL || check_default) + auth_mech_list_verify_passdb(auth); + } +} + +void auths_init(void) +{ + struct auth *auth; + + /* sanity checks */ + i_assert(auth_request_var_expand_static_tab[AUTH_REQUEST_VAR_TAB_USER_IDX].key == 'u'); + i_assert(auth_request_var_expand_static_tab[AUTH_REQUEST_VAR_TAB_USERNAME_IDX].key == 'n'); + i_assert(auth_request_var_expand_static_tab[AUTH_REQUEST_VAR_TAB_DOMAIN_IDX].key == 'd'); + i_assert(auth_request_var_expand_static_tab[AUTH_REQUEST_VAR_TAB_COUNT].key == '\0' && + auth_request_var_expand_static_tab[AUTH_REQUEST_VAR_TAB_COUNT].long_key == NULL); + i_assert(auth_request_var_expand_static_tab[AUTH_REQUEST_VAR_TAB_COUNT-1].key != '\0' || + auth_request_var_expand_static_tab[AUTH_REQUEST_VAR_TAB_COUNT-1].long_key != NULL); + + array_foreach_elem(&auths, auth) + auth_init(auth); +} + +void auths_deinit(void) +{ + struct auth *auth; + + array_foreach_elem(&auths, auth) + auth_deinit(auth); + event_unref(&auth_event); +} + +void auths_free(void) +{ + struct auth **auth; + unsigned int i, count; + + /* deinit in reverse order, because modules have been allocated by + the first auth pool that used them */ + auth = array_get_modifiable(&auths, &count); + for (i = count; i > 0; i--) + pool_unref(&auth[i-1]->pool); + array_free(&auths); +} diff --git a/src/auth/auth.h b/src/auth/auth.h new file mode 100644 index 0000000..3ca5a9b --- /dev/null +++ b/src/auth/auth.h @@ -0,0 +1,91 @@ +#ifndef AUTH_H +#define AUTH_H + +#include "auth-settings.h" + +#define PASSWORD_HIDDEN_STR "<hidden>" + +ARRAY_DEFINE_TYPE(auth, struct auth *); +extern ARRAY_TYPE(auth) auths; + +enum auth_passdb_skip { + AUTH_PASSDB_SKIP_NEVER, + AUTH_PASSDB_SKIP_AUTHENTICATED, + AUTH_PASSDB_SKIP_UNAUTHENTICATED +}; + +enum auth_userdb_skip { + AUTH_USERDB_SKIP_NEVER, + AUTH_USERDB_SKIP_FOUND, + AUTH_USERDB_SKIP_NOTFOUND +}; + +enum auth_db_rule { + AUTH_DB_RULE_RETURN, + AUTH_DB_RULE_RETURN_OK, + AUTH_DB_RULE_RETURN_FAIL, + AUTH_DB_RULE_CONTINUE, + AUTH_DB_RULE_CONTINUE_OK, + AUTH_DB_RULE_CONTINUE_FAIL +}; + +struct auth_passdb { + struct auth_passdb *next; + + const struct auth_passdb_settings *set; + struct passdb_module *passdb; + + /* The caching key for this passdb, or NULL if caching isn't wanted. */ + const char *cache_key; + + struct passdb_template *default_fields_tmpl; + struct passdb_template *override_fields_tmpl; + + enum auth_passdb_skip skip; + enum auth_db_rule result_success; + enum auth_db_rule result_failure; + enum auth_db_rule result_internalfail; +}; + +struct auth_userdb { + struct auth_userdb *next; + + const struct auth_userdb_settings *set; + struct userdb_module *userdb; + + /* The caching key for this userdb, or NULL if caching isn't wanted. */ + const char *cache_key; + + struct userdb_template *default_fields_tmpl; + struct userdb_template *override_fields_tmpl; + + enum auth_userdb_skip skip; + enum auth_db_rule result_success; + enum auth_db_rule result_failure; + enum auth_db_rule result_internalfail; +}; + +struct auth { + pool_t pool; + const char *service; + const struct auth_settings *set; + + const struct mechanisms_register *reg; + struct auth_passdb *masterdbs; + struct auth_passdb *passdbs; + struct auth_userdb *userdbs; +}; + +extern struct auth_penalty *auth_penalty; + +struct auth *auth_find_service(const char *name); +struct auth *auth_default_service(void); + +void auths_preinit(const struct auth_settings *set, pool_t pool, + const struct mechanisms_register *reg, + const char *const *services); +void auths_init(void); +void auths_deinit(void); +void auths_free(void); + +#endif diff --git a/src/auth/checkpassword-reply.c b/src/auth/checkpassword-reply.c new file mode 100644 index 0000000..71f231a --- /dev/null +++ b/src/auth/checkpassword-reply.c @@ -0,0 +1,110 @@ +/* simple checkpassword wrapper to send userdb data back to dovecot-auth */ + +#include "lib.h" +#include "str.h" +#include "strescape.h" +#include "write-full.h" + +#include <unistd.h> + +int main(void) +{ + string_t *str; + const char *user, *home, *authorized, *orig_uid_env; + const char *extra_env, *key, *value, *const *tmp; + bool uid_found = FALSE, gid_found = FALSE; + uid_t orig_uid; + + lib_init(); + str = t_str_new(1024); + + orig_uid_env = getenv("ORIG_UID"); + if (orig_uid_env == NULL || str_to_uid(orig_uid_env, &orig_uid) < 0) + orig_uid = (uid_t)-1; + + /* ORIG_UID should have the auth process's UID that forked us. + if the checkpassword changed the UID, this could be a security hole + because the UID's other processes can ptrace this process and write + any kind of a reply to fd 4. so we can run only if: + + a) INSECURE_SETUID environment is set. + b) process isn't ptraceable (this binary is setuid/setgid) + c) checkpassword didn't actually change the UID (but used + userdb_uid instead) + */ + if (getenv("INSECURE_SETUID") == NULL && + (orig_uid == (uid_t)-1 || orig_uid != getuid()) && + getuid() == geteuid() && getgid() == getegid()) { + if (orig_uid_env == NULL) { + i_error("checkpassword: ORIG_UID environment was dropped by checkpassword. " + "Can't verify if we're safe to run. See " + "http://wiki2.dovecot.org/AuthDatabase/CheckPassword#Security"); + } else { + i_error("checkpassword: The checkpassword couldn't be run securely. See " + "http://wiki2.dovecot.org/AuthDatabase/CheckPassword#Security"); + } + return 111; + } + + user = getenv("USER"); + if (user != NULL) { + if (strchr(user, '\t') != NULL) { + i_error("checkpassword: USER contains TAB"); + return 1; + } + str_printfa(str, "user="); + str_append_tabescaped(str, user); + str_append_c(str, '\t'); + } + + home = getenv("HOME"); + if (home != NULL) { + if (strchr(home, '\t') != NULL) { + i_error("checkpassword: HOME contains TAB"); + return 1; + } + str_printfa(str, "userdb_home="); + str_append_tabescaped(str, home); + str_append_c(str, '\t'); + } + + extra_env = getenv("EXTRA"); + if (extra_env != NULL) { + for (tmp = t_strsplit(extra_env, " "); *tmp != NULL; tmp++) { + value = getenv(*tmp); + if (value != NULL) { + key = t_str_lcase(*tmp); + if (strcmp(key, "userdb_uid") == 0) + uid_found = TRUE; + else if (strcmp(key, "userdb_gid") == 0) + gid_found = TRUE; + str_append_tabescaped(str, key); + str_append_c(str, '='); + str_append_tabescaped(str, value); + str_append_c(str, '\t'); + } + } + } + if (!uid_found) + str_printfa(str, "userdb_uid=%s\t", dec2str(getuid())); + if (!gid_found) + str_printfa(str, "userdb_gid=%s\t", dec2str(getgid())); + + i_assert(str_len(str) > 0); + + if (write_full(4, str_data(str), str_len(str)) < 0) { + i_error("checkpassword: write_full() failed: %m"); + lib_exit(111); + } + authorized = getenv("AUTHORIZED"); + if (authorized == NULL) { + /* authentication */ + return 0; + } else if (strcmp(authorized, "2") == 0) { + /* successful passdb/userdb lookup */ + return 2; + } else { + i_error("checkpassword: Script doesn't support passdb/userdb lookup"); + return 111; + } +} diff --git a/src/auth/crypt-blowfish.c b/src/auth/crypt-blowfish.c new file mode 100644 index 0000000..460f8cf --- /dev/null +++ b/src/auth/crypt-blowfish.c @@ -0,0 +1,900 @@ +/* + * The crypt_blowfish homepage is: + * + * http://www.openwall.com/crypt/ + * + * This code comes from John the Ripper password cracker, with reentrant + * and crypt(3) interfaces added, but optimizations specific to password + * cracking removed. + * + * Written by Solar Designer <solar at openwall.com> in 1998-2014. + * No copyright is claimed, and the software is hereby placed in the public + * domain. In case this attempt to disclaim copyright and place the software + * in the public domain is deemed null and void, then the software is + * Copyright (c) 1998-2014 Solar Designer and it is hereby released to the + * general public under the following terms: + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted. + * + * There's ABSOLUTELY NO WARRANTY, express or implied. + * + * It is my intent that you should be able to use this on your system, + * as part of a software package, or anywhere else to improve security, + * ensure compatibility, or for any other purpose. I would appreciate + * it if you give credit where it is due and keep your modifications in + * the public domain as well, but I don't require that in order to let + * you place this code and any modifications you make under a license + * of your choice. + * + * This implementation is fully compatible with OpenBSD's bcrypt.c for prefix + * "$2b$", originally by Niels Provos <provos at citi.umich.edu>, and it uses + * some of his ideas. The password hashing algorithm was designed by David + * Mazieres <dm at lcs.mit.edu>. For information on the level of + * compatibility for bcrypt hash prefixes other than "$2b$", please refer to + * the comments in BF_set_key() below and to the included crypt(3) man page. + * + * There's a paper on the algorithm that explains its design decisions: + * + * http://www.usenix.org/events/usenix99/provos.html + * + * Some of the tricks in BF_ROUND might be inspired by Eric Young's + * Blowfish library (I can't be sure if I would think of something if I + * hadn't seen his code). + * + * 2017-10-10 - Adapted for dovecot code by Aki Tuomi <aki.tuomi@dovecot.fi> + */ + +#include "lib.h" + +#include <errno.h> +#ifndef __set_errno +#define __set_errno(val) errno = (val) +#endif + +/* Just to make sure the prototypes match the actual definitions */ +#include "crypt-blowfish.h" + +#ifdef __i386__ +#define BF_SCALE 1 +#elif defined(__x86_64__) || defined(__alpha__) || defined(__hppa__) +#define BF_SCALE 1 +#else +#define BF_SCALE 0 +#endif + +typedef unsigned int BF_word; +typedef signed int BF_word_signed; + +/* Number of Blowfish rounds, this is also hardcoded into a few places */ +#define BF_N 16 + +typedef BF_word BF_key[BF_N + 2]; + +typedef struct { + BF_word S[4][0x100]; + BF_key P; +} BF_ctx; + +/* + * Magic IV for 64 Blowfish encryptions that we do at the end. + * The string is "OrpheanBeholderScryDoubt" on big-endian. + */ +static BF_word BF_magic_w[6] = { + 0x4F727068, 0x65616E42, 0x65686F6C, + 0x64657253, 0x63727944, 0x6F756274 +}; + +/* + * P-box and S-box tables initialized with digits of Pi. + */ +static BF_ctx BF_init_state = { + { + { + 0xd1310ba6, 0x98dfb5ac, 0x2ffd72db, 0xd01adfb7, + 0xb8e1afed, 0x6a267e96, 0xba7c9045, 0xf12c7f99, + 0x24a19947, 0xb3916cf7, 0x0801f2e2, 0x858efc16, + 0x636920d8, 0x71574e69, 0xa458fea3, 0xf4933d7e, + 0x0d95748f, 0x728eb658, 0x718bcd58, 0x82154aee, + 0x7b54a41d, 0xc25a59b5, 0x9c30d539, 0x2af26013, + 0xc5d1b023, 0x286085f0, 0xca417918, 0xb8db38ef, + 0x8e79dcb0, 0x603a180e, 0x6c9e0e8b, 0xb01e8a3e, + 0xd71577c1, 0xbd314b27, 0x78af2fda, 0x55605c60, + 0xe65525f3, 0xaa55ab94, 0x57489862, 0x63e81440, + 0x55ca396a, 0x2aab10b6, 0xb4cc5c34, 0x1141e8ce, + 0xa15486af, 0x7c72e993, 0xb3ee1411, 0x636fbc2a, + 0x2ba9c55d, 0x741831f6, 0xce5c3e16, 0x9b87931e, + 0xafd6ba33, 0x6c24cf5c, 0x7a325381, 0x28958677, + 0x3b8f4898, 0x6b4bb9af, 0xc4bfe81b, 0x66282193, + 0x61d809cc, 0xfb21a991, 0x487cac60, 0x5dec8032, + 0xef845d5d, 0xe98575b1, 0xdc262302, 0xeb651b88, + 0x23893e81, 0xd396acc5, 0x0f6d6ff3, 0x83f44239, + 0x2e0b4482, 0xa4842004, 0x69c8f04a, 0x9e1f9b5e, + 0x21c66842, 0xf6e96c9a, 0x670c9c61, 0xabd388f0, + 0x6a51a0d2, 0xd8542f68, 0x960fa728, 0xab5133a3, + 0x6eef0b6c, 0x137a3be4, 0xba3bf050, 0x7efb2a98, + 0xa1f1651d, 0x39af0176, 0x66ca593e, 0x82430e88, + 0x8cee8619, 0x456f9fb4, 0x7d84a5c3, 0x3b8b5ebe, + 0xe06f75d8, 0x85c12073, 0x401a449f, 0x56c16aa6, + 0x4ed3aa62, 0x363f7706, 0x1bfedf72, 0x429b023d, + 0x37d0d724, 0xd00a1248, 0xdb0fead3, 0x49f1c09b, + 0x075372c9, 0x80991b7b, 0x25d479d8, 0xf6e8def7, + 0xe3fe501a, 0xb6794c3b, 0x976ce0bd, 0x04c006ba, + 0xc1a94fb6, 0x409f60c4, 0x5e5c9ec2, 0x196a2463, + 0x68fb6faf, 0x3e6c53b5, 0x1339b2eb, 0x3b52ec6f, + 0x6dfc511f, 0x9b30952c, 0xcc814544, 0xaf5ebd09, + 0xbee3d004, 0xde334afd, 0x660f2807, 0x192e4bb3, + 0xc0cba857, 0x45c8740f, 0xd20b5f39, 0xb9d3fbdb, + 0x5579c0bd, 0x1a60320a, 0xd6a100c6, 0x402c7279, + 0x679f25fe, 0xfb1fa3cc, 0x8ea5e9f8, 0xdb3222f8, + 0x3c7516df, 0xfd616b15, 0x2f501ec8, 0xad0552ab, + 0x323db5fa, 0xfd238760, 0x53317b48, 0x3e00df82, + 0x9e5c57bb, 0xca6f8ca0, 0x1a87562e, 0xdf1769db, + 0xd542a8f6, 0x287effc3, 0xac6732c6, 0x8c4f5573, + 0x695b27b0, 0xbbca58c8, 0xe1ffa35d, 0xb8f011a0, + 0x10fa3d98, 0xfd2183b8, 0x4afcb56c, 0x2dd1d35b, + 0x9a53e479, 0xb6f84565, 0xd28e49bc, 0x4bfb9790, + 0xe1ddf2da, 0xa4cb7e33, 0x62fb1341, 0xcee4c6e8, + 0xef20cada, 0x36774c01, 0xd07e9efe, 0x2bf11fb4, + 0x95dbda4d, 0xae909198, 0xeaad8e71, 0x6b93d5a0, + 0xd08ed1d0, 0xafc725e0, 0x8e3c5b2f, 0x8e7594b7, + 0x8ff6e2fb, 0xf2122b64, 0x8888b812, 0x900df01c, + 0x4fad5ea0, 0x688fc31c, 0xd1cff191, 0xb3a8c1ad, + 0x2f2f2218, 0xbe0e1777, 0xea752dfe, 0x8b021fa1, + 0xe5a0cc0f, 0xb56f74e8, 0x18acf3d6, 0xce89e299, + 0xb4a84fe0, 0xfd13e0b7, 0x7cc43b81, 0xd2ada8d9, + 0x165fa266, 0x80957705, 0x93cc7314, 0x211a1477, + 0xe6ad2065, 0x77b5fa86, 0xc75442f5, 0xfb9d35cf, + 0xebcdaf0c, 0x7b3e89a0, 0xd6411bd3, 0xae1e7e49, + 0x00250e2d, 0x2071b35e, 0x226800bb, 0x57b8e0af, + 0x2464369b, 0xf009b91e, 0x5563911d, 0x59dfa6aa, + 0x78c14389, 0xd95a537f, 0x207d5ba2, 0x02e5b9c5, + 0x83260376, 0x6295cfa9, 0x11c81968, 0x4e734a41, + 0xb3472dca, 0x7b14a94a, 0x1b510052, 0x9a532915, + 0xd60f573f, 0xbc9bc6e4, 0x2b60a476, 0x81e67400, + 0x08ba6fb5, 0x571be91f, 0xf296ec6b, 0x2a0dd915, + 0xb6636521, 0xe7b9f9b6, 0xff34052e, 0xc5855664, + 0x53b02d5d, 0xa99f8fa1, 0x08ba4799, 0x6e85076a + }, { + 0x4b7a70e9, 0xb5b32944, 0xdb75092e, 0xc4192623, + 0xad6ea6b0, 0x49a7df7d, 0x9cee60b8, 0x8fedb266, + 0xecaa8c71, 0x699a17ff, 0x5664526c, 0xc2b19ee1, + 0x193602a5, 0x75094c29, 0xa0591340, 0xe4183a3e, + 0x3f54989a, 0x5b429d65, 0x6b8fe4d6, 0x99f73fd6, + 0xa1d29c07, 0xefe830f5, 0x4d2d38e6, 0xf0255dc1, + 0x4cdd2086, 0x8470eb26, 0x6382e9c6, 0x021ecc5e, + 0x09686b3f, 0x3ebaefc9, 0x3c971814, 0x6b6a70a1, + 0x687f3584, 0x52a0e286, 0xb79c5305, 0xaa500737, + 0x3e07841c, 0x7fdeae5c, 0x8e7d44ec, 0x5716f2b8, + 0xb03ada37, 0xf0500c0d, 0xf01c1f04, 0x0200b3ff, + 0xae0cf51a, 0x3cb574b2, 0x25837a58, 0xdc0921bd, + 0xd19113f9, 0x7ca92ff6, 0x94324773, 0x22f54701, + 0x3ae5e581, 0x37c2dadc, 0xc8b57634, 0x9af3dda7, + 0xa9446146, 0x0fd0030e, 0xecc8c73e, 0xa4751e41, + 0xe238cd99, 0x3bea0e2f, 0x3280bba1, 0x183eb331, + 0x4e548b38, 0x4f6db908, 0x6f420d03, 0xf60a04bf, + 0x2cb81290, 0x24977c79, 0x5679b072, 0xbcaf89af, + 0xde9a771f, 0xd9930810, 0xb38bae12, 0xdccf3f2e, + 0x5512721f, 0x2e6b7124, 0x501adde6, 0x9f84cd87, + 0x7a584718, 0x7408da17, 0xbc9f9abc, 0xe94b7d8c, + 0xec7aec3a, 0xdb851dfa, 0x63094366, 0xc464c3d2, + 0xef1c1847, 0x3215d908, 0xdd433b37, 0x24c2ba16, + 0x12a14d43, 0x2a65c451, 0x50940002, 0x133ae4dd, + 0x71dff89e, 0x10314e55, 0x81ac77d6, 0x5f11199b, + 0x043556f1, 0xd7a3c76b, 0x3c11183b, 0x5924a509, + 0xf28fe6ed, 0x97f1fbfa, 0x9ebabf2c, 0x1e153c6e, + 0x86e34570, 0xeae96fb1, 0x860e5e0a, 0x5a3e2ab3, + 0x771fe71c, 0x4e3d06fa, 0x2965dcb9, 0x99e71d0f, + 0x803e89d6, 0x5266c825, 0x2e4cc978, 0x9c10b36a, + 0xc6150eba, 0x94e2ea78, 0xa5fc3c53, 0x1e0a2df4, + 0xf2f74ea7, 0x361d2b3d, 0x1939260f, 0x19c27960, + 0x5223a708, 0xf71312b6, 0xebadfe6e, 0xeac31f66, + 0xe3bc4595, 0xa67bc883, 0xb17f37d1, 0x018cff28, + 0xc332ddef, 0xbe6c5aa5, 0x65582185, 0x68ab9802, + 0xeecea50f, 0xdb2f953b, 0x2aef7dad, 0x5b6e2f84, + 0x1521b628, 0x29076170, 0xecdd4775, 0x619f1510, + 0x13cca830, 0xeb61bd96, 0x0334fe1e, 0xaa0363cf, + 0xb5735c90, 0x4c70a239, 0xd59e9e0b, 0xcbaade14, + 0xeecc86bc, 0x60622ca7, 0x9cab5cab, 0xb2f3846e, + 0x648b1eaf, 0x19bdf0ca, 0xa02369b9, 0x655abb50, + 0x40685a32, 0x3c2ab4b3, 0x319ee9d5, 0xc021b8f7, + 0x9b540b19, 0x875fa099, 0x95f7997e, 0x623d7da8, + 0xf837889a, 0x97e32d77, 0x11ed935f, 0x16681281, + 0x0e358829, 0xc7e61fd6, 0x96dedfa1, 0x7858ba99, + 0x57f584a5, 0x1b227263, 0x9b83c3ff, 0x1ac24696, + 0xcdb30aeb, 0x532e3054, 0x8fd948e4, 0x6dbc3128, + 0x58ebf2ef, 0x34c6ffea, 0xfe28ed61, 0xee7c3c73, + 0x5d4a14d9, 0xe864b7e3, 0x42105d14, 0x203e13e0, + 0x45eee2b6, 0xa3aaabea, 0xdb6c4f15, 0xfacb4fd0, + 0xc742f442, 0xef6abbb5, 0x654f3b1d, 0x41cd2105, + 0xd81e799e, 0x86854dc7, 0xe44b476a, 0x3d816250, + 0xcf62a1f2, 0x5b8d2646, 0xfc8883a0, 0xc1c7b6a3, + 0x7f1524c3, 0x69cb7492, 0x47848a0b, 0x5692b285, + 0x095bbf00, 0xad19489d, 0x1462b174, 0x23820e00, + 0x58428d2a, 0x0c55f5ea, 0x1dadf43e, 0x233f7061, + 0x3372f092, 0x8d937e41, 0xd65fecf1, 0x6c223bdb, + 0x7cde3759, 0xcbee7460, 0x4085f2a7, 0xce77326e, + 0xa6078084, 0x19f8509e, 0xe8efd855, 0x61d99735, + 0xa969a7aa, 0xc50c06c2, 0x5a04abfc, 0x800bcadc, + 0x9e447a2e, 0xc3453484, 0xfdd56705, 0x0e1e9ec9, + 0xdb73dbd3, 0x105588cd, 0x675fda79, 0xe3674340, + 0xc5c43465, 0x713e38d8, 0x3d28f89e, 0xf16dff20, + 0x153e21e7, 0x8fb03d4a, 0xe6e39f2b, 0xdb83adf7 + }, { + 0xe93d5a68, 0x948140f7, 0xf64c261c, 0x94692934, + 0x411520f7, 0x7602d4f7, 0xbcf46b2e, 0xd4a20068, + 0xd4082471, 0x3320f46a, 0x43b7d4b7, 0x500061af, + 0x1e39f62e, 0x97244546, 0x14214f74, 0xbf8b8840, + 0x4d95fc1d, 0x96b591af, 0x70f4ddd3, 0x66a02f45, + 0xbfbc09ec, 0x03bd9785, 0x7fac6dd0, 0x31cb8504, + 0x96eb27b3, 0x55fd3941, 0xda2547e6, 0xabca0a9a, + 0x28507825, 0x530429f4, 0x0a2c86da, 0xe9b66dfb, + 0x68dc1462, 0xd7486900, 0x680ec0a4, 0x27a18dee, + 0x4f3ffea2, 0xe887ad8c, 0xb58ce006, 0x7af4d6b6, + 0xaace1e7c, 0xd3375fec, 0xce78a399, 0x406b2a42, + 0x20fe9e35, 0xd9f385b9, 0xee39d7ab, 0x3b124e8b, + 0x1dc9faf7, 0x4b6d1856, 0x26a36631, 0xeae397b2, + 0x3a6efa74, 0xdd5b4332, 0x6841e7f7, 0xca7820fb, + 0xfb0af54e, 0xd8feb397, 0x454056ac, 0xba489527, + 0x55533a3a, 0x20838d87, 0xfe6ba9b7, 0xd096954b, + 0x55a867bc, 0xa1159a58, 0xcca92963, 0x99e1db33, + 0xa62a4a56, 0x3f3125f9, 0x5ef47e1c, 0x9029317c, + 0xfdf8e802, 0x04272f70, 0x80bb155c, 0x05282ce3, + 0x95c11548, 0xe4c66d22, 0x48c1133f, 0xc70f86dc, + 0x07f9c9ee, 0x41041f0f, 0x404779a4, 0x5d886e17, + 0x325f51eb, 0xd59bc0d1, 0xf2bcc18f, 0x41113564, + 0x257b7834, 0x602a9c60, 0xdff8e8a3, 0x1f636c1b, + 0x0e12b4c2, 0x02e1329e, 0xaf664fd1, 0xcad18115, + 0x6b2395e0, 0x333e92e1, 0x3b240b62, 0xeebeb922, + 0x85b2a20e, 0xe6ba0d99, 0xde720c8c, 0x2da2f728, + 0xd0127845, 0x95b794fd, 0x647d0862, 0xe7ccf5f0, + 0x5449a36f, 0x877d48fa, 0xc39dfd27, 0xf33e8d1e, + 0x0a476341, 0x992eff74, 0x3a6f6eab, 0xf4f8fd37, + 0xa812dc60, 0xa1ebddf8, 0x991be14c, 0xdb6e6b0d, + 0xc67b5510, 0x6d672c37, 0x2765d43b, 0xdcd0e804, + 0xf1290dc7, 0xcc00ffa3, 0xb5390f92, 0x690fed0b, + 0x667b9ffb, 0xcedb7d9c, 0xa091cf0b, 0xd9155ea3, + 0xbb132f88, 0x515bad24, 0x7b9479bf, 0x763bd6eb, + 0x37392eb3, 0xcc115979, 0x8026e297, 0xf42e312d, + 0x6842ada7, 0xc66a2b3b, 0x12754ccc, 0x782ef11c, + 0x6a124237, 0xb79251e7, 0x06a1bbe6, 0x4bfb6350, + 0x1a6b1018, 0x11caedfa, 0x3d25bdd8, 0xe2e1c3c9, + 0x44421659, 0x0a121386, 0xd90cec6e, 0xd5abea2a, + 0x64af674e, 0xda86a85f, 0xbebfe988, 0x64e4c3fe, + 0x9dbc8057, 0xf0f7c086, 0x60787bf8, 0x6003604d, + 0xd1fd8346, 0xf6381fb0, 0x7745ae04, 0xd736fccc, + 0x83426b33, 0xf01eab71, 0xb0804187, 0x3c005e5f, + 0x77a057be, 0xbde8ae24, 0x55464299, 0xbf582e61, + 0x4e58f48f, 0xf2ddfda2, 0xf474ef38, 0x8789bdc2, + 0x5366f9c3, 0xc8b38e74, 0xb475f255, 0x46fcd9b9, + 0x7aeb2661, 0x8b1ddf84, 0x846a0e79, 0x915f95e2, + 0x466e598e, 0x20b45770, 0x8cd55591, 0xc902de4c, + 0xb90bace1, 0xbb8205d0, 0x11a86248, 0x7574a99e, + 0xb77f19b6, 0xe0a9dc09, 0x662d09a1, 0xc4324633, + 0xe85a1f02, 0x09f0be8c, 0x4a99a025, 0x1d6efe10, + 0x1ab93d1d, 0x0ba5a4df, 0xa186f20f, 0x2868f169, + 0xdcb7da83, 0x573906fe, 0xa1e2ce9b, 0x4fcd7f52, + 0x50115e01, 0xa70683fa, 0xa002b5c4, 0x0de6d027, + 0x9af88c27, 0x773f8641, 0xc3604c06, 0x61a806b5, + 0xf0177a28, 0xc0f586e0, 0x006058aa, 0x30dc7d62, + 0x11e69ed7, 0x2338ea63, 0x53c2dd94, 0xc2c21634, + 0xbbcbee56, 0x90bcb6de, 0xebfc7da1, 0xce591d76, + 0x6f05e409, 0x4b7c0188, 0x39720a3d, 0x7c927c24, + 0x86e3725f, 0x724d9db9, 0x1ac15bb4, 0xd39eb8fc, + 0xed545578, 0x08fca5b5, 0xd83d7cd3, 0x4dad0fc4, + 0x1e50ef5e, 0xb161e6f8, 0xa28514d9, 0x6c51133c, + 0x6fd5c7e7, 0x56e14ec4, 0x362abfce, 0xddc6c837, + 0xd79a3234, 0x92638212, 0x670efa8e, 0x406000e0 + }, { + 0x3a39ce37, 0xd3faf5cf, 0xabc27737, 0x5ac52d1b, + 0x5cb0679e, 0x4fa33742, 0xd3822740, 0x99bc9bbe, + 0xd5118e9d, 0xbf0f7315, 0xd62d1c7e, 0xc700c47b, + 0xb78c1b6b, 0x21a19045, 0xb26eb1be, 0x6a366eb4, + 0x5748ab2f, 0xbc946e79, 0xc6a376d2, 0x6549c2c8, + 0x530ff8ee, 0x468dde7d, 0xd5730a1d, 0x4cd04dc6, + 0x2939bbdb, 0xa9ba4650, 0xac9526e8, 0xbe5ee304, + 0xa1fad5f0, 0x6a2d519a, 0x63ef8ce2, 0x9a86ee22, + 0xc089c2b8, 0x43242ef6, 0xa51e03aa, 0x9cf2d0a4, + 0x83c061ba, 0x9be96a4d, 0x8fe51550, 0xba645bd6, + 0x2826a2f9, 0xa73a3ae1, 0x4ba99586, 0xef5562e9, + 0xc72fefd3, 0xf752f7da, 0x3f046f69, 0x77fa0a59, + 0x80e4a915, 0x87b08601, 0x9b09e6ad, 0x3b3ee593, + 0xe990fd5a, 0x9e34d797, 0x2cf0b7d9, 0x022b8b51, + 0x96d5ac3a, 0x017da67d, 0xd1cf3ed6, 0x7c7d2d28, + 0x1f9f25cf, 0xadf2b89b, 0x5ad6b472, 0x5a88f54c, + 0xe029ac71, 0xe019a5e6, 0x47b0acfd, 0xed93fa9b, + 0xe8d3c48d, 0x283b57cc, 0xf8d56629, 0x79132e28, + 0x785f0191, 0xed756055, 0xf7960e44, 0xe3d35e8c, + 0x15056dd4, 0x88f46dba, 0x03a16125, 0x0564f0bd, + 0xc3eb9e15, 0x3c9057a2, 0x97271aec, 0xa93a072a, + 0x1b3f6d9b, 0x1e6321f5, 0xf59c66fb, 0x26dcf319, + 0x7533d928, 0xb155fdf5, 0x03563482, 0x8aba3cbb, + 0x28517711, 0xc20ad9f8, 0xabcc5167, 0xccad925f, + 0x4de81751, 0x3830dc8e, 0x379d5862, 0x9320f991, + 0xea7a90c2, 0xfb3e7bce, 0x5121ce64, 0x774fbe32, + 0xa8b6e37e, 0xc3293d46, 0x48de5369, 0x6413e680, + 0xa2ae0810, 0xdd6db224, 0x69852dfd, 0x09072166, + 0xb39a460a, 0x6445c0dd, 0x586cdecf, 0x1c20c8ae, + 0x5bbef7dd, 0x1b588d40, 0xccd2017f, 0x6bb4e3bb, + 0xdda26a7e, 0x3a59ff45, 0x3e350a44, 0xbcb4cdd5, + 0x72eacea8, 0xfa6484bb, 0x8d6612ae, 0xbf3c6f47, + 0xd29be463, 0x542f5d9e, 0xaec2771b, 0xf64e6370, + 0x740e0d8d, 0xe75b1357, 0xf8721671, 0xaf537d5d, + 0x4040cb08, 0x4eb4e2cc, 0x34d2466a, 0x0115af84, + 0xe1b00428, 0x95983a1d, 0x06b89fb4, 0xce6ea048, + 0x6f3f3b82, 0x3520ab82, 0x011a1d4b, 0x277227f8, + 0x611560b1, 0xe7933fdc, 0xbb3a792b, 0x344525bd, + 0xa08839e1, 0x51ce794b, 0x2f32c9b7, 0xa01fbac9, + 0xe01cc87e, 0xbcc7d1f6, 0xcf0111c3, 0xa1e8aac7, + 0x1a908749, 0xd44fbd9a, 0xd0dadecb, 0xd50ada38, + 0x0339c32a, 0xc6913667, 0x8df9317c, 0xe0b12b4f, + 0xf79e59b7, 0x43f5bb3a, 0xf2d519ff, 0x27d9459c, + 0xbf97222c, 0x15e6fc2a, 0x0f91fc71, 0x9b941525, + 0xfae59361, 0xceb69ceb, 0xc2a86459, 0x12baa8d1, + 0xb6c1075e, 0xe3056a0c, 0x10d25065, 0xcb03a442, + 0xe0ec6e0e, 0x1698db3b, 0x4c98a0be, 0x3278e964, + 0x9f1f9532, 0xe0d392df, 0xd3a0342b, 0x8971f21e, + 0x1b0a7441, 0x4ba3348c, 0xc5be7120, 0xc37632d8, + 0xdf359f8d, 0x9b992f2e, 0xe60b6f47, 0x0fe3f11d, + 0xe54cda54, 0x1edad891, 0xce6279cf, 0xcd3e7e6f, + 0x1618b166, 0xfd2c1d05, 0x848fd2c5, 0xf6fb2299, + 0xf523f357, 0xa6327623, 0x93a83531, 0x56cccd02, + 0xacf08162, 0x5a75ebb5, 0x6e163697, 0x88d273cc, + 0xde966292, 0x81b949d0, 0x4c50901b, 0x71c65614, + 0xe6c6c7bd, 0x327a140a, 0x45e1d006, 0xc3f27b9a, + 0xc9aa53fd, 0x62a80f00, 0xbb25bfe2, 0x35bdd2f6, + 0x71126905, 0xb2040222, 0xb6cbcf7c, 0xcd769c2b, + 0x53113ec0, 0x1640e3d3, 0x38abbd60, 0x2547adf0, + 0xba38209c, 0xf746ce76, 0x77afa1c5, 0x20756060, + 0x85cbfe4e, 0x8ae88dd8, 0x7aaaf9b0, 0x4cf9aa7e, + 0x1948c25c, 0x02fb8a8c, 0x01c36ae4, 0xd6ebe1f9, + 0x90d4f869, 0xa65cdea0, 0x3f09252d, 0xc208e69f, + 0xb74e6132, 0xce77e25b, 0x578fdfe3, 0x3ac372e6 + } + }, { + 0x243f6a88, 0x85a308d3, 0x13198a2e, 0x03707344, + 0xa4093822, 0x299f31d0, 0x082efa98, 0xec4e6c89, + 0x452821e6, 0x38d01377, 0xbe5466cf, 0x34e90c6c, + 0xc0ac29b7, 0xc97c50dd, 0x3f84d5b5, 0xb5470917, + 0x9216d5d9, 0x8979fb1b + } +}; + +static unsigned char BF_itoa64[64 + 1] = + "./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + +static unsigned char BF_atoi64[0x60] = { + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 0, 1, + 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 64, 64, 64, 64, 64, + 64, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, + 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 64, 64, 64, 64, 64, + 64, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, + 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 64, 64, 64, 64, 64 +}; + +#define BF_safe_atoi64(dst, src) \ +{ \ + tmp = (unsigned char)(src); \ + if ((unsigned int)(tmp -= 0x20) >= 0x60) return -1; \ + tmp = BF_atoi64[tmp]; \ + if (tmp > 63) return -1; \ + (dst) = tmp; \ +} + +static int BF_decode(BF_word *dst, const char *src, size_t size) +{ + unsigned char *dptr = (unsigned char *)dst; + unsigned char *end = dptr + size; + const unsigned char *sptr = (const unsigned char *)src; + unsigned int tmp, c1, c2, c3, c4; + + do { + BF_safe_atoi64(c1, *sptr++); + BF_safe_atoi64(c2, *sptr++); + *dptr++ = (c1 << 2) | ((c2 & 0x30) >> 4); + if (dptr >= end) break; + + BF_safe_atoi64(c3, *sptr++); + *dptr++ = ((c2 & 0x0F) << 4) | ((c3 & 0x3C) >> 2); + if (dptr >= end) break; + + BF_safe_atoi64(c4, *sptr++); + *dptr++ = ((c3 & 0x03) << 6) | c4; + } while (dptr < end); + + return 0; +} + +static void BF_encode(char *dst, const BF_word *src, size_t size) +{ + const unsigned char *sptr = (const unsigned char *)src; + const unsigned char *end = sptr + size; + unsigned char *dptr = (unsigned char *)dst; + unsigned int c1, c2; + + do { + c1 = *sptr++; + *dptr++ = BF_itoa64[c1 >> 2]; + c1 = (c1 & 0x03) << 4; + if (sptr >= end) { + *dptr++ = BF_itoa64[c1]; + break; + } + + c2 = *sptr++; + c1 |= c2 >> 4; + *dptr++ = BF_itoa64[c1]; + c1 = (c2 & 0x0f) << 2; + if (sptr >= end) { + *dptr++ = BF_itoa64[c1]; + break; + } + + c2 = *sptr++; + c1 |= c2 >> 6; + *dptr++ = BF_itoa64[c1]; + *dptr++ = BF_itoa64[c2 & 0x3f]; + } while (sptr < end); +} + +static void ATTR_UNSIGNED_WRAPS +BF_swap(BF_word *x, int count) +{ + static int endianness_check = 1; + char *is_little_endian = (char *)&endianness_check; + BF_word tmp; + + if (*is_little_endian != 0) + do { + tmp = *x; + tmp = (tmp << 16) | (tmp >> 16); + *x++ = ((tmp & 0x00FF00FF) << 8) | ((tmp >> 8) & 0x00FF00FF); + } while (--count > 0); +} + +#if BF_SCALE +/* Architectures which can shift addresses left by 2 bits with no extra cost */ +#define BF_ROUND(L, R, N) \ + tmp1 = L & 0xFF; \ + tmp2 = L >> 8; \ + tmp2 &= 0xFF; \ + tmp3 = L >> 16; \ + tmp3 &= 0xFF; \ + tmp4 = L >> 24; \ + tmp1 = data.ctx.S[3][tmp1]; \ + tmp2 = data.ctx.S[2][tmp2]; \ + tmp3 = data.ctx.S[1][tmp3]; \ + tmp3 += data.ctx.S[0][tmp4]; \ + tmp3 ^= tmp2; \ + R ^= data.ctx.P[N + 1]; \ + tmp3 += tmp1; \ + R ^= tmp3; +#else +/* Architectures with no complicated addressing modes supported */ +#define BF_INDEX(S, i) \ + (*((BF_word *)(((unsigned char *)S) + (i)))) +#define BF_ROUND(L, R, N) \ + tmp1 = L & 0xFF; \ + tmp1 <<= 2; \ + tmp2 = L >> 6; \ + tmp2 &= 0x3FC; \ + tmp3 = L >> 14; \ + tmp3 &= 0x3FC; \ + tmp4 = L >> 22; \ + tmp4 &= 0x3FC; \ + tmp1 = BF_INDEX(data.ctx.S[3], tmp1); \ + tmp2 = BF_INDEX(data.ctx.S[2], tmp2); \ + tmp3 = BF_INDEX(data.ctx.S[1], tmp3); \ + tmp3 += BF_INDEX(data.ctx.S[0], tmp4); \ + tmp3 ^= tmp2; \ + R ^= data.ctx.P[N + 1]; \ + tmp3 += tmp1; \ + R ^= tmp3; +#endif + +/* + * Encrypt one block, BF_N is hardcoded here. + */ +#define BF_ENCRYPT \ + L ^= data.ctx.P[0]; \ + BF_ROUND(L, R, 0); \ + BF_ROUND(R, L, 1); \ + BF_ROUND(L, R, 2); \ + BF_ROUND(R, L, 3); \ + BF_ROUND(L, R, 4); \ + BF_ROUND(R, L, 5); \ + BF_ROUND(L, R, 6); \ + BF_ROUND(R, L, 7); \ + BF_ROUND(L, R, 8); \ + BF_ROUND(R, L, 9); \ + BF_ROUND(L, R, 10); \ + BF_ROUND(R, L, 11); \ + BF_ROUND(L, R, 12); \ + BF_ROUND(R, L, 13); \ + BF_ROUND(L, R, 14); \ + BF_ROUND(R, L, 15); \ + tmp4 = R; \ + R = L; \ + L = tmp4 ^ data.ctx.P[BF_N + 1]; + +#define BF_body() \ + L = R = 0; \ + ptr = data.ctx.P; \ + do { \ + ptr += 2; \ + BF_ENCRYPT; \ + *(ptr - 2) = L; \ + *(ptr - 1) = R; \ + } while (ptr < &data.ctx.P[BF_N + 2]); \ +\ + ptr = data.ctx.S[0]; \ + do { \ + ptr += 2; \ + BF_ENCRYPT; \ + *(ptr - 2) = L; \ + *(ptr - 1) = R; \ + } while (ptr < &data.ctx.S[3][0xFF]); + +static void ATTR_UNSIGNED_WRAPS +BF_set_key(const char *key, BF_key expanded, BF_key initial, + unsigned char flags) +{ + const char *ptr = key; + unsigned int bug, i, j; + BF_word safety, sign, diff, tmp[2]; + +/* + * There was a sign extension bug in older revisions of this function. While + * we would have liked to simply fix the bug and move on, we have to provide + * a backwards compatibility feature (essentially the bug) for some systems and + * a safety measure for some others. The latter is needed because for certain + * multiple inputs to the buggy algorithm there exist easily found inputs to + * the correct algorithm that produce the same hash. Thus, we optionally + * deviate from the correct algorithm just enough to avoid such collisions. + * While the bug itself affected the majority of passwords containing + * characters with the 8th bit set (although only a percentage of those in a + * collision-producing way), the anti-collision safety measure affects + * only a subset of passwords containing the '\xff' character (not even all of + * those passwords, just some of them). This character is not found in valid + * UTF-8 sequences and is rarely used in popular 8-bit character encodings. + * Thus, the safety measure is unlikely to cause much annoyance, and is a + * reasonable tradeoff to use when authenticating against existing hashes that + * are not reliably known to have been computed with the correct algorithm. + * + * We use an approach that tries to minimize side-channel leaks of password + * information - that is, we mostly use fixed-cost bitwise operations instead + * of branches or table lookups. (One conditional branch based on password + * length remains. It is not part of the bug aftermath, though, and is + * difficult and possibly unreasonable to avoid given the use of C strings by + * the caller, which results in similar timing leaks anyway.) + * + * For actual implementation, we set an array index in the variable "bug" + * (0 means no bug, 1 means sign extension bug emulation) and a flag in the + * variable "safety" (bit 16 is set when the safety measure is requested). + * Valid combinations of settings are: + * + * Prefix "$2a$": bug = 0, safety = 0x10000 + * Prefix "$2b$": bug = 0, safety = 0 + * Prefix "$2x$": bug = 1, safety = 0 + * Prefix "$2y$": bug = 0, safety = 0 + */ + bug = (unsigned int)flags & 1; + safety = ((BF_word)flags & 2) << 15; + + sign = diff = 0; + + for (i = 0; i < BF_N + 2; i++) { + tmp[0] = tmp[1] = 0; + for (j = 0; j < 4; j++) { + tmp[0] <<= 8; + tmp[0] |= (unsigned char)*ptr; /* correct */ + tmp[1] <<= 8; + tmp[1] |= (BF_word)(signed char)*ptr; /* bug */ +/* + * Sign extension in the first char has no effect - nothing to overwrite yet, + * and those extra 24 bits will be fully shifted out of the 32-bit word. For + * chars 2, 3, 4 in each four-char block, we set bit 7 of "sign" if sign + * extension in tmp[1] occurs. Once this flag is set, it remains set. + */ + if (j != 0) + sign |= tmp[1] & 0x80; + if (*ptr == '\0') + ptr = key; + else + ptr++; + } + diff |= tmp[0] ^ tmp[1]; /* Non-zero on any differences */ + + expanded[i] = tmp[bug]; + initial[i] = BF_init_state.P[i] ^ tmp[bug]; + } + +/* + * At this point, "diff" is zero iff the correct and buggy algorithms produced + * exactly the same result. If so and if "sign" is non-zero, which indicates + * that there was a non-benign sign extension, this means that we have a + * collision between the correctly computed hash for this password and a set of + * passwords that could be supplied to the buggy algorithm. Our safety measure + * is meant to protect from such many-buggy to one-correct collisions, by + * deviating from the correct algorithm in such cases. Let's check for this. + */ + diff |= diff >> 16; /* still zero iff exact match */ + diff &= 0xffff; /* ditto */ + diff += 0xffff; /* bit 16 set iff "diff" was non-zero (on non-match) */ + sign <<= 9; /* move the non-benign sign extension flag to bit 16 */ + sign &= ~diff & safety; /* action needed? */ + +/* + * If we have determined that we need to deviate from the correct algorithm, + * flip bit 16 in initial expanded key. (The choice of 16 is arbitrary, but + * let's stick to it now. It came out of the approach we used above, and it's + * not any worse than any other choice we could make.) + * + * It is crucial that we don't do the same to the expanded key used in the main + * Eksblowfish loop. By doing it to only one of these two, we deviate from a + * state that could be directly specified by a password to the buggy algorithm + * (and to the fully correct one as well, but that's a side-effect). + */ + initial[0] ^= sign; +} + +static const unsigned char flags_by_subtype[26] = + {2, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 4, 0}; + +static char * ATTR_NO_SANITIZE_UNDEFINED ATTR_NO_SANITIZE_INTEGER + ATTR_NO_SANITIZE_IMPLICIT_CONVERSION +BF_crypt(const char *key, const char *setting, + char *output, size_t size, + BF_word min) +{ + struct { + BF_ctx ctx; + BF_key expanded_key; + union { + BF_word salt[4]; + BF_word output[6]; + } binary; + } data; + BF_word L, R; + BF_word tmp1, tmp2, tmp3, tmp4; + BF_word *ptr; + BF_word count; + int i; + + if (size < 7 + 22 + 31 + 1) { + __set_errno(ERANGE); + return NULL; + } + + if (setting[0] != '$' || + setting[1] != '2' || + setting[2] < 'a' || setting[2] > 'z' || + flags_by_subtype[(unsigned int)(unsigned char)setting[2] - 'a'] == 0 || + setting[3] != '$' || + setting[4] < '0' || setting[4] > '3' || + setting[5] < '0' || setting[5] > '9' || + (setting[4] == '3' && setting[5] > '1') || + setting[6] != '$') { + __set_errno(EINVAL); + return NULL; + } + + count = (BF_word)1 << ((setting[4] - '0') * 10 + (setting[5] - '0')); + if (count < min || BF_decode(data.binary.salt, &setting[7], 16) != 0) { + __set_errno(EINVAL); + return NULL; + } + BF_swap(data.binary.salt, 4); + + BF_set_key(key, data.expanded_key, data.ctx.P, + flags_by_subtype[(unsigned int)(unsigned char)setting[2] - 'a']); + + memcpy(data.ctx.S, BF_init_state.S, sizeof(data.ctx.S)); + + L = R = 0; + for (i = 0; i < BF_N + 2; i += 2) { + L ^= data.binary.salt[i & 2]; + R ^= data.binary.salt[(i & 2) + 1]; + BF_ENCRYPT; + data.ctx.P[i] = L; + data.ctx.P[i + 1] = R; + } + + ptr = data.ctx.S[0]; + do { + ptr += 4; + L ^= data.binary.salt[(BF_N + 2) & 3]; + R ^= data.binary.salt[(BF_N + 3) & 3]; + BF_ENCRYPT; + *(ptr - 4) = L; + *(ptr - 3) = R; + + L ^= data.binary.salt[(BF_N + 4) & 3]; + R ^= data.binary.salt[(BF_N + 5) & 3]; + BF_ENCRYPT; + *(ptr - 2) = L; + *(ptr - 1) = R; + } while (ptr < &data.ctx.S[3][0xFF]); + + do { + bool done; + + for (i = 0; i < BF_N + 2; i += 2) { + data.ctx.P[i] ^= data.expanded_key[i]; + data.ctx.P[i + 1] ^= data.expanded_key[i + 1]; + } + + done = FALSE; + do { + BF_body(); + if (done) + break; + done = TRUE; + + tmp1 = data.binary.salt[0]; + tmp2 = data.binary.salt[1]; + tmp3 = data.binary.salt[2]; + tmp4 = data.binary.salt[3]; + for (i = 0; i < BF_N; i += 4) { + data.ctx.P[i] ^= tmp1; + data.ctx.P[i + 1] ^= tmp2; + data.ctx.P[i + 2] ^= tmp3; + data.ctx.P[i + 3] ^= tmp4; + } + data.ctx.P[16] ^= tmp1; + data.ctx.P[17] ^= tmp2; + } while (TRUE); + } while (--count > 0); + + for (i = 0; i < 6; i += 2) { + L = BF_magic_w[i]; + R = BF_magic_w[i + 1]; + + count = 64; + do { + BF_ENCRYPT; + } while (--count > 0); + + data.binary.output[i] = L; + data.binary.output[i + 1] = R; + } + + memcpy(output, setting, 7 + 22 - 1); + output[7 + 22 - 1] = BF_itoa64[(int) + BF_atoi64[(int)setting[7 + 22 - 1] - 0x20] & 0x30]; + +/* This has to be bug-compatible with the original implementation, so + * only encode 23 of the 24 bytes. :-) */ + BF_swap(data.binary.output, 6); + BF_encode(&output[7 + 22], data.binary.output, 23); + output[7 + 22 + 31] = '\0'; + + return output; +} + +int crypt_output_magic(const char *setting, char *output, size_t size) +{ + if (size < 3) + return -1; + + output[0] = '*'; + output[1] = '0'; + output[2] = '\0'; + + if (setting[0] == '*' && setting[1] == '0') + output[1] = '1'; + + return 0; +} + +/* + * Please preserve the runtime self-test. It serves two purposes at once: + * + * 1. We really can't afford the risk of producing incompatible hashes e.g. + * when there's something like gcc bug 26587 again, whereas an application or + * library integrating this code might not also integrate our external tests or + * it might not run them after every build. Even if it does, the miscompile + * might only occur on the production build, but not on a testing build (such + * as because of different optimization settings). It is painful to recover + * from incorrectly-computed hashes - merely fixing whatever broke is not + * enough. Thus, a proactive measure like this self-test is needed. + * + * 2. We don't want to leave sensitive data from our actual password hash + * computation on the stack or in registers. Previous revisions of the code + * would do explicit cleanups, but simply running the self-test after hash + * computation is more reliable. + * + * The performance cost of this quick self-test is around 0.6% at the "$2a$08" + * setting. + */ +char *crypt_blowfish_rn(const char *key, const char *setting, + char *output, size_t size) +{ + const char *test_key = "8b \xd0\xc1\xd2\xcf\xcc\xd8"; + const char *test_setting = "$2a$00$abcdefghijklmnopqrstuu"; + static const char * const test_hashes[2] = + {"i1D709vfamulimlGcq0qq3UvuUasvEa\0\x55", /* 'a', 'b', 'y' */ + "VUrPmXD6q/nVSSp7pNDhCR9071IfIRe\0\x55"}; /* 'x' */ + const char *test_hash = test_hashes[0]; + char *retval; + const char *p; + int save_errno; + bool ok; + struct { + char s[7 + 22 + 1]; + char o[7 + 22 + 31 + 1 + 1 + 1]; + } buf; + +/* Hash the supplied password */ + crypt_output_magic(setting, output, size); + retval = BF_crypt(key, setting, output, size, 16); + save_errno = errno; + +/* + * Do a quick self-test. It is important that we make both calls to BF_crypt() + * from the same scope such that they likely use the same stack locations, + * which makes the second call overwrite the first call's sensitive data on the + * stack and makes it more likely that any alignment related issues would be + * detected by the self-test. + */ + memcpy(buf.s, test_setting, sizeof(buf.s)); + if (retval != NULL) { + unsigned int flags = flags_by_subtype[ + (unsigned int)(unsigned char)setting[2] - 'a']; + test_hash = test_hashes[flags & 1]; + buf.s[2] = setting[2]; + } + memset(buf.o, 0x55, sizeof(buf.o)); + buf.o[sizeof(buf.o) - 1] = 0; + p = BF_crypt(test_key, buf.s, buf.o, sizeof(buf.o) - (1 + 1), 1); + + ok = (p == buf.o && + memcmp(p, buf.s, 7 + 22) == 0 && + memcmp(p + (7 + 22), test_hash, 31 + 1 + 1 + 1) == 0); + + { + const char *k = "\xff\xa3" "34" "\xff\xff\xff\xa3" "345"; + BF_key ae, ai, ye, yi; + BF_set_key(k, ae, ai, 2); /* $2a$ */ + BF_set_key(k, ye, yi, 4); /* $2y$ */ + ai[0] ^= 0x10000; /* undo the safety (for comparison) */ + ok = ok && ai[0] == 0xdb9c59bc && ye[17] == 0x33343500 && + memcmp(ae, ye, sizeof(ae)) == 0 && + memcmp(ai, yi, sizeof(ai)) == 0; + } + + __set_errno(save_errno); + if (ok) + return retval; + + i_unreached(); +} + +char *crypt_gensalt_blowfish_rn(const char *prefix, unsigned long count, + const char *input, size_t size, char *output, size_t output_size) +{ + if (size < 16 || output_size < 7 + 22 + 1 || + (count > 0 && (count < 4 || count > 31)) || + prefix[0] != '$' || prefix[1] != '2' || + (prefix[2] != 'a' && prefix[2] != 'b' && prefix[2] != 'y')) { + if (output_size > 0) output[0] = '\0'; + __set_errno((output_size < 7 + 22 + 1) ? ERANGE : EINVAL); + return NULL; + } + + if (count == 0) count = 5; + + output[0] = '$'; + output[1] = '2'; + output[2] = prefix[2]; + output[3] = '$'; + output[4] = '0' + count / 10; + output[5] = '0' + count % 10; + output[6] = '$'; + + BF_encode(&output[7], (const BF_word *)input, 16); + output[7 + 22] = '\0'; + + return output; +} diff --git a/src/auth/crypt-blowfish.h b/src/auth/crypt-blowfish.h new file mode 100644 index 0000000..e7123b3 --- /dev/null +++ b/src/auth/crypt-blowfish.h @@ -0,0 +1,29 @@ +/* + * Written by Solar Designer <solar at openwall.com> in 2000-2011. + * No copyright is claimed, and the software is hereby placed in the public + * domain. In case this attempt to disclaim copyright and place the software + * in the public domain is deemed null and void, then the software is + * Copyright (c) 2000-2011 Solar Designer and it is hereby released to the + * general public under the following terms: + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted. + * + * There's ABSOLUTELY NO WARRANTY, express or implied. + * + * See crypt_blowfish.c for more information. + * + * 2017-10-10 - Adapted for dovecot code by Aki Tuomi <aki.tuomi@dovecot.fi> + */ + +#ifndef CRYPT_BLOWFISH_H +#define CRYPT_BLOWFISH_H + +extern int crypt_output_magic(const char *setting, char *output, size_t size); +extern char *crypt_blowfish_rn(const char *key, const char *setting, + char *output, size_t size); +extern char *crypt_gensalt_blowfish_rn(const char *prefix, + unsigned long count, + const char *input, size_t size, char *output, size_t output_size); + +#endif diff --git a/src/auth/db-checkpassword.c b/src/auth/db-checkpassword.c new file mode 100644 index 0000000..7d52eef --- /dev/null +++ b/src/auth/db-checkpassword.c @@ -0,0 +1,563 @@ +/* Copyright (c) 2004-2018 Dovecot authors, see the included COPYING file */ + +#include "auth-common.h" + +#if defined(PASSDB_CHECKPASSWORD) || defined(USERDB_CHECKPASSWORD) + +#include "lib-signals.h" +#include "array.h" +#include "buffer.h" +#include "str.h" +#include "ioloop.h" +#include "hash.h" +#include "execv-const.h" +#include "env-util.h" +#include "safe-memset.h" +#include "strescape.h" +#include "child-wait.h" +#include "db-checkpassword.h" + +#include <unistd.h> +#include <sys/wait.h> + +#define CHECKPASSWORD_MAX_REQUEST_LEN 512 + +struct chkpw_auth_request { + struct db_checkpassword *db; + struct auth_request *request; + char *auth_password; + + db_checkpassword_callback_t *callback; + void (*request_callback)(); + + pid_t pid; + int fd_out, fd_in; + struct io *io_out, *io_in; + + string_t *input_buf; + size_t output_pos, output_len; + + int exit_status; + bool exited:1; +}; + +struct db_checkpassword { + char *checkpassword_path, *checkpassword_reply_path; + + HASH_TABLE(void *, struct chkpw_auth_request *) clients; + struct child_wait *child_wait; +}; + +static void +env_put_extra_fields(const ARRAY_TYPE(auth_field) *extra_fields) +{ + const struct auth_field *field; + const char *key, *value; + + array_foreach(extra_fields, field) { + key = t_str_ucase(field->key); + value = field->value != NULL ? field->value : "1"; + env_put(key, value); + } +} + +static void checkpassword_request_close(struct chkpw_auth_request *request) +{ + io_remove(&request->io_in); + io_remove(&request->io_out); + + i_close_fd(&request->fd_in); + i_close_fd(&request->fd_out); +} + +static void checkpassword_request_free(struct chkpw_auth_request **_request) +{ + struct chkpw_auth_request *request = *_request; + + *_request = NULL; + + if (!request->exited) { + hash_table_remove(request->db->clients, + POINTER_CAST(request->pid)); + child_wait_remove_pid(request->db->child_wait, request->pid); + } + checkpassword_request_close(request); + + if (request->auth_password != NULL) { + safe_memset(request->auth_password, 0, + strlen(request->auth_password)); + i_free(request->auth_password); + } + auth_request_unref(&request->request); + str_free(&request->input_buf); + i_free(request); +} + +static void checkpassword_finish(struct chkpw_auth_request **_request, + enum db_checkpassword_status status) +{ + struct chkpw_auth_request *request = *_request; + const char *const *extra_fields; + + *_request = NULL; + + extra_fields = t_strsplit_tabescaped(str_c(request->input_buf)); + request->callback(request->request, status, extra_fields, + request->request_callback); + checkpassword_request_free(&request); +} + +static void checkpassword_internal_failure(struct chkpw_auth_request **request) +{ + checkpassword_finish(request, DB_CHECKPASSWORD_STATUS_INTERNAL_FAILURE); +} + +static void +checkpassword_request_finish_auth(struct chkpw_auth_request *request) +{ + switch (request->exit_status) { + /* standard checkpassword exit codes: */ + case 1: + e_info(authdb_event(request->request), + "Login failed (status=%d)", + request->exit_status); + checkpassword_finish(&request, DB_CHECKPASSWORD_STATUS_FAILURE); + break; + case 0: + if (request->input_buf->used == 0) { + e_error(authdb_event(request->request), + "Received no input"); + checkpassword_internal_failure(&request); + break; + } + checkpassword_finish(&request, DB_CHECKPASSWORD_STATUS_OK); + break; + case 2: + /* checkpassword is called with wrong parameters? unlikely */ + e_error(authdb_event(request->request), + "Child %s exited with status 2 (tried to use " + "userdb-only checkpassword program for passdb?)", + dec2str(request->pid)); + checkpassword_internal_failure(&request); + break; + case 111: + /* temporary problem, treat as internal error */ + default: + /* whatever error.. */ + e_error(authdb_event(request->request), + "Child %s exited with status %d", + dec2str(request->pid), request->exit_status); + checkpassword_internal_failure(&request); + break; + } +} + +static void +checkpassword_request_finish_lookup(struct chkpw_auth_request *request) +{ + switch (request->exit_status) { + case 3: + /* User does not exist. */ + e_info(authdb_event(request->request), + "User unknown"); + checkpassword_finish(&request, DB_CHECKPASSWORD_STATUS_FAILURE); + break; + case 2: + /* This is intentionally not 0. checkpassword-reply exits with + 2 on success when AUTHORIZED is set. */ + if (request->input_buf->used == 0) { + e_error(authdb_event(request->request), + "Received no input"); + checkpassword_internal_failure(&request); + break; + } + checkpassword_finish(&request, DB_CHECKPASSWORD_STATUS_OK); + break; + default: + /* whatever error... */ + e_error(authdb_event(request->request), + "Child %s exited with status %d", + dec2str(request->pid), request->exit_status); + checkpassword_internal_failure(&request); + break; + } +} + +static void +checkpassword_request_half_finish(struct chkpw_auth_request *request) +{ + /* the process must have exited, and the input fd must have closed */ + if (!request->exited || request->fd_in != -1) + return; + + if (request->auth_password != NULL) + checkpassword_request_finish_auth(request); + else + checkpassword_request_finish_lookup(request); +} + +static void env_put_auth_vars(struct auth_request *request) +{ + const struct var_expand_table *tab; + unsigned int i; + + tab = auth_request_get_var_expand_table(request, NULL); + for (i = 0; tab[i].key != '\0' || tab[i].long_key != NULL; i++) { + /* avoid keeping passwords in environment .. just in case + an attacker might find it from there. environment is no + longer world-readable in modern OSes, but maybe the attacker + could be running with the same UID. of course then the + attacker could usually ptrace() the process, except that is + disabled on some secured systems. so, although I find it + highly unlikely anyone could actually attack Dovecot this + way in a real system, be safe just in case. besides, lets + try to keep at least minimally compatible with the + checkpassword API. */ + if (tab[i].long_key != NULL && tab[i].value != NULL && + strcasecmp(tab[i].long_key, "password") != 0) { + env_put(t_strdup_printf("AUTH_%s", + t_str_ucase(tab[i].long_key)), + tab[i].value); + } + } +} + +static void checkpassword_setup_env(struct auth_request *request) +{ + const struct auth_request_fields *fields = &request->fields; + + /* Besides passing the standard username and password in a + pipe, also pass some other possibly interesting information + via environment. Use UCSPI names for local/remote IPs. */ + env_put("PROTO", "TCP"); /* UCSPI */ + env_put("ORIG_UID", dec2str(getuid())); + env_put("SERVICE", fields->service); + if (fields->local_ip.family != 0) { + env_put("TCPLOCALIP", net_ip2addr(&fields->local_ip)); + /* FIXME: for backwards compatibility only, + remove some day */ + env_put("LOCAL_IP", net_ip2addr(&fields->local_ip)); + } + if (fields->remote_ip.family != 0) { + env_put("TCPREMOTEIP", net_ip2addr(&fields->remote_ip)); + /* FIXME: for backwards compatibility only, + remove some day */ + env_put("REMOTE_IP", net_ip2addr(&fields->remote_ip)); + } + if (fields->local_port != 0) + env_put("TCPLOCALPORT", dec2str(fields->local_port)); + if (fields->remote_port != 0) + env_put("TCPREMOTEPORT", dec2str(fields->remote_port)); + if (fields->master_user != NULL) + env_put("MASTER_USER", fields->master_user); + if (!auth_fields_is_empty(fields->extra_fields)) { + const ARRAY_TYPE(auth_field) *extra_fields = + auth_fields_export(fields->extra_fields); + + /* extra fields could come from master db */ + env_put_extra_fields(extra_fields); + } + env_put_auth_vars(request); +} + +static const char * +checkpassword_get_cmd(struct auth_request *request, const char *args, + const char *checkpassword_reply_path) +{ + string_t *str; + const char *error; + + str = t_str_new(256); + if (auth_request_var_expand(str, args, request, NULL, &error) <= 0) { + e_error(authdb_event(request), + "Failed to expand checkpassword_path=%s: %s", + args, error); + } + + return t_strconcat(str_c(str), " ", checkpassword_reply_path, NULL); +} + +static void checkpassword_child_input(struct chkpw_auth_request *request) +{ + unsigned char buf[1024]; + ssize_t ret; + + ret = read(request->fd_in, buf, sizeof(buf)); + if (ret > 0) { + str_append_data(request->input_buf, buf, ret); + return; + } + + if (ret < 0) { + e_error(authdb_event(request->request), + "read() failed: %m"); + checkpassword_internal_failure(&request); + } else if (memchr(str_data(request->input_buf), '\0', + str_len(request->input_buf)) != NULL) { + e_error(authdb_event(request->request), + "NUL characters in checkpassword reply"); + checkpassword_internal_failure(&request); + } else if (strchr(str_c(request->input_buf), '\n') != NULL) { + e_error(authdb_event(request->request), + "LF characters in checkpassword reply"); + checkpassword_internal_failure(&request); + } else { + e_debug(authdb_event(request->request), + "Received input: %s", str_c(request->input_buf)); + checkpassword_request_close(request); + checkpassword_request_half_finish(request); + } +} + +static void checkpassword_child_output(struct chkpw_auth_request *request) +{ + /* Send: username \0 password \0 timestamp \0. + Must be 512 bytes or less. The "timestamp" parameter is actually + useful only for APOP authentication. We don't support it, so + keep it empty */ + struct auth_request *auth_request = request->request; + buffer_t *buf; + const unsigned char *data; + size_t size; + ssize_t ret; + + buf = t_buffer_create(CHECKPASSWORD_MAX_REQUEST_LEN); + buffer_append(buf, auth_request->fields.user, + strlen(auth_request->fields.user)+1); + if (request->auth_password != NULL) { + buffer_append(buf, request->auth_password, + strlen(request->auth_password)+1); + } else { + buffer_append_c(buf, '\0'); + } + buffer_append_c(buf, '\0'); + data = buffer_get_data(buf, &size); + + i_assert(size == request->output_len); + /* already checked this */ + i_assert(size <= CHECKPASSWORD_MAX_REQUEST_LEN); + + ret = write(request->fd_out, data + request->output_pos, + size - request->output_pos); + if (ret <= 0) { + if (ret < 0) { + e_error(authdb_event(request->request), + "write() failed: %m"); + } else { + e_error(authdb_event(request->request), + "write() returned 0"); + } + checkpassword_internal_failure(&request); + return; + } + + request->output_pos += ret; + if (request->output_pos < size) + return; + + /* finished sending the data */ + io_remove(&request->io_out); + + if (close(request->fd_out) < 0) + e_error(authdb_event(request->request), "close() failed: %m"); + request->fd_out = -1; +} + +static void ATTR_NORETURN +checkpassword_exec(struct db_checkpassword *db, struct auth_request *request, + int fd_in, int fd_out, bool authenticate) +{ + const char *cmd, *const *args; + + /* fd 3 is used to send the username+password for the script + fd 4 is used to communicate with checkpassword-reply */ + if (dup2(fd_out, 3) < 0 || dup2(fd_in, 4) < 0) { + e_error(authdb_event(request), + "dup2() failed: %m"); + lib_exit(111); + } + + if (!authenticate) { + /* We want to retrieve passdb/userdb data and don't do + authorization, so we need to signalize the + checkpassword program that the password shall be + ignored by setting AUTHORIZED. This needs a + special checkpassword program which knows how to + handle this. */ + env_put("AUTHORIZED", "1"); + if (request->wanted_credentials_scheme != NULL) { + /* passdb credentials lookup */ + env_put("CREDENTIALS_LOOKUP", "1"); + env_put("SCHEME", request->wanted_credentials_scheme); + } + } + checkpassword_setup_env(request); + cmd = checkpassword_get_cmd(request, db->checkpassword_path, + db->checkpassword_reply_path); + e_debug(authdb_event(request), "execute: %s", cmd); + + /* very simple argument splitting. */ + args = t_strsplit(cmd, " "); + execv_const(args[0], args); +} + +static void sigchld_handler(const struct child_wait_status *status, + struct db_checkpassword *db) +{ + struct chkpw_auth_request *request = + hash_table_lookup(db->clients, POINTER_CAST(status->pid)); + + i_assert(request != NULL); + + hash_table_remove(db->clients, POINTER_CAST(status->pid)); + request->exited = TRUE; + + if (WIFSIGNALED(status->status)) { + e_error(authdb_event(request->request), + "Child %s died with signal %d", + dec2str(status->pid), WTERMSIG(status->status)); + checkpassword_internal_failure(&request); + } else if (WIFEXITED(status->status)) { + request->exit_status = WEXITSTATUS(status->status); + + e_debug(authdb_event(request->request), + "exit_status=%d", request->exit_status); + checkpassword_request_half_finish(request); + } else { + /* shouldn't happen */ + e_debug(authdb_event(request->request), + "Child %s exited with status=%d", + dec2str(status->pid), status->status); + checkpassword_internal_failure(&request); + } +} + +void db_checkpassword_call(struct db_checkpassword *db, + struct auth_request *request, + const char *auth_password, + db_checkpassword_callback_t *callback, + void (*request_callback)()) +{ + struct chkpw_auth_request *chkpw_auth_request; + size_t output_len; + int fd_in[2], fd_out[2]; + pid_t pid; + + /* <username> \0 <password> \0 timestamp \0 */ + output_len = strlen(request->fields.user) + 3; + if (auth_password != NULL) + output_len += strlen(auth_password); + if (output_len > CHECKPASSWORD_MAX_REQUEST_LEN) { + e_info(authdb_event(request), + "Username+password combination too long (%zu bytes)", + output_len); + callback(request, DB_CHECKPASSWORD_STATUS_FAILURE, + NULL, request_callback); + return; + } + + fd_in[0] = -1; + if (pipe(fd_in) < 0 || pipe(fd_out) < 0) { + e_error(authdb_event(request), + "pipe() failed: %m"); + if (fd_in[0] != -1) { + i_close_fd(&fd_in[0]); + i_close_fd(&fd_in[1]); + } + callback(request, DB_CHECKPASSWORD_STATUS_INTERNAL_FAILURE, + NULL, request_callback); + return; + } + + pid = fork(); + if (pid == -1) { + e_error(authdb_event(request), + "fork() failed: %m"); + i_close_fd(&fd_in[0]); + i_close_fd(&fd_in[1]); + i_close_fd(&fd_out[0]); + i_close_fd(&fd_out[1]); + callback(request, DB_CHECKPASSWORD_STATUS_INTERNAL_FAILURE, + NULL, request_callback); + return; + } + + if (pid == 0) { + /* child */ + i_close_fd(&fd_in[0]); + i_close_fd(&fd_out[1]); + checkpassword_exec(db, request, fd_in[1], fd_out[0], + auth_password != NULL); + /* not reached */ + } + + if (close(fd_in[1]) < 0) { + e_error(authdb_event(request), + "close(fd_in[1]) failed: %m"); + } + if (close(fd_out[0]) < 0) { + e_error(authdb_event(request), + "close(fd_out[0]) failed: %m"); + } + + auth_request_ref(request); + chkpw_auth_request = i_new(struct chkpw_auth_request, 1); + chkpw_auth_request->db = db; + chkpw_auth_request->pid = pid; + chkpw_auth_request->fd_in = fd_in[0]; + chkpw_auth_request->fd_out = fd_out[1]; + chkpw_auth_request->auth_password = i_strdup(auth_password); + chkpw_auth_request->request = request; + chkpw_auth_request->output_len = output_len; + chkpw_auth_request->input_buf = str_new(default_pool, 256); + chkpw_auth_request->callback = callback; + chkpw_auth_request->request_callback = request_callback; + + chkpw_auth_request->io_in = + io_add(fd_in[0], IO_READ, checkpassword_child_input, + chkpw_auth_request); + chkpw_auth_request->io_out = + io_add(fd_out[1], IO_WRITE, checkpassword_child_output, + chkpw_auth_request); + + hash_table_insert(db->clients, POINTER_CAST(pid), chkpw_auth_request); + child_wait_add_pid(db->child_wait, pid); +} + +struct db_checkpassword * +db_checkpassword_init(const char *checkpassword_path, + const char *checkpassword_reply_path) +{ + struct db_checkpassword *db; + + db = i_new(struct db_checkpassword, 1); + db->checkpassword_path = i_strdup(checkpassword_path); + db->checkpassword_reply_path = i_strdup(checkpassword_reply_path); + hash_table_create_direct(&db->clients, default_pool, 0); + db->child_wait = + child_wait_new_with_pid((pid_t)-1, sigchld_handler, db); + return db; +} + +void db_checkpassword_deinit(struct db_checkpassword **_db) +{ + struct db_checkpassword *db = *_db; + struct hash_iterate_context *iter; + void *key; + struct chkpw_auth_request *request; + + *_db = NULL; + + iter = hash_table_iterate_init(db->clients); + while (hash_table_iterate(iter, db->clients, &key, &request)) + checkpassword_internal_failure(&request); + hash_table_iterate_deinit(&iter); + + child_wait_free(&db->child_wait); + hash_table_destroy(&db->clients); + i_free(db->checkpassword_reply_path); + i_free(db->checkpassword_path); + i_free(db); +} + +#endif diff --git a/src/auth/db-checkpassword.h b/src/auth/db-checkpassword.h new file mode 100644 index 0000000..64eedd8 --- /dev/null +++ b/src/auth/db-checkpassword.h @@ -0,0 +1,29 @@ +#ifndef CHECKPASSWORD_COMMON_H +#define CHECKPASSWORD_COMMON_H + +#include "auth-request.h" + +enum db_checkpassword_status { + DB_CHECKPASSWORD_STATUS_INTERNAL_FAILURE = -1, + /* auth unsuccessful / user not found */ + DB_CHECKPASSWORD_STATUS_FAILURE = 0, + DB_CHECKPASSWORD_STATUS_OK = 1 +}; + +typedef void db_checkpassword_callback_t(struct auth_request *request, + enum db_checkpassword_status status, + const char *const *extra_fields, + void (*request_callback)()); + +struct db_checkpassword * +db_checkpassword_init(const char *checkpassword_path, + const char *checkpassword_reply_path); +void db_checkpassword_deinit(struct db_checkpassword **db); + +void db_checkpassword_call(struct db_checkpassword *db, + struct auth_request *request, + const char *auth_password, + db_checkpassword_callback_t *callback, + void (*request_callback)()) ATTR_NULL(3); + +#endif diff --git a/src/auth/db-dict-cache-key.c b/src/auth/db-dict-cache-key.c new file mode 100644 index 0000000..a220d8d --- /dev/null +++ b/src/auth/db-dict-cache-key.c @@ -0,0 +1,64 @@ +/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */ + +#include "auth-common.h" +#include "array.h" +#include "str.h" +#include "var-expand.h" +#include "db-dict.h" + +const struct db_dict_key * +db_dict_set_key_find(const ARRAY_TYPE(db_dict_key) *keys, const char *name) +{ + const struct db_dict_key *key; + + array_foreach(keys, key) { + if (strcmp(key->name, name) == 0) + return key; + } + return NULL; +} + + +const char * +db_dict_parse_cache_key(const ARRAY_TYPE(db_dict_key) *keys, + const ARRAY_TYPE(db_dict_field) *fields, + const ARRAY_TYPE(db_dict_key_p) *objects) +{ + const struct db_dict_field *field; + const struct db_dict_key *key; + const char *p, *name; + unsigned int idx, size; + string_t *str = t_str_new(128); + + array_foreach(fields, field) { + for (p = field->value; *p != '\0'; ) { + if (*p != '%') { + p++; + continue; + } + + var_get_key_range(++p, &idx, &size); + if (size == 0) { + /* broken %variable ending too early */ + break; + } + p += idx; + if (size > 5 && memcmp(p, "dict:", 5) == 0) { + name = t_strcut(t_strndup(p+5, size-5), ':'); + key = db_dict_set_key_find(keys, name); + if (key != NULL) + str_printfa(str, "\t%s", key->key); + } else if (size == 1) { + str_printfa(str, "\t%%%c", p[0]); + } else { + str_append(str, "\t%{"); + str_append_data(str, p, size); + str_append_c(str, '}'); + } + p += size; + } + } + array_foreach_elem(objects, key) + str_printfa(str, "\t%s", key->key); + return str_c(str); +} diff --git a/src/auth/db-dict.c b/src/auth/db-dict.c new file mode 100644 index 0000000..96b1a63 --- /dev/null +++ b/src/auth/db-dict.c @@ -0,0 +1,654 @@ +/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */ + +#include "auth-common.h" + +#include "array.h" +#include "istream.h" +#include "str.h" +#include "json-parser.h" +#include "settings.h" +#include "dict.h" +#include "auth-request.h" +#include "auth-worker-client.h" +#include "db-dict.h" + +#include <stddef.h> + +enum dict_settings_section { + DICT_SETTINGS_SECTION_ROOT = 0, + DICT_SETTINGS_SECTION_KEY, + DICT_SETTINGS_SECTION_PASSDB, + DICT_SETTINGS_SECTION_USERDB +}; + +struct dict_settings_parser_ctx { + struct dict_connection *conn; + enum dict_settings_section section; + struct db_dict_key *cur_key; +}; + +struct db_dict_iter_key { + const struct db_dict_key *key; + bool used; + const char *value; +}; + +struct db_dict_value_iter { + pool_t pool; + struct auth_request *auth_request; + struct dict_connection *conn; + const struct var_expand_table *var_expand_table; + ARRAY(struct db_dict_iter_key) keys; + + const ARRAY_TYPE(db_dict_field) *fields; + const ARRAY_TYPE(db_dict_key_p) *objects; + unsigned int field_idx; + unsigned int object_idx; + + struct json_parser *json_parser; + string_t *tmpstr; + const char *error; +}; + +#define DEF_STR(name) DEF_STRUCT_STR(name, db_dict_settings) +#define DEF_BOOL(name) DEF_STRUCT_BOOL(name, db_dict_settings) +static struct setting_def setting_defs[] = { + DEF_STR(uri), + DEF_STR(default_pass_scheme), + DEF_STR(iterate_prefix), + DEF_BOOL(iterate_disable), + + DEF_STR(passdb_objects), + DEF_STR(userdb_objects), + { 0, NULL, 0 } +}; + +static struct db_dict_settings default_dict_settings = { + .uri = NULL, + .default_pass_scheme = "MD5", + .iterate_prefix = "", + .iterate_disable = FALSE, + .passdb_objects = "", + .userdb_objects = "" +}; + +#undef DEF_STR +#define DEF_STR(name) DEF_STRUCT_STR(name, db_dict_key) +static struct setting_def key_setting_defs[] = { + DEF_STR(name), + DEF_STR(key), + DEF_STR(format), + DEF_STR(default_value), + + { 0, NULL, 0 } +}; + +static struct db_dict_key default_key_settings = { + .name = NULL, + .key = "", + .format = "value", + .default_value = NULL +}; + +static struct dict_connection *connections = NULL; + +static struct dict_connection *dict_conn_find(const char *config_path) +{ + struct dict_connection *conn; + + for (conn = connections; conn != NULL; conn = conn->next) { + if (strcmp(conn->config_path, config_path) == 0) + return conn; + } + + return NULL; +} + +static bool +parse_obsolete_setting(const char *key, const char *value, + struct dict_settings_parser_ctx *ctx, + const char **error_r) +{ + const struct db_dict_key *dbkey; + + if (strcmp(key, "password_key") == 0) { + /* key passdb { key=<value> format=json } + passdb_objects = passdb */ + ctx->cur_key = array_append_space(&ctx->conn->set.keys); + *ctx->cur_key = default_key_settings; + ctx->cur_key->name = "passdb"; + ctx->cur_key->format = "json"; + ctx->cur_key->parsed_format = DB_DICT_VALUE_FORMAT_JSON; + ctx->cur_key->key = p_strdup(ctx->conn->pool, value); + + dbkey = ctx->cur_key; + array_push_back(&ctx->conn->set.parsed_passdb_objects, &dbkey); + return TRUE; + } + if (strcmp(key, "user_key") == 0) { + /* key userdb { key=<value> format=json } + userdb_objects = userdb */ + ctx->cur_key = array_append_space(&ctx->conn->set.keys); + *ctx->cur_key = default_key_settings; + ctx->cur_key->name = "userdb"; + ctx->cur_key->format = "json"; + ctx->cur_key->parsed_format = DB_DICT_VALUE_FORMAT_JSON; + ctx->cur_key->key = p_strdup(ctx->conn->pool, value); + + dbkey = ctx->cur_key; + array_push_back(&ctx->conn->set.parsed_userdb_objects, &dbkey); + return TRUE; + } + if (strcmp(key, "value_format") == 0) { + if (strcmp(value, "json") == 0) + return TRUE; + *error_r = "Deprecated value_format must be 'json'"; + return FALSE; + } + return FALSE; +} + +static const char *parse_setting(const char *key, const char *value, + struct dict_settings_parser_ctx *ctx) +{ + struct db_dict_field *field; + const char *error = NULL; + + switch (ctx->section) { + case DICT_SETTINGS_SECTION_ROOT: + if (parse_obsolete_setting(key, value, ctx, &error)) + return NULL; + if (error != NULL) + return error; + return parse_setting_from_defs(ctx->conn->pool, setting_defs, + &ctx->conn->set, key, value); + case DICT_SETTINGS_SECTION_KEY: + return parse_setting_from_defs(ctx->conn->pool, key_setting_defs, + ctx->cur_key, key, value); + case DICT_SETTINGS_SECTION_PASSDB: + field = array_append_space(&ctx->conn->set.passdb_fields); + field->name = p_strdup(ctx->conn->pool, key); + field->value = p_strdup(ctx->conn->pool, value); + return NULL; + case DICT_SETTINGS_SECTION_USERDB: + field = array_append_space(&ctx->conn->set.userdb_fields); + field->name = p_strdup(ctx->conn->pool, key); + field->value = p_strdup(ctx->conn->pool, value); + return NULL; + } + i_unreached(); +} + +static bool parse_section(const char *type, const char *name, + struct dict_settings_parser_ctx *ctx, + const char **errormsg) +{ + if (type == NULL) { + ctx->section = DICT_SETTINGS_SECTION_ROOT; + if (ctx->cur_key != NULL) { + if (strcmp(ctx->cur_key->format, "value") == 0) { + ctx->cur_key->parsed_format = + DB_DICT_VALUE_FORMAT_VALUE; + } else if (strcmp(ctx->cur_key->format, "json") == 0) { + ctx->cur_key->parsed_format = + DB_DICT_VALUE_FORMAT_JSON; + } else { + *errormsg = t_strconcat("Unknown key format: ", + ctx->cur_key->format, NULL); + return FALSE; + } + } + ctx->cur_key = NULL; + return TRUE; + } + if (ctx->section != DICT_SETTINGS_SECTION_ROOT) { + *errormsg = "Nested sections not supported"; + return FALSE; + } + if (strcmp(type, "key") == 0) { + if (name == NULL) { + *errormsg = "Key section is missing name"; + return FALSE; + } + if (strchr(name, '.') != NULL) { + *errormsg = "Key section names must not contain '.'"; + return FALSE; + } + ctx->section = DICT_SETTINGS_SECTION_KEY; + ctx->cur_key = array_append_space(&ctx->conn->set.keys); + *ctx->cur_key = default_key_settings; + ctx->cur_key->name = p_strdup(ctx->conn->pool, name); + return TRUE; + } + if (strcmp(type, "passdb_fields") == 0) { + ctx->section = DICT_SETTINGS_SECTION_PASSDB; + return TRUE; + } + if (strcmp(type, "userdb_fields") == 0) { + ctx->section = DICT_SETTINGS_SECTION_USERDB; + return TRUE; + } + *errormsg = "Unknown section"; + return FALSE; +} + +static void +db_dict_settings_parse(struct db_dict_settings *set) +{ + const struct db_dict_key *key; + const char *const *tmp; + + tmp = t_strsplit_spaces(set->passdb_objects, " "); + for (; *tmp != NULL; tmp++) { + key = db_dict_set_key_find(&set->keys, *tmp); + if (key == NULL) { + i_fatal("dict: passdb_objects refers to key %s, " + "which doesn't exist", *tmp); + } + if (key->parsed_format == DB_DICT_VALUE_FORMAT_VALUE) { + i_fatal("dict: passdb_objects refers to key %s, " + "but it's in value-only format", *tmp); + } + array_push_back(&set->parsed_passdb_objects, &key); + } + + tmp = t_strsplit_spaces(set->userdb_objects, " "); + for (; *tmp != NULL; tmp++) { + key = db_dict_set_key_find(&set->keys, *tmp); + if (key == NULL) { + i_fatal("dict: userdb_objects refers to key %s, " + "which doesn't exist", *tmp); + } + if (key->parsed_format == DB_DICT_VALUE_FORMAT_VALUE) { + i_fatal("dict: userdb_objects refers to key %s, " + "but it's in value-only format", *tmp); + } + array_push_back(&set->parsed_userdb_objects, &key); + } +} + +struct dict_connection *db_dict_init(const char *config_path) +{ + struct dict_settings dict_set; + struct dict_settings_parser_ctx ctx; + struct dict_connection *conn; + const char *error; + pool_t pool; + + conn = dict_conn_find(config_path); + if (conn != NULL) { + conn->refcount++; + return conn; + } + + if (*config_path == '\0') + i_fatal("dict: Configuration file path not given"); + + pool = pool_alloconly_create("dict_connection", 1024); + conn = p_new(pool, struct dict_connection, 1); + conn->pool = pool; + + conn->refcount = 1; + + conn->config_path = p_strdup(pool, config_path); + conn->set = default_dict_settings; + p_array_init(&conn->set.keys, pool, 8); + p_array_init(&conn->set.passdb_fields, pool, 8); + p_array_init(&conn->set.userdb_fields, pool, 8); + p_array_init(&conn->set.parsed_passdb_objects, pool, 2); + p_array_init(&conn->set.parsed_userdb_objects, pool, 2); + + i_zero(&ctx); + ctx.conn = conn; + if (!settings_read(config_path, NULL, parse_setting, + parse_section, &ctx, &error)) + i_fatal("dict %s: %s", config_path, error); + db_dict_settings_parse(&conn->set); + + if (conn->set.uri == NULL) + i_fatal("dict %s: Empty uri setting", config_path); + + i_zero(&dict_set); + dict_set.base_dir = global_auth_settings->base_dir; + dict_set.event_parent = auth_event; + if (dict_init(conn->set.uri, &dict_set, &conn->dict, &error) < 0) + i_fatal("dict %s: Failed to init dict: %s", config_path, error); + + conn->next = connections; + connections = conn; + return conn; +} + +void db_dict_unref(struct dict_connection **_conn) +{ + struct dict_connection *conn = *_conn; + + *_conn = NULL; + if (--conn->refcount > 0) + return; + + dict_deinit(&conn->dict); + pool_unref(&conn->pool); +} + +static struct db_dict_iter_key * +db_dict_iter_find_key(struct db_dict_value_iter *iter, const char *name) +{ + struct db_dict_iter_key *key; + + array_foreach_modifiable(&iter->keys, key) { + if (strcmp(key->key->name, name) == 0) + return key; + } + return NULL; +} + +static void db_dict_iter_find_used_keys(struct db_dict_value_iter *iter) +{ + const struct db_dict_field *field; + struct db_dict_iter_key *key; + const char *p, *name; + unsigned int idx, size; + + array_foreach(iter->fields, field) { + for (p = field->value; *p != '\0'; ) { + if (*p != '%') { + p++; + continue; + } + + var_get_key_range(++p, &idx, &size); + if (size == 0) { + /* broken %variable ending too early */ + break; + } + p += idx; + if (size > 5 && memcmp(p, "dict:", 5) == 0) { + name = t_strcut(t_strndup(p+5, size-5), ':'); + key = db_dict_iter_find_key(iter, name); + if (key != NULL) + key->used = TRUE; + } + p += size; + } + } +} + +static void db_dict_iter_find_used_objects(struct db_dict_value_iter *iter) +{ + const struct db_dict_key *dict_key; + struct db_dict_iter_key *key; + + array_foreach_elem(iter->objects, dict_key) { + key = db_dict_iter_find_key(iter, dict_key->name); + i_assert(key != NULL); /* checked at init */ + i_assert(key->key->parsed_format != DB_DICT_VALUE_FORMAT_VALUE); + key->used = TRUE; + } +} + +static int +db_dict_iter_key_cmp(const struct db_dict_iter_key *k1, + const struct db_dict_iter_key *k2) +{ + return null_strcmp(k1->key->default_value, k2->key->default_value); +} + +static int db_dict_iter_lookup_key_values(struct db_dict_value_iter *iter) +{ + struct db_dict_iter_key *key; + string_t *path; + const char *error; + int ret; + + /* sort the keys so that we'll first lookup the keys without + default value. if their lookup fails, the user doesn't exist. */ + array_sort(&iter->keys, db_dict_iter_key_cmp); + + path = t_str_new(128); + str_append(path, DICT_PATH_SHARED); + + struct dict_op_settings set = { + .username = iter->auth_request->fields.user, + }; + + array_foreach_modifiable(&iter->keys, key) { + if (!key->used) + continue; + + str_truncate(path, strlen(DICT_PATH_SHARED)); + str_append(path, key->key->key); + ret = dict_lookup(iter->conn->dict, &set, iter->pool, + str_c(path), &key->value, &error); + if (ret > 0) { + e_debug(authdb_event(iter->auth_request), + "Lookup: %s = %s", str_c(path), + key->value); + } else if (ret < 0) { + e_error(authdb_event(iter->auth_request), + "Failed to lookup key %s: %s", str_c(path), error); + return -1; + } else if (key->key->default_value != NULL) { + e_debug(authdb_event(iter->auth_request), + "Lookup: %s not found, using default value %s", + str_c(path), key->key->default_value); + key->value = key->key->default_value; + } else { + return 0; + } + } + return 1; +} + +int db_dict_value_iter_init(struct dict_connection *conn, + struct auth_request *auth_request, + const ARRAY_TYPE(db_dict_field) *fields, + const ARRAY_TYPE(db_dict_key_p) *objects, + struct db_dict_value_iter **iter_r) +{ + struct db_dict_value_iter *iter; + struct db_dict_iter_key *iterkey; + const struct db_dict_key *key; + pool_t pool; + int ret; + + pool = pool_alloconly_create(MEMPOOL_GROWING"auth dict lookup", 1024); + iter = p_new(pool, struct db_dict_value_iter, 1); + iter->pool = pool; + iter->conn = conn; + iter->fields = fields; + iter->objects = objects; + iter->tmpstr = str_new(pool, 128); + iter->auth_request = auth_request; + iter->var_expand_table = auth_request_get_var_expand_table(auth_request, NULL); + + /* figure out what keys we need to lookup, and lookup them */ + p_array_init(&iter->keys, pool, array_count(&conn->set.keys)); + array_foreach(&conn->set.keys, key) { + iterkey = array_append_space(&iter->keys); + struct db_dict_key *new_key = p_new(iter->pool, struct db_dict_key, 1); + memcpy(new_key, key, sizeof(struct db_dict_key)); + string_t *expanded_key = str_new(iter->pool, strlen(key->key)); + const char *error; + if (auth_request_var_expand_with_table(expanded_key, key->key, auth_request, + iter->var_expand_table, + NULL, &error) <= 0) { + e_error(authdb_event(iter->auth_request), + "Failed to expand key %s: %s", key->key, error); + pool_unref(&pool); + return -1; + } + new_key->key = str_c(expanded_key); + iterkey->key = new_key; + } + T_BEGIN { + db_dict_iter_find_used_keys(iter); + db_dict_iter_find_used_objects(iter); + ret = db_dict_iter_lookup_key_values(iter); + } T_END; + if (ret <= 0) { + pool_unref(&pool); + return ret; + } + *iter_r = iter; + return 1; +} + +static bool +db_dict_value_iter_json_next(struct db_dict_value_iter *iter, + string_t *tmpstr, + const char **key_r, const char **value_r) +{ + enum json_type type; + const char *value; + + if (json_parse_next(iter->json_parser, &type, &value) < 0) + return FALSE; + if (type != JSON_TYPE_OBJECT_KEY) { + iter->error = "Object expected"; + return FALSE; + } + if (*value == '\0') { + iter->error = "Empty object key"; + return FALSE; + } + str_truncate(tmpstr, 0); + str_append(tmpstr, value); + + if (json_parse_next(iter->json_parser, &type, &value) < 0) { + iter->error = "Missing value"; + return FALSE; + } + if (type == JSON_TYPE_OBJECT) { + iter->error = "Nested objects not supported"; + return FALSE; + } + *key_r = str_c(tmpstr); + *value_r = value; + return TRUE; +} + +static void +db_dict_value_iter_json_init(struct db_dict_value_iter *iter, const char *data) +{ + struct istream *input; + + i_assert(iter->json_parser == NULL); + + input = i_stream_create_from_data(data, strlen(data)); + iter->json_parser = json_parser_init(input); + i_stream_unref(&input); +} + +static bool +db_dict_value_iter_object_next(struct db_dict_value_iter *iter, + const char **key_r, const char **value_r) +{ + const struct db_dict_key *dict_key; + struct db_dict_iter_key *key; + + if (iter->json_parser != NULL) + return db_dict_value_iter_json_next(iter, iter->tmpstr, key_r, value_r); + if (iter->object_idx == array_count(iter->objects)) + return FALSE; + + dict_key = array_idx_elem(iter->objects, iter->object_idx); + key = db_dict_iter_find_key(iter, dict_key->name); + i_assert(key != NULL); /* checked at init */ + + switch (key->key->parsed_format) { + case DB_DICT_VALUE_FORMAT_VALUE: + i_unreached(); + case DB_DICT_VALUE_FORMAT_JSON: + db_dict_value_iter_json_init(iter, key->value); + return db_dict_value_iter_json_next(iter, iter->tmpstr, key_r, value_r); + } + i_unreached(); +} + +static int +db_dict_field_find(const char *data, void *context, + const char **value_r, + const char **error_r ATTR_UNUSED) +{ + struct db_dict_value_iter *iter = context; + struct db_dict_iter_key *key; + const char *name, *value, *dotname = strchr(data, '.'); + string_t *tmpstr; + + *value_r = NULL; + + if (dotname != NULL) + data = t_strdup_until(data, dotname++); + key = db_dict_iter_find_key(iter, data); + if (key == NULL) + return 1; + + switch (key->key->parsed_format) { + case DB_DICT_VALUE_FORMAT_VALUE: + *value_r = dotname != NULL ? NULL : + (key->value == NULL ? "" : key->value); + return 1; + case DB_DICT_VALUE_FORMAT_JSON: + if (dotname == NULL) + return 1; + db_dict_value_iter_json_init(iter, key->value); + *value_r = ""; + tmpstr = t_str_new(64); + while (db_dict_value_iter_json_next(iter, tmpstr, &name, &value)) { + if (strcmp(name, dotname) == 0) { + *value_r = t_strdup(value); + break; + } + } + (void)json_parser_deinit(&iter->json_parser, &iter->error); + return 1; + } + i_unreached(); +} + +bool db_dict_value_iter_next(struct db_dict_value_iter *iter, + const char **key_r, const char **value_r) +{ + static struct var_expand_func_table var_funcs_table[] = { + { "dict", db_dict_field_find }, + { NULL, NULL } + }; + const struct db_dict_field *field; + const char *error; + + if (iter->field_idx == array_count(iter->fields)) + return db_dict_value_iter_object_next(iter, key_r, value_r); + field = array_idx(iter->fields, iter->field_idx++); + + str_truncate(iter->tmpstr, 0); + if (var_expand_with_funcs(iter->tmpstr, field->value, + iter->var_expand_table, var_funcs_table, + iter, &error) <= 0) { + iter->error = p_strdup_printf(iter->pool, + "Failed to expand %s=%s: %s", + field->name, field->value, error); + return FALSE; + } + *key_r = field->name; + *value_r = str_c(iter->tmpstr); + return TRUE; +} + +int db_dict_value_iter_deinit(struct db_dict_value_iter **_iter, + const char **error_r) +{ + struct db_dict_value_iter *iter = *_iter; + + *_iter = NULL; + + *error_r = iter->error; + if (iter->json_parser != NULL) { + if (json_parser_deinit(&iter->json_parser, &iter->error) < 0 && + *error_r == NULL) + *error_r = iter->error; + } + + pool_unref(&iter->pool); + return *error_r != NULL ? -1 : 0; +} diff --git a/src/auth/db-dict.h b/src/auth/db-dict.h new file mode 100644 index 0000000..f4feb0a --- /dev/null +++ b/src/auth/db-dict.h @@ -0,0 +1,82 @@ +#ifndef DB_DICT_H +#define DB_DICT_H + +#include "sql-api.h" + +struct auth_request; +struct db_dict_value_iter; + +enum db_dict_value_format { + DB_DICT_VALUE_FORMAT_VALUE = 0, + DB_DICT_VALUE_FORMAT_JSON +}; + +struct db_dict_key { + const char *name; + const char *key; + const char *format; + const char *default_value; + + enum db_dict_value_format parsed_format; +}; +ARRAY_DEFINE_TYPE(db_dict_key, struct db_dict_key); +ARRAY_DEFINE_TYPE(db_dict_key_p, const struct db_dict_key *); + +struct db_dict_field { + const char *name; + const char *value; +}; +ARRAY_DEFINE_TYPE(db_dict_field, struct db_dict_field); + +struct db_dict_settings { + const char *uri; + const char *default_pass_scheme; + const char *iterate_prefix; + bool iterate_disable; + + ARRAY_TYPE(db_dict_key) keys; + + const char *passdb_objects; + const char *userdb_objects; + ARRAY_TYPE(db_dict_field) passdb_fields; + ARRAY_TYPE(db_dict_field) userdb_fields; + + ARRAY_TYPE(db_dict_key_p) parsed_passdb_objects; + ARRAY_TYPE(db_dict_key_p) parsed_userdb_objects; +}; + +struct dict_connection { + struct dict_connection *next; + + pool_t pool; + int refcount; + + char *config_path; + struct db_dict_settings set; + struct dict *dict; +}; + +struct dict_connection *db_dict_init(const char *config_path); +void db_dict_unref(struct dict_connection **conn); + +/* Returns 1 if ok, 0 if a key without default_value wasn't returned + ("user doesn't exist"), -1 if internal error */ +int db_dict_value_iter_init(struct dict_connection *conn, + struct auth_request *auth_request, + const ARRAY_TYPE(db_dict_field) *fields, + const ARRAY_TYPE(db_dict_key_p) *objects, + struct db_dict_value_iter **iter_r); +bool db_dict_value_iter_next(struct db_dict_value_iter *iter, + const char **key_r, const char **value_r); +int db_dict_value_iter_deinit(struct db_dict_value_iter **iter, + const char **error_r); + +const char *db_dict_parse_cache_key(const ARRAY_TYPE(db_dict_key) *keys, + const ARRAY_TYPE(db_dict_field) *fields, + const ARRAY_TYPE(db_dict_key_p) *objects); + +/* private: */ +const struct db_dict_key * +db_dict_set_key_find(const ARRAY_TYPE(db_dict_key) *keys, const char *name); + +#endif diff --git a/src/auth/db-ldap.c b/src/auth/db-ldap.c new file mode 100644 index 0000000..8083108 --- /dev/null +++ b/src/auth/db-ldap.c @@ -0,0 +1,2035 @@ +/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */ + +#include "auth-common.h" + +#if defined(BUILTIN_LDAP) || defined(PLUGIN_BUILD) + +#include "net.h" +#include "ioloop.h" +#include "array.h" +#include "hash.h" +#include "aqueue.h" +#include "str.h" +#include "time-util.h" +#include "env-util.h" +#include "var-expand.h" +#include "settings.h" +#include "userdb.h" +#include "db-ldap.h" + +#include <stddef.h> +#include <unistd.h> + +#define HAVE_LDAP_SASL +#ifdef HAVE_SASL_SASL_H +# include <sasl/sasl.h> +#elif defined (HAVE_SASL_H) +# include <sasl.h> +#else +# undef HAVE_LDAP_SASL +#endif +#ifdef LDAP_OPT_X_TLS +# define OPENLDAP_TLS_OPTIONS +#endif +#if !defined(SASL_VERSION_MAJOR) || SASL_VERSION_MAJOR < 2 +# undef HAVE_LDAP_SASL +#endif + +#ifndef LDAP_SASL_QUIET +# define LDAP_SASL_QUIET 0 /* Doesn't exist in Solaris LDAP */ +#endif + +/* Older versions may require calling ldap_result() twice */ +#if LDAP_VENDOR_VERSION <= 20112 +# define OPENLDAP_ASYNC_WORKAROUND +#endif + +/* Solaris LDAP library doesn't have LDAP_OPT_SUCCESS */ +#ifndef LDAP_OPT_SUCCESS +# define LDAP_OPT_SUCCESS LDAP_SUCCESS +#endif + +#define DB_LDAP_REQUEST_MAX_ATTEMPT_COUNT 3 + +static const char *LDAP_ESCAPE_CHARS = "*,\\#+<>;\"()= "; + +struct db_ldap_result { + int refcount; + LDAPMessage *msg; +}; + +struct db_ldap_value { + const char **values; + bool used; +}; + +struct db_ldap_result_iterate_context { + pool_t pool; + + struct ldap_request *ldap_request; + const ARRAY_TYPE(ldap_field) *attr_map; + unsigned int attr_idx; + + /* attribute name => value */ + HASH_TABLE(char *, struct db_ldap_value *) ldap_attrs; + + const char *val_1_arr[2]; + string_t *var, *debug; + + bool skip_null_values; + bool iter_dn_values; + LDAPMessage *ldap_msg; + LDAP *ld; +}; + +struct db_ldap_sasl_bind_context { + const char *authcid; + const char *passwd; + const char *realm; + const char *authzid; +}; + +#define DEF_STR(name) DEF_STRUCT_STR(name, ldap_settings) +#define DEF_INT(name) DEF_STRUCT_INT(name, ldap_settings) +#define DEF_BOOL(name) DEF_STRUCT_BOOL(name, ldap_settings) + +static struct setting_def setting_defs[] = { + DEF_STR(hosts), + DEF_STR(uris), + DEF_STR(dn), + DEF_STR(dnpass), + DEF_BOOL(auth_bind), + DEF_STR(auth_bind_userdn), + DEF_BOOL(tls), + DEF_BOOL(sasl_bind), + DEF_STR(sasl_mech), + DEF_STR(sasl_realm), + DEF_STR(sasl_authz_id), + DEF_STR(tls_ca_cert_file), + DEF_STR(tls_ca_cert_dir), + DEF_STR(tls_cert_file), + DEF_STR(tls_key_file), + DEF_STR(tls_cipher_suite), + DEF_STR(tls_require_cert), + DEF_STR(deref), + DEF_STR(scope), + DEF_STR(base), + DEF_INT(ldap_version), + DEF_STR(debug_level), + DEF_STR(ldaprc_path), + DEF_STR(user_attrs), + DEF_STR(user_filter), + DEF_STR(pass_attrs), + DEF_STR(pass_filter), + DEF_STR(iterate_attrs), + DEF_STR(iterate_filter), + DEF_STR(default_pass_scheme), + DEF_BOOL(userdb_warning_disable), + DEF_BOOL(blocking), + + { 0, NULL, 0 } +}; + +static struct ldap_settings default_ldap_settings = { + .hosts = NULL, + .uris = NULL, + .dn = NULL, + .dnpass = NULL, + .auth_bind = FALSE, + .auth_bind_userdn = NULL, + .tls = FALSE, + .sasl_bind = FALSE, + .sasl_mech = NULL, + .sasl_realm = NULL, + .sasl_authz_id = NULL, + .tls_ca_cert_file = NULL, + .tls_ca_cert_dir = NULL, + .tls_cert_file = NULL, + .tls_key_file = NULL, + .tls_cipher_suite = NULL, + .tls_require_cert = NULL, + .deref = "never", + .scope = "subtree", + .base = NULL, + .ldap_version = 3, + .debug_level = "0", + .ldaprc_path = "", + .user_attrs = "homeDirectory=home,uidNumber=uid,gidNumber=gid", + .user_filter = "(&(objectClass=posixAccount)(uid=%u))", + .pass_attrs = "uid=user,userPassword=password", + .pass_filter = "(&(objectClass=posixAccount)(uid=%u))", + .iterate_attrs = "uid=user", + .iterate_filter = "(objectClass=posixAccount)", + .default_pass_scheme = "crypt", + .userdb_warning_disable = FALSE, + .blocking = FALSE +}; + +static struct ldap_connection *ldap_connections = NULL; + +static int db_ldap_bind(struct ldap_connection *conn); +static void db_ldap_conn_close(struct ldap_connection *conn); +struct db_ldap_result_iterate_context * +db_ldap_result_iterate_init_full(struct ldap_connection *conn, + struct ldap_request_search *ldap_request, + LDAPMessage *res, bool skip_null_values, + bool iter_dn_values); +static bool db_ldap_abort_requests(struct ldap_connection *conn, + unsigned int max_count, + unsigned int timeout_secs, + bool error, const char *reason); +static void db_ldap_request_free(struct ldap_request *request); + +static int deref2str(const char *str, int *ref_r) +{ + if (strcasecmp(str, "never") == 0) + *ref_r = LDAP_DEREF_NEVER; + else if (strcasecmp(str, "searching") == 0) + *ref_r = LDAP_DEREF_SEARCHING; + else if (strcasecmp(str, "finding") == 0) + *ref_r = LDAP_DEREF_FINDING; + else if (strcasecmp(str, "always") == 0) + *ref_r = LDAP_DEREF_ALWAYS; + else + return -1; + return 0; +} + +static int scope2str(const char *str, int *scope_r) +{ + if (strcasecmp(str, "base") == 0) + *scope_r = LDAP_SCOPE_BASE; + else if (strcasecmp(str, "onelevel") == 0) + *scope_r = LDAP_SCOPE_ONELEVEL; + else if (strcasecmp(str, "subtree") == 0) + *scope_r = LDAP_SCOPE_SUBTREE; + else + return -1; + return 0; +} + +#ifdef OPENLDAP_TLS_OPTIONS +static int tls_require_cert2str(const char *str, int *value_r) +{ + if (strcasecmp(str, "never") == 0) + *value_r = LDAP_OPT_X_TLS_NEVER; + else if (strcasecmp(str, "hard") == 0) + *value_r = LDAP_OPT_X_TLS_HARD; + else if (strcasecmp(str, "demand") == 0) + *value_r = LDAP_OPT_X_TLS_DEMAND; + else if (strcasecmp(str, "allow") == 0) + *value_r = LDAP_OPT_X_TLS_ALLOW; + else if (strcasecmp(str, "try") == 0) + *value_r = LDAP_OPT_X_TLS_TRY; + else + return -1; + return 0; +} +#endif + +static int ldap_get_errno(struct ldap_connection *conn) +{ + int ret, err; + + ret = ldap_get_option(conn->ld, LDAP_OPT_ERROR_NUMBER, (void *) &err); + if (ret != LDAP_SUCCESS) { + e_error(conn->event, "Can't get error number: %s", + ldap_err2string(ret)); + return LDAP_UNAVAILABLE; + } + + return err; +} + +const char *ldap_get_error(struct ldap_connection *conn) +{ + const char *ret; + char *str = NULL; + + ret = ldap_err2string(ldap_get_errno(conn)); + + ldap_get_option(conn->ld, LDAP_OPT_ERROR_STRING, (void *)&str); + if (str != NULL) { + ret = t_strconcat(ret, ", ", str, NULL); + ldap_memfree(str); + } + ldap_set_option(conn->ld, LDAP_OPT_ERROR_STRING, NULL); + return ret; +} + +static void ldap_conn_reconnect(struct ldap_connection *conn) +{ + db_ldap_conn_close(conn); + if (db_ldap_connect(conn) < 0) + db_ldap_conn_close(conn); +} + +static int ldap_handle_error(struct ldap_connection *conn) +{ + int err = ldap_get_errno(conn); + + switch (err) { + case LDAP_SUCCESS: + i_unreached(); + case LDAP_SIZELIMIT_EXCEEDED: + case LDAP_TIMELIMIT_EXCEEDED: + case LDAP_NO_SUCH_ATTRIBUTE: + case LDAP_UNDEFINED_TYPE: + case LDAP_INAPPROPRIATE_MATCHING: + case LDAP_CONSTRAINT_VIOLATION: + case LDAP_TYPE_OR_VALUE_EXISTS: + case LDAP_INVALID_SYNTAX: + case LDAP_NO_SUCH_OBJECT: + case LDAP_ALIAS_PROBLEM: + case LDAP_INVALID_DN_SYNTAX: + case LDAP_IS_LEAF: + case LDAP_ALIAS_DEREF_PROBLEM: + case LDAP_FILTER_ERROR: + /* invalid input */ + return -1; + case LDAP_SERVER_DOWN: + case LDAP_TIMEOUT: + case LDAP_UNAVAILABLE: + case LDAP_BUSY: +#ifdef LDAP_CONNECT_ERROR + case LDAP_CONNECT_ERROR: +#endif + case LDAP_LOCAL_ERROR: + case LDAP_INVALID_CREDENTIALS: + case LDAP_OPERATIONS_ERROR: + default: + /* connection problems */ + ldap_conn_reconnect(conn); + return 0; + } +} + +static int db_ldap_request_bind(struct ldap_connection *conn, + struct ldap_request *request) +{ + struct ldap_request_bind *brequest = + (struct ldap_request_bind *)request; + + i_assert(request->type == LDAP_REQUEST_TYPE_BIND); + i_assert(request->msgid == -1); + i_assert(conn->conn_state == LDAP_CONN_STATE_BOUND_AUTH || + conn->conn_state == LDAP_CONN_STATE_BOUND_DEFAULT); + i_assert(conn->pending_count == 0); + + request->msgid = ldap_bind(conn->ld, brequest->dn, + request->auth_request->mech_password, + LDAP_AUTH_SIMPLE); + if (request->msgid == -1) { + e_error(authdb_event(request->auth_request), + "ldap_bind(%s) failed: %s", + brequest->dn, ldap_get_error(conn)); + if (ldap_handle_error(conn) < 0) { + /* broken request, remove it */ + return 0; + } + return -1; + } + conn->conn_state = LDAP_CONN_STATE_BINDING; + return 1; +} + +static int db_ldap_request_search(struct ldap_connection *conn, + struct ldap_request *request) +{ + struct ldap_request_search *srequest = + (struct ldap_request_search *)request; + + i_assert(conn->conn_state == LDAP_CONN_STATE_BOUND_DEFAULT); + i_assert(request->msgid == -1); + + request->msgid = + ldap_search(conn->ld, *srequest->base == '\0' ? NULL : + srequest->base, conn->set.ldap_scope, + srequest->filter, srequest->attributes, 0); + if (request->msgid == -1) { + e_error(authdb_event(request->auth_request), + "ldap_search(%s) parsing failed: %s", + srequest->filter, ldap_get_error(conn)); + if (ldap_handle_error(conn) < 0) { + /* broken request, remove it */ + return 0; + } + return -1; + } + return 1; +} + +static bool db_ldap_request_queue_next(struct ldap_connection *conn) +{ + struct ldap_request *request; + int ret = -1; + + /* connecting may call db_ldap_connect_finish(), which gets us back + here. so do the connection before checking the request queue. */ + if (db_ldap_connect(conn) < 0) + return FALSE; + + if (conn->pending_count == aqueue_count(conn->request_queue)) { + /* no non-pending requests */ + return FALSE; + } + if (conn->pending_count > DB_LDAP_MAX_PENDING_REQUESTS) { + /* wait until server has replied to some requests */ + return FALSE; + } + + request = array_idx_elem(&conn->request_array, + aqueue_idx(conn->request_queue, + conn->pending_count)); + + if (conn->pending_count > 0 && + request->type == LDAP_REQUEST_TYPE_BIND) { + /* we can't do binds until all existing requests are finished */ + return FALSE; + } + + switch (conn->conn_state) { + case LDAP_CONN_STATE_DISCONNECTED: + case LDAP_CONN_STATE_BINDING: + /* wait until we're in bound state */ + return FALSE; + case LDAP_CONN_STATE_BOUND_AUTH: + if (request->type == LDAP_REQUEST_TYPE_BIND) + break; + + /* bind to default dn first */ + i_assert(conn->pending_count == 0); + (void)db_ldap_bind(conn); + return FALSE; + case LDAP_CONN_STATE_BOUND_DEFAULT: + /* we can do anything in this state */ + break; + } + + if (request->send_count >= DB_LDAP_REQUEST_MAX_ATTEMPT_COUNT) { + /* Enough many times retried. Server just keeps disconnecting + whenever attempting to send the request. */ + ret = 0; + } else { + /* clear away any partial results saved before reconnecting */ + db_ldap_request_free(request); + + switch (request->type) { + case LDAP_REQUEST_TYPE_BIND: + ret = db_ldap_request_bind(conn, request); + break; + case LDAP_REQUEST_TYPE_SEARCH: + ret = db_ldap_request_search(conn, request); + break; + } + } + + if (ret > 0) { + /* success */ + i_assert(request->msgid != -1); + request->send_count++; + conn->pending_count++; + return TRUE; + } else if (ret < 0) { + /* disconnected */ + return FALSE; + } else { + /* broken request, remove from queue */ + aqueue_delete_tail(conn->request_queue); + request->callback(conn, request, NULL); + return TRUE; + } +} + +static void +db_ldap_check_hanging(struct ldap_connection *conn) +{ + struct ldap_request *first_request; + unsigned int count; + time_t secs_diff; + + count = aqueue_count(conn->request_queue); + if (count == 0) + return; + + first_request = array_idx_elem(&conn->request_array, + aqueue_idx(conn->request_queue, 0)); + secs_diff = ioloop_time - first_request->create_time; + if (secs_diff > DB_LDAP_REQUEST_LOST_TIMEOUT_SECS) { + db_ldap_abort_requests(conn, UINT_MAX, 0, TRUE, + "LDAP connection appears to be hanging"); + ldap_conn_reconnect(conn); + } +} + +void db_ldap_request(struct ldap_connection *conn, + struct ldap_request *request) +{ + i_assert(request->auth_request != NULL); + + request->msgid = -1; + request->create_time = ioloop_time; + + db_ldap_check_hanging(conn); + + aqueue_append(conn->request_queue, &request); + (void)db_ldap_request_queue_next(conn); +} + +static int db_ldap_connect_finish(struct ldap_connection *conn, int ret) +{ + if (ret == LDAP_SERVER_DOWN) { + e_error(conn->event, "Can't connect to server: %s", + conn->set.uris != NULL ? + conn->set.uris : conn->set.hosts); + return -1; + } + if (ret != LDAP_SUCCESS) { + e_error(conn->event, "binding failed (dn %s): %s", + conn->set.dn == NULL ? "(none)" : conn->set.dn, + ldap_get_error(conn)); + return -1; + } + + timeout_remove(&conn->to); + conn->conn_state = LDAP_CONN_STATE_BOUND_DEFAULT; + while (db_ldap_request_queue_next(conn)) + ; + return 0; +} + +static void db_ldap_default_bind_finished(struct ldap_connection *conn, + struct db_ldap_result *res) +{ + int ret; + + i_assert(conn->pending_count == 0); + conn->default_bind_msgid = -1; + + ret = ldap_result2error(conn->ld, res->msg, FALSE); + if (db_ldap_connect_finish(conn, ret) < 0) { + /* lost connection, close it */ + db_ldap_conn_close(conn); + } +} + +static bool db_ldap_abort_requests(struct ldap_connection *conn, + unsigned int max_count, + unsigned int timeout_secs, + bool error, const char *reason) +{ + struct ldap_request *request; + time_t diff; + bool aborts = FALSE; + + while (aqueue_count(conn->request_queue) > 0 && max_count > 0) { + request = array_idx_elem(&conn->request_array, + aqueue_idx(conn->request_queue, 0)); + + diff = ioloop_time - request->create_time; + if (diff < (time_t)timeout_secs) + break; + + /* timed out, abort */ + aqueue_delete_tail(conn->request_queue); + + if (request->msgid != -1) { + i_assert(conn->pending_count > 0); + conn->pending_count--; + } + if (error) { + e_error(authdb_event(request->auth_request), + "%s", reason); + } else { + e_info(authdb_event(request->auth_request), + "%s", reason); + } + request->callback(conn, request, NULL); + max_count--; + aborts = TRUE; + } + return aborts; +} + +static struct ldap_request * +db_ldap_find_request(struct ldap_connection *conn, int msgid, + unsigned int *idx_r) +{ + struct ldap_request *const *requests, *request = NULL; + unsigned int i, count; + + count = aqueue_count(conn->request_queue); + if (count == 0) + return NULL; + + requests = array_front(&conn->request_array); + for (i = 0; i < count; i++) { + request = requests[aqueue_idx(conn->request_queue, i)]; + if (request->msgid == msgid) { + *idx_r = i; + return request; + } + if (request->msgid == -1) + break; + } + return NULL; +} + +static int db_ldap_fields_get_dn(struct ldap_connection *conn, + struct ldap_request_search *request, + struct db_ldap_result *res) +{ + struct auth_request *auth_request = request->request.auth_request; + struct ldap_request_named_result *named_res; + struct db_ldap_result_iterate_context *ldap_iter; + const char *name, *const *values; + + ldap_iter = db_ldap_result_iterate_init_full(conn, request, res->msg, + TRUE, TRUE); + while (db_ldap_result_iterate_next(ldap_iter, &name, &values)) { + if (values[1] != NULL) { + e_warning(authdb_event(auth_request), + "Multiple values found for '%s', " + "using value '%s'", name, values[0]); + } + array_foreach_modifiable(&request->named_results, named_res) { + if (strcmp(named_res->field->name, name) != 0) + continue; + /* In future we could also support LDAP URLs here */ + named_res->dn = p_strdup(auth_request->pool, + values[0]); + } + } + db_ldap_result_iterate_deinit(&ldap_iter); + return 0; +} + +struct ldap_field_find_subquery_context { + ARRAY_TYPE(string) attr_names; + const char *name; +}; + +static int +db_ldap_field_subquery_find(const char *data, void *context, + const char **value_r, + const char **error_r ATTR_UNUSED) +{ + struct ldap_field_find_subquery_context *ctx = context; + char *ldap_attr; + const char *p; + + if (*data != '\0') { + data = t_strcut(data, ':'); + p = strchr(data, '@'); + if (p != NULL && strcmp(p+1, ctx->name) == 0) { + ldap_attr = p_strdup_until(unsafe_data_stack_pool, + data, p); + array_push_back(&ctx->attr_names, &ldap_attr); + } + } + *value_r = NULL; + return 1; +} + +static int +ldap_request_send_subquery(struct ldap_connection *conn, + struct ldap_request_search *request, + struct ldap_request_named_result *named_res) +{ + const struct ldap_field *field; + const char *p, *error; + char *name; + struct auth_request *auth_request = request->request.auth_request; + struct ldap_field_find_subquery_context ctx; + const struct var_expand_table *table = + auth_request_get_var_expand_table(auth_request, NULL); + const struct var_expand_func_table *ptr; + struct var_expand_func_table *ftable; + string_t *tmp_str = t_str_new(64); + ARRAY(struct var_expand_func_table) var_funcs_table; + t_array_init(&var_funcs_table, 8); + + for(ptr = auth_request_var_funcs_table; ptr->key != NULL; ptr++) { + array_push_back(&var_funcs_table, ptr); + } + ftable = array_append_space(&var_funcs_table); + ftable->key = "ldap"; + ftable->func = db_ldap_field_subquery_find; + ftable = array_append_space(&var_funcs_table); + ftable->key = "ldap_ptr"; + ftable->func = db_ldap_field_subquery_find; + array_append_zero(&var_funcs_table); + + i_zero(&ctx); + t_array_init(&ctx.attr_names, 8); + ctx.name = named_res->field->name; + + /* get the attributes names into array (ldapAttr@name -> ldapAttr) */ + array_foreach(request->attr_map, field) { + if (field->ldap_attr_name[0] == '\0') { + str_truncate(tmp_str, 0); + if (var_expand_with_funcs(tmp_str, field->value, table, + array_front(&var_funcs_table), &ctx, &error) <= 0) { + e_error(authdb_event(auth_request), + "Failed to expand subquery %s: %s", + field->value, error); + return -1; + } + } else { + p = strchr(field->ldap_attr_name, '@'); + if (p != NULL && + strcmp(p+1, named_res->field->name) == 0) { + name = p_strdup_until(unsafe_data_stack_pool, + field->ldap_attr_name, p); + array_push_back(&ctx.attr_names, &name); + } + } + } + array_append_zero(&ctx.attr_names); + + request->request.msgid = + ldap_search(conn->ld, named_res->dn, LDAP_SCOPE_BASE, + NULL, array_front_modifiable(&ctx.attr_names), 0); + if (request->request.msgid == -1) { + e_error(authdb_event(auth_request), + "ldap_search(dn=%s) failed: %s", + named_res->dn, ldap_get_error(conn)); + return -1; + } + return 0; +} + +static int db_ldap_search_save_result(struct ldap_request_search *request, + struct db_ldap_result *res) +{ + struct ldap_request_named_result *named_res; + + if (!array_is_created(&request->named_results)) { + if (request->result != NULL) + return -1; + request->result = res; + } else { + named_res = array_idx_modifiable(&request->named_results, + request->name_idx); + if (named_res->result != NULL) + return -1; + named_res->result = res; + } + res->refcount++; + return 0; +} + +static int db_ldap_search_next_subsearch(struct ldap_connection *conn, + struct ldap_request_search *request, + struct db_ldap_result *res) +{ + struct ldap_request_named_result *named_res; + const struct ldap_field *field; + + if (request->result != NULL) + res = request->result; + + if (!array_is_created(&request->named_results)) { + /* see if we need to do more LDAP queries */ + p_array_init(&request->named_results, + request->request.auth_request->pool, 2); + array_foreach(request->attr_map, field) { + if (!field->value_is_dn) + continue; + named_res = array_append_space(&request->named_results); + named_res->field = field; + } + if (db_ldap_fields_get_dn(conn, request, res) < 0) + return -1; + } else { + request->name_idx++; + } + while (request->name_idx < array_count(&request->named_results)) { + /* send the next LDAP query */ + named_res = array_idx_modifiable(&request->named_results, + request->name_idx); + if (named_res->dn != NULL) { + if (ldap_request_send_subquery(conn, request, + named_res) < 0) + return -1; + return 1; + } + /* dn field wasn't returned, skip this */ + request->name_idx++; + } + return 0; +} + +static bool +db_ldap_handle_request_result(struct ldap_connection *conn, + struct ldap_request *request, unsigned int idx, + struct db_ldap_result *res) +{ + struct ldap_request_search *srequest = NULL; + const struct ldap_request_named_result *named_res; + int ret; + bool final_result; + + i_assert(conn->pending_count > 0); + + if (request->type == LDAP_REQUEST_TYPE_BIND) { + i_assert(conn->conn_state == LDAP_CONN_STATE_BINDING); + i_assert(conn->pending_count == 1); + conn->conn_state = LDAP_CONN_STATE_BOUND_AUTH; + } else { + srequest = (struct ldap_request_search *)request; + switch (ldap_msgtype(res->msg)) { + case LDAP_RES_SEARCH_ENTRY: + case LDAP_RES_SEARCH_RESULT: + break; + case LDAP_RES_SEARCH_REFERENCE: + /* we're going to ignore this */ + return FALSE; + default: + e_error(conn->event, "Reply with unexpected type %d", + ldap_msgtype(res->msg)); + return TRUE; + } + } + if (ldap_msgtype(res->msg) == LDAP_RES_SEARCH_ENTRY) { + ret = LDAP_SUCCESS; + final_result = FALSE; + } else { + final_result = TRUE; + ret = ldap_result2error(conn->ld, res->msg, 0); + } + /* LDAP_NO_SUCH_OBJECT is returned for nonexistent base */ + if (ret != LDAP_SUCCESS && ret != LDAP_NO_SUCH_OBJECT && + request->type == LDAP_REQUEST_TYPE_SEARCH) { + /* handle search failures here */ + struct ldap_request_search *srequest = + (struct ldap_request_search *)request; + + if (!array_is_created(&srequest->named_results)) { + e_error(authdb_event(request->auth_request), + "ldap_search(base=%s filter=%s) failed: %s", + srequest->base, srequest->filter, + ldap_err2string(ret)); + } else { + named_res = array_idx(&srequest->named_results, + srequest->name_idx); + e_error(authdb_event(request->auth_request), + "ldap_search(base=%s) failed: %s", + named_res->dn, ldap_err2string(ret)); + } + res = NULL; + } + if (ret == LDAP_SUCCESS && srequest != NULL && !srequest->multi_entry) { + /* expand any @results */ + if (!final_result) { + if (db_ldap_search_save_result(srequest, res) < 0) { + e_error(authdb_event(request->auth_request), + "LDAP search returned multiple entries"); + res = NULL; + } else { + /* wait for finish */ + return FALSE; + } + } else { + ret = db_ldap_search_next_subsearch(conn, srequest, res); + if (ret > 0) { + /* more LDAP queries left */ + return FALSE; + } + if (ret < 0) + res = NULL; + } + } + if (res == NULL && !final_result) { + /* wait for the final reply */ + request->failed = TRUE; + return TRUE; + } + if (request->failed) + res = NULL; + if (final_result) { + conn->pending_count--; + aqueue_delete(conn->request_queue, idx); + } + + T_BEGIN { + if (res != NULL && srequest != NULL && srequest->result != NULL) + request->callback(conn, request, srequest->result->msg); + + request->callback(conn, request, res == NULL ? NULL : res->msg); + } T_END; + + if (idx > 0) { + /* see if there are timed out requests */ + if (db_ldap_abort_requests(conn, idx, + DB_LDAP_REQUEST_LOST_TIMEOUT_SECS, + TRUE, "Request lost")) + ldap_conn_reconnect(conn); + } + return TRUE; +} + +static void db_ldap_result_unref(struct db_ldap_result **_res) +{ + struct db_ldap_result *res = *_res; + + *_res = NULL; + i_assert(res->refcount > 0); + if (--res->refcount == 0) { + ldap_msgfree(res->msg); + i_free(res); + } +} + +static void +db_ldap_request_free(struct ldap_request *request) +{ + if (request->type == LDAP_REQUEST_TYPE_SEARCH) { + struct ldap_request_search *srequest = + (struct ldap_request_search *)request; + struct ldap_request_named_result *named_res; + + if (srequest->result != NULL) + db_ldap_result_unref(&srequest->result); + + if (array_is_created(&srequest->named_results)) { + array_foreach_modifiable(&srequest->named_results, named_res) { + if (named_res->result != NULL) + db_ldap_result_unref(&named_res->result); + } + array_free(&srequest->named_results); + srequest->name_idx = 0; + } + } +} + +static void +db_ldap_handle_result(struct ldap_connection *conn, struct db_ldap_result *res) +{ + struct auth_request *auth_request; + struct ldap_request *request; + unsigned int idx; + int msgid; + + msgid = ldap_msgid(res->msg); + if (msgid == conn->default_bind_msgid) { + db_ldap_default_bind_finished(conn, res); + return; + } + + request = db_ldap_find_request(conn, msgid, &idx); + if (request == NULL) { + e_error(conn->event, "Reply with unknown msgid %d", msgid); + ldap_conn_reconnect(conn); + return; + } + /* request is allocated from auth_request's pool */ + auth_request = request->auth_request; + auth_request_ref(auth_request); + if (db_ldap_handle_request_result(conn, request, idx, res)) + db_ldap_request_free(request); + auth_request_unref(&auth_request); +} + +static void ldap_input(struct ldap_connection *conn) +{ + struct timeval timeout; + struct db_ldap_result *res; + LDAPMessage *msg; + time_t prev_reply_diff; + int ret; + + do { + if (conn->ld == NULL) + return; + + i_zero(&timeout); + ret = ldap_result(conn->ld, LDAP_RES_ANY, 0, &timeout, &msg); +#ifdef OPENLDAP_ASYNC_WORKAROUND + if (ret == 0) { + /* try again, there may be another in buffer */ + ret = ldap_result(conn->ld, LDAP_RES_ANY, 0, + &timeout, &msg); + } +#endif + if (ret <= 0) + break; + + res = i_new(struct db_ldap_result, 1); + res->refcount = 1; + res->msg = msg; + db_ldap_handle_result(conn, res); + db_ldap_result_unref(&res); + } while (conn->io != NULL); + + prev_reply_diff = ioloop_time - conn->last_reply_stamp; + conn->last_reply_stamp = ioloop_time; + + if (ret > 0) { + /* input disabled, continue once it's enabled */ + i_assert(conn->io == NULL); + } else if (ret == 0) { + /* send more requests */ + while (db_ldap_request_queue_next(conn)) + ; + } else if (ldap_get_errno(conn) != LDAP_SERVER_DOWN) { + e_error(conn->event, "ldap_result() failed: %s", ldap_get_error(conn)); + ldap_conn_reconnect(conn); + } else if (aqueue_count(conn->request_queue) > 0 || + prev_reply_diff < DB_LDAP_IDLE_RECONNECT_SECS) { + e_error(conn->event, "Connection lost to LDAP server, reconnecting"); + ldap_conn_reconnect(conn); + } else { + /* server probably disconnected an idle connection. don't + reconnect until the next request comes. */ + db_ldap_conn_close(conn); + } +} + +#ifdef HAVE_LDAP_SASL +static int +sasl_interact(LDAP *ld ATTR_UNUSED, unsigned flags ATTR_UNUSED, + void *defaults, void *interact) +{ + struct db_ldap_sasl_bind_context *context = defaults; + sasl_interact_t *in; + const char *str; + + for (in = interact; in->id != SASL_CB_LIST_END; in++) { + switch (in->id) { + case SASL_CB_GETREALM: + str = context->realm; + break; + case SASL_CB_AUTHNAME: + str = context->authcid; + break; + case SASL_CB_USER: + str = context->authzid; + break; + case SASL_CB_PASS: + str = context->passwd; + break; + default: + str = NULL; + break; + } + if (str != NULL) { + in->len = strlen(str); + in->result = str; + } + + } + return LDAP_SUCCESS; +} +#endif + +static void ldap_connection_timeout(struct ldap_connection *conn) +{ + i_assert(conn->conn_state == LDAP_CONN_STATE_BINDING); + + e_error(conn->event, "Initial binding to LDAP server timed out"); + db_ldap_conn_close(conn); +} + +#ifdef HAVE_LDAP_SASL +static int db_ldap_bind_sasl(struct ldap_connection *conn) +{ + struct db_ldap_sasl_bind_context context; + int ret; + + i_zero(&context); + context.authcid = conn->set.dn; + context.passwd = conn->set.dnpass; + context.realm = conn->set.sasl_realm; + context.authzid = conn->set.sasl_authz_id; + + /* There doesn't seem to be a way to do SASL binding + asynchronously.. */ + ret = ldap_sasl_interactive_bind_s(conn->ld, NULL, + conn->set.sasl_mech, + NULL, NULL, LDAP_SASL_QUIET, + sasl_interact, &context); + if (db_ldap_connect_finish(conn, ret) < 0) + return -1; + + conn->conn_state = LDAP_CONN_STATE_BOUND_DEFAULT; + + return 0; +} +#else +static int db_ldap_bind_sasl(struct ldap_connection *conn ATTR_UNUSED) +{ + i_unreached(); /* already checked at init */ + + return -1; +} +#endif + +static int db_ldap_bind_simple(struct ldap_connection *conn) +{ + int msgid; + + i_assert(conn->conn_state != LDAP_CONN_STATE_BINDING); + i_assert(conn->default_bind_msgid == -1); + i_assert(conn->pending_count == 0); + + msgid = ldap_bind(conn->ld, conn->set.dn, conn->set.dnpass, + LDAP_AUTH_SIMPLE); + if (msgid == -1) { + i_assert(ldap_get_errno(conn) != LDAP_SUCCESS); + if (db_ldap_connect_finish(conn, ldap_get_errno(conn)) < 0) { + /* lost connection, close it */ + db_ldap_conn_close(conn); + } + return -1; + } + + conn->conn_state = LDAP_CONN_STATE_BINDING; + conn->default_bind_msgid = msgid; + + timeout_remove(&conn->to); + conn->to = timeout_add(DB_LDAP_REQUEST_LOST_TIMEOUT_SECS*1000, + ldap_connection_timeout, conn); + return 0; +} + +static int db_ldap_bind(struct ldap_connection *conn) +{ + if (conn->set.sasl_bind) { + if (db_ldap_bind_sasl(conn) < 0) + return -1; + } else { + if (db_ldap_bind_simple(conn) < 0) + return -1; + } + + return 0; +} + +static void db_ldap_get_fd(struct ldap_connection *conn) +{ + int ret; + + /* get the connection's fd */ + ret = ldap_get_option(conn->ld, LDAP_OPT_DESC, (void *)&conn->fd); + if (ret != LDAP_SUCCESS) { + i_fatal("LDAP %s: Can't get connection fd: %s", + conn->config_path, ldap_err2string(ret)); + } + if (conn->fd <= STDERR_FILENO) { + /* Solaris LDAP library seems to be broken */ + i_fatal("LDAP %s: Buggy LDAP library returned wrong fd: %d", + conn->config_path, conn->fd); + } + i_assert(conn->fd != -1); + net_set_nonblock(conn->fd, TRUE); +} + +static void ATTR_NULL(1) +db_ldap_set_opt(struct ldap_connection *conn, LDAP *ld, int opt, + const void *value, const char *optname, const char *value_str) +{ + int ret; + + ret = ldap_set_option(ld, opt, value); + if (ret != LDAP_SUCCESS) { + i_fatal("LDAP %s: Can't set option %s to %s: %s", + conn->config_path, optname, value_str, ldap_err2string(ret)); + } +} + +static void ATTR_NULL(1) +db_ldap_set_opt_str(struct ldap_connection *conn, LDAP *ld, int opt, + const char *value, const char *optname) +{ + if (value != NULL) + db_ldap_set_opt(conn, ld, opt, value, optname, value); +} + +static void db_ldap_set_tls_options(struct ldap_connection *conn) +{ +#ifdef OPENLDAP_TLS_OPTIONS + db_ldap_set_opt_str(conn, NULL, LDAP_OPT_X_TLS_CACERTFILE, + conn->set.tls_ca_cert_file, "tls_ca_cert_file"); + db_ldap_set_opt_str(conn, NULL, LDAP_OPT_X_TLS_CACERTDIR, + conn->set.tls_ca_cert_dir, "tls_ca_cert_dir"); + db_ldap_set_opt_str(conn, NULL, LDAP_OPT_X_TLS_CERTFILE, + conn->set.tls_cert_file, "tls_cert_file"); + db_ldap_set_opt_str(conn, NULL, LDAP_OPT_X_TLS_KEYFILE, + conn->set.tls_key_file, "tls_key_file"); + db_ldap_set_opt_str(conn, NULL, LDAP_OPT_X_TLS_CIPHER_SUITE, + conn->set.tls_cipher_suite, "tls_cipher_suite"); + if (conn->set.tls_require_cert != NULL) { + db_ldap_set_opt(conn, NULL, LDAP_OPT_X_TLS_REQUIRE_CERT, &conn->set.ldap_tls_require_cert_parsed, + "tls_require_cert", conn->set.tls_require_cert); + } +#else + if (conn->set.tls_ca_cert_file != NULL || + conn->set.tls_ca_cert_dir != NULL || + conn->set.tls_cert_file != NULL || + conn->set.tls_key_file != NULL || + conn->set.tls_cipher_suite != NULL) { + i_fatal("LDAP %s: tls_* settings aren't supported by your LDAP library - they must not be set", + conn->config_path); + } +#endif +} + +static void db_ldap_set_options(struct ldap_connection *conn) +{ + unsigned int ldap_version; + int value; + +#ifdef LDAP_OPT_NETWORK_TIMEOUT + struct timeval tv; + int ret; + + tv.tv_sec = DB_LDAP_CONNECT_TIMEOUT_SECS; tv.tv_usec = 0; + ret = ldap_set_option(conn->ld, LDAP_OPT_NETWORK_TIMEOUT, &tv); + if (ret != LDAP_SUCCESS) { + i_fatal("LDAP %s: Can't set network-timeout: %s", + conn->config_path, ldap_err2string(ret)); + } +#endif + + db_ldap_set_opt(conn, conn->ld, LDAP_OPT_DEREF, &conn->set.ldap_deref, + "deref", conn->set.deref); +#ifdef LDAP_OPT_DEBUG_LEVEL + if (str_to_int(conn->set.debug_level, &value) >= 0 && value != 0) { + db_ldap_set_opt(conn, NULL, LDAP_OPT_DEBUG_LEVEL, &value, + "debug_level", conn->set.debug_level); + event_set_forced_debug(conn->event, TRUE); + } +#endif + + ldap_version = conn->set.ldap_version; + db_ldap_set_opt(conn, conn->ld, LDAP_OPT_PROTOCOL_VERSION, &ldap_version, + "protocol_version", dec2str(ldap_version)); + db_ldap_set_tls_options(conn); +} + +static void db_ldap_init_ld(struct ldap_connection *conn) +{ + int ret; + + if (conn->set.uris != NULL) { +#ifdef LDAP_HAVE_INITIALIZE + ret = ldap_initialize(&conn->ld, conn->set.uris); + if (ret != LDAP_SUCCESS) { + i_fatal("LDAP %s: ldap_initialize() failed with uris %s: %s", + conn->config_path, conn->set.uris, + ldap_err2string(ret)); + } +#else + i_unreached(); /* already checked at init */ +#endif + } else { + conn->ld = ldap_init(conn->set.hosts, LDAP_PORT); + if (conn->ld == NULL) { + i_fatal("LDAP %s: ldap_init() failed with hosts: %s", + conn->config_path, conn->set.hosts); + } + } + db_ldap_set_options(conn); +} + +int db_ldap_connect(struct ldap_connection *conn) +{ + struct timeval start, end; + int ret; + + if (conn->conn_state != LDAP_CONN_STATE_DISCONNECTED) + return 0; + + i_gettimeofday(&start); + i_assert(conn->pending_count == 0); + + if (conn->delayed_connect) { + conn->delayed_connect = FALSE; + timeout_remove(&conn->to); + } + if (conn->ld == NULL) + db_ldap_init_ld(conn); + + if (conn->set.tls) { +#ifdef LDAP_HAVE_START_TLS_S + ret = ldap_start_tls_s(conn->ld, NULL, NULL); + if (ret != LDAP_SUCCESS) { + if (ret == LDAP_OPERATIONS_ERROR && + conn->set.uris != NULL && + str_begins(conn->set.uris, "ldaps:")) { + i_fatal("LDAP %s: Don't use both tls=yes " + "and ldaps URI", conn->config_path); + } + e_error(conn->event, "ldap_start_tls_s() failed: %s", + ldap_err2string(ret)); + return -1; + } +#else + i_unreached(); /* already checked at init */ +#endif + } + + if (db_ldap_bind(conn) < 0) + return -1; + + i_gettimeofday(&end); + int msecs = timeval_diff_msecs(&end, &start); + e_debug(conn->event, "LDAP initialization took %d msecs", msecs); + + db_ldap_get_fd(conn); + conn->io = io_add(conn->fd, IO_READ, ldap_input, conn); + return 0; +} + +static void db_ldap_connect_callback(struct ldap_connection *conn) +{ + i_assert(conn->conn_state == LDAP_CONN_STATE_DISCONNECTED); + (void)db_ldap_connect(conn); +} + +void db_ldap_connect_delayed(struct ldap_connection *conn) +{ + if (conn->delayed_connect) + return; + conn->delayed_connect = TRUE; + + i_assert(conn->to == NULL); + conn->to = timeout_add_short(0, db_ldap_connect_callback, conn); +} + +void db_ldap_enable_input(struct ldap_connection *conn, bool enable) +{ + if (!enable) { + io_remove(&conn->io); + } else { + if (conn->io == NULL && conn->fd != -1) { + conn->io = io_add(conn->fd, IO_READ, ldap_input, conn); + ldap_input(conn); + } + } +} + +static void db_ldap_disconnect_timeout(struct ldap_connection *conn) +{ + db_ldap_abort_requests(conn, UINT_MAX, + DB_LDAP_REQUEST_DISCONNECT_TIMEOUT_SECS, FALSE, + "Aborting (timeout), we're not connected to LDAP server"); + + if (aqueue_count(conn->request_queue) == 0) { + /* no requests left, remove this timeout handler */ + timeout_remove(&conn->to); + } +} + +static void db_ldap_conn_close(struct ldap_connection *conn) +{ + struct ldap_request *const *requests, *request; + unsigned int i; + + conn->conn_state = LDAP_CONN_STATE_DISCONNECTED; + conn->delayed_connect = FALSE; + conn->default_bind_msgid = -1; + + timeout_remove(&conn->to); + + if (conn->pending_count != 0) { + requests = array_front(&conn->request_array); + for (i = 0; i < conn->pending_count; i++) { + request = requests[aqueue_idx(conn->request_queue, i)]; + + i_assert(request->msgid != -1); + request->msgid = -1; + } + conn->pending_count = 0; + } + + if (conn->ld != NULL) { + ldap_unbind(conn->ld); + conn->ld = NULL; + } + conn->fd = -1; + + /* the fd may have already been closed before ldap_unbind(), + so we'll have to use io_remove_closed(). */ + io_remove_closed(&conn->io); + + if (aqueue_count(conn->request_queue) > 0) { + conn->to = timeout_add(DB_LDAP_REQUEST_DISCONNECT_TIMEOUT_SECS * + 1000/2, db_ldap_disconnect_timeout, conn); + } +} + +struct ldap_field_find_context { + ARRAY_TYPE(string) attr_names; + pool_t pool; +}; + +static int +db_ldap_field_find(const char *data, void *context, + const char **value_r, + const char **error_r ATTR_UNUSED) +{ + struct ldap_field_find_context *ctx = context; + char *ldap_attr; + + if (*data != '\0') { + ldap_attr = p_strdup(ctx->pool, t_strcut(data, ':')); + if (strchr(ldap_attr, '@') == NULL) + array_push_back(&ctx->attr_names, &ldap_attr); + } + *value_r = NULL; + return 1; +} + +void db_ldap_set_attrs(struct ldap_connection *conn, const char *attrlist, + char ***attr_names_r, ARRAY_TYPE(ldap_field) *attr_map, + const char *skip_attr) +{ + static struct var_expand_func_table var_funcs_table[] = { + { "ldap", db_ldap_field_find }, + { "ldap_ptr", db_ldap_field_find }, + { NULL, NULL } + }; + struct ldap_field_find_context ctx; + struct ldap_field *field; + string_t *tmp_str; + const char *const *attr, *attr_data, *p, *error; + char *ldap_attr, *name, *templ; + unsigned int i; + + if (*attrlist == '\0') + return; + + attr = t_strsplit_spaces(attrlist, ","); + + tmp_str = t_str_new(128); + ctx.pool = conn->pool; + p_array_init(&ctx.attr_names, conn->pool, 16); + for (i = 0; attr[i] != NULL; i++) { + /* allow spaces here so "foo=1, bar=2" works */ + attr_data = attr[i]; + while (*attr_data == ' ') attr_data++; + + p = strchr(attr_data, '='); + if (p == NULL) + ldap_attr = name = p_strdup(conn->pool, attr_data); + else if (attr_data[0] == '@') { + ldap_attr = ""; + name = p_strdup(conn->pool, attr_data); + } else { + ldap_attr = p_strdup_until(conn->pool, attr_data, p); + name = p_strdup(conn->pool, p + 1); + } + + templ = strchr(name, '='); + if (templ == NULL) { + if (*ldap_attr == '\0') { + /* =foo static value */ + templ = ""; + } + } else { + *templ++ = '\0'; + str_truncate(tmp_str, 0); + if (var_expand_with_funcs(tmp_str, templ, NULL, + var_funcs_table, &ctx, &error) <= 0) { + /* This var_expand_with_funcs call fills the + * ldap_field_find_context in ctx, but the + * resulting string_t is not used, and the + * return value or error_r is not checked since + * it gives errors for non-ldap variable + * expansions. */ + } + if (strchr(templ, '%') == NULL) { + /* backwards compatibility: + attr=name=prefix means same as + attr=name=prefix%$ when %vars are missing */ + templ = p_strconcat(conn->pool, templ, + "%$", NULL); + } + } + + if (*name == '\0') + e_error(conn->event, "Invalid attrs entry: %s", attr_data); + else if (skip_attr == NULL || strcmp(skip_attr, name) != 0) { + field = array_append_space(attr_map); + if (name[0] == '@') { + /* @name=ldapField */ + name++; + field->value_is_dn = TRUE; + } else if (name[0] == '!' && name == ldap_attr) { + /* !ldapAttr */ + name = ""; + i_assert(ldap_attr[0] == '!'); + ldap_attr++; + field->skip = TRUE; + } + field->name = name; + field->value = templ; + field->ldap_attr_name = ldap_attr; + if (*ldap_attr != '\0' && + strchr(ldap_attr, '@') == NULL) { + /* root request's attribute */ + array_push_back(&ctx.attr_names, &ldap_attr); + } + } + } + array_append_zero(&ctx.attr_names); + *attr_names_r = array_front_modifiable(&ctx.attr_names); +} + +static const struct var_expand_table * +db_ldap_value_get_var_expand_table(struct auth_request *auth_request, + const char *ldap_value) +{ + struct var_expand_table *table; + unsigned int count = 1; + + table = auth_request_get_var_expand_table_full(auth_request, + auth_request->fields.user, NULL, &count); + table[0].key = '$'; + table[0].value = ldap_value; + return table; +} + +#define IS_LDAP_ESCAPED_CHAR(c) \ + ((((unsigned char)(c)) & 0x80) != 0 || strchr(LDAP_ESCAPE_CHARS, (c)) != NULL) + +const char *ldap_escape(const char *str, + const struct auth_request *auth_request ATTR_UNUSED) +{ + string_t *ret = NULL; + + for (const char *p = str; *p != '\0'; p++) { + if (IS_LDAP_ESCAPED_CHAR(*p)) { + if (ret == NULL) { + ret = t_str_new((size_t) (p - str) + 64); + str_append_data(ret, str, (size_t) (p - str)); + } + str_printfa(ret, "\\%02X", (unsigned char)*p); + } else if (ret != NULL) + str_append_c(ret, *p); + } + + return ret == NULL ? str : str_c(ret); +} + +static bool +ldap_field_hide_password(struct db_ldap_result_iterate_context *ctx, + const char *attr) +{ + const struct ldap_field *field; + + if (ctx->ldap_request->auth_request->set->debug_passwords) + return FALSE; + + array_foreach(ctx->attr_map, field) { + if (strcmp(field->ldap_attr_name, attr) == 0) { + if (strcmp(field->name, "password") == 0 || + strcmp(field->name, "password_noscheme") == 0) + return TRUE; + } + } + return FALSE; +} + +static void +get_ldap_fields(struct db_ldap_result_iterate_context *ctx, + struct ldap_connection *conn, LDAPMessage *entry, + const char *suffix) +{ + struct db_ldap_value *ldap_value; + char *attr, **vals; + unsigned int i, count; + BerElement *ber; + + attr = ldap_first_attribute(conn->ld, entry, &ber); + while (attr != NULL) { + vals = ldap_get_values(conn->ld, entry, attr); + + ldap_value = p_new(ctx->pool, struct db_ldap_value, 1); + if (vals == NULL) { + ldap_value->values = p_new(ctx->pool, const char *, 1); + count = 0; + } else { + for (count = 0; vals[count] != NULL; count++) ; + } + + ldap_value->values = p_new(ctx->pool, const char *, count + 1); + for (i = 0; i < count; i++) + ldap_value->values[i] = p_strdup(ctx->pool, vals[i]); + + if (ctx->debug != NULL) { + str_printfa(ctx->debug, " %s%s=", attr, suffix); + if (count == 0) + str_append(ctx->debug, "<no values>"); + else if (ldap_field_hide_password(ctx, attr)) + str_append(ctx->debug, PASSWORD_HIDDEN_STR); + else { + str_append(ctx->debug, ldap_value->values[0]); + for (i = 1; i < count; i++) { + str_printfa(ctx->debug, ",%s", + ldap_value->values[0]); + } + } + } + hash_table_insert(ctx->ldap_attrs, + p_strconcat(ctx->pool, attr, suffix, NULL), + ldap_value); + + ldap_value_free(vals); + ldap_memfree(attr); + attr = ldap_next_attribute(conn->ld, entry, ber); + } + ber_free(ber, 0); +} + +struct db_ldap_result_iterate_context * +db_ldap_result_iterate_init_full(struct ldap_connection *conn, + struct ldap_request_search *ldap_request, + LDAPMessage *res, bool skip_null_values, + bool iter_dn_values) +{ + struct db_ldap_result_iterate_context *ctx; + const struct ldap_request_named_result *named_res; + const char *suffix; + pool_t pool; + + pool = pool_alloconly_create(MEMPOOL_GROWING"ldap result iter", 1024); + ctx = p_new(pool, struct db_ldap_result_iterate_context, 1); + ctx->pool = pool; + ctx->ldap_request = &ldap_request->request; + ctx->attr_map = ldap_request->attr_map; + ctx->skip_null_values = skip_null_values; + ctx->iter_dn_values = iter_dn_values; + hash_table_create(&ctx->ldap_attrs, pool, 0, strcase_hash, strcasecmp); + ctx->var = str_new(ctx->pool, 256); + if (event_want_debug(ctx->ldap_request->auth_request->event)) + ctx->debug = t_str_new(256); + ctx->ldap_msg = res; + ctx->ld = conn->ld; + + get_ldap_fields(ctx, conn, res, ""); + if (array_is_created(&ldap_request->named_results)) { + array_foreach(&ldap_request->named_results, named_res) { + suffix = t_strdup_printf("@%s", named_res->field->name); + if (named_res->result != NULL) { + get_ldap_fields(ctx, conn, + named_res->result->msg, suffix); + } + } + } + return ctx; +} + +struct db_ldap_result_iterate_context * +db_ldap_result_iterate_init(struct ldap_connection *conn, + struct ldap_request_search *ldap_request, + LDAPMessage *res, bool skip_null_values) +{ + return db_ldap_result_iterate_init_full(conn, ldap_request, res, + skip_null_values, FALSE); +} + +static const char *db_ldap_field_get_default(const char *data) +{ + const char *p; + + p = i_strchr_to_next(data, ':'); + if (p == NULL) + return ""; + else { + /* default value given */ + return p; + } +} + +static int +db_ldap_field_expand(const char *data, void *context, + const char **value_r, const char **error_r ATTR_UNUSED) +{ + struct db_ldap_result_iterate_context *ctx = context; + struct db_ldap_value *ldap_value; + const char *field_name = t_strcut(data, ':'); + + ldap_value = hash_table_lookup(ctx->ldap_attrs, field_name); + if (ldap_value == NULL) { + /* requested ldap attribute wasn't returned at all */ + if (ctx->debug != NULL) + str_printfa(ctx->debug, "; %s missing", field_name); + *value_r = db_ldap_field_get_default(data); + return 1; + } + ldap_value->used = TRUE; + + if (ldap_value->values[0] == NULL) { + /* no value for ldap attribute */ + *value_r = db_ldap_field_get_default(data); + return 1; + } + if (ldap_value->values[1] != NULL) { + e_warning(authdb_event(ctx->ldap_request->auth_request), + "Multiple values found for '%s', using value '%s'", + field_name, ldap_value->values[0]); + } + *value_r = ldap_value->values[0]; + return 1; +} + +static int +db_ldap_field_ptr_expand(const char *data, void *context, + const char **value_r, const char **error_r) +{ + struct db_ldap_result_iterate_context *ctx = context; + const char *field_name, *suffix; + + suffix = strchr(t_strcut(data, ':'), '@'); + if (db_ldap_field_expand(data, ctx, &field_name, error_r) <= 0) + i_unreached(); + if (field_name[0] == '\0') { + *value_r = ""; + return 1; + } + field_name = t_strconcat(field_name, suffix, NULL); + return db_ldap_field_expand(field_name, ctx, value_r, error_r); +} + +static int +db_ldap_field_dn_expand(const char *data ATTR_UNUSED, void *context ATTR_UNUSED, + const char **value_r, const char **error_r ATTR_UNUSED) +{ + struct db_ldap_result_iterate_context *ctx = context; + char *dn = ldap_get_dn(ctx->ld, ctx->ldap_msg); + *value_r = t_strdup(dn); + ldap_memfree(dn); + return 1; +} + +static struct var_expand_func_table ldap_var_funcs_table[] = { + { "ldap", db_ldap_field_expand }, + { "ldap_ptr", db_ldap_field_ptr_expand }, + { "ldap_dn", db_ldap_field_dn_expand }, + { NULL, NULL } +}; + +static const char *const * +db_ldap_result_return_value(struct db_ldap_result_iterate_context *ctx, + const struct ldap_field *field, + struct db_ldap_value *ldap_value) +{ + const struct var_expand_table *var_table; + const char *const *values, *error; + + if (ldap_value != NULL) + values = ldap_value->values; + else { + /* LDAP attribute doesn't exist */ + ctx->val_1_arr[0] = NULL; + values = ctx->val_1_arr; + } + + if (field->value == NULL) { + /* use the LDAP attribute's value */ + } else { + /* template */ + if (values[0] == NULL && *field->ldap_attr_name != '\0') { + /* ldapAttr=key=template%$, but ldapAttr doesn't + exist. */ + return values; + } + if (values[0] != NULL && values[1] != NULL) { + e_warning(authdb_event(ctx->ldap_request->auth_request), + "Multiple values found for '%s', " + "using value '%s'", + field->name, values[0]); + } + + /* do this lookup separately for each expansion, because: + 1) the values are allocated from data stack + 2) if "user" field is updated, we want %u/%n/%d updated + (and less importantly the same for other variables) */ + var_table = db_ldap_value_get_var_expand_table( + ctx->ldap_request->auth_request, values[0]); + if (var_expand_with_funcs(ctx->var, field->value, var_table, + ldap_var_funcs_table, ctx, &error) <= 0) { + e_warning(authdb_event(ctx->ldap_request->auth_request), + "Failed to expand template %s: %s", + field->value, error); + } + ctx->val_1_arr[0] = str_c(ctx->var); + values = ctx->val_1_arr; + } + return values; +} + +bool db_ldap_result_iterate_next(struct db_ldap_result_iterate_context *ctx, + const char **name_r, + const char *const **values_r) +{ + const struct var_expand_table *tab; + const struct ldap_field *field; + struct db_ldap_value *ldap_value; + unsigned int pos; + const char *error; + + do { + if (ctx->attr_idx == array_count(ctx->attr_map)) + return FALSE; + field = array_idx(ctx->attr_map, ctx->attr_idx++); + } while (field->value_is_dn != ctx->iter_dn_values || + field->skip); + + ldap_value = *field->ldap_attr_name == '\0' ? NULL : + hash_table_lookup(ctx->ldap_attrs, field->ldap_attr_name); + if (ldap_value != NULL) + ldap_value->used = TRUE; + else if (ctx->debug != NULL && *field->ldap_attr_name != '\0') + str_printfa(ctx->debug, "; %s missing", field->ldap_attr_name); + + str_truncate(ctx->var, 0); + *values_r = db_ldap_result_return_value(ctx, field, ldap_value); + + if (strchr(field->name, '%') == NULL) + *name_r = field->name; + else { + /* expand %variables also for LDAP name fields. we'll use the + same ctx->var, which may already contain the value. */ + str_append_c(ctx->var, '\0'); + pos = str_len(ctx->var); + + tab = auth_request_get_var_expand_table( + ctx->ldap_request->auth_request, NULL); + if (var_expand_with_funcs(ctx->var, field->name, tab, + ldap_var_funcs_table, ctx, &error) <= 0) { + e_warning(authdb_event(ctx->ldap_request->auth_request), + "Failed to expand %s: %s", field->name, error); + } + *name_r = str_c(ctx->var) + pos; + } + + if (ctx->skip_null_values && (*values_r)[0] == NULL) { + /* no values. don't confuse the caller with this reply. */ + return db_ldap_result_iterate_next(ctx, name_r, values_r); + } + return TRUE; +} + +static void +db_ldap_result_finish_debug(struct db_ldap_result_iterate_context *ctx) +{ + struct hash_iterate_context *iter; + char *name; + struct db_ldap_value *value; + unsigned int unused_count = 0; + size_t orig_len; + + if (ctx->ldap_request->result_logged) + return; + + orig_len = str_len(ctx->debug); + if (orig_len == 0) { + e_debug(authdb_event(ctx->ldap_request->auth_request), + "no fields returned by the server"); + return; + } + + str_append(ctx->debug, "; "); + + iter = hash_table_iterate_init(ctx->ldap_attrs); + while (hash_table_iterate(iter, ctx->ldap_attrs, &name, &value)) { + if (!value->used) { + str_printfa(ctx->debug, "%s,", name); + unused_count++; + } + } + hash_table_iterate_deinit(&iter); + + if (unused_count == 0) + str_truncate(ctx->debug, orig_len); + else { + str_truncate(ctx->debug, str_len(ctx->debug)-1); + str_append(ctx->debug, " unused"); + } + e_debug(authdb_event(ctx->ldap_request->auth_request), + "result: %s", str_c(ctx->debug) + 1); + + ctx->ldap_request->result_logged = TRUE; +} + +void db_ldap_result_iterate_deinit(struct db_ldap_result_iterate_context **_ctx) +{ + struct db_ldap_result_iterate_context *ctx = *_ctx; + + *_ctx = NULL; + + if (ctx->debug != NULL) + db_ldap_result_finish_debug(ctx); + hash_table_destroy(&ctx->ldap_attrs); + pool_unref(&ctx->pool); +} + +static const char *parse_setting(const char *key, const char *value, + struct ldap_connection *conn) +{ + return parse_setting_from_defs(conn->pool, setting_defs, + &conn->set, key, value); +} + +static struct ldap_connection *ldap_conn_find(const char *config_path) +{ + struct ldap_connection *conn; + + for (conn = ldap_connections; conn != NULL; conn = conn->next) { + if (strcmp(conn->config_path, config_path) == 0) + return conn; + } + + return NULL; +} + +struct ldap_connection *db_ldap_init(const char *config_path, bool userdb) +{ + struct ldap_connection *conn; + const char *str, *error; + pool_t pool; + + /* see if it already exists */ + conn = ldap_conn_find(config_path); + if (conn != NULL) { + if (userdb) + conn->userdb_used = TRUE; + conn->refcount++; + return conn; + } + + if (*config_path == '\0') + i_fatal("LDAP: Configuration file path not given"); + + pool = pool_alloconly_create("ldap_connection", 1024); + conn = p_new(pool, struct ldap_connection, 1); + conn->pool = pool; + conn->refcount = 1; + + conn->userdb_used = userdb; + conn->conn_state = LDAP_CONN_STATE_DISCONNECTED; + conn->default_bind_msgid = -1; + conn->fd = -1; + conn->config_path = p_strdup(pool, config_path); + conn->set = default_ldap_settings; + if (!settings_read_nosection(config_path, parse_setting, conn, &error)) + i_fatal("ldap %s: %s", config_path, error); + + if (conn->set.base == NULL) + i_fatal("LDAP %s: No base given", config_path); + + if (conn->set.uris == NULL && conn->set.hosts == NULL) + i_fatal("LDAP %s: No uris or hosts set", config_path); +#ifndef LDAP_HAVE_INITIALIZE + if (conn->set.uris != NULL) { + i_fatal("LDAP %s: uris set, but Dovecot compiled without support for LDAP uris " + "(ldap_initialize() not supported by LDAP library)", config_path); + } +#endif +#ifndef LDAP_HAVE_START_TLS_S + if (conn->set.tls) + i_fatal("LDAP %s: tls=yes, but your LDAP library doesn't support TLS", config_path); +#endif +#ifndef HAVE_LDAP_SASL + if (conn->set.sasl_bind) + i_fatal("LDAP %s: sasl_bind=yes but no SASL support compiled in", conn->config_path); +#endif + if (conn->set.ldap_version < 3) { + if (conn->set.sasl_bind) + i_fatal("LDAP %s: sasl_bind=yes requires ldap_version=3", config_path); + if (conn->set.tls) + i_fatal("LDAP %s: tls=yes requires ldap_version=3", config_path); + } +#ifdef OPENLDAP_TLS_OPTIONS + if (conn->set.tls_require_cert != NULL) { + if (tls_require_cert2str(conn->set.tls_require_cert, + &conn->set.ldap_tls_require_cert_parsed) < 0) + i_fatal("LDAP %s: Unknown tls_require_cert value '%s'", + config_path, conn->set.tls_require_cert); + } +#endif + + if (*conn->set.ldaprc_path != '\0') { + str = getenv("LDAPRC"); + if (str != NULL && strcmp(str, conn->set.ldaprc_path) != 0) { + i_fatal("LDAP %s: Multiple different ldaprc_path " + "settings not allowed (%s and %s)", + config_path, str, conn->set.ldaprc_path); + } + env_put("LDAPRC", conn->set.ldaprc_path); + } + + if (deref2str(conn->set.deref, &conn->set.ldap_deref) < 0) + i_fatal("LDAP %s: Unknown deref option '%s'", config_path, conn->set.deref); + if (scope2str(conn->set.scope, &conn->set.ldap_scope) < 0) + i_fatal("LDAP %s: Unknown scope option '%s'", config_path, conn->set.scope); + + conn->event = event_create(auth_event); + event_set_append_log_prefix(conn->event, t_strdup_printf( + "ldap(%s): ", conn->config_path)); + + i_array_init(&conn->request_array, 512); + conn->request_queue = aqueue_init(&conn->request_array.arr); + + conn->next = ldap_connections; + ldap_connections = conn; + + db_ldap_init_ld(conn); + return conn; +} + +void db_ldap_unref(struct ldap_connection **_conn) +{ + struct ldap_connection *conn = *_conn; + struct ldap_connection **p; + + *_conn = NULL; + i_assert(conn->refcount >= 0); + if (--conn->refcount > 0) + return; + + for (p = &ldap_connections; *p != NULL; p = &(*p)->next) { + if (*p == conn) { + *p = conn->next; + break; + } + } + + db_ldap_abort_requests(conn, UINT_MAX, 0, FALSE, "Shutting down"); + i_assert(conn->pending_count == 0); + db_ldap_conn_close(conn); + i_assert(conn->to == NULL); + + array_free(&conn->request_array); + aqueue_deinit(&conn->request_queue); + + event_unref(&conn->event); + pool_unref(&conn->pool); +} + +#ifndef BUILTIN_LDAP +/* Building a plugin */ +extern struct passdb_module_interface passdb_ldap_plugin; +extern struct userdb_module_interface userdb_ldap_plugin; + +void authdb_ldap_init(void); +void authdb_ldap_deinit(void); + +void authdb_ldap_init(void) +{ + passdb_register_module(&passdb_ldap_plugin); + userdb_register_module(&userdb_ldap_plugin); + +} +void authdb_ldap_deinit(void) +{ + passdb_unregister_module(&passdb_ldap_plugin); + userdb_unregister_module(&userdb_ldap_plugin); +} +#endif + +#endif diff --git a/src/auth/db-ldap.h b/src/auth/db-ldap.h new file mode 100644 index 0000000..e919e79 --- /dev/null +++ b/src/auth/db-ldap.h @@ -0,0 +1,221 @@ +#ifndef DB_LDAP_H +#define DB_LDAP_H + +/* Functions like ldap_bind() have been deprecated in OpenLDAP 2.3 + This define enables them until the code here can be refactored */ +#define LDAP_DEPRECATED 1 + +/* Maximum number of pending requests before delaying new requests. */ +#define DB_LDAP_MAX_PENDING_REQUESTS 8 +/* connect() timeout to LDAP */ +#define DB_LDAP_CONNECT_TIMEOUT_SECS 5 +/* If LDAP connection is down, fail requests after waiting for this long. */ +#define DB_LDAP_REQUEST_DISCONNECT_TIMEOUT_SECS 4 +/* If request is still in queue after this many seconds and other requests + have been replied, assume the request was lost and abort it. */ +#define DB_LDAP_REQUEST_LOST_TIMEOUT_SECS 60 +/* If server disconnects us, don't reconnect if no requests have been sent + for this many seconds. */ +#define DB_LDAP_IDLE_RECONNECT_SECS 60 + +#include <ldap.h> + +struct auth_request; +struct ldap_connection; +struct ldap_request; + +typedef void db_search_callback_t(struct ldap_connection *conn, + struct ldap_request *request, + LDAPMessage *res); + +struct ldap_settings { + const char *hosts; + const char *uris; + const char *dn; + const char *dnpass; + bool auth_bind; + const char *auth_bind_userdn; + + bool tls; + bool sasl_bind; + const char *sasl_mech; + const char *sasl_realm; + const char *sasl_authz_id; + + const char *tls_ca_cert_file; + const char *tls_ca_cert_dir; + const char *tls_cert_file; + const char *tls_key_file; + const char *tls_cipher_suite; + const char *tls_require_cert; + + const char *deref; + const char *scope; + const char *base; + unsigned int ldap_version; + + const char *ldaprc_path; + const char *debug_level; + + const char *user_attrs; + const char *user_filter; + const char *pass_attrs; + const char *pass_filter; + const char *iterate_attrs; + const char *iterate_filter; + + const char *default_pass_scheme; + bool userdb_warning_disable; /* deprecated for now at least */ + bool blocking; + + /* ... */ + int ldap_deref, ldap_scope, ldap_tls_require_cert_parsed; + uid_t uid; + gid_t gid; +}; + +enum ldap_request_type { + LDAP_REQUEST_TYPE_SEARCH, + LDAP_REQUEST_TYPE_BIND +}; + +struct ldap_field { + /* Dovecot field name. */ + const char *name; + /* Field value template with %vars. NULL = same as LDAP value. */ + const char *value; + /* LDAP attribute name, or "" if this is a static field. */ + const char *ldap_attr_name; + + /* LDAP value contains a DN, which is looked up and used for @name + attributes. */ + bool value_is_dn; + /* This attribute is used internally only via %{ldap_ptr}, + it shouldn't be returned in iteration. */ + bool skip; +}; +ARRAY_DEFINE_TYPE(ldap_field, struct ldap_field); + +struct ldap_request { + enum ldap_request_type type; + + /* msgid for sent requests, -1 if not sent */ + int msgid; + /* timestamp when request was created */ + time_t create_time; + + /* Number of times this request has been sent to LDAP server. This + increases when LDAP gets disconnected and reconnect send the request + again. */ + unsigned int send_count; + + bool failed:1; + /* This is to prevent double logging the result */ + bool result_logged:1; + + db_search_callback_t *callback; + struct auth_request *auth_request; +}; + +struct ldap_request_named_result { + const struct ldap_field *field; + const char *dn; + struct db_ldap_result *result; +}; + +struct ldap_request_search { + struct ldap_request request; + + const char *base; + const char *filter; + char **attributes; /* points to pass_attr_names / user_attr_names */ + const ARRAY_TYPE(ldap_field) *attr_map; + + struct db_ldap_result *result; + ARRAY(struct ldap_request_named_result) named_results; + unsigned int name_idx; + + bool multi_entry; +}; + +struct ldap_request_bind { + struct ldap_request request; + + const char *dn; +}; + +enum ldap_connection_state { + /* Not connected */ + LDAP_CONN_STATE_DISCONNECTED, + /* Binding - either to default dn or doing auth bind */ + LDAP_CONN_STATE_BINDING, + /* Bound to auth dn */ + LDAP_CONN_STATE_BOUND_AUTH, + /* Bound to default dn */ + LDAP_CONN_STATE_BOUND_DEFAULT +}; + +struct ldap_connection { + struct ldap_connection *next; + + pool_t pool; + int refcount; + struct event *event; + + char *config_path; + struct ldap_settings set; + + LDAP *ld; + enum ldap_connection_state conn_state; + int default_bind_msgid; + + int fd; + struct io *io; + struct timeout *to; + + /* Request queue contains sent requests at tail (msgid != -1) and + queued requests at head (msgid == -1). */ + struct aqueue *request_queue; + ARRAY(struct ldap_request *) request_array; + /* Number of messages in queue with msgid != -1 */ + unsigned int pending_count; + + /* Timestamp when we last received a reply */ + time_t last_reply_stamp; + + char **pass_attr_names, **user_attr_names, **iterate_attr_names; + ARRAY_TYPE(ldap_field) pass_attr_map, user_attr_map, iterate_attr_map; + bool userdb_used; + bool delayed_connect; +}; + +/* Send/queue request */ +void db_ldap_request(struct ldap_connection *conn, + struct ldap_request *request); + +void db_ldap_set_attrs(struct ldap_connection *conn, const char *attrlist, + char ***attr_names_r, ARRAY_TYPE(ldap_field) *attr_map, + const char *skip_attr) ATTR_NULL(5); + +struct ldap_connection *db_ldap_init(const char *config_path, bool userdb); +void db_ldap_unref(struct ldap_connection **conn); + +int db_ldap_connect(struct ldap_connection *conn); +void db_ldap_connect_delayed(struct ldap_connection *conn); + +void db_ldap_enable_input(struct ldap_connection *conn, bool enable); + +const char *ldap_escape(const char *str, + const struct auth_request *auth_request); +const char *ldap_get_error(struct ldap_connection *conn); + +struct db_ldap_result_iterate_context * +db_ldap_result_iterate_init(struct ldap_connection *conn, + struct ldap_request_search *ldap_request, + LDAPMessage *res, bool skip_null_values); +bool db_ldap_result_iterate_next(struct db_ldap_result_iterate_context *ctx, + const char **name_r, + const char *const **values_r); +void db_ldap_result_iterate_deinit(struct db_ldap_result_iterate_context **ctx); + +#endif diff --git a/src/auth/db-lua.c b/src/auth/db-lua.c new file mode 100644 index 0000000..dd4e77b --- /dev/null +++ b/src/auth/db-lua.c @@ -0,0 +1,798 @@ +/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#if defined(BUILTIN_LUA) || defined(PLUGIN_BUILD) + +#include "llist.h" +#include "istream.h" +#include "array.h" +#include "sha1.h" +#include "hex-binary.h" +#include "auth.h" +#include "passdb.h" +#include "userdb.h" +#include "auth-request.h" +#include "userdb-template.h" +#include "passdb-template.h" +#include "password-scheme.h" +#include "auth-request-var-expand.h" + +#define AUTH_LUA_PASSDB_LOOKUP "auth_passdb_lookup" +#define AUTH_LUA_USERDB_LOOKUP "auth_userdb_lookup" +#define AUTH_LUA_USERDB_ITERATE "auth_userdb_iterate" + +#define AUTH_LUA_DOVECOT_AUTH "dovecot_auth" +#define AUTH_LUA_AUTH_REQUEST "auth_request*" + +#include "db-lua.h" +#include "dlua-script-private.h" + +struct auth_lua_userdb_iterate_context { + struct userdb_iterate_context ctx; + pool_t pool; + unsigned int idx; + ARRAY_TYPE(const_string) users; +}; + +static struct auth_request * +auth_lua_check_auth_request(lua_State *L, int arg); + +static int +auth_request_lua_do_var_expand(struct auth_request *req, const char *tpl, + const char **value_r, const char **error_r) +{ + const char *error; + if (t_auth_request_var_expand(tpl, req, NULL, value_r, &error) < 0) { + *error_r = t_strdup_printf("var_expand(%s) failed: %s", + tpl, error); + return -1; + } + return 0; +} + +static int auth_request_lua_var_expand(lua_State *L) +{ + struct auth_request *req = auth_lua_check_auth_request(L, 1); + const char *tpl = luaL_checkstring(L, 2); + const char *value, *error; + + if (auth_request_lua_do_var_expand(req, tpl, &value, &error) < 0) { + return luaL_error(L, "%s", error); + } else { + lua_pushstring(L, value); + } + return 1; +} + +static const char *const * +auth_request_template_build(struct auth_request *req, const char *str, + unsigned int *count_r) +{ + if (req->userdb_lookup) { + struct userdb_template *tpl = + userdb_template_build(pool_datastack_create(), "lua", str); + if (userdb_template_is_empty(tpl)) + return NULL; + return userdb_template_get_args(tpl, count_r); + } else { + struct passdb_template *tpl = + passdb_template_build(pool_datastack_create(), str); + if (passdb_template_is_empty(tpl)) + return NULL; + return passdb_template_get_args(tpl, count_r); + } +} + +static int auth_request_lua_response_from_template(lua_State *L) +{ + struct auth_request *req = auth_lua_check_auth_request(L, 1); + const char *tplstr = luaL_checkstring(L, 2); + const char *error,*expanded; + unsigned int count,i; + + const char *const *fields = auth_request_template_build(req, tplstr, &count); + + /* push new table to stack */ + lua_newtable(L); + + if (fields == NULL) + return 1; + + i_assert((count % 2) == 0); + + for(i = 0; i < count; i+=2) { + const char *key = fields[i]; + const char *value = fields[i+1]; + + if (value == NULL) { + lua_pushnil(L); + } else if (auth_request_lua_do_var_expand(req, value, &expanded, &error) < 0) { + return luaL_error(L, "%s", error); + } else { + lua_pushstring(L, expanded); + } + lua_setfield(L, -2, key); + } + + /* stack should be left with table */ + return 1; +} + +static int auth_request_lua_log_debug(lua_State *L) +{ + if (global_auth_settings->debug) { + struct auth_request *request = auth_lua_check_auth_request(L, 1); + const char *msg = luaL_checkstring(L, 2); + e_debug(authdb_event(request), "db-lua: %s", msg); + } + return 0; +} + +static int auth_request_lua_log_info(lua_State *L) +{ + struct auth_request *request = auth_lua_check_auth_request(L, 1); + const char *msg = luaL_checkstring(L, 2); + e_info(authdb_event(request), "db-lua: %s", msg); + return 0; +} + +static int auth_request_lua_log_warning(lua_State *L) +{ + struct auth_request *request = auth_lua_check_auth_request(L, 1); + const char *msg = luaL_checkstring(L, 2); + e_warning(authdb_event(request), "db-lua: %s", msg); + return 0; +} + +static int auth_request_lua_log_error(lua_State *L) +{ + struct auth_request *request = auth_lua_check_auth_request(L, 1); + const char *msg = luaL_checkstring(L, 2); + e_error(authdb_event(request), "db-lua: %s", msg); + return 0; +} + +static int auth_request_lua_passdb(lua_State *L) +{ + struct auth_request *request = auth_lua_check_auth_request(L, 1); + const char *key = luaL_checkstring(L, 2); + lua_pop(L, 1); + + if (request->fields.extra_fields == NULL) { + lua_pushnil(L); + return 1; + } + + lua_pushstring(L, auth_fields_find(request->fields.extra_fields, key)); + return 1; +} + +static int auth_request_lua_userdb(lua_State *L) +{ + struct auth_request *request = auth_lua_check_auth_request(L, 1); + const char *key = luaL_checkstring(L, 2); + lua_pop(L, 1); + + if (request->fields.userdb_reply == NULL) { + lua_pushnil(L); + return 1; + } + + lua_pushstring(L, auth_fields_find(request->fields.userdb_reply, key)); + return 1; +} + +static int auth_request_lua_password_verify(lua_State *L) +{ + struct auth_request *request = auth_lua_check_auth_request(L, 1); + const char *crypted_password = lua_tostring(L, 2); + const char *scheme; + const char *plain_password = lua_tostring(L, 3); + const char *error = NULL; + const unsigned char *raw_password = NULL; + size_t raw_password_size; + int ret; + struct password_generate_params gen_params = { + .user = request->fields.original_username, + .rounds = 0 + }; + scheme = password_get_scheme(&crypted_password); + if (scheme == NULL) + scheme = "PLAIN"; + ret = password_decode(crypted_password, scheme, + &raw_password, &raw_password_size, &error); + if (ret <= 0) { + if (ret < 0) { + error = t_strdup_printf("Password data is not valid for scheme %s: %s", + scheme, error); + } else { + error = t_strdup_printf("Unknown scheme %s", scheme); + } + } else { + /* Use original_username since it may be important for some + password schemes (eg. digest-md5). + */ + ret = password_verify(plain_password, &gen_params, + scheme, raw_password, raw_password_size, &error); + } + + lua_pushnumber(L, ret); + lua_pushstring(L, error); + + return 2; +} + +static int auth_request_lua_event(lua_State *L) +{ + struct auth_request *request = auth_lua_check_auth_request(L, 1); + struct event *event = event_create(authdb_event(request)); + + dlua_push_event(L, event); + event_unref(&event); + return 1; +} + +/* put all methods here */ +static const luaL_Reg auth_request_methods[] ={ + { "var_expand", auth_request_lua_var_expand }, + { "response_from_template", auth_request_lua_response_from_template }, + { "log_debug", auth_request_lua_log_debug }, + { "log_info", auth_request_lua_log_info }, + { "log_warning", auth_request_lua_log_warning }, + { "log_error", auth_request_lua_log_error }, + { "password_verify", auth_request_lua_password_verify }, + { "event", auth_request_lua_event }, + { NULL, NULL } +}; + +static int auth_request_lua_index(lua_State *L) +{ + struct auth_request *req = auth_lua_check_auth_request(L, 1); + const char *key = luaL_checkstring(L, 2); + lua_pop(L, 1); + + const struct var_expand_table *table = + auth_request_get_var_expand_table(req, NULL); + + /* check if it's variable */ + for(unsigned int i = 0; i < AUTH_REQUEST_VAR_TAB_COUNT; i++) { + if (null_strcmp(table[i].long_key, key) == 0) { + lua_pushstring(L, table[i].value); + return 1; + } + } + + /* check if it's function, then */ + const luaL_Reg *ptr = auth_request_methods; + while(ptr->name != NULL) { + if (null_strcmp(key, ptr->name) == 0) { + lua_pushcfunction(L, ptr->func); + return 1; + } + ptr++; + } + + lua_pushstring(L, key); + lua_rawget(L, 1); + + return 1; +} + +static void auth_lua_push_auth_request(lua_State *L, struct auth_request *req) +{ + luaL_checkstack(L, 4, "out of memory"); + /* create a table for holding few things */ + lua_createtable(L, 0, 3); + luaL_setmetatable(L, AUTH_LUA_AUTH_REQUEST); + + lua_pushlightuserdata(L, req); + lua_setfield(L, -2, "item"); + + lua_newtable(L); + lua_pushlightuserdata(L, req); + lua_setfield(L, -2, "item"); + luaL_setmetatable(L, "passdb_"AUTH_LUA_AUTH_REQUEST); + lua_setfield(L, -2, "passdb"); + + lua_newtable(L); + lua_pushlightuserdata(L, req); + lua_setfield(L, -2, "item"); + luaL_setmetatable(L, "userdb_"AUTH_LUA_AUTH_REQUEST); + lua_setfield(L, -2, "userdb"); + + lua_pushboolean(L, req->fields.skip_password_check); + lua_setfield(L, -2, "skip_password_check"); + +#undef LUA_TABLE_SET_BOOL +#define LUA_TABLE_SET_BOOL(field) \ + lua_pushboolean(L, req->field); \ + lua_setfield(L, -2, #field); + + LUA_TABLE_SET_BOOL(passdbs_seen_user_unknown); + LUA_TABLE_SET_BOOL(passdbs_seen_internal_failure); + LUA_TABLE_SET_BOOL(userdbs_seen_internal_failure); +} + +static struct auth_request * +auth_lua_check_auth_request(lua_State *L, int arg) +{ + if (!lua_istable(L, arg)) { + (void)luaL_error(L, "Bad argument #%d, expected %s got %s", + arg, "auth_request", + lua_typename(L, lua_type(L, arg))); + } + lua_pushstring(L, "item"); + lua_rawget(L, arg); + void *bp = (void*)lua_touserdata(L, -1); + lua_pop(L, 1); + return (struct auth_request*)bp; +} + +static void auth_lua_auth_request_register(lua_State *L) +{ + luaL_newmetatable(L, AUTH_LUA_AUTH_REQUEST); + lua_pushcfunction(L, auth_request_lua_index); + lua_setfield(L, -2, "__index"); + lua_pop(L, 1); + + /* register passdb */ + luaL_newmetatable(L, "passdb_"AUTH_LUA_AUTH_REQUEST); + lua_pushcfunction(L, auth_request_lua_passdb); + lua_setfield(L, -2, "__index"); + lua_pop(L, 1); + + /* register userdb */ + luaL_newmetatable(L, "userdb_"AUTH_LUA_AUTH_REQUEST); + lua_pushcfunction(L, auth_request_lua_userdb); + lua_setfield(L, -2, "__index"); + lua_pop(L, 1); +} + +static struct dlua_table_values auth_lua_dovecot_auth_values[] = { + DLUA_TABLE_ENUM(PASSDB_RESULT_INTERNAL_FAILURE), + DLUA_TABLE_ENUM(PASSDB_RESULT_SCHEME_NOT_AVAILABLE), + DLUA_TABLE_ENUM(PASSDB_RESULT_USER_UNKNOWN), + DLUA_TABLE_ENUM(PASSDB_RESULT_USER_DISABLED), + DLUA_TABLE_ENUM(PASSDB_RESULT_PASS_EXPIRED), + DLUA_TABLE_ENUM(PASSDB_RESULT_NEXT), + DLUA_TABLE_ENUM(PASSDB_RESULT_PASSWORD_MISMATCH), + DLUA_TABLE_ENUM(PASSDB_RESULT_OK), + + DLUA_TABLE_ENUM(USERDB_RESULT_INTERNAL_FAILURE), + DLUA_TABLE_ENUM(USERDB_RESULT_USER_UNKNOWN), + DLUA_TABLE_ENUM(USERDB_RESULT_OK), + + DLUA_TABLE_END +}; +static luaL_Reg auth_lua_dovecot_auth_methods[] = { + { NULL, NULL } +}; + +static void auth_lua_dovecot_auth_register(lua_State *L) +{ + dlua_get_dovecot(L); + /* Create new table for holding values */ + lua_newtable(L); + + /* register constants */ + dlua_set_members(L, auth_lua_dovecot_auth_values, -1); + + /* push new metatable to stack */ + luaL_newmetatable(L, AUTH_LUA_DOVECOT_AUTH); + /* this will register functions to the metatable itself */ + luaL_setfuncs(L, auth_lua_dovecot_auth_methods, 0); + /* point __index to self */ + lua_pushvalue(L, -1); + lua_setfield(L, -1, "__index"); + /* set table's metatable, pops stack */ + lua_setmetatable(L, -2); + + /* put this as "dovecot.auth" */ + lua_setfield(L, -2, "auth"); + + /* pop dovecot */ + lua_pop(L, 1); +} + +int auth_lua_script_init(struct dlua_script *script, const char **error_r) +{ + dlua_dovecot_register(script); + auth_lua_dovecot_auth_register(script->L); + auth_lua_auth_request_register(script->L); + return dlua_script_init(script, error_r); +} + +static int auth_lua_call_lookup(lua_State *L, const char *fn, + struct auth_request *req, const char **error_r) +{ + int err = 0; + + e_debug(authdb_event(req), "Calling %s", fn); + + /* call with auth request as parameter */ + auth_lua_push_auth_request(L, req); + if (dlua_pcall(L, fn, 1, 2, error_r) < 0) + return -1; + + if (!lua_isnumber(L, -2)) { + *error_r = t_strdup_printf("db-lua: %s(req) invalid return value " + "(expected number got %s)", + fn, luaL_typename(L, -2)); + err = -1; + } else if (!lua_isstring(L, -1) && !lua_istable(L, -1)) { + *error_r = t_strdup_printf("db-lua: %s(req) invalid return value " + "(expected string or table, got %s)", + fn, luaL_typename(L, -1)); + err = -1; + } + + if (err != 0) { + lua_pop(L, 2); + lua_gc(L, LUA_GCCOLLECT, 0); + i_assert(lua_gettop(L) == 0); + return PASSDB_RESULT_INTERNAL_FAILURE; + } + + return 0; +} + +static void +auth_lua_export_fields(struct auth_request *req, + const char *str, + const char **scheme_r, const char **password_r) +{ + const char *const *fields = t_strsplit_spaces(str, " "); + while(*fields != NULL) { + const char *value = strchr(*fields, '='); + const char *key; + + if (value == NULL) { + key = *fields; + value = ""; + } else { + key = t_strdup_until(*fields, value++); + } + + if (password_r != NULL && strcmp(key, "password") == 0) { + *scheme_r = password_get_scheme(&value); + *password_r = value; + } else if (req->userdb_lookup) { + auth_request_set_userdb_field(req, key, value); + } else { + auth_request_set_field(req, key, value, STATIC_PASS_SCHEME); + } + fields++; + } +} + +static void auth_lua_export_table(lua_State *L, struct auth_request *req, + const char **scheme_r, const char **password_r) +{ + lua_pushvalue(L, -1); + lua_pushnil(L); + while (lua_next(L, -2) != 0) { + const char *key = t_strdup(lua_tostring(L, -2)); + const char *value; + int type = lua_type(L, -1); + switch(type) { + case LUA_TNUMBER: + value = dec2str(lua_tointeger(L, -1)); + break; + case LUA_TBOOLEAN: + value = lua_toboolean(L, -1) ? "yes" : "no"; + break; + case LUA_TSTRING: + value = t_strdup(lua_tostring(L, -1)); + break; + case LUA_TNIL: + value = ""; + break; + default: + e_warning(authdb_event(req), + "db-lua: '%s' has invalid value type %s - ignoring", + key, lua_typename(L, -1)); + value = ""; + } + + if (password_r != NULL && strcmp(key, "password") == 0) { + *scheme_r = password_get_scheme(&value); + *password_r = value; + } else if (req->userdb_lookup) { + auth_request_set_userdb_field(req, key, value); + } else { + auth_request_set_field(req, key, value, STATIC_PASS_SCHEME); + } + lua_pop(L, 1); + } + + /* stack has + key + table + passdb_result + */ + lua_pop(L, 3); + lua_gc(L, LUA_GCCOLLECT, 0); + i_assert(lua_gettop(L) == 0); +} + +static enum userdb_result +auth_lua_export_userdb_table(lua_State *L, struct auth_request *req, + const char **error_r) +{ + enum userdb_result ret = lua_tointeger(L, -2); + + if (ret != USERDB_RESULT_OK) { + lua_pop(L, 2); + lua_gc(L, LUA_GCCOLLECT, 0); + *error_r = "userdb failed"; + return ret; + } + + auth_lua_export_table(L, req, NULL, NULL); + return USERDB_RESULT_OK; +} + +static enum passdb_result +auth_lua_export_passdb_table(lua_State *L, struct auth_request *req, + const char **scheme_r, const char **password_r, + const char **error_r) +{ + enum passdb_result ret = lua_tointeger(L, -2); + + if (ret != PASSDB_RESULT_OK) { + lua_pop(L, 2); + lua_gc(L, LUA_GCCOLLECT, 0); + *error_r = "passb failed"; + return ret; + } + + auth_lua_export_table(L, req, scheme_r, password_r); + return PASSDB_RESULT_OK; +} + +static enum passdb_result +auth_lua_call_lookup_finish(lua_State *L, struct auth_request *req, + const char **scheme_r, const char **password_r, + const char **error_r) +{ + if (lua_istable(L, -1)) { + return auth_lua_export_passdb_table(L, req, scheme_r, + password_r, error_r); + } + + enum passdb_result ret = lua_tointeger(L, -2); + const char *str = t_strdup(lua_tostring(L, -1)); + lua_pop(L, 2); + lua_gc(L, LUA_GCCOLLECT, 0); + /* stack should be empty now */ + i_assert(lua_gettop(L) == 0); + + if (ret != PASSDB_RESULT_OK && ret != PASSDB_RESULT_NEXT) { + *error_r = str; + } else { + auth_lua_export_fields(req, str, scheme_r, password_r); + } + + if (scheme_r != NULL && *scheme_r == NULL) + *scheme_r = "PLAIN"; + + return ret; +} + +enum passdb_result +auth_lua_call_password_verify(struct dlua_script *script, + struct auth_request *req, const char *password, const char **error_r) +{ + lua_State *L = script->L; + int err = 0; + + e_debug(authdb_event(req), "Calling %s", AUTH_LUA_PASSWORD_VERIFY); + + /* call with auth request, password as parameters */ + auth_lua_push_auth_request(L, req); + lua_pushstring(L, password); + + if (dlua_pcall(L, AUTH_LUA_PASSWORD_VERIFY, 2, 2, error_r) < 0) + return PASSDB_RESULT_INTERNAL_FAILURE; + + if (!lua_isnumber(L, -2)) { + *error_r = t_strdup_printf("db-lua: %s invalid return value " + "(expected number got %s)", + AUTH_LUA_PASSWORD_VERIFY, + luaL_typename(L, -2)); + err = -1; + } else if (!lua_isstring(L, -1) && !lua_istable(L, -1)) { + *error_r = t_strdup_printf("db-lua: %s invalid return value " + "(expected string or table, got %s)", + AUTH_LUA_PASSWORD_VERIFY, + luaL_typename(L, -1)); + err = -1; + } + + if (err != 0) { + lua_pop(L, 2); + lua_gc(L, LUA_GCCOLLECT, 0); + i_assert(lua_gettop(L) == 0); + return PASSDB_RESULT_INTERNAL_FAILURE; + } + + + return auth_lua_call_lookup_finish(L, req, NULL, NULL, error_r); +} + + +enum passdb_result +auth_lua_call_passdb_lookup(struct dlua_script *script, + struct auth_request *req, const char **scheme_r, + const char **password_r, const char **error_r) +{ + lua_State *L = script->L; + + *scheme_r = *password_r = NULL; + if (auth_lua_call_lookup(L, AUTH_LUA_PASSDB_LOOKUP, req, error_r) < 0) { + lua_gc(L, LUA_GCCOLLECT, 0); + i_assert(lua_gettop(L) == 0); + return PASSDB_RESULT_INTERNAL_FAILURE; + } + + return auth_lua_call_lookup_finish(L, req, scheme_r, password_r, error_r); +} + + +enum userdb_result +auth_lua_call_userdb_lookup(struct dlua_script *script, + struct auth_request *req, const char **error_r) +{ + lua_State *L = script->L; + + if (auth_lua_call_lookup(L, AUTH_LUA_USERDB_LOOKUP, req, error_r) < 0) { + lua_gc(L, LUA_GCCOLLECT, 0); + i_assert(lua_gettop(L) == 0); + return USERDB_RESULT_INTERNAL_FAILURE; + } + + if (lua_istable(L, -1)) + return auth_lua_export_userdb_table(L, req, error_r); + + enum userdb_result ret = lua_tointeger(L, -2); + const char *str = t_strdup(lua_tostring(L, -1)); + lua_pop(L, 2); + lua_gc(L, LUA_GCCOLLECT, 0); + i_assert(lua_gettop(L) == 0); + + if (ret != USERDB_RESULT_OK) { + *error_r = str; + return ret; + } + auth_lua_export_fields(req, str, NULL, NULL); + + return USERDB_RESULT_OK; +} + +struct userdb_iterate_context * +auth_lua_call_userdb_iterate_init(struct dlua_script *script, struct auth_request *req, + userdb_iter_callback_t *callback, void *context) +{ + lua_State *L = script->L; + + pool_t pool = pool_alloconly_create(MEMPOOL_GROWING"lua userdb iterate", 128); + struct auth_lua_userdb_iterate_context *actx = + p_new(pool, struct auth_lua_userdb_iterate_context, 1); + + actx->pool = pool; + actx->ctx.auth_request = req; + actx->ctx.callback = callback; + actx->ctx.context = context; + + if (!dlua_script_has_function(script, AUTH_LUA_USERDB_ITERATE)) { + actx->ctx.failed = TRUE; + return &actx->ctx; + } + + e_debug(authdb_event(req), "Calling %s", AUTH_LUA_USERDB_ITERATE); + + const char *error; + if (dlua_pcall(L, AUTH_LUA_USERDB_ITERATE, 0, 1, &error) < 0) { + e_error(authdb_event(req), + "db-lua: " AUTH_LUA_USERDB_ITERATE " failed: %s", + error); + actx->ctx.failed = TRUE; + return &actx->ctx; + } + + if (!lua_istable(L, -1)) { + e_error(authdb_event(req), + "db-lua: Cannot iterate, return value is not table"); + actx->ctx.failed = TRUE; + lua_pop(L, 1); + lua_gc(L, LUA_GCCOLLECT, 0); + i_assert(lua_gettop(L) == 0); + return &actx->ctx; + } + + p_array_init(&actx->users, pool, 8); + + /* stack is now + table */ + + /* see lua_next documentation */ + lua_pushnil(L); + while (lua_next(L, -2) != 0) { + /* stack is now + value + key + table */ + if (!lua_isstring(L, -1)) { + e_error(authdb_event(req), + "db-lua: Value is not string"); + actx->ctx.failed = TRUE; + lua_pop(L, 3); + lua_gc(L, LUA_GCCOLLECT, 0); + i_assert(lua_gettop(L) == 0); + return &actx->ctx; + } + const char *str = p_strdup(pool, lua_tostring(L, -1)); + array_push_back(&actx->users, &str); + lua_pop(L, 1); + /* stack is now + key + table */ + } + + /* stack is now + table + */ + + lua_pop(L, 1); + lua_gc(L, LUA_GCCOLLECT, 0); + i_assert(lua_gettop(L) == 0); + + return &actx->ctx; +} + +void auth_lua_userdb_iterate_next(struct userdb_iterate_context *ctx) +{ + struct auth_lua_userdb_iterate_context *actx = + container_of(ctx, struct auth_lua_userdb_iterate_context, ctx); + + if (ctx->failed || actx->idx >= array_count(&actx->users)) { + ctx->callback(NULL, ctx->context); + return; + } + + const char *user = array_idx_elem(&actx->users, actx->idx++); + ctx->callback(user, ctx->context); +} + +int auth_lua_userdb_iterate_deinit(struct userdb_iterate_context *ctx) +{ + struct auth_lua_userdb_iterate_context *actx = + container_of(ctx, struct auth_lua_userdb_iterate_context, ctx); + + int ret = ctx->failed ? -1 : 0; + pool_unref(&actx->pool); + return ret; +} + +#ifndef BUILTIN_LUA +/* Building a plugin */ +extern struct passdb_module_interface passdb_lua_plugin; +extern struct userdb_module_interface userdb_lua_plugin; + +void authdb_lua_init(void); +void authdb_lua_deinit(void); + +void authdb_lua_init(void) +{ + passdb_register_module(&passdb_lua_plugin); + userdb_register_module(&userdb_lua_plugin); + +} +void authdb_lua_deinit(void) +{ + passdb_unregister_module(&passdb_lua_plugin); + userdb_unregister_module(&userdb_lua_plugin); +} +#endif + +#endif diff --git a/src/auth/db-lua.h b/src/auth/db-lua.h new file mode 100644 index 0000000..ebb697a --- /dev/null +++ b/src/auth/db-lua.h @@ -0,0 +1,33 @@ +#ifndef DB_LUA_H +#define DB_LUA_H 1 + +#include "dlua-script.h" + +#define DB_LUA_CACHE_KEY "%u" + +#define AUTH_LUA_PASSWORD_VERIFY "auth_password_verify" + +struct dlua_script; + +int auth_lua_script_init(struct dlua_script *script, const char **error_r); + +int auth_lua_call_password_verify(struct dlua_script *script, + struct auth_request *req, const char *password, + const char **error_r); + +enum passdb_result +auth_lua_call_passdb_lookup(struct dlua_script *script, + struct auth_request *req, const char **scheme_r, + const char **password_r, const char **error_r); + +enum userdb_result +auth_lua_call_userdb_lookup(struct dlua_script *script, + struct auth_request *req, const char **error_r); + +struct userdb_iterate_context * +auth_lua_call_userdb_iterate_init(struct dlua_script *script, struct auth_request *req, + userdb_iter_callback_t *callback, void *context); +void auth_lua_userdb_iterate_next(struct userdb_iterate_context *ctx); +int auth_lua_userdb_iterate_deinit(struct userdb_iterate_context *ctx); + +#endif diff --git a/src/auth/db-oauth2.c b/src/auth/db-oauth2.c new file mode 100644 index 0000000..3c6ef3a --- /dev/null +++ b/src/auth/db-oauth2.c @@ -0,0 +1,885 @@ +/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */ + +#include "auth-common.h" +#include "array.h" +#include "str.h" +#include "var-expand.h" +#include "env-util.h" +#include "var-expand.h" +#include "settings.h" +#include "oauth2.h" +#include "http-client.h" +#include "http-url.h" +#include "iostream-ssl.h" +#include "auth-request.h" +#include "auth-settings.h" +#include "passdb.h" +#include "passdb-template.h" +#include "llist.h" +#include "db-oauth2.h" +#include "dcrypt.h" +#include "dict.h" + +#include <stddef.h> + +struct passdb_oauth2_settings { + /* tokeninfo endpoint, format https://endpoint/somewhere?token= */ + const char *tokeninfo_url; + /* password grant endpoint, format https://endpoint/somewhere */ + const char *grant_url; + /* introspection endpoint, format https://endpoint/somewhere */ + const char *introspection_url; + /* expected scope, optional */ + const char *scope; + /* mode of introspection, one of get, get-auth, post + - get: append token to url + - get-auth: send token with header Authorization: Bearer token + - post: send token=<token> as POST request + */ + const char *introspection_mode; + /* normalization var-expand template for username, defaults to %Lu */ + const char *username_format; + /* name of username attribute to lookup, mandatory */ + const char *username_attribute; + /* name of account is active attribute, optional */ + const char *active_attribute; + /* expected active value for active attribute, optional */ + const char *active_value; + /* client identificator for oauth2 server */ + const char *client_id; + /* not really used, but have to present by oauth2 specs */ + const char *client_secret; + /* template to expand into passdb */ + const char *pass_attrs; + /* template to expand into key path, turns on local validation support */ + const char *local_validation_key_dict; + /* valid token issuers */ + const char *issuers; + /* The URL for a document following the OpenID Provider Configuration + Information schema, see + + https://datatracker.ietf.org/doc/html/rfc7628#section-3.2.2 + */ + const char *openid_configuration_url; + + /* TLS options */ + const char *tls_ca_cert_file; + const char *tls_ca_cert_dir; + const char *tls_cert_file; + const char *tls_key_file; + const char *tls_cipher_suite; + + /* HTTP rawlog directory */ + const char *rawlog_dir; + + /* HTTP client options */ + unsigned int timeout_msecs; + unsigned int max_idle_time_msecs; + unsigned int max_parallel_connections; + unsigned int max_pipelined_requests; + bool tls_allow_invalid_cert; + + bool debug; + /* Should introspection be done even if not necessary */ + bool force_introspection; + /* Should we send service and local/remote endpoints as X-Dovecot-Auth headers */ + bool send_auth_headers; + bool use_grant_password; +}; + +struct db_oauth2 { + struct db_oauth2 *prev,*next; + + pool_t pool; + + const char *config_path; + struct passdb_oauth2_settings set; + struct http_client *client; + struct passdb_template *tmpl; + struct oauth2_settings oauth2_set; + + struct db_oauth2_request *head; + + unsigned int refcount; +}; + +static struct db_oauth2 *db_oauth2_head = NULL; + +#undef DEF_STR +#undef DEF_BOOL +#undef DEF_INT + +#define DEF_STR(name) DEF_STRUCT_STR(name, passdb_oauth2_settings) +#define DEF_BOOL(name) DEF_STRUCT_BOOL(name, passdb_oauth2_settings) +#define DEF_INT(name) DEF_STRUCT_INT(name, passdb_oauth2_settings) + +static struct setting_def setting_defs[] = { + DEF_STR(tokeninfo_url), + DEF_STR(grant_url), + DEF_STR(introspection_url), + DEF_STR(scope), + DEF_BOOL(force_introspection), + DEF_STR(introspection_mode), + DEF_STR(username_format), + DEF_STR(username_attribute), + DEF_STR(pass_attrs), + DEF_STR(local_validation_key_dict), + DEF_STR(active_attribute), + DEF_STR(active_value), + DEF_STR(client_id), + DEF_STR(client_secret), + DEF_STR(issuers), + DEF_STR(openid_configuration_url), + DEF_INT(timeout_msecs), + DEF_INT(max_idle_time_msecs), + DEF_INT(max_parallel_connections), + DEF_INT(max_pipelined_requests), + DEF_BOOL(send_auth_headers), + DEF_BOOL(use_grant_password), + + DEF_STR(tls_ca_cert_file), + DEF_STR(tls_ca_cert_dir), + DEF_STR(tls_cert_file), + DEF_STR(tls_key_file), + DEF_STR(tls_cipher_suite), + DEF_BOOL(tls_allow_invalid_cert), + + DEF_STR(rawlog_dir), + + DEF_BOOL(debug), + + { 0, NULL, 0 } +}; + +static struct passdb_oauth2_settings default_oauth2_settings = { + .tokeninfo_url = "", + .grant_url = "", + .introspection_url = "", + .scope = "", + .force_introspection = FALSE, + .introspection_mode = "", + .username_format = "%Lu", + .username_attribute = "email", + .active_attribute = "active", + .active_value = "true", + .client_id = "", + .client_secret = "", + .issuers = "", + .openid_configuration_url = "", + .pass_attrs = "", + .local_validation_key_dict = "", + .rawlog_dir = "", + .timeout_msecs = 0, + .max_idle_time_msecs = 60000, + .max_parallel_connections = 10, + .max_pipelined_requests = 1, + .tls_ca_cert_file = NULL, + .tls_ca_cert_dir = NULL, + .tls_cert_file = NULL, + .tls_key_file = NULL, + .tls_cipher_suite = "HIGH:!SSLv2", + .tls_allow_invalid_cert = FALSE, + .send_auth_headers = FALSE, + .use_grant_password = FALSE, + .debug = FALSE, +}; + +static const char *parse_setting(const char *key, const char *value, + struct db_oauth2 *db) +{ + return parse_setting_from_defs(db->pool, setting_defs, + &db->set, key, value); +} + +struct db_oauth2 *db_oauth2_init(const char *config_path) +{ + struct db_oauth2 *db; + const char *error; + struct ssl_iostream_settings ssl_set; + struct http_client_settings http_set; + + for(db = db_oauth2_head; db != NULL; db = db->next) { + if (strcmp(db->config_path, config_path) == 0) { + db->refcount++; + return db; + } + } + + pool_t pool = pool_alloconly_create("db_oauth2", 128); + db = p_new(pool, struct db_oauth2, 1); + db->pool = pool; + db->refcount = 1; + db->config_path = p_strdup(pool, config_path); + db->set = default_oauth2_settings; + + if (!settings_read_nosection(config_path, parse_setting, db, &error)) + i_fatal("oauth2 %s: %s", config_path, error); + + db->tmpl = passdb_template_build(pool, db->set.pass_attrs); + + i_zero(&ssl_set); + i_zero(&http_set); + + ssl_set.cipher_list = db->set.tls_cipher_suite; + ssl_set.ca_file = db->set.tls_ca_cert_file; + ssl_set.ca_dir = db->set.tls_ca_cert_dir; + if (db->set.tls_cert_file != NULL && *db->set.tls_cert_file != '\0') { + ssl_set.cert.cert = db->set.tls_cert_file; + ssl_set.cert.key = db->set.tls_key_file; + } + ssl_set.prefer_server_ciphers = TRUE; + ssl_set.allow_invalid_cert = db->set.tls_allow_invalid_cert; + ssl_set.verbose = db->set.debug; + ssl_set.verbose_invalid_cert = db->set.debug; + http_set.ssl = &ssl_set; + + http_set.dns_client_socket_path = "dns-client"; + http_set.user_agent = "dovecot-oauth2-passdb/" DOVECOT_VERSION; + + if (*db->set.local_validation_key_dict == '\0' && + *db->set.tokeninfo_url == '\0' && + (*db->set.grant_url == '\0' || *db->set.client_id == '\0') && + *db->set.introspection_url == '\0') + i_fatal("oauth2: Password grant, tokeninfo, introspection URL or " + "validation key dictionary must be given"); + + if (*db->set.rawlog_dir != '\0') + http_set.rawlog_dir = db->set.rawlog_dir; + + http_set.max_idle_time_msecs = db->set.max_idle_time_msecs; + http_set.max_parallel_connections = db->set.max_parallel_connections; + http_set.max_pipelined_requests = db->set.max_pipelined_requests; + http_set.no_auto_redirect = FALSE; + http_set.no_auto_retry = TRUE; + http_set.debug = db->set.debug; + http_set.event_parent = auth_event; + + db->client = http_client_init(&http_set); + + i_zero(&db->oauth2_set); + db->oauth2_set.client = db->client; + db->oauth2_set.tokeninfo_url = db->set.tokeninfo_url, + db->oauth2_set.grant_url = db->set.grant_url, + db->oauth2_set.introspection_url = db->set.introspection_url; + db->oauth2_set.client_id = db->set.client_id; + db->oauth2_set.client_secret = db->set.client_secret; + db->oauth2_set.timeout_msecs = db->set.timeout_msecs; + db->oauth2_set.send_auth_headers = db->set.send_auth_headers; + db->oauth2_set.use_grant_password = db->set.use_grant_password; + db->oauth2_set.scope = db->set.scope; + + if (*db->set.active_attribute != '\0' && + *db->set.active_value == '\0') + i_fatal("oauth2: Cannot have empty active_value if active_attribute is set"); + if (*db->set.active_attribute == '\0' && + *db->set.active_value != '\0') + i_fatal("oauth2: Cannot have empty active_attribute is active_value is set"); + + if (*db->set.introspection_mode == '\0' || + strcmp(db->set.introspection_mode, "auth") == 0) { + db->oauth2_set.introspection_mode = INTROSPECTION_MODE_GET_AUTH; + } else if (strcmp(db->set.introspection_mode, "get") == 0) { + db->oauth2_set.introspection_mode = INTROSPECTION_MODE_GET; + } else if (strcmp(db->set.introspection_mode, "post") == 0) { + db->oauth2_set.introspection_mode = INTROSPECTION_MODE_POST; + } else if (strcmp(db->set.introspection_mode, "local") == 0) { + if (*db->set.local_validation_key_dict == '\0') + i_fatal("oauth2: local_validation_key_dict is required " + "for local introspection."); + db->oauth2_set.introspection_mode = INTROSPECTION_MODE_LOCAL; + } else { + i_fatal("oauth2: Invalid value '%s' for introspection mode, must be on auth, get, post or local", + db->set.introspection_mode); + } + + if (db->oauth2_set.introspection_mode == INTROSPECTION_MODE_LOCAL) { + struct dict_settings dict_set = { + .base_dir = global_auth_settings->base_dir, + .event_parent = auth_event, + }; + if (dict_init(db->set.local_validation_key_dict, &dict_set, + &db->oauth2_set.key_dict, &error) < 0) + i_fatal("Cannot initialize key dict: %s", error); + /* failure to initialize dcrypt is not fatal - we can still + validate HMAC based keys */ + (void)dcrypt_initialize(NULL, NULL, NULL); + /* initialize key cache */ + db->oauth2_set.key_cache = oauth2_validation_key_cache_init(); + } + + if (*db->set.issuers != '\0') + db->oauth2_set.issuers = (const char *const *) + p_strsplit_spaces(pool, db->set.issuers, " "); + + if (*db->set.openid_configuration_url != '\0') { + struct http_url *parsed_url ATTR_UNUSED; + if (http_url_parse(db->set.openid_configuration_url, NULL, 0, + pool_datastack_create(), &parsed_url, + &error) < 0) { + i_fatal("Invalid openid_configuration_url: %s", + error); + } + } + + DLLIST_PREPEND(&db_oauth2_head, db); + + return db; +} + +void db_oauth2_ref(struct db_oauth2 *db) +{ + i_assert(db->refcount > 0); + db->refcount++; +} + +void db_oauth2_unref(struct db_oauth2 **_db) +{ + struct db_oauth2 *ptr, *db = *_db; + i_assert(db->refcount > 0); + + if (--db->refcount > 0) return; + + for(ptr = db_oauth2_head; ptr != NULL; ptr = ptr->next) { + if (ptr == db) { + DLLIST_REMOVE(&db_oauth2_head, ptr); + break; + } + } + + i_assert(ptr != NULL && ptr == db); + + /* make sure all requests are aborted */ + while (db->head != NULL) + oauth2_request_abort(&db->head->req); + + http_client_deinit(&db->client); + if (db->oauth2_set.key_dict != NULL) + dict_deinit(&db->oauth2_set.key_dict); + oauth2_validation_key_cache_deinit(&db->oauth2_set.key_cache); + pool_unref(&db->pool); +} + +static void +db_oauth2_add_openid_config_url(struct db_oauth2_request *req) +{ + /* FIXME: HORRIBLE HACK - REMOVE ME!!! + It is because the mech has not been implemented properly + that we need to pass the config url in this strange way. + + This **must** be moved to mech-oauth2 once the validation + result et al is handled there. + */ + req->auth_request->openid_config_url = + p_strdup_empty(req->auth_request->pool, + req->db->set.openid_configuration_url); +} + +static bool +db_oauth2_have_all_fields(struct db_oauth2_request *req) +{ + unsigned int n,i; + unsigned int size,idx; + const char *const *args = passdb_template_get_args(req->db->tmpl, &n); + + if (req->fields == NULL) + return FALSE; + + for(i=1;i<n;i+=2) { + const char *ptr = args[i]; + while(ptr != NULL) { + ptr = strchr(ptr, '%'); + if (ptr != NULL) { + const char *field; + ptr++; + var_get_key_range(ptr, &idx, &size); + ptr = ptr+idx; + field = t_strndup(ptr,size); + if (str_begins(field, "oauth2:") && + !auth_fields_exists(req->fields, ptr+7)) + return FALSE; + ptr = ptr+size; + } + } + } + + if (!auth_fields_exists(req->fields, req->db->set.username_attribute)) + return FALSE; + if (*req->db->set.active_attribute != '\0' && !auth_fields_exists(req->fields, req->db->set.active_attribute)) + return FALSE; + + return TRUE; +} + +static const char *field_get_default(const char *data) +{ + const char *p; + + p = strchr(data, ':'); + if (p == NULL) + return ""; + else { + /* default value given */ + return p+1; + } +} + +static int db_oauth2_var_expand_func_oauth2(const char *data, void *context, + const char **value_r, + const char **error_r ATTR_UNUSED) +{ + struct db_oauth2_request *ctx = context; + const char *field_name = t_strcut(data, ':'); + const char *value = NULL; + + if (ctx->fields != NULL) + value = auth_fields_find(ctx->fields, field_name); + *value_r = value != NULL ? value : field_get_default(data); + + return 1; +} + +static const char *escape_none(const char *value, const struct auth_request *req ATTR_UNUSED) +{ + return value; +} + +static const struct var_expand_table * +db_oauth2_value_get_var_expand_table(struct auth_request *auth_request, + const char *oauth2_value) +{ + struct var_expand_table *table; + unsigned int count = 1; + + table = auth_request_get_var_expand_table_full(auth_request, + auth_request->fields.user, NULL, &count); + table[0].key = '$'; + table[0].value = oauth2_value; + return table; +} + +static bool +db_oauth2_template_export(struct db_oauth2_request *req, + enum passdb_result *result_r, const char **error_r) +{ + /* var=$ expands into var=${oauth2:var} */ + const struct var_expand_func_table funcs_table[] = { + { "oauth2", db_oauth2_var_expand_func_oauth2 }, + { NULL, NULL } + }; + string_t *dest; + const char *const *args, *value, *error; + struct passdb_template *tmpl = req->db->tmpl; + unsigned int i, count; + + if (passdb_template_is_empty(tmpl)) + return TRUE; + + dest = t_str_new(256); + args = passdb_template_get_args(tmpl, &count); + i_assert((count % 2) == 0); + for (i = 0; i < count; i += 2) { + if (args[i+1] == NULL) + value = ""; + else { + str_truncate(dest, 0); + const struct var_expand_table * + table = db_oauth2_value_get_var_expand_table(req->auth_request, + auth_fields_find(req->fields, args[i])); + if (var_expand_with_funcs(dest, args[i+1], table, funcs_table, + req, &error) < 0) { + *error_r = t_strdup_printf( + "var_expand(%s) failed: %s", + args[i+1], error); + *result_r = PASSDB_RESULT_INTERNAL_FAILURE; + return FALSE; + } + value = str_c(dest); + } + + auth_request_set_field(req->auth_request, args[i], value, + STATIC_PASS_SCHEME); + } + return TRUE; +} + +static void db_oauth2_fields_merge(struct db_oauth2_request *req, + ARRAY_TYPE(oauth2_field) *fields) +{ + const struct oauth2_field *field; + + if (req->fields == NULL) + req->fields = auth_fields_init(req->pool); + + array_foreach(fields, field) { + e_debug(authdb_event(req->auth_request), + "Processing field %s", + field->name); + auth_fields_add(req->fields, field->name, field->value, 0); + } +} + +static const char * +db_oauth2_field_find(const ARRAY_TYPE(oauth2_field) *fields, const char *name) +{ + const struct oauth2_field *f; + + array_foreach(fields, f) { + if (strcmp(f->name, name) == 0) + return f->value; + } + return NULL; +} + +static void db_oauth2_callback(struct db_oauth2_request *req, + enum passdb_result result, + const char *error_prefix, const char *error) +{ + db_oauth2_lookup_callback_t *callback = req->callback; + req->callback = NULL; + + i_assert(result == PASSDB_RESULT_OK || error != NULL); + + if (result != PASSDB_RESULT_OK) + db_oauth2_add_openid_config_url(req); + + /* Successful lookups were logged by the caller. Failed lookups will be + logged either with e_error() or e_info() by the callback. */ + if (callback != NULL) { + DLLIST_REMOVE(&req->db->head, req); + if (result != PASSDB_RESULT_OK) + error = t_strconcat(error_prefix, error, NULL); + callback(req, result, error, req->context); + } +} + +static bool +db_oauth2_validate_username(struct db_oauth2_request *req, + enum passdb_result *result_r, const char **error_r) +{ + const char *error; + struct var_expand_table table[] = { + { 'u', NULL, "user" }, + { 'n', NULL, "username" }, + { 'd', NULL, "domain" }, + { '\0', NULL, NULL } + }; + const char *username_value = + auth_fields_find(req->fields, req->db->set.username_attribute); + + if (username_value == NULL) { + *result_r = PASSDB_RESULT_INTERNAL_FAILURE; + *error_r = "No username returned"; + return FALSE; + } + + table[0].value = username_value; + table[1].value = t_strcut(username_value, '@'); + table[2].value = i_strchr_to_next(username_value, '@'); + + string_t *username_req = t_str_new(32); + string_t *username_val = t_str_new(strlen(username_value)); + + if (auth_request_var_expand(username_req, req->db->set.username_format, req->auth_request, escape_none, &error) < 0 || + var_expand(username_val, req->db->set.username_format, table, &error) < 0) { + *error_r = t_strdup_printf("var_expand(%s) failed: %s", + req->db->set.username_format, error); + *result_r = PASSDB_RESULT_INTERNAL_FAILURE; + return FALSE; + } else if (!str_equals(username_req, username_val)) { + *error_r = t_strdup_printf("Username '%s' did not match '%s'", + str_c(username_req), str_c(username_val)); + *result_r = PASSDB_RESULT_USER_UNKNOWN; + return FALSE; + } else { + return TRUE; + } +} + +static bool +db_oauth2_user_is_enabled(struct db_oauth2_request *req, + enum passdb_result *result_r, const char **error_r) +{ + if (*req->db->set.active_attribute != '\0' && + *req->db->set.active_value != '\0') { + const char *active_value = + auth_fields_find(req->fields, req->db->set.active_attribute); + if (active_value != NULL && + strcmp(req->db->set.active_value, active_value) != 0) { + *error_r = "Provided token is not valid"; + *result_r = PASSDB_RESULT_PASSWORD_MISMATCH; + return FALSE; + } + } + return TRUE; +} + +static bool +db_oauth2_token_in_scope(struct db_oauth2_request *req, + enum passdb_result *result_r, const char **error_r) +{ + if (*req->db->set.scope != '\0') { + bool found = FALSE; + const char *value = auth_fields_find(req->fields, "scope"); + if (value == NULL) + value = auth_fields_find(req->fields, "aud"); + e_debug(authdb_event(req->auth_request), + "Token scope(s): %s", + value); + if (value != NULL) { + const char **wanted_scopes = + t_strsplit_spaces(req->db->set.scope, " "); + const char **scopes = t_strsplit_spaces(value, " "); + for (; !found && *wanted_scopes != NULL; wanted_scopes++) + found = str_array_find(scopes, *wanted_scopes); + } + if (!found) { + *error_r = t_strdup_printf("Token is not valid for scope '%s'", + req->db->set.scope); + *result_r = PASSDB_RESULT_USER_DISABLED; + return FALSE; + } + } + return TRUE; +} + +static void db_oauth2_process_fields(struct db_oauth2_request *req, + enum passdb_result *result_r, + const char **error_r) +{ + *error_r = NULL; + + if (db_oauth2_user_is_enabled(req, result_r, error_r) && + db_oauth2_validate_username(req, result_r, error_r) && + db_oauth2_token_in_scope(req, result_r, error_r) && + db_oauth2_template_export(req, result_r, error_r)) { + *result_r = PASSDB_RESULT_OK; + } else { + i_assert(*result_r != PASSDB_RESULT_OK && *error_r != NULL); + } +} + +static void +db_oauth2_introspect_continue(struct oauth2_request_result *result, + struct db_oauth2_request *req) +{ + enum passdb_result passdb_result; + const char *error; + + req->req = NULL; + + if (result->error != NULL) { + /* fail here */ + passdb_result = PASSDB_RESULT_INTERNAL_FAILURE; + error = result->error; + } else { + e_debug(authdb_event(req->auth_request), + "Introspection succeeded"); + db_oauth2_fields_merge(req, result->fields); + db_oauth2_process_fields(req, &passdb_result, &error); + } + db_oauth2_callback(req, passdb_result, "Introspection failed: ", error); +} + +static void db_oauth2_lookup_introspect(struct db_oauth2_request *req) +{ + struct oauth2_request_input input; + i_zero(&input); + + e_debug(authdb_event(req->auth_request), + "Making introspection request to %s", + req->db->set.introspection_url); + input.token = req->token; + input.local_ip = req->auth_request->fields.local_ip; + input.local_port = req->auth_request->fields.local_port; + input.remote_ip = req->auth_request->fields.remote_ip; + input.remote_port = req->auth_request->fields.remote_port; + input.real_local_ip = req->auth_request->fields.real_local_ip; + input.real_local_port = req->auth_request->fields.real_local_port; + input.real_remote_ip = req->auth_request->fields.real_remote_ip; + input.real_remote_port = req->auth_request->fields.real_remote_port; + input.service = req->auth_request->fields.service; + + req->req = oauth2_introspection_start(&req->db->oauth2_set, &input, + db_oauth2_introspect_continue, req); +} + +static void db_oauth2_local_validation(struct db_oauth2_request *req, + const char *token) +{ + bool is_jwt ATTR_UNUSED; + const char *error = NULL; + enum passdb_result passdb_result; + ARRAY_TYPE(oauth2_field) fields; + t_array_init(&fields, 8); + if (oauth2_try_parse_jwt(&req->db->oauth2_set, token, + &fields, &is_jwt, &error) < 0) { + passdb_result = PASSDB_RESULT_PASSWORD_MISMATCH; + } else { + db_oauth2_fields_merge(req, &fields); + db_oauth2_process_fields(req, &passdb_result, &error); + } + if (passdb_result == PASSDB_RESULT_OK) { + e_debug(authdb_event(req->auth_request), + "Local validation succeeded"); + } + db_oauth2_callback(req, passdb_result, + "Local validation failed: ", error); +} + +static void +db_oauth2_lookup_continue_valid(struct db_oauth2_request *req, + ARRAY_TYPE(oauth2_field) *fields, + const char *error_prefix) +{ + enum passdb_result passdb_result; + const char *error; + + db_oauth2_fields_merge(req, fields); + if (db_oauth2_have_all_fields(req) && + !req->db->set.force_introspection) { + /* pass */ + } else if (req->db->oauth2_set.introspection_mode == + INTROSPECTION_MODE_LOCAL) { + e_debug(authdb_event(req->auth_request), + "Attempting to locally validate token"); + db_oauth2_local_validation(req, req->token); + return; + } else if (!db_oauth2_user_is_enabled(req, &passdb_result, &error)) { + db_oauth2_callback(req, passdb_result, + "Token is not valid: ", error); + return; + } else if (*req->db->set.introspection_url != '\0') { + db_oauth2_lookup_introspect(req); + return; + } + db_oauth2_process_fields(req, &passdb_result, &error); + db_oauth2_callback(req, passdb_result, error_prefix, error); +} + +static void +db_oauth2_lookup_continue(struct oauth2_request_result *result, + struct db_oauth2_request *req) +{ + i_assert(req->token != NULL); + req->req = NULL; + + if (result->error != NULL) { + db_oauth2_callback(req, PASSDB_RESULT_INTERNAL_FAILURE, + "Token validation failed: ", result->error); + } else if (!result->valid) { + db_oauth2_callback(req, PASSDB_RESULT_PASSWORD_MISMATCH, + "Token validation failed: ", + "Invalid token"); + } else { + e_debug(authdb_event(req->auth_request), + "Token validation succeeded"); + db_oauth2_lookup_continue_valid(req, result->fields, + "Token validation failed: "); + } +} + +static void +db_oauth2_lookup_passwd_grant(struct oauth2_request_result *result, + struct db_oauth2_request *req) +{ + enum passdb_result passdb_result; + const char *token, *error; + + i_assert(req->token == NULL); + req->req = NULL; + + if (result->valid) { + e_debug(authdb_event(req->auth_request), + "Password grant succeeded"); + token = db_oauth2_field_find(result->fields, "access_token"); + if (token == NULL) { + db_oauth2_callback(req, PASSDB_RESULT_INTERNAL_FAILURE, + "Password grant failed: ", + "OAuth2 token missing from reply"); + } else { + req->token = p_strdup(req->pool, token); + db_oauth2_lookup_continue_valid(req, result->fields, + "Password grant failed: "); + } + return; + } + + passdb_result = PASSDB_RESULT_INTERNAL_FAILURE; + if (result->error != NULL) + error = result->error; + else { + passdb_result = PASSDB_RESULT_INTERNAL_FAILURE; + error = db_oauth2_field_find(result->fields, "error"); + if (error == NULL) + error = "OAuth2 server returned failure without error field"; + else if (strcmp("invalid_grant", error) == 0) + passdb_result = PASSDB_RESULT_PASSWORD_MISMATCH; + } + db_oauth2_callback(req, passdb_result, + "Password grant failed: ", error); +} + +#undef db_oauth2_lookup +void db_oauth2_lookup(struct db_oauth2 *db, struct db_oauth2_request *req, + const char *token, struct auth_request *request, + db_oauth2_lookup_callback_t *callback, void *context) +{ + struct oauth2_request_input input; + i_zero(&input); + + req->db = db; + req->token = p_strdup(req->pool, token); + req->callback = callback; + req->context = context; + req->auth_request = request; + + input.token = token; + input.local_ip = req->auth_request->fields.local_ip; + input.local_port = req->auth_request->fields.local_port; + input.remote_ip = req->auth_request->fields.remote_ip; + input.remote_port = req->auth_request->fields.remote_port; + input.real_local_ip = req->auth_request->fields.real_local_ip; + input.real_local_port = req->auth_request->fields.real_local_port; + input.real_remote_ip = req->auth_request->fields.real_remote_ip; + input.real_remote_port = req->auth_request->fields.real_remote_port; + input.service = req->auth_request->fields.service; + + if (db->oauth2_set.introspection_mode == INTROSPECTION_MODE_LOCAL && + !db_oauth2_uses_password_grant(db)) { + /* try to validate token locally */ + e_debug(authdb_event(req->auth_request), + "Attempting to locally validate token"); + db_oauth2_local_validation(req, request->mech_password); + return; + + } + if (db->oauth2_set.use_grant_password) { + e_debug(authdb_event(req->auth_request), + "Making grant url request to %s", + db->set.grant_url); + /* There is no valid token until grant looks it up. */ + req->token = NULL; + req->req = oauth2_passwd_grant_start(&db->oauth2_set, &input, + request->fields.user, request->mech_password, + db_oauth2_lookup_passwd_grant, req); + } else if (*db->oauth2_set.tokeninfo_url == '\0') { + e_debug(authdb_event(req->auth_request), + "Making introspection request to %s", + db->set.introspection_url); + req->req = oauth2_introspection_start(&req->db->oauth2_set, &input, + db_oauth2_introspect_continue, req); + } else { + e_debug(authdb_event(req->auth_request), + "Making token validation lookup to %s", + db->oauth2_set.tokeninfo_url); + req->req = oauth2_token_validation_start(&db->oauth2_set, &input, + db_oauth2_lookup_continue, req); + } + i_assert(req->req != NULL); + DLLIST_PREPEND(&db->head, req); +} + +bool db_oauth2_uses_password_grant(const struct db_oauth2 *db) +{ + return db->set.use_grant_password; +} diff --git a/src/auth/db-oauth2.h b/src/auth/db-oauth2.h new file mode 100644 index 0000000..cb653db --- /dev/null +++ b/src/auth/db-oauth2.h @@ -0,0 +1,46 @@ +#ifndef DB_OAUTH2_H +#define DB_OAUTH2_H 1 + +struct db_oauth2; +struct oauth2_request; +struct db_oauth2_request; + +typedef void db_oauth2_lookup_callback_t(struct db_oauth2_request *request, + enum passdb_result result, + const char *error, + void *context); +struct db_oauth2_request { + pool_t pool; + struct db_oauth2_request *prev,*next; + + struct db_oauth2 *db; + struct oauth2_request *req; + + /* username to match */ + const char *username; + /* token to use */ + const char *token; + + struct auth_request *auth_request; + struct auth_fields *fields; + + db_oauth2_lookup_callback_t *callback; + void *context; + verify_plain_callback_t *verify_callback; +}; + + +struct db_oauth2 *db_oauth2_init(const char *config_path); + +void db_oauth2_ref(struct db_oauth2 *); +void db_oauth2_unref(struct db_oauth2 **); + +bool db_oauth2_uses_password_grant(const struct db_oauth2 *db); + +void db_oauth2_lookup(struct db_oauth2 *db, struct db_oauth2_request *req, const char *token, struct auth_request *request, db_oauth2_lookup_callback_t *callback, void *context); +#define db_oauth2_lookup(db, req, token, request, callback, context) \ + db_oauth2_lookup(db, req, token - \ + CALLBACK_TYPECHECK(callback, void(*)(struct db_oauth2_request *, enum passdb_result, const char*, typeof(context))), \ + request, (db_oauth2_lookup_callback_t*)callback, (void*)context) + +#endif diff --git a/src/auth/db-passwd-file.c b/src/auth/db-passwd-file.c new file mode 100644 index 0000000..e26c146 --- /dev/null +++ b/src/auth/db-passwd-file.c @@ -0,0 +1,493 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +#include "auth-common.h" + +#if defined (USERDB_PASSWD_FILE) || defined(PASSDB_PASSWD_FILE) + +#include "userdb.h" +#include "db-passwd-file.h" + +#include "array.h" +#include "buffer.h" +#include "istream.h" +#include "hash.h" +#include "str.h" +#include "eacces-error.h" +#include "ioloop.h" + +#include <unistd.h> +#include <fcntl.h> +#include <time.h> +#include <sys/stat.h> + +#define PARSE_TIME_STARTUP_WARN_SECS 60 +#define PARSE_TIME_RELOAD_WARN_SECS 10 + +static struct db_passwd_file *passwd_files; + +static void ATTR_NULL(3) +passwd_file_add(struct passwd_file *pw, const char *username, + const char *pass, const char *const *args) +{ + /* args = uid, gid, user info, home dir, shell, extra_fields */ + struct passwd_user *pu; + const char *extra_fields = NULL; + char *user; + size_t len; + + if (hash_table_lookup(pw->users, username) != NULL) { + e_error(pw->event, "User %s exists more than once", username); + return; + } + + pu = p_new(pw->pool, struct passwd_user, 1); + user = p_strdup(pw->pool, username); + + len = pass == NULL ? 0 : strlen(pass); + if (len > 4 && pass[0] != '{' && pass[0] != '$' && + pass[len-1] == ']' && pass[len-4] == '[') { + /* password[type] - we're being libpam-pwdfile compatible + here. it uses 13 = DES and 34 = MD5. For backwards + compatibility with ourself, we have also 56 = Digest-MD5. */ + int num = (pass[len-3] - '0') * 10 + (pass[len-2] - '0'); + + pass = t_strndup(pass, len-4); + if (num == 34) { + pu->password = p_strconcat(pw->pool, "{PLAIN-MD5}", + pass, NULL); + } else if (num == 56) { + pu->password = p_strconcat(pw->pool, "{DIGEST-MD5}", + pass, NULL); + if (strlen(pu->password) != 32 + 12) { + e_error(pw->event, "User %s " + "has invalid password", username); + return; + } + } else { + pu->password = p_strconcat(pw->pool, "{CRYPT}", + pass, NULL); + } + } else { + pu->password = p_strdup(pw->pool, pass); + } + + pu->uid = (uid_t)-1; + pu->gid = (gid_t)-1; + + if (*args == NULL) + ; + else if (!pw->db->userdb || **args == '\0') { + args++; + } else { + pu->uid = userdb_parse_uid(NULL, *args); + if (pu->uid == 0 || pu->uid == (uid_t)-1) { + e_error(pw->event, "User %s has invalid UID '%s'", + username, *args); + return; + } + args++; + } + + if (*args == NULL) { + if (pw->db->userdb_warn_missing) { + e_error(pw->event, "User %s is missing userdb info", + username); + } + /* don't allow userdb lookups */ + pu->uid = 0; + pu->gid = 0; + } else if (!pw->db->userdb || **args == '\0') + args++; + else { + pu->gid = userdb_parse_gid(NULL, *args); + if (pu->gid == 0 || pu->gid == (gid_t)-1) { + e_error(pw->event, "User %s has invalid GID '%s'", + username, *args); + return; + } + args++; + } + + /* user info */ + if (*args != NULL) + args++; + + /* home */ + if (*args != NULL) { + if (pw->db->userdb) + pu->home = p_strdup_empty(pw->pool, *args); + args++; + } + + /* shell */ + if (*args != NULL) + args++; + + if (*args != NULL && **args == '\0') { + /* old format, this field is empty and next field may + contain MAIL */ + args++; + if (*args != NULL && **args != '\0' && pw->db->userdb) { + extra_fields = + t_strconcat("userdb_mail=", + t_strarray_join(args, ":"), NULL); + } + } else if (*args != NULL) { + /* new format, contains a space separated list of + extra fields */ + extra_fields = t_strarray_join(args, ":"); + } + + if (extra_fields != NULL) { + pu->extra_fields = + p_strsplit_spaces(pw->pool, extra_fields, " "); + } + + hash_table_insert(pw->users, user, pu); +} + +static struct passwd_file * +passwd_file_new(struct db_passwd_file *db, const char *expanded_path) +{ + struct passwd_file *pw; + + pw = i_new(struct passwd_file, 1); + pw->db = db; + pw->path = i_strdup(expanded_path); + pw->fd = -1; + pw->event = event_create(db->event); + event_set_append_log_prefix(pw->event, + t_strdup_printf("passwd-file %s:", pw->path)); + + if (hash_table_is_created(db->files)) + hash_table_insert(db->files, pw->path, pw); + return pw; +} + +static int passwd_file_open(struct passwd_file *pw, bool startup, + const char **error_r) +{ + const char *no_args = NULL; + struct istream *input; + const char *line; + struct stat st; + time_t start_time, end_time; + unsigned int time_secs; + int fd; + + fd = open(pw->path, O_RDONLY); + if (fd == -1) { + if (errno == EACCES) + *error_r = eacces_error_get("open", pw->path); + else { + *error_r = t_strdup_printf("open(%s) failed: %m", + pw->path); + } + return -1; + } + + if (fstat(fd, &st) != 0) { + *error_r = t_strdup_printf("fstat(%s) failed: %m", + pw->path); + i_close_fd(&fd); + return -1; + } + + pw->fd = fd; + pw->stamp = st.st_mtime; + pw->size = st.st_size; + + pw->pool = pool_alloconly_create(MEMPOOL_GROWING"passwd_file", 10240); + hash_table_create(&pw->users, pw->pool, 0, str_hash, strcmp); + + start_time = time(NULL); + input = i_stream_create_fd(pw->fd, SIZE_MAX); + i_stream_set_return_partial_line(input, TRUE); + while ((line = i_stream_read_next_line(input)) != NULL) { + if (*line == '\0' || *line == ':' || *line == '#') + continue; /* no username or comment */ + + T_BEGIN { + const char *const *args = t_strsplit(line, ":"); + if (args[1] != NULL) { + /* at least username+password */ + passwd_file_add(pw, args[0], args[1], args+2); + } else { + /* only username */ + passwd_file_add(pw, args[0], NULL, &no_args); + } + } T_END; + } + i_stream_destroy(&input); + end_time = time(NULL); + time_secs = end_time - start_time; + + if ((time_secs > PARSE_TIME_STARTUP_WARN_SECS && startup) || + (time_secs > PARSE_TIME_RELOAD_WARN_SECS && !startup)) { + e_warning(pw->event, "Reading %u users took %u secs", + hash_table_count(pw->users), time_secs); + } else { + e_debug(pw->event, "Read %u users in %u secs", + hash_table_count(pw->users), time_secs); + } + return 0; +} + +static void passwd_file_close(struct passwd_file *pw) +{ + i_close_fd_path(&pw->fd, pw->path); + + hash_table_destroy(&pw->users); + pool_unref(&pw->pool); +} + +static void passwd_file_free(struct passwd_file *pw) +{ + if (hash_table_is_created(pw->db->files)) + hash_table_remove(pw->db->files, pw->path); + + passwd_file_close(pw); + event_unref(&pw->event); + i_free(pw->path); + i_free(pw); +} + +static int passwd_file_sync(struct auth_request *request, + struct passwd_file *pw) +{ + struct stat st; + const char *error; + + if (pw->last_sync_time == ioloop_time) + return hash_table_is_created(pw->users) ? 1 : -1; + pw->last_sync_time = ioloop_time; + + if (stat(pw->path, &st) < 0) { + /* with variables don't give hard errors, or errors about + nonexistent files */ + int ret = -1; + + if (errno == EACCES) { + e_error(authdb_event(request), + "%s", eacces_error_get("stat", pw->path)); + } else if (errno == ENOENT) { + auth_request_log_info(request, "passwd-file", + "missing passwd file: %s", pw->path); + ret = 0; + } else { + e_error(authdb_event(request), + "stat(%s) failed: %m", pw->path); + } + + if (pw->db->default_file != pw) + passwd_file_free(pw); + return ret; + } + + if (st.st_mtime != pw->stamp || st.st_size != pw->size) { + passwd_file_close(pw); + if (passwd_file_open(pw, FALSE, &error) < 0) { + e_error(authdb_event(request), + "%s", error); + return -1; + } + } + return 1; +} + +static struct db_passwd_file *db_passwd_file_find(const char *path) +{ + struct db_passwd_file *f; + + for (f = passwd_files; f != NULL; f = f->next) { + if (strcmp(f->path, path) == 0) + return f; + } + + return NULL; +} + +static void db_passwd_file_set_userdb(struct db_passwd_file *db) +{ + db->userdb = TRUE; + /* warn about missing userdb fields only when there aren't any other + userdbs. */ + db->userdb_warn_missing = + array_is_created(&global_auth_settings->userdbs) && + array_count(&global_auth_settings->userdbs) == 1; +} + +struct db_passwd_file * +db_passwd_file_init(const char *path, bool userdb, bool debug) +{ + struct db_passwd_file *db; + const char *p; + bool percents = FALSE; + + db = db_passwd_file_find(path); + if (db != NULL) { + db->refcount++; + if (userdb) + db_passwd_file_set_userdb(db); + return db; + } + + db = i_new(struct db_passwd_file, 1); + db->refcount = 1; + if (userdb) + db_passwd_file_set_userdb(db); + db->event = event_create(auth_event); + event_set_forced_debug(db->event, debug); + + for (p = path; *p != '\0'; p++) { + if (*p == '%' && p[1] != '\0') { + if (var_get_key(++p) == '%') + percents = TRUE; + else + db->vars = TRUE; + } + } + + if (percents && !db->vars) { + /* just extra escaped % chars. remove them. */ + struct var_expand_table empty_table[1] = { + { .key = '\0' }, + }; + string_t *dest; + const char *error; + + dest = t_str_new(256); + if (var_expand(dest, path, empty_table, &error) <= 0) + i_unreached(); + path = str_c(dest); + } + + db->path = i_strdup(path); + if (db->vars) { + hash_table_create(&db->files, default_pool, 0, + str_hash, strcmp); + } else { + db->default_file = passwd_file_new(db, path); + } + + db->next = passwd_files; + passwd_files = db; + return db; +} + +void db_passwd_file_parse(struct db_passwd_file *db) +{ + const char *error; + + if (db->default_file != NULL && db->default_file->stamp == 0) { + /* no variables, open the file immediately */ + if (passwd_file_open(db->default_file, TRUE, &error) < 0) + e_error(db->default_file->event, "%s", error); + } +} + +void db_passwd_file_unref(struct db_passwd_file **_db) +{ + struct db_passwd_file *db = *_db; + struct db_passwd_file **p; + struct hash_iterate_context *iter; + char *path; + struct passwd_file *file; + + *_db = NULL; + i_assert(db->refcount >= 0); + if (--db->refcount > 0) + return; + + for (p = &passwd_files; *p != NULL; p = &(*p)->next) { + if (*p == db) { + *p = db->next; + break; + } + } + + if (db->default_file != NULL) + passwd_file_free(db->default_file); + else { + iter = hash_table_iterate_init(db->files); + while (hash_table_iterate(iter, db->files, &path, &file)) + passwd_file_free(file); + hash_table_iterate_deinit(&iter); + hash_table_destroy(&db->files); + } + event_unref(&db->event); + i_free(db->path); + i_free(db); +} + +static const char * +path_fix(const char *path, + const struct auth_request *auth_request ATTR_UNUSED) +{ + const char *p; + + p = strchr(path, '/'); + if (p == NULL) + return path; + + /* most likely this is an invalid request. just cut off the '/' and + everything after it. */ + return t_strdup_until(path, p); +} + +int db_passwd_file_lookup(struct db_passwd_file *db, + struct auth_request *request, + const char *username_format, + struct passwd_user **user_r) +{ + struct passwd_file *pw; + string_t *username, *dest; + const char *error; + int ret; + + if (!db->vars) + pw = db->default_file; + else { + dest = t_str_new(256); + if (auth_request_var_expand(dest, db->path, request, path_fix, + &error) <= 0) { + e_error(authdb_event(request), + "Failed to expand passwd-file path %s: %s", + db->path, error); + return -1; + } + + pw = hash_table_lookup(db->files, str_c(dest)); + if (pw == NULL) { + /* doesn't exist yet. create lookup for it. */ + pw = passwd_file_new(db, str_c(dest)); + } + } + + if ((ret = passwd_file_sync(request, pw)) <= 0) { + /* pw may be freed now */ + return ret; + } + + username = t_str_new(256); + if (auth_request_var_expand(username, username_format, request, + auth_request_str_escape, &error) <= 0) { + e_error(authdb_event(request), + "Failed to expand username_format=%s: %s", + username_format, error); + return -1; + } + + e_debug(authdb_event(request), + "lookup: user=%s file=%s", + str_c(username), pw->path); + + *user_r = hash_table_lookup(pw->users, str_c(username)); + if (*user_r == NULL) { + auth_request_log_unknown_user(request, AUTH_SUBSYS_DB); + return 0; + } + return 1; +} + +#endif diff --git a/src/auth/db-passwd-file.h b/src/auth/db-passwd-file.h new file mode 100644 index 0000000..498f9c3 --- /dev/null +++ b/src/auth/db-passwd-file.h @@ -0,0 +1,58 @@ +#ifndef DB_PASSWD_FILE_H +#define DB_PASSWD_FILE_H + +#include "hash.h" + +#define PASSWD_FILE_DEFAULT_USERNAME_FORMAT "%u" +#define PASSWD_FILE_DEFAULT_SCHEME "CRYPT" + +struct passwd_user { + uid_t uid; + gid_t gid; + + char *home; + char *password; + char **extra_fields; +}; + +struct passwd_file { + struct db_passwd_file *db; + pool_t pool; + int refcount; + struct event *event; + + time_t last_sync_time; + char *path; + time_t stamp; + off_t size; + int fd; + + HASH_TABLE(char *, struct passwd_user *) users; +}; + +struct db_passwd_file { + struct db_passwd_file *next; + + int refcount; + struct event *event; + + char *path; + HASH_TABLE(char *, struct passwd_file *) files; + struct passwd_file *default_file; + + bool vars:1; + bool userdb:1; + bool userdb_warn_missing:1; +}; + +int db_passwd_file_lookup(struct db_passwd_file *db, + struct auth_request *request, + const char *username_format, + struct passwd_user **user_r); + +struct db_passwd_file * +db_passwd_file_init(const char *path, bool userdb, bool debug); +void db_passwd_file_parse(struct db_passwd_file *db); +void db_passwd_file_unref(struct db_passwd_file **db); + +#endif diff --git a/src/auth/db-sql.c b/src/auth/db-sql.c new file mode 100644 index 0000000..b77edd1 --- /dev/null +++ b/src/auth/db-sql.c @@ -0,0 +1,178 @@ +/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */ + +#include "auth-common.h" + +#if defined(PASSDB_SQL) || defined(USERDB_SQL) + +#include "settings.h" +#include "auth-request.h" +#include "auth-worker-client.h" +#include "db-sql.h" + +#include <stddef.h> + +#define DEF_STR(name) DEF_STRUCT_STR(name, db_sql_settings) +#define DEF_INT(name) DEF_STRUCT_INT(name, db_sql_settings) +#define DEF_BOOL(name) DEF_STRUCT_BOOL(name, db_sql_settings) + +static struct setting_def setting_defs[] = { + DEF_STR(driver), + DEF_STR(connect), + DEF_STR(password_query), + DEF_STR(user_query), + DEF_STR(update_query), + DEF_STR(iterate_query), + DEF_STR(default_pass_scheme), + DEF_BOOL(userdb_warning_disable), + + { 0, NULL, 0 } +}; + +static struct db_sql_settings default_db_sql_settings = { + .driver = NULL, + .connect = NULL, + .password_query = "SELECT username, domain, password FROM users WHERE username = '%n' AND domain = '%d'", + .user_query = "SELECT home, uid, gid FROM users WHERE username = '%n' AND domain = '%d'", + .update_query = "UPDATE users SET password = '%w' WHERE username = '%n' AND domain = '%d'", + .iterate_query = "SELECT username, domain FROM users", + .default_pass_scheme = "MD5", + .userdb_warning_disable = FALSE +}; + +static struct db_sql_connection *connections = NULL; + +static struct db_sql_connection *sql_conn_find(const char *config_path) +{ + struct db_sql_connection *conn; + + for (conn = connections; conn != NULL; conn = conn->next) { + if (strcmp(conn->config_path, config_path) == 0) + return conn; + } + + return NULL; +} + +static const char *parse_setting(const char *key, const char *value, + struct db_sql_connection *conn) +{ + return parse_setting_from_defs(conn->pool, setting_defs, + &conn->set, key, value); +} + +struct db_sql_connection *db_sql_init(const char *config_path, bool userdb) +{ + struct db_sql_connection *conn; + struct sql_settings set; + const char *error; + pool_t pool; + + conn = sql_conn_find(config_path); + if (conn != NULL) { + if (userdb) + conn->userdb_used = TRUE; + conn->refcount++; + return conn; + } + + if (*config_path == '\0') + i_fatal("sql: Configuration file path not given"); + + pool = pool_alloconly_create("db_sql_connection", 1024); + conn = p_new(pool, struct db_sql_connection, 1); + conn->pool = pool; + conn->userdb_used = userdb; + + conn->refcount = 1; + + conn->config_path = p_strdup(pool, config_path); + conn->set = default_db_sql_settings; + if (!settings_read_nosection(config_path, parse_setting, conn, &error)) + i_fatal("sql %s: %s", config_path, error); + + if (conn->set.password_query == default_db_sql_settings.password_query) + conn->default_password_query = TRUE; + if (conn->set.user_query == default_db_sql_settings.user_query) + conn->default_user_query = TRUE; + if (conn->set.update_query == default_db_sql_settings.update_query) + conn->default_update_query = TRUE; + if (conn->set.iterate_query == default_db_sql_settings.iterate_query) + conn->default_iterate_query = TRUE; + + if (conn->set.driver == NULL) { + i_fatal("sql: driver not set in configuration file %s", + config_path); + } + if (conn->set.connect == NULL) { + i_fatal("sql: connect string not set in configuration file %s", + config_path); + } + i_zero(&set); + set.driver = conn->set.driver; + set.connect_string = conn->set.connect; + set.event_parent = auth_event; + if (sql_init_full(&set, &conn->db, &error) < 0) { + i_fatal("sql: %s", error); + } + + conn->next = connections; + connections = conn; + return conn; +} + +void db_sql_unref(struct db_sql_connection **_conn) +{ + struct db_sql_connection *conn = *_conn; + + /* abort all pending auth requests before setting conn to NULL, + so that callbacks can still access it */ + sql_disconnect(conn->db); + + *_conn = NULL; + if (--conn->refcount > 0) + return; + + sql_unref(&conn->db); + pool_unref(&conn->pool); +} + +void db_sql_connect(struct db_sql_connection *conn) +{ + if (sql_connect(conn->db) < 0 && worker) { + /* auth worker's sql connection failed. we can't do anything + useful until the connection works. there's no point in + having tons of worker processes all logging failures, + so tell the auth master to stop creating new workers (and + maybe close old ones). this handling is especially useful if + we reach the max. number of connections for sql server. */ + auth_worker_client_send_error(); + } +} + +void db_sql_success(struct db_sql_connection *conn ATTR_UNUSED) +{ + if (worker) + auth_worker_client_send_success(); +} + +void db_sql_check_userdb_warning(struct db_sql_connection *conn) +{ + if (worker || conn->userdb_used || conn->set.userdb_warning_disable) + return; + + if (strcmp(conn->set.user_query, + default_db_sql_settings.user_query) != 0) { + i_warning("sql: Ignoring changed user_query in %s, " + "because userdb sql not used. " + "(If this is intentional, set userdb_warning_disable=yes)", + conn->config_path); + } else if (strcmp(conn->set.iterate_query, + default_db_sql_settings.iterate_query) != 0) { + i_warning("sql: Ignoring changed iterate_query in %s, " + "because userdb sql not used. " + "(If this is intentional, set userdb_warning_disable=yes)", + conn->config_path); + } +} + +#endif diff --git a/src/auth/db-sql.h b/src/auth/db-sql.h new file mode 100644 index 0000000..27e177b --- /dev/null +++ b/src/auth/db-sql.h @@ -0,0 +1,42 @@ +#ifndef DB_SQL_H +#define DB_SQL_H + +#include "sql-api.h" + +struct db_sql_settings { + const char *driver; + const char *connect; + const char *password_query; + const char *user_query; + const char *update_query; + const char *iterate_query; + const char *default_pass_scheme; + bool userdb_warning_disable; +}; + +struct db_sql_connection { + struct db_sql_connection *next; + + pool_t pool; + int refcount; + + char *config_path; + struct db_sql_settings set; + struct sql_db *db; + + bool default_password_query:1; + bool default_user_query:1; + bool default_update_query:1; + bool default_iterate_query:1; + bool userdb_used:1; +}; + +struct db_sql_connection *db_sql_init(const char *config_path, bool userdb); +void db_sql_unref(struct db_sql_connection **conn); + +void db_sql_connect(struct db_sql_connection *conn); +void db_sql_success(struct db_sql_connection *conn); + +void db_sql_check_userdb_warning(struct db_sql_connection *conn); + +#endif diff --git a/src/auth/main.c b/src/auth/main.c new file mode 100644 index 0000000..5f09fca --- /dev/null +++ b/src/auth/main.c @@ -0,0 +1,398 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +#include "auth-common.h" +#include "array.h" +#include "ioloop.h" +#include "net.h" +#include "lib-signals.h" +#include "restrict-access.h" +#include "child-wait.h" +#include "sql-api.h" +#include "module-dir.h" +#include "randgen.h" +#include "process-title.h" +#include "settings-parser.h" +#include "master-service.h" +#include "master-service-settings.h" +#include "master-interface.h" +#include "dict.h" +#include "password-scheme.h" +#include "passdb-cache.h" +#include "mech.h" +#include "otp.h" +#include "mech-otp-common.h" +#include "auth.h" +#include "auth-penalty.h" +#include "auth-token.h" +#include "auth-request-handler.h" +#include "auth-request-stats.h" +#include "auth-worker-server.h" +#include "auth-worker-client.h" +#include "auth-master-connection.h" +#include "auth-client-connection.h" +#include "auth-policy.h" + +#include <unistd.h> +#include <sys/stat.h> + +#define AUTH_PENALTY_ANVIL_PATH "anvil-auth-penalty" + +enum auth_socket_type { + AUTH_SOCKET_UNKNOWN = 0, + AUTH_SOCKET_CLIENT, + AUTH_SOCKET_LOGIN_CLIENT, + AUTH_SOCKET_MASTER, + AUTH_SOCKET_USERDB, + AUTH_SOCKET_TOKEN, + AUTH_SOCKET_TOKEN_LOGIN +}; + +struct auth_socket_listener { + enum auth_socket_type type; + struct stat st; + char *path; +}; + +bool worker = FALSE, worker_restart_request = FALSE; +time_t process_start_time; +struct auth_penalty *auth_penalty; + +static pool_t auth_set_pool; +static struct module *modules = NULL; +static struct mechanisms_register *mech_reg; +static ARRAY(struct auth_socket_listener) listeners; + +void auth_refresh_proctitle(void) +{ + if (!global_auth_settings->verbose_proctitle || worker) + return; + + process_title_set(t_strdup_printf( + "[%u wait, %u passdb, %u userdb]", + auth_request_state_count[AUTH_REQUEST_STATE_NEW] + + auth_request_state_count[AUTH_REQUEST_STATE_MECH_CONTINUE] + + auth_request_state_count[AUTH_REQUEST_STATE_FINISHED], + auth_request_state_count[AUTH_REQUEST_STATE_PASSDB], + auth_request_state_count[AUTH_REQUEST_STATE_USERDB])); +} + +static const char *const *read_global_settings(void) +{ + struct master_service_settings_output set_output; + const char **services; + unsigned int i, count; + + auth_set_pool = pool_alloconly_create("auth settings", 8192); + global_auth_settings = + auth_settings_read(NULL, auth_set_pool, &set_output); + + /* strdup() the service names, because they're allocated from + set parser pool, and we'll later clear it. */ + count = str_array_length(set_output.specific_services); + services = p_new(auth_set_pool, const char *, count + 1); + for (i = 0; i < count; i++) { + services[i] = p_strdup(auth_set_pool, + set_output.specific_services[i]); + } + return services; +} + +static enum auth_socket_type +auth_socket_type_get(const char *path) +{ + const char *name, *suffix; + + name = strrchr(path, '/'); + if (name == NULL) + name = path; + else + name++; + + suffix = strrchr(name, '-'); + if (suffix == NULL) + suffix = name; + else + suffix++; + + if (strcmp(suffix, "login") == 0) + return AUTH_SOCKET_LOGIN_CLIENT; + else if (strcmp(suffix, "master") == 0) + return AUTH_SOCKET_MASTER; + else if (strcmp(suffix, "userdb") == 0) + return AUTH_SOCKET_USERDB; + else if (strcmp(suffix, "token") == 0) + return AUTH_SOCKET_TOKEN; + else if (strcmp(suffix, "tokenlogin") == 0) + return AUTH_SOCKET_TOKEN_LOGIN; + else + return AUTH_SOCKET_CLIENT; +} + +static void listeners_init(void) +{ + unsigned int i, n; + const char *path; + + i_array_init(&listeners, 8); + n = master_service_get_socket_count(master_service); + for (i = 0; i < n; i++) { + int fd = MASTER_LISTEN_FD_FIRST + i; + struct auth_socket_listener *l; + + l = array_idx_get_space(&listeners, fd); + if (net_getunixname(fd, &path) < 0) { + if (errno != ENOTSOCK) + i_fatal("getunixname(%d) failed: %m", fd); + /* not a unix socket, set its name and type lazily */ + } else { + l->type = auth_socket_type_get(path); + l->path = i_strdup(path); + if (l->type == AUTH_SOCKET_USERDB) { + if (stat(path, &l->st) < 0) + i_error("stat(%s) failed: %m", path); + } + } + } +} + +static bool auth_module_filter(const char *name, void *context ATTR_UNUSED) +{ + if (str_begins(name, "authdb_") || + str_begins(name, "mech_")) { + /* this is lazily loaded */ + return FALSE; + } + return TRUE; +} + +static void main_preinit(void) +{ + struct module_dir_load_settings mod_set; + const char *const *services; + + /* Load built-in SQL drivers (if any) */ + sql_drivers_init(); + sql_drivers_register_all(); + + /* Initialize databases so their configuration files can be readable + only by root. Also load all modules here. */ + passdbs_init(); + userdbs_init(); + /* init schemes before plugins are loaded */ + password_schemes_init(); + + services = read_global_settings(); + + i_zero(&mod_set); + mod_set.abi_version = DOVECOT_ABI_VERSION; + mod_set.require_init_funcs = TRUE; + mod_set.debug = global_auth_settings->debug; + mod_set.filter_callback = auth_module_filter; + + modules = module_dir_load(AUTH_MODULE_DIR, NULL, &mod_set); + module_dir_init(modules); + + if (!worker) + auth_penalty = auth_penalty_init(AUTH_PENALTY_ANVIL_PATH); + auth_request_stats_init(); + mech_init(global_auth_settings); + mech_reg = mech_register_init(global_auth_settings); + dict_drivers_register_builtin(); + auths_preinit(global_auth_settings, auth_set_pool, + mech_reg, services); + + listeners_init(); + if (!worker) + auth_token_init(); + + /* Password lookups etc. may require roots, allow it. */ + restrict_access_by_env(RESTRICT_ACCESS_FLAG_ALLOW_ROOT, NULL); + restrict_access_allow_coredumps(TRUE); +} + +void auth_module_load(const char *names) +{ + struct module_dir_load_settings mod_set; + + i_zero(&mod_set); + mod_set.abi_version = DOVECOT_ABI_VERSION; + mod_set.require_init_funcs = TRUE; + mod_set.debug = global_auth_settings->debug; + mod_set.ignore_missing = TRUE; + + modules = module_dir_load_missing(modules, AUTH_MODULE_DIR, names, + &mod_set); + module_dir_init(modules); +} + +static void main_init(void) +{ + process_start_time = ioloop_time; + + /* If auth caches aren't used, just ignore these signals */ + lib_signals_ignore(SIGHUP, TRUE); + lib_signals_ignore(SIGUSR2, TRUE); + + /* set proctitles before init()s, since they may set them to error */ + auth_refresh_proctitle(); + auth_worker_refresh_proctitle(""); + + child_wait_init(); + auth_worker_server_init(); + auths_init(); + auth_request_handler_init(); + auth_policy_init(); + + if (worker) { + /* workers have only a single connection from the master + auth process */ + master_service_set_client_limit(master_service, 1); + auth_worker_set_max_service_count( + master_service_get_service_count(master_service)); + /* make sure this process cycles if auth connection drops */ + master_service_set_service_count(master_service, 1); + } else { + /* caching is handled only by the main auth process */ + passdb_cache_init(global_auth_settings); + } +} + +static void main_deinit(void) +{ + struct auth_socket_listener *l; + + if (auth_penalty != NULL) { + /* cancel all pending anvil penalty lookups */ + auth_penalty_deinit(&auth_penalty); + } + /* deinit auth workers, which aborts pending requests */ + auth_worker_server_deinit(); + /* deinit passdbs and userdbs. it aborts any pending async requests. */ + auths_deinit(); + /* flush pending requests */ + auth_request_handler_deinit(); + /* there are no more auth requests */ + auths_free(); + dict_drivers_unregister_builtin(); + + auth_token_deinit(); + + auth_client_connections_destroy_all(); + auth_master_connections_destroy_all(); + auth_worker_connections_destroy_all(); + + auth_policy_deinit(); + mech_register_deinit(&mech_reg); + mech_otp_deinit(); + mech_deinit(global_auth_settings); + + /* allow modules to unregister their dbs/drivers/etc. before freeing + the whole data structures containing them. */ + module_dir_unload(&modules); + + userdbs_deinit(); + passdbs_deinit(); + passdb_cache_deinit(); + password_schemes_deinit(); + auth_request_stats_deinit(); + + sql_drivers_deinit(); + child_wait_deinit(); + + array_foreach_modifiable(&listeners, l) + i_free(l->path); + array_free(&listeners); + pool_unref(&auth_set_pool); +} + +static void worker_connected(struct master_service_connection *conn) +{ + if (auth_worker_has_client()) { + i_error("Auth workers can handle only a single client"); + return; + } + + master_service_client_connection_accept(conn); + (void)auth_worker_client_create(auth_default_service(), conn); +} + +static void client_connected(struct master_service_connection *conn) +{ + struct auth_socket_listener *l; + struct auth *auth; + + l = array_idx_modifiable(&listeners, conn->listen_fd); + if (l->type == AUTH_SOCKET_UNKNOWN) { + /* first connection from inet socket, figure out its type + from the listener name */ + l->type = auth_socket_type_get(conn->name); + l->path = i_strdup(conn->name); + } + auth = auth_default_service(); + switch (l->type) { + case AUTH_SOCKET_MASTER: + (void)auth_master_connection_create(auth, conn->fd, + l->path, NULL, FALSE); + break; + case AUTH_SOCKET_USERDB: + (void)auth_master_connection_create(auth, conn->fd, + l->path, &l->st, TRUE); + break; + case AUTH_SOCKET_LOGIN_CLIENT: + auth_client_connection_create(auth, conn->fd, TRUE, FALSE); + break; + case AUTH_SOCKET_CLIENT: + auth_client_connection_create(auth, conn->fd, FALSE, FALSE); + break; + case AUTH_SOCKET_TOKEN_LOGIN: + auth_client_connection_create(auth, conn->fd, TRUE, TRUE); + break; + case AUTH_SOCKET_TOKEN: + auth_client_connection_create(auth, conn->fd, FALSE, TRUE); + break; + default: + i_unreached(); + } + master_service_client_connection_accept(conn); +} + +static void auth_die(void) +{ + if (!worker) { + /* do nothing. auth clients should disconnect soon. */ + } else { + /* ask auth master to disconnect us */ + auth_worker_client_send_shutdown(); + } +} + +int main(int argc, char *argv[]) +{ + int c; + enum master_service_flags service_flags = + MASTER_SERVICE_FLAG_NO_SSL_INIT; + + master_service = master_service_init("auth", service_flags, &argc, &argv, "w"); + master_service_init_log(master_service); + + while ((c = master_getopt(master_service)) > 0) { + switch (c) { + case 'w': + master_service_init_log_with_pid(master_service); + worker = TRUE; + break; + default: + return FATAL_DEFAULT; + } + } + + main_preinit(); + master_service_set_die_callback(master_service, auth_die); + main_init(); + master_service_init_finish(master_service); + master_service_run(master_service, worker ? worker_connected : + client_connected); + main_deinit(); + master_service_deinit(&master_service); + return 0; +} diff --git a/src/auth/mech-anonymous.c b/src/auth/mech-anonymous.c new file mode 100644 index 0000000..fbbfccd --- /dev/null +++ b/src/auth/mech-anonymous.c @@ -0,0 +1,48 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +#include "auth-common.h" +#include "mech.h" + +static void +mech_anonymous_auth_continue(struct auth_request *request, + const unsigned char *data, size_t data_size) +{ + i_assert(*request->set->anonymous_username != '\0'); + + if (request->set->verbose) { + /* temporarily set the user to the one that was given, + so that the log message goes right */ + auth_request_set_username_forced(request, + t_strndup(data, data_size)); + e_info(request->mech_event, "login"); + } + + auth_request_set_username_forced(request, + request->set->anonymous_username); + + request->passdb_success = TRUE; + auth_request_success(request, "", 0); +} + +static struct auth_request *mech_anonymous_auth_new(void) +{ + struct auth_request *request; + pool_t pool; + + pool = pool_alloconly_create(MEMPOOL_GROWING"anonymous_auth_request", 2048); + request = p_new(pool, struct auth_request, 1); + request->pool = pool; + return request; +} + +const struct mech_module mech_anonymous = { + "ANONYMOUS", + + .flags = MECH_SEC_ANONYMOUS | MECH_SEC_ALLOW_NULS, + .passdb_need = MECH_PASSDB_NEED_NOTHING, + + mech_anonymous_auth_new, + mech_generic_auth_initial, + mech_anonymous_auth_continue, + mech_generic_auth_free +}; diff --git a/src/auth/mech-apop.c b/src/auth/mech-apop.c new file mode 100644 index 0000000..f28171f --- /dev/null +++ b/src/auth/mech-apop.c @@ -0,0 +1,173 @@ +/* + * APOP (RFC-1460) authentication mechanism. + * + * Copyright (c) 2004 Andrey Panin <pazke@donpac.ru> + * + * This software is released under the MIT license. + */ + +#include "auth-common.h" +#include "mech.h" +#include "passdb.h" +#include "md5.h" +#include "buffer.h" +#include "auth-client-connection.h" +#include "auth-master-connection.h" + +#include <stdio.h> +#include <unistd.h> + +struct apop_auth_request { + struct auth_request auth_request; + + pool_t pool; + + /* requested: */ + char *challenge; + + /* received: */ + unsigned char response_digest[16]; +}; + +static bool verify_credentials(struct apop_auth_request *request, + const unsigned char *credentials, size_t size) +{ + unsigned char digest[16]; + struct md5_context ctx; + + md5_init(&ctx); + md5_update(&ctx, request->challenge, strlen(request->challenge)); + md5_update(&ctx, credentials, size); + md5_final(&ctx, digest); + + return mem_equals_timing_safe(digest, request->response_digest, 16); +} + +static void +apop_credentials_callback(enum passdb_result result, + const unsigned char *credentials, size_t size, + struct auth_request *auth_request) +{ + struct apop_auth_request *request = + (struct apop_auth_request *)auth_request; + + switch (result) { + case PASSDB_RESULT_OK: + if (verify_credentials(request, credentials, size)) + auth_request_success(auth_request, "", 0); + else + auth_request_fail(auth_request); + break; + case PASSDB_RESULT_INTERNAL_FAILURE: + auth_request_internal_failure(auth_request); + break; + default: + auth_request_fail(auth_request); + break; + } +} + +static void +mech_apop_auth_initial(struct auth_request *auth_request, + const unsigned char *data, size_t data_size) +{ + struct apop_auth_request *request = + (struct apop_auth_request *)auth_request; + const unsigned char *tmp, *end, *username = NULL; + unsigned long pid, connect_uid, timestamp; + const char *error; + + /* pop3-login handles sending the challenge and getting the response. + Our input here is: <challenge> \0 <username> \0 <response> */ + + if (data_size == 0) { + /* Should never happen */ + e_info(auth_request->mech_event, + "no initial response"); + auth_request_fail(auth_request); + return; + } + + tmp = data; + end = data + data_size; + + /* get the challenge */ + while (tmp != end && *tmp != '\0') + tmp++; + request->challenge = p_strdup_until(request->pool, data, tmp); + + if (tmp != end) { + /* get the username */ + username = ++tmp; + while (tmp != end && *tmp != '\0') + tmp++; + } else { + /* should never happen */ + e_info(auth_request->mech_event, + "malformed data"); + auth_request_fail(auth_request); + return; + } + + if (tmp + 1 + 16 != end) { + /* Should never happen */ + e_info(auth_request->mech_event, + "malformed data"); + auth_request_fail(auth_request); + return; + } + memcpy(request->response_digest, tmp + 1, + sizeof(request->response_digest)); + + /* the challenge must begin with trusted unique ID. we trust only + ourself, so make sure it matches our connection specific UID + which we told to client in handshake. Also require a timestamp + which is later than this process's start time. */ + + if (sscanf(request->challenge, "<%lx.%lx.%lx.", + &pid, &connect_uid, ×tamp) != 3 || + connect_uid != auth_request->connect_uid || + pid != (unsigned long)getpid() || + (time_t)timestamp < process_start_time) { + e_info(auth_request->mech_event, + "invalid challenge"); + auth_request_fail(auth_request); + return; + } + + if (!auth_request_set_username(auth_request, (const char *)username, + &error)) { + e_info(auth_request->mech_event, "%s", error); + auth_request_fail(auth_request); + return; + } + + auth_request_lookup_credentials(auth_request, "PLAIN", + apop_credentials_callback); +} + +static struct auth_request *mech_apop_auth_new(void) +{ + struct apop_auth_request *request; + pool_t pool; + + pool = pool_alloconly_create(MEMPOOL_GROWING"apop_auth_request", 2048); + request = p_new(pool, struct apop_auth_request, 1); + request->pool = pool; + + request->auth_request.pool = pool; + return &request->auth_request; +} + +const struct mech_module mech_apop = { + "APOP", + + .flags = MECH_SEC_PRIVATE | MECH_SEC_DICTIONARY | MECH_SEC_ACTIVE | + MECH_SEC_ALLOW_NULS, + .passdb_need = MECH_PASSDB_NEED_VERIFY_RESPONSE, + + mech_apop_auth_new, + mech_apop_auth_initial, + NULL, + mech_generic_auth_free +}; diff --git a/src/auth/mech-cram-md5.c b/src/auth/mech-cram-md5.c new file mode 100644 index 0000000..e6ab888 --- /dev/null +++ b/src/auth/mech-cram-md5.c @@ -0,0 +1,193 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +/* CRAM-MD5 SASL authentication, see RFC-2195 + Joshua Goodall <joshua@roughtrade.net> */ + +#include "auth-common.h" +#include "ioloop.h" +#include "buffer.h" +#include "hex-binary.h" +#include "hmac-cram-md5.h" +#include "hmac.h" +#include "md5.h" +#include "randgen.h" +#include "mech.h" +#include "passdb.h" +#include "hostpid.h" + +#include <time.h> + +struct cram_auth_request { + struct auth_request auth_request; + + pool_t pool; + + /* requested: */ + char *challenge; + + /* received: */ + char *username; + char *response; + unsigned long maxbuf; +}; + +static const char *get_cram_challenge(void) +{ + unsigned char buf[17]; + size_t i; + + random_fill(buf, sizeof(buf)-1); + + for (i = 0; i < sizeof(buf)-1; i++) + buf[i] = (buf[i] % 10) + '0'; + buf[sizeof(buf)-1] = '\0'; + + return t_strdup_printf("<%s.%s@%s>", (const char *)buf, + dec2str(ioloop_time), my_hostname); +} + +static bool verify_credentials(struct cram_auth_request *request, + const unsigned char *credentials, size_t size) +{ + + unsigned char digest[MD5_RESULTLEN]; + struct hmac_context ctx; + const char *response_hex; + + if (size != CRAM_MD5_CONTEXTLEN) { + e_error(request->auth_request.mech_event, + "invalid credentials length"); + return FALSE; + } + + hmac_init(&ctx, NULL, 0, &hash_method_md5); + hmac_md5_set_cram_context(&ctx, credentials); + hmac_update(&ctx, request->challenge, strlen(request->challenge)); + hmac_final(&ctx, digest); + + response_hex = binary_to_hex(digest, sizeof(digest)); + + if (!mem_equals_timing_safe(response_hex, request->response, sizeof(digest)*2)) { + e_info(request->auth_request.mech_event, + AUTH_LOG_MSG_PASSWORD_MISMATCH); + return FALSE; + } + + return TRUE; +} + +static bool parse_cram_response(struct cram_auth_request *request, + const unsigned char *data, size_t size, + const char **error_r) +{ + size_t i, space; + + *error_r = NULL; + + /* <username> SPACE <response>. Username may contain spaces, so assume + the rightmost space is the response separator. */ + for (i = space = 0; i < size; i++) { + if (data[i] == '\0') { + *error_r = "NULs in response"; + return FALSE; + } + if (data[i] == ' ') + space = i; + } + + if (space == 0) { + *error_r = "missing digest"; + return FALSE; + } + + request->username = p_strndup(request->pool, data, space); + space++; + request->response = + p_strndup(request->pool, data + space, size - space); + return TRUE; +} + +static void credentials_callback(enum passdb_result result, + const unsigned char *credentials, size_t size, + struct auth_request *auth_request) +{ + struct cram_auth_request *request = + (struct cram_auth_request *)auth_request; + + switch (result) { + case PASSDB_RESULT_OK: + if (verify_credentials(request, credentials, size)) + auth_request_success(auth_request, "", 0); + else + auth_request_fail(auth_request); + break; + case PASSDB_RESULT_INTERNAL_FAILURE: + auth_request_internal_failure(auth_request); + break; + default: + auth_request_fail(auth_request); + break; + } +} + +static void +mech_cram_md5_auth_continue(struct auth_request *auth_request, + const unsigned char *data, size_t data_size) +{ + struct cram_auth_request *request = + (struct cram_auth_request *)auth_request; + const char *error; + + if (parse_cram_response(request, data, data_size, &error)) { + if (auth_request_set_username(auth_request, request->username, + &error)) { + auth_request_lookup_credentials(auth_request, + "CRAM-MD5", credentials_callback); + return; + } + } + + if (error == NULL) + error = "authentication failed"; + + e_info(auth_request->mech_event, "%s", error); + auth_request_fail(auth_request); +} + +static void +mech_cram_md5_auth_initial(struct auth_request *auth_request, + const unsigned char *data ATTR_UNUSED, + size_t data_size ATTR_UNUSED) +{ + struct cram_auth_request *request = + (struct cram_auth_request *)auth_request; + + request->challenge = p_strdup(request->pool, get_cram_challenge()); + auth_request_handler_reply_continue(auth_request, request->challenge, + strlen(request->challenge)); +} + +static struct auth_request *mech_cram_md5_auth_new(void) +{ + struct cram_auth_request *request; + pool_t pool; + + pool = pool_alloconly_create(MEMPOOL_GROWING"cram_md5_auth_request", 2048); + request = p_new(pool, struct cram_auth_request, 1); + request->pool = pool; + + request->auth_request.pool = pool; + return &request->auth_request; +} + +const struct mech_module mech_cram_md5 = { + "CRAM-MD5", + + .flags = MECH_SEC_DICTIONARY | MECH_SEC_ACTIVE, + .passdb_need = MECH_PASSDB_NEED_VERIFY_RESPONSE, + + mech_cram_md5_auth_new, + mech_cram_md5_auth_initial, + mech_cram_md5_auth_continue, + mech_generic_auth_free +}; diff --git a/src/auth/mech-digest-md5-private.h b/src/auth/mech-digest-md5-private.h new file mode 100644 index 0000000..fb9ff80 --- /dev/null +++ b/src/auth/mech-digest-md5-private.h @@ -0,0 +1,38 @@ +#ifndef MECH_DIGEST_MD5_PRIVATE_H +#define MECH_DIGEST_MD5_PRIVATE_H + +#include "auth-request.h" + +enum qop_option { + QOP_AUTH = 0x01, /* authenticate */ + QOP_AUTH_INT = 0x02, /* + integrity protection, not supported yet */ + QOP_AUTH_CONF = 0x04, /* + encryption, not supported yet */ + + QOP_COUNT = 3 +}; + +struct digest_auth_request { + struct auth_request auth_request; + + pool_t pool; + + /* requested: */ + char *nonce; + enum qop_option qop; + + /* received: */ + char *username; + char *cnonce; + char *nonce_count; + char *qop_value; + char *digest_uri; /* may be NULL */ + char *authzid; /* may be NULL, authorization ID */ + unsigned char response[32]; + unsigned long maxbuf; + bool nonce_found:1; + + /* final reply: */ + char *rspauth; +}; + +#endif diff --git a/src/auth/mech-digest-md5.c b/src/auth/mech-digest-md5.c new file mode 100644 index 0000000..d7f4383 --- /dev/null +++ b/src/auth/mech-digest-md5.c @@ -0,0 +1,592 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +/* Digest-MD5 SASL authentication, see RFC-2831 */ + +#include "auth-common.h" +#include "base64.h" +#include "buffer.h" +#include "hex-binary.h" +#include "mech-digest-md5-private.h" +#include "md5.h" +#include "randgen.h" +#include "str.h" +#include "str-sanitize.h" +#include "mech.h" +#include "passdb.h" + + +#define MAX_REALM_LEN 64 + +/* Linear whitespace */ +#define IS_LWS(c) ((c) == ' ' || (c) == '\t') + +static const char *qop_names[] = { "auth", "auth-int", "auth-conf" }; + +static string_t *get_digest_challenge(struct digest_auth_request *request) +{ + const struct auth_settings *set = request->auth_request.set; + buffer_t buf; + string_t *str; + const char *const *tmp; + unsigned char nonce[16]; + unsigned char nonce_base64[MAX_BASE64_ENCODED_SIZE(sizeof(nonce))+1]; + int i; + bool first_qop; + + /* + realm="hostname" (multiple allowed) + nonce="randomized data, at least 64bit" + qop="auth,auth-int,auth-conf" + maxbuf=number (with auth-int, auth-conf, defaults to 64k) + charset="utf-8" (iso-8859-1 if it doesn't exist) + algorithm="md5-sess" + cipher="3des,des,rc4-40,rc4,rc4-56" (with auth-conf) + */ + + /* get 128bit of random data as nonce */ + random_fill(nonce, sizeof(nonce)); + + buffer_create_from_data(&buf, nonce_base64, sizeof(nonce_base64)); + base64_encode(nonce, sizeof(nonce), &buf); + buffer_append_c(&buf, '\0'); + request->nonce = p_strdup(request->pool, buf.data); + + str = t_str_new(256); + if (*set->realms_arr == NULL) { + /* If no realms are given, at least Cyrus SASL client defaults + to destination host name */ + str_append(str, "realm=\"\","); + } else { + for (tmp = set->realms_arr; *tmp != NULL; tmp++) + str_printfa(str, "realm=\"%s\",", *tmp); + } + + str_printfa(str, "nonce=\"%s\",", request->nonce); + + str_append(str, "qop=\""); first_qop = TRUE; + for (i = 0; i < QOP_COUNT; i++) { + if ((request->qop & (1 << i)) != 0) { + if (first_qop) + first_qop = FALSE; + else + str_append_c(str, ','); + str_append(str, qop_names[i]); + } + } + str_append(str, "\","); + + str_append(str, "charset=\"utf-8\"," + "algorithm=\"md5-sess\""); + return str; +} + +static bool verify_credentials(struct digest_auth_request *request, + const unsigned char *credentials, size_t size) +{ + struct md5_context ctx; + unsigned char digest[MD5_RESULTLEN]; + const char *a1_hex, *a2_hex, *response_hex; + int i; + + /* get the MD5 password */ + if (size != MD5_RESULTLEN) { + e_error(request->auth_request.mech_event, + "invalid credentials length"); + return FALSE; + } + + /* + response = + HEX( KD ( HEX(H(A1)), + { nonce-value, ":" nc-value, ":", + cnonce-value, ":", qop-value, ":", HEX(H(A2)) })) + + and if authzid is not empty: + + A1 = { H( { username-value, ":", realm-value, ":", passwd } ), + ":", nonce-value, ":", cnonce-value, ":", authzid } + + else: + + A1 = { H( { username-value, ":", realm-value, ":", passwd } ), + ":", nonce-value, ":", cnonce-value } + + If the "qop" directive's value is "auth", then A2 is: + + A2 = { "AUTHENTICATE:", digest-uri-value } + + If the "qop" value is "auth-int" or "auth-conf" then A2 is: + + A2 = { "AUTHENTICATE:", digest-uri-value, + ":00000000000000000000000000000000" } + */ + + /* A1 */ + md5_init(&ctx); + md5_update(&ctx, credentials, size); + md5_update(&ctx, ":", 1); + md5_update(&ctx, request->nonce, strlen(request->nonce)); + md5_update(&ctx, ":", 1); + md5_update(&ctx, request->cnonce, strlen(request->cnonce)); + if (request->authzid != NULL) { + md5_update(&ctx, ":", 1); + md5_update(&ctx, request->authzid, strlen(request->authzid)); + } + md5_final(&ctx, digest); + a1_hex = binary_to_hex(digest, 16); + + /* do it twice, first verify the user's response, the second is + sent for client as a reply */ + for (i = 0; i < 2; i++) { + /* A2 */ + md5_init(&ctx); + if (i == 0) + md5_update(&ctx, "AUTHENTICATE:", 13); + else + md5_update(&ctx, ":", 1); + + if (request->digest_uri != NULL) { + md5_update(&ctx, request->digest_uri, + strlen(request->digest_uri)); + } + if (request->qop == QOP_AUTH_INT || + request->qop == QOP_AUTH_CONF) { + md5_update(&ctx, ":00000000000000000000000000000000", + 33); + } + md5_final(&ctx, digest); + a2_hex = binary_to_hex(digest, 16); + + /* response */ + md5_init(&ctx); + md5_update(&ctx, a1_hex, 32); + md5_update(&ctx, ":", 1); + md5_update(&ctx, request->nonce, strlen(request->nonce)); + md5_update(&ctx, ":", 1); + md5_update(&ctx, request->nonce_count, + strlen(request->nonce_count)); + md5_update(&ctx, ":", 1); + md5_update(&ctx, request->cnonce, strlen(request->cnonce)); + md5_update(&ctx, ":", 1); + md5_update(&ctx, request->qop_value, + strlen(request->qop_value)); + md5_update(&ctx, ":", 1); + md5_update(&ctx, a2_hex, 32); + md5_final(&ctx, digest); + response_hex = binary_to_hex(digest, 16); + + if (i == 0) { + /* verify response */ + if (!mem_equals_timing_safe(response_hex, request->response, 32)) { + auth_request_log_info(&request->auth_request, + AUTH_SUBSYS_MECH, + AUTH_LOG_MSG_PASSWORD_MISMATCH); + return FALSE; + } + } else { + request->rspauth = + p_strconcat(request->pool, "rspauth=", + response_hex, NULL); + } + } + + return TRUE; +} + +static bool parse_next(char **data, char **key, char **value) +{ + /* @UNSAFE */ + char *p, *dest; + + p = *data; + while (IS_LWS(*p)) p++; + + /* get key */ + *key = p; + while (*p != '\0' && *p != '=' && *p != ',') + p++; + + if (*p != '=') { + *data = p; + return FALSE; + } + + *value = p+1; + + /* skip trailing whitespace in key */ + while (p > *data && IS_LWS(p[-1])) + p--; + *p = '\0'; + + /* get value */ + p = *value; + while (IS_LWS(*p)) p++; + + if (*p != '"') { + while (*p != '\0' && *p != ',') + p++; + + *data = p; + /* If there is more to parse, ensure it won't get skipped + because *p is set to NUL below */ + if (**data != '\0') (*data)++; + while (IS_LWS(p[-1])) + p--; + *p = '\0'; + } else { + /* quoted string */ + *value = dest = ++p; + while (*p != '\0' && *p != '"') { + if (*p == '\\' && p[1] != '\0') + p++; + *dest++ = *p++; + } + + *data = *p == '"' ? p+1 : p; + *dest = '\0'; + } + + return TRUE; +} + +static bool auth_handle_response(struct digest_auth_request *request, + char *key, char *value, const char **error) +{ + unsigned int i; + + (void)str_lcase(key); + + if (strcmp(key, "realm") == 0) { + if (request->auth_request.fields.realm == NULL && *value != '\0') + auth_request_set_realm(&request->auth_request, value); + return TRUE; + } + + if (strcmp(key, "username") == 0) { + if (request->username != NULL) { + *error = "username must not exist more than once"; + return FALSE; + } + + if (*value == '\0') { + *error = "empty username"; + return FALSE; + } + + request->username = p_strdup(request->pool, value); + return TRUE; + } + + if (strcmp(key, "nonce") == 0) { + /* nonce must be same */ + if (strcmp(value, request->nonce) != 0) { + *error = "Invalid nonce"; + return FALSE; + } + + request->nonce_found = TRUE; + return TRUE; + } + + if (strcmp(key, "cnonce") == 0) { + if (request->cnonce != NULL) { + *error = "cnonce must not exist more than once"; + return FALSE; + } + + if (*value == '\0') { + *error = "cnonce can't contain empty value"; + return FALSE; + } + + request->cnonce = p_strdup(request->pool, value); + return TRUE; + } + + if (strcmp(key, "nc") == 0) { + unsigned int nc; + + if (request->nonce_count != NULL) { + *error = "nonce-count must not exist more than once"; + return FALSE; + } + + if (str_to_uint(value, &nc) < 0) { + *error = "nonce-count value invalid"; + return FALSE; + } + + if (nc != 1) { + *error = "re-auth not supported currently"; + return FALSE; + } + + request->nonce_count = p_strdup(request->pool, value); + return TRUE; + } + + if (strcmp(key, "qop") == 0) { + for (i = 0; i < QOP_COUNT; i++) { + if (strcasecmp(qop_names[i], value) == 0) + break; + } + + if (i == QOP_COUNT) { + *error = t_strdup_printf("Unknown QoP value: %s", + str_sanitize(value, 32)); + return FALSE; + } + + request->qop &= (1 << i); + if (request->qop == 0) { + *error = "Nonallowed QoP requested"; + return FALSE; + } + + request->qop_value = p_strdup(request->pool, value); + return TRUE; + } + + if (strcmp(key, "digest-uri") == 0) { + /* type / host / serv-name */ + const char *const *uri = t_strsplit(value, "/"); + + if (uri[0] == NULL || uri[1] == NULL) { + *error = "Invalid digest-uri"; + return FALSE; + } + + /* FIXME: RFC recommends that we verify the host/serv-type. + But isn't the realm enough already? That'd be just extra + configuration.. Maybe optionally list valid hosts in + config file? */ + request->digest_uri = p_strdup(request->pool, value); + return TRUE; + } + + if (strcmp(key, "maxbuf") == 0) { + if (request->maxbuf != 0) { + *error = "maxbuf must not exist more than once"; + return FALSE; + } + + if (str_to_ulong(value, &request->maxbuf) < 0 || + request->maxbuf == 0) { + *error = "Invalid maxbuf value"; + return FALSE; + } + return TRUE; + } + + if (strcmp(key, "charset") == 0) { + if (strcasecmp(value, "utf-8") != 0) { + *error = "Only utf-8 charset is allowed"; + return FALSE; + } + + return TRUE; + } + + if (strcmp(key, "response") == 0) { + if (strlen(value) != 32) { + *error = "Invalid response value"; + return FALSE; + } + + memcpy(request->response, value, 32); + return TRUE; + } + + if (strcmp(key, "cipher") == 0) { + /* not supported, ignore */ + return TRUE; + } + + if (strcmp(key, "authzid") == 0) { + if (request->authzid != NULL) { + *error = "authzid must not exist more than once"; + return FALSE; + } + + if (*value == '\0') { + *error = "empty authzid"; + return FALSE; + } + + request->authzid = p_strdup(request->pool, value); + return TRUE; + } + + /* unknown key, ignore */ + return TRUE; +} + +static bool parse_digest_response(struct digest_auth_request *request, + const unsigned char *data, size_t size, + const char **error) +{ + char *copy, *key, *value; + bool failed; + + /* + realm="realm" + username="username" + nonce="randomized data" + cnonce="??" + nc=00000001 + qop="auth|auth-int|auth-conf" + digest-uri="serv-type/host[/serv-name]" + response=32 HEX digits + maxbuf=number (with auth-int, auth-conf, defaults to 64k) + charset="utf-8" (iso-8859-1 if it doesn't exist) + cipher="cipher-value" + authzid="authzid-value" + */ + + *error = NULL; + failed = FALSE; + + if (size == 0) { + *error = "Client sent no input"; + return FALSE; + } + + /* treating response as NUL-terminated string also gets rid of all + potential problems with NUL characters in strings. */ + copy = t_strdup_noconst(t_strndup(data, size)); + while (*copy != '\0') { + if (parse_next(©, &key, &value)) { + if (!auth_handle_response(request, key, value, error)) { + failed = TRUE; + break; + } + } + + if (*copy == ',') + copy++; + } + + if (!failed) { + if (!request->nonce_found) { + *error = "Missing nonce parameter"; + failed = TRUE; + } else if (request->cnonce == NULL) { + *error = "Missing cnonce parameter"; + failed = TRUE; + } else if (request->username == NULL) { + *error = "Missing username parameter"; + failed = TRUE; + } + } + + if (request->nonce_count == NULL) + request->nonce_count = p_strdup(request->pool, "00000001"); + if (request->qop_value == NULL) + request->qop_value = p_strdup(request->pool, "auth"); + + return !failed; +} + +static void credentials_callback(enum passdb_result result, + const unsigned char *credentials, size_t size, + struct auth_request *auth_request) +{ + struct digest_auth_request *request = + (struct digest_auth_request *)auth_request; + + switch (result) { + case PASSDB_RESULT_OK: + if (!verify_credentials(request, credentials, size)) { + auth_request_fail(auth_request); + return; + } + + auth_request_success(auth_request, request->rspauth, + strlen(request->rspauth)); + break; + case PASSDB_RESULT_INTERNAL_FAILURE: + auth_request_internal_failure(auth_request); + break; + default: + auth_request_fail(auth_request); + break; + } +} + +static void +mech_digest_md5_auth_continue(struct auth_request *auth_request, + const unsigned char *data, size_t data_size) +{ + struct digest_auth_request *request = + (struct digest_auth_request *)auth_request; + const char *username, *error; + + if (parse_digest_response(request, data, data_size, &error)) { + if (auth_request->fields.realm != NULL && + strchr(request->username, '@') == NULL) { + username = t_strconcat(request->username, "@", + auth_request->fields.realm, NULL); + auth_request->domain_is_realm = TRUE; + } else { + username = request->username; + } + + if (auth_request_set_username(auth_request, username, &error) && + (request->authzid == NULL || + auth_request_set_login_username(auth_request, + request->authzid, + &error))) { + auth_request_lookup_credentials(auth_request, + "DIGEST-MD5", credentials_callback); + return; + } + } + + if (error != NULL) + e_info(auth_request->mech_event, "%s", error); + + auth_request_fail(auth_request); +} + +static void +mech_digest_md5_auth_initial(struct auth_request *auth_request, + const unsigned char *data ATTR_UNUSED, + size_t data_size ATTR_UNUSED) +{ + struct digest_auth_request *request = + (struct digest_auth_request *)auth_request; + string_t *challenge; + + /* FIXME: there's no support for subsequent authentication */ + + challenge = get_digest_challenge(request); + auth_request_handler_reply_continue(auth_request, str_data(challenge), + str_len(challenge)); +} + +static struct auth_request *mech_digest_md5_auth_new(void) +{ + struct digest_auth_request *request; + pool_t pool; + + pool = pool_alloconly_create(MEMPOOL_GROWING"digest_md5_auth_request", 2048); + request = p_new(pool, struct digest_auth_request, 1); + request->pool = pool; + request->qop = QOP_AUTH; + + request->auth_request.pool = pool; + return &request->auth_request; +} + +const struct mech_module mech_digest_md5 = { + "DIGEST-MD5", + + .flags = MECH_SEC_DICTIONARY | MECH_SEC_ACTIVE | + MECH_SEC_MUTUAL_AUTH, + .passdb_need = MECH_PASSDB_NEED_LOOKUP_CREDENTIALS, + + mech_digest_md5_auth_new, + mech_digest_md5_auth_initial, + mech_digest_md5_auth_continue, + mech_generic_auth_free +}; diff --git a/src/auth/mech-dovecot-token.c b/src/auth/mech-dovecot-token.c new file mode 100644 index 0000000..408ef6e --- /dev/null +++ b/src/auth/mech-dovecot-token.c @@ -0,0 +1,92 @@ +/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */ + +/* Used internally by Dovecot processes to authenticate against each others + (e.g. imap to imap-urlauth). See auth-token.c */ + +#include "auth-common.h" +#include "mech.h" +#include "safe-memset.h" +#include "auth-token.h" + +static void +mech_dovecot_token_auth_continue(struct auth_request *request, + const unsigned char *data, size_t data_size) +{ + const char *session_id, *username, *pid, *service, *error; + char *auth_token; + size_t i, len; + int count; + + /* service \0 pid \0 username \0 session_id \0 auth_token */ + service = (const char *) data; + session_id = username = pid = auth_token = NULL; + count = 0; + for (i = 0; i < data_size; i++) { + if (data[i] == '\0') { + count++; i++; + if (count == 1) + pid = (const char *)data + i; + else if (count == 2) + username = (const char *)data + i; + else if (count == 3) + session_id = (const char *)data + i; + else if (count == 4) { + len = data_size - i; + auth_token = p_strndup(unsafe_data_stack_pool, + data+i, len); + } + else + break; + } + } + + if (count != 4) { + /* invalid input */ + e_info(request->mech_event, "invalid input"); + auth_request_fail(request); + } else if (!auth_request_set_username(request, username, &error)) { + /* invalid username */ + e_info(request->mech_event, "%s", error); + auth_request_fail(request); + } else { + const char *valid_token = + auth_token_get(service, pid, request->fields.user, + session_id); + + if (auth_token != NULL && + str_equals_timing_almost_safe(auth_token, valid_token)) { + request->passdb_success = TRUE; + auth_request_set_field(request, "userdb_client_service", service, ""); + auth_request_success(request, NULL, 0); + } else { + auth_request_fail(request); + } + } + + /* make sure it's cleared */ + if (auth_token != NULL) + safe_memset(auth_token, 0, strlen(auth_token)); +} + +static struct auth_request *mech_dovecot_token_auth_new(void) +{ + struct auth_request *request; + pool_t pool; + + pool = pool_alloconly_create(MEMPOOL_GROWING"dovecot_token_auth_request", 512); + request = p_new(pool, struct auth_request, 1); + request->pool = pool; + return request; +} + +const struct mech_module mech_dovecot_token = { + "DOVECOT-TOKEN", + + .flags = MECH_SEC_PRIVATE | MECH_SEC_ALLOW_NULS, + .passdb_need = MECH_PASSDB_NEED_NOTHING, + + mech_dovecot_token_auth_new, + mech_generic_auth_initial, + mech_dovecot_token_auth_continue, + mech_generic_auth_free +}; diff --git a/src/auth/mech-external.c b/src/auth/mech-external.c new file mode 100644 index 0000000..39809b4 --- /dev/null +++ b/src/auth/mech-external.c @@ -0,0 +1,64 @@ +/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */ + +#include "auth-common.h" +#include "passdb.h" +#include "mech.h" +#include "mech-plain-common.h" + +static void +mech_external_auth_continue(struct auth_request *request, + const unsigned char *data, size_t data_size) +{ + const char *authzid, *error; + + authzid = t_strndup(data, data_size); + if (request->fields.user == NULL) { + e_info(request->mech_event, + "username not known"); + auth_request_fail(request); + return; + } + + /* this call is done simply to put the username through translation + settings */ + if (!auth_request_set_username(request, "", &error)) { + e_info(request->mech_event, + "Invalid username"); + auth_request_fail(request); + return; + } + + if (*authzid != '\0' && + !auth_request_set_login_username(request, authzid, &error)) { + /* invalid login username */ + e_info(request->mech_event, + "login user: %s", error); + auth_request_fail(request); + } else { + auth_request_verify_plain(request, "", + plain_verify_callback); + } +} + +static struct auth_request *mech_external_auth_new(void) +{ + struct auth_request *request; + pool_t pool; + + pool = pool_alloconly_create(MEMPOOL_GROWING"external_auth_request", 2048); + request = p_new(pool, struct auth_request, 1); + request->pool = pool; + return request; +} + +const struct mech_module mech_external = { + "EXTERNAL", + + .flags = 0, + .passdb_need = MECH_PASSDB_NEED_VERIFY_PLAIN, + + mech_external_auth_new, + mech_generic_auth_initial, + mech_external_auth_continue, + mech_generic_auth_free +}; diff --git a/src/auth/mech-gssapi.c b/src/auth/mech-gssapi.c new file mode 100644 index 0000000..e9f2af2 --- /dev/null +++ b/src/auth/mech-gssapi.c @@ -0,0 +1,790 @@ +/* + * GSSAPI Module + * + * Copyright (c) 2005 Jelmer Vernooij <jelmer@samba.org> + * + * Related standards: + * - draft-ietf-sasl-gssapi-03 + * - RFC2222 + * + * Some parts inspired by an older patch from Colin Walters + * + * This software is released under the MIT license. + */ + +#include "auth-common.h" +#include "env-util.h" +#include "str.h" +#include "str-sanitize.h" +#include "hex-binary.h" +#include "safe-memset.h" +#include "mech.h" +#include "passdb.h" + + +#if defined(BUILTIN_GSSAPI) || defined(PLUGIN_BUILD) + +#ifndef HAVE___GSS_USEROK +# define USE_KRB5_USEROK +# include <krb5.h> +#endif + +#ifdef HAVE_GSSAPI_GSSAPI_H +# include <gssapi/gssapi.h> +#elif defined (HAVE_GSSAPI_H) +# include <gssapi.h> +#endif + +#ifdef HAVE_GSSAPI_GSSAPI_KRB5_H +# include <gssapi/gssapi_krb5.h> +#elif defined (HAVE_GSSAPI_KRB5_H) +# include <gssapi_krb5.h> +#else +# undef USE_KRB5_USEROK +#endif + +#ifdef HAVE_GSSAPI_GSSAPI_EXT_H +# include <gssapi/gssapi_ext.h> +#endif + +#define krb5_boolean2bool(X) ((X) != 0) + +/* Non-zero flags defined in RFC 2222 */ +enum sasl_gssapi_qop { + SASL_GSSAPI_QOP_UNSPECIFIED = 0x00, + SASL_GSSAPI_QOP_AUTH_ONLY = 0x01, + SASL_GSSAPI_QOP_AUTH_INT = 0x02, + SASL_GSSAPI_QOP_AUTH_CONF = 0x04 +}; + +struct gssapi_auth_request { + struct auth_request auth_request; + gss_ctx_id_t gss_ctx; + gss_cred_id_t service_cred; + + enum { + GSS_STATE_SEC_CONTEXT, + GSS_STATE_WRAP, + GSS_STATE_UNWRAP + } sasl_gssapi_state; + + gss_name_t authn_name; + gss_name_t authz_name; + + pool_t pool; +}; + +static bool gssapi_initialized = FALSE; + +static gss_OID_desc mech_gssapi_krb5_oid = + { 9, "\x2a\x86\x48\x86\xf7\x12\x01\x02\x02" }; + +static int +mech_gssapi_wrap(struct gssapi_auth_request *request, gss_buffer_desc inbuf); + +static void mech_gssapi_log_error(struct auth_request *request, + OM_uint32 status_value, int status_type, + const char *description) +{ + OM_uint32 message_context = 0; + OM_uint32 minor_status; + gss_buffer_desc status_string; + + do { + (void)gss_display_status(&minor_status, status_value, + status_type, GSS_C_NO_OID, + &message_context, &status_string); + + e_info(request->mech_event, + "While %s: %s", description, + str_sanitize(status_string.value, SIZE_MAX)); + + (void)gss_release_buffer(&minor_status, &status_string); + } while (message_context != 0); +} + +static void mech_gssapi_initialize(const struct auth_settings *set) +{ + const char *path = set->krb5_keytab; + + if (*path != '\0') { + /* environment may be used by Kerberos 5 library directly */ + env_put("KRB5_KTNAME", path); +#ifdef HAVE_GSSKRB5_REGISTER_ACCEPTOR_IDENTITY + gsskrb5_register_acceptor_identity(path); +#elif defined (HAVE_KRB5_GSS_REGISTER_ACCEPTOR_IDENTITY) + krb5_gss_register_acceptor_identity(path); +#endif + } +} + +static struct auth_request *mech_gssapi_auth_new(void) +{ + struct gssapi_auth_request *request; + pool_t pool; + + pool = pool_alloconly_create(MEMPOOL_GROWING"gssapi_auth_request", 2048); + request = p_new(pool, struct gssapi_auth_request, 1); + request->pool = pool; + + request->gss_ctx = GSS_C_NO_CONTEXT; + + request->auth_request.pool = pool; + return &request->auth_request; +} + +static OM_uint32 +obtain_service_credentials(struct auth_request *request, gss_cred_id_t *ret_r) +{ + OM_uint32 major_status, minor_status; + string_t *principal_name; + gss_buffer_desc inbuf; + gss_name_t gss_principal; + const char *service_name; + + if (!gssapi_initialized) { + gssapi_initialized = TRUE; + mech_gssapi_initialize(request->set); + } + + if (strcmp(request->set->gssapi_hostname, "$ALL") == 0) { + e_debug(request->mech_event, + "Using all keytab entries"); + *ret_r = GSS_C_NO_CREDENTIAL; + return GSS_S_COMPLETE; + } + + if (strcasecmp(request->fields.service, "POP3") == 0) { + /* The standard POP3 service name with GSSAPI is called + just "pop". */ + service_name = "pop"; + } else { + service_name = t_str_lcase(request->fields.service); + } + + principal_name = t_str_new(128); + str_append(principal_name, service_name); + str_append_c(principal_name, '@'); + str_append(principal_name, request->set->gssapi_hostname); + + e_debug(request->mech_event, + "Obtaining credentials for %s", str_c(principal_name)); + + inbuf.length = str_len(principal_name); + inbuf.value = str_c_modifiable(principal_name); + + major_status = gss_import_name(&minor_status, &inbuf, + GSS_C_NT_HOSTBASED_SERVICE, + &gss_principal); + str_free(&principal_name); + + if (GSS_ERROR(major_status) != 0) { + mech_gssapi_log_error(request, major_status, GSS_C_GSS_CODE, + "importing principal name"); + return major_status; + } + + major_status = gss_acquire_cred(&minor_status, gss_principal, 0, + GSS_C_NULL_OID_SET, GSS_C_ACCEPT, + ret_r, NULL, NULL); + if (GSS_ERROR(major_status) != 0) { + mech_gssapi_log_error(request, major_status, GSS_C_GSS_CODE, + "acquiring service credentials"); + mech_gssapi_log_error(request, minor_status, GSS_C_MECH_CODE, + "acquiring service credentials"); + return major_status; + } + + gss_release_name(&minor_status, &gss_principal); + return major_status; +} + +static gss_name_t +import_name(struct auth_request *request, void *str, size_t len) +{ + OM_uint32 major_status, minor_status; + gss_buffer_desc name_buf; + gss_name_t name; + + name_buf.value = str; + name_buf.length = len; + major_status = gss_import_name(&minor_status, &name_buf, + GSS_C_NO_OID, &name); + if (GSS_ERROR(major_status) != 0) { + mech_gssapi_log_error(request, major_status, GSS_C_GSS_CODE, + "gss_import_name"); + return GSS_C_NO_NAME; + } + return name; +} + +static gss_name_t +duplicate_name(struct auth_request *request, gss_name_t old) +{ + OM_uint32 major_status, minor_status; + gss_name_t new; + + major_status = gss_duplicate_name(&minor_status, old, &new); + if (GSS_ERROR(major_status) != 0) { + mech_gssapi_log_error(request, major_status, GSS_C_GSS_CODE, + "gss_duplicate_name"); + return GSS_C_NO_NAME; + } + return new; +} + +static bool data_has_nuls(const void *data, size_t len) +{ + const unsigned char *c = data; + size_t i; + + /* apparently all names end with NUL? */ + if (len > 0 && c[len-1] == '\0') + len--; + + for (i = 0; i < len; i++) { + if (c[i] == '\0') + return TRUE; + } + return FALSE; +} + +static int get_display_name(struct auth_request *auth_request, gss_name_t name, + gss_OID *name_type_r, const char **display_name_r) +{ + OM_uint32 major_status, minor_status; + gss_buffer_desc buf; + + major_status = gss_display_name(&minor_status, name, + &buf, name_type_r); + if (major_status != GSS_S_COMPLETE) { + mech_gssapi_log_error(auth_request, major_status, + GSS_C_GSS_CODE, "gss_display_name"); + return -1; + } + if (data_has_nuls(buf.value, buf.length)) { + e_info(auth_request->mech_event, + "authn_name has NULs"); + return -1; + } + *display_name_r = t_strndup(buf.value, buf.length); + (void)gss_release_buffer(&minor_status, &buf); + return 0; +} + +static bool mech_gssapi_oid_cmp(const gss_OID_desc *oid1, + const gss_OID_desc *oid2) +{ + return oid1->length == oid2->length && + mem_equals_timing_safe(oid1->elements, oid2->elements, oid1->length); +} + +static int +mech_gssapi_sec_context(struct gssapi_auth_request *request, + gss_buffer_desc inbuf) +{ + struct auth_request *auth_request = &request->auth_request; + OM_uint32 major_status, minor_status; + gss_buffer_desc output_token; + gss_OID name_type; + gss_OID mech_type; + const char *username, *error; + int ret = 0; + + major_status = gss_accept_sec_context ( + &minor_status, + &request->gss_ctx, + request->service_cred, + &inbuf, + GSS_C_NO_CHANNEL_BINDINGS, + &request->authn_name, + &mech_type, + &output_token, + NULL, /* ret_flags */ + NULL, /* time_rec */ + NULL /* delegated_cred_handle */ + ); + + if (GSS_ERROR(major_status) != 0) { + mech_gssapi_log_error(auth_request, major_status, + GSS_C_GSS_CODE, + "processing incoming data"); + mech_gssapi_log_error(auth_request, minor_status, + GSS_C_MECH_CODE, + "processing incoming data"); + return -1; + } + + switch (major_status) { + case GSS_S_COMPLETE: + if (!mech_gssapi_oid_cmp(mech_type, &mech_gssapi_krb5_oid)) { + e_info(auth_request->mech_event, + "GSSAPI mechanism not Kerberos5"); + ret = -1; + } else if (get_display_name(auth_request, request->authn_name, + &name_type, &username) < 0) + ret = -1; + else if (!auth_request_set_username(auth_request, username, + &error)) { + e_info(auth_request->mech_event, + "authn_name: %s", error); + ret = -1; + } else { + request->sasl_gssapi_state = GSS_STATE_WRAP; + e_debug(auth_request->mech_event, + "security context state completed."); + } + break; + case GSS_S_CONTINUE_NEEDED: + e_debug(auth_request->mech_event, + "Processed incoming packet correctly, " + "waiting for another."); + break; + default: + e_error(auth_request->mech_event, + "Received unexpected major status %d", major_status); + break; + } + + if (ret == 0) { + if (output_token.length > 0) { + auth_request_handler_reply_continue(auth_request, + output_token.value, + output_token.length); + } else { + /* If there is no output token, go straight to wrap, + which is expecting an empty input token. */ + ret = mech_gssapi_wrap(request, output_token); + } + } + (void)gss_release_buffer(&minor_status, &output_token); + return ret; +} + +static int +mech_gssapi_wrap(struct gssapi_auth_request *request, gss_buffer_desc inbuf) +{ + OM_uint32 major_status, minor_status; + gss_buffer_desc outbuf; + unsigned char ret[4]; + + /* The client's return data should be empty here */ + + /* Only authentication, no integrity or confidentiality + protection (yet?) */ + ret[0] = (SASL_GSSAPI_QOP_UNSPECIFIED | + SASL_GSSAPI_QOP_AUTH_ONLY); + ret[1] = 0xFF; + ret[2] = 0xFF; + ret[3] = 0xFF; + + inbuf.length = 4; + inbuf.value = ret; + + major_status = gss_wrap(&minor_status, request->gss_ctx, 0, + GSS_C_QOP_DEFAULT, &inbuf, NULL, &outbuf); + + if (GSS_ERROR(major_status) != 0) { + mech_gssapi_log_error(&request->auth_request, major_status, + GSS_C_GSS_CODE, "sending security layer negotiation"); + mech_gssapi_log_error(&request->auth_request, minor_status, + GSS_C_MECH_CODE, "sending security layer negotiation"); + return -1; + } + + e_debug(request->auth_request.mech_event, + "Negotiated security layer"); + + auth_request_handler_reply_continue(&request->auth_request, + outbuf.value, outbuf.length); + + (void)gss_release_buffer(&minor_status, &outbuf); + request->sasl_gssapi_state = GSS_STATE_UNWRAP; + return 0; +} + +#ifdef USE_KRB5_USEROK +static bool +k5_principal_is_authorized(struct auth_request *request, const char *name) +{ + const char *value, *const *authorized_names, *const *tmp; + + value = auth_fields_find(request->fields.extra_fields, "k5principals"); + if (value == NULL) + return FALSE; + + authorized_names = t_strsplit_spaces(value, ","); + for (tmp = authorized_names; *tmp != NULL; tmp++) { + if (strcmp(*tmp, name) == 0) { + e_debug(request->mech_event, + "authorized by k5principals field: %s", name); + return TRUE; + } + } + return FALSE; +} + +static bool +mech_gssapi_krb5_userok(struct gssapi_auth_request *request, + gss_name_t name, const char *login_user, + bool check_name_type) +{ + krb5_context ctx; + krb5_principal princ; + krb5_error_code krb5_err; + gss_OID name_type; + const char *princ_display_name; + bool authorized = FALSE; + + /* Parse out the principal's username */ + if (get_display_name(&request->auth_request, name, &name_type, + &princ_display_name) < 0) + return FALSE; + + if (!mech_gssapi_oid_cmp(name_type, GSS_KRB5_NT_PRINCIPAL_NAME) && + check_name_type) { + e_info(request->auth_request.mech_event, + "OID not kerberos principal name"); + return FALSE; + } + + /* Init a krb5 context and parse the principal username */ + krb5_err = krb5_init_context(&ctx); + if (krb5_err != 0) { + e_error(request->auth_request.mech_event, + "krb5_init_context() failed: %d", (int)krb5_err); + return FALSE; + } + krb5_err = krb5_parse_name(ctx, princ_display_name, &princ); + if (krb5_err != 0) { + /* writing the error string would be better, but we probably + rarely get here and there doesn't seem to be a standard + way of getting it */ + e_info(request->auth_request.mech_event, + "krb5_parse_name() failed: %d", + (int)krb5_err); + } else { + /* See if the principal is in the list of authorized + * principals for the user */ + authorized = k5_principal_is_authorized(&request->auth_request, + princ_display_name); + + /* See if the principal is authorized to act as the + specified (UNIX) user */ + if (!authorized) { + authorized = krb5_boolean2bool(krb5_kuserok(ctx, princ, login_user)); + } + + krb5_free_principal(ctx, princ); + } + krb5_free_context(ctx); + return authorized; +} +#endif + +static int +mech_gssapi_userok(struct gssapi_auth_request *request, const char *login_user) +{ + struct auth_request *auth_request = &request->auth_request; + OM_uint32 major_status, minor_status; + int equal_authn_authz; +#ifdef HAVE___GSS_USEROK + int login_ok; +#endif + + /* if authn and authz names equal, don't bother checking further. */ + major_status = gss_compare_name(&minor_status, + request->authn_name, + request->authz_name, + &equal_authn_authz); + if (GSS_ERROR(major_status) != 0) { + mech_gssapi_log_error(auth_request, major_status, + GSS_C_GSS_CODE, + "gss_compare_name failed"); + return -1; + } + + if (equal_authn_authz != 0) + return 0; + + /* handle cross-realm authentication */ +#ifdef HAVE___GSS_USEROK + /* Solaris */ + major_status = __gss_userok(&minor_status, request->authn_name, + login_user, &login_ok); + if (GSS_ERROR(major_status) != 0) { + mech_gssapi_log_error(auth_request, major_status, + GSS_C_GSS_CODE, "__gss_userok failed"); + return -1; + } + + if (login_ok == 0) { + e_info(auth_request->mech_event, + "User not authorized to log in as %s", login_user); + return -1; + } + return 0; +#elif defined(USE_KRB5_USEROK) + if (!mech_gssapi_krb5_userok(request, request->authn_name, + login_user, TRUE)) { + e_info(auth_request->mech_event, + "User not authorized to log in as %s", login_user); + return -1; + } + + return 0; +#else + e_info(auth_request->mech_event, + "Cross-realm authentication not supported " + "(authn_name=%s, authz_name=%s)", + request->auth_request.fields.original_username, login_user); + return -1; +#endif +} + +static void +gssapi_credentials_callback(enum passdb_result result, + const unsigned char *credentials ATTR_UNUSED, + size_t size ATTR_UNUSED, + struct auth_request *request) +{ + struct gssapi_auth_request *gssapi_request = + (struct gssapi_auth_request *)request; + + /* We don't care much whether the lookup succeeded or not because GSSAPI + * does not strictly require a passdb. But if a passdb is configured, + * now the k5principals field will have been filled in. */ + switch (result) { + case PASSDB_RESULT_INTERNAL_FAILURE: + auth_request_internal_failure(request); + return; + case PASSDB_RESULT_USER_DISABLED: + case PASSDB_RESULT_PASS_EXPIRED: + /* user is explicitly disabled, don't allow it to log in */ + auth_request_fail(request); + return; + case PASSDB_RESULT_NEXT: + case PASSDB_RESULT_SCHEME_NOT_AVAILABLE: + case PASSDB_RESULT_USER_UNKNOWN: + case PASSDB_RESULT_PASSWORD_MISMATCH: + case PASSDB_RESULT_OK: + break; + } + + if (mech_gssapi_userok(gssapi_request, request->fields.user) == 0) + auth_request_success(request, NULL, 0); + else + auth_request_fail(request); +} + +static int +mech_gssapi_unwrap(struct gssapi_auth_request *request, gss_buffer_desc inbuf) +{ + struct auth_request *auth_request = &request->auth_request; + OM_uint32 major_status, minor_status; + gss_buffer_desc outbuf; + const char *login_user, *error; + unsigned char *name; + size_t name_len; + + major_status = gss_unwrap(&minor_status, request->gss_ctx, + &inbuf, &outbuf, NULL, NULL); + + if (GSS_ERROR(major_status) != 0) { + mech_gssapi_log_error(auth_request, major_status, + GSS_C_GSS_CODE, + "final negotiation: gss_unwrap"); + return -1; + } + + /* outbuf[0] contains bitmask for selected security layer, + outbuf[1..3] contains maximum output_message size */ + if (outbuf.length < 4) { + e_error(auth_request->mech_event, + "Invalid response length"); + return -1; + } + + if (outbuf.length > 4) { + name = (unsigned char *)outbuf.value + 4; + name_len = outbuf.length - 4; + + if (data_has_nuls(name, name_len)) { + e_info(auth_request->mech_event, + "authz_name has NULs"); + return -1; + } + + login_user = p_strndup(auth_request->pool, name, name_len); + request->authz_name = import_name(auth_request, name, name_len); + } else { + request->authz_name = duplicate_name(auth_request, + request->authn_name); + if (get_display_name(auth_request, request->authz_name, + NULL, &login_user) < 0) + return -1; + } + + if (request->authz_name == GSS_C_NO_NAME) { + e_info(auth_request->mech_event, + "no authz_name"); + return -1; + } + + /* Set username early, so that the credential lookup is for the + * authorizing user. This means the username in subsequent log + * messages will be the authorization name, not the authentication + * name, which may mean that future log messages should be adjusted + * to log the right thing. */ + if (!auth_request_set_username(auth_request, login_user, &error)) { + e_info(auth_request->mech_event, + "authz_name: %s", error); + return -1; + } + + /* Continue in callback once auth_request is populated with passdb + information. */ + auth_request->passdb_success = TRUE; /* default to success */ + auth_request_lookup_credentials(&request->auth_request, "", + gssapi_credentials_callback); + return 0; +} + +static void +mech_gssapi_auth_continue(struct auth_request *request, + const unsigned char *data, size_t data_size) +{ + struct gssapi_auth_request *gssapi_request = + (struct gssapi_auth_request *)request; + gss_buffer_desc inbuf; + int ret = -1; + + inbuf.value = (void *)data; + inbuf.length = data_size; + + switch (gssapi_request->sasl_gssapi_state) { + case GSS_STATE_SEC_CONTEXT: + ret = mech_gssapi_sec_context(gssapi_request, inbuf); + break; + case GSS_STATE_WRAP: + ret = mech_gssapi_wrap(gssapi_request, inbuf); + break; + case GSS_STATE_UNWRAP: + ret = mech_gssapi_unwrap(gssapi_request, inbuf); + break; + default: + i_unreached(); + } + if (ret < 0) + auth_request_fail(request); +} + +static void +mech_gssapi_auth_initial(struct auth_request *request, + const unsigned char *data, size_t data_size) +{ + struct gssapi_auth_request *gssapi_request = + (struct gssapi_auth_request *)request; + OM_uint32 major_status; + + major_status = + obtain_service_credentials(request, + &gssapi_request->service_cred); + + if (GSS_ERROR(major_status) != 0) { + auth_request_internal_failure(request); + return; + } + gssapi_request->authn_name = GSS_C_NO_NAME; + gssapi_request->authz_name = GSS_C_NO_NAME; + + gssapi_request->sasl_gssapi_state = GSS_STATE_SEC_CONTEXT; + + if (data_size == 0) { + /* The client should go first */ + auth_request_handler_reply_continue(request, NULL, 0); + } else { + mech_gssapi_auth_continue(request, data, data_size); + } +} + +static void +mech_gssapi_auth_free(struct auth_request *request) +{ + struct gssapi_auth_request *gssapi_request = + (struct gssapi_auth_request *)request; + OM_uint32 minor_status; + + if (gssapi_request->gss_ctx != GSS_C_NO_CONTEXT) { + (void)gss_delete_sec_context(&minor_status, + &gssapi_request->gss_ctx, + GSS_C_NO_BUFFER); + } + + (void)gss_release_cred(&minor_status, &gssapi_request->service_cred); + if (gssapi_request->authn_name != GSS_C_NO_NAME) { + (void)gss_release_name(&minor_status, + &gssapi_request->authn_name); + } + if (gssapi_request->authz_name != GSS_C_NO_NAME) { + (void)gss_release_name(&minor_status, + &gssapi_request->authz_name); + } + pool_unref(&request->pool); +} + +const struct mech_module mech_gssapi = { + "GSSAPI", + + .flags = MECH_SEC_ALLOW_NULS, + .passdb_need = MECH_PASSDB_NEED_NOTHING, + + mech_gssapi_auth_new, + mech_gssapi_auth_initial, + mech_gssapi_auth_continue, + mech_gssapi_auth_free +}; + +/* MTI Kerberos v1.5+ and Heimdal v0.7+ supports SPNEGO for Kerberos tickets + internally. Nothing else needs to be done here. Note however that this does + not support SPNEGO when the only available credential is NTLM.. */ +const struct mech_module mech_gssapi_spnego = { + "GSS-SPNEGO", + + .flags = MECH_SEC_ALLOW_NULS, + .passdb_need = MECH_PASSDB_NEED_NOTHING, + + mech_gssapi_auth_new, + mech_gssapi_auth_initial, + mech_gssapi_auth_continue, + mech_gssapi_auth_free +}; + +#ifndef BUILTIN_GSSAPI +void mech_gssapi_init(void); +void mech_gssapi_deinit(void); + +void mech_gssapi_init(void) +{ + mech_register_module(&mech_gssapi); +#ifdef HAVE_GSSAPI_SPNEGO + /* load if we already didn't load it using winbind */ + if (mech_module_find(mech_gssapi_spnego.mech_name) == NULL) + mech_register_module(&mech_gssapi_spnego); +#endif +} + +void mech_gssapi_deinit(void) +{ +#ifdef HAVE_GSSAPI_SPNEGO + const struct mech_module *mech; + + mech = mech_module_find(mech_gssapi_spnego.mech_name); + if (mech != NULL && mech->auth_new == mech_gssapi_auth_new) + mech_unregister_module(&mech_gssapi_spnego); +#endif + mech_unregister_module(&mech_gssapi); +} +#endif + +#endif diff --git a/src/auth/mech-login.c b/src/auth/mech-login.c new file mode 100644 index 0000000..6f27433 --- /dev/null +++ b/src/auth/mech-login.c @@ -0,0 +1,76 @@ +/* + * LOGIN authentication mechanism. + * + * Copyright (c) 2004 Andrey Panin <pazke@donpac.ru> + * + * This software is released under the MIT license. + */ + +#include "auth-common.h" +#include "mech.h" +#include "passdb.h" +#include "safe-memset.h" +#include "mech-plain-common.h" + + +static void +mech_login_auth_continue(struct auth_request *request, + const unsigned char *data, size_t data_size) +{ + static const char prompt2[] = "Password:"; + const char *username, *error; + + if (request->fields.user == NULL) { + username = t_strndup(data, data_size); + + if (!auth_request_set_username(request, username, &error)) { + e_info(request->mech_event, "%s", error); + auth_request_fail(request); + return; + } + + auth_request_handler_reply_continue(request, prompt2, + strlen(prompt2)); + } else { + char *pass = p_strndup(unsafe_data_stack_pool, data, data_size); + auth_request_verify_plain(request, pass, plain_verify_callback); + safe_memset(pass, 0, strlen(pass)); + } +} + +static void +mech_login_auth_initial(struct auth_request *request, + const unsigned char *data, size_t data_size) +{ + static const char prompt1[] = "Username:"; + + if (data_size == 0) { + auth_request_handler_reply_continue(request, prompt1, + strlen(prompt1)); + } else { + mech_login_auth_continue(request, data, data_size); + } +} + +static struct auth_request *mech_login_auth_new(void) +{ + struct auth_request *request; + pool_t pool; + + pool = pool_alloconly_create(MEMPOOL_GROWING"login_auth_request", 2048); + request = p_new(pool, struct auth_request, 1); + request->pool = pool; + return request; +} + +const struct mech_module mech_login = { + "LOGIN", + + .flags = MECH_SEC_PLAINTEXT, + .passdb_need = MECH_PASSDB_NEED_VERIFY_PLAIN, + + mech_login_auth_new, + mech_login_auth_initial, + mech_login_auth_continue, + mech_generic_auth_free +}; diff --git a/src/auth/mech-oauth2.c b/src/auth/mech-oauth2.c new file mode 100644 index 0000000..1b14567 --- /dev/null +++ b/src/auth/mech-oauth2.c @@ -0,0 +1,315 @@ +/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */ + +#include "auth-common.h" +#include "safe-memset.h" +#include "str.h" +#include "mech.h" +#include "passdb.h" +#include "oauth2.h" +#include "json-parser.h" +#include <ctype.h> + +struct oauth2_auth_request { + struct auth_request auth; + bool failed; +}; + +/* RFC5801 based unescaping */ +static bool oauth2_unescape_username(const char *in, const char **username_r) +{ + string_t *out; + out = t_str_new(64); + for (; *in != '\0'; in++) { + if (in[0] == ',') + return FALSE; + if (in[0] == '=') { + if (in[1] == '2' && in[2] == 'C') + str_append_c(out, ','); + else if (in[1] == '3' && in[2] == 'D') + str_append_c(out, '='); + else + return FALSE; + in += 2; + } else { + str_append_c(out, *in); + } + } + *username_r = str_c(out); + return TRUE; +} + +static void oauth2_verify_callback(enum passdb_result result, + const char *const *error_fields, + struct auth_request *request) +{ + struct oauth2_auth_request *oauth2_req = + (struct oauth2_auth_request*)request; + + i_assert(result == PASSDB_RESULT_OK || error_fields != NULL); + switch (result) { + case PASSDB_RESULT_OK: + auth_request_success(request, "", 0); + break; + case PASSDB_RESULT_INTERNAL_FAILURE: + auth_request_internal_failure(request); + break; + default: + /* we could get new token after this */ + if (request->mech_password != NULL) + request->mech_password = NULL; + string_t *error = t_str_new(64); + str_append_c(error, '{'); + for (unsigned int i = 0; error_fields[i] != NULL; i += 2) { + i_assert(error_fields[i+1] != NULL); + if (i > 0) + str_append_c(error, ','); + str_append_c(error, '"'); + json_append_escaped(error, error_fields[i]); + str_append(error, "\":\""); + json_append_escaped(error, error_fields[i+1]); + str_append_c(error, '"'); + } + /* FIXME: HORRIBLE HACK - REMOVE ME!!! + It is because the mech has not been implemented properly + that we need to pass the config url in this strange way. + + This **must** be removed from here and db-oauth2 once the + validation result et al is handled here. + */ + if (request->openid_config_url != NULL) { + if (str_len(error) > 0) + str_append_c(error, ','); + str_printfa(error, "\"openid-configuration\":\""); + json_append_escaped(error, request->openid_config_url); + str_append_c(error, '"'); + } + str_append_c(error, '}'); + auth_request_handler_reply_continue(request, str_data(error), + str_len(error)); + oauth2_req->failed = TRUE; + break; + } +} + +static void +xoauth2_verify_callback(enum passdb_result result, struct auth_request *request) +{ + const char *const error_fields[] = { + "status", "401", + "schemes", "bearer", + "scope", "mail", + NULL + }; + oauth2_verify_callback(result, error_fields, request); +} + +static void +oauthbearer_verify_callback(enum passdb_result result, struct auth_request *request) +{ + const char *error_fields[] = { + "status", "invalid_token", + NULL + }; + oauth2_verify_callback(result, error_fields, request); +} + +/* Input syntax: + user=Username^Aauth=Bearer token^A^A +*/ +static void +mech_xoauth2_auth_continue(struct auth_request *request, + const unsigned char *data, + size_t data_size) +{ + struct oauth2_auth_request *oauth2_req = + (struct oauth2_auth_request*)request; + + /* Specification says that client is sent "invalid token" challenge + which the client is supposed to ack with empty response */ + if (oauth2_req->failed) { + auth_request_fail(request); + return; + } + + /* split the data from ^A */ + bool user_given = FALSE; + const char *error; + const char *token = NULL; + const char *const *ptr; + const char *username; + const char *const *fields = + t_strsplit(t_strndup(data, data_size), "\x01"); + for(ptr = fields; *ptr != NULL; ptr++) { + if (str_begins(*ptr, "user=")) { + /* xoauth2 does not require unescaping because the data + format does not contain anything to escape */ + username = (*ptr)+5; + user_given = TRUE; + } else if (str_begins(*ptr, "auth=")) { + const char *value = (*ptr)+5; + if (strncasecmp(value, "bearer ", 7) == 0 && + oauth2_valid_token(value+7)) { + token = value+7; + } else { + e_info(request->mech_event, + "Invalid continued data"); + auth_request_fail(request); + return; + } + } + /* do not fail on unexpected fields */ + } + + if (user_given && !auth_request_set_username(request, username, &error)) { + e_info(request->mech_event, + "%s", error); + auth_request_fail(request); + return; + } + + if (user_given && token != NULL) + auth_request_verify_plain(request, token, + xoauth2_verify_callback); + else { + e_info(request->mech_event, "Username or token missing"); + auth_request_fail(request); + } +} + +/* Input syntax for data: + gs2flag,a=username,^Afield=...^Afield=...^Aauth=Bearer token^A^A +*/ +static void +mech_oauthbearer_auth_continue(struct auth_request *request, + const unsigned char *data, + size_t data_size) +{ + struct oauth2_auth_request *oauth2_req = + (struct oauth2_auth_request*)request; + + if (oauth2_req->failed) { + auth_request_fail(request); + return; + } + + bool user_given = FALSE; + const char *error; + const char *username; + const char *const *ptr; + /* split the data from ^A */ + const char **fields = + t_strsplit(t_strndup(data, data_size), "\x01"); + const char *token = NULL; + /* ensure initial field is OK */ + if (*fields == NULL || *(fields[0]) == '\0') { + e_info(request->mech_event, + "Invalid continued data"); + auth_request_fail(request); + return; + } + + /* the first field is specified by RFC5801 as gs2-header */ + for(ptr = t_strsplit_spaces(fields[0], ","); *ptr != NULL; ptr++) { + switch(*ptr[0]) { + case 'f': + e_info(request->mech_event, + "Client requested non-standard mechanism"); + auth_request_fail(request); + return; + case 'p': + /* channel binding is not supported */ + e_info(request->mech_event, + "Client requested and used channel-binding"); + auth_request_fail(request); + return; + case 'n': + case 'y': + /* we don't need to use channel-binding */ + continue; + case 'a': /* authzid */ + if ((*ptr)[1] != '=' || + !oauth2_unescape_username((*ptr)+2, &username)) { + e_info(request->mech_event, + "Invalid username escaping"); + auth_request_fail(request); + return; + } else { + user_given = TRUE; + } + break; + default: + e_info(request->mech_event, + "Invalid gs2-header in request"); + auth_request_fail(request); + return; + } + } + + for(ptr = fields; *ptr != NULL; ptr++) { + if (str_begins(*ptr, "auth=")) { + const char *value = (*ptr)+5; + if (strncasecmp(value, "bearer ", 7) == 0 && + oauth2_valid_token(value+7)) { + token = value+7; + } else { + e_info(request->mech_event, + "Invalid continued data"); + auth_request_fail(request); + return; + } + } + /* do not fail on unexpected fields */ + } + if (user_given && !auth_request_set_username(request, username, &error)) { + e_info(request->mech_event, + "%s", error); + auth_request_fail(request); + return; + } + if (user_given && token != NULL) + auth_request_verify_plain(request, token, + oauthbearer_verify_callback); + else { + e_info(request->mech_event, "Missing username or token"); + auth_request_fail(request); + } +} + +static struct auth_request *mech_oauth2_auth_new(void) +{ + struct oauth2_auth_request *request; + pool_t pool; + + pool = pool_alloconly_create(MEMPOOL_GROWING"oauth2_auth_request", 2048); + request = p_new(pool, struct oauth2_auth_request, 1); + request->auth.pool = pool; + return &request->auth; +} + +const struct mech_module mech_oauthbearer = { + "OAUTHBEARER", + + /* while this does not transfer plaintext password, + the token is still considered as password */ + .flags = MECH_SEC_PLAINTEXT, + .passdb_need = 0, + + mech_oauth2_auth_new, + mech_generic_auth_initial, + mech_oauthbearer_auth_continue, + mech_generic_auth_free +}; + +const struct mech_module mech_xoauth2 = { + "XOAUTH2", + + .flags = MECH_SEC_PLAINTEXT, + .passdb_need = 0, + + mech_oauth2_auth_new, + mech_generic_auth_initial, + mech_xoauth2_auth_continue, + mech_generic_auth_free +}; + + diff --git a/src/auth/mech-otp-common.c b/src/auth/mech-otp-common.c new file mode 100644 index 0000000..753fcbb --- /dev/null +++ b/src/auth/mech-otp-common.c @@ -0,0 +1,71 @@ +/* + * Common code for OTP authentication mechanisms. + * + * Copyright (c) 2006 Andrey Panin <pazke@donpac.ru> + * + * This software is released under the MIT license. + */ + +#include "auth-common.h" +#include "hash.h" +#include "mech.h" + +#include "otp.h" +#include "mech-otp-common.h" + +static HASH_TABLE(char *, struct auth_request *) otp_lock_table; + +void otp_lock_init(void) +{ + if (hash_table_is_created(otp_lock_table)) + return; + + hash_table_create(&otp_lock_table, default_pool, 128, + strcase_hash, strcasecmp); +} + +bool otp_try_lock(struct auth_request *auth_request) +{ + if (hash_table_lookup(otp_lock_table, auth_request->fields.user) != NULL) + return FALSE; + + hash_table_insert(otp_lock_table, auth_request->fields.user, auth_request); + return TRUE; +} + +void otp_unlock(struct auth_request *auth_request) +{ + struct otp_auth_request *request = + (struct otp_auth_request *)auth_request; + + if (!request->lock) + return; + + hash_table_remove(otp_lock_table, auth_request->fields.user); + request->lock = FALSE; +} + +void otp_set_credentials_callback(bool success, + struct auth_request *auth_request) +{ + if (success) + auth_request_success(auth_request, "", 0); + else { + auth_request_internal_failure(auth_request); + otp_unlock(auth_request); + } + + otp_unlock(auth_request); +} + +void mech_otp_auth_free(struct auth_request *auth_request) +{ + otp_unlock(auth_request); + + pool_unref(&auth_request->pool); +} + +void mech_otp_deinit(void) +{ + hash_table_destroy(&otp_lock_table); +} diff --git a/src/auth/mech-otp-common.h b/src/auth/mech-otp-common.h new file mode 100644 index 0000000..37a6551 --- /dev/null +++ b/src/auth/mech-otp-common.h @@ -0,0 +1,23 @@ +#ifndef MECH_OTP_COMMON_H +#define MECH_OTP_COMMON_H + +struct otp_auth_request { + struct auth_request auth_request; + + pool_t pool; + + bool lock; + + struct otp_state state; +}; + +void otp_lock_init(void); +bool otp_try_lock(struct auth_request *auth_request); +void otp_unlock(struct auth_request *auth_request); + +void otp_set_credentials_callback(bool success, + struct auth_request *auth_request); +void mech_otp_auth_free(struct auth_request *auth_request); +void mech_otp_deinit(void); + +#endif diff --git a/src/auth/mech-otp.c b/src/auth/mech-otp.c new file mode 100644 index 0000000..58f3db1 --- /dev/null +++ b/src/auth/mech-otp.c @@ -0,0 +1,240 @@ +/* + * One-Time-Password (RFC 2444) authentication mechanism. + * + * Copyright (c) 2006 Andrey Panin <pazke@donpac.ru> + * + * This software is released under the MIT license. + */ + +#include "auth-common.h" +#include "safe-memset.h" +#include "hash.h" +#include "mech.h" +#include "passdb.h" +#include "hex-binary.h" +#include "otp.h" +#include "mech-otp-common.h" + +static void +otp_send_challenge(struct auth_request *auth_request, + const unsigned char *credentials, size_t size) +{ + struct otp_auth_request *request = + (struct otp_auth_request *)auth_request; + const char *answer; + + if (otp_parse_dbentry(t_strndup(credentials, size), + &request->state) != 0) { + e_error(request->auth_request.mech_event, + "invalid OTP data in passdb"); + auth_request_fail(auth_request); + return; + } + + if (--request->state.seq < 1) { + e_error(request->auth_request.mech_event, + "sequence number < 1"); + auth_request_fail(auth_request); + return; + } + + request->lock = otp_try_lock(auth_request); + if (!request->lock) { + e_error(request->auth_request.mech_event, + "user is locked, race attack?"); + auth_request_fail(auth_request); + return; + } + + answer = p_strdup_printf(request->pool, "otp-%s %u %s ext", + digest_name(request->state.algo), + request->state.seq, request->state.seed); + + auth_request_handler_reply_continue(auth_request, answer, + strlen(answer)); +} + +static void +otp_credentials_callback(enum passdb_result result, + const unsigned char *credentials, size_t size, + struct auth_request *auth_request) +{ + switch (result) { + case PASSDB_RESULT_OK: + otp_send_challenge(auth_request, credentials, size); + break; + case PASSDB_RESULT_INTERNAL_FAILURE: + auth_request_internal_failure(auth_request); + break; + default: + auth_request_fail(auth_request); + break; + } +} + +static void +mech_otp_auth_phase1(struct auth_request *auth_request, + const unsigned char *data, size_t data_size) +{ + struct otp_auth_request *request = + (struct otp_auth_request *)auth_request; + const char *authenid, *error; + size_t i, count; + + /* authorization ID \0 authentication ID + FIXME: we'll ignore authorization ID for now. */ + authenid = NULL; + + count = 0; + for (i = 0; i < data_size; i++) { + if (data[i] == '\0') { + if (++count == 1) + authenid = (const char *) data + i + 1; + } + } + + if (count != 1) { + e_error(request->auth_request.mech_event, + "invalid input"); + auth_request_fail(auth_request); + return; + } + + if (!auth_request_set_username(auth_request, authenid, &error)) { + e_info(auth_request->mech_event, "%s", error); + auth_request_fail(auth_request); + return; + } + + auth_request_lookup_credentials(auth_request, "OTP", + otp_credentials_callback); +} + +static void mech_otp_verify(struct auth_request *auth_request, + const char *data, bool hex) +{ + struct otp_auth_request *request = + (struct otp_auth_request *)auth_request; + struct otp_state *state = &request->state; + unsigned char hash[OTP_HASH_SIZE], cur_hash[OTP_HASH_SIZE]; + int ret; + + ret = otp_parse_response(data, hash, hex); + if (ret < 0) { + e_error(request->auth_request.mech_event, + "invalid response"); + auth_request_fail(auth_request); + otp_unlock(auth_request); + return; + } + + otp_next_hash(state->algo, hash, cur_hash); + + ret = memcmp(cur_hash, state->hash, OTP_HASH_SIZE); + if (ret != 0) { + auth_request_fail(auth_request); + otp_unlock(auth_request); + return; + } + + memcpy(state->hash, hash, sizeof(state->hash)); + + auth_request_set_credentials(auth_request, "OTP", + otp_print_dbentry(state), + otp_set_credentials_callback); +} + +static void mech_otp_verify_init(struct auth_request *auth_request, + const char *data, bool hex) +{ + struct otp_auth_request *request = + (struct otp_auth_request *)auth_request; + struct otp_state new_state; + unsigned char hash[OTP_HASH_SIZE], cur_hash[OTP_HASH_SIZE]; + const char *error; + int ret; + + ret = otp_parse_init_response(data, &new_state, cur_hash, hex, &error); + if (ret < 0) { + e_error(request->auth_request.mech_event, + "invalid init response, %s", error); + auth_request_fail(auth_request); + otp_unlock(auth_request); + return; + } + + otp_next_hash(request->state.algo, cur_hash, hash); + + ret = memcmp(hash, request->state.hash, OTP_HASH_SIZE); + if (ret != 0) { + auth_request_fail(auth_request); + otp_unlock(auth_request); + return; + } + + auth_request_set_credentials(auth_request, "OTP", + otp_print_dbentry(&new_state), + otp_set_credentials_callback); +} + +static void +mech_otp_auth_phase2(struct auth_request *auth_request, + const unsigned char *data, size_t data_size) +{ + const char *str = t_strndup(data, data_size); + + if (str_begins(str, "hex:")) { + mech_otp_verify(auth_request, str + 4, TRUE); + } else if (str_begins(str, "word:")) { + mech_otp_verify(auth_request, str + 5, FALSE); + } else if (str_begins(str, "init-hex:")) { + mech_otp_verify_init(auth_request, str + 9, TRUE); + } else if (str_begins(str, "init-word:")) { + mech_otp_verify_init(auth_request, str + 10, FALSE); + } else { + e_error(auth_request->mech_event, + "unsupported response type"); + auth_request_fail(auth_request); + otp_unlock(auth_request); + } +} + +static void +mech_otp_auth_continue(struct auth_request *auth_request, + const unsigned char *data, size_t data_size) +{ + if (auth_request->fields.user == NULL) { + mech_otp_auth_phase1(auth_request, data, data_size); + } else { + mech_otp_auth_phase2(auth_request, data, data_size); + } +} + +static struct auth_request *mech_otp_auth_new(void) +{ + struct otp_auth_request *request; + pool_t pool; + + otp_lock_init(); + + pool = pool_alloconly_create(MEMPOOL_GROWING"otp_auth_request", 2048); + request = p_new(pool, struct otp_auth_request, 1); + request->pool = pool; + request->lock = FALSE; + + request->auth_request.refcount = 1; + request->auth_request.pool = pool; + return &request->auth_request; +} + +const struct mech_module mech_otp = { + "OTP", + + .flags = MECH_SEC_DICTIONARY | MECH_SEC_ACTIVE | MECH_SEC_ALLOW_NULS, + .passdb_need = MECH_PASSDB_NEED_SET_CREDENTIALS, + + mech_otp_auth_new, + mech_generic_auth_initial, + mech_otp_auth_continue, + mech_otp_auth_free +}; diff --git a/src/auth/mech-plain-common.c b/src/auth/mech-plain-common.c new file mode 100644 index 0000000..2d947b9 --- /dev/null +++ b/src/auth/mech-plain-common.c @@ -0,0 +1,22 @@ +/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */ + +#include "auth-common.h" +#include "mech.h" +#include "passdb.h" +#include "mech-plain-common.h" + +void plain_verify_callback(enum passdb_result result, + struct auth_request *request) +{ + switch (result) { + case PASSDB_RESULT_OK: + auth_request_success(request, "", 0); + break; + case PASSDB_RESULT_INTERNAL_FAILURE: + auth_request_internal_failure(request); + break; + default: + auth_request_fail(request); + break; + } +} diff --git a/src/auth/mech-plain-common.h b/src/auth/mech-plain-common.h new file mode 100644 index 0000000..1dc339c --- /dev/null +++ b/src/auth/mech-plain-common.h @@ -0,0 +1,7 @@ +#ifndef MECH_PLAIN_COMMON_H +#define MECH_PLAIN_COMMON_H + +void plain_verify_callback(enum passdb_result result, + struct auth_request *request); + +#endif diff --git a/src/auth/mech-plain.c b/src/auth/mech-plain.c new file mode 100644 index 0000000..3bb715f --- /dev/null +++ b/src/auth/mech-plain.c @@ -0,0 +1,88 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +#include "auth-common.h" +#include "safe-memset.h" +#include "mech.h" +#include "passdb.h" +#include "mech-plain-common.h" + +static void +mech_plain_auth_continue(struct auth_request *request, + const unsigned char *data, size_t data_size) +{ + const char *authid, *authenid, *error; + char *pass; + size_t i, len; + int count; + + /* authorization ID \0 authentication ID \0 pass. */ + authid = (const char *) data; + authenid = NULL; pass = NULL; + + count = 0; + for (i = 0; i < data_size; i++) { + if (data[i] == '\0') { + if (++count == 1) + authenid = (const char *) data + i+1; + else if (count == 2) { + i++; + len = data_size - i; + pass = p_strndup(unsafe_data_stack_pool, + data+i, len); + } + else + break; + } + } + + if (count == 2 && authenid != NULL && strcmp(authid, authenid) == 0) { + /* the login username isn't different */ + authid = ""; + } + + if (count != 2) { + /* invalid input */ + e_info(request->mech_event, "invalid input"); + auth_request_fail(request); + } else if (!auth_request_set_username(request, authenid, &error)) { + /* invalid username */ + e_info(request->mech_event, "%s", error); + auth_request_fail(request); + } else if (*authid != '\0' && + !auth_request_set_login_username(request, authid, &error)) { + /* invalid login username */ + e_info(request->mech_event, + "login user: %s", error); + auth_request_fail(request); + } else { + auth_request_verify_plain(request, pass, + plain_verify_callback); + } + + /* make sure it's cleared */ + if (pass != NULL) + safe_memset(pass, 0, strlen(pass)); +} + +static struct auth_request *mech_plain_auth_new(void) +{ + struct auth_request *request; + pool_t pool; + + pool = pool_alloconly_create(MEMPOOL_GROWING"plain_auth_request", 2048); + request = p_new(pool, struct auth_request, 1); + request->pool = pool; + return request; +} + +const struct mech_module mech_plain = { + "PLAIN", + + .flags = MECH_SEC_PLAINTEXT | MECH_SEC_ALLOW_NULS, + .passdb_need = MECH_PASSDB_NEED_VERIFY_PLAIN, + + mech_plain_auth_new, + mech_generic_auth_initial, + mech_plain_auth_continue, + mech_generic_auth_free +}; diff --git a/src/auth/mech-scram.c b/src/auth/mech-scram.c new file mode 100644 index 0000000..a90d0d1 --- /dev/null +++ b/src/auth/mech-scram.c @@ -0,0 +1,550 @@ +/* + * SCRAM-SHA-1 SASL authentication, see RFC-5802 + * + * Copyright (c) 2011-2016 Florian Zeitz <florob@babelmonkeys.de> + * + * This software is released under the MIT license. + */ + +#include <limits.h> + +#include "auth-common.h" +#include "base64.h" +#include "buffer.h" +#include "hmac.h" +#include "sha1.h" +#include "sha2.h" +#include "randgen.h" +#include "safe-memset.h" +#include "str.h" +#include "strfuncs.h" +#include "strnum.h" +#include "password-scheme.h" +#include "mech.h" +#include "mech-scram.h" + +/* s-nonce length */ +#define SCRAM_SERVER_NONCE_LEN 64 + +struct scram_auth_request { + struct auth_request auth_request; + + pool_t pool; + + const struct hash_method *hash_method; + const char *password_scheme; + + /* sent: */ + const char *server_first_message; + const char *snonce; + + /* received: */ + const char *gs2_header; + const char *cnonce; + const char *client_first_message_bare; + const char *client_final_message_without_proof; + buffer_t *proof; + + /* stored */ + unsigned char *stored_key; + unsigned char *server_key; +}; + +static const char * +get_scram_server_first(struct scram_auth_request *request, + int iter, const char *salt) +{ + unsigned char snonce[SCRAM_SERVER_NONCE_LEN+1]; + string_t *str; + size_t i; + + /* RFC 5802, Section 7: + + server-first-message = + [reserved-mext ","] nonce "," salt "," + iteration-count ["," extensions] + + nonce = "r=" c-nonce [s-nonce] + + salt = "s=" base64 + + iteration-count = "i=" posit-number + ;; A positive number. + */ + + random_fill(snonce, sizeof(snonce)-1); + + /* make sure snonce is printable and does not contain ',' */ + for (i = 0; i < sizeof(snonce)-1; i++) { + snonce[i] = (snonce[i] % ('~' - '!')) + '!'; + if (snonce[i] == ',') + snonce[i] = '~'; + } + snonce[sizeof(snonce)-1] = '\0'; + request->snonce = p_strndup(request->pool, snonce, sizeof(snonce)); + + str = t_str_new(32 + strlen(request->cnonce) + sizeof(snonce) + + strlen(salt)); + str_printfa(str, "r=%s%s,s=%s,i=%d", request->cnonce, request->snonce, + salt, iter); + return str_c(str); +} + +static const char *get_scram_server_final(struct scram_auth_request *request) +{ + const struct hash_method *hmethod = request->hash_method; + struct hmac_context ctx; + const char *auth_message; + unsigned char server_signature[hmethod->digest_size]; + string_t *str; + + /* RFC 5802, Section 3: + + AuthMessage := client-first-message-bare + "," + + server-first-message + "," + + client-final-message-without-proof + ServerSignature := HMAC(ServerKey, AuthMessage) + */ + auth_message = t_strconcat(request->client_first_message_bare, ",", + request->server_first_message, ",", + request->client_final_message_without_proof, NULL); + + hmac_init(&ctx, request->server_key, hmethod->digest_size, hmethod); + hmac_update(&ctx, auth_message, strlen(auth_message)); + hmac_final(&ctx, server_signature); + + /* RFC 5802, Section 7: + + server-final-message = (server-error / verifier) + ["," extensions] + + verifier = "v=" base64 + ;; base-64 encoded ServerSignature. + + */ + str = t_str_new(2 + MAX_BASE64_ENCODED_SIZE(sizeof(server_signature))); + str_append(str, "v="); + base64_encode(server_signature, sizeof(server_signature), str); + + return str_c(str); +} + +static const char *scram_unescape_username(const char *in) +{ + string_t *out; + + /* RFC 5802, Section 5.1: + + The characters ',' or '=' in usernames are sent as '=2C' and '=3D' + respectively. If the server receives a username that contains '=' + not followed by either '2C' or '3D', then the server MUST fail the + authentication. + */ + + out = t_str_new(64); + for (; *in != '\0'; in++) { + i_assert(in[0] != ','); /* strsplit should have caught this */ + + if (in[0] == '=') { + if (in[1] == '2' && in[2] == 'C') + str_append_c(out, ','); + else if (in[1] == '3' && in[2] == 'D') + str_append_c(out, '='); + else + return NULL; + in += 2; + } else { + str_append_c(out, *in); + } + } + return str_c(out); +} + +static bool +parse_scram_client_first(struct scram_auth_request *request, + const unsigned char *data, size_t size, + const char **error_r) +{ + const char *login_username = NULL; + const char *data_cstr, *p; + const char *gs2_header, *gs2_cbind_flag, *authzid; + const char *cfm_bare, *username, *nonce; + const char *const *fields; + + data_cstr = gs2_header = t_strndup(data, size); + + /* RFC 5802, Section 7: + + client-first-message = gs2-header client-first-message-bare + gs2-header = gs2-cbind-flag "," [ authzid ] "," + + client-first-message-bare = [reserved-mext ","] + username "," nonce ["," extensions] + + extensions = attr-val *("," attr-val) + ;; All extensions are optional, + ;; i.e., unrecognized attributes + ;; not defined in this document + ;; MUST be ignored. + attr-val = ALPHA "=" value + */ + p = strchr(data_cstr, ','); + if (p == NULL) { + *error_r = "Invalid initial client message: " + "Missing first ',' in GS2 header"; + return FALSE; + } + gs2_cbind_flag = t_strdup_until(data_cstr, p); + data_cstr = p + 1; + + p = strchr(data_cstr, ','); + if (p == NULL) { + *error_r = "Invalid initial client message: " + "Missing second ',' in GS2 header"; + return FALSE; + } + authzid = t_strdup_until(data_cstr, p); + gs2_header = t_strdup_until(gs2_header, p + 1); + cfm_bare = p + 1; + + fields = t_strsplit(cfm_bare, ","); + if (str_array_length(fields) < 2) { + *error_r = "Invalid initial client message: " + "Missing nonce field"; + return FALSE; + } + username = fields[0]; + nonce = fields[1]; + + /* gs2-cbind-flag = ("p=" cb-name) / "n" / "y" + */ + switch (gs2_cbind_flag[0]) { + case 'p': + *error_r = "Channel binding not supported"; + return FALSE; + case 'y': + case 'n': + break; + default: + *error_r = "Invalid GS2 header"; + return FALSE; + } + + /* authzid = "a=" saslname + ;; Protocol specific. + */ + if (authzid[0] == '\0') + ; + else if (authzid[0] == 'a' && authzid[1] == '=') { + /* Unescape authzid */ + login_username = scram_unescape_username(authzid + 2); + + if (login_username == NULL) { + *error_r = "authzid escaping is invalid"; + return FALSE; + } + } else { + *error_r = "Invalid authzid field"; + return FALSE; + } + + /* reserved-mext = "m=" 1*(value-char) + */ + if (username[0] == 'm') { + *error_r = "Mandatory extension(s) not supported"; + return FALSE; + } + /* username = "n=" saslname + */ + if (username[0] == 'n' && username[1] == '=') { + /* Unescape username */ + username = scram_unescape_username(username + 2); + if (username == NULL) { + *error_r = "Username escaping is invalid"; + return FALSE; + } + if (!auth_request_set_username(&request->auth_request, + username, error_r)) + return FALSE; + } else { + *error_r = "Invalid username field"; + return FALSE; + } + if (login_username != NULL) { + if (!auth_request_set_login_username(&request->auth_request, + login_username, error_r)) + return FALSE; + } + + /* nonce = "r=" c-nonce [s-nonce] */ + if (nonce[0] == 'r' && nonce[1] == '=') + request->cnonce = p_strdup(request->pool, nonce+2); + else { + *error_r = "Invalid client nonce"; + return FALSE; + } + + request->gs2_header = p_strdup(request->pool, gs2_header); + request->client_first_message_bare = p_strdup(request->pool, cfm_bare); + return TRUE; +} + +static bool verify_credentials(struct scram_auth_request *request) +{ + const struct hash_method *hmethod = request->hash_method; + struct hmac_context ctx; + const char *auth_message; + unsigned char client_key[hmethod->digest_size]; + unsigned char client_signature[hmethod->digest_size]; + unsigned char stored_key[hmethod->digest_size]; + size_t i; + + /* RFC 5802, Section 3: + + AuthMessage := client-first-message-bare + "," + + server-first-message + "," + + client-final-message-without-proof + ClientSignature := HMAC(StoredKey, AuthMessage) + */ + auth_message = t_strconcat(request->client_first_message_bare, ",", + request->server_first_message, ",", + request->client_final_message_without_proof, NULL); + + hmac_init(&ctx, request->stored_key, hmethod->digest_size, hmethod); + hmac_update(&ctx, auth_message, strlen(auth_message)); + hmac_final(&ctx, client_signature); + + /* ClientProof := ClientKey XOR ClientSignature */ + const unsigned char *proof_data = request->proof->data; + for (i = 0; i < sizeof(client_signature); i++) + client_key[i] = proof_data[i] ^ client_signature[i]; + + /* StoredKey := H(ClientKey) */ + hash_method_get_digest(hmethod, client_key, sizeof(client_key), + stored_key); + + safe_memset(client_key, 0, sizeof(client_key)); + safe_memset(client_signature, 0, sizeof(client_signature)); + + return mem_equals_timing_safe(stored_key, request->stored_key, + sizeof(stored_key)); +} + +static void +credentials_callback(enum passdb_result result, + const unsigned char *credentials, size_t size, + struct auth_request *auth_request) +{ + struct scram_auth_request *request = + (struct scram_auth_request *)auth_request; + const char *salt, *error; + unsigned int iter_count; + + switch (result) { + case PASSDB_RESULT_OK: + if (scram_scheme_parse(request->hash_method, + request->password_scheme, + credentials, size, &iter_count, &salt, + request->stored_key, request->server_key, + &error) < 0) { + e_info(auth_request->mech_event, + "%s", error); + auth_request_fail(auth_request); + break; + } + + request->server_first_message = p_strdup(request->pool, + get_scram_server_first(request, iter_count, salt)); + + auth_request_handler_reply_continue(auth_request, + request->server_first_message, + strlen(request->server_first_message)); + break; + case PASSDB_RESULT_INTERNAL_FAILURE: + auth_request_internal_failure(auth_request); + break; + default: + auth_request_fail(auth_request); + break; + } +} + +static bool +parse_scram_client_final(struct scram_auth_request *request, + const unsigned char *data, size_t size, + const char **error_r) +{ + const struct hash_method *hmethod = request->hash_method; + const char **fields, *cbind_input, *nonce_str; + unsigned int field_count; + string_t *str; + + /* RFC 5802, Section 7: + + client-final-message-without-proof = + channel-binding "," nonce ["," + extensions] + client-final-message = + client-final-message-without-proof "," proof + */ + fields = t_strsplit(t_strndup(data, size), ","); + field_count = str_array_length(fields); + if (field_count < 3) { + *error_r = "Invalid final client message"; + return FALSE; + } + + /* channel-binding = "c=" base64 + ;; base64 encoding of cbind-input. + + cbind-data = 1*OCTET + cbind-input = gs2-header [ cbind-data ] + ;; cbind-data MUST be present for + ;; gs2-cbind-flag of "p" and MUST be absent + ;; for "y" or "n". + */ + cbind_input = request->gs2_header; + str = t_str_new(2 + MAX_BASE64_ENCODED_SIZE(strlen(cbind_input))); + str_append(str, "c="); + base64_encode(cbind_input, strlen(cbind_input), str); + + if (strcmp(fields[0], str_c(str)) != 0) { + *error_r = "Invalid channel binding data"; + return FALSE; + } + + /* nonce = "r=" c-nonce [s-nonce] + ;; Second part provided by server. + c-nonce = printable + s-nonce = printable + */ + nonce_str = t_strconcat("r=", request->cnonce, request->snonce, NULL); + if (strcmp(fields[1], nonce_str) != 0) { + *error_r = "Wrong nonce"; + return FALSE; + } + + /* proof = "p=" base64 + */ + if (fields[field_count-1][0] == 'p') { + size_t len = strlen(&fields[field_count-1][2]); + + request->proof = buffer_create_dynamic(request->pool, + MAX_BASE64_DECODED_SIZE(len)); + if (base64_decode(&fields[field_count-1][2], len, NULL, + request->proof) < 0) { + *error_r = "Invalid base64 encoding"; + return FALSE; + } + if (request->proof->used != hmethod->digest_size) { + *error_r = "Invalid ClientProof length"; + return FALSE; + } + } else { + *error_r = "Invalid ClientProof"; + return FALSE; + } + + (void)str_array_remove(fields, fields[field_count-1]); + request->client_final_message_without_proof = + p_strdup(request->pool, t_strarray_join(fields, ",")); + + return TRUE; +} + +void mech_scram_auth_continue(struct auth_request *auth_request, + const unsigned char *data, size_t data_size) +{ + struct scram_auth_request *request = + (struct scram_auth_request *)auth_request; + const char *error = NULL; + const char *server_final_message; + size_t len; + + if (request->client_first_message_bare == NULL) { + /* Received client-first-message */ + if (parse_scram_client_first(request, data, + data_size, &error)) { + auth_request_lookup_credentials( + &request->auth_request, + request->password_scheme, + credentials_callback); + return; + } + } else { + /* Received client-final-message */ + if (parse_scram_client_final(request, data, data_size, + &error)) { + if (!verify_credentials(request)) { + e_info(auth_request->mech_event, + AUTH_LOG_MSG_PASSWORD_MISMATCH); + } else { + server_final_message = + get_scram_server_final(request); + len = strlen(server_final_message); + auth_request_success(auth_request, + server_final_message, len); + return; + } + } + } + + if (error != NULL) + e_info(auth_request->mech_event, "%s", error); + auth_request_fail(auth_request); +} + +struct auth_request * +mech_scram_auth_new(const struct hash_method *hash_method, + const char *password_scheme) +{ + struct scram_auth_request *request; + pool_t pool; + + pool = pool_alloconly_create(MEMPOOL_GROWING"scram_auth_request", 2048); + request = p_new(pool, struct scram_auth_request, 1); + request->pool = pool; + + request->hash_method = hash_method; + request->password_scheme = password_scheme; + + request->stored_key = p_malloc(pool, hash_method->digest_size); + request->server_key = p_malloc(pool, hash_method->digest_size); + + request->auth_request.pool = pool; + return &request->auth_request; +} + +static struct auth_request *mech_scram_sha1_auth_new(void) +{ + return mech_scram_auth_new(&hash_method_sha1, "SCRAM-SHA-1"); +} + +static struct auth_request *mech_scram_sha256_auth_new(void) +{ + return mech_scram_auth_new(&hash_method_sha256, "SCRAM-SHA-256"); +} + +const struct mech_module mech_scram_sha1 = { + "SCRAM-SHA-1", + + .flags = MECH_SEC_MUTUAL_AUTH, + .passdb_need = MECH_PASSDB_NEED_LOOKUP_CREDENTIALS, + + mech_scram_sha1_auth_new, + mech_generic_auth_initial, + mech_scram_auth_continue, + mech_generic_auth_free +}; + +const struct mech_module mech_scram_sha256 = { + "SCRAM-SHA-256", + + .flags = MECH_SEC_MUTUAL_AUTH, + .passdb_need = MECH_PASSDB_NEED_LOOKUP_CREDENTIALS, + + mech_scram_sha256_auth_new, + mech_generic_auth_initial, + mech_scram_auth_continue, + mech_generic_auth_free +}; diff --git a/src/auth/mech-scram.h b/src/auth/mech-scram.h new file mode 100644 index 0000000..0275aa7 --- /dev/null +++ b/src/auth/mech-scram.h @@ -0,0 +1,10 @@ +#ifndef MECH_SCRAM_H +#define MECH_SCRAM_H + +struct auth_request * +mech_scram_auth_new(const struct hash_method *hash_method, + const char *password_scheme); +void mech_scram_auth_continue(struct auth_request *auth_request, + const unsigned char *data, size_t data_size); + +#endif diff --git a/src/auth/mech-winbind.c b/src/auth/mech-winbind.c new file mode 100644 index 0000000..d2525db --- /dev/null +++ b/src/auth/mech-winbind.c @@ -0,0 +1,364 @@ +/* + * NTLM and Negotiate authentication mechanisms, + * using Samba winbind daemon + * + * Copyright (c) 2007 Dmitry Butskoy <dmitry@butskoy.name> + * + * This software is released under the MIT license. + */ + +#include "auth-common.h" +#include "lib-signals.h" +#include "mech.h" +#include "str.h" +#include "buffer.h" +#include "base64.h" +#include "execv-const.h" +#include "istream.h" +#include "ostream.h" + +#include <unistd.h> +#include <sys/wait.h> + +#define DEFAULT_WINBIND_HELPER_PATH "/usr/bin/ntlm_auth" + +enum helper_result { + HR_OK = 0, /* OK or continue */ + HR_FAIL = -1, /* authentication failed */ + HR_RESTART = -2 /* FAIL + try to restart helper */ +}; + +struct winbind_helper { + const char *param; + pid_t pid; + + struct istream *in_pipe; + struct ostream *out_pipe; +}; + +struct winbind_auth_request { + struct auth_request auth_request; + + struct winbind_helper *winbind; + bool continued; +}; + +static struct winbind_helper winbind_ntlm_context = { + "--helper-protocol=squid-2.5-ntlmssp", -1, NULL, NULL +}; +static struct winbind_helper winbind_spnego_context = { + "--helper-protocol=gss-spnego", -1, NULL, NULL +}; + +static bool sigchld_handler_set = FALSE; + +static void winbind_helper_disconnect(struct winbind_helper *winbind) +{ + i_stream_destroy(&winbind->in_pipe); + o_stream_destroy(&winbind->out_pipe); +} + +static void winbind_wait_pid(struct winbind_helper *winbind) +{ + int status, ret; + + if (winbind->pid == -1) + return; + + /* FIXME: use child-wait.h API */ + if ((ret = waitpid(winbind->pid, &status, WNOHANG)) <= 0) { + if (ret < 0 && errno != ECHILD && errno != EINTR) + i_error("waitpid() failed: %m"); + return; + } + + if (WIFSIGNALED(status)) { + i_error("winbind: ntlm_auth died with signal %d", + WTERMSIG(status)); + } else if (WIFEXITED(status)) { + i_error("winbind: ntlm_auth exited with exit code %d", + WEXITSTATUS(status)); + } else { + /* shouldn't happen */ + i_error("winbind: ntlm_auth exited with status %d", + status); + } + winbind->pid = -1; +} + +static void sigchld_handler(const siginfo_t *si ATTR_UNUSED, + void *context ATTR_UNUSED) +{ + winbind_wait_pid(&winbind_ntlm_context); + winbind_wait_pid(&winbind_spnego_context); +} + +static void +winbind_helper_connect(const struct auth_settings *set, + struct winbind_helper *winbind, + struct event *event) +{ + int infd[2], outfd[2]; + pid_t pid; + + if (winbind->in_pipe != NULL || winbind->pid != -1) + return; + + if (pipe(infd) < 0) { + e_error(event, "pipe() failed: %m"); + return; + } + if (pipe(outfd) < 0) { + i_close_fd(&infd[0]); i_close_fd(&infd[1]); + return; + } + + pid = fork(); + if (pid < 0) { + e_error(event, "fork() failed: %m"); + i_close_fd(&infd[0]); i_close_fd(&infd[1]); + i_close_fd(&outfd[0]); i_close_fd(&outfd[1]); + return; + } + + if (pid == 0) { + /* child */ + const char *args[3]; + + i_close_fd(&infd[0]); + i_close_fd(&outfd[1]); + + if (dup2(outfd[0], STDIN_FILENO) < 0 || + dup2(infd[1], STDOUT_FILENO) < 0) + i_fatal("dup2() failed: %m"); + + args[0] = set->winbind_helper_path; + args[1] = winbind->param; + args[2] = NULL; + execv_const(args[0], args); + } + + /* parent */ + i_close_fd(&infd[1]); + i_close_fd(&outfd[0]); + + winbind->pid = pid; + winbind->in_pipe = + i_stream_create_fd_autoclose(&infd[0], AUTH_CLIENT_MAX_LINE_LENGTH); + winbind->out_pipe = + o_stream_create_fd_autoclose(&outfd[1], SIZE_MAX); + + if (!sigchld_handler_set) { + sigchld_handler_set = TRUE; + lib_signals_set_handler(SIGCHLD, LIBSIG_FLAGS_SAFE, + sigchld_handler, NULL); + } +} + +static enum helper_result +do_auth_continue(struct auth_request *auth_request, + const unsigned char *data, size_t data_size) +{ + struct winbind_auth_request *request = + (struct winbind_auth_request *)auth_request; + struct istream *in_pipe = request->winbind->in_pipe; + string_t *str; + char *answer; + const char **token; + bool gss_spnego = request->winbind == &winbind_spnego_context; + + if (request->winbind->in_pipe == NULL) + return HR_RESTART; + + str = t_str_new(MAX_BASE64_ENCODED_SIZE(data_size + 1) + 4); + str_printfa(str, "%s ", request->continued ? "KK" : "YR"); + base64_encode(data, data_size, str); + str_append_c(str, '\n'); + + if (o_stream_send(request->winbind->out_pipe, + str_data(str), str_len(str)) < 0 || + o_stream_flush(request->winbind->out_pipe) < 0) { + e_error(auth_request->mech_event, + "write(out_pipe) failed: %s", + o_stream_get_error(request->winbind->out_pipe)); + return HR_RESTART; + } + request->continued = FALSE; + + while ((answer = i_stream_read_next_line(in_pipe)) == NULL) { + if (in_pipe->stream_errno != 0 || in_pipe->eof) + break; + } + if (answer == NULL) { + if (in_pipe->stream_errno != 0) { + e_error(auth_request->mech_event, + "read(in_pipe) failed: %m"); + } else { + e_error(auth_request->mech_event, + "read(in_pipe) failed: " + "unexpected end of file"); + } + return HR_RESTART; + } + + token = t_strsplit_spaces(answer, " "); + if (token[0] == NULL || + (token[1] == NULL && strcmp(token[0], "BH") != 0) || + (gss_spnego && (token[1] == NULL || token[2] == NULL))) { + e_error(auth_request->mech_event, + "Invalid input from helper: %s", answer); + return HR_RESTART; + } + + /* + * NTLM: + * The child's reply contains 2 parts: + * - The code: TT, AF or NA + * - The argument: + * For TT it's the blob to send to the client, coded in base64 + * For AF it's user or DOMAIN\user + * For NA it's the NT error code + * + * GSS-SPNEGO: + * The child's reply contains 3 parts: + * - The code: TT, AF or NA + * - The blob to send to the client, coded in base64 + * - The argument: + * For TT it's a dummy '*' + * For AF it's DOMAIN\user + * For NA it's the NT error code + */ + + if (strcmp(token[0], "TT") == 0) { + buffer_t *buf; + + i_assert(token[1] != NULL); + buf = t_base64_decode_str(token[1]); + auth_request_handler_reply_continue(auth_request, buf->data, + buf->used); + request->continued = TRUE; + return HR_OK; + } else if (strcmp(token[0], "NA") == 0) { + const char *error = gss_spnego ? token[2] : token[1]; + + e_info(auth_request->mech_event, + "user not authenticated: %s", error); + return HR_FAIL; + } else if (strcmp(token[0], "AF") == 0) { + const char *user, *p, *error; + + user = t_strarray_join(gss_spnego ? token+2 : token+1, " "); + i_assert(user != NULL); + + p = strchr(user, '\\'); + if (p != NULL) { + /* change "DOMAIN\user" to uniform style + "user@DOMAIN" */ + user = t_strconcat(p+1, "@", + t_strdup_until(user, p), NULL); + } + + if (!auth_request_set_username(auth_request, user, &error)) { + e_info(auth_request->mech_event, + "%s", error); + return HR_FAIL; + } + + request->auth_request.passdb_success = TRUE; + if (gss_spnego && strcmp(token[1], "*") != 0) { + buffer_t *buf; + + buf = t_base64_decode_str(token[1]); + auth_request_success(&request->auth_request, + buf->data, buf->used); + } else { + auth_request_success(&request->auth_request, "", 0); + } + return HR_OK; + } else if (strcmp(token[0], "BH") == 0) { + e_info(auth_request->mech_event, + "ntlm_auth reports broken helper: %s", + token[1] != NULL ? token[1] : ""); + return HR_RESTART; + } else { + e_error(auth_request->mech_event, + "Invalid input from helper: %s", answer); + return HR_RESTART; + } +} + +static void +mech_winbind_auth_initial(struct auth_request *auth_request, + const unsigned char *data, size_t data_size) +{ + struct winbind_auth_request *request = + (struct winbind_auth_request *)auth_request; + + winbind_helper_connect(auth_request->set, request->winbind, + auth_request->event); + mech_generic_auth_initial(auth_request, data, data_size); +} + +static void +mech_winbind_auth_continue(struct auth_request *auth_request, + const unsigned char *data, size_t data_size) +{ + struct winbind_auth_request *request = + (struct winbind_auth_request *)auth_request; + enum helper_result res; + + res = do_auth_continue(auth_request, data, data_size); + if (res != HR_OK) { + if (res == HR_RESTART) + winbind_helper_disconnect(request->winbind); + auth_request_fail(auth_request); + } +} + +static struct auth_request *do_auth_new(struct winbind_helper *winbind) +{ + struct winbind_auth_request *request; + pool_t pool; + + pool = pool_alloconly_create(MEMPOOL_GROWING"winbind_auth_request", 2048); + request = p_new(pool, struct winbind_auth_request, 1); + request->auth_request.pool = pool; + + request->winbind = winbind; + return &request->auth_request; +} + +static struct auth_request *mech_winbind_ntlm_auth_new(void) +{ + return do_auth_new(&winbind_ntlm_context); +} + +static struct auth_request *mech_winbind_spnego_auth_new(void) +{ + return do_auth_new(&winbind_spnego_context); +} + +const struct mech_module mech_winbind_ntlm = { + "NTLM", + + .flags = MECH_SEC_DICTIONARY | MECH_SEC_ACTIVE | + MECH_SEC_ALLOW_NULS, + .passdb_need = MECH_PASSDB_NEED_NOTHING, + + mech_winbind_ntlm_auth_new, + mech_winbind_auth_initial, + mech_winbind_auth_continue, + mech_generic_auth_free +}; + +const struct mech_module mech_winbind_spnego = { + "GSS-SPNEGO", + + .flags = 0, + .passdb_need = MECH_PASSDB_NEED_NOTHING, + + mech_winbind_spnego_auth_new, + mech_winbind_auth_initial, + mech_winbind_auth_continue, + mech_generic_auth_free +}; diff --git a/src/auth/mech.c b/src/auth/mech.c new file mode 100644 index 0000000..477f27b --- /dev/null +++ b/src/auth/mech.c @@ -0,0 +1,245 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +#include "auth-common.h" +#include "ioloop.h" +#include "mech.h" +#include "str.h" +#include "strfuncs.h" +#include "passdb.h" + +#include <ctype.h> + +static struct mech_module_list *mech_modules; + +void mech_register_module(const struct mech_module *module) +{ + struct mech_module_list *list; + i_assert(strcmp(module->mech_name, t_str_ucase(module->mech_name)) == 0); + + list = i_new(struct mech_module_list, 1); + list->module = *module; + + list->next = mech_modules; + mech_modules = list; +} + +void mech_unregister_module(const struct mech_module *module) +{ + struct mech_module_list **pos, *list; + + for (pos = &mech_modules; *pos != NULL; pos = &(*pos)->next) { + if (strcmp((*pos)->module.mech_name, module->mech_name) == 0) { + list = *pos; + *pos = (*pos)->next; + i_free(list); + break; + } + } +} + +const struct mech_module *mech_module_find(const char *name) +{ + struct mech_module_list *list; + name = t_str_ucase(name); + + for (list = mech_modules; list != NULL; list = list->next) { + if (strcmp(list->module.mech_name, name) == 0) + return &list->module; + } + return NULL; +} + +void mech_generic_auth_initial(struct auth_request *request, + const unsigned char *data, size_t data_size) +{ + if (data == NULL) { + auth_request_handler_reply_continue(request, uchar_empty_ptr, 0); + } else { + /* initial reply given, even if it was 0 bytes */ + request->mech->auth_continue(request, data, data_size); + } +} + +void mech_generic_auth_free(struct auth_request *request) +{ + pool_unref(&request->pool); +} + +extern const struct mech_module mech_plain; +extern const struct mech_module mech_login; +extern const struct mech_module mech_apop; +extern const struct mech_module mech_cram_md5; +extern const struct mech_module mech_digest_md5; +extern const struct mech_module mech_external; +extern const struct mech_module mech_otp; +extern const struct mech_module mech_scram_sha1; +extern const struct mech_module mech_scram_sha256; +extern const struct mech_module mech_anonymous; +#ifdef HAVE_GSSAPI +extern const struct mech_module mech_gssapi; +#endif +#ifdef HAVE_GSSAPI_SPNEGO +extern const struct mech_module mech_gssapi_spnego; +#endif +extern const struct mech_module mech_winbind_ntlm; +extern const struct mech_module mech_winbind_spnego; +extern const struct mech_module mech_oauthbearer; +extern const struct mech_module mech_xoauth2; + +static void mech_register_add(struct mechanisms_register *reg, + const struct mech_module *mech) +{ + struct mech_module_list *list; + + list = p_new(reg->pool, struct mech_module_list, 1); + list->module = *mech; + + str_printfa(reg->handshake, "MECH\t%s", mech->mech_name); + if ((mech->flags & MECH_SEC_PRIVATE) != 0) + str_append(reg->handshake, "\tprivate"); + if ((mech->flags & MECH_SEC_ANONYMOUS) != 0) + str_append(reg->handshake, "\tanonymous"); + if ((mech->flags & MECH_SEC_PLAINTEXT) != 0) + str_append(reg->handshake, "\tplaintext"); + if ((mech->flags & MECH_SEC_DICTIONARY) != 0) + str_append(reg->handshake, "\tdictionary"); + if ((mech->flags & MECH_SEC_ACTIVE) != 0) + str_append(reg->handshake, "\tactive"); + if ((mech->flags & MECH_SEC_FORWARD_SECRECY) != 0) + str_append(reg->handshake, "\tforward-secrecy"); + if ((mech->flags & MECH_SEC_MUTUAL_AUTH) != 0) + str_append(reg->handshake, "\tmutual-auth"); + str_append_c(reg->handshake, '\n'); + + list->next = reg->modules; + reg->modules = list; +} + +static const char *mech_get_plugin_name(const char *name) +{ + string_t *str = t_str_new(32); + + str_append(str, "mech_"); + for (; *name != '\0'; name++) { + if (*name == '-') + str_append_c(str, '_'); + else + str_append_c(str, i_tolower(*name)); + } + return str_c(str); +} + +struct mechanisms_register * +mech_register_init(const struct auth_settings *set) +{ + struct mechanisms_register *reg; + const struct mech_module *mech; + const char *const *mechanisms; + pool_t pool; + + pool = pool_alloconly_create("mechanisms register", 1024); + reg = p_new(pool, struct mechanisms_register, 1); + reg->pool = pool; + reg->set = set; + reg->handshake = str_new(pool, 512); + + mechanisms = t_strsplit_spaces(set->mechanisms, " "); + for (; *mechanisms != NULL; mechanisms++) { + const char *name = t_str_ucase(*mechanisms); + + if (strcmp(name, "ANONYMOUS") == 0) { + if (*set->anonymous_username == '\0') { + i_fatal("ANONYMOUS listed in mechanisms, " + "but anonymous_username not set"); + } + } + mech = mech_module_find(name); + if (mech == NULL) { + /* maybe it's a plugin. try to load it. */ + auth_module_load(mech_get_plugin_name(name)); + mech = mech_module_find(name); + } + if (mech == NULL) + i_fatal("Unknown authentication mechanism '%s'", name); + mech_register_add(reg, mech); + } + + if (reg->modules == NULL) + i_fatal("No authentication mechanisms configured"); + return reg; +} + +void mech_register_deinit(struct mechanisms_register **_reg) +{ + struct mechanisms_register *reg = *_reg; + + *_reg = NULL; + pool_unref(®->pool); +} + +const struct mech_module * +mech_register_find(const struct mechanisms_register *reg, const char *name) +{ + const struct mech_module_list *list; + name = t_str_ucase(name); + + for (list = reg->modules; list != NULL; list = list->next) { + if (strcmp(list->module.mech_name, name) == 0) + return &list->module; + } + return NULL; +} + +void mech_init(const struct auth_settings *set) +{ + mech_register_module(&mech_plain); + mech_register_module(&mech_login); + mech_register_module(&mech_apop); + mech_register_module(&mech_cram_md5); + mech_register_module(&mech_digest_md5); + mech_register_module(&mech_external); + if (set->use_winbind) { + mech_register_module(&mech_winbind_ntlm); + mech_register_module(&mech_winbind_spnego); + } else { +#if defined(HAVE_GSSAPI_SPNEGO) && defined(BUILTIN_GSSAPI) + mech_register_module(&mech_gssapi_spnego); +#endif + } + mech_register_module(&mech_otp); + mech_register_module(&mech_scram_sha1); + mech_register_module(&mech_scram_sha256); + mech_register_module(&mech_anonymous); +#ifdef BUILTIN_GSSAPI + mech_register_module(&mech_gssapi); +#endif + mech_register_module(&mech_oauthbearer); + mech_register_module(&mech_xoauth2); +} + +void mech_deinit(const struct auth_settings *set) +{ + mech_unregister_module(&mech_plain); + mech_unregister_module(&mech_login); + mech_unregister_module(&mech_apop); + mech_unregister_module(&mech_cram_md5); + mech_unregister_module(&mech_digest_md5); + mech_unregister_module(&mech_external); + if (set->use_winbind) { + mech_unregister_module(&mech_winbind_ntlm); + mech_unregister_module(&mech_winbind_spnego); + } else { +#if defined(HAVE_GSSAPI_SPNEGO) && defined(BUILTIN_GSSAPI) + mech_unregister_module(&mech_gssapi_spnego); +#endif + } + mech_unregister_module(&mech_otp); + mech_unregister_module(&mech_scram_sha1); + mech_unregister_module(&mech_scram_sha256); + mech_unregister_module(&mech_anonymous); +#ifdef BUILTIN_GSSAPI + mech_unregister_module(&mech_gssapi); +#endif + mech_unregister_module(&mech_oauthbearer); + mech_unregister_module(&mech_xoauth2); +} diff --git a/src/auth/mech.h b/src/auth/mech.h new file mode 100644 index 0000000..4a9f593 --- /dev/null +++ b/src/auth/mech.h @@ -0,0 +1,77 @@ +#ifndef MECH_H +#define MECH_H + +#include "auth-client-interface.h" + +struct auth_settings; +struct auth_request; + +#include "auth-request.h" +#include "auth-request-handler.h" + +/* Used only for string sanitization. */ +#define MAX_MECH_NAME_LEN 64 + +enum mech_passdb_need { + /* Mechanism doesn't need a passdb at all */ + MECH_PASSDB_NEED_NOTHING = 0, + /* Mechanism just needs to verify a given plaintext password */ + MECH_PASSDB_NEED_VERIFY_PLAIN, + /* Mechanism needs to verify a given challenge+response combination, + i.e. there is only a single response from client. + (Currently implemented the same as _LOOKUP_CREDENTIALS) */ + MECH_PASSDB_NEED_VERIFY_RESPONSE, + /* Mechanism needs to look up credentials with appropriate scheme */ + MECH_PASSDB_NEED_LOOKUP_CREDENTIALS, + /* Mechanism needs to look up credentials and also modify them */ + MECH_PASSDB_NEED_SET_CREDENTIALS +}; + +struct mech_module { + const char *mech_name; + + enum mech_security_flags flags; + enum mech_passdb_need passdb_need; + + struct auth_request *(*auth_new)(void); + void (*auth_initial)(struct auth_request *request, + const unsigned char *data, size_t data_size); + void (*auth_continue)(struct auth_request *request, + const unsigned char *data, size_t data_size); + void (*auth_free)(struct auth_request *request); +}; + +struct mech_module_list { + struct mech_module_list *next; + + struct mech_module module; +}; + +struct mechanisms_register { + pool_t pool; + const struct auth_settings *set; + + struct mech_module_list *modules; + buffer_t *handshake; +}; + +extern const struct mech_module mech_dovecot_token; + +void mech_register_module(const struct mech_module *module); +void mech_unregister_module(const struct mech_module *module); +const struct mech_module *mech_module_find(const char *name); + +void mech_generic_auth_initial(struct auth_request *request, + const unsigned char *data, size_t data_size); +void mech_generic_auth_free(struct auth_request *request); + +struct mechanisms_register * +mech_register_init(const struct auth_settings *set); +void mech_register_deinit(struct mechanisms_register **reg); +const struct mech_module * +mech_register_find(const struct mechanisms_register *reg, const char *name); + +void mech_init(const struct auth_settings *set); +void mech_deinit(const struct auth_settings *set); + +#endif diff --git a/src/auth/mycrypt.c b/src/auth/mycrypt.c new file mode 100644 index 0000000..0bd00bc --- /dev/null +++ b/src/auth/mycrypt.c @@ -0,0 +1,26 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#define _XOPEN_SOURCE 4 +#define _XOPEN_SOURCE_EXTENDED 1 /* 1 needed for AIX */ +#ifndef _AIX +# define _XOPEN_VERSION 4 /* breaks AIX */ +#endif +#define _XPG4_2 +#ifdef CRYPT_USE_XPG6 +# define _XPG6 /* Some Solaris versions require this, some break with this */ +#endif +#include <unistd.h> +#ifdef HAVE_CRYPT_H +# include <crypt.h> +#endif + +#include "mycrypt.h" + +char *mycrypt(const char *key, const char *salt) +{ + return crypt(key, salt); +} diff --git a/src/auth/mycrypt.h b/src/auth/mycrypt.h new file mode 100644 index 0000000..550adfc --- /dev/null +++ b/src/auth/mycrypt.h @@ -0,0 +1,8 @@ +#ifndef MYCRYPT_H +#define MYCRYPT_H + +/* A simple wrapper to crypt(). Problem with it is that it requires + _XOPEN_SOURCE define which breaks other things. */ +char *mycrypt(const char *key, const char *salt); + +#endif diff --git a/src/auth/passdb-blocking.c b/src/auth/passdb-blocking.c new file mode 100644 index 0000000..6b7030c --- /dev/null +++ b/src/auth/passdb-blocking.c @@ -0,0 +1,173 @@ +/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */ + +#include "auth-common.h" +#include "str.h" +#include "strescape.h" +#include "auth-worker-server.h" +#include "password-scheme.h" +#include "passdb.h" +#include "passdb-blocking.h" + + +static void +auth_worker_reply_parse_args(struct auth_request *request, + const char *const *args) +{ + if (**args != '\0') + request->passdb_password = p_strdup(request->pool, *args); + args++; + + if (*args != NULL) + auth_request_set_fields(request, args, NULL); +} + +enum passdb_result +passdb_blocking_auth_worker_reply_parse(struct auth_request *request, const char *reply) +{ + enum passdb_result ret; + const char *const *args; + + args = t_strsplit_tabescaped(reply); + + if (strcmp(*args, "OK") == 0 && args[1] != NULL && args[2] != NULL) { + /* OK \t user \t password [\t extra] */ + if (args[1][0] != '\0') + auth_request_set_field(request, "user", args[1], NULL); + auth_worker_reply_parse_args(request, args + 2); + return PASSDB_RESULT_OK; + } + + if (strcmp(*args, "NEXT") == 0 && args[1] != NULL) { + /* NEXT \t user [\t extra] */ + if (args[1][0] != '\0') + auth_request_set_field(request, "user", args[1], NULL); + auth_worker_reply_parse_args(request, args + 1); + return PASSDB_RESULT_NEXT; + } + + if (strcmp(*args, "FAIL") == 0 && args[1] != NULL) { + int result; + /* FAIL \t result [\t user \t password [\t extra]] */ + if (str_to_int(args[1], &result) < 0) { + /* shouldn't happen */ + } else { + ret = (enum passdb_result)result; + if (ret == PASSDB_RESULT_OK) { + /* shouldn't happen */ + } else if (args[2] == NULL) { + /* internal failure most likely */ + return ret; + } else if (args[3] != NULL) { + if (*args[2] != '\0') { + auth_request_set_field(request, "user", + args[2], NULL); + } + auth_worker_reply_parse_args(request, args + 3); + return ret; + } + } + } + + e_error(authdb_event(request), + "Received invalid reply from worker: %s", reply); + return PASSDB_RESULT_INTERNAL_FAILURE; +} + +static bool +verify_plain_callback(struct auth_worker_connection *conn ATTR_UNUSED, + const char *reply, void *context) +{ + struct auth_request *request = context; + enum passdb_result result; + + result = passdb_blocking_auth_worker_reply_parse(request, reply); + auth_request_verify_plain_callback(result, request); + auth_request_unref(&request); + return TRUE; +} + +void passdb_blocking_verify_plain(struct auth_request *request) +{ + string_t *str; + + str = t_str_new(128); + str_printfa(str, "PASSV\t%u\t", request->passdb->passdb->id); + str_append_tabescaped(str, request->mech_password); + str_append_c(str, '\t'); + auth_request_export(request, str); + + auth_request_ref(request); + auth_worker_call(request->pool, request->fields.user, str_c(str), + verify_plain_callback, request); +} + +static bool +lookup_credentials_callback(struct auth_worker_connection *conn ATTR_UNUSED, + const char *reply, void *context) +{ + struct auth_request *request = context; + enum passdb_result result; + const char *password = NULL, *scheme = NULL; + + result = passdb_blocking_auth_worker_reply_parse(request, reply); + if (result == PASSDB_RESULT_OK && request->passdb_password != NULL) { + password = request->passdb_password; + scheme = password_get_scheme(&password); + if (scheme == NULL) { + e_error(authdb_event(request), + "Received reply from worker without " + "password scheme"); + result = PASSDB_RESULT_INTERNAL_FAILURE; + } + } + + passdb_handle_credentials(result, password, scheme, + auth_request_lookup_credentials_callback, + request); + auth_request_unref(&request); + return TRUE; +} + +void passdb_blocking_lookup_credentials(struct auth_request *request) +{ + string_t *str; + + str = t_str_new(128); + str_printfa(str, "PASSL\t%u\t", request->passdb->passdb->id); + str_append_tabescaped(str, request->wanted_credentials_scheme); + str_append_c(str, '\t'); + auth_request_export(request, str); + + auth_request_ref(request); + auth_worker_call(request->pool, request->fields.user, str_c(str), + lookup_credentials_callback, request); +} + +static bool +set_credentials_callback(struct auth_worker_connection *conn ATTR_UNUSED, + const char *reply, void *context) +{ + struct auth_request *request = context; + bool success; + + success = strcmp(reply, "OK") == 0 || str_begins(reply, "OK\t"); + request->private_callback.set_credentials(success, request); + auth_request_unref(&request); + return TRUE; +} + +void passdb_blocking_set_credentials(struct auth_request *request, + const char *new_credentials) +{ + string_t *str; + + str = t_str_new(128); + str_printfa(str, "SETCRED\t%u\t", request->passdb->passdb->id); + str_append_tabescaped(str, new_credentials); + str_append_c(str, '\t'); + auth_request_export(request, str); + + auth_request_ref(request); + auth_worker_call(request->pool, request->fields.user, str_c(str), + set_credentials_callback, request); +} diff --git a/src/auth/passdb-blocking.h b/src/auth/passdb-blocking.h new file mode 100644 index 0000000..3963998 --- /dev/null +++ b/src/auth/passdb-blocking.h @@ -0,0 +1,11 @@ +#ifndef PASSDB_BLOCKING_H +#define PASSDB_BLOCKING_H + +enum passdb_result +passdb_blocking_auth_worker_reply_parse(struct auth_request *request, const char *reply); +void passdb_blocking_verify_plain(struct auth_request *request); +void passdb_blocking_lookup_credentials(struct auth_request *request); +void passdb_blocking_set_credentials(struct auth_request *request, + const char *new_credentials); + +#endif diff --git a/src/auth/passdb-bsdauth.c b/src/auth/passdb-bsdauth.c new file mode 100644 index 0000000..36469c6 --- /dev/null +++ b/src/auth/passdb-bsdauth.c @@ -0,0 +1,97 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +#include "auth-common.h" +#include "passdb.h" + +#ifdef PASSDB_BSDAUTH + +#include "safe-memset.h" +#include "auth-cache.h" +#include "ipwd.h" +#include "mycrypt.h" + +#include <login_cap.h> +#include <bsd_auth.h> + +static void +bsdauth_verify_plain(struct auth_request *request, const char *password, + verify_plain_callback_t *callback) +{ + struct passwd pw; + const char *type; + int result; + + e_debug(authdb_event(request), "lookup"); + + switch (i_getpwnam(request->fields.user, &pw)) { + case -1: + e_error(authdb_event(request), + "getpwnam() failed: %m"); + callback(PASSDB_RESULT_INTERNAL_FAILURE, request); + return; + case 0: + auth_request_log_unknown_user(request, AUTH_SUBSYS_DB); + callback(PASSDB_RESULT_USER_UNKNOWN, request); + return; + } + + /* check if the password is valid */ + type = t_strdup_printf("auth-%s", request->fields.service); + result = auth_userokay(request->fields.user, NULL, + t_strdup_noconst(type), + t_strdup_noconst(password)); + + /* clear the passwords from memory */ + safe_memset(pw.pw_passwd, 0, strlen(pw.pw_passwd)); + + if (result == 0) { + auth_request_log_password_mismatch(request, AUTH_SUBSYS_DB); + callback(PASSDB_RESULT_PASSWORD_MISMATCH, request); + return; + } + + /* make sure we're using the username exactly as it's in the database */ + auth_request_set_field(request, "user", pw.pw_name, NULL); + + callback(PASSDB_RESULT_OK, request); +} + +static struct passdb_module * +bsdauth_preinit(pool_t pool, const char *args) +{ + struct passdb_module *module; + + module = p_new(pool, struct passdb_module, 1); + module->default_pass_scheme = "PLAIN"; /* same reason as PAM */ + module->blocking = TRUE; + + if (strcmp(args, "blocking=no") == 0) + module->blocking = FALSE; + else if (str_begins(args, "cache_key=")) + module->default_cache_key = auth_cache_parse_key(pool, args + 10); + else if (*args != '\0') + i_fatal("passdb bsdauth: Unknown setting: %s", args); + return module; +} + +static void bsdauth_deinit(struct passdb_module *module ATTR_UNUSED) +{ + endpwent(); +} + +struct passdb_module_interface passdb_bsdauth = { + "bsdauth", + + bsdauth_preinit, + NULL, + bsdauth_deinit, + + bsdauth_verify_plain, + NULL, + NULL +}; +#else +struct passdb_module_interface passdb_bsdauth = { + .name = "bsdauth" +}; +#endif diff --git a/src/auth/passdb-cache.c b/src/auth/passdb-cache.c new file mode 100644 index 0000000..2e73134 --- /dev/null +++ b/src/auth/passdb-cache.c @@ -0,0 +1,214 @@ +/* Copyright (c) 2004-2018 Dovecot authors, see the included COPYING file */ + +#include "auth-common.h" +#include "str.h" +#include "strescape.h" +#include "restrict-process-size.h" +#include "auth-request-stats.h" +#include "auth-worker-server.h" +#include "password-scheme.h" +#include "passdb.h" +#include "passdb-cache.h" +#include "passdb-blocking.h" + +struct auth_cache *passdb_cache = NULL; + +static void +passdb_cache_log_hit(struct auth_request *request, const char *value) +{ + const char *p; + + if (!request->set->debug_passwords && + *value != '\0' && *value != '\t') { + /* hide the password */ + p = strchr(value, '\t'); + value = t_strconcat(PASSWORD_HIDDEN_STR, p, NULL); + } + e_debug(authdb_event(request), "cache hit: %s", value); +} + +static bool +passdb_cache_lookup(struct auth_request *request, const char *key, + bool use_expired, struct auth_cache_node **node_r, + const char **value_r, bool *neg_expired_r) +{ + struct auth_stats *stats = auth_request_stats_get(request); + const char *value; + bool expired; + + request->passdb_cache_result = AUTH_REQUEST_CACHE_MISS; + + /* value = password \t ... */ + value = auth_cache_lookup(passdb_cache, request, key, node_r, + &expired, neg_expired_r); + if (value == NULL || (expired && !use_expired)) { + stats->auth_cache_miss_count++; + e_debug(authdb_event(request), + value == NULL ? "cache miss" : + "cache expired"); + return FALSE; + } + stats->auth_cache_hit_count++; + passdb_cache_log_hit(request, value); + request->passdb_cache_result = AUTH_REQUEST_CACHE_HIT; + + *value_r = value; + return TRUE; +} + +static bool +passdb_cache_verify_plain_callback(struct auth_worker_connection *conn ATTR_UNUSED, + const char *reply, void *context) +{ + struct auth_request *request = context; + enum passdb_result result; + + result = passdb_blocking_auth_worker_reply_parse(request, reply); + if (result != PASSDB_RESULT_OK) + auth_fields_rollback(request->fields.extra_fields); + auth_request_verify_plain_callback_finish(result, request); + auth_request_unref(&request); + return TRUE; +} + +bool passdb_cache_verify_plain(struct auth_request *request, const char *key, + const char *password, + enum passdb_result *result_r, bool use_expired) +{ + const char *value, *cached_pw, *scheme, *const *list; + struct auth_cache_node *node; + int ret; + bool neg_expired; + + if (passdb_cache == NULL || key == NULL) + return FALSE; + + if (!passdb_cache_lookup(request, key, use_expired, + &node, &value, &neg_expired)) + return FALSE; + + if (*value == '\0') { + /* negative cache entry */ + auth_request_log_unknown_user(request, AUTH_SUBSYS_DB); + *result_r = PASSDB_RESULT_USER_UNKNOWN; + auth_request_verify_plain_callback_finish(*result_r, request); + return TRUE; + } + + list = t_strsplit_tabescaped(value); + + cached_pw = list[0]; + if (*cached_pw == '\0') { + /* NULL password */ + e_info(authdb_event(request), + "Cached NULL password access"); + ret = 1; + } else if (request->set->cache_verify_password_with_worker) { + string_t *str; + + str = t_str_new(128); + str_printfa(str, "PASSW\t%u\t", request->passdb->passdb->id); + str_append_tabescaped(str, password); + str_append_c(str, '\t'); + str_append_tabescaped(str, cached_pw); + str_append_c(str, '\t'); + auth_request_export(request, str); + + e_debug(authdb_event(request), "cache: " + "validating password on worker"); + auth_request_ref(request); + /* Save the extra fields already here, and take a snapshot. + If verification fails, roll back fields. */ + auth_request_set_fields(request, list + 1, NULL); + auth_fields_snapshot(request->fields.extra_fields); + auth_worker_call(request->pool, request->fields.user, str_c(str), + passdb_cache_verify_plain_callback, request); + return TRUE; + } else { + scheme = password_get_scheme(&cached_pw); + i_assert(scheme != NULL); + + ret = auth_request_password_verify_log(request, password, cached_pw, + scheme, AUTH_SUBSYS_DB, + !(node->last_success || neg_expired)); + + if (ret == 0 && (node->last_success || neg_expired)) { + /* a) the last authentication was successful. assume + that the password was changed and cache is expired. + b) negative TTL reached, use it for password + mismatches too. */ + node->last_success = FALSE; + return FALSE; + } + } + node->last_success = ret > 0; + + /* save the extra_fields only after we know we're using the + cached data */ + auth_request_set_fields(request, list + 1, NULL); + + *result_r = ret > 0 ? PASSDB_RESULT_OK : + PASSDB_RESULT_PASSWORD_MISMATCH; + + auth_request_verify_plain_callback_finish(*result_r, request); + return TRUE; +} + +bool passdb_cache_lookup_credentials(struct auth_request *request, + const char *key, const char **password_r, + const char **scheme_r, + enum passdb_result *result_r, + bool use_expired) +{ + const char *value, *const *list; + struct auth_cache_node *node; + bool neg_expired; + + if (passdb_cache == NULL) + return FALSE; + + if (!passdb_cache_lookup(request, key, use_expired, + &node, &value, &neg_expired)) + return FALSE; + + if (*value == '\0') { + /* negative cache entry */ + *result_r = PASSDB_RESULT_USER_UNKNOWN; + *password_r = NULL; + *scheme_r = NULL; + return TRUE; + } + + list = t_strsplit_tabescaped(value); + auth_request_set_fields(request, list + 1, NULL); + + *result_r = PASSDB_RESULT_OK; + *password_r = *list[0] == '\0' ? NULL : list[0]; + *scheme_r = password_get_scheme(password_r); + i_assert(*scheme_r != NULL || *password_r == NULL); + return TRUE; +} + +void passdb_cache_init(const struct auth_settings *set) +{ + rlim_t limit; + + if (set->cache_size == 0 || set->cache_ttl == 0) + return; + + if (restrict_get_process_size(&limit) == 0 && + set->cache_size > (uoff_t)limit) { + i_warning("auth_cache_size (%"PRIuUOFF_T"M) is higher than " + "process VSZ limit (%"PRIuUOFF_T"M)", + set->cache_size/1024/1024, + (uoff_t)(limit/1024/1024)); + } + passdb_cache = auth_cache_new(set->cache_size, set->cache_ttl, + set->cache_negative_ttl); +} + +void passdb_cache_deinit(void) +{ + if (passdb_cache != NULL) + auth_cache_free(&passdb_cache); +} diff --git a/src/auth/passdb-cache.h b/src/auth/passdb-cache.h new file mode 100644 index 0000000..1b9ff08 --- /dev/null +++ b/src/auth/passdb-cache.h @@ -0,0 +1,21 @@ +#ifndef PASSDB_CACHE_H +#define PASSDB_CACHE_H + +#include "auth-cache.h" +#include "passdb.h" + +extern struct auth_cache *passdb_cache; + +bool passdb_cache_verify_plain(struct auth_request *request, const char *key, + const char *password, + enum passdb_result *result_r, bool use_expired); +bool passdb_cache_lookup_credentials(struct auth_request *request, + const char *key, const char **password_r, + const char **scheme_r, + enum passdb_result *result_r, + bool use_expired); + +void passdb_cache_init(const struct auth_settings *set); +void passdb_cache_deinit(void); + +#endif diff --git a/src/auth/passdb-checkpassword.c b/src/auth/passdb-checkpassword.c new file mode 100644 index 0000000..101c7f0 --- /dev/null +++ b/src/auth/passdb-checkpassword.c @@ -0,0 +1,153 @@ +/* Copyright (c) 2004-2018 Dovecot authors, see the included COPYING file */ + +#include "auth-common.h" +#include "passdb.h" + +#ifdef PASSDB_CHECKPASSWORD + +#include "password-scheme.h" +#include "db-checkpassword.h" + +struct checkpassword_passdb_module { + struct passdb_module module; + struct db_checkpassword *db; +}; + +static void +auth_checkpassword_callback(struct auth_request *request, + enum db_checkpassword_status status, + const char *const *extra_fields, + verify_plain_callback_t *callback) +{ + const char *scheme, *crypted_pass = NULL; + unsigned int i; + + switch (status) { + case DB_CHECKPASSWORD_STATUS_INTERNAL_FAILURE: + callback(PASSDB_RESULT_INTERNAL_FAILURE, request); + return; + case DB_CHECKPASSWORD_STATUS_FAILURE: + callback(PASSDB_RESULT_PASSWORD_MISMATCH, request); + return; + case DB_CHECKPASSWORD_STATUS_OK: + break; + } + for (i = 0; extra_fields[i] != NULL; i++) { + if (str_begins(extra_fields[i], "password=")) + crypted_pass = extra_fields[i]+9; + else if (extra_fields[i][0] != '\0') { + auth_request_set_field_keyvalue(request, + extra_fields[i], NULL); + } + } + if (crypted_pass != NULL) { + /* for cache */ + scheme = password_get_scheme(&crypted_pass); + if (scheme != NULL) { + auth_request_set_field(request, "password", + crypted_pass, scheme); + } else { + e_error(authdb_event(request), + "password field returned without {scheme} prefix"); + } + } + callback(PASSDB_RESULT_OK, request); +} + +static void +checkpassword_verify_plain(struct auth_request *request, const char *password, + verify_plain_callback_t *callback) +{ + struct passdb_module *_module = request->passdb->passdb; + struct checkpassword_passdb_module *module = + (struct checkpassword_passdb_module *)_module; + + db_checkpassword_call(module->db, request, password, + auth_checkpassword_callback, callback); +} + +static void +credentials_checkpassword_callback(struct auth_request *request, + enum db_checkpassword_status status, + const char *const *extra_fields, + lookup_credentials_callback_t *callback) +{ + const char *scheme, *crypted_pass = NULL; + unsigned int i; + + switch (status) { + case DB_CHECKPASSWORD_STATUS_INTERNAL_FAILURE: + callback(PASSDB_RESULT_INTERNAL_FAILURE, NULL, 0, request); + return; + case DB_CHECKPASSWORD_STATUS_FAILURE: + callback(PASSDB_RESULT_USER_UNKNOWN, NULL, 0, request); + return; + case DB_CHECKPASSWORD_STATUS_OK: + break; + } + for (i = 0; extra_fields[i] != NULL; i++) { + if (str_begins(extra_fields[i], "password=")) + crypted_pass = extra_fields[i]+9; + else if (extra_fields[i][0] != '\0') { + auth_request_set_field_keyvalue(request, + extra_fields[i], NULL); + } + } + scheme = password_get_scheme(&crypted_pass); + if (scheme == NULL) + scheme = request->wanted_credentials_scheme; + + passdb_handle_credentials(PASSDB_RESULT_OK, crypted_pass, scheme, + callback, request); +} + +static void +checkpassword_lookup_credentials(struct auth_request *request, + lookup_credentials_callback_t *callback) +{ + struct passdb_module *_module = request->passdb->passdb; + struct checkpassword_passdb_module *module = + (struct checkpassword_passdb_module *)_module; + + db_checkpassword_call(module->db, request, NULL, + credentials_checkpassword_callback, callback); +} + +static struct passdb_module * +checkpassword_preinit(pool_t pool, const char *args) +{ + struct checkpassword_passdb_module *module; + const char *checkpassword_path = args; + const char *checkpassword_reply_path = + PKG_LIBEXECDIR"/checkpassword-reply"; + + module = p_new(pool, struct checkpassword_passdb_module, 1); + module->db = db_checkpassword_init(checkpassword_path, + checkpassword_reply_path); + return &module->module; +} + +static void checkpassword_deinit(struct passdb_module *_module) +{ + struct checkpassword_passdb_module *module = + (struct checkpassword_passdb_module *)_module; + + db_checkpassword_deinit(&module->db); +} + +struct passdb_module_interface passdb_checkpassword = { + "checkpassword", + + checkpassword_preinit, + NULL, + checkpassword_deinit, + + checkpassword_verify_plain, + checkpassword_lookup_credentials, + NULL +}; +#else +struct passdb_module_interface passdb_checkpassword = { + .name = "checkpassword" +}; +#endif diff --git a/src/auth/passdb-dict.c b/src/auth/passdb-dict.c new file mode 100644 index 0000000..f264035 --- /dev/null +++ b/src/auth/passdb-dict.c @@ -0,0 +1,186 @@ +/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */ + +#include "auth-common.h" +#include "passdb.h" + +#include "array.h" +#include "str.h" +#include "var-expand.h" +#include "dict.h" +#include "password-scheme.h" +#include "auth-cache.h" +#include "db-dict.h" + +#include <string.h> + +struct dict_passdb_module { + struct passdb_module module; + + struct dict_connection *conn; +}; + +struct passdb_dict_request { + struct auth_request *auth_request; + union { + verify_plain_callback_t *verify_plain; + lookup_credentials_callback_t *lookup_credentials; + } callback; +}; + +static int +dict_query_save_results(struct auth_request *auth_request, + struct dict_connection *conn, + struct db_dict_value_iter *iter) +{ + const char *key, *value, *error; + + while (db_dict_value_iter_next(iter, &key, &value)) { + if (value != NULL) { + auth_request_set_field(auth_request, key, value, + conn->set.default_pass_scheme); + } + } + if (db_dict_value_iter_deinit(&iter, &error) < 0) { + e_error(authdb_event(auth_request), "%s", error); + return -1; + } + return 0; +} + +static enum passdb_result +passdb_dict_lookup_key(struct auth_request *auth_request, + struct dict_passdb_module *module) +{ + struct db_dict_value_iter *iter; + int ret; + + ret = db_dict_value_iter_init(module->conn, auth_request, + &module->conn->set.passdb_fields, + &module->conn->set.parsed_passdb_objects, + &iter); + if (ret < 0) + return PASSDB_RESULT_INTERNAL_FAILURE; + else if (ret == 0) { + auth_request_log_unknown_user(auth_request, AUTH_SUBSYS_DB); + return PASSDB_RESULT_USER_UNKNOWN; + } else { + if (dict_query_save_results(auth_request, module->conn, iter) < 0) + return PASSDB_RESULT_INTERNAL_FAILURE; + + if (auth_request->passdb_password == NULL && + !auth_fields_exists(auth_request->fields.extra_fields, + "nopassword")) { + return auth_request_password_missing(auth_request); + } else { + return PASSDB_RESULT_OK; + } + } +} + +static void passdb_dict_lookup_pass(struct passdb_dict_request *dict_request) +{ + struct auth_request *auth_request = dict_request->auth_request; + struct passdb_module *_module = auth_request->passdb->passdb; + struct dict_passdb_module *module = + (struct dict_passdb_module *)_module; + const char *password = NULL, *scheme = NULL; + enum passdb_result passdb_result; + int ret; + + if (array_count(&module->conn->set.passdb_fields) == 0 && + array_count(&module->conn->set.parsed_passdb_objects) == 0) { + e_error(authdb_event(auth_request), + "No passdb_objects or passdb_fields specified"); + passdb_result = PASSDB_RESULT_INTERNAL_FAILURE; + } else { + passdb_result = passdb_dict_lookup_key(auth_request, module); + } + + if (passdb_result == PASSDB_RESULT_OK) { + /* passdb_password may change on the way, + so we'll need to strdup. */ + password = t_strdup(auth_request->passdb_password); + scheme = password_get_scheme(&password); + /* auth_request_set_field() sets scheme */ + i_assert(password == NULL || scheme != NULL); + } + + if (auth_request->wanted_credentials_scheme != NULL) { + passdb_handle_credentials(passdb_result, password, scheme, + dict_request->callback.lookup_credentials, + auth_request); + } else { + if (password != NULL) { + ret = auth_request_password_verify(auth_request, + auth_request->mech_password, + password, scheme, AUTH_SUBSYS_DB); + passdb_result = ret > 0 ? PASSDB_RESULT_OK : + PASSDB_RESULT_PASSWORD_MISMATCH; + } + + dict_request->callback.verify_plain(passdb_result, + auth_request); + } +} + +static void dict_verify_plain(struct auth_request *request, + const char *password ATTR_UNUSED, + verify_plain_callback_t *callback) +{ + struct passdb_dict_request *dict_request; + + dict_request = p_new(request->pool, struct passdb_dict_request, 1); + dict_request->auth_request = request; + dict_request->callback.verify_plain = callback; + + passdb_dict_lookup_pass(dict_request); +} + +static void dict_lookup_credentials(struct auth_request *request, + lookup_credentials_callback_t *callback) +{ + struct passdb_dict_request *dict_request; + + dict_request = p_new(request->pool, struct passdb_dict_request, 1); + dict_request->auth_request = request; + dict_request->callback.lookup_credentials = callback; + + passdb_dict_lookup_pass(dict_request); +} + +static struct passdb_module * +passdb_dict_preinit(pool_t pool, const char *args) +{ + struct dict_passdb_module *module; + struct dict_connection *conn; + + module = p_new(pool, struct dict_passdb_module, 1); + module->conn = conn = db_dict_init(args); + + module->module.blocking = TRUE; + module->module.default_cache_key = auth_cache_parse_key(pool, + db_dict_parse_cache_key(&conn->set.keys, &conn->set.passdb_fields, + &conn->set.parsed_passdb_objects)); + module->module.default_pass_scheme = conn->set.default_pass_scheme; + return &module->module; +} + +static void passdb_dict_deinit(struct passdb_module *_module) +{ + struct dict_passdb_module *module = + (struct dict_passdb_module *)_module; + + db_dict_unref(&module->conn); +} + +struct passdb_module_interface passdb_dict = { + "dict", + + passdb_dict_preinit, + NULL, + passdb_dict_deinit, + + dict_verify_plain, + dict_lookup_credentials, + NULL +}; diff --git a/src/auth/passdb-imap.c b/src/auth/passdb-imap.c new file mode 100644 index 0000000..9965732 --- /dev/null +++ b/src/auth/passdb-imap.c @@ -0,0 +1,241 @@ +/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */ + +#include "auth-common.h" +#include "ioloop.h" +#include "passdb.h" +#include "str.h" +#include "imap-resp-code.h" +#include "imapc-client.h" + +#define IMAP_DEFAULT_PORT 143 +#define IMAPS_DEFAULT_PORT 993 +#define DNS_CLIENT_SOCKET_NAME "dns-client" + +struct imap_passdb_module { + struct passdb_module module; + struct imapc_client_settings set; + bool set_have_vars; +}; + +struct imap_auth_request { + struct imapc_client *client; + struct auth_request *auth_request; + verify_plain_callback_t *verify_callback; + struct timeout *to_free; +}; + +static enum passdb_result +passdb_imap_get_failure_result(const struct imapc_command_reply *reply) +{ + const char *key = reply->resp_text_key; + + if (key == NULL) + return PASSDB_RESULT_PASSWORD_MISMATCH; + + if (strcasecmp(key, IMAP_RESP_CODE_AUTHFAILED) == 0 || + strcasecmp(key, IMAP_RESP_CODE_AUTHZFAILED) == 0) + return PASSDB_RESULT_PASSWORD_MISMATCH; + if (strcasecmp(key, IMAP_RESP_CODE_EXPIRED) == 0) + return PASSDB_RESULT_PASS_EXPIRED; + return PASSDB_RESULT_INTERNAL_FAILURE; +} + +static void passdb_imap_login_free(struct imap_auth_request *request) +{ + timeout_remove(&request->to_free); + imapc_client_deinit(&request->client); + auth_request_unref(&request->auth_request); +} + +static void +passdb_imap_login_callback(const struct imapc_command_reply *reply, + void *context) +{ + struct imap_auth_request *request = context; + enum passdb_result result = PASSDB_RESULT_INTERNAL_FAILURE; + + switch (reply->state) { + case IMAPC_COMMAND_STATE_OK: + result = PASSDB_RESULT_OK; + break; + case IMAPC_COMMAND_STATE_NO: + result = passdb_imap_get_failure_result(reply); + e_info(authdb_event(request->auth_request), + "%s", reply->text_full); + break; + case IMAPC_COMMAND_STATE_AUTH_FAILED: + case IMAPC_COMMAND_STATE_BAD: + case IMAPC_COMMAND_STATE_DISCONNECTED: + e_error(authdb_event(request->auth_request), + "%s", reply->text_full); + break; + } + request->verify_callback(result, request->auth_request); + /* imapc_client can't be freed in this callback, so do it in a + separate callback. FIXME: remove this once imapc supports proper + refcounting. */ + request->to_free = timeout_add_short(0, passdb_imap_login_free, request); +} + +static void +passdb_imap_verify_plain(struct auth_request *auth_request, + const char *password, + verify_plain_callback_t *callback) +{ + struct passdb_module *_module = auth_request->passdb->passdb; + struct imap_passdb_module *module = + (struct imap_passdb_module *)_module; + struct imap_auth_request *request; + struct imapc_client_settings set; + const char *error; + string_t *str; + + set = module->set; + set.debug = event_want_debug(auth_request->event); + set.dns_client_socket_path = + t_strconcat(auth_request->set->base_dir, "/", + DNS_CLIENT_SOCKET_NAME, NULL); + set.password = password; + set.max_idle_time = IMAPC_DEFAULT_MAX_IDLE_TIME; + if (set.ssl_set.ca_dir == NULL) + set.ssl_set.ca_dir = auth_request->set->ssl_client_ca_dir; + if (set.ssl_set.ca_file == NULL) + set.ssl_set.ca_file = auth_request->set->ssl_client_ca_file; + + if (module->set_have_vars) { + str = t_str_new(128); + if (auth_request_var_expand(str, set.username, auth_request, + NULL, &error) <= 0) { + e_error(authdb_event(auth_request), + "Failed to expand username=%s: %s", + set.username, error); + callback(PASSDB_RESULT_INTERNAL_FAILURE, auth_request); + return; + } + set.username = t_strdup(str_c(str)); + + str_truncate(str, 0); + if (auth_request_var_expand(str, set.host, auth_request, + NULL, &error) <= 0) { + e_error(authdb_event(auth_request), + "Failed to expand host=%s: %s", + set.host, error); + callback(PASSDB_RESULT_INTERNAL_FAILURE, auth_request); + return; + } + set.host = t_strdup(str_c(str)); + } + e_debug(authdb_event(auth_request), + "lookup host=%s port=%d", set.host, set.port); + + request = p_new(auth_request->pool, struct imap_auth_request, 1); + request->client = imapc_client_init(&set, authdb_event(auth_request)); + request->auth_request = auth_request; + request->verify_callback = callback; + + auth_request_ref(auth_request); + imapc_client_set_login_callback(request->client, passdb_imap_login_callback, request); + imapc_client_login(request->client); +} + +static struct passdb_module * +passdb_imap_preinit(pool_t pool, const char *args) +{ + struct imap_passdb_module *module; + char **tmp; + const char *key, *value; + bool port_set = FALSE; + + module = p_new(pool, struct imap_passdb_module, 1); + module->module.default_pass_scheme = "PLAIN"; + module->set.port = IMAP_DEFAULT_PORT; + module->set.ssl_mode = IMAPC_CLIENT_SSL_MODE_NONE; + module->set.username = "%u"; + module->set.rawlog_dir = ""; + + for (tmp = p_strsplit(pool, args, " "); *tmp != NULL; tmp++) { + key = *tmp; + value = strchr(key, '='); + if (value == NULL) + value = ""; + else + key = t_strdup_until(key, value++); + if (strcmp(key, "host") == 0) + module->set.host = value; + else if (strcmp(key, "port") == 0) { + if (net_str2port(value, &module->set.port) < 0) + i_fatal("passdb imap: Invalid port: %s", value); + port_set = TRUE; + } else if (strcmp(key, "username") == 0) + module->set.username = value; + else if (strcmp(key, "ssl_ca_dir") == 0) + module->set.ssl_set.ca_dir = value; + else if (strcmp(key, "ssl_ca_file") == 0) + module->set.ssl_set.ca_file = value; + else if (strcmp(key, "rawlog_dir") == 0) + module->set.rawlog_dir = value; + else if (strcmp(key, "ssl") == 0) { + if (strcmp(value, "imaps") == 0) { + if (!port_set) + module->set.port = IMAPS_DEFAULT_PORT; + module->set.ssl_mode = + IMAPC_CLIENT_SSL_MODE_IMMEDIATE; + } else if (strcmp(value, "starttls") == 0) { + module->set.ssl_mode = + IMAPC_CLIENT_SSL_MODE_STARTTLS; + } else { + i_fatal("passdb imap: Invalid ssl mode: %s", + value); + } + } else if (strcmp(key, "allow_invalid_cert") == 0) { + if (strcmp(value, "yes") == 0) { + module->set.ssl_set.allow_invalid_cert = TRUE; + } else if (strcmp(value, "no") == 0) { + module->set.ssl_set.allow_invalid_cert = FALSE; + } else { + i_fatal("passdb imap: Invalid allow_invalid_cert value: %s", + value); + } + } else { + i_fatal("passdb imap: Unknown parameter: %s", key); + } + } + + if (!module->set.ssl_set.allow_invalid_cert && module->set.ssl_mode != IMAPC_CLIENT_SSL_MODE_NONE) { + if (module->set.ssl_set.ca_dir == NULL && module->set.ssl_set.ca_file == NULL) + i_fatal("passdb imap: Cannot verify certificate without ssl_ca_dir or ssl_ca_file setting"); + } + + if (module->set.host == NULL) + i_fatal("passdb imap: Missing host parameter"); + + module->set_have_vars = + strchr(module->set.username, '%') != NULL || + strchr(module->set.host, '%') != NULL; + return &module->module; +} + +static struct passdb_module_interface passdb_imap_plugin = { + "imap", + + passdb_imap_preinit, + NULL, + NULL, + + passdb_imap_verify_plain, + NULL, + NULL +}; + +void authdb_imap_init(void); +void authdb_imap_deinit(void); + +void authdb_imap_init(void) +{ + passdb_register_module(&passdb_imap_plugin); + +} +void authdb_imap_deinit(void) +{ + passdb_unregister_module(&passdb_imap_plugin); +} diff --git a/src/auth/passdb-ldap.c b/src/auth/passdb-ldap.c new file mode 100644 index 0000000..11b9ae8 --- /dev/null +++ b/src/auth/passdb-ldap.c @@ -0,0 +1,519 @@ +/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */ + +#include "auth-common.h" +#include "passdb.h" + +#if defined(PASSDB_LDAP) && (defined(BUILTIN_LDAP) || defined(PLUGIN_BUILD)) + +#include "ioloop.h" +#include "array.h" +#include "str.h" +#include "password-scheme.h" +#include "auth-cache.h" +#include "db-ldap.h" + +#include <ldap.h> + +struct ldap_passdb_module { + struct passdb_module module; + + struct ldap_connection *conn; +}; + +struct passdb_ldap_request { + union { + struct ldap_request ldap; + struct ldap_request_search search; + struct ldap_request_bind bind; + } request; + const char *dn; + + union { + verify_plain_callback_t *verify_plain; + lookup_credentials_callback_t *lookup_credentials; + } callback; + + unsigned int entries; + bool require_password; +}; + +static void +ldap_query_save_result(struct ldap_connection *conn, + struct auth_request *auth_request, + struct ldap_request_search *ldap_request, + LDAPMessage *res) +{ + struct db_ldap_result_iterate_context *ldap_iter; + const char *name, *const *values; + + ldap_iter = db_ldap_result_iterate_init(conn, ldap_request, res, FALSE); + while (db_ldap_result_iterate_next(ldap_iter, &name, &values)) { + if (values[0] == NULL) { + auth_request_set_null_field(auth_request, name); + continue; + } + if (values[1] != NULL) { + e_warning(authdb_event(auth_request), + "Multiple values found for '%s', " + "using value '%s'", name, values[0]); + } + auth_request_set_field(auth_request, name, values[0], + conn->set.default_pass_scheme); + } + db_ldap_result_iterate_deinit(&ldap_iter); +} + +static void +ldap_lookup_finish(struct auth_request *auth_request, + struct passdb_ldap_request *ldap_request, + LDAPMessage *res) +{ + enum passdb_result passdb_result; + const char *password = NULL, *scheme; + int ret; + + if (res == NULL) { + passdb_result = PASSDB_RESULT_INTERNAL_FAILURE; + } else if (ldap_request->entries == 0) { + passdb_result = PASSDB_RESULT_USER_UNKNOWN; + auth_request_log_unknown_user(auth_request, AUTH_SUBSYS_DB); + } else if (ldap_request->entries > 1) { + e_error(authdb_event(auth_request), + "pass_filter matched multiple objects, aborting"); + passdb_result = PASSDB_RESULT_INTERNAL_FAILURE; + } else if (auth_request->passdb_password == NULL && + ldap_request->require_password && + !auth_fields_exists(auth_request->fields.extra_fields, "nopassword")) { + passdb_result = auth_request_password_missing(auth_request); + } else { + /* passdb_password may change on the way, + so we'll need to strdup. */ + password = t_strdup(auth_request->passdb_password); + passdb_result = PASSDB_RESULT_OK; + } + + scheme = password_get_scheme(&password); + /* auth_request_set_field() sets scheme */ + i_assert(password == NULL || scheme != NULL); + + if (auth_request->wanted_credentials_scheme != NULL) { + passdb_handle_credentials(passdb_result, password, scheme, + ldap_request->callback.lookup_credentials, + auth_request); + } else { + if (password != NULL) { + ret = auth_request_password_verify(auth_request, + auth_request->mech_password, + password, scheme, AUTH_SUBSYS_DB); + passdb_result = ret > 0 ? PASSDB_RESULT_OK : + PASSDB_RESULT_PASSWORD_MISMATCH; + } + + ldap_request->callback.verify_plain(passdb_result, + auth_request); + } +} + +static void +ldap_lookup_pass_callback(struct ldap_connection *conn, + struct ldap_request *request, LDAPMessage *res) +{ + struct passdb_ldap_request *ldap_request = + (struct passdb_ldap_request *)request; + struct auth_request *auth_request = request->auth_request; + + if (res == NULL || ldap_msgtype(res) == LDAP_RES_SEARCH_RESULT) { + ldap_lookup_finish(auth_request, ldap_request, res); + auth_request_unref(&auth_request); + return; + } + + if (ldap_request->entries++ == 0) { + /* first entry */ + ldap_query_save_result(conn, auth_request, + &ldap_request->request.search, res); + } +} + +static void +ldap_auth_bind_callback(struct ldap_connection *conn, + struct ldap_request *ldap_request, LDAPMessage *res) +{ + struct passdb_ldap_request *passdb_ldap_request = + (struct passdb_ldap_request *)ldap_request; + struct auth_request *auth_request = ldap_request->auth_request; + enum passdb_result passdb_result; + int ret; + + passdb_result = PASSDB_RESULT_INTERNAL_FAILURE; + + if (res != NULL) { + ret = ldap_result2error(conn->ld, res, 0); + if (ret == LDAP_SUCCESS) + passdb_result = PASSDB_RESULT_OK; + else if (ret == LDAP_INVALID_CREDENTIALS) { + auth_request_log_login_failure(auth_request, + AUTH_SUBSYS_DB, + AUTH_LOG_MSG_PASSWORD_MISMATCH" (for LDAP bind)"); + passdb_result = PASSDB_RESULT_PASSWORD_MISMATCH; + } else if (ret == LDAP_NO_SUCH_OBJECT) { + passdb_result = PASSDB_RESULT_USER_UNKNOWN; + auth_request_log_unknown_user(auth_request, + AUTH_SUBSYS_DB); + } else { + e_error(authdb_event(auth_request), + "ldap_bind() failed: %s", + ldap_err2string(ret)); + } + } + + passdb_ldap_request->callback. + verify_plain(passdb_result, auth_request); + auth_request_unref(&auth_request); +} + +static void ldap_auth_bind(struct ldap_connection *conn, + struct ldap_request_bind *brequest) +{ + struct passdb_ldap_request *passdb_ldap_request = + (struct passdb_ldap_request *)brequest; + struct auth_request *auth_request = brequest->request.auth_request; + + if (*auth_request->mech_password == '\0') { + /* Assume that empty password fails. This is especially + important with Windows 2003 AD, which always returns success + with empty passwords. */ + e_info(authdb_event(auth_request), + "Login attempt with empty password"); + passdb_ldap_request->callback. + verify_plain(PASSDB_RESULT_PASSWORD_MISMATCH, + auth_request); + return; + } + + brequest->request.callback = ldap_auth_bind_callback; + db_ldap_request(conn, &brequest->request); +} + +static void passdb_ldap_request_fail(struct passdb_ldap_request *request, + enum passdb_result passdb_result) +{ + struct auth_request *auth_request = request->request.ldap.auth_request; + + if (auth_request->wanted_credentials_scheme != NULL) { + request->callback.lookup_credentials(passdb_result, NULL, 0, + auth_request); + } else { + request->callback.verify_plain(passdb_result, auth_request); + } + auth_request_unref(&auth_request); +} + +static void +ldap_bind_lookup_dn_fail(struct auth_request *auth_request, + struct passdb_ldap_request *request, + LDAPMessage *res) +{ + enum passdb_result passdb_result; + + if (res == NULL) + passdb_result = PASSDB_RESULT_INTERNAL_FAILURE; + else if (request->entries == 0) { + passdb_result = PASSDB_RESULT_USER_UNKNOWN; + auth_request_log_unknown_user(auth_request, AUTH_SUBSYS_DB); + } else { + i_assert(request->entries > 1); + e_error(authdb_event(auth_request), + "pass_filter matched multiple objects, aborting"); + passdb_result = PASSDB_RESULT_INTERNAL_FAILURE; + } + + passdb_ldap_request_fail(request, passdb_result); +} + +static void ldap_bind_lookup_dn_callback(struct ldap_connection *conn, + struct ldap_request *ldap_request, + LDAPMessage *res) +{ + struct passdb_ldap_request *passdb_ldap_request = + (struct passdb_ldap_request *)ldap_request; + struct auth_request *auth_request = ldap_request->auth_request; + struct passdb_ldap_request *brequest; + char *dn; + + if (res != NULL && ldap_msgtype(res) == LDAP_RES_SEARCH_ENTRY) { + if (passdb_ldap_request->entries++ > 0) { + /* too many replies */ + return; + } + + /* first entry */ + ldap_query_save_result(conn, auth_request, + &passdb_ldap_request->request.search, res); + + /* save dn */ + dn = ldap_get_dn(conn->ld, res); + passdb_ldap_request->dn = p_strdup(auth_request->pool, dn); + ldap_memfree(dn); + } else if (res == NULL || passdb_ldap_request->entries != 1) { + /* failure */ + ldap_bind_lookup_dn_fail(auth_request, passdb_ldap_request, res); + } else if (auth_request->fields.skip_password_check) { + /* we've already verified that the password matched - + we just wanted to get any extra fields */ + passdb_ldap_request->callback. + verify_plain(PASSDB_RESULT_OK, auth_request); + auth_request_unref(&auth_request); + } else { + /* create a new bind request */ + brequest = p_new(auth_request->pool, + struct passdb_ldap_request, 1); + brequest->dn = passdb_ldap_request->dn; + brequest->callback = passdb_ldap_request->callback; + brequest->request.bind.dn = brequest->dn; + brequest->request.bind.request.type = LDAP_REQUEST_TYPE_BIND; + brequest->request.bind.request.auth_request = auth_request; + + ldap_auth_bind(conn, &brequest->request.bind); + } +} + +static void ldap_lookup_pass(struct auth_request *auth_request, + struct passdb_ldap_request *request, + bool require_password) +{ + struct passdb_module *_module = auth_request->passdb->passdb; + struct ldap_passdb_module *module = + (struct ldap_passdb_module *)_module; + struct ldap_connection *conn = module->conn; + struct ldap_request_search *srequest = &request->request.search; + const char **attr_names = (const char **)conn->pass_attr_names; + const char *error; + string_t *str; + + request->require_password = require_password; + srequest->request.type = LDAP_REQUEST_TYPE_SEARCH; + + str = t_str_new(512); + if (auth_request_var_expand(str, conn->set.base, auth_request, + ldap_escape, &error) <= 0) { + e_error(authdb_event(auth_request), + "Failed to expand base=%s: %s", conn->set.base, error); + passdb_ldap_request_fail(request, PASSDB_RESULT_INTERNAL_FAILURE); + return; + } + srequest->base = p_strdup(auth_request->pool, str_c(str)); + + str_truncate(str, 0); + if (auth_request_var_expand(str, conn->set.pass_filter, + auth_request, ldap_escape, &error) <= 0) { + e_error(authdb_event(auth_request), + "Failed to expand pass_filter=%s: %s", + conn->set.pass_filter, error); + passdb_ldap_request_fail(request, PASSDB_RESULT_INTERNAL_FAILURE); + return; + } + srequest->filter = p_strdup(auth_request->pool, str_c(str)); + srequest->attr_map = &conn->pass_attr_map; + srequest->attributes = conn->pass_attr_names; + + e_debug(authdb_event(auth_request), "pass search: " + "base=%s scope=%s filter=%s fields=%s", + srequest->base, conn->set.scope, + srequest->filter, attr_names == NULL ? "(all)" : + t_strarray_join(attr_names, ",")); + + srequest->request.callback = ldap_lookup_pass_callback; + db_ldap_request(conn, &srequest->request); +} + +static void ldap_bind_lookup_dn(struct auth_request *auth_request, + struct passdb_ldap_request *request) +{ + struct passdb_module *_module = auth_request->passdb->passdb; + struct ldap_passdb_module *module = + (struct ldap_passdb_module *)_module; + struct ldap_connection *conn = module->conn; + struct ldap_request_search *srequest = &request->request.search; + const char *error; + string_t *str; + + srequest->request.type = LDAP_REQUEST_TYPE_SEARCH; + + str = t_str_new(512); + if (auth_request_var_expand(str, conn->set.base, auth_request, + ldap_escape, &error) <= 0) { + e_error(authdb_event(auth_request), + "Failed to expand base=%s: %s", conn->set.base, error); + passdb_ldap_request_fail(request, PASSDB_RESULT_INTERNAL_FAILURE); + return; + } + srequest->base = p_strdup(auth_request->pool, str_c(str)); + + str_truncate(str, 0); + if (auth_request_var_expand(str, conn->set.pass_filter, + auth_request, ldap_escape, &error) <= 0) { + e_error(authdb_event(auth_request), + "Failed to expand pass_filter=%s: %s", + conn->set.pass_filter, error); + passdb_ldap_request_fail(request, PASSDB_RESULT_INTERNAL_FAILURE); + return; + } + srequest->filter = p_strdup(auth_request->pool, str_c(str)); + + /* we don't need the attributes to perform authentication, but they + may contain some extra parameters. if a password is returned, + it's just ignored. */ + srequest->attr_map = &conn->pass_attr_map; + srequest->attributes = conn->pass_attr_names; + + e_debug(authdb_event(auth_request), + "bind search: base=%s filter=%s", + srequest->base, srequest->filter); + + srequest->request.callback = ldap_bind_lookup_dn_callback; + db_ldap_request(conn, &srequest->request); +} + +static void +ldap_verify_plain_auth_bind_userdn(struct auth_request *auth_request, + struct passdb_ldap_request *request) +{ + struct passdb_module *_module = auth_request->passdb->passdb; + struct ldap_passdb_module *module = + (struct ldap_passdb_module *)_module; + struct ldap_connection *conn = module->conn; + struct ldap_request_bind *brequest = &request->request.bind; + string_t *dn; + const char *error; + + brequest->request.type = LDAP_REQUEST_TYPE_BIND; + + dn = t_str_new(512); + if (auth_request_var_expand(dn, conn->set.auth_bind_userdn, + auth_request, ldap_escape, &error) <= 0) { + e_error(authdb_event(auth_request), + "Failed to expand auth_bind_userdn=%s: %s", + conn->set.auth_bind_userdn, error); + passdb_ldap_request_fail(request, PASSDB_RESULT_INTERNAL_FAILURE); + return; + } + + brequest->dn = p_strdup(auth_request->pool, str_c(dn)); + ldap_auth_bind(conn, brequest); +} + +static void +ldap_verify_plain(struct auth_request *request, + const char *password ATTR_UNUSED, + verify_plain_callback_t *callback) +{ + struct passdb_module *_module = request->passdb->passdb; + struct ldap_passdb_module *module = + (struct ldap_passdb_module *)_module; + struct ldap_connection *conn = module->conn; + struct passdb_ldap_request *ldap_request; + + /* reconnect if needed. this is also done by db_ldap_search(), but + with auth binds we'll have to do it ourself */ + if (db_ldap_connect(conn)< 0) { + callback(PASSDB_RESULT_INTERNAL_FAILURE, request); + return; + } + + ldap_request = p_new(request->pool, struct passdb_ldap_request, 1); + ldap_request->callback.verify_plain = callback; + + auth_request_ref(request); + ldap_request->request.ldap.auth_request = request; + + if (!conn->set.auth_bind) + ldap_lookup_pass(request, ldap_request, TRUE); + else if (conn->set.auth_bind_userdn == NULL) + ldap_bind_lookup_dn(request, ldap_request); + else + ldap_verify_plain_auth_bind_userdn(request, ldap_request); +} + +static void ldap_lookup_credentials(struct auth_request *request, + lookup_credentials_callback_t *callback) +{ + struct passdb_module *_module = request->passdb->passdb; + struct ldap_passdb_module *module = + (struct ldap_passdb_module *)_module; + struct passdb_ldap_request *ldap_request; + bool require_password; + + ldap_request = p_new(request->pool, struct passdb_ldap_request, 1); + ldap_request->callback.lookup_credentials = callback; + + auth_request_ref(request); + ldap_request->request.ldap.auth_request = request; + + /* with auth_bind=yes we don't necessarily have a password. + this will fail actual password credentials lookups, but it's fine + for passdb lookups done by lmtp/doveadm */ + require_password = !module->conn->set.auth_bind; + ldap_lookup_pass(request, ldap_request, require_password); +} + +static struct passdb_module * +passdb_ldap_preinit(pool_t pool, const char *args) +{ + struct ldap_passdb_module *module; + struct ldap_connection *conn; + + module = p_new(pool, struct ldap_passdb_module, 1); + module->conn = conn = db_ldap_init(args, FALSE); + p_array_init(&conn->pass_attr_map, pool, 16); + db_ldap_set_attrs(conn, conn->set.pass_attrs, &conn->pass_attr_names, + &conn->pass_attr_map, + conn->set.auth_bind ? "password" : NULL); + module->module.blocking = conn->set.blocking; + module->module.default_cache_key = + auth_cache_parse_key(pool, + t_strconcat(conn->set.base, + conn->set.pass_attrs, + conn->set.pass_filter, NULL)); + module->module.default_pass_scheme = conn->set.default_pass_scheme; + return &module->module; +} + +static void passdb_ldap_init(struct passdb_module *_module) +{ + struct ldap_passdb_module *module = + (struct ldap_passdb_module *)_module; + + if (!module->module.blocking || worker) + db_ldap_connect_delayed(module->conn); +} + +static void passdb_ldap_deinit(struct passdb_module *_module) +{ + struct ldap_passdb_module *module = + (struct ldap_passdb_module *)_module; + + db_ldap_unref(&module->conn); +} + +#ifndef PLUGIN_BUILD +struct passdb_module_interface passdb_ldap = +#else +struct passdb_module_interface passdb_ldap_plugin = +#endif +{ + "ldap", + + passdb_ldap_preinit, + passdb_ldap_init, + passdb_ldap_deinit, + + ldap_verify_plain, + ldap_lookup_credentials, + NULL +}; +#else +struct passdb_module_interface passdb_ldap = { + .name = "ldap" +}; +#endif diff --git a/src/auth/passdb-lua.c b/src/auth/passdb-lua.c new file mode 100644 index 0000000..b49b98e --- /dev/null +++ b/src/auth/passdb-lua.c @@ -0,0 +1,193 @@ +/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */ + +#include "auth-common.h" +#include "passdb.h" +#include "auth-cache.h" + +#if defined(BUILTIN_LUA) || defined(PLUGIN_BUILD) + +#include "db-lua.h" + +struct dlua_passdb_module { + struct passdb_module module; + struct dlua_script *script; + const char *file; + bool has_password_verify; +}; + +static enum passdb_result +passdb_lua_verify_password(struct dlua_passdb_module *module, + struct auth_request *request, const char *password) +{ + const char *error = NULL; + enum passdb_result result = + auth_lua_call_password_verify(module->script, request, + password, &error); + if (result == PASSDB_RESULT_PASSWORD_MISMATCH) { + auth_request_log_password_mismatch(request, AUTH_SUBSYS_DB); + } else if (result == PASSDB_RESULT_INTERNAL_FAILURE && error != NULL) { + e_error(authdb_event(request), "passdb-lua: %s", + error); + } + return result; +} + +static enum passdb_result +passdb_lua_lookup(struct auth_request *request, + const char **scheme_r, const char **password_r) +{ + const char *error = NULL; + enum passdb_result result; + struct dlua_passdb_module *module = + (struct dlua_passdb_module *)request->passdb->passdb; + + *scheme_r = *password_r = NULL; + + result = auth_lua_call_passdb_lookup(module->script, request, scheme_r, + password_r, &error); + + if (result == PASSDB_RESULT_INTERNAL_FAILURE && error != NULL) { + e_error(authdb_event(request), "db-lua: %s", error); + } else if (result != PASSDB_RESULT_OK) { + /* skip next bit */ + } else if (!auth_fields_exists(request->fields.extra_fields, "nopassword")) { + if (*password_r == NULL || **password_r == '\0') { + result = auth_request_password_missing(request); + } else { + if (*scheme_r == NULL) + *scheme_r = request->passdb->passdb->default_pass_scheme; + auth_request_set_field(request, "password", + *password_r, *scheme_r); + } + } else if (*password_r != NULL && **password_r != '\0') { + e_info(authdb_event(request), + "nopassword given and password is not empty"); + result = PASSDB_RESULT_PASSWORD_MISMATCH; + } + return result; +} + +static void +passdb_lua_lookup_credentials(struct auth_request *request, + lookup_credentials_callback_t *callback) +{ + const char *lua_password, *lua_scheme; + enum passdb_result result = + passdb_lua_lookup(request, &lua_scheme, &lua_password); + + passdb_handle_credentials(result, lua_password, lua_scheme, callback, request); +} + +static void +passdb_lua_verify_plain(struct auth_request *request, const char *password, + verify_plain_callback_t *callback) +{ + struct dlua_passdb_module *module = + (struct dlua_passdb_module *)request->passdb->passdb; + const char *lua_password, *lua_scheme; + enum passdb_result result; + + if (module->has_password_verify) { + result = passdb_lua_verify_password(module, request, password); + } else { + result = passdb_lua_lookup(request, &lua_scheme, &lua_password); + if (result == PASSDB_RESULT_OK) { + if (lua_scheme == NULL) + lua_scheme = "PLAIN"; + if ((auth_request_password_verify(request, password, lua_password, + lua_scheme, AUTH_SUBSYS_DB)) <=0) { + result = PASSDB_RESULT_PASSWORD_MISMATCH; + } + } + } + callback(result, request); +} + +static struct passdb_module * +passdb_lua_preinit(pool_t pool, const char *args) +{ + const char *cache_key = "%u"; + const char *scheme = "PLAIN"; + struct dlua_passdb_module *module; + bool blocking = TRUE; + + module = p_new(pool, struct dlua_passdb_module, 1); + const char *const *fields = t_strsplit_spaces(args, " "); + while(*fields != NULL) { + if (str_begins(*fields, "file=")) { + module->file = p_strdup(pool, (*fields)+5); + } else if (str_begins(*fields, "blocking=")) { + const char *value = (*fields)+9; + if (strcmp(value, "yes") == 0) { + blocking = TRUE; + } else if (strcmp(value, "no") == 0) { + blocking = FALSE; + } else { + i_fatal("Invalid value %s. " + "Field blocking must be yes or no", + value); + } + } else if (str_begins(*fields, "cache_key=")) { + if (*((*fields)+10) != '\0') + cache_key = (*fields)+10; + else /* explicitly disable auth caching for lua */ + cache_key = NULL; + } else if (str_begins(*fields, "scheme=")) { + scheme = p_strdup(pool, (*fields)+7); + } else { + i_fatal("Unsupported parameter %s", *fields); + } + fields++; + } + + if (module->file == NULL) + i_fatal("passdb-lua: Missing mandatory file= parameter"); + + module->module.blocking = blocking; + module->module.default_cache_key = + auth_cache_parse_key(pool, cache_key); + module->module.default_pass_scheme = scheme; + return &module->module; +} + +static void passdb_lua_init(struct passdb_module *_module) +{ + struct dlua_passdb_module *module = + (struct dlua_passdb_module *)_module; + const char *error; + + if (dlua_script_create_file(module->file, &module->script, auth_event, &error) < 0 || + auth_lua_script_init(module->script, &error) < 0) + i_fatal("passdb-lua: initialization failed: %s", error); + module->has_password_verify = + dlua_script_has_function(module->script, AUTH_LUA_PASSWORD_VERIFY); +} + +static void passdb_lua_deinit(struct passdb_module *_module) +{ + struct dlua_passdb_module *module = + (struct dlua_passdb_module *)_module; + dlua_script_unref(&module->script); +} + +#ifndef PLUGIN_BUILD +struct passdb_module_interface passdb_lua = +#else +struct passdb_module_interface passdb_lua_plugin = +#endif +{ + "lua", + + passdb_lua_preinit, + passdb_lua_init, + passdb_lua_deinit, + + passdb_lua_verify_plain, + passdb_lua_lookup_credentials, + NULL +}; +#else +struct passdb_module_interface passdb_lua = { + .name = "lua" +}; +#endif diff --git a/src/auth/passdb-oauth2.c b/src/auth/passdb-oauth2.c new file mode 100644 index 0000000..000d1ec --- /dev/null +++ b/src/auth/passdb-oauth2.c @@ -0,0 +1,80 @@ +/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */ + +#include "auth-common.h" +#include "passdb.h" +#include "db-oauth2.h" + +struct oauth2_passdb_module { + struct passdb_module module; + struct db_oauth2 *db; +}; + +static void +oauth2_verify_plain_continue(struct db_oauth2_request *req, + enum passdb_result result, const char *error, + struct auth_request *request) +{ + if (result == PASSDB_RESULT_INTERNAL_FAILURE) + e_error(authdb_event(request), "oauth2 failed: %s", + error); + else if (result != PASSDB_RESULT_OK) + e_info(authdb_event(request), "oauth2 failed: %s", + error); + else { + auth_request_set_field(request, "token", req->token, "PLAIN"); + } + req->verify_callback(result, request); + auth_request_unref(&request); +} + +static void +oauth2_verify_plain(struct auth_request *request, const char *password, + verify_plain_callback_t *callback) +{ + struct oauth2_passdb_module *module = + (struct oauth2_passdb_module *)request->passdb->passdb; + struct db_oauth2_request *req = + p_new(request->pool, struct db_oauth2_request, 1); + req->pool = request->pool; + req->verify_callback = callback; + + auth_request_ref(request); + + db_oauth2_lookup(module->db, req, password, request, oauth2_verify_plain_continue, request); +} + +static struct passdb_module * +oauth2_preinit(pool_t pool, const char *args) +{ + struct oauth2_passdb_module *module; + + module = p_new(pool, struct oauth2_passdb_module, 1); + module->db = db_oauth2_init(args); + module->module.default_pass_scheme = "PLAIN"; + + if (db_oauth2_uses_password_grant(module->db)) { + module->module.default_cache_key = "%u"; + } else { + module->module.default_cache_key = "%u%w"; + } + + return &module->module; +} + +static void oauth2_deinit(struct passdb_module *passdb) +{ + struct oauth2_passdb_module *module = (struct oauth2_passdb_module *)passdb; + db_oauth2_unref(&module->db); +} + +struct passdb_module_interface passdb_oauth2 = { + "oauth2", + + oauth2_preinit, + NULL, + oauth2_deinit, + + oauth2_verify_plain, + NULL, + NULL +}; diff --git a/src/auth/passdb-pam.c b/src/auth/passdb-pam.c new file mode 100644 index 0000000..6a1032d --- /dev/null +++ b/src/auth/passdb-pam.c @@ -0,0 +1,401 @@ +/* + Based on auth_pam.c from popa3d by Solar Designer <solar@openwall.com>. + + You're allowed to do whatever you like with this software (including + re-distribution in source and/or binary form, with or without + modification), provided that credit is given where it is due and any + modified versions are marked as such. There's absolutely no warranty. +*/ + +#include "auth-common.h" +#include "passdb.h" + +#ifdef PASSDB_PAM + +#include "lib-signals.h" +#include "str.h" +#include "net.h" +#include "safe-memset.h" +#include "auth-cache.h" + +#include <sys/stat.h> + +#ifdef HAVE_SECURITY_PAM_APPL_H +# include <security/pam_appl.h> +#elif defined(HAVE_PAM_PAM_APPL_H) +# include <pam/pam_appl.h> +#endif + +#if defined(sun) || defined(__sun__) || defined(_HPUX_SOURCE) +# define pam_const +#else +# define pam_const const +#endif + +typedef pam_const void *pam_item_t; + +#define PASSDB_PAM_DEFAULT_MAX_REQUESTS 100 + +struct pam_passdb_module { + struct passdb_module module; + + const char *service_name, *pam_cache_key; + unsigned int requests_left; + + bool pam_setcred:1; + bool pam_session:1; + bool failure_show_msg:1; +}; + +struct pam_conv_context { + struct auth_request *request; + const char *pass; + const char *failure_msg; +}; + +static int +pam_userpass_conv(int num_msg, pam_const struct pam_message **msg, + struct pam_response **resp_r, void *appdata_ptr) +{ + /* @UNSAFE */ + struct pam_conv_context *ctx = appdata_ptr; + struct passdb_module *_passdb = ctx->request->passdb->passdb; + struct pam_passdb_module *passdb = (struct pam_passdb_module *)_passdb; + struct pam_response *resp; + char *string; + int i; + + *resp_r = NULL; + + resp = calloc(num_msg, sizeof(struct pam_response)); + if (resp == NULL) + i_fatal_status(FATAL_OUTOFMEM, "Out of memory"); + + for (i = 0; i < num_msg; i++) { + e_debug(authdb_event(ctx->request), + "#%d/%d style=%d msg=%s", i+1, num_msg, + msg[i]->msg_style, + msg[i]->msg != NULL ? msg[i]->msg : ""); + switch (msg[i]->msg_style) { + case PAM_PROMPT_ECHO_ON: + /* Assume we're asking for user. We might not ever + get here because PAM already knows the user. */ + string = strdup(ctx->request->fields.user); + if (string == NULL) + i_fatal_status(FATAL_OUTOFMEM, "Out of memory"); + break; + case PAM_PROMPT_ECHO_OFF: + /* Assume we're asking for password */ + if (passdb->failure_show_msg) + ctx->failure_msg = t_strdup(msg[i]->msg); + string = strdup(ctx->pass); + if (string == NULL) + i_fatal_status(FATAL_OUTOFMEM, "Out of memory"); + break; + case PAM_ERROR_MSG: + case PAM_TEXT_INFO: + string = NULL; + break; + default: + while (--i >= 0) { + if (resp[i].resp != NULL) { + safe_memset(resp[i].resp, 0, + strlen(resp[i].resp)); + free(resp[i].resp); + } + } + + free(resp); + return PAM_CONV_ERR; + } + + resp[i].resp_retcode = PAM_SUCCESS; + resp[i].resp = string; + } + + *resp_r = resp; + return PAM_SUCCESS; +} + +static const char * +pam_get_missing_service_file_path(const char *service ATTR_UNUSED) +{ +#ifdef SUNPAM + /* Uses /etc/pam.conf - we're not going to parse that */ + return NULL; +#else + static bool service_checked = FALSE; + const char *path; + struct stat st; + + if (service_checked) { + /* check and complain only once */ + return NULL; + } + service_checked = TRUE; + + path = t_strdup_printf("/etc/pam.d/%s", service); + if (stat(path, &st) < 0 && errno == ENOENT) { + /* looks like it's missing. but before assuming that the system + even uses /etc/pam.d, make sure that it exists. */ + if (stat("/etc/pam.d", &st) == 0) + return path; + } + /* exists or is unknown */ + return NULL; +#endif +} + +static int try_pam_auth(struct auth_request *request, pam_handle_t *pamh, + const char *service) +{ + struct passdb_module *_module = request->passdb->passdb; + struct pam_passdb_module *module = (struct pam_passdb_module *)_module; + const char *path, *str; + pam_item_t item; + int status; + + if ((status = pam_authenticate(pamh, 0)) != PAM_SUCCESS) { + path = pam_get_missing_service_file_path(service); + switch (status) { + case PAM_USER_UNKNOWN: + str = "unknown user"; + break; + default: + str = t_strconcat("pam_authenticate() failed: ", + pam_strerror(pamh, status), NULL); + break; + } + if (path != NULL) { + /* log this as error, since it probably is */ + str = t_strdup_printf("%s (%s missing?)", str, path); + e_error(authdb_event(request), "%s", str); + } else if (status == PAM_AUTH_ERR) { + str = t_strconcat(str, " ("AUTH_LOG_MSG_PASSWORD_MISMATCH"?)", NULL); + if (request->set->debug_passwords) { + str = t_strconcat(str, " (given password: ", + request->mech_password, + ")", NULL); + } + e_info(authdb_event(request), "%s", str); + } else { + if (status == PAM_USER_UNKNOWN) + auth_request_log_unknown_user(request, AUTH_SUBSYS_DB); + else { + e_info(authdb_event(request), + "%s", str); + } + } + return status; + } + +#ifdef HAVE_PAM_SETCRED + if (module->pam_setcred) { + if ((status = pam_setcred(pamh, PAM_ESTABLISH_CRED)) != + PAM_SUCCESS) { + e_error(authdb_event(request), + "pam_setcred() failed: %s", + pam_strerror(pamh, status)); + return status; + } + } +#endif + + if ((status = pam_acct_mgmt(pamh, 0)) != PAM_SUCCESS) { + e_error(authdb_event(request), + "pam_acct_mgmt() failed: %s", + pam_strerror(pamh, status)); + return status; + } + + if (module->pam_session) { + if ((status = pam_open_session(pamh, 0)) != PAM_SUCCESS) { + e_error(authdb_event(request), + "pam_open_session() failed: %s", + pam_strerror(pamh, status)); + return status; + } + + if ((status = pam_close_session(pamh, 0)) != PAM_SUCCESS) { + e_error(authdb_event(request), + "pam_close_session() failed: %s", + pam_strerror(pamh, status)); + return status; + } + } + + status = pam_get_item(pamh, PAM_USER, &item); + if (status != PAM_SUCCESS) { + e_error(authdb_event(request), + "pam_get_item(PAM_USER) failed: %s", + pam_strerror(pamh, status)); + return status; + } + auth_request_set_field(request, "user", item, NULL); + return PAM_SUCCESS; +} + +static void set_pam_items(struct auth_request *request, pam_handle_t *pamh) +{ + const char *host; + + /* These shouldn't fail, and we don't really care if they do. */ + host = net_ip2addr(&request->fields.remote_ip); + if (host[0] != '\0') + (void)pam_set_item(pamh, PAM_RHOST, host); + (void)pam_set_item(pamh, PAM_RUSER, request->fields.user); + /* TTY is needed by eg. pam_access module */ + (void)pam_set_item(pamh, PAM_TTY, "dovecot"); +} + +static enum passdb_result +pam_verify_plain_call(struct auth_request *request, const char *service, + const char *password) +{ + pam_handle_t *pamh; + struct pam_conv_context ctx; + struct pam_conv conv; + enum passdb_result result; + int status, status2; + + conv.conv = pam_userpass_conv; + conv.appdata_ptr = &ctx; + + i_zero(&ctx); + ctx.request = request; + ctx.pass = password; + + status = pam_start(service, request->fields.user, &conv, &pamh); + if (status != PAM_SUCCESS) { + e_error(authdb_event(request), + "pam_start() failed: %s", + pam_strerror(pamh, status)); + return PASSDB_RESULT_INTERNAL_FAILURE; + } + + set_pam_items(request, pamh); + status = try_pam_auth(request, pamh, service); + if ((status2 = pam_end(pamh, status)) != PAM_SUCCESS) { + e_error(authdb_event(request), + "pam_end() failed: %s", + pam_strerror(pamh, status2)); + return PASSDB_RESULT_INTERNAL_FAILURE; + } + + switch (status) { + case PAM_SUCCESS: + result = PASSDB_RESULT_OK; + break; + case PAM_USER_UNKNOWN: + result = PASSDB_RESULT_USER_UNKNOWN; + break; + case PAM_NEW_AUTHTOK_REQD: + case PAM_ACCT_EXPIRED: + result = PASSDB_RESULT_PASS_EXPIRED; + break; + default: + result = PASSDB_RESULT_PASSWORD_MISMATCH; + break; + } + + if (result != PASSDB_RESULT_OK && ctx.failure_msg != NULL) { + auth_request_set_field(request, "reason", + ctx.failure_msg, NULL); + } + return result; +} + +static void +pam_verify_plain(struct auth_request *request, const char *password, + verify_plain_callback_t *callback) +{ + struct passdb_module *_module = request->passdb->passdb; + struct pam_passdb_module *module = (struct pam_passdb_module *)_module; + enum passdb_result result; + const char *service, *error; + + if (module->requests_left > 0) { + if (--module->requests_left == 0) + worker_restart_request = TRUE; + } + + if (t_auth_request_var_expand(module->service_name, request, NULL, + &service, &error) <= 0) { + e_debug(authdb_event(request), + "Failed to expand service %s: %s", + module->service_name, error); + callback(PASSDB_RESULT_INTERNAL_FAILURE, request); + return; + } + + e_debug(authdb_event(request), + "lookup service=%s", service); + + result = pam_verify_plain_call(request, service, password); + callback(result, request); +} + +static struct passdb_module * +pam_preinit(pool_t pool, const char *args) +{ + struct pam_passdb_module *module; + const char *const *t_args; + int i; + + module = p_new(pool, struct pam_passdb_module, 1); + module->service_name = "dovecot"; + /* we're caching the password by using directly the plaintext password + given by the auth mechanism */ + module->module.default_pass_scheme = "PLAIN"; + module->module.blocking = TRUE; + module->requests_left = PASSDB_PAM_DEFAULT_MAX_REQUESTS; + + t_args = t_strsplit_spaces(args, " "); + for(i = 0; t_args[i] != NULL; i++) { + /* -session for backwards compatibility */ + if (strcmp(t_args[i], "-session") == 0 || + strcmp(t_args[i], "session=yes") == 0) + module->pam_session = TRUE; + else if (strcmp(t_args[i], "setcred=yes") == 0) + module->pam_setcred = TRUE; + else if (str_begins(t_args[i], "cache_key=")) { + module->module.default_cache_key = + auth_cache_parse_key(pool, t_args[i] + 10); + } else if (strcmp(t_args[i], "blocking=yes") == 0) { + /* ignore, for backwards compatibility */ + } else if (strcmp(t_args[i], "failure_show_msg=yes") == 0) { + module->failure_show_msg = TRUE; + } else if (strcmp(t_args[i], "*") == 0) { + /* for backwards compatibility */ + module->service_name = "%Ls"; + } else if (str_begins(t_args[i], "max_requests=")) { + if (str_to_uint(t_args[i] + 13, + &module->requests_left) < 0) { + i_error("pam: Invalid requests_left value: %s", + t_args[i] + 13); + } + } else if (t_args[i+1] == NULL) { + module->service_name = p_strdup(pool, t_args[i]); + } else { + i_fatal("pam: Unknown setting: %s", t_args[i]); + } + } + return &module->module; +} + +struct passdb_module_interface passdb_pam = { + "pam", + + pam_preinit, + NULL, + NULL, + + pam_verify_plain, + NULL, + NULL +}; +#else +struct passdb_module_interface passdb_pam = { + .name = "pam" +}; +#endif diff --git a/src/auth/passdb-passwd-file.c b/src/auth/passdb-passwd-file.c new file mode 100644 index 0000000..227fab3 --- /dev/null +++ b/src/auth/passdb-passwd-file.c @@ -0,0 +1,210 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +#include "auth-common.h" +#include "passdb.h" + +#ifdef PASSDB_PASSWD_FILE + +#include "str.h" +#include "auth-cache.h" +#include "password-scheme.h" +#include "db-passwd-file.h" + +struct passwd_file_passdb_module { + struct passdb_module module; + + struct db_passwd_file *pwf; + const char *username_format; +}; + +static int +passwd_file_add_extra_fields(struct auth_request *request, char *const *fields) +{ + string_t *str = t_str_new(512); + const struct var_expand_table *table; + const char *key, *value, *error; + unsigned int i; + + table = auth_request_get_var_expand_table(request, NULL); + + for (i = 0; fields[i] != NULL; i++) { + value = strchr(fields[i], '='); + if (value != NULL) { + key = t_strdup_until(fields[i], value); + str_truncate(str, 0); + if (auth_request_var_expand_with_table(str, value + 1, + request, table, NULL, &error) <= 0) { + e_error(authdb_event(request), + "Failed to expand extra field %s: %s", + fields[i], error); + return -1; + } + value = str_c(str); + } else { + key = fields[i]; + value = ""; + } + auth_request_set_field(request, key, value, NULL); + } + return 0; +} + +static int passwd_file_save_results(struct auth_request *request, + const struct passwd_user *pu, + const char **crypted_pass_r, + const char **scheme_r) +{ + *crypted_pass_r = pu->password != NULL ? pu->password : ""; + *scheme_r = password_get_scheme(crypted_pass_r); + if (*scheme_r == NULL) + *scheme_r = request->passdb->passdb->default_pass_scheme; + + /* save the password so cache can use it */ + auth_request_set_field(request, "password", + *crypted_pass_r, *scheme_r); + + if (pu->extra_fields != NULL) { + if (passwd_file_add_extra_fields(request, pu->extra_fields) < 0) + return -1; + } + return 0; +} + +static void +passwd_file_verify_plain(struct auth_request *request, const char *password, + verify_plain_callback_t *callback) +{ + struct passdb_module *_module = request->passdb->passdb; + struct passwd_file_passdb_module *module = + (struct passwd_file_passdb_module *)_module; + struct passwd_user *pu; + const char *scheme, *crypted_pass; + int ret; + + ret = db_passwd_file_lookup(module->pwf, request, + module->username_format, &pu); + if (ret <= 0) { + callback(ret < 0 ? PASSDB_RESULT_INTERNAL_FAILURE : + PASSDB_RESULT_USER_UNKNOWN, request); + return; + } + + if (passwd_file_save_results(request, pu, &crypted_pass, &scheme) < 0) { + callback(PASSDB_RESULT_INTERNAL_FAILURE, request); + return; + } + + ret = auth_request_password_verify(request, password, crypted_pass, + scheme, AUTH_SUBSYS_DB); + + callback(ret > 0 ? PASSDB_RESULT_OK : PASSDB_RESULT_PASSWORD_MISMATCH, + request); +} + +static void +passwd_file_lookup_credentials(struct auth_request *request, + lookup_credentials_callback_t *callback) +{ + struct passdb_module *_module = request->passdb->passdb; + struct passwd_file_passdb_module *module = + (struct passwd_file_passdb_module *)_module; + struct passwd_user *pu; + const char *crypted_pass, *scheme; + int ret; + + ret = db_passwd_file_lookup(module->pwf, request, + module->username_format, &pu); + if (ret <= 0) { + callback(ret < 0 ? PASSDB_RESULT_INTERNAL_FAILURE : + PASSDB_RESULT_USER_UNKNOWN, NULL, 0, request); + return; + } + + if (passwd_file_save_results(request, pu, &crypted_pass, &scheme) < 0) { + callback(PASSDB_RESULT_INTERNAL_FAILURE, NULL, 0, request); + return; + } + + passdb_handle_credentials(PASSDB_RESULT_OK, crypted_pass, scheme, + callback, request); +} + +static struct passdb_module * +passwd_file_preinit(pool_t pool, const char *args) +{ + struct passwd_file_passdb_module *module; + const char *scheme = PASSWD_FILE_DEFAULT_SCHEME; + const char *format = PASSWD_FILE_DEFAULT_USERNAME_FORMAT; + const char *key, *value; + + while (*args != '\0') { + if (*args == '/') + break; + + key = args; + value = strchr(key, '='); + if (value == NULL) { + value = ""; + args = strchr(key, ' '); + } else { + key = t_strdup_until(key, value); + args = strchr(++value, ' '); + if (args != NULL) + value = t_strdup_until(value, args); + } + if (args == NULL) + args = ""; + else + args++; + + if (strcmp(key, "scheme") == 0) + scheme = p_strdup(pool, value); + else if (strcmp(key, "username_format") == 0) + format = p_strdup(pool, value); + else + i_fatal("passdb passwd-file: Unknown setting: %s", key); + } + + if (*args == '\0') + i_fatal("passdb passwd-file: Missing args"); + + module = p_new(pool, struct passwd_file_passdb_module, 1); + module->pwf = db_passwd_file_init(args, FALSE, + global_auth_settings->debug); + module->username_format = format; + module->module.default_pass_scheme = scheme; + return &module->module; +} + +static void passwd_file_init(struct passdb_module *_module) +{ + struct passwd_file_passdb_module *module = + (struct passwd_file_passdb_module *)_module; + + db_passwd_file_parse(module->pwf); +} + +static void passwd_file_deinit(struct passdb_module *_module) +{ + struct passwd_file_passdb_module *module = + (struct passwd_file_passdb_module *)_module; + + db_passwd_file_unref(&module->pwf); +} + +struct passdb_module_interface passdb_passwd_file = { + "passwd-file", + + passwd_file_preinit, + passwd_file_init, + passwd_file_deinit, + + passwd_file_verify_plain, + passwd_file_lookup_credentials, + NULL +}; +#else +struct passdb_module_interface passdb_passwd_file = { + .name = "passwd-file" +}; +#endif diff --git a/src/auth/passdb-passwd.c b/src/auth/passdb-passwd.c new file mode 100644 index 0000000..e09f9ed --- /dev/null +++ b/src/auth/passdb-passwd.c @@ -0,0 +1,128 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +#include "auth-common.h" +#include "passdb.h" + +#ifdef PASSDB_PASSWD + +#include "safe-memset.h" +#include "ipwd.h" + +#define PASSWD_CACHE_KEY "%u" +#define PASSWD_PASS_SCHEME "CRYPT" + +static enum passdb_result +passwd_lookup(struct auth_request *request, struct passwd *pw_r) +{ + e_debug(authdb_event(request), "lookup"); + + switch (i_getpwnam(request->fields.user, pw_r)) { + case -1: + e_error(authdb_event(request), + "getpwnam() failed: %m"); + return PASSDB_RESULT_INTERNAL_FAILURE; + case 0: + auth_request_log_unknown_user(request, AUTH_SUBSYS_DB); + return PASSDB_RESULT_USER_UNKNOWN; + } + + if (!IS_VALID_PASSWD(pw_r->pw_passwd)) { + e_info(authdb_event(request), + "invalid password field '%s'", pw_r->pw_passwd); + return PASSDB_RESULT_USER_DISABLED; + } + + /* save the password so cache can use it */ + auth_request_set_field(request, "password", pw_r->pw_passwd, + PASSWD_PASS_SCHEME); + return PASSDB_RESULT_OK; +} + +static void +passwd_verify_plain(struct auth_request *request, const char *password, + verify_plain_callback_t *callback) +{ + struct passwd pw; + enum passdb_result res; + int ret; + + res = passwd_lookup(request, &pw); + if (res != PASSDB_RESULT_OK) { + callback(res, request); + return; + } + /* check if the password is valid */ + ret = auth_request_password_verify(request, password, pw.pw_passwd, + PASSWD_PASS_SCHEME, AUTH_SUBSYS_DB); + + /* clear the passwords from memory */ + safe_memset(pw.pw_passwd, 0, strlen(pw.pw_passwd)); + + if (ret <= 0) { + callback(PASSDB_RESULT_PASSWORD_MISMATCH, request); + return; + } + + /* make sure we're using the username exactly as it's in the database */ + auth_request_set_field(request, "user", pw.pw_name, NULL); + + callback(PASSDB_RESULT_OK, request); +} + +static void +passwd_lookup_credentials(struct auth_request *request, + lookup_credentials_callback_t *callback) +{ + struct passwd pw; + enum passdb_result res; + + res = passwd_lookup(request, &pw); + if (res != PASSDB_RESULT_OK) { + callback(res, NULL, 0, request); + return; + } + /* make sure we're using the username exactly as it's in the database */ + auth_request_set_field(request, "user", pw.pw_name, NULL); + passdb_handle_credentials(PASSDB_RESULT_OK, pw.pw_passwd, + PASSWD_PASS_SCHEME, callback, request); +} + +static struct passdb_module * +passwd_preinit(pool_t pool, const char *args) +{ + struct passdb_module *module; + + module = p_new(pool, struct passdb_module, 1); + module->blocking = TRUE; + if (strcmp(args, "blocking=no") == 0) + module->blocking = FALSE; + else if (*args != '\0') + i_fatal("passdb passwd: Unknown setting: %s", args); + + module->default_cache_key = PASSWD_CACHE_KEY; + module->default_pass_scheme = PASSWD_PASS_SCHEME; + return module; +} + +static void passwd_deinit(struct passdb_module *module ATTR_UNUSED) +{ + endpwent(); +} + +struct passdb_module_interface passdb_passwd = { + "passwd", + + passwd_preinit, + NULL, + passwd_deinit, + + passwd_verify_plain, + passwd_lookup_credentials, + NULL +}; + +#else +struct passdb_module_interface passdb_passwd = { + .name = "passwd" +}; +#endif diff --git a/src/auth/passdb-shadow.c b/src/auth/passdb-shadow.c new file mode 100644 index 0000000..e312b45 --- /dev/null +++ b/src/auth/passdb-shadow.c @@ -0,0 +1,125 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +#include "auth-common.h" +#include "passdb.h" + +#ifdef PASSDB_SHADOW + +#include "safe-memset.h" + +#include <shadow.h> + +#define SHADOW_CACHE_KEY "%u" +#define SHADOW_PASS_SCHEME "CRYPT" + +static enum passdb_result +shadow_lookup(struct auth_request *request, struct spwd **spw_r) +{ + e_debug(authdb_event(request), "lookup"); + + *spw_r = getspnam(request->fields.user); + if (*spw_r == NULL) { + auth_request_log_unknown_user(request, AUTH_SUBSYS_DB); + return PASSDB_RESULT_USER_UNKNOWN; + } + + if (!IS_VALID_PASSWD((*spw_r)->sp_pwdp)) { + e_info(authdb_event(request), + "invalid password field"); + return PASSDB_RESULT_USER_DISABLED; + } + + /* save the password so cache can use it */ + auth_request_set_field(request, "password", (*spw_r)->sp_pwdp, + SHADOW_PASS_SCHEME); + return PASSDB_RESULT_OK; +} + +static void +shadow_verify_plain(struct auth_request *request, const char *password, + verify_plain_callback_t *callback) +{ + struct spwd *spw; + enum passdb_result res; + int ret; + + res = shadow_lookup(request, &spw); + if (res != PASSDB_RESULT_OK) { + callback(res, request); + return; + } + + /* check if the password is valid */ + ret = auth_request_password_verify(request, password, spw->sp_pwdp, + SHADOW_PASS_SCHEME, AUTH_SUBSYS_DB); + + /* clear the passwords from memory */ + safe_memset(spw->sp_pwdp, 0, strlen(spw->sp_pwdp)); + + if (ret <= 0) { + callback(PASSDB_RESULT_PASSWORD_MISMATCH, request); + return; + } + + /* make sure we're using the username exactly as it's in the database */ + auth_request_set_field(request, "user", spw->sp_namp, NULL); + + callback(PASSDB_RESULT_OK, request); +} + +static void +shadow_lookup_credentials(struct auth_request *request, + lookup_credentials_callback_t *callback) +{ + struct spwd *spw; + enum passdb_result res; + + res = shadow_lookup(request, &spw); + if (res != PASSDB_RESULT_OK) { + callback(res, NULL, 0, request); + return; + } + /* make sure we're using the username exactly as it's in the database */ + auth_request_set_field(request, "user", spw->sp_namp, NULL); + passdb_handle_credentials(PASSDB_RESULT_OK, spw->sp_pwdp, + SHADOW_PASS_SCHEME, callback, request); +} + +static struct passdb_module * +shadow_preinit(pool_t pool, const char *args) +{ + struct passdb_module *module; + + module = p_new(pool, struct passdb_module, 1); + module->blocking = TRUE; + if (strcmp(args, "blocking=no") == 0) + module->blocking = FALSE; + else if (*args != '\0') + i_fatal("passdb shadow: Unknown setting: %s", args); + + module->default_cache_key = SHADOW_CACHE_KEY; + module->default_pass_scheme = SHADOW_PASS_SCHEME; + return module; +} + +static void shadow_deinit(struct passdb_module *module ATTR_UNUSED) +{ + endspent(); +} + +struct passdb_module_interface passdb_shadow = { + "shadow", + + shadow_preinit, + NULL, + shadow_deinit, + + shadow_verify_plain, + shadow_lookup_credentials, + NULL +}; +#else +struct passdb_module_interface passdb_shadow = { + .name = "shadow" +}; +#endif diff --git a/src/auth/passdb-sql.c b/src/auth/passdb-sql.c new file mode 100644 index 0000000..2c8b131 --- /dev/null +++ b/src/auth/passdb-sql.c @@ -0,0 +1,312 @@ +/* Copyright (c) 2004-2018 Dovecot authors, see the included COPYING file */ + +#include "auth-common.h" +#include "passdb.h" + +#ifdef PASSDB_SQL + +#include "safe-memset.h" +#include "password-scheme.h" +#include "auth-cache.h" +#include "db-sql.h" + +#include <string.h> + +struct sql_passdb_module { + struct passdb_module module; + + struct db_sql_connection *conn; +}; + +struct passdb_sql_request { + struct auth_request *auth_request; + union { + verify_plain_callback_t *verify_plain; + lookup_credentials_callback_t *lookup_credentials; + set_credentials_callback_t *set_credentials; + } callback; +}; + +static void sql_query_save_results(struct sql_result *result, + struct passdb_sql_request *sql_request) +{ + struct auth_request *auth_request = sql_request->auth_request; + struct passdb_module *_module = auth_request->passdb->passdb; + struct sql_passdb_module *module = (struct sql_passdb_module *)_module; + unsigned int i, fields_count; + const char *name, *value; + + fields_count = sql_result_get_fields_count(result); + for (i = 0; i < fields_count; i++) { + name = sql_result_get_field_name(result, i); + value = sql_result_get_field_value(result, i); + + if (*name == '\0') + ; + else if (value == NULL) + auth_request_set_null_field(auth_request, name); + else { + auth_request_set_field(auth_request, name, value, + module->conn->set.default_pass_scheme); + } + } +} + +static void sql_query_callback(struct sql_result *result, + struct passdb_sql_request *sql_request) +{ + struct auth_request *auth_request = sql_request->auth_request; + struct passdb_module *_module = auth_request->passdb->passdb; + struct sql_passdb_module *module = (struct sql_passdb_module *)_module; + enum passdb_result passdb_result; + const char *password, *scheme; + int ret; + + passdb_result = PASSDB_RESULT_INTERNAL_FAILURE; + password = NULL; + + ret = sql_result_next_row(result); + if (ret >= 0) + db_sql_success(module->conn); + if (ret < 0) { + if (!module->conn->default_password_query) { + e_error(authdb_event(auth_request), + "Password query failed: %s", + sql_result_get_error(result)); + } else { + e_error(authdb_event(auth_request), + "Password query failed: %s " + "(using built-in default password_query: %s)", + sql_result_get_error(result), + module->conn->set.password_query); + } + } else if (ret == 0) { + auth_request_log_unknown_user(auth_request, AUTH_SUBSYS_DB); + passdb_result = PASSDB_RESULT_USER_UNKNOWN; + } else { + sql_query_save_results(result, sql_request); + + /* Note that we really want to check if the password field is + found. Just checking if password is set isn't enough, + because with proxies we might want to return NULL as + password. */ + if (sql_result_find_field(result, "password") < 0 && + sql_result_find_field(result, "password_noscheme") < 0) { + e_error(authdb_event(auth_request), + "Password query must return a field named " + "'password'"); + } else if (sql_result_next_row(result) > 0) { + e_error(authdb_event(auth_request), + "Password query returned multiple matches"); + } else if (auth_request->passdb_password == NULL && + !auth_fields_exists(auth_request->fields.extra_fields, + "nopassword")) { + passdb_result = auth_request_password_missing(auth_request); + } else { + /* passdb_password may change on the way, + so we'll need to strdup. */ + password = t_strdup(auth_request->passdb_password); + passdb_result = PASSDB_RESULT_OK; + } + } + + scheme = password_get_scheme(&password); + /* auth_request_set_field() sets scheme */ + i_assert(password == NULL || scheme != NULL); + + if (auth_request->wanted_credentials_scheme != NULL) { + passdb_handle_credentials(passdb_result, password, scheme, + sql_request->callback.lookup_credentials, + auth_request); + auth_request_unref(&auth_request); + return; + } + + /* verify plain */ + if (password == NULL) { + sql_request->callback.verify_plain(passdb_result, auth_request); + auth_request_unref(&auth_request); + return; + } + + ret = auth_request_password_verify(auth_request, + auth_request->mech_password, + password, scheme, AUTH_SUBSYS_DB); + + sql_request->callback.verify_plain(ret > 0 ? PASSDB_RESULT_OK : + PASSDB_RESULT_PASSWORD_MISMATCH, + auth_request); + auth_request_unref(&auth_request); +} + +static const char * +passdb_sql_escape(const char *str, const struct auth_request *auth_request) +{ + struct passdb_module *_module = auth_request->passdb->passdb; + struct sql_passdb_module *module = (struct sql_passdb_module *)_module; + + return sql_escape_string(module->conn->db, str); +} + +static void sql_lookup_pass(struct passdb_sql_request *sql_request) +{ + struct passdb_module *_module = + sql_request->auth_request->passdb->passdb; + struct sql_passdb_module *module = (struct sql_passdb_module *)_module; + const char *query, *error; + + if (t_auth_request_var_expand(module->conn->set.password_query, + sql_request->auth_request, + passdb_sql_escape, &query, &error) <= 0) { + e_debug(authdb_event(sql_request->auth_request), + "Failed to expand password_query=%s: %s", + module->conn->set.password_query, error); + sql_request->callback.verify_plain(PASSDB_RESULT_INTERNAL_FAILURE, + sql_request->auth_request); + return; + } + + e_debug(authdb_event(sql_request->auth_request), + "query: %s", query); + + auth_request_ref(sql_request->auth_request); + sql_query(module->conn->db, query, + sql_query_callback, sql_request); +} + +static void sql_verify_plain(struct auth_request *request, + const char *password ATTR_UNUSED, + verify_plain_callback_t *callback) +{ + struct passdb_sql_request *sql_request; + + sql_request = p_new(request->pool, struct passdb_sql_request, 1); + sql_request->auth_request = request; + sql_request->callback.verify_plain = callback; + + sql_lookup_pass(sql_request); +} + +static void sql_lookup_credentials(struct auth_request *request, + lookup_credentials_callback_t *callback) +{ + struct passdb_sql_request *sql_request; + + sql_request = p_new(request->pool, struct passdb_sql_request, 1); + sql_request->auth_request = request; + sql_request->callback.lookup_credentials = callback; + + sql_lookup_pass(sql_request); +} + +static void sql_set_credentials_callback(const struct sql_commit_result *sql_result, + struct passdb_sql_request *sql_request) +{ + struct passdb_module *_module = + sql_request->auth_request->passdb->passdb; + struct sql_passdb_module *module = (struct sql_passdb_module *)_module; + + if (sql_result->error != NULL) { + if (!module->conn->default_update_query) { + auth_request_log_error(sql_request->auth_request, + AUTH_SUBSYS_DB, + "Set credentials query failed: %s", sql_result->error); + } else { + auth_request_log_error(sql_request->auth_request, + AUTH_SUBSYS_DB, + "Set credentials query failed: %s" + "(using built-in default update_query: %s)", + sql_result->error, module->conn->set.update_query); + } + } + + sql_request->callback. + set_credentials(sql_result->error == NULL, sql_request->auth_request); + i_free(sql_request); +} + +static void sql_set_credentials(struct auth_request *request, + const char *new_credentials, + set_credentials_callback_t *callback) +{ + struct sql_passdb_module *module = + (struct sql_passdb_module *) request->passdb->passdb; + struct sql_transaction_context *transaction; + struct passdb_sql_request *sql_request; + const char *query, *error; + + request->mech_password = p_strdup(request->pool, new_credentials); + + if (t_auth_request_var_expand(module->conn->set.update_query, + request, passdb_sql_escape, + &query, &error) <= 0) { + e_error(authdb_event(request), + "Failed to expand update_query=%s: %s", + module->conn->set.update_query, error); + callback(FALSE, request); + return; + } + + sql_request = i_new(struct passdb_sql_request, 1); + sql_request->auth_request = request; + sql_request->callback.set_credentials = callback; + + transaction = sql_transaction_begin(module->conn->db); + sql_update(transaction, query); + sql_transaction_commit(&transaction, + sql_set_credentials_callback, sql_request); +} + +static struct passdb_module * +passdb_sql_preinit(pool_t pool, const char *args) +{ + struct sql_passdb_module *module; + struct db_sql_connection *conn; + + module = p_new(pool, struct sql_passdb_module, 1); + module->conn = conn = db_sql_init(args, FALSE); + + module->module.default_cache_key = + auth_cache_parse_key(pool, conn->set.password_query); + module->module.default_pass_scheme = conn->set.default_pass_scheme; + return &module->module; +} + +static void passdb_sql_init(struct passdb_module *_module) +{ + struct sql_passdb_module *module = + (struct sql_passdb_module *)_module; + enum sql_db_flags flags; + + flags = sql_get_flags(module->conn->db); + module->module.blocking = (flags & SQL_DB_FLAG_BLOCKING) != 0; + + if (!module->module.blocking || worker) + db_sql_connect(module->conn); + db_sql_check_userdb_warning(module->conn); +} + +static void passdb_sql_deinit(struct passdb_module *_module) +{ + struct sql_passdb_module *module = + (struct sql_passdb_module *)_module; + + db_sql_unref(&module->conn); +} + +struct passdb_module_interface passdb_sql = { + "sql", + + passdb_sql_preinit, + passdb_sql_init, + passdb_sql_deinit, + + sql_verify_plain, + sql_lookup_credentials, + sql_set_credentials +}; +#else +struct passdb_module_interface passdb_sql = { + .name = "sql" +}; +#endif diff --git a/src/auth/passdb-static.c b/src/auth/passdb-static.c new file mode 100644 index 0000000..f43123f --- /dev/null +++ b/src/auth/passdb-static.c @@ -0,0 +1,120 @@ +/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */ + +#include "auth-common.h" +#include "passdb.h" +#include "passdb-template.h" +#include "password-scheme.h" + +struct static_passdb_module { + struct passdb_module module; + struct passdb_template *tmpl; + const char *static_password_tmpl; +}; + +static enum passdb_result +static_save_fields(struct auth_request *request, const char **password_r, + const char **scheme_r) +{ + struct static_passdb_module *module = + (struct static_passdb_module *)request->passdb->passdb; + const char *error; + + *password_r = NULL; + *scheme_r = NULL; + + e_debug(authdb_event(request), "lookup"); + if (passdb_template_export(module->tmpl, request, &error) < 0) { + e_error(authdb_event(request), + "Failed to expand template: %s", error); + return PASSDB_RESULT_INTERNAL_FAILURE; + } + + if (module->static_password_tmpl != NULL) { + if (t_auth_request_var_expand(module->static_password_tmpl, + request, NULL, password_r, &error) <= 0) { + e_error(authdb_event(request), + "Failed to expand password=%s: %s", + module->static_password_tmpl, error); + return PASSDB_RESULT_INTERNAL_FAILURE; + } + } else if (auth_fields_exists(request->fields.extra_fields, "nopassword")) { + *password_r = ""; + } else { + return auth_request_password_missing(request); + } + + *scheme_r = password_get_scheme(password_r); + + if (*scheme_r == NULL) + *scheme_r = STATIC_PASS_SCHEME; + + auth_request_set_field(request, "password", + *password_r, *scheme_r); + + return PASSDB_RESULT_OK; +} + +static void +static_verify_plain(struct auth_request *request, const char *password, + verify_plain_callback_t *callback) +{ + enum passdb_result result; + const char *static_password; + const char *static_scheme; + + int ret; + + result = static_save_fields(request, &static_password, &static_scheme); + if (result != PASSDB_RESULT_OK) { + callback(result, request); + return; + } + + ret = auth_request_password_verify(request, password, static_password, + static_scheme, AUTH_SUBSYS_DB); + if (ret <= 0) { + callback(PASSDB_RESULT_PASSWORD_MISMATCH, request); + return; + } + + callback(PASSDB_RESULT_OK, request); +} + +static void +static_lookup_credentials(struct auth_request *request, + lookup_credentials_callback_t *callback) +{ + enum passdb_result result; + const char *static_password; + const char *static_scheme; + + result = static_save_fields(request, &static_password, &static_scheme); + passdb_handle_credentials(result, static_password, + static_scheme, callback, request); +} + +static struct passdb_module * +static_preinit(pool_t pool, const char *args) +{ + struct static_passdb_module *module; + const char *value; + + module = p_new(pool, struct static_passdb_module, 1); + module->tmpl = passdb_template_build(pool, args); + + if (passdb_template_remove(module->tmpl, "password", &value)) + module->static_password_tmpl = value; + return &module->module; +} + +struct passdb_module_interface passdb_static = { + "static", + + static_preinit, + NULL, + NULL, + + static_verify_plain, + static_lookup_credentials, + NULL +}; diff --git a/src/auth/passdb-template.c b/src/auth/passdb-template.c new file mode 100644 index 0000000..3bb7f54 --- /dev/null +++ b/src/auth/passdb-template.c @@ -0,0 +1,102 @@ +/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */ + +#include "auth-common.h" +#include "array.h" +#include "str.h" +#include "passdb.h" +#include "passdb-template.h" + +struct passdb_template { + ARRAY(const char *) args; +}; + +struct passdb_template *passdb_template_build(pool_t pool, const char *args) +{ + struct passdb_template *tmpl; + const char *const *tmp, *key, *value; + + tmpl = p_new(pool, struct passdb_template, 1); + + tmp = t_strsplit_spaces(args, " "); + p_array_init(&tmpl->args, pool, str_array_length(tmp)); + + for (; *tmp != NULL; tmp++) { + value = strchr(*tmp, '='); + if (value == NULL) + key = *tmp; + else + key = t_strdup_until(*tmp, value++); + + if (*key == '\0') + i_fatal("Invalid passdb template %s - key must not be empty", + args); + + key = p_strdup(pool, key); + value = p_strdup(pool, value); + array_push_back(&tmpl->args, &key); + array_push_back(&tmpl->args, &value); + } + return tmpl; +} + +int passdb_template_export(struct passdb_template *tmpl, + struct auth_request *auth_request, + const char **error_r) +{ + const struct var_expand_table *table; + string_t *str; + const char *const *args, *value; + unsigned int i, count; + + if (passdb_template_is_empty(tmpl)) + return 0; + + str = t_str_new(256); + table = auth_request_get_var_expand_table(auth_request, NULL); + + args = array_get(&tmpl->args, &count); + i_assert((count % 2) == 0); + for (i = 0; i < count; i += 2) { + if (args[i+1] == NULL) + value = ""; + else { + str_truncate(str, 0); + if (auth_request_var_expand_with_table(str, args[i+1], + auth_request, table, NULL, error_r) <= 0) + return -1; + value = str_c(str); + } + auth_request_set_field(auth_request, args[i], value, + STATIC_PASS_SCHEME); + } + return 0; +} + +bool passdb_template_remove(struct passdb_template *tmpl, + const char *key, const char **value_r) +{ + const char *const *args; + unsigned int i, count; + + args = array_get(&tmpl->args, &count); + i_assert((count % 2) == 0); + for (i = 0; i < count; i += 2) { + if (strcmp(args[i], key) == 0) { + *value_r = args[i+1]; + array_delete(&tmpl->args, i, 2); + return TRUE; + } + } + return FALSE; +} + +bool passdb_template_is_empty(struct passdb_template *tmpl) +{ + return array_count(&tmpl->args) == 0; +} + +const char *const *passdb_template_get_args(struct passdb_template *tmpl, unsigned int *count_r) +{ + return array_get(&tmpl->args, count_r); +} + diff --git a/src/auth/passdb-template.h b/src/auth/passdb-template.h new file mode 100644 index 0000000..b681c80 --- /dev/null +++ b/src/auth/passdb-template.h @@ -0,0 +1,16 @@ +#ifndef PASSDB_TEMPLATE_H +#define PASSDB_TEMPLATE_H + +#define STATIC_PASS_SCHEME "PLAIN" + +struct passdb_template *passdb_template_build(pool_t pool, const char *args); +int passdb_template_export(struct passdb_template *tmpl, + struct auth_request *auth_request, + const char **error_r); +bool passdb_template_remove(struct passdb_template *tmpl, + const char *key, const char **value_r); +bool passdb_template_is_empty(struct passdb_template *tmpl); + +const char *const *passdb_template_get_args(struct passdb_template *tmpl, unsigned int *count_r); + +#endif diff --git a/src/auth/passdb.c b/src/auth/passdb.c new file mode 100644 index 0000000..9bc2b87 --- /dev/null +++ b/src/auth/passdb.c @@ -0,0 +1,351 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +#include "auth-common.h" +#include "array.h" +#include "password-scheme.h" +#include "auth-worker-server.h" +#include "passdb.h" + +static ARRAY(struct passdb_module_interface *) passdb_interfaces; +static ARRAY(struct passdb_module *) passdb_modules; + +static const struct passdb_module_interface passdb_iface_deinit = { + .name = "deinit" +}; + +static struct passdb_module_interface *passdb_interface_find(const char *name) +{ + struct passdb_module_interface *iface; + + array_foreach_elem(&passdb_interfaces, iface) { + if (strcmp(iface->name, name) == 0) + return iface; + } + return NULL; +} + +void passdb_register_module(struct passdb_module_interface *iface) +{ + struct passdb_module_interface *old_iface; + + old_iface = passdb_interface_find(iface->name); + if (old_iface != NULL && old_iface->verify_plain == NULL) { + /* replacing a "support not compiled in" passdb */ + passdb_unregister_module(old_iface); + } else if (old_iface != NULL) { + i_panic("passdb_register_module(%s): Already registered", + iface->name); + } + array_push_back(&passdb_interfaces, &iface); +} + +void passdb_unregister_module(struct passdb_module_interface *iface) +{ + struct passdb_module_interface *const *ifaces; + unsigned int idx; + + array_foreach(&passdb_interfaces, ifaces) { + if (*ifaces == iface) { + idx = array_foreach_idx(&passdb_interfaces, ifaces); + array_delete(&passdb_interfaces, idx, 1); + return; + } + } + i_panic("passdb_unregister_module(%s): Not registered", iface->name); +} + +bool passdb_get_credentials(struct auth_request *auth_request, + const char *input, const char *input_scheme, + const unsigned char **credentials_r, size_t *size_r) +{ + const char *wanted_scheme = auth_request->wanted_credentials_scheme; + const char *plaintext, *error; + int ret; + struct password_generate_params pwd_gen_params; + + if (auth_request->prefer_plain_credentials && + password_scheme_is_alias(input_scheme, "PLAIN")) { + /* we've a plaintext scheme and we prefer to get it instead + of converting it to the fallback scheme */ + wanted_scheme = ""; + } + + ret = password_decode(input, input_scheme, + credentials_r, size_r, &error); + if (ret <= 0) { + if (ret < 0) { + e_error(authdb_event(auth_request), + "Password data is not valid for scheme %s: %s", + input_scheme, error); + } else { + e_error(authdb_event(auth_request), + "Unknown scheme %s", input_scheme); + } + return FALSE; + } + + if (*wanted_scheme == '\0') { + /* anything goes. change the wanted_credentials_scheme to what + we actually got, so blocking passdbs work. */ + auth_request->wanted_credentials_scheme = + p_strdup(auth_request->pool, t_strcut(input_scheme, '.')); + return TRUE; + } + + if (!password_scheme_is_alias(input_scheme, wanted_scheme)) { + if (!password_scheme_is_alias(input_scheme, "PLAIN")) { + const char *error = t_strdup_printf( + "Requested %s scheme, but we have only %s", + wanted_scheme, input_scheme); + if (auth_request->set->debug_passwords) { + error = t_strdup_printf("%s (input: %s)", + error, input); + } + e_info(authdb_event(auth_request), + "%s", error); + return FALSE; + } + + /* we can generate anything out of plaintext passwords */ + plaintext = t_strndup(*credentials_r, *size_r); + i_zero(&pwd_gen_params); + pwd_gen_params.user = auth_request->fields.original_username; + if (!auth_request->domain_is_realm && + strchr(pwd_gen_params.user, '@') != NULL) { + /* domain must not be used as realm. add the @realm. */ + pwd_gen_params.user = t_strconcat(pwd_gen_params.user, "@", + auth_request->fields.realm, NULL); + } + if (auth_request->set->debug_passwords) { + e_debug(authdb_event(auth_request), + "Generating %s from user '%s', password '%s'", + wanted_scheme, pwd_gen_params.user, plaintext); + } + if (!password_generate(plaintext, &pwd_gen_params, + wanted_scheme, credentials_r, size_r)) { + e_error(authdb_event(auth_request), + "Requested unknown scheme %s", wanted_scheme); + return FALSE; + } + } + + return TRUE; +} + +void passdb_handle_credentials(enum passdb_result result, + const char *password, const char *scheme, + lookup_credentials_callback_t *callback, + struct auth_request *auth_request) +{ + const unsigned char *credentials = NULL; + size_t size = 0; + + if (result != PASSDB_RESULT_OK) { + callback(result, NULL, 0, auth_request); + return; + } else if (auth_fields_exists(auth_request->fields.extra_fields, + "noauthenticate")) { + callback(PASSDB_RESULT_NEXT, NULL, 0, auth_request); + return; + } + + if (password != NULL) { + if (!passdb_get_credentials(auth_request, password, scheme, + &credentials, &size)) + result = PASSDB_RESULT_SCHEME_NOT_AVAILABLE; + } else if (*auth_request->wanted_credentials_scheme == '\0') { + /* We're doing a passdb lookup (not authenticating). + Pass through a NULL password without an error. */ + } else if (auth_request->fields.delayed_credentials != NULL) { + /* We already have valid credentials from an earlier + passdb lookup. auth_request_lookup_credentials_finish() + will use them. */ + } else { + e_info(authdb_event(auth_request), + "Requested %s scheme, but we have a NULL password", + auth_request->wanted_credentials_scheme); + result = PASSDB_RESULT_SCHEME_NOT_AVAILABLE; + } + + callback(result, credentials, size, auth_request); +} + +static struct passdb_module * +passdb_find(const char *driver, const char *args, unsigned int *idx_r) +{ + struct passdb_module *const *passdbs; + unsigned int i, count; + + passdbs = array_get(&passdb_modules, &count); + for (i = 0; i < count; i++) { + if (strcmp(passdbs[i]->iface.name, driver) == 0 && + strcmp(passdbs[i]->args, args) == 0) { + *idx_r = i; + return passdbs[i]; + } + } + return NULL; +} + +struct passdb_module * +passdb_preinit(pool_t pool, const struct auth_passdb_settings *set) +{ + static unsigned int auth_passdb_id = 0; + struct passdb_module_interface *iface; + struct passdb_module *passdb; + unsigned int idx; + + iface = passdb_interface_find(set->driver); + if (iface == NULL || iface->verify_plain == NULL) { + /* maybe it's a plugin. try to load it. */ + auth_module_load(t_strconcat("authdb_", set->driver, NULL)); + iface = passdb_interface_find(set->driver); + } + if (iface == NULL) + i_fatal("Unknown passdb driver '%s'", set->driver); + if (iface->verify_plain == NULL) { + i_fatal("Support not compiled in for passdb driver '%s'", + set->driver); + } + if (iface->preinit == NULL && iface->init == NULL && + *set->args != '\0') { + i_fatal("passdb %s: No args are supported: %s", + set->driver, set->args); + } + + passdb = passdb_find(set->driver, set->args, &idx); + if (passdb != NULL) + return passdb; + + if (iface->preinit == NULL) + passdb = p_new(pool, struct passdb_module, 1); + else + passdb = iface->preinit(pool, set->args); + passdb->id = ++auth_passdb_id; + passdb->iface = *iface; + passdb->args = p_strdup(pool, set->args); + if (*set->mechanisms == '\0') { + passdb->mechanisms = NULL; + } else if (strcasecmp(set->mechanisms, "none") == 0) { + passdb->mechanisms = (const char *const[]){NULL}; + } else { + passdb->mechanisms = (const char* const*)p_strsplit_spaces(pool, set->mechanisms, " ,"); + } + + if (*set->username_filter == '\0') { + passdb->username_filter = NULL; + } else { + passdb->username_filter = (const char* const*)p_strsplit_spaces(pool, set->username_filter, " ,"); + } + array_push_back(&passdb_modules, &passdb); + return passdb; +} + +void passdb_init(struct passdb_module *passdb) +{ + if (passdb->iface.init != NULL && passdb->init_refcount == 0) + passdb->iface.init(passdb); + passdb->init_refcount++; +} + +void passdb_deinit(struct passdb_module *passdb) +{ + unsigned int idx; + + i_assert(passdb->init_refcount > 0); + + if (--passdb->init_refcount > 0) + return; + + if (passdb_find(passdb->iface.name, passdb->args, &idx) == NULL) + i_unreached(); + array_delete(&passdb_modules, idx, 1); + + if (passdb->iface.deinit != NULL) + passdb->iface.deinit(passdb); + + /* make sure passdb isn't accessed again */ + passdb->iface = passdb_iface_deinit; +} + +void passdbs_generate_md5(unsigned char md5[STATIC_ARRAY MD5_RESULTLEN]) +{ + struct md5_context ctx; + struct passdb_module *const *passdbs; + unsigned int i, count; + + md5_init(&ctx); + passdbs = array_get(&passdb_modules, &count); + for (i = 0; i < count; i++) { + md5_update(&ctx, &passdbs[i]->id, sizeof(passdbs[i]->id)); + md5_update(&ctx, passdbs[i]->iface.name, + strlen(passdbs[i]->iface.name)); + md5_update(&ctx, passdbs[i]->args, strlen(passdbs[i]->args)); + } + md5_final(&ctx, md5); +} + +const char * +passdb_result_to_string(enum passdb_result result) +{ + switch (result) { + case PASSDB_RESULT_INTERNAL_FAILURE: + return "internal_failure"; + case PASSDB_RESULT_SCHEME_NOT_AVAILABLE: + return "scheme_not_available"; + case PASSDB_RESULT_USER_UNKNOWN: + return "user_unknown"; + case PASSDB_RESULT_USER_DISABLED: + return "user_disabled"; + case PASSDB_RESULT_PASS_EXPIRED: + return "pass_expired"; + case PASSDB_RESULT_NEXT: + return "next"; + case PASSDB_RESULT_PASSWORD_MISMATCH: + return "password_mismatch"; + case PASSDB_RESULT_OK: + return "ok"; + } + i_unreached(); +} + +extern struct passdb_module_interface passdb_passwd; +extern struct passdb_module_interface passdb_bsdauth; +extern struct passdb_module_interface passdb_dict; +#ifdef HAVE_LUA +extern struct passdb_module_interface passdb_lua; +#endif +extern struct passdb_module_interface passdb_shadow; +extern struct passdb_module_interface passdb_passwd_file; +extern struct passdb_module_interface passdb_pam; +extern struct passdb_module_interface passdb_checkpassword; +extern struct passdb_module_interface passdb_ldap; +extern struct passdb_module_interface passdb_sql; +extern struct passdb_module_interface passdb_static; +extern struct passdb_module_interface passdb_oauth2; + +void passdbs_init(void) +{ + i_array_init(&passdb_interfaces, 16); + i_array_init(&passdb_modules, 16); + passdb_register_module(&passdb_passwd); + passdb_register_module(&passdb_bsdauth); + passdb_register_module(&passdb_dict); +#ifdef HAVE_LUA + passdb_register_module(&passdb_lua); +#endif + passdb_register_module(&passdb_passwd_file); + passdb_register_module(&passdb_pam); + passdb_register_module(&passdb_checkpassword); + passdb_register_module(&passdb_shadow); + passdb_register_module(&passdb_ldap); + passdb_register_module(&passdb_sql); + passdb_register_module(&passdb_static); + passdb_register_module(&passdb_oauth2); +} + +void passdbs_deinit(void) +{ + array_free(&passdb_modules); + array_free(&passdb_interfaces); +} diff --git a/src/auth/passdb.h b/src/auth/passdb.h new file mode 100644 index 0000000..b405aa7 --- /dev/null +++ b/src/auth/passdb.h @@ -0,0 +1,121 @@ +#ifndef PASSDB_H +#define PASSDB_H + +#include "md5.h" + +#define IS_VALID_PASSWD(pass) \ + ((pass)[0] != '\0' && (pass)[0] != '*' && (pass)[0] != '!') + +struct auth_request; +struct auth_passdb_settings; + +enum passdb_result { + PASSDB_RESULT_INTERNAL_FAILURE = -1, + PASSDB_RESULT_SCHEME_NOT_AVAILABLE = -2, + + PASSDB_RESULT_USER_UNKNOWN = -3, + PASSDB_RESULT_USER_DISABLED = -4, + PASSDB_RESULT_PASS_EXPIRED = -5, + PASSDB_RESULT_NEXT = -6, + + PASSDB_RESULT_PASSWORD_MISMATCH = 0, + PASSDB_RESULT_OK = 1 +}; + +typedef void verify_plain_callback_t(enum passdb_result result, + struct auth_request *request); +typedef void verify_plain_continue_callback_t(struct auth_request *request, + verify_plain_callback_t *callback); +typedef void lookup_credentials_callback_t(enum passdb_result result, + const unsigned char *credentials, + size_t size, + struct auth_request *request); +typedef void set_credentials_callback_t(bool success, + struct auth_request *request); + +struct passdb_module_interface { + const char *name; + + struct passdb_module *(*preinit)(pool_t pool, const char *args); + void (*init)(struct passdb_module *module); + void (*deinit)(struct passdb_module *module); + + /* Check if plaintext password matches */ + void (*verify_plain)(struct auth_request *request, const char *password, + verify_plain_callback_t *callback); + + /* Return authentication credentials, set in + auth_request->credentials. */ + void (*lookup_credentials)(struct auth_request *request, + lookup_credentials_callback_t *callback); + + /* Update credentials */ + void (*set_credentials)(struct auth_request *request, + const char *new_credentials, + set_credentials_callback_t *callback); +}; + +struct passdb_module { + const char *args; + /* The default caching key for this module, or NULL if caching isn't + wanted. This is updated by settings in auth_passdb. */ + const char *default_cache_key; + /* Default password scheme for this module. + If default_cache_key is set, must not be NULL. */ + const char *default_pass_scheme; + /* Supported authentication mechanisms, NULL is all, [NULL] is none*/ + const char *const *mechanisms; + /* Username filter, NULL is no filter */ + const char *const *username_filter; + + /* If blocking is set to TRUE, use child processes to access + this passdb. */ + bool blocking; + /* id is used by blocking passdb to identify the passdb */ + unsigned int id; + + /* number of time init() has been called */ + int init_refcount; + + /* WARNING: avoid adding anything here that isn't based on args. + if you do, you need to change passdb.c:passdb_find() also to avoid + accidentally merging wrong passdbs. */ + + struct passdb_module_interface iface; +}; + +const char *passdb_result_to_string(enum passdb_result result); + +/* Try to get credentials in wanted scheme (request->credentials_scheme) from + given input. Returns FALSE if this wasn't possible (unknown scheme, + conversion not possible or invalid credentials). + + If wanted scheme is "", the credentials are returned as-is without any + checks. This is useful mostly just to see if there exist any credentials + at all. */ +bool passdb_get_credentials(struct auth_request *auth_request, + const char *input, const char *input_scheme, + const unsigned char **credentials_r, + size_t *size_r); + +void passdb_handle_credentials(enum passdb_result result, + const char *password, const char *scheme, + lookup_credentials_callback_t *callback, + struct auth_request *auth_request); + +struct passdb_module * +passdb_preinit(pool_t pool, const struct auth_passdb_settings *set); +void passdb_init(struct passdb_module *passdb); +void passdb_deinit(struct passdb_module *passdb); + +void passdb_register_module(struct passdb_module_interface *iface); +void passdb_unregister_module(struct passdb_module_interface *iface); + +void passdbs_generate_md5(unsigned char md5[STATIC_ARRAY MD5_RESULTLEN]); + +void passdbs_init(void); +void passdbs_deinit(void); + +#include "auth-request.h" + +#endif diff --git a/src/auth/password-scheme-crypt.c b/src/auth/password-scheme-crypt.c new file mode 100644 index 0000000..34febad --- /dev/null +++ b/src/auth/password-scheme-crypt.c @@ -0,0 +1,196 @@ +/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "mycrypt.h" +#include "password-scheme.h" +#include "crypt-blowfish.h" +#include "randgen.h" + +/* Lengths and limits for some crypt() algorithms. */ +#define CRYPT_BLF_ROUNDS_DEFAULT 5 +#define CRYPT_BLF_ROUNDS_MIN 4 +#define CRYPT_BLF_ROUNDS_MAX 31 +#define CRYPT_BLF_SALT_LEN 16 /* raw salt */ +#define CRYPT_BLF_PREFIX_LEN (7+22+1) /* $2.$nn$ + salt */ +#define CRYPT_BLF_BUFFER_LEN 128 +#define CRYPT_BLF_PREFIX "$2y" +#define CRYPT_SHA2_ROUNDS_DEFAULT 5000 +#define CRYPT_SHA2_ROUNDS_MIN 1000 +#define CRYPT_SHA2_ROUNDS_MAX 999999999 +#define CRYPT_SHA2_SALT_LEN 16 + +static void +crypt_generate_des(const char *plaintext, const struct password_generate_params *params ATTR_UNUSED, + const unsigned char **raw_password_r, size_t *size_r) +{ +#define CRYPT_SALT_LEN 2 + const char *password, *salt; + + salt = password_generate_salt(CRYPT_SALT_LEN); + password = t_strdup(mycrypt(plaintext, salt)); + *raw_password_r = (const unsigned char *)password; + *size_r = strlen(password); +} + +static void +crypt_generate_blowfish(const char *plaintext, const struct password_generate_params *params, + const unsigned char **raw_password_r, size_t *size_r) +{ + char salt[CRYPT_BLF_SALT_LEN]; + char password[CRYPT_BLF_BUFFER_LEN]; + char magic_salt[CRYPT_BLF_PREFIX_LEN]; + unsigned int rounds = params->rounds; + + if (rounds == 0) + rounds = CRYPT_BLF_ROUNDS_DEFAULT; + else if (rounds < CRYPT_BLF_ROUNDS_MIN) + rounds = CRYPT_BLF_ROUNDS_MIN; + else if (rounds > CRYPT_BLF_ROUNDS_MAX) + rounds = CRYPT_BLF_ROUNDS_MAX; + + random_fill(salt, CRYPT_BLF_SALT_LEN); + if (crypt_gensalt_blowfish_rn(CRYPT_BLF_PREFIX, rounds, + salt, CRYPT_BLF_SALT_LEN, + magic_salt, CRYPT_BLF_PREFIX_LEN) == NULL) + i_fatal("crypt_gensalt_blowfish_rn failed: %m"); + + if (crypt_blowfish_rn(plaintext, magic_salt, password, + CRYPT_BLF_BUFFER_LEN) == NULL) + i_fatal("crypt_blowfish_rn failed: %m"); + + *raw_password_r = (const unsigned char *)t_strdup(password); + *size_r = strlen(password); +} + +static int +crypt_verify_blowfish(const char *plaintext, const struct password_generate_params *params ATTR_UNUSED, + const unsigned char *raw_password, size_t size, + const char **error_r) +{ + const char *password; + const char *salt; + char crypted[CRYPT_BLF_BUFFER_LEN]; + + if (size == 0) { + /* the default mycrypt() handler would return match */ + return 0; + } + password = t_strndup(raw_password, size); + + if (size < CRYPT_BLF_PREFIX_LEN || + !str_begins(password, "$2") || + password[2] < 'a' || password[2] > 'z' || + password[3] != '$') { + *error_r = "Password is not blowfish password"; + return -1; + } + + salt = t_strndup(password, CRYPT_BLF_PREFIX_LEN); + if (crypt_blowfish_rn(plaintext, salt, crypted, CRYPT_BLF_BUFFER_LEN) == NULL) { + /* really shouldn't happen unless the system is broken */ + *error_r = t_strdup_printf("crypt_blowfish_rn failed: %m"); + return -1; + } + + return strcmp(crypted, password) == 0 ? 1 : 0; +} + +static void +crypt_generate_sha256(const char *plaintext, const struct password_generate_params *params, + const unsigned char **raw_password_r, size_t *size_r) +{ + const char *password, *salt, *magic_salt; + unsigned int rounds = params->rounds; + + if (rounds == 0) + rounds = CRYPT_SHA2_ROUNDS_DEFAULT; + else if (rounds < CRYPT_SHA2_ROUNDS_MIN) + rounds = CRYPT_SHA2_ROUNDS_MIN; + else if (rounds > CRYPT_SHA2_ROUNDS_MAX) + rounds = CRYPT_SHA2_ROUNDS_MAX; + + salt = password_generate_salt(CRYPT_SHA2_SALT_LEN); + if (rounds == CRYPT_SHA2_ROUNDS_DEFAULT) + magic_salt = t_strdup_printf("$5$%s", salt); + else + magic_salt = t_strdup_printf("$5$rounds=%u$%s", rounds, salt); + password = t_strdup(mycrypt(plaintext, magic_salt)); + *raw_password_r = (const unsigned char *)password; + *size_r = strlen(password); +} + +static void +crypt_generate_sha512(const char *plaintext, const struct password_generate_params *params, + const unsigned char **raw_password_r, size_t *size_r) +{ + const char *password, *salt, *magic_salt; + unsigned int rounds = params->rounds; + + if (rounds == 0) + rounds = CRYPT_SHA2_ROUNDS_DEFAULT; + else if (rounds < CRYPT_SHA2_ROUNDS_MIN) + rounds = CRYPT_SHA2_ROUNDS_MIN; + else if (rounds > CRYPT_SHA2_ROUNDS_MAX) + rounds = CRYPT_SHA2_ROUNDS_MAX; + + salt = password_generate_salt(CRYPT_SHA2_SALT_LEN); + if (rounds == CRYPT_SHA2_ROUNDS_DEFAULT) + magic_salt = t_strdup_printf("$6$%s", salt); + else + magic_salt = t_strdup_printf("$6$rounds=%u$%s", rounds, salt); + password = t_strdup(mycrypt(plaintext, magic_salt)); + *raw_password_r = (const unsigned char *)password; + *size_r = strlen(password); +} + +/* keep in sync with the crypt_schemes struct below */ +static const struct { + const char *key; + const char *salt; + const char *expected; +} sample[] = { + { "08/15!test~4711", "JB", "JBOZ0DgmtucwE" }, + { "08/15!test~4711", "$5$rounds=1000$0123456789abcdef", + "$5$rounds=1000$0123456789abcdef$K/DksR0DT01hGc8g/kt" + "9McEgrbFMKi9qrb1jehe7hn4" }, + { "08/15!test~4711", "$6$rounds=1000$0123456789abcdef", + "$6$rounds=1000$0123456789abcdef$ZIAd5WqfyLkpvsVCVUU1GrvqaZTq" + "vhJoouxdSqJO71l9Ld3tVrfOatEjarhghvEYADkq//LpDnTeO90tcbtHR1" } +}; + +/* keep in sync with the sample struct above */ +static const struct password_scheme crypt_schemes[] = { + { "DES-CRYPT", PW_ENCODING_NONE, 0, crypt_verify, + crypt_generate_des }, + { "SHA256-CRYPT", PW_ENCODING_NONE, 0, crypt_verify, + crypt_generate_sha256 }, + { "SHA512-CRYPT", PW_ENCODING_NONE, 0, crypt_verify, + crypt_generate_sha512 } +}; + +static const struct password_scheme blf_crypt_scheme = { + "BLF-CRYPT", PW_ENCODING_NONE, 0, crypt_verify_blowfish, + crypt_generate_blowfish +}; + +static const struct password_scheme default_crypt_scheme = { + "CRYPT", PW_ENCODING_NONE, 0, crypt_verify, + crypt_generate_blowfish +}; + +void password_scheme_register_crypt(void) +{ + unsigned int i; + const char *crypted; + + i_assert(N_ELEMENTS(crypt_schemes) == N_ELEMENTS(sample)); + + for (i = 0; i < N_ELEMENTS(crypt_schemes); i++) { + crypted = mycrypt(sample[i].key, sample[i].salt); + if (crypted != NULL && + (strcmp(crypted, sample[i].expected) == 0)) + password_scheme_register(&crypt_schemes[i]); + } + password_scheme_register(&blf_crypt_scheme); + password_scheme_register(&default_crypt_scheme); +} diff --git a/src/auth/password-scheme-md5crypt.c b/src/auth/password-scheme-md5crypt.c new file mode 100644 index 0000000..a75e3b2 --- /dev/null +++ b/src/auth/password-scheme-md5crypt.c @@ -0,0 +1,147 @@ +/* + * ---------------------------------------------------------------------------- + * "THE BEER-WARE LICENSE" (Revision 42): + * <phk@login.dknet.dk> wrote this file. As long as you retain this notice you + * can do whatever you want with this stuff. If we meet some day, and you think + * this stuff is worth it, you can buy me a beer in return. Poul-Henning Kamp + * ---------------------------------------------------------------------------- + */ + +/* + * Ported from FreeBSD to Linux, only minimal changes. --marekm + */ + +/* + * Adapted from shadow-19990607 by Tudor Bosman, tudorb@jm.nu + */ + +#include "lib.h" +#include "safe-memset.h" +#include "str.h" +#include "md5.h" +#include "password-scheme.h" + +static unsigned char itoa64[] = /* 0 ... 63 => ascii - 64 */ + "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + +static char magic[] = "$1$"; /* + * This string is magic for + * this algorithm. Having + * it this way, we can get + * get better later on + */ + +static void +to64(string_t *str, unsigned long v, int n) +{ + while (--n >= 0) { + str_append_c(str, itoa64[v&0x3f]); + v >>= 6; + } +} + +/* + * UNIX password + * + * Use MD5 for what it is best at... + */ + +const char *password_generate_md5_crypt(const char *pw, const char *salt) +{ + const char *sp,*ep; + unsigned char final[MD5_RESULTLEN]; + int sl,pl,i,j; + struct md5_context ctx,ctx1; + unsigned long l; + string_t *passwd; + size_t pw_len = strlen(pw); + + /* Refine the Salt first */ + sp = salt; + + /* If it starts with the magic string, then skip that */ + if (strncmp(sp, magic, sizeof(magic)-1) == 0) + sp += sizeof(magic)-1; + + /* It stops at the first '$', max 8 chars */ + for(ep=sp;*ep != '\0' && *ep != '$' && ep < (sp+8);ep++) + continue; + + /* get the length of the true salt */ + sl = ep - sp; + + md5_init(&ctx); + + /* The password first, since that is what is most unknown */ + md5_update(&ctx,pw,pw_len); + + /* Then our magic string */ + md5_update(&ctx,magic,sizeof(magic)-1); + + /* Then the raw salt */ + md5_update(&ctx,sp,sl); + + /* Then just as many characters of the MD5(pw,salt,pw) */ + md5_init(&ctx1); + md5_update(&ctx1,pw,pw_len); + md5_update(&ctx1,sp,sl); + md5_update(&ctx1,pw,pw_len); + md5_final(&ctx1,final); + for(pl = pw_len; pl > 0; pl -= MD5_RESULTLEN) + md5_update(&ctx,final,pl>MD5_RESULTLEN ? MD5_RESULTLEN : pl); + + /* Don't leave anything around in vm they could use. */ + safe_memset(final, 0, sizeof(final)); + + /* Then something really weird... */ + for (j=0,i = pw_len; i != 0; i >>= 1) + if ((i&1) != 0) + md5_update(&ctx, final+j, 1); + else + md5_update(&ctx, pw+j, 1); + + /* Now make the output string */ + passwd = t_str_new(sl + 64); + str_append(passwd, magic); + str_append_data(passwd, sp, sl); + str_append_c(passwd, '$'); + + md5_final(&ctx,final); + + /* + * and now, just to make sure things don't run too fast + * On a 60 Mhz Pentium this takes 34 msec, so you would + * need 30 seconds to build a 1000 entry dictionary... + */ + for(i=0;i<1000;i++) { + md5_init(&ctx1); + if((i & 1) != 0) + md5_update(&ctx1,pw,pw_len); + else + md5_update(&ctx1,final,MD5_RESULTLEN); + + if((i % 3) != 0) + md5_update(&ctx1,sp,sl); + + if((i % 7) != 0) + md5_update(&ctx1,pw,pw_len); + + if((i & 1) != 0) + md5_update(&ctx1,final,MD5_RESULTLEN); + else + md5_update(&ctx1,pw,pw_len); + md5_final(&ctx1,final); + } + + l = (final[ 0]<<16) | (final[ 6]<<8) | final[12]; to64(passwd,l,4); + l = (final[ 1]<<16) | (final[ 7]<<8) | final[13]; to64(passwd,l,4); + l = (final[ 2]<<16) | (final[ 8]<<8) | final[14]; to64(passwd,l,4); + l = (final[ 3]<<16) | (final[ 9]<<8) | final[15]; to64(passwd,l,4); + l = (final[ 4]<<16) | (final[10]<<8) | final[ 5]; to64(passwd,l,4); + l = final[11] ; to64(passwd,l,2); + + /* Don't leave anything around in vm they could use. */ + safe_memset(final, 0, sizeof(final)); + + return str_c(passwd); +} diff --git a/src/auth/password-scheme-otp.c b/src/auth/password-scheme-otp.c new file mode 100644 index 0000000..ad7951b --- /dev/null +++ b/src/auth/password-scheme-otp.c @@ -0,0 +1,40 @@ +/* + * OTP password scheme. + * + * Copyright (c) 2006 Andrey Panin <pazke@donpac.ru> + * + * This software is released under the MIT license. + */ + +#include "lib.h" +#include "hex-binary.h" +#include "password-scheme.h" +#include "randgen.h" +#include "otp.h" + +int password_generate_otp(const char *pw, const char *state_data, + unsigned int algo, const char **result_r) +{ + struct otp_state state; + + if (state_data != NULL) { + if (otp_parse_dbentry(state_data, &state) != 0) + return -1; + } else { + /* Generate new OTP credentials from plaintext */ + unsigned char random_data[OTP_MAX_SEED_LEN / 2]; + const char *random_hex; + + random_fill(random_data, sizeof(random_data)); + random_hex = binary_to_hex(random_data, sizeof(random_data)); + if (i_strocpy(state.seed, random_hex, sizeof(state.seed)) < 0) + i_unreached(); + + state.seq = 1024; + state.algo = algo; + } + + otp_hash(state.algo, state.seed, pw, state.seq, state.hash); + *result_r = otp_print_dbentry(&state); + return 0; +} diff --git a/src/auth/password-scheme-pbkdf2.c b/src/auth/password-scheme-pbkdf2.c new file mode 100644 index 0000000..1e8eb3b --- /dev/null +++ b/src/auth/password-scheme-pbkdf2.c @@ -0,0 +1,82 @@ +/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "buffer.h" +#include "str.h" +#include "password-scheme.h" +#include "hex-binary.h" +#include "hash-method.h" +#include "pkcs5.h" + +#define PBKDF2_KEY_SIZE_SHA1 20 + +#define PBKDF2_GENERATE_SALT_LEN 16 +#define PBKDF2_ROUNDS_DEFAULT 5000 + +static void +pbkdf_run(const char *plaintext, const char *salt, + unsigned int rounds, unsigned char key_r[PBKDF2_KEY_SIZE_SHA1]) +{ + memset(key_r, 0, PBKDF2_KEY_SIZE_SHA1); + buffer_t buf; + buffer_create_from_data(&buf, key_r, PBKDF2_KEY_SIZE_SHA1); + + pkcs5_pbkdf(PKCS5_PBKDF2, hash_method_lookup("sha1"), + (const unsigned char *)plaintext, strlen(plaintext), + (const unsigned char *)salt, strlen(salt), + rounds, PBKDF2_KEY_SIZE_SHA1, &buf); +} + +void pbkdf2_generate(const char *plaintext, const struct password_generate_params *params ATTR_UNUSED, + const unsigned char **raw_password_r, size_t *size_r) +{ + unsigned char key[PBKDF2_KEY_SIZE_SHA1]; + const char *salt; + string_t *str = t_str_new(64); + unsigned int rounds = params->rounds; + + if (rounds == 0) + rounds = PBKDF2_ROUNDS_DEFAULT; + salt = password_generate_salt(PBKDF2_GENERATE_SALT_LEN); + pbkdf_run(plaintext, salt, rounds, key); + + str_printfa(str, "$1$%s$%u$", salt, rounds); + binary_to_hex_append(str, key, sizeof(key)); + + *raw_password_r = str_data(str); + *size_r = str_len(str); +} + +int pbkdf2_verify(const char *plaintext, const struct password_generate_params *params ATTR_UNUSED, + const unsigned char *raw_password, size_t size, + const char **error_r) +{ + const char *const *fields; + const char *salt; + unsigned int rounds; + unsigned char key1[PBKDF2_KEY_SIZE_SHA1], key2[PBKDF2_KEY_SIZE_SHA1]; + buffer_t buf; + + /* $1$salt$rounds$hash */ + if (size < 3 || memcmp(raw_password, "$1$", 3) != 0) { + *error_r = "Invalid PBKDF2 passdb entry prefix"; + return -1; + } + + fields = t_strsplit(t_strndup(raw_password + 3, size - 3), "$"); + salt = fields[0]; + if (str_array_length(fields) != 3 || + str_to_uint(fields[1], &rounds) < 0) { + *error_r = "Invalid PBKDF2 passdb entry format"; + return -1; + } + buffer_create_from_data(&buf, key1, sizeof(key1)); + if (strlen(fields[2]) != sizeof(key1)*2 || + hex_to_binary(fields[2], &buf) < 0) { + *error_r = "PBKDF2 hash not 160bit hex-encoded"; + return -1; + } + + pbkdf_run(plaintext, salt, rounds, key2); + return mem_equals_timing_safe(key1, key2, sizeof(key1)) ? 1 : 0; +} diff --git a/src/auth/password-scheme-scram.c b/src/auth/password-scheme-scram.c new file mode 100644 index 0000000..a395074 --- /dev/null +++ b/src/auth/password-scheme-scram.c @@ -0,0 +1,224 @@ +/* + * SCRAM-SHA-1 SASL authentication, see RFC-5802 + * + * Copyright (c) 2012 Florian Zeitz <florob@babelmonkeys.de> + * + * This software is released under the MIT license. + */ + + +#include "lib.h" +#include "safe-memset.h" +#include "base64.h" +#include "buffer.h" +#include "hmac.h" +#include "randgen.h" +#include "hash-method.h" +#include "sha1.h" +#include "sha2.h" +#include "str.h" +#include "password-scheme.h" + +/* SCRAM allowed iteration count range. RFC says it SHOULD be at least 4096 */ +#define SCRAM_MIN_ITERATE_COUNT 4096 +#define SCRAM_MAX_ITERATE_COUNT INT_MAX + +#define SCRAM_DEFAULT_ITERATE_COUNT 4096 + +static void +Hi(const struct hash_method *hmethod, const unsigned char *str, size_t str_size, + const unsigned char *salt, size_t salt_size, unsigned int i, + unsigned char *result) +{ + struct hmac_context ctx; + unsigned char U[hmethod->digest_size]; + unsigned int j, k; + + /* Calculate U1 */ + hmac_init(&ctx, str, str_size, hmethod); + hmac_update(&ctx, salt, salt_size); + hmac_update(&ctx, "\0\0\0\1", 4); + hmac_final(&ctx, U); + + memcpy(result, U, hmethod->digest_size); + + /* Calculate U2 to Ui and Hi */ + for (j = 2; j <= i; j++) { + hmac_init(&ctx, str, str_size, hmethod); + hmac_update(&ctx, U, sizeof(U)); + hmac_final(&ctx, U); + for (k = 0; k < hmethod->digest_size; k++) + result[k] ^= U[k]; + } +} + +int scram_scheme_parse(const struct hash_method *hmethod, const char *name, + const unsigned char *credentials, size_t size, + unsigned int *iter_count_r, const char **salt_r, + unsigned char stored_key_r[], + unsigned char server_key_r[], const char **error_r) +{ + const char *const *fields; + buffer_t *buf; + + /* password string format: iter,salt,stored_key,server_key */ + fields = t_strsplit(t_strndup(credentials, size), ","); + + if (str_array_length(fields) != 4) { + *error_r = t_strdup_printf( + "Invalid %s passdb entry format", name); + return -1; + } + if (str_to_uint(fields[0], iter_count_r) < 0 || + *iter_count_r < SCRAM_MIN_ITERATE_COUNT || + *iter_count_r > SCRAM_MAX_ITERATE_COUNT) { + *error_r = t_strdup_printf( + "Invalid %s iteration count in passdb", name); + return -1; + } + *salt_r = fields[1]; + + buf = t_buffer_create(hmethod->digest_size); + if (base64_decode(fields[2], strlen(fields[2]), NULL, buf) < 0 || + buf->used != hmethod->digest_size) { + *error_r = t_strdup_printf( + "Invalid %s StoredKey in passdb", name); + return -1; + } + memcpy(stored_key_r, buf->data, hmethod->digest_size); + + buffer_set_used_size(buf, 0); + if (base64_decode(fields[3], strlen(fields[3]), NULL, buf) < 0 || + buf->used != hmethod->digest_size) { + *error_r = t_strdup_printf( + "Invalid %s ServerKey in passdb", name); + return -1; + } + memcpy(server_key_r, buf->data, hmethod->digest_size); + return 0; +} + +int scram_verify(const struct hash_method *hmethod, const char *scheme_name, + const char *plaintext, const unsigned char *raw_password, + size_t size, const char **error_r) +{ + struct hmac_context ctx; + const char *salt_base64; + unsigned int iter_count; + const unsigned char *salt; + size_t salt_len; + unsigned char salted_password[hmethod->digest_size]; + unsigned char client_key[hmethod->digest_size]; + unsigned char stored_key[hmethod->digest_size]; + unsigned char calculated_stored_key[hmethod->digest_size]; + unsigned char server_key[hmethod->digest_size]; + int ret; + + if (scram_scheme_parse(hmethod, scheme_name, raw_password, size, + &iter_count, &salt_base64, + stored_key, server_key, error_r) < 0) + return -1; + + salt = buffer_get_data(t_base64_decode_str(salt_base64), &salt_len); + + /* FIXME: credentials should be SASLprepped UTF8 data here */ + Hi(hmethod, (const unsigned char *)plaintext, strlen(plaintext), + salt, salt_len, iter_count, salted_password); + + /* Calculate ClientKey */ + hmac_init(&ctx, salted_password, sizeof(salted_password), hmethod); + hmac_update(&ctx, "Client Key", 10); + hmac_final(&ctx, client_key); + + /* Calculate StoredKey */ + hash_method_get_digest(hmethod, client_key, sizeof(client_key), + calculated_stored_key); + ret = mem_equals_timing_safe(stored_key, calculated_stored_key, + sizeof(stored_key)) ? 1 : 0; + + safe_memset(salted_password, 0, sizeof(salted_password)); + safe_memset(client_key, 0, sizeof(client_key)); + safe_memset(stored_key, 0, sizeof(stored_key)); + + return ret; +} + +void scram_generate(const struct hash_method *hmethod, const char *plaintext, + const unsigned char **raw_password_r, size_t *size_r) +{ + string_t *str; + struct hmac_context ctx; + unsigned char salt[16]; + unsigned char salted_password[hmethod->digest_size]; + unsigned char client_key[hmethod->digest_size]; + unsigned char server_key[hmethod->digest_size]; + unsigned char stored_key[hmethod->digest_size]; + + random_fill(salt, sizeof(salt)); + + str = t_str_new(MAX_BASE64_ENCODED_SIZE(sizeof(salt))); + str_printfa(str, "%d,", SCRAM_DEFAULT_ITERATE_COUNT); + base64_encode(salt, sizeof(salt), str); + + /* FIXME: credentials should be SASLprepped UTF8 data here */ + Hi(hmethod, (const unsigned char *)plaintext, strlen(plaintext), salt, + sizeof(salt), SCRAM_DEFAULT_ITERATE_COUNT, salted_password); + + /* Calculate ClientKey */ + hmac_init(&ctx, salted_password, sizeof(salted_password), hmethod); + hmac_update(&ctx, "Client Key", 10); + hmac_final(&ctx, client_key); + + /* Calculate StoredKey */ + hash_method_get_digest(hmethod, client_key, sizeof(client_key), + stored_key); + str_append_c(str, ','); + base64_encode(stored_key, sizeof(stored_key), str); + + /* Calculate ServerKey */ + hmac_init(&ctx, salted_password, sizeof(salted_password), hmethod); + hmac_update(&ctx, "Server Key", 10); + hmac_final(&ctx, server_key); + str_append_c(str, ','); + base64_encode(server_key, sizeof(server_key), str); + + safe_memset(salted_password, 0, sizeof(salted_password)); + safe_memset(client_key, 0, sizeof(client_key)); + safe_memset(server_key, 0, sizeof(server_key)); + safe_memset(stored_key, 0, sizeof(stored_key)); + + *raw_password_r = (const unsigned char *)str_c(str); + *size_r = str_len(str); +} + +int scram_sha1_verify(const char *plaintext, + const struct password_generate_params *params ATTR_UNUSED, + const unsigned char *raw_password, size_t size, + const char **error_r) +{ + return scram_verify(&hash_method_sha1, "SCRAM-SHA-1", plaintext, + raw_password, size, error_r); +} + +void scram_sha1_generate(const char *plaintext, + const struct password_generate_params *params ATTR_UNUSED, + const unsigned char **raw_password_r, size_t *size_r) +{ + scram_generate(&hash_method_sha1, plaintext, raw_password_r, size_r); +} + +int scram_sha256_verify(const char *plaintext, + const struct password_generate_params *params ATTR_UNUSED, + const unsigned char *raw_password, size_t size, + const char **error_r) +{ + return scram_verify(&hash_method_sha256, "SCRAM-SHA-256", plaintext, + raw_password, size, error_r); +} + +void scram_sha256_generate(const char *plaintext, + const struct password_generate_params *params ATTR_UNUSED, + const unsigned char **raw_password_r, size_t *size_r) +{ + scram_generate(&hash_method_sha256, plaintext, raw_password_r, size_r); +} diff --git a/src/auth/password-scheme-sodium.c b/src/auth/password-scheme-sodium.c new file mode 100644 index 0000000..3e2f6bd --- /dev/null +++ b/src/auth/password-scheme-sodium.c @@ -0,0 +1,92 @@ +/* Copyright (c) 2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "password-scheme.h" + +#ifdef HAVE_LIBSODIUM +#include <sodium.h> + +static void +generate_argon2i(const char *plaintext, const struct password_generate_params *params, + const unsigned char **raw_password_r, size_t *size_r) +{ + unsigned long long rounds = params->rounds; + size_t memlimit; + char result[crypto_pwhash_STRBYTES]; + + if (rounds == 0) + rounds = crypto_pwhash_argon2i_OPSLIMIT_INTERACTIVE; + + if (rounds >= crypto_pwhash_argon2i_OPSLIMIT_SENSITIVE) + memlimit = crypto_pwhash_argon2i_MEMLIMIT_SENSITIVE; + else if (rounds >= crypto_pwhash_argon2i_OPSLIMIT_MODERATE) + memlimit = crypto_pwhash_argon2i_MEMLIMIT_MODERATE; + else + memlimit = crypto_pwhash_argon2i_MEMLIMIT_INTERACTIVE; + + if (crypto_pwhash_argon2i_str(result, plaintext, strlen(plaintext), rounds, memlimit) < 0) + i_fatal("crypto_pwhash_argon2i_str failed: %m"); + *raw_password_r = (const unsigned char*)t_strdup(result); + *size_r = strlen(result); +} + +#ifdef crypto_pwhash_ALG_ARGON2ID13 +static void +generate_argon2id(const char *plaintext, const struct password_generate_params *params, + const unsigned char **raw_password_r, size_t *size_r) +{ + unsigned long long rounds = params->rounds; + size_t memlimit; + char result[crypto_pwhash_argon2id_STRBYTES]; + i_zero(&result); + + if (rounds == 0) + rounds = crypto_pwhash_argon2id_OPSLIMIT_INTERACTIVE; + + if (rounds >= crypto_pwhash_argon2id_OPSLIMIT_SENSITIVE) + memlimit = crypto_pwhash_argon2id_MEMLIMIT_SENSITIVE; + else if (rounds >= crypto_pwhash_argon2id_OPSLIMIT_MODERATE) + memlimit = crypto_pwhash_argon2id_MEMLIMIT_MODERATE; + else + memlimit = crypto_pwhash_argon2id_MEMLIMIT_INTERACTIVE; + + /* XXX: Bug in sodium-1.0.13, it expects rounds to be 3 */ + if (rounds < 3) + rounds = 3; + + if (crypto_pwhash_argon2id_str(result, plaintext, strlen(plaintext), rounds, memlimit) < 0) + i_fatal("crypto_pwhash_argon2id_str failed: %m"); + *raw_password_r = (const unsigned char*)t_strdup(result); + *size_r = strlen(result); +} +#endif + +static int +verify_argon2(const char *plaintext, const struct password_generate_params *params ATTR_UNUSED, + const unsigned char *raw_password, size_t size, + const char **error_r ATTR_UNUSED) +{ + const char *passwd = t_strndup(raw_password, size); + if (crypto_pwhash_str_verify(passwd, plaintext, strlen(plaintext)) < 0) + return 0; + return 1; +} + + +static const struct password_scheme sodium_schemes[] = { + { "ARGON2I", PW_ENCODING_NONE, 0, verify_argon2, + generate_argon2i }, +#ifdef crypto_pwhash_ALG_ARGON2ID13 + { "ARGON2ID", PW_ENCODING_NONE, 0, verify_argon2, + generate_argon2id }, +#endif +}; + +void password_scheme_register_sodium(void) +{ + if (sodium_init() != 0) + i_fatal("sodium_init() failed"); + for(size_t i = 0; i < N_ELEMENTS(sodium_schemes); i++) + password_scheme_register(&sodium_schemes[i]); +} +#endif diff --git a/src/auth/password-scheme.c b/src/auth/password-scheme.c new file mode 100644 index 0000000..c572cd3 --- /dev/null +++ b/src/auth/password-scheme.c @@ -0,0 +1,822 @@ +/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "array.h" +#include "hash.h" +#include "base64.h" +#include "hex-binary.h" +#include "md4.h" +#include "md5.h" +#include "hmac.h" +#include "hmac-cram-md5.h" +#include "mycrypt.h" +#include "randgen.h" +#include "sha1.h" +#include "sha2.h" +#include "otp.h" +#include "str.h" +#include "password-scheme.h" + +static const char salt_chars[] = + "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + +static HASH_TABLE(const char*, const struct password_scheme *) password_schemes; + +static const struct password_scheme * +password_scheme_lookup_name(const char *name) +{ + return hash_table_lookup(password_schemes, name); +} + +/* Lookup scheme and encoding by given name. The encoding is taken from + ".base64", ".b64" or ".hex" suffix if it exists, otherwise the default + encoding is used. */ +static const struct password_scheme * +password_scheme_lookup(const char *name, enum password_encoding *encoding_r) +{ + const struct password_scheme *scheme; + const char *encoding = NULL; + + *encoding_r = PW_ENCODING_NONE; + if ((encoding = strchr(name, '.')) != NULL) { + name = t_strdup_until(name, encoding); + encoding++; + } + + scheme = password_scheme_lookup_name(name); + if (scheme == NULL) + return NULL; + + if (encoding == NULL) + *encoding_r = scheme->default_encoding; + else if (strcasecmp(encoding, "b64") == 0 || + strcasecmp(encoding, "base64") == 0) + *encoding_r = PW_ENCODING_BASE64; + else if (strcasecmp(encoding, "hex") == 0) + *encoding_r = PW_ENCODING_HEX; + else { + /* unknown encoding. treat as invalid scheme. */ + return NULL; + } + return scheme; +} + +int password_verify(const char *plaintext, + const struct password_generate_params *params, + const char *scheme, const unsigned char *raw_password, + size_t size, const char **error_r) +{ + const struct password_scheme *s; + enum password_encoding encoding; + const unsigned char *generated; + size_t generated_size; + int ret; + + s = password_scheme_lookup(scheme, &encoding); + if (s == NULL) { + *error_r = "Unknown password scheme"; + return -1; + } + + if (s->password_verify != NULL) { + ret = s->password_verify(plaintext, params, raw_password, size, + error_r); + } else { + /* generic verification handler: generate the password and + compare it to the one in database */ + s->password_generate(plaintext, params, + &generated, &generated_size); + ret = size != generated_size ? 0 : + mem_equals_timing_safe(generated, raw_password, size) ? 1 : 0; + } + + if (ret == 0) + *error_r = AUTH_LOG_MSG_PASSWORD_MISMATCH; + return ret; +} + +const char *password_get_scheme(const char **password) +{ + const char *p, *scheme; + + if (*password == NULL) + return NULL; + + if (str_begins(*password, "$1$")) { + /* $1$<salt>$<password>[$<ignored>] */ + p = strchr(*password + 3, '$'); + if (p != NULL) { + /* stop at next '$' after password */ + p = strchr(p+1, '$'); + if (p != NULL) + *password = t_strdup_until(*password, p); + return "MD5-CRYPT"; + } + } + + if (**password != '{') + return NULL; + + p = strchr(*password, '}'); + if (p == NULL) + return NULL; + + scheme = t_strdup_until(*password + 1, p); + *password = p + 1; + return scheme; +} + +int password_decode(const char *password, const char *scheme, + const unsigned char **raw_password_r, size_t *size_r, + const char **error_r) +{ + const struct password_scheme *s; + enum password_encoding encoding; + buffer_t *buf; + size_t len; + bool guessed_encoding; + + *error_r = NULL; + + s = password_scheme_lookup(scheme, &encoding); + if (s == NULL) { + *error_r = "Unknown scheme"; + return 0; + } + + len = strlen(password); + if (encoding != PW_ENCODING_NONE && s->raw_password_len != 0 && + strchr(scheme, '.') == NULL) { + /* encoding not specified. we can guess quite well between + base64 and hex encodings. the only problem is distinguishing + 2 character strings, but there shouldn't be any that short + raw_password_lens. */ + encoding = len == s->raw_password_len * 2 ? + PW_ENCODING_HEX : PW_ENCODING_BASE64; + guessed_encoding = TRUE; + } else { + guessed_encoding = FALSE; + } + + switch (encoding) { + case PW_ENCODING_NONE: + *raw_password_r = (const unsigned char *)password; + *size_r = len; + break; + case PW_ENCODING_HEX: + buf = t_buffer_create(len / 2 + 1); + if (hex_to_binary(password, buf) == 0) { + *raw_password_r = buf->data; + *size_r = buf->used; + break; + } + if (!guessed_encoding) { + *error_r = "Input isn't valid HEX encoded data"; + return -1; + } + /* check if it's base64-encoded after all. some input lengths + produce matching hex and base64 encoded lengths. */ + /* fall through */ + case PW_ENCODING_BASE64: + buf = t_buffer_create(MAX_BASE64_DECODED_SIZE(len)); + if (base64_decode(password, len, NULL, buf) < 0) { + *error_r = "Input isn't valid base64 encoded data"; + return -1; + } + + *raw_password_r = buf->data; + *size_r = buf->used; + break; + } + if (s->raw_password_len != *size_r && s->raw_password_len != 0) { + /* password has invalid length */ + *error_r = t_strdup_printf( + "Input length isn't valid (%u instead of %u)", + (unsigned int)*size_r, s->raw_password_len); + return -1; + } + return 1; +} + +bool password_generate(const char *plaintext, const struct password_generate_params *params, + const char *scheme, + const unsigned char **raw_password_r, size_t *size_r) +{ + const struct password_scheme *s; + enum password_encoding encoding; + + s = password_scheme_lookup(scheme, &encoding); + if (s == NULL) + return FALSE; + + s->password_generate(plaintext, params, raw_password_r, size_r); + return TRUE; +} + +bool password_generate_encoded(const char *plaintext, const struct password_generate_params *params, + const char *scheme, const char **password_r) +{ + const struct password_scheme *s; + const unsigned char *raw_password; + enum password_encoding encoding; + string_t *str; + size_t size; + + s = password_scheme_lookup(scheme, &encoding); + if (s == NULL) + return FALSE; + + s->password_generate(plaintext, params, &raw_password, &size); + switch (encoding) { + case PW_ENCODING_NONE: + *password_r = t_strndup(raw_password, size); + break; + case PW_ENCODING_BASE64: + str = t_str_new(MAX_BASE64_ENCODED_SIZE(size) + 1); + base64_encode(raw_password, size, str); + *password_r = str_c(str); + break; + case PW_ENCODING_HEX: + *password_r = binary_to_hex(raw_password, size); + break; + } + return TRUE; +} + +const char *password_generate_salt(size_t len) +{ + char *salt; + salt = t_malloc_no0(len + 1); + for (size_t i = 0; i < len; i++) + salt[i] = salt_chars[i_rand_limit(sizeof(salt_chars) - 1)]; + salt[len] = '\0'; + return salt; +} + +bool password_scheme_is_alias(const char *scheme1, const char *scheme2) +{ + const struct password_scheme *s1 = NULL, *s2 = NULL; + + if (*scheme1 == '\0' || *scheme2 == '\0') + return FALSE; + + scheme1 = t_strcut(scheme1, '.'); + scheme2 = t_strcut(scheme2, '.'); + + if (strcasecmp(scheme1, scheme2) == 0) + return TRUE; + + s1 = hash_table_lookup(password_schemes, scheme1); + s2 = hash_table_lookup(password_schemes, scheme2); + + /* if they've the same generate function, they're equivalent */ + return s1 != NULL && s2 != NULL && + s1->password_generate == s2->password_generate; +} + +const char * +password_scheme_detect(const char *plain_password, const char *crypted_password, + const struct password_generate_params *params) +{ + struct hash_iterate_context *ctx; + const char *key; + const struct password_scheme *scheme; + const unsigned char *raw_password; + size_t raw_password_size; + const char *error; + + ctx = hash_table_iterate_init(password_schemes); + while (hash_table_iterate(ctx, password_schemes, &key, &scheme)) { + if (password_decode(crypted_password, scheme->name, + &raw_password, &raw_password_size, + &error) <= 0) + continue; + + if (password_verify(plain_password, params, scheme->name, + raw_password, raw_password_size, + &error) > 0) + break; + key = NULL; + } + hash_table_iterate_deinit(&ctx); + return key; +} + +int crypt_verify(const char *plaintext, const struct password_generate_params *params ATTR_UNUSED, + const unsigned char *raw_password, size_t size, + const char **error_r) +{ + const char *password, *crypted; + + if (size > 4 && raw_password[0] == '$' && raw_password[1] == '2' && + raw_password[3] == '$') + return password_verify(plaintext, params, "BLF-CRYPT", + raw_password, size, error_r); + + if (size == 0) { + /* the default mycrypt() handler would return match */ + return 0; + } + + password = t_strndup(raw_password, size); + crypted = mycrypt(plaintext, password); + if (crypted == NULL) { + /* really shouldn't happen unless the system is broken */ + *error_r = t_strdup_printf("crypt() failed: %m"); + return -1; + } + + return str_equals_timing_almost_safe(crypted, password) ? 1 : 0; +} + +static int +md5_verify(const char *plaintext, const struct password_generate_params *params, + const unsigned char *raw_password, size_t size, const char **error_r) +{ + const char *password, *str, *error; + const unsigned char *md5_password; + size_t md5_size; + + password = t_strndup(raw_password, size); + if (str_begins(password, "$1$")) { + /* MD5-CRYPT */ + str = password_generate_md5_crypt(plaintext, password); + return str_equals_timing_almost_safe(str, password) ? 1 : 0; + } else if (password_decode(password, "PLAIN-MD5", + &md5_password, &md5_size, &error) <= 0) { + *error_r = "Not a valid MD5-CRYPT or PLAIN-MD5 password"; + return -1; + } else { + return password_verify(plaintext, params, "PLAIN-MD5", + md5_password, md5_size, error_r); + } +} + +static int +md5_crypt_verify(const char *plaintext, const struct password_generate_params *params ATTR_UNUSED, + const unsigned char *raw_password, size_t size, + const char **error_r ATTR_UNUSED) +{ + const char *password, *str; + + password = t_strndup(raw_password, size); + str = password_generate_md5_crypt(plaintext, password); + return str_equals_timing_almost_safe(str, password) ? 1 : 0; +} + +static void +md5_crypt_generate(const char *plaintext, const struct password_generate_params *params ATTR_UNUSED, + const unsigned char **raw_password_r, size_t *size_r) +{ + const char *password; + const char *salt; + + salt = password_generate_salt(8); + + password = password_generate_md5_crypt(plaintext, salt); + *raw_password_r = (const unsigned char *)password; + *size_r = strlen(password); +} + +static void +sha1_generate(const char *plaintext, const struct password_generate_params *params ATTR_UNUSED, + const unsigned char **raw_password_r, size_t *size_r) +{ + unsigned char *digest; + + digest = t_malloc_no0(SHA1_RESULTLEN); + sha1_get_digest(plaintext, strlen(plaintext), digest); + + *raw_password_r = digest; + *size_r = SHA1_RESULTLEN; +} + +static void +sha256_generate(const char *plaintext, const struct password_generate_params *params ATTR_UNUSED, + const unsigned char **raw_password_r, size_t *size_r) +{ + unsigned char *digest; + + digest = t_malloc_no0(SHA256_RESULTLEN); + sha256_get_digest(plaintext, strlen(plaintext), digest); + + *raw_password_r = digest; + *size_r = SHA256_RESULTLEN; +} + +static void +sha512_generate(const char *plaintext, const struct password_generate_params *params ATTR_UNUSED, + const unsigned char **raw_password_r, size_t *size_r) +{ + unsigned char *digest; + + digest = t_malloc_no0(SHA512_RESULTLEN); + sha512_get_digest(plaintext, strlen(plaintext), digest); + + *raw_password_r = digest; + *size_r = SHA512_RESULTLEN; +} + +static void +ssha_generate(const char *plaintext, const struct password_generate_params *params ATTR_UNUSED, + const unsigned char **raw_password_r, size_t *size_r) +{ +#define SSHA_SALT_LEN 4 + unsigned char *digest, *salt; + struct sha1_ctxt ctx; + + digest = t_malloc_no0(SHA1_RESULTLEN + SSHA_SALT_LEN); + salt = digest + SHA1_RESULTLEN; + random_fill(salt, SSHA_SALT_LEN); + + sha1_init(&ctx); + sha1_loop(&ctx, plaintext, strlen(plaintext)); + sha1_loop(&ctx, salt, SSHA_SALT_LEN); + sha1_result(&ctx, digest); + + *raw_password_r = digest; + *size_r = SHA1_RESULTLEN + SSHA_SALT_LEN; +} + +static int ssha_verify(const char *plaintext, const struct password_generate_params *params ATTR_UNUSED, + const unsigned char *raw_password, size_t size, + const char **error_r) +{ + unsigned char sha1_digest[SHA1_RESULTLEN]; + struct sha1_ctxt ctx; + + /* format: <SHA1 hash><salt> */ + if (size <= SHA1_RESULTLEN) { + *error_r = "SSHA password is too short"; + return -1; + } + + sha1_init(&ctx); + sha1_loop(&ctx, plaintext, strlen(plaintext)); + sha1_loop(&ctx, raw_password + SHA1_RESULTLEN, size - SHA1_RESULTLEN); + sha1_result(&ctx, sha1_digest); + return mem_equals_timing_safe(sha1_digest, raw_password, SHA1_RESULTLEN) ? 1 : 0; +} + +static void +ssha256_generate(const char *plaintext, const struct password_generate_params *params ATTR_UNUSED, + const unsigned char **raw_password_r, size_t *size_r) +{ +#define SSHA256_SALT_LEN 4 + unsigned char *digest, *salt; + struct sha256_ctx ctx; + + digest = t_malloc_no0(SHA256_RESULTLEN + SSHA256_SALT_LEN); + salt = digest + SHA256_RESULTLEN; + random_fill(salt, SSHA256_SALT_LEN); + + sha256_init(&ctx); + sha256_loop(&ctx, plaintext, strlen(plaintext)); + sha256_loop(&ctx, salt, SSHA256_SALT_LEN); + sha256_result(&ctx, digest); + + *raw_password_r = digest; + *size_r = SHA256_RESULTLEN + SSHA256_SALT_LEN; +} + +static int ssha256_verify(const char *plaintext, const struct password_generate_params *params ATTR_UNUSED, + const unsigned char *raw_password, size_t size, + const char **error_r) +{ + unsigned char sha256_digest[SHA256_RESULTLEN]; + struct sha256_ctx ctx; + + /* format: <SHA256 hash><salt> */ + if (size <= SHA256_RESULTLEN) { + *error_r = "SSHA256 password is too short"; + return -1; + } + + sha256_init(&ctx); + sha256_loop(&ctx, plaintext, strlen(plaintext)); + sha256_loop(&ctx, raw_password + SHA256_RESULTLEN, + size - SHA256_RESULTLEN); + sha256_result(&ctx, sha256_digest); + return mem_equals_timing_safe(sha256_digest, raw_password, + SHA256_RESULTLEN) ? 1 : 0; +} + +static void +ssha512_generate(const char *plaintext, const struct password_generate_params *params ATTR_UNUSED, + const unsigned char **raw_password_r, size_t *size_r) +{ +#define SSHA512_SALT_LEN 4 + unsigned char *digest, *salt; + struct sha512_ctx ctx; + + digest = t_malloc_no0(SHA512_RESULTLEN + SSHA512_SALT_LEN); + salt = digest + SHA512_RESULTLEN; + random_fill(salt, SSHA512_SALT_LEN); + + sha512_init(&ctx); + sha512_loop(&ctx, plaintext, strlen(plaintext)); + sha512_loop(&ctx, salt, SSHA512_SALT_LEN); + sha512_result(&ctx, digest); + + *raw_password_r = digest; + *size_r = SHA512_RESULTLEN + SSHA512_SALT_LEN; +} + +static int ssha512_verify(const char *plaintext, const struct password_generate_params *params ATTR_UNUSED, + const unsigned char *raw_password, size_t size, + const char **error_r) +{ + unsigned char sha512_digest[SHA512_RESULTLEN]; + struct sha512_ctx ctx; + + /* format: <SHA512 hash><salt> */ + if (size <= SHA512_RESULTLEN) { + *error_r = "SSHA512 password is too short"; + return -1; + } + + sha512_init(&ctx); + sha512_loop(&ctx, plaintext, strlen(plaintext)); + sha512_loop(&ctx, raw_password + SHA512_RESULTLEN, + size - SHA512_RESULTLEN); + sha512_result(&ctx, sha512_digest); + return mem_equals_timing_safe(sha512_digest, raw_password, + SHA512_RESULTLEN) ? 1 : 0; +} + +static void +smd5_generate(const char *plaintext, const struct password_generate_params *params ATTR_UNUSED, + const unsigned char **raw_password_r, size_t *size_r) +{ +#define SMD5_SALT_LEN 4 + unsigned char *digest, *salt; + struct md5_context ctx; + + digest = t_malloc_no0(MD5_RESULTLEN + SMD5_SALT_LEN); + salt = digest + MD5_RESULTLEN; + random_fill(salt, SMD5_SALT_LEN); + + md5_init(&ctx); + md5_update(&ctx, plaintext, strlen(plaintext)); + md5_update(&ctx, salt, SMD5_SALT_LEN); + md5_final(&ctx, digest); + + *raw_password_r = digest; + *size_r = MD5_RESULTLEN + SMD5_SALT_LEN; +} + +static int smd5_verify(const char *plaintext, const struct password_generate_params *params ATTR_UNUSED, + const unsigned char *raw_password, size_t size, + const char **error_r) +{ + unsigned char md5_digest[MD5_RESULTLEN]; + struct md5_context ctx; + + /* format: <MD5 hash><salt> */ + if (size <= MD5_RESULTLEN) { + *error_r = "SMD5 password is too short"; + return -1; + } + + md5_init(&ctx); + md5_update(&ctx, plaintext, strlen(plaintext)); + md5_update(&ctx, raw_password + MD5_RESULTLEN, size - MD5_RESULTLEN); + md5_final(&ctx, md5_digest); + return mem_equals_timing_safe(md5_digest, raw_password, MD5_RESULTLEN) ? 1 : 0; +} + +static void +plain_generate(const char *plaintext, const struct password_generate_params *params ATTR_UNUSED, + const unsigned char **raw_password_r, size_t *size_r) +{ + *raw_password_r = (const unsigned char *)plaintext, + *size_r = strlen(plaintext); +} + +static int +plain_verify(const char *plaintext, const struct password_generate_params *params ATTR_UNUSED, + const unsigned char *raw_password, size_t size, + const char **error_r ATTR_UNUSED) +{ + size_t plaintext_len = strlen(plaintext); + + if (plaintext_len != size) + return 0; + return mem_equals_timing_safe(plaintext, raw_password, size) ? 1 : 0; +} + +static int +plain_trunc_verify(const char *plaintext, const struct password_generate_params *params ATTR_UNUSED, + const unsigned char *raw_password, size_t size, + const char **error_r) +{ + size_t i, plaintext_len, trunc_len = 0; + + /* format: <length>-<password> */ + for (i = 0; i < size; i++) { + if (raw_password[i] >= '0' && raw_password[i] <= '9') + trunc_len = trunc_len*10 + raw_password[i]-'0'; + else + break; + } + if (i == size || raw_password[i] != '-') { + *error_r = "PLAIN-TRUNC missing length: prefix"; + return -1; + } + i++; + + plaintext_len = strlen(plaintext); + if (size-i == trunc_len && plaintext_len >= trunc_len) { + /* possibly truncated password. allow the given password as + long as the prefix matches. */ + return mem_equals_timing_safe(raw_password+i, plaintext, trunc_len) ? 1 : 0; + } + return plaintext_len == size-i && + mem_equals_timing_safe(raw_password+i, plaintext, plaintext_len) ? 1 : 0; +} + +static void +cram_md5_generate(const char *plaintext, const struct password_generate_params *params ATTR_UNUSED, + const unsigned char **raw_password_r, size_t *size_r) +{ + struct hmac_context ctx; + unsigned char *context_digest; + + context_digest = t_malloc_no0(CRAM_MD5_CONTEXTLEN); + hmac_init(&ctx, (const unsigned char *)plaintext, + strlen(plaintext), &hash_method_md5); + hmac_md5_get_cram_context(&ctx, context_digest); + + *raw_password_r = context_digest; + *size_r = CRAM_MD5_CONTEXTLEN; +} + +static void +digest_md5_generate(const char *plaintext, const struct password_generate_params *params, + const unsigned char **raw_password_r, size_t *size_r) +{ + const char *realm, *str, *user; + unsigned char *digest; + + if (params->user == NULL) + i_fatal("digest_md5_generate(): username not given"); + + user = params->user; + + + /* assume user@realm format for username. If user@domain is wanted + in the username, allow also user@domain@realm. */ + realm = strrchr(user, '@'); + if (realm != NULL) { + user = t_strdup_until(user, realm); + realm++; + } else { + realm = ""; + } + + /* user:realm:passwd */ + digest = t_malloc_no0(MD5_RESULTLEN); + str = t_strdup_printf("%s:%s:%s", user, realm, plaintext); + md5_get_digest(str, strlen(str), digest); + + *raw_password_r = digest; + *size_r = MD5_RESULTLEN; +} + +static void +plain_md4_generate(const char *plaintext, const struct password_generate_params *params ATTR_UNUSED, + const unsigned char **raw_password_r, size_t *size_r) +{ + unsigned char *digest; + + digest = t_malloc_no0(MD4_RESULTLEN); + md4_get_digest(plaintext, strlen(plaintext), digest); + + *raw_password_r = digest; + *size_r = MD4_RESULTLEN; +} + +static void +plain_md5_generate(const char *plaintext, const struct password_generate_params *params ATTR_UNUSED, + const unsigned char **raw_password_r, size_t *size_r) +{ + unsigned char *digest; + + digest = t_malloc_no0(MD5_RESULTLEN); + md5_get_digest(plaintext, strlen(plaintext), digest); + + *raw_password_r = digest; + *size_r = MD5_RESULTLEN; +} + +static int otp_verify(const char *plaintext, const struct password_generate_params *params ATTR_UNUSED, + const unsigned char *raw_password, size_t size, + const char **error_r) +{ + const char *password, *generated; + + password = t_strndup(raw_password, size); + if (password_generate_otp(plaintext, password, UINT_MAX, &generated) < 0) { + *error_r = "Invalid OTP data in passdb"; + return -1; + } + + return strcasecmp(password, generated) == 0 ? 1 : 0; +} + +static void +otp_generate(const char *plaintext, const struct password_generate_params *params ATTR_UNUSED, + const unsigned char **raw_password_r, size_t *size_r) +{ + const char *password; + + if (password_generate_otp(plaintext, NULL, OTP_HASH_SHA1, &password) < 0) + i_unreached(); + *raw_password_r = (const unsigned char *)password; + *size_r = strlen(password); +} + +static const struct password_scheme builtin_schemes[] = { + { "MD5", PW_ENCODING_NONE, 0, md5_verify, md5_crypt_generate }, + { "MD5-CRYPT", PW_ENCODING_NONE, 0, + md5_crypt_verify, md5_crypt_generate }, + { "SHA", PW_ENCODING_BASE64, SHA1_RESULTLEN, NULL, sha1_generate }, + { "SHA1", PW_ENCODING_BASE64, SHA1_RESULTLEN, NULL, sha1_generate }, + { "SHA256", PW_ENCODING_BASE64, SHA256_RESULTLEN, + NULL, sha256_generate }, + { "SHA512", PW_ENCODING_BASE64, SHA512_RESULTLEN, + NULL, sha512_generate }, + { "SMD5", PW_ENCODING_BASE64, 0, smd5_verify, smd5_generate }, + { "SSHA", PW_ENCODING_BASE64, 0, ssha_verify, ssha_generate }, + { "SSHA256", PW_ENCODING_BASE64, 0, ssha256_verify, ssha256_generate }, + { "SSHA512", PW_ENCODING_BASE64, 0, ssha512_verify, ssha512_generate }, + { "PLAIN", PW_ENCODING_NONE, 0, plain_verify, plain_generate }, + { "CLEAR", PW_ENCODING_NONE, 0, plain_verify, plain_generate }, + { "CLEARTEXT", PW_ENCODING_NONE, 0, plain_verify, plain_generate }, + { "PLAIN-TRUNC", PW_ENCODING_NONE, 0, plain_trunc_verify, plain_generate }, + { "CRAM-MD5", PW_ENCODING_HEX, CRAM_MD5_CONTEXTLEN, + NULL, cram_md5_generate }, + { "SCRAM-SHA-1", PW_ENCODING_NONE, 0, scram_sha1_verify, + scram_sha1_generate}, + { "SCRAM-SHA-256", PW_ENCODING_NONE, 0, scram_sha256_verify, + scram_sha256_generate}, + { "HMAC-MD5", PW_ENCODING_HEX, CRAM_MD5_CONTEXTLEN, + NULL, cram_md5_generate }, + { "DIGEST-MD5", PW_ENCODING_HEX, MD5_RESULTLEN, + NULL, digest_md5_generate }, + { "PLAIN-MD4", PW_ENCODING_HEX, MD4_RESULTLEN, + NULL, plain_md4_generate }, + { "PLAIN-MD5", PW_ENCODING_HEX, MD5_RESULTLEN, + NULL, plain_md5_generate }, + { "LDAP-MD5", PW_ENCODING_BASE64, MD5_RESULTLEN, + NULL, plain_md5_generate }, + { "OTP", PW_ENCODING_NONE, 0, otp_verify, otp_generate }, + { "PBKDF2", PW_ENCODING_NONE, 0, pbkdf2_verify, pbkdf2_generate }, +}; + +void password_scheme_register(const struct password_scheme *scheme) +{ + if (password_scheme_lookup_name(scheme->name) != NULL) { + i_panic("password_scheme_register(%s): Already registered", + scheme->name); + } + hash_table_insert(password_schemes, scheme->name, scheme); +} + +void password_scheme_unregister(const struct password_scheme *scheme) +{ + if (!hash_table_try_remove(password_schemes, scheme->name)) + i_panic("password_scheme_unregister(%s): Not registered", scheme->name); +} + +void password_schemes_get(ARRAY_TYPE(password_scheme_p) *schemes_r) +{ + struct hash_iterate_context *ctx; + const char *key; + const struct password_scheme *scheme; + ctx = hash_table_iterate_init(password_schemes); + while(hash_table_iterate(ctx, password_schemes, &key, &scheme)) { + array_push_back(schemes_r, &scheme); + } + hash_table_iterate_deinit(&ctx); +} + +void password_schemes_init(void) +{ + unsigned int i; + + hash_table_create(&password_schemes, default_pool, + N_ELEMENTS(builtin_schemes)*2, strfastcase_hash, + strcasecmp); + for (i = 0; i < N_ELEMENTS(builtin_schemes); i++) + password_scheme_register(&builtin_schemes[i]); + password_scheme_register_crypt(); +#ifdef HAVE_LIBSODIUM + password_scheme_register_sodium(); +#endif +} + +void password_schemes_deinit(void) +{ + hash_table_destroy(&password_schemes); +} diff --git a/src/auth/password-scheme.h b/src/auth/password-scheme.h new file mode 100644 index 0000000..b3c3f56 --- /dev/null +++ b/src/auth/password-scheme.h @@ -0,0 +1,147 @@ +#ifndef PASSWORD_SCHEME_H +#define PASSWORD_SCHEME_H + +#define AUTH_LOG_MSG_PASSWORD_MISMATCH "Password mismatch" + +struct hash_method; + +enum password_encoding { + PW_ENCODING_NONE, + PW_ENCODING_BASE64, + PW_ENCODING_HEX +}; + +struct password_generate_params { + const char *user; + unsigned int rounds; +}; + +struct password_scheme { + const char *name; + enum password_encoding default_encoding; + /* If non-zero, this is the expected raw password length. + It can be used to automatically detect encoding between + hex and base64 encoded passwords. */ + unsigned int raw_password_len; + + int (*password_verify)(const char *plaintext, + const struct password_generate_params *params, + const unsigned char *raw_password, size_t size, + const char **error_r); + void (*password_generate)(const char *plaintext, + const struct password_generate_params *params, + const unsigned char **raw_password_r, + size_t *size_r); +}; +ARRAY_DEFINE_TYPE(password_scheme_p, const struct password_scheme *); +void password_schemes_get(ARRAY_TYPE(password_scheme_p) *schemes_r); + +extern unsigned int password_scheme_encryption_rounds; + +/* Returns 1 = matched, 0 = didn't match, -1 = unknown scheme or invalid + raw_password */ +int password_verify(const char *plaintext, + const struct password_generate_params *params, + const char *scheme, + const unsigned char *raw_password, size_t size, + const char **error_r); + +/* Extracts scheme from password, or returns NULL if it isn't found. + If auth_request is given, it's used for debug logging. */ +const char *password_get_scheme(const char **password); + +/* Decode encoded (base64/hex) password to raw form. Returns 1 if ok, + 0 if scheme is unknown, -1 if password is invalid. */ +int password_decode(const char *password, const char *scheme, + const unsigned char **raw_password_r, size_t *size_r, + const char **error_r); + +/* Create password with wanted scheme out of plaintext password and username. + Potential base64/hex directives are ignored in scheme. Returns FALSE if + the scheme is unknown. */ +bool password_generate(const char *plaintext, + const struct password_generate_params *params, + const char *scheme, + const unsigned char **raw_password_r, size_t *size_r); +/* Like above, but generate encoded passwords. If hex/base64 directive isn't + specified in the scheme, the default encoding for the scheme is used. + Returns FALSE if the scheme is unknown. */ +bool password_generate_encoded(const char *plaintext, + const struct password_generate_params *params, + const char *scheme, const char **password_r); + +/* Returns TRUE if schemes are equivalent. */ +bool password_scheme_is_alias(const char *scheme1, const char *scheme2); + +/* Try to detect in which scheme crypted password is. Returns the scheme name + or NULL if nothing was found. */ +const char * +password_scheme_detect(const char *plain_password, const char *crypted_password, + const struct password_generate_params *params); + +void password_scheme_register(const struct password_scheme *scheme); +void password_scheme_unregister(const struct password_scheme *scheme); + +void password_schemes_init(void); +void password_schemes_deinit(void); + +/* some password schemes/algorithms supports a variable number of + encryption rounds. */ +void password_set_encryption_rounds(unsigned int rounds); + +/* INTERNAL: */ +const char *password_generate_salt(size_t len); +const char *password_generate_md5_crypt(const char *pw, const char *salt); +int password_generate_otp(const char *pw, const char *state_data, + unsigned int algo, const char **result_r) + ATTR_NULL(2); + +int crypt_verify(const char *plaintext, + const struct password_generate_params *params, + const unsigned char *raw_password, size_t size, + const char **error_r); + +int scram_scheme_parse(const struct hash_method *hmethod, const char *name, + const unsigned char *credentials, size_t size, + unsigned int *iter_count_r, const char **salt_r, + unsigned char stored_key_r[], + unsigned char server_key_r[], const char **error_r); +int scram_verify(const struct hash_method *hmethod, const char *scheme_name, + const char *plaintext, const unsigned char *raw_password, + size_t size, const char **error_r); +void scram_generate(const struct hash_method *hmethod, const char *plaintext, + const unsigned char **raw_password_r, size_t *size_r); + +int scram_sha1_verify(const char *plaintext, + const struct password_generate_params *params ATTR_UNUSED, + const unsigned char *raw_password, size_t size, + const char **error_r ATTR_UNUSED); +void scram_sha1_generate(const char *plaintext, + const struct password_generate_params *params ATTR_UNUSED, + const unsigned char **raw_password_r, size_t *size_r); + +int scram_sha256_verify(const char *plaintext, + const struct password_generate_params *params ATTR_UNUSED, + const unsigned char *raw_password, size_t size, + const char **error_r); +void scram_sha256_generate(const char *plaintext, + const struct password_generate_params *params ATTR_UNUSED, + const unsigned char **raw_password_r, size_t *size_r); + +void pbkdf2_generate(const char *plaintext, + const struct password_generate_params *params ATTR_UNUSED, + const unsigned char **raw_password_r, size_t *size_r); +int pbkdf2_verify(const char *plaintext, + const struct password_generate_params *params ATTR_UNUSED, + const unsigned char *raw_password, size_t size, + const char **error_r); + +/* check which of the algorithms Blowfish, SHA-256 and SHA-512 are + supported by the used libc's/glibc's crypt() */ +void password_scheme_register_crypt(void); + +#ifdef HAVE_LIBSODIUM +void password_scheme_register_sodium(void); +#endif + +#endif diff --git a/src/auth/test-auth-cache.c b/src/auth/test-auth-cache.c new file mode 100644 index 0000000..ea71b1b --- /dev/null +++ b/src/auth/test-auth-cache.c @@ -0,0 +1,82 @@ +/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "str.h" +#include "auth-request.h" +#include "auth-cache.h" +#include "test-common.h" + +const struct var_expand_table +auth_request_var_expand_static_tab[AUTH_REQUEST_VAR_TAB_COUNT + 1] = { + /* these 3 must be in this order */ + { 'u', NULL, "user" }, + { 'n', NULL, "username" }, + { 'd', NULL, "domain" }, + + { 'a', NULL, NULL }, + { '\0', NULL, "longb" }, + { 'c', NULL, "longc" }, + { '\0', NULL, NULL } +}; + +struct var_expand_table * +auth_request_get_var_expand_table_full(const struct auth_request *auth_request ATTR_UNUSED, + const char *username ATTR_UNUSED, + auth_request_escape_func_t *escape_func ATTR_UNUSED, + unsigned int *count ATTR_UNUSED) +{ + i_unreached(); +} + +int auth_request_var_expand_with_table(string_t *dest, const char *str, + const struct auth_request *auth_request ATTR_UNUSED, + const struct var_expand_table *table ATTR_UNUSED, + auth_request_escape_func_t *escape_func ATTR_UNUSED, + const char **error_r ATTR_UNUSED) +{ + return var_expand(dest, str, auth_request_var_expand_static_tab, error_r); +} + +static void test_auth_cache_parse_key(void) +{ + static const struct { + const char *in, *out; + } tests[] = { + { "%n@%d", "%u" }, + { "%{username}@%{domain}", "%u" }, + { "%n%d%u", "%u" }, + { "%n", "%n" }, + { "%d", "%d" }, + { "%a%b%u", "%u\t%a\t%b" }, + + { "foo%5.5Mabar", "%a" }, + { "foo%5.5M{longb}bar", "%{longb}" }, + { "foo%5.5Mcbar", "%c" }, + { "foo%5.5M{longc}bar", "%c" }, + { "%a%b", "%a\t%b" }, + { "%a%{longb}%a", "%a\t%{longb}" }, + { "%{longc}%c", "%c" }, + { "%c%a%{longc}%c", "%a\t%c" }, + { "%a%{env:foo}%{env:foo}%a", "%a\t%{env:foo}\t%{env:foo}" } + }; + const char *cache_key; + unsigned int i; + + test_begin("auth cache parse key"); + + for (i = 0; i < N_ELEMENTS(tests); i++) { + cache_key = auth_cache_parse_key(pool_datastack_create(), + tests[i].in); + test_assert(strcmp(cache_key, tests[i].out) == 0); + } + test_end(); +} + +int main(void) +{ + static void (*const test_functions[])(void) = { + test_auth_cache_parse_key, + NULL + }; + return test_run(test_functions); +} diff --git a/src/auth/test-auth-request-fields.c b/src/auth/test-auth-request-fields.c new file mode 100644 index 0000000..e43a1bf --- /dev/null +++ b/src/auth/test-auth-request-fields.c @@ -0,0 +1,147 @@ +/* Copyright (c) 2020 Dovecot authors, see the included COPYING file */ + +#include "test-auth.h" +#include "str.h" +#include "strescape.h" +#include "auth-request.h" + +struct test_auth_request_field { + const char *internal_name; + const char *event_field; + const char *value; +}; + +static const struct test_auth_request_field auth_request_field_names[] = { + /* use the order in auth_request_export() */ +#define PREFIX "\t\r\n\001prefix-" + { "user", "user", PREFIX"testuser" }, + { "service", "service", PREFIX"testservice" }, + { "master-user", "master_user", PREFIX"testmasteruser" }, + { "original-username", "original_user", PREFIX"testoriguser" }, + { "requested-login-user", "login_user", PREFIX"testloginuser" }, + { "lip", "local_ip", "255.254.253.252" }, + { "rip", "remote_ip", "155.154.153.152" }, + { "lport", "local_port", "12" }, + { "rport", "remote_port", "13" }, + { "real_lip", "real_local_ip", "1.2.3.4" }, + { "real_rip", "real_remote_ip", "5.6.7.8" }, + { "real_lport", "real_local_port", "14" }, + { "real_rport", "real_remote_port", "15" }, + { "local_name", "local_name", PREFIX"testlocalname" }, + { "session", "session", PREFIX"testsession" }, + { "secured", NULL, "" }, + { "skip-password-check", NULL, "" }, + { "delayed-credentials", NULL, "" }, + { "valid-client-cert", NULL, "" }, + { "no-penalty", NULL, "" }, + { "successful", NULL, "" }, + { "mech", "mechanism", "TOKEN" }, + { "client_id", "client_id", PREFIX"testclientid" }, + { "passdb_extrafield1", NULL, PREFIX"extravalue1" }, + { "passdb_extrafield2", NULL, PREFIX"extravalue2" }, + { "userdb_uextrafield1", NULL, PREFIX"userextravalue1" }, + { "userdb_uextrafield2", NULL, PREFIX"userextravalue2" }, +}; + +static struct auth_request * +test_auth_request_init(const struct mech_module *mech) +{ + struct auth_request *request; + pool_t pool = pool_alloconly_create("test auth request", 1024); + + request = p_new(pool, struct auth_request, 1); + request->pool = pool; + request->event = event_create(NULL); + request->mech = mech; + auth_request_fields_init(request); + + /* fill out fields that are always exported */ + request->fields.user = "user"; + request->fields.original_username = "user"; + request->fields.service = "service"; + return request; +} + +static void test_auth_request_deinit(struct auth_request *request) +{ + event_unref(&request->event); + pool_unref(&request->pool); +} + +static void test_auth_request_fields_list(void) +{ + struct auth_request *request = + test_auth_request_init(&mech_dovecot_token); + string_t *exported = t_str_new(512); + for (unsigned int i = 0; i < N_ELEMENTS(auth_request_field_names); i++) { + const struct test_auth_request_field *test = + &auth_request_field_names[i]; + test_assert_idx(auth_request_import(request, + test->internal_name, test->value), i); + + str_append(exported, test->internal_name); + if (test->value[0] != '\0') { + str_append_c(exported, '='); + str_append_tabescaped(exported, test->value); + } + str_append_c(exported, '\t'); + + if (test->event_field != NULL) { + const char *value = + event_find_field_recursive_str(request->event, test->event_field); + test_assert_idx(null_strcmp(value, test->value) == 0, i); + } + } + str_truncate(exported, str_len(exported)-1); + + string_t *exported2 = t_str_new(512); + auth_request_export(request, exported2); + test_assert_strcmp(str_c(exported), str_c(exported2)); + + test_auth_request_deinit(request); +} + +static bool +test_auth_request_export_cmp(struct auth_request *request, + const char *key, const char *value) +{ + string_t *exported = t_str_new(128); + str_append(exported, "user=user\tservice=service\toriginal-username=user\t"); + str_append(exported, key); + if (value[0] != '\0') { + str_append_c(exported, '='); + str_append_tabescaped(exported, value); + } + + string_t *exported2 = t_str_new(128); + auth_request_export(request, exported2); + test_assert_strcmp(str_c(exported), str_c(exported2)); + return strcmp(str_c(exported), str_c(exported2)) == 0; + +} + +static void test_auth_request_fields_secured(void) +{ + struct auth_request *request = test_auth_request_init(NULL); + + test_assert(auth_request_import(request, "secured", "")); + test_assert(test_auth_request_export_cmp(request, "secured", "")); + test_assert(null_strcmp(event_find_field_recursive_str(request->event, "transport"), "trusted") == 0); + + test_assert(auth_request_import(request, "secured", "tls")); + test_assert(test_auth_request_export_cmp(request, "secured", "tls")); + test_assert(null_strcmp(event_find_field_recursive_str(request->event, "transport"), "TLS") == 0); + + test_assert(auth_request_import(request, "secured", "blah")); + test_assert(test_auth_request_export_cmp(request, "secured", "")); + test_assert(null_strcmp(event_find_field_recursive_str(request->event, "transport"), "trusted") == 0); + test_auth_request_deinit(request); +} + +void test_auth_request_fields(void) +{ + test_begin("auth request fields"); + test_auth_request_fields_list(); + test_auth_request_fields_secured(); + test_end(); +} diff --git a/src/auth/test-auth-request-var-expand.c b/src/auth/test-auth-request-var-expand.c new file mode 100644 index 0000000..e54e2ba --- /dev/null +++ b/src/auth/test-auth-request-var-expand.c @@ -0,0 +1,259 @@ +/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */ + +#include "test-auth.h" +#include "str.h" +#include "auth.h" +#include "passdb.h" +#include "userdb.h" +#include "auth-request.h" + +static struct passdb_module test_passdb = { + .id = 40 +}; +static struct userdb_module test_userdb = { + .id = 41 +}; + +static struct auth_passdb test_auth_passdb = { + .passdb = &test_passdb +}; +static struct auth_userdb test_auth_userdb = { + .userdb = &test_userdb +}; + +static struct auth_request default_test_request = { + .fields = { + .user = "-user@+domain1@+domain2", + .service = "-service", + .local_ip = { .family = AF_INET }, + .remote_ip = { .family = AF_INET }, + .mech_name = "-mech", + .secured = AUTH_REQUEST_SECURED, + .local_port = 21, + .remote_port = 210, + .valid_client_cert = TRUE, + .requested_login_user = "-loginuser@+logindomain1@+logindomain2", + .session_id = "-session", + .real_local_ip = { .family = AF_INET }, + .real_remote_ip = { .family = AF_INET }, + .real_local_port = 200, + .real_remote_port = 201, + .master_user = "-masteruser@-masterdomain1@-masterdomain2", + .original_username = "-origuser@-origdomain1@-origdomain2", + }, + .client_pid = 54321, + .mech_password = "-password", + + .session_pid = 5000, + + .passdb = &test_auth_passdb, + .userdb = &test_auth_userdb +}; + +static struct auth_request test_request; +static struct auth_request empty_test_request = { .fields = { .user = "" } }; + +static const char * +test_escape(const char *string, const struct auth_request *request) +{ + char *dest; + unsigned int i; + + test_assert(request == &test_request); + + dest = t_strdup_noconst(string); + for (i = 0; dest[i] != '\0'; i++) { + if (dest[i] == '-') + dest[i] = '+'; + } + return dest; +} + +static bool test_empty_request(string_t *str, const char *input) +{ + const struct var_expand_table *tab = + auth_request_get_var_expand_table(&empty_test_request, NULL); + const char *error; + + str_truncate(str, 0); + test_assert(var_expand(str, input, tab, &error) == 1); + return strspn(str_c(str), "\n0") == str_len(str); +} + +static void test_auth_request_var_expand_shortlong(void) +{ + static const char *test_input_short = + "%u\n%n\n%d\n%s\n%h\n%l\n%r\n%l\n%r\n%p\n%w\n%m\n%c\n" + "%a\n%b\n%a\n%b\n%k\n"; + static const char *test_input_long = + "%{user}\n%{username}\n%{domain}\n%{service}\n%{home}\n" + "%{lip}\n%{rip}\n%{local_ip}\n%{remote_ip}\n" + "%{pid}\n%{password}\n%{mech}\n%{secured}\n" + "%{lport}\n%{rport}\n%{local_port}\n%{remote_port}\n%{cert}\n"; + static const char *test_output = + /* %{home} is intentionally always expanding to empty */ + "+user@+domain1@+domain2\n+user\n+domain1@+domain2\n+service\n\n" + "7.91.205.21\n73.150.2.210\n7.91.205.21\n73.150.2.210\n" + "54321\n+password\n+mech\nsecured\n" + "21\n210\n21\n210\nvalid\n"; + const struct var_expand_table *tab; + string_t *str = t_str_new(256); + const char *error; + + test_begin("auth request var expand short and long"); + + tab = auth_request_get_var_expand_table(&test_request, test_escape); + test_assert(var_expand(str, test_input_short, tab, &error) == 1); + test_assert(strcmp(str_c(str), test_output) == 0); + + str_truncate(str, 0); + test_assert(var_expand(str, test_input_long, tab, &error) == 1); + test_assert(strcmp(str_c(str), test_output) == 0); + + /* test with empty input that it won't crash */ + test_assert(test_empty_request(str, test_input_short)); + test_assert(test_empty_request(str, test_input_long)); + + test_end(); +} + +static void test_auth_request_var_expand_flags(void) +{ + static const char *test_input = "%!\n%{secured}\n%{cert}\n"; + string_t *str = t_str_new(10); + const char *error; + + test_begin("auth request var expand flags"); + + test_request.userdb_lookup = FALSE; + test_request.fields.secured = AUTH_REQUEST_SECURED_NONE; + test_request.fields.valid_client_cert = FALSE; + test_assert(var_expand(str, test_input, + auth_request_get_var_expand_table(&test_request, test_escape), + &error) == 1); + test_assert(strcmp(str_c(str), "40\n\n\n") == 0); + + test_request.userdb_lookup = TRUE; + test_request.fields.secured = AUTH_REQUEST_SECURED; + test_request.fields.valid_client_cert = TRUE; + + str_truncate(str, 0); + test_assert(var_expand(str, test_input, + auth_request_get_var_expand_table(&test_request, test_escape), + &error) == 1); + test_assert(strcmp(str_c(str), "41\nsecured\nvalid\n") == 0); + + test_assert(test_empty_request(str, test_input)); + test_end(); +} + +static void test_auth_request_var_expand_long(void) +{ + static const char *test_input = + "%{login_user}\n%{login_username}\n%{login_domain}\n%{session}\n" + "%{real_lip}\n%{real_rip}\n%{real_lport}\n%{real_rport}\n" + "%{real_local_ip}\n%{real_remote_ip}\n" + "%{real_local_port}\n%{real_remote_port}\n" + "%{master_user}\n%{session_pid}\n" + "%{orig_user}\n%{orig_username}\n%{orig_domain}\n"; + static const char *test_output = + "+loginuser@+logindomain1@+logindomain2\n+loginuser\n+logindomain1@+logindomain2\n+session\n" + "13.81.174.20\n13.81.174.21\n200\n201\n" + "13.81.174.20\n13.81.174.21\n" + "200\n201\n" + "+masteruser@+masterdomain1@+masterdomain2\n5000\n" + "+origuser@+origdomain1@+origdomain2\n+origuser\n+origdomain1@+origdomain2\n"; + string_t *str = t_str_new(256); + const char *error; + + test_begin("auth request var expand long-only"); + + test_assert(var_expand(str, test_input, + auth_request_get_var_expand_table(&test_request, test_escape), + &error) == 1); + test_assert(strcmp(str_c(str), test_output) == 0); + + test_assert(test_empty_request(str, test_input)); + test_end(); +} + +static void test_auth_request_var_expand_usernames(void) +{ + static const struct { + const char *username, *output; + } tests[] = { + { "-foo", "+foo\n\n\n\n+foo" }, + { "-foo@-domain", "+foo\n+domain\n+domain\n+domain\n+foo@+domain" }, + { "-foo@-domain1@-domain2", "+foo\n+domain1@+domain2\n+domain1\n+domain2\n+foo@+domain1@+domain2" } + }; + static const char *test_input = + "%{username}\n%{domain}\n%{domain_first}\n%{domain_last}\n%{user}"; + string_t *str = t_str_new(64); + const char *error; + unsigned int i; + + test_begin("auth request var expand usernames"); + for (i = 0; i < N_ELEMENTS(tests); i++) { + test_request.fields.user = t_strdup_noconst(tests[i].username); + str_truncate(str, 0); + test_assert(var_expand(str, test_input, + auth_request_get_var_expand_table(&test_request, test_escape), + &error) == 1); + test_assert_idx(strcmp(str_c(str), tests[i].output) == 0, i); + } + test_request.fields.user = default_test_request.fields.user; + test_end(); +} + +static void test_auth_request_var_expand_funcs(void) +{ + pool_t pool; + const char *value, *error; + + test_begin("auth request var expand funcs"); + + pool = pool_alloconly_create("test var expand funcs", 1024); + test_request.fields.extra_fields = auth_fields_init(pool); + test_request.fields.userdb_reply = auth_fields_init(pool); + + auth_fields_add(test_request.fields.extra_fields, "pkey1", "-pval1", 0); + auth_fields_add(test_request.fields.extra_fields, "pkey2", "", 0); + + auth_fields_add(test_request.fields.userdb_reply, "ukey1", "-uval1", 0); + auth_fields_add(test_request.fields.userdb_reply, "ukey2", "", 0); + + test_assert(t_auth_request_var_expand( + "%{passdb:pkey1}\n%{passdb:pkey1:default1}\n" + "%{passdb:pkey2}\n%{passdb:pkey2:default2}\n" + "%{passdb:pkey3}\n%{passdb:pkey3:default3}\n" + "%{passdb:ukey1}\n%{passdb:ukey1:default4}\n", + &test_request, test_escape, &value, &error) == 1); + test_assert(strcmp(value, "+pval1\n+pval1\n\n\n\ndefault3\n\ndefault4\n") == 0); + + test_assert(t_auth_request_var_expand( + "%{userdb:ukey1}\n%{userdb:ukey1:default1}\n" + "%{userdb:ukey2}\n%{userdb:ukey2:default2}\n" + "%{userdb:ukey3}\n%{userdb:ukey3:default3}\n" + "%{userdb:pkey1}\n%{userdb:pkey1:default4}\n", + &test_request, test_escape, &value, &error) == 1); + test_assert(strcmp(value, "+uval1\n+uval1\n\n\n\ndefault3\n\ndefault4\n") == 0); + + pool_unref(&pool); + test_end(); +} + +void test_auth_request_var_expand(void) +{ + default_test_request.fields.local_ip.u.ip4.s_addr = htonl(123456789); + default_test_request.fields.remote_ip.u.ip4.s_addr = htonl(1234567890); + default_test_request.fields.real_local_ip.u.ip4.s_addr = htonl(223456788); + default_test_request.fields.real_remote_ip.u.ip4.s_addr = htonl(223456789); + + test_request = default_test_request; + + test_auth_request_var_expand_shortlong(); + test_auth_request_var_expand_flags(); + test_auth_request_var_expand_long(); + test_auth_request_var_expand_usernames(); + test_auth_request_var_expand_funcs(); +} diff --git a/src/auth/test-auth.h b/src/auth/test-auth.h new file mode 100644 index 0000000..4641fe3 --- /dev/null +++ b/src/auth/test-auth.h @@ -0,0 +1,25 @@ +/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */ + +#ifndef TEST_AUTH_H +#define TEST_AUTH_H 1 + +#define AUTH_REQUEST_FIELDS_CONST + +#include "lib.h" +#include "test-common.h" + +struct auth_passdb; + +extern struct auth_passdb_settings mock_passdb_set; + +void test_auth_request_var_expand(void); +void test_auth_request_fields(void); +void test_db_dict_parse_cache_key(void); +void test_username_filter(void); +void test_db_lua(void); +struct auth_passdb *passdb_mock(void); +void passdb_mock_mod_init(void); +void passdb_mock_mod_deinit(void); + +#endif + diff --git a/src/auth/test-db-dict.c b/src/auth/test-db-dict.c new file mode 100644 index 0000000..59b0e78 --- /dev/null +++ b/src/auth/test-db-dict.c @@ -0,0 +1,43 @@ +/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "auth-settings.h" +#include "test-auth.h" +#include "array.h" +#include "db-dict.h" + +void test_db_dict_parse_cache_key(void) +{ + struct db_dict_key keys[] = { + { "key0", "%d and %n", NULL, NULL, 0 }, + { "key1", "%{foo}%r%{bar}", NULL, NULL, 0 }, + { "key2", "%{test1}/path", NULL, NULL, 0 }, + { "key3", "path2/%{test2}", NULL, NULL, 0 }, + { "key4", "%{plop}", NULL, NULL, 0 }, + { "key5", "%{unused}", NULL, NULL, 0 } + }; + struct db_dict_field fields[] = { + { "name1", "hello %{dict:key0} %l and %{dict:key1}" }, + { "name2", "%{dict:key2} also %{extra} plus" } + }; + const struct db_dict_key *objects[] = { + &keys[3], &keys[4] + }; + buffer_t keybuf, fieldbuf, objectbuf; + ARRAY_TYPE(db_dict_key) keyarr; + ARRAY_TYPE(db_dict_field) fieldarr; + ARRAY_TYPE(db_dict_key_p) objectarr; + + test_begin("db dict parse cache key"); + + buffer_create_from_const_data(&keybuf, keys, sizeof(keys)); + buffer_create_from_const_data(&fieldbuf, fields, sizeof(fields)); + buffer_create_from_const_data(&objectbuf, objects, sizeof(objects)); + array_create_from_buffer(&keyarr, &keybuf, sizeof(keys[0])); + array_create_from_buffer(&fieldarr, &fieldbuf, sizeof(fields[0])); + array_create_from_buffer(&objectarr, &objectbuf, sizeof(objects[0])); + + test_assert(strcmp(db_dict_parse_cache_key(&keyarr, &fieldarr, &objectarr), + "\t%d and %n\t%l\t%{foo}%r%{bar}\t%{test1}/path\t%{extra}\tpath2/%{test2}\t%{plop}") == 0); + test_end(); +} diff --git a/src/auth/test-libpassword.c b/src/auth/test-libpassword.c new file mode 100644 index 0000000..114daf6 --- /dev/null +++ b/src/auth/test-libpassword.c @@ -0,0 +1,137 @@ +/* Copyright (c) 2018 Dovecot authors, see the included COPYING file */ + +#include "test-lib.h" +#include "password-scheme.h" + +#ifdef HAVE_LIBSODIUM +#include <sodium.h> +#endif + +static struct { + const char *scheme_generated; + const char *scheme_detected; +} known_non_aliases[] = { + { "MD5", "DES-CRYPT" }, + { "MD5-CRYPT", "DES-CRYPT" }, + { "ARGON2ID", "ARGON2I" }, +}; + +/* some algorithms are detected as something other, because they are compatible + but not considered aliases by dovecot. treat those here to avoid false errors. */ +static bool schemes_are_known_non_alias(const char *generated, const char *detected) +{ + for(size_t i = 0; i < N_ELEMENTS(known_non_aliases); i++) { + if (strcmp(known_non_aliases[i].scheme_generated, generated) == 0 && + strcmp(known_non_aliases[i].scheme_detected, detected) == 0) + return TRUE; + } + return FALSE; +} + +static void +test_password_scheme(const char *scheme, const char *crypted, + const char *plaintext) +{ + struct password_generate_params params = { + .user = "testuser1", + .rounds = 0, + }; + const unsigned char *raw_password; + size_t siz; + const char *error, *scheme2; + + test_begin(t_strdup_printf("password scheme(%s)", scheme)); + + test_assert(strcmp(password_get_scheme(&crypted), scheme) == 0); + test_assert(password_decode(crypted, scheme, &raw_password, &siz, &error) == 1); + test_assert(password_verify(plaintext, ¶ms, scheme, raw_password, siz, &error) == 1); + + test_assert(password_generate_encoded(plaintext, ¶ms, scheme, &crypted)); + crypted = t_strdup_printf("{%s}%s", scheme, crypted); + test_assert(strcmp(password_get_scheme(&crypted), scheme) == 0); + test_assert(password_decode(crypted, scheme, &raw_password, &siz, &error) == 1); + test_assert(password_verify(plaintext, ¶ms, scheme, raw_password, siz, &error) == 1); + + scheme2 = password_scheme_detect(plaintext, crypted, ¶ms); + + test_assert(scheme2 != NULL && + (password_scheme_is_alias(scheme, scheme2) || + schemes_are_known_non_alias(scheme, scheme2))); + + test_end(); +} + +static void test_password_failures(void) +{ + const char *scheme = "PLAIN"; + const char *crypted = "{PLAIN}invalid"; + const char *plaintext = "test"; + + struct password_generate_params params = { + .user = "testuser1", + .rounds = 0, + }; + const unsigned char *raw_password; + size_t siz; + const char *error; + + test_begin("password scheme failures"); + + /* wrong password */ + test_assert(strcmp(password_get_scheme(&crypted), scheme) == 0); + test_assert(password_decode(crypted, scheme, &raw_password, &siz, &error) == 1); + test_assert(password_verify(plaintext, ¶ms, scheme, raw_password, siz, &error) == 0); + + /* unknown scheme */ + crypted = "{INVALID}invalid"; + scheme = password_get_scheme(&crypted); + test_assert(password_decode(crypted, scheme, &raw_password, &siz, &error) == 0); + + /* crypt with empty value */ + test_assert(password_verify(plaintext, ¶ms, "CRYPT", NULL, 0, &error) == 0); + + test_end(); +} + +static void test_password_schemes(void) +{ + test_password_scheme("PLAIN", "{PLAIN}test", "test"); + test_password_scheme("CRYPT", "{CRYPT}//EsnG9FLTKjo", "test"); + test_password_scheme("PLAIN-MD4", "{PLAIN-MD4}db346d691d7acc4dc2625db19f9e3f52", "test"); + test_password_scheme("MD5", "{MD5}$1$wmyrgRuV$kImF6.9MAFQNHe23kq5vI/", "test"); + test_password_scheme("SHA1", "{SHA1}qUqP5cyxm6YcTAhz05Hph5gvu9M=", "test"); + test_password_scheme("SMD5", "{SMD5}JTu1KRwptKZJg/RLd+6Vn5GUd0M=", "test"); + test_password_scheme("LDAP-MD5", "{LDAP-MD5}CY9rzUYh03PK3k6DJie09g==", "test"); + test_password_scheme("SHA256", "{SHA256}n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg=", "test"); + test_password_scheme("SHA512", "{SHA512}7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w==", "test"); + test_password_scheme("SSHA", "{SSHA}H/zrDv8FXUu1JmwvVYijfrYEF34jVZcO", "test"); + test_password_scheme("MD5-CRYPT", "{MD5-CRYPT}$1$GgvxyNz8$OjZhLh4P.gF1lxYEbLZ3e/", "test"); + test_password_scheme("OTP", "{OTP}sha1 1024 ae6b49aa481f7233 f69fc7f98b8fbf54", "test"); + test_password_scheme("PBKDF2", "{PBKDF2}$1$bUnT4Pl7yFtYX0KU$5000$50a83cafdc517b9f46519415e53c6a858908680a", "test"); + test_password_scheme("CRAM-MD5", "{CRAM-MD5}e02d374fde0dc75a17a557039a3a5338c7743304777dccd376f332bee68d2cf6", "test"); + test_password_scheme("DIGEST-MD5", "{DIGEST-MD5}77c1a8c437c9b08ba2f460fe5d58db5d", "test"); + test_password_scheme("SCRAM-SHA-1", "{SCRAM-SHA-1}4096,GetyLXdBuHzf1FWf8SLz2Q==,NA/OqmF4hhrsrB9KR7po+dliTGM=,QBiURvQaE6H6qYTmeghDHLANBFQ=", "test"); + test_password_scheme("SCRAM-SHA-256", "{SCRAM-SHA-256}4096,LfNGSFqiFykEZ1xDAYlnKQ==," + "HACNf9CII7cMz3XjRy/Oh3Ae2LHApoDyNw74d3YtFws=," + "AQH0j7Hf8J12g8eNBadvzlNB2am3PxgNwFCFd3RxEaw=", + "test"); + test_password_scheme("BLF-CRYPT", "{BLF-CRYPT}$2y$05$11ipvo5dR6CwkzwmhwM26OXgzXwhV2PyPuLV.Qi31ILcRcThQpEiW", "test"); +#ifdef HAVE_LIBSODIUM + test_password_scheme("ARGON2I", "{ARGON2I}$argon2i$v=19$m=32768,t=4,p=1$f2iuP4aUeNMrgu34fhOkkg$1XSZZMWlIs0zmE+snlUIcLADO3GXbA2O/hsQmmc317k", "test"); +#ifdef crypto_pwhash_ALG_ARGON2ID13 + test_password_scheme("ARGON2ID", "{ARGON2ID}$argon2id$v=19$m=65536,t=3,p=1$vBb99oJ12p3WAdYlaMHz1A$jtFOtbo/sYV9OSlTxDo/nVNq3uArHd5GJSEx0ty85Cc", "test"); +#endif +#endif +} + + +int main(void) +{ + static void (*const test_functions[])(void) = { + test_password_schemes, + test_password_failures, + NULL + }; + password_schemes_init(); + return test_run(test_functions); +} diff --git a/src/auth/test-lua.c b/src/auth/test-lua.c new file mode 100644 index 0000000..c358489 --- /dev/null +++ b/src/auth/test-lua.c @@ -0,0 +1,137 @@ +/* Copyright (c) 2018 Dovecot authors, see the included COPYING file */ + +#include "test-auth.h" + +#ifdef BUILTIN_LUA +#include "istream.h" +#include "auth-settings.h" +#include "auth-request.h" +#include "db-lua.h" + +static struct auth_settings test_lua_auth_set = { + .master_user_separator = "", + .default_realm = "", + .username_format = "", +}; + +static struct auth_request *test_db_lua_auth_request_new(void) +{ + const char *error; + struct auth_request *req = auth_request_new_dummy(NULL); + req->set = global_auth_settings; + struct event *event = event_create(req->event); + array_push_back(&req->authdb_event, &event); + req->passdb = passdb_mock(); + test_assert(auth_request_set_username(req, "testuser", &error)); + return req; +} + +static void test_db_lua_auth_verify(void) +{ + struct auth_request *req = test_db_lua_auth_request_new(); + + static const char *luascript = +"function auth_password_verify(req, pass)\n" +" req:log_debug(\"user \" .. req.user)\n" +" if req:password_verify(\"{SHA256-CRYPT}$5$XtUywQCSjW0zAJgE$YjuPKQnsLuH4iE9kranZyy1lbil5IrRUfs7X6EyJyG1\", pass) then\n" +" return dovecot.auth.PASSDB_RESULT_OK, {}\n" +" end\n" +"end\n"; + const char *error = NULL; + struct dlua_script *script = NULL; + + test_begin("auth db lua passdb_verify"); + + test_assert(dlua_script_create_string(luascript, &script, NULL, &error) == 0); + if (script != NULL) { + test_assert(auth_lua_script_init(script, &error) == 0); + test_assert(auth_lua_call_password_verify(script, req, "password", &error) == 1); + dlua_script_unref(&script); + } + if (error != NULL) { + i_error("Test failed: %s", error); + } + i_free(req->passdb); + + auth_request_passdb_lookup_end(req, PASSDB_RESULT_OK); + auth_request_unref(&req); + + test_end(); +} + +static void test_db_lua_auth_lookup_numberish_value(void) +{ + const char *scheme,*pass; + + struct auth_request *req = test_db_lua_auth_request_new(); + + static const char *luascript = +"function auth_passdb_lookup(req)\n" +" local fields = {}\n" +" fields[\"user\"] = \"01234\"\n" +" return dovecot.auth.PASSDB_RESULT_OK, fields\n" +"end\n"; + const char *error = NULL; + struct dlua_script *script = NULL; + + test_begin("auth db lua passdb_lookup"); + + test_assert(dlua_script_create_string(luascript, &script, NULL, &error) == 0); + if (script != NULL) { + test_assert(auth_lua_script_init(script, &error) == 0); + test_assert(auth_lua_call_passdb_lookup(script, req, &scheme, &pass, &error) == 1); + test_assert(strcmp(req->fields.user, "01234") == 0); + dlua_script_unref(&script); + } + if (error != NULL) { + i_error("Test failed: %s", error); + } + i_free(req->passdb); + auth_request_passdb_lookup_end(req, PASSDB_RESULT_OK); + auth_request_unref(&req); + + test_end(); +} + +static void test_db_lua_auth_lookup(void) +{ + const char *scheme,*pass; + + struct auth_request *req = test_db_lua_auth_request_new(); + + static const char *luascript = +"function auth_passdb_lookup(req)\n" +" req:log_debug(\"user \" .. req.user)\n" +" return dovecot.auth.PASSDB_RESULT_OK, req:var_expand(\"password=pass\")\n" +"end\n"; + const char *error = NULL; + struct dlua_script *script = NULL; + + test_begin("auth db lua passdb_lookup"); + + test_assert(dlua_script_create_string(luascript, &script, NULL, &error) == 0); + if (script != NULL) { + test_assert(auth_lua_script_init(script, &error) == 0); + test_assert(auth_lua_call_passdb_lookup(script, req, &scheme, &pass, &error) == 1); + dlua_script_unref(&script); + } + if (error != NULL) { + i_error("Test failed: %s", error); + } + i_free(req->passdb); + auth_request_passdb_lookup_end(req, PASSDB_RESULT_OK); + auth_request_unref(&req); + + test_end(); +} + +void test_db_lua(void) { + memset(test_lua_auth_set.username_chars_map, 0xff, + sizeof(test_lua_auth_set.username_chars_map)); + global_auth_settings = &test_lua_auth_set; + test_db_lua_auth_lookup(); + test_db_lua_auth_lookup_numberish_value(); + test_db_lua_auth_verify(); +} + +#endif diff --git a/src/auth/test-main.c b/src/auth/test-main.c new file mode 100644 index 0000000..762c34b --- /dev/null +++ b/src/auth/test-main.c @@ -0,0 +1,39 @@ +/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "auth-settings.h" +#include "test-common.h" +#include "test-auth.h" +#include "password-scheme.h" +#include "passdb.h" + +int main(int argc, const char *argv[]) +{ + const char *match = ""; + int ret; + static const struct named_test test_functions[] = { + TEST_NAMED(test_auth_request_var_expand) + TEST_NAMED(test_auth_request_fields) + TEST_NAMED(test_db_dict_parse_cache_key) + TEST_NAMED(test_username_filter) +#if defined(BUILTIN_LUA) + TEST_NAMED(test_db_lua) +#endif + { NULL, NULL } + }; + + password_schemes_init(); + passdbs_init(); + passdb_mock_mod_init(); + + if (argc > 2 && strcasecmp(argv[1], "--match") == 0) + match = argv[2]; + + ret = test_run_named(test_functions, match); + + passdb_mock_mod_deinit(); + password_schemes_deinit(); + passdbs_deinit(); + + return ret; +} diff --git a/src/auth/test-mech.c b/src/auth/test-mech.c new file mode 100644 index 0000000..8a2c7b3 --- /dev/null +++ b/src/auth/test-mech.c @@ -0,0 +1,412 @@ +/* Copyright (c) 2020 Dovecot authors, see the included COPYING file */ + +#include "test-auth.h" +#include "auth.h" +#include "str.h" +#include "auth-common.h" +#include "auth-request.h" +#include "auth-request-handler-private.h" +#include "auth-settings.h" +#include "mech-digest-md5-private.h" +#include "otp.h" +#include "mech-otp-common.h" +#include "settings-parser.h" +#include "password-scheme.h" +#include "auth-token.h" + +#include <unistd.h> +#include <time.h> + +#define UCHAR_LEN(str) (const unsigned char *)(str), sizeof(str)-1 + +extern const struct mech_module mech_anonymous; +extern const struct mech_module mech_apop; +extern const struct mech_module mech_cram_md5; +extern const struct mech_module mech_digest_md5; +extern const struct mech_module mech_dovecot_token; +extern const struct mech_module mech_external; +extern const struct mech_module mech_login; +extern const struct mech_module mech_oauthbearer; +extern const struct mech_module mech_otp; +extern const struct mech_module mech_plain; +extern const struct mech_module mech_scram_sha1; +extern const struct mech_module mech_scram_sha256; +extern const struct mech_module mech_xoauth2; + +static struct auth_settings set; +static struct mechanisms_register *mech_reg; + +struct test_case { + const struct mech_module *mech; + const unsigned char *in; + size_t len; + const char *username; + const char *expect_error; + bool success; + bool set_username_before_test; + bool set_cert_username; +}; + +static void +verify_plain_continue_mock_callback(struct auth_request *request, + verify_plain_callback_t *callback) +{ + request->passdb_success = TRUE; + callback(PASSDB_RESULT_OK, request); +} + +static void +request_handler_reply_mock_callback(struct auth_request *request, + enum auth_client_result result, + const void *auth_reply ATTR_UNUSED, + size_t reply_size ATTR_UNUSED) +{ + request->failed = result != AUTH_CLIENT_RESULT_SUCCESS; + + if (request->passdb_result == PASSDB_RESULT_OK) + request->failed = FALSE; + else if (request->mech == &mech_otp) { + if (null_strcmp(request->fields.user, "otp_phase_2") == 0) + request->failed = FALSE; + } else if (request->mech == &mech_oauthbearer) { + } +}; + +static void +request_handler_reply_continue_mock_callback(struct auth_request *request, + const void *reply, + size_t reply_size) +{ + request->context = p_strndup(request->pool, reply, reply_size); +} + +static void +auth_client_request_mock_callback(const char *reply ATTR_UNUSED, + struct auth_client_connection *conn ATTR_UNUSED) +{ +} + +static void test_mechs_init(void) +{ + const char *const services[] = {NULL}; + process_start_time = time(NULL); + + /* Copy default settings */ + set = *(const struct auth_settings *)auth_setting_parser_info.defaults; + global_auth_settings = &set; + global_auth_settings->base_dir = "."; + memset((&set)->username_chars_map, 1, sizeof((&set)->username_chars_map)); + set.username_format = ""; + + t_array_init(&set.passdbs, 2); + struct auth_passdb_settings *mock_set = t_new(struct auth_passdb_settings, 1); + *mock_set = mock_passdb_set; + array_push_back(&set.passdbs, &mock_set); + mock_set = t_new(struct auth_passdb_settings, 1); + *mock_set = mock_passdb_set; + mock_set->master = TRUE; + array_push_back(&set.passdbs, &mock_set); + t_array_init(&set.userdbs, 1); + + /* Disable stats */ + set.stats = FALSE; + + /* For tests of digest-md5. */ + set.realms_arr = t_strsplit_spaces("example.com ", " "); + /* For tests of mech-anonymous. */ + set.anonymous_username = "anonuser"; + + mech_init(global_auth_settings); + mech_reg = mech_register_init(global_auth_settings); + passdbs_init(); + userdbs_init(); + passdb_mock_mod_init(); + password_schemes_init(); + + auths_preinit(&set, pool_datastack_create(), mech_reg, services); + auths_init(); + auth_token_init(); +} + +static void test_mech_prepare_request(struct auth_request **request_r, + const struct mech_module *mech, + struct auth_request_handler *handler, + unsigned int running_test, + const struct test_case *test_case) +{ + global_auth_settings->ssl_username_from_cert = test_case->set_cert_username; + struct auth *auth = auth_default_service(); + + struct auth_request *request = auth_request_new(mech, NULL); + request->handler = handler; + request->id = running_test+1; + request->mech_password = NULL; + request->state = AUTH_REQUEST_STATE_NEW; + request->set = global_auth_settings; + request->connect_uid = running_test; + request->passdb = auth->passdbs; + request->userdb = auth->userdbs; + handler->refcount = 1; + + auth_fields_add(request->fields.extra_fields, "nodelay", "", 0); + auth_request_ref(request); + auth_request_state_count[AUTH_REQUEST_STATE_NEW] = 1; + + if (test_case->set_username_before_test || test_case->set_cert_username) + request->fields.user = p_strdup(request->pool, test_case->username); + if (test_case->set_cert_username) + request->fields.cert_username = TRUE; + + *request_r = request; +} + +static void test_mech_handle_challenge(struct auth_request *request, + const unsigned char *in, + size_t in_len, + unsigned int running_test, + bool expected_success) +{ + string_t *out = t_str_new(16); + str_append_data(out, in, in_len); + const char *challenge = request->context; + if (request->mech == &mech_login) { + /* We do not care about any specific password just give + * the username input as password also in case it's wanted. */ + if (expected_success) + test_assert_strcmp_idx(challenge, "Password:", running_test); + else + test_assert_strcmp_idx(challenge, "Username:", running_test); + } else if (request->mech == &mech_cram_md5 && *in != '\0') { + str_truncate(out, 0); + str_append(out, "testuser b913a602c7eda7a495b4e6e7334d3890"); + } else if (request->mech == &mech_digest_md5) { + struct digest_auth_request *digest_request = + (struct digest_auth_request *) request; + digest_request->nonce = "OA6MG9tEQGm2hh"; + } + auth_request_continue(request, out->data, out->used); +} + +static inline const unsigned char * +test_mech_construct_apop_challenge(unsigned int connect_uid, size_t *len_r) +{ + string_t *apop_challenge = t_str_new(128); + + str_printfa(apop_challenge,"<%lx.%lx.%"PRIxTIME_T".", (unsigned long)getpid(), + (unsigned long)connect_uid, process_start_time+10); + str_append_data(apop_challenge, "\0testuser\0responseoflen16-", 26); + *len_r = apop_challenge->used; + return apop_challenge->data; +} + +static void test_mechs(void) +{ + static struct auth_request_handler handler = { + .callback = auth_client_request_mock_callback, + .reply_callback = request_handler_reply_mock_callback, + .reply_continue_callback = request_handler_reply_continue_mock_callback, + .verify_plain_continue_callback = verify_plain_continue_mock_callback, + }; + + static struct test_case tests[] = { + /* Expected to be successful */ + {&mech_anonymous, UCHAR_LEN("\0any \0 bad \0 content"), "anonuser", NULL, TRUE, FALSE, FALSE}, + {&mech_apop, NULL, 0, "testuser", NULL, TRUE, FALSE, FALSE}, + {&mech_cram_md5, UCHAR_LEN("testuser b913a602c7eda7a495b4e6e7334d3890"), "testuser", NULL, TRUE, FALSE, FALSE}, + {&mech_digest_md5, UCHAR_LEN("username=\"testuser@example.com\",realm=\"example.com\",nonce=\"OA6MG9tEQGm2hh\",cnonce=\"OA6MHXh6VqTrRk\",nc=00000001,digest-uriresponse=d388dad90d4bbd760a152321f2143af7,qop=\"auth\""), "testuser@example.com", NULL,TRUE, FALSE, FALSE}, + {&mech_digest_md5, UCHAR_LEN("username=\"testuser@example.com\",realm=\"example.com\",nonce=\"OA6MG9tEQGm2hh\",cnonce=\"OA6MHXh6VqTrRk\",nc=00000001,digest-uriresponse=d388dad90d4bbd760a152321f2143af7,qop=\"auth\",authzid=\"masteruser\""), "testuser@example.com", NULL, TRUE, FALSE, FALSE}, + {&mech_digest_md5, UCHAR_LEN("username=\"test\xc3\xbaser@example.com\",realm=\"example.com\",nonce=\"OA6MG9tEQGm2hh\",cnonce=\"OA6MHXh6VqTrRk\",nc=00000001,digest-uriresponse=d388dad90d4bbd760a152321f2143af7,qop=\"auth\",authzid=\"masteruser\""), "test\xc3\xbaser@example.com", NULL, TRUE, FALSE, FALSE}, + {&mech_digest_md5, UCHAR_LEN("username=\"test\xc3\xbaser@example.com\",realm=\"example.com\",nonce=\"OA6MG9tEQGm2hh\",cnonce=\"OA6MHXh6VqTrRk\",charset=\"utf-8\",cipher=unsupported,nc=00000001,digest-uri=imap/server.com,response=d388dad90d4bbd760a152321f2143af7,qop=\"auth\",authzid=\"masteruser\""), "test\xc3\xbaser@example.com", NULL, TRUE, FALSE, FALSE}, + {&mech_digest_md5, UCHAR_LEN("username=\"testuser\",realm=\"example.com\",nonce=\"OA6MG9tEQGm2hh\",cnonce=\"OA6MHXh6VqTrRk\",charset=\"utf-8\",cipher=unsupported,nc=00000001,digest-uri=imap/server.com,response=d388dad90d4bbd760a152321f2143af7,qop=\"auth\",authzid=\"masteruser\""), "testuser@example.com", NULL, TRUE, FALSE, FALSE}, + {&mech_external, UCHAR_LEN(""), "testuser", NULL, TRUE, TRUE, TRUE}, + {&mech_dovecot_token, NULL, 0, "testuser", NULL, TRUE, FALSE, FALSE}, + {&mech_login, UCHAR_LEN("testuser"), "testuser", NULL, TRUE, FALSE, FALSE}, + {&mech_plain, UCHAR_LEN("\0testuser\0testpass"), "testuser", NULL, TRUE, FALSE, FALSE}, + {&mech_plain, UCHAR_LEN("normaluser\0masteruser\0masterpass"), "masteruser", NULL, TRUE, FALSE, FALSE}, + {&mech_plain, UCHAR_LEN("normaluser\0normaluser\0masterpass"), "normaluser", NULL, TRUE, FALSE, FALSE}, + {&mech_otp, UCHAR_LEN("hex:5Bf0 75d9 959d 036f"), "otp_phase_2", NULL, TRUE, TRUE, FALSE}, + {&mech_otp, UCHAR_LEN("word:BOND FOGY DRAB NE RISE MART"), "otp_phase_2", NULL, TRUE, TRUE, FALSE}, + {&mech_otp, UCHAR_LEN("init-hex:f6bd 6b33 89b8 7203:md5 499 ke6118:23d1 b253 5ae0 2b7e"), "otp_phase_2", NULL, TRUE, TRUE, FALSE}, + {&mech_otp, UCHAR_LEN("init-word:END KERN BALM NICK EROS WAVY:md5 499 ke1235:BABY FAIN OILY NIL TIDY DADE"), "otp_phase_2", NULL , TRUE, TRUE, FALSE}, + {&mech_oauthbearer, UCHAR_LEN("n,a=testuser,p=cHJvb2Y=,f=nonstandart\x01host=server\x01port=143\x01""auth=Bearer vF9dft4qmTc2Nvb3RlckBhbHRhdmlzdGEuY29tCg==\x01\x01"), "testuser", NULL, FALSE, TRUE, FALSE}, + {&mech_scram_sha1, UCHAR_LEN("n,,n=testuser,r=rOprNGfwEbeRWgbNEkqO"), "testuser", NULL, TRUE, FALSE, FALSE}, + {&mech_scram_sha256, UCHAR_LEN("n,,n=testuser,r=rOprNGfwEbeRWgbNEkqO"), "testuser", NULL, TRUE, FALSE, FALSE}, + {&mech_xoauth2, UCHAR_LEN("user=testuser\x01""auth=Bearer vF9dft4qmTc2Nvb3RlckBhdHRhdmlzdGEuY29tCg==\x01\x01"), "testuser", NULL, TRUE, FALSE, FALSE}, + + /* Below tests are expected to fail */ + /* Empty input tests*/ + {&mech_apop, UCHAR_LEN(""), NULL, NULL, FALSE, FALSE, FALSE}, + {&mech_cram_md5, UCHAR_LEN(""), NULL, NULL, FALSE, FALSE, FALSE}, + {&mech_digest_md5, UCHAR_LEN(""), NULL, NULL, FALSE, FALSE, FALSE}, + {&mech_dovecot_token, UCHAR_LEN(""), NULL, NULL, FALSE, FALSE, FALSE}, + {&mech_external, UCHAR_LEN(""), "testuser", NULL, FALSE, TRUE, FALSE}, + {&mech_external, UCHAR_LEN(""), NULL, NULL, FALSE, FALSE, FALSE}, + {&mech_login, UCHAR_LEN(""), NULL, NULL, FALSE, FALSE, FALSE}, + {&mech_otp, UCHAR_LEN(""), NULL, "invalid input", FALSE, FALSE, FALSE}, + {&mech_otp, UCHAR_LEN(""), "testuser", "invalid input", FALSE, FALSE, FALSE}, + {&mech_plain, UCHAR_LEN(""), NULL, NULL, FALSE, FALSE, FALSE}, + {&mech_oauthbearer, UCHAR_LEN(""), NULL, NULL, FALSE, FALSE, FALSE}, + {&mech_xoauth2, UCHAR_LEN(""), NULL, NULL, FALSE, FALSE, FALSE}, + {&mech_scram_sha1, UCHAR_LEN(""), NULL, NULL, FALSE, FALSE, FALSE}, + {&mech_scram_sha256, UCHAR_LEN(""), NULL, NULL, FALSE, FALSE, FALSE}, + + /* Bad input tests*/ + {&mech_apop, UCHAR_LEN("1.1.1\0test\0user\0response"), NULL, NULL, FALSE, FALSE, FALSE}, + {&mech_apop, UCHAR_LEN("1.1.1\0testuser\0tooshort"), NULL, NULL, FALSE, FALSE, FALSE}, + {&mech_apop, UCHAR_LEN("1.1.1\0testuser\0responseoflen16-"), NULL, NULL, FALSE, FALSE, FALSE}, + {&mech_apop, UCHAR_LEN("1.1.1"), NULL, NULL, FALSE, FALSE, FALSE}, + {&mech_otp, UCHAR_LEN("somebody\0testuser"), "testuser", "otp(testuser): unsupported response type", FALSE, TRUE, FALSE}, + {&mech_cram_md5, UCHAR_LEN("testuser\0response"), "testuser", NULL, FALSE, FALSE, FALSE}, + {&mech_plain, UCHAR_LEN("testuser\0"), "testuser", NULL, FALSE, FALSE, FALSE}, + + /* Covering most of the digest md5 parsing */ + {&mech_digest_md5, UCHAR_LEN("username=\"testuser@example.com\",realm=\"example.com\",cnonce=\"OA6MHXh6VqTrRk\",response=d388dad90d4bbd760a152321f2143af7,qop=\"auth\""), NULL, NULL, FALSE, FALSE, FALSE}, + {&mech_digest_md5, UCHAR_LEN("realm=\"example.com\",cnonce=\"OA6MHXh6VqTrRk\",nonce=\"OA6MG9tEQGm2hh\""), NULL, NULL, FALSE, FALSE, FALSE}, + {&mech_digest_md5, UCHAR_LEN("username=\"testuser@example.com\",realm=\"example.com\", nonce=\"OA6MG9tEQGm2hh\""), NULL, NULL, FALSE, FALSE, FALSE}, + {&mech_digest_md5, UCHAR_LEN("qop=\"auth-int\""), NULL, NULL, FALSE, FALSE, FALSE}, + {&mech_digest_md5, UCHAR_LEN("qop=\"auth-int\""), NULL, NULL, FALSE, FALSE, FALSE}, + {&mech_digest_md5, UCHAR_LEN("qop=\"auth-conf\",\"cipher=rc4\""), NULL, NULL, FALSE, FALSE, FALSE}, + {&mech_digest_md5, UCHAR_LEN("cnonce=\"OA6MHXh6VqTrRk\",cnonce=\"OA6MHXh6VqTrRk\""), NULL, NULL, FALSE, FALSE, FALSE}, + {&mech_digest_md5, UCHAR_LEN("cnonce=\"\""), NULL, NULL, FALSE, FALSE, FALSE}, + {&mech_digest_md5, UCHAR_LEN("nonce=\"not matching\""), NULL, NULL, FALSE, FALSE, FALSE}, + {&mech_digest_md5, UCHAR_LEN("nc=00000001,nc=00000002"), NULL, NULL, FALSE, FALSE, FALSE}, + {&mech_digest_md5, UCHAR_LEN("nc=NAN"), NULL, NULL, FALSE, FALSE, FALSE}, + {&mech_digest_md5, UCHAR_LEN("nc=00000002"), NULL, NULL, FALSE, FALSE, FALSE}, + {&mech_digest_md5, UCHAR_LEN("cipher=unsupported"), NULL, NULL, FALSE, FALSE, FALSE}, + {&mech_digest_md5, UCHAR_LEN("digest-uri="), NULL, NULL, FALSE, FALSE, FALSE}, + {&mech_digest_md5, UCHAR_LEN("username=\"\""), NULL, NULL, FALSE, FALSE, FALSE}, + {&mech_digest_md5, UCHAR_LEN("username=\"a\",username=\"b\""), NULL, NULL, FALSE, FALSE, FALSE}, + {&mech_digest_md5, UCHAR_LEN("response=broken"), NULL, NULL, FALSE, FALSE, FALSE}, + {&mech_digest_md5, UCHAR_LEN("maxbuf=32,maxbuf=1024"), NULL, NULL, FALSE, FALSE, FALSE}, + {&mech_digest_md5, UCHAR_LEN("maxbuf=broken"), NULL, NULL, FALSE, FALSE, FALSE}, + {&mech_digest_md5, UCHAR_LEN("authzid=\"somebody\",authzid=\"else\""), NULL, NULL, FALSE, FALSE, FALSE}, + {&mech_digest_md5, UCHAR_LEN("authzid=\"\""), NULL, NULL, FALSE, FALSE, FALSE}, + {&mech_digest_md5, UCHAR_LEN("charset=unsupported"), NULL, NULL, FALSE, FALSE, FALSE}, + {&mech_digest_md5, UCHAR_LEN("qop=unsupported"), NULL, NULL, FALSE, FALSE, FALSE}, + + /* Too much nuls */ + {&mech_dovecot_token, UCHAR_LEN("service\0pid\0fail\0se\0ssion_id\0deadbeef"), NULL , NULL, FALSE, FALSE, FALSE}, + {&mech_login, UCHAR_LEN("test user\0user"), NULL, NULL, FALSE, FALSE, FALSE}, + {&mech_oauthbearer, UCHAR_LEN("n,a==testuser,\x01""auth=Bearer token\x01\x01"), NULL, NULL, FALSE, FALSE, FALSE}, + {&mech_oauthbearer, UCHAR_LEN("n,a=testuser,f=non-standard\x01""auth=Bearer token\x01\x01"), "testuser", NULL, FALSE, FALSE, FALSE}, + {&mech_oauthbearer, UCHAR_LEN("n,a=testuser\x01""auth=token\x01\x01"), "testuser", NULL, FALSE, FALSE, FALSE}, + {&mech_xoauth2, UCHAR_LEN("testuser\x01auth=Bearer token\x01\x01"), NULL, NULL, FALSE, FALSE, FALSE}, + /* does not start with [B|b]earer */ + {&mech_xoauth2, UCHAR_LEN("user=testuser\x01""auth=token\x01\x01"), "testuser", NULL, FALSE, FALSE, FALSE}, + /* Too much nuls */ + {&mech_plain, UCHAR_LEN("\0fa\0il\0ing\0withthis"), NULL, NULL, FALSE, FALSE, FALSE}, + {&mech_plain, UCHAR_LEN("failingwiththis"), NULL, NULL, FALSE, FALSE, FALSE}, + {&mech_plain, UCHAR_LEN("failing\0withthis"), NULL, NULL, FALSE, FALSE, FALSE}, + {&mech_otp, UCHAR_LEN("someb\0ody\0testuser"), NULL, "invalid input", FALSE, FALSE, FALSE}, + /* phase 2 */ + {&mech_otp, UCHAR_LEN("someb\0ody\0testuser"), "testuser", "otp(testuser): unsupported response type", FALSE, TRUE, FALSE}, + {&mech_scram_sha1, UCHAR_LEN("c=biws,r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,p=v0X8v3Bz2T0CJGbJQyF0X+HI4Ts="), NULL, NULL, FALSE, FALSE, FALSE}, + {&mech_scram_sha1, UCHAR_LEN("iws0X8v3Bz2T0CJGbJQyF0X+HI4Ts=,,,,"), NULL, NULL, FALSE, FALSE, FALSE}, + {&mech_scram_sha1, UCHAR_LEN("n,a=masteruser,,"), NULL, NULL, FALSE, FALSE, FALSE}, + {&mech_scram_sha1, UCHAR_LEN("n,a==masteruser,,"), NULL, NULL, FALSE, FALSE, FALSE}, + {&mech_scram_sha1, UCHAR_LEN("n,,m=testuser,,"), NULL, NULL, FALSE, FALSE, FALSE}, + {&mech_scram_sha1, UCHAR_LEN("broken\0input"), NULL, NULL, FALSE, FALSE, FALSE}, + {&mech_scram_sha256, UCHAR_LEN("broken\0input"), NULL, NULL, FALSE, FALSE, FALSE}, + }; + + test_mechs_init(); + + string_t *d_token = t_str_new(32); + str_append_data(d_token, UCHAR_LEN("service\0pid\0testuser\0session\0")); + str_append(d_token, auth_token_get("service","pid","testuser","session")); + + for (unsigned int running_test = 0; running_test < N_ELEMENTS(tests); + running_test++) T_BEGIN { + struct test_case *test_case = &tests[running_test]; + const struct mech_module *mech = test_case->mech; + struct auth_request *request; + const char *testname = t_strdup_printf("auth mech %s %d/%zu", + mech->mech_name, + running_test+1, + N_ELEMENTS(tests)); + test_begin(testname); + + test_mech_prepare_request(&request, mech, &handler, running_test, + test_case); + + if (mech == &mech_apop && test_case->in == NULL) + test_case->in = + test_mech_construct_apop_challenge(request->connect_uid, + &test_case->len); + if (mech == &mech_dovecot_token && test_case->in == NULL) { + test_case->in = d_token->data; + test_case->len = d_token->used; + } + + if (test_case->expect_error != NULL) + test_expect_error_string(test_case->expect_error); + + request->state = AUTH_REQUEST_STATE_NEW; + unsigned char *input_dup = test_case->len == 0 ? NULL : + i_memdup(test_case->in, test_case->len); + request->initial_response = input_dup; + request->initial_response_len = test_case->len; + auth_request_initial(request); + + const char *challenge = request->context; + + if (challenge != NULL) { + test_mech_handle_challenge(request, test_case->in, + test_case->len, + running_test, + test_case->success); + } + + const char *username = request->fields.user; + + if (request->fields.master_user != NULL) + username = request->fields.master_user; + + if (!test_case->set_username_before_test && test_case->success) { + /* If the username was set by the test logic, do not + * compare it as it does not give any additional + * information */ + test_assert_strcmp_idx(test_case->username, username, + running_test); + } else if (!test_case->set_username_before_test && !test_case->success) { + /* If the username is not set by the testlogic and we + * expect failure, verify that the mechanism failed by + * checking that the username is not set */ + test_assert_idx(username == NULL, running_test); + } + + if (test_case->success) + test_assert_idx(request->failed == FALSE, running_test); + else + test_assert_idx(request->failed == TRUE, running_test); + + event_unref(&request->event); + event_unref(&request->mech_event); + i_free(input_dup); + mech->auth_free(request); + + test_end(); + } T_END; + mech_otp_deinit(); + auths_deinit(); + auth_token_deinit(); + password_schemes_deinit(); + passdb_mock_mod_deinit(); + passdbs_deinit(); + userdbs_deinit(); + event_unref(&auth_event); + mech_deinit(global_auth_settings); + mech_register_deinit(&mech_reg); + auths_free(); + i_unlink("auth-token-secret.dat"); +} + +int main(void) +{ + static void (*const test_functions[])(void) = { + test_mechs, + NULL + }; + + return test_run(test_functions); +} diff --git a/src/auth/test-mock.c b/src/auth/test-mock.c new file mode 100644 index 0000000..9584912 --- /dev/null +++ b/src/auth/test-mock.c @@ -0,0 +1,109 @@ +/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */ + +#include "test-auth.h" +#include "auth-common.h" +#include "passdb.h" + +struct auth_penalty *auth_penalty; +time_t process_start_time; +bool worker, worker_restart_request; +static struct passdb_module *mock_passdb_mod = NULL; +static pool_t mock_pool; + +void auth_module_load(const char *names ATTR_UNUSED) +{ +} +void auth_refresh_proctitle(void) { +} + +static void passdb_mock_init(struct passdb_module *module ATTR_UNUSED) +{ +} +static void passdb_mock_deinit(struct passdb_module *module ATTR_UNUSED) +{ +} +static void passdb_mock_verify_plain(struct auth_request *request, const char *password ATTR_UNUSED, + verify_plain_callback_t *callback) +{ + callback(PASSDB_RESULT_OK, request); +} + +static void passdb_mock_lookup_credentials(struct auth_request *request, + lookup_credentials_callback_t *callback) +{ + passdb_handle_credentials(PASSDB_RESULT_OK, "password", "PLAIN", + callback, request); +} + +static struct passdb_module_interface mock_interface = { + .name = "mock", + .init = passdb_mock_init, + .deinit = passdb_mock_deinit, + .verify_plain = passdb_mock_verify_plain, + .lookup_credentials = passdb_mock_lookup_credentials, +}; + +struct auth_passdb_settings mock_passdb_set = { + .name = "mock", + .driver = "mock", + .args = "", + .default_fields = "", + .override_fields = "", + .mechanisms = "", + .username_filter = "", + .skip = "never", + .result_success = "return-ok", + .result_failure = "continue", + .result_internalfail = "continue", + .deny = FALSE, + .pass = FALSE, + .master = FALSE, + .auth_verbose = "default" +}; + +void passdb_mock_mod_init(void) +{ + if (mock_passdb_mod != NULL) + return; + + mock_pool = pool_allocfree_create("auth mock"); + + passdb_register_module(&mock_interface); + + struct auth_passdb_settings set = { + .name = "mock", + .driver = "mock", + .args = "", + .default_fields = "", + .override_fields = "", + .mechanisms = "", + .username_filter = "", + + .skip = "never", + .result_success = "return-ok", + .result_failure = "continue", + .result_internalfail = "continue", + + .deny = FALSE, + .pass = FALSE, + .master = FALSE, + .auth_verbose = "default" + }; + mock_passdb_mod = passdb_preinit(mock_pool, &set); + passdb_init(mock_passdb_mod); +} + +void passdb_mock_mod_deinit(void) +{ + passdb_deinit(mock_passdb_mod); + passdb_unregister_module(&mock_interface); + pool_unref(&mock_pool); +} + +struct auth_passdb *passdb_mock(void) +{ + struct auth_passdb *ret = i_new(struct auth_passdb, 1); + ret->set = &mock_passdb_set; + ret->passdb = mock_passdb_mod; + return ret; +} diff --git a/src/auth/test-username-filter.c b/src/auth/test-username-filter.c new file mode 100644 index 0000000..4a1c221 --- /dev/null +++ b/src/auth/test-username-filter.c @@ -0,0 +1,50 @@ +/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "auth-settings.h" +#include "test-auth.h" +#include "auth-request.h" + +void test_username_filter(void) +{ + const struct { + const char *filter; + const char *input; + bool accepted; + } cases[] = { + { "", "", TRUE }, + { "*", "", TRUE }, + { "", "testuser1", TRUE }, + { "*", "testuser1", TRUE }, + { "!*", "testuser1", FALSE }, + { "!*", "", FALSE }, + { "*@*", "", FALSE }, + { "*@*", "@", TRUE }, + { "!*@*", "@", FALSE }, + { "!*@*", "", TRUE }, + { "*@*", "testuser1", FALSE }, + { "!*@*", "testuser1", TRUE }, + { "*@*", "testuser1@testdomain", TRUE }, + { "!*@*", "testuser1@testdomain", FALSE }, + { "*@testdomain *@testdomain2", "testuser1@testdomain", TRUE }, + { "*@testdomain *@testdomain2", "testuser1@testdomain2", TRUE }, + { "*@testdomain *@testdomain2", "testuser1@testdomain3", FALSE }, + { "!testuser@testdomain *@testdomain", "testuser@testdomain", FALSE }, + { "!testuser@testdomain *@testdomain", "testuser2@testdomain", TRUE }, + { "*@testdomain !testuser@testdomain !testuser2@testdomain", "testuser@testdomain", FALSE }, + { "*@testdomain !testuser@testdomain !testuser2@testdomain", "testuser3@testdomain", TRUE }, + { "!testuser@testdomain !testuser2@testdomain", "testuser", TRUE }, + { "!testuser@testdomain !testuser2@testdomain", "testuser@testdomain", FALSE }, + { "!testuser@testdomain *@testdomain !testuser2@testdomain", "testuser3@testdomain", TRUE }, + { "!testuser@testdomain *@testdomain !testuser2@testdomain", "testuser@testdomain", FALSE }, + }; + + test_begin("test username_filter"); + + for(size_t i = 0; i < N_ELEMENTS(cases); i++) { + const char *const *filter = t_strsplit_spaces(cases[i].filter, " ,"); + test_assert_idx(auth_request_username_accepted(filter, cases[i].input) == cases[i].accepted, i); + } + + test_end(); +} diff --git a/src/auth/userdb-blocking.c b/src/auth/userdb-blocking.c new file mode 100644 index 0000000..9f928e7 --- /dev/null +++ b/src/auth/userdb-blocking.c @@ -0,0 +1,144 @@ +/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */ + +#include "auth-common.h" +#include "str.h" +#include "auth-worker-server.h" +#include "userdb.h" +#include "userdb-blocking.h" + + +struct blocking_userdb_iterate_context { + struct userdb_iterate_context ctx; + struct auth_worker_connection *conn; + bool next; + bool destroyed; +}; + +static bool user_callback(struct auth_worker_connection *conn ATTR_UNUSED, + const char *reply, void *context) +{ + struct auth_request *request = context; + enum userdb_result result; + const char *username, *args; + + if (str_begins(reply, "FAIL\t")) { + result = USERDB_RESULT_INTERNAL_FAILURE; + args = reply + 5; + } else if (str_begins(reply, "NOTFOUND\t")) { + result = USERDB_RESULT_USER_UNKNOWN; + args = reply + 9; + } else if (str_begins(reply, "OK\t")) { + result = USERDB_RESULT_OK; + username = reply + 3; + args = strchr(username, '\t'); + if (args == NULL) + args = ""; + else + username = t_strdup_until(username, args++); + if (username[0] != '\0' && + strcmp(request->fields.user, username) != 0) { + auth_request_set_username_forced(request, username); + request->user_changed_by_lookup = TRUE; + } + } else { + result = USERDB_RESULT_INTERNAL_FAILURE; + e_error(authdb_event(request), + "BUG: auth-worker sent invalid user reply"); + args = ""; + } + + if (*args != '\0') { + auth_fields_import(request->fields.userdb_reply, args, 0); + if (auth_fields_exists(request->fields.userdb_reply, "tempfail")) + request->userdb_lookup_tempfailed = TRUE; + } + + auth_request_userdb_callback(result, request); + auth_request_unref(&request); + return TRUE; +} + +void userdb_blocking_lookup(struct auth_request *request) +{ + string_t *str; + + str = t_str_new(128); + str_printfa(str, "USER\t%u\t", request->userdb->userdb->id); + auth_request_export(request, str); + + auth_request_ref(request); + auth_worker_call(request->pool, request->fields.user, + str_c(str), user_callback, request); +} + +static bool iter_callback(struct auth_worker_connection *conn, + const char *reply, void *context) +{ + struct blocking_userdb_iterate_context *ctx = context; + + ctx->conn = conn; + + if (str_begins(reply, "*\t")) { + if (ctx->destroyed) + return TRUE; + ctx->next = FALSE; + ctx->ctx.callback(reply + 2, ctx->ctx.context); + return ctx->next || ctx->destroyed; + } + + if (strcmp(reply, "OK") != 0) + ctx->ctx.failed = TRUE; + if (!ctx->destroyed) + ctx->ctx.callback(NULL, ctx->ctx.context); + auth_request_unref(&ctx->ctx.auth_request); + return TRUE; +} + +struct userdb_iterate_context * +userdb_blocking_iter_init(struct auth_request *request, + userdb_iter_callback_t *callback, void *context) +{ + struct blocking_userdb_iterate_context *ctx; + string_t *str; + + str = t_str_new(128); + str_printfa(str, "LIST\t%u\t", request->userdb->userdb->id); + auth_request_export(request, str); + + ctx = p_new(request->pool, struct blocking_userdb_iterate_context, 1); + ctx->ctx.auth_request = request; + ctx->ctx.callback = callback; + ctx->ctx.context = context; + + auth_request_ref(request); + auth_worker_call(request->pool, "*", + str_c(str), iter_callback, ctx); + return &ctx->ctx; +} + +void userdb_blocking_iter_next(struct userdb_iterate_context *_ctx) +{ + struct blocking_userdb_iterate_context *ctx = + (struct blocking_userdb_iterate_context *)_ctx; + + i_assert(ctx->conn != NULL); + + ctx->next = TRUE; + auth_worker_server_resume_input(ctx->conn); +} + +int userdb_blocking_iter_deinit(struct userdb_iterate_context **_ctx) +{ + struct blocking_userdb_iterate_context *ctx = + (struct blocking_userdb_iterate_context *)*_ctx; + int ret = ctx->ctx.failed ? -1 : 0; + + *_ctx = NULL; + + /* iter_callback() may still be called */ + ctx->destroyed = TRUE; + + if (ctx->conn != NULL) + auth_worker_server_resume_input(ctx->conn); + return ret; +} diff --git a/src/auth/userdb-blocking.h b/src/auth/userdb-blocking.h new file mode 100644 index 0000000..2bbf075 --- /dev/null +++ b/src/auth/userdb-blocking.h @@ -0,0 +1,12 @@ +#ifndef USERDB_BLOCKING_H +#define USERDB_BLOCKING_H + +void userdb_blocking_lookup(struct auth_request *request); + +struct userdb_iterate_context * +userdb_blocking_iter_init(struct auth_request *request, + userdb_iter_callback_t *callback, void *context); +void userdb_blocking_iter_next(struct userdb_iterate_context *ctx); +int userdb_blocking_iter_deinit(struct userdb_iterate_context **ctx); + +#endif diff --git a/src/auth/userdb-checkpassword.c b/src/auth/userdb-checkpassword.c new file mode 100644 index 0000000..aa6455e --- /dev/null +++ b/src/auth/userdb-checkpassword.c @@ -0,0 +1,92 @@ +/* Copyright (c) 2004-2018 Dovecot authors, see the included COPYING file */ + +#include "auth-common.h" +#include "userdb.h" + +#ifdef USERDB_CHECKPASSWORD + +#include "db-checkpassword.h" + +struct checkpassword_userdb_module { + struct userdb_module module; + struct db_checkpassword *db; +}; + +static void +userdb_checkpassword_callback(struct auth_request *request, + enum db_checkpassword_status status, + const char *const *extra_fields, + userdb_callback_t *callback) +{ + unsigned int i; + + switch (status) { + case DB_CHECKPASSWORD_STATUS_INTERNAL_FAILURE: + callback(USERDB_RESULT_INTERNAL_FAILURE, request); + break; + case DB_CHECKPASSWORD_STATUS_FAILURE: + callback(USERDB_RESULT_USER_UNKNOWN, request); + break; + case DB_CHECKPASSWORD_STATUS_OK: + for (i = 0; extra_fields[i] != NULL; i++) { + if (!str_begins(extra_fields[i], "userdb_")) + continue; + auth_request_set_field_keyvalue(request, + extra_fields[i], NULL); + } + callback(USERDB_RESULT_OK, request); + break; + } +} + +static void +checkpassword_lookup(struct auth_request *request, userdb_callback_t *callback) +{ + struct userdb_module *_module = request->userdb->userdb; + struct checkpassword_userdb_module *module = + (struct checkpassword_userdb_module *)_module; + + db_checkpassword_call(module->db, request, NULL, + userdb_checkpassword_callback, callback); +} + +static struct userdb_module * +checkpassword_preinit(pool_t pool, const char *args) +{ + struct checkpassword_userdb_module *module; + const char *checkpassword_path = args; + const char *checkpassword_reply_path = + PKG_LIBEXECDIR"/checkpassword-reply"; + + module = p_new(pool, struct checkpassword_userdb_module, 1); + module->db = db_checkpassword_init(checkpassword_path, + checkpassword_reply_path); + return &module->module; +} + +static void checkpassword_deinit(struct userdb_module *_module) +{ + struct checkpassword_userdb_module *module = + (struct checkpassword_userdb_module *)_module; + + db_checkpassword_deinit(&module->db); +} + +struct userdb_module_interface userdb_checkpassword = { + "checkpassword", + + checkpassword_preinit, + NULL, + checkpassword_deinit, + + checkpassword_lookup, + + NULL, + NULL, + NULL +}; +#else +struct userdb_module_interface userdb_checkpassword = { + .name = "checkpassword" +}; +#endif diff --git a/src/auth/userdb-dict.c b/src/auth/userdb-dict.c new file mode 100644 index 0000000..51e618e --- /dev/null +++ b/src/auth/userdb-dict.c @@ -0,0 +1,205 @@ +/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */ + +#include "auth-common.h" +#include "userdb.h" + +#include "ioloop.h" +#include "array.h" +#include "str.h" +#include "auth-cache.h" +#include "db-dict.h" + +#include <dict.h> + +struct dict_userdb_module { + struct userdb_module module; + + struct dict_connection *conn; +}; + +struct dict_userdb_iterate_context { + struct userdb_iterate_context ctx; + + userdb_callback_t *userdb_callback; + const char *key_prefix; + size_t key_prefix_len; + struct dict_iterate_context *iter; +}; + +static int +dict_query_save_results(struct auth_request *auth_request, + struct db_dict_value_iter *iter) +{ + const char *key, *value, *error; + + while (db_dict_value_iter_next(iter, &key, &value)) { + if (value != NULL) + auth_request_set_userdb_field(auth_request, key, value); + } + if (db_dict_value_iter_deinit(&iter, &error) < 0) { + e_error(authdb_event(auth_request), "%s", error); + return -1; + } + return 0; +} + +static void userdb_dict_lookup(struct auth_request *auth_request, + userdb_callback_t *callback) +{ + struct userdb_module *_module = auth_request->userdb->userdb; + struct dict_userdb_module *module = + (struct dict_userdb_module *)_module; + struct db_dict_value_iter *iter; + enum userdb_result userdb_result; + int ret; + + if (array_count(&module->conn->set.userdb_fields) == 0 && + array_count(&module->conn->set.parsed_userdb_objects) == 0) { + e_error(authdb_event(auth_request), + "No userdb_objects or userdb_fields specified"); + callback(USERDB_RESULT_INTERNAL_FAILURE, auth_request); + return; + } + + ret = db_dict_value_iter_init(module->conn, auth_request, + &module->conn->set.userdb_fields, + &module->conn->set.parsed_userdb_objects, + &iter); + if (ret < 0) + userdb_result = USERDB_RESULT_INTERNAL_FAILURE; + else if (ret == 0) { + auth_request_log_unknown_user(auth_request, AUTH_SUBSYS_DB); + userdb_result = USERDB_RESULT_USER_UNKNOWN; + } else { + if (dict_query_save_results(auth_request, iter) < 0) + userdb_result = USERDB_RESULT_INTERNAL_FAILURE; + else + userdb_result = USERDB_RESULT_OK; + } + callback(userdb_result, auth_request); +} + +static struct userdb_iterate_context * +userdb_dict_iterate_init(struct auth_request *auth_request, + userdb_iter_callback_t *callback, void *context) +{ + struct userdb_module *_module = auth_request->userdb->userdb; + struct dict_userdb_module *module = + (struct dict_userdb_module *)_module; + struct dict_userdb_iterate_context *ctx; + string_t *path; + const char *error; + + ctx = i_new(struct dict_userdb_iterate_context, 1); + ctx->ctx.auth_request = auth_request; + ctx->ctx.callback = callback; + ctx->ctx.context = context; + auth_request_ref(auth_request); + + if (*module->conn->set.iterate_prefix == '\0') { + if (!module->conn->set.iterate_disable) { + e_error(authdb_event(auth_request), + "iterate: iterate_prefix not set"); + ctx->ctx.failed = TRUE; + } + return &ctx->ctx; + } + + path = t_str_new(128); + str_append(path, DICT_PATH_SHARED); + if (auth_request_var_expand(path, module->conn->set.iterate_prefix, + auth_request, NULL, &error) <= 0) { + e_error(authdb_event(auth_request), + "Failed to expand iterate_prefix=%s: %s", + module->conn->set.iterate_prefix, error); + ctx->ctx.failed = TRUE; + return &ctx->ctx; + } + ctx->key_prefix = p_strdup(auth_request->pool, str_c(path)); + ctx->key_prefix_len = strlen(ctx->key_prefix); + + struct dict_op_settings set = { + .username = auth_request->fields.user, + }; + ctx->iter = dict_iterate_init(module->conn->dict, &set, ctx->key_prefix, 0); + e_debug(authdb_event(auth_request), + "iterate: prefix=%s", ctx->key_prefix); + return &ctx->ctx; +} + +static const char * +userdb_dict_get_user(struct dict_userdb_iterate_context *ctx, const char *key) +{ + i_assert(strncmp(key, ctx->key_prefix, ctx->key_prefix_len) == 0); + + return key + ctx->key_prefix_len; +} + +static void userdb_dict_iterate_next(struct userdb_iterate_context *_ctx) +{ + struct dict_userdb_iterate_context *ctx = + (struct dict_userdb_iterate_context *)_ctx; + const char *key, *value; + + if (ctx->iter != NULL && dict_iterate(ctx->iter, &key, &value)) + _ctx->callback(userdb_dict_get_user(ctx, key), _ctx->context); + else + _ctx->callback(NULL, _ctx->context); +} + +static int userdb_dict_iterate_deinit(struct userdb_iterate_context *_ctx) +{ + struct dict_userdb_iterate_context *ctx = + (struct dict_userdb_iterate_context *)_ctx; + const char *error; + int ret = _ctx->failed ? -1 : 0; + + if (dict_iterate_deinit(&ctx->iter, &error) < 0) { + e_error(authdb_event(_ctx->auth_request), + "dict_iterate(%s) failed: %s", + ctx->key_prefix, error); + ret = -1; + } + auth_request_unref(&ctx->ctx.auth_request); + i_free(ctx); + return ret; +} + +static struct userdb_module * +userdb_dict_preinit(pool_t pool, const char *args) +{ + struct dict_userdb_module *module; + struct dict_connection *conn; + + module = p_new(pool, struct dict_userdb_module, 1); + module->conn = conn = db_dict_init(args); + + module->module.blocking = TRUE; + module->module.default_cache_key = auth_cache_parse_key(pool, + db_dict_parse_cache_key(&conn->set.keys, &conn->set.userdb_fields, + &conn->set.parsed_userdb_objects)); + return &module->module; +} + +static void userdb_dict_deinit(struct userdb_module *_module) +{ + struct dict_userdb_module *module = + (struct dict_userdb_module *)_module; + + db_dict_unref(&module->conn); +} + +struct userdb_module_interface userdb_dict = +{ + "dict", + + userdb_dict_preinit, + NULL, + userdb_dict_deinit, + + userdb_dict_lookup, + + userdb_dict_iterate_init, + userdb_dict_iterate_next, + userdb_dict_iterate_deinit +}; diff --git a/src/auth/userdb-ldap.c b/src/auth/userdb-ldap.c new file mode 100644 index 0000000..ecbf09e --- /dev/null +++ b/src/auth/userdb-ldap.c @@ -0,0 +1,343 @@ +/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */ + +#include "auth-common.h" +#include "userdb.h" + +#if defined(USERDB_LDAP) && (defined(BUILTIN_LDAP) || defined(PLUGIN_BUILD)) + +#include "ioloop.h" +#include "array.h" +#include "str.h" +#include "auth-cache.h" +#include "db-ldap.h" + +#include <ldap.h> + +struct ldap_userdb_module { + struct userdb_module module; + + struct ldap_connection *conn; +}; + +struct userdb_ldap_request { + struct ldap_request_search request; + userdb_callback_t *userdb_callback; + unsigned int entries; +}; + +struct userdb_iter_ldap_request { + struct ldap_request_search request; + struct ldap_userdb_iterate_context *ctx; + userdb_callback_t *userdb_callback; +}; + +struct ldap_userdb_iterate_context { + struct userdb_iterate_context ctx; + struct userdb_iter_ldap_request request; + pool_t pool; + struct ldap_connection *conn; + bool continued, in_callback, deinitialized; +}; + +static void +ldap_query_get_result(struct ldap_connection *conn, + struct auth_request *auth_request, + struct ldap_request_search *ldap_request, + LDAPMessage *res) +{ + struct db_ldap_result_iterate_context *ldap_iter; + const char *name, *const *values; + + ldap_iter = db_ldap_result_iterate_init(conn, ldap_request, res, TRUE); + while (db_ldap_result_iterate_next(ldap_iter, &name, &values)) { + auth_request_set_userdb_field_values(auth_request, + name, values); + } + db_ldap_result_iterate_deinit(&ldap_iter); +} + +static void +userdb_ldap_lookup_finish(struct auth_request *auth_request, + struct userdb_ldap_request *urequest, + LDAPMessage *res) +{ + enum userdb_result result = USERDB_RESULT_INTERNAL_FAILURE; + + if (res == NULL) { + result = USERDB_RESULT_INTERNAL_FAILURE; + } else if (urequest->entries == 0) { + result = USERDB_RESULT_USER_UNKNOWN; + auth_request_log_unknown_user(auth_request, AUTH_SUBSYS_DB); + } else if (urequest->entries > 1) { + e_error(authdb_event(auth_request), + "user_filter matched multiple objects, aborting"); + result = USERDB_RESULT_INTERNAL_FAILURE; + } else { + result = USERDB_RESULT_OK; + } + + urequest->userdb_callback(result, auth_request); +} + +static void userdb_ldap_lookup_callback(struct ldap_connection *conn, + struct ldap_request *request, + LDAPMessage *res) +{ + struct userdb_ldap_request *urequest = + (struct userdb_ldap_request *) request; + struct auth_request *auth_request = + urequest->request.request.auth_request; + + if (res == NULL || ldap_msgtype(res) == LDAP_RES_SEARCH_RESULT) { + userdb_ldap_lookup_finish(auth_request, urequest, res); + auth_request_unref(&auth_request); + return; + } + + if (urequest->entries++ == 0) { + /* first entry */ + ldap_query_get_result(conn, auth_request, + &urequest->request, res); + } +} + +static void userdb_ldap_lookup(struct auth_request *auth_request, + userdb_callback_t *callback) +{ + struct userdb_module *_module = auth_request->userdb->userdb; + struct ldap_userdb_module *module = + (struct ldap_userdb_module *)_module; + struct ldap_connection *conn = module->conn; + const char **attr_names = (const char **)conn->user_attr_names; + struct userdb_ldap_request *request; + const char *error; + string_t *str; + + auth_request_ref(auth_request); + request = p_new(auth_request->pool, struct userdb_ldap_request, 1); + request->userdb_callback = callback; + + str = t_str_new(512); + if (auth_request_var_expand(str, conn->set.base, auth_request, + ldap_escape, &error) <= 0) { + e_error(authdb_event(auth_request), + "Failed to expand base=%s: %s", conn->set.base, error); + callback(USERDB_RESULT_INTERNAL_FAILURE, auth_request); + return; + } + request->request.base = p_strdup(auth_request->pool, str_c(str)); + + str_truncate(str, 0); + if (auth_request_var_expand(str, conn->set.user_filter, auth_request, + ldap_escape, &error) <= 0) { + e_error(authdb_event(auth_request), + "Failed to expand user_filter=%s: %s", + conn->set.user_filter, error); + callback(USERDB_RESULT_INTERNAL_FAILURE, auth_request); + return; + } + request->request.filter = p_strdup(auth_request->pool, str_c(str)); + + request->request.attr_map = &conn->user_attr_map; + request->request.attributes = conn->user_attr_names; + + e_debug(authdb_event(auth_request), "user search: " + "base=%s scope=%s filter=%s fields=%s", + request->request.base, conn->set.scope, + request->request.filter, + attr_names == NULL ? "(all)" : + t_strarray_join(attr_names, ",")); + + request->request.request.auth_request = auth_request; + request->request.request.callback = userdb_ldap_lookup_callback; + db_ldap_request(conn, &request->request.request); +} + +static void userdb_ldap_iterate_callback(struct ldap_connection *conn, + struct ldap_request *request, + LDAPMessage *res) +{ + struct userdb_iter_ldap_request *urequest = + (struct userdb_iter_ldap_request *)request; + struct ldap_userdb_iterate_context *ctx = urequest->ctx; + struct db_ldap_result_iterate_context *ldap_iter; + const char *name, *const *values; + + if (res == NULL || ldap_msgtype(res) == LDAP_RES_SEARCH_RESULT) { + if (res == NULL) + ctx->ctx.failed = TRUE; + if (!ctx->deinitialized) + ctx->ctx.callback(NULL, ctx->ctx.context); + auth_request_unref(&request->auth_request); + return; + } + + if (ctx->deinitialized) + return; + + /* the iteration can take a while. reset the request's create time so + it won't be aborted while it's still running */ + request->create_time = ioloop_time; + + ctx->in_callback = TRUE; + ldap_iter = db_ldap_result_iterate_init(conn, &urequest->request, + res, TRUE); + while (db_ldap_result_iterate_next(ldap_iter, &name, &values)) { + if (strcmp(name, "user") != 0) { + e_warning(authdb_event(request->auth_request), "iterate: " + "Ignoring field not named 'user': %s", name); + continue; + } + for (; *values != NULL; values++) { + ctx->continued = FALSE; + ctx->ctx.callback(*values, ctx->ctx.context); + } + } + db_ldap_result_iterate_deinit(&ldap_iter); + if (!ctx->continued) + db_ldap_enable_input(conn, FALSE); + ctx->in_callback = FALSE; +} + +static struct userdb_iterate_context * +userdb_ldap_iterate_init(struct auth_request *auth_request, + userdb_iter_callback_t *callback, void *context) +{ + struct userdb_module *_module = auth_request->userdb->userdb; + struct ldap_userdb_module *module = + (struct ldap_userdb_module *)_module; + struct ldap_connection *conn = module->conn; + struct ldap_userdb_iterate_context *ctx; + struct userdb_iter_ldap_request *request; + const char **attr_names = (const char **)conn->iterate_attr_names; + const char *error; + string_t *str; + + ctx = p_new(auth_request->pool, struct ldap_userdb_iterate_context, 1); + ctx->ctx.auth_request = auth_request; + ctx->ctx.callback = callback; + ctx->ctx.context = context; + ctx->conn = conn; + request = &ctx->request; + request->ctx = ctx; + + auth_request_ref(auth_request); + request->request.request.auth_request = auth_request; + + str = t_str_new(512); + if (auth_request_var_expand(str, conn->set.base, auth_request, + ldap_escape, &error) <= 0) { + e_error(authdb_event(auth_request), + "Failed to expand base=%s: %s", conn->set.base, error); + ctx->ctx.failed = TRUE; + } + request->request.base = p_strdup(auth_request->pool, str_c(str)); + + str_truncate(str, 0); + if (auth_request_var_expand(str, conn->set.iterate_filter, + auth_request, ldap_escape, &error) <= 0) { + e_error(authdb_event(auth_request), + "Failed to expand iterate_filter=%s: %s", + conn->set.iterate_filter, error); + ctx->ctx.failed = TRUE; + } + request->request.filter = p_strdup(auth_request->pool, str_c(str)); + request->request.attr_map = &conn->iterate_attr_map; + request->request.attributes = conn->iterate_attr_names; + request->request.multi_entry = TRUE; + + e_debug(auth_request->event, "ldap: iterate: base=%s scope=%s filter=%s fields=%s", + request->request.base, conn->set.scope, + request->request.filter, attr_names == NULL ? "(all)" : + t_strarray_join(attr_names, ",")); + request->request.request.callback = userdb_ldap_iterate_callback; + db_ldap_request(conn, &request->request.request); + return &ctx->ctx; +} + +static void userdb_ldap_iterate_next(struct userdb_iterate_context *_ctx) +{ + struct ldap_userdb_iterate_context *ctx = + (struct ldap_userdb_iterate_context *)_ctx; + + ctx->continued = TRUE; + if (!ctx->in_callback) + db_ldap_enable_input(ctx->conn, TRUE); +} + +static int userdb_ldap_iterate_deinit(struct userdb_iterate_context *_ctx) +{ + struct ldap_userdb_iterate_context *ctx = + (struct ldap_userdb_iterate_context *)_ctx; + int ret = _ctx->failed ? -1 : 0; + + db_ldap_enable_input(ctx->conn, TRUE); + ctx->deinitialized = TRUE; + return ret; +} + +static struct userdb_module * +userdb_ldap_preinit(pool_t pool, const char *args) +{ + struct ldap_userdb_module *module; + struct ldap_connection *conn; + + module = p_new(pool, struct ldap_userdb_module, 1); + module->conn = conn = db_ldap_init(args, TRUE); + p_array_init(&conn->user_attr_map, pool, 16); + p_array_init(&conn->iterate_attr_map, pool, 16); + + db_ldap_set_attrs(conn, conn->set.user_attrs, &conn->user_attr_names, + &conn->user_attr_map, NULL); + db_ldap_set_attrs(conn, conn->set.iterate_attrs, + &conn->iterate_attr_names, + &conn->iterate_attr_map, NULL); + module->module.blocking = conn->set.blocking; + module->module.default_cache_key = + auth_cache_parse_key(pool, + t_strconcat(conn->set.base, + conn->set.user_attrs, + conn->set.user_filter, NULL)); + return &module->module; +} + +static void userdb_ldap_init(struct userdb_module *_module) +{ + struct ldap_userdb_module *module = + (struct ldap_userdb_module *)_module; + + if (!module->module.blocking || worker) + db_ldap_connect_delayed(module->conn); +} + +static void userdb_ldap_deinit(struct userdb_module *_module) +{ + struct ldap_userdb_module *module = + (struct ldap_userdb_module *)_module; + + db_ldap_unref(&module->conn); +} + +#ifndef PLUGIN_BUILD +struct userdb_module_interface userdb_ldap = +#else +struct userdb_module_interface userdb_ldap_plugin = +#endif +{ + "ldap", + + userdb_ldap_preinit, + userdb_ldap_init, + userdb_ldap_deinit, + + userdb_ldap_lookup, + + userdb_ldap_iterate_init, + userdb_ldap_iterate_next, + userdb_ldap_iterate_deinit +}; +#else +struct userdb_module_interface userdb_ldap = { + .name = "ldap" +}; +#endif diff --git a/src/auth/userdb-lua.c b/src/auth/userdb-lua.c new file mode 100644 index 0000000..6b75c0d --- /dev/null +++ b/src/auth/userdb-lua.c @@ -0,0 +1,139 @@ +/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */ + +#include "auth-common.h" +#include "userdb.h" +#include "auth-cache.h" + +#if defined(BUILTIN_LUA) || defined(PLUGIN_BUILD) + +#include "db-lua.h" + +struct dlua_userdb_module { + struct userdb_module module; + struct dlua_script *script; + const char *file; +}; + +static void userdb_lua_lookup(struct auth_request *auth_request, + userdb_callback_t *callback) +{ + struct userdb_module *_module = auth_request->userdb->userdb; + struct dlua_userdb_module *module = + (struct dlua_userdb_module *)_module; + const char *error; + enum userdb_result result = + auth_lua_call_userdb_lookup(module->script, auth_request, &error); + if (result == USERDB_RESULT_INTERNAL_FAILURE) + e_error(authdb_event(auth_request), + "userdb-lua: %s", error); + callback(result, auth_request); +} + +static struct userdb_module * +userdb_lua_preinit(pool_t pool, const char *args) +{ + struct dlua_userdb_module *module; + const char *cache_key = "%u"; + bool blocking = TRUE; + + module = p_new(pool, struct dlua_userdb_module, 1); + const char *const *fields = t_strsplit_spaces(args, " "); + while(*fields != NULL) { + if (str_begins(*fields, "file=")) { + module->file = p_strdup(pool, (*fields)+5); + } else if (str_begins(*fields, "blocking=")) { + const char *value = (*fields)+9; + if (strcmp(value, "yes") == 0) { + blocking = TRUE; + } else if (strcmp(value, "no") == 0) { + blocking = FALSE; + } else { + i_fatal("Invalid value %s. " + "Field blocking must be yes or no", + value); + } + } else if (str_begins(*fields, "cache_key=")) { + if (*((*fields)+10) != '\0') + cache_key = (*fields)+10; + else /* explicitly disable auth caching for lua */ + cache_key = NULL; + } else { + i_fatal("Unsupported parameter %s", *fields); + } + fields++; + } + + if (module->file == NULL) + i_fatal("userdb-lua: Missing mandatory file= parameter"); + + module->module.blocking = blocking; + if (cache_key != NULL) { + module->module.default_cache_key = + auth_cache_parse_key(pool, cache_key); + } + return &module->module; +} + +static void userdb_lua_init(struct userdb_module *_module) +{ + struct dlua_userdb_module *module = + (struct dlua_userdb_module *)_module; + const char *error; + + if (dlua_script_create_file(module->file, &module->script, auth_event, &error) < 0 || + auth_lua_script_init(module->script, &error) < 0) + i_fatal("userdb-lua: initialization failed: %s", error); +} + +static void userdb_lua_deinit(struct userdb_module *_module) +{ + struct dlua_userdb_module *module = + (struct dlua_userdb_module *)_module; + dlua_script_unref(&module->script); +} + +static struct userdb_iterate_context * +userdb_lua_iterate_init(struct auth_request *auth_request, + userdb_iter_callback_t *callback, + void *context) +{ + struct userdb_module *_module = auth_request->userdb->userdb; + struct dlua_userdb_module *module = + (struct dlua_userdb_module *)_module; + return auth_lua_call_userdb_iterate_init(module->script, auth_request, + callback, context); +} + +static void userdb_lua_iterate_next(struct userdb_iterate_context *ctx) +{ + auth_lua_userdb_iterate_next(ctx); +} + +static int userdb_lua_iterate_deinit(struct userdb_iterate_context *ctx) +{ + return auth_lua_userdb_iterate_deinit(ctx); +} + +#ifndef PLUGIN_BUILD +struct userdb_module_interface userdb_lua = +#else +struct userdb_module_interface userdb_lua_plugin = +#endif +{ + "lua", + + userdb_lua_preinit, + userdb_lua_init, + userdb_lua_deinit, + + userdb_lua_lookup, + + userdb_lua_iterate_init, + userdb_lua_iterate_next, + userdb_lua_iterate_deinit +}; +#else +struct userdb_module_interface userdb_lua = { + .name = "lua" +}; +#endif diff --git a/src/auth/userdb-passwd-file.c b/src/auth/userdb-passwd-file.c new file mode 100644 index 0000000..1e87138 --- /dev/null +++ b/src/auth/userdb-passwd-file.c @@ -0,0 +1,247 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +#include "auth-common.h" +#include "userdb.h" + +#ifdef USERDB_PASSWD_FILE + +#include "istream.h" +#include "str.h" +#include "auth-cache.h" +#include "db-passwd-file.h" + +#include <unistd.h> +#include <fcntl.h> + +struct passwd_file_userdb_iterate_context { + struct userdb_iterate_context ctx; + struct istream *input; + char *path; + bool skip_passdb_entries; +}; + +struct passwd_file_userdb_module { + struct userdb_module module; + + struct db_passwd_file *pwf; + const char *username_format; +}; + +static int +passwd_file_add_extra_fields(struct auth_request *request, char *const *fields) +{ + string_t *str = t_str_new(512); + const struct var_expand_table *table; + const char *key, *value, *error; + unsigned int i; + + table = auth_request_get_var_expand_table(request, NULL); + + for (i = 0; fields[i] != NULL; i++) { + if (!str_begins(fields[i], "userdb_")) + continue; + + key = fields[i] + 7; + value = strchr(key, '='); + if (value != NULL) { + key = t_strdup_until(key, value); + str_truncate(str, 0); + if (auth_request_var_expand_with_table(str, value + 1, + request, table, NULL, &error) <= 0) { + e_error(authdb_event(request), + "Failed to expand extra field %s: %s", + fields[i], error); + return -1; + } + value = str_c(str); + } else { + value = ""; + } + auth_request_set_userdb_field(request, key, value); + } + return 0; +} + +static void passwd_file_lookup(struct auth_request *auth_request, + userdb_callback_t *callback) +{ + struct userdb_module *_module = auth_request->userdb->userdb; + struct passwd_file_userdb_module *module = + (struct passwd_file_userdb_module *)_module; + struct passwd_user *pu; + int ret; + + ret = db_passwd_file_lookup(module->pwf, auth_request, + module->username_format, &pu); + if (ret <= 0 || pu->uid == 0) { + callback(ret < 0 ? USERDB_RESULT_INTERNAL_FAILURE : + USERDB_RESULT_USER_UNKNOWN, auth_request); + return; + } + + if (pu->uid != (uid_t)-1) { + auth_request_set_userdb_field(auth_request, "uid", + dec2str(pu->uid)); + } + if (pu->gid != (gid_t)-1) { + auth_request_set_userdb_field(auth_request, "gid", + dec2str(pu->gid)); + } + + if (pu->home != NULL) + auth_request_set_userdb_field(auth_request, "home", pu->home); + + if (pu->extra_fields != NULL && + passwd_file_add_extra_fields(auth_request, pu->extra_fields) < 0) { + callback(USERDB_RESULT_INTERNAL_FAILURE, auth_request); + return; + } + + callback(USERDB_RESULT_OK, auth_request); +} + +static struct userdb_iterate_context * +passwd_file_iterate_init(struct auth_request *auth_request, + userdb_iter_callback_t *callback, void *context) +{ + struct userdb_module *_module = auth_request->userdb->userdb; + struct passwd_file_userdb_module *module = + (struct passwd_file_userdb_module *)_module; + struct passwd_file_userdb_iterate_context *ctx; + int fd; + + ctx = i_new(struct passwd_file_userdb_iterate_context, 1); + ctx->ctx.auth_request = auth_request; + ctx->ctx.callback = callback; + ctx->ctx.context = context; + ctx->skip_passdb_entries = !module->pwf->userdb_warn_missing; + if (module->pwf->default_file == NULL) { + e_error(authdb_event(auth_request), + "passwd-file: User iteration isn't currently supported " + "with %%variable paths"); + ctx->ctx.failed = TRUE; + return &ctx->ctx; + } + ctx->path = i_strdup(module->pwf->default_file->path); + + /* for now we support only a single passwd-file */ + fd = open(ctx->path, O_RDONLY); + if (fd == -1) { + e_error(authdb_event(auth_request), + "open(%s) failed: %m", ctx->path); + ctx->ctx.failed = TRUE; + } else { + ctx->input = i_stream_create_fd_autoclose(&fd, SIZE_MAX); + } + return &ctx->ctx; +} + +static void passwd_file_iterate_next(struct userdb_iterate_context *_ctx) +{ + struct passwd_file_userdb_iterate_context *ctx = + (struct passwd_file_userdb_iterate_context *)_ctx; + const char *line, *p; + + if (ctx->input == NULL) + line = NULL; + else { + while ((line = i_stream_read_next_line(ctx->input)) != NULL) { + if (*line == '\0' || *line == ':' || *line == '#') + continue; /* no username or comment */ + if (ctx->skip_passdb_entries && + ((p = i_strchr_to_next(line, ':')) == NULL || + strchr(p, ':') == NULL)) { + /* only passdb info */ + continue; + } + break; + } + if (line == NULL && ctx->input->stream_errno != 0) { + e_error(authdb_event(_ctx->auth_request), + "read(%s) failed: %s", ctx->path, + i_stream_get_error(ctx->input)); + _ctx->failed = TRUE; + } + } + if (line == NULL) + _ctx->callback(NULL, _ctx->context); + else T_BEGIN { + _ctx->callback(t_strcut(line, ':'), _ctx->context); + } T_END; +} + +static int passwd_file_iterate_deinit(struct userdb_iterate_context *_ctx) +{ + struct passwd_file_userdb_iterate_context *ctx = + (struct passwd_file_userdb_iterate_context *)_ctx; + int ret = _ctx->failed ? -1 : 0; + + i_stream_destroy(&ctx->input); + i_free(ctx->path); + i_free(ctx); + return ret; +} + +static struct userdb_module * +passwd_file_preinit(pool_t pool, const char *args) +{ + struct passwd_file_userdb_module *module; + const char *format = PASSWD_FILE_DEFAULT_USERNAME_FORMAT; + const char *p; + + if (str_begins(args, "username_format=")) { + args += 16; + p = strchr(args, ' '); + if (p == NULL) { + format = p_strdup(pool, args); + args = ""; + } else { + format = p_strdup_until(pool, args, p); + args = p + 1; + } + } + + if (*args == '\0') + i_fatal("userdb passwd-file: Missing args"); + + module = p_new(pool, struct passwd_file_userdb_module, 1); + module->pwf = db_passwd_file_init(args, TRUE, + global_auth_settings->debug); + module->username_format = format; + return &module->module; +} + +static void passwd_file_init(struct userdb_module *_module) +{ + struct passwd_file_userdb_module *module = + (struct passwd_file_userdb_module *)_module; + + db_passwd_file_parse(module->pwf); +} + +static void passwd_file_deinit(struct userdb_module *_module) +{ + struct passwd_file_userdb_module *module = + (struct passwd_file_userdb_module *)_module; + + db_passwd_file_unref(&module->pwf); +} + +struct userdb_module_interface userdb_passwd_file = { + "passwd-file", + + passwd_file_preinit, + passwd_file_init, + passwd_file_deinit, + + passwd_file_lookup, + + passwd_file_iterate_init, + passwd_file_iterate_next, + passwd_file_iterate_deinit +}; +#else +struct userdb_module_interface userdb_passwd_file = { + .name = "passwd-file" +}; +#endif diff --git a/src/auth/userdb-passwd.c b/src/auth/userdb-passwd.c new file mode 100644 index 0000000..3c99102 --- /dev/null +++ b/src/auth/userdb-passwd.c @@ -0,0 +1,253 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +#include "auth-common.h" +#include "userdb.h" + +#ifdef USERDB_PASSWD + +#include "ioloop.h" +#include "ipwd.h" +#include "time-util.h" +#include "userdb-template.h" + +#define USER_CACHE_KEY "%u" +#define PASSWD_SLOW_WARN_MSECS (10*1000) +#define PASSWD_SLOW_MASTER_WARN_MSECS 50 +#define PASSDB_SLOW_MASTER_WARN_COUNT_INTERVAL 100 +#define PASSDB_SLOW_MASTER_WARN_MIN_PERCENTAGE 5 + +struct passwd_userdb_module { + struct userdb_module module; + struct userdb_template *tmpl; + + unsigned int fast_count, slow_count; + bool slow_warned:1; +}; + +struct passwd_userdb_iterate_context { + struct userdb_iterate_context ctx; + struct passwd_userdb_iterate_context *next_waiting; +}; + +static struct passwd_userdb_iterate_context *cur_userdb_iter = NULL; +static struct timeout *cur_userdb_iter_to = NULL; + +static void +passwd_check_warnings(struct auth_request *auth_request, + struct passwd_userdb_module *module, + const struct timeval *start_tv) +{ + struct timeval end_tv; + unsigned int msecs, percentage; + + i_gettimeofday(&end_tv); + + msecs = timeval_diff_msecs(&end_tv, start_tv); + if (msecs >= PASSWD_SLOW_WARN_MSECS) { + e_warning(authdb_event(auth_request), "Lookup for %s took %u secs", + auth_request->fields.user, msecs/1000); + return; + } + if (worker || module->slow_warned) + return; + + if (msecs < PASSWD_SLOW_MASTER_WARN_MSECS) { + module->fast_count++; + return; + } + module->slow_count++; + if (module->fast_count + module->slow_count < + PASSDB_SLOW_MASTER_WARN_COUNT_INTERVAL) + return; + + percentage = module->slow_count * 100 / + (module->slow_count + module->fast_count); + if (percentage < PASSDB_SLOW_MASTER_WARN_MIN_PERCENTAGE) { + /* start from beginning */ + module->slow_count = module->fast_count = 0; + } else { + e_warning(authdb_event(auth_request), + "%u%% of last %u lookups took over " + "%u milliseconds, " + "you may want to set blocking=yes for userdb", + percentage, PASSDB_SLOW_MASTER_WARN_COUNT_INTERVAL, + PASSWD_SLOW_MASTER_WARN_MSECS); + module->slow_warned = TRUE; + } +} + +static void passwd_lookup(struct auth_request *auth_request, + userdb_callback_t *callback) +{ + struct userdb_module *_module = auth_request->userdb->userdb; + struct passwd_userdb_module *module = + (struct passwd_userdb_module *)_module; + struct passwd pw; + struct timeval start_tv; + const char *error; + int ret; + + e_debug(authdb_event(auth_request), "lookup"); + + i_gettimeofday(&start_tv); + ret = i_getpwnam(auth_request->fields.user, &pw); + if (start_tv.tv_sec != 0) + passwd_check_warnings(auth_request, module, &start_tv); + + switch (ret) { + case -1: + e_error(authdb_event(auth_request), + "getpwnam() failed: %m"); + callback(USERDB_RESULT_INTERNAL_FAILURE, auth_request); + return; + case 0: + auth_request_log_unknown_user(auth_request, AUTH_SUBSYS_DB); + callback(USERDB_RESULT_USER_UNKNOWN, auth_request); + return; + } + + auth_request_set_field(auth_request, "user", pw.pw_name, NULL); + + auth_request_set_userdb_field(auth_request, "system_groups_user", + pw.pw_name); + auth_request_set_userdb_field(auth_request, "uid", dec2str(pw.pw_uid)); + auth_request_set_userdb_field(auth_request, "gid", dec2str(pw.pw_gid)); + auth_request_set_userdb_field(auth_request, "home", pw.pw_dir); + + if (userdb_template_export(module->tmpl, auth_request, &error) < 0) { + e_error(authdb_event(auth_request), + "Failed to expand template: %s", error); + callback(USERDB_RESULT_INTERNAL_FAILURE, auth_request); + } + + callback(USERDB_RESULT_OK, auth_request); +} + +static struct userdb_iterate_context * +passwd_iterate_init(struct auth_request *auth_request, + userdb_iter_callback_t *callback, void *context) +{ + struct passwd_userdb_iterate_context *ctx; + + ctx = i_new(struct passwd_userdb_iterate_context, 1); + ctx->ctx.auth_request = auth_request; + ctx->ctx.callback = callback; + ctx->ctx.context = context; + setpwent(); + + if (cur_userdb_iter == NULL) + cur_userdb_iter = ctx; + return &ctx->ctx; +} + +static bool +passwd_iterate_want_pw(struct passwd *pw, const struct auth_settings *set) +{ + /* skip entries not in valid UID range. + they're users for daemons and such. */ + if (pw->pw_uid < (uid_t)set->first_valid_uid) + return FALSE; + if (pw->pw_uid > (uid_t)set->last_valid_uid && set->last_valid_uid != 0) + return FALSE; + if (pw->pw_gid < (gid_t)set->first_valid_gid) + return FALSE; + if (pw->pw_gid > (gid_t)set->last_valid_gid && set->last_valid_gid != 0) + return FALSE; + return TRUE; +} + +static void passwd_iterate_next(struct userdb_iterate_context *_ctx) +{ + struct passwd_userdb_iterate_context *ctx = + (struct passwd_userdb_iterate_context *)_ctx; + const struct auth_settings *set = _ctx->auth_request->set; + struct passwd *pw; + + if (cur_userdb_iter != NULL && cur_userdb_iter != ctx) { + /* we can't support concurrent userdb iteration. + wait until the previous one is done */ + ctx->next_waiting = cur_userdb_iter->next_waiting; + cur_userdb_iter->next_waiting = ctx; + return; + } + + /* reset errno since it might have been set when we got here */ + errno = 0; + while ((pw = getpwent()) != NULL) { + if (passwd_iterate_want_pw(pw, set)) { + _ctx->callback(pw->pw_name, _ctx->context); + return; + } + /* getpwent might set errno to something even if it + returns non-NULL. */ + errno = 0; + } + if (errno != 0) { + e_error(authdb_event(_ctx->auth_request), + "getpwent() failed: %m"); + _ctx->failed = TRUE; + } + _ctx->callback(NULL, _ctx->context); +} + +static void ATTR_NULL(1) +passwd_iterate_next_timeout(void *context ATTR_UNUSED) +{ + timeout_remove(&cur_userdb_iter_to); + passwd_iterate_next(&cur_userdb_iter->ctx); +} + +static int passwd_iterate_deinit(struct userdb_iterate_context *_ctx) +{ + struct passwd_userdb_iterate_context *ctx = + (struct passwd_userdb_iterate_context *)_ctx; + int ret = _ctx->failed ? -1 : 0; + + cur_userdb_iter = ctx->next_waiting; + i_free(ctx); + + if (cur_userdb_iter != NULL) { + cur_userdb_iter_to = timeout_add(0, passwd_iterate_next_timeout, + NULL); + } + endpwent(); + return ret; +} + +static struct userdb_module * +passwd_passwd_preinit(pool_t pool, const char *args) +{ + struct passwd_userdb_module *module; + const char *value; + + module = p_new(pool, struct passwd_userdb_module, 1); + module->module.default_cache_key = USER_CACHE_KEY; + module->tmpl = userdb_template_build(pool, "passwd", args); + module->module.blocking = TRUE; + + if (userdb_template_remove(module->tmpl, "blocking", &value)) + module->module.blocking = strcasecmp(value, "yes") == 0; + /* FIXME: backwards compatibility */ + if (!userdb_template_is_empty(module->tmpl)) + i_warning("userdb passwd: Move templates args to override_fields setting"); + return &module->module; +} + +struct userdb_module_interface userdb_passwd = { + "passwd", + + passwd_passwd_preinit, + NULL, + NULL, + + passwd_lookup, + + passwd_iterate_init, + passwd_iterate_next, + passwd_iterate_deinit +}; +#else +struct userdb_module_interface userdb_passwd = { + .name = "passwd" +}; +#endif diff --git a/src/auth/userdb-prefetch.c b/src/auth/userdb-prefetch.c new file mode 100644 index 0000000..a5730b9 --- /dev/null +++ b/src/auth/userdb-prefetch.c @@ -0,0 +1,59 @@ +/* Copyright (c) 2004-2018 Dovecot authors, see the included COPYING file */ + +#include "auth-common.h" +#include "userdb.h" + +#ifdef USERDB_PREFETCH + +#include "str.h" +#include "var-expand.h" + + +static void prefetch_lookup(struct auth_request *auth_request, + userdb_callback_t *callback) +{ + /* auth_request_set_field() should have already placed the userdb_* + values to userdb_reply. */ + if (!auth_request->userdb_prefetch_set) { + if (auth_request_get_auth(auth_request)->userdbs->next == NULL) { + /* no other userdbs */ + if (auth_request->userdb_lookup) { + e_error(authdb_event(auth_request), + "userdb lookup not possible with only userdb prefetch"); + } else { + e_error(authdb_event(auth_request), + "passdb didn't return userdb entries"); + } + callback(USERDB_RESULT_INTERNAL_FAILURE, auth_request); + return; + } + /* more userdbs, they may know the user */ + e_debug(authdb_event(auth_request), + "passdb didn't return userdb entries, " + "trying the next userdb"); + callback(USERDB_RESULT_USER_UNKNOWN, auth_request); + return; + } + + e_debug(authdb_event(auth_request), "success"); + callback(USERDB_RESULT_OK, auth_request); +} + +struct userdb_module_interface userdb_prefetch = { + "prefetch", + + NULL, + NULL, + NULL, + + prefetch_lookup, + + NULL, + NULL, + NULL +}; +#else +struct userdb_module_interface userdb_prefetch = { + .name = "prefetch" +}; +#endif diff --git a/src/auth/userdb-sql.c b/src/auth/userdb-sql.c new file mode 100644 index 0000000..3a87640 --- /dev/null +++ b/src/auth/userdb-sql.c @@ -0,0 +1,319 @@ +/* Copyright (c) 2004-2018 Dovecot authors, see the included COPYING file */ + +#include "auth-common.h" +#include "userdb.h" + +#ifdef USERDB_SQL + +#include "auth-cache.h" +#include "db-sql.h" + +#include <string.h> + +struct sql_userdb_module { + struct userdb_module module; + + struct db_sql_connection *conn; +}; + +struct userdb_sql_request { + struct auth_request *auth_request; + userdb_callback_t *callback; +}; + +struct sql_userdb_iterate_context { + struct userdb_iterate_context ctx; + struct sql_result *result; + bool freed:1; + bool call_iter:1; +}; + +static void userdb_sql_iterate_next(struct userdb_iterate_context *_ctx); +static int userdb_sql_iterate_deinit(struct userdb_iterate_context *_ctx); + +static void +sql_query_get_result(struct sql_result *result, + struct auth_request *auth_request) +{ + const char *name, *value; + unsigned int i, fields_count; + + fields_count = sql_result_get_fields_count(result); + for (i = 0; i < fields_count; i++) { + name = sql_result_get_field_name(result, i); + value = sql_result_get_field_value(result, i); + + if (*name != '\0' && value != NULL) { + auth_request_set_userdb_field(auth_request, + name, value); + } + } +} + +static void sql_query_callback(struct sql_result *sql_result, + struct userdb_sql_request *sql_request) +{ + struct auth_request *auth_request = sql_request->auth_request; + struct userdb_module *_module = auth_request->userdb->userdb; + struct sql_userdb_module *module = + (struct sql_userdb_module *)_module; + enum userdb_result result = USERDB_RESULT_INTERNAL_FAILURE; + int ret; + + ret = sql_result_next_row(sql_result); + if (ret >= 0) + db_sql_success(module->conn); + if (ret < 0) { + if (!module->conn->default_user_query) { + e_error(authdb_event(auth_request), + "User query failed: %s", + sql_result_get_error(sql_result)); + } else { + e_error(authdb_event(auth_request), + "User query failed: %s " + "(using built-in default user_query: %s)", + sql_result_get_error(sql_result), + module->conn->set.user_query); + } + } else if (ret == 0) { + result = USERDB_RESULT_USER_UNKNOWN; + auth_request_log_unknown_user(auth_request, AUTH_SUBSYS_DB); + } else { + sql_query_get_result(sql_result, auth_request); + result = USERDB_RESULT_OK; + } + + sql_request->callback(result, auth_request); + auth_request_unref(&auth_request); + i_free(sql_request); +} + +static const char * +userdb_sql_escape(const char *str, const struct auth_request *auth_request) +{ + struct userdb_module *_module = auth_request->userdb->userdb; + struct sql_userdb_module *module = + (struct sql_userdb_module *)_module; + + return sql_escape_string(module->conn->db, str); +} + +static void userdb_sql_lookup(struct auth_request *auth_request, + userdb_callback_t *callback) +{ + struct userdb_module *_module = auth_request->userdb->userdb; + struct sql_userdb_module *module = + (struct sql_userdb_module *)_module; + struct userdb_sql_request *sql_request; + const char *query, *error; + + if (t_auth_request_var_expand(module->conn->set.user_query, + auth_request, userdb_sql_escape, + &query, &error) <= 0) { + e_error(authdb_event(auth_request), + "Failed to expand user_query=%s: %s", + module->conn->set.user_query, error); + callback(USERDB_RESULT_INTERNAL_FAILURE, auth_request); + return; + } + + auth_request_ref(auth_request); + sql_request = i_new(struct userdb_sql_request, 1); + sql_request->callback = callback; + sql_request->auth_request = auth_request; + + e_debug(authdb_event(auth_request), "%s", query); + + sql_query(module->conn->db, query, + sql_query_callback, sql_request); +} + +static void sql_iter_query_callback(struct sql_result *sql_result, + struct sql_userdb_iterate_context *ctx) +{ + ctx->result = sql_result; + sql_result_ref(sql_result); + + if (ctx->freed) + (void)userdb_sql_iterate_deinit(&ctx->ctx); + else if (ctx->call_iter) + userdb_sql_iterate_next(&ctx->ctx); +} + +static struct userdb_iterate_context * +userdb_sql_iterate_init(struct auth_request *auth_request, + userdb_iter_callback_t *callback, void *context) +{ + struct userdb_module *_module = auth_request->userdb->userdb; + struct sql_userdb_module *module = + (struct sql_userdb_module *)_module; + struct sql_userdb_iterate_context *ctx; + const char *query, *error; + + if (t_auth_request_var_expand(module->conn->set.iterate_query, + auth_request, userdb_sql_escape, + &query, &error) <= 0) { + e_error(authdb_event(auth_request), + "Failed to expand iterate_query=%s: %s", + module->conn->set.iterate_query, error); + } + + ctx = i_new(struct sql_userdb_iterate_context, 1); + ctx->ctx.auth_request = auth_request; + ctx->ctx.callback = callback; + ctx->ctx.context = context; + auth_request_ref(auth_request); + + sql_query(module->conn->db, query, + sql_iter_query_callback, ctx); + e_debug(authdb_event(auth_request), "%s", query); + return &ctx->ctx; +} + +static int userdb_sql_iterate_get_user(struct sql_userdb_iterate_context *ctx, + const char **user_r) +{ + const char *domain; + int idx; + + /* try user first */ + idx = sql_result_find_field(ctx->result, "user"); + if (idx == 0) { + *user_r = sql_result_get_field_value(ctx->result, idx); + return 0; + } + + /* username [+ domain]? */ + idx = sql_result_find_field(ctx->result, "username"); + if (idx < 0) { + /* no user or username, fail */ + return -1; + } + + *user_r = sql_result_get_field_value(ctx->result, idx); + if (*user_r == NULL) + return 0; + + domain = sql_result_find_field_value(ctx->result, "domain"); + if (domain != NULL) + *user_r = t_strconcat(*user_r, "@", domain, NULL); + return 0; +} + +static void userdb_sql_iterate_next(struct userdb_iterate_context *_ctx) +{ + struct sql_userdb_iterate_context *ctx = + (struct sql_userdb_iterate_context *)_ctx; + struct userdb_module *_module = _ctx->auth_request->userdb->userdb; + struct sql_userdb_module *module = (struct sql_userdb_module *)_module; + const char *user; + int ret; + + if (ctx->result == NULL) { + /* query not finished yet */ + ctx->call_iter = TRUE; + return; + } + + ret = sql_result_next_row(ctx->result); + if (ret >= 0) + db_sql_success(module->conn); + if (ret > 0) { + if (userdb_sql_iterate_get_user(ctx, &user) < 0) + e_error(authdb_event(_ctx->auth_request), + "sql: Iterate query didn't return 'user' field"); + else if (user == NULL) + e_error(authdb_event(_ctx->auth_request), + "sql: Iterate query returned NULL user"); + else { + _ctx->callback(user, _ctx->context); + return; + } + _ctx->failed = TRUE; + } else if (ret < 0) { + if (!module->conn->default_iterate_query) { + e_error(authdb_event(_ctx->auth_request), + "sql: Iterate query failed: %s", + sql_result_get_error(ctx->result)); + } else { + e_error(authdb_event(_ctx->auth_request), + "sql: Iterate query failed: %s " + "(using built-in default iterate_query: %s)", + sql_result_get_error(ctx->result), + module->conn->set.iterate_query); + } + _ctx->failed = TRUE; + } + _ctx->callback(NULL, _ctx->context); +} + +static int userdb_sql_iterate_deinit(struct userdb_iterate_context *_ctx) +{ + struct sql_userdb_iterate_context *ctx = + (struct sql_userdb_iterate_context *)_ctx; + int ret = _ctx->failed ? -1 : 0; + + auth_request_unref(&_ctx->auth_request); + if (ctx->result == NULL) { + /* sql query hasn't finished yet */ + ctx->freed = TRUE; + } else { + if (ctx->result != NULL) + sql_result_unref(ctx->result); + i_free(ctx); + } + return ret; +} + +static struct userdb_module * +userdb_sql_preinit(pool_t pool, const char *args) +{ + struct sql_userdb_module *module; + + module = p_new(pool, struct sql_userdb_module, 1); + module->conn = db_sql_init(args, TRUE); + + module->module.default_cache_key = + auth_cache_parse_key(pool, module->conn->set.user_query); + return &module->module; +} + +static void userdb_sql_init(struct userdb_module *_module) +{ + struct sql_userdb_module *module = + (struct sql_userdb_module *)_module; + enum sql_db_flags flags; + + flags = sql_get_flags(module->conn->db); + _module->blocking = (flags & SQL_DB_FLAG_BLOCKING) != 0; + + if (!_module->blocking || worker) + db_sql_connect(module->conn); +} + +static void userdb_sql_deinit(struct userdb_module *_module) +{ + struct sql_userdb_module *module = + (struct sql_userdb_module *)_module; + + db_sql_unref(&module->conn); +} + +struct userdb_module_interface userdb_sql = { + "sql", + + userdb_sql_preinit, + userdb_sql_init, + userdb_sql_deinit, + + userdb_sql_lookup, + + userdb_sql_iterate_init, + userdb_sql_iterate_next, + userdb_sql_iterate_deinit +}; +#else +struct userdb_module_interface userdb_sql = { + .name = "sql" +}; +#endif diff --git a/src/auth/userdb-static.c b/src/auth/userdb-static.c new file mode 100644 index 0000000..bed0f58 --- /dev/null +++ b/src/auth/userdb-static.c @@ -0,0 +1,144 @@ +/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */ + +#include "auth-common.h" + +#include "array.h" +#include "str.h" +#include "var-expand.h" +#include "userdb.h" +#include "userdb-template.h" + + +struct static_context { + userdb_callback_t *callback, *old_callback; + void *old_context; +}; + +struct static_userdb_module { + struct userdb_module module; + struct userdb_template *tmpl; + + bool allow_all_users:1; +}; + +static void static_lookup_real(struct auth_request *auth_request, + userdb_callback_t *callback) +{ + struct userdb_module *_module = auth_request->userdb->userdb; + struct static_userdb_module *module = + (struct static_userdb_module *)_module; + const char *error; + + if (userdb_template_export(module->tmpl, auth_request, &error) < 0) { + e_error(authdb_event(auth_request), + "Failed to expand template: %s", error); + callback(USERDB_RESULT_INTERNAL_FAILURE, auth_request); + } + callback(USERDB_RESULT_OK, auth_request); +} + +static void +static_credentials_callback(enum passdb_result result, + const unsigned char *credentials ATTR_UNUSED, + size_t size ATTR_UNUSED, + struct auth_request *auth_request) +{ + struct static_context *ctx = auth_request->context; + + auth_request->userdb_lookup = TRUE; + + auth_request->private_callback.userdb = ctx->old_callback; + auth_request->context = ctx->old_context; + auth_request_set_state(auth_request, AUTH_REQUEST_STATE_USERDB); + + switch (result) { + case PASSDB_RESULT_OK: + static_lookup_real(auth_request, ctx->callback); + break; + case PASSDB_RESULT_USER_UNKNOWN: + case PASSDB_RESULT_USER_DISABLED: + case PASSDB_RESULT_PASS_EXPIRED: + ctx->callback(USERDB_RESULT_USER_UNKNOWN, auth_request); + break; + case PASSDB_RESULT_SCHEME_NOT_AVAILABLE: + e_error(authdb_event(auth_request), + "passdb doesn't support lookups, " + "can't verify user's existence"); + /* fall through */ + default: + ctx->callback(USERDB_RESULT_INTERNAL_FAILURE, auth_request); + break; + } + + i_free(ctx); +} + +static void static_lookup(struct auth_request *auth_request, + userdb_callback_t *callback) +{ + struct userdb_module *_module = auth_request->userdb->userdb; + struct static_userdb_module *module = + (struct static_userdb_module *)_module; + struct static_context *ctx; + + if (!auth_request->fields.successful && !module->allow_all_users) { + /* this is a userdb-only lookup. we need to know if this + users exists or not. use a passdb lookup to do that. + if the passdb doesn't support returning credentials, this + will of course fail.. */ + ctx = i_new(struct static_context, 1); + ctx->old_callback = auth_request->private_callback.userdb; + ctx->old_context = auth_request->context; + ctx->callback = callback; + + i_assert(auth_request->state == AUTH_REQUEST_STATE_USERDB); + auth_request_set_state(auth_request, + AUTH_REQUEST_STATE_MECH_CONTINUE); + + auth_request->context = ctx; + if (auth_request->passdb != NULL) { + /* kludge: temporarily work as if we weren't doing + a userdb lookup. this is to get auth cache to use + passdb caching instead of userdb caching. */ + auth_request->userdb_lookup = FALSE; + auth_request_lookup_credentials(auth_request, "", + static_credentials_callback); + } else { + static_credentials_callback( + PASSDB_RESULT_SCHEME_NOT_AVAILABLE, + uchar_empty_ptr, 0, auth_request); + } + } else { + static_lookup_real(auth_request, callback); + } +} + +static struct userdb_module * +static_preinit(pool_t pool, const char *args) +{ + struct static_userdb_module *module; + const char *value; + + module = p_new(pool, struct static_userdb_module, 1); + module->tmpl = userdb_template_build(pool, "static", args); + + if (userdb_template_remove(module->tmpl, "allow_all_users", &value)) { + module->allow_all_users = value == NULL || + strcasecmp(value, "yes") == 0; + } + return &module->module; +} + +struct userdb_module_interface userdb_static = { + "static", + + static_preinit, + NULL, + NULL, + + static_lookup, + + NULL, + NULL, + NULL +}; diff --git a/src/auth/userdb-template.c b/src/auth/userdb-template.c new file mode 100644 index 0000000..a1e3011 --- /dev/null +++ b/src/auth/userdb-template.c @@ -0,0 +1,124 @@ +/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */ + +#include "auth-common.h" +#include "array.h" +#include "str.h" +#include "userdb.h" +#include "userdb-template.h" + +struct userdb_template { + ARRAY(const char *) args; +}; + +struct userdb_template * +userdb_template_build(pool_t pool, const char *userdb_name, const char *args) +{ + struct userdb_template *tmpl; + const char *const *tmp, *key, *value, *nonull_value; + uid_t uid; + gid_t gid; + + tmpl = p_new(pool, struct userdb_template, 1); + + tmp = t_strsplit_spaces(args, " "); + p_array_init(&tmpl->args, pool, str_array_length(tmp)); + + for (; *tmp != NULL; tmp++) { + value = strchr(*tmp, '='); + if (value == NULL) + key = *tmp; + else + key = t_strdup_until(*tmp, value++); + + + if (*key == '\0') + i_fatal("Invalid userdb template %s - key must not be empty", + args); + + nonull_value = value == NULL ? "" : value; + if (strcasecmp(key, "uid") == 0) { + uid = userdb_parse_uid(NULL, nonull_value); + if (uid == (uid_t)-1) { + i_fatal("%s userdb: Invalid uid: %s", + userdb_name, nonull_value); + } + value = dec2str(uid); + } else if (strcasecmp(key, "gid") == 0) { + gid = userdb_parse_gid(NULL, nonull_value); + if (gid == (gid_t)-1) { + i_fatal("%s userdb: Invalid gid: %s", + userdb_name, nonull_value); + } + value = dec2str(gid); + } else if (*key == '\0') { + i_fatal("%s userdb: Empty key (=%s)", + userdb_name, nonull_value); + } + key = p_strdup(pool, key); + value = p_strdup(pool, value); + + array_push_back(&tmpl->args, &key); + array_push_back(&tmpl->args, &value); + } + return tmpl; +} + +int userdb_template_export(struct userdb_template *tmpl, + struct auth_request *auth_request, + const char **error_r) +{ + const struct var_expand_table *table; + string_t *str; + const char *const *args, *value; + unsigned int i, count; + + if (userdb_template_is_empty(tmpl)) + return 0; + + str = t_str_new(256); + table = auth_request_get_var_expand_table(auth_request, NULL); + + args = array_get(&tmpl->args, &count); + i_assert((count % 2) == 0); + for (i = 0; i < count; i += 2) { + if (args[i+1] == NULL) + value = ""; + else { + str_truncate(str, 0); + if (auth_request_var_expand_with_table(str, args[i+1], + auth_request, table, NULL, error_r) <= 0) + return -1; + value = str_c(str); + } + auth_request_set_userdb_field(auth_request, args[i], value); + } + return 0; +} + +bool userdb_template_remove(struct userdb_template *tmpl, + const char *key, const char **value_r) +{ + const char *const *args; + unsigned int i, count; + + args = array_get(&tmpl->args, &count); + i_assert((count % 2) == 0); + for (i = 0; i < count; i += 2) { + if (strcmp(args[i], key) == 0) { + *value_r = args[i+1]; + array_delete(&tmpl->args, i, 2); + return TRUE; + } + } + return FALSE; +} + +bool userdb_template_is_empty(struct userdb_template *tmpl) +{ + return array_count(&tmpl->args) == 0; +} + +const char *const *userdb_template_get_args(struct userdb_template *tmpl, unsigned int *count_r) +{ + return array_get(&tmpl->args, count_r); +} diff --git a/src/auth/userdb-template.h b/src/auth/userdb-template.h new file mode 100644 index 0000000..a146a87 --- /dev/null +++ b/src/auth/userdb-template.h @@ -0,0 +1,15 @@ +#ifndef USERDB_TEMPLATE_H +#define USERDB_TEMPLATE_H + +struct userdb_template * +userdb_template_build(pool_t pool, const char *userdb_name, const char *args); +int userdb_template_export(struct userdb_template *tmpl, + struct auth_request *auth_request, + const char **error_r); +bool userdb_template_remove(struct userdb_template *tmpl, + const char *key, const char **value_r); +bool userdb_template_is_empty(struct userdb_template *tmpl); +const char *const *userdb_template_get_args(struct userdb_template *tmpl, + unsigned int *count_r); + +#endif diff --git a/src/auth/userdb.c b/src/auth/userdb.c new file mode 100644 index 0000000..21751f9 --- /dev/null +++ b/src/auth/userdb.c @@ -0,0 +1,256 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +#include "auth-common.h" +#include "array.h" +#include "ipwd.h" +#include "auth-worker-server.h" +#include "userdb.h" + +static ARRAY(struct userdb_module_interface *) userdb_interfaces; +static ARRAY(struct userdb_module *) userdb_modules; + +static const struct userdb_module_interface userdb_iface_deinit = { + .name = "deinit" +}; + +static struct userdb_module_interface *userdb_interface_find(const char *name) +{ + struct userdb_module_interface *iface; + + array_foreach_elem(&userdb_interfaces, iface) { + if (strcmp(iface->name, name) == 0) + return iface; + } + return NULL; +} + +void userdb_register_module(struct userdb_module_interface *iface) +{ + struct userdb_module_interface *old_iface; + + old_iface = userdb_interface_find(iface->name); + if (old_iface != NULL && old_iface->lookup == NULL) { + /* replacing a "support not compiled in" userdb */ + userdb_unregister_module(old_iface); + } else if (old_iface != NULL) { + i_panic("userdb_register_module(%s): Already registered", + iface->name); + } + array_push_back(&userdb_interfaces, &iface); +} + +void userdb_unregister_module(struct userdb_module_interface *iface) +{ + struct userdb_module_interface *const *ifaces; + unsigned int idx; + + array_foreach(&userdb_interfaces, ifaces) { + if (*ifaces == iface) { + idx = array_foreach_idx(&userdb_interfaces, ifaces); + array_delete(&userdb_interfaces, idx, 1); + return; + } + } + i_panic("userdb_unregister_module(%s): Not registered", iface->name); +} + +uid_t userdb_parse_uid(struct auth_request *request, const char *str) +{ + struct passwd pw; + uid_t uid; + + if (str == NULL) + return (uid_t)-1; + + if (str_to_uid(str, &uid) == 0) + return uid; + + switch (i_getpwnam(str, &pw)) { + case -1: + e_error(request == NULL ? auth_event : authdb_event(request), + "getpwnam() failed: %m"); + return (uid_t)-1; + case 0: + e_error(request == NULL ? auth_event : authdb_event(request), + "Invalid UID value '%s'", str); + return (uid_t)-1; + default: + return pw.pw_uid; + } +} + +gid_t userdb_parse_gid(struct auth_request *request, const char *str) +{ + struct group gr; + gid_t gid; + + if (str == NULL) + return (gid_t)-1; + + if (str_to_gid(str, &gid) == 0) + return gid; + + switch (i_getgrnam(str, &gr)) { + case -1: + e_error(request == NULL ? auth_event : authdb_event(request), + "getgrnam() failed: %m"); + return (gid_t)-1; + case 0: + e_error(request == NULL ? auth_event : authdb_event(request), + "Invalid GID value '%s'", str); + return (gid_t)-1; + default: + return gr.gr_gid; + } +} + +static struct userdb_module * +userdb_find(const char *driver, const char *args, unsigned int *idx_r) +{ + struct userdb_module *const *userdbs; + unsigned int i, count; + + userdbs = array_get(&userdb_modules, &count); + for (i = 0; i < count; i++) { + if (strcmp(userdbs[i]->iface->name, driver) == 0 && + strcmp(userdbs[i]->args, args) == 0) { + *idx_r = i; + return userdbs[i]; + } + } + return NULL; +} + +struct userdb_module * +userdb_preinit(pool_t pool, const struct auth_userdb_settings *set) +{ + static unsigned int auth_userdb_id = 0; + struct userdb_module_interface *iface; + struct userdb_module *userdb; + unsigned int idx; + + iface = userdb_interface_find(set->driver); + if (iface == NULL || iface->lookup == NULL) { + /* maybe it's a plugin. try to load it. */ + auth_module_load(t_strconcat("authdb_", set->driver, NULL)); + iface = userdb_interface_find(set->driver); + } + if (iface == NULL) + i_fatal("Unknown userdb driver '%s'", set->driver); + if (iface->lookup == NULL) { + i_fatal("Support not compiled in for userdb driver '%s'", + set->driver); + } + if (iface->preinit == NULL && iface->init == NULL && + *set->args != '\0') { + i_fatal("userdb %s: No args are supported: %s", + set->driver, set->args); + } + + userdb = userdb_find(set->driver, set->args, &idx); + if (userdb != NULL) + return userdb; + + if (iface->preinit == NULL) + userdb = p_new(pool, struct userdb_module, 1); + else + userdb = iface->preinit(pool, set->args); + userdb->id = ++auth_userdb_id; + userdb->iface = iface; + userdb->args = p_strdup(pool, set->args); + + array_push_back(&userdb_modules, &userdb); + return userdb; +} + +void userdb_init(struct userdb_module *userdb) +{ + if (userdb->iface->init != NULL && userdb->init_refcount == 0) + userdb->iface->init(userdb); + userdb->init_refcount++; +} + +void userdb_deinit(struct userdb_module *userdb) +{ + unsigned int idx; + + i_assert(userdb->init_refcount > 0); + + if (--userdb->init_refcount > 0) + return; + + if (userdb_find(userdb->iface->name, userdb->args, &idx) == NULL) + i_unreached(); + array_delete(&userdb_modules, idx, 1); + + if (userdb->iface->deinit != NULL) + userdb->iface->deinit(userdb); + + /* make sure userdb isn't accessed again */ + userdb->iface = &userdb_iface_deinit; +} + +void userdbs_generate_md5(unsigned char md5[STATIC_ARRAY MD5_RESULTLEN]) +{ + struct md5_context ctx; + struct userdb_module *const *userdbs; + unsigned int i, count; + + md5_init(&ctx); + userdbs = array_get(&userdb_modules, &count); + for (i = 0; i < count; i++) { + md5_update(&ctx, &userdbs[i]->id, sizeof(userdbs[i]->id)); + md5_update(&ctx, userdbs[i]->iface->name, + strlen(userdbs[i]->iface->name)); + md5_update(&ctx, userdbs[i]->args, strlen(userdbs[i]->args)); + } + md5_final(&ctx, md5); +} + +const char *userdb_result_to_string(enum userdb_result result) +{ + switch (result) { + case USERDB_RESULT_INTERNAL_FAILURE: + return "internal_failure"; + case USERDB_RESULT_USER_UNKNOWN: + return "user_unknown"; + case USERDB_RESULT_OK: + return "ok"; + } + i_unreached(); +} + +extern struct userdb_module_interface userdb_prefetch; +extern struct userdb_module_interface userdb_static; +extern struct userdb_module_interface userdb_passwd; +extern struct userdb_module_interface userdb_passwd_file; +extern struct userdb_module_interface userdb_ldap; +extern struct userdb_module_interface userdb_sql; +extern struct userdb_module_interface userdb_checkpassword; +extern struct userdb_module_interface userdb_dict; +#ifdef HAVE_LUA +extern struct userdb_module_interface userdb_lua; +#endif + +void userdbs_init(void) +{ + i_array_init(&userdb_interfaces, 16); + i_array_init(&userdb_modules, 16); + userdb_register_module(&userdb_passwd); + userdb_register_module(&userdb_passwd_file); + userdb_register_module(&userdb_prefetch); + userdb_register_module(&userdb_static); + userdb_register_module(&userdb_ldap); + userdb_register_module(&userdb_sql); + userdb_register_module(&userdb_checkpassword); + userdb_register_module(&userdb_dict); +#ifdef HAVE_LUA + userdb_register_module(&userdb_lua); +#endif +} + +void userdbs_deinit(void) +{ + array_free(&userdb_modules); + array_free(&userdb_interfaces); +} diff --git a/src/auth/userdb.h b/src/auth/userdb.h new file mode 100644 index 0000000..be64144 --- /dev/null +++ b/src/auth/userdb.h @@ -0,0 +1,92 @@ +#ifndef USERDB_H +#define USERDB_H + +#include "md5.h" +#include "auth-fields.h" + +struct auth; +struct auth_request; +struct auth_userdb_settings; + +enum userdb_result { + USERDB_RESULT_INTERNAL_FAILURE = -1, + USERDB_RESULT_USER_UNKNOWN = -2, + + USERDB_RESULT_OK = 1 +}; + +typedef void userdb_callback_t(enum userdb_result result, + struct auth_request *request); +/* user=NULL when there are no more users */ +typedef void userdb_iter_callback_t(const char *user, void *context); + +struct userdb_module { + const char *args; + /* The default caching key for this module, or NULL if caching isn't + wanted. This is updated by settings in auth_userdb. */ + const char *default_cache_key; + + /* If blocking is set to TRUE, use child processes to access + this userdb. */ + bool blocking; + /* id is used by blocking userdb to identify the userdb */ + unsigned int id; + + /* number of time init() has been called */ + int init_refcount; + + /* WARNING: avoid adding anything here that isn't based on args. + if you do, you need to change userdb.c:userdb_find() also to avoid + accidentally merging wrong userdbs. */ + + const struct userdb_module_interface *iface; +}; + +struct userdb_iterate_context { + struct auth_request *auth_request; + userdb_iter_callback_t *callback; + void *context; + bool failed; +}; + +struct userdb_module_interface { + const char *name; + + struct userdb_module *(*preinit)(pool_t pool, const char *args); + void (*init)(struct userdb_module *module); + void (*deinit)(struct userdb_module *module); + + void (*lookup)(struct auth_request *auth_request, + userdb_callback_t *callback); + + struct userdb_iterate_context * + (*iterate_init)(struct auth_request *auth_request, + userdb_iter_callback_t *callback, + void *context); + void (*iterate_next)(struct userdb_iterate_context *ctx); + int (*iterate_deinit)(struct userdb_iterate_context *ctx); +}; + +const char *userdb_result_to_string(enum userdb_result result); + +uid_t userdb_parse_uid(struct auth_request *request, const char *str) + ATTR_NULL(1); +gid_t userdb_parse_gid(struct auth_request *request, const char *str) + ATTR_NULL(1); + +struct userdb_module * +userdb_preinit(pool_t pool, const struct auth_userdb_settings *set); +void userdb_init(struct userdb_module *userdb); +void userdb_deinit(struct userdb_module *userdb); + +void userdb_register_module(struct userdb_module_interface *iface); +void userdb_unregister_module(struct userdb_module_interface *iface); + +void userdbs_generate_md5(unsigned char md5[STATIC_ARRAY MD5_RESULTLEN]); + +void userdbs_init(void); +void userdbs_deinit(void); + +#include "auth-request.h" + +#endif |