]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
home: add new systemd-homed service that can manage LUKS homes
authorLennart Poettering <lennart@poettering.net>
Thu, 4 Jul 2019 16:35:39 +0000 (18:35 +0200)
committerLennart Poettering <lennart@poettering.net>
Tue, 28 Jan 2020 21:36:07 +0000 (22:36 +0100)
Fixes more or less: https://bugs.freedesktop.org/show_bug.cgi?id=67474

50 files changed:
meson.build
meson_options.txt
src/home/home-util.c [new file with mode: 0644]
src/home/home-util.h [new file with mode: 0644]
src/home/homed-bus.c [new file with mode: 0644]
src/home/homed-bus.h [new file with mode: 0644]
src/home/homed-home-bus.c [new file with mode: 0644]
src/home/homed-home-bus.h [new file with mode: 0644]
src/home/homed-home.c [new file with mode: 0644]
src/home/homed-home.h [new file with mode: 0644]
src/home/homed-manager-bus.c [new file with mode: 0644]
src/home/homed-manager-bus.h [new file with mode: 0644]
src/home/homed-manager.c [new file with mode: 0644]
src/home/homed-manager.h [new file with mode: 0644]
src/home/homed-operation.c [new file with mode: 0644]
src/home/homed-operation.h [new file with mode: 0644]
src/home/homed-varlink.c [new file with mode: 0644]
src/home/homed-varlink.h [new file with mode: 0644]
src/home/homed.c [new file with mode: 0644]
src/home/homework-cifs.c [new file with mode: 0644]
src/home/homework-cifs.h [new file with mode: 0644]
src/home/homework-directory.c [new file with mode: 0644]
src/home/homework-directory.h [new file with mode: 0644]
src/home/homework-fscrypt.c [new file with mode: 0644]
src/home/homework-fscrypt.h [new file with mode: 0644]
src/home/homework-luks.c [new file with mode: 0644]
src/home/homework-luks.h [new file with mode: 0644]
src/home/homework-mount.c [new file with mode: 0644]
src/home/homework-mount.h [new file with mode: 0644]
src/home/homework-pkcs11.c [new file with mode: 0644]
src/home/homework-pkcs11.h [new file with mode: 0644]
src/home/homework-quota.c [new file with mode: 0644]
src/home/homework-quota.h [new file with mode: 0644]
src/home/homework.c [new file with mode: 0644]
src/home/homework.h [new file with mode: 0644]
src/home/meson.build [new file with mode: 0644]
src/home/org.freedesktop.home1.conf [new file with mode: 0644]
src/home/org.freedesktop.home1.policy [new file with mode: 0644]
src/home/org.freedesktop.home1.service [new file with mode: 0644]
src/home/pwquality-util.c [new file with mode: 0644]
src/home/pwquality-util.h [new file with mode: 0644]
src/home/user-record-sign.c [new file with mode: 0644]
src/home/user-record-sign.h [new file with mode: 0644]
src/home/user-record-util.c [new file with mode: 0644]
src/home/user-record-util.h [new file with mode: 0644]
src/libsystemd/sd-bus/bus-common-errors.c
src/libsystemd/sd-bus/bus-common-errors.h
src/shared/gpt.h
units/meson.build
units/systemd-homed.service.in [new file with mode: 0644]

index 54820d3f6a876ac510edca4e90400a70173c7933..f0f9bdb0ce43da2aac709af2dc3a9898482e7537 100644 (file)
@@ -243,6 +243,7 @@ conf.set_quoted('SYSTEMD_EXPORT_PATH',                        join_paths(rootlib
 conf.set_quoted('VENDOR_KEYRING_PATH',                        join_paths(rootlibexecdir, 'import-pubring.gpg'))
 conf.set_quoted('USER_KEYRING_PATH',                          join_paths(pkgsysconfdir, 'import-pubring.gpg'))
 conf.set_quoted('DOCUMENT_ROOT',                              join_paths(pkgdatadir, 'gatewayd'))
+conf.set_quoted('SYSTEMD_HOMEWORK_PATH',                      join_paths(rootlibexecdir, 'systemd-homework'))
 conf.set_quoted('SYSTEMD_USERWORK_PATH',                      join_paths(rootlibexecdir, 'systemd-userwork'))
 conf.set10('MEMORY_ACCOUNTING_DEFAULT',                       memory_accounting_default)
 conf.set_quoted('MEMORY_ACCOUNTING_DEFAULT_YES_NO',           memory_accounting_default ? 'yes' : 'no')
@@ -884,6 +885,16 @@ else
 endif
 conf.set10('HAVE_LIBFDISK', have)
 
+want_pwquality = get_option('pwquality')
+if want_pwquality != 'false' and not skip_deps
+        libpwquality = dependency('pwquality', required : want_pwquality == 'true')
+        have = libpwquality.found()
+else
+        have = false
+        libpwquality = []
+endif
+conf.set10('HAVE_PWQUALITY', have)
+
 want_seccomp = get_option('seccomp')
 if want_seccomp != 'false' and not skip_deps
         libseccomp = dependency('libseccomp',
@@ -1011,6 +1022,9 @@ if want_libcryptsetup != 'false' and not skip_deps
                                    version : '>= 2.0.1',
                                    required : want_libcryptsetup == 'true')
         have = libcryptsetup.found()
+
+        conf.set10('HAVE_CRYPT_SET_METADATA_SIZE',
+                   have and cc.has_function('crypt_set_metadata_size', dependencies : libcryptsetup))
 else
         have = false
         libcryptsetup = []
@@ -1316,6 +1330,19 @@ else
 endif
 conf.set10('ENABLE_IMPORTD', have)
 
+want_homed = get_option('homed')
+if want_homed != 'false'
+        have = (conf.get('HAVE_OPENSSL') == 1 and
+                conf.get('HAVE_LIBFDISK') == 1 and
+                conf.get('HAVE_LIBCRYPTSETUP') == 1)
+        if want_homed == 'true' and not have
+                error('homed support was requested, but dependencies are not available')
+        endif
+else
+        have = false
+endif
+conf.set10('ENABLE_HOMED', have)
+
 want_remote = get_option('remote')
 if want_remote != 'false'
         have_deps = [conf.get('HAVE_MICROHTTPD') == 1,
@@ -1564,6 +1591,7 @@ subdir('src/locale')
 subdir('src/machine')
 subdir('src/portable')
 subdir('src/userdb')
+subdir('src/home')
 subdir('src/nspawn')
 subdir('src/resolve')
 subdir('src/timedate')
@@ -2034,6 +2062,35 @@ if conf.get('ENABLE_USERDB') == 1
                    install_dir : rootbindir)
 endif
 
+if conf.get('ENABLE_HOMED') == 1
+        executable('systemd-homework',
+                   systemd_homework_sources,
+                   include_directories : includes,
+                   link_with : [libshared],
+                   dependencies : [threads,
+                                   libcryptsetup,
+                                   libblkid,
+                                   libcrypt,
+                                   libopenssl,
+                                   libfdisk,
+                                   libp11kit],
+                   install_rpath : rootlibexecdir,
+                   install : true,
+                   install_dir : rootlibexecdir)
+
+        executable('systemd-homed',
+                   systemd_homed_sources,
+                   include_directories : includes,
+                   link_with : [libshared],
+                   dependencies : [threads,
+                                   libcrypt,
+                                   libopenssl,
+                                   libpwquality],
+                   install_rpath : rootlibexecdir,
+                   install : true,
+                   install_dir : rootlibexecdir)
+endif
+
 foreach alias : ['halt', 'poweroff', 'reboot', 'runlevel', 'shutdown', 'telinit']
         meson.add_install_script(meson_make_symlink,
                                  join_paths(rootbindir, 'systemctl'),
@@ -3291,6 +3348,8 @@ missing = []
 foreach tuple : [
         ['libcryptsetup'],
         ['PAM'],
+        ['pwquality'],
+        ['fdisk'],
         ['p11kit'],
         ['AUDIT'],
         ['IMA'],
@@ -3329,6 +3388,7 @@ foreach tuple : [
         ['machined'],
         ['portabled'],
         ['userdb'],
+        ['homed'],
         ['importd'],
         ['hostnamed'],
         ['timedated'],
index e512d25480803aa6f039fa313cc70aed404fc6e1..1434ae706f1096a57bedc47b55e34d41af6f41ee 100644 (file)
@@ -98,6 +98,8 @@ option('portabled', type : 'boolean',
        description : 'install the systemd-portabled stack')
 option('userdb', type : 'boolean',
        description : 'install the systemd-userdbd stack')
+option('homed', type : 'boolean',
+       description : 'install the systemd-homed stack')
 option('networkd', type : 'boolean',
        description : 'install the systemd-networkd stack')
 option('timedated', type : 'boolean',
@@ -268,6 +270,8 @@ option('kmod', type : 'combo', choices : ['auto', 'true', 'false'],
        description : 'support for loadable modules')
 option('pam', type : 'combo', choices : ['auto', 'true', 'false'],
        description : 'PAM support')
+option('pwquality', type : 'combo', choices : ['auto', 'true', 'false'],
+       description : 'libpwquality support')
 option('microhttpd', type : 'combo', choices : ['auto', 'true', 'false'],
        description : 'libµhttpd support')
 option('libcryptsetup', type : 'combo', choices : ['auto', 'true', 'false'],
diff --git a/src/home/home-util.c b/src/home/home-util.c
new file mode 100644 (file)
index 0000000..bf4f238
--- /dev/null
@@ -0,0 +1,160 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include "dns-domain.h"
+#include "errno-util.h"
+#include "home-util.h"
+#include "libcrypt-util.h"
+#include "memory-util.h"
+#include "path-util.h"
+#include "string-util.h"
+#include "strv.h"
+#include "user-util.h"
+
+bool suitable_user_name(const char *name) {
+
+        /* Checks whether the specified name is suitable for management via homed. Note that our client side
+         * usually validate susing a simple valid_user_group_name(), while server side we are a bit more
+         * restrictive, so that we can change the rules server side without having to update things client
+         * side, too. */
+
+        if (!valid_user_group_name(name))
+                return false;
+
+        /* We generally rely on NSS to tell us which users not to care for, but let's filter out some
+         * particularly well-known users. */
+        if (STR_IN_SET(name,
+                       "root",
+                       "nobody",
+                       NOBODY_USER_NAME, NOBODY_GROUP_NAME))
+                return false;
+
+        /* Let's also defend our own namespace, as well as Debian's (unwritten?) logic of prefixing system
+         * users with underscores. */
+        if (STARTSWITH_SET(name, "systemd-", "_"))
+                return false;
+
+        return true;
+}
+
+int suitable_realm(const char *realm) {
+        _cleanup_free_ char *normalized = NULL;
+        int r;
+
+        /* Similar to the above: let's validate the realm a bit stricter server-side than client side */
+
+        r = dns_name_normalize(realm, 0, &normalized); /* this also checks general validity */
+        if (r == -EINVAL)
+                return 0;
+        if (r < 0)
+                return r;
+
+        if (!streq(realm, normalized)) /* is this normalized? */
+                return false;
+
+        if (dns_name_is_root(realm)) /* Don't allow top level domain */
+                return false;
+
+        return true;
+}
+
+int suitable_image_path(const char *path) {
+
+        return !empty_or_root(path) &&
+                path_is_valid(path) &&
+                path_is_absolute(path);
+}
+
+int split_user_name_realm(const char *t, char **ret_user_name, char **ret_realm) {
+        _cleanup_free_ char *user_name = NULL, *realm = NULL;
+        const char *c;
+        int r;
+
+        assert(t);
+        assert(ret_user_name);
+        assert(ret_realm);
+
+        c = strchr(t, '@');
+        if (!c) {
+                user_name = strdup(t);
+                if (!user_name)
+                        return -ENOMEM;
+        } else {
+                user_name = strndup(t, c - t);
+                if (!user_name)
+                        return -ENOMEM;
+
+                realm = strdup(c + 1);
+                if (!realm)
+                        return -ENOMEM;
+        }
+
+        if (!suitable_user_name(user_name))
+                return -EINVAL;
+
+        if (realm) {
+                r = suitable_realm(realm);
+                if (r < 0)
+                        return r;
+                if (r == 0)
+                        return -EINVAL;
+        }
+
+        *ret_user_name = TAKE_PTR(user_name);
+        *ret_realm = TAKE_PTR(realm);
+
+        return 0;
+}
+
+int bus_message_append_secret(sd_bus_message *m, UserRecord *secret) {
+        _cleanup_(erase_and_freep) char *formatted = NULL;
+        JsonVariant *v;
+        int r;
+
+        assert(m);
+        assert(secret);
+
+        if (!FLAGS_SET(secret->mask, USER_RECORD_SECRET))
+                return sd_bus_message_append(m, "s", "{}");
+
+        v = json_variant_by_key(secret->json, "secret");
+        if (!v)
+                return -EINVAL;
+
+        r = json_variant_format(v, 0, &formatted);
+        if (r < 0)
+                return r;
+
+        return sd_bus_message_append(m, "s", formatted);
+}
+
+int test_password_one(const char *hashed_password, const char *password) {
+        struct crypt_data cc = {};
+        const char *k;
+        bool b;
+
+        errno = 0;
+        k = crypt_r(password, hashed_password, &cc);
+        if (!k) {
+                explicit_bzero_safe(&cc, sizeof(cc));
+                return errno_or_else(EINVAL);
+        }
+
+        b = streq(k, hashed_password);
+        explicit_bzero_safe(&cc, sizeof(cc));
+        return b;
+}
+
+int test_password_many(char **hashed_password, const char *password) {
+        char **hpw;
+        int r;
+
+        STRV_FOREACH(hpw, hashed_password) {
+                r = test_password_one(*hpw, password);
+                if (r < 0)
+                        return r;
+                if (r > 0)
+                        return true;
+        }
+
+        return false;
+}
diff --git a/src/home/home-util.h b/src/home/home-util.h
new file mode 100644 (file)
index 0000000..df20c0a
--- /dev/null
@@ -0,0 +1,24 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include <stdbool.h>
+
+#include "sd-bus.h"
+
+#include "time-util.h"
+#include "user-record.h"
+
+bool suitable_user_name(const char *name);
+int suitable_realm(const char *realm);
+int suitable_image_path(const char *path);
+
+int split_user_name_realm(const char *t, char **ret_user_name, char **ret_realm);
+
+int bus_message_append_secret(sd_bus_message *m, UserRecord *secret);
+
+/* Many of our operations might be slow due to crypto, fsck, recursive chown() and so on. For these
+ * operations permit a *very* long time-out */
+#define HOME_SLOW_BUS_CALL_TIMEOUT_USEC (2*USEC_PER_MINUTE)
+
+int test_password_one(const char *hashed_password, const char *password);
+int test_password_many(char **hashed_password, const char *password);
diff --git a/src/home/homed-bus.c b/src/home/homed-bus.c
new file mode 100644 (file)
index 0000000..0193089
--- /dev/null
@@ -0,0 +1,64 @@
+#include "homed-bus.h"
+#include "strv.h"
+
+int bus_message_read_secret(sd_bus_message *m, UserRecord **ret, sd_bus_error *error) {
+        _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *full = NULL;
+        _cleanup_(user_record_unrefp) UserRecord *hr = NULL;
+        unsigned line = 0, column = 0;
+        const char *json;
+        int r;
+
+        assert(ret);
+
+        r = sd_bus_message_read(m, "s", &json);
+        if (r < 0)
+                return r;
+
+        r = json_parse(json, JSON_PARSE_SENSITIVE, &v, &line, &column);
+        if (r < 0)
+                return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Failed to parse JSON secret record at %u:%u: %m", line, column);
+
+        r = json_build(&full, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("secret", JSON_BUILD_VARIANT(v))));
+        if (r < 0)
+                return r;
+
+        hr = user_record_new();
+        if (!hr)
+                return -ENOMEM;
+
+        r = user_record_load(hr, full, USER_RECORD_REQUIRE_SECRET);
+        if (r < 0)
+                return r;
+
+        *ret = TAKE_PTR(hr);
+        return 0;
+}
+
+int bus_message_read_home_record(sd_bus_message *m, UserRecordLoadFlags flags, UserRecord **ret, sd_bus_error *error) {
+        _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+        _cleanup_(user_record_unrefp) UserRecord *hr = NULL;
+        unsigned line = 0, column = 0;
+        const char *json;
+        int r;
+
+        assert(ret);
+
+        r = sd_bus_message_read(m, "s", &json);
+        if (r < 0)
+                return r;
+
+        r = json_parse(json, JSON_PARSE_SENSITIVE, &v, &line, &column);
+        if (r < 0)
+                return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Failed to parse JSON identity record at %u:%u: %m", line, column);
+
+        hr = user_record_new();
+        if (!hr)
+                return -ENOMEM;
+
+        r = user_record_load(hr, v, flags);
+        if (r < 0)
+                return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "JSON data is not a valid identity record");
+
+        *ret = TAKE_PTR(hr);
+        return 0;
+}
diff --git a/src/home/homed-bus.h b/src/home/homed-bus.h
new file mode 100644 (file)
index 0000000..20f13b4
--- /dev/null
@@ -0,0 +1,10 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include "sd-bus.h"
+
+#include "user-record.h"
+#include "json.h"
+
+int bus_message_read_secret(sd_bus_message *m, UserRecord **ret, sd_bus_error *error);
+int bus_message_read_home_record(sd_bus_message *m, UserRecordLoadFlags flags, UserRecord **ret, sd_bus_error *error);
diff --git a/src/home/homed-home-bus.c b/src/home/homed-home-bus.c
new file mode 100644 (file)
index 0000000..02a87a5
--- /dev/null
@@ -0,0 +1,877 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include <linux/capability.h>
+
+#include "bus-common-errors.h"
+#include "bus-polkit.h"
+#include "fd-util.h"
+#include "homed-bus.h"
+#include "homed-home-bus.h"
+#include "homed-home.h"
+#include "strv.h"
+#include "user-record-util.h"
+#include "user-util.h"
+
+static int property_get_unix_record(
+                sd_bus *bus,
+                const char *path,
+                const char *interface,
+                const char *property,
+                sd_bus_message *reply,
+                void *userdata,
+                sd_bus_error *error) {
+
+        Home *h = userdata;
+
+        assert(bus);
+        assert(reply);
+        assert(h);
+
+        return sd_bus_message_append(
+                        reply, "(suusss)",
+                        h->user_name,
+                        (uint32_t) h->uid,
+                        h->record ? (uint32_t) user_record_gid(h->record) : GID_INVALID,
+                        h->record ? user_record_real_name(h->record) : NULL,
+                        h->record ? user_record_home_directory(h->record) : NULL,
+                        h->record ? user_record_shell(h->record) : NULL);
+}
+
+static int property_get_state(
+                sd_bus *bus,
+                const char *path,
+                const char *interface,
+                const char *property,
+                sd_bus_message *reply,
+                void *userdata,
+                sd_bus_error *error) {
+
+        Home *h = userdata;
+
+        assert(bus);
+        assert(reply);
+        assert(h);
+
+        return sd_bus_message_append(reply, "s", home_state_to_string(home_get_state(h)));
+}
+
+int bus_home_client_is_trusted(Home *h, sd_bus_message *message) {
+        _cleanup_(sd_bus_creds_unrefp) sd_bus_creds *creds = NULL;
+        uid_t euid;
+        int r;
+
+        assert(h);
+
+        if (!message)
+                return -EINVAL;
+
+        r = sd_bus_query_sender_creds(message, SD_BUS_CREDS_EUID, &creds);
+        if (r < 0)
+                return r;
+
+        r = sd_bus_creds_get_euid(creds, &euid);
+        if (r < 0)
+                return r;
+
+        return euid == 0 || h->uid == euid;
+}
+
+int bus_home_get_record_json(
+                Home *h,
+                sd_bus_message *message,
+                char **ret,
+                bool *ret_incomplete) {
+
+        _cleanup_(user_record_unrefp) UserRecord *augmented = NULL;
+        UserRecordLoadFlags flags;
+        int r, trusted;
+
+        assert(h);
+        assert(ret);
+
+        trusted = bus_home_client_is_trusted(h, message);
+        if (trusted < 0) {
+                log_warning_errno(trusted, "Failed to determine whether client is trusted, assuming untrusted.");
+                trusted = false;
+        }
+
+        flags = USER_RECORD_REQUIRE_REGULAR|USER_RECORD_ALLOW_PER_MACHINE|USER_RECORD_ALLOW_BINDING|USER_RECORD_STRIP_SECRET|USER_RECORD_ALLOW_STATUS|USER_RECORD_ALLOW_SIGNATURE;
+        if (trusted)
+                flags |= USER_RECORD_ALLOW_PRIVILEGED;
+        else
+                flags |= USER_RECORD_STRIP_PRIVILEGED;
+
+        r = home_augment_status(h, flags, &augmented);
+        if (r < 0)
+                return r;
+
+        r = json_variant_format(augmented->json, 0, ret);
+        if (r < 0)
+                return r;
+
+        if (ret_incomplete)
+                *ret_incomplete = augmented->incomplete;
+
+        return 0;
+}
+
+static int property_get_user_record(
+                sd_bus *bus,
+                const char *path,
+                const char *interface,
+                const char *property,
+                sd_bus_message *reply,
+                void *userdata,
+                sd_bus_error *error) {
+
+        _cleanup_free_ char *json = NULL;
+        Home *h = userdata;
+        bool incomplete;
+        int r;
+
+        assert(bus);
+        assert(reply);
+        assert(h);
+
+        r = bus_home_get_record_json(h, sd_bus_get_current_message(bus), &json, &incomplete);
+        if (r < 0)
+                return r;
+
+        return sd_bus_message_append(reply, "(sb)", json, incomplete);
+}
+
+int bus_home_method_activate(
+                sd_bus_message *message,
+                void *userdata,
+                sd_bus_error *error) {
+
+        _cleanup_(user_record_unrefp) UserRecord *secret = NULL;
+        Home *h = userdata;
+        int r;
+
+        assert(message);
+        assert(h);
+
+        r = bus_message_read_secret(message, &secret, error);
+        if (r < 0)
+                return r;
+
+        r = home_activate(h, secret, error);
+        if (r < 0)
+                return r;
+
+        assert(r == 0);
+        assert(!h->current_operation);
+
+        /* The operation is now in process, keep track of this message so that we can later reply to it. */
+        r = home_set_current_message(h, message);
+        if (r < 0)
+                return r;
+
+        return 1;
+}
+
+int bus_home_method_deactivate(
+                sd_bus_message *message,
+                void *userdata,
+                sd_bus_error *error) {
+
+        Home *h = userdata;
+        int r;
+
+        assert(message);
+        assert(h);
+
+        r = home_deactivate(h, false, error);
+        if (r < 0)
+                return r;
+
+        assert(r == 0);
+        assert(!h->current_operation);
+
+        r = home_set_current_message(h, message);
+        if (r < 0)
+                return r;
+
+        return 1;
+}
+
+int bus_home_method_unregister(
+                sd_bus_message *message,
+                void *userdata,
+                sd_bus_error *error) {
+
+        Home *h = userdata;
+        int r;
+
+        assert(message);
+        assert(h);
+
+        r = bus_verify_polkit_async(
+                        message,
+                        CAP_SYS_ADMIN,
+                        "org.freedesktop.home1.remove-home",
+                        NULL,
+                        true,
+                        UID_INVALID,
+                        &h->manager->polkit_registry,
+                        error);
+        if (r < 0)
+                return r;
+        if (r == 0)
+                return 1; /* Will call us back */
+
+        r = home_unregister(h, error);
+        if (r < 0)
+                return r;
+
+        assert(r > 0);
+
+        /* Note that home_unregister() destroyed 'h' here, so no more accesses */
+
+        return sd_bus_reply_method_return(message, NULL);
+}
+
+int bus_home_method_realize(
+                sd_bus_message *message,
+                void *userdata,
+                sd_bus_error *error) {
+
+        _cleanup_(user_record_unrefp) UserRecord *secret = NULL;
+        Home *h = userdata;
+        int r;
+
+        assert(message);
+        assert(h);
+
+        r = bus_message_read_secret(message, &secret, error);
+        if (r < 0)
+                return r;
+
+        r = bus_verify_polkit_async(
+                        message,
+                        CAP_SYS_ADMIN,
+                        "org.freedesktop.home1.create-home",
+                        NULL,
+                        true,
+                        UID_INVALID,
+                        &h->manager->polkit_registry,
+                        error);
+        if (r < 0)
+                return r;
+        if (r == 0)
+                return 1; /* Will call us back */
+
+        r = home_create(h, secret, error);
+        if (r < 0)
+                return r;
+
+        assert(r == 0);
+        assert(!h->current_operation);
+
+        h->unregister_on_failure = false;
+
+        r = home_set_current_message(h, message);
+        if (r < 0)
+                return r;
+
+        return 1;
+}
+
+int bus_home_method_remove(
+                sd_bus_message *message,
+                void *userdata,
+                sd_bus_error *error) {
+
+        Home *h = userdata;
+        int r;
+
+        assert(message);
+        assert(h);
+
+        r = bus_verify_polkit_async(
+                        message,
+                        CAP_SYS_ADMIN,
+                        "org.freedesktop.home1.remove-home",
+                        NULL,
+                        true,
+                        UID_INVALID,
+                        &h->manager->polkit_registry,
+                        error);
+        if (r < 0)
+                return r;
+        if (r == 0)
+                return 1; /* Will call us back */
+
+        r = home_remove(h, error);
+        if (r < 0)
+                return r;
+        if (r > 0) /* Done already. Note that home_remove() destroyed 'h' here, so no more accesses */
+                return sd_bus_reply_method_return(message, NULL);
+
+        assert(!h->current_operation);
+
+        r = home_set_current_message(h, message);
+        if (r < 0)
+                return r;
+
+        return 1;
+}
+
+int bus_home_method_fixate(
+                sd_bus_message *message,
+                void *userdata,
+                sd_bus_error *error) {
+
+        _cleanup_(user_record_unrefp) UserRecord *secret = NULL;
+        Home *h = userdata;
+        int r;
+
+        assert(message);
+        assert(h);
+
+        r = bus_message_read_secret(message, &secret, error);
+        if (r < 0)
+                return r;
+
+        r = home_fixate(h, secret, error);
+        if (r < 0)
+                return r;
+
+        assert(r == 0);
+        assert(!h->current_operation);
+
+        r = home_set_current_message(h, message);
+        if (r < 0)
+                return r;
+
+        return 1;
+}
+
+int bus_home_method_authenticate(
+                sd_bus_message *message,
+                void *userdata,
+                sd_bus_error *error) {
+
+        _cleanup_(user_record_unrefp) UserRecord *secret = NULL;
+        Home *h = userdata;
+        int r;
+
+        assert(message);
+        assert(h);
+
+        r = bus_message_read_secret(message, &secret, error);
+        if (r < 0)
+                return r;
+
+        r = bus_verify_polkit_async(
+                        message,
+                        CAP_SYS_ADMIN,
+                        "org.freedesktop.home1.authenticate-home",
+                        NULL,
+                        true,
+                        h->uid,
+                        &h->manager->polkit_registry,
+                        error);
+        if (r < 0)
+                return r;
+        if (r == 0)
+                return 1; /* Will call us back */
+
+        r = home_authenticate(h, secret, error);
+        if (r < 0)
+                return r;
+
+        assert(r == 0);
+        assert(!h->current_operation);
+
+        r = home_set_current_message(h, message);
+        if (r < 0)
+                return r;
+
+        return 1;
+}
+
+int bus_home_method_update_record(Home *h, sd_bus_message *message, UserRecord *hr, sd_bus_error *error) {
+        int r;
+
+        assert(h);
+        assert(message);
+        assert(hr);
+
+        r = user_record_is_supported(hr, error);
+        if (r < 0)
+                return r;
+
+        r = bus_verify_polkit_async(
+                        message,
+                        CAP_SYS_ADMIN,
+                        "org.freedesktop.home1.update-home",
+                        NULL,
+                        true,
+                        UID_INVALID,
+                        &h->manager->polkit_registry,
+                        error);
+        if (r < 0)
+                return r;
+        if (r == 0)
+                return 1; /* Will call us back */
+
+        r = home_update(h, hr, error);
+        if (r < 0)
+                return r;
+
+        assert(r == 0);
+        assert(!h->current_operation);
+
+        r = home_set_current_message(h, message);
+        if (r < 0)
+                return r;
+
+        return 1;
+}
+
+int bus_home_method_update(
+                sd_bus_message *message,
+                void *userdata,
+                sd_bus_error *error) {
+
+        _cleanup_(user_record_unrefp) UserRecord *hr = NULL;
+        Home *h = userdata;
+        int r;
+
+        assert(message);
+        assert(h);
+
+        r = bus_message_read_home_record(message, USER_RECORD_REQUIRE_REGULAR|USER_RECORD_REQUIRE_SECRET|USER_RECORD_ALLOW_PRIVILEGED|USER_RECORD_ALLOW_PER_MACHINE|USER_RECORD_ALLOW_SIGNATURE, &hr, error);
+        if (r < 0)
+                return r;
+
+        return bus_home_method_update_record(h, message, hr, error);
+}
+
+int bus_home_method_resize(
+                sd_bus_message *message,
+                void *userdata,
+                sd_bus_error *error) {
+
+        _cleanup_(user_record_unrefp) UserRecord *secret = NULL;
+        Home *h = userdata;
+        uint64_t sz;
+        int r;
+
+        assert(message);
+        assert(h);
+
+        r = sd_bus_message_read(message, "t", &sz);
+        if (r < 0)
+                return r;
+
+        r = bus_message_read_secret(message, &secret, error);
+        if (r < 0)
+                return r;
+
+        r = bus_verify_polkit_async(
+                        message,
+                        CAP_SYS_ADMIN,
+                        "org.freedesktop.home1.resize-home",
+                        NULL,
+                        true,
+                        UID_INVALID,
+                        &h->manager->polkit_registry,
+                        error);
+        if (r < 0)
+                return r;
+        if (r == 0)
+                return 1; /* Will call us back */
+
+        r = home_resize(h, sz, secret, error);
+        if (r < 0)
+                return r;
+
+        assert(r == 0);
+        assert(!h->current_operation);
+
+        r = home_set_current_message(h, message);
+        if (r < 0)
+                return r;
+
+        return 1;
+}
+
+int bus_home_method_change_password(
+                sd_bus_message *message,
+                void *userdata,
+                sd_bus_error *error) {
+
+        _cleanup_(user_record_unrefp) UserRecord *new_secret = NULL, *old_secret = NULL;
+        Home *h = userdata;
+        int r;
+
+        assert(message);
+        assert(h);
+
+        r = bus_message_read_secret(message, &new_secret, error);
+        if (r < 0)
+                return r;
+
+        r = bus_message_read_secret(message, &old_secret, error);
+        if (r < 0)
+                return r;
+
+        r = bus_verify_polkit_async(
+                        message,
+                        CAP_SYS_ADMIN,
+                        "org.freedesktop.home1.passwd-home",
+                        NULL,
+                        true,
+                        h->uid,
+                        &h->manager->polkit_registry,
+                        error);
+        if (r < 0)
+                return r;
+        if (r == 0)
+                return 1; /* Will call us back */
+
+        r = home_passwd(h, new_secret, old_secret, error);
+        if (r < 0)
+                return r;
+
+        assert(r == 0);
+        assert(!h->current_operation);
+
+        r = home_set_current_message(h, message);
+        if (r < 0)
+                return r;
+
+        return 1;
+}
+
+int bus_home_method_lock(
+                sd_bus_message *message,
+                void *userdata,
+                sd_bus_error *error) {
+
+        Home *h = userdata;
+        int r;
+
+        assert(message);
+        assert(h);
+
+        r = home_lock(h, error);
+        if (r < 0)
+                return r;
+        if (r > 0) /* Done */
+                return sd_bus_reply_method_return(message, NULL);
+
+        /* The operation is now in process, keep track of this message so that we can later reply to it. */
+        assert(!h->current_operation);
+
+        r = home_set_current_message(h, message);
+        if (r < 0)
+                return r;
+
+        return 1;
+}
+
+int bus_home_method_unlock(
+                sd_bus_message *message,
+                void *userdata,
+                sd_bus_error *error) {
+
+        _cleanup_(user_record_unrefp) UserRecord *secret = NULL;
+        Home *h = userdata;
+        int r;
+
+        assert(message);
+        assert(h);
+
+        r = bus_message_read_secret(message, &secret, error);
+        if (r < 0)
+                return r;
+
+        r = home_unlock(h, secret, error);
+        if (r < 0)
+                return r;
+
+        assert(r == 0);
+        assert(!h->current_operation);
+
+        /* The operation is now in process, keep track of this message so that we can later reply to it. */
+        r = home_set_current_message(h, message);
+        if (r < 0)
+                return r;
+
+        return 1;
+}
+
+int bus_home_method_acquire(
+                sd_bus_message *message,
+                void *userdata,
+                sd_bus_error *error) {
+
+        _cleanup_(user_record_unrefp) UserRecord *secret = NULL;
+        _cleanup_(operation_unrefp) Operation *o = NULL;
+        _cleanup_close_ int fd = -1;
+        int r, please_suspend;
+        Home *h = userdata;
+
+        assert(message);
+        assert(h);
+
+        r = bus_message_read_secret(message, &secret, error);
+        if (r < 0)
+                return r;
+
+        r = sd_bus_message_read(message, "b", &please_suspend);
+        if (r < 0)
+                return r;
+
+        /* This operation might not be something we can executed immediately, hence queue it */
+        fd = home_create_fifo(h, please_suspend);
+        if (fd < 0)
+                return sd_bus_reply_method_errnof(message, fd, "Failed to allocate fifo for %s: %m", h->user_name);
+
+        o = operation_new(OPERATION_ACQUIRE, message);
+        if (!o)
+                return -ENOMEM;
+
+        o->secret = TAKE_PTR(secret);
+        o->send_fd = TAKE_FD(fd);
+
+        r = home_schedule_operation(h, o, error);
+        if (r < 0)
+                return r;
+
+        return 1;
+}
+
+int bus_home_method_ref(
+                sd_bus_message *message,
+                void *userdata,
+                sd_bus_error *error) {
+
+        _cleanup_close_ int fd = -1;
+        Home *h = userdata;
+        HomeState state;
+        int please_suspend, r;
+
+        assert(message);
+        assert(h);
+
+        r = sd_bus_message_read(message, "b", &please_suspend);
+        if (r < 0)
+                return r;
+
+        state = home_get_state(h);
+        switch (state) {
+        case HOME_ABSENT:
+                return sd_bus_error_setf(error, BUS_ERROR_HOME_ABSENT, "Home %s is currently missing or not plugged in.", h->user_name);
+        case HOME_UNFIXATED:
+        case HOME_INACTIVE:
+                return sd_bus_error_setf(error, BUS_ERROR_HOME_NOT_ACTIVE, "Home %s not active.", h->user_name);
+        case HOME_LOCKED:
+                return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name);
+        default:
+                if (HOME_STATE_IS_ACTIVE(state))
+                        break;
+
+                return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "An operation on home %s is currently being executed.", h->user_name);
+        }
+
+        fd = home_create_fifo(h, please_suspend);
+        if (fd < 0)
+                return sd_bus_reply_method_errnof(message, fd, "Failed to allocate fifo for %s: %m", h->user_name);
+
+        return sd_bus_reply_method_return(message, "h", fd);
+}
+
+int bus_home_method_release(
+                sd_bus_message *message,
+                void *userdata,
+                sd_bus_error *error) {
+
+        _cleanup_(operation_unrefp) Operation *o = NULL;
+        Home *h = userdata;
+        int r;
+
+        assert(message);
+        assert(h);
+
+        o = operation_new(OPERATION_RELEASE, message);
+        if (!o)
+                return -ENOMEM;
+
+        r = home_schedule_operation(h, o, error);
+        if (r < 0)
+                return r;
+
+        return 1;
+}
+
+/* We map a uid_t as uint32_t bus property, let's ensure this is safe. */
+assert_cc(sizeof(uid_t) == sizeof(uint32_t));
+
+const sd_bus_vtable home_vtable[] = {
+        SD_BUS_VTABLE_START(0),
+        SD_BUS_PROPERTY("UserName", "s", NULL, offsetof(Home, user_name), SD_BUS_VTABLE_PROPERTY_CONST),
+        SD_BUS_PROPERTY("UID", "u", NULL, offsetof(Home, uid), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
+        SD_BUS_PROPERTY("UnixRecord", "(suusss)", property_get_unix_record, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
+        SD_BUS_PROPERTY("State", "s", property_get_state, 0, 0),
+        SD_BUS_PROPERTY("UserRecord", "(sb)", property_get_user_record, 0, SD_BUS_VTABLE_PROPERTY_EMITS_INVALIDATION|SD_BUS_VTABLE_SENSITIVE),
+        SD_BUS_METHOD("Activate", "s", NULL, bus_home_method_activate, SD_BUS_VTABLE_SENSITIVE),
+        SD_BUS_METHOD("Deactivate", NULL, NULL, bus_home_method_deactivate, 0),
+        SD_BUS_METHOD("Unregister", NULL, NULL, bus_home_method_unregister, SD_BUS_VTABLE_UNPRIVILEGED),
+        SD_BUS_METHOD("Realize", "s", NULL, bus_home_method_realize, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),
+        SD_BUS_METHOD("Remove", NULL, NULL, bus_home_method_remove, SD_BUS_VTABLE_UNPRIVILEGED),
+        SD_BUS_METHOD("Fixate", "s", NULL, bus_home_method_fixate, SD_BUS_VTABLE_SENSITIVE),
+        SD_BUS_METHOD("Authenticate", "s", NULL, bus_home_method_authenticate, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),
+        SD_BUS_METHOD("Update", "s", NULL, bus_home_method_update, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),
+        SD_BUS_METHOD("Resize", "ts", NULL, bus_home_method_resize, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),
+        SD_BUS_METHOD("ChangePassword", "ss", NULL, bus_home_method_change_password, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),
+        SD_BUS_METHOD("Lock", NULL, NULL, bus_home_method_lock, 0),
+        SD_BUS_METHOD("Unlock", "s", NULL, bus_home_method_unlock, SD_BUS_VTABLE_SENSITIVE),
+        SD_BUS_METHOD("Acquire", "sb", "h", bus_home_method_acquire, SD_BUS_VTABLE_SENSITIVE),
+        SD_BUS_METHOD("Ref", "b", "h", bus_home_method_ref, 0),
+        SD_BUS_METHOD("Release", NULL, NULL, bus_home_method_release, 0),
+        SD_BUS_VTABLE_END
+};
+
+int bus_home_path(Home *h, char **ret) {
+        assert(ret);
+
+        return sd_bus_path_encode("/org/freedesktop/home1/home", h->user_name, ret);
+}
+
+int bus_home_object_find(
+                sd_bus *bus,
+                const char *path,
+                const char *interface,
+                void *userdata,
+                void **found,
+                sd_bus_error *error) {
+
+        _cleanup_free_ char *e = NULL;
+        Manager *m = userdata;
+        uid_t uid;
+        Home *h;
+        int r;
+
+        r = sd_bus_path_decode(path, "/org/freedesktop/home1/home", &e);
+        if (r <= 0)
+                return 0;
+
+        if (parse_uid(e, &uid) >= 0)
+                h = hashmap_get(m->homes_by_uid, UID_TO_PTR(uid));
+        else
+                h = hashmap_get(m->homes_by_name, e);
+        if (!h)
+                return 0;
+
+        *found = h;
+        return 1;
+}
+
+int bus_home_node_enumerator(
+                sd_bus *bus,
+                const char *path,
+                void *userdata,
+                char ***nodes,
+                sd_bus_error *error) {
+
+        _cleanup_strv_free_ char **l = NULL;
+        Manager *m = userdata;
+        size_t k = 0;
+        Iterator i;
+        Home *h;
+        int r;
+
+        assert(nodes);
+
+        l = new0(char*, hashmap_size(m->homes_by_uid) + 1);
+        if (!l)
+                return -ENOMEM;
+
+        HASHMAP_FOREACH(h, m->homes_by_uid, i) {
+                r = bus_home_path(h, l + k);
+                if (r < 0)
+                        return r;
+        }
+
+        *nodes = TAKE_PTR(l);
+        return 1;
+}
+
+static int on_deferred_change(sd_event_source *s, void *userdata) {
+        _cleanup_free_ char *path = NULL;
+        Home *h = userdata;
+        int r;
+
+        assert(h);
+
+        h->deferred_change_event_source = sd_event_source_unref(h->deferred_change_event_source);
+
+        r = bus_home_path(h, &path);
+        if (r < 0) {
+                log_warning_errno(r, "Failed to generate home bus path, ignoring: %m");
+                return 0;
+        }
+
+        if (h->announced)
+                r = sd_bus_emit_properties_changed_strv(h->manager->bus, path, "org.freedesktop.home1.Home", NULL);
+        else
+                r = sd_bus_emit_object_added(h->manager->bus, path);
+        if (r < 0)
+                log_warning_errno(r, "Failed to send home change event, ignoring: %m");
+        else
+                h->announced = true;
+
+        return 0;
+}
+
+int bus_home_emit_change(Home *h) {
+        int r;
+
+        assert(h);
+
+        if (h->deferred_change_event_source)
+                return 1;
+
+        if (!h->manager->event)
+                return 0;
+
+        if (IN_SET(sd_event_get_state(h->manager->event), SD_EVENT_FINISHED, SD_EVENT_EXITING))
+                return 0;
+
+        r = sd_event_add_defer(h->manager->event, &h->deferred_change_event_source, on_deferred_change, h);
+        if (r < 0)
+                return log_error_errno(r, "Failed to allocate deferred change event source: %m");
+
+        r = sd_event_source_set_priority(h->deferred_change_event_source, SD_EVENT_PRIORITY_IDLE+5);
+        if (r < 0)
+                log_warning_errno(r, "Failed to tweak priority of event source, ignoring: %m");
+
+        (void) sd_event_source_set_description(h->deferred_change_event_source, "deferred-change-event");
+        return 1;
+}
+
+int bus_home_emit_remove(Home *h) {
+        _cleanup_free_ char *path = NULL;
+        int r;
+
+        assert(h);
+
+        if (!h->announced)
+                return 0;
+
+        r = bus_home_path(h, &path);
+        if (r < 0)
+                return r;
+
+        r = sd_bus_emit_object_removed(h->manager->bus, path);
+        if (r < 0)
+                return r;
+
+        h->announced = false;
+        return 1;
+}
diff --git a/src/home/homed-home-bus.h b/src/home/homed-home-bus.h
new file mode 100644 (file)
index 0000000..20516b1
--- /dev/null
@@ -0,0 +1,36 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include "sd-bus.h"
+
+#include "homed-home.h"
+
+int bus_home_client_is_trusted(Home *h, sd_bus_message *message);
+int bus_home_get_record_json(Home *h, sd_bus_message *message, char **ret, bool *ret_incomplete);
+
+int bus_home_method_activate(sd_bus_message *message, void *userdata, sd_bus_error *error);
+int bus_home_method_deactivate(sd_bus_message *message, void *userdata, sd_bus_error *error);
+int bus_home_method_unregister(sd_bus_message *message, void *userdata, sd_bus_error *error);
+int bus_home_method_realize(sd_bus_message *message, void *userdata, sd_bus_error *error);
+int bus_home_method_remove(sd_bus_message *message, void *userdata, sd_bus_error *error);
+int bus_home_method_fixate(sd_bus_message *message, void *userdata, sd_bus_error *error);
+int bus_home_method_authenticate(sd_bus_message *message, void *userdata, sd_bus_error *error);
+int bus_home_method_update(sd_bus_message *message, void *userdata, sd_bus_error *error);
+int bus_home_method_update_record(Home *home, sd_bus_message *message, UserRecord *hr, sd_bus_error *error);
+int bus_home_method_resize(sd_bus_message *message, void *userdata, sd_bus_error *error);
+int bus_home_method_change_password(sd_bus_message *message, void *userdata, sd_bus_error *error);
+int bus_home_method_lock(sd_bus_message *message, void *userdata, sd_bus_error *error);
+int bus_home_method_unlock(sd_bus_message *message, void *userdata, sd_bus_error *error);
+int bus_home_method_acquire(sd_bus_message *message, void *userdata, sd_bus_error *error);
+int bus_home_method_ref(sd_bus_message *message, void *userdata, sd_bus_error *error);
+int bus_home_method_release(sd_bus_message *message, void *userdata, sd_bus_error *error);
+
+extern const sd_bus_vtable home_vtable[];
+
+int bus_home_path(Home *h, char **ret);
+
+int bus_home_object_find(sd_bus *bus, const char *path, const char *interface, void *userdata, void **found, sd_bus_error *error);
+int bus_home_node_enumerator(sd_bus *bus, const char *path, void *userdata, char ***nodes, sd_bus_error *error);
+
+int bus_home_emit_change(Home *h);
+int bus_home_emit_remove(Home *h);
diff --git a/src/home/homed-home.c b/src/home/homed-home.c
new file mode 100644 (file)
index 0000000..f50de26
--- /dev/null
@@ -0,0 +1,2712 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#if HAVE_LINUX_MEMFD_H
+#include <linux/memfd.h>
+#endif
+
+#include <sys/mman.h>
+#include <sys/quota.h>
+#include <sys/vfs.h>
+
+#include "blockdev-util.h"
+#include "btrfs-util.h"
+#include "bus-common-errors.h"
+#include "env-util.h"
+#include "errno-list.h"
+#include "errno-util.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "home-util.h"
+#include "homed-home-bus.h"
+#include "homed-home.h"
+#include "missing_syscall.h"
+#include "mkdir.h"
+#include "path-util.h"
+#include "process-util.h"
+#include "pwquality-util.h"
+#include "quota-util.h"
+#include "resize-fs.h"
+#include "set.h"
+#include "signal-util.h"
+#include "stat-util.h"
+#include "string-table.h"
+#include "strv.h"
+#include "user-record-sign.h"
+#include "user-record-util.h"
+#include "user-record.h"
+#include "user-util.h"
+
+#define HOME_USERS_MAX 500
+#define PENDING_OPERATIONS_MAX 100
+
+assert_cc(HOME_UID_MIN <= HOME_UID_MAX);
+assert_cc(HOME_USERS_MAX <= (HOME_UID_MAX - HOME_UID_MIN + 1));
+
+static int home_start_work(Home *h, const char *verb, UserRecord *hr, UserRecord *secret);
+
+DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(operation_hash_ops, void, trivial_hash_func, trivial_compare_func, Operation, operation_unref);
+
+static int suitable_home_record(UserRecord *hr) {
+        int r;
+
+        assert(hr);
+
+        if (!hr->user_name)
+                return -EUNATCH;
+
+        /* We are a bit more restrictive with what we accept as homed-managed user than what we accept in
+         * home records in general. Let's enforce the stricter rule here. */
+        if (!suitable_user_name(hr->user_name))
+                return -EINVAL;
+        if (!uid_is_valid(hr->uid))
+                return -EINVAL;
+
+        /* Insist we are outside of the dynamic and system range */
+        if (uid_is_system(hr->uid) || gid_is_system(user_record_gid(hr)) ||
+            uid_is_dynamic(hr->uid) || gid_is_dynamic(user_record_gid(hr)))
+                return -EADDRNOTAVAIL;
+
+        /* Insist that GID and UID match */
+        if (user_record_gid(hr) != (gid_t) hr->uid)
+                return -EBADSLT;
+
+        /* Similar for the realm */
+        if (hr->realm) {
+                r = suitable_realm(hr->realm);
+                if (r < 0)
+                        return r;
+                if (r == 0)
+                        return -EINVAL;
+        }
+
+        return 0;
+}
+
+int home_new(Manager *m, UserRecord *hr, const char *sysfs, Home **ret) {
+        _cleanup_(home_freep) Home *home = NULL;
+        _cleanup_free_ char *nm = NULL, *ns = NULL;
+        int r;
+
+        assert(m);
+        assert(hr);
+
+        r = suitable_home_record(hr);
+        if (r < 0)
+                return r;
+
+        if (hashmap_contains(m->homes_by_name, hr->user_name))
+                return -EBUSY;
+
+        if (hashmap_contains(m->homes_by_uid, UID_TO_PTR(hr->uid)))
+                return -EBUSY;
+
+        if (sysfs && hashmap_contains(m->homes_by_sysfs, sysfs))
+                return -EBUSY;
+
+        if (hashmap_size(m->homes_by_name) >= HOME_USERS_MAX)
+                return -EUSERS;
+
+        nm = strdup(hr->user_name);
+        if (!nm)
+                return -ENOMEM;
+
+        if (sysfs) {
+                ns = strdup(sysfs);
+                if (!ns)
+                        return -ENOMEM;
+        }
+
+        home = new(Home, 1);
+        if (!home)
+                return -ENOMEM;
+
+        *home = (Home) {
+                .manager = m,
+                .user_name = TAKE_PTR(nm),
+                .uid = hr->uid,
+                .state = _HOME_STATE_INVALID,
+                .worker_stdout_fd = -1,
+                .sysfs = TAKE_PTR(ns),
+                .signed_locally = -1,
+        };
+
+        r = hashmap_put(m->homes_by_name, home->user_name, home);
+        if (r < 0)
+                return r;
+
+        r = hashmap_put(m->homes_by_uid, UID_TO_PTR(home->uid), home);
+        if (r < 0)
+                return r;
+
+        if (home->sysfs) {
+                r = hashmap_put(m->homes_by_sysfs, home->sysfs, home);
+                if (r < 0)
+                        return r;
+        }
+
+        r = user_record_clone(hr, USER_RECORD_LOAD_MASK_SECRET, &home->record);
+        if (r < 0)
+                return r;
+
+        (void) bus_manager_emit_auto_login_changed(m);
+        (void) bus_home_emit_change(home);
+
+        if (ret)
+                *ret = TAKE_PTR(home);
+        else
+                TAKE_PTR(home);
+
+        return 0;
+}
+
+Home *home_free(Home *h) {
+
+        if (!h)
+                return NULL;
+
+        if (h->manager) {
+                (void) bus_home_emit_remove(h);
+                (void) bus_manager_emit_auto_login_changed(h->manager);
+
+                if (h->user_name)
+                        (void) hashmap_remove_value(h->manager->homes_by_name, h->user_name, h);
+
+                if (uid_is_valid(h->uid))
+                        (void) hashmap_remove_value(h->manager->homes_by_uid, UID_TO_PTR(h->uid), h);
+
+                if (h->sysfs)
+                        (void) hashmap_remove_value(h->manager->homes_by_sysfs, h->sysfs, h);
+
+                if (h->worker_pid > 0)
+                        (void) hashmap_remove_value(h->manager->homes_by_worker_pid, PID_TO_PTR(h->worker_pid), h);
+
+                if (h->manager->gc_focus == h)
+                        h->manager->gc_focus = NULL;
+        }
+
+        user_record_unref(h->record);
+        user_record_unref(h->secret);
+
+        h->worker_event_source = sd_event_source_unref(h->worker_event_source);
+        safe_close(h->worker_stdout_fd);
+        free(h->user_name);
+        free(h->sysfs);
+
+        h->ref_event_source_please_suspend = sd_event_source_unref(h->ref_event_source_please_suspend);
+        h->ref_event_source_dont_suspend = sd_event_source_unref(h->ref_event_source_dont_suspend);
+
+        h->pending_operations = ordered_set_free(h->pending_operations);
+        h->pending_event_source = sd_event_source_unref(h->pending_event_source);
+        h->deferred_change_event_source = sd_event_source_unref(h->deferred_change_event_source);
+
+        h->current_operation = operation_unref(h->current_operation);
+
+        return mfree(h);
+}
+
+int home_set_record(Home *h, UserRecord *hr) {
+        _cleanup_(user_record_unrefp) UserRecord *new_hr = NULL;
+        Home *other;
+        int r;
+
+        assert(h);
+        assert(h->user_name);
+        assert(h->record);
+        assert(hr);
+
+        if (user_record_equal(h->record, hr))
+                return 0;
+
+        r = suitable_home_record(hr);
+        if (r < 0)
+                return r;
+
+        if (!user_record_compatible(h->record, hr))
+                return -EREMCHG;
+
+        if (!FLAGS_SET(hr->mask, USER_RECORD_REGULAR) ||
+            FLAGS_SET(hr->mask, USER_RECORD_SECRET))
+                return -EINVAL;
+
+        if (FLAGS_SET(h->record->mask, USER_RECORD_STATUS)) {
+                _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+
+                /* Hmm, the existing record has status fields? If so, copy them over */
+
+                v = json_variant_ref(hr->json);
+                r = json_variant_set_field(&v, "status", json_variant_by_key(h->record->json, "status"));
+                if (r < 0)
+                        return r;
+
+                new_hr = user_record_new();
+                if (!new_hr)
+                        return -ENOMEM;
+
+                r = user_record_load(new_hr, v, USER_RECORD_LOAD_REFUSE_SECRET);
+                if (r < 0)
+                        return r;
+
+                hr = new_hr;
+        }
+
+        other = hashmap_get(h->manager->homes_by_uid, UID_TO_PTR(hr->uid));
+        if (other && other != h)
+                return -EBUSY;
+
+        if (h->uid != hr->uid) {
+                r = hashmap_remove_and_replace(h->manager->homes_by_uid, UID_TO_PTR(h->uid), UID_TO_PTR(hr->uid), h);
+                if (r < 0)
+                        return r;
+        }
+
+        user_record_unref(h->record);
+        h->record = user_record_ref(hr);
+        h->uid = h->record->uid;
+
+        /* The updated record might have a different autologin setting, trigger a PropertiesChanged event for it */
+        (void) bus_manager_emit_auto_login_changed(h->manager);
+        (void) bus_home_emit_change(h);
+
+        return 0;
+}
+
+int home_save_record(Home *h) {
+        _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+        _cleanup_free_ char *text = NULL;
+        const char *fn;
+        int r;
+
+        assert(h);
+
+        v = json_variant_ref(h->record->json);
+        r = json_variant_normalize(&v);
+        if (r < 0)
+                log_warning_errno(r, "User record could not be normalized.");
+
+        r = json_variant_format(v, JSON_FORMAT_PRETTY|JSON_FORMAT_NEWLINE, &text);
+        if (r < 0)
+                return r;
+
+        (void) mkdir("/var/lib/systemd/", 0755);
+        (void) mkdir("/var/lib/systemd/home/", 0700);
+
+        fn = strjoina("/var/lib/systemd/home/", h->user_name, ".identity");
+
+        r = write_string_file(fn, text, WRITE_STRING_FILE_ATOMIC|WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_MODE_0600);
+        if (r < 0)
+                return r;
+
+        return 0;
+}
+
+int home_unlink_record(Home *h) {
+        const char *fn;
+
+        assert(h);
+
+        fn = strjoina("/var/lib/systemd/home/", h->user_name, ".identity");
+        if (unlink(fn) < 0 && errno != ENOENT)
+                return -errno;
+
+        fn = strjoina("/run/systemd/home/", h->user_name, ".ref");
+        if (unlink(fn) < 0 && errno != ENOENT)
+                return -errno;
+
+        return 0;
+}
+
+static void home_set_state(Home *h, HomeState state) {
+        HomeState old_state, new_state;
+
+        assert(h);
+
+        old_state = home_get_state(h);
+        h->state = state;
+        new_state = home_get_state(h); /* Query the new state, since the 'state' variable might be set to -1,
+                                        * in which case we synthesize an high-level state on demand */
+
+        log_info("%s: changing state %s → %s", h->user_name,
+                 home_state_to_string(old_state),
+                 home_state_to_string(new_state));
+
+        if (HOME_STATE_IS_EXECUTING_OPERATION(old_state) && !HOME_STATE_IS_EXECUTING_OPERATION(new_state)) {
+                /* If we just finished executing some operation, process the queue of pending operations. And
+                 * enqueue it for GC too. */
+
+                home_schedule_operation(h, NULL, NULL);
+                manager_enqueue_gc(h->manager, h);
+        }
+}
+
+static int home_parse_worker_stdout(int _fd, UserRecord **ret) {
+        _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+        _cleanup_close_ int fd = _fd; /* take possession, even on failure */
+        _cleanup_(user_record_unrefp) UserRecord *hr = NULL;
+        _cleanup_fclose_ FILE *f = NULL;
+        unsigned line, column;
+        struct stat st;
+        int r;
+
+        if (fstat(fd, &st) < 0)
+                return log_error_errno(errno, "Failed to stat stdout fd: %m");
+
+        assert(S_ISREG(st.st_mode));
+
+        if (st.st_size == 0) { /* empty record */
+                *ret = NULL;
+                return 0;
+        }
+
+        if (lseek(fd, SEEK_SET, 0) == (off_t) -1)
+                return log_error_errno(errno, "Failed to seek to beginning of memfd: %m");
+
+        f = fdopen(fd, "r");
+        if (!f)
+                return log_error_errno(errno, "Failed to reopen memfd: %m");
+
+        TAKE_FD(fd);
+
+        if (DEBUG_LOGGING) {
+                _cleanup_free_ char *text = NULL;
+
+                r = read_full_stream(f, &text, NULL);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to read from client: %m");
+
+                log_debug("Got from worker: %s", text);
+                rewind(f);
+        }
+
+        r = json_parse_file(f, "stdout", JSON_PARSE_SENSITIVE, &v, &line, &column);
+        if (r < 0)
+                return log_error_errno(r, "Failed to parse identity at %u:%u: %m", line, column);
+
+        hr = user_record_new();
+        if (!hr)
+                return log_oom();
+
+        r = user_record_load(hr, v, USER_RECORD_LOAD_REFUSE_SECRET);
+        if (r < 0)
+                return log_error_errno(r, "Failed to load home record identity: %m");
+
+        *ret = TAKE_PTR(hr);
+        return 1;
+}
+
+static int home_verify_user_record(Home *h, UserRecord *hr, bool *ret_signed_locally, sd_bus_error *ret_error) {
+        int is_signed;
+
+        assert(h);
+        assert(hr);
+        assert(ret_signed_locally);
+
+        is_signed = manager_verify_user_record(h->manager, hr);
+        switch (is_signed) {
+
+        case USER_RECORD_SIGNED_EXCLUSIVE:
+                log_info("Home %s is signed exclusively by our key, accepting.", hr->user_name);
+                *ret_signed_locally = true;
+                return 0;
+
+        case USER_RECORD_SIGNED:
+                log_info("Home %s is signed by our key (and others), accepting.", hr->user_name);
+                *ret_signed_locally = false;
+                return 0;
+
+        case USER_RECORD_FOREIGN:
+                log_info("Home %s is signed by foreign key we like, accepting.", hr->user_name);
+                *ret_signed_locally = false;
+                return 0;
+
+        case USER_RECORD_UNSIGNED:
+                sd_bus_error_setf(ret_error, BUS_ERROR_BAD_SIGNATURE, "User record %s is not signed at all, refusing.", hr->user_name);
+                return log_error_errno(SYNTHETIC_ERRNO(EPERM), "Home %s contains user record that is not signed at all, refusing.", hr->user_name);
+
+        case -ENOKEY:
+                sd_bus_error_setf(ret_error, BUS_ERROR_BAD_SIGNATURE, "User record %s is not signed by any known key, refusing.", hr->user_name);
+                return log_error_errno(is_signed, "Home %s contians user record that is not signed by any known key, refusing.", hr->user_name);
+
+        default:
+                assert(is_signed < 0);
+                return log_error_errno(is_signed, "Failed to verify signature on user record for %s, refusing fixation: %m", hr->user_name);
+        }
+}
+
+static int convert_worker_errno(Home *h, int e, sd_bus_error *error) {
+        /* Converts the error numbers the worker process returned into somewhat sensible dbus errors */
+
+        switch (e) {
+
+        case -EMSGSIZE:
+                return sd_bus_error_setf(error, BUS_ERROR_BAD_HOME_SIZE, "File systems of this type cannot shrinked");
+        case -ETXTBSY:
+                return sd_bus_error_setf(error, BUS_ERROR_BAD_HOME_SIZE, "File systems of this type can only be shrinked offline");
+        case -ERANGE:
+                return sd_bus_error_setf(error, BUS_ERROR_BAD_HOME_SIZE, "File system size too small");
+        case -ENOLINK:
+                return sd_bus_error_setf(error, SD_BUS_ERROR_NOT_SUPPORTED, "System does not support selected storage backend");
+        case -EPROTONOSUPPORT:
+                return sd_bus_error_setf(error, SD_BUS_ERROR_NOT_SUPPORTED, "System does not support selected file system");
+        case -ENOTTY:
+                return sd_bus_error_setf(error, SD_BUS_ERROR_NOT_SUPPORTED, "Operation not supported on storage backend");
+        case -ESOCKTNOSUPPORT:
+                return sd_bus_error_setf(error, SD_BUS_ERROR_NOT_SUPPORTED, "Operation not supported on file system");
+        case -ENOKEY:
+                return sd_bus_error_setf(error, BUS_ERROR_BAD_PASSWORD, "Password for home %s is incorrect or not sufficient for authentication.", h->user_name);
+        case -EBADSLT:
+                return sd_bus_error_setf(error, BUS_ERROR_BAD_PASSWORD_AND_NO_TOKEN, "Password for home %s is incorrect or not sufficient, and configured security token not found either.", h->user_name);
+        case -ENOANO:
+                return sd_bus_error_setf(error, BUS_ERROR_TOKEN_PIN_NEEDED, "PIN for security token required.");
+        case -ERFKILL:
+                return sd_bus_error_setf(error, BUS_ERROR_TOKEN_PROTECTED_AUTHENTICATION_PATH_NEEDED, "Security token requires protected authentication path.");
+        case -EOWNERDEAD:
+                return sd_bus_error_setf(error, BUS_ERROR_TOKEN_PIN_LOCKED, "PIN of security token locked.");
+        case -ENOLCK:
+                return sd_bus_error_setf(error, BUS_ERROR_TOKEN_BAD_PIN, "Bad PIN of security token.");
+        case -ETOOMANYREFS:
+                return sd_bus_error_setf(error, BUS_ERROR_TOKEN_BAD_PIN_FEW_TRIES_LEFT, "Bad PIN of security token, and only a few tries left.");
+        case -EUCLEAN:
+                return sd_bus_error_setf(error, BUS_ERROR_TOKEN_BAD_PIN_ONE_TRY_LEFT, "Bad PIN of security token, and only one try left.");
+        case -EBUSY:
+                return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "Home %s is currently being used, or an operation on home %s is currently being executed.", h->user_name, h->user_name);
+        case -ENOEXEC:
+                return sd_bus_error_setf(error, BUS_ERROR_HOME_NOT_ACTIVE, "Home %s is currently not active", h->user_name);
+        case -ENOSPC:
+                return sd_bus_error_setf(error, BUS_ERROR_NO_DISK_SPACE, "Not enough disk space for home %s", h->user_name);
+        }
+
+        return 0;
+}
+
+static void home_count_bad_authentication(Home *h, bool save) {
+        int r;
+
+        assert(h);
+
+        r = user_record_bad_authentication(h->record);
+        if (r < 0) {
+                log_warning_errno(r, "Failed to increase bad authentication counter, ignoring: %m");
+                return;
+        }
+
+        if (save) {
+                r = home_save_record(h);
+                if (r < 0)
+                        log_warning_errno(r, "Failed to write home record to disk, ignoring: %m");
+        }
+}
+
+static void home_fixate_finish(Home *h, int ret, UserRecord *hr) {
+        _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+        _cleanup_(user_record_unrefp) UserRecord *secret = NULL;
+        bool signed_locally;
+        int r;
+
+        assert(h);
+        assert(IN_SET(h->state, HOME_FIXATING, HOME_FIXATING_FOR_ACTIVATION, HOME_FIXATING_FOR_ACQUIRE));
+
+        secret = TAKE_PTR(h->secret); /* Take possession */
+
+        if (ret < 0) {
+                if (ret == -ENOKEY)
+                        (void) home_count_bad_authentication(h, false);
+
+                (void) convert_worker_errno(h, ret, &error);
+                r = log_error_errno(ret, "Fixation failed: %m");
+                goto fail;
+        }
+        if (!hr) {
+                r = log_error_errno(SYNTHETIC_ERRNO(EIO), "Did not receive user record from worker process, fixation failed.");
+                goto fail;
+        }
+
+        r = home_verify_user_record(h, hr, &signed_locally, &error);
+        if (r < 0)
+                goto fail;
+
+        r = home_set_record(h, hr);
+        if (r < 0) {
+                log_error_errno(r, "Failed to update home record: %m");
+                goto fail;
+        }
+
+        h->signed_locally = signed_locally;
+
+        /* When we finished fixating (and don't follow-up with activation), let's count this as good authentication */
+        if (h->state == HOME_FIXATING) {
+                r = user_record_good_authentication(h->record);
+                if (r < 0)
+                        log_warning_errno(r, "Failed to increase good authentication counter, ignoring: %m");
+        }
+
+        r = home_save_record(h);
+        if (r < 0)
+                log_warning_errno(r, "Failed to write home record to disk, ignoring: %m");
+
+        if (IN_SET(h->state, HOME_FIXATING_FOR_ACTIVATION, HOME_FIXATING_FOR_ACQUIRE)) {
+
+                r = home_start_work(h, "activate", h->record, secret);
+                if (r < 0) {
+                        h->current_operation = operation_result_unref(h->current_operation, r, NULL);
+                        home_set_state(h, _HOME_STATE_INVALID);
+                } else
+                        home_set_state(h, h->state == HOME_FIXATING_FOR_ACTIVATION ? HOME_ACTIVATING : HOME_ACTIVATING_FOR_ACQUIRE);
+
+                return;
+        }
+
+        log_debug("Fixation of %s completed.", h->user_name);
+
+        h->current_operation = operation_result_unref(h->current_operation, 0, NULL);
+
+        /* Reset the state to "invalid", which makes home_get_state() test if the image exists and returns
+         * HOME_ABSENT vs. HOME_INACTIVE as necessary. */
+        home_set_state(h, _HOME_STATE_INVALID);
+        return;
+
+fail:
+        /* If fixation fails, we stay in unfixated state! */
+        h->current_operation = operation_result_unref(h->current_operation, r, &error);
+        home_set_state(h, HOME_UNFIXATED);
+}
+
+static void home_activate_finish(Home *h, int ret, UserRecord *hr) {
+        _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+        int r;
+
+        assert(h);
+        assert(IN_SET(h->state, HOME_ACTIVATING, HOME_ACTIVATING_FOR_ACQUIRE));
+
+        if (ret < 0) {
+                if (ret == -ENOKEY)
+                        home_count_bad_authentication(h, true);
+
+                (void) convert_worker_errno(h, ret, &error);
+                r = log_error_errno(ret, "Activation failed: %m");
+                goto finish;
+        }
+
+        if (hr) {
+                bool signed_locally;
+
+                r = home_verify_user_record(h, hr, &signed_locally, &error);
+                if (r < 0)
+                        goto finish;
+
+                r = home_set_record(h, hr);
+                if (r < 0) {
+                        log_error_errno(r, "Failed to update home record, ignoring: %m");
+                        goto finish;
+                }
+
+                h->signed_locally = signed_locally;
+
+                r = user_record_good_authentication(h->record);
+                if (r < 0)
+                        log_warning_errno(r, "Failed to increase good authentication counter, ignoring: %m");
+
+                r = home_save_record(h);
+                if (r < 0)
+                        log_warning_errno(r, "Failed to write home record to disk, ignoring: %m");
+        }
+
+        log_debug("Activation of %s completed.", h->user_name);
+        r = 0;
+
+finish:
+        h->current_operation = operation_result_unref(h->current_operation, r, &error);
+        home_set_state(h, _HOME_STATE_INVALID);
+}
+
+static void home_deactivate_finish(Home *h, int ret, UserRecord *hr) {
+        _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+        int r;
+
+        assert(h);
+        assert(h->state == HOME_DEACTIVATING);
+        assert(!hr); /* We don't expect a record on this operation */
+
+        if (ret < 0) {
+                (void) convert_worker_errno(h, ret, &error);
+                r = log_error_errno(ret, "Deactivation of %s failed: %m", h->user_name);
+                goto finish;
+        }
+
+        log_debug("Deactivation of %s completed.", h->user_name);
+        r = 0;
+
+finish:
+        h->current_operation = operation_result_unref(h->current_operation, r, &error);
+        home_set_state(h, _HOME_STATE_INVALID);
+}
+
+static void home_remove_finish(Home *h, int ret, UserRecord *hr) {
+        _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+        Manager *m;
+        int r;
+
+        assert(h);
+        assert(h->state == HOME_REMOVING);
+        assert(!hr); /* We don't expect a record on this operation */
+
+        m = h->manager;
+
+        if (ret < 0 && ret != -EALREADY) {
+                (void) convert_worker_errno(h, ret, &error);
+                r = log_error_errno(ret, "Removing %s failed: %m", h->user_name);
+                goto fail;
+        }
+
+        /* For a couple of storage types we can't delete the actual data storage when called (such as LUKS on
+         * partitions like USB sticks, or so). Sometimes these storage locations are among those we normally
+         * automatically discover in /home or in udev. When such a home is deleted let's hence issue a rescan
+         * after completion, so that "unfixated" entries are rediscovered.  */
+        if (!IN_SET(user_record_test_image_path(h->record), USER_TEST_UNDEFINED, USER_TEST_ABSENT))
+                manager_enqueue_rescan(m);
+
+        /* The image is now removed from disk. Now also remove our stored record */
+        r = home_unlink_record(h);
+        if (r < 0) {
+                log_error_errno(r, "Removing record file failed: %m");
+                goto fail;
+        }
+
+        log_debug("Removal of %s completed.", h->user_name);
+        h->current_operation = operation_result_unref(h->current_operation, 0, NULL);
+
+        /* Unload this record from memory too now. */
+        h = home_free(h);
+        return;
+
+fail:
+        h->current_operation = operation_result_unref(h->current_operation, r, &error);
+        home_set_state(h, _HOME_STATE_INVALID);
+}
+
+static void home_create_finish(Home *h, int ret, UserRecord *hr) {
+        int r;
+
+        assert(h);
+        assert(h->state == HOME_CREATING);
+
+        if (ret < 0) {
+                _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+
+                (void) convert_worker_errno(h, ret, &error);
+                log_error_errno(ret, "Operation on %s failed: %m", h->user_name);
+                h->current_operation = operation_result_unref(h->current_operation, ret, &error);
+
+                if (h->unregister_on_failure) {
+                        (void) home_unlink_record(h);
+                        h = home_free(h);
+                        return;
+                }
+
+                home_set_state(h, _HOME_STATE_INVALID);
+                return;
+        }
+
+        if (hr) {
+                r = home_set_record(h, hr);
+                if (r < 0)
+                        log_warning_errno(r, "Failed to update home record, ignoring: %m");
+        }
+
+        r = home_save_record(h);
+        if (r < 0)
+                log_warning_errno(r, "Failed to save record to disk, ignoring: %m");
+
+        log_debug("Creation of %s completed.", h->user_name);
+
+        h->current_operation = operation_result_unref(h->current_operation, 0, NULL);
+        home_set_state(h, _HOME_STATE_INVALID);
+}
+
+static void home_change_finish(Home *h, int ret, UserRecord *hr) {
+        _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+        int r;
+
+        assert(h);
+
+        if (ret < 0) {
+                if (ret == -ENOKEY)
+                        (void) home_count_bad_authentication(h, true);
+
+                (void) convert_worker_errno(h, ret, &error);
+                r = log_error_errno(ret, "Change operation failed: %m");
+                goto finish;
+        }
+
+        if (hr) {
+                r = home_set_record(h, hr);
+                if (r < 0)
+                        log_warning_errno(r, "Failed to update home record, ignoring: %m");
+                else {
+                        r = user_record_good_authentication(h->record);
+                        if (r < 0)
+                                log_warning_errno(r, "Failed to increase good authentication counter, ignoring: %m");
+
+                        r = home_save_record(h);
+                        if (r < 0)
+                                log_warning_errno(r, "Failed to write home record to disk, ignoring: %m");
+                }
+        }
+
+        log_debug("Change operation of %s completed.", h->user_name);
+        r = 0;
+
+finish:
+        h->current_operation = operation_result_unref(h->current_operation, r, &error);
+        home_set_state(h, _HOME_STATE_INVALID);
+}
+
+static void home_locking_finish(Home *h, int ret, UserRecord *hr) {
+        _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+        int r;
+
+        assert(h);
+        assert(h->state == HOME_LOCKING);
+
+        if (ret < 0) {
+                (void) convert_worker_errno(h, ret, &error);
+                r = log_error_errno(ret, "Locking operation failed: %m");
+                goto finish;
+        }
+
+        log_debug("Locking operation of %s completed.", h->user_name);
+        h->current_operation = operation_result_unref(h->current_operation, 0, NULL);
+        home_set_state(h, HOME_LOCKED);
+        return;
+
+finish:
+        /* If a specific home doesn't know the concept of locking, then that's totally OK, don't propagate
+         * the error if we are executing a LockAllHomes() operation. */
+
+        if (h->current_operation->type == OPERATION_LOCK_ALL && r == -ENOTTY)
+                h->current_operation = operation_result_unref(h->current_operation, 0, NULL);
+        else
+                h->current_operation = operation_result_unref(h->current_operation, r, &error);
+
+        home_set_state(h, _HOME_STATE_INVALID);
+}
+
+static void home_unlocking_finish(Home *h, int ret, UserRecord *hr) {
+        _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+        int r;
+
+        assert(h);
+        assert(IN_SET(h->state, HOME_UNLOCKING, HOME_UNLOCKING_FOR_ACQUIRE));
+
+        if (ret < 0) {
+                if (ret == -ENOKEY)
+                        (void) home_count_bad_authentication(h, true);
+
+                (void) convert_worker_errno(h, ret, &error);
+                r = log_error_errno(ret, "Unlocking operation failed: %m");
+
+                /* Revert to locked state */
+                home_set_state(h, HOME_LOCKED);
+                h->current_operation = operation_result_unref(h->current_operation, r, &error);
+                return;
+        }
+
+        r = user_record_good_authentication(h->record);
+        if (r < 0)
+                log_warning_errno(r, "Failed to increase good authentication counter, ignoring: %m");
+        else {
+                r = home_save_record(h);
+                if (r < 0)
+                        log_warning_errno(r, "Failed to write home record to disk, ignoring: %m");
+        }
+
+        log_debug("Unlocking operation of %s completed.", h->user_name);
+
+        h->current_operation = operation_result_unref(h->current_operation, r, &error);
+        home_set_state(h, _HOME_STATE_INVALID);
+        return;
+}
+
+static void home_authenticating_finish(Home *h, int ret, UserRecord *hr) {
+        _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+        int r;
+
+        assert(h);
+        assert(IN_SET(h->state, HOME_AUTHENTICATING, HOME_AUTHENTICATING_WHILE_ACTIVE, HOME_AUTHENTICATING_FOR_ACQUIRE));
+
+        if (ret < 0) {
+                if (ret == -ENOKEY)
+                        (void) home_count_bad_authentication(h, true);
+
+                (void) convert_worker_errno(h, ret, &error);
+                r = log_error_errno(ret, "Authentication failed: %m");
+                goto finish;
+        }
+
+        if (hr) {
+                r = home_set_record(h, hr);
+                if (r < 0)
+                        log_warning_errno(r, "Failed to update home record, ignoring: %m");
+                else {
+                        r = user_record_good_authentication(h->record);
+                        if (r < 0)
+                                log_warning_errno(r, "Failed to increase good authentication counter, ignoring: %m");
+
+                        r = home_save_record(h);
+                        if (r < 0)
+                                log_warning_errno(r, "Failed to write home record to disk, ignoring: %m");
+                }
+        }
+
+        log_debug("Authentication of %s completed.", h->user_name);
+        r = 0;
+
+finish:
+        h->current_operation = operation_result_unref(h->current_operation, r, &error);
+        home_set_state(h, _HOME_STATE_INVALID);
+}
+
+static int home_on_worker_process(sd_event_source *s, const siginfo_t *si, void *userdata) {
+        _cleanup_(user_record_unrefp) UserRecord *hr = NULL;
+        Home *h = userdata;
+        int ret;
+
+        assert(s);
+        assert(si);
+        assert(h);
+
+        assert(h->worker_pid == si->si_pid);
+        assert(h->worker_event_source);
+        assert(h->worker_stdout_fd >= 0);
+
+        (void) hashmap_remove_value(h->manager->homes_by_worker_pid, PID_TO_PTR(h->worker_pid), h);
+
+        h->worker_pid = 0;
+        h->worker_event_source = sd_event_source_unref(h->worker_event_source);
+
+        if (si->si_code != CLD_EXITED) {
+                assert(IN_SET(si->si_code, CLD_KILLED, CLD_DUMPED));
+                ret = log_debug_errno(SYNTHETIC_ERRNO(EPROTO), "Worker process died abnormally with signal %s.", signal_to_string(si->si_status));
+        } else if (si->si_status != EXIT_SUCCESS) {
+                /* If we received an error code via sd_notify(), use it */
+                if (h->worker_error_code != 0)
+                        ret = log_debug_errno(h->worker_error_code, "Worker reported error code %s.", errno_to_name(h->worker_error_code));
+                else
+                        ret = log_debug_errno(SYNTHETIC_ERRNO(EPROTO), "Worker exited with exit code %i.", si->si_status);
+        } else
+                ret = home_parse_worker_stdout(TAKE_FD(h->worker_stdout_fd), &hr);
+
+        h->worker_stdout_fd = safe_close(h->worker_stdout_fd);
+
+        switch (h->state) {
+
+        case HOME_FIXATING:
+        case HOME_FIXATING_FOR_ACTIVATION:
+        case HOME_FIXATING_FOR_ACQUIRE:
+                home_fixate_finish(h, ret, hr);
+                break;
+
+        case HOME_ACTIVATING:
+        case HOME_ACTIVATING_FOR_ACQUIRE:
+                home_activate_finish(h, ret, hr);
+                break;
+
+        case HOME_DEACTIVATING:
+                home_deactivate_finish(h, ret, hr);
+                break;
+
+        case HOME_LOCKING:
+                home_locking_finish(h, ret, hr);
+                break;
+
+        case HOME_UNLOCKING:
+        case HOME_UNLOCKING_FOR_ACQUIRE:
+                home_unlocking_finish(h, ret, hr);
+                break;
+
+        case HOME_CREATING:
+                home_create_finish(h, ret, hr);
+                break;
+
+        case HOME_REMOVING:
+                home_remove_finish(h, ret, hr);
+                break;
+
+        case HOME_UPDATING:
+        case HOME_UPDATING_WHILE_ACTIVE:
+        case HOME_RESIZING:
+        case HOME_RESIZING_WHILE_ACTIVE:
+        case HOME_PASSWD:
+        case HOME_PASSWD_WHILE_ACTIVE:
+                home_change_finish(h, ret, hr);
+                break;
+
+        case HOME_AUTHENTICATING:
+        case HOME_AUTHENTICATING_WHILE_ACTIVE:
+        case HOME_AUTHENTICATING_FOR_ACQUIRE:
+                home_authenticating_finish(h, ret, hr);
+                break;
+
+        default:
+                assert_not_reached("Unexpected state after worker exited");
+        }
+
+        return 0;
+}
+
+static int home_start_work(Home *h, const char *verb, UserRecord *hr, UserRecord *secret) {
+        _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+        _cleanup_(erase_and_freep) char *formatted = NULL;
+        _cleanup_close_ int stdin_fd = -1, stdout_fd = -1;
+        pid_t pid = 0;
+        int r;
+
+        assert(h);
+        assert(verb);
+        assert(hr);
+
+        if (h->worker_pid != 0)
+                return -EBUSY;
+
+        assert(h->worker_stdout_fd < 0);
+        assert(!h->worker_event_source);
+
+        v = json_variant_ref(hr->json);
+
+        if (secret) {
+                JsonVariant *sub = NULL;
+
+                sub = json_variant_by_key(secret->json, "secret");
+                if (!sub)
+                        return -ENOKEY;
+
+                r = json_variant_set_field(&v, "secret", sub);
+                if (r < 0)
+                        return r;
+        }
+
+        r = json_variant_format(v, 0, &formatted);
+        if (r < 0)
+                return r;
+
+        stdin_fd = acquire_data_fd(formatted, strlen(formatted), 0);
+        if (stdin_fd < 0)
+                return stdin_fd;
+
+        log_debug("Sending to worker: %s", formatted);
+
+        stdout_fd = memfd_create("homework-stdout", MFD_CLOEXEC);
+        if (stdout_fd < 0)
+                return -errno;
+
+        r = safe_fork_full("(sd-homework)",
+                           (int[]) { stdin_fd, stdout_fd }, 2,
+                           FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG|FORK_LOG, &pid);
+        if (r < 0)
+                return r;
+        if (r == 0) {
+                /* Child */
+
+                if (setenv("NOTIFY_SOCKET", "/run/systemd/home/notify", 1) < 0) {
+                        log_error_errno(errno, "Failed to set $NOTIFY_SOCKET: %m");
+                        _exit(EXIT_FAILURE);
+                }
+
+                r = rearrange_stdio(stdin_fd, stdout_fd, STDERR_FILENO);
+                if (r < 0) {
+                        log_error_errno(r, "Failed to rearrange stdin/stdout/stderr: %m");
+                        _exit(EXIT_FAILURE);
+                }
+
+                stdin_fd = stdout_fd = -1; /* have been invalidated by rearrange_stdio() */
+
+                execl(SYSTEMD_HOMEWORK_PATH, SYSTEMD_HOMEWORK_PATH, verb, NULL);
+                log_error_errno(errno, "Failed to invoke " SYSTEMD_HOMEWORK_PATH ": %m");
+                _exit(EXIT_FAILURE);
+        }
+
+        r = sd_event_add_child(h->manager->event, &h->worker_event_source, pid, WEXITED, home_on_worker_process, h);
+        if (r < 0)
+                return r;
+
+        (void) sd_event_source_set_description(h->worker_event_source, "worker");
+
+        r = hashmap_put(h->manager->homes_by_worker_pid, PID_TO_PTR(pid), h);
+        if (r < 0) {
+                h->worker_event_source = sd_event_source_unref(h->worker_event_source);
+                return r;
+        }
+
+        h->worker_stdout_fd = TAKE_FD(stdout_fd);
+        h->worker_pid = pid;
+        h->worker_error_code = 0;
+
+        return 0;
+}
+
+static int home_ratelimit(Home *h, sd_bus_error *error) {
+        int r, ret;
+
+        assert(h);
+
+        ret = user_record_ratelimit(h->record);
+        if (ret < 0)
+                return ret;
+
+        if (h->state != HOME_UNFIXATED) {
+                r = home_save_record(h);
+                if (r < 0)
+                        log_warning_errno(r, "Failed to save updated record, ignoring: %m");
+        }
+
+        if (ret == 0) {
+                char buf[FORMAT_TIMESPAN_MAX];
+                usec_t t, n;
+
+                n = now(CLOCK_REALTIME);
+                t = user_record_ratelimit_next_try(h->record);
+
+                if (t != USEC_INFINITY && t > n)
+                        return sd_bus_error_setf(error, BUS_ERROR_AUTHENTICATION_LIMIT_HIT, "Too many login attempts, please try again in %s!",
+                                                 format_timespan(buf, sizeof(buf), t - n, USEC_PER_SEC));
+
+                return sd_bus_error_setf(error, BUS_ERROR_AUTHENTICATION_LIMIT_HIT, "Too many login attempts, please try again later.");
+        }
+
+        return 0;
+}
+
+static int home_fixate_internal(
+                Home *h,
+                UserRecord *secret,
+                HomeState for_state,
+                sd_bus_error *error) {
+
+        int r;
+
+        assert(h);
+        assert(IN_SET(for_state, HOME_FIXATING, HOME_FIXATING_FOR_ACTIVATION, HOME_FIXATING_FOR_ACQUIRE));
+
+        r = home_start_work(h, "inspect", h->record, secret);
+        if (r < 0)
+                return r;
+
+        if (for_state == HOME_FIXATING_FOR_ACTIVATION) {
+                /* Remember the secret data, since we need it for the activation again, later on. */
+                user_record_unref(h->secret);
+                h->secret = user_record_ref(secret);
+        }
+
+        home_set_state(h, for_state);
+        return 0;
+}
+
+int home_fixate(Home *h, UserRecord *secret, sd_bus_error *error) {
+        int r;
+
+        assert(h);
+
+        switch (home_get_state(h)) {
+        case HOME_ABSENT:
+                return sd_bus_error_setf(error, BUS_ERROR_HOME_ABSENT, "Home %s is currently missing or not plugged in.", h->user_name);
+        case HOME_INACTIVE:
+        case HOME_ACTIVE:
+        case HOME_LOCKED:
+                return sd_bus_error_setf(error, BUS_ERROR_HOME_ALREADY_FIXATED, "Home %s is already fixated.", h->user_name);
+        case HOME_UNFIXATED:
+                break;
+        default:
+                return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "An operation on home %s is currently being executed.", h->user_name);
+        }
+
+        r = home_ratelimit(h, error);
+        if (r < 0)
+                return r;
+
+        return home_fixate_internal(h, secret, HOME_FIXATING, error);
+}
+
+static int home_activate_internal(Home *h, UserRecord *secret, HomeState for_state, sd_bus_error *error) {
+        int r;
+
+        assert(h);
+        assert(IN_SET(for_state, HOME_ACTIVATING, HOME_ACTIVATING_FOR_ACQUIRE));
+
+        r = home_start_work(h, "activate", h->record, secret);
+        if (r < 0)
+                return r;
+
+        home_set_state(h, for_state);
+        return 0;
+}
+
+int home_activate(Home *h, UserRecord *secret, sd_bus_error *error) {
+        int r;
+
+        assert(h);
+
+        switch (home_get_state(h)) {
+        case HOME_UNFIXATED:
+                return home_fixate_internal(h, secret, HOME_FIXATING_FOR_ACTIVATION, error);
+        case HOME_ABSENT:
+                return sd_bus_error_setf(error, BUS_ERROR_HOME_ABSENT, "Home %s is currently missing or not plugged in.", h->user_name);
+        case HOME_ACTIVE:
+                return sd_bus_error_setf(error, BUS_ERROR_HOME_ALREADY_ACTIVE, "Home %s is already active.", h->user_name);
+        case HOME_LOCKED:
+                return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name);
+        case HOME_INACTIVE:
+                break;
+        default:
+                return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "An operation on home %s is currently being executed.", h->user_name);
+        }
+
+        r = home_ratelimit(h, error);
+        if (r < 0)
+                return r;
+
+        return home_activate_internal(h, secret, HOME_ACTIVATING, error);
+}
+
+static int home_authenticate_internal(Home *h, UserRecord *secret, HomeState for_state, sd_bus_error *error) {
+        int r;
+
+        assert(h);
+        assert(IN_SET(for_state, HOME_AUTHENTICATING, HOME_AUTHENTICATING_WHILE_ACTIVE, HOME_AUTHENTICATING_FOR_ACQUIRE));
+
+        r = home_start_work(h, "inspect", h->record, secret);
+        if (r < 0)
+                return r;
+
+        home_set_state(h, for_state);
+        return 0;
+}
+
+int home_authenticate(Home *h, UserRecord *secret, sd_bus_error *error) {
+        HomeState state;
+        int r;
+
+        assert(h);
+
+        state = home_get_state(h);
+        switch (state) {
+        case HOME_ABSENT:
+                return sd_bus_error_setf(error, BUS_ERROR_HOME_ABSENT, "Home %s is currently missing or not plugged in.", h->user_name);
+        case HOME_LOCKED:
+                return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name);
+        case HOME_UNFIXATED:
+        case HOME_INACTIVE:
+        case HOME_ACTIVE:
+                break;
+        default:
+                return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "An operation on home %s is currently being executed.", h->user_name);
+        }
+
+        r = home_ratelimit(h, error);
+        if (r < 0)
+                return r;
+
+        return home_authenticate_internal(h, secret, state == HOME_ACTIVE ? HOME_AUTHENTICATING_WHILE_ACTIVE : HOME_AUTHENTICATING, error);
+}
+
+static int home_deactivate_internal(Home *h, bool force, sd_bus_error *error) {
+        int r;
+
+        assert(h);
+
+        r = home_start_work(h, force ? "deactivate-force" : "deactivate", h->record, NULL);
+        if (r < 0)
+                return r;
+
+        home_set_state(h, HOME_DEACTIVATING);
+        return 0;
+}
+
+int home_deactivate(Home *h, bool force, sd_bus_error *error) {
+        assert(h);
+
+        switch (home_get_state(h)) {
+        case HOME_UNFIXATED:
+        case HOME_ABSENT:
+        case HOME_INACTIVE:
+                return sd_bus_error_setf(error, BUS_ERROR_HOME_NOT_ACTIVE, "Home %s not active.", h->user_name);
+        case HOME_LOCKED:
+                return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name);
+        case HOME_ACTIVE:
+                break;
+        default:
+                return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "An operation on home %s is currently being executed.", h->user_name);
+        }
+
+        return home_deactivate_internal(h, force, error);
+}
+
+int home_create(Home *h, UserRecord *secret, sd_bus_error *error) {
+        int r;
+
+        assert(h);
+
+        switch (home_get_state(h)) {
+        case HOME_INACTIVE:
+                if (h->record->storage < 0)
+                        break; /* if no storage is defined we don't know what precisely to look for, hence
+                                * HOME_INACTIVE is OK in that case too. */
+
+                if (IN_SET(user_record_test_image_path(h->record), USER_TEST_MAYBE, USER_TEST_UNDEFINED))
+                        break; /* And if the image path test isn't conclusive, let's also go on */
+
+                _fallthrough_;
+        case HOME_UNFIXATED:
+                return sd_bus_error_setf(error, BUS_ERROR_HOME_EXISTS, "Home of user %s already exists.", h->user_name);
+        case HOME_ABSENT:
+                break;
+        case HOME_ACTIVE:
+        case HOME_LOCKED:
+        default:
+                return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "Home %s is currently being used, or an operation on home %s is currently being executed.", h->user_name, h->user_name);
+        }
+
+        if (h->record->enforce_password_policy == false)
+                log_debug("Password quality check turned off for account, skipping.");
+        else {
+                r = quality_check_password(h->record, secret, error);
+                if (r < 0)
+                        return r;
+        }
+
+        r = home_start_work(h, "create", h->record, secret);
+        if (r < 0)
+                return r;
+
+        home_set_state(h, HOME_CREATING);
+        return 0;
+}
+
+int home_remove(Home *h, sd_bus_error *error) {
+        HomeState state;
+        int r;
+
+        assert(h);
+
+        state = home_get_state(h);
+        switch (state) {
+        case HOME_ABSENT: /* If the home directory is absent, then this is just like unregistering */
+                return home_unregister(h, error);
+        case HOME_LOCKED:
+                return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name);
+        case HOME_UNFIXATED:
+        case HOME_INACTIVE:
+                break;
+        case HOME_ACTIVE:
+        default:
+                return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "Home %s is currently being used, or an operation on home %s is currently being executed.", h->user_name, h->user_name);
+        }
+
+        r = home_start_work(h, "remove", h->record, NULL);
+        if (r < 0)
+                return r;
+
+        home_set_state(h, HOME_REMOVING);
+        return 0;
+}
+
+static int user_record_extend_with_binding(UserRecord *hr, UserRecord *with_binding, UserRecordLoadFlags flags, UserRecord **ret) {
+        _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+        _cleanup_(user_record_unrefp) UserRecord *nr = NULL;
+        JsonVariant *binding;
+        int r;
+
+        assert(hr);
+        assert(with_binding);
+        assert(ret);
+
+        assert_se(v = json_variant_ref(hr->json));
+
+        binding = json_variant_by_key(with_binding->json, "binding");
+        if (binding) {
+                r = json_variant_set_field(&v, "binding", binding);
+                if (r < 0)
+                        return r;
+        }
+
+        nr = user_record_new();
+        if (!nr)
+                return -ENOMEM;
+
+        r = user_record_load(nr, v, flags);
+        if (r < 0)
+                return r;
+
+        *ret = TAKE_PTR(nr);
+        return 0;
+}
+
+static int home_update_internal(Home *h, const char *verb, UserRecord *hr, UserRecord *secret, sd_bus_error *error) {
+        _cleanup_(user_record_unrefp) UserRecord *new_hr = NULL, *saved_secret = NULL, *signed_hr = NULL;
+        int r, c;
+
+        assert(h);
+        assert(verb);
+        assert(hr);
+
+        if (!user_record_compatible(hr, h->record))
+                return sd_bus_error_setf(error, BUS_ERROR_HOME_RECORD_MISMATCH, "Updated user record is not compatible with existing one.");
+        c = user_record_compare_last_change(hr, h->record); /* refuse downgrades */
+        if (c < 0)
+                return sd_bus_error_setf(error, BUS_ERROR_HOME_RECORD_DOWNGRADE, "Refusing to update to older home record.");
+
+        if (!secret && FLAGS_SET(hr->mask, USER_RECORD_SECRET)) {
+                r = user_record_clone(hr, USER_RECORD_EXTRACT_SECRET, &saved_secret);
+                if (r < 0)
+                        return r;
+
+                secret = saved_secret;
+        }
+
+        r = manager_verify_user_record(h->manager, hr);
+        switch (r) {
+
+        case USER_RECORD_UNSIGNED:
+                if (h->signed_locally <= 0) /* If the existing record is not owned by us, don't accept an
+                                             * unsigned new record. i.e. only implicitly sign new records
+                                             * that where previously signed by us too. */
+                        return sd_bus_error_setf(error, BUS_ERROR_HOME_RECORD_SIGNED, "Home %s is signed and cannot be modified locally.", h->user_name);
+
+                /* The updated record is not signed, then do so now */
+                r = manager_sign_user_record(h->manager, hr, &signed_hr, error);
+                if (r < 0)
+                        return r;
+
+                hr = signed_hr;
+                break;
+
+        case USER_RECORD_SIGNED_EXCLUSIVE:
+        case USER_RECORD_SIGNED:
+        case USER_RECORD_FOREIGN:
+                /* Has already been signed. Great! */
+                break;
+
+        case -ENOKEY:
+        default:
+                return r;
+        }
+
+        r = user_record_extend_with_binding(hr, h->record, USER_RECORD_LOAD_MASK_SECRET, &new_hr);
+        if (r < 0)
+                return r;
+
+        if (c == 0) {
+                /* different payload but same lastChangeUSec field? That's not cool! */
+
+                r = user_record_masked_equal(new_hr, h->record, USER_RECORD_REGULAR|USER_RECORD_PRIVILEGED|USER_RECORD_PER_MACHINE);
+                if (r < 0)
+                        return r;
+                if (r == 0)
+                        return sd_bus_error_setf(error, BUS_ERROR_HOME_RECORD_MISMATCH, "Home record different but timestamp remained the same, refusing.");
+        }
+
+        r = home_start_work(h, verb, new_hr, secret);
+        if (r < 0)
+                return r;
+
+        return 0;
+}
+
+int home_update(Home *h, UserRecord *hr, sd_bus_error *error) {
+        HomeState state;
+        int r;
+
+        assert(h);
+        assert(hr);
+
+        state = home_get_state(h);
+        switch (state) {
+        case HOME_UNFIXATED:
+                return sd_bus_error_setf(error, BUS_ERROR_HOME_UNFIXATED, "Home %s has not been fixated yet.", h->user_name);
+        case HOME_ABSENT:
+                return sd_bus_error_setf(error, BUS_ERROR_HOME_ABSENT, "Home %s is currently missing or not plugged in.", h->user_name);
+        case HOME_LOCKED:
+                return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name);
+        case HOME_INACTIVE:
+        case HOME_ACTIVE:
+                break;
+        default:
+                return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "An operation on home %s is currently being executed.", h->user_name);
+        }
+
+        r = home_ratelimit(h, error);
+        if (r < 0)
+                return r;
+
+        r = home_update_internal(h, "update", hr, NULL, error);
+        if (r < 0)
+                return r;
+
+        home_set_state(h, state == HOME_ACTIVE ? HOME_UPDATING_WHILE_ACTIVE : HOME_UPDATING);
+        return 0;
+}
+
+int home_resize(Home *h, uint64_t disk_size, UserRecord *secret, sd_bus_error *error) {
+        _cleanup_(user_record_unrefp) UserRecord *c = NULL;
+        HomeState state;
+        int r;
+
+        assert(h);
+
+        state = home_get_state(h);
+        switch (state) {
+        case HOME_UNFIXATED:
+                return sd_bus_error_setf(error, BUS_ERROR_HOME_UNFIXATED, "Home %s has not been fixated yet.", h->user_name);
+        case HOME_ABSENT:
+                return sd_bus_error_setf(error, BUS_ERROR_HOME_ABSENT, "Home %s is currently missing or not plugged in.", h->user_name);
+        case HOME_LOCKED:
+                return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name);
+        case HOME_INACTIVE:
+        case HOME_ACTIVE:
+                break;
+        default:
+                return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "An operation on home %s is currently being executed.", h->user_name);
+        }
+
+        r = home_ratelimit(h, error);
+        if (r < 0)
+                return r;
+
+        if (disk_size == UINT64_MAX || disk_size == h->record->disk_size) {
+                if (h->record->disk_size == UINT64_MAX)
+                        return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Not disk size to resize to specified.");
+
+                c = user_record_ref(h->record); /* Shortcut if size is unspecified or matches the record */
+        } else {
+                _cleanup_(user_record_unrefp) UserRecord *signed_c = NULL;
+
+                if (h->signed_locally <= 0) /* Don't allow changing of records not signed only by us */
+                        return sd_bus_error_setf(error, BUS_ERROR_HOME_RECORD_SIGNED, "Home %s is signed and cannot be modified locally.", h->user_name);
+
+                r = user_record_clone(h->record, USER_RECORD_LOAD_REFUSE_SECRET, &c);
+                if (r < 0)
+                        return r;
+
+                r = user_record_set_disk_size(c, disk_size);
+                if (r == -ERANGE)
+                        return sd_bus_error_setf(error, BUS_ERROR_BAD_HOME_SIZE, "Requested size for home %s out of acceptable range.", h->user_name);
+                if (r < 0)
+                        return r;
+
+                r = user_record_update_last_changed(c, false);
+                if (r == -ECHRNG)
+                        return sd_bus_error_setf(error, BUS_ERROR_HOME_RECORD_MISMATCH, "Record last change time of %s is newer than current time, cannot update.", h->user_name);
+                if (r < 0)
+                        return r;
+
+                r = manager_sign_user_record(h->manager, c, &signed_c, error);
+                if (r < 0)
+                        return r;
+
+                user_record_unref(c);
+                c = TAKE_PTR(signed_c);
+        }
+
+        r = home_update_internal(h, "resize", c, secret, error);
+        if (r < 0)
+                return r;
+
+        home_set_state(h, state == HOME_ACTIVE ? HOME_RESIZING_WHILE_ACTIVE : HOME_RESIZING);
+        return 0;
+}
+
+static int home_may_change_password(
+                Home *h,
+                sd_bus_error *error) {
+
+        int r;
+
+        assert(h);
+
+        r = user_record_test_password_change_required(h->record);
+        if (IN_SET(r, -EKEYREVOKED, -EOWNERDEAD, -EKEYEXPIRED))
+                return 0; /* expired in some form, but chaning is allowed */
+        if (IN_SET(r, -EKEYREJECTED, -EROFS))
+                return sd_bus_error_setf(error, SD_BUS_ERROR_ACCESS_DENIED, "Expiration settings of account %s do not allow changing of password.", h->user_name);
+        if (r < 0)
+                return log_error_errno(r, "Failed to test password expiry: %m");
+
+        return 0; /* not expired */
+}
+
+int home_passwd(Home *h,
+                UserRecord *new_secret,
+                UserRecord *old_secret,
+                sd_bus_error *error) {
+
+        _cleanup_(user_record_unrefp) UserRecord *c = NULL, *merged_secret = NULL, *signed_c = NULL;
+        HomeState state;
+        int r;
+
+        assert(h);
+
+        if (h->signed_locally <= 0) /* Don't allow changing of records not signed only by us */
+                return sd_bus_error_setf(error, BUS_ERROR_HOME_RECORD_SIGNED, "Home %s is signed and cannot be modified locally.", h->user_name);
+
+        state = home_get_state(h);
+        switch (state) {
+        case HOME_UNFIXATED:
+                return sd_bus_error_setf(error, BUS_ERROR_HOME_UNFIXATED, "Home %s has not been fixated yet.", h->user_name);
+        case HOME_ABSENT:
+                return sd_bus_error_setf(error, BUS_ERROR_HOME_ABSENT, "Home %s is currently missing or not plugged in.", h->user_name);
+        case HOME_LOCKED:
+                return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name);
+        case HOME_INACTIVE:
+        case HOME_ACTIVE:
+                break;
+        default:
+                return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "An operation on home %s is currently being executed.", h->user_name);
+        }
+
+        r = home_ratelimit(h, error);
+        if (r < 0)
+                return r;
+
+        r = home_may_change_password(h, error);
+        if (r < 0)
+                return r;
+
+        r = user_record_clone(h->record, USER_RECORD_LOAD_REFUSE_SECRET, &c);
+        if (r < 0)
+                return r;
+
+        merged_secret = user_record_new();
+        if (!merged_secret)
+                return -ENOMEM;
+
+        r = user_record_merge_secret(merged_secret, old_secret);
+        if (r < 0)
+                return r;
+
+        r = user_record_merge_secret(merged_secret, new_secret);
+        if (r < 0)
+                return r;
+
+        if (!strv_isempty(new_secret->password)) {
+                /* Update the password only if one is specified, otherwise let's just reuse the old password
+                 * data. This is useful as a way to propagate updated user records into the LUKS backends
+                 * properly. */
+
+                r = user_record_make_hashed_password(c, new_secret->password, /* extend = */ false);
+                if (r < 0)
+                        return r;
+
+                r = user_record_set_password_change_now(c, -1 /* remove */);
+                if (r < 0)
+                        return r;
+        }
+
+        r = user_record_update_last_changed(c, true);
+        if (r == -ECHRNG)
+                return sd_bus_error_setf(error, BUS_ERROR_HOME_RECORD_MISMATCH, "Record last change time of %s is newer than current time, cannot update.", h->user_name);
+        if (r < 0)
+                return r;
+
+        r = manager_sign_user_record(h->manager, c, &signed_c, error);
+        if (r < 0)
+                return r;
+
+        if (c->enforce_password_policy == false)
+                log_debug("Password quality check turned off for account, skipping.");
+        else {
+                r = quality_check_password(c, merged_secret, error);
+                if (r < 0)
+                        return r;
+        }
+
+        r = home_update_internal(h, "passwd", signed_c, merged_secret, error);
+        if (r < 0)
+                return r;
+
+        home_set_state(h, state == HOME_ACTIVE ? HOME_PASSWD_WHILE_ACTIVE : HOME_PASSWD);
+        return 0;
+}
+
+int home_unregister(Home *h, sd_bus_error *error) {
+        int r;
+
+        assert(h);
+
+        switch (home_get_state(h)) {
+        case HOME_UNFIXATED:
+                return sd_bus_error_setf(error, BUS_ERROR_HOME_UNFIXATED, "Home %s is not registered.", h->user_name);
+        case HOME_LOCKED:
+                return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name);
+        case HOME_ABSENT:
+        case HOME_INACTIVE:
+                break;
+        case HOME_ACTIVE:
+        default:
+                return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "Home %s is currently being used, or an operation on home %s is currently being executed.", h->user_name, h->user_name);
+        }
+
+        r = home_unlink_record(h);
+        if (r < 0)
+                return r;
+
+        /* And destroy the whole entry. The caller needs to be prepared for that. */
+        h = home_free(h);
+        return 1;
+}
+
+int home_lock(Home *h, sd_bus_error *error) {
+        int r;
+
+        assert(h);
+
+        switch (home_get_state(h)) {
+        case HOME_UNFIXATED:
+        case HOME_ABSENT:
+        case HOME_INACTIVE:
+                return sd_bus_error_setf(error, BUS_ERROR_HOME_NOT_ACTIVE, "Home %s is not active.", h->user_name);
+        case HOME_LOCKED:
+                return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is already locked.", h->user_name);
+        case HOME_ACTIVE:
+                break;
+        default:
+                return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "An operation on home %s is currently being executed.", h->user_name);
+        }
+
+        r = home_start_work(h, "lock", h->record, NULL);
+        if (r < 0)
+                return r;
+
+        home_set_state(h, HOME_LOCKING);
+        return 0;
+}
+
+static int home_unlock_internal(Home *h, UserRecord *secret, HomeState for_state, sd_bus_error *error) {
+        int r;
+
+        assert(h);
+        assert(IN_SET(for_state, HOME_UNLOCKING, HOME_UNLOCKING_FOR_ACQUIRE));
+
+        r = home_start_work(h, "unlock", h->record, secret);
+        if (r < 0)
+                return r;
+
+        home_set_state(h, for_state);
+        return 0;
+}
+
+int home_unlock(Home *h, UserRecord *secret, sd_bus_error *error) {
+        int r;
+        assert(h);
+
+        r = home_ratelimit(h, error);
+        if (r < 0)
+                return r;
+
+        switch (home_get_state(h)) {
+        case HOME_UNFIXATED:
+        case HOME_ABSENT:
+        case HOME_INACTIVE:
+        case HOME_ACTIVE:
+                return sd_bus_error_setf(error, BUS_ERROR_HOME_NOT_LOCKED, "Home %s is not locked.", h->user_name);
+        case HOME_LOCKED:
+                break;
+        default:
+                return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "An operation on home %s is currently being executed.", h->user_name);
+        }
+
+        return home_unlock_internal(h, secret, HOME_UNLOCKING, error);
+}
+
+HomeState home_get_state(Home *h) {
+        assert(h);
+
+        /* When the state field is initialized, it counts. */
+        if (h->state >= 0)
+                return h->state;
+
+        /* Otherwise, let's see if the home directory is mounted. If so, we assume for sure the home
+         * directory is active */
+        if (user_record_test_home_directory(h->record) == USER_TEST_MOUNTED)
+                return HOME_ACTIVE;
+
+        /* And if we see the image being gone, we report this as absent */
+        if (user_record_test_image_path(h->record) == USER_TEST_ABSENT)
+                return HOME_ABSENT;
+
+        /* And for all other cases we return "inactive". */
+        return HOME_INACTIVE;
+}
+
+void home_process_notify(Home *h, char **l) {
+        const char *e;
+        int error;
+        int r;
+
+        assert(h);
+
+        e = strv_env_get(l, "ERRNO");
+        if (!e) {
+                log_debug("Got notify message lacking ERRNO= field, ignoring.");
+                return;
+        }
+
+        r = safe_atoi(e, &error);
+        if (r < 0) {
+                log_debug_errno(r, "Failed to parse receieved error number, ignoring: %s", e);
+                return;
+        }
+        if (error <= 0) {
+                log_debug("Error number is out of range: %i", error);
+                return;
+        }
+
+        h->worker_error_code = error;
+}
+
+int home_killall(Home *h) {
+        _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+        _cleanup_free_ char *unit = NULL;
+        int r;
+
+        assert(h);
+
+        if (!uid_is_valid(h->uid))
+                return 0;
+
+        assert(h->uid > 0); /* We never should be UID 0 */
+
+        /* Let's kill everything matching the specified UID */
+        r = safe_fork("(sd-killer)", FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG|FORK_WAIT|FORK_LOG, NULL);
+        if (r < 0)
+                return r;
+        if (r == 0) {
+                gid_t gid;
+
+                /* Child */
+
+                gid = user_record_gid(h->record);
+                if (setresgid(gid, gid, gid) < 0) {
+                        log_error_errno(errno, "Failed to change GID to " GID_FMT ": %m", gid);
+                        _exit(EXIT_FAILURE);
+                }
+
+                if (setgroups(0, NULL) < 0) {
+                        log_error_errno(errno, "Failed to reset auxiliary groups list: %m");
+                        _exit(EXIT_FAILURE);
+                }
+
+                if (setresuid(h->uid, h->uid, h->uid) < 0) {
+                        log_error_errno(errno, "Failed to change UID to " UID_FMT ": %m", h->uid);
+                        _exit(EXIT_FAILURE);
+                }
+
+                if (kill(-1, SIGKILL) < 0) {
+                        log_error_errno(errno, "Failed to kill all processes of UID " UID_FMT ": %m", h->uid);
+                        _exit(EXIT_FAILURE);
+                }
+
+                _exit(EXIT_SUCCESS);
+        }
+
+        /* Let's also kill everything in the user's slice */
+        if (asprintf(&unit, "user-" UID_FMT ".slice", h->uid) < 0)
+                return log_oom();
+
+        r = sd_bus_call_method(
+                        h->manager->bus,
+                        "org.freedesktop.systemd1",
+                        "/org/freedesktop/systemd1",
+                        "org.freedesktop.systemd1.Manager",
+                        "KillUnit",
+                        &error,
+                        NULL,
+                        "ssi", unit, "all", SIGKILL);
+        if (r < 0)
+                log_full_errno(sd_bus_error_has_name(&error, BUS_ERROR_NO_SUCH_UNIT) ? LOG_DEBUG : LOG_WARNING,
+                               r, "Failed to kill login processes of user, ignoring: %s", bus_error_message(&error, r));
+
+        return 1;
+}
+
+static int home_get_disk_status_luks(
+                Home *h,
+                HomeState state,
+                uint64_t *ret_disk_size,
+                uint64_t *ret_disk_usage,
+                uint64_t *ret_disk_free,
+                uint64_t *ret_disk_ceiling,
+                uint64_t *ret_disk_floor) {
+
+        uint64_t disk_size = UINT64_MAX, disk_usage = UINT64_MAX, disk_free = UINT64_MAX,
+                disk_ceiling = UINT64_MAX, disk_floor = UINT64_MAX,
+                stat_used = UINT64_MAX, fs_size = UINT64_MAX, header_size = 0;
+
+        struct statfs sfs;
+        const char *hd;
+        int r;
+
+        assert(h);
+        assert(ret_disk_size);
+        assert(ret_disk_usage);
+        assert(ret_disk_free);
+        assert(ret_disk_ceiling);
+
+        if (state != HOME_ABSENT) {
+                const char *ip;
+
+                ip = user_record_image_path(h->record);
+                if (ip) {
+                        struct stat st;
+
+                        if (stat(ip, &st) < 0)
+                                log_debug_errno(errno, "Failed to stat() %s, ignoring: %m", ip);
+                        else if (S_ISREG(st.st_mode)) {
+                                _cleanup_free_ char *parent = NULL;
+
+                                disk_size = st.st_size;
+                                stat_used = st.st_blocks * 512;
+
+                                parent = dirname_malloc(ip);
+                                if (!parent)
+                                        return log_oom();
+
+                                if (statfs(parent, &sfs) < 0)
+                                        log_debug_errno(errno, "Failed to statfs() %s, ignoring: %m", parent);
+                                else
+                                        disk_ceiling = stat_used + sfs.f_bsize * sfs.f_bavail;
+
+                        } else if (S_ISBLK(st.st_mode)) {
+                                _cleanup_free_ char *szbuf = NULL;
+                                char p[SYS_BLOCK_PATH_MAX("/size")];
+
+                                /* Let's read the size off sysfs, so that we don't have to open the device */
+                                xsprintf_sys_block_path(p, "/size", st.st_rdev);
+                                r = read_one_line_file(p, &szbuf);
+                                if (r < 0)
+                                        log_debug_errno(r, "Failed to read %s, ignoring: %m", p);
+                                else {
+                                        uint64_t sz;
+
+                                        r = safe_atou64(szbuf, &sz);
+                                        if (r < 0)
+                                                log_debug_errno(r, "Failed to parse %s, ignoring: %s", p, szbuf);
+                                        else
+                                                disk_size = sz * 512;
+                                }
+                        } else
+                                log_debug("Image path is not a block device or regular file, not able to acquire size.");
+                }
+        }
+
+        if (!HOME_STATE_IS_ACTIVE(state))
+                goto finish;
+
+        hd = user_record_home_directory(h->record);
+        if (!hd)
+                goto finish;
+
+        if (statfs(hd, &sfs) < 0) {
+                log_debug_errno(errno, "Failed  to statfs() %s, ignoring: %m", hd);
+                goto finish;
+        }
+
+        disk_free = sfs.f_bsize * sfs.f_bavail;
+        fs_size = sfs.f_bsize * sfs.f_blocks;
+        if (disk_size != UINT64_MAX && disk_size > fs_size)
+                header_size = disk_size - fs_size;
+
+        /* We take a perspective from the user here (as opposed to from the host): the used disk space is the
+         * difference from the limit and what's free. This makes a difference if sparse mode is not used: in
+         * that case the image is pre-allocated and thus appears all used from the host PoV but is not used
+         * up at all yet from the user's PoV.
+         *
+         * That said, we use use the stat() reported loopback file size as upper boundary: our footprint can
+         * never be larger than what we take up on the lowest layers. */
+
+        if (disk_size != UINT64_MAX && disk_size > disk_free) {
+                disk_usage = disk_size - disk_free;
+
+                if (stat_used != UINT64_MAX && disk_usage > stat_used)
+                        disk_usage = stat_used;
+        } else
+                disk_usage = stat_used;
+
+        /* If we have the magic, determine floor preferably by magic */
+        disk_floor = minimal_size_by_fs_magic(sfs.f_type) + header_size;
+
+finish:
+        /* If we don't know the magic, go by file system name */
+        if (disk_floor == UINT64_MAX)
+                disk_floor = minimal_size_by_fs_name(user_record_file_system_type(h->record));
+
+        *ret_disk_size = disk_size;
+        *ret_disk_usage = disk_usage;
+        *ret_disk_free = disk_free;
+        *ret_disk_ceiling = disk_ceiling;
+        *ret_disk_floor = disk_floor;
+
+        return 0;
+}
+
+static int home_get_disk_status_directory(
+                Home *h,
+                HomeState state,
+                uint64_t *ret_disk_size,
+                uint64_t *ret_disk_usage,
+                uint64_t *ret_disk_free,
+                uint64_t *ret_disk_ceiling,
+                uint64_t *ret_disk_floor) {
+
+        uint64_t disk_size = UINT64_MAX, disk_usage = UINT64_MAX, disk_free = UINT64_MAX,
+                disk_ceiling = UINT64_MAX, disk_floor = UINT64_MAX;
+        struct statfs sfs;
+        struct dqblk req;
+        const char *path = NULL;
+        int r;
+
+        assert(ret_disk_size);
+        assert(ret_disk_usage);
+        assert(ret_disk_free);
+        assert(ret_disk_ceiling);
+        assert(ret_disk_floor);
+
+        if (HOME_STATE_IS_ACTIVE(state))
+                path = user_record_home_directory(h->record);
+
+        if (!path) {
+                if (state == HOME_ABSENT)
+                        goto finish;
+
+                path = user_record_image_path(h->record);
+        }
+
+        if (!path)
+                goto finish;
+
+        if (statfs(path, &sfs) < 0)
+                log_debug_errno(errno, "Failed to statfs() %s, ignoring: %m", path);
+        else {
+                disk_free = sfs.f_bsize * sfs.f_bavail;
+                disk_size = sfs.f_bsize * sfs.f_blocks;
+
+                /* We don't initialize disk_usage from statfs() data here, since the device is likely not used
+                 * by us alone, and disk_usage should only reflect our own use. */
+        }
+
+        if (IN_SET(h->record->storage, USER_CLASSIC, USER_DIRECTORY, USER_SUBVOLUME)) {
+
+                r = btrfs_is_subvol(path);
+                if (r < 0)
+                        log_debug_errno(r, "Failed to determine whether %s is a btrfs subvolume: %m", path);
+                else if (r > 0) {
+                        BtrfsQuotaInfo qi;
+
+                        r = btrfs_subvol_get_subtree_quota(path, 0, &qi);
+                        if (r < 0)
+                                log_debug_errno(r, "Failed to query btrfs subtree quota, ignoring: %m");
+                        else {
+                                disk_usage = qi.referenced;
+
+                                if (disk_free != UINT64_MAX) {
+                                        disk_ceiling = qi.referenced + disk_free;
+
+                                        if (disk_size != UINT64_MAX && disk_ceiling > disk_size)
+                                                disk_ceiling = disk_size;
+                                }
+
+                                if (qi.referenced_max != UINT64_MAX) {
+                                        if (disk_size != UINT64_MAX)
+                                                disk_size = MIN(qi.referenced_max, disk_size);
+                                        else
+                                                disk_size = qi.referenced_max;
+                                }
+
+                                if (disk_size != UINT64_MAX) {
+                                        if (disk_size > disk_usage)
+                                                disk_free = disk_size - disk_usage;
+                                        else
+                                                disk_free = 0;
+                                }
+                        }
+
+                        goto finish;
+                }
+        }
+
+        if (IN_SET(h->record->storage, USER_CLASSIC, USER_DIRECTORY, USER_FSCRYPT)) {
+                r = quotactl_path(QCMD_FIXED(Q_GETQUOTA, USRQUOTA), path, h->uid, &req);
+                if (r < 0) {
+                        if (ERRNO_IS_NOT_SUPPORTED(r)) {
+                                log_debug_errno(r, "No UID quota support on %s.", path);
+                                goto finish;
+                        }
+
+                        if (r != -ESRCH) {
+                                log_debug_errno(r, "Failed to query disk quota for UID " UID_FMT ": %m", h->uid);
+                                goto finish;
+                        }
+
+                        disk_usage = 0; /* No record of this user? then nothing was used */
+                } else {
+                        if (FLAGS_SET(req.dqb_valid, QIF_SPACE) && disk_free != UINT64_MAX) {
+                                disk_ceiling = req.dqb_curspace + disk_free;
+
+                                if (disk_size != UINT64_MAX && disk_ceiling > disk_size)
+                                        disk_ceiling = disk_size;
+                        }
+
+                        if (FLAGS_SET(req.dqb_valid, QIF_BLIMITS)) {
+                                uint64_t q;
+
+                                /* Take the minimum of the quota and the available disk space here */
+                                q = req.dqb_bhardlimit * QIF_DQBLKSIZE;
+                                if (disk_size != UINT64_MAX)
+                                        disk_size = MIN(disk_size, q);
+                                else
+                                        disk_size = q;
+                        }
+                        if (FLAGS_SET(req.dqb_valid, QIF_SPACE)) {
+                                disk_usage = req.dqb_curspace;
+
+                                if (disk_size != UINT64_MAX) {
+                                        if (disk_size > disk_usage)
+                                                disk_free = disk_size - disk_usage;
+                                        else
+                                                disk_free = 0;
+                                }
+                        }
+                }
+        }
+
+finish:
+        *ret_disk_size = disk_size;
+        *ret_disk_usage = disk_usage;
+        *ret_disk_free = disk_free;
+        *ret_disk_ceiling = disk_ceiling;
+        *ret_disk_floor = disk_floor;
+
+        return 0;
+}
+
+int home_augment_status(
+                Home *h,
+                UserRecordLoadFlags flags,
+                UserRecord **ret) {
+
+        uint64_t disk_size = UINT64_MAX, disk_usage = UINT64_MAX, disk_free = UINT64_MAX, disk_ceiling = UINT64_MAX, disk_floor = UINT64_MAX;
+        _cleanup_(json_variant_unrefp) JsonVariant *j = NULL, *v = NULL, *m = NULL, *status = NULL;
+        _cleanup_(user_record_unrefp) UserRecord *ur = NULL;
+        char ids[SD_ID128_STRING_MAX];
+        HomeState state;
+        sd_id128_t id;
+        int r;
+
+        assert(h);
+        assert(ret);
+
+        /* We are supposed to add this, this can't be on hence. */
+        assert(!FLAGS_SET(flags, USER_RECORD_STRIP_STATUS));
+
+        r = sd_id128_get_machine(&id);
+        if (r < 0)
+                return r;
+
+        state = home_get_state(h);
+
+        switch (h->record->storage) {
+
+        case USER_LUKS:
+                r = home_get_disk_status_luks(h, state, &disk_size, &disk_usage, &disk_free, &disk_ceiling, &disk_floor);
+                if (r < 0)
+                        return r;
+
+                break;
+
+        case USER_CLASSIC:
+        case USER_DIRECTORY:
+        case USER_SUBVOLUME:
+        case USER_FSCRYPT:
+        case USER_CIFS:
+                r = home_get_disk_status_directory(h, state, &disk_size, &disk_usage, &disk_free, &disk_ceiling, &disk_floor);
+                if (r < 0)
+                        return r;
+
+                break;
+
+        default:
+                ; /* unset */
+        }
+
+        if (disk_floor == UINT64_MAX || (disk_usage != UINT64_MAX && disk_floor < disk_usage))
+                disk_floor = disk_usage;
+        if (disk_floor == UINT64_MAX || disk_floor < USER_DISK_SIZE_MIN)
+                disk_floor = USER_DISK_SIZE_MIN;
+        if (disk_ceiling == UINT64_MAX || disk_ceiling > USER_DISK_SIZE_MAX)
+                disk_ceiling = USER_DISK_SIZE_MAX;
+
+        r = json_build(&status,
+                       JSON_BUILD_OBJECT(
+                                       JSON_BUILD_PAIR("state", JSON_BUILD_STRING(home_state_to_string(state))),
+                                       JSON_BUILD_PAIR("service", JSON_BUILD_STRING("io.systemd.Home")),
+                                       JSON_BUILD_PAIR_CONDITION(disk_size != UINT64_MAX, "diskSize", JSON_BUILD_UNSIGNED(disk_size)),
+                                       JSON_BUILD_PAIR_CONDITION(disk_usage != UINT64_MAX, "diskUsage", JSON_BUILD_UNSIGNED(disk_usage)),
+                                       JSON_BUILD_PAIR_CONDITION(disk_free != UINT64_MAX, "diskFree", JSON_BUILD_UNSIGNED(disk_free)),
+                                       JSON_BUILD_PAIR_CONDITION(disk_ceiling != UINT64_MAX, "diskCeiling", JSON_BUILD_UNSIGNED(disk_ceiling)),
+                                       JSON_BUILD_PAIR_CONDITION(disk_floor != UINT64_MAX, "diskFloor", JSON_BUILD_UNSIGNED(disk_floor)),
+                                       JSON_BUILD_PAIR_CONDITION(h->signed_locally >= 0, "signedLocally", JSON_BUILD_BOOLEAN(h->signed_locally))
+                       ));
+        if (r < 0)
+                return r;
+
+        j = json_variant_ref(h->record->json);
+        v = json_variant_ref(json_variant_by_key(j, "status"));
+        m = json_variant_ref(json_variant_by_key(v, sd_id128_to_string(id, ids)));
+
+        r = json_variant_filter(&m, STRV_MAKE("diskSize", "diskUsage", "diskFree", "diskCeiling", "diskFloor", "signedLocally"));
+        if (r < 0)
+                return r;
+
+        r = json_variant_merge(&m, status);
+        if (r < 0)
+                return r;
+
+        r = json_variant_set_field(&v, ids, m);
+        if (r < 0)
+                return r;
+
+        r = json_variant_set_field(&j, "status", v);
+        if (r < 0)
+                return r;
+
+        ur = user_record_new();
+        if (!ur)
+                return -ENOMEM;
+
+        r = user_record_load(ur, j, flags);
+        if (r < 0)
+                return r;
+
+        ur->incomplete =
+                FLAGS_SET(h->record->mask, USER_RECORD_PRIVILEGED) &&
+                !FLAGS_SET(ur->mask, USER_RECORD_PRIVILEGED);
+
+        *ret = TAKE_PTR(ur);
+        return 0;
+}
+
+static int on_home_ref_eof(sd_event_source *s, int fd, uint32_t revents, void *userdata) {
+        _cleanup_(operation_unrefp) Operation *o = NULL;
+        Home *h = userdata;
+
+        assert(s);
+        assert(h);
+
+        if (h->ref_event_source_please_suspend == s)
+                h->ref_event_source_please_suspend = sd_event_source_disable_unref(h->ref_event_source_please_suspend);
+
+        if (h->ref_event_source_dont_suspend == s)
+                h->ref_event_source_dont_suspend = sd_event_source_disable_unref(h->ref_event_source_dont_suspend);
+
+        if (h->ref_event_source_dont_suspend || h->ref_event_source_please_suspend)
+                return 0;
+
+        log_info("Got notification that all sessions of user %s ended, deactivating automatically.", h->user_name);
+
+        o = operation_new(OPERATION_PIPE_EOF, NULL);
+        if (!o) {
+                log_oom();
+                return 0;
+        }
+
+        home_schedule_operation(h, o, NULL);
+        return 0;
+}
+
+int home_create_fifo(Home *h, bool please_suspend) {
+        _cleanup_close_ int ret_fd = -1;
+        sd_event_source **ss;
+        const char *fn, *suffix;
+        int r;
+
+        assert(h);
+
+        if (please_suspend) {
+                suffix = ".please-suspend";
+                ss = &h->ref_event_source_please_suspend;
+        } else {
+                suffix = ".dont-suspend";
+                ss = &h->ref_event_source_dont_suspend;
+        }
+
+        fn = strjoina("/run/systemd/home/", h->user_name, suffix);
+
+        if (!*ss) {
+                _cleanup_close_ int ref_fd = -1;
+
+                (void) mkdir("/run/systemd/home/", 0755);
+                if (mkfifo(fn, 0600) < 0 && errno != EEXIST)
+                        return log_error_errno(errno, "Failed to create FIFO %s: %m", fn);
+
+                ref_fd = open(fn, O_RDONLY|O_CLOEXEC|O_NONBLOCK);
+                if (ref_fd < 0)
+                        return log_error_errno(errno, "Failed to open FIFO %s for reading: %m", fn);
+
+                r = sd_event_add_io(h->manager->event, ss, ref_fd, 0, on_home_ref_eof, h);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to allocate reference FIFO event source: %m");
+
+                (void) sd_event_source_set_description(*ss, "acquire-ref");
+
+                r = sd_event_source_set_priority(*ss, SD_EVENT_PRIORITY_IDLE-1);
+                if (r < 0)
+                        return r;
+
+                r = sd_event_source_set_io_fd_own(*ss, true);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to pass ownership of FIFO event fd to event source: %m");
+
+                TAKE_FD(ref_fd);
+        }
+
+        ret_fd = open(fn, O_WRONLY|O_CLOEXEC|O_NONBLOCK);
+        if (ret_fd < 0)
+                return log_error_errno(errno, "Failed to open FIFO %s for writing: %m", fn);
+
+        return TAKE_FD(ret_fd);
+}
+
+static int home_dispatch_acquire(Home *h, Operation *o) {
+        _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+        int (*call)(Home *h, UserRecord *secret, HomeState for_state, sd_bus_error *error) = NULL;
+        HomeState for_state;
+        int r;
+
+        assert(h);
+        assert(o);
+        assert(o->type == OPERATION_ACQUIRE);
+
+        switch (home_get_state(h)) {
+
+        case HOME_UNFIXATED:
+                for_state = HOME_FIXATING_FOR_ACQUIRE;
+                call = home_fixate_internal;
+                break;
+
+        case HOME_ABSENT:
+                r = sd_bus_error_setf(&error, BUS_ERROR_HOME_ABSENT, "Home %s is currently missing or not plugged in.", h->user_name);
+                break;
+
+        case HOME_INACTIVE:
+                for_state = HOME_ACTIVATING_FOR_ACQUIRE;
+                call = home_activate_internal;
+                break;
+
+        case HOME_ACTIVE:
+                for_state = HOME_AUTHENTICATING_FOR_ACQUIRE;
+                call = home_authenticate_internal;
+                break;
+
+        case HOME_LOCKED:
+                for_state = HOME_UNLOCKING_FOR_ACQUIRE;
+                call = home_unlock_internal;
+                break;
+
+        default:
+                /* All other cases means we are currently executing an operation, which means the job remains
+                 * pending. */
+                return 0;
+        }
+
+        assert(!h->current_operation);
+
+        if (call) {
+                r = home_ratelimit(h, &error);
+                if (r >= 0)
+                        r = call(h, o->secret, for_state, &error);
+        }
+
+        if (r != 0) /* failure or completed */
+                operation_result(o, r, &error);
+        else /* ongoing */
+                h->current_operation = operation_ref(o);
+
+        return 1;
+}
+
+static int home_dispatch_release(Home *h, Operation *o) {
+        _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+        int r;
+
+        assert(h);
+        assert(o);
+        assert(o->type == OPERATION_RELEASE);
+
+        if (h->ref_event_source_dont_suspend || h->ref_event_source_please_suspend)
+                /* If there's now a reference again, then let's abort the release attempt */
+                r = sd_bus_error_setf(&error, BUS_ERROR_HOME_BUSY, "Home %s is currently referenced.", h->user_name);
+        else {
+                switch (home_get_state(h)) {
+
+                case HOME_UNFIXATED:
+                case HOME_ABSENT:
+                case HOME_INACTIVE:
+                        r = 0; /* done */
+                        break;
+
+                case HOME_LOCKED:
+                        r = sd_bus_error_setf(&error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name);
+                        break;
+
+                case HOME_ACTIVE:
+                        r = home_deactivate_internal(h, false, &error);
+                        break;
+
+                default:
+                        /* All other cases means we are currently executing an operation, which means the job remains
+                         * pending. */
+                        return 0;
+                }
+        }
+
+        assert(!h->current_operation);
+
+        if (r <= 0) /* failure or completed */
+                operation_result(o, r, &error);
+        else /* ongoing */
+                h->current_operation = operation_ref(o);
+
+        return 1;
+}
+
+static int home_dispatch_lock_all(Home *h, Operation *o) {
+        _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+        int r;
+
+        assert(h);
+        assert(o);
+        assert(o->type == OPERATION_LOCK_ALL);
+
+        switch (home_get_state(h)) {
+
+        case HOME_UNFIXATED:
+        case HOME_ABSENT:
+        case HOME_INACTIVE:
+                log_info("Home %s is not active, no locking necessary.", h->user_name);
+                r = 0; /* done */
+                break;
+
+        case HOME_LOCKED:
+                log_info("Home %s is already locked.", h->user_name);
+                r = 0; /* done */
+                break;
+
+        case HOME_ACTIVE:
+                log_info("Locking home %s.", h->user_name);
+                r = home_lock(h, &error);
+                break;
+
+        default:
+                /* All other cases means we are currently executing an operation, which means the job remains
+                 * pending. */
+                return 0;
+        }
+
+        assert(!h->current_operation);
+
+        if (r != 0) /* failure or completed */
+                operation_result(o, r, &error);
+        else /* ongoing */
+                h->current_operation = operation_ref(o);
+
+        return 1;
+}
+
+static int home_dispatch_pipe_eof(Home *h, Operation *o) {
+        _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+        int r;
+
+        assert(h);
+        assert(o);
+        assert(o->type == OPERATION_PIPE_EOF);
+
+        if (h->ref_event_source_please_suspend || h->ref_event_source_dont_suspend)
+                return 1; /* Hmm, there's a reference again, let's cancel this */
+
+        switch (home_get_state(h)) {
+
+        case HOME_UNFIXATED:
+        case HOME_ABSENT:
+        case HOME_INACTIVE:
+                log_info("Home %s already deactivated, no automatic deactivation needed.", h->user_name);
+                break;
+
+        case HOME_DEACTIVATING:
+                log_info("Home %s is already being deactivated, automatic deactivated unnecessary.", h->user_name);
+                break;
+
+        case HOME_ACTIVE:
+                r = home_deactivate_internal(h, false, &error);
+                if (r < 0)
+                        log_warning_errno(r, "Failed to deactivate %s, ignoring: %s", h->user_name, bus_error_message(&error, r));
+                break;
+
+        case HOME_LOCKED:
+        default:
+                /* If the device is locked or any operation is being executed, let's leave this pending */
+                return 0;
+        }
+
+        /* Note that we don't call operation_fail() or operation_success() here, because this kind of
+         * operation has no message associated with it, and thus there's no need to propagate success. */
+
+        assert(!o->message);
+        return 1;
+}
+
+static int home_dispatch_deactivate_force(Home *h, Operation *o) {
+        _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+        int r;
+
+        assert(h);
+        assert(o);
+        assert(o->type == OPERATION_DEACTIVATE_FORCE);
+
+        switch (home_get_state(h)) {
+
+        case HOME_UNFIXATED:
+        case HOME_ABSENT:
+        case HOME_INACTIVE:
+                log_debug("Home %s already deactivated, no forced deactivation due to unplug needed.", h->user_name);
+                break;
+
+        case HOME_DEACTIVATING:
+                log_debug("Home %s is already being deactivated, forced deactivation due to unplug unnecessary.", h->user_name);
+                break;
+
+        case HOME_ACTIVE:
+        case HOME_LOCKED:
+                r = home_deactivate_internal(h, true, &error);
+                if (r < 0)
+                        log_warning_errno(r, "Failed to forcibly deactivate %s, ignoring: %s", h->user_name, bus_error_message(&error, r));
+                break;
+
+        default:
+                /* If any operation is being executed, let's leave this pending */
+                return 0;
+        }
+
+        /* Note that we don't call operation_fail() or operation_success() here, because this kind of
+         * operation has no message associated with it, and thus there's no need to propagate success. */
+
+        assert(!o->message);
+        return 1;
+}
+
+static int on_pending(sd_event_source *s, void *userdata) {
+        Home *h = userdata;
+        Operation *o;
+        int r;
+
+        assert(s);
+        assert(h);
+
+        o = ordered_set_first(h->pending_operations);
+        if (o) {
+                static int (* const operation_table[_OPERATION_MAX])(Home *h, Operation *o) = {
+                        [OPERATION_ACQUIRE]          = home_dispatch_acquire,
+                        [OPERATION_RELEASE]          = home_dispatch_release,
+                        [OPERATION_LOCK_ALL]         = home_dispatch_lock_all,
+                        [OPERATION_PIPE_EOF]         = home_dispatch_pipe_eof,
+                        [OPERATION_DEACTIVATE_FORCE] = home_dispatch_deactivate_force,
+                };
+
+                assert(operation_table[o->type]);
+                r = operation_table[o->type](h, o);
+                if (r != 0) {
+                        /* The operation completed, let's remove it from the pending list, and exit while
+                         * leaving the event source enabled as it is. */
+                        assert_se(ordered_set_remove(h->pending_operations, o) == o);
+                        operation_unref(o);
+                        return 0;
+                }
+        }
+
+        /* Nothing to do anymore, let's turn off this event source */
+        r = sd_event_source_set_enabled(s, SD_EVENT_OFF);
+        if (r < 0)
+                return log_error_errno(r, "Failed to disable event source: %m");
+
+        return 0;
+}
+
+int home_schedule_operation(Home *h, Operation *o, sd_bus_error *error) {
+        int r;
+
+        assert(h);
+
+        if (o) {
+                if (ordered_set_size(h->pending_operations) >= PENDING_OPERATIONS_MAX)
+                        return sd_bus_error_setf(error, BUS_ERROR_TOO_MANY_OPERATIONS, "Too many client operations requested");
+
+                r = ordered_set_ensure_allocated(&h->pending_operations, &operation_hash_ops);
+                if (r < 0)
+                        return r;
+
+                r = ordered_set_put(h->pending_operations, o);
+                if (r < 0)
+                        return r;
+
+                operation_ref(o);
+        }
+
+        if (!h->pending_event_source) {
+                r = sd_event_add_defer(h->manager->event, &h->pending_event_source, on_pending, h);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to allocate pending defer event source: %m");
+
+                (void) sd_event_source_set_description(h->pending_event_source, "pending");
+
+                r = sd_event_source_set_priority(h->pending_event_source, SD_EVENT_PRIORITY_IDLE);
+                if (r < 0)
+                        return r;
+        }
+
+        r = sd_event_source_set_enabled(h->pending_event_source, SD_EVENT_ON);
+        if (r < 0)
+                return log_error_errno(r, "Failed to trigger pending event source: %m");
+
+        return 0;
+}
+
+static int home_get_image_path_seat(Home *h, char **ret) {
+        _cleanup_(sd_device_unrefp) sd_device *d = NULL;
+        _cleanup_free_ char *c = NULL;
+        const char *ip, *seat;
+        struct stat st;
+        int r;
+
+        assert(h);
+
+        if (user_record_storage(h->record) != USER_LUKS)
+                return -ENXIO;
+
+        ip = user_record_image_path(h->record);
+        if (!ip)
+                return -ENXIO;
+
+        if (!path_startswith(ip, "/dev/"))
+                return -ENXIO;
+
+        if (stat(ip, &st) < 0)
+                return -errno;
+
+        if (!S_ISBLK(st.st_mode))
+                return -ENOTBLK;
+
+        r = sd_device_new_from_devnum(&d, 'b', st.st_rdev);
+        if (r < 0)
+                return r;
+
+        r = sd_device_get_property_value(d, "ID_SEAT", &seat);
+        if (r == -ENOENT) /* no property means seat0 */
+                seat = "seat0";
+        else if (r < 0)
+                return r;
+
+        c = strdup(seat);
+        if (!c)
+                return -ENOMEM;
+
+        *ret = TAKE_PTR(c);
+        return 0;
+}
+
+int home_auto_login(Home *h, char ***ret_seats) {
+        _cleanup_free_ char *seat = NULL, *seat2 = NULL;
+
+        assert(h);
+        assert(ret_seats);
+
+        (void) home_get_image_path_seat(h, &seat);
+
+        if (h->record->auto_login > 0 && !streq_ptr(seat, "seat0")) {
+                /* For now, when the auto-login boolean is set for a user, let's make it mean
+                 * "seat0". Eventually we can extend the concept and allow configuration of any kind of seat,
+                 * but let's keep simple initially, most likely the feature is interesting on single-user
+                 * systems anyway, only.
+                 *
+                 * We filter out users marked for auto-login in we know for sure their home directory is
+                 * absent. */
+
+                if (user_record_test_image_path(h->record) != USER_TEST_ABSENT) {
+                        seat2 = strdup("seat0");
+                        if (!seat2)
+                                return -ENOMEM;
+                }
+        }
+
+        if (seat || seat2) {
+                _cleanup_strv_free_ char **list = NULL;
+                size_t i = 0;
+
+                list = new(char*, 3);
+                if (!list)
+                        return -ENOMEM;
+
+                if (seat)
+                        list[i++] = TAKE_PTR(seat);
+                if (seat2)
+                        list[i++] = TAKE_PTR(seat2);
+
+                list[i] = NULL;
+                *ret_seats = TAKE_PTR(list);
+                return 1;
+        }
+
+        *ret_seats = NULL;
+        return 0;
+}
+
+int home_set_current_message(Home *h, sd_bus_message *m) {
+        assert(h);
+
+        if (!m)
+                return 0;
+
+        if (h->current_operation)
+                return -EBUSY;
+
+        h->current_operation = operation_new(OPERATION_IMMEDIATE, m);
+        if (!h->current_operation)
+                return -ENOMEM;
+
+        return 1;
+}
+
+static const char* const home_state_table[_HOME_STATE_MAX] = {
+        [HOME_UNFIXATED]                   = "unfixated",
+        [HOME_ABSENT]                      = "absent",
+        [HOME_INACTIVE]                    = "inactive",
+        [HOME_FIXATING]                    = "fixating",
+        [HOME_FIXATING_FOR_ACTIVATION]     = "fixating-for-activation",
+        [HOME_FIXATING_FOR_ACQUIRE]        = "fixating-for-acquire",
+        [HOME_ACTIVATING]                  = "activating",
+        [HOME_ACTIVATING_FOR_ACQUIRE]      = "activating-for-acquire",
+        [HOME_DEACTIVATING]                = "deactivating",
+        [HOME_ACTIVE]                      = "active",
+        [HOME_LOCKING]                     = "locking",
+        [HOME_LOCKED]                      = "locked",
+        [HOME_UNLOCKING]                   = "unlocking",
+        [HOME_UNLOCKING_FOR_ACQUIRE]       = "unlocking-for-acquire",
+        [HOME_CREATING]                    = "creating",
+        [HOME_REMOVING]                    = "removing",
+        [HOME_UPDATING]                    = "updating",
+        [HOME_UPDATING_WHILE_ACTIVE]       = "updating-while-active",
+        [HOME_RESIZING]                    = "resizing",
+        [HOME_RESIZING_WHILE_ACTIVE]       = "resizing-while-active",
+        [HOME_PASSWD]                      = "passwd",
+        [HOME_PASSWD_WHILE_ACTIVE]         = "passwd-while-active",
+        [HOME_AUTHENTICATING]              = "authenticating",
+        [HOME_AUTHENTICATING_WHILE_ACTIVE] = "authenticating-while-active",
+        [HOME_AUTHENTICATING_FOR_ACQUIRE]  = "authenticating-for-acquire",
+};
+
+DEFINE_STRING_TABLE_LOOKUP(home_state, HomeState);
diff --git a/src/home/homed-home.h b/src/home/homed-home.h
new file mode 100644 (file)
index 0000000..c75b067
--- /dev/null
@@ -0,0 +1,168 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+typedef struct Home Home;
+
+#include "homed-manager.h"
+#include "homed-operation.h"
+#include "list.h"
+#include "ordered-set.h"
+#include "user-record.h"
+
+typedef enum HomeState {
+        HOME_UNFIXATED,               /* home exists, but local record does not */
+        HOME_ABSENT,                  /* local record exists, but home does not */
+        HOME_INACTIVE,                /* record and home exist, but is not logged in */
+        HOME_FIXATING,                /* generating local record from home */
+        HOME_FIXATING_FOR_ACTIVATION, /* fixating in order to activate soon */
+        HOME_FIXATING_FOR_ACQUIRE,    /* fixating because Acquire() was called */
+        HOME_ACTIVATING,
+        HOME_ACTIVATING_FOR_ACQUIRE,  /* activating because Acquire() was called */
+        HOME_DEACTIVATING,
+        HOME_ACTIVE,                  /* logged in right now */
+        HOME_LOCKING,
+        HOME_LOCKED,
+        HOME_UNLOCKING,
+        HOME_UNLOCKING_FOR_ACQUIRE,   /* unlocking because Acquire() was called */
+        HOME_CREATING,
+        HOME_REMOVING,
+        HOME_UPDATING,
+        HOME_UPDATING_WHILE_ACTIVE,
+        HOME_RESIZING,
+        HOME_RESIZING_WHILE_ACTIVE,
+        HOME_PASSWD,
+        HOME_PASSWD_WHILE_ACTIVE,
+        HOME_AUTHENTICATING,
+        HOME_AUTHENTICATING_WHILE_ACTIVE,
+        HOME_AUTHENTICATING_FOR_ACQUIRE,  /* authenticating because Acquire() was called */
+        _HOME_STATE_MAX,
+        _HOME_STATE_INVALID = -1
+} HomeState;
+
+static inline bool HOME_STATE_IS_ACTIVE(HomeState state) {
+        return IN_SET(state,
+                      HOME_ACTIVE,
+                      HOME_UPDATING_WHILE_ACTIVE,
+                      HOME_RESIZING_WHILE_ACTIVE,
+                      HOME_PASSWD_WHILE_ACTIVE,
+                      HOME_AUTHENTICATING_WHILE_ACTIVE,
+                      HOME_AUTHENTICATING_FOR_ACQUIRE);
+}
+
+static inline bool HOME_STATE_IS_EXECUTING_OPERATION(HomeState state) {
+        return IN_SET(state,
+                      HOME_FIXATING,
+                      HOME_FIXATING_FOR_ACTIVATION,
+                      HOME_FIXATING_FOR_ACQUIRE,
+                      HOME_ACTIVATING,
+                      HOME_ACTIVATING_FOR_ACQUIRE,
+                      HOME_DEACTIVATING,
+                      HOME_LOCKING,
+                      HOME_UNLOCKING,
+                      HOME_UNLOCKING_FOR_ACQUIRE,
+                      HOME_CREATING,
+                      HOME_REMOVING,
+                      HOME_UPDATING,
+                      HOME_UPDATING_WHILE_ACTIVE,
+                      HOME_RESIZING,
+                      HOME_RESIZING_WHILE_ACTIVE,
+                      HOME_PASSWD,
+                      HOME_PASSWD_WHILE_ACTIVE,
+                      HOME_AUTHENTICATING,
+                      HOME_AUTHENTICATING_WHILE_ACTIVE,
+                      HOME_AUTHENTICATING_FOR_ACQUIRE);
+}
+
+struct Home {
+        Manager *manager;
+        char *user_name;
+        uid_t uid;
+
+        char *sysfs; /* When found via plugged in device, the sysfs path to it */
+
+        /* Note that the 'state' field is only set to a state while we are doing something (i.e. activating,
+         * deactivating, creating, removing, and such), or when the home is an "unfixated" one. When we are
+         * done with an operation we invalidate the state. This is hint for home_get_state() to check the
+         * state on request as needed from the mount table and similar.*/
+        HomeState state;
+        int signed_locally; /* signed only by us */
+
+        UserRecord *record;
+
+        pid_t worker_pid;
+        int worker_stdout_fd;
+        sd_event_source *worker_event_source;
+        int worker_error_code;
+
+        /* The message we are currently processing, and thus need to reply to on completion */
+        Operation *current_operation;
+
+        /* Stores the raw, plaintext passwords, but only for short periods of time */
+        UserRecord *secret;
+
+        /* When we create a home and that fails, we should possibly unregister the record altogether
+         * again, which is remembered in this boolean. */
+        bool unregister_on_failure;
+
+        /* The reading side of a FIFO stored in /run/systemd/home/, the writing side being used for reference
+         * counting. The references dropped to zero as soon as we see EOF. This concept exists twice: once
+         * for clients that are fine if we suspend the home directory on system suspend, and once for cliets
+         * that are not ok with that. This allows us to determine for each home whether there are any clients
+         * that support unsuspend. */
+        sd_event_source *ref_event_source_please_suspend;
+        sd_event_source *ref_event_source_dont_suspend;
+
+        /* Any pending operations we still need to execute. These are for operations we want to queue if we
+         * can't execute them right-away. */
+        OrderedSet *pending_operations;
+
+        /* A defer event source that processes pending acquire/release/eof events. We have a common
+         * dispatcher that processes all three kinds of events. */
+        sd_event_source *pending_event_source;
+
+        /* Did we send out a D-Bus notification about this entry? */
+        bool announced;
+
+        /* Used to coalesce bus PropertiesChanged events */
+        sd_event_source *deferred_change_event_source;
+};
+
+int home_new(Manager *m, UserRecord *hr, const char *sysfs, Home **ret);
+Home *home_free(Home *h);
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(Home*, home_free);
+
+int home_set_record(Home *h, UserRecord *hr);
+int home_save_record(Home *h);
+int home_unlink_record(Home *h);
+
+int home_fixate(Home *h, UserRecord *secret, sd_bus_error *error);
+int home_activate(Home *h, UserRecord *secret, sd_bus_error *error);
+int home_authenticate(Home *h, UserRecord *secret, sd_bus_error *error);
+int home_deactivate(Home *h, bool force, sd_bus_error *error);
+int home_create(Home *h, UserRecord *secret, sd_bus_error *error);
+int home_remove(Home *h, sd_bus_error *error);
+int home_update(Home *h, UserRecord *new_record, sd_bus_error *error);
+int home_resize(Home *h, uint64_t disk_size, UserRecord *secret, sd_bus_error *error);
+int home_passwd(Home *h, UserRecord *new_secret, UserRecord *old_secret, sd_bus_error *error);
+int home_unregister(Home *h, sd_bus_error *error);
+int home_lock(Home *h, sd_bus_error *error);
+int home_unlock(Home *h, UserRecord *secret, sd_bus_error *error);
+
+HomeState home_get_state(Home *h);
+
+void home_process_notify(Home *h, char **l);
+
+int home_killall(Home *h);
+
+int home_augment_status(Home *h, UserRecordLoadFlags flags, UserRecord **ret);
+
+int home_create_fifo(Home *h, bool please_suspend);
+int home_schedule_operation(Home *h, Operation *o, sd_bus_error *error);
+
+int home_auto_login(Home *h, char ***ret_seats);
+
+int home_set_current_message(Home *h, sd_bus_message *m);
+
+const char *home_state_to_string(HomeState state);
+HomeState home_state_from_string(const char *s);
diff --git a/src/home/homed-manager-bus.c b/src/home/homed-manager-bus.c
new file mode 100644 (file)
index 0000000..e1d6a99
--- /dev/null
@@ -0,0 +1,690 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include <linux/capability.h>
+
+#include "alloc-util.h"
+#include "bus-common-errors.h"
+#include "bus-polkit.h"
+#include "format-util.h"
+#include "homed-bus.h"
+#include "homed-home-bus.h"
+#include "homed-manager-bus.h"
+#include "homed-manager.h"
+#include "strv.h"
+#include "user-record-sign.h"
+#include "user-record-util.h"
+#include "user-util.h"
+
+static int property_get_auto_login(
+                sd_bus *bus,
+                const char *path,
+                const char *interface,
+                const char *property,
+                sd_bus_message *reply,
+                void *userdata,
+                sd_bus_error *error) {
+
+        Manager *m = userdata;
+        Iterator i;
+        Home *h;
+        int r;
+
+        assert(bus);
+        assert(reply);
+        assert(m);
+
+        r = sd_bus_message_open_container(reply, 'a', "(sso)");
+        if (r < 0)
+                return r;
+
+        HASHMAP_FOREACH(h, m->homes_by_name, i) {
+                _cleanup_(strv_freep) char **seats = NULL;
+                _cleanup_free_ char *home_path = NULL;
+                char **s;
+
+                r = home_auto_login(h, &seats);
+                if (r < 0) {
+                        log_debug_errno(r, "Failed to determine whether home '%s' is candidate for auto-login, ignoring: %m", h->user_name);
+                        continue;
+                }
+                if (!r)
+                        continue;
+
+                r = bus_home_path(h, &home_path);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to generate home bus path: %m");
+
+                STRV_FOREACH(s, seats) {
+                        r = sd_bus_message_append(reply, "(sso)", h->user_name, *s, home_path);
+                        if (r < 0)
+                                return r;
+                }
+        }
+
+        return sd_bus_message_close_container(reply);
+}
+
+static int method_get_home_by_name(
+                sd_bus_message *message,
+                void *userdata,
+                sd_bus_error *error) {
+
+        _cleanup_free_ char *path = NULL;
+        const char *user_name;
+        Manager *m = userdata;
+        Home *h;
+        int r;
+
+        assert(message);
+        assert(m);
+
+        r = sd_bus_message_read(message, "s", &user_name);
+        if (r < 0)
+                return r;
+        if (!valid_user_group_name(user_name))
+                return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "User name %s is not valid", user_name);
+
+        h = hashmap_get(m->homes_by_name, user_name);
+        if (!h)
+                return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_HOME, "No home for user %s known", user_name);
+
+        r = bus_home_path(h, &path);
+        if (r < 0)
+                return r;
+
+        return sd_bus_reply_method_return(
+                        message, "usussso",
+                        (uint32_t) h->uid,
+                        home_state_to_string(home_get_state(h)),
+                        h->record ? (uint32_t) user_record_gid(h->record) : GID_INVALID,
+                        h->record ? user_record_real_name(h->record) : NULL,
+                        h->record ? user_record_home_directory(h->record) : NULL,
+                        h->record ? user_record_shell(h->record) : NULL,
+                        path);
+}
+
+static int method_get_home_by_uid(
+                sd_bus_message *message,
+                void *userdata,
+                sd_bus_error *error) {
+
+        _cleanup_free_ char *path = NULL;
+        Manager *m = userdata;
+        uint32_t uid;
+        int r;
+        Home *h;
+
+        assert(message);
+        assert(m);
+
+        r = sd_bus_message_read(message, "u", &uid);
+        if (r < 0)
+                return r;
+        if (!uid_is_valid(uid))
+                return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "UID " UID_FMT " is not valid", uid);
+
+        h = hashmap_get(m->homes_by_uid, UID_TO_PTR(uid));
+        if (!h)
+                return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_HOME, "No home for UID " UID_FMT " known", uid);
+
+        /* Note that we don't use bus_home_path() here, but build the path manually, since if we are queried
+         * for a UID we should also generate the bus path with a UID, and bus_home_path() uses our more
+         * typical bus path by name. */
+        if (asprintf(&path, "/org/freedesktop/home1/home/" UID_FMT, h->uid) < 0)
+                return -ENOMEM;
+
+        return sd_bus_reply_method_return(
+                        message, "ssussso",
+                        h->user_name,
+                        home_state_to_string(home_get_state(h)),
+                        h->record ? (uint32_t) user_record_gid(h->record) : GID_INVALID,
+                        h->record ? user_record_real_name(h->record) : NULL,
+                        h->record ? user_record_home_directory(h->record) : NULL,
+                        h->record ? user_record_shell(h->record) : NULL,
+                        path);
+}
+
+static int method_list_homes(
+                sd_bus_message *message,
+                void *userdata,
+                sd_bus_error *error) {
+
+        _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+        Manager *m = userdata;
+        Iterator i;
+        Home *h;
+        int r;
+
+        assert(message);
+        assert(m);
+
+        r = sd_bus_message_new_method_return(message, &reply);
+        if (r < 0)
+                return r;
+
+        r = sd_bus_message_open_container(reply, 'a', "(susussso)");
+        if (r < 0)
+                return r;
+
+        HASHMAP_FOREACH(h, m->homes_by_uid, i) {
+                _cleanup_free_ char *path = NULL;
+
+                r = bus_home_path(h, &path);
+                if (r < 0)
+                        return r;
+
+                r = sd_bus_message_append(
+                                reply, "(susussso)",
+                                h->user_name,
+                                (uint32_t) h->uid,
+                                home_state_to_string(home_get_state(h)),
+                                h->record ? (uint32_t) user_record_gid(h->record) : GID_INVALID,
+                                h->record ? user_record_real_name(h->record) : NULL,
+                                h->record ? user_record_home_directory(h->record) : NULL,
+                                h->record ? user_record_shell(h->record) : NULL,
+                                path);
+                if (r < 0)
+                        return r;
+        }
+
+        r = sd_bus_message_close_container(reply);
+        if (r < 0)
+                return r;
+
+        return sd_bus_send(NULL, reply, NULL);
+}
+
+static int method_get_user_record_by_name(
+                sd_bus_message *message,
+                void *userdata,
+                sd_bus_error *error) {
+
+        _cleanup_free_ char *json = NULL, *path = NULL;
+        Manager *m = userdata;
+        const char *user_name;
+        bool incomplete;
+        Home *h;
+        int r;
+
+        assert(message);
+        assert(m);
+
+        r = sd_bus_message_read(message, "s", &user_name);
+        if (r < 0)
+                return r;
+        if (!valid_user_group_name(user_name))
+                return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "User name %s is not valid", user_name);
+
+        h = hashmap_get(m->homes_by_name, user_name);
+        if (!h)
+                return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_HOME, "No home for user %s known", user_name);
+
+        r = bus_home_get_record_json(h, message, &json, &incomplete);
+        if (r < 0)
+                return r;
+
+        r = bus_home_path(h, &path);
+        if (r < 0)
+                return r;
+
+        return sd_bus_reply_method_return(
+                        message, "sbo",
+                        json,
+                        incomplete,
+                        path);
+}
+
+static int method_get_user_record_by_uid(
+                sd_bus_message *message,
+                void *userdata,
+                sd_bus_error *error) {
+
+        _cleanup_free_ char *json = NULL, *path = NULL;
+        Manager *m = userdata;
+        bool incomplete;
+        uint32_t uid;
+        Home *h;
+        int r;
+
+        assert(message);
+        assert(m);
+
+        r = sd_bus_message_read(message, "u", &uid);
+        if (r < 0)
+                return r;
+        if (!uid_is_valid(uid))
+                return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "UID " UID_FMT " is not valid", uid);
+
+        h = hashmap_get(m->homes_by_uid, UID_TO_PTR(uid));
+        if (!h)
+                return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_HOME, "No home for UID " UID_FMT " known", uid);
+
+        r = bus_home_get_record_json(h, message, &json, &incomplete);
+        if (r < 0)
+                return r;
+
+        if (asprintf(&path, "/org/freedesktop/home1/home/" UID_FMT, h->uid) < 0)
+                return -ENOMEM;
+
+        return sd_bus_reply_method_return(
+                        message, "sbo",
+                        json,
+                        incomplete,
+                        path);
+}
+
+static int generic_home_method(
+                Manager *m,
+                sd_bus_message *message,
+                sd_bus_message_handler_t handler,
+                sd_bus_error *error) {
+
+        const char *user_name;
+        Home *h;
+        int r;
+
+        r = sd_bus_message_read(message, "s", &user_name);
+        if (r < 0)
+                return r;
+
+        if (!valid_user_group_name(user_name))
+                return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "User name %s is not valid", user_name);
+
+        h = hashmap_get(m->homes_by_name, user_name);
+        if (!h)
+                return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_HOME, "No home for user %s known", user_name);
+
+        return handler(message, h, error);
+}
+
+static int method_activate_home(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+        return generic_home_method(userdata, message, bus_home_method_activate, error);
+}
+
+static int method_deactivate_home(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+        return generic_home_method(userdata, message, bus_home_method_deactivate, error);
+}
+
+static int validate_and_allocate_home(Manager *m, UserRecord *hr, Home **ret, sd_bus_error *error) {
+        _cleanup_(user_record_unrefp) UserRecord *signed_hr = NULL;
+        struct passwd *pw;
+        struct group *gr;
+        bool signed_locally;
+        Home *other;
+        int r;
+
+        assert(m);
+        assert(hr);
+        assert(ret);
+
+        r = user_record_is_supported(hr, error);
+        if (r < 0)
+                return r;
+
+        other = hashmap_get(m->homes_by_name, hr->user_name);
+        if (other)
+                return sd_bus_error_setf(error, BUS_ERROR_USER_NAME_EXISTS, "Specified user name %s exists already, refusing.", hr->user_name);
+
+        pw = getpwnam(hr->user_name);
+        if (pw)
+                return sd_bus_error_setf(error, BUS_ERROR_USER_NAME_EXISTS, "Specified user name %s exists in the NSS user database, refusing.", hr->user_name);
+
+        gr = getgrnam(hr->user_name);
+        if (gr)
+                return sd_bus_error_setf(error, BUS_ERROR_USER_NAME_EXISTS, "Specified user name %s conflicts with an NSS group by the same name, refusing.", hr->user_name);
+
+        r = manager_verify_user_record(m, hr);
+        switch (r) {
+
+        case USER_RECORD_UNSIGNED:
+                /* If the record is unsigned, then let's sign it with our own key */
+                r = manager_sign_user_record(m, hr, &signed_hr, error);
+                if (r < 0)
+                        return r;
+
+                hr = signed_hr;
+                _fallthrough_;
+
+        case USER_RECORD_SIGNED_EXCLUSIVE:
+                signed_locally = true;
+                break;
+
+        case USER_RECORD_SIGNED:
+        case USER_RECORD_FOREIGN:
+                signed_locally = false;
+                break;
+
+        case -ENOKEY:
+                return sd_bus_error_setf(error, BUS_ERROR_BAD_SIGNATURE, "Specified user record for %s is signed by a key we don't recognize, refusing.", hr->user_name);
+
+        default:
+                return sd_bus_error_set_errnof(error, r, "Failed to validate signature for '%s': %m", hr->user_name);
+        }
+
+        if (uid_is_valid(hr->uid)) {
+                other = hashmap_get(m->homes_by_uid, UID_TO_PTR(hr->uid));
+                if (other)
+                        return sd_bus_error_setf(error, BUS_ERROR_UID_IN_USE, "Specified UID " UID_FMT " already in use by home %s, refusing.", hr->uid, other->user_name);
+
+                pw = getpwuid(hr->uid);
+                if (pw)
+                        return sd_bus_error_setf(error, BUS_ERROR_UID_IN_USE, "Specified UID " UID_FMT " already in use by NSS user %s, refusing.", hr->uid, pw->pw_name);
+
+                gr = getgrgid(hr->uid);
+                if (gr)
+                        return sd_bus_error_setf(error, BUS_ERROR_UID_IN_USE, "Specified UID " UID_FMT " already in use as GID by NSS group %s, refusing.", hr->uid, gr->gr_name);
+        } else {
+                r = manager_augment_record_with_uid(m, hr);
+                if (r < 0)
+                        return sd_bus_error_set_errnof(error, r, "Failed to acquire UID for '%s': %m", hr->user_name);
+        }
+
+        r = home_new(m, hr, NULL, ret);
+        if (r < 0)
+                return r;
+
+        (*ret)->signed_locally = signed_locally;
+        return r;
+}
+
+static int method_register_home(
+                sd_bus_message *message,
+                void *userdata,
+                sd_bus_error *error) {
+
+        _cleanup_(user_record_unrefp) UserRecord *hr = NULL;
+        Manager *m = userdata;
+        Home *h;
+        int r;
+
+        assert(message);
+        assert(m);
+
+        r = bus_message_read_home_record(message, USER_RECORD_LOAD_EMBEDDED, &hr, error);
+        if (r < 0)
+                return r;
+
+        r = bus_verify_polkit_async(
+                        message,
+                        CAP_SYS_ADMIN,
+                        "org.freedesktop.home1.create-home",
+                        NULL,
+                        true,
+                        UID_INVALID,
+                        &m->polkit_registry,
+                        error);
+        if (r < 0)
+                return r;
+        if (r == 0)
+                return 1; /* Will call us back */
+
+        r = validate_and_allocate_home(m, hr, &h, error);
+        if (r < 0)
+                return r;
+
+        r = home_save_record(h);
+        if (r < 0) {
+                home_free(h);
+                return r;
+        }
+
+        return sd_bus_reply_method_return(message, NULL);
+}
+
+static int method_unregister_home(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+        return generic_home_method(userdata, message, bus_home_method_unregister, error);
+}
+
+static int method_create_home(
+                sd_bus_message *message,
+                void *userdata,
+                sd_bus_error *error) {
+
+        _cleanup_(user_record_unrefp) UserRecord *hr = NULL;
+        Manager *m = userdata;
+        Home *h;
+        int r;
+
+        assert(message);
+        assert(m);
+
+        r = bus_message_read_home_record(message, USER_RECORD_REQUIRE_REGULAR|USER_RECORD_ALLOW_SECRET|USER_RECORD_ALLOW_PRIVILEGED|USER_RECORD_ALLOW_PER_MACHINE|USER_RECORD_ALLOW_SIGNATURE, &hr, error);
+        if (r < 0)
+                return r;
+
+        r = bus_verify_polkit_async(
+                        message,
+                        CAP_SYS_ADMIN,
+                        "org.freedesktop.home1.create-home",
+                        NULL,
+                        true,
+                        UID_INVALID,
+                        &m->polkit_registry,
+                        error);
+        if (r < 0)
+                return r;
+        if (r == 0)
+                return 1; /* Will call us back */
+
+        r = validate_and_allocate_home(m, hr, &h, error);
+        if (r < 0)
+                return r;
+
+        r = home_create(h, hr, error);
+        if (r < 0)
+                goto fail;
+
+        assert(r == 0);
+        h->unregister_on_failure = true;
+        assert(!h->current_operation);
+
+        r = home_set_current_message(h, message);
+        if (r < 0)
+                return r;
+
+        return 1;
+
+fail:
+        (void) home_unlink_record(h);
+        h = home_free(h);
+        return r;
+}
+
+static int method_realize_home(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+        return generic_home_method(userdata, message, bus_home_method_realize, error);
+}
+
+static int method_remove_home(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+        return generic_home_method(userdata, message, bus_home_method_remove, error);
+}
+
+static int method_fixate_home(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+        return generic_home_method(userdata, message, bus_home_method_fixate, error);
+}
+
+static int method_authenticate_home(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+        return generic_home_method(userdata, message, bus_home_method_authenticate, error);
+}
+
+static int method_update_home(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+        _cleanup_(user_record_unrefp) UserRecord *hr = NULL;
+        Manager *m = userdata;
+        Home *h;
+        int r;
+
+        assert(message);
+        assert(m);
+
+        r = bus_message_read_home_record(message, USER_RECORD_REQUIRE_REGULAR|USER_RECORD_ALLOW_SECRET|USER_RECORD_ALLOW_PRIVILEGED|USER_RECORD_ALLOW_PER_MACHINE|USER_RECORD_ALLOW_SIGNATURE, &hr, error);
+        if (r < 0)
+                return r;
+
+        assert(hr->user_name);
+
+        h = hashmap_get(m->homes_by_name, hr->user_name);
+        if (!h)
+                return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_HOME, "No home for user %s known", hr->user_name);
+
+        return bus_home_method_update_record(h, message, hr, error);
+}
+
+static int method_resize_home(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+        return generic_home_method(userdata, message, bus_home_method_resize, error);
+}
+
+static int method_change_password_home(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+        return generic_home_method(userdata, message, bus_home_method_change_password, error);
+}
+
+static int method_lock_home(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+        return generic_home_method(userdata, message, bus_home_method_lock, error);
+}
+
+static int method_unlock_home(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+        return generic_home_method(userdata, message, bus_home_method_unlock, error);
+}
+
+static int method_acquire_home(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+        return generic_home_method(userdata, message, bus_home_method_acquire, error);
+}
+
+static int method_ref_home(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+        return generic_home_method(userdata, message, bus_home_method_ref, error);
+}
+
+static int method_release_home(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+        return generic_home_method(userdata, message, bus_home_method_release, error);
+}
+
+static int method_lock_all_homes(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+        _cleanup_(operation_unrefp) Operation *o = NULL;
+        bool waiting = false;
+        Manager *m = userdata;
+        Iterator i;
+        Home *h;
+        int r;
+
+        assert(m);
+
+        /* This is called from logind when we are preparing for system suspend. We enqueue a lock operation
+         * for every suitable home we have and only when all of them completed we send a reply indicating
+         * completion. */
+
+        HASHMAP_FOREACH(h, m->homes_by_name, i) {
+
+                /* Automatically suspend all homes that have at least one client referencing it that asked
+                 * for "please suspend", and no client that asked for "please do not suspend". */
+                if (h->ref_event_source_dont_suspend ||
+                    !h->ref_event_source_please_suspend)
+                        continue;
+
+                if (!o) {
+                        o = operation_new(OPERATION_LOCK_ALL, message);
+                        if (!o)
+                                return -ENOMEM;
+                }
+
+                log_info("Automatically locking of home of user %s.", h->user_name);
+
+                r = home_schedule_operation(h, o, error);
+                if (r < 0)
+                        return r;
+
+                waiting = true;
+        }
+
+        if (waiting) /* At least one lock operation was enqeued, let's leave here without a reply: it will
+                        * be sent as soon as the last of the lock operations completed. */
+                return 1;
+
+        return sd_bus_reply_method_return(message, NULL);
+}
+
+const sd_bus_vtable manager_vtable[] = {
+        SD_BUS_VTABLE_START(0),
+
+        SD_BUS_PROPERTY("AutoLogin", "a(sso)", property_get_auto_login, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
+
+        SD_BUS_METHOD("GetHomeByName", "s", "usussso", method_get_home_by_name, SD_BUS_VTABLE_UNPRIVILEGED),
+        SD_BUS_METHOD("GetHomeByUID", "u", "ssussso", method_get_home_by_uid, SD_BUS_VTABLE_UNPRIVILEGED),
+        SD_BUS_METHOD("GetUserRecordByName", "s", "sbo", method_get_user_record_by_name, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),
+        SD_BUS_METHOD("GetUserRecordByUID", "u", "sbo", method_get_user_record_by_uid, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),
+        SD_BUS_METHOD("ListHomes", NULL, "a(susussso)", method_list_homes, SD_BUS_VTABLE_UNPRIVILEGED),
+
+        /* The following methods directly execute an operation on a home, without ref-counting, queing or
+         * anything, and are accessible through homectl. */
+        SD_BUS_METHOD("ActivateHome", "ss", NULL, method_activate_home, SD_BUS_VTABLE_SENSITIVE),
+        SD_BUS_METHOD("DeactivateHome", "s", NULL, method_deactivate_home, 0),
+        SD_BUS_METHOD("RegisterHome", "s", NULL, method_register_home, SD_BUS_VTABLE_UNPRIVILEGED),                                  /* Add JSON record to homed, but don't create actual $HOME */
+        SD_BUS_METHOD("UnregisterHome", "s", NULL, method_unregister_home, SD_BUS_VTABLE_UNPRIVILEGED),                              /* Remove JSON record from homed, but don't remove actual $HOME  */
+        SD_BUS_METHOD("CreateHome", "s", NULL, method_create_home, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),              /* Add JSON record, and create $HOME for it */
+        SD_BUS_METHOD("RealizeHome", "ss", NULL, method_realize_home, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),           /* Create $HOME for already registered JSON entry */
+        SD_BUS_METHOD("RemoveHome", "s", NULL, method_remove_home, SD_BUS_VTABLE_UNPRIVILEGED),                                      /* Remove JSON record and remove $HOME */
+        SD_BUS_METHOD("FixateHome", "ss", NULL, method_fixate_home, SD_BUS_VTABLE_SENSITIVE),                                        /* Investigate $HOME and propagate contained JSON record into our database */
+        SD_BUS_METHOD("AuthenticateHome", "ss", NULL, method_authenticate_home, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE), /* Just check credentials */
+        SD_BUS_METHOD("UpdateHome", "s", NULL, method_update_home, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),              /* Update JSON record of existing user */
+        SD_BUS_METHOD("ResizeHome", "sts", NULL, method_resize_home, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),
+        SD_BUS_METHOD("ChangePasswordHome", "sss", NULL, method_change_password_home, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),
+        SD_BUS_METHOD("LockHome", "s", NULL, method_lock_home, 0),                                                                   /* Prepare active home for system suspend: flush out passwords, suspend access */
+        SD_BUS_METHOD("UnlockHome", "ss", NULL, method_unlock_home, SD_BUS_VTABLE_SENSITIVE),                                        /* Make $HOME usable after system resume again */
+
+        /* The following methods implement ref-counted activation, and are what the PAM module calls (and
+         * what "homectl with" runs). In contrast to the methods above which fail if an operation is already
+         * being executed on a home directory, these ones will queue the request, and are thus more
+         * reliable. Moreover, they are a bit smarter: AcquireHome() will fixate, activate, unlock, or
+         * authenticate depending on the state of the home, so that the end result is always the same
+         * (i.e. the home directory is accessible), and we always validate the specified passwords. RefHome()
+         * will not authenticate, and thus only works if home is already active. */
+        SD_BUS_METHOD("AcquireHome", "ssb", "h", method_acquire_home, SD_BUS_VTABLE_SENSITIVE),
+        SD_BUS_METHOD("RefHome", "sb", "h", method_ref_home, 0),
+        SD_BUS_METHOD("ReleaseHome", "s", NULL, method_release_home, 0),
+
+        /* An operation that acts on all homes that allow it */
+        SD_BUS_METHOD("LockAllHomes", NULL, NULL, method_lock_all_homes, 0),
+
+        SD_BUS_VTABLE_END
+};
+
+static int on_deferred_auto_login(sd_event_source *s, void *userdata) {
+        Manager *m = userdata;
+        int r;
+
+        assert(m);
+
+        m->deferred_auto_login_event_source = sd_event_source_unref(m->deferred_auto_login_event_source);
+
+        r = sd_bus_emit_properties_changed(
+                        m->bus,
+                        "/org/freedesktop/home1",
+                        "org.freedesktop.home1.Manager",
+                        "AutoLogin", NULL);
+        if (r < 0)
+                log_warning_errno(r, "Failed to send AutoLogin property change event, ignoring: %m");
+
+        return 0;
+}
+
+int bus_manager_emit_auto_login_changed(Manager *m) {
+        int r;
+        assert(m);
+
+        if (m->deferred_auto_login_event_source)
+                return 0;
+
+        if (!m->event)
+                return 0;
+
+        if (IN_SET(sd_event_get_state(m->event), SD_EVENT_FINISHED, SD_EVENT_EXITING))
+                return 0;
+
+        r = sd_event_add_defer(m->event, &m->deferred_auto_login_event_source, on_deferred_auto_login, m);
+        if (r < 0)
+                return log_error_errno(r, "Failed to allocate auto login event source: %m");
+
+        r = sd_event_source_set_priority(m->deferred_auto_login_event_source, SD_EVENT_PRIORITY_IDLE+10);
+        if (r < 0)
+                log_warning_errno(r, "Failed to tweak priority of event source, ignoring: %m");
+
+        (void) sd_event_source_set_description(m->deferred_auto_login_event_source, "deferred-auto-login");
+        return 1;
+}
diff --git a/src/home/homed-manager-bus.h b/src/home/homed-manager-bus.h
new file mode 100644 (file)
index 0000000..40e1cc3
--- /dev/null
@@ -0,0 +1,6 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include "sd-bus.h"
+
+extern const sd_bus_vtable manager_vtable[];
diff --git a/src/home/homed-manager.c b/src/home/homed-manager.c
new file mode 100644 (file)
index 0000000..f4fec0e
--- /dev/null
@@ -0,0 +1,1672 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include <grp.h>
+#include <linux/fs.h>
+#include <linux/magic.h>
+#include <openssl/pem.h>
+#include <pwd.h>
+#include <sys/ioctl.h>
+#include <sys/quota.h>
+#include <sys/stat.h>
+
+#include "btrfs-util.h"
+#include "bus-common-errors.h"
+#include "bus-error.h"
+#include "bus-polkit.h"
+#include "clean-ipc.h"
+#include "conf-files.h"
+#include "device-util.h"
+#include "dirent-util.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "format-util.h"
+#include "fs-util.h"
+#include "gpt.h"
+#include "home-util.h"
+#include "homed-home-bus.h"
+#include "homed-home.h"
+#include "homed-manager-bus.h"
+#include "homed-manager.h"
+#include "homed-varlink.h"
+#include "io-util.h"
+#include "mkdir.h"
+#include "process-util.h"
+#include "quota-util.h"
+#include "random-util.h"
+#include "socket-util.h"
+#include "stat-util.h"
+#include "strv.h"
+#include "tmpfile-util.h"
+#include "udev-util.h"
+#include "user-record-sign.h"
+#include "user-record-util.h"
+#include "user-record.h"
+#include "user-util.h"
+
+/* Where to look for private/public keys that are used to sign the user records. We are not using
+ * CONF_PATHS_NULSTR() here since we want to insert /var/lib/systemd/home/ in the middle. And we insert that
+ * since we want to auto-generate a persistent private/public key pair if we need to. */
+#define KEY_PATHS_NULSTR                        \
+        "/etc/systemd/home/\0"                  \
+        "/run/systemd/home/\0"                  \
+        "/var/lib/systemd/home/\0"              \
+        "/usr/local/lib/systemd/home/\0"        \
+        "/usr/lib/systemd/home/\0"
+
+static bool uid_is_home(uid_t uid) {
+        return uid >= HOME_UID_MIN && uid <= HOME_UID_MAX;
+}
+/* Takes a value generated randomly or by hashing and turns it into a UID in the right range */
+
+#define UID_CLAMP_INTO_HOME_RANGE(rnd) (((uid_t) (rnd) % (HOME_UID_MAX - HOME_UID_MIN + 1)) + HOME_UID_MIN)
+
+DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(homes_by_uid_hash_ops, void, trivial_hash_func, trivial_compare_func, Home, home_free);
+DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(homes_by_name_hash_ops, char, string_hash_func, string_compare_func, Home, home_free);
+DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(homes_by_worker_pid_hash_ops, void, trivial_hash_func, trivial_compare_func, Home, home_free);
+DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(homes_by_sysfs_hash_ops, char, path_hash_func, path_compare, Home, home_free);
+
+static int on_home_inotify(sd_event_source *s, const struct inotify_event *event, void *userdata);
+static int manager_gc_images(Manager *m);
+static int manager_enumerate_images(Manager *m);
+static int manager_assess_image(Manager *m, int dir_fd, const char *dir_path, const char *dentry_name);
+static void manager_revalidate_image(Manager *m, Home *h);
+
+static void manager_watch_home(Manager *m) {
+        struct statfs sfs;
+        int r;
+
+        assert(m);
+
+        m->inotify_event_source = sd_event_source_unref(m->inotify_event_source);
+        m->scan_slash_home = false;
+
+        if (statfs("/home/", &sfs) < 0) {
+                log_full_errno(errno == ENOENT ? LOG_DEBUG : LOG_WARNING, errno,
+                               "Failed to statfs() /home/ directory, disabling automatic scanning.");
+                return;
+        }
+
+        if (is_network_fs(&sfs)) {
+                log_info("/home/ is a network file system, disabling automatic scanning.");
+                return;
+        }
+
+        if (is_fs_type(&sfs, AUTOFS_SUPER_MAGIC)) {
+                log_info("/home/ is on autofs, disabling automatic scanning.");
+                return;
+        }
+
+        m->scan_slash_home = true;
+
+        r = sd_event_add_inotify(m->event, &m->inotify_event_source, "/home/", IN_CREATE|IN_CLOSE_WRITE|IN_DELETE_SELF|IN_MOVE_SELF|IN_ONLYDIR|IN_MOVED_TO|IN_MOVED_FROM|IN_DELETE, on_home_inotify, m);
+        if (r < 0)
+                log_full_errno(r == -ENOENT ? LOG_DEBUG : LOG_WARNING, r,
+                               "Failed to create inotify watch on /home/, ignoring.");
+
+        (void) sd_event_source_set_description(m->inotify_event_source, "home-inotify");
+}
+
+static int on_home_inotify(sd_event_source *s, const struct inotify_event *event, void *userdata) {
+        Manager *m = userdata;
+        const char *e, *n;
+
+        assert(m);
+        assert(event);
+
+        if ((event->mask & (IN_Q_OVERFLOW|IN_MOVE_SELF|IN_DELETE_SELF|IN_IGNORED|IN_UNMOUNT)) != 0) {
+
+                if (FLAGS_SET(event->mask, IN_Q_OVERFLOW))
+                        log_debug("/home/ inotify queue overflow, rescanning.");
+                else if (FLAGS_SET(event->mask, IN_MOVE_SELF))
+                        log_info("/home/ moved or renamed, recreating watch and rescanning.");
+                else if (FLAGS_SET(event->mask, IN_DELETE_SELF))
+                        log_info("/home/ deleted, recreating watch and rescanning.");
+                else if (FLAGS_SET(event->mask, IN_UNMOUNT))
+                        log_info("/home/ unmounted, recreating watch and rescanning.");
+                else if (FLAGS_SET(event->mask, IN_IGNORED))
+                        log_info("/home/ watch invalidated, recreating watch and rescanning.");
+
+                manager_watch_home(m);
+                (void) manager_gc_images(m);
+                (void) manager_enumerate_images(m);
+                (void) bus_manager_emit_auto_login_changed(m);
+                return 0;
+        }
+
+        /* For the other inotify events, let's ignore all events for file names that don't match our
+         * expectations */
+        if (isempty(event->name))
+                return 0;
+        e = endswith(event->name, FLAGS_SET(event->mask, IN_ISDIR) ? ".homedir" : ".home");
+        if (!e)
+                return 0;
+
+        n = strndupa(event->name, e - event->name);
+        if (!suitable_user_name(n))
+                return 0;
+
+        if ((event->mask & (IN_CREATE|IN_CLOSE_WRITE|IN_MOVED_TO)) != 0) {
+                if (FLAGS_SET(event->mask, IN_CREATE))
+                        log_debug("/home/%s has been created, having a look.", event->name);
+                else if (FLAGS_SET(event->mask, IN_CLOSE_WRITE))
+                        log_debug("/home/%s has been modified, having a look.", event->name);
+                else if (FLAGS_SET(event->mask, IN_MOVED_TO))
+                        log_debug("/home/%s has been moved in, having a look.", event->name);
+
+                (void) manager_assess_image(m, -1, "/home/", event->name);
+                (void) bus_manager_emit_auto_login_changed(m);
+        }
+
+        if ((event->mask & (IN_DELETE|IN_MOVED_FROM|IN_DELETE)) != 0) {
+                Home *h;
+
+                if (FLAGS_SET(event->mask, IN_DELETE))
+                        log_debug("/home/%s has been deleted, revalidating.", event->name);
+                else if (FLAGS_SET(event->mask, IN_CLOSE_WRITE))
+                        log_debug("/home/%s has been closed after writing, revalidating.", event->name);
+                else if (FLAGS_SET(event->mask, IN_MOVED_FROM))
+                        log_debug("/home/%s has been moved away, revalidating.", event->name);
+
+                h = hashmap_get(m->homes_by_name, n);
+                if (h) {
+                        manager_revalidate_image(m, h);
+                        (void) bus_manager_emit_auto_login_changed(m);
+                }
+        }
+
+        return 0;
+}
+
+int manager_new(Manager **ret) {
+        _cleanup_(manager_freep) Manager *m = NULL;
+        int r;
+
+        assert(ret);
+
+        m = new0(Manager, 1);
+        if (!m)
+                return -ENOMEM;
+
+        r = sd_event_default(&m->event);
+        if (r < 0)
+                return r;
+
+        r = sd_event_add_signal(m->event, NULL, SIGINT, NULL, NULL);
+        if (r < 0)
+                return r;
+
+        r = sd_event_add_signal(m->event, NULL, SIGTERM, NULL, NULL);
+        if (r < 0)
+                return r;
+
+        (void) sd_event_set_watchdog(m->event, true);
+
+        m->homes_by_uid = hashmap_new(&homes_by_uid_hash_ops);
+        if (!m->homes_by_uid)
+                return -ENOMEM;
+
+        m->homes_by_name = hashmap_new(&homes_by_name_hash_ops);
+        if (!m->homes_by_name)
+                return -ENOMEM;
+
+        m->homes_by_worker_pid = hashmap_new(&homes_by_worker_pid_hash_ops);
+        if (!m->homes_by_worker_pid)
+                return -ENOMEM;
+
+        m->homes_by_sysfs = hashmap_new(&homes_by_sysfs_hash_ops);
+        if (!m->homes_by_sysfs)
+                return -ENOMEM;
+
+        *ret = TAKE_PTR(m);
+        return 0;
+}
+
+Manager* manager_free(Manager *m) {
+        assert(m);
+
+        hashmap_free(m->homes_by_uid);
+        hashmap_free(m->homes_by_name);
+        hashmap_free(m->homes_by_worker_pid);
+        hashmap_free(m->homes_by_sysfs);
+
+        m->inotify_event_source = sd_event_source_unref(m->inotify_event_source);
+
+        bus_verify_polkit_async_registry_free(m->polkit_registry);
+
+        sd_bus_flush_close_unref(m->bus);
+        sd_event_unref(m->event);
+
+        m->notify_socket_event_source = sd_event_source_unref(m->notify_socket_event_source);
+        m->device_monitor = sd_device_monitor_unref(m->device_monitor);
+
+        m->deferred_rescan_event_source = sd_event_source_unref(m->deferred_rescan_event_source);
+        m->deferred_gc_event_source = sd_event_source_unref(m->deferred_gc_event_source);
+        m->deferred_auto_login_event_source = sd_event_source_unref(m->deferred_auto_login_event_source);
+
+        if (m->private_key)
+                EVP_PKEY_free(m->private_key);
+
+        hashmap_free(m->public_keys);
+
+        varlink_server_unref(m->varlink_server);
+
+        return mfree(m);
+}
+
+int manager_verify_user_record(Manager *m, UserRecord *hr) {
+        EVP_PKEY *pkey;
+        Iterator i;
+        int r;
+
+        assert(m);
+        assert(hr);
+
+        if (!m->private_key && hashmap_isempty(m->public_keys)) {
+                r = user_record_has_signature(hr);
+                if (r < 0)
+                        return r;
+
+                return r ? -ENOKEY : USER_RECORD_UNSIGNED;
+        }
+
+        /* Is it our own? */
+        if (m->private_key) {
+                r = user_record_verify(hr, m->private_key);
+                switch (r) {
+
+                case USER_RECORD_FOREIGN:
+                        /* This record is not signed by this key, but let's see below */
+                        break;
+
+                case USER_RECORD_SIGNED:               /* Signed by us, but also by others, let's propagate that */
+                case USER_RECORD_SIGNED_EXCLUSIVE:     /* Signed by us, and nothing else, ditto */
+                case USER_RECORD_UNSIGNED:             /* Not signed at all, ditto  */
+                default:
+                        return r;
+                }
+        }
+
+        HASHMAP_FOREACH(pkey, m->public_keys, i) {
+                r = user_record_verify(hr, pkey);
+                switch (r) {
+
+                case USER_RECORD_FOREIGN:
+                        /* This record is not signed by this key, but let's see our other keys */
+                        break;
+
+                case USER_RECORD_SIGNED:            /* It's signed by this key we are happy with, but which is not our own. */
+                case USER_RECORD_SIGNED_EXCLUSIVE:
+                        return USER_RECORD_FOREIGN;
+
+                case USER_RECORD_UNSIGNED: /* It's not signed at all */
+                default:
+                        return r;
+                }
+        }
+
+        return -ENOKEY;
+}
+
+static int manager_add_home_by_record(
+                Manager *m,
+                const char *name,
+                int dir_fd,
+                const char *fname) {
+
+        _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+        _cleanup_(user_record_unrefp) UserRecord *hr;
+        unsigned line, column;
+        int r, is_signed;
+        Home *h;
+
+        assert(m);
+        assert(name);
+        assert(fname);
+
+        r = json_parse_file_at(NULL, dir_fd, fname, JSON_PARSE_SENSITIVE, &v, &line, &column);
+        if (r < 0)
+                return log_error_errno(r, "Failed to parse identity record at %s:%u%u: %m", fname, line, column);
+
+        hr = user_record_new();
+        if (!hr)
+                return log_oom();
+
+        r = user_record_load(hr, v, USER_RECORD_LOAD_REFUSE_SECRET);
+        if (r < 0)
+                return r;
+
+        if (!streq_ptr(hr->user_name, name))
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Identity's user name %s does not match file name %s, refusing.", hr->user_name, name);
+
+        is_signed = manager_verify_user_record(m, hr);
+        switch (is_signed) {
+
+        case -ENOKEY:
+                return log_warning_errno(is_signed, "User record %s is not signed by any accepted key, ignoring.", fname);
+        case USER_RECORD_UNSIGNED:
+                return log_warning_errno(SYNTHETIC_ERRNO(EPERM), "User record %s is not signed at all, ignoring.", fname);
+        case USER_RECORD_SIGNED:
+                log_info("User record %s is signed by us (and others), accepting.", fname);
+                break;
+        case USER_RECORD_SIGNED_EXCLUSIVE:
+                log_info("User record %s is signed only by us, accepting.", fname);
+                break;
+        case USER_RECORD_FOREIGN:
+                log_info("User record %s is signed by registered key from others, accepting.", fname);
+                break;
+        default:
+                assert(is_signed < 0);
+                return log_error_errno(is_signed, "Failed to verify signature of user record in %s: %m", fname);
+        }
+
+        h = hashmap_get(m->homes_by_name, name);
+        if (h) {
+                r = home_set_record(h, hr);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to update home record for %s: %m", name);
+
+                /* If we acquired a record now for a previously unallocated entry, then reset the state. This
+                 * makes sure home_get_state() will check for the availability of the image file dynamically
+                 * in order to detect to distuingish HOME_INACTIVE and HOME_ABSENT. */
+                if (h->state == HOME_UNFIXATED)
+                        h->state = _HOME_STATE_INVALID;
+        } else {
+                r = home_new(m, hr, NULL, &h);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to allocate new home object: %m");
+
+                log_info("Added registered home for user %s.", hr->user_name);
+        }
+
+        /* Only entries we exclusively signed are writable to us, hence remember the result */
+        h->signed_locally = is_signed == USER_RECORD_SIGNED_EXCLUSIVE;
+
+        return 1;
+}
+
+static int manager_enumerate_records(Manager *m) {
+        _cleanup_closedir_ DIR *d = NULL;
+        struct dirent *de;
+
+        assert(m);
+
+        d = opendir("/var/lib/systemd/home/");
+        if (!d)
+                return log_full_errno(errno == ENOENT ? LOG_DEBUG : LOG_ERR, errno,
+                                      "Failed to open /var/lib/systemd/home/: %m");
+
+        FOREACH_DIRENT(de, d, return log_error_errno(errno, "Failed to read record directory: %m")) {
+                _cleanup_free_ char *n = NULL;
+                const char *e;
+
+                if (!dirent_is_file(de))
+                        continue;
+
+                e = endswith(de->d_name, ".identity");
+                if (!e)
+                        continue;
+
+                n = strndup(de->d_name, e - de->d_name);
+                if (!n)
+                        return log_oom();
+
+                if (!suitable_user_name(n))
+                        continue;
+
+                (void) manager_add_home_by_record(m, n, dirfd(d), de->d_name);
+        }
+
+        return 0;
+}
+
+static int search_quota(uid_t uid, const char *exclude_quota_path) {
+        struct stat exclude_st = {};
+        dev_t previous_devno = 0;
+        const char *where;
+        int r;
+
+        /* Checks whether the specified UID owns any files on the files system, but ignore any file system
+         * backing the specified file. The file is used when operating on home directories, where it's OK if
+         * the UID of them already owns files. */
+
+        if (exclude_quota_path && stat(exclude_quota_path, &exclude_st) < 0) {
+                if (errno != ENOENT)
+                        return log_warning_errno(errno, "Failed to stat %s, ignoring: %m", exclude_quota_path);
+        }
+
+        /* Check a few usual suspects where regular users might own files. Note that this is by no means
+         * comprehensive, but should cover most cases. Note that in an ideal world every user would be
+         * registered in NSS and avoid our own UID range, but for all other cases, it's a good idea to be
+         * paranoid and check quota if we can. */
+        FOREACH_STRING(where, "/home/", "/tmp/", "/var/", "/var/mail/", "/var/tmp/", "/var/spool/") {
+                struct dqblk req;
+                struct stat st;
+
+                if (stat(where, &st) < 0) {
+                        log_full_errno(errno == ENOENT ? LOG_DEBUG : LOG_ERR, errno,
+                                       "Failed to stat %s, ignoring: %m", where);
+                        continue;
+                }
+
+                if (major(st.st_dev) == 0) {
+                        log_debug("Directory %s is not on a real block device, not checking quota for UID use.", where);
+                        continue;
+                }
+
+                if (st.st_dev == exclude_st.st_dev) { /* If an exclude path is specified, then ignore quota
+                                                       * reported on the same block device as that path. */
+                        log_debug("Directory %s is where the home directory is located, not checking quota for UID use.", where);
+                        continue;
+                }
+
+                if (st.st_dev == previous_devno) { /* Does this directory have the same devno as the previous
+                                                    * one we tested? If so, there's no point in testing this
+                                                    * again. */
+                        log_debug("Directory %s is on same device as previous tested directory, not checking quota for UID use a second time.", where);
+                        continue;
+                }
+
+                previous_devno = st.st_dev;
+
+                r = quotactl_devno(QCMD_FIXED(Q_GETQUOTA, USRQUOTA), st.st_dev, uid, &req);
+                if (r < 0) {
+                        if (ERRNO_IS_NOT_SUPPORTED(r))
+                                log_debug_errno(r, "No UID quota support on %s, ignoring.", where);
+                        else
+                                log_warning_errno(r, "Failed to query quota on %s, ignoring.", where);
+
+                        continue;
+                }
+
+                if ((FLAGS_SET(req.dqb_valid, QIF_SPACE) && req.dqb_curspace > 0) ||
+                    (FLAGS_SET(req.dqb_valid, QIF_INODES) && req.dqb_curinodes > 0)) {
+                        log_debug_errno(errno, "Quota reports UID " UID_FMT " occupies disk space on %s.", uid, where);
+                        return 1;
+                }
+        }
+
+        return 0;
+}
+
+static int manager_acquire_uid(
+                Manager *m,
+                uid_t start_uid,
+                const char *user_name,
+                const char *exclude_quota_path,
+                uid_t *ret) {
+
+        static const uint8_t hash_key[] = {
+                0xa3, 0xb8, 0x82, 0x69, 0x9a, 0x71, 0xf7, 0xa9,
+                0xe0, 0x7c, 0xf6, 0xf1, 0x21, 0x69, 0xd2, 0x1e
+        };
+
+        enum {
+                PHASE_SUGGESTED,
+                PHASE_HASHED,
+                PHASE_RANDOM
+        } phase = PHASE_SUGGESTED;
+
+        unsigned n_tries = 100;
+        int r;
+
+        assert(m);
+        assert(ret);
+
+        for (;;) {
+                struct passwd *pw;
+                struct group *gr;
+                uid_t candidate;
+                Home *other;
+
+                if (--n_tries <= 0)
+                        return -EBUSY;
+
+                switch (phase) {
+
+                case PHASE_SUGGESTED:
+                        phase = PHASE_HASHED;
+
+                        if (!uid_is_home(start_uid))
+                                continue;
+
+                        candidate = start_uid;
+                        break;
+
+                case PHASE_HASHED:
+                        phase = PHASE_RANDOM;
+
+                        if (!user_name)
+                                continue;
+
+                        candidate = UID_CLAMP_INTO_HOME_RANGE(siphash24(user_name, strlen(user_name), hash_key));
+                        break;
+
+                case PHASE_RANDOM:
+                        random_bytes(&candidate, sizeof(candidate));
+                        candidate = UID_CLAMP_INTO_HOME_RANGE(candidate);
+                        break;
+
+                default:
+                        assert_not_reached("unknown phase");
+                }
+
+                other = hashmap_get(m->homes_by_uid, UID_TO_PTR(candidate));
+                if (other) {
+                        log_debug("Candidate UID " UID_FMT " already used by another home directory (%s), let's try another.", candidate, other->user_name);
+                        continue;
+                }
+
+                pw = getpwuid(candidate);
+                if (pw) {
+                        log_debug("Candidate UID " UID_FMT " already registered by another user in NSS (%s), let's try another.", candidate, pw->pw_name);
+                        continue;
+                }
+
+                gr = getgrgid((gid_t) candidate);
+                if (gr) {
+                        log_debug("Candidate UID " UID_FMT " already registered by another group in NSS (%s), let's try another.", candidate, gr->gr_name);
+                        continue;
+                }
+
+                r = search_ipc(candidate, (gid_t) candidate);
+                if (r < 0)
+                        continue;
+                if (r > 0) {
+                        log_debug_errno(r, "Candidate UID " UID_FMT " already owns IPC objects, let's try another: %m", candidate);
+                        continue;
+                }
+
+                r = search_quota(candidate, exclude_quota_path);
+                if (r != 0)
+                        continue;
+
+                *ret = candidate;
+                return 0;
+        }
+}
+
+static int manager_add_home_by_image(
+                Manager *m,
+                const char *user_name,
+                const char *realm,
+                const char *image_path,
+                const char *sysfs,
+                UserStorage storage,
+                uid_t start_uid) {
+
+        _cleanup_(user_record_unrefp) UserRecord *hr = NULL;
+        uid_t uid;
+        Home *h;
+        int r;
+
+        assert(m);
+
+        assert(m);
+        assert(user_name);
+        assert(image_path);
+        assert(storage >= 0);
+        assert(storage < _USER_STORAGE_MAX);
+
+        h = hashmap_get(m->homes_by_name, user_name);
+        if (h) {
+                bool same;
+
+                if (h->state != HOME_UNFIXATED) {
+                        log_debug("Found an image for user %s which already has a record, skipping.", user_name);
+                        return 0; /* ignore images that synthesize a user we already have a record for */
+                }
+
+                same = user_record_storage(h->record) == storage;
+                if (same) {
+                        if (h->sysfs && sysfs)
+                                same = path_equal(h->sysfs, sysfs);
+                        else if (!!h->sysfs != !!sysfs)
+                                same = false;
+                        else {
+                                const char *p;
+
+                                p = user_record_image_path(h->record);
+                                same = p && path_equal(p, image_path);
+                        }
+                }
+
+                if (!same) {
+                        log_debug("Found a multiple images for a user '%s', ignoring image '%s'.", user_name, image_path);
+                        return 0;
+                }
+        } else {
+                /* Check NSS, in case there's another user or group by this name */
+                if (getpwnam(user_name) || getgrnam(user_name)) {
+                        log_debug("Found an existing user or group by name '%s', ignoring image '%s'.", user_name, image_path);
+                        return 0;
+                }
+        }
+
+        if (h && uid_is_valid(h->uid))
+                uid = h->uid;
+        else {
+                r = manager_acquire_uid(m, start_uid, user_name, IN_SET(storage, USER_SUBVOLUME, USER_DIRECTORY, USER_FSCRYPT) ? image_path : NULL, &uid);
+                if (r < 0)
+                        return log_warning_errno(r, "Failed to acquire unused UID for %s: %m", user_name);
+        }
+
+        hr = user_record_new();
+        if (!hr)
+                return log_oom();
+
+        r = user_record_synthesize(hr, user_name, realm, image_path, storage, uid, (gid_t) uid);
+        if (r < 0)
+                return log_error_errno(r, "Failed to synthesize home record for %s (image %s): %m", user_name, image_path);
+
+        if (h) {
+                r = home_set_record(h, hr);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to update home record for %s: %m", user_name);
+        } else {
+                r = home_new(m, hr, sysfs, &h);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to allocate new home object: %m");
+
+                h->state = HOME_UNFIXATED;
+
+                log_info("Discovered new home for user %s through image %s.", user_name, image_path);
+        }
+
+        return 1;
+}
+
+int manager_augment_record_with_uid(
+                Manager *m,
+                UserRecord *hr) {
+
+        const char *exclude_quota_path = NULL;
+        uid_t start_uid = UID_INVALID, uid;
+        int r;
+
+        assert(m);
+        assert(hr);
+
+        if (uid_is_valid(hr->uid))
+                return 0;
+
+        if (IN_SET(hr->storage, USER_CLASSIC, USER_SUBVOLUME, USER_DIRECTORY, USER_FSCRYPT)) {
+                const char * ip;
+
+                ip = user_record_image_path(hr);
+                if (ip) {
+                        struct stat st;
+
+                        if (stat(ip, &st) < 0) {
+                                if (errno != ENOENT)
+                                        log_warning_errno(errno, "Failed to stat(%s): %m", ip);
+                        }  else if (uid_is_home(st.st_uid)) {
+                                start_uid = st.st_uid;
+                                exclude_quota_path = ip;
+                        }
+                }
+        }
+
+        r = manager_acquire_uid(m, start_uid, hr->user_name, exclude_quota_path, &uid);
+        if (r < 0)
+                return r;
+
+        log_debug("Acquired new UID " UID_FMT " for %s.", uid, hr->user_name);
+
+        r = user_record_add_binding(
+                        hr,
+                        _USER_STORAGE_INVALID,
+                        NULL,
+                        SD_ID128_NULL,
+                        SD_ID128_NULL,
+                        SD_ID128_NULL,
+                        NULL,
+                        NULL,
+                        UINT64_MAX,
+                        NULL,
+                        NULL,
+                        uid,
+                        (gid_t) uid);
+        if (r < 0)
+                return r;
+
+        return 1;
+}
+
+static int manager_assess_image(
+                Manager *m,
+                int dir_fd,
+                const char *dir_path,
+                const char *dentry_name) {
+
+        char *luks_suffix, *directory_suffix;
+        _cleanup_free_ char *path = NULL;
+        struct stat st;
+        int r;
+
+        assert(m);
+        assert(dir_path);
+        assert(dentry_name);
+
+        luks_suffix = endswith(dentry_name, ".home");
+        if (luks_suffix)
+                directory_suffix = NULL;
+        else
+                directory_suffix = endswith(dentry_name, ".homedir");
+
+        /* Early filter out: by name */
+        if (!luks_suffix && !directory_suffix)
+                return 0;
+
+        path = path_join(dir_path, dentry_name);
+        if (!path)
+                return log_oom();
+
+        /* Follow symlinks here, to allow people to link in stuff to make them available locally. */
+        if (dir_fd >= 0)
+                r = fstatat(dir_fd, dentry_name, &st, 0);
+        else
+                r = stat(path, &st);
+        if (r < 0)
+                return log_full_errno(errno == ENOENT ? LOG_DEBUG : LOG_WARNING, errno,
+                                      "Failed to stat directory entry '%s', ignoring: %m", dentry_name);
+
+        if (S_ISREG(st.st_mode)) {
+                _cleanup_free_ char *n = NULL, *user_name = NULL, *realm = NULL;
+
+                if (!luks_suffix)
+                        return 0;
+
+                n = strndup(dentry_name, luks_suffix - dentry_name);
+                if (!n)
+                        return log_oom();
+
+                r = split_user_name_realm(n, &user_name, &realm);
+                if (r == -EINVAL) /* Not the right format: ignore */
+                        return 0;
+                if (r < 0)
+                        return log_error_errno(r, "Failed to split image name into user name/realm: %m");
+
+                return manager_add_home_by_image(m, user_name, realm, path, NULL, USER_LUKS, UID_INVALID);
+        }
+
+        if (S_ISDIR(st.st_mode)) {
+                _cleanup_free_ char *n = NULL, *user_name = NULL, *realm = NULL;
+                _cleanup_close_ int fd = -1;
+                UserStorage storage;
+
+                if (!directory_suffix)
+                        return 0;
+
+                n = strndup(dentry_name, directory_suffix - dentry_name);
+                if (!n)
+                        return log_oom();
+
+                r = split_user_name_realm(n, &user_name, &realm);
+                if (r == -EINVAL) /* Not the right format: ignore */
+                        return 0;
+                if (r < 0)
+                        return log_error_errno(r, "Failed to split image name into user name/realm: %m");
+
+                if (dir_fd >= 0)
+                        fd = openat(dir_fd, dentry_name, O_DIRECTORY|O_RDONLY|O_CLOEXEC);
+                else
+                        fd = open(path, O_DIRECTORY|O_RDONLY|O_CLOEXEC);
+                if (fd < 0)
+                        return log_full_errno(errno == ENOENT ? LOG_DEBUG : LOG_WARNING, errno,
+                                              "Failed to open directory '%s', ignoring: %m", path);
+
+                if (fstat(fd, &st) < 0)
+                        return log_warning_errno(errno, "Failed to fstat() %s, ignoring: %m", path);
+
+                assert(S_ISDIR(st.st_mode)); /* Must hold, we used O_DIRECTORY above */
+
+                r = btrfs_is_subvol_fd(fd);
+                if (r < 0)
+                        return log_warning_errno(errno, "Failed to determine whether %s is a btrfs subvolume: %m", path);
+                if (r > 0)
+                        storage = USER_SUBVOLUME;
+                else {
+                        struct fscrypt_policy policy;
+
+                        if (ioctl(fd, FS_IOC_GET_ENCRYPTION_POLICY, &policy) < 0) {
+
+                                if (errno == ENODATA)
+                                        log_debug_errno(errno, "Determined %s is not fscrypt encrypted.", path);
+                                else if (ERRNO_IS_NOT_SUPPORTED(errno))
+                                        log_debug_errno(errno, "Determined %s is not fscrypt encrypted because kernel or file system don't support it.", path);
+                                else
+                                        log_debug_errno(errno, "FS_IOC_GET_ENCRYPTION_POLICY failed with unexpected error code on %s, ignoring: %m", path);
+
+                                storage = USER_DIRECTORY;
+                        } else
+                                storage = USER_FSCRYPT;
+                }
+
+                return manager_add_home_by_image(m, user_name, realm, path, NULL, storage, st.st_uid);
+        }
+
+        return 0;
+}
+
+int manager_enumerate_images(Manager *m) {
+        _cleanup_closedir_ DIR *d = NULL;
+        struct dirent *de;
+
+        assert(m);
+
+        if (!m->scan_slash_home)
+                return 0;
+
+        d = opendir("/home/");
+        if (!d)
+                return log_full_errno(errno == ENOENT ? LOG_DEBUG : LOG_ERR, errno,
+                                      "Failed to open /home/: %m");
+
+        FOREACH_DIRENT(de, d, return log_error_errno(errno, "Failed to read /home/ directory: %m"))
+                (void) manager_assess_image(m, dirfd(d), "/home", de->d_name);
+
+        return 0;
+}
+
+static int manager_connect_bus(Manager *m) {
+        int r;
+
+        assert(m);
+        assert(!m->bus);
+
+        r = sd_bus_default_system(&m->bus);
+        if (r < 0)
+                return log_error_errno(r, "Failed to connect to system bus: %m");
+
+        r = sd_bus_add_object_vtable(m->bus, NULL, "/org/freedesktop/home1", "org.freedesktop.home1.Manager", manager_vtable, m);
+        if (r < 0)
+                return log_error_errno(r, "Failed to add manager object vtable: %m");
+
+        r = sd_bus_add_fallback_vtable(m->bus, NULL, "/org/freedesktop/home1/home", "org.freedesktop.home1.Home", home_vtable, bus_home_object_find, m);
+        if (r < 0)
+                return log_error_errno(r, "Failed to add image object vtable: %m");
+
+        r = sd_bus_add_node_enumerator(m->bus, NULL, "/org/freedesktop/home1/home", bus_home_node_enumerator, m);
+        if (r < 0)
+                return log_error_errno(r, "Failed to add image enumerator: %m");
+
+        r = sd_bus_add_object_manager(m->bus, NULL, "/org/freedesktop/home1/home");
+        if (r < 0)
+                return log_error_errno(r, "Failed to add object manager: %m");
+
+        r = sd_bus_request_name_async(m->bus, NULL, "org.freedesktop.home1", 0, NULL, NULL);
+        if (r < 0)
+                return log_error_errno(r, "Failed to request name: %m");
+
+        r = sd_bus_attach_event(m->bus, m->event, 0);
+        if (r < 0)
+                return log_error_errno(r, "Failed to attach bus to event loop: %m");
+
+        (void) sd_bus_set_exit_on_disconnect(m->bus, true);
+
+        return 0;
+}
+
+static int manager_bind_varlink(Manager *m) {
+        int r;
+
+        assert(m);
+        assert(!m->varlink_server);
+
+        r = varlink_server_new(&m->varlink_server, VARLINK_SERVER_ACCOUNT_UID);
+        if (r < 0)
+                return log_error_errno(r, "Failed to allocate varlink server object: %m");
+
+        varlink_server_set_userdata(m->varlink_server, m);
+
+        r = varlink_server_bind_method_many(
+                        m->varlink_server,
+                        "io.systemd.UserDatabase.GetUserRecord",  vl_method_get_user_record,
+                        "io.systemd.UserDatabase.GetGroupRecord", vl_method_get_group_record,
+                        "io.systemd.UserDatabase.GetMemberships", vl_method_get_memberships);
+        if (r < 0)
+                return log_error_errno(r, "Failed to register varlink methods: %m");
+
+        (void) mkdir_p("/run/systemd/userdb", 0755);
+
+        r = varlink_server_listen_address(m->varlink_server, "/run/systemd/userdb/io.systemd.Home", 0666);
+        if (r < 0)
+                return log_error_errno(r, "Failed to bind to varlink socket: %m");
+
+        r = varlink_server_attach_event(m->varlink_server, m->event, SD_EVENT_PRIORITY_NORMAL);
+        if (r < 0)
+                return log_error_errno(r, "Failed to attach varlink connection to event loop: %m");
+
+        return 0;
+}
+
+static ssize_t read_datagram(int fd, struct ucred *ret_sender, void **ret) {
+        _cleanup_free_ void *buffer = NULL;
+        ssize_t n, m;
+
+        assert(fd >= 0);
+        assert(ret_sender);
+        assert(ret);
+
+        n = next_datagram_size_fd(fd);
+        if (n < 0)
+                return n;
+
+        buffer = malloc(n + 2);
+        if (!buffer)
+                return -ENOMEM;
+
+        if (ret_sender) {
+                union {
+                        struct cmsghdr cmsghdr;
+                        uint8_t buf[CMSG_SPACE(sizeof(struct ucred))];
+                } control;
+                bool found_ucred = false;
+                struct cmsghdr *cmsg;
+                struct msghdr mh;
+                struct iovec iov;
+
+                /* Pass one extra byte, as a size check */
+                iov = IOVEC_MAKE(buffer, n + 1);
+
+                mh = (struct msghdr) {
+                        .msg_iov = &iov,
+                        .msg_iovlen = 1,
+                        .msg_control = &control,
+                        .msg_controllen = sizeof(control),
+                };
+
+                m = recvmsg(fd, &mh, MSG_DONTWAIT|MSG_CMSG_CLOEXEC);
+                if (m < 0)
+                        return -errno;
+
+                cmsg_close_all(&mh);
+
+                /* Ensure the size matches what we determined before */
+                if (m != n)
+                        return -EMSGSIZE;
+
+                CMSG_FOREACH(cmsg, &mh)
+                        if (cmsg->cmsg_level == SOL_SOCKET &&
+                            cmsg->cmsg_type == SCM_CREDENTIALS &&
+                            cmsg->cmsg_len == CMSG_LEN(sizeof(struct ucred))) {
+
+                                memcpy(ret_sender, CMSG_DATA(cmsg), sizeof(struct ucred));
+                                found_ucred = true;
+                        }
+
+                if (!found_ucred)
+                        *ret_sender = (struct ucred) {
+                                .pid = 0,
+                                .uid = UID_INVALID,
+                                .gid = GID_INVALID,
+                        };
+        } else {
+                m = recv(fd, buffer, n + 1, MSG_DONTWAIT);
+                if (m < 0)
+                        return -errno;
+
+                /* Ensure the size matches what we determined before */
+                if (m != n)
+                        return -EMSGSIZE;
+        }
+
+        /* For safety reasons: let's always NUL terminate.  */
+        ((char*) buffer)[n] = 0;
+        *ret = TAKE_PTR(buffer);
+
+        return 0;
+}
+
+static int on_notify_socket(sd_event_source *s, int fd, uint32_t revents, void *userdata) {
+        _cleanup_strv_free_ char **l = NULL;
+        _cleanup_free_ void *datagram = NULL;
+        struct ucred sender;
+        Manager *m = userdata;
+        ssize_t n;
+        Home *h;
+
+        assert(s);
+        assert(m);
+
+        n = read_datagram(fd, &sender, &datagram);
+        if (IN_SET(n, -EAGAIN, -EINTR))
+                return 0;
+        if (n < 0)
+                return log_error_errno(n, "Failed to read notify datagram: %m");
+
+        if (sender.pid <= 0) {
+                log_warning("Received notify datagram without valid sender PID, ignoring.");
+                return 0;
+        }
+
+        h = hashmap_get(m->homes_by_worker_pid, PID_TO_PTR(sender.pid));
+        if (!h) {
+                log_warning("Recieved notify datagram of unknown process, ignoring.");
+                return 0;
+        }
+
+        l = strv_split(datagram, "\n");
+        if (!l)
+                return log_oom();
+
+        home_process_notify(h, l);
+        return 0;
+}
+
+static int manager_listen_notify(Manager *m) {
+        _cleanup_close_ int fd = -1;
+        union sockaddr_union sa;
+        int r;
+
+        assert(m);
+        assert(!m->notify_socket_event_source);
+
+        fd = socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0);
+        if (fd < 0)
+                return log_error_errno(errno, "Failed to create listening socket: %m");
+
+        r = sockaddr_un_set_path(&sa.un, "/run/systemd/home/notify");
+        if (r < 0)
+                return log_error_errno(r, "Failed to set AF_UNIX socket path: %m");
+
+        (void) mkdir_parents(sa.un.sun_path, 0755);
+        (void) sockaddr_un_unlink(&sa.un);
+
+        if (bind(fd, &sa.sa, SOCKADDR_UN_LEN(sa.un)) < 0)
+                return log_error_errno(errno, "Failed to bind to socket: %m");
+
+        r = setsockopt_int(fd, SOL_SOCKET, SO_PASSCRED, true);
+        if (r < 0)
+                return r;
+
+        r = sd_event_add_io(m->event, &m->notify_socket_event_source, fd, EPOLLIN, on_notify_socket, m);
+        if (r < 0)
+                return log_error_errno(r, "Failed to allocate event source for notify socket: %m");
+
+        (void) sd_event_source_set_description(m->notify_socket_event_source, "notify-socket");
+
+        /* Make sure we process sd_notify() before SIGCHLD for any worker, so that we always know the error
+         * number of a client before it exits. */
+        r = sd_event_source_set_priority(m->notify_socket_event_source, SD_EVENT_PRIORITY_NORMAL - 5);
+        if (r < 0)
+                return log_error_errno(r, "Failed to alter priority of NOTIFY_SOCKET event source: %m");
+
+        r = sd_event_source_set_io_fd_own(m->notify_socket_event_source, true);
+        if (r < 0)
+                return log_error_errno(r, "Failed to pass ownership of notify socket: %m");
+
+        return TAKE_FD(fd);
+}
+
+static int manager_add_device(Manager *m, sd_device *d) {
+        _cleanup_free_ char *user_name = NULL, *realm = NULL, *node = NULL;
+        const char *tabletype, *parttype, *partname, *partuuid, *sysfs;
+        sd_id128_t id;
+        int r;
+
+        assert(m);
+        assert(d);
+
+        r = sd_device_get_syspath(d, &sysfs);
+        if (r < 0)
+                return log_error_errno(r, "Failed to acquire sysfs path of device: %m");
+
+        r = sd_device_get_property_value(d, "ID_PART_TABLE_TYPE", &tabletype);
+        if (r == -ENOENT)
+                return 0;
+        if (r < 0)
+                return log_error_errno(r, "Failed to acquire ID_PART_TABLE_TYPE device property, ignoring: %m");
+
+        if (!streq(tabletype, "gpt")) {
+                log_debug("Found partition (%s) on non-GPT table, ignoring.", sysfs);
+                return 0;
+        }
+
+        r = sd_device_get_property_value(d, "ID_PART_ENTRY_TYPE", &parttype);
+        if (r == -ENOENT)
+                return 0;
+        if (r < 0)
+                return log_error_errno(r, "Failed to acquire ID_PART_ENTRY_TYPE device property, ignoring: %m");
+        r = sd_id128_from_string(parttype, &id);
+        if (r < 0)
+                return log_debug_errno(r, "Failed to parse ID_PART_ENTRY_TYPE field '%s', ignoring: %m", parttype);
+        if (!sd_id128_equal(id, GPT_USER_HOME)) {
+                log_debug("Found partition (%s) we don't care about, ignoring.", sysfs);
+                return 0;
+        }
+
+        r = sd_device_get_property_value(d, "ID_PART_ENTRY_NAME", &partname);
+        if (r < 0)
+                return log_warning_errno(r, "Failed to acquire ID_PART_ENTRY_NAME device property, ignoring: %m");
+
+        r = split_user_name_realm(partname, &user_name, &realm);
+        if (r == -EINVAL)
+                return log_warning_errno(r, "Found partition with correct partition type but a non-parsable partition name '%s', ignoring.", partname);
+        if (r < 0)
+                return log_error_errno(r, "Failed to validate partition name '%s': %m", partname);
+
+        r = sd_device_get_property_value(d, "ID_FS_UUID", &partuuid);
+        if (r < 0)
+                return log_warning_errno(r, "Failed to acquire ID_FS_UUID device property, ignoring: %m");
+
+        r = sd_id128_from_string(partuuid, &id);
+        if (r < 0)
+                return log_warning_errno(r, "Failed to parse ID_FS_UUID field '%s', ignoring: %m", partuuid);
+
+        if (asprintf(&node, "/dev/disk/by-uuid/" SD_ID128_UUID_FORMAT_STR, SD_ID128_FORMAT_VAL(id)) < 0)
+                return log_oom();
+
+        return manager_add_home_by_image(m, user_name, realm, node, sysfs, USER_LUKS, UID_INVALID);
+}
+
+static int manager_on_device(sd_device_monitor *monitor, sd_device *d, void *userdata) {
+        Manager *m = userdata;
+        int r;
+
+        assert(m);
+        assert(d);
+
+        if (device_for_action(d, DEVICE_ACTION_REMOVE)) {
+                const char *sysfs;
+                Home *h;
+
+                r = sd_device_get_syspath(d, &sysfs);
+                if (r < 0) {
+                        log_warning_errno(r, "Failed to acquire sysfs path from device: %m");
+                        return 0;
+                }
+
+                log_info("block device %s has been removed.", sysfs);
+
+                /* Let's see if we previously synthesized a home record from this device, if so, let's just
+                 * revalidate that. Otherwise let's revalidate them all, but asynchronously. */
+                h = hashmap_get(m->homes_by_sysfs, sysfs);
+                if (h)
+                        manager_revalidate_image(m, h);
+                else
+                        manager_enqueue_gc(m, NULL);
+        } else
+                (void) manager_add_device(m, d);
+
+        (void) bus_manager_emit_auto_login_changed(m);
+        return 0;
+}
+
+static int manager_watch_devices(Manager *m) {
+        int r;
+
+        assert(m);
+        assert(!m->device_monitor);
+
+        r = sd_device_monitor_new(&m->device_monitor);
+        if (r < 0)
+                return log_error_errno(r, "Failed to allocate device monitor: %m");
+
+        r = sd_device_monitor_filter_add_match_subsystem_devtype(m->device_monitor, "block", NULL);
+        if (r < 0)
+                return log_error_errno(r, "Failed to configure device monitor match: %m");
+
+        r = sd_device_monitor_attach_event(m->device_monitor, m->event);
+        if (r < 0)
+                return log_error_errno(r, "Failed to attach device monitor to event loop: %m");
+
+        r = sd_device_monitor_start(m->device_monitor, manager_on_device, m);
+        if (r < 0)
+                return log_error_errno(r, "Failed to start device monitor: %m");
+
+        return 0;
+}
+
+static int manager_enumerate_devices(Manager *m) {
+        _cleanup_(sd_device_enumerator_unrefp) sd_device_enumerator *e = NULL;
+        sd_device *d;
+        int r;
+
+        assert(m);
+
+        r = sd_device_enumerator_new(&e);
+        if (r < 0)
+                return r;
+
+        r = sd_device_enumerator_add_match_subsystem(e, "block", true);
+        if (r < 0)
+                return r;
+
+        FOREACH_DEVICE(e, d)
+                (void) manager_add_device(m, d);
+
+        return 0;
+}
+
+static int manager_load_key_pair(Manager *m) {
+        _cleanup_(fclosep) FILE *f = NULL;
+        struct stat st;
+        int r;
+
+        assert(m);
+
+        if (m->private_key) {
+                EVP_PKEY_free(m->private_key);
+                m->private_key = NULL;
+        }
+
+        r = search_and_fopen_nulstr("local.private", "re", NULL, KEY_PATHS_NULSTR, &f);
+        if (r == -ENOENT)
+                return 0;
+        if (r < 0)
+                return log_error_errno(r, "Failed to read private key file: %m");
+
+        if (fstat(fileno(f), &st) < 0)
+                return log_error_errno(errno, "Failed to stat private key file: %m");
+
+        r = stat_verify_regular(&st);
+        if (r < 0)
+                return log_error_errno(r, "Private key file is not regular: %m");
+
+        if (st.st_uid != 0 || (st.st_mode & 0077) != 0)
+                return log_error_errno(SYNTHETIC_ERRNO(EPERM), "Private key file is readable by more than the root user");
+
+        m->private_key = PEM_read_PrivateKey(f, NULL, NULL, NULL);
+        if (!m->private_key)
+                return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to load private key pair");
+
+        log_info("Successfully loaded private key pair.");
+
+        return 1;
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(EVP_PKEY_CTX*, EVP_PKEY_CTX_free);
+
+static int manager_generate_key_pair(Manager *m) {
+        _cleanup_(EVP_PKEY_CTX_freep) EVP_PKEY_CTX *ctx = NULL;
+        _cleanup_(unlink_and_freep) char *temp_public = NULL, *temp_private = NULL;
+        _cleanup_fclose_ FILE *fpublic = NULL, *fprivate = NULL;
+        int r;
+
+        if (m->private_key) {
+                EVP_PKEY_free(m->private_key);
+                m->private_key = NULL;
+        }
+
+        ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_ED25519, NULL);
+        if (!ctx)
+                return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to allocate Ed25519 key generation context.");
+
+        if (EVP_PKEY_keygen_init(ctx) <= 0)
+                return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to initialize Ed25519 key generation context.");
+
+        log_info("Generating key pair for signing local user identity records.");
+
+        if (EVP_PKEY_keygen(ctx, &m->private_key) <= 0)
+                return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to generate Ed25519 key pair");
+
+        log_info("Successfully created Ed25519 key pair.");
+
+        (void) mkdir_p("/var/lib/systemd/home", 0755);
+
+        /* Write out public key (note that we only do that as a help to the user, we don't make use of this ever */
+        r = fopen_temporary("/var/lib/systemd/home/local.public", &fpublic, &temp_public);
+        if (r < 0)
+                return log_error_errno(errno, "Failed ot open key file for writing: %m");
+
+        if (PEM_write_PUBKEY(fpublic, m->private_key) <= 0)
+                return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to write public key.");
+
+        r = fflush_and_check(fpublic);
+        if (r < 0)
+                return log_error_errno(r, "Failed to write private key: %m");
+
+        fpublic = safe_fclose(fpublic);
+
+        /* Write out the private key (this actually writes out both private and public, OpenSSL is confusing) */
+        r = fopen_temporary("/var/lib/systemd/home/local.private", &fprivate, &temp_private);
+        if (r < 0)
+                return log_error_errno(errno, "Failed ot open key file for writing: %m");
+
+        if (PEM_write_PrivateKey(fprivate, m->private_key, NULL, NULL, 0, NULL, 0) <= 0)
+                return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to write private key pair.");
+
+        r = fflush_and_check(fprivate);
+        if (r < 0)
+                return log_error_errno(r, "Failed to write private key: %m");
+
+        fprivate = safe_fclose(fprivate);
+
+        /* Both are written now, move them into place */
+
+        if (rename(temp_public, "/var/lib/systemd/home/local.public") < 0)
+                return log_error_errno(errno, "Failed to move public key file into place: %m");
+        temp_public = mfree(temp_public);
+
+        if (rename(temp_private, "/var/lib/systemd/home/local.private") < 0) {
+                (void) unlink_noerrno("/var/lib/systemd/home/local.public"); /* try to remove the file we already created */
+                return log_error_errno(errno, "Failed to move privtate key file into place: %m");
+        }
+        temp_private = mfree(temp_private);
+
+        return 1;
+}
+
+int manager_acquire_key_pair(Manager *m) {
+        int r;
+
+        assert(m);
+
+        /* Already there? */
+        if (m->private_key)
+                return 1;
+
+        /* First try to load key off disk */
+        r = manager_load_key_pair(m);
+        if (r != 0)
+                return r;
+
+        /* Didn't work, generate a new one */
+        return manager_generate_key_pair(m);
+}
+
+int manager_sign_user_record(Manager *m, UserRecord *u, UserRecord **ret, sd_bus_error *error) {
+        int r;
+
+        assert(m);
+        assert(u);
+        assert(ret);
+
+        r = manager_acquire_key_pair(m);
+        if (r < 0)
+                return r;
+        if (r == 0)
+                return sd_bus_error_setf(error, BUS_ERROR_NO_PRIVATE_KEY, "Can't sign without local key.");
+
+        return user_record_sign(u, m->private_key, ret);
+}
+
+DEFINE_PRIVATE_HASH_OPS_FULL(public_key_hash_ops, char, string_hash_func, string_compare_func, free, EVP_PKEY, EVP_PKEY_free);
+DEFINE_TRIVIAL_CLEANUP_FUNC(EVP_PKEY*, EVP_PKEY_free);
+
+static int manager_load_public_key_one(Manager *m, const char *path) {
+        _cleanup_(EVP_PKEY_freep) EVP_PKEY *pkey = NULL;
+        _cleanup_fclose_ FILE *f = NULL;
+        _cleanup_free_ char *fn = NULL;
+        struct stat st;
+        int r;
+
+        assert(m);
+
+        if (streq(basename(path), "local.public")) /* we already loaded the private key, which includes the public one */
+                return 0;
+
+        f = fopen(path, "re");
+        if (!f) {
+                if (errno == ENOENT)
+                        return 0;
+
+                return log_error_errno(errno, "Failed to open public key %s: %m", path);
+        }
+
+        if (fstat(fileno(f), &st) < 0)
+                return log_error_errno(errno, "Failed to stat public key %s: %m", path);
+
+        r = stat_verify_regular(&st);
+        if (r < 0)
+                return log_error_errno(r, "Public key file %s is not a regular file: %m", path);
+
+        if (st.st_uid != 0 || (st.st_mode & 0022) != 0)
+                return log_error_errno(SYNTHETIC_ERRNO(EPERM), "Public key file %s is writable by more than the root user, refusing.", path);
+
+        r = hashmap_ensure_allocated(&m->public_keys, &public_key_hash_ops);
+        if (r < 0)
+                return log_oom();
+
+        pkey = PEM_read_PUBKEY(f, &pkey, NULL, NULL);
+        if (!pkey)
+                return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to parse public key file %s.", path);
+
+        fn = strdup(basename(path));
+        if (!fn)
+                return log_oom();
+
+        r = hashmap_put(m->public_keys, fn, pkey);
+        if (r < 0)
+                return log_error_errno(r, "Failed to add public key to set: %m");
+
+        TAKE_PTR(fn);
+        TAKE_PTR(pkey);
+
+        return 0;
+}
+
+static int manager_load_public_keys(Manager *m) {
+        _cleanup_strv_free_ char **files = NULL;
+        char **i;
+        int r;
+
+        assert(m);
+
+        m->public_keys = hashmap_free(m->public_keys);
+
+        r = conf_files_list_nulstr(
+                        &files,
+                        ".public",
+                        NULL,
+                        CONF_FILES_REGULAR|CONF_FILES_FILTER_MASKED,
+                        KEY_PATHS_NULSTR);
+        if (r < 0)
+                return log_error_errno(r, "Failed to assemble list of public key directories: %m");
+
+        STRV_FOREACH(i, files)
+                (void) manager_load_public_key_one(m, *i);
+
+        return 0;
+}
+
+int manager_startup(Manager *m) {
+        int r;
+
+        assert(m);
+
+        r = manager_listen_notify(m);
+        if (r < 0)
+                return r;
+
+        r = manager_connect_bus(m);
+        if (r < 0)
+                return r;
+
+        r = manager_bind_varlink(m);
+        if (r < 0)
+                return r;
+
+        r = manager_load_key_pair(m); /* only try to load it, don't generate any */
+        if (r < 0)
+                return r;
+
+        r = manager_load_public_keys(m);
+        if (r < 0)
+                return r;
+
+        manager_watch_home(m);
+        (void) manager_watch_devices(m);
+
+        (void) manager_enumerate_records(m);
+        (void) manager_enumerate_images(m);
+        (void) manager_enumerate_devices(m);
+
+        /* Let's clean up home directories whose devices got removed while we were not running */
+        (void) manager_enqueue_gc(m, NULL);
+
+        return 0;
+}
+
+void manager_revalidate_image(Manager *m, Home *h) {
+        int r;
+
+        assert(m);
+        assert(h);
+
+        /* Frees an automatically discovered image, if it's synthetic and its image disappeared. Unmounts any
+         * image if it's mounted but it's image vanished. */
+
+        if (h->current_operation || !ordered_set_isempty(h->pending_operations))
+                return;
+
+        if (h->state == HOME_UNFIXATED) {
+                r = user_record_test_image_path(h->record);
+                if (r < 0)
+                        log_warning_errno(r, "Can't determine if image of %s exists, freeing unfixated user: %m", h->user_name);
+                else if (r == USER_TEST_ABSENT)
+                        log_info("Image for %s disappeared, freeing unfixated user.", h->user_name);
+                else
+                        return;
+
+                home_free(h);
+
+        } else if (h->state < 0) {
+
+                r = user_record_test_home_directory(h->record);
+                if (r < 0) {
+                        log_warning_errno(r, "Unable to determine state of home directory, ignoring: %m");
+                        return;
+                }
+
+                if (r == USER_TEST_MOUNTED) {
+                        r = user_record_test_image_path(h->record);
+                        if (r < 0) {
+                                log_warning_errno(r, "Unable to determine state of image path, ignoring: %m");
+                                return;
+                        }
+
+                        if (r == USER_TEST_ABSENT) {
+                                _cleanup_(operation_unrefp) Operation *o = NULL;
+
+                                log_notice("Backing image disappeared while home directory %s was mounted, unmounting it forcibly.", h->user_name);
+                                /* Wowza, the thing is mounted, but the device is gone? Act on it. */
+
+                                r = home_killall(h);
+                                if (r < 0)
+                                        log_warning_errno(r, "Failed to kill processes of user %s, ignoring: %m", h->user_name);
+
+                                /* We enqueue the operation here, after all the home directory might
+                                 * currently already run some operation, and we can deactivate it only after
+                                 * that's complete. */
+                                o = operation_new(OPERATION_DEACTIVATE_FORCE, NULL);
+                                if (!o) {
+                                        log_oom();
+                                        return;
+                                }
+
+                                r = home_schedule_operation(h, o, NULL);
+                                if (r < 0)
+                                        log_warning_errno(r, "Failed to enqueue forced home directory %s deactivation, ignoring: %m", h->user_name);
+                        }
+                }
+        }
+}
+
+int manager_gc_images(Manager *m) {
+        Home *h;
+
+        assert_se(m);
+
+        if (m->gc_focus) {
+                /* Focus on a specific home */
+
+                h = TAKE_PTR(m->gc_focus);
+                manager_revalidate_image(m, h);
+        } else {
+                /* Gc all */
+                Iterator i;
+
+                HASHMAP_FOREACH(h, m->homes_by_name, i)
+                        manager_revalidate_image(m, h);
+        }
+
+        return 0;
+}
+
+static int on_deferred_rescan(sd_event_source *s, void *userdata) {
+        Manager *m = userdata;
+
+        assert(m);
+
+        m->deferred_rescan_event_source = sd_event_source_unref(m->deferred_rescan_event_source);
+
+        manager_enumerate_devices(m);
+        manager_enumerate_images(m);
+        return 0;
+}
+
+int manager_enqueue_rescan(Manager *m) {
+        int r;
+
+        assert(m);
+
+        if (m->deferred_rescan_event_source)
+                return 0;
+
+        if (!m->event)
+                return 0;
+
+        if (IN_SET(sd_event_get_state(m->event), SD_EVENT_FINISHED, SD_EVENT_EXITING))
+                return 0;
+
+        r = sd_event_add_defer(m->event, &m->deferred_rescan_event_source, on_deferred_rescan, m);
+        if (r < 0)
+                return log_error_errno(r, "Failed to allocate rescan event source: %m");
+
+        r = sd_event_source_set_priority(m->deferred_rescan_event_source, SD_EVENT_PRIORITY_IDLE+1);
+        if (r < 0)
+                log_warning_errno(r, "Failed to tweak priority of event source, ignoring: %m");
+
+        (void) sd_event_source_set_description(m->deferred_rescan_event_source, "deferred-rescan");
+        return 1;
+}
+
+static int on_deferred_gc(sd_event_source *s, void *userdata) {
+        Manager *m = userdata;
+
+        assert(m);
+
+        m->deferred_gc_event_source = sd_event_source_unref(m->deferred_gc_event_source);
+
+        manager_gc_images(m);
+        return 0;
+}
+
+int manager_enqueue_gc(Manager *m, Home *focus) {
+        int r;
+
+        assert(m);
+
+        /* This enqueues a request to GC dead homes. It may be called with focus=NULL in which case all homes
+         * will be scanned, or with the parameter set, in which case only that home is checked. */
+
+        if (!m->event)
+                return 0;
+
+        if (IN_SET(sd_event_get_state(m->event), SD_EVENT_FINISHED, SD_EVENT_EXITING))
+                return 0;
+
+        /* If a focus home is specified, then remember to focus just on this home. Otherwise invalidate any
+         * focus that might be set to look at all homes. */
+
+        if (m->deferred_gc_event_source) {
+                if (m->gc_focus != focus) /* not the same focus, then look at everything */
+                        m->gc_focus = NULL;
+
+                return 0;
+        } else
+                m->gc_focus = focus; /* start focussed */
+
+        r = sd_event_add_defer(m->event, &m->deferred_gc_event_source, on_deferred_gc, m);
+        if (r < 0)
+                return log_error_errno(r, "Failed to allocate gc event source: %m");
+
+        r = sd_event_source_set_priority(m->deferred_gc_event_source, SD_EVENT_PRIORITY_IDLE);
+        if (r < 0)
+                log_warning_errno(r, "Failed to tweak priority of event source, ignoring: %m");
+
+        (void) sd_event_source_set_description(m->deferred_gc_event_source, "deferred-gc");
+        return 1;
+}
diff --git a/src/home/homed-manager.h b/src/home/homed-manager.h
new file mode 100644 (file)
index 0000000..00298a3
--- /dev/null
@@ -0,0 +1,67 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include <openssl/evp.h>
+
+#include "sd-bus.h"
+#include "sd-device.h"
+#include "sd-event.h"
+
+typedef struct Manager Manager;
+
+#include "hashmap.h"
+#include "homed-home.h"
+#include "varlink.h"
+
+#define HOME_UID_MIN 60001
+#define HOME_UID_MAX 60513
+
+struct Manager {
+        sd_event *event;
+        sd_bus *bus;
+
+        Hashmap *polkit_registry;
+
+        Hashmap *homes_by_uid;
+        Hashmap *homes_by_name;
+        Hashmap *homes_by_worker_pid;
+        Hashmap *homes_by_sysfs;
+
+        bool scan_slash_home;
+
+        sd_event_source *inotify_event_source;
+
+        /* An even source we receieve sd_notify() messages from our worker from */
+        sd_event_source *notify_socket_event_source;
+
+        sd_device_monitor *device_monitor;
+
+        sd_event_source *deferred_rescan_event_source;
+        sd_event_source *deferred_gc_event_source;
+        sd_event_source *deferred_auto_login_event_source;
+
+        Home *gc_focus;
+
+        VarlinkServer *varlink_server;
+
+        EVP_PKEY *private_key; /* actually a pair of private and public key */
+        Hashmap *public_keys; /* key name [char*] → publick key [EVP_PKEY*] */
+};
+
+int manager_new(Manager **ret);
+Manager* manager_free(Manager *m);
+DEFINE_TRIVIAL_CLEANUP_FUNC(Manager*, manager_free);
+
+int manager_startup(Manager *m);
+
+int manager_augment_record_with_uid(Manager *m, UserRecord *hr);
+
+int manager_enqueue_rescan(Manager *m);
+int manager_enqueue_gc(Manager *m, Home *focus);
+
+int manager_verify_user_record(Manager *m, UserRecord *hr);
+
+int manager_acquire_key_pair(Manager *m);
+int manager_sign_user_record(Manager *m, UserRecord *u, UserRecord **ret, sd_bus_error *error);
+
+int bus_manager_emit_auto_login_changed(Manager *m);
diff --git a/src/home/homed-operation.c b/src/home/homed-operation.c
new file mode 100644 (file)
index 0000000..80dc555
--- /dev/null
@@ -0,0 +1,76 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include "fd-util.h"
+#include "homed-operation.h"
+
+Operation *operation_new(OperationType type, sd_bus_message *m) {
+        Operation *o;
+
+        assert(type >= 0);
+        assert(type < _OPERATION_MAX);
+
+        o = new(Operation, 1);
+        if (!o)
+                return NULL;
+
+        *o = (Operation) {
+                .type = type,
+                .n_ref = 1,
+                .message = sd_bus_message_ref(m),
+                .send_fd = -1,
+                .result = -1,
+        };
+
+        return o;
+}
+
+static Operation *operation_free(Operation *o) {
+        int r;
+
+        if (!o)
+                return NULL;
+
+        if (o->message && o->result >= 0) {
+
+                if (o->result) {
+                        /* Propagate success */
+                        if (o->send_fd < 0)
+                                r = sd_bus_reply_method_return(o->message, NULL);
+                        else
+                                r = sd_bus_reply_method_return(o->message, "h", o->send_fd);
+
+                } else {
+                        /* Propagate failure */
+                        if (sd_bus_error_is_set(&o->error))
+                                r = sd_bus_reply_method_error(o->message, &o->error);
+                        else
+                                r = sd_bus_reply_method_errnof(o->message, o->ret, "Failed to execute operation: %m");
+                }
+                if (r < 0)
+                        log_warning_errno(r, "Failed ot reply to %s method call, ignoring: %m", sd_bus_message_get_member(o->message));
+        }
+
+        sd_bus_message_unref(o->message);
+        user_record_unref(o->secret);
+        safe_close(o->send_fd);
+        sd_bus_error_free(&o->error);
+
+        return mfree(o);
+}
+
+DEFINE_TRIVIAL_REF_UNREF_FUNC(Operation, operation, operation_free);
+
+void operation_result(Operation *o, int ret, const sd_bus_error *error) {
+        assert(o);
+
+        if (ret >= 0)
+                o->result = true;
+        else {
+                o->ret = ret;
+
+                sd_bus_error_free(&o->error);
+                sd_bus_error_copy(&o->error, error);
+
+                o->result = false;
+        }
+}
diff --git a/src/home/homed-operation.h b/src/home/homed-operation.h
new file mode 100644 (file)
index 0000000..224de91
--- /dev/null
@@ -0,0 +1,62 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include <sd-bus.h>
+
+#include "user-record.h"
+
+typedef enum OperationType {
+        OPERATION_ACQUIRE,           /* enqueued on AcquireHome() */
+        OPERATION_RELEASE,           /* enqueued on ReleaseHome() */
+        OPERATION_LOCK_ALL,          /* enqueued on LockAllHomes() */
+        OPERATION_PIPE_EOF,          /* enqueued when we see EOF on the per-home reference pipes */
+        OPERATION_DEACTIVATE_FORCE,  /* enqueued on hard $HOME unplug */
+        OPERATION_IMMEDIATE,         /* this is never enqueued, it's just a marker we immediately started executing an operation without enqueuing anything first. */
+        _OPERATION_MAX,
+        _OPERATION_INVALID = -1,
+} OperationType;
+
+/* Encapsulates an operation on one or more home directories. This has two uses:
+ *
+ *     1) For queuing an operation when we need to execute one for some reason but there's already one being
+ *        executed.
+ *
+ *     2) When executing an operation without enqueuing it first (OPERATION_IMMEDIATE)
+ *
+ * Note that a single operation object can encapsulate operations on multiple home directories. This is used
+ * for the LockAllHomes() operation, which is one operation but applies to all homes at once. In case the
+ * operation applies to multiple homes the reference counter is increased once for each, and thus the
+ * operation is fully completed only after it reached zero again.
+ *
+ * The object (optionally) contains a reference of the D-Bus message triggering the operation, which is
+ * replied to when the operation is fully completed, i.e. when n_ref reaches zero.
+ */
+
+typedef struct Operation {
+        unsigned n_ref;
+        OperationType type;
+        sd_bus_message *message;
+
+        UserRecord *secret;
+        int send_fd;   /* pipe fd for AcquireHome() which is taken already when we start the operation */
+
+        int result;    /* < 0 if not completed yet, == 0 on failure, > 0 on success */
+        sd_bus_error error;
+        int ret;
+} Operation;
+
+Operation *operation_new(OperationType type, sd_bus_message *m);
+Operation *operation_ref(Operation *operation);
+Operation *operation_unref(Operation *operation);
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(Operation*, operation_unref);
+
+void operation_result(Operation *o, int ret, const sd_bus_error *error);
+
+static inline Operation* operation_result_unref(Operation *o, int ret, const sd_bus_error *error) {
+        if (!o)
+                return NULL;
+
+        operation_result(o, ret, error);
+        return operation_unref(o);
+}
diff --git a/src/home/homed-varlink.c b/src/home/homed-varlink.c
new file mode 100644 (file)
index 0000000..c5bbba6
--- /dev/null
@@ -0,0 +1,370 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include "group-record.h"
+#include "homed-varlink.h"
+#include "strv.h"
+#include "user-record-util.h"
+#include "user-record.h"
+#include "user-util.h"
+#include "format-util.h"
+
+typedef struct LookupParameters {
+        const char *user_name;
+        const char *group_name;
+        union {
+                uid_t uid;
+                gid_t gid;
+        };
+        const char *service;
+} LookupParameters;
+
+static bool client_is_trusted(Varlink *link, Home *h) {
+        uid_t peer_uid;
+        int r;
+
+        assert(link);
+        assert(h);
+
+        r = varlink_get_peer_uid(link, &peer_uid);
+        if (r < 0) {
+                log_debug_errno(r, "Unable to query peer UID, ignoring: %m");
+                return false;
+        }
+
+        return peer_uid == 0 || peer_uid == h->uid;
+}
+
+static int build_user_json(Home *h, bool trusted, JsonVariant **ret) {
+        _cleanup_(user_record_unrefp) UserRecord *augmented = NULL;
+        UserRecordLoadFlags flags;
+        int r;
+
+        assert(h);
+        assert(ret);
+
+        flags = USER_RECORD_REQUIRE_REGULAR|USER_RECORD_ALLOW_PER_MACHINE|USER_RECORD_ALLOW_BINDING|USER_RECORD_STRIP_SECRET|USER_RECORD_ALLOW_STATUS|USER_RECORD_ALLOW_SIGNATURE;
+        if (trusted)
+                flags |= USER_RECORD_ALLOW_PRIVILEGED;
+        else
+                flags |= USER_RECORD_STRIP_PRIVILEGED;
+
+        r = home_augment_status(h, flags, &augmented);
+        if (r < 0)
+                return r;
+
+        return json_build(ret, JSON_BUILD_OBJECT(
+                                          JSON_BUILD_PAIR("record", JSON_BUILD_VARIANT(augmented->json)),
+                                          JSON_BUILD_PAIR("incomplete", JSON_BUILD_BOOLEAN(augmented->incomplete))));
+}
+
+static bool home_user_match_lookup_parameters(LookupParameters *p, Home *h) {
+        assert(p);
+        assert(h);
+
+        if (p->user_name && !streq(p->user_name, h->user_name))
+                return false;
+
+        if (uid_is_valid(p->uid) && h->uid != p->uid)
+                return false;
+
+        return true;
+}
+
+int vl_method_get_user_record(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) {
+
+        static const JsonDispatch dispatch_table[] = {
+                { "uid",            JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid,      offsetof(LookupParameters, uid),       0         },
+                { "userName",       JSON_VARIANT_STRING,   json_dispatch_const_string, offsetof(LookupParameters, user_name), JSON_SAFE },
+                { "service",        JSON_VARIANT_STRING,   json_dispatch_const_string, offsetof(LookupParameters, service),   0         },
+                {}
+        };
+
+        _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+        LookupParameters p = {
+                .uid = UID_INVALID,
+        };
+        Manager *m = userdata;
+        bool trusted;
+        Home *h;
+        int r;
+
+        assert(parameters);
+        assert(m);
+
+        r = json_dispatch(parameters, dispatch_table, NULL, 0, &p);
+        if (r < 0)
+                return r;
+
+        if (!streq_ptr(p.service, "io.systemd.Home"))
+                return varlink_error(link, "io.systemd.UserDatabase.BadService", NULL);
+
+        if (uid_is_valid(p.uid))
+                h = hashmap_get(m->homes_by_uid, UID_TO_PTR(p.uid));
+        else if (p.user_name)
+                h = hashmap_get(m->homes_by_name, p.user_name);
+        else {
+                Iterator i;
+
+                /* If neither UID nor name was specified, then dump all homes. Do so with varlink_notify()
+                 * for all entries but the last, so that clients can stream the results, and easily process
+                 * them piecemeal. */
+
+                HASHMAP_FOREACH(h, m->homes_by_name, i) {
+
+                        if (!home_user_match_lookup_parameters(&p, h))
+                                continue;
+
+                        if (v) {
+                                /* An entry set from the previous iteration? Then send it now */
+                                r = varlink_notify(link, v);
+                                if (r < 0)
+                                        return r;
+
+                                v = json_variant_unref(v);
+                        }
+
+                        trusted = client_is_trusted(link, h);
+
+                        r = build_user_json(h, trusted, &v);
+                        if (r < 0)
+                                return r;
+                }
+
+                if (!v)
+                        return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL);
+
+                return varlink_reply(link, v);
+        }
+
+        if (!h)
+                return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL);
+
+        if (!home_user_match_lookup_parameters(&p, h))
+                return varlink_error(link, "io.systemd.UserDatabase.ConflictingRecordFound", NULL);
+
+        trusted = client_is_trusted(link, h);
+
+        r = build_user_json(h, trusted, &v);
+        if (r < 0)
+                return r;
+
+        return varlink_reply(link, v);
+}
+
+static int build_group_json(Home *h, JsonVariant **ret) {
+        _cleanup_(group_record_unrefp) GroupRecord *g = NULL;
+        int r;
+
+        assert(h);
+        assert(ret);
+
+        g = group_record_new();
+        if (!g)
+                return -ENOMEM;
+
+        r = group_record_synthesize(g, h->record);
+        if (r < 0)
+                return r;
+
+        assert(!FLAGS_SET(g->mask, USER_RECORD_SECRET));
+        assert(!FLAGS_SET(g->mask, USER_RECORD_PRIVILEGED));
+
+        return json_build(ret,
+                          JSON_BUILD_OBJECT(
+                                          JSON_BUILD_PAIR("record", JSON_BUILD_VARIANT(g->json))));
+}
+
+static bool home_group_match_lookup_parameters(LookupParameters *p, Home *h) {
+        assert(p);
+        assert(h);
+
+        if (p->group_name && !streq(h->user_name, p->group_name))
+                return false;
+
+        if (gid_is_valid(p->gid) && h->uid != (uid_t) p->gid)
+                return false;
+
+        return true;
+}
+
+int vl_method_get_group_record(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) {
+
+        static const JsonDispatch dispatch_table[] = {
+                { "gid",       JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid,      offsetof(LookupParameters, gid),        0         },
+                { "groupName", JSON_VARIANT_STRING,   json_dispatch_const_string, offsetof(LookupParameters, group_name), JSON_SAFE },
+                { "service",   JSON_VARIANT_STRING,   json_dispatch_const_string, offsetof(LookupParameters, service),    0         },
+                {}
+        };
+
+        _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+        LookupParameters p = {
+                .gid = GID_INVALID,
+        };
+        Manager *m = userdata;
+        Home *h;
+        int r;
+
+        assert(parameters);
+        assert(m);
+
+        r = json_dispatch(parameters, dispatch_table, NULL, 0, &p);
+        if (r < 0)
+                return r;
+
+        if (!streq_ptr(p.service, "io.systemd.Home"))
+                return varlink_error(link, "io.systemd.UserDatabase.BadService", NULL);
+
+        if (gid_is_valid(p.gid))
+                h = hashmap_get(m->homes_by_uid, UID_TO_PTR((uid_t) p.gid));
+        else if (p.group_name)
+                h = hashmap_get(m->homes_by_name, p.group_name);
+        else {
+                Iterator i;
+
+                HASHMAP_FOREACH(h, m->homes_by_name, i) {
+
+                        if (!home_group_match_lookup_parameters(&p, h))
+                                continue;
+
+                        if (v) {
+                                r = varlink_notify(link, v);
+                                if (r < 0)
+                                        return r;
+
+                                v = json_variant_unref(v);
+                        }
+
+                        r = build_group_json(h, &v);
+                        if (r < 0)
+                                return r;
+                }
+
+                if (!v)
+                        return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL);
+
+                return varlink_reply(link, v);
+        }
+
+        if (!h)
+                return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL);
+
+        if (!home_group_match_lookup_parameters(&p, h))
+                return varlink_error(link, "io.systemd.UserDatabase.ConflictingRecordFound", NULL);
+
+        r = build_group_json(h, &v);
+        if (r < 0)
+                return r;
+
+        return varlink_reply(link, v);
+}
+
+int vl_method_get_memberships(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) {
+
+        static const JsonDispatch dispatch_table[] = {
+                { "userName",  JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, user_name),  JSON_SAFE },
+                { "groupName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, group_name), JSON_SAFE },
+                { "service",   JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, service),    0         },
+                {}
+        };
+
+        Manager *m = userdata;
+        LookupParameters p = {};
+        Home *h;
+        int r;
+
+        assert(parameters);
+        assert(m);
+
+        r = json_dispatch(parameters, dispatch_table, NULL, 0, &p);
+        if (r < 0)
+                return r;
+
+        if (!streq_ptr(p.service, "io.systemd.Home"))
+                return varlink_error(link, "io.systemd.UserDatabase.BadService", NULL);
+
+        if (p.user_name) {
+                const char *last = NULL;
+                char **i;
+
+                h = hashmap_get(m->homes_by_name, p.user_name);
+                if (!h)
+                        return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL);
+
+                if (p.group_name) {
+                        if (!strv_contains(h->record->member_of, p.group_name))
+                                return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL);
+
+                        return varlink_replyb(link, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(h->user_name)),
+                                                                      JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(p.group_name))));
+                }
+
+                STRV_FOREACH(i, h->record->member_of) {
+                        if (last) {
+                                r = varlink_notifyb(link, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(h->user_name)),
+                                                                            JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(last))));
+                                if (r < 0)
+                                        return r;
+                        }
+
+                        last = *i;
+                }
+
+                if (last)
+                        return varlink_replyb(link, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(h->user_name)),
+                                                                      JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(last))));
+
+        } else if (p.group_name) {
+                const char *last = NULL;
+                Iterator i;
+
+                HASHMAP_FOREACH(h, m->homes_by_name, i) {
+
+                        if (!strv_contains(h->record->member_of, p.group_name))
+                                continue;
+
+                        if (last) {
+                                r = varlink_notifyb(link, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(last)),
+                                                                            JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(p.group_name))));
+                                if (r < 0)
+                                        return r;
+                        }
+
+                        last = h->user_name;
+                }
+
+                if (last)
+                        return varlink_replyb(link, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(last)),
+                                                                      JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(p.group_name))));
+        } else {
+                const char *last_user_name = NULL, *last_group_name = NULL;
+                Iterator i;
+
+                HASHMAP_FOREACH(h, m->homes_by_name, i) {
+                        char **j;
+
+                        STRV_FOREACH(j, h->record->member_of) {
+
+                                if (last_user_name) {
+                                        assert(last_group_name);
+
+                                        r = varlink_notifyb(link, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(last_user_name)),
+                                                                                    JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(last_group_name))));
+
+                                        if (r < 0)
+                                                return r;
+                                }
+
+                                last_user_name = h->user_name;
+                                last_group_name = *j;
+                        }
+                }
+
+                if (last_user_name) {
+                        assert(last_group_name);
+                        return varlink_replyb(link, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(last_user_name)),
+                                                                      JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(last_group_name))));
+                }
+        }
+
+        return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL);
+}
diff --git a/src/home/homed-varlink.h b/src/home/homed-varlink.h
new file mode 100644 (file)
index 0000000..4454d23
--- /dev/null
@@ -0,0 +1,8 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include "homed-manager.h"
+
+int vl_method_get_user_record(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata);
+int vl_method_get_group_record(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata);
+int vl_method_get_memberships(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata);
diff --git a/src/home/homed.c b/src/home/homed.c
new file mode 100644 (file)
index 0000000..ca43558
--- /dev/null
@@ -0,0 +1,46 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#include "daemon-util.h"
+#include "homed-manager.h"
+#include "log.h"
+#include "main-func.h"
+#include "signal-util.h"
+
+static int run(int argc, char *argv[]) {
+        _cleanup_(notify_on_cleanup) const char *notify_stop = NULL;
+        _cleanup_(manager_freep) Manager *m = NULL;
+        int r;
+
+        log_setup_service();
+
+        umask(0022);
+
+        if (argc != 1)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "This program takes no arguments.");
+
+        if (setenv("SYSTEMD_BYPASS_USERDB", "io.systemd.Home", 1) < 0)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to set $SYSTEMD_BYPASS_USERDB: %m");
+
+        assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGCHLD, SIGTERM, SIGINT, -1) >= 0);
+
+        r = manager_new(&m);
+        if (r < 0)
+                return log_error_errno(r, "Could not create manager: %m");
+
+        r = manager_startup(m);
+        if (r < 0)
+                return log_error_errno(r, "Failed to start up daemon: %m");
+
+        notify_stop = notify_start(NOTIFY_READY, NOTIFY_STOPPING);
+
+        r = sd_event_loop(m->event);
+        if (r < 0)
+                return log_error_errno(r, "Event loop failed: %m");
+
+        return 0;
+}
+
+DEFINE_MAIN_FUNCTION(run);
diff --git a/src/home/homework-cifs.c b/src/home/homework-cifs.c
new file mode 100644 (file)
index 0000000..f67e279
--- /dev/null
@@ -0,0 +1,215 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include "dirent-util.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "format-util.h"
+#include "fs-util.h"
+#include "homework-cifs.h"
+#include "homework-mount.h"
+#include "mount-util.h"
+#include "process-util.h"
+#include "strv.h"
+#include "tmpfile-util.h"
+
+int home_prepare_cifs(
+                UserRecord *h,
+                bool already_activated,
+                HomeSetup *setup) {
+
+        char **pw;
+        int r;
+
+        assert(h);
+        assert(setup);
+        assert(user_record_storage(h) == USER_CIFS);
+
+        if (already_activated)
+                setup->root_fd = open(user_record_home_directory(h), O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW);
+        else {
+                bool mounted = false;
+
+                r = home_unshare_and_mount(NULL, NULL, false);
+                if (r < 0)
+                        return r;
+
+                STRV_FOREACH(pw, h->password) {
+                        _cleanup_(unlink_and_freep) char *p = NULL;
+                        _cleanup_free_ char *options = NULL;
+                        _cleanup_(fclosep) FILE *f = NULL;
+                        pid_t mount_pid;
+                        int exit_status;
+
+                        r = fopen_temporary(NULL, &f, &p);
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to create temporary credentials file: %m");
+
+                        fprintf(f,
+                                "username=%s\n"
+                                "password=%s\n",
+                                user_record_cifs_user_name(h),
+                                *pw);
+
+                        if (h->cifs_domain)
+                                fprintf(f, "domain=%s\n", h->cifs_domain);
+
+                        r = fflush_and_check(f);
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to write temporary credentials file: %m");
+
+                        f = safe_fclose(f);
+
+                        if (asprintf(&options, "credentials=%s,uid=" UID_FMT ",forceuid,gid=" UID_FMT ",forcegid,file_mode=0%3o,dir_mode=0%3o",
+                                     p, h->uid, h->uid, h->access_mode, h->access_mode) < 0)
+                                return log_oom();
+
+                        r = safe_fork("(mount)", FORK_RESET_SIGNALS|FORK_RLIMIT_NOFILE_SAFE|FORK_DEATHSIG|FORK_LOG|FORK_STDOUT_TO_STDERR, &mount_pid);
+                        if (r < 0)
+                                return r;
+                        if (r == 0) {
+                                /* Child */
+                                execl("/bin/mount", "/bin/mount", "-n", "-t", "cifs",
+                                      h->cifs_service, "/run/systemd/user-home-mount",
+                                      "-o", options, NULL);
+
+                                log_error_errno(errno, "Failed to execute fsck: %m");
+                                _exit(EXIT_FAILURE);
+                        }
+
+                        exit_status = wait_for_terminate_and_check("mount", mount_pid, WAIT_LOG_ABNORMAL|WAIT_LOG_NON_ZERO_EXIT_STATUS);
+                        if (exit_status < 0)
+                                return exit_status;
+                        if (exit_status != EXIT_SUCCESS)
+                                return -EPROTO;
+
+                        mounted = true;
+                        break;
+                }
+
+                if (!mounted)
+                        return log_error_errno(ENOKEY, "Failed to mount home directory with supplied password.");
+
+                setup->root_fd = open("/run/systemd/user-home-mount", O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW);
+        }
+        if (setup->root_fd < 0)
+                return log_error_errno(r, "Failed to open home directory: %m");
+
+        return 0;
+}
+
+int home_activate_cifs(
+                UserRecord *h,
+                char ***pkcs11_decrypted_passwords,
+                UserRecord **ret_home) {
+
+        _cleanup_(home_setup_undo) HomeSetup setup = HOME_SETUP_INIT;
+        _cleanup_(user_record_unrefp) UserRecord *new_home = NULL;
+        const char *hdo, *hd;
+        int r;
+
+        assert(h);
+        assert(user_record_storage(h) == USER_CIFS);
+        assert(ret_home);
+
+        if (!h->cifs_service)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record lacks CIFS service, refusing.");
+
+        assert_se(hdo = user_record_home_directory(h));
+        hd = strdupa(hdo); /* copy the string out, since it might change later in the home record object */
+
+        r = home_prepare_cifs(h, false, &setup);
+        if (r < 0)
+                return r;
+
+        r = home_refresh(h, &setup, NULL, pkcs11_decrypted_passwords, NULL, &new_home);
+        if (r < 0)
+                return r;
+
+        setup.root_fd = safe_close(setup.root_fd);
+
+        r = home_move_mount(NULL, hd);
+        if (r < 0)
+                return r;
+
+        setup.undo_mount = false;
+
+        log_info("Everything completed.");
+
+        *ret_home = TAKE_PTR(new_home);
+        return 1;
+}
+
+int home_create_cifs(UserRecord *h, UserRecord **ret_home) {
+        _cleanup_(home_setup_undo) HomeSetup setup = HOME_SETUP_INIT;
+        _cleanup_(user_record_unrefp) UserRecord *new_home = NULL;
+        _cleanup_(closedirp) DIR *d = NULL;
+        int r, copy;
+
+        assert(h);
+        assert(user_record_storage(h) == USER_CIFS);
+        assert(ret_home);
+
+        if (!h->cifs_service)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record lacks CIFS service, refusing.");
+
+        if (access("/sbin/mount.cifs", F_OK) < 0) {
+                if (errno == ENOENT)
+                        return log_error_errno(SYNTHETIC_ERRNO(ENOLINK), "/sbin/mount.cifs is missing.");
+
+                return log_error_errno(errno, "Unable to detect whether /sbin/mount.cifs exists: %m");
+        }
+
+        r = home_prepare_cifs(h, false, &setup);
+        if (r < 0)
+                return r;
+
+        copy = fcntl(setup.root_fd, F_DUPFD_CLOEXEC, 3);
+        if (copy < 0)
+                return -errno;
+
+        d = fdopendir(copy);
+        if (!d) {
+                safe_close(copy);
+                return -errno;
+        }
+
+        errno = 0;
+        if (readdir_no_dot(d))
+                return log_error_errno(SYNTHETIC_ERRNO(ENOTEMPTY), "Selected CIFS directory not empty, refusing.");
+        if (errno != 0)
+                return log_error_errno(errno, "Failed to detect if CIFS directory is empty: %m");
+
+        r = home_populate(h, setup.root_fd);
+        if (r < 0)
+                return r;
+
+        r = home_sync_and_statfs(setup.root_fd, NULL);
+        if (r < 0)
+                return r;
+
+        r = user_record_clone(h, USER_RECORD_LOAD_MASK_SECRET, &new_home);
+        if (r < 0)
+                return log_error_errno(r, "Failed to clone record: %m");
+
+        r = user_record_add_binding(
+                        new_home,
+                        USER_CIFS,
+                        NULL,
+                        SD_ID128_NULL,
+                        SD_ID128_NULL,
+                        SD_ID128_NULL,
+                        NULL,
+                        NULL,
+                        UINT64_MAX,
+                        NULL,
+                        NULL,
+                        h->uid,
+                        (gid_t) h->uid);
+        if (r < 0)
+                return log_error_errno(r, "Failed to add binding to record: %m");
+
+        log_info("Everything completed.");
+
+        *ret_home = TAKE_PTR(new_home);
+        return 0;
+}
diff --git a/src/home/homework-cifs.h b/src/home/homework-cifs.h
new file mode 100644 (file)
index 0000000..346be88
--- /dev/null
@@ -0,0 +1,11 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include "homework.h"
+#include "user-record.h"
+
+int home_prepare_cifs(UserRecord *h, bool already_activated, HomeSetup *setup);
+
+int home_activate_cifs(UserRecord *h, char ***pkcs11_decrypted_passwords, UserRecord **ret_home);
+
+int home_create_cifs(UserRecord *h, UserRecord **ret_home);
diff --git a/src/home/homework-directory.c b/src/home/homework-directory.c
new file mode 100644 (file)
index 0000000..8a4cb17
--- /dev/null
@@ -0,0 +1,242 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include <sys/mount.h>
+
+#include "btrfs-util.h"
+#include "fd-util.h"
+#include "homework-directory.h"
+#include "homework-quota.h"
+#include "mkdir.h"
+#include "mount-util.h"
+#include "path-util.h"
+#include "rm-rf.h"
+#include "tmpfile-util.h"
+#include "umask-util.h"
+
+int home_prepare_directory(UserRecord *h, bool already_activated, HomeSetup *setup) {
+        assert(h);
+        assert(setup);
+
+        setup->root_fd = open(user_record_image_path(h), O_RDONLY|O_CLOEXEC|O_DIRECTORY);
+        if (setup->root_fd < 0)
+                return log_error_errno(errno, "Failed to open home directory: %m");
+
+        return 0;
+}
+
+int home_activate_directory(
+                UserRecord *h,
+                char ***pkcs11_decrypted_passwords,
+                UserRecord **ret_home) {
+
+        _cleanup_(user_record_unrefp) UserRecord *new_home = NULL, *header_home = NULL;
+        _cleanup_(home_setup_undo) HomeSetup setup = HOME_SETUP_INIT;
+        const char *hdo, *hd, *ipo, *ip;
+        int r;
+
+        assert(h);
+        assert(IN_SET(user_record_storage(h), USER_DIRECTORY, USER_SUBVOLUME, USER_FSCRYPT));
+        assert(ret_home);
+
+        assert_se(ipo = user_record_image_path(h));
+        ip = strdupa(ipo); /* copy out, since reconciliation might cause changing of the field */
+
+        assert_se(hdo = user_record_home_directory(h));
+        hd = strdupa(hdo);
+
+        r = home_prepare(h, false, pkcs11_decrypted_passwords, &setup, &header_home);
+        if (r < 0)
+                return r;
+
+        r = home_refresh(h, &setup, header_home, pkcs11_decrypted_passwords, NULL, &new_home);
+        if (r < 0)
+                return r;
+
+        setup.root_fd = safe_close(setup.root_fd);
+
+        /* Create mount point to mount over if necessary */
+        if (!path_equal(ip, hd))
+                (void) mkdir_p(hd, 0700);
+
+        /* Create a mount point (even if the directory is already placed correctly), as a way to indicate
+         * this mount point is now "activated". Moreover, we want to set per-user
+         * MS_NOSUID/MS_NOEXEC/MS_NODEV. */
+        r = mount_verbose(LOG_ERR, ip, hd, NULL, MS_BIND, NULL);
+        if (r < 0)
+                return r;
+
+        r = mount_verbose(LOG_ERR, NULL, hd, NULL, MS_BIND|MS_REMOUNT|user_record_mount_flags(h), NULL);
+        if (r < 0) {
+                (void) umount_verbose(hd);
+                return r;
+        }
+
+        log_info("Everything completed.");
+
+        *ret_home = TAKE_PTR(new_home);
+        return 0;
+}
+
+int home_create_directory_or_subvolume(UserRecord *h, UserRecord **ret_home) {
+        _cleanup_(rm_rf_subvolume_and_freep) char *temporary = NULL;
+        _cleanup_(user_record_unrefp) UserRecord *new_home = NULL;
+        _cleanup_close_ int root_fd = -1;
+        _cleanup_free_ char *d = NULL;
+        const char *ip;
+        int r;
+
+        assert(h);
+        assert(IN_SET(user_record_storage(h), USER_DIRECTORY, USER_SUBVOLUME));
+        assert(ret_home);
+
+        assert_se(ip = user_record_image_path(h));
+
+        r = tempfn_random(ip, "homework", &d);
+        if (r < 0)
+                return log_error_errno(r, "Failed to allocate temporary directory: %m");
+
+        (void) mkdir_parents(d, 0755);
+
+        switch (user_record_storage(h)) {
+
+        case USER_SUBVOLUME:
+                RUN_WITH_UMASK(0077)
+                        r = btrfs_subvol_make(d);
+
+                if (r >= 0) {
+                        log_info("Subvolume created.");
+
+                        if (h->disk_size != UINT64_MAX) {
+
+                                /* Enable quota for the subvolume we just created. Note we don't check for
+                                 * errors here and only log about debug level about this. */
+                                r = btrfs_quota_enable(d, true);
+                                if (r < 0)
+                                        log_debug_errno(r, "Failed to enable quota on %s, ignoring: %m", d);
+
+                                r = btrfs_subvol_auto_qgroup(d, 0, false);
+                                if (r < 0)
+                                        log_debug_errno(r, "Failed to set up automatic quota group on %s, ignoring: %m", d);
+
+                                /* Actually configure the quota. We also ignore errors here, but we do log
+                                 * about them loudly, to keep things discoverable even though we don't
+                                 * consider lacking quota support in kernel fatal. */
+                                (void) home_update_quota_btrfs(h, d);
+                        }
+
+                        break;
+                }
+                if (r != -ENOTTY)
+                        return log_error_errno(r, "Failed to create temporary home directory subvolume %s: %m", d);
+
+                log_info("Creating subvolume %s is not supported, as file system does not support subvolumes. Falling back to regular directory.", d);
+                _fallthrough_;
+
+        case USER_DIRECTORY:
+
+                if (mkdir(d, 0700) < 0)
+                        return log_error_errno(errno, "Failed to create temporary home directory %s: %m", d);
+
+                (void) home_update_quota_classic(h, d);
+                break;
+
+        default:
+                assert_not_reached("unexpected storage");
+        }
+
+        temporary = TAKE_PTR(d); /* Needs to be destroyed now */
+
+        root_fd = open(temporary, O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW);
+        if (root_fd < 0)
+                return log_error_errno(errno, "Failed to open temporary home directory: %m");
+
+        r = home_populate(h, root_fd);
+        if (r < 0)
+                return r;
+
+        r = home_sync_and_statfs(root_fd, NULL);
+        if (r < 0)
+                return r;
+
+        r = user_record_clone(h, USER_RECORD_LOAD_MASK_SECRET, &new_home);
+        if (r < 0)
+                return log_error_errno(r, "Failed to clone record: %m");
+
+        r = user_record_add_binding(
+                        new_home,
+                        user_record_storage(h),
+                        ip,
+                        SD_ID128_NULL,
+                        SD_ID128_NULL,
+                        SD_ID128_NULL,
+                        NULL,
+                        NULL,
+                        UINT64_MAX,
+                        NULL,
+                        NULL,
+                        h->uid,
+                        (gid_t) h->uid);
+        if (r < 0)
+                return log_error_errno(r, "Failed to add binding to record: %m");
+
+        if (rename(temporary, ip) < 0)
+                return log_error_errno(errno, "Failed to rename %s to %s: %m", temporary, ip);
+
+        temporary = mfree(temporary);
+
+        log_info("Everything completed.");
+
+        *ret_home = TAKE_PTR(new_home);
+        return 0;
+}
+
+int home_resize_directory(
+                UserRecord *h,
+                bool already_activated,
+                char ***pkcs11_decrypted_passwords,
+                HomeSetup *setup,
+                UserRecord **ret_home) {
+
+        _cleanup_(user_record_unrefp) UserRecord *embedded_home = NULL, *new_home = NULL;
+        int r;
+
+        assert(h);
+        assert(setup);
+        assert(ret_home);
+        assert(IN_SET(user_record_storage(h), USER_DIRECTORY, USER_SUBVOLUME, USER_FSCRYPT));
+
+        r = home_prepare(h, already_activated, pkcs11_decrypted_passwords, setup, NULL);
+        if (r < 0)
+                return r;
+
+        r = home_load_embedded_identity(h, setup->root_fd, NULL, USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL, pkcs11_decrypted_passwords, &embedded_home, &new_home);
+        if (r < 0)
+                return r;
+
+        r = home_update_quota_auto(h, NULL);
+        if (ERRNO_IS_NOT_SUPPORTED(r))
+                return -ESOCKTNOSUPPORT; /* make recognizable */
+        if (r < 0)
+                return r;
+
+        r = home_store_embedded_identity(new_home, setup->root_fd, h->uid, embedded_home);
+        if (r < 0)
+                return r;
+
+        r = home_extend_embedded_identity(new_home, h, setup);
+        if (r < 0)
+                return r;
+
+        r = home_sync_and_statfs(setup->root_fd, NULL);
+        if (r < 0)
+                return r;
+
+        r = home_setup_undo(setup);
+        if (r < 0)
+                return r;
+
+        log_info("Everything completed.");
+
+        *ret_home = TAKE_PTR(new_home);
+        return 0;
+}
diff --git a/src/home/homework-directory.h b/src/home/homework-directory.h
new file mode 100644 (file)
index 0000000..047c3a7
--- /dev/null
@@ -0,0 +1,10 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include "homework.h"
+#include "user-record.h"
+
+int home_prepare_directory(UserRecord *h, bool already_activated, HomeSetup *setup);
+int home_activate_directory(UserRecord *h, char ***pkcs11_decrypted_passwords, UserRecord **ret_home);
+int home_create_directory_or_subvolume(UserRecord *h, UserRecord **ret_home);
+int home_resize_directory(UserRecord *h, bool already_activated, char ***pkcs11_decrypted_passwords, HomeSetup *setup, UserRecord **ret_home);
diff --git a/src/home/homework-fscrypt.c b/src/home/homework-fscrypt.c
new file mode 100644 (file)
index 0000000..696e265
--- /dev/null
@@ -0,0 +1,644 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include <linux/fs.h>
+#include <openssl/evp.h>
+#include <openssl/sha.h>
+#include <sys/ioctl.h>
+#include <sys/xattr.h>
+
+#include "errno-util.h"
+#include "fd-util.h"
+#include "hexdecoct.h"
+#include "homework-fscrypt.h"
+#include "homework-quota.h"
+#include "memory-util.h"
+#include "missing_keyctl.h"
+#include "missing_syscall.h"
+#include "mkdir.h"
+#include "nulstr-util.h"
+#include "openssl-util.h"
+#include "parse-util.h"
+#include "process-util.h"
+#include "random-util.h"
+#include "rm-rf.h"
+#include "stdio-util.h"
+#include "strv.h"
+#include "tmpfile-util.h"
+#include "user-util.h"
+#include "xattr-util.h"
+
+static int fscrypt_upload_volume_key(
+                const uint8_t key_descriptor[static FS_KEY_DESCRIPTOR_SIZE],
+                const void *volume_key,
+                size_t volume_key_size,
+                key_serial_t where) {
+
+        _cleanup_free_ char *hex = NULL;
+        const char *description;
+        struct fscrypt_key key;
+        key_serial_t serial;
+
+        assert(key_descriptor);
+        assert(volume_key);
+        assert(volume_key_size > 0);
+
+        if (volume_key_size > sizeof(key.raw))
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Volume key too long.");
+
+        hex = hexmem(key_descriptor, FS_KEY_DESCRIPTOR_SIZE);
+        if (!hex)
+                return log_oom();
+
+        description = strjoina("fscrypt:", hex);
+
+        key = (struct fscrypt_key) {
+                .size = volume_key_size,
+        };
+        memcpy(key.raw, volume_key, volume_key_size);
+
+        /* Upload to the kernel */
+        serial = add_key("logon", description, &key, sizeof(key), where);
+        explicit_bzero_safe(&key, sizeof(key));
+
+        if (serial < 0)
+                return log_error_errno(errno, "Failed to install master key in keyring: %m");
+
+        log_info("Uploaded encryption key to kernel.");
+
+        return 0;
+}
+
+static void calculate_key_descriptor(
+                const void *key,
+                size_t key_size,
+                uint8_t ret_key_descriptor[static FS_KEY_DESCRIPTOR_SIZE]) {
+
+        uint8_t hashed[512 / 8] = {}, hashed2[512 / 8] = {};
+
+        /* Derive the key descriptor from the volume key via double SHA512, in order to be compatible with e4crypt */
+
+        assert_se(SHA512(key, key_size, hashed) == hashed);
+        assert_se(SHA512(hashed, sizeof(hashed), hashed2) == hashed2);
+
+        assert_cc(sizeof(hashed2) >= FS_KEY_DESCRIPTOR_SIZE);
+
+        memcpy(ret_key_descriptor, hashed2, FS_KEY_DESCRIPTOR_SIZE);
+}
+
+static int fscrypt_slot_try_one(
+                const char *password,
+                const void *salt, size_t salt_size,
+                const void *encrypted, size_t encrypted_size,
+                const uint8_t match_key_descriptor[static FS_KEY_DESCRIPTOR_SIZE],
+                void **ret_decrypted, size_t *ret_decrypted_size) {
+
+
+        _cleanup_(EVP_CIPHER_CTX_freep) EVP_CIPHER_CTX *context = NULL;
+        _cleanup_(erase_and_freep) void *decrypted = NULL;
+        uint8_t key_descriptor[FS_KEY_DESCRIPTOR_SIZE];
+        int decrypted_size_out1, decrypted_size_out2;
+        uint8_t derived[512 / 8] = {};
+        size_t decrypted_size;
+        const EVP_CIPHER *cc;
+        int r;
+
+        assert(password);
+        assert(salt);
+        assert(salt_size > 0);
+        assert(encrypted);
+        assert(encrypted_size > 0);
+        assert(match_key_descriptor);
+
+        /* Our construction is like this:
+         *
+         *   1. In each key slot we store a salt value plus the encrypted volume key
+         *
+         *   2. Unlocking is via calculating PBKDF2-HMAC-SHA512 of the supplied password (in combination with
+         *      the salt), then using the first 256 bit of the hash as key for decrypting the encrypted
+         *      volume key in AES256 counter mode.
+         *
+         *   3. Writing a password is similar: calculate PBKDF2-HMAC-SHA512 of the supplied password (in
+         *      combination with the salt), then encrypt the volume key in AES256 counter mode with the
+         *      resulting hash.
+         */
+
+        if (PKCS5_PBKDF2_HMAC(
+                            password, strlen(password),
+                            salt, salt_size,
+                            0xFFFF, EVP_sha512(),
+                            sizeof(derived), derived) != 1) {
+                r = log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "PBKDF2 failed");
+                goto finish;
+        }
+
+        context = EVP_CIPHER_CTX_new();
+        if (!context) {
+                r = log_oom();
+                goto finish;
+        }
+
+        /* We use AES256 in counter mode */
+        assert_se(cc = EVP_aes_256_ctr());
+
+        /* We only use the first half of the derived key */
+        assert(sizeof(derived) >= (size_t) EVP_CIPHER_key_length(cc));
+
+        if (EVP_DecryptInit_ex(context, cc, NULL, derived, NULL) != 1)  {
+                r = log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to initialize decryption context.");
+                goto finish;
+        }
+
+        /* Flush out the derived key now, we don't need it anymore */
+        explicit_bzero_safe(derived, sizeof(derived));
+
+        decrypted_size = encrypted_size + EVP_CIPHER_key_length(cc) * 2;
+        decrypted = malloc(decrypted_size);
+        if (!decrypted)
+                return log_oom();
+
+        if (EVP_DecryptUpdate(context, (uint8_t*) decrypted, &decrypted_size_out1, encrypted, encrypted_size) != 1)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to decrypt volume key.");
+
+        assert((size_t) decrypted_size_out1 <= decrypted_size);
+
+        if (EVP_DecryptFinal_ex(context, (uint8_t*) decrypted_size + decrypted_size_out1, &decrypted_size_out2) != 1)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to finish decryption of volume key.");
+
+        assert((size_t) decrypted_size_out1 + (size_t) decrypted_size_out2 < decrypted_size);
+        decrypted_size = (size_t) decrypted_size_out1 + (size_t) decrypted_size_out2;
+
+        calculate_key_descriptor(decrypted, decrypted_size, key_descriptor);
+
+        if (memcmp(key_descriptor, match_key_descriptor, FS_KEY_DESCRIPTOR_SIZE) != 0)
+                return -ENOANO; /* don't log here */
+
+        r = fscrypt_upload_volume_key(key_descriptor, decrypted, decrypted_size, KEY_SPEC_THREAD_KEYRING);
+        if (r < 0)
+                return r;
+
+        if (ret_decrypted)
+                *ret_decrypted = TAKE_PTR(decrypted);
+        if (ret_decrypted_size)
+                *ret_decrypted_size = decrypted_size;
+
+        return 0;
+
+finish:
+        explicit_bzero_safe(derived, sizeof(derived));
+        return r;
+}
+
+static int fscrypt_slot_try_many(
+                char **passwords,
+                const void *salt, size_t salt_size,
+                const void *encrypted, size_t encrypted_size,
+                const uint8_t match_key_descriptor[static FS_KEY_DESCRIPTOR_SIZE],
+                void **ret_decrypted, size_t *ret_decrypted_size) {
+
+        char **i;
+        int r;
+
+        STRV_FOREACH(i, passwords) {
+                r = fscrypt_slot_try_one(*i, salt, salt_size, encrypted, encrypted_size, match_key_descriptor, ret_decrypted, ret_decrypted_size);
+                if (r != -ENOANO)
+                        return r;
+        }
+
+        return -ENOANO;
+}
+
+static int fscrypt_setup(
+                char **pkcs11_decrypted_passwords,
+                char **password,
+                HomeSetup *setup,
+                void **ret_volume_key,
+                size_t *ret_volume_key_size) {
+
+        _cleanup_free_ char *xattr_buf = NULL;
+        const char *xa;
+        int r;
+
+        assert(setup);
+        assert(setup->root_fd >= 0);
+
+        r = flistxattr_malloc(setup->root_fd, &xattr_buf);
+        if (r < 0)
+                return log_error_errno(errno, "Failed to retrieve xattr list: %m");
+
+        NULSTR_FOREACH(xa, xattr_buf) {
+                _cleanup_free_ void *salt = NULL, *encrypted = NULL;
+                _cleanup_free_ char *value = NULL;
+                size_t salt_size, encrypted_size;
+                const char *nr, *e;
+                int n;
+
+                /* Check if this xattr has the format 'trusted.fscrypt_slot<nr>' where '<nr>' is a 32bit unsigned integer */
+                nr = startswith(xa, "trusted.fscrypt_slot");
+                if (!nr)
+                        continue;
+                if (safe_atou32(nr, NULL) < 0)
+                        continue;
+
+                n = fgetxattr_malloc(setup->root_fd, xa, &value);
+                if (n == -ENODATA) /* deleted by now? */
+                        continue;
+                if (n < 0)
+                        return log_error_errno(n, "Failed to read %s xattr: %m", xa);
+
+                e = memchr(value, ':', n);
+                if (!e)
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "xattr %s lacks ':' separator: %m", xa);
+
+                r = unbase64mem(value, e - value, &salt, &salt_size);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to decode salt of %s: %m", xa);
+                r = unbase64mem(e+1, n - (e - value) - 1, &encrypted, &encrypted_size);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to decode encrypted key of %s: %m", xa);
+
+                r = fscrypt_slot_try_many(
+                                pkcs11_decrypted_passwords,
+                                salt, salt_size,
+                                encrypted, encrypted_size,
+                                setup->fscrypt_key_descriptor,
+                                ret_volume_key, ret_volume_key_size);
+                if (r == -ENOANO)
+                        r = fscrypt_slot_try_many(
+                                        password,
+                                        salt, salt_size,
+                                        encrypted, encrypted_size,
+                                        setup->fscrypt_key_descriptor,
+                                        ret_volume_key, ret_volume_key_size);
+                if (r < 0) {
+                        if (r != -ENOANO)
+                                return r;
+                } else
+                        return 0;
+        }
+
+        return log_error_errno(SYNTHETIC_ERRNO(ENOKEY), "Failed to set up home directory with provided passwords.");
+}
+
+int home_prepare_fscrypt(
+                UserRecord *h,
+                bool already_activated,
+                char ***pkcs11_decrypted_passwords,
+                HomeSetup *setup) {
+
+        _cleanup_(erase_and_freep) void *volume_key = NULL;
+        struct fscrypt_policy policy = {};
+        size_t volume_key_size = 0;
+        const char *ip;
+        int r;
+
+        assert(h);
+        assert(setup);
+        assert(user_record_storage(h) == USER_FSCRYPT);
+
+        assert_se(ip = user_record_image_path(h));
+
+        setup->root_fd = open(ip, O_RDONLY|O_CLOEXEC|O_DIRECTORY);
+        if (setup->root_fd < 0)
+                return log_error_errno(errno, "Failed to open home directory: %m");
+
+        if (ioctl(setup->root_fd, FS_IOC_GET_ENCRYPTION_POLICY, &policy) < 0) {
+                if (errno == ENODATA)
+                        return log_error_errno(errno, "Home directory %s is not encrypted.", ip);
+                if (ERRNO_IS_NOT_SUPPORTED(errno)) {
+                        log_error_errno(errno, "File system does not support fscrypt: %m");
+                        return -ENOLINK; /* make recognizable */
+                }
+                return log_error_errno(errno, "Failed to acquire encryption policy of %s: %m", ip);
+        }
+
+        memcpy(setup->fscrypt_key_descriptor, policy.master_key_descriptor, FS_KEY_DESCRIPTOR_SIZE);
+
+        r = fscrypt_setup(
+                        pkcs11_decrypted_passwords ? *pkcs11_decrypted_passwords : NULL,
+                        h->password,
+                        setup,
+                        &volume_key,
+                        &volume_key_size);
+        if (r < 0)
+                return r;
+
+        /* Also install the access key in the user's own keyring */
+
+        if (uid_is_valid(h->uid)) {
+                r = safe_fork("(sd-addkey)", FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG|FORK_LOG|FORK_WAIT, NULL);
+                if (r < 0)
+                        return log_error_errno(r, "Failed install encryption key in user's keyring: %m");
+                if (r == 0) {
+                        gid_t gid;
+
+                        /* Child */
+
+                        gid = user_record_gid(h);
+                        if (setresgid(gid, gid, gid) < 0) {
+                                log_error_errno(errno, "Failed to change GID to " GID_FMT ": %m", gid);
+                                _exit(EXIT_FAILURE);
+                        }
+
+                        if (setgroups(0, NULL) < 0) {
+                                log_error_errno(errno, "Failed to reset auxiliary groups list: %m");
+                                _exit(EXIT_FAILURE);
+                        }
+
+                        if (setresuid(h->uid, h->uid, h->uid) < 0) {
+                                log_error_errno(errno, "Failed to change UID to " UID_FMT ": %m", h->uid);
+                                _exit(EXIT_FAILURE);
+                        }
+
+                        r = fscrypt_upload_volume_key(
+                                        setup->fscrypt_key_descriptor,
+                                        volume_key,
+                                        volume_key_size,
+                                        KEY_SPEC_USER_KEYRING);
+                        if (r < 0)
+                                _exit(EXIT_FAILURE);
+
+                        _exit(EXIT_SUCCESS);
+                }
+        }
+
+        return 0;
+}
+
+static int fscrypt_slot_set(
+                int root_fd,
+                const void *volume_key,
+                size_t volume_key_size,
+                const char *password,
+                uint32_t nr) {
+
+        _cleanup_free_ char *salt_base64 = NULL, *encrypted_base64 = NULL, *joined = NULL;
+        char label[STRLEN("trusted.fscrypt_slot") + DECIMAL_STR_MAX(nr) + 1];
+        _cleanup_(EVP_CIPHER_CTX_freep) EVP_CIPHER_CTX *context = NULL;
+        int r, encrypted_size_out1, encrypted_size_out2;
+        uint8_t salt[64], derived[512 / 8] = {};
+        _cleanup_free_ void *encrypted = NULL;
+        const EVP_CIPHER *cc;
+        size_t encrypted_size;
+
+        r = genuine_random_bytes(salt, sizeof(salt), RANDOM_BLOCK);
+        if (r < 0)
+                return log_error_errno(r, "Failed to generate salt: %m");
+
+        if (PKCS5_PBKDF2_HMAC(
+                            password, strlen(password),
+                            salt, sizeof(salt),
+                            0xFFFF, EVP_sha512(),
+                            sizeof(derived), derived) != 1) {
+                r = log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "PBKDF2 failed");
+                goto finish;
+        }
+
+        context = EVP_CIPHER_CTX_new();
+        if (!context) {
+                r = log_oom();
+                goto finish;
+        }
+
+        /* We use AES256 in counter mode */
+        cc = EVP_aes_256_ctr();
+
+        /* We only use the first half of the derived key */
+        assert(sizeof(derived) >= (size_t) EVP_CIPHER_key_length(cc));
+
+        if (EVP_EncryptInit_ex(context, cc, NULL, derived, NULL) != 1)  {
+                r = log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to initialize encryption context.");
+                goto finish;
+        }
+
+        /* Flush out the derived key now, we don't need it anymore */
+        explicit_bzero_safe(derived, sizeof(derived));
+
+        encrypted_size = volume_key_size + EVP_CIPHER_key_length(cc) * 2;
+        encrypted = malloc(encrypted_size);
+        if (!encrypted)
+                return log_oom();
+
+        if (EVP_EncryptUpdate(context, (uint8_t*) encrypted, &encrypted_size_out1, volume_key, volume_key_size) != 1)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to encrypt volume key.");
+
+        assert((size_t) encrypted_size_out1 <= encrypted_size);
+
+        if (EVP_EncryptFinal_ex(context, (uint8_t*) encrypted_size + encrypted_size_out1, &encrypted_size_out2) != 1)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to finish encryption of volume key.");
+
+        assert((size_t) encrypted_size_out1 + (size_t) encrypted_size_out2 < encrypted_size);
+        encrypted_size = (size_t) encrypted_size_out1 + (size_t) encrypted_size_out2;
+
+        r = base64mem(salt, sizeof(salt), &salt_base64);
+        if (r < 0)
+                return log_oom();
+
+        r = base64mem(encrypted, encrypted_size, &encrypted_base64);
+        if (r < 0)
+                return log_oom();
+
+        joined = strjoin(salt_base64, ":", encrypted_base64);
+        if (!joined)
+                return log_oom();
+
+        xsprintf(label, "trusted.fscrypt_slot%" PRIu32, nr);
+        if (fsetxattr(root_fd, label, joined, strlen(joined), 0) < 0)
+                return log_error_errno(errno, "Failed to write xattr %s: %m", label);
+
+        log_info("Written key slot %s.", label);
+
+        return 0;
+
+finish:
+        explicit_bzero_safe(derived, sizeof(derived));
+        return r;
+}
+
+int home_create_fscrypt(
+                UserRecord *h,
+                char **effective_passwords,
+                UserRecord **ret_home) {
+
+        _cleanup_(rm_rf_physical_and_freep) char *temporary = NULL;
+        _cleanup_(user_record_unrefp) UserRecord *new_home = NULL;
+        _cleanup_(erase_and_freep) void *volume_key = NULL;
+        struct fscrypt_policy policy = {};
+        size_t volume_key_size = 512 / 8;
+        _cleanup_close_ int root_fd = -1;
+        _cleanup_free_ char *d = NULL;
+        uint32_t nr = 0;
+        const char *ip;
+        char **i;
+        int r;
+
+        assert(h);
+        assert(user_record_storage(h) == USER_FSCRYPT);
+        assert(ret_home);
+
+        assert_se(ip = user_record_image_path(h));
+
+        r = tempfn_random(ip, "homework", &d);
+        if (r < 0)
+                return log_error_errno(r, "Failed to allocate temporary directory: %m");
+
+        (void) mkdir_parents(d, 0755);
+
+        if (mkdir(d, 0700) < 0)
+                return log_error_errno(errno, "Failed to create temporary home directory %s: %m", d);
+
+        temporary = TAKE_PTR(d); /* Needs to be destroyed now */
+
+        root_fd = open(temporary, O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW);
+        if (root_fd < 0)
+                return log_error_errno(errno, "Failed to open temporary home directory: %m");
+
+        if (ioctl(root_fd, FS_IOC_GET_ENCRYPTION_POLICY, &policy) < 0) {
+                if (ERRNO_IS_NOT_SUPPORTED(errno)) {
+                        log_error_errno(errno, "File system does not support fscrypt: %m");
+                        return -ENOLINK; /* make recognizable */
+                }
+                if (errno != ENODATA)
+                        return log_error_errno(errno, "Failed to get fscrypt policy of directory: %m");
+        } else
+                return log_error_errno(SYNTHETIC_ERRNO(EBUSY), "Parent of %s already encrypted, refusing.", d);
+
+        volume_key = malloc(volume_key_size);
+        if (!volume_key)
+                return log_oom();
+
+        r = genuine_random_bytes(volume_key, volume_key_size, RANDOM_BLOCK);
+        if (r < 0)
+                return log_error_errno(r, "Failed to acquire volume key: %m");
+
+        log_info("Generated volume key of size %zu.", volume_key_size);
+
+        policy = (struct fscrypt_policy) {
+                .contents_encryption_mode = FS_ENCRYPTION_MODE_AES_256_XTS,
+                .filenames_encryption_mode = FS_ENCRYPTION_MODE_AES_256_CTS,
+                .flags = FS_POLICY_FLAGS_PAD_32,
+        };
+
+        calculate_key_descriptor(volume_key, volume_key_size, policy.master_key_descriptor);
+
+        r = fscrypt_upload_volume_key(policy.master_key_descriptor, volume_key, volume_key_size, KEY_SPEC_THREAD_KEYRING);
+        if (r < 0)
+                return r;
+
+        log_info("Uploaded volume key to kernel.");
+
+        if (ioctl(root_fd, FS_IOC_SET_ENCRYPTION_POLICY, &policy) < 0)
+                return log_error_errno(errno, "Failed to set fscrypt policy on directory: %m");
+
+        log_info("Encryption policy set.");
+
+        STRV_FOREACH(i, effective_passwords) {
+                r = fscrypt_slot_set(root_fd, volume_key, volume_key_size, *i, nr);
+                if (r < 0)
+                        return r;
+
+                nr++;
+        }
+
+        (void) home_update_quota_classic(h, temporary);
+
+        r = home_populate(h, root_fd);
+        if (r < 0)
+                return r;
+
+        r = home_sync_and_statfs(root_fd, NULL);
+        if (r < 0)
+                return r;
+
+        r = user_record_clone(h, USER_RECORD_LOAD_MASK_SECRET, &new_home);
+        if (r < 0)
+                return log_error_errno(r, "Failed to clone record: %m");
+
+        r = user_record_add_binding(
+                        new_home,
+                        USER_FSCRYPT,
+                        ip,
+                        SD_ID128_NULL,
+                        SD_ID128_NULL,
+                        SD_ID128_NULL,
+                        NULL,
+                        NULL,
+                        UINT64_MAX,
+                        NULL,
+                        NULL,
+                        h->uid,
+                        (gid_t) h->uid);
+        if (r < 0)
+                return log_error_errno(r, "Failed to add binding to record: %m");
+
+        if (rename(temporary, ip) < 0)
+                return log_error_errno(errno, "Failed to rename %s to %s: %m", temporary, ip);
+
+        temporary = mfree(temporary);
+
+        log_info("Everything completed.");
+
+        *ret_home = TAKE_PTR(new_home);
+        return 0;
+}
+
+int home_passwd_fscrypt(
+                UserRecord *h,
+                HomeSetup *setup,
+                char **pkcs11_decrypted_passwords, /* the passwords acquired via PKCS#11 security tokens */
+                char **effective_passwords          /* new passwords */) {
+
+        _cleanup_(erase_and_freep) void *volume_key = NULL;
+        _cleanup_free_ char *xattr_buf = NULL;
+        size_t volume_key_size = 0;
+        uint32_t slot = 0;
+        const char *xa;
+        char **p;
+        int r;
+
+        assert(h);
+        assert(user_record_storage(h) == USER_FSCRYPT);
+        assert(setup);
+
+        r = fscrypt_setup(
+                        pkcs11_decrypted_passwords,
+                        h->password,
+                        setup,
+                        &volume_key,
+                        &volume_key_size);
+        if (r < 0)
+                return r;
+
+        STRV_FOREACH(p, effective_passwords) {
+                r = fscrypt_slot_set(setup->root_fd, volume_key, volume_key_size, *p, slot);
+                if (r < 0)
+                        return r;
+
+                slot++;
+        }
+
+        r = flistxattr_malloc(setup->root_fd, &xattr_buf);
+        if (r < 0)
+                return log_error_errno(errno, "Failed to retrieve xattr list: %m");
+
+        NULSTR_FOREACH(xa, xattr_buf) {
+                const char *nr;
+                uint32_t z;
+
+                /* Check if this xattr has the format 'trusted.fscrypt_slot<nr>' where '<nr>' is a 32bit unsigned integer */
+                nr = startswith(xa, "trusted.fscrypt_slot");
+                if (!nr)
+                        continue;
+                if (safe_atou32(nr, &z) < 0)
+                        continue;
+
+                if (z < slot)
+                        continue;
+
+                if (fremovexattr(setup->root_fd, xa) < 0)
+
+                        if (errno != ENODATA)
+                                log_warning_errno(errno, "Failed to remove xattr %s: %m", xa);
+        }
+
+        return 0;
+}
diff --git a/src/home/homework-fscrypt.h b/src/home/homework-fscrypt.h
new file mode 100644 (file)
index 0000000..aa3bcd3
--- /dev/null
@@ -0,0 +1,10 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include "homework.h"
+#include "user-record.h"
+
+int home_prepare_fscrypt(UserRecord *h, bool already_activated, char ***pkcs11_decrypted_passwords, HomeSetup *setup);
+int home_create_fscrypt(UserRecord *h, char **effective_passwords, UserRecord **ret_home);
+
+int home_passwd_fscrypt(UserRecord *h, HomeSetup *setup, char **pkcs11_decrypted_passwords, char **effective_passwords);
diff --git a/src/home/homework-luks.c b/src/home/homework-luks.c
new file mode 100644 (file)
index 0000000..0cd5902
--- /dev/null
@@ -0,0 +1,2954 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include <libfdisk.h>
+#include <linux/loop.h>
+#include <poll.h>
+#include <sys/file.h>
+#include <sys/ioctl.h>
+
+#include "blkid-util.h"
+#include "blockdev-util.h"
+#include "chattr-util.h"
+#include "dm-util.h"
+#include "errno-util.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "fs-util.h"
+#include "fsck-util.h"
+#include "homework-luks.h"
+#include "homework-mount.h"
+#include "id128-util.h"
+#include "io-util.h"
+#include "memory-util.h"
+#include "missing_magic.h"
+#include "mkdir.h"
+#include "mount-util.h"
+#include "openssl-util.h"
+#include "parse-util.h"
+#include "path-util.h"
+#include "process-util.h"
+#include "random-util.h"
+#include "resize-fs.h"
+#include "stat-util.h"
+#include "strv.h"
+#include "tmpfile-util.h"
+
+/* Round down to the nearest 1K size. Note that Linux generally handles block devices with 512 blocks only,
+ * but actually doesn't accept uneven numbers in many cases. To avoid any confusion around this we'll
+ * strictly round disk sizes down to the next 1K boundary.*/
+#define DISK_SIZE_ROUND_DOWN(x) ((x) & ~UINT64_C(1023))
+
+static bool supported_fstype(const char *fstype) {
+        /* Limit the set of supported file systems a bit, as protection against little tested kernel file
+         * systems. Also, we only support the resize ioctls for these file systems. */
+        return STR_IN_SET(fstype, "ext4", "btrfs", "xfs");
+}
+
+static int probe_file_system_by_fd(
+                int fd,
+                char **ret_fstype,
+                sd_id128_t *ret_uuid) {
+
+        _cleanup_(blkid_free_probep) blkid_probe b = NULL;
+        _cleanup_free_ char *s = NULL;
+        const char *fstype = NULL, *uuid = NULL;
+        sd_id128_t id;
+        int r;
+
+        assert(fd >= 0);
+        assert(ret_fstype);
+        assert(ret_uuid);
+
+        b = blkid_new_probe();
+        if (!b)
+                return -ENOMEM;
+
+        errno = 0;
+        r = blkid_probe_set_device(b, fd, 0, 0);
+        if (r != 0)
+                return errno > 0 ? -errno : -ENOMEM;
+
+        (void) blkid_probe_enable_superblocks(b, 1);
+        (void) blkid_probe_set_superblocks_flags(b, BLKID_SUBLKS_TYPE|BLKID_SUBLKS_UUID);
+
+        errno = 0;
+        r = blkid_do_safeprobe(b);
+        if (IN_SET(r, -2, 1)) /* nothing found or ambiguous result */
+                return -ENOPKG;
+        if (r != 0)
+                return errno > 0 ? -errno : -EIO;
+
+        (void) blkid_probe_lookup_value(b, "TYPE", &fstype, NULL);
+        if (!fstype)
+                return -ENOPKG;
+
+        (void) blkid_probe_lookup_value(b, "UUID", &uuid, NULL);
+        if (!uuid)
+                return -ENOPKG;
+
+        r = sd_id128_from_string(uuid, &id);
+        if (r < 0)
+                return r;
+
+        s = strdup(fstype);
+        if (!s)
+                return -ENOMEM;
+
+        *ret_fstype = TAKE_PTR(s);
+        *ret_uuid = id;
+
+        return 0;
+}
+
+static int probe_file_system_by_path(const char *path, char **ret_fstype, sd_id128_t *ret_uuid) {
+        _cleanup_close_ int fd = -1;
+
+        fd = open(path, O_RDONLY|O_CLOEXEC|O_NOCTTY|O_NONBLOCK);
+        if (fd < 0)
+                return -errno;
+
+        return probe_file_system_by_fd(fd, ret_fstype, ret_uuid);
+}
+
+static int block_get_size_by_fd(int fd, uint64_t *ret) {
+        struct stat st;
+
+        assert(fd >= 0);
+        assert(ret);
+
+        if (fstat(fd, &st) < 0)
+                return -errno;
+
+        if (!S_ISBLK(st.st_mode))
+                return -ENOTBLK;
+
+        if (ioctl(fd, BLKGETSIZE64, ret) < 0)
+                return -errno;
+
+        return 0;
+}
+
+static int block_get_size_by_path(const char *path, uint64_t *ret) {
+        _cleanup_close_ int fd = -1;
+
+        fd = open(path, O_RDONLY|O_CLOEXEC|O_NOCTTY|O_NONBLOCK);
+        if (fd < 0)
+                return -errno;
+
+        return block_get_size_by_fd(fd, ret);
+}
+
+static int run_fsck(const char *node, const char *fstype) {
+        int r, exit_status;
+        pid_t fsck_pid;
+
+        assert(node);
+        assert(fstype);
+
+        r = fsck_exists(fstype);
+        if (r < 0)
+                return log_error_errno(r, "Failed to check if fsck for file system %s exists: %m", fstype);
+        if (r == 0) {
+                log_warning("No fsck for file system %s installed, ignoring.", fstype);
+                return 0;
+        }
+
+        r = safe_fork("(fsck)", FORK_RESET_SIGNALS|FORK_RLIMIT_NOFILE_SAFE|FORK_DEATHSIG|FORK_LOG|FORK_STDOUT_TO_STDERR, &fsck_pid);
+        if (r < 0)
+                return r;
+        if (r == 0) {
+                /* Child */
+                execl("/sbin/fsck", "/sbin/fsck", "-aTl", node, NULL);
+                log_error_errno(errno, "Failed to execute fsck: %m");
+                _exit(FSCK_OPERATIONAL_ERROR);
+        }
+
+        exit_status = wait_for_terminate_and_check("fsck", fsck_pid, WAIT_LOG_ABNORMAL);
+        if (exit_status < 0)
+                return exit_status;
+        if ((exit_status & ~FSCK_ERROR_CORRECTED) != 0) {
+                log_warning("fsck failed with exit status %i.", exit_status);
+
+                if ((exit_status & (FSCK_SYSTEM_SHOULD_REBOOT|FSCK_ERRORS_LEFT_UNCORRECTED)) != 0)
+                        return log_error_errno(SYNTHETIC_ERRNO(EIO), "File system is corrupted, refusing.");
+
+                log_warning("Ignoring fsck error.");
+        }
+
+        log_info("File system check completed.");
+
+        return 1;
+}
+
+static int luks_try_passwords(
+                struct crypt_device *cd,
+                char **passwords,
+                void *volume_key,
+                size_t *volume_key_size) {
+
+        char **pp;
+        int r;
+
+        assert(cd);
+
+        STRV_FOREACH(pp, passwords) {
+                size_t vks = *volume_key_size;
+
+                r = crypt_volume_key_get(
+                                cd,
+                                CRYPT_ANY_SLOT,
+                                volume_key,
+                                &vks,
+                                *pp,
+                                strlen(*pp));
+                if (r >= 0) {
+                        *volume_key_size = vks;
+                        return 0;
+                }
+
+                log_debug_errno(r, "Password %zu didn't work for unlocking LUKS superblock: %m", (size_t) (pp - passwords));
+        }
+
+        return -ENOKEY;
+}
+
+static int luks_setup(
+                const char *node,
+                const char *dm_name,
+                sd_id128_t uuid,
+                const char *cipher,
+                const char *cipher_mode,
+                uint64_t volume_key_size,
+                char **passwords,
+                char **pkcs11_decrypted_passwords,
+                bool discard,
+                struct crypt_device **ret,
+                sd_id128_t *ret_found_uuid,
+                void **ret_volume_key,
+                size_t *ret_volume_key_size) {
+
+        _cleanup_(crypt_freep) struct crypt_device *cd = NULL;
+        _cleanup_(erase_and_freep) void *vk = NULL;
+        sd_id128_t p;
+        size_t vks;
+        int r;
+
+        assert(node);
+        assert(dm_name);
+        assert(ret);
+
+        r = crypt_init(&cd, node);
+        if (r < 0)
+                return log_error_errno(r, "Failed to allocate libcryptsetup context: %m");
+
+        crypt_set_log_callback(cd, cryptsetup_log_glue, NULL);
+
+        r = crypt_load(cd, CRYPT_LUKS2, NULL);
+        if (r < 0)
+                return log_error_errno(r, "Failed to load LUKS superblock: %m");
+
+        r = crypt_get_volume_key_size(cd);
+        if (r <= 0)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to determine LUKS volume key size");
+        vks = (size_t) r;
+
+        if (!sd_id128_is_null(uuid) || ret_found_uuid) {
+                const char *s;
+
+                s = crypt_get_uuid(cd);
+                if (!s)
+                        return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "LUKS superblock has no UUID.");
+
+                r = sd_id128_from_string(s, &p);
+                if (r < 0)
+                        return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "LUKS superblock has invalid UUID.");
+
+                /* Check that the UUID matches, if specified */
+                if (!sd_id128_is_null(uuid) &&
+                    !sd_id128_equal(uuid, p))
+                        return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "LUKS superblock has wrong UUID.");
+        }
+
+        if (cipher && !streq_ptr(cipher, crypt_get_cipher(cd)))
+                return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "LUKS superblock declares wrong cipher.");
+
+        if (cipher_mode && !streq_ptr(cipher_mode, crypt_get_cipher_mode(cd)))
+                return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "LUKS superblock declares wrong cipher mode.");
+
+        if (volume_key_size != UINT64_MAX && vks != volume_key_size)
+                return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "LUKS superblock declares wrong volume key size.");
+
+        vk = malloc(vks);
+        if (!vk)
+                return log_oom();
+
+        r = luks_try_passwords(cd, pkcs11_decrypted_passwords, vk, &vks);
+        if (r == -ENOKEY) {
+                r = luks_try_passwords(cd, passwords, vk, &vks);
+                if (r == -ENOKEY)
+                        return log_error_errno(r, "No valid password for LUKS superblock.");
+        }
+        if (r < 0)
+                return log_error_errno(r, "Failed to unlocks LUKS superblock: %m");
+
+        r = crypt_activate_by_volume_key(
+                        cd,
+                        dm_name,
+                        vk, vks,
+                        discard ? CRYPT_ACTIVATE_ALLOW_DISCARDS : 0);
+        if (r < 0)
+                return log_error_errno(r, "Failed to unlock LUKS superblock: %m");
+
+        log_info("Setting up LUKS device /dev/mapper/%s completed.", dm_name);
+
+        *ret = TAKE_PTR(cd);
+
+        if (ret_found_uuid) /* Return the UUID actually found if the caller wants to know */
+                *ret_found_uuid = p;
+        if (ret_volume_key)
+                *ret_volume_key = TAKE_PTR(vk);
+        if (ret_volume_key_size)
+                *ret_volume_key_size = vks;
+
+        return 0;
+}
+
+static int luks_open(
+                const char *dm_name,
+                char **passwords,
+                char **pkcs11_decrypted_passwords,
+                struct crypt_device **ret,
+                sd_id128_t *ret_found_uuid,
+                void **ret_volume_key,
+                size_t *ret_volume_key_size) {
+
+        _cleanup_(crypt_freep) struct crypt_device *cd = NULL;
+        _cleanup_(erase_and_freep) void *vk = NULL;
+        sd_id128_t p;
+        size_t vks;
+        int r;
+
+        assert(dm_name);
+        assert(ret);
+
+        /* Opens a LUKS device that is already set up. Re-validates the password while doing so (which also
+         * provides us with the volume key, which we want). */
+
+        r = crypt_init_by_name(&cd, dm_name);
+        if (r < 0)
+                return log_error_errno(r, "Failed to initialize cryptsetup context for %s: %m", dm_name);
+
+        crypt_set_log_callback(cd, cryptsetup_log_glue, NULL);
+
+        r = crypt_load(cd, CRYPT_LUKS2, NULL);
+        if (r < 0)
+                return log_error_errno(r, "Failed to load LUKS superblock: %m");
+
+        r = crypt_get_volume_key_size(cd);
+        if (r <= 0)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to determine LUKS volume key size");
+        vks = (size_t) r;
+
+        if (ret_found_uuid) {
+                const char *s;
+
+                s = crypt_get_uuid(cd);
+                if (!s)
+                        return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "LUKS superblock has no UUID.");
+
+                r = sd_id128_from_string(s, &p);
+                if (r < 0)
+                        return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "LUKS superblock has invalid UUID.");
+        }
+
+        vk = malloc(vks);
+        if (!vk)
+                return log_oom();
+
+        r = luks_try_passwords(cd, pkcs11_decrypted_passwords, vk, &vks);
+        if (r == -ENOKEY) {
+                r = luks_try_passwords(cd, passwords, vk, &vks);
+                if (r == -ENOKEY)
+                        return log_error_errno(r, "No valid password for LUKS superblock.");
+        }
+        if (r < 0)
+                return log_error_errno(r, "Failed to unlocks LUKS superblock: %m");
+
+        log_info("Discovered used LUKS device /dev/mapper/%s, and validated password.", dm_name);
+
+        /* This is needed so that crypt_resize() can operate correctly for pre-existing LUKS devices. We need
+         * to tell libcryptsetup the volume key explicitly, so that it is in the kernel keyring. */
+        r = crypt_activate_by_volume_key(cd, NULL, vk, vks, CRYPT_ACTIVATE_KEYRING_KEY);
+        if (r < 0)
+                return log_error_errno(r, "Failed to upload volume key again: %m");
+
+        log_info("Successfully re-activated LUKS device.");
+
+        *ret = TAKE_PTR(cd);
+
+        if (ret_found_uuid)
+                *ret_found_uuid = p;
+        if (ret_volume_key)
+                *ret_volume_key = TAKE_PTR(vk);
+        if (ret_volume_key_size)
+                *ret_volume_key_size = vks;
+
+        return 0;
+}
+
+static int fs_validate(
+                const char *dm_node,
+                sd_id128_t uuid,
+                char **ret_fstype,
+                sd_id128_t *ret_found_uuid) {
+
+        _cleanup_free_ char *fstype = NULL;
+        sd_id128_t u;
+        int r;
+
+        assert(dm_node);
+        assert(ret_fstype);
+
+        r = probe_file_system_by_path(dm_node, &fstype, &u);
+        if (r < 0)
+                return log_error_errno(r, "Failed to probe file system: %m");
+
+        /* Limit the set of supported file systems a bit, as protection against little tested kernel file
+         * systems. Also, we only support the resize ioctls for these file systems. */
+        if (!supported_fstype(fstype))
+                return log_error_errno(SYNTHETIC_ERRNO(EPROTONOSUPPORT), "Image contains unsupported file system: %s", strna(fstype));
+
+        if (!sd_id128_is_null(uuid) &&
+            !sd_id128_equal(uuid, u))
+                return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "File system has wrong UUID.");
+
+        log_info("Probing file system completed (found %s).", fstype);
+
+        *ret_fstype = TAKE_PTR(fstype);
+
+        if (ret_found_uuid) /* Return the UUID actually found if the caller wants to know */
+                *ret_found_uuid = u;
+
+        return 0;
+}
+
+static int make_dm_names(const char *user_name, char **ret_dm_name, char **ret_dm_node) {
+        _cleanup_free_ char *name = NULL, *node = NULL;
+
+        assert(user_name);
+        assert(ret_dm_name);
+        assert(ret_dm_node);
+
+        name = strjoin("home-", user_name);
+        if (!name)
+                return log_oom();
+
+        node = path_join("/dev/mapper/", name);
+        if (!node)
+                return log_oom();
+
+        *ret_dm_name = TAKE_PTR(name);
+        *ret_dm_node = TAKE_PTR(node);
+        return 0;
+}
+
+static int luks_validate(
+                int fd,
+                const char *label,
+                sd_id128_t partition_uuid,
+                sd_id128_t *ret_partition_uuid,
+                uint64_t *ret_offset,
+                uint64_t *ret_size) {
+
+        _cleanup_(blkid_free_probep) blkid_probe b = NULL;
+        sd_id128_t found_partition_uuid = SD_ID128_NULL;
+        const char *fstype = NULL, *pttype = NULL;
+        blkid_loff_t offset = 0, size = 0;
+        blkid_partlist pl;
+        bool found = false;
+        int r, i, n;
+
+        assert(fd >= 0);
+        assert(label);
+        assert(ret_offset);
+        assert(ret_size);
+
+        b = blkid_new_probe();
+        if (!b)
+                return -ENOMEM;
+
+        errno = 0;
+        r = blkid_probe_set_device(b, fd, 0, 0);
+        if (r != 0)
+                return errno > 0 ? -errno : -ENOMEM;
+
+        (void) blkid_probe_enable_superblocks(b, 1);
+        (void) blkid_probe_set_superblocks_flags(b, BLKID_SUBLKS_TYPE);
+        (void) blkid_probe_enable_partitions(b, 1);
+        (void) blkid_probe_set_partitions_flags(b, BLKID_PARTS_ENTRY_DETAILS);
+
+        errno = 0;
+        r = blkid_do_safeprobe(b);
+        if (IN_SET(r, -2, 1)) /* nothing found or ambiguous result */
+                return -ENOPKG;
+        if (r != 0)
+                return errno > 0 ? -errno : -EIO;
+
+        (void) blkid_probe_lookup_value(b, "TYPE", &fstype, NULL);
+        if (streq_ptr(fstype, "crypto_LUKS")) {
+                /* Directly a LUKS image */
+                *ret_offset = 0;
+                *ret_size = UINT64_MAX; /* full disk */
+                *ret_partition_uuid = SD_ID128_NULL;
+                return 0;
+        } else if (fstype)
+                return -ENOPKG;
+
+        (void) blkid_probe_lookup_value(b, "PTTYPE", &pttype, NULL);
+        if (!streq_ptr(pttype, "gpt"))
+                return -ENOPKG;
+
+        errno = 0;
+        pl = blkid_probe_get_partitions(b);
+        if (!pl)
+                return errno > 0 ? -errno : -ENOMEM;
+
+        errno = 0;
+        n = blkid_partlist_numof_partitions(pl);
+        if (n < 0)
+                return errno > 0 ? -errno : -EIO;
+
+        for (i = 0; i < n; i++) {
+                blkid_partition pp;
+                sd_id128_t id;
+                const char *sid;
+
+                errno = 0;
+                pp = blkid_partlist_get_partition(pl, i);
+                if (!pp)
+                        return errno > 0 ? -errno : -EIO;
+
+                if (!streq_ptr(blkid_partition_get_type_string(pp), "773f91ef-66d4-49b5-bd83-d683bf40ad16"))
+                        continue;
+
+                if (!streq_ptr(blkid_partition_get_name(pp), label))
+                        continue;
+
+                sid = blkid_partition_get_uuid(pp);
+                if (sid) {
+                        r = sd_id128_from_string(sid, &id);
+                        if (r < 0)
+                                log_debug_errno(r, "Couldn't parse partition UUID %s, weird: %m", sid);
+
+                        if (!sd_id128_is_null(partition_uuid) && !sd_id128_equal(id, partition_uuid))
+                                continue;
+                }
+
+                if (found)
+                        return -ENOPKG;
+
+                offset = blkid_partition_get_start(pp);
+                size = blkid_partition_get_size(pp);
+                found_partition_uuid = id;
+
+                found = true;
+        }
+
+        if (!found)
+                return -ENOPKG;
+
+        if (offset < 0)
+                return -EINVAL;
+        if ((uint64_t) offset > UINT64_MAX / 512U)
+                return -EINVAL;
+        if (size <= 0)
+                return -EINVAL;
+        if ((uint64_t) size > UINT64_MAX / 512U)
+                return -EINVAL;
+
+        *ret_offset = offset * 512U;
+        *ret_size = size * 512U;
+        *ret_partition_uuid = found_partition_uuid;
+
+        return 0;
+}
+
+static int crypt_device_to_evp_cipher(struct crypt_device *cd, const EVP_CIPHER **ret) {
+        _cleanup_free_ char *cipher_name = NULL;
+        const char *cipher, *cipher_mode, *e;
+        size_t key_size, key_bits;
+        const EVP_CIPHER *cc;
+        int r;
+
+        assert(cd);
+
+        /* Let's find the right OpenSSL EVP_CIPHER object that matches the encryption settings of the LUKS
+         * device */
+
+        cipher = crypt_get_cipher(cd);
+        if (!cipher)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Cannot get cipher from LUKS device.");
+
+        cipher_mode = crypt_get_cipher_mode(cd);
+        if (!cipher_mode)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Cannot get cipher mode from LUKS device.");
+
+        e = strchr(cipher_mode, '-');
+        if (e)
+                cipher_mode = strndupa(cipher_mode, e - cipher_mode);
+
+        r = crypt_get_volume_key_size(cd);
+        if (r <= 0)
+                return log_error_errno(r < 0 ? r : SYNTHETIC_ERRNO(EINVAL), "Cannot get volume key size from LUKS device.");
+
+        key_size = r;
+        key_bits = key_size * 8;
+        if (streq(cipher_mode, "xts"))
+                key_bits /= 2;
+
+        if (asprintf(&cipher_name, "%s-%zu-%s", cipher, key_bits, cipher_mode) < 0)
+                return log_oom();
+
+        cc = EVP_get_cipherbyname(cipher_name);
+        if (!cc)
+                return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Selected cipher mode '%s' not supported, can't encrypt JSON record.", cipher_name);
+
+        /* Verify that our key length calculations match what OpenSSL thinks */
+        r = EVP_CIPHER_key_length(cc);
+        if (r < 0 || (uint64_t) r != key_size)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Key size of selected cipher doesn't meet out expectations.");
+
+        *ret = cc;
+        return 0;
+}
+
+static int luks_validate_home_record(
+                struct crypt_device *cd,
+                UserRecord *h,
+                const void *volume_key,
+                char ***pkcs11_decrypted_passwords,
+                UserRecord **ret_luks_home_record) {
+
+        int r, token;
+
+        assert(cd);
+        assert(h);
+
+        for (token = 0;; token++) {
+                _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *rr = NULL;
+                _cleanup_(EVP_CIPHER_CTX_freep) EVP_CIPHER_CTX *context = NULL;
+                _cleanup_(user_record_unrefp) UserRecord *lhr = NULL;
+                _cleanup_free_ void *encrypted = NULL, *iv = NULL;
+                size_t decrypted_size, encrypted_size, iv_size;
+                int decrypted_size_out1, decrypted_size_out2;
+                _cleanup_free_ char *decrypted = NULL;
+                const char *text, *type;
+                crypt_token_info state;
+                JsonVariant *jr, *jiv;
+                unsigned line, column;
+                const EVP_CIPHER *cc;
+
+                state = crypt_token_status(cd, token, &type);
+                if (state == CRYPT_TOKEN_INACTIVE) /* First unconfigured token, give up */
+                        break;
+                if (IN_SET(state, CRYPT_TOKEN_INTERNAL, CRYPT_TOKEN_INTERNAL_UNKNOWN, CRYPT_TOKEN_EXTERNAL))
+                        continue;
+                if (state != CRYPT_TOKEN_EXTERNAL_UNKNOWN)
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unexpected token state of token %i: %i", token, (int) state);
+
+                if (!streq(type, "systemd-homed"))
+                        continue;
+
+                r = crypt_token_json_get(cd, token, &text);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to read LUKS token %i: %m", token);
+
+                r = json_parse(text, JSON_PARSE_SENSITIVE, &v, &line, &column);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to parse LUKS token JSON data %u:%u: %m", line, column);
+
+                jr = json_variant_by_key(v, "record");
+                if (!jr)
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "LUKS token lacks 'record' field.");
+                jiv = json_variant_by_key(v, "iv");
+                if (!jiv)
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "LUKS token lacks 'iv' field.");
+
+                r = json_variant_unbase64(jr, &encrypted, &encrypted_size);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to base64 decode record: %m");
+
+                r = json_variant_unbase64(jiv, &iv, &iv_size);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to base64 decode IV: %m");
+
+                r = crypt_device_to_evp_cipher(cd, &cc);
+                if (r < 0)
+                        return r;
+                if (iv_size > INT_MAX || EVP_CIPHER_iv_length(cc) != (int) iv_size)
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "IV size doesn't match.");
+
+                context = EVP_CIPHER_CTX_new();
+                if (!context)
+                        return log_oom();
+
+                if (EVP_DecryptInit_ex(context, cc, NULL, volume_key, iv) != 1)
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to initialize decryption context.");
+
+                decrypted_size = encrypted_size + EVP_CIPHER_key_length(cc) * 2;
+                decrypted = new(char, decrypted_size);
+                if (!decrypted)
+                        return log_oom();
+
+                if (EVP_DecryptUpdate(context, (uint8_t*) decrypted, &decrypted_size_out1, encrypted, encrypted_size) != 1)
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to decrypt JSON record.");
+
+                assert((size_t) decrypted_size_out1 <= decrypted_size);
+
+                if (EVP_DecryptFinal_ex(context, (uint8_t*) decrypted + decrypted_size_out1, &decrypted_size_out2) != 1)
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to finish decryption of JSON record.");
+
+                assert((size_t) decrypted_size_out1 + (size_t) decrypted_size_out2 < decrypted_size);
+                decrypted_size = (size_t) decrypted_size_out1 + (size_t) decrypted_size_out2;
+
+                if (memchr(decrypted, 0, decrypted_size))
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Inner NUL byte in JSON record, refusing.");
+
+                decrypted[decrypted_size] = 0;
+
+                r = json_parse(decrypted, JSON_PARSE_SENSITIVE, &rr, NULL, NULL);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to parse decrypted JSON record, refusing.");
+
+                lhr = user_record_new();
+                if (!lhr)
+                        return log_oom();
+
+                r = user_record_load(lhr, rr, USER_RECORD_LOAD_EMBEDDED);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to parse user record: %m");
+
+                if (!user_record_compatible(h, lhr))
+                        return log_error_errno(SYNTHETIC_ERRNO(EREMCHG), "LUKS home record not compatible with host record, refusing.");
+
+                r = user_record_authenticate(lhr, h, pkcs11_decrypted_passwords);
+                if (r < 0)
+                        return r;
+
+                *ret_luks_home_record = TAKE_PTR(lhr);
+                return 0;
+        }
+
+        return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Couldn't find home record in LUKS2 header, refusing.");
+}
+
+static int format_luks_token_text(
+                struct crypt_device *cd,
+                UserRecord *hr,
+                const void *volume_key,
+                char **ret) {
+
+        int r, encrypted_size_out1 = 0, encrypted_size_out2 = 0, iv_size, key_size;
+        _cleanup_(EVP_CIPHER_CTX_freep) EVP_CIPHER_CTX *context = NULL;
+        _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+        _cleanup_free_ void *iv = NULL, *encrypted = NULL;
+        size_t text_length, encrypted_size;
+        _cleanup_free_ char *text = NULL;
+        const EVP_CIPHER *cc;
+
+        assert(cd);
+        assert(hr);
+        assert(volume_key);
+        assert(ret);
+
+        r = crypt_device_to_evp_cipher(cd, &cc);
+        if (r < 0)
+                return r;
+
+        key_size = EVP_CIPHER_key_length(cc);
+        iv_size = EVP_CIPHER_iv_length(cc);
+
+        if (iv_size > 0) {
+                iv = malloc(iv_size);
+                if (!iv)
+                        return log_oom();
+
+                r = genuine_random_bytes(iv, iv_size, RANDOM_BLOCK);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to generate IV: %m");
+        }
+
+        context = EVP_CIPHER_CTX_new();
+        if (!context)
+                return log_oom();
+
+        if (EVP_EncryptInit_ex(context, cc, NULL, volume_key, iv) != 1)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to initialize encryption context.");
+
+        r = json_variant_format(hr->json, 0, &text);
+        if (r < 0)
+                return log_error_errno(r,"Failed to format user record for LUKS: %m");
+
+        text_length = strlen(text);
+        encrypted_size = text_length + 2*key_size - 1;
+
+        encrypted = malloc(encrypted_size);
+        if (!encrypted)
+                return log_oom();
+
+        if (EVP_EncryptUpdate(context, encrypted, &encrypted_size_out1, (uint8_t*) text, text_length) != 1)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to encrypt JSON record.");
+
+        assert((size_t) encrypted_size_out1 <= encrypted_size);
+
+        if (EVP_EncryptFinal_ex(context, (uint8_t*) encrypted + encrypted_size_out1, &encrypted_size_out2) != 1)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to finish encryption of JSON record. ");
+
+        assert((size_t) encrypted_size_out1 + (size_t) encrypted_size_out2 <= encrypted_size);
+
+        r = json_build(&v,
+                       JSON_BUILD_OBJECT(
+                                       JSON_BUILD_PAIR("type", JSON_BUILD_STRING("systemd-homed")),
+                                       JSON_BUILD_PAIR("keyslots", JSON_BUILD_EMPTY_ARRAY),
+                                       JSON_BUILD_PAIR("record", JSON_BUILD_BASE64(encrypted, encrypted_size_out1 + encrypted_size_out2)),
+                                       JSON_BUILD_PAIR("iv", JSON_BUILD_BASE64(iv, iv_size))));
+        if (r < 0)
+                return log_error_errno(r, "Failed to prepare LUKS JSON token object: %m");
+
+        r = json_variant_format(v, 0, ret);
+        if (r < 0)
+                return log_error_errno(r, "Failed to format encrypted user record for LUKS: %m");
+
+        return 0;
+}
+
+int home_store_header_identity_luks(
+                UserRecord *h,
+                HomeSetup *setup,
+                UserRecord *old_home) {
+
+        _cleanup_(user_record_unrefp) UserRecord *header_home = NULL;
+        _cleanup_free_ char *text = NULL;
+        int token = 0, r;
+
+        assert(h);
+
+        if (!setup->crypt_device)
+                return 0;
+
+        assert(setup->volume_key);
+
+        /* Let's store the user's identity record in the LUKS2 "token" header data fields, in an encrypted
+         * fashion. Why that? If we'd rely on the record being embedded in the payload file system itself we
+         * would have to mount the file system before we can validate the JSON record, its signatures and
+         * whether it matches what we are looking for. However, kernel file system implementations are
+         * generally not ready to be used on untrusted media. Hence let's store the record independently of
+         * the file system, so that we can validate it first, and only then mount the file system. To keep
+         * things simple we use the same encryption settings for this record as for the file system itself. */
+
+        r = user_record_clone(h, USER_RECORD_EXTRACT_EMBEDDED, &header_home);
+        if (r < 0)
+                return log_error_errno(r, "Failed to determine new header record: %m");
+
+        if (old_home && user_record_equal(old_home, header_home)) {
+                log_debug("Not updating header home record.");
+                return 0;
+        }
+
+        r = format_luks_token_text(setup->crypt_device, header_home, setup->volume_key, &text);
+        if (r < 0)
+                return r;
+
+        for (;; token++) {
+                crypt_token_info state;
+                const char *type;
+
+                state = crypt_token_status(setup->crypt_device, token, &type);
+                if (state == CRYPT_TOKEN_INACTIVE) /* First unconfigured token, we are done */
+                        break;
+                if (IN_SET(state, CRYPT_TOKEN_INTERNAL, CRYPT_TOKEN_INTERNAL_UNKNOWN, CRYPT_TOKEN_EXTERNAL))
+                        continue; /* Not ours */
+                if (state != CRYPT_TOKEN_EXTERNAL_UNKNOWN)
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unexpected token state of token %i: %i", token, (int) state);
+
+                if (!streq(type, "systemd-homed"))
+                        continue;
+
+                r = crypt_token_json_set(setup->crypt_device, token, text);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to set JSON token for slot %i: %m", token);
+
+                /* Now, let's free the text so that for all further matching tokens we all crypt_json_token_set()
+                 * with a NULL text in order to invalidate the tokens. */
+                text = mfree(text);
+                token++;
+        }
+
+        if (text)
+                return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Didn't find any record token to update.");
+
+        log_info("Wrote LUKS header user record.");
+
+        return 1;
+}
+
+static int run_fitrim(int root_fd) {
+        char buf[FORMAT_BYTES_MAX];
+        struct fstrim_range range = {
+                .len = UINT64_MAX,
+        };
+
+        /* If discarding is on, discard everything right after mounting, so that the discard setting takes
+         * effect on activation. */
+
+        assert(root_fd >= 0);
+
+        if (ioctl(root_fd, FITRIM, &range) < 0) {
+                if (IN_SET(errno, ENOTTY, EOPNOTSUPP, EBADF)) {
+                        log_debug_errno(errno, "File system does not support FITRIM, not trimming.");
+                        return 0;
+                }
+
+                return log_warning_errno(errno, "Failed to invoke FITRIM, ignoring: %m");
+        }
+
+        log_info("Discarded unused %s.",
+                 format_bytes(buf, sizeof(buf), range.len));
+        return 1;
+}
+
+static int run_fallocate(int backing_fd, const struct stat *st) {
+        char buf[FORMAT_BYTES_MAX];
+
+        assert(backing_fd >= 0);
+        assert(st);
+
+        /* If discarding is off, let's allocate the whole image before mounting, so that the setting takes
+         * effect on activation */
+
+        if (!S_ISREG(st->st_mode))
+                return 0;
+
+        if (st->st_blocks >= DIV_ROUND_UP(st->st_size, 512)) {
+                log_info("Backing file is fully allocated already.");
+                return 0;
+        }
+
+        if (fallocate(backing_fd, FALLOC_FL_KEEP_SIZE, 0, st->st_size) < 0) {
+
+                if (ERRNO_IS_NOT_SUPPORTED(errno)) {
+                        log_debug_errno(errno, "fallocate() not supported on file system, ignoring.");
+                        return 0;
+                }
+
+                if (ERRNO_IS_DISK_SPACE(errno)) {
+                        log_debug_errno(errno, "Not enough disk space to fully allocate home.");
+                        return -ENOSPC; /* make recognizable */
+                }
+
+                return log_error_errno(errno, "Failed to allocate backing file blocks: %m");
+        }
+
+        log_info("Allocated additional %s.",
+                 format_bytes(buf, sizeof(buf), (DIV_ROUND_UP(st->st_size, 512) - st->st_blocks) * 512));
+        return 1;
+}
+
+int home_prepare_luks(
+                UserRecord *h,
+                bool already_activated,
+                const char *force_image_path,
+                char ***pkcs11_decrypted_passwords,
+                HomeSetup *setup,
+                UserRecord **ret_luks_home) {
+
+        sd_id128_t found_partition_uuid, found_luks_uuid, found_fs_uuid;
+        _cleanup_(user_record_unrefp) UserRecord *luks_home = NULL;
+        _cleanup_(loop_device_unrefp) LoopDevice *loop = NULL;
+        _cleanup_(crypt_freep) struct crypt_device *cd = NULL;
+        _cleanup_(erase_and_freep) void *volume_key = NULL;
+        bool dm_activated = false, mounted = false;
+        _cleanup_close_ int root_fd = -1;
+        size_t volume_key_size = 0;
+        uint64_t offset, size;
+        int r;
+
+        assert(h);
+        assert(setup);
+        assert(setup->dm_name);
+        assert(setup->dm_node);
+
+        assert(user_record_storage(h) == USER_LUKS);
+
+        if (already_activated) {
+                struct loop_info64 info;
+                const char *n;
+
+                r = luks_open(setup->dm_name,
+                              h->password,
+                              pkcs11_decrypted_passwords ? *pkcs11_decrypted_passwords : NULL,
+                              &cd,
+                              &found_luks_uuid,
+                              &volume_key,
+                              &volume_key_size);
+                if (r < 0)
+                        return r;
+
+                r = luks_validate_home_record(cd, h, volume_key, pkcs11_decrypted_passwords, &luks_home);
+                if (r < 0)
+                        return r;
+
+                n = crypt_get_device_name(cd);
+                if (!n)
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to determine backing device for DM %s.", setup->dm_name);
+
+                r = loop_device_open(n, O_RDWR, &loop);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to open loopback device %s: %m", n);
+
+                if (ioctl(loop->fd, LOOP_GET_STATUS64, &info) < 0) {
+                        _cleanup_free_ char *sysfs = NULL;
+                        struct stat st;
+
+                        if (!IN_SET(errno, ENOTTY, EINVAL))
+                                return log_error_errno(errno, "Failed to get block device metrics of %s: %m", n);
+
+                        if (ioctl(loop->fd, BLKGETSIZE64, &size) < 0)
+                                return log_error_errno(r, "Failed to read block device size of %s: %m", n);
+
+                        if (fstat(loop->fd, &st) < 0)
+                                return log_error_errno(r, "Failed to stat block device %s: %m", n);
+                        assert(S_ISBLK(st.st_mode));
+
+                        if (asprintf(&sysfs, "/sys/dev/block/%u:%u/partition", major(st.st_rdev), minor(st.st_rdev)) < 0)
+                                return log_oom();
+
+                        if (access(sysfs, F_OK) < 0) {
+                                if (errno != ENOENT)
+                                        return log_error_errno(errno, "Failed to determine whether %s exists: %m", sysfs);
+
+                                offset = 0;
+                        } else {
+                                _cleanup_free_ char *buffer = NULL;
+
+                                if (asprintf(&sysfs, "/sys/dev/block/%u:%u/start", major(st.st_rdev), minor(st.st_rdev)) < 0)
+                                        return log_oom();
+
+                                r = read_one_line_file(sysfs, &buffer);
+                                if (r < 0)
+                                        return log_error_errno(r, "Failed to read partition start offset: %m");
+
+                                r = safe_atou64(buffer, &offset);
+                                if (r < 0)
+                                        return log_error_errno(r, "Failed to parse partition start offset: %m");
+
+                                if (offset > UINT64_MAX / 512U)
+                                        return log_error_errno(SYNTHETIC_ERRNO(E2BIG), "Offset too large for 64 byte range, refusing.");
+
+                                offset *= 512U;
+                        }
+                } else {
+                        offset = info.lo_offset;
+                        size = info.lo_sizelimit;
+                }
+
+                found_partition_uuid = found_fs_uuid = SD_ID128_NULL;
+
+                log_info("Discovered used loopback device %s.", loop->node);
+
+                root_fd = open(user_record_home_directory(h), O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW);
+                if (root_fd < 0) {
+                        r = log_error_errno(r, "Failed to open home directory: %m");
+                        goto fail;
+                }
+        } else {
+                _cleanup_free_ char *fstype = NULL, *subdir = NULL;
+                _cleanup_close_ int fd = -1;
+                const char *ip;
+                struct stat st;
+
+                ip = force_image_path ?: user_record_image_path(h);
+
+                subdir = path_join("/run/systemd/user-home-mount/", user_record_user_name_and_realm(h));
+                if (!subdir)
+                        return log_oom();
+
+                fd = open(ip, O_RDWR|O_CLOEXEC|O_NOCTTY|O_NONBLOCK);
+                if (fd < 0)
+                        return log_error_errno(errno, "Failed to open image file %s: %m", ip);
+
+                if (fstat(fd, &st) < 0)
+                        return log_error_errno(errno, "Failed to fstat() image file: %m");
+                if (!S_ISREG(st.st_mode) && !S_ISBLK(st.st_mode))
+                        return log_error_errno(errno, "Image file %s is not a regular file or block device: %m", ip);
+
+                r = luks_validate(fd, user_record_user_name_and_realm(h), h->partition_uuid, &found_partition_uuid, &offset, &size);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to validate disk label: %m");
+
+                if (!user_record_luks_discard(h)) {
+                        r = run_fallocate(fd, &st);
+                        if (r < 0)
+                                return r;
+                }
+
+                r = loop_device_make(fd, O_RDWR, offset, size, 0, &loop);
+                if (r == -ENOENT) {
+                        log_error_errno(r, "Loopback block device support is not available on this system.");
+                        return -ENOLINK; /* make recognizable */
+                }
+                if (r < 0)
+                        return log_error_errno(r, "Failed to allocate loopback context: %m");
+
+                log_info("Setting up loopback device %s completed.", loop->node ?: ip);
+
+                r = luks_setup(loop->node ?: ip,
+                               setup->dm_name,
+                               h->luks_uuid,
+                               h->luks_cipher,
+                               h->luks_cipher_mode,
+                               h->luks_volume_key_size,
+                               h->password,
+                               pkcs11_decrypted_passwords ? *pkcs11_decrypted_passwords : NULL,
+                               user_record_luks_discard(h),
+                               &cd,
+                               &found_luks_uuid,
+                               &volume_key,
+                               &volume_key_size);
+                if (r < 0)
+                        return r;
+
+                dm_activated = true;
+
+                r = luks_validate_home_record(cd, h, volume_key, pkcs11_decrypted_passwords, &luks_home);
+                if (r < 0)
+                        goto fail;
+
+                r = fs_validate(setup->dm_node, h->file_system_uuid, &fstype, &found_fs_uuid);
+                if (r < 0)
+                        goto fail;
+
+                r = run_fsck(setup->dm_node, fstype);
+                if (r < 0)
+                        goto fail;
+
+                r = home_unshare_and_mount(setup->dm_node, fstype, user_record_luks_discard(h));
+                if (r < 0)
+                        goto fail;
+
+                mounted = true;
+
+                root_fd = open(subdir, O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW);
+                if (root_fd < 0) {
+                        r = log_error_errno(r, "Failed to open home directory: %m");
+                        goto fail;
+                }
+
+                if (user_record_luks_discard(h))
+                        (void) run_fitrim(root_fd);
+        }
+
+        setup->loop = TAKE_PTR(loop);
+        setup->crypt_device = TAKE_PTR(cd);
+        setup->root_fd = TAKE_FD(root_fd);
+        setup->found_partition_uuid = found_partition_uuid;
+        setup->found_luks_uuid = found_luks_uuid;
+        setup->found_fs_uuid = found_fs_uuid;
+        setup->partition_offset = offset;
+        setup->partition_size = size;
+        setup->volume_key = TAKE_PTR(volume_key);
+        setup->volume_key_size = volume_key_size;
+
+        setup->undo_mount = mounted;
+        setup->undo_dm = dm_activated;
+
+        if (ret_luks_home)
+                *ret_luks_home = TAKE_PTR(luks_home);
+
+        return 0;
+
+fail:
+        if (mounted)
+                (void) umount_verbose("/run/systemd/user-home-mount");
+
+        if (dm_activated)
+                (void) crypt_deactivate(cd, setup->dm_name);
+
+        return r;
+}
+
+static void print_size_summary(uint64_t host_size, uint64_t encrypted_size, struct statfs *sfs) {
+        char buffer1[FORMAT_BYTES_MAX], buffer2[FORMAT_BYTES_MAX], buffer3[FORMAT_BYTES_MAX], buffer4[FORMAT_BYTES_MAX];
+
+        assert(sfs);
+
+        log_info("Image size is %s, file system size is %s, file system payload size is %s, file system free is %s.",
+                 format_bytes(buffer1, sizeof(buffer1), host_size),
+                 format_bytes(buffer2, sizeof(buffer2), encrypted_size),
+                 format_bytes(buffer3, sizeof(buffer3), (uint64_t) sfs->f_blocks * (uint64_t) sfs->f_frsize),
+                 format_bytes(buffer4, sizeof(buffer4), (uint64_t) sfs->f_bfree * (uint64_t) sfs->f_frsize));
+}
+
+int home_activate_luks(
+                UserRecord *h,
+                char ***pkcs11_decrypted_passwords,
+                UserRecord **ret_home) {
+
+        _cleanup_(user_record_unrefp) UserRecord *new_home = NULL, *luks_home_record = NULL;
+        _cleanup_(home_setup_undo) HomeSetup setup = HOME_SETUP_INIT;
+        uint64_t host_size, encrypted_size;
+        const char *hdo, *hd;
+        struct statfs sfs;
+        int r;
+
+        assert(h);
+        assert(user_record_storage(h) == USER_LUKS);
+        assert(ret_home);
+
+        assert_se(hdo = user_record_home_directory(h));
+        hd = strdupa(hdo); /* copy the string out, since it might change later in the home record object */
+
+        r = make_dm_names(h->user_name, &setup.dm_name, &setup.dm_node);
+        if (r < 0)
+                return r;
+
+        r = access(setup.dm_node, F_OK);
+        if (r < 0) {
+                if (errno != ENOENT)
+                        return log_error_errno(errno, "Failed to determine whether %s exists: %m", setup.dm_node);
+        } else
+                return log_error_errno(SYNTHETIC_ERRNO(EEXIST), "Device mapper device %s already exists, refusing.", setup.dm_node);
+
+        r = home_prepare_luks(
+                        h,
+                        false,
+                        NULL,
+                        pkcs11_decrypted_passwords,
+                        &setup,
+                        &luks_home_record);
+        if (r < 0)
+                return r;
+
+        r = block_get_size_by_fd(setup.loop->fd, &host_size);
+        if (r < 0)
+                return log_error_errno(r, "Failed to get loopback block device size: %m");
+
+        r = block_get_size_by_path(setup.dm_node, &encrypted_size);
+        if (r < 0)
+                return log_error_errno(r, "Failed to get LUKS block device size: %m");
+
+        r = home_refresh(
+                        h,
+                        &setup,
+                        luks_home_record,
+                        pkcs11_decrypted_passwords,
+                        &sfs,
+                        &new_home);
+        if (r < 0)
+                return r;
+
+        r = home_extend_embedded_identity(new_home, h, &setup);
+        if (r < 0)
+                return r;
+
+        setup.root_fd = safe_close(setup.root_fd);
+
+        r = home_move_mount(user_record_user_name_and_realm(h), hd);
+        if (r < 0)
+                return r;
+
+        setup.undo_mount = false;
+
+        loop_device_relinquish(setup.loop);
+
+        r = dm_deferred_remove(setup.dm_name);
+        if (r < 0)
+                log_warning_errno(r, "Failed to relinquish dm device, ignoring: %m");
+
+        setup.undo_dm = false;
+
+        log_info("Everything completed.");
+
+        print_size_summary(host_size, encrypted_size, &sfs);
+
+        *ret_home = TAKE_PTR(new_home);
+        return 1;
+}
+
+int home_deactivate_luks(UserRecord *h) {
+        _cleanup_(crypt_freep) struct crypt_device *cd = NULL;
+        _cleanup_free_ char *dm_name = NULL, *dm_node = NULL;
+        int r;
+
+        /* Note that the DM device and loopback device are set to auto-detach, hence strictly speaking we
+         * don't have to explicitly have to detach them. However, we do that nonetheless (in case of the DM
+         * device), to avoid races: by explicitly detaching them we know when the detaching is complete. We
+         * don't bother about the loopback device because unlike the DM device it doesn't have a fixed
+         * name. */
+
+        r = make_dm_names(h->user_name, &dm_name, &dm_node);
+        if (r < 0)
+                return r;
+
+        r = crypt_init_by_name(&cd, dm_name);
+        if (IN_SET(r, -ENODEV, -EINVAL, -ENOENT)) {
+                log_debug_errno(r, "LUKS device %s is already detached.", dm_name);
+                return false;
+        } else if (r < 0)
+                return log_error_errno(r, "Failed to initialize cryptsetup context for %s: %m", dm_name);
+
+        log_info("Discovered used LUKS device %s.", dm_node);
+
+        crypt_set_log_callback(cd, cryptsetup_log_glue, NULL);
+
+        r = crypt_deactivate(cd, dm_name);
+        if (IN_SET(r, -ENODEV, -EINVAL, -ENOENT))
+                log_debug_errno(r, "LUKS device %s is already detached.", dm_node);
+        else if (r < 0)
+                return log_info_errno(r, "LUKS device %s couldn't be deactivated: %m", dm_node);
+
+        log_info("LUKS device detaching completed.");
+        return true;
+}
+
+static int run_mkfs(
+                const char *node,
+                const char *fstype,
+                const char *label,
+                sd_id128_t uuid,
+                bool discard) {
+
+        int r;
+
+        assert(node);
+        assert(fstype);
+        assert(label);
+
+        r = mkfs_exists(fstype);
+        if (r < 0)
+                return log_error_errno(r, "Failed to check if mkfs for file system %s exists: %m", fstype);
+        if (r == 0)
+                return log_error_errno(SYNTHETIC_ERRNO(EPROTONOSUPPORT), "Nt mkfs for file system %s installed.", fstype);
+
+        r = safe_fork("(mkfs)", FORK_RESET_SIGNALS|FORK_RLIMIT_NOFILE_SAFE|FORK_DEATHSIG|FORK_LOG|FORK_WAIT|FORK_STDOUT_TO_STDERR, NULL);
+        if (r < 0)
+                return r;
+        if (r == 0) {
+                const char *mkfs;
+                char suuid[37];
+
+                /* Child */
+
+                mkfs = strjoina("mkfs.", fstype);
+                id128_to_uuid_string(uuid, suuid);
+
+                if (streq(fstype, "ext4"))
+                        execlp(mkfs, mkfs,
+                               "-L", label,
+                               "-U", suuid,
+                               "-I", "256",
+                               "-O", "has_journal",
+                               "-m", "0",
+                               "-E", discard ? "lazy_itable_init=1,discard" : "lazy_itable_init=1,nodiscard",
+                               node, NULL);
+                else if (streq(fstype, "btrfs")) {
+                        if (discard)
+                                execlp(mkfs, mkfs, "-L", label, "-U", suuid, node, NULL);
+                        else
+                                execlp(mkfs, mkfs, "-L", label, "-U", suuid, "--nodiscard", node, NULL);
+                } else if (streq(fstype, "xfs")) {
+                        const char *j;
+
+                        j = strjoina("uuid=", suuid);
+                        if (discard)
+                                execlp(mkfs, mkfs, "-L", label, "-m", j, "-m", "reflink=1", node, NULL);
+                        else
+                                execlp(mkfs, mkfs, "-L", label, "-m", j, "-m", "reflink=1", "-K", node, NULL);
+                } else {
+                        log_error("Cannot make file system: %s", fstype);
+                        _exit(EXIT_FAILURE);
+                }
+
+                log_error_errno(errno, "Failed to execute %s: %m", mkfs);
+                _exit(EXIT_FAILURE);
+        }
+
+        return 0;
+}
+
+static struct crypt_pbkdf_type* build_good_pbkdf(struct crypt_pbkdf_type *buffer, UserRecord *hr) {
+        assert(buffer);
+        assert(hr);
+
+        *buffer = (struct crypt_pbkdf_type) {
+                .hash = user_record_luks_pbkdf_hash_algorithm(hr),
+                .type = user_record_luks_pbkdf_type(hr),
+                .time_ms = user_record_luks_pbkdf_time_cost_usec(hr) / USEC_PER_MSEC,
+                .max_memory_kb = user_record_luks_pbkdf_memory_cost(hr) / 1024,
+                .parallel_threads = user_record_luks_pbkdf_parallel_threads(hr),
+        };
+
+        return buffer;
+}
+
+static struct crypt_pbkdf_type* build_minimal_pbkdf(struct crypt_pbkdf_type *buffer, UserRecord *hr) {
+        assert(buffer);
+        assert(hr);
+
+        /* For PKCS#11 derived keys (which are generated randomly and are of high quality already) we use a
+         * minimal PBKDF */
+        *buffer = (struct crypt_pbkdf_type) {
+                .hash = user_record_luks_pbkdf_hash_algorithm(hr),
+                .type = CRYPT_KDF_PBKDF2,
+                .iterations = 1,
+                .time_ms = 1,
+        };
+
+        return buffer;
+}
+
+static int luks_format(
+                const char *node,
+                const char *dm_name,
+                sd_id128_t uuid,
+                const char *label,
+                char **pkcs11_decrypted_passwords,
+                char **effective_passwords,
+                bool discard,
+                UserRecord *hr,
+                struct crypt_device **ret) {
+
+        _cleanup_(user_record_unrefp) UserRecord *reduced = NULL;
+        _cleanup_(crypt_freep) struct crypt_device *cd = NULL;
+        _cleanup_(erase_and_freep) void *volume_key = NULL;
+        struct crypt_pbkdf_type good_pbkdf, minimal_pbkdf;
+        _cleanup_free_ char *text = NULL;
+        size_t volume_key_size;
+        char suuid[37], **pp;
+        int slot = 0, r;
+
+        assert(node);
+        assert(dm_name);
+        assert(hr);
+        assert(ret);
+
+        r = crypt_init(&cd, node);
+        if (r < 0)
+                return log_error_errno(r, "Failed to allocate libcryptsetup context: %m");
+
+        crypt_set_log_callback(cd, cryptsetup_log_glue, NULL);
+
+        /* Normally we'd, just leave volume key generation to libcryptsetup. However, we can't, since we
+         * can't extract the volume key from the library again, but we need it in order to encrypt the JSON
+         * record. Hence, let's generate it on our own, so that we can keep track of it. */
+
+        volume_key_size = user_record_luks_volume_key_size(hr);
+        volume_key = malloc(volume_key_size);
+        if (!volume_key)
+                return log_oom();
+
+        r = genuine_random_bytes(volume_key, volume_key_size, RANDOM_BLOCK);
+        if (r < 0)
+                return log_error_errno(r, "Failed to generate volume key: %m");
+
+#if HAVE_CRYPT_SET_METADATA_SIZE
+        /* Increase the metadata space to 4M, the largest LUKS2 supports */
+        r = crypt_set_metadata_size(cd, 4096U*1024U, 0);
+        if (r < 0)
+                return log_error_errno(r, "Failed to change LUKS2 metadata size: %m");
+#endif
+
+        build_good_pbkdf(&good_pbkdf, hr);
+        build_minimal_pbkdf(&minimal_pbkdf, hr);
+
+        r = crypt_format(cd,
+                         CRYPT_LUKS2,
+                         user_record_luks_cipher(hr),
+                         user_record_luks_cipher_mode(hr),
+                         id128_to_uuid_string(uuid, suuid),
+                         volume_key,
+                         volume_key_size,
+                         &(struct crypt_params_luks2) {
+                                 .label = label,
+                                 .subsystem = "systemd-home",
+                                 .sector_size = 512U,
+                                 .pbkdf = &good_pbkdf,
+                         });
+        if (r < 0)
+                return log_error_errno(r, "Failed to format LUKS image: %m");
+
+        log_info("LUKS formatting completed.");
+
+        STRV_FOREACH(pp, effective_passwords) {
+
+                if (strv_contains(pkcs11_decrypted_passwords, *pp)) {
+                        log_debug("Using minimal PBKDF for slot %i", slot);
+                        r = crypt_set_pbkdf_type(cd, &minimal_pbkdf);
+                } else {
+                        log_debug("Using good PBKDF for slot %i", slot);
+                        r = crypt_set_pbkdf_type(cd, &good_pbkdf);
+                }
+                if (r < 0)
+                        return log_error_errno(r, "Failed to tweak PBKDF for slot %i: %m", slot);
+
+                r = crypt_keyslot_add_by_volume_key(
+                                cd,
+                                slot,
+                                volume_key,
+                                volume_key_size,
+                                *pp,
+                                strlen(*pp));
+                if (r < 0)
+                        return log_error_errno(r, "Failed to set up LUKS password for slot %i: %m", slot);
+
+                log_info("Writing password to LUKS keyslot %i completed.", slot);
+                slot++;
+        }
+
+        r = crypt_activate_by_volume_key(
+                        cd,
+                        dm_name,
+                        volume_key,
+                        volume_key_size,
+                        discard ? CRYPT_ACTIVATE_ALLOW_DISCARDS : 0);
+        if (r < 0)
+                return log_error_errno(r, "Failed to activate LUKS superblock: %m");
+
+        log_info("LUKS activation by volume key succeeded.");
+
+        r = user_record_clone(hr, USER_RECORD_EXTRACT_EMBEDDED, &reduced);
+        if (r < 0)
+                return log_error_errno(r, "Failed to prepare home record for LUKS: %m");
+
+        r = format_luks_token_text(cd, reduced, volume_key, &text);
+        if (r < 0)
+                return r;
+
+        r = crypt_token_json_set(cd, CRYPT_ANY_TOKEN, text);
+        if (r < 0)
+                return log_error_errno(r, "Failed to set LUKS JSON token: %m");
+
+        log_info("Writing user record as LUKS token completed.");
+
+        if (ret)
+                *ret = TAKE_PTR(cd);
+
+        return 0;
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(struct fdisk_context*, fdisk_unref_context);
+DEFINE_TRIVIAL_CLEANUP_FUNC(struct fdisk_partition*, fdisk_unref_partition);
+DEFINE_TRIVIAL_CLEANUP_FUNC(struct fdisk_parttype*, fdisk_unref_parttype);
+DEFINE_TRIVIAL_CLEANUP_FUNC(struct fdisk_table*, fdisk_unref_table);
+
+static int make_partition_table(
+                int fd,
+                const char *label,
+                sd_id128_t uuid,
+                uint64_t *ret_offset,
+                uint64_t *ret_size,
+                sd_id128_t *ret_disk_uuid) {
+
+        _cleanup_(fdisk_unref_partitionp) struct fdisk_partition *p = NULL, *q = NULL;
+        _cleanup_(fdisk_unref_parttypep) struct fdisk_parttype *t = NULL;
+        _cleanup_(fdisk_unref_contextp) struct fdisk_context *c = NULL;
+        _cleanup_free_ char *path = NULL, *disk_uuid_as_string = NULL;
+        uint64_t offset, size;
+        sd_id128_t disk_uuid;
+        char uuids[37];
+        int r;
+
+        assert(fd >= 0);
+        assert(label);
+        assert(ret_offset);
+        assert(ret_size);
+
+        t = fdisk_new_parttype();
+        if (!t)
+                return log_oom();
+
+        r = fdisk_parttype_set_typestr(t, "773f91ef-66d4-49b5-bd83-d683bf40ad16");
+        if (r < 0)
+                return log_error_errno(r, "Failed to initialize partition type: %m");
+
+        c = fdisk_new_context();
+        if (!c)
+                return log_oom();
+
+        if (asprintf(&path, "/proc/self/fd/%i", fd) < 0)
+                return log_oom();
+
+        r = fdisk_assign_device(c, path, 0);
+        if (r < 0)
+                return log_error_errno(r, "Failed to open device: %m");
+
+        r = fdisk_create_disklabel(c, "gpt");
+        if (r < 0)
+                return log_error_errno(r, "Failed to create gpt disk label: %m");
+
+        p = fdisk_new_partition();
+        if (!p)
+                return log_oom();
+
+        r = fdisk_partition_set_type(p, t);
+        if (r < 0)
+                return log_error_errno(r, "Failed to set partition type: %m");
+
+        r = fdisk_partition_start_follow_default(p, 1);
+        if (r < 0)
+                return log_error_errno(r, "Failed to place partition at beginning of space: %m");
+
+        r = fdisk_partition_partno_follow_default(p, 1);
+        if (r < 0)
+                return log_error_errno(r, "Failed to place partition at first free partition index: %m");
+
+        r = fdisk_partition_end_follow_default(p, 1);
+        if (r < 0)
+                return log_error_errno(r, "Failed to make partition cover all free space: %m");
+
+        r = fdisk_partition_set_name(p, label);
+        if (r < 0)
+                return log_error_errno(r, "Failed to set partition name: %m");
+
+        r = fdisk_partition_set_uuid(p, id128_to_uuid_string(uuid, uuids));
+        if (r < 0)
+                return log_error_errno(r, "Failed to set partition UUID: %m");
+
+        r = fdisk_add_partition(c, p, NULL);
+        if (r < 0)
+                return log_error_errno(r, "Failed to add partition: %m");
+
+        r = fdisk_write_disklabel(c);
+        if (r < 0)
+                return log_error_errno(r, "Failed to write disk label: %m");
+
+        r = fdisk_get_disklabel_id(c, &disk_uuid_as_string);
+        if (r < 0)
+                return log_error_errno(r, "Failed to determine disk label UUID: %m");
+
+        r = sd_id128_from_string(disk_uuid_as_string, &disk_uuid);
+        if (r < 0)
+                return log_error_errno(r, "Failed to parse disk label UUID: %m");
+
+        r = fdisk_get_partition(c, 0, &q);
+        if (r < 0)
+                return log_error_errno(r, "Failed to read created partition metadata: %m");
+
+        assert(fdisk_partition_has_start(q));
+        offset = fdisk_partition_get_start(q);
+        if (offset > UINT64_MAX / 512U)
+                return log_error_errno(SYNTHETIC_ERRNO(ERANGE), "Partition offset too large.");
+
+        assert(fdisk_partition_has_size(q));
+        size = fdisk_partition_get_size(q);
+        if (size > UINT64_MAX / 512U)
+                return log_error_errno(SYNTHETIC_ERRNO(ERANGE), "Partition size too large.");
+
+        *ret_offset = offset * 512U;
+        *ret_size = size * 512U;
+        *ret_disk_uuid = disk_uuid;
+
+        return 0;
+}
+
+static bool supported_fs_size(const char *fstype, uint64_t host_size) {
+        uint64_t m;
+
+        m = minimal_size_by_fs_name(fstype);
+        if (m == UINT64_MAX)
+                return false;
+
+        return host_size >= m;
+}
+
+static int wait_for_devlink(const char *path) {
+        _cleanup_close_ int inotify_fd = -1;
+        usec_t until;
+        int r;
+
+        /* let's wait for a device link to show up in /dev, with a time-out. This is good to do since we
+         * return a /dev/disk/by-uuid/… link to our callers and they likely want to access it right-away,
+         * hence let's wait until udev has caught up with our changes, and wait for the symlink to be
+         * created. */
+
+        until = usec_add(now(CLOCK_MONOTONIC), 45 * USEC_PER_SEC);
+
+        for (;;) {
+                _cleanup_free_ char *dn = NULL;
+                usec_t w;
+
+                if (laccess(path, F_OK) < 0) {
+                        if (errno != ENOENT)
+                                return log_error_errno(errno, "Failed to determine whether %s exists: %m", path);
+                } else
+                        return 0; /* Found it */
+
+                if (inotify_fd < 0) {
+                        /* We need to wait for the device symlink to show up, let's create an inotify watch for it */
+                        inotify_fd = inotify_init1(IN_NONBLOCK|IN_CLOEXEC);
+                        if (inotify_fd < 0)
+                                return log_error_errno(errno, "Failed to allocate inotify fd: %m");
+                }
+
+                dn = dirname_malloc(path);
+                for (;;) {
+                        if (!dn)
+                                return log_oom();
+
+                        log_info("Watching %s", dn);
+
+                        if (inotify_add_watch(inotify_fd, dn, IN_CREATE|IN_MOVED_TO|IN_ONLYDIR|IN_DELETE_SELF|IN_MOVE_SELF) < 0) {
+                                if (errno != ENOENT)
+                                        return log_error_errno(errno, "Failed to add watch on %s: %m", dn);
+                        } else
+                                break;
+
+                        if (empty_or_root(dn))
+                                break;
+
+                        dn = dirname_malloc(dn);
+                }
+
+                w = now(CLOCK_MONOTONIC);
+                if (w >= until)
+                        return log_error_errno(SYNTHETIC_ERRNO(ETIMEDOUT), "Device link %s still hasn't shown up, giving up.", path);
+
+                r = fd_wait_for_event(inotify_fd, POLLIN, usec_sub_unsigned(until, w));
+                if (r < 0)
+                        return log_error_errno(r, "Failed to watch inotify: %m");
+
+                (void) flush_fd(inotify_fd);
+        }
+}
+
+static int calculate_disk_size(UserRecord *h, const char *parent_dir, uint64_t *ret) {
+        char buf[FORMAT_BYTES_MAX];
+        struct statfs sfs;
+        uint64_t m;
+
+        assert(h);
+        assert(parent_dir);
+        assert(ret);
+
+        if (h->disk_size != UINT64_MAX) {
+                *ret = DISK_SIZE_ROUND_DOWN(h->disk_size);
+                return 0;
+        }
+
+        if (statfs(parent_dir, &sfs) < 0)
+                return log_error_errno(errno, "statfs() on %s failed: %m", parent_dir);
+
+        m = sfs.f_bsize * sfs.f_bavail;
+
+        if (h->disk_size_relative == UINT64_MAX) {
+
+                if (m > UINT64_MAX / USER_DISK_SIZE_DEFAULT_PERCENT)
+                        return log_error_errno(SYNTHETIC_ERRNO(EOVERFLOW), "Disk size too large.");
+
+                *ret = DISK_SIZE_ROUND_DOWN(m * USER_DISK_SIZE_DEFAULT_PERCENT / 100);
+
+                log_info("Sizing home to %u%% of available disk space, which is %s.",
+                         USER_DISK_SIZE_DEFAULT_PERCENT,
+                         format_bytes(buf, sizeof(buf), *ret));
+        } else {
+                *ret = DISK_SIZE_ROUND_DOWN((uint64_t) ((double) m * (double) h->disk_size_relative / (double) UINT32_MAX));
+
+                log_info("Sizing home to %" PRIu64 ".%01" PRIu64 "%% of available disk space, which is %s.",
+                         (h->disk_size_relative * 100) / UINT32_MAX,
+                         ((h->disk_size_relative * 1000) / UINT32_MAX) % 10,
+                         format_bytes(buf, sizeof(buf), *ret));
+        }
+
+        if (*ret < USER_DISK_SIZE_MIN)
+                *ret = USER_DISK_SIZE_MIN;
+
+        return 0;
+}
+
+int home_create_luks(
+                UserRecord *h,
+                char **pkcs11_decrypted_passwords,
+                char **effective_passwords,
+                UserRecord **ret_home) {
+
+        _cleanup_free_ char *dm_name = NULL, *dm_node = NULL, *subdir = NULL, *disk_uuid_path = NULL, *temporary_image_path = NULL;
+        uint64_t host_size, encrypted_size, partition_offset, partition_size;
+        bool image_created = false, dm_activated = false, mounted = false;
+        _cleanup_(user_record_unrefp) UserRecord *new_home = NULL;
+        sd_id128_t partition_uuid, fs_uuid, luks_uuid, disk_uuid;
+        _cleanup_(loop_device_unrefp) LoopDevice *loop = NULL;
+        _cleanup_(crypt_freep) struct crypt_device *cd = NULL;
+        _cleanup_close_ int image_fd = -1, root_fd = -1;
+        const char *fstype, *ip;
+        struct statfs sfs;
+        int r;
+
+        assert(h);
+        assert(h->storage < 0 || h->storage == USER_LUKS);
+        assert(ret_home);
+
+        assert_se(ip = user_record_image_path(h));
+
+        fstype = user_record_file_system_type(h);
+        if (!supported_fstype(fstype))
+                return log_error_errno(SYNTHETIC_ERRNO(EPROTONOSUPPORT), "Unsupported file system type: %s", h->file_system_type);
+
+        if (sd_id128_is_null(h->partition_uuid)) {
+                r = sd_id128_randomize(&partition_uuid);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to acquire partition UUID: %m");
+        } else
+                partition_uuid = h->partition_uuid;
+
+        if (sd_id128_is_null(h->luks_uuid)) {
+                r = sd_id128_randomize(&luks_uuid);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to acquire LUKS UUID: %m");
+        } else
+                luks_uuid = h->luks_uuid;
+
+        if (sd_id128_is_null(h->file_system_uuid)) {
+                r = sd_id128_randomize(&fs_uuid);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to acquire file system UUID: %m");
+        } else
+                fs_uuid = h->file_system_uuid;
+
+        r = make_dm_names(h->user_name, &dm_name, &dm_node);
+        if (r < 0)
+                return r;
+
+        r = access(dm_node, F_OK);
+        if (r < 0) {
+                if (errno != ENOENT)
+                        return log_error_errno(errno, "Failed to determine whether %s exists: %m", dm_node);
+        } else
+                return log_error_errno(SYNTHETIC_ERRNO(EEXIST), "Device mapper device %s already exists, refusing.", dm_node);
+
+        if (path_startswith(ip, "/dev/")) {
+                _cleanup_free_ char *sysfs = NULL;
+                uint64_t block_device_size;
+                struct stat st;
+
+                /* Let's place the home directory on a real device, i.e. an USB stick or such */
+
+                image_fd = open(ip, O_RDWR|O_CLOEXEC|O_NOCTTY|O_NONBLOCK);
+                if (image_fd < 0)
+                        return log_error_errno(errno, "Failed to open device %s: %m", ip);
+
+                if (fstat(image_fd, &st) < 0)
+                        return log_error_errno(errno, "Failed to stat device %s: %m", ip);
+                if (!S_ISBLK(st.st_mode))
+                        return log_error_errno(SYNTHETIC_ERRNO(ENOTBLK), "Device is not a block device, refusing.");
+
+                if (asprintf(&sysfs, "/sys/dev/block/%u:%u/partition", major(st.st_rdev), minor(st.st_rdev)) < 0)
+                        return log_oom();
+                if (access(sysfs, F_OK) < 0) {
+                        if (errno != ENOENT)
+                                return log_error_errno(errno, "Failed to check whether %s exists: %m", sysfs);
+                } else
+                        return log_error_errno(SYNTHETIC_ERRNO(ENOTBLK), "Operating on partitions is currently not supported, sorry. Please specify a top-level block device.");
+
+                if (flock(image_fd, LOCK_EX) < 0) /* make sure udev doesn't read from it while we operate on the device */
+                        return log_error_errno(errno, "Failed to lock block device %s: %m", ip);
+
+                if (ioctl(image_fd, BLKGETSIZE64, &block_device_size) < 0)
+                        return log_error_errno(errno, "Failed to read block device size: %m");
+
+                if (h->disk_size == UINT64_MAX) {
+
+                        /* If a relative disk size is requested, apply it relative to the block device size */
+                        if (h->disk_size_relative < UINT32_MAX)
+                                host_size = CLAMP(DISK_SIZE_ROUND_DOWN(block_device_size * h->disk_size_relative / UINT32_MAX),
+                                                  USER_DISK_SIZE_MIN, USER_DISK_SIZE_MAX);
+                        else
+                                host_size = block_device_size; /* Otherwise, take the full device */
+
+                } else if (h->disk_size > block_device_size)
+                        return log_error_errno(SYNTHETIC_ERRNO(EMSGSIZE), "Selected disk size larger than backing block device, refusing.");
+                else
+                        host_size = DISK_SIZE_ROUND_DOWN(h->disk_size);
+
+                if (!supported_fs_size(fstype, host_size))
+                        return log_error_errno(SYNTHETIC_ERRNO(ERANGE), "Selected file system size too small for %s.", h->file_system_type);
+
+                /* After creation we should reference this partition by its UUID instead of the block
+                 * device. That's preferable since the user might have specified a device node such as
+                 * /dev/sdb to us, which might look very different when replugged. */
+                if (asprintf(&disk_uuid_path, "/dev/disk/by-uuid/" SD_ID128_UUID_FORMAT_STR, SD_ID128_FORMAT_VAL(luks_uuid)) < 0)
+                        return log_oom();
+
+                if (user_record_luks_discard(h)) {
+                        if (ioctl(image_fd, BLKDISCARD, (uint64_t[]) { 0, block_device_size }) < 0)
+                                log_full_errno(errno == EOPNOTSUPP ? LOG_DEBUG : LOG_WARNING, errno,
+                                               "Failed to issue full-device BLKDISCARD on device, ignoring: %m");
+                        else
+                                log_info("Full device discard completed.");
+                }
+        } else {
+                _cleanup_free_ char *parent = NULL;
+
+                parent = dirname_malloc(ip);
+                if (!parent)
+                        return log_oom();
+
+                r = mkdir_p(parent, 0755);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to create parent directory %s: %m", parent);
+
+                r = calculate_disk_size(h, parent, &host_size);
+                if (r < 0)
+                        return r;
+
+                if (!supported_fs_size(fstype, host_size))
+                        return log_error_errno(SYNTHETIC_ERRNO(ERANGE), "Selected file system size too small for %s.", h->file_system_type);
+
+                r = tempfn_random(ip, "homework", &temporary_image_path);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to derive temporary file name for %s: %m", ip);
+
+                image_fd = open(temporary_image_path, O_RDWR|O_CREAT|O_EXCL|O_CLOEXEC|O_NOCTTY|O_NOFOLLOW, 0600);
+                if (image_fd < 0)
+                        return log_error_errno(errno, "Failed to create home image %s: %m", temporary_image_path);
+
+                image_created = true;
+
+                r = chattr_fd(image_fd, FS_NOCOW_FL, FS_NOCOW_FL, NULL);
+                if (r < 0)
+                        log_warning_errno(r, "Failed to set file attributes on %s, ignoring: %m", temporary_image_path);
+
+                if (user_record_luks_discard(h))
+                        r = ftruncate(image_fd, host_size);
+                else
+                        r = fallocate(image_fd, 0, 0, host_size);
+                if (r < 0) {
+                        if (ERRNO_IS_DISK_SPACE(errno)) {
+                                log_debug_errno(errno, "Not enough disk space to allocate home.");
+                                r = -ENOSPC; /* make recognizable */
+                                goto fail;
+                        }
+
+                        r = log_error_errno(errno, "Failed to truncate home image %s: %m", temporary_image_path);
+                        goto fail;
+                }
+
+                log_info("Allocating image file completed.");
+        }
+
+        r = make_partition_table(
+                        image_fd,
+                        user_record_user_name_and_realm(h),
+                        partition_uuid,
+                        &partition_offset,
+                        &partition_size,
+                        &disk_uuid);
+        if (r < 0)
+                goto fail;
+
+        log_info("Writing of partition table completed.");
+
+        r = loop_device_make(image_fd, O_RDWR, partition_offset, partition_size, 0, &loop);
+        if (r < 0) {
+                if (r == -ENOENT) { /* this means /dev/loop-control doesn't exist, i.e. we are in a container
+                                     * or similar and loopback bock devices are not available, return a
+                                     * recognizable error in this case. */
+                        log_error_errno(r, "Loopback block device support is not available on this system.");
+                        r = -ENOLINK;
+                        goto fail;
+                }
+
+                log_error_errno(r, "Failed to set up loopback device for %s: %m", temporary_image_path);
+                goto fail;
+        }
+
+        r = loop_device_flock(loop, LOCK_EX); /* make sure udev won't read before we are done */
+        if (r < 0) {
+                log_error_errno(r, "Failed to take lock on loop device: %m");
+                goto fail;
+        }
+
+        log_info("Setting up loopback device %s completed.", loop->node ?: ip);
+
+        r = luks_format(loop->node,
+                        dm_name,
+                        luks_uuid,
+                        user_record_user_name_and_realm(h),
+                        pkcs11_decrypted_passwords,
+                        effective_passwords,
+                        user_record_luks_discard(h),
+                        h,
+                        &cd);
+        if (r < 0)
+                goto fail;
+
+        dm_activated = true;
+
+        r = block_get_size_by_path(dm_node, &encrypted_size);
+        if (r < 0) {
+                log_error_errno(r, "Failed to get encrypted block device size: %m");
+                goto fail;
+        }
+
+        log_info("Setting up LUKS device %s completed.", dm_node);
+
+        r = run_mkfs(dm_node, fstype, user_record_user_name_and_realm(h), fs_uuid, user_record_luks_discard(h));
+        if (r < 0)
+                goto fail;
+
+        log_info("Formatting file system completed.");
+
+        r = home_unshare_and_mount(dm_node, fstype, user_record_luks_discard(h));
+        if (r < 0)
+                goto fail;
+
+        mounted = true;
+
+        subdir = path_join("/run/systemd/user-home-mount/", user_record_user_name_and_realm(h));
+        if (!subdir) {
+                r = log_oom();
+                goto fail;
+        }
+
+        if (mkdir(subdir, 0700) < 0) {
+                r = log_error_errno(errno, "Failed to create user directory in mounted image file: %m");
+                goto fail;
+        }
+
+        root_fd = open(subdir, O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW);
+        if (root_fd < 0) {
+                r = log_error_errno(errno, "Failed to open user directory in mounted image file: %m");
+                goto fail;
+        }
+
+        r = home_populate(h, root_fd);
+        if (r < 0)
+                goto fail;
+
+        r = home_sync_and_statfs(root_fd, &sfs);
+        if (r < 0)
+                goto fail;
+
+        r = user_record_clone(h, USER_RECORD_LOAD_MASK_SECRET|USER_RECORD_LOG, &new_home);
+        if (r < 0) {
+                log_error_errno(r, "Failed to clone record: %m");
+                goto fail;
+        }
+
+        r = user_record_add_binding(
+                        new_home,
+                        USER_LUKS,
+                        disk_uuid_path ?: ip,
+                        partition_uuid,
+                        luks_uuid,
+                        fs_uuid,
+                        crypt_get_cipher(cd),
+                        crypt_get_cipher_mode(cd),
+                        luks_volume_key_size_convert(cd),
+                        fstype,
+                        NULL,
+                        h->uid,
+                        (gid_t) h->uid);
+        if (r < 0) {
+                log_error_errno(r, "Failed to add binding to record: %m");
+                goto fail;
+        }
+
+        root_fd = safe_close(root_fd);
+
+        r = umount_verbose("/run/systemd/user-home-mount");
+        if (r < 0)
+                goto fail;
+
+        mounted = false;
+
+        r = crypt_deactivate(cd, dm_name);
+        if (r < 0) {
+                log_error_errno(r, "Failed to deactivate LUKS device: %m");
+                goto fail;
+        }
+
+        dm_activated = false;
+
+        loop = loop_device_unref(loop);
+
+        if (disk_uuid_path)
+                (void) ioctl(image_fd, BLKRRPART, 0);
+
+        /* Let's close the image fd now. If we are operating on a real block device this will release the BSD
+         * lock that ensures udev doesn't interfere with what we are doing */
+        image_fd = safe_close(image_fd);
+
+        if (temporary_image_path) {
+                if (rename(temporary_image_path, ip) < 0) {
+                        log_error_errno(errno, "Failed to rename image file: %m");
+                        goto fail;
+                }
+
+                log_info("Moved image file into place.");
+        }
+
+        if (disk_uuid_path)
+                (void) wait_for_devlink(disk_uuid_path);
+
+        log_info("Everything completed.");
+
+        print_size_summary(host_size, encrypted_size, &sfs);
+
+        *ret_home = TAKE_PTR(new_home);
+        return 0;
+
+fail:
+        /* Let's close all files before we unmount the file system, to avoid EBUSY */
+        root_fd = safe_close(root_fd);
+
+        if (mounted)
+                (void) umount_verbose("/run/systemd/user-home-mount");
+
+        if (dm_activated)
+                (void) crypt_deactivate(cd, dm_name);
+
+        loop = loop_device_unref(loop);
+
+        if (image_created)
+                (void) unlink(temporary_image_path);
+
+        return r;
+}
+
+int home_validate_update_luks(UserRecord *h, HomeSetup *setup) {
+        _cleanup_free_ char *dm_name = NULL, *dm_node = NULL;
+        int r;
+
+        assert(h);
+        assert(setup);
+
+        r = make_dm_names(h->user_name, &dm_name, &dm_node);
+        if (r < 0)
+                return r;
+
+        r = access(dm_node, F_OK);
+        if (r < 0 && errno != ENOENT)
+                return log_error_errno(errno, "Failed to determine whether %s exists: %m", dm_node);
+
+        free_and_replace(setup->dm_name, dm_name);
+        free_and_replace(setup->dm_node, dm_node);
+
+        return r >= 0;
+}
+
+enum {
+        CAN_RESIZE_ONLINE,
+        CAN_RESIZE_OFFLINE,
+};
+
+static int can_resize_fs(int fd, uint64_t old_size, uint64_t new_size) {
+        struct statfs sfs;
+
+        assert(fd >= 0);
+
+        /* Filter out bogus requests early */
+        if (old_size == 0 || old_size == UINT64_MAX ||
+            new_size == 0 || new_size == UINT64_MAX)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid resize parameters.");
+
+        if ((old_size & 511) != 0 || (new_size & 511) != 0)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Resize parameters not multiple of 512.");
+
+        if (fstatfs(fd, &sfs) < 0)
+                return log_error_errno(errno, "Failed to fstatfs() file system: %m");
+
+        if (is_fs_type(&sfs, BTRFS_SUPER_MAGIC)) {
+
+                if (new_size < BTRFS_MINIMAL_SIZE)
+                        return log_error_errno(SYNTHETIC_ERRNO(ERANGE), "New file system size too small for btrfs (needs to be 256M at least.");
+
+                /* btrfs can grow and shrink online */
+
+        } else if (is_fs_type(&sfs, XFS_SB_MAGIC)) {
+
+                if (new_size < XFS_MINIMAL_SIZE)
+                        return log_error_errno(SYNTHETIC_ERRNO(ERANGE), "New file system size too small for xfs (needs to be 14M at least).");
+
+                /* XFS can grow, but not shrink */
+                if (new_size < old_size)
+                        return log_error_errno(SYNTHETIC_ERRNO(EMSGSIZE), "Shrinking this type of file system is not supported.");
+
+        } else if (is_fs_type(&sfs, EXT4_SUPER_MAGIC)) {
+
+                if (new_size < EXT4_MINIMAL_SIZE)
+                        return log_error_errno(SYNTHETIC_ERRNO(ERANGE), "New file system size too small for ext4 (needs to be 1M at least).");
+
+                /* ext4 can grow online, and shrink offline */
+                if (new_size < old_size)
+                        return CAN_RESIZE_OFFLINE;
+
+        } else
+                return log_error_errno(SYNTHETIC_ERRNO(ESOCKTNOSUPPORT), "Resizing this type of file system is not supported.");
+
+        return CAN_RESIZE_ONLINE;
+}
+
+static int ext4_offline_resize_fs(HomeSetup *setup, uint64_t new_size, bool discard) {
+        _cleanup_free_ char *size_str = NULL;
+        bool re_open = false, re_mount = false;
+        pid_t resize_pid, fsck_pid;
+        int r, exit_status;
+
+        assert(setup);
+        assert(setup->dm_node);
+
+        /* First, unmount the file system */
+        if (setup->root_fd >= 0) {
+                setup->root_fd = safe_close(setup->root_fd);
+                re_open = true;
+        }
+
+        if (setup->undo_mount) {
+                r = umount_verbose("/run/systemd/user-home-mount");
+                if (r < 0)
+                        return r;
+
+                setup->undo_mount = false;
+                re_mount = true;
+        }
+
+        log_info("Temporarary unmounting of file system completed.");
+
+        /* resize2fs requires that the file system is force checked first, do so. */
+        r = safe_fork("(e2fsck)", FORK_RESET_SIGNALS|FORK_RLIMIT_NOFILE_SAFE|FORK_DEATHSIG|FORK_LOG|FORK_STDOUT_TO_STDERR, &fsck_pid);
+        if (r < 0)
+                return r;
+        if (r == 0) {
+                /* Child */
+                execlp("e2fsck" ,"e2fsck", "-fp", setup->dm_node, NULL);
+                log_error_errno(errno, "Failed to execute e2fsck: %m");
+                _exit(EXIT_FAILURE);
+        }
+
+        exit_status = wait_for_terminate_and_check("e2fsck", fsck_pid, WAIT_LOG_ABNORMAL);
+        if (exit_status < 0)
+                return exit_status;
+        if ((exit_status & ~FSCK_ERROR_CORRECTED) != 0) {
+                log_warning("e2fsck failed with exit status %i.", exit_status);
+
+                if ((exit_status & (FSCK_SYSTEM_SHOULD_REBOOT|FSCK_ERRORS_LEFT_UNCORRECTED)) != 0)
+                        return log_error_errno(SYNTHETIC_ERRNO(EIO), "File system is corrupted, refusing.");
+
+                log_warning("Ignoring fsck error.");
+        }
+
+        log_info("Forced file system check completed.");
+
+        /* We use 512 sectors here, because resize2fs doesn't do byte sizes */
+        if (asprintf(&size_str, "%" PRIu64 "s", new_size / 512) < 0)
+                return log_oom();
+
+        /* Resize the thing */
+        r = safe_fork("(e2resize)", FORK_RESET_SIGNALS|FORK_RLIMIT_NOFILE_SAFE|FORK_DEATHSIG|FORK_LOG|FORK_WAIT|FORK_STDOUT_TO_STDERR, &resize_pid);
+        if (r < 0)
+                return r;
+        if (r == 0) {
+                /* Child */
+                execlp("resize2fs" ,"resize2fs", setup->dm_node, size_str, NULL);
+                log_error_errno(errno, "Failed to execute resize2fs: %m");
+                _exit(EXIT_FAILURE);
+        }
+
+        log_info("Offline file system resize completed.");
+
+        /* Re-establish mounts and reopen the directory */
+        if (re_mount) {
+                r = home_mount_node(setup->dm_node, "ext4", discard);
+                if (r < 0)
+                        return r;
+
+                setup->undo_mount = true;
+        }
+
+        if (re_open) {
+                setup->root_fd = open("/run/systemd/user-home-mount", O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW);
+                if (setup->root_fd < 0)
+                        return log_error_errno(errno, "Failed to reopen file system: %m");
+        }
+
+        log_info("File system mounted again.");
+
+        return 0;
+}
+
+static int prepare_resize_partition(
+                int fd,
+                uint64_t partition_offset,
+                uint64_t old_partition_size,
+                uint64_t new_partition_size,
+                sd_id128_t *ret_disk_uuid,
+                struct fdisk_table **ret_table) {
+
+        _cleanup_(fdisk_unref_contextp) struct fdisk_context *c = NULL;
+        _cleanup_(fdisk_unref_tablep) struct fdisk_table *t = NULL;
+        _cleanup_free_ char *path = NULL, *disk_uuid_as_string = NULL;
+        size_t n_partitions, i;
+        sd_id128_t disk_uuid;
+        bool found = false;
+        int r;
+
+        assert(fd >= 0);
+        assert(ret_disk_uuid);
+        assert(ret_table);
+
+        assert((partition_offset & 511) == 0);
+        assert((old_partition_size & 511) == 0);
+        assert((new_partition_size & 511) == 0);
+        assert(UINT64_MAX - old_partition_size >= partition_offset);
+        assert(UINT64_MAX - new_partition_size >= partition_offset);
+
+        if (partition_offset == 0) {
+                /* If the offset is at the beginning we assume no partition table, let's exit early. */
+                log_debug("Not rewriting partition table, operating on naked device.");
+                *ret_disk_uuid = SD_ID128_NULL;
+                *ret_table = NULL;
+                return 0;
+        }
+
+        c = fdisk_new_context();
+        if (!c)
+                return log_oom();
+
+        if (asprintf(&path, "/proc/self/fd/%i", fd) < 0)
+                return log_oom();
+
+        r = fdisk_assign_device(c, path, 0);
+        if (r < 0)
+                return log_error_errno(r, "Failed to open device: %m");
+
+        if (!fdisk_is_labeltype(c, FDISK_DISKLABEL_GPT))
+                return log_error_errno(SYNTHETIC_ERRNO(ENOMEDIUM), "Disk has no GPT partition table.");
+
+        r = fdisk_get_disklabel_id(c, &disk_uuid_as_string);
+        if (r < 0)
+                return log_error_errno(r, "Failed to acquire disk UUID: %m");
+
+        r = sd_id128_from_string(disk_uuid_as_string, &disk_uuid);
+        if (r < 0)
+                return log_error_errno(r, "Failed parse disk UUID: %m");
+
+        r = fdisk_get_partitions(c, &t);
+        if (r < 0)
+                return log_error_errno(r, "Failed to acquire partition table: %m");
+
+        n_partitions = fdisk_table_get_nents(t);
+        for (i = 0; i < n_partitions; i++)  {
+                struct fdisk_partition *p;
+
+                p = fdisk_table_get_partition(t, i);
+                if (!p)
+                        return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to read partition metadata: %m");
+
+                if (fdisk_partition_is_used(p) <= 0)
+                        continue;
+                if (fdisk_partition_has_start(p) <= 0 || fdisk_partition_has_size(p) <= 0 || fdisk_partition_has_end(p) <= 0)
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Found partition without a size.");
+
+                if (fdisk_partition_get_start(p) == partition_offset / 512U &&
+                    fdisk_partition_get_size(p) == old_partition_size / 512U) {
+
+                        if (found)
+                                return log_error_errno(SYNTHETIC_ERRNO(ENOTUNIQ), "Partition found twice, refusing.");
+
+                        /* Found our partition, now patch it */
+                        r = fdisk_partition_size_explicit(p, 1);
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to enable explicit partition size: %m");
+
+                        r = fdisk_partition_set_size(p, new_partition_size / 512U);
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to change partition size: %m");
+
+                        found = true;
+                        continue;
+
+                } else {
+                        if (fdisk_partition_get_start(p) < partition_offset + new_partition_size / 512U &&
+                            fdisk_partition_get_end(p) >= partition_offset / 512)
+                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Can't extend, conflicting partition found.");
+                }
+        }
+
+        if (!found)
+                return log_error_errno(SYNTHETIC_ERRNO(ENOPKG), "Failed to find matching partition to resize.");
+
+        *ret_table = TAKE_PTR(t);
+        *ret_disk_uuid = disk_uuid;
+
+        return 1;
+}
+
+static int ask_cb(struct fdisk_context *c, struct fdisk_ask *ask, void *userdata) {
+        char *result;
+
+        assert(c);
+
+        switch (fdisk_ask_get_type(ask)) {
+
+        case FDISK_ASKTYPE_STRING:
+                result = new(char, 37);
+                if (!result)
+                        return log_oom();
+
+                fdisk_ask_string_set_result(ask, id128_to_uuid_string(*(sd_id128_t*) userdata, result));
+                break;
+
+        default:
+                log_debug("Unexpected question from libfdisk, ignoring.");
+        }
+
+        return 0;
+}
+
+static int apply_resize_partition(int fd, sd_id128_t disk_uuids, struct fdisk_table *t) {
+        _cleanup_(fdisk_unref_contextp) struct fdisk_context *c = NULL;
+        _cleanup_free_ void *two_zero_lbas = NULL;
+        _cleanup_free_ char *path = NULL;
+        ssize_t n;
+        int r;
+
+        assert(fd >= 0);
+
+        if (!t) /* no partition table to apply, exit early */
+                return 0;
+
+        two_zero_lbas = malloc0(1024U);
+        if (!two_zero_lbas)
+                return log_oom();
+
+        /* libfdisk appears to get confused by the existing PMBR. Let's explicitly flush it out. */
+        n = pwrite(fd, two_zero_lbas, 1024U, 0);
+        if (n < 0)
+                return log_error_errno(errno, "Failed to wipe partition table: %m");
+        if (n != 1024)
+                return log_error_errno(SYNTHETIC_ERRNO(EIO), "Short write while whiping partition table.");
+
+        c = fdisk_new_context();
+        if (!c)
+                return log_oom();
+
+        if (asprintf(&path, "/proc/self/fd/%i", fd) < 0)
+                return log_oom();
+
+        r = fdisk_assign_device(c, path, 0);
+        if (r < 0)
+                return log_error_errno(r, "Failed to open device: %m");
+
+        r = fdisk_create_disklabel(c, "gpt");
+        if (r < 0)
+                return log_error_errno(r, "Failed to create GPT disk label: %m");
+
+        r = fdisk_apply_table(c, t);
+        if (r < 0)
+                return log_error_errno(r, "Failed to apply partition table: %m");
+
+        r = fdisk_set_ask(c, ask_cb, &disk_uuids);
+        if (r < 0)
+                return log_error_errno(r, "Failed to set libfdisk query function: %m");
+
+        r = fdisk_set_disklabel_id(c);
+        if (r < 0)
+                return log_error_errno(r, "Failed to change disklabel ID: %m");
+
+        r = fdisk_write_disklabel(c);
+        if (r < 0)
+                return log_error_errno(r, "Failed to write disk label: %m");
+
+        return 1;
+}
+
+int home_resize_luks(
+                UserRecord *h,
+                bool already_activated,
+                char ***pkcs11_decrypted_passwords,
+                HomeSetup *setup,
+                UserRecord **ret_home) {
+
+        char buffer1[FORMAT_BYTES_MAX], buffer2[FORMAT_BYTES_MAX], buffer3[FORMAT_BYTES_MAX],
+                buffer4[FORMAT_BYTES_MAX], buffer5[FORMAT_BYTES_MAX], buffer6[FORMAT_BYTES_MAX];
+        uint64_t old_image_size, new_image_size, old_fs_size, new_fs_size, crypto_offset, new_partition_size;
+        _cleanup_(user_record_unrefp) UserRecord *header_home = NULL, *embedded_home = NULL, *new_home = NULL;
+        _cleanup_(fdisk_unref_tablep) struct fdisk_table *table = NULL;
+        _cleanup_free_ char *whole_disk = NULL;
+        _cleanup_close_ int image_fd = -1;
+        sd_id128_t disk_uuid;
+        const char *ip, *ipo;
+        struct statfs sfs;
+        struct stat st;
+        int r, resize_type;
+
+        assert(h);
+        assert(user_record_storage(h) == USER_LUKS);
+        assert(setup);
+        assert(ret_home);
+
+        assert_se(ipo = user_record_image_path(h));
+        ip = strdupa(ipo); /* copy out since original might change later in home record object */
+
+        image_fd = open(ip, O_RDWR|O_CLOEXEC|O_NOCTTY|O_NONBLOCK);
+        if (image_fd < 0)
+                return log_error_errno(errno, "Failed to open image file %s: %m", ip);
+
+        if (fstat(image_fd, &st) < 0)
+                return log_error_errno(errno, "Failed to stat image file %s: %m", ip);
+        if (S_ISBLK(st.st_mode)) {
+                dev_t parent;
+
+                r = block_get_whole_disk(st.st_rdev, &parent);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to acquire whole block device for %s: %m", ip);
+                if (r > 0) {
+                        /* If we shall resize a file system on a partition device, then let's figure out the
+                         * whole disk device and operate on that instead, since we need to rewrite the
+                         * partition table to resize the partition. */
+
+                        log_info("Operating on partition device %s, using parent device.", ip);
+
+                        r = device_path_make_major_minor(st.st_mode, parent, &whole_disk);
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to derive whole disk path for %s: %m", ip);
+
+                        safe_close(image_fd);
+
+                        image_fd = open(whole_disk, O_RDWR|O_CLOEXEC|O_NOCTTY|O_NONBLOCK);
+                        if (image_fd < 0)
+                                return log_error_errno(errno, "Failed to open whole block device %s: %m", whole_disk);
+
+                        if (fstat(image_fd, &st) < 0)
+                                return log_error_errno(errno, "Failed to stat whole block device %s: %m", whole_disk);
+                        if (!S_ISBLK(st.st_mode))
+                                return log_error_errno(SYNTHETIC_ERRNO(ENOTBLK), "Whole block device %s is not actually a block device, refusing.", whole_disk);
+                } else
+                        log_info("Operating on whole block device %s.", ip);
+
+                if (ioctl(image_fd, BLKGETSIZE64, &old_image_size) < 0)
+                        return log_error_errno(errno, "Failed to determine size of original block device: %m");
+
+                if (flock(image_fd, LOCK_EX) < 0) /* make sure udev doesn't read from it while we operate on the device */
+                        return log_error_errno(errno, "Failed to lock block device %s: %m", ip);
+
+                new_image_size = old_image_size; /* we can't resize physical block devices */
+        } else {
+                r = stat_verify_regular(&st);
+                if (r < 0)
+                        return log_error_errno(r, "Image file %s is not a block device nor regular: %m", ip);
+
+                old_image_size = st.st_size;
+
+                /* Note an asymetry here: when we operate on loopback files the specified disk size we get we
+                 * apply onto the loopback file as a whole. When we operate on block devices we instead apply
+                 * to the partition itself only. */
+
+                new_image_size = DISK_SIZE_ROUND_DOWN(h->disk_size);
+                if (new_image_size == old_image_size) {
+                        log_info("Image size already matching, skipping operation.");
+                        return 0;
+                }
+        }
+
+        r = home_prepare_luks(h, already_activated, whole_disk, pkcs11_decrypted_passwords, setup, &header_home);
+        if (r < 0)
+                return r;
+
+        r = home_load_embedded_identity(h, setup->root_fd, header_home, USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL, pkcs11_decrypted_passwords, &embedded_home, &new_home);
+        if (r < 0)
+                return r;
+
+        log_info("offset = %" PRIu64 ", size = %" PRIu64 ", image = %" PRIu64, setup->partition_offset, setup->partition_size, old_image_size);
+
+        if ((UINT64_MAX - setup->partition_offset) < setup->partition_size ||
+            setup->partition_offset + setup->partition_size > old_image_size)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Old partition doesn't fit in backing storage, refusing.");
+
+        if (S_ISREG(st.st_mode)) {
+                uint64_t partition_table_extra;
+
+                partition_table_extra = old_image_size - setup->partition_size;
+                if (new_image_size <= partition_table_extra)
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "New size smaller than partition table metadata.");
+
+                new_partition_size = new_image_size - partition_table_extra;
+        } else {
+                assert(S_ISBLK(st.st_mode));
+
+                new_partition_size = DISK_SIZE_ROUND_DOWN(h->disk_size);
+                if (new_partition_size == setup->partition_size) {
+                        log_info("Partition size already matching, skipping operation.");
+                        return 0;
+                }
+        }
+
+        if ((UINT64_MAX - setup->partition_offset) < new_partition_size ||
+            setup->partition_offset + new_partition_size > new_image_size)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "New partition doesn't fit into backing storage, refusing.");
+
+        crypto_offset = crypt_get_data_offset(setup->crypt_device);
+        if (setup->partition_size / 512U <= crypto_offset)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Weird, old crypto payload offset doesn't actually fit in partition size?");
+        if (new_partition_size / 512U <= crypto_offset)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "New size smaller than crypto payload offset?");
+
+        old_fs_size = (setup->partition_size / 512U - crypto_offset) * 512U;
+        new_fs_size = (new_partition_size / 512U - crypto_offset) * 512U;
+
+        /* Before we start doing anything, let's figure out if we actually can */
+        resize_type = can_resize_fs(setup->root_fd, old_fs_size, new_fs_size);
+        if (resize_type < 0)
+                return resize_type;
+        if (resize_type == CAN_RESIZE_OFFLINE && already_activated)
+                return log_error_errno(SYNTHETIC_ERRNO(ETXTBSY), "File systems of this type can only be resized offline, but is currently online.");
+
+        log_info("Ready to resize image size %s → %s, partition size %s → %s, file system size %s → %s.",
+                 format_bytes(buffer1, sizeof(buffer1), old_image_size),
+                 format_bytes(buffer2, sizeof(buffer2), new_image_size),
+                 format_bytes(buffer3, sizeof(buffer3), setup->partition_size),
+                 format_bytes(buffer4, sizeof(buffer4), new_partition_size),
+                 format_bytes(buffer5, sizeof(buffer5), old_fs_size),
+                 format_bytes(buffer6, sizeof(buffer6), new_fs_size));
+
+        r = prepare_resize_partition(
+                        image_fd,
+                        setup->partition_offset,
+                        setup->partition_size,
+                        new_partition_size,
+                        &disk_uuid,
+                        &table);
+        if (r < 0)
+                return r;
+
+        if (new_fs_size > old_fs_size) {
+
+                if (S_ISREG(st.st_mode)) {
+                        /* Grow file size */
+
+                        if (user_record_luks_discard(h))
+                                r = ftruncate(image_fd, new_image_size);
+                        else
+                                r = fallocate(image_fd, 0, 0, new_image_size);
+                        if (r < 0) {
+                                if (ERRNO_IS_DISK_SPACE(errno)) {
+                                        log_debug_errno(errno, "Not enough disk space to grow home.");
+                                        return -ENOSPC; /* make recognizable */
+                                }
+
+                                return log_error_errno(errno, "Failed to grow image file %s: %m", ip);
+                        }
+
+                        log_info("Growing of image file completed.");
+                }
+
+                /* Make sure loopback device sees the new bigger size */
+                r = loop_device_refresh_size(setup->loop, UINT64_MAX, new_partition_size);
+                if (r == -ENOTTY)
+                        log_debug_errno(r, "Device is not a loopback device, not refreshing size.");
+                else if (r < 0)
+                        return log_error_errno(r, "Failed to refresh loopback device size: %m");
+                else
+                        log_info("Refreshing loop device size completed.");
+
+                r = apply_resize_partition(image_fd, disk_uuid, table);
+                if (r < 0)
+                        return r;
+                if (r > 0)
+                        log_info("Growing of partition completed.");
+
+                if (ioctl(image_fd, BLKRRPART, 0) < 0)
+                        log_debug_errno(errno, "BLKRRPART failed on block device, ignoring: %m");
+
+                /* Tell LUKS about the new bigger size too */
+                r = crypt_resize(setup->crypt_device, setup->dm_name, new_fs_size / 512U);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to grow LUKS device: %m");
+
+                log_info("LUKS device growing completed.");
+        } else {
+                r = home_store_embedded_identity(new_home, setup->root_fd, h->uid, embedded_home);
+                if (r < 0)
+                        return r;
+
+                if (S_ISREG(st.st_mode)) {
+                        if (user_record_luks_discard(h))
+                                /* Before we shrink, let's trim the file system, so that we need less space on disk during the shrinking */
+                                (void) run_fitrim(setup->root_fd);
+                        else {
+                                /* If discard is off, let's ensure all backing blocks are allocated, so that our resize operation doesn't fail half-way */
+                                r = run_fallocate(image_fd, &st);
+                                if (r < 0)
+                                        return r;
+                        }
+                }
+        }
+
+        /* Now resize the file system */
+        if (resize_type == CAN_RESIZE_ONLINE)
+                r = resize_fs(setup->root_fd, new_fs_size, NULL);
+        else
+                r = ext4_offline_resize_fs(setup, new_fs_size, user_record_luks_discard(h));
+        if (r < 0)
+                return log_error_errno(r, "Failed to resize file system: %m");
+
+        log_info("File system resizing completed.");
+
+        /* Immediately sync afterwards */
+        r = home_sync_and_statfs(setup->root_fd, NULL);
+        if (r < 0)
+                return r;
+
+        if (new_fs_size < old_fs_size) {
+
+                /* Shrink the LUKS device now, matching the new file system size */
+                r = crypt_resize(setup->crypt_device, setup->dm_name, new_fs_size / 512);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to shrink LUKS device: %m");
+
+                log_info("LUKS device shrinking completed.");
+
+                if (S_ISREG(st.st_mode)) {
+                        /* Shrink the image file */
+                        if (ftruncate(image_fd, new_image_size) < 0)
+                                return log_error_errno(errno, "Failed to shrink image file %s: %m", ip);
+
+                        log_info("Shrinking of image file completed.");
+                }
+
+                /* Refresh the loop devices size */
+                r = loop_device_refresh_size(setup->loop, UINT64_MAX, new_partition_size);
+                if (r == -ENOTTY)
+                        log_debug_errno(r, "Device is not a loopback device, not refreshing size.");
+                else if (r < 0)
+                        return log_error_errno(r, "Failed to refresh loopback device size: %m");
+                else
+                        log_info("Refreshing loop device size completed.");
+
+                r = apply_resize_partition(image_fd, disk_uuid, table);
+                if (r < 0)
+                        return r;
+                if (r > 0)
+                        log_info("Shrinking of partition completed.");
+
+                if (ioctl(image_fd, BLKRRPART, 0) < 0)
+                        log_debug_errno(errno, "BLKRRPART failed on block device, ignoring: %m");
+        } else {
+                r = home_store_embedded_identity(new_home, setup->root_fd, h->uid, embedded_home);
+                if (r < 0)
+                        return r;
+        }
+
+        r = home_store_header_identity_luks(new_home, setup, header_home);
+        if (r < 0)
+                return r;
+
+        r = home_extend_embedded_identity(new_home, h, setup);
+        if (r < 0)
+                return r;
+
+        if (user_record_luks_discard(h))
+                (void) run_fitrim(setup->root_fd);
+
+        r = home_sync_and_statfs(setup->root_fd, &sfs);
+        if (r < 0)
+                return r;
+
+        r = home_setup_undo(setup);
+        if (r < 0)
+                return r;
+
+        log_info("Everything completed.");
+
+        print_size_summary(new_image_size, new_fs_size, &sfs);
+
+        *ret_home = TAKE_PTR(new_home);
+        return 0;
+}
+
+int home_passwd_luks(
+                UserRecord *h,
+                HomeSetup *setup,
+                char **pkcs11_decrypted_passwords, /* the passwords acquired via PKCS#11 security tokens */
+                char **effective_passwords         /* new passwords */) {
+
+        size_t volume_key_size, i, max_key_slots, n_effective;
+        _cleanup_(erase_and_freep) void *volume_key = NULL;
+        struct crypt_pbkdf_type good_pbkdf, minimal_pbkdf;
+        const char *type;
+        int r;
+
+        assert(h);
+        assert(user_record_storage(h) == USER_LUKS);
+        assert(setup);
+
+        type = crypt_get_type(setup->crypt_device);
+        if (!type)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to determine crypto device type.");
+
+        r = crypt_keyslot_max(type);
+        if (r <= 0)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to determine number of key slots.");
+        max_key_slots = r;
+
+        r = crypt_get_volume_key_size(setup->crypt_device);
+        if (r <= 0)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to determine volume key size.");
+        volume_key_size = (size_t) r;
+
+        volume_key = malloc(volume_key_size);
+        if (!volume_key)
+                return log_oom();
+
+        r = luks_try_passwords(setup->crypt_device, pkcs11_decrypted_passwords, volume_key, &volume_key_size);
+        if (r == -ENOKEY) {
+                r = luks_try_passwords(setup->crypt_device, h->password, volume_key, &volume_key_size);
+                if (r == -ENOKEY)
+                        return log_error_errno(SYNTHETIC_ERRNO(ENOKEY), "Failed to unlock LUKS superblock with supplied passwords.");
+        }
+        if (r < 0)
+                return log_error_errno(r, "Failed to unlocks LUKS superblock: %m");
+
+        n_effective = strv_length(effective_passwords);
+
+        build_good_pbkdf(&good_pbkdf, h);
+        build_minimal_pbkdf(&minimal_pbkdf, h);
+
+        for (i = 0; i < max_key_slots; i++) {
+                r = crypt_keyslot_destroy(setup->crypt_device, i);
+                if (r < 0 && !IN_SET(r, -ENOENT, -EINVAL)) /* Returns EINVAL or ENOENT if there's no key in this slot already */
+                        return log_error_errno(r, "Failed to destroy LUKS password: %m");
+
+                if (i >= n_effective) {
+                        if (r >= 0)
+                                log_info("Destroyed LUKS key slot %zu.", i);
+                        continue;
+                }
+
+                if (strv_find(pkcs11_decrypted_passwords, effective_passwords[i])) {
+                        log_debug("Using minimal PBKDF for slot %zu", i);
+                        r = crypt_set_pbkdf_type(setup->crypt_device, &minimal_pbkdf);
+                } else {
+                        log_debug("Using good PBKDF for slot %zu", i);
+                        r = crypt_set_pbkdf_type(setup->crypt_device, &good_pbkdf);
+                }
+                if (r < 0)
+                        return log_error_errno(r, "Failed to tweak PBKDF for slot %zu: %m", i);
+
+                r = crypt_keyslot_add_by_volume_key(
+                                setup->crypt_device,
+                                i,
+                                volume_key,
+                                volume_key_size,
+                                effective_passwords[i],
+                                strlen(effective_passwords[i]));
+                if (r < 0)
+                        return log_error_errno(r, "Failed to set up LUKS password: %m");
+
+                log_info("Updated LUKS key slot %zu.", i);
+        }
+
+        return 1;
+}
+
+int home_lock_luks(UserRecord *h) {
+        _cleanup_(crypt_freep) struct crypt_device *cd = NULL;
+        _cleanup_free_ char *dm_name = NULL, *dm_node = NULL;
+        _cleanup_close_ int root_fd = -1;
+        const char *p;
+        int r;
+
+        assert(h);
+
+        assert_se(p = user_record_home_directory(h));
+        root_fd = open(p, O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW);
+        if (root_fd < 0)
+                return log_error_errno(errno, "Failed to open home directory: %m");
+
+        r = make_dm_names(h->user_name, &dm_name, &dm_node);
+        if (r < 0)
+                return r;
+
+        r = crypt_init_by_name(&cd, dm_name);
+        if (r < 0)
+                return log_error_errno(r, "Failed to initialize cryptsetup context for %s: %m", dm_name);
+
+        log_info("Discovered used LUKS device %s.", dm_node);
+        crypt_set_log_callback(cd, cryptsetup_log_glue, NULL);
+
+        if (syncfs(root_fd) < 0) /* Snake oil, but let's better be safe than sorry */
+                return log_error_errno(errno, "Failed to synchronize file system %s: %m", p);
+
+        root_fd = safe_close(root_fd);
+
+        log_info("File system synchronized.");
+
+        /* Note that we don't invoke FIFREEZE here, it appears libcryptsetup/device-mapper already does that on its own for us */
+
+        r = crypt_suspend(cd, dm_name);
+        if (r < 0)
+                return log_error_errno(r, "Failed to suspend cryptsetup device: %s: %m", dm_node);
+
+        log_info("LUKS device suspended.");
+        return 0;
+}
+
+static int luks_try_resume(
+                struct crypt_device *cd,
+                const char *dm_name,
+                char **password) {
+
+        char **pp;
+        int r;
+
+        assert(cd);
+        assert(dm_name);
+
+        STRV_FOREACH(pp, password) {
+                r = crypt_resume_by_passphrase(
+                                cd,
+                                dm_name,
+                                CRYPT_ANY_SLOT,
+                                *pp,
+                                strlen(*pp));
+                if (r >= 0) {
+                        log_info("Resumed LUKS device %s.", dm_name);
+                        return 0;
+                }
+
+                log_debug_errno(r, "Password %zu didn't work for resuming device: %m", (size_t) (pp - password));
+        }
+
+        return -ENOKEY;
+}
+
+int home_unlock_luks(UserRecord *h, char ***pkcs11_decrypted_passwords) {
+        _cleanup_free_ char *dm_name = NULL, *dm_node = NULL;
+        _cleanup_(crypt_freep) struct crypt_device *cd = NULL;
+        int r;
+
+        assert(h);
+
+        r = make_dm_names(h->user_name, &dm_name, &dm_node);
+        if (r < 0)
+                return r;
+
+        r = crypt_init_by_name(&cd, dm_name);
+        if (r < 0)
+                return log_error_errno(r, "Failed to initialize cryptsetup context for %s: %m", dm_name);
+
+        log_info("Discovered used LUKS device %s.", dm_node);
+        crypt_set_log_callback(cd, cryptsetup_log_glue, NULL);
+
+        r = luks_try_resume(cd, dm_name, pkcs11_decrypted_passwords ? *pkcs11_decrypted_passwords : NULL);
+        if (r == -ENOKEY) {
+                r = luks_try_resume(cd, dm_name, h->password);
+                if (r == -ENOKEY)
+                        return log_error_errno(r, "No valid password for LUKS superblock.");
+        }
+        if (r < 0)
+                return log_error_errno(r, "Failed to resume LUKS superblock: %m");
+
+        log_info("LUKS device resumed.");
+        return 0;
+}
diff --git a/src/home/homework-luks.h b/src/home/homework-luks.h
new file mode 100644 (file)
index 0000000..581255a
--- /dev/null
@@ -0,0 +1,38 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include "crypt-util.h"
+#include "homework.h"
+#include "user-record.h"
+
+int home_prepare_luks(UserRecord *h, bool already_activated, const char *force_image_path, char ***pkcs11_decrypted_passwords, HomeSetup *setup, UserRecord **ret_luks_home);
+
+int home_activate_luks(UserRecord *h, char ***pkcs11_decrypted_passwords, UserRecord **ret_home);
+int home_deactivate_luks(UserRecord *h);
+
+int home_store_header_identity_luks(UserRecord *h, HomeSetup *setup, UserRecord *old_home);
+
+int home_create_luks(UserRecord *h, char **pkcs11_decrypted_passwords, char **effective_passwords, UserRecord **ret_home);
+
+int home_validate_update_luks(UserRecord *h, HomeSetup *setup);
+
+int home_resize_luks(UserRecord *h, bool already_activated, char ***pkcs11_decrypted_passwords, HomeSetup *setup, UserRecord **ret_home);
+
+int home_passwd_luks(UserRecord *h, HomeSetup *setup, char **pkcs11_decrypted_passwords, char **effective_passwords);
+
+int home_lock_luks(UserRecord *h);
+int home_unlock_luks(UserRecord *h, char ***pkcs11_decrypted_passwords);
+
+static inline uint64_t luks_volume_key_size_convert(struct crypt_device *cd) {
+        int k;
+
+        assert(cd);
+
+        /* Convert the "int" to uint64_t, which we usually use for byte sizes stored on disk. */
+
+        k = crypt_get_volume_key_size(cd);
+        if (k <= 0)
+                return UINT64_MAX;
+
+        return (uint64_t) k;
+}
diff --git a/src/home/homework-mount.c b/src/home/homework-mount.c
new file mode 100644 (file)
index 0000000..9e11168
--- /dev/null
@@ -0,0 +1,96 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include <sched.h>
+#include <sys/mount.h>
+
+#include "alloc-util.h"
+#include "homework-mount.h"
+#include "mkdir.h"
+#include "mount-util.h"
+#include "path-util.h"
+#include "string-util.h"
+
+static const char *mount_options_for_fstype(const char *fstype) {
+        if (streq(fstype, "ext4"))
+                return "noquota,user_xattr";
+        if (streq(fstype, "xfs"))
+                return "noquota";
+        if (streq(fstype, "btrfs"))
+                return "noacl";
+        return NULL;
+}
+
+int home_mount_node(const char *node, const char *fstype, bool discard) {
+        _cleanup_free_ char *joined = NULL;
+        const char *options, *discard_option;
+        int r;
+
+        options = mount_options_for_fstype(fstype);
+
+        discard_option = discard ? "discard" : "nodiscard";
+
+        if (options) {
+                joined = strjoin(options, ",", discard_option);
+                if (!joined)
+                        return log_oom();
+
+                options = joined;
+        } else
+                options = discard_option;
+
+        r = mount_verbose(LOG_ERR, node, "/run/systemd/user-home-mount", fstype, MS_NODEV|MS_NOSUID|MS_RELATIME, strempty(options));
+        if (r < 0)
+                return r;
+
+        log_info("Mounting file system completed.");
+        return 0;
+}
+
+int home_unshare_and_mount(const char *node, const char *fstype, bool discard) {
+        int r;
+
+        if (unshare(CLONE_NEWNS) < 0)
+                return log_error_errno(errno, "Couldn't unshare file system namespace: %m");
+
+        r = mount_verbose(LOG_ERR, "/run", "/run", NULL, MS_SLAVE|MS_REC, NULL); /* Mark /run as MS_SLAVE in our new namespace */
+        if (r < 0)
+                return r;
+
+        (void) mkdir_p("/run/systemd/user-home-mount", 0700);
+
+        if (node)
+                return home_mount_node(node, fstype, discard);
+
+        return 0;
+}
+
+int home_move_mount(const char *user_name_and_realm, const char *target) {
+        _cleanup_free_ char *subdir = NULL;
+        const char *d;
+        int r;
+
+        assert(user_name_and_realm);
+        assert(target);
+
+        if (user_name_and_realm) {
+                subdir = path_join("/run/systemd/user-home-mount/", user_name_and_realm);
+                if (!subdir)
+                        return log_oom();
+
+                d = subdir;
+        } else
+                d = "/run/systemd/user-home-mount/";
+
+        (void) mkdir_p(target, 0700);
+
+        r = mount_verbose(LOG_ERR, d, target, NULL, MS_BIND, NULL);
+        if (r < 0)
+                return r;
+
+        r = umount_verbose("/run/systemd/user-home-mount");
+        if (r < 0)
+                return r;
+
+        log_info("Moving to final mount point %s completed.", target);
+        return 0;
+}
diff --git a/src/home/homework-mount.h b/src/home/homework-mount.h
new file mode 100644 (file)
index 0000000..d926756
--- /dev/null
@@ -0,0 +1,8 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include <stdbool.h>
+
+int home_mount_node(const char *node, const char *fstype, bool discard);
+int home_unshare_and_mount(const char *node, const char *fstype, bool discard);
+int home_move_mount(const char *user_name_and_realm, const char *target);
diff --git a/src/home/homework-pkcs11.c b/src/home/homework-pkcs11.c
new file mode 100644 (file)
index 0000000..941ba23
--- /dev/null
@@ -0,0 +1,104 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include "hexdecoct.h"
+#include "homework-pkcs11.h"
+#include "pkcs11-util.h"
+#include "strv.h"
+
+int pkcs11_callback(
+                CK_FUNCTION_LIST *m,
+                CK_SESSION_HANDLE session,
+                CK_SLOT_ID slot_id,
+                const CK_SLOT_INFO *slot_info,
+                const CK_TOKEN_INFO *token_info,
+                P11KitUri *uri,
+                void *userdata) {
+
+        _cleanup_(erase_and_freep) void *decrypted_key = NULL;
+        struct pkcs11_callback_data *data = userdata;
+        _cleanup_free_ char *token_label = NULL;
+        CK_TOKEN_INFO updated_token_info;
+        size_t decrypted_key_size;
+        CK_OBJECT_HANDLE object;
+        char **i;
+        CK_RV rv;
+        int r;
+
+        assert(m);
+        assert(slot_info);
+        assert(token_info);
+        assert(uri);
+        assert(data);
+
+        /* Special return values:
+         *
+         * -ENOANO       → if we need a PIN but have none
+         * -ERFKILL      → if a "protected authentication path" is needed but we have no OK to use it
+         * -EOWNERDEAD   → if the PIN is locked
+         * -ENOLCK       → if the supplied PIN is incorrect
+         * -ETOOMANYREFS → ditto, but only a few tries left
+         * -EUCLEAN      → ditto, but only a single try left
+         */
+
+        token_label = pkcs11_token_label(token_info);
+        if (!token_label)
+                return log_oom();
+
+        if (FLAGS_SET(token_info->flags, CKF_PROTECTED_AUTHENTICATION_PATH)) {
+
+                if (data->secret->pkcs11_protected_authentication_path_permitted <= 0)
+                        return log_error_errno(SYNTHETIC_ERRNO(ERFKILL), "Security token requires authentication through protected authentication path.");
+
+                rv = m->C_Login(session, CKU_USER, NULL, 0);
+                if (rv != CKR_OK)
+                        return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to log into security token '%s': %s", token_label, p11_kit_strerror(rv));
+
+                log_info("Successully logged into security token '%s' via protected authentication path.", token_label);
+                goto decrypt;
+        }
+
+        if (!FLAGS_SET(token_info->flags, CKF_LOGIN_REQUIRED)) {
+                log_info("No login into security token '%s' required.", token_label);
+                goto decrypt;
+        }
+
+        if (strv_isempty(data->secret->pkcs11_pin))
+                return log_error_errno(SYNTHETIC_ERRNO(ENOANO), "Security Token requires PIN.");
+
+        STRV_FOREACH(i, data->secret->pkcs11_pin) {
+                rv = m->C_Login(session, CKU_USER, (CK_UTF8CHAR*) *i, strlen(*i));
+                if (rv == CKR_OK) {
+                        log_info("Successfully logged into security token '%s' with PIN.", token_label);
+                        goto decrypt;
+                }
+                if (rv == CKR_PIN_LOCKED)
+                        return log_error_errno(SYNTHETIC_ERRNO(EOWNERDEAD), "PIN of security token is blocked. Please unblock it first.");
+                if (!IN_SET(rv, CKR_PIN_INCORRECT, CKR_PIN_LEN_RANGE))
+                        return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to log into security token '%s': %s", token_label, p11_kit_strerror(rv));
+        }
+
+        rv = m->C_GetTokenInfo(slot_id, &updated_token_info);
+        if (rv != CKR_OK)
+                return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to acquire updated security token information for slot %lu: %s", slot_id, p11_kit_strerror(rv));
+
+        if (FLAGS_SET(updated_token_info.flags, CKF_USER_PIN_FINAL_TRY))
+                return log_error_errno(SYNTHETIC_ERRNO(EUCLEAN), "PIN of security token incorrect, only a single try left.");
+        if (FLAGS_SET(updated_token_info.flags, CKF_USER_PIN_COUNT_LOW))
+                return log_error_errno(SYNTHETIC_ERRNO(ETOOMANYREFS), "PIN of security token incorrect, only a few tries left.");
+
+        return log_error_errno(SYNTHETIC_ERRNO(ENOLCK), "PIN of security token incorrect.");
+
+decrypt:
+        r = pkcs11_token_find_private_key(m, session, uri, &object);
+        if (r < 0)
+                return r;
+
+        r = pkcs11_token_decrypt_data(m, session, object, data->encrypted_key->data, data->encrypted_key->size, &decrypted_key, &decrypted_key_size);
+        if (r < 0)
+                return r;
+
+        if (base64mem(decrypted_key, decrypted_key_size, &data->decrypted_password) < 0)
+                return log_oom();
+
+        return 1;
+}
diff --git a/src/home/homework-pkcs11.h b/src/home/homework-pkcs11.h
new file mode 100644 (file)
index 0000000..469ba71
--- /dev/null
@@ -0,0 +1,21 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#if HAVE_P11KIT
+#include "memory-util.h"
+#include "user-record.h"
+#include "pkcs11-util.h"
+
+struct pkcs11_callback_data {
+        UserRecord *user_record;
+        UserRecord *secret;
+        Pkcs11EncryptedKey *encrypted_key;
+        char *decrypted_password;
+};
+
+static inline void pkcs11_callback_data_release(struct pkcs11_callback_data *data) {
+        erase_and_free(data->decrypted_password);
+}
+
+int pkcs11_callback(CK_FUNCTION_LIST *m, CK_SESSION_HANDLE session, CK_SLOT_ID slot_id, const CK_SLOT_INFO *slot_info, const CK_TOKEN_INFO *token_info, P11KitUri *uri, void *userdata);
+#endif
diff --git a/src/home/homework-quota.c b/src/home/homework-quota.c
new file mode 100644 (file)
index 0000000..ba3917b
--- /dev/null
@@ -0,0 +1,124 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#include <sys/quota.h>
+
+#include "blockdev-util.h"
+#include "btrfs-util.h"
+#include "errno-util.h"
+#include "format-util.h"
+#include "homework-quota.h"
+#include "missing_magic.h"
+#include "quota-util.h"
+#include "stat-util.h"
+#include "user-util.h"
+
+int home_update_quota_btrfs(UserRecord *h, const char *path) {
+        int r;
+
+        assert(h);
+        assert(path);
+
+        if (h->disk_size == UINT64_MAX)
+                return 0;
+
+        /* If the user wants quota, enable it */
+        r = btrfs_quota_enable(path, true);
+        if (r == -ENOTTY)
+                return log_error_errno(r, "No btrfs quota support on subvolume %s.", path);
+        if (r < 0)
+                return log_error_errno(r, "Failed to enable btrfs quota support on %s.", path);
+
+        r = btrfs_qgroup_set_limit(path, 0, h->disk_size);
+        if (r < 0)
+                return log_error_errno(r, "Faled to set disk quota on subvolume %s: %m", path);
+
+        log_info("Set btrfs quota.");
+
+        return 0;
+}
+
+int home_update_quota_classic(UserRecord *h, const char *path) {
+        struct dqblk req;
+        dev_t devno;
+        int r;
+
+        assert(h);
+        assert(uid_is_valid(h->uid));
+        assert(path);
+
+        if (h->disk_size == UINT64_MAX)
+                return 0;
+
+        r = get_block_device(path, &devno);
+        if (r < 0)
+                return log_error_errno(r, "Failed to determine block device of %s: %m", path);
+        if (devno == 0)
+                return log_error_errno(SYNTHETIC_ERRNO(ENODEV), "File system %s not backed by a block device.", path);
+
+        r = quotactl_devno(QCMD_FIXED(Q_GETQUOTA, USRQUOTA), devno, h->uid, &req);
+        if (r < 0) {
+                if (ERRNO_IS_NOT_SUPPORTED(r))
+                        return log_error_errno(r, "No UID quota support on %s.", path);
+
+                if (r != -ESRCH)
+                        return log_error_errno(r, "Failed to query disk quota for UID " UID_FMT ": %m", h->uid);
+
+                zero(req);
+        } else {
+                /* Shortcut things if everything is set up properly already */
+                if (FLAGS_SET(req.dqb_valid, QIF_BLIMITS) && h->disk_size / QIF_DQBLKSIZE == req.dqb_bhardlimit) {
+                        log_info("Configured quota already matches the intended setting, not updating quota.");
+                        return 0;
+                }
+        }
+
+        req.dqb_valid = QIF_BLIMITS;
+        req.dqb_bsoftlimit = req.dqb_bhardlimit = h->disk_size / QIF_DQBLKSIZE;
+
+        r = quotactl_devno(QCMD_FIXED(Q_SETQUOTA, USRQUOTA), devno, h->uid, &req);
+        if (r < 0) {
+                if (r == -ESRCH)
+                        return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), "UID quota not available on %s.", path);
+
+                return log_error_errno(r, "Failed to set disk quota for UID " UID_FMT ": %m", h->uid);
+        }
+
+        log_info("Updated per-UID quota.");
+
+        return 0;
+}
+
+int home_update_quota_auto(UserRecord *h, const char *path) {
+        struct statfs sfs;
+        int r;
+
+        assert(h);
+
+        if (h->disk_size == UINT64_MAX)
+                return 0;
+
+        if (!path) {
+                path = user_record_image_path(h);
+                if (!path)
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Home record lacks image path.");
+        }
+
+        if (statfs(path, &sfs) < 0)
+                return log_error_errno(errno, "Failed to statfs() file system: %m");
+
+        if (is_fs_type(&sfs, XFS_SB_MAGIC) ||
+            is_fs_type(&sfs, EXT4_SUPER_MAGIC))
+                return home_update_quota_classic(h, path);
+
+        if (is_fs_type(&sfs, BTRFS_SUPER_MAGIC)) {
+
+                r = btrfs_is_subvol(path);
+                if (r < 0)
+                        return log_error_errno(errno, "Failed to test if %s is a subvolume: %m", path);
+                if (r == 0)
+                        return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), "Directory %s is not a subvolume, cannot apply quota.", path);
+
+                return home_update_quota_btrfs(h, path);
+        }
+
+        return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), "Type of directory %s not known, cannot apply quota.", path);
+}
diff --git a/src/home/homework-quota.h b/src/home/homework-quota.h
new file mode 100644 (file)
index 0000000..e6cc16d
--- /dev/null
@@ -0,0 +1,8 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include "user-record.h"
+
+int home_update_quota_btrfs(UserRecord *h, const char *path);
+int home_update_quota_classic(UserRecord *h, const char *path);
+int home_update_quota_auto(UserRecord *h, const char *path);
diff --git a/src/home/homework.c b/src/home/homework.c
new file mode 100644 (file)
index 0000000..ecf07ff
--- /dev/null
@@ -0,0 +1,1482 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include <stddef.h>
+#include <sys/mount.h>
+
+#include "chown-recursive.h"
+#include "copy.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "home-util.h"
+#include "homework-cifs.h"
+#include "homework-directory.h"
+#include "homework-fscrypt.h"
+#include "homework-luks.h"
+#include "homework-mount.h"
+#include "homework-pkcs11.h"
+#include "homework.h"
+#include "main-func.h"
+#include "memory-util.h"
+#include "missing_magic.h"
+#include "mount-util.h"
+#include "path-util.h"
+#include "pkcs11-util.h"
+#include "rm-rf.h"
+#include "stat-util.h"
+#include "strv.h"
+#include "tmpfile-util.h"
+#include "user-util.h"
+#include "virt.h"
+
+/* Make sure a bad password always results in a 3s delay, no matter what */
+#define BAD_PASSWORD_DELAY_USEC (3 * USEC_PER_SEC)
+
+int user_record_authenticate(
+                UserRecord *h,
+                UserRecord *secret,
+                char ***pkcs11_decrypted_passwords) {
+
+        bool need_password = false, need_token = false, need_pin = false, need_protected_authentication_path_permitted = false,
+                pin_locked = false, pin_incorrect = false, pin_incorrect_few_tries_left = false, pin_incorrect_one_try_left = false;
+        size_t n;
+        int r;
+
+        assert(h);
+        assert(secret);
+
+        /* Tries to authenticate a user record with the supplied secrets. i.e. checks whether at least one
+         * supplied plaintext passwords matches a hashed password field of the user record. Or if a
+         * configured PKCS#11 token is around and can unlock the record.
+         *
+         * Note that the pkcs11_decrypted_passwords parameter is both an input and and output parameter: it
+         * is a list of configured, decrypted PKCS#11 passwords. We typically have to call this function
+         * multiple times over the course of an operation (think: on login we authenticate the host user
+         * record, the record embedded in the LUKS record and the one embedded in $HOME). Hence we keep a
+         * list of passwords we already decrypted, so that we don't have to do the (slow an potentially
+         * interactive) PKCS#11 dance for the relevant token again and again. */
+
+        /* First, let's see if the supplied plain-text passwords work? */
+        r = user_record_test_secret(h, secret);
+        if (r == -ENOKEY) {
+                log_info_errno(r, "None of the supplied plaintext passwords unlocks the user record's hashed passwords.");
+                need_password = true;
+        } else if (r == -ENXIO)
+                log_debug_errno(r, "User record has no hashed passwords, plaintext passwords not tested.");
+        else if (r < 0)
+                return log_error_errno(r, "Failed to validate password of record: %m");
+        else {
+                log_info("Provided password unlocks user record.");
+                return 0;
+        }
+
+        /* Second, let's see if any of the PKCS#11 security tokens are plugged in and help us */
+        for (n = 0; n < h->n_pkcs11_encrypted_key; n++) {
+#if HAVE_P11KIT
+                _cleanup_(pkcs11_callback_data_release) struct pkcs11_callback_data data = {
+                        .user_record = h,
+                        .secret = secret,
+                        .encrypted_key = h->pkcs11_encrypted_key + n,
+                };
+                char **pp;
+
+                /* See if any of the previously calculated passwords work */
+                STRV_FOREACH(pp, *pkcs11_decrypted_passwords) {
+                        r = test_password_one(data.encrypted_key->hashed_password, *pp);
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to check supplied PKCS#11 password: %m");
+                        if (r > 0) {
+                                log_info("Previously acquired PKCS#11 password unlocks user record.");
+                                return 0;
+                        }
+                }
+
+                r = pkcs11_find_token(data.encrypted_key->uri, pkcs11_callback, &data);
+                switch (r) {
+                case -EAGAIN:
+                        need_token = true;
+                        break;
+                case -ENOANO:
+                        need_pin = true;
+                        break;
+                case -ERFKILL:
+                        need_protected_authentication_path_permitted = true;
+                        break;
+                case -EOWNERDEAD:
+                        pin_locked = true;
+                        break;
+                case -ENOLCK:
+                        pin_incorrect = true;
+                        break;
+                case -ETOOMANYREFS:
+                        pin_incorrect = pin_incorrect_few_tries_left = true;
+                        break;
+                case -EUCLEAN:
+                        pin_incorrect = pin_incorrect_few_tries_left = pin_incorrect_one_try_left = true;
+                        break;
+                default:
+                        if (r < 0)
+                                return r;
+
+                        r = test_password_one(data.encrypted_key->hashed_password, data.decrypted_password);
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to test PKCS#11 password: %m");
+                        if (r == 0)
+                                return log_error_errno(SYNTHETIC_ERRNO(EPERM), "Configured PKCS#11 security token %s does not decrypt encrypted key correctly.", data.encrypted_key->uri);
+
+                        log_info("Decrypted password from PKCS#11 security token %s unlocks user record.", data.encrypted_key->uri);
+
+                        r = strv_extend(pkcs11_decrypted_passwords, data.decrypted_password);
+                        if (r < 0)
+                                return log_oom();
+
+                        return 0;
+                }
+#else
+                need_token = true;
+                break;
+#endif
+        }
+
+        /* Ordered by "relevance", i.e. the most "important" or "interesting" error condition is returned. */
+        if (pin_incorrect_one_try_left)
+                return -EUCLEAN;
+        if (pin_incorrect_few_tries_left)
+                return -ETOOMANYREFS;
+        if (pin_incorrect)
+                return -ENOLCK;
+        if (pin_locked)
+                return -EOWNERDEAD;
+        if (need_protected_authentication_path_permitted)
+                return -ERFKILL;
+        if (need_pin)
+                return -ENOANO;
+        if (need_token)
+                return -EBADSLT;
+        if (need_password)
+                return -ENOKEY;
+
+        /* Hmm, this means neither PCKS#11 nor classic hashed passwords were supplied, we cannot authenticate this reasonably */
+        return log_debug_errno(SYNTHETIC_ERRNO(EKEYREVOKED), "No hashed passwords and no PKCS#11 tokens defined, cannot authenticate user record.");
+}
+
+int home_setup_undo(HomeSetup *setup) {
+        int r = 0, q;
+
+        assert(setup);
+
+        setup->root_fd = safe_close(setup->root_fd);
+
+        if (setup->undo_mount) {
+                q = umount_verbose("/run/systemd/user-home-mount");
+                if (q < 0)
+                        r = q;
+        }
+
+        if (setup->undo_dm && setup->crypt_device && setup->dm_name) {
+                q = crypt_deactivate(setup->crypt_device, setup->dm_name);
+                if (q < 0)
+                        r = q;
+        }
+
+        setup->undo_mount = false;
+        setup->undo_dm = false;
+
+        setup->dm_name = mfree(setup->dm_name);
+        setup->dm_node = mfree(setup->dm_node);
+
+        setup->loop = loop_device_unref(setup->loop);
+        crypt_free(setup->crypt_device);
+        setup->crypt_device = NULL;
+
+        explicit_bzero_safe(setup->volume_key, setup->volume_key_size);
+        setup->volume_key = mfree(setup->volume_key);
+        setup->volume_key_size = 0;
+
+        return r;
+}
+
+int home_prepare(
+                UserRecord *h,
+                bool already_activated,
+                char ***pkcs11_decrypted_passwords,
+                HomeSetup *setup,
+                UserRecord **ret_header_home) {
+
+        int r;
+
+        assert(h);
+        assert(setup);
+        assert(!setup->loop);
+        assert(!setup->crypt_device);
+        assert(setup->root_fd < 0);
+        assert(!setup->undo_dm);
+        assert(!setup->undo_mount);
+
+        /* Makes a home directory accessible (through the root_fd file descriptor, not by path!). */
+
+        switch (user_record_storage(h)) {
+
+        case USER_LUKS:
+                return home_prepare_luks(h, already_activated, NULL, pkcs11_decrypted_passwords, setup, ret_header_home);
+
+        case USER_SUBVOLUME:
+        case USER_DIRECTORY:
+                r = home_prepare_directory(h, already_activated, setup);
+                break;
+
+        case USER_FSCRYPT:
+                r = home_prepare_fscrypt(h, already_activated, pkcs11_decrypted_passwords, setup);
+                break;
+
+        case USER_CIFS:
+                r = home_prepare_cifs(h, already_activated, setup);
+                break;
+
+        default:
+                return log_error_errno(SYNTHETIC_ERRNO(ENOLINK), "Processing home directories of type '%s' currently not supported.", user_storage_to_string(user_record_storage(h)));
+        }
+
+        if (r < 0)
+                return r;
+
+        if (ret_header_home)
+                *ret_header_home = NULL;
+
+        return r;
+}
+
+int home_sync_and_statfs(int root_fd, struct statfs *ret) {
+        assert(root_fd >= 0);
+
+        /* Let's sync this to disk, so that the disk space reported by fstatfs() below is accurate (for file
+         * systems such as btrfs where this is determined lazily). */
+
+        if (syncfs(root_fd) < 0)
+                return log_error_errno(errno, "Failed to synchronize file system: %m");
+
+        if (ret)
+                if (fstatfs(root_fd, ret) < 0)
+                        return log_error_errno(errno, "Failed to statfs() file system: %m");
+
+        log_info("Synchronized disk.");
+
+        return 0;
+}
+
+static int read_identity_file(int root_fd, JsonVariant **ret) {
+        _cleanup_(fclosep) FILE *identity_file = NULL;
+        _cleanup_close_ int identity_fd = -1;
+        unsigned line, column;
+        int r;
+
+        assert(root_fd >= 0);
+        assert(ret);
+
+        identity_fd = openat(root_fd, ".identity", O_RDONLY|O_CLOEXEC|O_NOCTTY|O_NOFOLLOW|O_NONBLOCK);
+        if (identity_fd < 0)
+                return log_error_errno(errno, "Failed to open .identity file in home directory: %m");
+
+        r = fd_verify_regular(identity_fd);
+        if (r < 0)
+                return log_error_errno(r, "Embedded identity file is not a regular file, refusing: %m");
+
+        identity_file = fdopen(identity_fd, "r");
+        if (!identity_file)
+                return log_oom();
+
+        identity_fd = -1;
+
+        r = json_parse_file(identity_file, ".identity", JSON_PARSE_SENSITIVE, ret, &line, &column);
+        if (r < 0)
+                return log_error_errno(r, "[.identity:%u:%u] Failed to parse JSON data: %m", line, column);
+
+        log_info("Read embedded .identity file.");
+
+        return 0;
+}
+
+static int write_identity_file(int root_fd, JsonVariant *v, uid_t uid) {
+        _cleanup_(json_variant_unrefp) JsonVariant *normalized = NULL;
+        _cleanup_(fclosep) FILE *identity_file = NULL;
+        _cleanup_close_ int identity_fd = -1;
+        _cleanup_free_ char *fn = NULL;
+        int r;
+
+        assert(root_fd >= 0);
+        assert(v);
+
+        normalized = json_variant_ref(v);
+
+        r = json_variant_normalize(&normalized);
+        if (r < 0)
+                log_warning_errno(r, "Failed to normalize user record, ignoring: %m");
+
+        r = tempfn_random(".identity", NULL, &fn);
+        if (r < 0)
+                return r;
+
+        identity_fd = openat(root_fd, fn, O_WRONLY|O_CREAT|O_EXCL|O_CLOEXEC|O_NOCTTY|O_NOFOLLOW, 0600);
+        if (identity_fd < 0)
+                return log_error_errno(errno, "Failed to create .identity file in home directory: %m");
+
+        identity_file = fdopen(identity_fd, "w");
+        if (!identity_file) {
+                r = log_oom();
+                goto fail;
+        }
+
+        identity_fd = -1;
+
+        json_variant_dump(normalized, JSON_FORMAT_PRETTY, identity_file, NULL);
+
+        r = fflush_and_check(identity_file);
+        if (r < 0) {
+                log_error_errno(r, "Failed to write .identity file: %m");
+                goto fail;
+        }
+
+        if (fchown(fileno(identity_file), uid, uid) < 0) {
+                log_error_errno(r, "Failed to change ownership of identity file: %m");
+                goto fail;
+        }
+
+        if (renameat(root_fd, fn, root_fd, ".identity") < 0) {
+                r = log_error_errno(errno, "Failed to move identity file into place: %m");
+                goto fail;
+        }
+
+        log_info("Wrote embedded .identity file.");
+
+        return 0;
+
+fail:
+        (void) unlinkat(root_fd, fn, 0);
+        return r;
+}
+
+int home_load_embedded_identity(
+                UserRecord *h,
+                int root_fd,
+                UserRecord *header_home,
+                UserReconcileMode mode,
+                char ***pkcs11_decrypted_passwords,
+                UserRecord **ret_embedded_home,
+                UserRecord **ret_new_home) {
+
+        _cleanup_(user_record_unrefp) UserRecord *embedded_home = NULL, *intermediate_home = NULL, *new_home = NULL;
+        _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+        int r;
+
+        assert(h);
+        assert(root_fd >= 0);
+
+        r = read_identity_file(root_fd, &v);
+        if (r < 0)
+                return r;
+
+        embedded_home = user_record_new();
+        if (!embedded_home)
+                return log_oom();
+
+        r = user_record_load(embedded_home, v, USER_RECORD_LOAD_EMBEDDED);
+        if (r < 0)
+                return r;
+
+        if (!user_record_compatible(h, embedded_home))
+                return log_error_errno(SYNTHETIC_ERRNO(EREMCHG), "Hmbedded home record not compatible with host record, refusing.");
+
+        /* Insist that credentials the user supplies also unlocks any embedded records. */
+        r = user_record_authenticate(embedded_home, h, pkcs11_decrypted_passwords);
+        if (r < 0)
+                return r;
+
+        /* At this point we have three records to deal with:
+         *
+         *      · The record we got passed from the host
+         *      · The record included in the LUKS header (only if LUKS is used)
+         *      · The record in the home directory itself (~.identity)
+         *
+         *  Now we have to reconcile all three, and let the newest one win. */
+
+        if (header_home) {
+                /* Note we relax the requirements here. Instead of insisting that the host record is strictly
+                 * newer, let's also be OK if its equally new. If it is, we'll however insist that the
+                 * embedded record must be newer, so that we update at least one of the two. */
+
+                r = user_record_reconcile(h, header_home, mode == USER_RECONCILE_REQUIRE_NEWER ? USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL : mode, &intermediate_home);
+                if (r == -EREMCHG) /* this was supposed to be checked earlier already, but let's check this again */
+                        return log_error_errno(r, "Identity stored on host and in header don't match, refusing.");
+                if (r == -ESTALE)
+                        return log_error_errno(r, "Embedded identity record is newer than supplied record, refusing.");
+                if (r < 0)
+                        return log_error_errno(r, "Failed to reconcile host and header identities: %m");
+                if (r == USER_RECONCILE_EMBEDDED_WON)
+                        log_info("Reconciling header user identity completed (header version was newer).");
+                else if (r == USER_RECONCILE_HOST_WON) {
+                        log_info("Reconciling header user identity completed (host version was newer).");
+
+                        if (mode == USER_RECONCILE_REQUIRE_NEWER) /* Host version is newer than the header
+                                                                   * version, hence we'll update
+                                                                   * something. This means we can relax the
+                                                                   * requirements on the embedded
+                                                                   * identity. */
+                                mode = USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL;
+                } else {
+                        assert(r == USER_RECONCILE_IDENTICAL);
+                        log_info("Reconciling user identities completed (host and header version were identical).");
+                }
+
+                h = intermediate_home;
+        }
+
+        r = user_record_reconcile(h, embedded_home, mode, &new_home);
+        if (r == -EREMCHG)
+                return log_error_errno(r, "Identity stored on host and in home don't match, refusing.");
+        if (r == -ESTALE)
+                return log_error_errno(r, "Embedded identity record is equally new or newer than supplied record, refusing.");
+        if (r < 0)
+                return log_error_errno(r, "Failed to reconcile host and embedded identities: %m");
+        if (r == USER_RECONCILE_EMBEDDED_WON)
+                log_info("Reconciling embedded user identity completed (embedded version was newer).");
+        else if (r == USER_RECONCILE_HOST_WON)
+                log_info("Reconciling embedded user identity completed (host version was newer).");
+        else {
+                assert(r == USER_RECONCILE_IDENTICAL);
+                log_info("Reconciling embedded user identity completed (host and embedded version were identical).");
+        }
+
+        if (ret_embedded_home)
+                *ret_embedded_home = TAKE_PTR(embedded_home);
+
+        if (ret_new_home)
+                *ret_new_home = TAKE_PTR(new_home);
+
+        return 0;
+}
+
+int home_store_embedded_identity(UserRecord *h, int root_fd, uid_t uid, UserRecord *old_home) {
+        _cleanup_(user_record_unrefp) UserRecord *embedded = NULL;
+        int r;
+
+        assert(h);
+        assert(root_fd >= 0);
+        assert(uid_is_valid(uid));
+
+        r = user_record_clone(h, USER_RECORD_EXTRACT_EMBEDDED, &embedded);
+        if (r < 0)
+                return log_error_errno(r, "Failed to determine new embedded record: %m");
+
+        if (old_home && user_record_equal(old_home, embedded)) {
+                log_debug("Not updating embedded home record.");
+                return 0;
+        }
+
+        /* The identity has changed, let's update it in the image */
+        r = write_identity_file(root_fd, embedded->json, h->uid);
+        if (r < 0)
+                return r;
+
+        return 1;
+}
+
+static const char *file_system_type_fd(int fd) {
+        struct statfs sfs;
+
+        assert(fd >= 0);
+
+        if (fstatfs(fd, &sfs) < 0) {
+                log_debug_errno(errno, "Failed to statfs(): %m");
+                return NULL;
+        }
+
+        if (is_fs_type(&sfs, XFS_SB_MAGIC))
+                return "xfs";
+        if (is_fs_type(&sfs, EXT4_SUPER_MAGIC))
+                return "ext4";
+        if (is_fs_type(&sfs, BTRFS_SUPER_MAGIC))
+                return "btrfs";
+
+        return NULL;
+}
+
+int home_extend_embedded_identity(UserRecord *h, UserRecord *used, HomeSetup *setup) {
+        int r;
+
+        assert(h);
+        assert(used);
+        assert(setup);
+
+        r = user_record_add_binding(
+                        h,
+                        user_record_storage(used),
+                        user_record_image_path(used),
+                        setup->found_partition_uuid,
+                        setup->found_luks_uuid,
+                        setup->found_fs_uuid,
+                        setup->crypt_device ? crypt_get_cipher(setup->crypt_device) : NULL,
+                        setup->crypt_device ? crypt_get_cipher_mode(setup->crypt_device) : NULL,
+                        setup->crypt_device ? luks_volume_key_size_convert(setup->crypt_device) : UINT64_MAX,
+                        file_system_type_fd(setup->root_fd),
+                        user_record_home_directory(used),
+                        used->uid,
+                        (gid_t) used->uid);
+        if (r < 0)
+                return log_error_errno(r, "Failed to update binding in record: %m");
+
+        return 0;
+}
+
+static int chown_recursive_directory(int root_fd, uid_t uid) {
+        int r;
+
+        assert(root_fd >= 0);
+        assert(uid_is_valid(uid));
+
+        r = fd_chown_recursive(root_fd, uid, (gid_t) uid, 0777);
+        if (r < 0)
+                return log_error_errno(r, "Failed to change ownership of files and directories: %m");
+        if (r == 0)
+                log_info("Recursive changing of ownership not necessary, skipped.");
+        else
+                log_info("Recursive changing of ownership completed.");
+
+        return 0;
+}
+
+int home_refresh(
+                UserRecord *h,
+                HomeSetup *setup,
+                UserRecord *header_home,
+                char ***pkcs11_decrypted_passwords,
+                struct statfs *ret_statfs,
+                UserRecord **ret_new_home) {
+
+        _cleanup_(user_record_unrefp) UserRecord *embedded_home = NULL, *new_home = NULL;
+        int r;
+
+        assert(h);
+        assert(setup);
+        assert(ret_new_home);
+
+        /* When activating a home directory, does the identity work: loads the identity from the $HOME
+         * directory, reconciles it with our idea, chown()s everything. */
+
+        r = home_load_embedded_identity(h, setup->root_fd, header_home, USER_RECONCILE_ANY, pkcs11_decrypted_passwords, &embedded_home, &new_home);
+        if (r < 0)
+                return r;
+
+        r = home_store_header_identity_luks(new_home, setup, header_home);
+        if (r < 0)
+                return r;
+
+        r = home_store_embedded_identity(new_home, setup->root_fd, h->uid, embedded_home);
+        if (r < 0)
+                return r;
+
+        r = chown_recursive_directory(setup->root_fd, h->uid);
+        if (r < 0)
+                return r;
+
+        r = home_sync_and_statfs(setup->root_fd, ret_statfs);
+        if (r < 0)
+                return r;
+
+        *ret_new_home = TAKE_PTR(new_home);
+        return 0;
+}
+
+static int home_activate(UserRecord *h, UserRecord **ret_home) {
+        _cleanup_(strv_free_erasep) char **pkcs11_decrypted_passwords = NULL;
+        _cleanup_(user_record_unrefp) UserRecord *new_home = NULL;
+        int r;
+
+        assert(h);
+
+        if (!h->user_name)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record lacks user name, refusing.");
+        if (!uid_is_valid(h->uid))
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record lacks UID, refusing.");
+        if (!IN_SET(user_record_storage(h), USER_LUKS, USER_DIRECTORY, USER_SUBVOLUME, USER_FSCRYPT, USER_CIFS))
+                return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), "Activating home directories of type '%s' currently not supported.", user_storage_to_string(user_record_storage(h)));
+
+        r = user_record_authenticate(h, h, &pkcs11_decrypted_passwords);
+        if (r < 0)
+                return r;
+
+        r = user_record_test_home_directory_and_warn(h);
+        if (r < 0)
+                return r;
+        if (r == USER_TEST_MOUNTED)
+                return log_error_errno(SYNTHETIC_ERRNO(EALREADY), "Home directory %s is already mounted, refusing.", user_record_home_directory(h));
+
+        r = user_record_test_image_path_and_warn(h);
+        if (r < 0)
+                return r;
+        if (r == USER_TEST_ABSENT)
+                return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Image path %s is missing, refusing.", user_record_image_path(h));
+
+        switch (user_record_storage(h)) {
+
+        case USER_LUKS:
+                r = home_activate_luks(h, &pkcs11_decrypted_passwords, &new_home);
+                if (r < 0)
+                        return r;
+
+                break;
+
+        case USER_SUBVOLUME:
+        case USER_DIRECTORY:
+        case USER_FSCRYPT:
+                r = home_activate_directory(h, &pkcs11_decrypted_passwords, &new_home);
+                if (r < 0)
+                        return r;
+
+                break;
+
+        case USER_CIFS:
+                r = home_activate_cifs(h, &pkcs11_decrypted_passwords, &new_home);
+                if (r < 0)
+                        return r;
+
+                break;
+
+        default:
+                assert_not_reached("unexpected type");
+        }
+
+        /* Note that the returned object might either be a reference to an updated version of the existing
+         * home object, or a reference to a newly allocated home object. The caller has to be able to deal
+         * with both, and consider the old object out-of-date. */
+        if (user_record_equal(h, new_home)) {
+                *ret_home = NULL;
+                return 0; /* no identity change */
+        }
+
+        *ret_home = TAKE_PTR(new_home);
+        return 1; /* identity updated */
+}
+
+static int home_deactivate(UserRecord *h, bool force) {
+        bool done = false;
+        int r;
+
+        assert(h);
+
+        if (!h->user_name)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record incomplete, refusing.");
+        if (!IN_SET(user_record_storage(h), USER_LUKS, USER_DIRECTORY, USER_SUBVOLUME, USER_FSCRYPT, USER_CIFS))
+                return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), "Deactivating home directories of type '%s' currently not supported.", user_storage_to_string(user_record_storage(h)));
+
+        r = user_record_test_home_directory_and_warn(h);
+        if (r < 0)
+                return r;
+        if (r == USER_TEST_MOUNTED) {
+                if (umount2(user_record_home_directory(h), UMOUNT_NOFOLLOW | (force ? MNT_FORCE|MNT_DETACH : 0)) < 0)
+                        return log_error_errno(errno, "Failed to unmount %s: %m", user_record_home_directory(h));
+
+                log_info("Unmounting completed.");
+                done = true;
+        } else
+                log_info("Directory %s is already unmounted.", user_record_home_directory(h));
+
+        if (user_record_storage(h) == USER_LUKS) {
+                r = home_deactivate_luks(h);
+                if (r < 0)
+                        return r;
+                if (r > 0)
+                        done = true;
+        }
+
+        if (!done)
+                return log_error_errno(SYNTHETIC_ERRNO(ENOEXEC), "Home is not active.");
+
+        log_info("Everything completed.");
+        return 0;
+}
+
+static int copy_skel(int root_fd, const char *skel) {
+        int r;
+
+        assert(root_fd >= 0);
+
+        r = copy_tree_at(AT_FDCWD, skel, root_fd, ".", UID_INVALID, GID_INVALID, COPY_MERGE|COPY_REPLACE);
+        if (r == -ENOENT) {
+                log_info("Skeleton directory %s missing, ignoring.", skel);
+                return 0;
+        }
+        if (r < 0)
+                return log_error_errno(r, "Failed to copy in %s: %m", skel);
+
+        log_info("Copying in %s completed.", skel);
+        return 0;
+}
+
+static int change_access_mode(int root_fd, mode_t m) {
+        assert(root_fd >= 0);
+
+        if (fchmod(root_fd, m) < 0)
+                return log_error_errno(errno, "Failed to change access mode of top-level directory: %m");
+
+        log_info("Changed top-level directory access mode to 0%o.", m);
+        return 0;
+}
+
+int home_populate(UserRecord *h, int dir_fd) {
+        int r;
+
+        assert(h);
+        assert(dir_fd >= 0);
+
+        r = copy_skel(dir_fd, user_record_skeleton_directory(h));
+        if (r < 0)
+                return r;
+
+        r = home_store_embedded_identity(h, dir_fd, h->uid, NULL);
+        if (r < 0)
+                return r;
+
+        r = chown_recursive_directory(dir_fd, h->uid);
+        if (r < 0)
+                return r;
+
+        r = change_access_mode(dir_fd, user_record_access_mode(h));
+        if (r < 0)
+                return r;
+
+        return 0;
+}
+
+static int user_record_compile_effective_passwords(
+                UserRecord *h,
+                char ***ret_effective_passwords,
+                char ***ret_pkcs11_decrypted_passwords) {
+
+        _cleanup_(strv_free_erasep) char **effective = NULL, **pkcs11_passwords = NULL;
+        size_t n;
+        char **i;
+        int r;
+
+        assert(h);
+
+        /* We insist on at least one classic hashed password to be defined in addition to any PKCS#11 one, as
+         * a safe fallback, but also to simplify the password changing algorithm: there we require providing
+         * the old literal password only (and do not care for the old PKCS#11 token) */
+
+        if (strv_isempty(h->hashed_password))
+                return log_error_errno(EINVAL, "User record has no hashed passwords, refusing.");
+
+        /* Generates the list of plaintext passwords to propagate to LUKS/fscrypt devices, and checks whether
+         * we have a plaintext password for each hashed one. If we are missing one we'll fail, since we
+         * couldn't sync fscrypt/LUKS to the login account properly. */
+
+        STRV_FOREACH(i, h->hashed_password) {
+                bool found = false;
+                char **j;
+
+                log_debug("Looking for plaintext password for: %s", *i);
+
+                /* Let's scan all provided plaintext passwords */
+                STRV_FOREACH(j, h->password) {
+                        r = test_password_one(*i, *j);
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to test plain text password: %m");
+                        if (r > 0) {
+                                if (ret_effective_passwords) {
+                                        r = strv_extend(&effective, *j);
+                                        if (r < 0)
+                                                return log_oom();
+                                }
+
+                                log_debug("Found literal plaintext password.");
+                                found = true;
+                                break;
+                        }
+                }
+
+                if (!found)
+                        return log_error_errno(SYNTHETIC_ERRNO(ENOKEY), "Missing plaintext password for defined hashed password");
+        }
+
+        for (n = 0; n < h->n_pkcs11_encrypted_key; n++) {
+#if HAVE_P11KIT
+                _cleanup_(pkcs11_callback_data_release) struct pkcs11_callback_data data = {
+                        .user_record = h,
+                        .secret = h,
+                        .encrypted_key = h->pkcs11_encrypted_key + n,
+                };
+
+                r = pkcs11_find_token(data.encrypted_key->uri, pkcs11_callback, &data);
+                if (r == -EAGAIN)
+                        return -EBADSLT;
+                if (r < 0)
+                        return r;
+
+                r = test_password_one(data.encrypted_key->hashed_password, data.decrypted_password);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to test PKCS#11 password: %m");
+                if (r == 0)
+                        return log_error_errno(SYNTHETIC_ERRNO(EPERM), "Decrypted password from token is not correct, refusing.");
+
+                if (ret_effective_passwords) {
+                        r = strv_extend(&effective, data.decrypted_password);
+                        if (r < 0)
+                                return log_oom();
+                }
+
+                if (ret_pkcs11_decrypted_passwords) {
+                        r = strv_extend(&pkcs11_passwords, data.decrypted_password);
+                        if (r < 0)
+                                return log_oom();
+                }
+#else
+                return -EBADSLT;
+#endif
+        }
+
+        if (ret_effective_passwords)
+                *ret_effective_passwords = TAKE_PTR(effective);
+        if (ret_pkcs11_decrypted_passwords)
+                *ret_pkcs11_decrypted_passwords = TAKE_PTR(pkcs11_passwords);
+
+        return 0;
+}
+
+static int home_create(UserRecord *h, UserRecord **ret_home) {
+        _cleanup_(strv_free_erasep) char **effective_passwords = NULL, **pkcs11_decrypted_passwords = NULL;
+        _cleanup_(user_record_unrefp) UserRecord *new_home = NULL;
+        int r;
+
+        assert(h);
+
+        if (!h->user_name)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record lacks name, refusing.");
+        if (!uid_is_valid(h->uid))
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record lacks UID, refusing.");
+
+        r = user_record_compile_effective_passwords(h, &effective_passwords, &pkcs11_decrypted_passwords);
+        if (r < 0)
+                return r;
+
+        r = user_record_test_home_directory_and_warn(h);
+        if (r < 0)
+                return r;
+        if (r != USER_TEST_ABSENT)
+                return log_error_errno(SYNTHETIC_ERRNO(EEXIST), "Home directory %s already exists, refusing.", user_record_home_directory(h));
+
+        /* When the user didn't specify the storage type to use, fix it to be LUKS -- unless we run in a
+         * container where loopback devices and LUKS/DM are not available. Note that we typically default to
+         * the assumption of "classic" storage for most operations. However, if we create a new home, then
+         * let's user LUKS if nothing is specified. */
+        if (h->storage < 0) {
+                UserStorage new_storage;
+
+                r = detect_container();
+                if (r < 0)
+                        return log_error_errno(r, "Failed to determine whether we are in a container: %m");
+                if (r > 0) {
+                        new_storage = USER_DIRECTORY;
+
+                        r = path_is_fs_type("/home", BTRFS_SUPER_MAGIC);
+                        if (r < 0)
+                                log_debug_errno(r, "Failed to determine file system of /home, ignoring: %m");
+
+                        new_storage = r > 0 ? USER_SUBVOLUME : USER_DIRECTORY;
+                } else
+                        new_storage = USER_LUKS;
+
+                r = user_record_add_binding(
+                                h,
+                                new_storage,
+                                NULL,
+                                SD_ID128_NULL,
+                                SD_ID128_NULL,
+                                SD_ID128_NULL,
+                                NULL,
+                                NULL,
+                                UINT64_MAX,
+                                NULL,
+                                NULL,
+                                UID_INVALID,
+                                GID_INVALID);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to change storage type to LUKS: %m");
+
+                if (!h->image_path_auto) {
+                        h->image_path_auto = strjoin("/home/", user_record_user_name_and_realm(h), new_storage == USER_LUKS ? ".home" : ".homedir");
+                        if (!h->image_path_auto)
+                                return log_oom();
+                }
+        }
+
+        r = user_record_test_image_path_and_warn(h);
+        if (r < 0)
+                return r;
+        if (!IN_SET(r, USER_TEST_ABSENT, USER_TEST_UNDEFINED, USER_TEST_MAYBE))
+                return log_error_errno(SYNTHETIC_ERRNO(EEXIST), "Image path %s already exists, refusing.", user_record_image_path(h));
+
+        switch (user_record_storage(h)) {
+
+        case USER_LUKS:
+                r = home_create_luks(h, pkcs11_decrypted_passwords, effective_passwords, &new_home);
+                break;
+
+        case USER_DIRECTORY:
+        case USER_SUBVOLUME:
+                r = home_create_directory_or_subvolume(h, &new_home);
+                break;
+
+        case USER_FSCRYPT:
+                r = home_create_fscrypt(h, effective_passwords, &new_home);
+                break;
+
+        case USER_CIFS:
+                r = home_create_cifs(h, &new_home);
+                break;
+
+        default:
+                return log_error_errno(SYNTHETIC_ERRNO(ENOTTY),
+                                       "Creating home directories of type '%s' currently not supported.", user_storage_to_string(user_record_storage(h)));
+        }
+        if (r < 0)
+                return r;
+
+        if (user_record_equal(h, new_home)) {
+                *ret_home = NULL;
+                return 0;
+        }
+
+        *ret_home = TAKE_PTR(new_home);
+        return 1;
+}
+
+static int home_remove(UserRecord *h) {
+        bool deleted = false;
+        const char *ip, *hd;
+        int r;
+
+        assert(h);
+
+        if (!h->user_name)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record lacks user name, refusing.");
+        if (!IN_SET(user_record_storage(h), USER_LUKS, USER_DIRECTORY, USER_SUBVOLUME, USER_FSCRYPT, USER_CIFS))
+                return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), "Removing home directories of type '%s' currently not supported.", user_storage_to_string(user_record_storage(h)));
+
+        hd = user_record_home_directory(h);
+
+        r = user_record_test_home_directory_and_warn(h);
+        if (r < 0)
+                return r;
+        if (r == USER_TEST_MOUNTED)
+                return log_error_errno(SYNTHETIC_ERRNO(EBUSY), "Directory %s is still mounted, refusing.", hd);
+
+        assert(hd);
+
+        r = user_record_test_image_path_and_warn(h);
+        if (r < 0)
+                return r;
+
+        ip = user_record_image_path(h);
+
+        switch (user_record_storage(h)) {
+
+        case USER_LUKS: {
+                struct stat st;
+
+                assert(ip);
+
+                if (stat(ip, &st) < 0) {
+                        if (errno != -ENOENT)
+                                return log_error_errno(errno, "Failed to stat %s: %m", ip);
+
+                } else {
+                        if (S_ISREG(st.st_mode)) {
+                                if (unlink(ip) < 0) {
+                                        if (errno != ENOENT)
+                                                return log_error_errno(errno, "Failed to remove %s: %m", ip);
+                                } else
+                                        deleted = true;
+
+                        } else if (S_ISBLK(st.st_mode))
+                                log_info("Not removing file system on block device %s.", ip);
+                        else
+                                return log_error_errno(SYNTHETIC_ERRNO(ENOTBLK), "Image file %s is neither block device, nor regular, refusing removal.", ip);
+                }
+
+                break;
+        }
+
+        case USER_SUBVOLUME:
+        case USER_DIRECTORY:
+        case USER_FSCRYPT:
+                assert(ip);
+
+                r = rm_rf(ip, REMOVE_ROOT|REMOVE_PHYSICAL|REMOVE_SUBVOLUME);
+                if (r < 0) {
+                        if (r != -ENOENT)
+                                return log_warning_errno(r, "Failed to remove %s: %m", ip);
+                } else
+                        deleted = true;
+
+                /* If the image path and the home directory are the same invalidate the home directory, so
+                 * that we don't remove it anymore */
+                if (path_equal(ip, hd))
+                        hd = NULL;
+
+                break;
+
+        case USER_CIFS:
+                /* Nothing else to do here: we won't remove remote stuff. */
+                log_info("Not removing home directory on remote server.");
+                break;
+
+        default:
+                assert_not_reached("unknown storage type");
+        }
+
+        if (hd) {
+                if (rmdir(hd) < 0) {
+                        if (errno != ENOENT)
+                                return log_error_errno(errno, "Failed to remove %s, ignoring: %m", hd);
+                } else
+                        deleted = true;
+        }
+
+        if (deleted)
+                log_info("Everything completed.");
+        else {
+                log_notice("Nothing to remove.");
+                return -EALREADY;
+        }
+
+        return 0;
+}
+
+static int home_validate_update(UserRecord *h, HomeSetup *setup) {
+        bool has_mount = false;
+        int r;
+
+        assert(h);
+        assert(setup);
+
+        if (!h->user_name)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record lacks user name, refusing.");
+        if (!uid_is_valid(h->uid))
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record lacks UID, refusing.");
+        if (!IN_SET(user_record_storage(h), USER_LUKS, USER_DIRECTORY, USER_SUBVOLUME, USER_FSCRYPT, USER_CIFS))
+                return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), "Processing home directories of type '%s' currently not supported.", user_storage_to_string(user_record_storage(h)));
+
+        r = user_record_test_home_directory_and_warn(h);
+        if (r < 0)
+                return r;
+
+        has_mount = r == USER_TEST_MOUNTED;
+
+        r = user_record_test_image_path_and_warn(h);
+        if (r < 0)
+                return r;
+        if (r == USER_TEST_ABSENT)
+                return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Image path %s does not exist", user_record_image_path(h));
+
+        switch (user_record_storage(h)) {
+
+        case USER_DIRECTORY:
+        case USER_SUBVOLUME:
+        case USER_FSCRYPT:
+        case USER_CIFS:
+                break;
+
+        case USER_LUKS: {
+                r = home_validate_update_luks(h, setup);
+                if (r < 0)
+                        return r;
+                if ((r > 0) != has_mount)
+                        return log_error_errno(SYNTHETIC_ERRNO(EBUSY), "Home mount incompletely set up.");
+
+                break;
+        }
+
+        default:
+                assert_not_reached("unexpected storage type");
+        }
+
+        return has_mount; /* return true if the home record is already active */
+}
+
+static int home_update(UserRecord *h, UserRecord **ret) {
+        _cleanup_(user_record_unrefp) UserRecord *new_home = NULL, *header_home = NULL, *embedded_home = NULL;
+        _cleanup_(strv_free_erasep) char **pkcs11_decrypted_passwords = NULL;
+        _cleanup_(home_setup_undo) HomeSetup setup = HOME_SETUP_INIT;
+        bool already_activated = false;
+        int r;
+
+        assert(h);
+        assert(ret);
+
+        r = user_record_authenticate(h, h, &pkcs11_decrypted_passwords);
+        if (r < 0)
+                return r;
+
+        r = home_validate_update(h, &setup);
+        if (r < 0)
+                return r;
+
+        already_activated = r > 0;
+
+        r = home_prepare(h, already_activated, &pkcs11_decrypted_passwords, &setup, &header_home);
+        if (r < 0)
+                return r;
+
+        r = home_load_embedded_identity(h, setup.root_fd, header_home, USER_RECONCILE_REQUIRE_NEWER, &pkcs11_decrypted_passwords, &embedded_home, &new_home);
+        if (r < 0)
+                return r;
+
+        r = home_store_header_identity_luks(new_home, &setup, header_home);
+        if (r < 0)
+                return r;
+
+        r = home_store_embedded_identity(new_home, setup.root_fd, h->uid, embedded_home);
+        if (r < 0)
+                return r;
+
+        r = home_extend_embedded_identity(new_home, h, &setup);
+        if (r < 0)
+                return r;
+
+        r = home_sync_and_statfs(setup.root_fd, NULL);
+        if (r < 0)
+                return r;
+
+        r = home_setup_undo(&setup);
+        if (r < 0)
+                return r;
+
+        log_info("Everything completed.");
+
+        *ret = TAKE_PTR(new_home);
+        return 0;
+}
+
+static int home_resize(UserRecord *h, UserRecord **ret) {
+        _cleanup_(home_setup_undo) HomeSetup setup = HOME_SETUP_INIT;
+        _cleanup_(strv_free_erasep) char **pkcs11_decrypted_passwords = NULL;
+        bool already_activated = false;
+        int r;
+
+        assert(h);
+        assert(ret);
+
+        if (h->disk_size == UINT64_MAX)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No target size specified, refusing.");
+
+        r = user_record_authenticate(h, h, &pkcs11_decrypted_passwords);
+        if (r < 0)
+                return r;
+
+        r = home_validate_update(h, &setup);
+        if (r < 0)
+                return r;
+
+        already_activated = r > 0;
+
+        switch (user_record_storage(h)) {
+
+        case USER_LUKS:
+                return home_resize_luks(h, already_activated, &pkcs11_decrypted_passwords, &setup, ret);
+
+        case USER_DIRECTORY:
+        case USER_SUBVOLUME:
+        case USER_FSCRYPT:
+                return home_resize_directory(h, already_activated, &pkcs11_decrypted_passwords, &setup, ret);
+
+        default:
+                return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), "Resizing home directories of type '%s' currently not supported.", user_storage_to_string(user_record_storage(h)));
+        }
+}
+
+static int home_passwd(UserRecord *h, UserRecord **ret_home) {
+        _cleanup_(user_record_unrefp) UserRecord *header_home = NULL, *embedded_home = NULL, *new_home = NULL;
+        _cleanup_(strv_free_erasep) char **effective_passwords = NULL, **pkcs11_decrypted_passwords = NULL;
+        _cleanup_(home_setup_undo) HomeSetup setup = HOME_SETUP_INIT;
+        bool already_activated = false;
+        int r;
+
+        assert(h);
+        assert(ret_home);
+
+        if (!IN_SET(user_record_storage(h), USER_LUKS, USER_DIRECTORY, USER_SUBVOLUME, USER_FSCRYPT))
+                return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), "Changing password of home directories of type '%s' currently not supported.", user_storage_to_string(user_record_storage(h)));
+
+        r = user_record_compile_effective_passwords(h, &effective_passwords, &pkcs11_decrypted_passwords);
+        if (r < 0)
+                return r;
+
+        r = home_validate_update(h, &setup);
+        if (r < 0)
+                return r;
+
+        already_activated = r > 0;
+
+        r = home_prepare(h, already_activated, &pkcs11_decrypted_passwords, &setup, &header_home);
+        if (r < 0)
+                return r;
+
+        r = home_load_embedded_identity(h, setup.root_fd, header_home, USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL, &pkcs11_decrypted_passwords, &embedded_home, &new_home);
+        if (r < 0)
+                return r;
+
+        switch (user_record_storage(h)) {
+
+        case USER_LUKS:
+                r = home_passwd_luks(h, &setup, pkcs11_decrypted_passwords, effective_passwords);
+                if (r < 0)
+                        return r;
+                break;
+
+        case USER_FSCRYPT:
+                r = home_passwd_fscrypt(h, &setup, pkcs11_decrypted_passwords, effective_passwords);
+                if (r < 0)
+                        return r;
+                break;
+
+        default:
+                break;
+        }
+
+        r = home_store_header_identity_luks(new_home, &setup, header_home);
+        if (r < 0)
+                return r;
+
+        r = home_store_embedded_identity(new_home, setup.root_fd, h->uid, embedded_home);
+        if (r < 0)
+                return r;
+
+        r = home_extend_embedded_identity(new_home, h, &setup);
+        if (r < 0)
+                return r;
+
+        r = home_sync_and_statfs(setup.root_fd, NULL);
+        if (r < 0)
+                return r;
+
+        r = home_setup_undo(&setup);
+        if (r < 0)
+                return r;
+
+        log_info("Everything completed.");
+
+        *ret_home = TAKE_PTR(new_home);
+        return 1;
+}
+
+static int home_inspect(UserRecord *h, UserRecord **ret_home) {
+        _cleanup_(user_record_unrefp) UserRecord *header_home = NULL, *new_home = NULL;
+        _cleanup_(home_setup_undo) HomeSetup setup = HOME_SETUP_INIT;
+        _cleanup_(strv_free_erasep) char **pkcs11_decrypted_passwords = NULL;
+        bool already_activated = false;
+        int r;
+
+        assert(h);
+        assert(ret_home);
+
+        r = user_record_authenticate(h, h, &pkcs11_decrypted_passwords);
+        if (r < 0)
+                return r;
+
+        r = home_validate_update(h, &setup);
+        if (r < 0)
+                return r;
+
+        already_activated = r > 0;
+
+        r = home_prepare(h, already_activated, &pkcs11_decrypted_passwords, &setup, &header_home);
+        if (r < 0)
+                return r;
+
+        r = home_load_embedded_identity(h, setup.root_fd, header_home, USER_RECONCILE_ANY, &pkcs11_decrypted_passwords, NULL, &new_home);
+        if (r < 0)
+                return r;
+
+        r = home_extend_embedded_identity(new_home, h, &setup);
+        if (r < 0)
+                return r;
+
+        r = home_setup_undo(&setup);
+        if (r < 0)
+                return r;
+
+        log_info("Everything completed.");
+
+        *ret_home = TAKE_PTR(new_home);
+        return 1;
+}
+
+static int home_lock(UserRecord *h) {
+        int r;
+
+        assert(h);
+
+        if (!h->user_name)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record incomplete, refusing.");
+        if (user_record_storage(h) != USER_LUKS)
+                return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), "Locking home directories of type '%s' currently not supported.", user_storage_to_string(user_record_storage(h)));
+
+        r = user_record_test_home_directory_and_warn(h);
+        if (r < 0)
+                return r;
+        if (r != USER_TEST_MOUNTED)
+                return log_error_errno(SYNTHETIC_ERRNO(ENOEXEC), "Home directory of %s is not mounted, can't lock.", h->user_name);
+
+        r = home_lock_luks(h);
+        if (r < 0)
+                return r;
+
+        log_info("Everything completed.");
+        return 1;
+}
+
+static int home_unlock(UserRecord *h) {
+        _cleanup_(strv_free_erasep) char **pkcs11_decrypted_passwords = NULL;
+        int r;
+
+        assert(h);
+
+        if (!h->user_name)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record incomplete, refusing.");
+        if (user_record_storage(h) != USER_LUKS)
+                return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), "Unlocking home directories of type '%s' currently not supported.", user_storage_to_string(user_record_storage(h)));
+
+        /* Note that we don't check if $HOME is actually mounted, since we want to avoid disk accesses on
+         * that mount until we have resumed the device. */
+
+        r = user_record_authenticate(h, h, &pkcs11_decrypted_passwords);
+        if (r < 0)
+                return r;
+
+        r = home_unlock_luks(h, &pkcs11_decrypted_passwords);
+        if (r < 0)
+                return r;
+
+        log_info("Everything completed.");
+        return 1;
+}
+
+static int run(int argc, char *argv[]) {
+        _cleanup_(user_record_unrefp) UserRecord *home = NULL, *new_home = NULL;
+        _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+        _cleanup_(fclosep) FILE *opened_file = NULL;
+        unsigned line = 0, column = 0;
+        const char *json_path = NULL;
+        FILE *json_file;
+        usec_t start;
+        int r;
+
+        start = now(CLOCK_MONOTONIC);
+
+        log_setup_service();
+
+        umask(0022);
+
+        if (argc < 2 || argc > 3)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "This program takes one or two arguments.");
+
+        if (argc > 2) {
+                json_path = argv[2];
+
+                opened_file = fopen(json_path, "re");
+                if (!opened_file)
+                        return log_error_errno(errno, "Failed to open %s: %m", json_path);
+
+                json_file = opened_file;
+        } else {
+                json_path = "<stdin>";
+                json_file = stdin;
+        }
+
+        r = json_parse_file(json_file, json_path, JSON_PARSE_SENSITIVE, &v, &line, &column);
+        if (r < 0)
+                return log_error_errno(r, "[%s:%u:%u] Failed to parse JSON data: %m", json_path, line, column);
+
+        home = user_record_new();
+        if (!home)
+                return log_oom();
+
+        r = user_record_load(home, v, USER_RECORD_LOAD_FULL|USER_RECORD_LOG);
+        if (r < 0)
+                return r;
+
+        /* Well known return values of these operations, that systemd-homed knows and converts to proper D-Bus errors:
+         *
+         * EMSGSIZE        → file systems of this type cannnot be shrinked
+         * ETXTBSY         → file systems of this type can only be shrinked offline
+         * ERANGE          → file system size too small
+         * ENOLINK         → system does not support selected storage backend
+         * EPROTONOSUPPORT → system does not support selected file system
+         * ENOTTY          → operation not support on this storage
+         * ESOCKTNOSUPPORT → operation not support on this file system
+         * ENOKEY          → password incorrect (or not sufficient, or not supplied)
+         * EBADSLT         → similar, but PKCS#11 device is defined and might be able to provide password, if it was plugged in which it is not
+         * ENOANO          → suitable PKCS#11 device found, but PIN is missing to unlock it
+         * ERFKILL         → suitable PKCS#11 device found, but OK to ask for on-device interactive authentication not given
+         * EOWNERDEAD      → suitable PKCS#11 device found, but its PIN is locked
+         * ENOLCK          → suitable PKCS#11 device found, but PIN incorrect
+         * ETOOMANYREFS    → suitable PKCS#11 device found, but PIN incorrect, and only few tries left
+         * EUCLEAN         → suitable PKCS#11 device found, but PIN incorrect, and only one try left
+         * EBUSY           → file system is currently active
+         * ENOEXEC         → file system is currently not active
+         * ENOSPC          → not enough disk space for operation
+         */
+
+        if (streq(argv[1], "activate"))
+                r = home_activate(home, &new_home);
+        else if (streq(argv[1], "deactivate"))
+                r = home_deactivate(home, false);
+        else if (streq(argv[1], "deactivate-force"))
+                r = home_deactivate(home, true);
+        else if (streq(argv[1], "create"))
+                r = home_create(home, &new_home);
+        else if (streq(argv[1], "remove"))
+                r = home_remove(home);
+        else if (streq(argv[1], "update"))
+                r = home_update(home, &new_home);
+        else if (streq(argv[1], "resize"))
+                r = home_resize(home, &new_home);
+        else if (streq(argv[1], "passwd"))
+                r = home_passwd(home, &new_home);
+        else if (streq(argv[1], "inspect"))
+                r = home_inspect(home, &new_home);
+        else if (streq(argv[1], "lock"))
+                r = home_lock(home);
+        else if (streq(argv[1], "unlock"))
+                r = home_unlock(home);
+        else
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown verb '%s'.", argv[1]);
+        if (r == -ENOKEY && !strv_isempty(home->password)) { /* There were passwords specified but they were incorrect */
+                usec_t end, n, d;
+
+                /* Make sure bad password replies always take at least 3s, and if longer multiples of 3s, so
+                 * that it's not clear how long we actually needed for our calculations. */
+                n = now(CLOCK_MONOTONIC);
+                assert(n >= start);
+
+                d = usec_sub_unsigned(n, start);
+                if (d > BAD_PASSWORD_DELAY_USEC)
+                        end = start + DIV_ROUND_UP(d, BAD_PASSWORD_DELAY_USEC) * BAD_PASSWORD_DELAY_USEC;
+                else
+                        end = start + BAD_PASSWORD_DELAY_USEC;
+
+                if (n < end)
+                        (void) usleep(usec_sub_unsigned(end, n));
+        }
+        if (r < 0)
+                return r;
+
+        /* We always pass the new record back, regardless if it changed or not. This allows our caller to
+         * prepare a fresh record, send to us, and only if it works use it without having to keep a local
+         * copy. */
+        if (new_home)
+                json_variant_dump(new_home->json, JSON_FORMAT_NEWLINE, stdout, NULL);
+
+        return 0;
+}
+
+DEFINE_MAIN_FUNCTION(run);
diff --git a/src/home/homework.h b/src/home/homework.h
new file mode 100644 (file)
index 0000000..81698b7
--- /dev/null
@@ -0,0 +1,57 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include <linux/fs.h>
+#include <sys/vfs.h>
+
+#include "sd-id128.h"
+
+#include "loop-util.h"
+#include "user-record.h"
+#include "user-record-util.h"
+
+typedef struct HomeSetup {
+        char *dm_name;
+        char *dm_node;
+
+        LoopDevice *loop;
+        struct crypt_device *crypt_device;
+        int root_fd;
+        sd_id128_t found_partition_uuid;
+        sd_id128_t found_luks_uuid;
+        sd_id128_t found_fs_uuid;
+
+        uint8_t fscrypt_key_descriptor[FS_KEY_DESCRIPTOR_SIZE];
+
+        void *volume_key;
+        size_t volume_key_size;
+
+        bool undo_dm;
+        bool undo_mount;
+
+        uint64_t partition_offset;
+        uint64_t partition_size;
+} HomeSetup;
+
+#define HOME_SETUP_INIT                                 \
+        {                                               \
+                .root_fd = -1,                          \
+                .partition_offset = UINT64_MAX,         \
+                .partition_size = UINT64_MAX,           \
+        }
+
+int home_setup_undo(HomeSetup *setup);
+
+int home_prepare(UserRecord *h, bool already_activated, char ***pkcs11_decrypted_passwords, HomeSetup *setup, UserRecord **ret_header_home);
+
+int home_refresh(UserRecord *h, HomeSetup *setup, UserRecord *header_home, char ***pkcs11_decrypted_passwords, struct statfs *ret_statfs, UserRecord **ret_new_home);
+
+int home_populate(UserRecord *h, int dir_fd);
+
+int home_load_embedded_identity(UserRecord *h, int root_fd, UserRecord *header_home, UserReconcileMode mode, char ***pkcs11_decrypted_passwords, UserRecord **ret_embedded_home, UserRecord **ret_new_home);
+int home_store_embedded_identity(UserRecord *h, int root_fd, uid_t uid, UserRecord *old_home);
+int home_extend_embedded_identity(UserRecord *h, UserRecord *used, HomeSetup *setup);
+
+int user_record_authenticate(UserRecord *h, UserRecord *secret, char ***pkcs11_decrypted_passwords);
+
+int home_sync_and_statfs(int root_fd, struct statfs *ret);
diff --git a/src/home/meson.build b/src/home/meson.build
new file mode 100644 (file)
index 0000000..82c6735
--- /dev/null
@@ -0,0 +1,62 @@
+# SPDX-License-Identifier: LGPL-2.1+
+
+systemd_homework_sources = files('''
+        home-util.c
+        home-util.h
+        homework-cifs.c
+        homework-cifs.h
+        homework-directory.c
+        homework-directory.h
+        homework-fscrypt.c
+        homework-fscrypt.h
+        homework-luks.c
+        homework-luks.h
+        homework-mount.c
+        homework-mount.h
+        homework-pkcs11.h
+        homework-quota.c
+        homework-quota.h
+        homework.c
+        homework.h
+        user-record-util.c
+        user-record-util.h
+'''.split())
+
+if conf.get('HAVE_P11KIT') == 1
+        systemd_homework_sources += files('homework-pkcs11.c')
+endif
+
+systemd_homed_sources = files('''
+        home-util.c
+        home-util.h
+        homed-bus.c
+        homed-bus.h
+        homed-home-bus.c
+        homed-home-bus.h
+        homed-home.c
+        homed-home.h
+        homed-manager-bus.c
+        homed-manager-bus.h
+        homed-manager.c
+        homed-manager.h
+        homed-operation.c
+        homed-operation.h
+        homed-varlink.c
+        homed-varlink.h
+        homed.c
+        pwquality-util.c
+        pwquality-util.h
+        user-record-sign.c
+        user-record-sign.h
+        user-record-util.c
+        user-record-util.h
+'''.split())
+
+if conf.get('ENABLE_HOMED') == 1
+        install_data('org.freedesktop.home1.conf',
+                     install_dir : dbuspolicydir)
+        install_data('org.freedesktop.home1.service',
+                     install_dir : dbussystemservicedir)
+        install_data('org.freedesktop.home1.policy',
+                     install_dir : polkitpolicydir)
+endif
diff --git a/src/home/org.freedesktop.home1.conf b/src/home/org.freedesktop.home1.conf
new file mode 100644 (file)
index 0000000..d615501
--- /dev/null
@@ -0,0 +1,193 @@
+<?xml version="1.0"?> <!--*-nxml-*-->
+<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
+        "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
+
+<!-- SPDX-License-Identifier: LGPL-2.1+ -->
+
+<busconfig>
+
+        <policy user="root">
+                <allow own="org.freedesktop.home1"/>
+                <allow send_destination="org.freedesktop.home1"/>
+                <allow receive_sender="org.freedesktop.home1"/>
+        </policy>
+
+        <policy context="default">
+                <deny send_destination="org.freedesktop.home1"/>
+
+                <!-- generic interfaces -->
+
+                <allow send_destination="org.freedesktop.home1"
+                       send_interface="org.freedesktop.DBus.Introspectable"/>
+
+                <allow send_destination="org.freedesktop.home1"
+                       send_interface="org.freedesktop.DBus.Peer"/>
+
+                <allow send_destination="org.freedesktop.home1"
+                       send_interface="org.freedesktop.DBus.Properties"
+                       send_member="Get"/>
+
+                <allow send_destination="org.freedesktop.home1"
+                       send_interface="org.freedesktop.DBus.Properties"
+                       send_member="GetAll"/>
+
+                <!-- Manager object -->
+
+                <allow send_destination="org.freedesktop.home1"
+                       send_interface="org.freedesktop.home1.Manager"
+                       send_member="GetHomeByName"/>
+
+                <allow send_destination="org.freedesktop.home1"
+                       send_interface="org.freedesktop.home1.Manager"
+                       send_member="GetHomeByUID"/>
+
+                <allow send_destination="org.freedesktop.home1"
+                       send_interface="org.freedesktop.home1.Manager"
+                       send_member="GetUserRecordByName"/>
+
+                <allow send_destination="org.freedesktop.home1"
+                       send_interface="org.freedesktop.home1.Manager"
+                       send_member="GetUserRecordByUID"/>
+
+                <allow send_destination="org.freedesktop.home1"
+                       send_interface="org.freedesktop.home1.Manager"
+                       send_member="ListHomes"/>
+
+                <allow send_destination="org.freedesktop.home1"
+                       send_interface="org.freedesktop.home1.Manager"
+                       send_member="ActivateHome"/>
+
+                <allow send_destination="org.freedesktop.home1"
+                       send_interface="org.freedesktop.home1.Manager"
+                       send_member="DeactivateHome"/>
+
+                <allow send_destination="org.freedesktop.home1"
+                       send_interface="org.freedesktop.home1.Manager"
+                       send_member="RegisterHome"/>
+
+                <allow send_destination="org.freedesktop.home1"
+                       send_interface="org.freedesktop.home1.Manager"
+                       send_member="UnregisterHome"/>
+
+                <allow send_destination="org.freedesktop.home1"
+                       send_interface="org.freedesktop.home1.Manager"
+                       send_member="CreateHome"/>
+
+                <allow send_destination="org.freedesktop.home1"
+                       send_interface="org.freedesktop.home1.Manager"
+                       send_member="RealizeHome"/>
+
+                <allow send_destination="org.freedesktop.home1"
+                       send_interface="org.freedesktop.home1.Manager"
+                       send_member="RemoveHome"/>
+
+                <allow send_destination="org.freedesktop.home1"
+                       send_interface="org.freedesktop.home1.Manager"
+                       send_member="FixateHome"/>
+
+                <allow send_destination="org.freedesktop.home1"
+                       send_interface="org.freedesktop.home1.Manager"
+                       send_member="AuthenticateHome"/>
+
+                <allow send_destination="org.freedesktop.home1"
+                       send_interface="org.freedesktop.home1.Manager"
+                       send_member="UpdateHome"/>
+
+                <allow send_destination="org.freedesktop.home1"
+                       send_interface="org.freedesktop.home1.Manager"
+                       send_member="ResizeHome"/>
+
+                <allow send_destination="org.freedesktop.home1"
+                       send_interface="org.freedesktop.home1.Manager"
+                       send_member="ChangePasswordHome"/>
+
+                <allow send_destination="org.freedesktop.home1"
+                       send_interface="org.freedesktop.home1.Manager"
+                       send_member="LockHome"/>
+
+                <allow send_destination="org.freedesktop.home1"
+                       send_interface="org.freedesktop.home1.Manager"
+                       send_member="UnlockHome"/>
+
+                <allow send_destination="org.freedesktop.home1"
+                       send_interface="org.freedesktop.home1.Manager"
+                       send_member="AcquireHome"/>
+
+                <allow send_destination="org.freedesktop.home1"
+                       send_interface="org.freedesktop.home1.Manager"
+                       send_member="RefHome"/>
+
+                <allow send_destination="org.freedesktop.home1"
+                       send_interface="org.freedesktop.home1.Manager"
+                       send_member="ReleaseHome"/>
+
+                <allow send_destination="org.freedesktop.home1"
+                       send_interface="org.freedesktop.home1.Manager"
+                       send_member="LockAllHomes"/>
+
+                <!-- Home object -->
+
+                <allow send_destination="org.freedesktop.home1"
+                       send_interface="org.freedesktop.home1.Home"
+                       send_member="Activate"/>
+
+                <allow send_destination="org.freedesktop.home1"
+                       send_interface="org.freedesktop.home1.Home"
+                       send_member="Deactivate"/>
+
+                <allow send_destination="org.freedesktop.home1"
+                       send_interface="org.freedesktop.home1.Home"
+                       send_member="Unregister"/>
+
+                <allow send_destination="org.freedesktop.home1"
+                       send_interface="org.freedesktop.home1.Home"
+                       send_member="Realize"/>
+
+                <allow send_destination="org.freedesktop.home1"
+                       send_interface="org.freedesktop.home1.Home"
+                       send_member="Remove"/>
+
+                <allow send_destination="org.freedesktop.home1"
+                       send_interface="org.freedesktop.home1.Home"
+                       send_member="Fixate"/>
+
+                <allow send_destination="org.freedesktop.home1"
+                       send_interface="org.freedesktop.home1.Home"
+                       send_member="Authenticate"/>
+
+                <allow send_destination="org.freedesktop.home1"
+                       send_interface="org.freedesktop.home1.Home"
+                       send_member="Update"/>
+
+                <allow send_destination="org.freedesktop.home1"
+                       send_interface="org.freedesktop.home1.Home"
+                       send_member="Resize"/>
+
+                <allow send_destination="org.freedesktop.home1"
+                       send_interface="org.freedesktop.home1.Home"
+                       send_member="ChangePassword"/>
+
+                <allow send_destination="org.freedesktop.home1"
+                       send_interface="org.freedesktop.home1.Home"
+                       send_member="Lock"/>
+
+                <allow send_destination="org.freedesktop.home1"
+                       send_interface="org.freedesktop.home1.Home"
+                       send_member="Unlock"/>
+
+                <allow send_destination="org.freedesktop.home1"
+                       send_interface="org.freedesktop.home1.Home"
+                       send_member="Acquire"/>
+
+                <allow send_destination="org.freedesktop.home1"
+                       send_interface="org.freedesktop.home1.Home"
+                       send_member="Ref"/>
+
+                <allow send_destination="org.freedesktop.home1"
+                       send_interface="org.freedesktop.home1.Home"
+                       send_member="Release"/>
+
+                <allow receive_sender="org.freedesktop.home1"/>
+        </policy>
+
+</busconfig>
diff --git a/src/home/org.freedesktop.home1.policy b/src/home/org.freedesktop.home1.policy
new file mode 100644 (file)
index 0000000..66ef8e0
--- /dev/null
@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="UTF-8"?> <!--*-nxml-*-->
+<!DOCTYPE policyconfig PUBLIC "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
+        "http://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">
+
+<!-- SPDX-License-Identifier: LGPL-2.1+ -->
+
+<policyconfig>
+
+        <vendor>The systemd Project</vendor>
+        <vendor_url>http://www.freedesktop.org/wiki/Software/systemd</vendor_url>
+
+        <action id="org.freedesktop.home1.create-home">
+                <description gettext-domain="systemd">Create a home</description>
+                <message gettext-domain="systemd">Authentication is required for creating a user's home.</message>
+                <defaults>
+                        <allow_any>auth_admin_keep</allow_any>
+                        <allow_inactive>auth_admin_keep</allow_inactive>
+                        <allow_active>auth_admin_keep</allow_active>
+                </defaults>
+        </action>
+
+        <action id="org.freedesktop.home1.remove-home">
+                <description gettext-domain="systemd">Remove a home</description>
+                <message gettext-domain="systemd">Authentication is required for removing a user's home.</message>
+                <defaults>
+                        <allow_any>auth_admin_keep</allow_any>
+                        <allow_inactive>auth_admin_keep</allow_inactive>
+                        <allow_active>auth_admin_keep</allow_active>
+                </defaults>
+        </action>
+
+        <action id="org.freedesktop.home1.authenticate-home">
+                <description gettext-domain="systemd">Check credentials of a home</description>
+                <message gettext-domain="systemd">Authentication is required for checking credentials against a user's home.</message>
+                <defaults>
+                        <allow_any>auth_admin_keep</allow_any>
+                        <allow_inactive>auth_admin_keep</allow_inactive>
+                        <allow_active>auth_admin_keep</allow_active>
+                </defaults>
+        </action>
+
+        <action id="org.freedesktop.home1.update-home">
+                <description gettext-domain="systemd">Update a home</description>
+                <message gettext-domain="systemd">Authentication is required for updating a user's home.</message>
+                <defaults>
+                        <allow_any>auth_admin_keep</allow_any>
+                        <allow_inactive>auth_admin_keep</allow_inactive>
+                        <allow_active>auth_admin_keep</allow_active>
+                </defaults>
+        </action>
+
+        <action id="org.freedesktop.home1.resize-home">
+                <description gettext-domain="systemd">Resize a home</description>
+                <message gettext-domain="systemd">Authentication is required for resizing a user's home.</message>
+                <defaults>
+                        <allow_any>auth_admin_keep</allow_any>
+                        <allow_inactive>auth_admin_keep</allow_inactive>
+                        <allow_active>auth_admin_keep</allow_active>
+                </defaults>
+        </action>
+
+        <action id="org.freedesktop.home1.passwd-home">
+                <description gettext-domain="systemd">Change password of a home</description>
+                <message gettext-domain="systemd">Authentication is required for changing the password of a user's home.</message>
+                <defaults>
+                        <allow_any>auth_admin_keep</allow_any>
+                        <allow_inactive>auth_admin_keep</allow_inactive>
+                        <allow_active>auth_admin_keep</allow_active>
+                </defaults>
+        </action>
+
+</policyconfig>
diff --git a/src/home/org.freedesktop.home1.service b/src/home/org.freedesktop.home1.service
new file mode 100644 (file)
index 0000000..cff19b3
--- /dev/null
@@ -0,0 +1,7 @@
+# SPDX-License-Identifier: LGPL-2.1+
+
+[D-BUS Service]
+Name=org.freedesktop.home1
+Exec=/bin/false
+User=root
+SystemdService=dbus-org.freedesktop.home1.service
diff --git a/src/home/pwquality-util.c b/src/home/pwquality-util.c
new file mode 100644 (file)
index 0000000..c814c8f
--- /dev/null
@@ -0,0 +1,140 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include <unistd.h>
+
+#if HAVE_PWQUALITY
+/* pwquality.h uses size_t but doesn't include sys/types.h on its own */
+#include <sys/types.h>
+#include <pwquality.h>
+#endif
+
+#include "bus-common-errors.h"
+#include "home-util.h"
+#include "pwquality-util.h"
+#include "strv.h"
+
+#if HAVE_PWQUALITY
+DEFINE_TRIVIAL_CLEANUP_FUNC(pwquality_settings_t*, pwquality_free_settings);
+
+static void pwquality_maybe_disable_dictionary(
+                pwquality_settings_t *pwq) {
+
+        char buf[PWQ_MAX_ERROR_MESSAGE_LEN];
+        const char *path;
+        int r;
+
+        r = pwquality_get_str_value(pwq, PWQ_SETTING_DICT_PATH, &path);
+        if (r < 0) {
+                log_warning("Failed to read libpwquality dictionary path, ignoring: %s", pwquality_strerror(buf, sizeof(buf), r, NULL));
+                return;
+        }
+
+        // REMOVE THIS AS SOON AS https://github.com/libpwquality/libpwquality/pull/21 IS MERGED AND RELEASED
+        if (isempty(path))
+                path = "/usr/share/cracklib/pw_dict.pwd.gz";
+
+        if (isempty(path)) {
+                log_warning("Weird, no dictionary file configured, ignoring.");
+                return;
+        }
+
+        if (access(path, F_OK) >= 0)
+                return;
+
+        if (errno != ENOENT) {
+                log_warning_errno(errno, "Failed to check if dictionary file %s exists, ignoring: %m", path);
+                return;
+        }
+
+        r = pwquality_set_int_value(pwq, PWQ_SETTING_DICT_CHECK, 0);
+        if (r < 0) {
+                log_warning("Failed to disable libpwquality dictionary check, ignoring: %s", pwquality_strerror(buf, sizeof(buf), r, NULL));
+                return;
+        }
+}
+
+int quality_check_password(
+                UserRecord *hr,
+                UserRecord *secret,
+                sd_bus_error *error) {
+
+        _cleanup_(pwquality_free_settingsp) pwquality_settings_t *pwq = NULL;
+        char buf[PWQ_MAX_ERROR_MESSAGE_LEN], **pp;
+        void *auxerror;
+        int r;
+
+        assert(hr);
+        assert(secret);
+
+        pwq = pwquality_default_settings();
+        if (!pwq)
+                return log_oom();
+
+        r = pwquality_read_config(pwq, NULL, &auxerror);
+        if (r < 0)
+                log_warning_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to read libpwquality configuation, ignoring: %s",
+                                  pwquality_strerror(buf, sizeof(buf), r, auxerror));
+
+        pwquality_maybe_disable_dictionary(pwq);
+
+        /* This is a bit more complex than one might think at first. pwquality_check() would like to know the
+         * old password to make security checks. We support arbitrary numbers of passwords however, hence we
+         * call the function once for each combination of old and new password. */
+
+        /* Iterate through all new passwords */
+        STRV_FOREACH(pp, secret->password) {
+                bool called = false;
+                char **old;
+
+                r = test_password_many(hr->hashed_password, *pp);
+                if (r < 0)
+                        return r;
+                if (r == 0) /* This is an old password as it isn't listed in the hashedPassword field, skip it */
+                        continue;
+
+                /* Check this password against all old passwords */
+                STRV_FOREACH(old, secret->password) {
+
+                        if (streq(*pp, *old))
+                                continue;
+
+                        r = test_password_many(hr->hashed_password, *old);
+                        if (r < 0)
+                                return r;
+                        if (r > 0) /* This is a new password, not suitable as old password */
+                                continue;
+
+                        r = pwquality_check(pwq, *pp, *old, hr->user_name, &auxerror);
+                        if (r < 0)
+                                return sd_bus_error_setf(error, BUS_ERROR_LOW_PASSWORD_QUALITY, "Password too weak: %s",
+                                                         pwquality_strerror(buf, sizeof(buf), r, auxerror));
+
+                        called = true;
+                }
+
+                if (called)
+                        continue;
+
+                /* If there are no old passwords, let's call pwquality_check() without any. */
+                r = pwquality_check(pwq, *pp, NULL, hr->user_name, &auxerror);
+                if (r < 0)
+                        return sd_bus_error_setf(error, BUS_ERROR_LOW_PASSWORD_QUALITY, "Password too weak: %s",
+                                                 pwquality_strerror(buf, sizeof(buf), r, auxerror));
+        }
+
+        return 0;
+}
+
+#else
+
+int quality_check_password(
+                UserRecord *hr,
+                UserRecord *secret,
+                sd_bus_error *error) {
+
+        assert(hr);
+        assert(secret);
+
+        return 0;
+}
+#endif
diff --git a/src/home/pwquality-util.h b/src/home/pwquality-util.h
new file mode 100644 (file)
index 0000000..b44150b
--- /dev/null
@@ -0,0 +1,7 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include "sd-bus.h"
+#include "user-record.h"
+
+int quality_check_password(UserRecord *hr, UserRecord *secret, sd_bus_error *error);
diff --git a/src/home/user-record-sign.c b/src/home/user-record-sign.c
new file mode 100644 (file)
index 0000000..91f8639
--- /dev/null
@@ -0,0 +1,174 @@
+#include <openssl/pem.h>
+
+#include "fd-util.h"
+#include "user-record-sign.h"
+#include "fileio.h"
+
+static int user_record_signable_json(UserRecord *ur, char **ret) {
+        _cleanup_(user_record_unrefp) UserRecord *reduced = NULL;
+        _cleanup_(json_variant_unrefp) JsonVariant *j = NULL;
+        int r;
+
+        assert(ur);
+        assert(ret);
+
+        r = user_record_clone(ur, USER_RECORD_REQUIRE_REGULAR|USER_RECORD_ALLOW_PRIVILEGED|USER_RECORD_ALLOW_PER_MACHINE|USER_RECORD_STRIP_SECRET|USER_RECORD_STRIP_BINDING|USER_RECORD_STRIP_STATUS|USER_RECORD_STRIP_SIGNATURE, &reduced);
+        if (r < 0)
+                return r;
+
+        j = json_variant_ref(reduced->json);
+
+        r = json_variant_normalize(&j);
+        if (r < 0)
+                return r;
+
+        return json_variant_format(j, 0, ret);
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(EVP_MD_CTX*, EVP_MD_CTX_free);
+
+int user_record_sign(UserRecord *ur, EVP_PKEY *private_key, UserRecord **ret) {
+        _cleanup_(json_variant_unrefp) JsonVariant *encoded = NULL, *v = NULL;
+        _cleanup_(user_record_unrefp) UserRecord *signed_ur = NULL;
+        _cleanup_(EVP_MD_CTX_freep) EVP_MD_CTX *md_ctx = NULL;
+        _cleanup_free_ char *text = NULL, *key = NULL;
+        size_t signature_size = 0, key_size = 0;
+        _cleanup_free_ void *signature = NULL;
+        _cleanup_fclose_ FILE *mf = NULL;
+        int r;
+
+        assert(ur);
+        assert(private_key);
+        assert(ret);
+
+        r = user_record_signable_json(ur, &text);
+        if (r < 0)
+                return r;
+
+        md_ctx = EVP_MD_CTX_new();
+        if (!md_ctx)
+                return -ENOMEM;
+
+        if (EVP_DigestSignInit(md_ctx, NULL, NULL, NULL, private_key) <= 0)
+                return -EIO;
+
+        /* Request signature size */
+        if (EVP_DigestSign(md_ctx, NULL, &signature_size, (uint8_t*) text, strlen(text)) <= 0)
+                return -EIO;
+
+        signature = malloc(signature_size);
+        if (!signature)
+                return -ENOMEM;
+
+        if (EVP_DigestSign(md_ctx, signature, &signature_size, (uint8_t*) text, strlen(text)) <= 0)
+                return -EIO;
+
+        mf = open_memstream_unlocked(&key, &key_size);
+        if (!mf)
+                return -ENOMEM;
+
+        if (PEM_write_PUBKEY(mf, private_key) <= 0)
+                return -EIO;
+
+        r = fflush_and_check(mf);
+        if (r < 0)
+                return r;
+
+        r = json_build(&encoded, JSON_BUILD_ARRAY(
+                                       JSON_BUILD_OBJECT(JSON_BUILD_PAIR("data", JSON_BUILD_BASE64(signature, signature_size)),
+                                                         JSON_BUILD_PAIR("key", JSON_BUILD_STRING(key)))));
+        if (r < 0)
+                return r;
+
+        v = json_variant_ref(ur->json);
+
+        r = json_variant_set_field(&v, "signature", encoded);
+        if (r < 0)
+                return r;
+
+        if (DEBUG_LOGGING)
+                json_variant_dump(v, JSON_FORMAT_PRETTY|JSON_FORMAT_COLOR_AUTO, NULL, NULL);
+
+        signed_ur = user_record_new();
+        if (!signed_ur)
+                return log_oom();
+
+        r = user_record_load(signed_ur, v, USER_RECORD_LOAD_FULL);
+        if (r < 0)
+                return r;
+
+        *ret = TAKE_PTR(signed_ur);
+        return 0;
+}
+
+int user_record_verify(UserRecord *ur, EVP_PKEY *public_key) {
+        _cleanup_free_ char *text = NULL;
+        unsigned n_good = 0, n_bad = 0;
+        JsonVariant *array, *e;
+        int r;
+
+        assert(ur);
+        assert(public_key);
+
+        array = json_variant_by_key(ur->json, "signature");
+        if (!array)
+                return USER_RECORD_UNSIGNED;
+
+        if (!json_variant_is_array(array))
+                return -EINVAL;
+
+        if (json_variant_elements(array) == 0)
+                return USER_RECORD_UNSIGNED;
+
+        r = user_record_signable_json(ur, &text);
+        if (r < 0)
+                return r;
+
+        JSON_VARIANT_ARRAY_FOREACH(e, array) {
+                _cleanup_(EVP_MD_CTX_freep) EVP_MD_CTX *md_ctx = NULL;
+                _cleanup_free_ void *signature = NULL;
+                size_t signature_size = 0;
+                JsonVariant *data;
+
+                if (!json_variant_is_object(e))
+                        return -EINVAL;
+
+                data = json_variant_by_key(e, "data");
+                if (!data)
+                        return -EINVAL;
+
+                r = json_variant_unbase64(data, &signature, &signature_size);
+                if (r < 0)
+                        return r;
+
+                md_ctx = EVP_MD_CTX_new();
+                if (!md_ctx)
+                        return -ENOMEM;
+
+                if (EVP_DigestVerifyInit(md_ctx, NULL, NULL, NULL, public_key) <= 0)
+                        return -EIO;
+
+                if (EVP_DigestVerify(md_ctx, signature, signature_size, (uint8_t*) text, strlen(text)) <= 0) {
+                        n_bad ++;
+                        continue;
+                }
+
+                n_good ++;
+        }
+
+        return n_good > 0 ? (n_bad == 0 ? USER_RECORD_SIGNED_EXCLUSIVE : USER_RECORD_SIGNED) :
+                (n_bad == 0 ? USER_RECORD_UNSIGNED : USER_RECORD_FOREIGN);
+}
+
+int user_record_has_signature(UserRecord *ur) {
+        JsonVariant *array;
+
+        array = json_variant_by_key(ur->json, "signature");
+        if (!array)
+                return false;
+
+        if (!json_variant_is_array(array))
+                return -EINVAL;
+
+        return json_variant_elements(array) > 0;
+}
diff --git a/src/home/user-record-sign.h b/src/home/user-record-sign.h
new file mode 100644 (file)
index 0000000..f045c88
--- /dev/null
@@ -0,0 +1,19 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include <openssl/evp.h>
+
+#include "user-record.h"
+
+int user_record_sign(UserRecord *ur, EVP_PKEY *private_key, UserRecord **ret);
+
+enum {
+        USER_RECORD_UNSIGNED,           /* user record has no signature */
+        USER_RECORD_SIGNED_EXCLUSIVE,   /* user record has only a signature by our own key */
+        USER_RECORD_SIGNED,             /* user record is signed by us, but by others too */
+        USER_RECORD_FOREIGN,            /* user record is not signed by us, but by others */
+};
+
+int user_record_verify(UserRecord *ur, EVP_PKEY *public_key);
+
+int user_record_has_signature(UserRecord *ur);
diff --git a/src/home/user-record-util.c b/src/home/user-record-util.c
new file mode 100644 (file)
index 0000000..cb840f9
--- /dev/null
@@ -0,0 +1,1225 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include "errno-util.h"
+#include "home-util.h"
+#include "id128-util.h"
+#include "libcrypt-util.h"
+#include "mountpoint-util.h"
+#include "path-util.h"
+#include "stat-util.h"
+#include "user-record-util.h"
+#include "user-util.h"
+
+int user_record_synthesize(
+                UserRecord *h,
+                const char *user_name,
+                const char *realm,
+                const char *image_path,
+                UserStorage storage,
+                uid_t uid,
+                gid_t gid) {
+
+        _cleanup_free_ char *hd = NULL, *un = NULL, *ip = NULL, *rr = NULL, *user_name_and_realm = NULL;
+        char smid[SD_ID128_STRING_MAX];
+        sd_id128_t mid;
+        int r;
+
+        assert(h);
+        assert(user_name);
+        assert(image_path);
+        assert(IN_SET(storage, USER_LUKS, USER_SUBVOLUME, USER_FSCRYPT, USER_DIRECTORY));
+        assert(uid_is_valid(uid));
+        assert(gid_is_valid(gid));
+
+        /* Fill in a home record from just a username and an image path. */
+
+        if (h->json)
+                return -EBUSY;
+
+        if (!suitable_user_name(user_name))
+                return -EINVAL;
+
+        if (realm) {
+                r = suitable_realm(realm);
+                if (r < 0)
+                        return r;
+                if (r == 0)
+                        return -EINVAL;
+        }
+
+        if (!suitable_image_path(image_path))
+                return -EINVAL;
+
+        r = sd_id128_get_machine(&mid);
+        if (r < 0)
+                return r;
+
+        un = strdup(user_name);
+        if (!un)
+                return -ENOMEM;
+
+        if (realm) {
+                rr = strdup(realm);
+                if (!rr)
+                        return -ENOMEM;
+
+                user_name_and_realm = strjoin(user_name, "@", realm);
+                if (!user_name_and_realm)
+                        return -ENOMEM;
+        }
+
+        ip = strdup(image_path);
+        if (!ip)
+                return -ENOMEM;
+
+        hd = path_join("/home/", user_name);
+        if (!hd)
+                return -ENOMEM;
+
+        r = json_build(&h->json,
+                       JSON_BUILD_OBJECT(
+                                       JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(user_name)),
+                                       JSON_BUILD_PAIR_CONDITION(!!rr, "realm", JSON_BUILD_STRING(realm)),
+                                       JSON_BUILD_PAIR("disposition", JSON_BUILD_STRING("regular")),
+                                       JSON_BUILD_PAIR("binding", JSON_BUILD_OBJECT(
+                                                                       JSON_BUILD_PAIR(sd_id128_to_string(mid, smid), JSON_BUILD_OBJECT(
+                                                                                                       JSON_BUILD_PAIR("imagePath", JSON_BUILD_STRING(image_path)),
+                                                                                                       JSON_BUILD_PAIR("homeDirectory", JSON_BUILD_STRING(hd)),
+                                                                                                       JSON_BUILD_PAIR("storage", JSON_BUILD_STRING(user_storage_to_string(storage))),
+                                                                                                       JSON_BUILD_PAIR("uid", JSON_BUILD_UNSIGNED(uid)),
+                                                                                                       JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(gid))))))));
+        if (r < 0)
+                return r;
+
+        free_and_replace(h->user_name, un);
+        free_and_replace(h->realm, rr);
+        free_and_replace(h->user_name_and_realm_auto, user_name_and_realm);
+        free_and_replace(h->image_path, ip);
+        free_and_replace(h->home_directory, hd);
+        h->storage = storage;
+        h->uid = uid;
+
+        h->mask = USER_RECORD_REGULAR|USER_RECORD_BINDING;
+        return 0;
+}
+
+int group_record_synthesize(GroupRecord *g, UserRecord *h) {
+        _cleanup_free_ char *un = NULL, *rr = NULL, *group_name_and_realm = NULL;
+        char smid[SD_ID128_STRING_MAX];
+        sd_id128_t mid;
+        int r;
+
+        assert(g);
+        assert(h);
+
+        if (g->json)
+                return -EBUSY;
+
+        r = sd_id128_get_machine(&mid);
+        if (r < 0)
+                return r;
+
+        un = strdup(h->user_name);
+        if (!un)
+                return -ENOMEM;
+
+        if (h->realm) {
+                rr = strdup(h->realm);
+                if (!rr)
+                        return -ENOMEM;
+
+                group_name_and_realm = strjoin(un, "@", rr);
+                if (!group_name_and_realm)
+                        return -ENOMEM;
+        }
+
+        r = json_build(&g->json,
+                       JSON_BUILD_OBJECT(
+                                       JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(un)),
+                                       JSON_BUILD_PAIR_CONDITION(!!rr, "realm", JSON_BUILD_STRING(rr)),
+                                       JSON_BUILD_PAIR("binding", JSON_BUILD_OBJECT(
+                                                                       JSON_BUILD_PAIR(sd_id128_to_string(mid, smid), JSON_BUILD_OBJECT(
+                                                                                                       JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(user_record_gid(h))))))),
+                                       JSON_BUILD_PAIR_CONDITION(h->disposition >= 0, "disposition", JSON_BUILD_STRING(user_disposition_to_string(user_record_disposition(h)))),
+                                       JSON_BUILD_PAIR("status", JSON_BUILD_OBJECT(
+                                                                       JSON_BUILD_PAIR(sd_id128_to_string(mid, smid), JSON_BUILD_OBJECT(
+                                                                                                       JSON_BUILD_PAIR("service", JSON_BUILD_STRING("io.systemd.Home"))))))));
+        if (r < 0)
+                return r;
+
+        free_and_replace(g->group_name, un);
+        free_and_replace(g->realm, rr);
+        free_and_replace(g->group_name_and_realm_auto, group_name_and_realm);
+        g->gid = user_record_gid(h);
+        g->disposition = h->disposition;
+
+        g->mask = USER_RECORD_REGULAR|USER_RECORD_BINDING;
+        return 0;
+}
+
+int user_record_reconcile(
+                UserRecord *host,
+                UserRecord *embedded,
+                UserReconcileMode mode,
+                UserRecord **ret) {
+
+        int r, result;
+
+        /* Reconciles the identity record stored on the host with the one embedded in a $HOME
+         * directory. Returns the following error codes:
+         *
+         *     -EINVAL: one of the records not valid
+         *     -REMCHG: identity records are not about the same user
+         *     -ESTALE: embedded identity record is equally new or newer than supplied record
+         *
+         * Return the new record to use, which is either the the embedded record updated with the host
+         * binding or the host record. In both cases the secret data is stripped. */
+
+        assert(host);
+        assert(embedded);
+
+        /* Make sure both records are initialized */
+        if (!host->json || !embedded->json)
+                return -EINVAL;
+
+        /* Ensure these records actually contain user data */
+        if (!(embedded->mask & host->mask & USER_RECORD_REGULAR))
+                return -EINVAL;
+
+        /* Make sure the user name and realm matches */
+        if (!user_record_compatible(host, embedded))
+                return -EREMCHG;
+
+        /* Embedded identities may not contain secrets or binding info*/
+        if ((embedded->mask & (USER_RECORD_SECRET|USER_RECORD_BINDING)) != 0)
+                return -EINVAL;
+
+        /* The embedded record checked out, let's now figure out which of the two identities we'll consider
+         * in effect from now on. We do this by checking the last change timestamp, and in doubt always let
+         * the embedded data win. */
+        if (host->last_change_usec != UINT64_MAX &&
+            (embedded->last_change_usec == UINT64_MAX || host->last_change_usec > embedded->last_change_usec))
+
+                /* The host version is definitely newer, either because it has a version at all and the
+                 * embedded version doesn't or because it is numerically newer. */
+                result = USER_RECONCILE_HOST_WON;
+
+        else if (host->last_change_usec == embedded->last_change_usec) {
+
+                /* The nominal version number of the host and the embedded identity is the same. If so, let's
+                 * verify that, and tell the caller if we are ignoring embedded data. */
+
+                r = user_record_masked_equal(host, embedded, USER_RECORD_REGULAR|USER_RECORD_PRIVILEGED|USER_RECORD_PER_MACHINE);
+                if (r < 0)
+                        return r;
+                if (r > 0) {
+                        if (mode == USER_RECONCILE_REQUIRE_NEWER)
+                                return -ESTALE;
+
+                        result = USER_RECONCILE_IDENTICAL;
+                } else
+                        result = USER_RECONCILE_HOST_WON;
+        } else {
+                _cleanup_(json_variant_unrefp) JsonVariant *extended = NULL;
+                _cleanup_(user_record_unrefp) UserRecord *merged = NULL;
+                JsonVariant *e;
+
+                /* The embedded version is newer */
+
+                if (mode == USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL)
+                        return -ESTALE;
+
+                /* Copy in the binding data */
+                extended = json_variant_ref(embedded->json);
+
+                e = json_variant_by_key(host->json, "binding");
+                if (e) {
+                        r = json_variant_set_field(&extended, "binding", e);
+                        if (r < 0)
+                                return r;
+                }
+
+                merged = user_record_new();
+                if (!merged)
+                        return -ENOMEM;
+
+                r = user_record_load(merged, extended, USER_RECORD_LOAD_MASK_SECRET);
+                if (r < 0)
+                        return r;
+
+                *ret = TAKE_PTR(merged);
+                return USER_RECONCILE_EMBEDDED_WON; /* update */
+        }
+
+        /* Strip out secrets */
+        r = user_record_clone(host, USER_RECORD_LOAD_MASK_SECRET, ret);
+        if (r < 0)
+                return r;
+
+        return result;
+}
+
+int user_record_add_binding(
+                UserRecord *h,
+                UserStorage storage,
+                const char *image_path,
+                sd_id128_t partition_uuid,
+                sd_id128_t luks_uuid,
+                sd_id128_t fs_uuid,
+                const char *luks_cipher,
+                const char *luks_cipher_mode,
+                uint64_t luks_volume_key_size,
+                const char *file_system_type,
+                const char *home_directory,
+                uid_t uid,
+                gid_t gid) {
+
+        _cleanup_(json_variant_unrefp) JsonVariant *new_binding_entry = NULL, *binding = NULL;
+        char smid[SD_ID128_STRING_MAX], partition_uuids[37], luks_uuids[37], fs_uuids[37];
+        _cleanup_free_ char *ip = NULL, *hd = NULL;
+        sd_id128_t mid;
+        int r;
+
+        assert(h);
+
+        if (!h->json)
+                return -EUNATCH;
+
+        r = sd_id128_get_machine(&mid);
+        if (r < 0)
+                return r;
+        sd_id128_to_string(mid, smid);
+
+        if (image_path) {
+                ip = strdup(image_path);
+                if (!ip)
+                        return -ENOMEM;
+        }
+
+        if (home_directory) {
+                hd = strdup(home_directory);
+                if (!hd)
+                        return -ENOMEM;
+        }
+
+        r = json_build(&new_binding_entry,
+                       JSON_BUILD_OBJECT(
+                                       JSON_BUILD_PAIR_CONDITION(!!image_path, "imagePath", JSON_BUILD_STRING(image_path)),
+                                       JSON_BUILD_PAIR_CONDITION(!sd_id128_is_null(partition_uuid), "partitionUuid", JSON_BUILD_STRING(id128_to_uuid_string(partition_uuid, partition_uuids))),
+                                       JSON_BUILD_PAIR_CONDITION(!sd_id128_is_null(luks_uuid), "luksUuid", JSON_BUILD_STRING(id128_to_uuid_string(luks_uuid, luks_uuids))),
+                                       JSON_BUILD_PAIR_CONDITION(!sd_id128_is_null(fs_uuid), "fileSystemUuid", JSON_BUILD_STRING(id128_to_uuid_string(fs_uuid, fs_uuids))),
+                                       JSON_BUILD_PAIR_CONDITION(!!luks_cipher, "luksCipher", JSON_BUILD_STRING(luks_cipher)),
+                                       JSON_BUILD_PAIR_CONDITION(!!luks_cipher_mode, "luksCipherMode", JSON_BUILD_STRING(luks_cipher_mode)),
+                                       JSON_BUILD_PAIR_CONDITION(luks_volume_key_size != UINT64_MAX, "luksVolumeKeySize", JSON_BUILD_UNSIGNED(luks_volume_key_size)),
+                                       JSON_BUILD_PAIR_CONDITION(!!file_system_type, "fileSystemType", JSON_BUILD_STRING(file_system_type)),
+                                       JSON_BUILD_PAIR_CONDITION(!!home_directory, "homeDirectory", JSON_BUILD_STRING(home_directory)),
+                                       JSON_BUILD_PAIR_CONDITION(uid_is_valid(uid), "uid", JSON_BUILD_UNSIGNED(uid)),
+                                       JSON_BUILD_PAIR_CONDITION(gid_is_valid(gid), "gid", JSON_BUILD_UNSIGNED(gid)),
+                                       JSON_BUILD_PAIR_CONDITION(storage >= 0, "storage", JSON_BUILD_STRING(user_storage_to_string(storage)))));
+        if (r < 0)
+                return r;
+
+        binding = json_variant_ref(json_variant_by_key(h->json, "binding"));
+        if (binding) {
+                _cleanup_(json_variant_unrefp) JsonVariant *be = NULL;
+
+                /* Merge the new entry with an old one, if that exists */
+                be = json_variant_ref(json_variant_by_key(binding, smid));
+                if (be) {
+                        r = json_variant_merge(&be, new_binding_entry);
+                        if (r < 0)
+                                return r;
+
+                        json_variant_unref(new_binding_entry);
+                        new_binding_entry = TAKE_PTR(be);
+                }
+        }
+
+        r = json_variant_set_field(&binding, smid, new_binding_entry);
+        if (r < 0)
+                return r;
+
+        r = json_variant_set_field(&h->json, "binding", binding);
+        if (r < 0)
+                return r;
+
+        if (storage >= 0)
+                h->storage = storage;
+
+        if (ip)
+                free_and_replace(h->image_path, ip);
+
+        if (!sd_id128_is_null(partition_uuid))
+                h->partition_uuid = partition_uuid;
+
+        if (!sd_id128_is_null(luks_uuid))
+                h->luks_uuid = luks_uuid;
+
+        if (!sd_id128_is_null(fs_uuid))
+                h->file_system_uuid = fs_uuid;
+
+        if (hd)
+                free_and_replace(h->home_directory, hd);
+
+        if (uid_is_valid(uid))
+                h->uid = uid;
+
+        h->mask |= USER_RECORD_BINDING;
+        return 1;
+}
+
+int user_record_test_home_directory(UserRecord *h) {
+        const char *hd;
+        int r;
+
+        assert(h);
+
+        /* Returns one of USER_TEST_ABSENT, USER_TEST_MOUNTED, USER_TEST_EXISTS on success */
+
+        hd = user_record_home_directory(h);
+        if (!hd)
+                return -ENXIO;
+
+        r = is_dir(hd, false);
+        if (r == -ENOENT)
+                return USER_TEST_ABSENT;
+        if (r < 0)
+                return r;
+        if (r == 0)
+                return -ENOTDIR;
+
+        r = path_is_mount_point(hd, NULL, 0);
+        if (r < 0)
+                return r;
+        if (r > 0)
+                return USER_TEST_MOUNTED;
+
+        /* If the image path and the home directory are identical, then it's OK if the directory is
+         * populated. */
+        if (IN_SET(user_record_storage(h), USER_CLASSIC, USER_DIRECTORY, USER_SUBVOLUME, USER_FSCRYPT)) {
+                const char *ip;
+
+                ip = user_record_image_path(h);
+                if (ip && path_equal(ip, hd))
+                        return USER_TEST_EXISTS;
+        }
+
+        /* Otherwise it's not OK */
+        r = dir_is_empty(hd);
+        if (r < 0)
+                return r;
+        if (r == 0)
+                return -EBUSY;
+
+        return USER_TEST_EXISTS;
+}
+
+int user_record_test_home_directory_and_warn(UserRecord *h) {
+        int r;
+
+        assert(h);
+
+        r = user_record_test_home_directory(h);
+        if (r == -ENXIO)
+                return log_error_errno(r, "User record lacks home directory, refusing.");
+        if (r == -ENOTDIR)
+                return log_error_errno(r, "Home directory %s is not a directory, refusing.", user_record_home_directory(h));
+        if (r == -EBUSY)
+                return log_error_errno(r, "Home directory %s exists, is not mounted but populated, refusing.", user_record_home_directory(h));
+        if (r < 0)
+                return log_error_errno(r, "Failed to test whether the home directory %s exists: %m", user_record_home_directory(h));
+
+        return r;
+}
+
+int user_record_test_image_path(UserRecord *h) {
+        const char *ip;
+        struct stat st;
+
+        assert(h);
+
+        if (user_record_storage(h) == USER_CIFS)
+                return USER_TEST_UNDEFINED;
+
+        ip = user_record_image_path(h);
+        if (!ip)
+                return -ENXIO;
+
+        if (stat(ip, &st) < 0) {
+                if (errno == ENOENT)
+                        return USER_TEST_ABSENT;
+
+                return -errno;
+        }
+
+        switch (user_record_storage(h)) {
+
+        case USER_LUKS:
+                if (S_ISREG(st.st_mode))
+                        return USER_TEST_EXISTS;
+                if (S_ISBLK(st.st_mode)) {
+                        /* For block devices we can't really be sure if the device referenced actually is the
+                         * fs we look for or some other file system (think: what does /dev/sdb1 refer
+                         * to?). Hence, let's return USER_TEST_MAYBE as an ambigious return value for these
+                         * case, except if the device path used is one of the paths that is based on a
+                         * filesystem or partition UUID or label, because in those cases we can be sure we
+                         * are referring to the right device. */
+
+                        if (PATH_STARTSWITH_SET(ip,
+                                                "/dev/disk/by-uuid/",
+                                                "/dev/disk/by-partuuid/",
+                                                "/dev/disk/by-partlabel/",
+                                                "/dev/disk/by-label/"))
+                                return USER_TEST_EXISTS;
+
+                        return USER_TEST_MAYBE;
+                }
+
+                return -EBADFD;
+
+        case USER_CLASSIC:
+        case USER_DIRECTORY:
+        case USER_SUBVOLUME:
+        case USER_FSCRYPT:
+                if (S_ISDIR(st.st_mode))
+                        return USER_TEST_EXISTS;
+
+                return -ENOTDIR;
+
+        default:
+                assert_not_reached("Unexpected record type");
+        }
+}
+
+int user_record_test_image_path_and_warn(UserRecord *h) {
+        int r;
+
+        assert(h);
+
+        r = user_record_test_image_path(h);
+        if (r == -ENXIO)
+                return log_error_errno(r, "User record lacks image path, refusing.");
+        if (r == -EBADFD)
+                return log_error_errno(r, "Image path %s is not a regular file or block device, refusing.", user_record_image_path(h));
+        if (r == -ENOTDIR)
+                return log_error_errno(r, "Image path %s is not a directory, refusing.", user_record_image_path(h));
+        if (r < 0)
+                return log_error_errno(r, "Failed to test whether image path %s exists: %m", user_record_image_path(h));
+
+        return r;
+}
+
+int user_record_test_secret(UserRecord *h, UserRecord *secret) {
+        char **i;
+        int r;
+
+        assert(h);
+
+        /* Checks whether any of the specified passwords matches any of the hashed passwords of the entry */
+
+        if (strv_isempty(h->hashed_password))
+                return -ENXIO;
+
+        STRV_FOREACH(i, secret->password) {
+                r = test_password_many(h->hashed_password, *i);
+                if (r < 0)
+                        return r;
+                if (r > 0)
+                        return 0;
+        }
+
+        return -ENOKEY;
+}
+
+int user_record_set_disk_size(UserRecord *h, uint64_t disk_size) {
+        _cleanup_(json_variant_unrefp) JsonVariant *new_per_machine = NULL, *midv = NULL, *midav = NULL, *ne = NULL;
+        _cleanup_free_ JsonVariant **array = NULL;
+        char smid[SD_ID128_STRING_MAX];
+        size_t idx = SIZE_MAX, n;
+        JsonVariant *per_machine;
+        sd_id128_t mid;
+        int r;
+
+        assert(h);
+
+        if (!h->json)
+                return -EUNATCH;
+
+        if (disk_size < USER_DISK_SIZE_MIN || disk_size > USER_DISK_SIZE_MAX)
+                return -ERANGE;
+
+        r = sd_id128_get_machine(&mid);
+        if (r < 0)
+                return r;
+
+        sd_id128_to_string(mid, smid);
+
+        r = json_variant_new_string(&midv, smid);
+        if (r < 0)
+                return r;
+
+        r = json_variant_new_array(&midav, (JsonVariant*[]) { midv }, 1);
+        if (r < 0)
+                return r;
+
+        per_machine = json_variant_by_key(h->json, "perMachine");
+        if (per_machine) {
+                size_t i;
+
+                if (!json_variant_is_array(per_machine))
+                        return -EINVAL;
+
+                n = json_variant_elements(per_machine);
+
+                array = new(JsonVariant*, n + 1);
+                if (!array)
+                        return -ENOMEM;
+
+                for (i = 0; i < n; i++) {
+                        JsonVariant *m;
+
+                        array[i] = json_variant_by_index(per_machine, i);
+
+                        if (!json_variant_is_object(array[i]))
+                                return -EINVAL;
+
+                        m = json_variant_by_key(array[i], "matchMachineId");
+                        if (!m) {
+                                /* No machineId field? Let's ignore this, but invalidate what we found so far */
+                                idx = SIZE_MAX;
+                                continue;
+                        }
+
+                        if (json_variant_equal(m, midv) ||
+                            json_variant_equal(m, midav)) {
+                                /* Matches exactly what we are looking for. Let's use this */
+                                idx = i;
+                                continue;
+                        }
+
+                        r = per_machine_id_match(m, JSON_PERMISSIVE);
+                        if (r < 0)
+                                return r;
+                        if (r > 0)
+                                /* Also matches what we are looking for, but with a broader match. In this
+                                 * case let's ignore this entry, and add a new specific one to the end. */
+                                idx = SIZE_MAX;
+                }
+
+                if (idx == SIZE_MAX)
+                        idx = n++; /* Nothing suitable found, place new entry at end */
+                else
+                        ne = json_variant_ref(array[idx]);
+
+        } else {
+                array = new(JsonVariant*, 1);
+                if (!array)
+                        return -ENOMEM;
+
+                idx = 0;
+                n = 1;
+        }
+
+        if (!ne) {
+                r = json_variant_set_field(&ne, "matchMachineId", midav);
+                if (r < 0)
+                        return r;
+        }
+
+        r = json_variant_set_field_unsigned(&ne, "diskSize", disk_size);
+        if (r < 0)
+                return r;
+
+        assert(idx < n);
+        array[idx] = ne;
+
+        r = json_variant_new_array(&new_per_machine, array, n);
+        if (r < 0)
+                return r;
+
+        r = json_variant_set_field(&h->json, "perMachine", new_per_machine);
+        if (r < 0)
+                return r;
+
+        h->disk_size = disk_size;
+        h->mask |= USER_RECORD_PER_MACHINE;
+        return 0;
+}
+
+int user_record_update_last_changed(UserRecord *h, bool with_password) {
+        _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+        usec_t n;
+        int r;
+
+        assert(h);
+
+        if (!h->json)
+                return -EUNATCH;
+
+        n = now(CLOCK_REALTIME);
+
+        /* refuse downgrading */
+        if (h->last_change_usec != UINT64_MAX && h->last_change_usec >= n)
+                return -ECHRNG;
+        if (h->last_password_change_usec != UINT64_MAX && h->last_password_change_usec >= n)
+                return -ECHRNG;
+
+        v = json_variant_ref(h->json);
+
+        r = json_variant_set_field_unsigned(&v, "lastChangeUSec", n);
+        if (r < 0)
+                return r;
+
+        if (with_password) {
+                r = json_variant_set_field_unsigned(&v, "lastPasswordChangeUSec", n);
+                if (r < 0)
+                        return r;
+
+                h->last_password_change_usec = n;
+        }
+
+        h->last_change_usec = n;
+
+        json_variant_unref(h->json);
+        h->json = TAKE_PTR(v);
+
+        h->mask |= USER_RECORD_REGULAR;
+        return 0;
+}
+
+int user_record_make_hashed_password(UserRecord *h, char **secret, bool extend) {
+        _cleanup_(json_variant_unrefp) JsonVariant *priv = NULL;
+        _cleanup_strv_free_ char **np = NULL;
+        char **i;
+        int r;
+
+        assert(h);
+        assert(secret);
+
+        /* Initializes the hashed password list from the specified plaintext passwords */
+
+        if (extend) {
+                np = strv_copy(h->hashed_password);
+                if (!np)
+                        return -ENOMEM;
+
+                strv_uniq(np);
+        }
+
+        STRV_FOREACH(i, secret) {
+                _cleanup_free_ char *salt = NULL;
+                struct crypt_data cd = {};
+                char *k;
+
+                r = make_salt(&salt);
+                if (r < 0)
+                        return r;
+
+                errno = 0;
+                k = crypt_r(*i, salt, &cd);
+                if (!k)
+                        return errno_or_else(EINVAL);
+
+                r = strv_extend(&np, k);
+                if (r < 0)
+                        return r;
+        }
+
+        priv = json_variant_ref(json_variant_by_key(h->json, "privileged"));
+
+        if (strv_isempty(np))
+                r = json_variant_filter(&priv, STRV_MAKE("hashedPassword"));
+        else {
+                _cleanup_(json_variant_unrefp) JsonVariant *new_array = NULL;
+
+                r = json_variant_new_array_strv(&new_array, np);
+                if (r < 0)
+                        return r;
+
+                r = json_variant_set_field(&priv, "hashedPassword", new_array);
+        }
+
+        r = json_variant_set_field(&h->json, "privileged", priv);
+        if (r < 0)
+                return r;
+
+        strv_free_and_replace(h->hashed_password, np);
+
+        SET_FLAG(h->mask, USER_RECORD_PRIVILEGED, !json_variant_is_blank_object(priv));
+        return 0;
+}
+
+int user_record_set_hashed_password(UserRecord *h, char **hashed_password) {
+        _cleanup_(json_variant_unrefp) JsonVariant *priv = NULL;
+        _cleanup_strv_free_ char **copy = NULL;
+        int r;
+
+        assert(h);
+
+        priv = json_variant_ref(json_variant_by_key(h->json, "privileged"));
+
+        if (strv_isempty(hashed_password))
+                r = json_variant_filter(&priv, STRV_MAKE("hashedPassword"));
+        else {
+                _cleanup_(json_variant_unrefp) JsonVariant *array = NULL;
+
+                copy = strv_copy(hashed_password);
+                if (!copy)
+                        return -ENOMEM;
+
+                strv_uniq(copy);
+
+                r = json_variant_new_array_strv(&array, copy);
+                if (r < 0)
+                        return r;
+
+                r = json_variant_set_field(&priv, "hashedPassword", array);
+        }
+        if (r < 0)
+                return r;
+
+        r = json_variant_set_field(&h->json, "privileged", priv);
+        if (r < 0)
+                return r;
+
+        strv_free_and_replace(h->hashed_password, copy);
+
+        SET_FLAG(h->mask, USER_RECORD_PRIVILEGED, !json_variant_is_blank_object(priv));
+        return 0;
+}
+
+int user_record_set_password(UserRecord *h, char **password, bool prepend) {
+        _cleanup_(json_variant_unrefp) JsonVariant *w = NULL;
+        _cleanup_(strv_free_erasep) char **e = NULL;
+        int r;
+
+        assert(h);
+
+        if (prepend) {
+                e = strv_copy(password);
+                if (!e)
+                        return -ENOMEM;
+
+                r = strv_extend_strv(&e, h->password, true);
+                if (r < 0)
+                        return r;
+
+                strv_uniq(e);
+
+                if (strv_equal(h->password, e))
+                        return 0;
+
+        } else {
+                if (strv_equal(h->password, password))
+                        return 0;
+
+                e = strv_copy(password);
+                if (!e)
+                        return -ENOMEM;
+
+                strv_uniq(e);
+        }
+
+        w = json_variant_ref(json_variant_by_key(h->json, "secret"));
+
+        if (strv_isempty(e))
+                r = json_variant_filter(&w, STRV_MAKE("password"));
+        else {
+                _cleanup_(json_variant_unrefp) JsonVariant *l = NULL;
+
+                r = json_variant_new_array_strv(&l, e);
+                if (r < 0)
+                        return r;
+
+                json_variant_sensitive(l);
+
+                r = json_variant_set_field(&w, "password", l);
+        }
+        if (r < 0)
+                return r;
+
+        r = json_variant_set_field(&h->json, "secret", w);
+        if (r < 0)
+                return r;
+
+        strv_free_and_replace(h->password, e);
+
+        SET_FLAG(h->mask, USER_RECORD_SECRET, !json_variant_is_blank_object(w));
+        return 0;
+}
+
+int user_record_set_pkcs11_pin(UserRecord *h, char **pin, bool prepend) {
+        _cleanup_(json_variant_unrefp) JsonVariant *w = NULL;
+        _cleanup_(strv_free_erasep) char **e = NULL;
+        int r;
+
+        assert(h);
+
+        if (prepend) {
+                e = strv_copy(pin);
+                if (!e)
+                        return -ENOMEM;
+
+                r = strv_extend_strv(&e, h->pkcs11_pin, true);
+                if (r < 0)
+                        return r;
+
+                strv_uniq(e);
+
+                if (strv_equal(h->pkcs11_pin, e))
+                        return 0;
+
+        } else {
+                if (strv_equal(h->pkcs11_pin, pin))
+                        return 0;
+
+                e = strv_copy(pin);
+                if (!e)
+                        return -ENOMEM;
+
+                strv_uniq(e);
+        }
+
+        w = json_variant_ref(json_variant_by_key(h->json, "secret"));
+
+        if (strv_isempty(e))
+                r = json_variant_filter(&w, STRV_MAKE("pkcs11Pin"));
+        else {
+                _cleanup_(json_variant_unrefp) JsonVariant *l = NULL;
+
+                r = json_variant_new_array_strv(&l, e);
+                if (r < 0)
+                        return r;
+
+                json_variant_sensitive(l);
+
+                r = json_variant_set_field(&w, "pkcs11Pin", l);
+        }
+        if (r < 0)
+                return r;
+
+        r = json_variant_set_field(&h->json, "secret", w);
+        if (r < 0)
+                return r;
+
+        strv_free_and_replace(h->pkcs11_pin, e);
+
+        SET_FLAG(h->mask, USER_RECORD_SECRET, !json_variant_is_blank_object(w));
+        return 0;
+}
+
+int user_record_set_pkcs11_protected_authentication_path_permitted(UserRecord *h, int b) {
+        _cleanup_(json_variant_unrefp) JsonVariant *w = NULL;
+        int r;
+
+        assert(h);
+
+        w = json_variant_ref(json_variant_by_key(h->json, "secret"));
+
+        if (b < 0)
+                r = json_variant_filter(&w, STRV_MAKE("pkcs11ProtectedAuthenticationPathPermitted"));
+        else
+                r = json_variant_set_field_boolean(&w, "pkcs11ProtectedAuthenticationPathPermitted", b);
+        if (r < 0)
+                return r;
+
+        if (json_variant_is_blank_object(w))
+                r = json_variant_filter(&h->json, STRV_MAKE("secret"));
+        else
+                r = json_variant_set_field(&h->json, "secret", w);
+        if (r < 0)
+                return r;
+
+        h->pkcs11_protected_authentication_path_permitted = b;
+
+        SET_FLAG(h->mask, USER_RECORD_SECRET, !json_variant_is_blank_object(w));
+        return 0;
+}
+
+static bool per_machine_entry_empty(JsonVariant *v) {
+        const char *k;
+        _unused_ JsonVariant *e;
+
+        JSON_VARIANT_OBJECT_FOREACH(k, e, v)
+                if (!STR_IN_SET(k, "matchMachineId", "matchHostname"))
+                        return false;
+
+        return true;
+}
+
+int user_record_set_password_change_now(UserRecord *h, int b) {
+        _cleanup_(json_variant_unrefp) JsonVariant *w = NULL;
+        JsonVariant *per_machine;
+        int r;
+
+        assert(h);
+
+        w = json_variant_ref(h->json);
+
+        if (b < 0)
+                r = json_variant_filter(&w, STRV_MAKE("passwordChangeNow"));
+        else
+                r = json_variant_set_field_boolean(&w, "passwordChangeNow", b);
+        if (r < 0)
+                return r;
+
+        /* Also drop the field from all perMachine entries */
+        per_machine = json_variant_by_key(w, "perMachine");
+        if (per_machine) {
+                _cleanup_(json_variant_unrefp) JsonVariant *array = NULL;
+                JsonVariant *e;
+
+                JSON_VARIANT_ARRAY_FOREACH(e, per_machine) {
+                        _cleanup_(json_variant_unrefp) JsonVariant *z = NULL;
+
+                        if (!json_variant_is_object(e))
+                                return -EINVAL;
+
+                        z = json_variant_ref(e);
+
+                        r = json_variant_filter(&z, STRV_MAKE("passwordChangeNow"));
+                        if (r < 0)
+                                return r;
+
+                        if (per_machine_entry_empty(z))
+                                continue;
+
+                        r = json_variant_append_array(&array, z);
+                        if (r < 0)
+                                return r;
+                }
+
+                if (json_variant_is_blank_array(array))
+                        r = json_variant_filter(&w, STRV_MAKE("perMachine"));
+                else
+                        r = json_variant_set_field(&w, "perMachine", array);
+                if (r < 0)
+                        return r;
+
+                SET_FLAG(h->mask, USER_RECORD_PER_MACHINE, !json_variant_is_blank_array(array));
+        }
+
+        json_variant_unref(h->json);
+        h->json = TAKE_PTR(w);
+
+        h->password_change_now = b;
+
+        return 0;
+}
+
+int user_record_merge_secret(UserRecord *h, UserRecord *secret) {
+        int r;
+
+        assert(h);
+
+        /* Merges the secrets from 'secret' into 'h'. */
+
+        r = user_record_set_password(h, secret->password, true);
+        if (r < 0)
+                return r;
+
+        r = user_record_set_pkcs11_pin(h, secret->pkcs11_pin, true);
+        if (r < 0)
+                return r;
+
+        if (secret->pkcs11_protected_authentication_path_permitted >= 0) {
+                r = user_record_set_pkcs11_protected_authentication_path_permitted(h, secret->pkcs11_protected_authentication_path_permitted);
+                if (r < 0)
+                        return r;
+        }
+
+        return 0;
+}
+
+int user_record_good_authentication(UserRecord *h) {
+        _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *w = NULL, *z = NULL;
+        char buf[SD_ID128_STRING_MAX];
+        uint64_t counter, usec;
+        sd_id128_t mid;
+        int r;
+
+        assert(h);
+
+        switch (h->good_authentication_counter) {
+        case UINT64_MAX:
+                counter = 1;
+                break;
+        case UINT64_MAX-1:
+                counter = h->good_authentication_counter; /* saturate */
+                break;
+        default:
+                counter = h->good_authentication_counter + 1;
+                break;
+        }
+
+        usec = now(CLOCK_REALTIME);
+
+        r = sd_id128_get_machine(&mid);
+        if (r < 0)
+                return r;
+
+        v = json_variant_ref(h->json);
+        w = json_variant_ref(json_variant_by_key(v, "status"));
+        z = json_variant_ref(json_variant_by_key(w, sd_id128_to_string(mid, buf)));
+
+        r = json_variant_set_field_unsigned(&z, "goodAuthenticationCounter", counter);
+        if (r < 0)
+                return r;
+
+        r = json_variant_set_field_unsigned(&z, "lastGoodAuthenticationUSec", usec);
+        if (r < 0)
+                return r;
+
+        r = json_variant_set_field(&w, buf, z);
+        if (r < 0)
+                return r;
+
+        r = json_variant_set_field(&v, "status", w);
+        if (r < 0)
+                return r;
+
+        json_variant_unref(h->json);
+        h->json = TAKE_PTR(v);
+
+        h->good_authentication_counter = counter;
+        h->last_good_authentication_usec = usec;
+
+        h->mask |= USER_RECORD_STATUS;
+        return 0;
+}
+
+int user_record_bad_authentication(UserRecord *h) {
+        _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *w = NULL, *z = NULL;
+        char buf[SD_ID128_STRING_MAX];
+        uint64_t counter, usec;
+        sd_id128_t mid;
+        int r;
+
+        assert(h);
+
+        switch (h->bad_authentication_counter) {
+        case UINT64_MAX:
+                counter = 1;
+                break;
+        case UINT64_MAX-1:
+                counter = h->bad_authentication_counter; /* saturate */
+                break;
+        default:
+                counter = h->bad_authentication_counter + 1;
+                break;
+        }
+
+        usec = now(CLOCK_REALTIME);
+
+        r = sd_id128_get_machine(&mid);
+        if (r < 0)
+                return r;
+
+        v = json_variant_ref(h->json);
+        w = json_variant_ref(json_variant_by_key(v, "status"));
+        z = json_variant_ref(json_variant_by_key(w, sd_id128_to_string(mid, buf)));
+
+        r = json_variant_set_field_unsigned(&z, "badAuthenticationCounter", counter);
+        if (r < 0)
+                return r;
+
+        r = json_variant_set_field_unsigned(&z, "lastBadAuthenticationUSec", usec);
+        if (r < 0)
+                return r;
+
+        r = json_variant_set_field(&w, buf, z);
+        if (r < 0)
+                return r;
+
+        r = json_variant_set_field(&v, "status", w);
+        if (r < 0)
+                return r;
+
+        json_variant_unref(h->json);
+        h->json = TAKE_PTR(v);
+
+        h->bad_authentication_counter = counter;
+        h->last_bad_authentication_usec = usec;
+
+        h->mask |= USER_RECORD_STATUS;
+        return 0;
+}
+
+int user_record_ratelimit(UserRecord *h) {
+        _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *w = NULL, *z = NULL;
+        usec_t usec, new_ratelimit_begin_usec, new_ratelimit_count;
+        char buf[SD_ID128_STRING_MAX];
+        sd_id128_t mid;
+        int r;
+
+        assert(h);
+
+        usec = now(CLOCK_REALTIME);
+
+        if (h->ratelimit_begin_usec != UINT64_MAX && h->ratelimit_begin_usec > usec)
+                /* Hmm, time is running backwards? Say no! */
+                return 0;
+        else if (h->ratelimit_begin_usec == UINT64_MAX ||
+                 usec_add(h->ratelimit_begin_usec, user_record_ratelimit_interval_usec(h)) <= usec) {
+                /* Fresh start */
+                new_ratelimit_begin_usec = usec;
+                new_ratelimit_count = 1;
+        } else if (h->ratelimit_count < user_record_ratelimit_burst(h)) {
+                /* Count up */
+                new_ratelimit_begin_usec = h->ratelimit_begin_usec;
+                new_ratelimit_count = h->ratelimit_count + 1;
+        } else
+                /* Limit hit */
+                return 0;
+
+        r = sd_id128_get_machine(&mid);
+        if (r < 0)
+                return r;
+
+        v = json_variant_ref(h->json);
+        w = json_variant_ref(json_variant_by_key(v, "status"));
+        z = json_variant_ref(json_variant_by_key(w, sd_id128_to_string(mid, buf)));
+
+        r = json_variant_set_field_unsigned(&z, "rateLimitBeginUSec", new_ratelimit_begin_usec);
+        if (r < 0)
+                return r;
+
+        r = json_variant_set_field_unsigned(&z, "rateLimitCount", new_ratelimit_count);
+        if (r < 0)
+                return r;
+
+        r = json_variant_set_field(&w, buf, z);
+        if (r < 0)
+                return r;
+
+        r = json_variant_set_field(&v, "status", w);
+        if (r < 0)
+                return r;
+
+        json_variant_unref(h->json);
+        h->json = TAKE_PTR(v);
+
+        h->ratelimit_begin_usec = new_ratelimit_begin_usec;
+        h->ratelimit_count = new_ratelimit_count;
+
+        h->mask |= USER_RECORD_STATUS;
+        return 1;
+}
+
+int user_record_is_supported(UserRecord *hr, sd_bus_error *error) {
+        assert(hr);
+
+        if (hr->disposition >= 0 && hr->disposition != USER_REGULAR)
+                return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Cannot manage anything but regular users.");
+
+        if (hr->storage >= 0 && !IN_SET(hr->storage, USER_LUKS, USER_DIRECTORY, USER_SUBVOLUME, USER_FSCRYPT, USER_CIFS))
+                return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "User record has storage type this service cannot manage.");
+
+        if (gid_is_valid(hr->gid) && hr->uid != (uid_t) hr->gid)
+                return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "User record has to have matching UID/GID fields.");
+
+        if (hr->service && !streq(hr->service, "io.systemd.Home"))
+                return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Not accepted with service not matching io.systemd.Home.");
+
+        return 0;
+}
diff --git a/src/home/user-record-util.h b/src/home/user-record-util.h
new file mode 100644 (file)
index 0000000..6afc8df
--- /dev/null
@@ -0,0 +1,58 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include "sd-bus.h"
+
+#include "user-record.h"
+#include "group-record.h"
+
+int user_record_synthesize(UserRecord *h, const char *user_name, const char *realm, const char *image_path, UserStorage storage, uid_t uid, gid_t gid);
+int group_record_synthesize(GroupRecord *g, UserRecord *u);
+
+typedef enum UserReconcileMode {
+        USER_RECONCILE_ANY,
+        USER_RECONCILE_REQUIRE_NEWER,          /* host version must be newer than embedded version */
+        USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL, /* similar, but may also be equal */
+        _USER_RECONCILE_MODE_MAX,
+        _USER_RECONCILE_MODE_INVALID = -1,
+} UserReconcileMode;
+
+enum { /* return values */
+        USER_RECONCILE_HOST_WON,
+        USER_RECONCILE_EMBEDDED_WON,
+        USER_RECONCILE_IDENTICAL,
+};
+
+int user_record_reconcile(UserRecord *host, UserRecord *embedded, UserReconcileMode mode, UserRecord **ret);
+int user_record_add_binding(UserRecord *h, UserStorage storage, const char *image_path, sd_id128_t partition_uuid, sd_id128_t luks_uuid, sd_id128_t fs_uuid, const char *luks_cipher, const char *luks_cipher_mode, uint64_t luks_volume_key_size, const char *file_system_type, const char *home_directory, uid_t uid, gid_t gid);
+
+/* Results of the two test functions below. */
+enum {
+        USER_TEST_UNDEFINED, /* Returned by user_record_test_image_path() if the storage type knows no image paths */
+        USER_TEST_ABSENT,
+        USER_TEST_EXISTS,
+        USER_TEST_MOUNTED,   /* Only applies to user_record_test_home_directory(), when the home directory exists. */
+        USER_TEST_MAYBE,     /* Only applies to LUKS devices: block device exists, but we don't know if it's the right one */
+};
+
+int user_record_test_home_directory(UserRecord *h);
+int user_record_test_home_directory_and_warn(UserRecord *h);
+int user_record_test_image_path(UserRecord *h);
+int user_record_test_image_path_and_warn(UserRecord *h);
+
+int user_record_test_secret(UserRecord *h, UserRecord *secret);
+
+int user_record_update_last_changed(UserRecord *h, bool with_password);
+int user_record_set_disk_size(UserRecord *h, uint64_t disk_size);
+int user_record_set_password(UserRecord *h, char **password, bool prepend);
+int user_record_make_hashed_password(UserRecord *h, char **password, bool extend);
+int user_record_set_hashed_password(UserRecord *h, char **hashed_password);
+int user_record_set_pkcs11_pin(UserRecord *h, char **pin, bool prepend);
+int user_record_set_pkcs11_protected_authentication_path_permitted(UserRecord *h, int b);
+int user_record_set_password_change_now(UserRecord *h, int b);
+int user_record_merge_secret(UserRecord *h, UserRecord *secret);
+int user_record_good_authentication(UserRecord *h);
+int user_record_bad_authentication(UserRecord *h);
+int user_record_ratelimit(UserRecord *h);
+
+int user_record_is_supported(UserRecord *hr, sd_bus_error *error);
index 4e23edd923273ea4cf48968ca8d2bb73e4cfb8c8..174f1228af29fdc35cdbd2c14322de7543fb105f 100644 (file)
@@ -105,5 +105,35 @@ BUS_ERROR_MAP_ELF_REGISTER const sd_bus_error_map bus_common_errors[] = {
         SD_BUS_ERROR_MAP(BUS_ERROR_SPEED_METER_INACTIVE,         EOPNOTSUPP),
         SD_BUS_ERROR_MAP(BUS_ERROR_UNMANAGED_INTERFACE,          EOPNOTSUPP),
 
+        SD_BUS_ERROR_MAP(BUS_ERROR_NO_SUCH_HOME,                 EEXIST),
+        SD_BUS_ERROR_MAP(BUS_ERROR_UID_IN_USE,                   EEXIST),
+        SD_BUS_ERROR_MAP(BUS_ERROR_USER_NAME_EXISTS,             EEXIST),
+        SD_BUS_ERROR_MAP(BUS_ERROR_HOME_EXISTS,                  EEXIST),
+        SD_BUS_ERROR_MAP(BUS_ERROR_HOME_ALREADY_ACTIVE,          EALREADY),
+        SD_BUS_ERROR_MAP(BUS_ERROR_HOME_ALREADY_FIXATED,         EALREADY),
+        SD_BUS_ERROR_MAP(BUS_ERROR_HOME_UNFIXATED,               EADDRNOTAVAIL),
+        SD_BUS_ERROR_MAP(BUS_ERROR_HOME_NOT_ACTIVE,              EALREADY),
+        SD_BUS_ERROR_MAP(BUS_ERROR_HOME_ABSENT,                  EREMOTE),
+        SD_BUS_ERROR_MAP(BUS_ERROR_HOME_BUSY,                    EBUSY),
+        SD_BUS_ERROR_MAP(BUS_ERROR_BAD_PASSWORD,                 ENOKEY),
+        SD_BUS_ERROR_MAP(BUS_ERROR_LOW_PASSWORD_QUALITY,         EUCLEAN),
+        SD_BUS_ERROR_MAP(BUS_ERROR_BAD_PASSWORD_AND_NO_TOKEN,    EBADSLT),
+        SD_BUS_ERROR_MAP(BUS_ERROR_TOKEN_PIN_NEEDED,             ENOANO),
+        SD_BUS_ERROR_MAP(BUS_ERROR_TOKEN_PROTECTED_AUTHENTICATION_PATH_NEEDED, ERFKILL),
+        SD_BUS_ERROR_MAP(BUS_ERROR_TOKEN_PIN_LOCKED,             EOWNERDEAD),
+        SD_BUS_ERROR_MAP(BUS_ERROR_TOKEN_BAD_PIN,                ENOLCK),
+        SD_BUS_ERROR_MAP(BUS_ERROR_TOKEN_BAD_PIN_FEW_TRIES_LEFT, ETOOMANYREFS),
+        SD_BUS_ERROR_MAP(BUS_ERROR_TOKEN_BAD_PIN_ONE_TRY_LEFT,   EUCLEAN),
+        SD_BUS_ERROR_MAP(BUS_ERROR_BAD_SIGNATURE,                EKEYREJECTED),
+        SD_BUS_ERROR_MAP(BUS_ERROR_HOME_RECORD_MISMATCH,         EUCLEAN),
+        SD_BUS_ERROR_MAP(BUS_ERROR_HOME_RECORD_DOWNGRADE,        ESTALE),
+        SD_BUS_ERROR_MAP(BUS_ERROR_HOME_RECORD_SIGNED,           EROFS),
+        SD_BUS_ERROR_MAP(BUS_ERROR_BAD_HOME_SIZE,                ERANGE),
+        SD_BUS_ERROR_MAP(BUS_ERROR_NO_PRIVATE_KEY,               ENOPKG),
+        SD_BUS_ERROR_MAP(BUS_ERROR_HOME_LOCKED,                  ENOEXEC),
+        SD_BUS_ERROR_MAP(BUS_ERROR_HOME_NOT_LOCKED,              ENOEXEC),
+        SD_BUS_ERROR_MAP(BUS_ERROR_TOO_MANY_OPERATIONS,          ENOBUFS),
+        SD_BUS_ERROR_MAP(BUS_ERROR_AUTHENTICATION_LIMIT_HIT,     ETOOMANYREFS),
+
         SD_BUS_ERROR_MAP_END
 };
index 8da56551f697111b0a64a0e29509a4026ea30a94..e5f92b9ec261e3c669cf502348ffc6b2625a9967 100644 (file)
 #define BUS_ERROR_SPEED_METER_INACTIVE "org.freedesktop.network1.SpeedMeterInactive"
 #define BUS_ERROR_UNMANAGED_INTERFACE "org.freedesktop.network1.UnmanagedInterface"
 
+#define BUS_ERROR_NO_SUCH_HOME "org.freedesktop.home1.NoSuchHome"
+#define BUS_ERROR_UID_IN_USE "org.freedesktop.home1.UIDInUse"
+#define BUS_ERROR_USER_NAME_EXISTS "org.freedesktop.home1.UserNameExists"
+#define BUS_ERROR_HOME_EXISTS "org.freedesktop.home1.HomeExists"
+#define BUS_ERROR_HOME_ALREADY_ACTIVE "org.freedesktop.home1.HomeAlreadyActive"
+#define BUS_ERROR_HOME_ALREADY_FIXATED "org.freedesktop.home1.HomeAlreadyFixated"
+#define BUS_ERROR_HOME_UNFIXATED "org.freedesktop.home1.HomeUnfixated"
+#define BUS_ERROR_HOME_NOT_ACTIVE "org.freedesktop.home1.HomeNotActive"
+#define BUS_ERROR_HOME_ABSENT "org.freedesktop.home1.HomeAbsent"
+#define BUS_ERROR_HOME_BUSY "org.freedesktop.home1.HomeBusy"
+#define BUS_ERROR_BAD_PASSWORD "org.freedesktop.home1.BadPassword"
+#define BUS_ERROR_LOW_PASSWORD_QUALITY "org.freedesktop.home1.LowPasswordQuality"
+#define BUS_ERROR_BAD_PASSWORD_AND_NO_TOKEN "org.freedesktop.home1.BadPasswordAndNoToken"
+#define BUS_ERROR_TOKEN_PIN_NEEDED "org.freedesktop.home1.TokenPinNeeded"
+#define BUS_ERROR_TOKEN_PROTECTED_AUTHENTICATION_PATH_NEEDED "org.freedesktop.home1.TokenProtectedAuthenticationPathNeeded"
+#define BUS_ERROR_TOKEN_PIN_LOCKED "org.freedesktop.home1.TokenPinLocked"
+#define BUS_ERROR_TOKEN_BAD_PIN "org.freedesktop.home1.BadPin"
+#define BUS_ERROR_TOKEN_BAD_PIN_FEW_TRIES_LEFT "org.freedesktop.home1.BadPinFewTriesLeft"
+#define BUS_ERROR_TOKEN_BAD_PIN_ONE_TRY_LEFT "org.freedesktop.home1.BadPinOneTryLeft"
+#define BUS_ERROR_BAD_SIGNATURE "org.freedesktop.home1.BadSignature"
+#define BUS_ERROR_HOME_RECORD_MISMATCH "org.freedesktop.home1.RecordMismatch"
+#define BUS_ERROR_HOME_RECORD_DOWNGRADE "org.freedesktop.home1.RecordDowngrade"
+#define BUS_ERROR_HOME_RECORD_SIGNED "org.freedesktop.home1.RecordSigned"
+#define BUS_ERROR_BAD_HOME_SIZE "org.freedesktop.home1.BadHomeSize"
+#define BUS_ERROR_NO_PRIVATE_KEY "org.freedesktop.home1.NoPrivateKey"
+#define BUS_ERROR_HOME_LOCKED "org.freedesktop.home1.HomeLocked"
+#define BUS_ERROR_HOME_NOT_LOCKED "org.freedesktop.home1.HomeNotLocked"
+#define BUS_ERROR_NO_DISK_SPACE "org.freedesktop.home1.NoDiskSpace"
+#define BUS_ERROR_TOO_MANY_OPERATIONS "org.freedesktop.home1.TooManyOperations"
+#define BUS_ERROR_AUTHENTICATION_LIMIT_HIT "org.freedesktop.home1.AuthenticationLimitHit"
+
 BUS_ERROR_MAP_ELF_USE(bus_common_errors);
index dcceb076d6e7b72ef7d12470914e62cb14ac2603..9dc649d8d9e642a2a1306070a059ab63667cb1e4 100644 (file)
@@ -23,6 +23,7 @@
 #define GPT_SRV         SD_ID128_MAKE(3b,8f,84,25,20,e0,4f,3b,90,7f,1a,25,a7,6f,98,e8)
 #define GPT_VAR         SD_ID128_MAKE(4d,21,b0,16,b5,34,45,c2,a9,fb,5c,16,e0,91,fd,2d)
 #define GPT_TMP         SD_ID128_MAKE(7e,c6,f5,57,3b,c5,4a,ca,b2,93,16,ef,5d,f6,39,d1)
+#define GPT_USER_HOME   SD_ID128_MAKE(77,3f,91,ef,66,d4,49,b5,bd,83,d6,83,bf,40,ad,16)
 
 /* Verity partitions for the root partitions above (we only define them for the root partitions, because only they are
  * are commonly read-only and hence suitable for verity). */
index 581f44f99efdfa3b27951f274086cce97371cc35..d99cafb39fcd4dd33bbbfa49dbb6451e04a1c494 100644 (file)
@@ -195,6 +195,8 @@ in_units = [
         ['systemd-portabled.service',            'ENABLE_PORTABLED',
          'dbus-org.freedesktop.portable1.service'],
         ['systemd-userdbd.service',              'ENABLE_USERDB'],
+        ['systemd-homed.service',                'ENABLE_HOMED',
+         'multi-user.target.wants/ dbus-org.freedesktop.home1.service'],
         ['systemd-quotacheck.service',           'ENABLE_QUOTACHECK'],
         ['systemd-random-seed.service',          'ENABLE_RANDOMSEED',
          'sysinit.target.wants/'],
diff --git a/units/systemd-homed.service.in b/units/systemd-homed.service.in
new file mode 100644 (file)
index 0000000..512804c
--- /dev/null
@@ -0,0 +1,36 @@
+#  SPDX-License-Identifier: LGPL-2.1+
+#
+#  This file is part of systemd.
+#
+#  systemd is free software; you can redistribute it and/or modify it
+#  under the terms of the GNU Lesser General Public License as published by
+#  the Free Software Foundation; either version 2.1 of the License, or
+#  (at your option) any later version.
+
+[Unit]
+Description=Home Manager
+Documentation=man:systemd-homed.service(8)
+RequiresMountsFor=/home
+
+[Service]
+BusName=org.freedesktop.home1
+CapabilityBoundingSet=CAP_SYS_ADMIN CAP_CHOWN CAP_DAC_OVERRIDE CAP_FOWNER CAP_FSETID CAP_SETGID CAP_SETUID
+DeviceAllow=/dev/loop-control rw
+DeviceAllow=/dev/mapper/control rw
+DeviceAllow=block-* rw
+ExecStart=@rootlibexecdir@/systemd-homed
+IPAddressDeny=any
+KillMode=mixed
+LimitNOFILE=@HIGH_RLIMIT_NOFILE@
+LockPersonality=yes
+MemoryDenyWriteExecute=yes
+NoNewPrivileges=yes
+PrivateNetwork=yes
+RestrictAddressFamilies=AF_UNIX AF_NETLINK AF_ALG
+RestrictNamespaces=mnt
+RestrictRealtime=yes
+StateDirectory=systemd/home
+SystemCallArchitectures=native
+SystemCallErrorNumber=EPERM
+SystemCallFilter=@system-service @mount
+@SERVICE_WATCHDOG@