]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
home: add pam_systemd_home.so PAM hookup
authorLennart Poettering <lennart@poettering.net>
Thu, 4 Jul 2019 17:06:26 +0000 (19:06 +0200)
committerLennart Poettering <lennart@poettering.net>
Tue, 28 Jan 2020 21:36:41 +0000 (22:36 +0100)
In a way fixes: https://bugs.freedesktop.org/show_bug.cgi?id=67474

factory/etc/pam.d/system-auth
meson.build
src/home/meson.build
src/home/pam_systemd_home.c [new file with mode: 0644]
src/home/pam_systemd_home.sym [new file with mode: 0644]

index 747efc1fbdb69e32d50abc18bc138aab296ef570..522c51324af17a6bf3c13c5e7b9cb46671d17a28 100644 (file)
@@ -3,17 +3,21 @@
 # You really want to adjust this to your local distribution. If you use this
 # unmodified you are not building systems safely and securely.
 
-auth     sufficient pam_unix.so nullok try_first_pass
-auth     required   pam_deny.so
+auth      sufficient pam_unix.so
+-auth     sufficient pam_systemd_home.so
+auth      required   pam_deny.so
 
-account  required   pam_nologin.so
-account  sufficient pam_unix.so
-account  required   pam_permit.so
+account   required   pam_nologin.so
+-account  sufficient pam_systemd_home.so
+account   sufficient pam_unix.so
+account   required   pam_permit.so
 
-password sufficient pam_unix.so nullok sha512 shadow try_first_pass try_authtok
-password required   pam_deny.so
+-password sufficient pam_systemd_home.so
+password  sufficient pam_unix.so sha512 shadow try_first_pass try_authtok
+password  required   pam_deny.so
 
--session optional   pam_keyinit.so revoke
--session optional   pam_loginuid.so
--session optional   pam_systemd.so
-session  sufficient pam_unix.so
+-session  optional   pam_keyinit.so revoke
+-session  optional   pam_loginuid.so
+-session  optional   pam_systemd_home.so
+-session  optional   pam_systemd.so
+session   required   pam_unix.so
index 76f8153cd1a176eee3879844d082e069d64ae4c9..ea6927601962f9c9e552025b87570cc5ffe93816 100644 (file)
@@ -2102,6 +2102,26 @@ if conf.get('ENABLE_HOMED') == 1
                    install_rpath : rootlibexecdir,
                    install : true,
                    install_dir : rootbindir)
+
+        if conf.get('HAVE_PAM') == 1
+                version_script_arg = join_paths(project_source_root, pam_systemd_home_sym)
+                pam_systemd = shared_library(
+                        'pam_systemd_home',
+                        pam_systemd_home_c,
+                        name_prefix : '',
+                        include_directories : includes,
+                        link_args : ['-shared',
+                                     '-Wl,--version-script=' + version_script_arg],
+                        link_with : [libsystemd_static,
+                                     libshared_static],
+                        dependencies : [threads,
+                                        libpam,
+                                        libpam_misc,
+                                        libcrypt],
+                        link_depends : pam_systemd_home_sym,
+                        install : true,
+                        install_dir : pamlibdir)
+        endif
 endif
 
 foreach alias : ['halt', 'poweroff', 'reboot', 'runlevel', 'shutdown', 'telinit']
index fd32d3d847611094917f761dc460eb0ee05bbd1a..eb6da0b6969b5865bc9a30e42abf684900b40a68 100644 (file)
@@ -62,6 +62,15 @@ homectl_sources = files('''
         user-record-util.h
 '''.split())
 
+pam_systemd_home_sym = 'src/home/pam_systemd_home.sym'
+pam_systemd_home_c = files('''
+        home-util.c
+        home-util.h
+        pam_systemd_home.c
+        user-record-util.c
+        user-record-util.h
+'''.split())
+
 if conf.get('ENABLE_HOMED') == 1
         install_data('org.freedesktop.home1.conf',
                      install_dir : dbuspolicydir)
diff --git a/src/home/pam_systemd_home.c b/src/home/pam_systemd_home.c
new file mode 100644 (file)
index 0000000..a2235bf
--- /dev/null
@@ -0,0 +1,951 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include <security/pam_ext.h>
+#include <security/pam_modules.h>
+
+#include "sd-bus.h"
+
+#include "bus-common-errors.h"
+#include "errno-util.h"
+#include "fd-util.h"
+#include "home-util.h"
+#include "memory-util.h"
+#include "pam-util.h"
+#include "parse-util.h"
+#include "strv.h"
+#include "user-record-util.h"
+#include "user-record.h"
+#include "user-util.h"
+
+/* Used for the "systemd-user-record-is-homed" PAM data field, to indicate whether we know whether this user
+ * record is managed by homed or by something else. */
+#define USER_RECORD_IS_HOMED INT_TO_PTR(1)
+#define USER_RECORD_IS_OTHER INT_TO_PTR(2)
+
+static int parse_argv(
+                pam_handle_t *handle,
+                int argc, const char **argv,
+                bool *please_suspend,
+                bool *debug) {
+
+        int i;
+
+        assert(argc >= 0);
+        assert(argc == 0 || argv);
+
+        for (i = 0; i < argc; i++) {
+                const char *v;
+
+                if ((v = startswith(argv[1], "suspend="))) {
+                        int k;
+
+                        k = parse_boolean(v);
+                        if (k < 0)
+                                pam_syslog(handle, LOG_WARNING, "Failed to parse suspend-please= argument, ignoring: %s", v);
+                        else if (please_suspend)
+                                *please_suspend = k;
+
+                } else if ((v = startswith(argv[i], "debug="))) {
+                        int k;
+
+                        k = parse_boolean(v);
+                        if (k < 0)
+                                pam_syslog(handle, LOG_WARNING, "Failed to parse debug= argument, ignoring: %s", v);
+                        else if (debug)
+                                *debug = k;
+
+                } else
+                        pam_syslog(handle, LOG_WARNING, "Unknown parameter '%s', ignoring", argv[i]);
+        }
+
+        return 0;
+}
+
+static int acquire_user_record(
+                pam_handle_t *handle,
+                UserRecord **ret_record) {
+
+        _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+        _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+        _cleanup_(user_record_unrefp) UserRecord *ur = NULL;
+        _cleanup_(sd_bus_unrefp) sd_bus *bus = NULL;
+        const char *username = NULL, *json = NULL;
+        const void *b = NULL;
+        int r;
+
+        assert(handle);
+
+        r = pam_get_user(handle, &username, NULL);
+        if (r != PAM_SUCCESS) {
+                pam_syslog(handle, LOG_ERR, "Failed to get user name: %s", pam_strerror(handle, r));
+                return r;
+        }
+
+        if (isempty(username)) {
+                pam_syslog(handle, LOG_ERR, "User name not set.");
+                return PAM_SERVICE_ERR;
+        }
+
+        /* Let's bypass all IPC complexity for the two user names we know for sure we don't manage, and for
+         * user names we don't consider valid. */
+        if (STR_IN_SET(username, "root", NOBODY_USER_NAME) || !valid_user_group_name(username))
+                return PAM_USER_UNKNOWN;
+
+        /* Let's check if a previous run determined that this user is not managed by homed. If so, let's exit early */
+        r = pam_get_data(handle, "systemd-user-record-is-homed", &b);
+        if (!IN_SET(r, PAM_SUCCESS, PAM_NO_MODULE_DATA)) {
+                /* Failure */
+                pam_syslog(handle, LOG_ERR, "Failed to get PAM user record is homed flag: %s", pam_strerror(handle, r));
+                return r;
+        } else if (b == NULL)
+                /* Nothing cached yet, need to acquire fresh */
+                json = NULL;
+        else if (b != USER_RECORD_IS_HOMED)
+                /* Definitely not a homed record */
+                return PAM_USER_UNKNOWN;
+        else {
+                /* It's a homed record, let's use the cache, so that we can share it between the session and
+                 * the authentication hooks */
+                r = pam_get_data(handle, "systemd-user-record", (const void**) &json);
+                if (!IN_SET(r, PAM_SUCCESS, PAM_NO_MODULE_DATA)) {
+                        pam_syslog(handle, LOG_ERR, "Failed to get PAM user record data: %s", pam_strerror(handle, r));
+                        return r;
+                }
+        }
+
+        if (!json) {
+                _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+                _cleanup_free_ char *json_copy = NULL;
+
+                r = pam_acquire_bus_connection(handle, &bus);
+                if (r != PAM_SUCCESS)
+                        return r;
+
+                r = sd_bus_call_method(
+                                bus,
+                                "org.freedesktop.home1",
+                                "/org/freedesktop/home1",
+                                "org.freedesktop.home1.Manager",
+                                "GetUserRecordByName",
+                                &error,
+                                &reply,
+                                "s",
+                                username);
+                if (r < 0) {
+                        if (sd_bus_error_has_name(&error, SD_BUS_ERROR_SERVICE_UNKNOWN) ||
+                            sd_bus_error_has_name(&error, SD_BUS_ERROR_NAME_HAS_NO_OWNER)) {
+                                pam_syslog(handle, LOG_DEBUG, "systemd-homed is not available: %s", bus_error_message(&error, r));
+                                goto user_unknown;
+                        }
+
+                        if (sd_bus_error_has_name(&error, BUS_ERROR_NO_SUCH_HOME)) {
+                                pam_syslog(handle, LOG_DEBUG, "Not a user managed by systemd-homed: %s", bus_error_message(&error, r));
+                                goto user_unknown;
+                        }
+
+                        pam_syslog(handle, LOG_ERR, "Failed to query user record: %s", bus_error_message(&error, r));
+                        return PAM_SERVICE_ERR;
+                }
+
+                r = sd_bus_message_read(reply, "sbo", &json, NULL, NULL);
+                if (r < 0)
+                        return pam_bus_log_parse_error(handle, r);
+
+                json_copy = strdup(json);
+                if (!json_copy)
+                        return pam_log_oom(handle);
+
+                r = pam_set_data(handle, "systemd-user-record", json_copy, pam_cleanup_free);
+                if (r != PAM_SUCCESS) {
+                        pam_syslog(handle, LOG_ERR, "Failed to set PAM user record data: %s", pam_strerror(handle, r));
+                        return r;
+                }
+
+                TAKE_PTR(json_copy);
+
+                r = pam_set_data(handle, "systemd-user-record-is-homed", USER_RECORD_IS_HOMED, NULL);
+                if (r != PAM_SUCCESS) {
+                        pam_syslog(handle, LOG_ERR, "Failed to set PAM user record is homed flag: %s", pam_strerror(handle, r));
+                        return r;
+                }
+        }
+
+        r = json_parse(json, JSON_PARSE_SENSITIVE, &v, NULL, NULL);
+        if (r < 0) {
+                pam_syslog(handle, LOG_ERR, "Failed to parse JSON user record: %s", strerror_safe(r));
+                return PAM_SERVICE_ERR;
+        }
+
+        ur = user_record_new();
+        if (!ur)
+                return pam_log_oom(handle);
+
+        r = user_record_load(ur, v, USER_RECORD_LOAD_REFUSE_SECRET);
+        if (r < 0) {
+                pam_syslog(handle, LOG_ERR, "Failed to load user record: %s", strerror_safe(r));
+                return PAM_SERVICE_ERR;
+        }
+
+        if (!streq_ptr(username, ur->user_name)) {
+                pam_syslog(handle, LOG_ERR, "Acquired user record does not match user name.");
+                return PAM_SERVICE_ERR;
+        }
+
+        if (ret_record)
+                *ret_record = TAKE_PTR(ur);
+
+        return PAM_SUCCESS;
+
+user_unknown:
+        /* Cache this, so that we don't check again */
+        r = pam_set_data(handle, "systemd-user-record-is-homed", USER_RECORD_IS_OTHER, NULL);
+        if (r != PAM_SUCCESS)
+                pam_syslog(handle, LOG_ERR, "Failed to set PAM user record is homed flag, ignoring: %s", pam_strerror(handle, r));
+
+        return PAM_USER_UNKNOWN;
+}
+
+static int release_user_record(pam_handle_t *handle) {
+        int r, k;
+
+        r = pam_set_data(handle, "systemd-user-record", NULL, NULL);
+        if (r != PAM_SUCCESS)
+                pam_syslog(handle, LOG_ERR, "Failed to release PAM user record data: %s", pam_strerror(handle, r));
+
+        k = pam_set_data(handle, "systemd-user-record-is-homed", NULL, NULL);
+        if (k != PAM_SUCCESS)
+                pam_syslog(handle, LOG_ERR, "Failed to release PAM user record is homed flag: %s", pam_strerror(handle, k));
+
+        return IN_SET(r, PAM_SUCCESS, PAM_NO_MODULE_DATA) ? k : r;
+}
+
+static void cleanup_home_fd(pam_handle_t *handle, void *data, int error_status) {
+        safe_close(PTR_TO_FD(data));
+}
+
+static int handle_generic_user_record_error(
+                pam_handle_t *handle,
+                const char *user_name,
+                UserRecord *secret,
+                int ret,
+                const sd_bus_error *error) {
+
+        assert(user_name);
+        assert(secret);
+        assert(error);
+
+        int r;
+
+        /* Logs about all errors, except for PAM_CONV_ERR, i.e. when requesting more info failed. */
+
+        if (sd_bus_error_has_name(error, BUS_ERROR_HOME_ABSENT)) {
+                (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "Home of user %s is currently absent, please plug in the necessary storage device or backing file system.", user_name);
+                pam_syslog(handle, LOG_ERR, "Failed to acquire home for user %s: %s", user_name, bus_error_message(error, ret));
+                return PAM_PERM_DENIED;
+
+        } else if (sd_bus_error_has_name(error, BUS_ERROR_AUTHENTICATION_LIMIT_HIT)) {
+                (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "Too frequent unsuccessful login attempts for user %s, try again later.", user_name);
+                pam_syslog(handle, LOG_ERR, "Failed to acquire home for user %s: %s", user_name, bus_error_message(error, ret));
+                return PAM_MAXTRIES;
+
+        } else if (sd_bus_error_has_name(error, BUS_ERROR_BAD_PASSWORD)) {
+                _cleanup_(erase_and_freep) char *newp = NULL;
+
+                /* This didn't work? Ask for an (additional?) password */
+
+                if (strv_isempty(secret->password))
+                        r = pam_prompt(handle, PAM_PROMPT_ECHO_OFF, &newp, "Password: ");
+                else
+                        r = pam_prompt(handle, PAM_PROMPT_ECHO_OFF, &newp, "Password incorrect or not sufficient for authentication of user %s, please try again: ", user_name);
+                if (r != PAM_SUCCESS)
+                        return PAM_CONV_ERR; /* no logging here */
+
+                if (isempty(newp)) {
+                        pam_syslog(handle, LOG_DEBUG, "Password request aborted.");
+                        return PAM_AUTHTOK_ERR;
+                }
+
+                r = user_record_set_password(secret, STRV_MAKE(newp), true);
+                if (r < 0) {
+                        pam_syslog(handle, LOG_ERR, "Failed to store password: %s", strerror_safe(r));
+                        return PAM_SERVICE_ERR;
+                }
+
+        } else if (sd_bus_error_has_name(error, BUS_ERROR_BAD_PASSWORD_AND_NO_TOKEN)) {
+                _cleanup_(erase_and_freep) char *newp = NULL;
+
+                if (strv_isempty(secret->password))
+                        r = pam_prompt(handle, PAM_PROMPT_ECHO_OFF, &newp, "Security token of user %s not inserted, please enter password: ", user_name);
+                else
+                        r = pam_prompt(handle, PAM_PROMPT_ECHO_OFF, &newp, "Password incorrect or not sufficient, and configured security token of user %s not inserted, please enter password: ", user_name);
+                if (r != PAM_SUCCESS)
+                        return PAM_CONV_ERR; /* no logging here */
+
+                if (isempty(newp)) {
+                        pam_syslog(handle, LOG_DEBUG, "Password request aborted.");
+                        return PAM_AUTHTOK_ERR;
+                }
+
+                r = user_record_set_password(secret, STRV_MAKE(newp), true);
+                if (r < 0) {
+                        pam_syslog(handle, LOG_ERR, "Failed to store password: %s", strerror_safe(r));
+                        return PAM_SERVICE_ERR;
+                }
+
+        } else if (sd_bus_error_has_name(error, BUS_ERROR_TOKEN_PIN_NEEDED)) {
+                _cleanup_(erase_and_freep) char *newp = NULL;
+
+                r = pam_prompt(handle, PAM_PROMPT_ECHO_OFF, &newp, "Please enter security token PIN: ");
+                if (r != PAM_SUCCESS)
+                        return PAM_CONV_ERR; /* no logging here */
+
+                if (isempty(newp)) {
+                        pam_syslog(handle, LOG_DEBUG, "PIN request aborted.");
+                        return PAM_AUTHTOK_ERR;
+                }
+
+                r = user_record_set_pkcs11_pin(secret, STRV_MAKE(newp), false);
+                if (r < 0) {
+                        pam_syslog(handle, LOG_ERR, "Failed to store PIN: %s", strerror_safe(r));
+                        return PAM_SERVICE_ERR;
+                }
+
+        } else if (sd_bus_error_has_name(error, BUS_ERROR_TOKEN_PROTECTED_AUTHENTICATION_PATH_NEEDED)) {
+
+                (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "Please authenticate physically on security token of user %s.", user_name);
+
+                r = user_record_set_pkcs11_protected_authentication_path_permitted(secret, true);
+                if (r < 0) {
+                        pam_syslog(handle, LOG_ERR, "Failed to set PKCS#11 protected authentication path permitted flag: %s", strerror_safe(r));
+                        return PAM_SERVICE_ERR;
+                }
+
+        } else if (sd_bus_error_has_name(error, BUS_ERROR_TOKEN_BAD_PIN)) {
+                _cleanup_(erase_and_freep) char *newp = NULL;
+
+                r = pam_prompt(handle, PAM_PROMPT_ECHO_OFF, &newp, "Security token PIN incorrect, please enter PIN for security token of user %s again: ", user_name);
+                if (r != PAM_SUCCESS)
+                        return PAM_CONV_ERR; /* no logging here */
+
+                if (isempty(newp)) {
+                        pam_syslog(handle, LOG_DEBUG, "PIN request aborted.");
+                        return PAM_AUTHTOK_ERR;
+                }
+
+                r = user_record_set_pkcs11_pin(secret, STRV_MAKE(newp), false);
+                if (r < 0) {
+                        pam_syslog(handle, LOG_ERR, "Failed to store PIN: %s", strerror_safe(r));
+                        return PAM_SERVICE_ERR;
+                }
+
+        } else if (sd_bus_error_has_name(error, BUS_ERROR_TOKEN_BAD_PIN_FEW_TRIES_LEFT)) {
+                _cleanup_(erase_and_freep) char *newp = NULL;
+
+                r = pam_prompt(handle, PAM_PROMPT_ECHO_OFF, &newp, "Security token PIN incorrect (only a few tries left!), please enter PIN for security token of user %s again: ", user_name);
+                if (r != PAM_SUCCESS)
+                        return PAM_CONV_ERR; /* no logging here */
+
+                if (isempty(newp)) {
+                        pam_syslog(handle, LOG_DEBUG, "PIN request aborted.");
+                        return PAM_AUTHTOK_ERR;
+                }
+
+                r = user_record_set_pkcs11_pin(secret, STRV_MAKE(newp), false);
+                if (r < 0) {
+                        pam_syslog(handle, LOG_ERR, "Failed to store PIN: %s", strerror_safe(r));
+                        return PAM_SERVICE_ERR;
+                }
+
+        } else if (sd_bus_error_has_name(error, BUS_ERROR_TOKEN_BAD_PIN_ONE_TRY_LEFT)) {
+                _cleanup_(erase_and_freep) char *newp = NULL;
+
+                r = pam_prompt(handle, PAM_PROMPT_ECHO_OFF, &newp, "Security token PIN incorrect (only one try left!), please enter PIN for security token of user %s again: ", user_name);
+                if (r != PAM_SUCCESS)
+                        return PAM_CONV_ERR; /* no logging here */
+
+                if (isempty(newp)) {
+                        pam_syslog(handle, LOG_DEBUG, "PIN request aborted.");
+                        return PAM_AUTHTOK_ERR;
+                }
+
+                r = user_record_set_pkcs11_pin(secret, STRV_MAKE(newp), false);
+                if (r < 0) {
+                        pam_syslog(handle, LOG_ERR, "Failed to store PIN: %s", strerror_safe(r));
+                        return PAM_SERVICE_ERR;
+                }
+
+        } else {
+                pam_syslog(handle, LOG_ERR, "Failed to acquire home for user %s: %s", user_name, bus_error_message(error, ret));
+                return PAM_SERVICE_ERR;
+        }
+
+        return PAM_SUCCESS;
+}
+
+static int acquire_home(
+                pam_handle_t *handle,
+                bool please_authenticate,
+                bool please_suspend,
+                bool debug) {
+
+        _cleanup_(user_record_unrefp) UserRecord *ur = NULL, *secret = NULL;
+        bool do_auth = please_authenticate, home_not_active = false, home_locked = false;
+        _cleanup_(sd_bus_unrefp) sd_bus *bus = NULL;
+        _cleanup_close_ int acquired_fd = -1;
+        const void *home_fd_ptr = NULL;
+        unsigned n_attempts = 0;
+        int r;
+
+        assert(handle);
+
+        /* This acquires a reference to a home directory in one of two ways: if please_authenticate is true,
+         * then we'll call AcquireHome() after asking the user for a password. Otherwise it tries to call
+         * RefHome() and if that fails queries the user for a password and uses AcquireHome().
+         *
+         * The idea is that the PAM authentication hook sets please_authenticate and thus always
+         * authenticates, while the other PAM hooks unset it so that they can a ref of their own without
+         * authentication if possible, but with authentication if necessary. */
+
+        /* If we already have acquired the fd, let's shortcut this */
+        r = pam_get_data(handle, "systemd-home-fd", &home_fd_ptr);
+        if (r == PAM_SUCCESS && PTR_TO_INT(home_fd_ptr) >= 0)
+                return PAM_SUCCESS;
+
+        r = pam_acquire_bus_connection(handle, &bus);
+        if (r != PAM_SUCCESS)
+                return r;
+
+        r = acquire_user_record(handle, &ur);
+        if (r != PAM_SUCCESS)
+                return r;
+
+        /* Implement our own retry loop here instead of relying on the PAM client's one. That's because it
+         * might happen that the the record we stored on the host does not match the encryption password of
+         * the LUKS image in case the image was used in a different system where the password was
+         * changed. In that case it will happen that the LUKS password and the host password are
+         * different, and we handle that by collecting and passing multiple passwords in that case. Hence we
+         * treat bad passwords as a request to collect one more password and pass the new all all previously
+         * used passwords again. */
+
+        for (;;) {
+                _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL, *reply = NULL;
+                _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+
+                if (do_auth && !secret) {
+                        const char *cached_password = NULL;
+
+                        secret = user_record_new();
+                        if (!secret)
+                                return pam_log_oom(handle);
+
+                        /* If there's already a cached password, use it. But if not let's authenticate
+                         * without anything, maybe some other authentication mechanism systemd-homed
+                         * implements (such as PKCS#11) allows us to authenticate without anything else. */
+                        r = pam_get_item(handle, PAM_AUTHTOK, (const void**) &cached_password);
+                        if (!IN_SET(r, PAM_BAD_ITEM, PAM_SUCCESS)) {
+                                pam_syslog(handle, LOG_ERR, "Failed to get cached password: %s", pam_strerror(handle, r));
+                                return r;
+                        }
+
+                        if (!isempty(cached_password)) {
+                                r = user_record_set_password(secret, STRV_MAKE(cached_password), true);
+                                if (r < 0) {
+                                        pam_syslog(handle, LOG_ERR, "Failed to store password: %s", strerror_safe(r));
+                                        return PAM_SERVICE_ERR;
+                                }
+                        }
+                }
+
+                r = sd_bus_message_new_method_call(
+                                bus,
+                                &m,
+                                "org.freedesktop.home1",
+                                "/org/freedesktop/home1",
+                                "org.freedesktop.home1.Manager",
+                                do_auth ? "AcquireHome" : "RefHome");
+                if (r < 0)
+                        return pam_bus_log_create_error(handle, r);
+
+                r = sd_bus_message_append(m, "s", ur->user_name);
+                if (r < 0)
+                        return pam_bus_log_create_error(handle, r);
+
+                if (do_auth) {
+                        r = bus_message_append_secret(m, secret);
+                        if (r < 0)
+                                return pam_bus_log_create_error(handle, r);
+                }
+
+                r = sd_bus_message_append(m, "b", please_suspend);
+                if (r < 0)
+                        return pam_bus_log_create_error(handle, r);
+
+                r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, &reply);
+                if (r < 0) {
+
+                        if (sd_bus_error_has_name(&error, BUS_ERROR_HOME_NOT_ACTIVE))
+                                /* Only on RefHome(): We can't access the home directory currently, unless
+                                 * it's unlocked with a password. Hence, let's try this again, this time with
+                                 * authentication. */
+                                home_not_active = true;
+                        else if (sd_bus_error_has_name(&error, BUS_ERROR_HOME_LOCKED))
+                                home_locked = true; /* Similar */
+                        else {
+                                r = handle_generic_user_record_error(handle, ur->user_name, secret, r, &error);
+                                if (r == PAM_CONV_ERR) {
+                                        /* Password/PIN prompts will fail in certain environments, for example when
+                                         * we are called from OpenSSH's account or session hooks, or in systemd's
+                                         * per-service PAM logic. In that case, print a friendly message and accept
+                                         * failure. */
+
+                                        if (home_not_active)
+                                                (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "Home of user %s is currently not active, please log in locally first.", ur->user_name);
+                                        if (home_locked)
+                                                (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "Home of user %s is currently locked, please unlock locally first.", ur->user_name);
+
+                                        pam_syslog(handle, please_authenticate ? LOG_ERR : LOG_DEBUG, "Failed to prompt for password/prompt.");
+
+                                        return home_not_active || home_locked ? PAM_PERM_DENIED : PAM_CONV_ERR;
+                                }
+                                if (r != PAM_SUCCESS)
+                                        return r;
+                        }
+
+                } else {
+                        int fd;
+
+                        r = sd_bus_message_read(reply, "h", &fd);
+                        if (r < 0)
+                                return pam_bus_log_parse_error(handle, r);
+
+                        acquired_fd = fcntl(fd, F_DUPFD_CLOEXEC, 3);
+                        if (acquired_fd < 0) {
+                                pam_syslog(handle, LOG_ERR, "Failed to duplicate acquired fd: %s", bus_error_message(&error, r));
+                                return PAM_SERVICE_ERR;
+                        }
+
+                        break;
+                }
+
+                if (++n_attempts >= 5) {
+                        (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "Too many unsuccessful login attempts for user %s, refusing.", ur->user_name);
+                        pam_syslog(handle, LOG_ERR, "Failed to acquire home for user %s: %s", ur->user_name, bus_error_message(&error, r));
+                        return PAM_MAXTRIES;
+                }
+
+                /* Try again, this time with authentication if we didn't do that before. */
+                do_auth = true;
+        }
+
+        r = pam_set_data(handle, "systemd-home-fd", FD_TO_PTR(acquired_fd), cleanup_home_fd);
+        if (r < 0) {
+                pam_syslog(handle, LOG_ERR, "Failed to set PAM bus data: %s", pam_strerror(handle, r));
+                return r;
+        }
+        TAKE_FD(acquired_fd);
+
+        if (do_auth) {
+                /* We likely just activated the home directory, let's flush out the user record, since a
+                 * newer embedded user record might have been acquired from the activation. */
+
+                r = release_user_record(handle);
+                if (!IN_SET(r, PAM_SUCCESS, PAM_NO_MODULE_DATA))
+                        return r;
+        }
+
+        pam_syslog(handle, LOG_NOTICE, "Home for user %s successfully acquired.", ur->user_name);
+
+        return PAM_SUCCESS;
+}
+
+static int release_home_fd(pam_handle_t *handle) {
+        const void *home_fd_ptr = NULL;
+        int r;
+
+        r = pam_get_data(handle, "systemd-home-fd", &home_fd_ptr);
+        if (r == PAM_NO_MODULE_DATA || PTR_TO_FD(home_fd_ptr) < 0)
+                return PAM_NO_MODULE_DATA;
+
+        r = pam_set_data(handle, "systemd-home-fd", NULL, NULL);
+        if (r != PAM_SUCCESS)
+                pam_syslog(handle, LOG_ERR, "Failed to release PAM home reference fd: %s", pam_strerror(handle, r));
+
+        return r;
+}
+
+_public_ PAM_EXTERN int pam_sm_authenticate(
+                pam_handle_t *handle,
+                int flags,
+                int argc, const char **argv) {
+
+        bool debug = false, suspend_please = false;
+
+        if (parse_argv(handle,
+                       argc, argv,
+                       &suspend_please,
+                       &debug) < 0)
+                return PAM_AUTH_ERR;
+
+        if (debug)
+                pam_syslog(handle, LOG_DEBUG, "pam-systemd-homed authenticating");
+
+        return acquire_home(handle, /* please_authenticate= */ true, suspend_please, debug);
+}
+
+_public_ PAM_EXTERN int pam_sm_setcred(pam_handle_t *pamh, int flags, int argc, const char **argv) {
+        return PAM_SUCCESS;
+}
+
+_public_ PAM_EXTERN int pam_sm_open_session(
+                pam_handle_t *handle,
+                int flags,
+                int argc, const char **argv) {
+
+        bool debug = false, suspend_please = false;
+        int r;
+
+        if (parse_argv(handle,
+                       argc, argv,
+                       &suspend_please,
+                       &debug) < 0)
+                return PAM_SESSION_ERR;
+
+        if (debug)
+                pam_syslog(handle, LOG_DEBUG, "pam-systemd-homed session start");
+
+        r = acquire_home(handle, /* please_authenticate = */ false, suspend_please, debug);
+        if (r == PAM_USER_UNKNOWN) /* Not managed by us? Don't complain. */
+                return PAM_SUCCESS;
+        if (r != PAM_SUCCESS)
+                return r;
+
+        r = pam_putenv(handle, "SYSTEMD_HOME=1");
+        if (r != PAM_SUCCESS) {
+                pam_syslog(handle, LOG_ERR, "Failed to set PAM environment variable $SYSTEMD_HOME: %s", pam_strerror(handle, r));
+                return r;
+        }
+
+        /* Let's release the D-Bus connection, after all the session might live quite a long time, and we are
+         * not going to process the bus connection in that time, so let's better close before the daemon
+         * kicks us off because we are not processing anything. */
+        (void) pam_release_bus_connection(handle);
+        return PAM_SUCCESS;
+}
+
+_public_ PAM_EXTERN int pam_sm_close_session(
+                pam_handle_t *handle,
+                int flags,
+                int argc, const char **argv) {
+
+        _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+        _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
+        _cleanup_(sd_bus_unrefp) sd_bus *bus = NULL;
+        const char *username = NULL;
+        bool debug = false;
+        int r;
+
+        if (parse_argv(handle,
+                       argc, argv,
+                       NULL,
+                       &debug) < 0)
+                return PAM_SESSION_ERR;
+
+        if (debug)
+                pam_syslog(handle, LOG_DEBUG, "pam-systemd-homed session end");
+
+        /* Let's explicitly drop the reference to the homed session, so that the subsequent ReleaseHome()
+         * call will be able to do its thing. */
+        r = release_home_fd(handle);
+        if (r == PAM_NO_MODULE_DATA) /* Nothing to do, we never acquired an fd */
+                return PAM_SUCCESS;
+        if (r != PAM_SUCCESS)
+                return r;
+
+        r = pam_get_user(handle, &username, NULL);
+        if (r != PAM_SUCCESS) {
+                pam_syslog(handle, LOG_ERR, "Failed to get user name: %s", pam_strerror(handle, r));
+                return r;
+        }
+
+        r = pam_acquire_bus_connection(handle, &bus);
+        if (r != PAM_SUCCESS)
+                return r;
+
+        r = sd_bus_message_new_method_call(
+                        bus,
+                        &m,
+                        "org.freedesktop.home1",
+                        "/org/freedesktop/home1",
+                        "org.freedesktop.home1.Manager",
+                        "ReleaseHome");
+        if (r < 0)
+                return pam_bus_log_create_error(handle, r);
+
+        r = sd_bus_message_append(m, "s", username);
+        if (r < 0)
+                return pam_bus_log_create_error(handle, r);
+
+        r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
+        if (r < 0) {
+                if (sd_bus_error_has_name(&error, BUS_ERROR_HOME_BUSY))
+                        pam_syslog(handle, LOG_NOTICE, "Not deactivating home directory of %s, as it is still used.", username);
+                else {
+                        pam_syslog(handle, LOG_ERR, "Failed to release user home: %s", bus_error_message(&error, r));
+                        return PAM_SESSION_ERR;
+                }
+        }
+
+        return PAM_SUCCESS;
+}
+
+_public_ PAM_EXTERN int pam_sm_acct_mgmt(
+                pam_handle_t *handle,
+                int flags,
+                int argc,
+                const char **argv) {
+
+        _cleanup_(user_record_unrefp) UserRecord *ur = NULL;
+        bool debug = false, please_suspend = false;
+        usec_t t;
+        int r;
+
+        if (parse_argv(handle,
+                       argc, argv,
+                       &please_suspend,
+                       &debug) < 0)
+                return PAM_AUTH_ERR;
+
+        if (debug)
+                pam_syslog(handle, LOG_DEBUG, "pam-systemd-homed account management");
+
+        r = acquire_home(handle, /* please_authenticate = */ false, please_suspend, debug);
+        if (r == PAM_USER_UNKNOWN)
+                return PAM_SUCCESS; /* we don't have anything to say about users we don't manage */
+        if (r != PAM_SUCCESS)
+                return r;
+
+        r = acquire_user_record(handle, &ur);
+        if (r != PAM_SUCCESS)
+                return r;
+
+        r = user_record_test_blocked(ur);
+        switch (r) {
+
+        case -ESTALE:
+                (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "User record is newer than current system time, prohibiting access.");
+                return PAM_ACCT_EXPIRED;
+
+        case -ENOLCK:
+                (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "User record is blocked, prohibiting access.");
+                return PAM_ACCT_EXPIRED;
+
+        case -EL2HLT:
+                (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "User record is not valid yet, prohibiting access.");
+                return PAM_ACCT_EXPIRED;
+
+        case -EL3HLT:
+                (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "User record is not valid anymore, prohibiting access.");
+                return PAM_ACCT_EXPIRED;
+
+        default:
+                if (r < 0) {
+                        (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "User record not valid, prohibiting access.");
+                        return PAM_ACCT_EXPIRED;
+                }
+
+                break;
+        }
+
+        t = user_record_ratelimit_next_try(ur);
+        if (t != USEC_INFINITY) {
+                usec_t n = now(CLOCK_REALTIME);
+
+                if (t > n) {
+                        char buf[FORMAT_TIMESPAN_MAX];
+                        (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "Too many logins, try again in %s.",
+                                          format_timespan(buf, sizeof(buf), t - n, USEC_PER_SEC));
+
+                        return PAM_MAXTRIES;
+                }
+        }
+
+        r = user_record_test_password_change_required(ur);
+        switch (r) {
+
+        case -EKEYREVOKED:
+                (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "Password change required.");
+                return PAM_NEW_AUTHTOK_REQD;
+
+        case -EOWNERDEAD:
+                (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "Password expired, change requird.");
+                return PAM_NEW_AUTHTOK_REQD;
+
+        case -EKEYREJECTED:
+                /* Strictly speaking this is only about password expiration, and we might want to allow
+                 * authentication via PKCS#11 or so, but let's ignore this fine distinction for now. */
+                (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "Password is expired, but can't change, refusing login.");
+                return PAM_AUTHTOK_EXPIRED;
+
+        case -EKEYEXPIRED:
+                (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "Password will expire soon, please change.");
+                break;
+
+        case -EROFS:
+                /* All good, just means the password if we wanted to change we couldn't, but we don't need to */
+                break;
+
+        default:
+                if (r < 0) {
+                        (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "User record not valid, prohibiting access.");
+                        return PAM_AUTHTOK_EXPIRED;
+                }
+
+                break;
+        }
+
+        return PAM_SUCCESS;
+}
+
+_public_ PAM_EXTERN int pam_sm_chauthtok(
+                pam_handle_t *handle,
+                int flags,
+                int argc,
+                const char **argv) {
+
+        _cleanup_(user_record_unrefp) UserRecord *ur = NULL, *old_secret = NULL, *new_secret = NULL;
+        const char *old_password = NULL, *new_password = NULL;
+        _cleanup_(sd_bus_unrefp) sd_bus *bus = NULL;
+        unsigned n_attempts = 0;
+        bool debug = false;
+        int r;
+
+        if (parse_argv(handle,
+                       argc, argv,
+                       NULL,
+                       &debug) < 0)
+                return PAM_AUTH_ERR;
+
+        if (debug)
+                pam_syslog(handle, LOG_DEBUG, "pam-systemd-homed account management");
+
+        r = pam_acquire_bus_connection(handle, &bus);
+        if (r != PAM_SUCCESS)
+                return r;
+
+        r = acquire_user_record(handle, &ur);
+        if (r != PAM_SUCCESS)
+                return r;
+
+        /* Start with cached credentials */
+        r = pam_get_item(handle, PAM_OLDAUTHTOK, (const void**) &old_password);
+        if (!IN_SET(r, PAM_BAD_ITEM, PAM_SUCCESS)) {
+                pam_syslog(handle, LOG_ERR, "Failed to get old password: %s", pam_strerror(handle, r));
+                return r;
+        }
+        r = pam_get_item(handle, PAM_AUTHTOK, (const void**) &new_password);
+        if (!IN_SET(r, PAM_BAD_ITEM, PAM_SUCCESS)) {
+                pam_syslog(handle, LOG_ERR, "Failed to get cached password: %s", pam_strerror(handle, r));
+                return r;
+        }
+
+        if (isempty(new_password)) {
+                /* No, it's not cached, then let's ask for the password and its verification, and cache
+                 * it. */
+
+                r = pam_get_authtok_noverify(handle, &new_password, "New password: ");
+                if (r != PAM_SUCCESS) {
+                        pam_syslog(handle, LOG_ERR, "Failed to get new password: %s", pam_strerror(handle, r));
+                        return r;
+                }
+                if (isempty(new_password)) {
+                        pam_syslog(handle, LOG_DEBUG, "Password request aborted.");
+                        return PAM_AUTHTOK_ERR;
+                }
+
+                r = pam_get_authtok_verify(handle, &new_password, "new password: "); /* Lower case, since PAM prefixes 'Repeat' */
+                if (r != PAM_SUCCESS) {
+                        pam_syslog(handle, LOG_ERR, "Failed to get password again: %s", pam_strerror(handle, r));
+                        return r;
+                }
+
+                // FIXME: pam_pwquality will ask for the password a third time. It really shouldn't do
+                // that, and instead assume the password was already verified once when it is found to be
+                // cached already. needs to be fixed in pam_pwquality
+        }
+
+        /* Now everything is cached and checked, let's exit from the preliminary check */
+        if (FLAGS_SET(flags, PAM_PRELIM_CHECK))
+                return PAM_SUCCESS;
+
+
+        old_secret = user_record_new();
+        if (!old_secret)
+                return pam_log_oom(handle);
+
+        if (!isempty(old_password)) {
+                r = user_record_set_password(old_secret, STRV_MAKE(old_password), true);
+                if (r < 0) {
+                        pam_syslog(handle, LOG_ERR, "Failed to store old password: %s", strerror_safe(r));
+                        return PAM_SERVICE_ERR;
+                }
+        }
+
+        new_secret = user_record_new();
+        if (!new_secret)
+                return pam_log_oom(handle);
+
+        r = user_record_set_password(new_secret, STRV_MAKE(new_password), true);
+        if (r < 0) {
+                pam_syslog(handle, LOG_ERR, "Failed to store new password: %s", strerror_safe(r));
+                return PAM_SERVICE_ERR;
+        }
+
+        for (;;) {
+                _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+                _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
+
+                r = sd_bus_message_new_method_call(
+                                bus,
+                                &m,
+                                "org.freedesktop.home1",
+                                "/org/freedesktop/home1",
+                                "org.freedesktop.home1.Manager",
+                                "ChangePasswordHome");
+                if (r < 0)
+                        return pam_bus_log_create_error(handle, r);
+
+                r = sd_bus_message_append(m, "s", ur->user_name);
+                if (r < 0)
+                        return pam_bus_log_create_error(handle, r);
+
+                r = bus_message_append_secret(m, new_secret);
+                if (r < 0)
+                        return pam_bus_log_create_error(handle, r);
+
+                r = bus_message_append_secret(m, old_secret);
+                if (r < 0)
+                        return pam_bus_log_create_error(handle, r);
+
+                r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
+                if (r < 0) {
+                        r = handle_generic_user_record_error(handle, ur->user_name, old_secret, r, &error);
+                        if (r == PAM_CONV_ERR) {
+                                pam_syslog(handle, LOG_ERR, "Failed to prompt for password/prompt.");
+                                return PAM_CONV_ERR;
+                        }
+                        if (r != PAM_SUCCESS)
+                                return r;
+                } else {
+                        pam_syslog(handle, LOG_NOTICE, "Successfully changed password for user %s.", ur->user_name);
+                        return PAM_SUCCESS;
+                }
+
+                if (++n_attempts >= 5)
+                        break;
+
+                /* Try again */
+        };
+
+        pam_syslog(handle, LOG_NOTICE, "Failed to change password for user %s: %m", ur->user_name);
+        return PAM_MAXTRIES;
+}
diff --git a/src/home/pam_systemd_home.sym b/src/home/pam_systemd_home.sym
new file mode 100644 (file)
index 0000000..daec049
--- /dev/null
@@ -0,0 +1,12 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+{
+global:
+        pam_sm_authenticate;
+        pam_sm_setcred;
+        pam_sm_open_session;
+        pam_sm_close_session;
+        pam_sm_acct_mgmt;
+        pam_sm_chauthtok;
+local: *;
+};