summaryrefslogtreecommitdiffstats
path: root/src/home
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 13:00:47 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 13:00:47 +0000
commit2cb7e0aaedad73b076ea18c6900b0e86c5760d79 (patch)
treeda68ca54bb79f4080079bf0828acda937593a4e1 /src/home
parentInitial commit. (diff)
downloadsystemd-2cb7e0aaedad73b076ea18c6900b0e86c5760d79.tar.xz
systemd-2cb7e0aaedad73b076ea18c6900b0e86c5760d79.zip
Adding upstream version 247.3.upstream/247.3upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/home')
-rw-r--r--src/home/home-util.c135
-rw-r--r--src/home/home-util.h23
-rw-r--r--src/home/homectl-fido2.c534
-rw-r--r--src/home/homectl-fido2.h10
-rw-r--r--src/home/homectl-pkcs11.c477
-rw-r--r--src/home/homectl-pkcs11.h11
-rw-r--r--src/home/homectl-recovery-key.c199
-rw-r--r--src/home/homectl-recovery-key.h6
-rw-r--r--src/home/homectl.c3381
-rw-r--r--src/home/homed-bus.c66
-rw-r--r--src/home/homed-bus.h10
-rw-r--r--src/home/homed-conf.c54
-rw-r--r--src/home/homed-conf.h12
-rw-r--r--src/home/homed-gperf.gperf21
-rw-r--r--src/home/homed-home-bus.c953
-rw-r--r--src/home/homed-home-bus.h34
-rw-r--r--src/home/homed-home.c2836
-rw-r--r--src/home/homed-home.h171
-rw-r--r--src/home/homed-manager-bus.c899
-rw-r--r--src/home/homed-manager-bus.h6
-rw-r--r--src/home/homed-manager.c1742
-rw-r--r--src/home/homed-manager.h70
-rw-r--r--src/home/homed-operation.c76
-rw-r--r--src/home/homed-operation.h63
-rw-r--r--src/home/homed-varlink.c366
-rw-r--r--src/home/homed-varlink.h8
-rw-r--r--src/home/homed.c51
-rw-r--r--src/home/homed.conf16
-rw-r--r--src/home/homework-cifs.c213
-rw-r--r--src/home/homework-cifs.h11
-rw-r--r--src/home/homework-directory.c242
-rw-r--r--src/home/homework-directory.h10
-rw-r--r--src/home/homework-fido2.c197
-rw-r--r--src/home/homework-fido2.h6
-rw-r--r--src/home/homework-fscrypt.c643
-rw-r--r--src/home/homework-fscrypt.h10
-rw-r--r--src/home/homework-luks.c3087
-rw-r--r--src/home/homework-luks.h46
-rw-r--r--src/home/homework-mount.c96
-rw-r--r--src/home/homework-mount.h8
-rw-r--r--src/home/homework-pkcs11.c104
-rw-r--r--src/home/homework-pkcs11.h21
-rw-r--r--src/home/homework-quota.c124
-rw-r--r--src/home/homework-quota.h8
-rw-r--r--src/home/homework.c1747
-rw-r--r--src/home/homework.h70
-rw-r--r--src/home/meson.build122
-rw-r--r--src/home/modhex.c74
-rw-r--r--src/home/modhex.h14
-rw-r--r--src/home/org.freedesktop.home1.conf193
-rw-r--r--src/home/org.freedesktop.home1.policy72
-rw-r--r--src/home/org.freedesktop.home1.service7
-rw-r--r--src/home/pam_systemd_home.c1070
-rw-r--r--src/home/pam_systemd_home.sym12
-rw-r--r--src/home/test-modhex.c51
-rw-r--r--src/home/user-record-pwquality.c91
-rw-r--r--src/home/user-record-pwquality.h7
-rw-r--r--src/home/user-record-sign.c176
-rw-r--r--src/home/user-record-sign.h19
-rw-r--r--src/home/user-record-util.c1366
-rw-r--r--src/home/user-record-util.h61
61 files changed, 22208 insertions, 0 deletions
diff --git a/src/home/home-util.c b/src/home/home-util.c
new file mode 100644
index 0000000..cd971b7
--- /dev/null
+++ b/src/home/home-util.c
@@ -0,0 +1,135 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "dns-domain.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 client-side
+ * we usually validate with the 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, 0))
+ 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);
+}
+
+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");
+}
+
+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;
+
+ (void) sd_bus_message_sensitive(m);
+
+ return sd_bus_message_append(m, "s", formatted);
+}
diff --git a/src/home/home-util.h b/src/home/home-util.h
new file mode 100644
index 0000000..fba1c7d
--- /dev/null
+++ b/src/home/home-util.h
@@ -0,0 +1,23 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#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);
+
+bool supported_fstype(const char *fstype);
+
+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 timeout */
+#define HOME_SLOW_BUS_CALL_TIMEOUT_USEC (2*USEC_PER_MINUTE)
diff --git a/src/home/homectl-fido2.c b/src/home/homectl-fido2.c
new file mode 100644
index 0000000..5557b70
--- /dev/null
+++ b/src/home/homectl-fido2.c
@@ -0,0 +1,534 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#if HAVE_LIBFIDO2
+#include <fido.h>
+#endif
+
+#include "ask-password-api.h"
+#include "errno-util.h"
+#include "format-table.h"
+#include "hexdecoct.h"
+#include "homectl-fido2.h"
+#include "homectl-pkcs11.h"
+#include "libcrypt-util.h"
+#include "locale-util.h"
+#include "memory-util.h"
+#include "random-util.h"
+#include "strv.h"
+
+#if HAVE_LIBFIDO2
+static int add_fido2_credential_id(
+ JsonVariant **v,
+ const void *cid,
+ size_t cid_size) {
+
+ _cleanup_(json_variant_unrefp) JsonVariant *w = NULL;
+ _cleanup_strv_free_ char **l = NULL;
+ _cleanup_free_ char *escaped = NULL;
+ int r;
+
+ assert(v);
+ assert(cid);
+
+ r = base64mem(cid, cid_size, &escaped);
+ if (r < 0)
+ return log_error_errno(r, "Failed to base64 encode FIDO2 credential ID: %m");
+
+ w = json_variant_ref(json_variant_by_key(*v, "fido2HmacCredential"));
+ if (w) {
+ r = json_variant_strv(w, &l);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse FIDO2 credential ID list: %m");
+
+ if (strv_contains(l, escaped))
+ return 0;
+ }
+
+ r = strv_extend(&l, escaped);
+ if (r < 0)
+ return log_oom();
+
+ w = json_variant_unref(w);
+ r = json_variant_new_array_strv(&w, l);
+ if (r < 0)
+ return log_error_errno(r, "Failed to create FIDO2 credential ID JSON: %m");
+
+ r = json_variant_set_field(v, "fido2HmacCredential", w);
+ if (r < 0)
+ return log_error_errno(r, "Failed to update FIDO2 credential ID: %m");
+
+ return 0;
+}
+
+static int add_fido2_salt(
+ JsonVariant **v,
+ const void *cid,
+ size_t cid_size,
+ const void *fido2_salt,
+ size_t fido2_salt_size,
+ const void *secret,
+ size_t secret_size) {
+
+ _cleanup_(json_variant_unrefp) JsonVariant *l = NULL, *w = NULL, *e = NULL;
+ _cleanup_(erase_and_freep) char *base64_encoded = NULL, *hashed = NULL;
+ int r;
+
+ /* Before using UNIX hashing on the supplied key we base64 encode it, since crypt_r() and friends
+ * expect a NUL terminated string, and we use a binary key */
+ r = base64mem(secret, secret_size, &base64_encoded);
+ if (r < 0)
+ return log_error_errno(r, "Failed to base64 encode secret key: %m");
+
+ r = hash_password(base64_encoded, &hashed);
+ if (r < 0)
+ return log_error_errno(errno_or_else(EINVAL), "Failed to UNIX hash secret key: %m");
+
+ r = json_build(&e, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("credential", JSON_BUILD_BASE64(cid, cid_size)),
+ JSON_BUILD_PAIR("salt", JSON_BUILD_BASE64(fido2_salt, fido2_salt_size)),
+ JSON_BUILD_PAIR("hashedPassword", JSON_BUILD_STRING(hashed))));
+ if (r < 0)
+ return log_error_errno(r, "Failed to build FIDO2 salt JSON key object: %m");
+
+ w = json_variant_ref(json_variant_by_key(*v, "privileged"));
+ l = json_variant_ref(json_variant_by_key(w, "fido2HmacSalt"));
+
+ r = json_variant_append_array(&l, e);
+ if (r < 0)
+ return log_error_errno(r, "Failed append FIDO2 salt: %m");
+
+ r = json_variant_set_field(&w, "fido2HmacSalt", l);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set FDO2 salt: %m");
+
+ r = json_variant_set_field(v, "privileged", w);
+ if (r < 0)
+ return log_error_errno(r, "Failed to update privileged field: %m");
+
+ return 0;
+}
+#endif
+
+#define FIDO2_SALT_SIZE 32
+
+int identity_add_fido2_parameters(
+ JsonVariant **v,
+ const char *device) {
+
+#if HAVE_LIBFIDO2
+ _cleanup_(fido_cbor_info_free) fido_cbor_info_t *di = NULL;
+ _cleanup_(fido_assert_free) fido_assert_t *a = NULL;
+ _cleanup_(erase_and_freep) char *used_pin = NULL;
+ _cleanup_(fido_cred_free) fido_cred_t *c = NULL;
+ _cleanup_(fido_dev_free) fido_dev_t *d = NULL;
+ _cleanup_(erase_and_freep) void *salt = NULL;
+ JsonVariant *un, *realm, *rn;
+ bool found_extension = false;
+ const void *cid, *secret;
+ const char *fido_un;
+ size_t n, cid_size, secret_size;
+ char **e;
+ int r;
+
+ /* Construction is like this: we generate a salt of 32 bytes. We then ask the FIDO2 device to
+ * HMAC-SHA256 it for us with its internal key. The result is the key used by LUKS and account
+ * authentication. LUKS and UNIX password auth all do their own salting before hashing, so that FIDO2
+ * device never sees the volume key.
+ *
+ * S = HMAC-SHA256(I, D)
+ *
+ * with: S → LUKS/account authentication key (never stored)
+ * I → internal key on FIDO2 device (stored in the FIDO2 device)
+ * D → salt we generate here (stored in the privileged part of the JSON record)
+ *
+ */
+
+ assert(v);
+ assert(device);
+
+ salt = malloc(FIDO2_SALT_SIZE);
+ if (!salt)
+ return log_oom();
+
+ r = genuine_random_bytes(salt, FIDO2_SALT_SIZE, RANDOM_BLOCK);
+ if (r < 0)
+ return log_error_errno(r, "Failed to generate salt: %m");
+
+ d = fido_dev_new();
+ if (!d)
+ return log_oom();
+
+ r = fido_dev_open(d, device);
+ if (r != FIDO_OK)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO),
+ "Failed to open FIDO2 device %s: %s", device, fido_strerr(r));
+
+ if (!fido_dev_is_fido2(d))
+ return log_error_errno(SYNTHETIC_ERRNO(ENODEV),
+ "Specified device %s is not a FIDO2 device.", device);
+
+ di = fido_cbor_info_new();
+ if (!di)
+ return log_oom();
+
+ r = fido_dev_get_cbor_info(d, di);
+ if (r != FIDO_OK)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO),
+ "Failed to get CBOR device info for %s: %s", device, fido_strerr(r));
+
+ e = fido_cbor_info_extensions_ptr(di);
+ n = fido_cbor_info_extensions_len(di);
+
+ for (size_t i = 0; i < n; i++)
+ if (streq(e[i], "hmac-secret")) {
+ found_extension = true;
+ break;
+ }
+
+ if (!found_extension)
+ return log_error_errno(SYNTHETIC_ERRNO(ENODEV),
+ "Specified device %s is a FIDO2 device, but does not support the required HMAC-SECRET extension.", device);
+
+ c = fido_cred_new();
+ if (!c)
+ return log_oom();
+
+ r = fido_cred_set_extensions(c, FIDO_EXT_HMAC_SECRET);
+ if (r != FIDO_OK)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO),
+ "Failed to enable HMAC-SECRET extension on FIDO2 credential: %s", fido_strerr(r));
+
+ r = fido_cred_set_rp(c, "io.systemd.home", "Home Directory");
+ if (r != FIDO_OK)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO),
+ "Failed to set FIDO2 credential relying party ID/name: %s", fido_strerr(r));
+
+ r = fido_cred_set_type(c, COSE_ES256);
+ if (r != FIDO_OK)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO),
+ "Failed to set FIDO2 credential type to ES256: %s", fido_strerr(r));
+
+ un = json_variant_by_key(*v, "userName");
+ if (!un)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "userName field of user record is missing");
+ if (!json_variant_is_string(un))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "userName field of user record is not a string");
+
+ realm = json_variant_by_key(*v, "realm");
+ if (realm) {
+ if (!json_variant_is_string(realm))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "realm field of user record is not a string");
+
+ fido_un = strjoina(json_variant_string(un), json_variant_string(realm));
+ } else
+ fido_un = json_variant_string(un);
+
+ rn = json_variant_by_key(*v, "realName");
+ if (rn && !json_variant_is_string(rn))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "realName field of user record is not a string");
+
+ r = fido_cred_set_user(c,
+ (const unsigned char*) fido_un, strlen(fido_un), /* We pass the user ID and name as the same */
+ fido_un,
+ rn ? json_variant_string(rn) : NULL,
+ NULL /* icon URL */);
+ if (r != FIDO_OK)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO),
+ "Failed to set FIDO2 credential user data: %s", fido_strerr(r));
+
+ r = fido_cred_set_clientdata_hash(c, (const unsigned char[32]) {}, 32);
+ if (r != FIDO_OK)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO),
+ "Failed to set FIDO2 client data hash: %s", fido_strerr(r));
+
+ r = fido_cred_set_rk(c, FIDO_OPT_FALSE);
+ if (r != FIDO_OK)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO),
+ "Failed to turn off FIDO2 resident key option of credential: %s", fido_strerr(r));
+
+ r = fido_cred_set_uv(c, FIDO_OPT_FALSE);
+ if (r != FIDO_OK)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO),
+ "Failed to turn off FIDO2 user verification option of credential: %s", fido_strerr(r));
+
+ log_info("Initializing FIDO2 credential on security token.");
+
+ log_notice("%s%s(Hint: This might require verification of user presence on security token.)",
+ emoji_enabled() ? special_glyph(SPECIAL_GLYPH_TOUCH) : "",
+ emoji_enabled() ? " " : "");
+
+ r = fido_dev_make_cred(d, c, NULL);
+ if (r == FIDO_ERR_PIN_REQUIRED) {
+ _cleanup_free_ char *text = NULL;
+
+ if (asprintf(&text, "Please enter security token PIN:") < 0)
+ return log_oom();
+
+ for (;;) {
+ _cleanup_(strv_free_erasep) char **pin = NULL;
+ char **i;
+
+ r = ask_password_auto(text, "user-home", NULL, "fido2-pin", USEC_INFINITY, 0, &pin);
+ if (r < 0)
+ return log_error_errno(r, "Failed to acquire user PIN: %m");
+
+ r = FIDO_ERR_PIN_INVALID;
+ STRV_FOREACH(i, pin) {
+ if (isempty(*i)) {
+ log_info("PIN may not be empty.");
+ continue;
+ }
+
+ r = fido_dev_make_cred(d, c, *i);
+ if (r == FIDO_OK) {
+ used_pin = strdup(*i);
+ if (!used_pin)
+ return log_oom();
+ break;
+ }
+ if (r != FIDO_ERR_PIN_INVALID)
+ break;
+ }
+
+ if (r != FIDO_ERR_PIN_INVALID)
+ break;
+
+ log_notice("PIN incorrect, please try again.");
+ }
+ }
+ if (r == FIDO_ERR_PIN_AUTH_BLOCKED)
+ return log_notice_errno(SYNTHETIC_ERRNO(EPERM),
+ "Token PIN is currently blocked, please remove and reinsert token.");
+ if (r == FIDO_ERR_ACTION_TIMEOUT)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOSTR),
+ "Token action timeout. (User didn't interact with token quickly enough.)");
+ if (r != FIDO_OK)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO),
+ "Failed to generate FIDO2 credential: %s", fido_strerr(r));
+
+ cid = fido_cred_id_ptr(c);
+ if (!cid)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to get FIDO2 credential ID.");
+
+ cid_size = fido_cred_id_len(c);
+
+ a = fido_assert_new();
+ if (!a)
+ return log_oom();
+
+ r = fido_assert_set_extensions(a, FIDO_EXT_HMAC_SECRET);
+ if (r != FIDO_OK)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO),
+ "Failed to enable HMAC-SECRET extension on FIDO2 assertion: %s", fido_strerr(r));
+
+ r = fido_assert_set_hmac_salt(a, salt, FIDO2_SALT_SIZE);
+ if (r != FIDO_OK)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO),
+ "Failed to set salt on FIDO2 assertion: %s", fido_strerr(r));
+
+ r = fido_assert_set_rp(a, "io.systemd.home");
+ if (r != FIDO_OK)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO),
+ "Failed to set FIDO2 assertion ID: %s", fido_strerr(r));
+
+ r = fido_assert_set_clientdata_hash(a, (const unsigned char[32]) {}, 32);
+ if (r != FIDO_OK)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO),
+ "Failed to set FIDO2 assertion client data hash: %s", fido_strerr(r));
+
+ r = fido_assert_allow_cred(a, cid, cid_size);
+ if (r != FIDO_OK)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO),
+ "Failed to add FIDO2 assertion credential ID: %s", fido_strerr(r));
+
+ r = fido_assert_set_up(a, FIDO_OPT_FALSE);
+ if (r != FIDO_OK)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO),
+ "Failed to turn off FIDO2 assertion user presence: %s", fido_strerr(r));
+
+ log_info("Generating secret key on FIDO2 security token.");
+
+ r = fido_dev_get_assert(d, a, used_pin);
+ if (r == FIDO_ERR_UP_REQUIRED) {
+ r = fido_assert_set_up(a, FIDO_OPT_TRUE);
+ if (r != FIDO_OK)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO),
+ "Failed to turn on FIDO2 assertion user presence: %s", fido_strerr(r));
+
+ log_notice("%s%sIn order to allow secret key generation, please verify presence on security token.",
+ emoji_enabled() ? special_glyph(SPECIAL_GLYPH_TOUCH) : "",
+ emoji_enabled() ? " " : "");
+
+ r = fido_dev_get_assert(d, a, used_pin);
+ }
+ if (r == FIDO_ERR_ACTION_TIMEOUT)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOSTR),
+ "Token action timeout. (User didn't interact with token quickly enough.)");
+ if (r != FIDO_OK)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO),
+ "Failed to ask token for assertion: %s", fido_strerr(r));
+
+ secret = fido_assert_hmac_secret_ptr(a, 0);
+ if (!secret)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to retrieve HMAC secret.");
+
+ secret_size = fido_assert_hmac_secret_len(a, 0);
+
+ r = add_fido2_credential_id(v, cid, cid_size);
+ if (r < 0)
+ return r;
+
+ r = add_fido2_salt(v,
+ cid,
+ cid_size,
+ salt,
+ FIDO2_SALT_SIZE,
+ secret,
+ secret_size);
+ if (r < 0)
+ return r;
+
+ /* If we acquired the PIN also include it in the secret section of the record, so that systemd-homed
+ * can use it if it needs to, given that it likely needs to decrypt the key again to pass to LUKS or
+ * fscrypt. */
+ r = identity_add_token_pin(v, used_pin);
+ if (r < 0)
+ return r;
+
+ return 0;
+#else
+ return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
+ "FIDO2 tokens not supported on this build.");
+#endif
+}
+
+int list_fido2_devices(void) {
+#if HAVE_LIBFIDO2
+ _cleanup_(table_unrefp) Table *t = NULL;
+ size_t allocated = 64, found = 0;
+ fido_dev_info_t *di = NULL;
+ int r;
+
+ di = fido_dev_info_new(allocated);
+ if (!di)
+ return log_oom();
+
+ r = fido_dev_info_manifest(di, allocated, &found);
+ if (r == FIDO_ERR_INTERNAL || (r == FIDO_OK && found == 0)) {
+ /* The library returns FIDO_ERR_INTERNAL when no devices are found. I wish it wouldn't. */
+ log_info("No FIDO2 devices found.");
+ r = 0;
+ goto finish;
+ }
+ if (r != FIDO_OK) {
+ r = log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to enumerate FIDO2 devices: %s", fido_strerr(r));
+ goto finish;
+ }
+
+ t = table_new("path", "manufacturer", "product");
+ if (!t) {
+ r = log_oom();
+ goto finish;
+ }
+
+ for (size_t i = 0; i < found; i++) {
+ const fido_dev_info_t *entry;
+
+ entry = fido_dev_info_ptr(di, i);
+ if (!entry) {
+ r = log_error_errno(SYNTHETIC_ERRNO(EIO),
+ "Failed to get device information for FIDO device %zu.", i);
+ goto finish;
+ }
+
+ r = table_add_many(
+ t,
+ TABLE_PATH, fido_dev_info_path(entry),
+ TABLE_STRING, fido_dev_info_manufacturer_string(entry),
+ TABLE_STRING, fido_dev_info_product_string(entry));
+ if (r < 0) {
+ table_log_add_error(r);
+ goto finish;
+ }
+ }
+
+ r = table_print(t, stdout);
+ if (r < 0) {
+ log_error_errno(r, "Failed to show device table: %m");
+ goto finish;
+ }
+
+ r = 0;
+
+finish:
+ fido_dev_info_free(&di, allocated);
+ return r;
+#else
+ return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
+ "FIDO2 tokens not supported on this build.");
+#endif
+}
+
+int find_fido2_auto(char **ret) {
+#if HAVE_LIBFIDO2
+ _cleanup_free_ char *copy = NULL;
+ size_t di_size = 64, found = 0;
+ const fido_dev_info_t *entry;
+ fido_dev_info_t *di = NULL;
+ const char *path;
+ int r;
+
+ di = fido_dev_info_new(di_size);
+ if (!di)
+ return log_oom();
+
+ r = fido_dev_info_manifest(di, di_size, &found);
+ if (r == FIDO_ERR_INTERNAL || (r == FIDO_OK && found == 0)) {
+ /* The library returns FIDO_ERR_INTERNAL when no devices are found. I wish it wouldn't. */
+ r = log_error_errno(SYNTHETIC_ERRNO(ENODEV), "No FIDO2 devices found.");
+ goto finish;
+ }
+ if (r != FIDO_OK) {
+ r = log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to enumerate FIDO2 devices: %s", fido_strerr(r));
+ goto finish;
+ }
+ if (found > 1) {
+ r = log_error_errno(SYNTHETIC_ERRNO(ENOTUNIQ), "More than one FIDO2 device found.");
+ goto finish;
+ }
+
+ entry = fido_dev_info_ptr(di, 0);
+ if (!entry) {
+ r = log_error_errno(SYNTHETIC_ERRNO(EIO),
+ "Failed to get device information for FIDO device 0.");
+ goto finish;
+ }
+
+ path = fido_dev_info_path(entry);
+ if (!path) {
+ r = log_error_errno(SYNTHETIC_ERRNO(EIO),
+ "Failed to query FIDO device path.");
+ goto finish;
+ }
+
+ copy = strdup(path);
+ if (!copy) {
+ r = log_oom();
+ goto finish;
+ }
+
+ *ret = TAKE_PTR(copy);
+ r = 0;
+
+finish:
+ fido_dev_info_free(&di, di_size);
+ return r;
+#else
+ return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
+ "FIDO2 tokens not supported on this build.");
+#endif
+}
diff --git a/src/home/homectl-fido2.h b/src/home/homectl-fido2.h
new file mode 100644
index 0000000..d0349f5
--- /dev/null
+++ b/src/home/homectl-fido2.h
@@ -0,0 +1,10 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "json.h"
+
+int identity_add_fido2_parameters(JsonVariant **v, const char *device);
+
+int list_fido2_devices(void);
+
+int find_fido2_auto(char **ret);
diff --git a/src/home/homectl-pkcs11.c b/src/home/homectl-pkcs11.c
new file mode 100644
index 0000000..4b7f833
--- /dev/null
+++ b/src/home/homectl-pkcs11.c
@@ -0,0 +1,477 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "errno-util.h"
+#include "format-table.h"
+#include "hexdecoct.h"
+#include "homectl-pkcs11.h"
+#include "libcrypt-util.h"
+#include "memory-util.h"
+#include "openssl-util.h"
+#include "pkcs11-util.h"
+#include "random-util.h"
+#include "strv.h"
+
+struct pkcs11_callback_data {
+ char *pin_used;
+ X509 *cert;
+};
+
+#if HAVE_P11KIT
+static void pkcs11_callback_data_release(struct pkcs11_callback_data *data) {
+ erase_and_free(data->pin_used);
+ X509_free(data->cert);
+}
+
+static 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) char *pin_used = NULL;
+ struct pkcs11_callback_data *data = userdata;
+ CK_OBJECT_HANDLE object;
+ int r;
+
+ assert(m);
+ assert(slot_info);
+ assert(token_info);
+ assert(uri);
+ assert(data);
+
+ /* Called for every token matching our URI */
+
+ r = pkcs11_token_login(m, session, slot_id, token_info, "home directory operation", "user-home", "pkcs11-pin", UINT64_MAX, &pin_used);
+ if (r < 0)
+ return r;
+
+ r = pkcs11_token_find_x509_certificate(m, session, uri, &object);
+ if (r < 0)
+ return r;
+
+ r = pkcs11_token_read_x509_certificate(m, session, object, &data->cert);
+ if (r < 0)
+ return r;
+
+ /* Let's read some random data off the token and write it to the kernel pool before we generate our
+ * random key from it. This way we can claim the quality of the RNG is at least as good as the
+ * kernel's and the token's pool */
+ (void) pkcs11_token_acquire_rng(m, session);
+
+ data->pin_used = TAKE_PTR(pin_used);
+ return 1;
+}
+#endif
+
+static int acquire_pkcs11_certificate(
+ const char *uri,
+ X509 **ret_cert,
+ char **ret_pin_used) {
+
+#if HAVE_P11KIT
+ _cleanup_(pkcs11_callback_data_release) struct pkcs11_callback_data data = {};
+ int r;
+
+ r = pkcs11_find_token(uri, pkcs11_callback, &data);
+ if (r == -EAGAIN) /* pkcs11_find_token() doesn't log about this error, but all others */
+ return log_error_errno(SYNTHETIC_ERRNO(ENXIO),
+ "Specified PKCS#11 token with URI '%s' not found.",
+ uri);
+ if (r < 0)
+ return r;
+
+ *ret_cert = TAKE_PTR(data.cert);
+ *ret_pin_used = TAKE_PTR(data.pin_used);
+
+ return 0;
+#else
+ return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
+ "PKCS#11 tokens not supported on this build.");
+#endif
+}
+
+static int encrypt_bytes(
+ EVP_PKEY *pkey,
+ const void *decrypted_key,
+ size_t decrypted_key_size,
+ void **ret_encrypt_key,
+ size_t *ret_encrypt_key_size) {
+
+ _cleanup_(EVP_PKEY_CTX_freep) EVP_PKEY_CTX *ctx = NULL;
+ _cleanup_free_ void *b = NULL;
+ size_t l;
+
+ ctx = EVP_PKEY_CTX_new(pkey, NULL);
+ if (!ctx)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to allocate public key context");
+
+ if (EVP_PKEY_encrypt_init(ctx) <= 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to initialize public key context");
+
+ if (EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_PADDING) <= 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to configure PKCS#1 padding");
+
+ if (EVP_PKEY_encrypt(ctx, NULL, &l, decrypted_key, decrypted_key_size) <= 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to determine encrypted key size");
+
+ b = malloc(l);
+ if (!b)
+ return log_oom();
+
+ if (EVP_PKEY_encrypt(ctx, b, &l, decrypted_key, decrypted_key_size) <= 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to determine encrypted key size");
+
+ *ret_encrypt_key = TAKE_PTR(b);
+ *ret_encrypt_key_size = l;
+
+ return 0;
+}
+
+static int add_pkcs11_encrypted_key(
+ JsonVariant **v,
+ const char *uri,
+ const void *encrypted_key, size_t encrypted_key_size,
+ const void *decrypted_key, size_t decrypted_key_size) {
+
+ _cleanup_(json_variant_unrefp) JsonVariant *l = NULL, *w = NULL, *e = NULL;
+ _cleanup_(erase_and_freep) char *base64_encoded = NULL, *hashed = NULL;
+ int r;
+
+ assert(v);
+ assert(uri);
+ assert(encrypted_key);
+ assert(encrypted_key_size > 0);
+ assert(decrypted_key);
+ assert(decrypted_key_size > 0);
+
+ /* Before using UNIX hashing on the supplied key we base64 encode it, since crypt_r() and friends
+ * expect a NUL terminated string, and we use a binary key */
+ r = base64mem(decrypted_key, decrypted_key_size, &base64_encoded);
+ if (r < 0)
+ return log_error_errno(r, "Failed to base64 encode secret key: %m");
+
+ r = hash_password(base64_encoded, &hashed);
+ if (r < 0)
+ return log_error_errno(errno_or_else(EINVAL), "Failed to UNIX hash secret key: %m");
+
+ r = json_build(&e, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("uri", JSON_BUILD_STRING(uri)),
+ JSON_BUILD_PAIR("data", JSON_BUILD_BASE64(encrypted_key, encrypted_key_size)),
+ JSON_BUILD_PAIR("hashedPassword", JSON_BUILD_STRING(hashed))));
+ if (r < 0)
+ return log_error_errno(r, "Failed to build encrypted JSON key object: %m");
+
+ w = json_variant_ref(json_variant_by_key(*v, "privileged"));
+ l = json_variant_ref(json_variant_by_key(w, "pkcs11EncryptedKey"));
+
+ r = json_variant_append_array(&l, e);
+ if (r < 0)
+ return log_error_errno(r, "Failed append PKCS#11 encrypted key: %m");
+
+ r = json_variant_set_field(&w, "pkcs11EncryptedKey", l);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set PKCS#11 encrypted key: %m");
+
+ r = json_variant_set_field(v, "privileged", w);
+ if (r < 0)
+ return log_error_errno(r, "Failed to update privileged field: %m");
+
+ return 0;
+}
+
+static int add_pkcs11_token_uri(JsonVariant **v, const char *uri) {
+ _cleanup_(json_variant_unrefp) JsonVariant *w = NULL;
+ _cleanup_strv_free_ char **l = NULL;
+ int r;
+
+ assert(v);
+ assert(uri);
+
+ w = json_variant_ref(json_variant_by_key(*v, "pkcs11TokenUri"));
+ if (w) {
+ r = json_variant_strv(w, &l);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse PKCS#11 token list: %m");
+
+ if (strv_contains(l, uri))
+ return 0;
+ }
+
+ r = strv_extend(&l, uri);
+ if (r < 0)
+ return log_oom();
+
+ w = json_variant_unref(w);
+ r = json_variant_new_array_strv(&w, l);
+ if (r < 0)
+ return log_error_errno(r, "Failed to create PKCS#11 token URI JSON: %m");
+
+ r = json_variant_set_field(v, "pkcs11TokenUri", w);
+ if (r < 0)
+ return log_error_errno(r, "Failed to update PKCS#11 token URI list: %m");
+
+ return 0;
+}
+
+int identity_add_token_pin(JsonVariant **v, const char *pin) {
+ _cleanup_(json_variant_unrefp) JsonVariant *w = NULL, *l = NULL;
+ _cleanup_(strv_free_erasep) char **pins = NULL;
+ int r;
+
+ assert(v);
+
+ if (isempty(pin))
+ return 0;
+
+ w = json_variant_ref(json_variant_by_key(*v, "secret"));
+ l = json_variant_ref(json_variant_by_key(w, "tokenPin"));
+
+ r = json_variant_strv(l, &pins);
+ if (r < 0)
+ return log_error_errno(r, "Failed to convert PIN array: %m");
+
+ if (strv_find(pins, pin))
+ return 0;
+
+ r = strv_extend(&pins, pin);
+ if (r < 0)
+ return log_oom();
+
+ strv_uniq(pins);
+
+ l = json_variant_unref(l);
+
+ r = json_variant_new_array_strv(&l, pins);
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate new PIN array JSON: %m");
+
+ json_variant_sensitive(l);
+
+ r = json_variant_set_field(&w, "tokenPin", l);
+ if (r < 0)
+ return log_error_errno(r, "Failed to update PIN field: %m");
+
+ r = json_variant_set_field(v, "secret", w);
+ if (r < 0)
+ return log_error_errno(r, "Failed to update secret object: %m");
+
+ return 1;
+}
+
+int identity_add_pkcs11_key_data(JsonVariant **v, const char *uri) {
+ _cleanup_(erase_and_freep) void *decrypted_key = NULL, *encrypted_key = NULL;
+ _cleanup_(erase_and_freep) char *pin = NULL;
+ size_t decrypted_key_size, encrypted_key_size;
+ _cleanup_(X509_freep) X509 *cert = NULL;
+ EVP_PKEY *pkey;
+ RSA *rsa;
+ int bits;
+ int r;
+
+ assert(v);
+
+ r = acquire_pkcs11_certificate(uri, &cert, &pin);
+ if (r < 0)
+ return r;
+
+ pkey = X509_get0_pubkey(cert);
+ if (!pkey)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to extract public key from X.509 certificate.");
+
+ if (EVP_PKEY_base_id(pkey) != EVP_PKEY_RSA)
+ return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "X.509 certificate does not refer to RSA key.");
+
+ rsa = EVP_PKEY_get0_RSA(pkey);
+ if (!rsa)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to acquire RSA public key from X.509 certificate.");
+
+ bits = RSA_bits(rsa);
+ log_debug("Bits in RSA key: %i", bits);
+
+ /* We use PKCS#1 padding for the RSA cleartext, hence let's leave some extra space for it, hence only
+ * generate a random key half the size of the RSA length */
+ decrypted_key_size = bits / 8 / 2;
+
+ if (decrypted_key_size < 1)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO), "Uh, RSA key size too short?");
+
+ log_debug("Generating %zu bytes random key.", decrypted_key_size);
+
+ decrypted_key = malloc(decrypted_key_size);
+ if (!decrypted_key)
+ return log_oom();
+
+ r = genuine_random_bytes(decrypted_key, decrypted_key_size, RANDOM_BLOCK);
+ if (r < 0)
+ return log_error_errno(r, "Failed to generate random key: %m");
+
+ r = encrypt_bytes(pkey, decrypted_key, decrypted_key_size, &encrypted_key, &encrypted_key_size);
+ if (r < 0)
+ return log_error_errno(r, "Failed to encrypt key: %m");
+
+ /* Add the token URI to the public part of the record. */
+ r = add_pkcs11_token_uri(v, uri);
+ if (r < 0)
+ return r;
+
+ /* Include the encrypted version of the random key we just generated in the privileged part of the record */
+ r = add_pkcs11_encrypted_key(
+ v,
+ uri,
+ encrypted_key, encrypted_key_size,
+ decrypted_key, decrypted_key_size);
+ if (r < 0)
+ return r;
+
+ /* If we acquired the PIN also include it in the secret section of the record, so that systemd-homed
+ * can use it if it needs to, given that it likely needs to decrypt the key again to pass to LUKS or
+ * fscrypt. */
+ r = identity_add_token_pin(v, pin);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+#if HAVE_P11KIT
+static int list_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_free_ char *token_uri_string = NULL, *token_label = NULL, *token_manufacturer_id = NULL, *token_model = NULL;
+ _cleanup_(p11_kit_uri_freep) P11KitUri *token_uri = NULL;
+ Table *t = userdata;
+ int uri_result, r;
+
+ assert(slot_info);
+ assert(token_info);
+
+ /* We only care about hardware devices here with a token inserted. Let's filter everything else
+ * out. (Note that the user can explicitly specify non-hardware tokens if they like, but during
+ * enumeration we'll filter those, since software tokens are typically the system certificate store
+ * and such, and it's typically not what people want to bind their home directories to.) */
+ if (!FLAGS_SET(token_info->flags, CKF_HW_SLOT|CKF_TOKEN_PRESENT))
+ return -EAGAIN;
+
+ token_label = pkcs11_token_label(token_info);
+ if (!token_label)
+ return log_oom();
+
+ token_manufacturer_id = pkcs11_token_manufacturer_id(token_info);
+ if (!token_manufacturer_id)
+ return log_oom();
+
+ token_model = pkcs11_token_model(token_info);
+ if (!token_model)
+ return log_oom();
+
+ token_uri = uri_from_token_info(token_info);
+ if (!token_uri)
+ return log_oom();
+
+ uri_result = p11_kit_uri_format(token_uri, P11_KIT_URI_FOR_ANY, &token_uri_string);
+ if (uri_result != P11_KIT_URI_OK)
+ return log_warning_errno(SYNTHETIC_ERRNO(EAGAIN), "Failed to format slot URI: %s", p11_kit_uri_message(uri_result));
+
+ r = table_add_many(
+ t,
+ TABLE_STRING, token_uri_string,
+ TABLE_STRING, token_label,
+ TABLE_STRING, token_manufacturer_id,
+ TABLE_STRING, token_model);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ return -EAGAIN; /* keep scanning */
+}
+#endif
+
+int list_pkcs11_tokens(void) {
+#if HAVE_P11KIT
+ _cleanup_(table_unrefp) Table *t = NULL;
+ int r;
+
+ t = table_new("uri", "label", "manufacturer", "model");
+ if (!t)
+ return log_oom();
+
+ r = pkcs11_find_token(NULL, list_callback, t);
+ if (r < 0 && r != -EAGAIN)
+ return r;
+
+ if (table_get_rows(t) <= 1) {
+ log_info("No suitable PKCS#11 tokens found.");
+ return 0;
+ }
+
+ r = table_print(t, stdout);
+ if (r < 0)
+ return log_error_errno(r, "Failed to show device table: %m");
+
+ return 0;
+#else
+ return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
+ "PKCS#11 tokens not supported on this build.");
+#endif
+}
+
+#if HAVE_P11KIT
+static int auto_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_(p11_kit_uri_freep) P11KitUri *token_uri = NULL;
+ char **t = userdata;
+ int uri_result;
+
+ assert(slot_info);
+ assert(token_info);
+
+ if (!FLAGS_SET(token_info->flags, CKF_HW_SLOT|CKF_TOKEN_PRESENT))
+ return -EAGAIN;
+
+ if (*t)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOTUNIQ),
+ "More than one suitable PKCS#11 token found.");
+
+ token_uri = uri_from_token_info(token_info);
+ if (!token_uri)
+ return log_oom();
+
+ uri_result = p11_kit_uri_format(token_uri, P11_KIT_URI_FOR_ANY, t);
+ if (uri_result != P11_KIT_URI_OK)
+ return log_warning_errno(SYNTHETIC_ERRNO(EAGAIN), "Failed to format slot URI: %s", p11_kit_uri_message(uri_result));
+
+ return 0;
+}
+#endif
+
+int find_pkcs11_token_auto(char **ret) {
+#if HAVE_P11KIT
+ int r;
+
+ r = pkcs11_find_token(NULL, auto_callback, ret);
+ if (r == -EAGAIN)
+ return log_error_errno(SYNTHETIC_ERRNO(ENODEV), "No suitable PKCS#11 tokens found.");
+ if (r < 0)
+ return r;
+
+ return 0;
+#else
+ return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
+ "PKCS#11 tokens not supported on this build.");
+#endif
+}
diff --git a/src/home/homectl-pkcs11.h b/src/home/homectl-pkcs11.h
new file mode 100644
index 0000000..5c30fee
--- /dev/null
+++ b/src/home/homectl-pkcs11.h
@@ -0,0 +1,11 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "json.h"
+
+int identity_add_token_pin(JsonVariant **v, const char *pin);
+
+int identity_add_pkcs11_key_data(JsonVariant **v, const char *token_uri);
+
+int list_pkcs11_tokens(void);
+int find_pkcs11_token_auto(char **ret);
diff --git a/src/home/homectl-recovery-key.c b/src/home/homectl-recovery-key.c
new file mode 100644
index 0000000..4a6649d
--- /dev/null
+++ b/src/home/homectl-recovery-key.c
@@ -0,0 +1,199 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "errno-util.h"
+#include "homectl-recovery-key.h"
+#include "libcrypt-util.h"
+#include "locale-util.h"
+#include "memory-util.h"
+#include "modhex.h"
+#include "qrcode-util.h"
+#include "random-util.h"
+#include "strv.h"
+#include "terminal-util.h"
+
+static int make_recovery_key(char **ret) {
+ _cleanup_(erase_and_freep) char *formatted = NULL;
+ _cleanup_(erase_and_freep) uint8_t *key = NULL;
+ int r;
+
+ assert(ret);
+
+ key = new(uint8_t, MODHEX_RAW_LENGTH);
+ if (!key)
+ return log_oom();
+
+ r = genuine_random_bytes(key, MODHEX_RAW_LENGTH, RANDOM_BLOCK);
+ if (r < 0)
+ return log_error_errno(r, "Failed to gather entropy for recovery key: %m");
+
+ /* Let's now format it as 64 modhex chars, and after each 8 chars insert a dash */
+ formatted = new(char, MODHEX_FORMATTED_LENGTH);
+ if (!formatted)
+ return log_oom();
+
+ for (size_t i = 0, j = 0; i < MODHEX_RAW_LENGTH; i++) {
+ formatted[j++] = modhex_alphabet[key[i] >> 4];
+ formatted[j++] = modhex_alphabet[key[i] & 0xF];
+
+ if (i % 4 == 3)
+ formatted[j++] = '-';
+ }
+
+ formatted[MODHEX_FORMATTED_LENGTH-1] = 0;
+
+ *ret = TAKE_PTR(formatted);
+ return 0;
+}
+
+static int add_privileged(JsonVariant **v, const char *hashed) {
+ _cleanup_(json_variant_unrefp) JsonVariant *e = NULL, *w = NULL, *l = NULL;
+ int r;
+
+ assert(v);
+ assert(hashed);
+
+ r = json_build(&e, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("type", JSON_BUILD_STRING("modhex64")),
+ JSON_BUILD_PAIR("hashedPassword", JSON_BUILD_STRING(hashed))));
+ if (r < 0)
+ return log_error_errno(r, "Failed to build recover key JSON object: %m");
+
+ json_variant_sensitive(e);
+
+ w = json_variant_ref(json_variant_by_key(*v, "privileged"));
+ l = json_variant_ref(json_variant_by_key(w, "recoveryKey"));
+
+ r = json_variant_append_array(&l, e);
+ if (r < 0)
+ return log_error_errno(r, "Failed append recovery key: %m");
+
+ r = json_variant_set_field(&w, "recoveryKey", l);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set recovery key array: %m");
+
+ r = json_variant_set_field(v, "privileged", w);
+ if (r < 0)
+ return log_error_errno(r, "Failed to update privileged field: %m");
+
+ return 0;
+}
+
+static int add_public(JsonVariant **v) {
+ _cleanup_strv_free_ char **types = NULL;
+ int r;
+
+ assert(v);
+
+ r = json_variant_strv(json_variant_by_key(*v, "recoveryKeyType"), &types);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse recovery key type list: %m");
+
+ r = strv_extend(&types, "modhex64");
+ if (r < 0)
+ return log_oom();
+
+ r = json_variant_set_field_strv(v, "recoveryKeyType", types);
+ if (r < 0)
+ return log_error_errno(r, "Failed to update recovery key types: %m");
+
+ return 0;
+}
+
+static int add_secret(JsonVariant **v, const char *password) {
+ _cleanup_(json_variant_unrefp) JsonVariant *w = NULL, *l = NULL;
+ _cleanup_(strv_free_erasep) char **passwords = NULL;
+ int r;
+
+ assert(v);
+ assert(password);
+
+ w = json_variant_ref(json_variant_by_key(*v, "secret"));
+ l = json_variant_ref(json_variant_by_key(w, "password"));
+
+ r = json_variant_strv(l, &passwords);
+ if (r < 0)
+ return log_error_errno(r, "Failed to convert password array: %m");
+
+ r = strv_extend(&passwords, password);
+ if (r < 0)
+ return log_oom();
+
+ r = json_variant_new_array_strv(&l, passwords);
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate new password array JSON: %m");
+
+ json_variant_sensitive(l);
+
+ r = json_variant_set_field(&w, "password", l);
+ if (r < 0)
+ return log_error_errno(r, "Failed to update password field: %m");
+
+ r = json_variant_set_field(v, "secret", w);
+ if (r < 0)
+ return log_error_errno(r, "Failed to update secret object: %m");
+
+ return 0;
+}
+
+int identity_add_recovery_key(JsonVariant **v) {
+ _cleanup_(erase_and_freep) char *password = NULL, *hashed = NULL;
+ int r;
+
+ assert(v);
+
+ /* First, let's generate a secret key */
+ r = make_recovery_key(&password);
+ if (r < 0)
+ return r;
+
+ /* Let's UNIX hash it */
+ r = hash_password(password, &hashed);
+ if (r < 0)
+ return log_error_errno(errno_or_else(EINVAL), "Failed to UNIX hash secret key: %m");
+
+ /* Let's now add the "privileged" version of the recovery key */
+ r = add_privileged(v, hashed);
+ if (r < 0)
+ return r;
+
+ /* Let's then add the public information about the recovery key */
+ r = add_public(v);
+ if (r < 0)
+ return r;
+
+ /* Finally, let's add the new key to the secret part, too */
+ r = add_secret(v, password);
+ if (r < 0)
+ return r;
+
+ /* We output the key itself with a trailing newline to stdout and the decoration around it to stderr
+ * instead. */
+
+ fflush(stdout);
+ fprintf(stderr,
+ "A secret recovery key has been generated for this account:\n\n"
+ " %s%s%s",
+ emoji_enabled() ? special_glyph(SPECIAL_GLYPH_LOCK_AND_KEY) : "",
+ emoji_enabled() ? " " : "",
+ ansi_highlight());
+ fflush(stderr);
+
+ fputs(password, stdout);
+ fflush(stdout);
+
+ fputs(ansi_normal(), stderr);
+ fflush(stderr);
+
+ fputc('\n', stdout);
+ fflush(stdout);
+
+ fputs("\nPlease save this secret recovery key at a secure location. It may be used to\n"
+ "regain access to the account if the other configured access credentials have\n"
+ "been lost or forgotten. The recovery key may be entered in place of a password\n"
+ "whenever authentication is requested.\n", stderr);
+ fflush(stderr);
+
+ (void) print_qrcode(stderr, "You may optionally scan the recovery key off screen", password);
+
+ return 0;
+}
diff --git a/src/home/homectl-recovery-key.h b/src/home/homectl-recovery-key.h
new file mode 100644
index 0000000..ab195f9
--- /dev/null
+++ b/src/home/homectl-recovery-key.h
@@ -0,0 +1,6 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "json.h"
+
+int identity_add_recovery_key(JsonVariant **v);
diff --git a/src/home/homectl.c b/src/home/homectl.c
new file mode 100644
index 0000000..7cfda7e
--- /dev/null
+++ b/src/home/homectl.c
@@ -0,0 +1,3381 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <getopt.h>
+
+#include "sd-bus.h"
+
+#include "ask-password-api.h"
+#include "bus-common-errors.h"
+#include "bus-error.h"
+#include "bus-locator.h"
+#include "cgroup-util.h"
+#include "dns-domain.h"
+#include "env-util.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "format-table.h"
+#include "home-util.h"
+#include "homectl-fido2.h"
+#include "homectl-pkcs11.h"
+#include "homectl-recovery-key.h"
+#include "locale-util.h"
+#include "main-func.h"
+#include "memory-util.h"
+#include "pager.h"
+#include "parse-util.h"
+#include "path-util.h"
+#include "pkcs11-util.h"
+#include "pretty-print.h"
+#include "process-util.h"
+#include "pwquality-util.h"
+#include "rlimit-util.h"
+#include "spawn-polkit-agent.h"
+#include "terminal-util.h"
+#include "user-record-pwquality.h"
+#include "user-record-show.h"
+#include "user-record-util.h"
+#include "user-record.h"
+#include "user-util.h"
+#include "verbs.h"
+
+static PagerFlags arg_pager_flags = 0;
+static bool arg_legend = true;
+static bool arg_ask_password = true;
+static BusTransport arg_transport = BUS_TRANSPORT_LOCAL;
+static const char *arg_host = NULL;
+static const char *arg_identity = NULL;
+static JsonVariant *arg_identity_extra = NULL;
+static JsonVariant *arg_identity_extra_privileged = NULL;
+static JsonVariant *arg_identity_extra_this_machine = NULL;
+static JsonVariant *arg_identity_extra_rlimits = NULL;
+static char **arg_identity_filter = NULL; /* this one is also applied to 'privileged' and 'thisMachine' subobjects */
+static char **arg_identity_filter_rlimits = NULL;
+static uint64_t arg_disk_size = UINT64_MAX;
+static uint64_t arg_disk_size_relative = UINT64_MAX;
+static char **arg_pkcs11_token_uri = NULL;
+static char **arg_fido2_device = NULL;
+static bool arg_recovery_key = false;
+static bool arg_json = false;
+static JsonFormatFlags arg_json_format_flags = 0;
+static bool arg_and_resize = false;
+static bool arg_and_change_password = false;
+static enum {
+ EXPORT_FORMAT_FULL, /* export the full record */
+ EXPORT_FORMAT_STRIPPED, /* strip "state" + "binding", but leave signature in place */
+ EXPORT_FORMAT_MINIMAL, /* also strip signature */
+} arg_export_format = EXPORT_FORMAT_FULL;
+
+STATIC_DESTRUCTOR_REGISTER(arg_identity_extra, json_variant_unrefp);
+STATIC_DESTRUCTOR_REGISTER(arg_identity_extra_this_machine, json_variant_unrefp);
+STATIC_DESTRUCTOR_REGISTER(arg_identity_extra_privileged, json_variant_unrefp);
+STATIC_DESTRUCTOR_REGISTER(arg_identity_extra_rlimits, json_variant_unrefp);
+STATIC_DESTRUCTOR_REGISTER(arg_identity_filter, strv_freep);
+STATIC_DESTRUCTOR_REGISTER(arg_identity_filter_rlimits, strv_freep);
+STATIC_DESTRUCTOR_REGISTER(arg_pkcs11_token_uri, strv_freep);
+STATIC_DESTRUCTOR_REGISTER(arg_fido2_device, strv_freep);
+
+static const BusLocator *bus_mgr;
+
+static bool identity_properties_specified(void) {
+ return
+ arg_identity ||
+ !json_variant_is_blank_object(arg_identity_extra) ||
+ !json_variant_is_blank_object(arg_identity_extra_privileged) ||
+ !json_variant_is_blank_object(arg_identity_extra_this_machine) ||
+ !json_variant_is_blank_object(arg_identity_extra_rlimits) ||
+ !strv_isempty(arg_identity_filter) ||
+ !strv_isempty(arg_identity_filter_rlimits) ||
+ !strv_isempty(arg_pkcs11_token_uri) ||
+ !strv_isempty(arg_fido2_device);
+}
+
+static int acquire_bus(sd_bus **bus) {
+ int r;
+
+ assert(bus);
+
+ if (*bus)
+ return 0;
+
+ r = bus_connect_transport(arg_transport, arg_host, false, bus);
+ if (r < 0)
+ return bus_log_connect_error(r);
+
+ (void) sd_bus_set_allow_interactive_authorization(*bus, arg_ask_password);
+
+ return 0;
+}
+
+static int list_homes(int argc, char *argv[], void *userdata) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ _cleanup_(table_unrefp) Table *table = NULL;
+ int r;
+
+ (void) pager_open(arg_pager_flags);
+
+ r = acquire_bus(&bus);
+ if (r < 0)
+ return r;
+
+ r = bus_call_method(bus, bus_mgr, "ListHomes", &error, &reply, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to list homes: %s", bus_error_message(&error, r));
+
+ table = table_new("name", "uid", "gid", "state", "realname", "home", "shell");
+ if (!table)
+ return log_oom();
+
+ r = sd_bus_message_enter_container(reply, 'a', "(susussso)");
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ for (;;) {
+ const char *name, *state, *realname, *home, *shell, *color;
+ TableCell *cell;
+ uint32_t uid, gid;
+
+ r = sd_bus_message_read(reply, "(susussso)", &name, &uid, &state, &gid, &realname, &home, &shell, NULL);
+ if (r < 0)
+ return bus_log_parse_error(r);
+ if (r == 0)
+ break;
+
+ r = table_add_many(table,
+ TABLE_STRING, name,
+ TABLE_UID, uid,
+ TABLE_GID, gid);
+ if (r < 0)
+ return table_log_add_error(r);
+
+
+ r = table_add_cell(table, &cell, TABLE_STRING, state);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ color = user_record_state_color(state);
+ if (color)
+ (void) table_set_color(table, cell, color);
+
+ r = table_add_many(table,
+ TABLE_STRING, strna(empty_to_null(realname)),
+ TABLE_STRING, home,
+ TABLE_STRING, strna(empty_to_null(shell)));
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ r = sd_bus_message_exit_container(reply);
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ if (table_get_rows(table) > 1 || arg_json) {
+ r = table_set_sort(table, (size_t) 0, (size_t) -1);
+ if (r < 0)
+ return table_log_sort_error(r);
+
+ table_set_header(table, arg_legend);
+
+ if (arg_json)
+ r = table_print_json(table, stdout, arg_json_format_flags);
+ else
+ r = table_print(table, NULL);
+ if (r < 0)
+ return table_log_print_error(r);
+ }
+
+ if (arg_legend && !arg_json) {
+ if (table_get_rows(table) > 1)
+ printf("\n%zu home areas listed.\n", table_get_rows(table) - 1);
+ else
+ printf("No home areas.\n");
+ }
+
+ return 0;
+}
+
+static int acquire_existing_password(const char *user_name, UserRecord *hr, bool emphasize_current) {
+ _cleanup_(strv_free_erasep) char **password = NULL;
+ _cleanup_free_ char *question = NULL;
+ char *e;
+ int r;
+
+ assert(user_name);
+ assert(hr);
+
+ e = getenv("PASSWORD");
+ if (e) {
+ /* People really shouldn't use environment variables for passing passwords. We support this
+ * only for testing purposes, and do not document the behaviour, so that people won't
+ * actually use this outside of testing. */
+
+ r = user_record_set_password(hr, STRV_MAKE(e), true);
+ if (r < 0)
+ return log_error_errno(r, "Failed to store password: %m");
+
+ string_erase(e);
+ assert_se(unsetenv("PASSWORD") == 0);
+
+ return 0;
+ }
+
+ if (asprintf(&question, emphasize_current ?
+ "Please enter current password for user %s:" :
+ "Please enter password for user %s:",
+ user_name) < 0)
+ return log_oom();
+
+ r = ask_password_auto(question, "user-home", NULL, "home-password", USEC_INFINITY, ASK_PASSWORD_ACCEPT_CACHED|ASK_PASSWORD_PUSH_CACHE, &password);
+ if (r < 0)
+ return log_error_errno(r, "Failed to acquire password: %m");
+
+ r = user_record_set_password(hr, password, true);
+ if (r < 0)
+ return log_error_errno(r, "Failed to store password: %m");
+
+ return 0;
+}
+
+static int acquire_token_pin(const char *user_name, UserRecord *hr) {
+ _cleanup_(strv_free_erasep) char **pin = NULL;
+ _cleanup_free_ char *question = NULL;
+ char *e;
+ int r;
+
+ assert(user_name);
+ assert(hr);
+
+ e = getenv("PIN");
+ if (e) {
+ r = user_record_set_token_pin(hr, STRV_MAKE(e), false);
+ if (r < 0)
+ return log_error_errno(r, "Failed to store token PIN: %m");
+
+ string_erase(e);
+ assert_se(unsetenv("PIN") == 0);
+
+ return 0;
+ }
+
+ if (asprintf(&question, "Please enter security token PIN for user %s:", user_name) < 0)
+ return log_oom();
+
+ /* We never cache or use cached PINs, since usually there are only very few attempts allowed before the PIN is blocked */
+ r = ask_password_auto(question, "user-home", NULL, "token-pin", USEC_INFINITY, 0, &pin);
+ if (r < 0)
+ return log_error_errno(r, "Failed to acquire security token PIN: %m");
+
+ r = user_record_set_token_pin(hr, pin, false);
+ if (r < 0)
+ return log_error_errno(r, "Failed to store security token PIN: %m");
+
+ return 0;
+}
+
+static int handle_generic_user_record_error(
+ const char *user_name,
+ UserRecord *hr,
+ const sd_bus_error *error,
+ int ret,
+ bool emphasize_current_password) {
+ int r;
+
+ assert(user_name);
+ assert(hr);
+
+ if (sd_bus_error_has_name(error, BUS_ERROR_HOME_ABSENT))
+ return log_error_errno(SYNTHETIC_ERRNO(EREMOTE),
+ "Home of user %s is currently absent, please plug in the necessary storage device or backing file system.", user_name);
+
+ else if (sd_bus_error_has_name(error, BUS_ERROR_AUTHENTICATION_LIMIT_HIT))
+ return log_error_errno(SYNTHETIC_ERRNO(ETOOMANYREFS),
+ "Too frequent unsuccessful login attempts for user %s, try again later.", user_name);
+
+ else if (sd_bus_error_has_name(error, BUS_ERROR_BAD_PASSWORD)) {
+
+ if (!strv_isempty(hr->password))
+ log_notice("Password incorrect or not sufficient, please try again.");
+
+ r = acquire_existing_password(user_name, hr, emphasize_current_password);
+ if (r < 0)
+ return r;
+
+ } else if (sd_bus_error_has_name(error, BUS_ERROR_BAD_PASSWORD_AND_NO_TOKEN)) {
+
+ if (strv_isempty(hr->password))
+ log_notice("Security token not inserted, please enter password.");
+ else
+ log_notice("Password incorrect or not sufficient, and configured security token not inserted, please try again.");
+
+ r = acquire_existing_password(user_name, hr, emphasize_current_password);
+ if (r < 0)
+ return r;
+
+ } else if (sd_bus_error_has_name(error, BUS_ERROR_TOKEN_PIN_NEEDED)) {
+
+ r = acquire_token_pin(user_name, hr);
+ if (r < 0)
+ return r;
+
+ } else if (sd_bus_error_has_name(error, BUS_ERROR_TOKEN_PROTECTED_AUTHENTICATION_PATH_NEEDED)) {
+
+ log_notice("%s%sPlease authenticate physically on security token.",
+ emoji_enabled() ? special_glyph(SPECIAL_GLYPH_TOUCH) : "",
+ emoji_enabled() ? " " : "");
+
+ r = user_record_set_pkcs11_protected_authentication_path_permitted(hr, true);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set PKCS#11 protected authentication path permitted flag: %m");
+
+ } else if (sd_bus_error_has_name(error, BUS_ERROR_TOKEN_USER_PRESENCE_NEEDED)) {
+
+ log_notice("%s%sAuthentication requires presence verification on security token.",
+ emoji_enabled() ? special_glyph(SPECIAL_GLYPH_TOUCH) : "",
+ emoji_enabled() ? " " : "");
+
+ r = user_record_set_fido2_user_presence_permitted(hr, true);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set FIDO2 user presence permitted flag: %m");
+
+ } else if (sd_bus_error_has_name(error, BUS_ERROR_TOKEN_PIN_LOCKED))
+ return log_error_errno(SYNTHETIC_ERRNO(EPERM), "Security token PIN is locked, please unlock it first. (Hint: Removal and re-insertion might suffice.)");
+
+ else if (sd_bus_error_has_name(error, BUS_ERROR_TOKEN_BAD_PIN)) {
+
+ log_notice("Security token PIN incorrect, please try again.");
+
+ r = acquire_token_pin(user_name, hr);
+ if (r < 0)
+ return r;
+
+ } else if (sd_bus_error_has_name(error, BUS_ERROR_TOKEN_BAD_PIN_FEW_TRIES_LEFT)) {
+
+ log_notice("Security token PIN incorrect, please try again (only a few tries left!).");
+
+ r = acquire_token_pin(user_name, hr);
+ if (r < 0)
+ return r;
+
+ } else if (sd_bus_error_has_name(error, BUS_ERROR_TOKEN_BAD_PIN_ONE_TRY_LEFT)) {
+
+ log_notice("Security token PIN incorrect, please try again (only one try left!).");
+
+ r = acquire_token_pin(user_name, hr);
+ if (r < 0)
+ return r;
+ } else
+ return log_error_errno(ret, "Operation on home %s failed: %s", user_name, bus_error_message(error, ret));
+
+ return 0;
+}
+
+static int activate_home(int argc, char *argv[], void *userdata) {
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ int r, ret = 0;
+ char **i;
+
+ r = acquire_bus(&bus);
+ if (r < 0)
+ return r;
+
+ STRV_FOREACH(i, strv_skip(argv, 1)) {
+ _cleanup_(user_record_unrefp) UserRecord *secret = NULL;
+
+ secret = user_record_new();
+ if (!secret)
+ return log_oom();
+
+ for (;;) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
+
+ r = bus_message_new_method_call(bus, &m, bus_mgr, "ActivateHome");
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_message_append(m, "s", *i);
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = bus_message_append_secret(m, secret);
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
+ if (r < 0) {
+ r = handle_generic_user_record_error(*i, secret, &error, r, false);
+ if (r < 0) {
+ if (ret == 0)
+ ret = r;
+
+ break;
+ }
+ } else
+ break;
+ }
+ }
+
+ return ret;
+}
+
+static int deactivate_home(int argc, char *argv[], void *userdata) {
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ int r, ret = 0;
+ char **i;
+
+ r = acquire_bus(&bus);
+ if (r < 0)
+ return r;
+
+ STRV_FOREACH(i, strv_skip(argv, 1)) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
+
+ r = bus_message_new_method_call(bus, &m, bus_mgr, "DeactivateHome");
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_message_append(m, "s", *i);
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
+ if (r < 0) {
+ log_error_errno(r, "Failed to deactivate user home: %s", bus_error_message(&error, r));
+ if (ret == 0)
+ ret = r;
+ }
+ }
+
+ return ret;
+}
+
+static void dump_home_record(UserRecord *hr) {
+ int r;
+
+ assert(hr);
+
+ if (hr->incomplete) {
+ fflush(stdout);
+ log_warning("Warning: lacking rights to acquire privileged fields of user record of '%s', output incomplete.", hr->user_name);
+ }
+
+ if (arg_json) {
+ _cleanup_(user_record_unrefp) UserRecord *stripped = NULL;
+
+ if (arg_export_format == EXPORT_FORMAT_STRIPPED)
+ r = user_record_clone(hr, USER_RECORD_EXTRACT_EMBEDDED, &stripped);
+ else if (arg_export_format == EXPORT_FORMAT_MINIMAL)
+ r = user_record_clone(hr, USER_RECORD_EXTRACT_SIGNABLE, &stripped);
+ else
+ r = 0;
+ if (r < 0)
+ log_warning_errno(r, "Failed to strip user record, ignoring: %m");
+ if (stripped)
+ hr = stripped;
+
+ json_variant_dump(hr->json, arg_json_format_flags, stdout, NULL);
+ } else
+ user_record_show(hr, true);
+}
+
+static char **mangle_user_list(char **list, char ***ret_allocated) {
+ _cleanup_free_ char *myself = NULL;
+ char **l;
+
+ if (!strv_isempty(list)) {
+ *ret_allocated = NULL;
+ return list;
+ }
+
+ myself = getusername_malloc();
+ if (!myself)
+ return NULL;
+
+ l = new(char*, 2);
+ if (!l)
+ return NULL;
+
+ l[0] = TAKE_PTR(myself);
+ l[1] = NULL;
+
+ *ret_allocated = l;
+ return l;
+}
+
+static int inspect_home(int argc, char *argv[], void *userdata) {
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ _cleanup_(strv_freep) char **mangled_list = NULL;
+ int r, ret = 0;
+ char **items, **i;
+
+ (void) pager_open(arg_pager_flags);
+
+ r = acquire_bus(&bus);
+ if (r < 0)
+ return r;
+
+ items = mangle_user_list(strv_skip(argv, 1), &mangled_list);
+ if (!items)
+ return log_oom();
+
+ STRV_FOREACH(i, items) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+ _cleanup_(user_record_unrefp) UserRecord *hr = NULL;
+ const char *json;
+ int incomplete;
+ uid_t uid;
+
+ r = parse_uid(*i, &uid);
+ if (r < 0) {
+ if (!valid_user_group_name(*i, 0)) {
+ log_error("Invalid user name '%s'.", *i);
+ if (ret == 0)
+ ret = -EINVAL;
+
+ continue;
+ }
+
+ r = bus_call_method(bus, bus_mgr, "GetUserRecordByName", &error, &reply, "s", *i);
+ } else
+ r = bus_call_method(bus, bus_mgr, "GetUserRecordByUID", &error, &reply, "u", (uint32_t) uid);
+
+ if (r < 0) {
+ log_error_errno(r, "Failed to inspect home: %s", bus_error_message(&error, r));
+ if (ret == 0)
+ ret = r;
+
+ continue;
+ }
+
+ r = sd_bus_message_read(reply, "sbo", &json, &incomplete, NULL);
+ if (r < 0) {
+ bus_log_parse_error(r);
+ if (ret == 0)
+ ret = r;
+
+ continue;
+ }
+
+ r = json_parse(json, JSON_PARSE_SENSITIVE, &v, NULL, NULL);
+ if (r < 0) {
+ log_error_errno(r, "Failed to parse JSON identity: %m");
+ if (ret == 0)
+ ret = r;
+
+ continue;
+ }
+
+ hr = user_record_new();
+ if (!hr)
+ return log_oom();
+
+ r = user_record_load(hr, v, USER_RECORD_LOAD_REFUSE_SECRET|USER_RECORD_LOG);
+ if (r < 0) {
+ if (ret == 0)
+ ret = r;
+
+ continue;
+ }
+
+ hr->incomplete = incomplete;
+ dump_home_record(hr);
+ }
+
+ return ret;
+}
+
+static int authenticate_home(int argc, char *argv[], void *userdata) {
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ _cleanup_(strv_freep) char **mangled_list = NULL;
+ int r, ret = 0;
+ char **i, **items;
+
+ items = mangle_user_list(strv_skip(argv, 1), &mangled_list);
+ if (!items)
+ return log_oom();
+
+ r = acquire_bus(&bus);
+ if (r < 0)
+ return r;
+
+ (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password);
+
+ STRV_FOREACH(i, items) {
+ _cleanup_(user_record_unrefp) UserRecord *secret = NULL;
+
+ secret = user_record_new();
+ if (!secret)
+ return log_oom();
+
+ for (;;) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
+
+ r = bus_message_new_method_call(bus, &m, bus_mgr, "AuthenticateHome");
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_message_append(m, "s", *i);
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = bus_message_append_secret(m, secret);
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
+ if (r < 0) {
+ r = handle_generic_user_record_error(*i, secret, &error, r, false);
+ if (r < 0) {
+ if (ret == 0)
+ ret = r;
+
+ break;
+ }
+ } else
+ break;
+ }
+ }
+
+ return ret;
+}
+
+static int update_last_change(JsonVariant **v, bool with_password, bool override) {
+ JsonVariant *c;
+ usec_t n;
+ int r;
+
+ assert(v);
+
+ n = now(CLOCK_REALTIME);
+
+ c = json_variant_by_key(*v, "lastChangeUSec");
+ if (c) {
+ uintmax_t u;
+
+ if (!override)
+ goto update_password;
+
+ if (!json_variant_is_unsigned(c))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "lastChangeUSec field is not an unsigned integer, refusing.");
+
+ u = json_variant_unsigned(c);
+ if (u >= n)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "lastChangeUSec is from the future, can't update.");
+ }
+
+ r = json_variant_set_field_unsigned(v, "lastChangeUSec", n);
+ if (r < 0)
+ return log_error_errno(r, "Failed to update lastChangeUSec: %m");
+
+update_password:
+ if (!with_password)
+ return 0;
+
+ c = json_variant_by_key(*v, "lastPasswordChangeUSec");
+ if (c) {
+ uintmax_t u;
+
+ if (!override)
+ return 0;
+
+ if (!json_variant_is_unsigned(c))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "lastPasswordChangeUSec field is not an unsigned integer, refusing.");
+
+ u = json_variant_unsigned(c);
+ if (u >= n)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "lastPasswordChangeUSec is from the future, can't update.");
+ }
+
+ r = json_variant_set_field_unsigned(v, "lastPasswordChangeUSec", n);
+ if (r < 0)
+ return log_error_errno(r, "Failed to update lastPasswordChangeUSec: %m");
+
+ return 1;
+}
+
+static int apply_identity_changes(JsonVariant **_v) {
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+ int r;
+
+ assert(_v);
+
+ v = json_variant_ref(*_v);
+
+ r = json_variant_filter(&v, arg_identity_filter);
+ if (r < 0)
+ return log_error_errno(r, "Failed to filter identity: %m");
+
+ r = json_variant_merge(&v, arg_identity_extra);
+ if (r < 0)
+ return log_error_errno(r, "Failed to merge identities: %m");
+
+ if (arg_identity_extra_this_machine || !strv_isempty(arg_identity_filter)) {
+ _cleanup_(json_variant_unrefp) JsonVariant *per_machine = NULL, *mmid = NULL;
+ char mids[SD_ID128_STRING_MAX];
+ sd_id128_t mid;
+
+ r = sd_id128_get_machine(&mid);
+ if (r < 0)
+ return log_error_errno(r, "Failed to acquire machine ID: %m");
+
+ r = json_variant_new_string(&mmid, sd_id128_to_string(mid, mids));
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate matchMachineId object: %m");
+
+ per_machine = json_variant_ref(json_variant_by_key(v, "perMachine"));
+ if (per_machine) {
+ _cleanup_(json_variant_unrefp) JsonVariant *npm = NULL, *add = NULL;
+ _cleanup_free_ JsonVariant **array = NULL;
+ JsonVariant *z;
+ size_t i = 0;
+
+ if (!json_variant_is_array(per_machine))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "perMachine field is not an array, refusing.");
+
+ array = new(JsonVariant*, json_variant_elements(per_machine) + 1);
+ if (!array)
+ return log_oom();
+
+ JSON_VARIANT_ARRAY_FOREACH(z, per_machine) {
+ JsonVariant *u;
+
+ if (!json_variant_is_object(z))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "perMachine entry is not an object, refusing.");
+
+ array[i++] = z;
+
+ u = json_variant_by_key(z, "matchMachineId");
+ if (!u)
+ continue;
+
+ if (!json_variant_equal(u, mmid))
+ continue;
+
+ r = json_variant_merge(&add, z);
+ if (r < 0)
+ return log_error_errno(r, "Failed to merge perMachine entry: %m");
+
+ i--;
+ }
+
+ r = json_variant_filter(&add, arg_identity_filter);
+ if (r < 0)
+ return log_error_errno(r, "Failed to filter perMachine: %m");
+
+ r = json_variant_merge(&add, arg_identity_extra_this_machine);
+ if (r < 0)
+ return log_error_errno(r, "Failed to merge in perMachine fields: %m");
+
+ if (arg_identity_filter_rlimits || arg_identity_extra_rlimits) {
+ _cleanup_(json_variant_unrefp) JsonVariant *rlv = NULL;
+
+ rlv = json_variant_ref(json_variant_by_key(add, "resourceLimits"));
+
+ r = json_variant_filter(&rlv, arg_identity_filter_rlimits);
+ if (r < 0)
+ return log_error_errno(r, "Failed to filter resource limits: %m");
+
+ r = json_variant_merge(&rlv, arg_identity_extra_rlimits);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set resource limits: %m");
+
+ if (json_variant_is_blank_object(rlv)) {
+ r = json_variant_filter(&add, STRV_MAKE("resourceLimits"));
+ if (r < 0)
+ return log_error_errno(r, "Failed to drop resource limits field from identity: %m");
+ } else {
+ r = json_variant_set_field(&add, "resourceLimits", rlv);
+ if (r < 0)
+ return log_error_errno(r, "Failed to update resource limits of identity: %m");
+ }
+ }
+
+ if (!json_variant_is_blank_object(add)) {
+ r = json_variant_set_field(&add, "matchMachineId", mmid);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set matchMachineId field: %m");
+
+ array[i++] = add;
+ }
+
+ r = json_variant_new_array(&npm, array, i);
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate new perMachine array: %m");
+
+ json_variant_unref(per_machine);
+ per_machine = TAKE_PTR(npm);
+ } else {
+ _cleanup_(json_variant_unrefp) JsonVariant *item = json_variant_ref(arg_identity_extra_this_machine);
+
+ if (arg_identity_extra_rlimits) {
+ r = json_variant_set_field(&item, "resourceLimits", arg_identity_extra_rlimits);
+ if (r < 0)
+ return log_error_errno(r, "Failed to update resource limits of identity: %m");
+ }
+
+ r = json_variant_set_field(&item, "matchMachineId", mmid);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set matchMachineId field: %m");
+
+ r = json_variant_append_array(&per_machine, item);
+ if (r < 0)
+ return log_error_errno(r, "Failed to append to perMachine array: %m");
+ }
+
+ r = json_variant_set_field(&v, "perMachine", per_machine);
+ if (r < 0)
+ return log_error_errno(r, "Failed to update per machine record: %m");
+ }
+
+ if (arg_identity_extra_privileged || arg_identity_filter) {
+ _cleanup_(json_variant_unrefp) JsonVariant *privileged = NULL;
+
+ privileged = json_variant_ref(json_variant_by_key(v, "privileged"));
+
+ r = json_variant_filter(&privileged, arg_identity_filter);
+ if (r < 0)
+ return log_error_errno(r, "Failed to filter identity (privileged part): %m");
+
+ r = json_variant_merge(&privileged, arg_identity_extra_privileged);
+ if (r < 0)
+ return log_error_errno(r, "Failed to merge identities (privileged part): %m");
+
+ if (json_variant_is_blank_object(privileged)) {
+ r = json_variant_filter(&v, STRV_MAKE("privileged"));
+ if (r < 0)
+ return log_error_errno(r, "Failed to drop privileged part from identity: %m");
+ } else {
+ r = json_variant_set_field(&v, "privileged", privileged);
+ if (r < 0)
+ return log_error_errno(r, "Failed to update privileged part of identity: %m");
+ }
+ }
+
+ if (arg_identity_filter_rlimits) {
+ _cleanup_(json_variant_unrefp) JsonVariant *rlv = NULL;
+
+ rlv = json_variant_ref(json_variant_by_key(v, "resourceLimits"));
+
+ r = json_variant_filter(&rlv, arg_identity_filter_rlimits);
+ if (r < 0)
+ return log_error_errno(r, "Failed to filter resource limits: %m");
+
+ /* Note that we only filter resource limits here, but don't apply them. We do that in the perMachine section */
+
+ if (json_variant_is_blank_object(rlv)) {
+ r = json_variant_filter(&v, STRV_MAKE("resourceLimits"));
+ if (r < 0)
+ return log_error_errno(r, "Failed to drop resource limits field from identity: %m");
+ } else {
+ r = json_variant_set_field(&v, "resourceLimits", rlv);
+ if (r < 0)
+ return log_error_errno(r, "Failed to update resource limits of identity: %m");
+ }
+ }
+
+ json_variant_unref(*_v);
+ *_v = TAKE_PTR(v);
+
+ return 0;
+}
+
+static int add_disposition(JsonVariant **v) {
+ int r;
+
+ assert(v);
+
+ if (json_variant_by_key(*v, "disposition"))
+ return 0;
+
+ /* Set the disposition to regular, if not configured explicitly */
+ r = json_variant_set_field_string(v, "disposition", "regular");
+ if (r < 0)
+ return log_error_errno(r, "Failed to set disposition field: %m");
+
+ return 1;
+}
+
+static int acquire_new_home_record(UserRecord **ret) {
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+ _cleanup_(user_record_unrefp) UserRecord *hr = NULL;
+ char **i;
+ int r;
+
+ assert(ret);
+
+ if (arg_identity) {
+ unsigned line, column;
+
+ r = json_parse_file(
+ streq(arg_identity, "-") ? stdin : NULL,
+ streq(arg_identity, "-") ? "<stdin>" : arg_identity, JSON_PARSE_SENSITIVE, &v, &line, &column);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse identity at %u:%u: %m", line, column);
+ }
+
+ r = apply_identity_changes(&v);
+ if (r < 0)
+ return r;
+
+ r = add_disposition(&v);
+ if (r < 0)
+ return r;
+
+ STRV_FOREACH(i, arg_pkcs11_token_uri) {
+ r = identity_add_pkcs11_key_data(&v, *i);
+ if (r < 0)
+ return r;
+ }
+
+ STRV_FOREACH(i, arg_fido2_device) {
+ r = identity_add_fido2_parameters(&v, *i);
+ if (r < 0)
+ return r;
+ }
+
+ if (arg_recovery_key) {
+ r = identity_add_recovery_key(&v);
+ if (r < 0)
+ return r;
+ }
+
+ r = update_last_change(&v, true, false);
+ if (r < 0)
+ return r;
+
+ if (DEBUG_LOGGING)
+ json_variant_dump(v, JSON_FORMAT_PRETTY, NULL, NULL);
+
+ hr = user_record_new();
+ if (!hr)
+ return log_oom();
+
+ r = user_record_load(hr, v, USER_RECORD_REQUIRE_REGULAR|USER_RECORD_ALLOW_SECRET|USER_RECORD_ALLOW_PRIVILEGED|USER_RECORD_ALLOW_PER_MACHINE|USER_RECORD_ALLOW_SIGNATURE|USER_RECORD_LOG);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(hr);
+ return 0;
+}
+
+static int acquire_new_password(
+ const char *user_name,
+ UserRecord *hr,
+ bool suggest,
+ char **ret) {
+
+ unsigned i = 5;
+ char *e;
+ int r;
+
+ assert(user_name);
+ assert(hr);
+
+ e = getenv("NEWPASSWORD");
+ if (e) {
+ _cleanup_(erase_and_freep) char *copy = NULL;
+
+ /* As above, this is not for use, just for testing */
+
+ if (ret) {
+ copy = strdup(e);
+ if (!copy)
+ return log_oom();
+ }
+
+ r = user_record_set_password(hr, STRV_MAKE(e), /* prepend = */ true);
+ if (r < 0)
+ return log_error_errno(r, "Failed to store password: %m");
+
+ string_erase(e);
+ assert_se(unsetenv("NEWPASSWORD") == 0);
+
+ if (ret)
+ *ret = TAKE_PTR(copy);
+
+ return 0;
+ }
+
+ if (suggest)
+ (void) suggest_passwords();
+
+ for (;;) {
+ _cleanup_(strv_free_erasep) char **first = NULL, **second = NULL;
+ _cleanup_free_ char *question = NULL;
+
+ if (--i == 0)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOKEY), "Too many attempts, giving up:");
+
+ if (asprintf(&question, "Please enter new password for user %s:", user_name) < 0)
+ return log_oom();
+
+ r = ask_password_auto(question, "user-home", NULL, "home-password", USEC_INFINITY, 0, &first);
+ if (r < 0)
+ return log_error_errno(r, "Failed to acquire password: %m");
+
+ question = mfree(question);
+ if (asprintf(&question, "Please enter new password for user %s (repeat):", user_name) < 0)
+ return log_oom();
+
+ r = ask_password_auto(question, "user-home", NULL, "home-password", USEC_INFINITY, 0, &second);
+ if (r < 0)
+ return log_error_errno(r, "Failed to acquire password: %m");
+
+ if (strv_equal(first, second)) {
+ _cleanup_(erase_and_freep) char *copy = NULL;
+
+ if (ret) {
+ copy = strdup(first[0]);
+ if (!copy)
+ return log_oom();
+ }
+
+ r = user_record_set_password(hr, first, /* prepend = */ true);
+ if (r < 0)
+ return log_error_errno(r, "Failed to store password: %m");
+
+ if (ret)
+ *ret = TAKE_PTR(copy);
+
+ return 0;
+ }
+
+ log_error("Password didn't match, try again.");
+ }
+}
+
+static int create_home(int argc, char *argv[], void *userdata) {
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ _cleanup_(user_record_unrefp) UserRecord *hr = NULL;
+ int r;
+
+ r = acquire_bus(&bus);
+ if (r < 0)
+ return r;
+
+ (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password);
+
+ if (argc >= 2) {
+ /* If a username was specified, use it */
+
+ if (valid_user_group_name(argv[1], 0))
+ r = json_variant_set_field_string(&arg_identity_extra, "userName", argv[1]);
+ else {
+ _cleanup_free_ char *un = NULL, *rr = NULL;
+
+ /* Before we consider the user name invalid, let's check if we can split it? */
+ r = split_user_name_realm(argv[1], &un, &rr);
+ if (r < 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User name '%s' is not valid: %m", argv[1]);
+
+ if (rr) {
+ r = json_variant_set_field_string(&arg_identity_extra, "realm", rr);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set realm field: %m");
+ }
+
+ r = json_variant_set_field_string(&arg_identity_extra, "userName", un);
+ }
+ if (r < 0)
+ return log_error_errno(r, "Failed to set userName field: %m");
+ } else {
+ /* If neither a username nor an identity have been specified we cannot operate. */
+ if (!arg_identity)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User name required.");
+ }
+
+ r = acquire_new_home_record(&hr);
+ if (r < 0)
+ return r;
+
+ /* If the JSON record carries no plain text password (besides the recovery key), then let's query it
+ * manually. */
+ if (strv_length(hr->password) <= arg_recovery_key) {
+
+ if (strv_isempty(hr->hashed_password)) {
+ _cleanup_(erase_and_freep) char *new_password = NULL;
+
+ /* No regular (i.e. non-PKCS#11) hashed passwords set in the record, let's fix that. */
+ r = acquire_new_password(hr->user_name, hr, /* suggest = */ true, &new_password);
+ if (r < 0)
+ return r;
+
+ r = user_record_make_hashed_password(hr, STRV_MAKE(new_password), /* extend = */ false);
+ if (r < 0)
+ return log_error_errno(r, "Failed to hash password: %m");
+ } else {
+ /* There's a hash password set in the record, acquire the unhashed version of it. */
+ r = acquire_existing_password(hr->user_name, hr, /* emphasize_current= */ false);
+ if (r < 0)
+ return r;
+ }
+ }
+
+ if (hr->enforce_password_policy == 0) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+
+ /* If password quality enforcement is disabled, let's at least warn client side */
+
+ r = user_record_quality_check_password(hr, hr, &error);
+ if (r < 0)
+ log_warning_errno(r, "Specified password does not pass quality checks (%s), proceeding anyway.", bus_error_message(&error, r));
+ }
+
+ for (;;) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
+ _cleanup_(erase_and_freep) char *formatted = NULL;
+
+ r = json_variant_format(hr->json, 0, &formatted);
+ if (r < 0)
+ return log_error_errno(r, "Failed to format user record: %m");
+
+ r = bus_message_new_method_call(bus, &m, bus_mgr, "CreateHome");
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ (void) sd_bus_message_sensitive(m);
+
+ r = sd_bus_message_append(m, "s", formatted);
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
+ if (r < 0) {
+ if (sd_bus_error_has_name(&error, BUS_ERROR_LOW_PASSWORD_QUALITY)) {
+ _cleanup_(erase_and_freep) char *new_password = NULL;
+
+ log_error_errno(r, "%s", bus_error_message(&error, r));
+ log_info("(Use --enforce-password-policy=no to turn off password quality checks for this account.)");
+
+ r = acquire_new_password(hr->user_name, hr, /* suggest = */ false, &new_password);
+ if (r < 0)
+ return r;
+
+ r = user_record_make_hashed_password(hr, STRV_MAKE(new_password), /* extend = */ false);
+ if (r < 0)
+ return log_error_errno(r, "Failed to hash passwords: %m");
+ } else {
+ r = handle_generic_user_record_error(hr->user_name, hr, &error, r, false);
+ if (r < 0)
+ return r;
+ }
+ } else
+ break; /* done */
+ }
+
+ return 0;
+}
+
+static int remove_home(int argc, char *argv[], void *userdata) {
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ int r, ret = 0;
+ char **i;
+
+ r = acquire_bus(&bus);
+ if (r < 0)
+ return r;
+
+ (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password);
+
+ STRV_FOREACH(i, strv_skip(argv, 1)) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
+
+ r = bus_message_new_method_call(bus, &m, bus_mgr, "RemoveHome");
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_message_append(m, "s", *i);
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
+ if (r < 0) {
+ log_error_errno(r, "Failed to remove home: %s", bus_error_message(&error, r));
+ if (ret == 0)
+ ret = r;
+ }
+ }
+
+ return ret;
+}
+
+static int acquire_updated_home_record(
+ sd_bus *bus,
+ const char *username,
+ UserRecord **ret) {
+
+ _cleanup_(json_variant_unrefp) JsonVariant *json = NULL;
+ _cleanup_(user_record_unrefp) UserRecord *hr = NULL;
+ char **i;
+ int r;
+
+ assert(ret);
+
+ if (arg_identity) {
+ unsigned line, column;
+ JsonVariant *un;
+
+ r = json_parse_file(
+ streq(arg_identity, "-") ? stdin : NULL,
+ streq(arg_identity, "-") ? "<stdin>" : arg_identity, JSON_PARSE_SENSITIVE, &json, &line, &column);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse identity at %u:%u: %m", line, column);
+
+ un = json_variant_by_key(json, "userName");
+ if (un) {
+ if (!json_variant_is_string(un) || (username && !streq(json_variant_string(un), username)))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User name specified on command line and in JSON record do not match.");
+ } else {
+ if (!username)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No username specified.");
+
+ r = json_variant_set_field_string(&arg_identity_extra, "userName", username);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set userName field: %m");
+ }
+
+ } else {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+ int incomplete;
+ const char *text;
+
+ if (!identity_properties_specified())
+ return log_error_errno(SYNTHETIC_ERRNO(EALREADY), "No field to change specified.");
+
+ r = bus_call_method(bus, bus_mgr, "GetUserRecordByName", &error, &reply, "s", username);
+ if (r < 0)
+ return log_error_errno(r, "Failed to acquire user home record: %s", bus_error_message(&error, r));
+
+ r = sd_bus_message_read(reply, "sbo", &text, &incomplete, NULL);
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ if (incomplete)
+ return log_error_errno(SYNTHETIC_ERRNO(EACCES), "Lacking rights to acquire user record including privileged metadata, can't update record.");
+
+ r = json_parse(text, JSON_PARSE_SENSITIVE, &json, NULL, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse JSON identity: %m");
+
+ reply = sd_bus_message_unref(reply);
+
+ r = json_variant_filter(&json, STRV_MAKE("binding", "status", "signature"));
+ if (r < 0)
+ return log_error_errno(r, "Failed to strip binding and status from record to update: %m");
+ }
+
+ r = apply_identity_changes(&json);
+ if (r < 0)
+ return r;
+
+ STRV_FOREACH(i, arg_pkcs11_token_uri) {
+ r = identity_add_pkcs11_key_data(&json, *i);
+ if (r < 0)
+ return r;
+ }
+
+ STRV_FOREACH(i, arg_fido2_device) {
+ r = identity_add_fido2_parameters(&json, *i);
+ if (r < 0)
+ return r;
+ }
+
+ /* If the user supplied a full record, then add in lastChange, but do not override. Otherwise always
+ * override. */
+ r = update_last_change(&json, arg_pkcs11_token_uri || arg_fido2_device, !arg_identity);
+ if (r < 0)
+ return r;
+
+ if (DEBUG_LOGGING)
+ json_variant_dump(json, JSON_FORMAT_PRETTY, NULL, NULL);
+
+ hr = user_record_new();
+ if (!hr)
+ return log_oom();
+
+ r = user_record_load(hr, json, USER_RECORD_REQUIRE_REGULAR|USER_RECORD_ALLOW_PRIVILEGED|USER_RECORD_ALLOW_PER_MACHINE|USER_RECORD_ALLOW_SECRET|USER_RECORD_ALLOW_SIGNATURE|USER_RECORD_LOG);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(hr);
+ return 0;
+}
+
+static int home_record_reset_human_interaction_permission(UserRecord *hr) {
+ int r;
+
+ assert(hr);
+
+ /* When we execute multiple operations one after the other, let's reset the permission to ask the
+ * user each time, so that if interaction is necessary we will be told so again and thus can print a
+ * nice message to the user, telling the user so. */
+
+ r = user_record_set_pkcs11_protected_authentication_path_permitted(hr, -1);
+ if (r < 0)
+ return log_error_errno(r, "Failed to reset PKCS#11 protected authentication path permission flag: %m");
+
+ r = user_record_set_fido2_user_presence_permitted(hr, -1);
+ if (r < 0)
+ return log_error_errno(r, "Failed to reset FIDO2 user presence permission flag: %m");
+
+ return 0;
+}
+
+static int update_home(int argc, char *argv[], void *userdata) {
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ _cleanup_(user_record_unrefp) UserRecord *hr = NULL;
+ _cleanup_free_ char *buffer = NULL;
+ const char *username;
+ int r;
+
+ if (argc >= 2)
+ username = argv[1];
+ else if (!arg_identity) {
+ buffer = getusername_malloc();
+ if (!buffer)
+ return log_oom();
+
+ username = buffer;
+ } else
+ username = NULL;
+
+ r = acquire_bus(&bus);
+ if (r < 0)
+ return r;
+
+ (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password);
+
+ r = acquire_updated_home_record(bus, username, &hr);
+ if (r < 0)
+ return r;
+
+ /* If we do multiple operations, let's output things more verbosely, since otherwise the repeated
+ * authentication might be confusing. */
+
+ if (arg_and_resize || arg_and_change_password)
+ log_info("Updating home directory.");
+
+ for (;;) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
+ _cleanup_free_ char *formatted = NULL;
+
+ r = bus_message_new_method_call(bus, &m, bus_mgr, "UpdateHome");
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = json_variant_format(hr->json, 0, &formatted);
+ if (r < 0)
+ return log_error_errno(r, "Failed to format user record: %m");
+
+ (void) sd_bus_message_sensitive(m);
+
+ r = sd_bus_message_append(m, "s", formatted);
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
+ if (r < 0) {
+ if (arg_and_change_password &&
+ sd_bus_error_has_name(&error, BUS_ERROR_BAD_PASSWORD_AND_NO_TOKEN))
+ /* In the generic handler we'd ask for a password in this case, but when
+ * changing passwords that's not sufficient, as we need to acquire all keys
+ * first. */
+ return log_error_errno(r, "Security token not inserted, refusing.");
+
+ r = handle_generic_user_record_error(hr->user_name, hr, &error, r, false);
+ if (r < 0)
+ return r;
+ } else
+ break;
+ }
+
+ if (arg_and_resize)
+ log_info("Resizing home.");
+
+ (void) home_record_reset_human_interaction_permission(hr);
+
+ /* Also sync down disk size to underlying LUKS/fscrypt/quota */
+ while (arg_and_resize) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
+
+ r = bus_message_new_method_call(bus, &m, bus_mgr, "ResizeHome");
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ /* Specify UINT64_MAX as size, in which case the underlying disk size will just be synced */
+ r = sd_bus_message_append(m, "st", hr->user_name, UINT64_MAX);
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = bus_message_append_secret(m, hr);
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
+ if (r < 0) {
+ if (arg_and_change_password &&
+ sd_bus_error_has_name(&error, BUS_ERROR_BAD_PASSWORD_AND_NO_TOKEN))
+ return log_error_errno(r, "Security token not inserted, refusing.");
+
+ r = handle_generic_user_record_error(hr->user_name, hr, &error, r, false);
+ if (r < 0)
+ return r;
+ } else
+ break;
+ }
+
+ if (arg_and_change_password)
+ log_info("Synchronizing passwords and encryption keys.");
+
+ (void) home_record_reset_human_interaction_permission(hr);
+
+ /* Also sync down passwords to underlying LUKS/fscrypt */
+ while (arg_and_change_password) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
+
+ r = bus_message_new_method_call(bus, &m, bus_mgr, "ChangePasswordHome");
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ /* Specify an empty new secret, in which case the underlying LUKS/fscrypt password will just be synced */
+ r = sd_bus_message_append(m, "ss", hr->user_name, "{}");
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = bus_message_append_secret(m, hr);
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
+ if (r < 0) {
+ if (sd_bus_error_has_name(&error, BUS_ERROR_BAD_PASSWORD_AND_NO_TOKEN))
+ return log_error_errno(r, "Security token not inserted, refusing.");
+
+ r = handle_generic_user_record_error(hr->user_name, hr, &error, r, false);
+ if (r < 0)
+ return r;
+ } else
+ break;
+ }
+
+ return 0;
+}
+
+static int passwd_home(int argc, char *argv[], void *userdata) {
+ _cleanup_(user_record_unrefp) UserRecord *old_secret = NULL, *new_secret = NULL;
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ _cleanup_free_ char *buffer = NULL;
+ const char *username;
+ int r;
+
+ if (arg_pkcs11_token_uri)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "To change the PKCS#11 security token use 'homectl update --pkcs11-token-uri=…'.");
+ if (arg_fido2_device)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "To change the FIDO2 security token use 'homectl update --fido2-device=…'.");
+ if (identity_properties_specified())
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The 'passwd' verb does not permit changing other record properties at the same time.");
+
+ if (argc >= 2)
+ username = argv[1];
+ else {
+ buffer = getusername_malloc();
+ if (!buffer)
+ return log_oom();
+
+ username = buffer;
+ }
+
+ r = acquire_bus(&bus);
+ if (r < 0)
+ return r;
+
+ (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password);
+
+ old_secret = user_record_new();
+ if (!old_secret)
+ return log_oom();
+
+ new_secret = user_record_new();
+ if (!new_secret)
+ return log_oom();
+
+ r = acquire_new_password(username, new_secret, /* suggest = */ true, NULL);
+ if (r < 0)
+ return r;
+
+ for (;;) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
+
+ r = bus_message_new_method_call(bus, &m, bus_mgr, "ChangePasswordHome");
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_message_append(m, "s", username);
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = bus_message_append_secret(m, new_secret);
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = bus_message_append_secret(m, old_secret);
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
+ if (r < 0) {
+ if (sd_bus_error_has_name(&error, BUS_ERROR_LOW_PASSWORD_QUALITY)) {
+
+ log_error_errno(r, "%s", bus_error_message(&error, r));
+
+ r = acquire_new_password(username, new_secret, /* suggest = */ false, NULL);
+
+ } else if (sd_bus_error_has_name(&error, BUS_ERROR_BAD_PASSWORD_AND_NO_TOKEN))
+
+ /* In the generic handler we'd ask for a password in this case, but when
+ * changing passwords that's not sufficeint, as we need to acquire all keys
+ * first. */
+ return log_error_errno(r, "Security token not inserted, refusing.");
+ else
+ r = handle_generic_user_record_error(username, old_secret, &error, r, true);
+ if (r < 0)
+ return r;
+ } else
+ break;
+ }
+
+ return 0;
+}
+
+static int resize_home(int argc, char *argv[], void *userdata) {
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ _cleanup_(user_record_unrefp) UserRecord *secret = NULL;
+ uint64_t ds = UINT64_MAX;
+ int r;
+
+ r = acquire_bus(&bus);
+ if (r < 0)
+ return r;
+
+ (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password);
+
+ if (arg_disk_size_relative != UINT64_MAX ||
+ (argc > 2 && parse_percent(argv[2]) >= 0))
+ return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
+ "Relative disk size specification currently not supported when resizing.");
+
+ if (argc > 2) {
+ r = parse_size(argv[2], 1024, &ds);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse disk size parameter: %s", argv[2]);
+ }
+
+ if (arg_disk_size != UINT64_MAX) {
+ if (ds != UINT64_MAX && ds != arg_disk_size)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Disk size specified twice and doesn't match, refusing.");
+
+ ds = arg_disk_size;
+ }
+
+ secret = user_record_new();
+ if (!secret)
+ return log_oom();
+
+ for (;;) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
+
+ r = bus_message_new_method_call(bus, &m, bus_mgr, "ResizeHome");
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_message_append(m, "st", argv[1], ds);
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = bus_message_append_secret(m, secret);
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
+ if (r < 0) {
+ r = handle_generic_user_record_error(argv[1], secret, &error, r, false);
+ if (r < 0)
+ return r;
+ } else
+ break;
+ }
+
+ return 0;
+}
+
+static int lock_home(int argc, char *argv[], void *userdata) {
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ int r, ret = 0;
+ char **i;
+
+ r = acquire_bus(&bus);
+ if (r < 0)
+ return r;
+
+ STRV_FOREACH(i, strv_skip(argv, 1)) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
+
+ r = bus_message_new_method_call(bus, &m, bus_mgr, "LockHome");
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_message_append(m, "s", *i);
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
+ if (r < 0) {
+ log_error_errno(r, "Failed to lock home: %s", bus_error_message(&error, r));
+ if (ret == 0)
+ ret = r;
+ }
+ }
+
+ return ret;
+}
+
+static int unlock_home(int argc, char *argv[], void *userdata) {
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ int r, ret = 0;
+ char **i;
+
+ r = acquire_bus(&bus);
+ if (r < 0)
+ return r;
+
+ STRV_FOREACH(i, strv_skip(argv, 1)) {
+ _cleanup_(user_record_unrefp) UserRecord *secret = NULL;
+
+ secret = user_record_new();
+ if (!secret)
+ return log_oom();
+
+ for (;;) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
+
+ r = bus_message_new_method_call(bus, &m, bus_mgr, "UnlockHome");
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_message_append(m, "s", *i);
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = bus_message_append_secret(m, secret);
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
+ if (r < 0) {
+ r = handle_generic_user_record_error(argv[1], secret, &error, r, false);
+ if (r < 0) {
+ if (ret == 0)
+ ret = r;
+
+ break;
+ }
+ } else
+ break;
+ }
+ }
+
+ return ret;
+}
+
+static int with_home(int argc, char *argv[], void *userdata) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL, *reply = NULL;
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ _cleanup_(user_record_unrefp) UserRecord *secret = NULL;
+ _cleanup_close_ int acquired_fd = -1;
+ _cleanup_strv_free_ char **cmdline = NULL;
+ const char *home;
+ int r, ret;
+ pid_t pid;
+
+ r = acquire_bus(&bus);
+ if (r < 0)
+ return r;
+
+ if (argc < 3) {
+ _cleanup_free_ char *shell = NULL;
+
+ /* If no command is specified, spawn a shell */
+ r = get_shell(&shell);
+ if (r < 0)
+ return log_error_errno(r, "Failed to acquire shell: %m");
+
+ cmdline = strv_new(shell);
+ } else
+ cmdline = strv_copy(argv + 2);
+ if (!cmdline)
+ return log_oom();
+
+ secret = user_record_new();
+ if (!secret)
+ return log_oom();
+
+ for (;;) {
+ r = bus_message_new_method_call(bus, &m, bus_mgr, "AcquireHome");
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_message_append(m, "s", argv[1]);
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = bus_message_append_secret(m, secret);
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_message_append(m, "b", /* please_suspend = */ getenv_bool("SYSTEMD_PLEASE_SUSPEND_HOME") > 0);
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, &reply);
+ m = sd_bus_message_unref(m);
+ if (r < 0) {
+ r = handle_generic_user_record_error(argv[1], secret, &error, r, false);
+ if (r < 0)
+ return r;
+
+ sd_bus_error_free(&error);
+ } else {
+ int fd;
+
+ r = sd_bus_message_read(reply, "h", &fd);
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ acquired_fd = fcntl(fd, F_DUPFD_CLOEXEC, 3);
+ if (acquired_fd < 0)
+ return log_error_errno(errno, "Failed to duplicate acquired fd: %m");
+
+ reply = sd_bus_message_unref(reply);
+ break;
+ }
+ }
+
+ r = bus_call_method(bus, bus_mgr, "GetHomeByName", &error, &reply, "s", argv[1]);
+ if (r < 0)
+ return log_error_errno(r, "Failed to inspect home: %s", bus_error_message(&error, r));
+
+ r = sd_bus_message_read(reply, "usussso", NULL, NULL, NULL, NULL, &home, NULL, NULL);
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ r = safe_fork("(with)", FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG|FORK_LOG|FORK_RLIMIT_NOFILE_SAFE|FORK_REOPEN_LOG, &pid);
+ if (r < 0)
+ return r;
+ if (r == 0) {
+ if (chdir(home) < 0) {
+ log_error_errno(errno, "Failed to change to directory %s: %m", home);
+ _exit(255);
+ }
+
+ execvp(cmdline[0], cmdline);
+ log_error_errno(errno, "Failed to execute %s: %m", cmdline[0]);
+ _exit(255);
+ }
+
+ ret = wait_for_terminate_and_check(cmdline[0], pid, WAIT_LOG_ABNORMAL);
+
+ /* Close the fd that pings the home now. */
+ acquired_fd = safe_close(acquired_fd);
+
+ r = bus_message_new_method_call(bus, &m, bus_mgr, "ReleaseHome");
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_message_append(m, "s", argv[1]);
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
+ if (r < 0) {
+ if (sd_bus_error_has_name(&error, BUS_ERROR_HOME_BUSY))
+ log_notice("Not deactivating home directory of %s, as it is still used.", argv[1]);
+ else
+ return log_error_errno(r, "Failed to release user home: %s", bus_error_message(&error, r));
+ }
+
+ return ret;
+}
+
+static int lock_all_homes(int argc, char *argv[], void *userdata) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ int r;
+
+ r = acquire_bus(&bus);
+ if (r < 0)
+ return r;
+
+ r = bus_message_new_method_call(bus, &m, bus_mgr, "LockAllHomes");
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to lock all homes: %s", bus_error_message(&error, r));
+
+ return 0;
+}
+
+static int deactivate_all_homes(int argc, char *argv[], void *userdata) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ int r;
+
+ r = acquire_bus(&bus);
+ if (r < 0)
+ return r;
+
+ r = bus_message_new_method_call(bus, &m, bus_mgr, "DeactivateAllHomes");
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to deactivate all homes: %s", bus_error_message(&error, r));
+
+ return 0;
+}
+
+static int drop_from_identity(const char *field) {
+ int r;
+
+ assert(field);
+
+ /* If we are called to update an identity record and drop some field, let's keep track of what to
+ * remove from the old record */
+ r = strv_extend(&arg_identity_filter, field);
+ if (r < 0)
+ return log_oom();
+
+ /* Let's also drop the field if it was previously set to a new value on the same command line */
+ r = json_variant_filter(&arg_identity_extra, STRV_MAKE(field));
+ if (r < 0)
+ return log_error_errno(r, "Failed to filter JSON identity data: %m");
+
+ r = json_variant_filter(&arg_identity_extra_this_machine, STRV_MAKE(field));
+ if (r < 0)
+ return log_error_errno(r, "Failed to filter JSON identity data: %m");
+
+ r = json_variant_filter(&arg_identity_extra_privileged, STRV_MAKE(field));
+ if (r < 0)
+ return log_error_errno(r, "Failed to filter JSON identity data: %m");
+
+ return 0;
+}
+
+static int help(int argc, char *argv[], void *userdata) {
+ _cleanup_free_ char *link = NULL;
+ int r;
+
+ (void) pager_open(arg_pager_flags);
+
+ r = terminal_urlify_man("homectl", "1", &link);
+ if (r < 0)
+ return log_oom();
+
+ printf("%1$s [OPTIONS...] COMMAND ...\n\n"
+ "%2$sCreate, manipulate or inspect home directories.%3$s\n"
+ "\n%4$sCommands:%5$s\n"
+ " list List home areas\n"
+ " activate USER… Activate a home area\n"
+ " deactivate USER… Deactivate a home area\n"
+ " inspect USER… Inspect a home area\n"
+ " authenticate USER… Authenticate a home area\n"
+ " create USER Create a home area\n"
+ " remove USER… Remove a home area\n"
+ " update USER Update a home area\n"
+ " passwd USER Change password of a home area\n"
+ " resize USER SIZE Resize a home area\n"
+ " lock USER… Temporarily lock an active home area\n"
+ " unlock USER… Unlock a temporarily locked home area\n"
+ " lock-all Lock all suitable home areas\n"
+ " deactivate-all Deactivate all active home areas\n"
+ " with USER [COMMAND…] Run shell or command with access to a home area\n"
+ "\n%4$sOptions:%5$s\n"
+ " -h --help Show this help\n"
+ " --version Show package version\n"
+ " --no-pager Do not pipe output into a pager\n"
+ " --no-legend Do not show the headers and footers\n"
+ " --no-ask-password Do not ask for system passwords\n"
+ " -H --host=[USER@]HOST Operate on remote host\n"
+ " -M --machine=CONTAINER Operate on local container\n"
+ " --identity=PATH Read JSON identity from file\n"
+ " --json=FORMAT Output inspection data in JSON (takes one of\n"
+ " pretty, short, off)\n"
+ " -j Equivalent to --json=pretty (on TTY) or\n"
+ " --json=short (otherwise)\n"
+ " --export-format= Strip JSON inspection data (full, stripped,\n"
+ " minimal)\n"
+ " -E When specified once equals -j --export-format=\n"
+ " stripped, when specified twice equals\n"
+ " -j --export-format=minimal\n"
+ "\n%4$sGeneral User Record Properties:%5$s\n"
+ " -c --real-name=REALNAME Real name for user\n"
+ " --realm=REALM Realm to create user in\n"
+ " --email-address=EMAIL Email address for user\n"
+ " --location=LOCATION Set location of user on earth\n"
+ " --icon-name=NAME Icon name for user\n"
+ " -d --home-dir=PATH Home directory\n"
+ " -u --uid=UID Numeric UID for user\n"
+ " -G --member-of=GROUP Add user to group\n"
+ " --skel=PATH Skeleton directory to use\n"
+ " --shell=PATH Shell for account\n"
+ " --setenv=VARIABLE=VALUE Set an environment variable at log-in\n"
+ " --timezone=TIMEZONE Set a time-zone\n"
+ " --language=LOCALE Set preferred language\n"
+ " --ssh-authorized-keys=KEYS\n"
+ " Specify SSH public keys\n"
+ " --pkcs11-token-uri=URI URI to PKCS#11 security token containing\n"
+ " private key and matching X.509 certificate\n"
+ " --fido2-device=PATH Path to FIDO2 hidraw device with hmac-secret\n"
+ " extension\n"
+ " --recovery-key=BOOL Add a recovery key\n"
+ "\n%4$sAccount Management User Record Properties:%5$s\n"
+ " --locked=BOOL Set locked account state\n"
+ " --not-before=TIMESTAMP Do not allow logins before\n"
+ " --not-after=TIMESTAMP Do not allow logins after\n"
+ " --rate-limit-interval=SECS\n"
+ " Login rate-limit interval in seconds\n"
+ " --rate-limit-burst=NUMBER\n"
+ " Login rate-limit attempts per interval\n"
+ "\n%4$sPassword Policy User Record Properties:%5$s\n"
+ " --password-hint=HINT Set Password hint\n"
+ " --enforce-password-policy=BOOL\n"
+ " Control whether to enforce system's password\n"
+ " policy for this user\n"
+ " -P Equivalent to --enforce-password-password=no\n"
+ " --password-change-now=BOOL\n"
+ " Require the password to be changed on next login\n"
+ " --password-change-min=TIME\n"
+ " Require minimum time between password changes\n"
+ " --password-change-max=TIME\n"
+ " Require maximum time between password changes\n"
+ " --password-change-warn=TIME\n"
+ " How much time to warn before password expiry\n"
+ " --password-change-inactive=TIME\n"
+ " How much time to block password after expiry\n"
+ "\n%4$sResource Management User Record Properties:%5$s\n"
+ " --disk-size=BYTES Size to assign the user on disk\n"
+ " --access-mode=MODE User home directory access mode\n"
+ " --umask=MODE Umask for user when logging in\n"
+ " --nice=NICE Nice level for user\n"
+ " --rlimit=LIMIT=VALUE[:VALUE]\n"
+ " Set resource limits\n"
+ " --tasks-max=MAX Set maximum number of per-user tasks\n"
+ " --memory-high=BYTES Set high memory threshold in bytes\n"
+ " --memory-max=BYTES Set maximum memory limit\n"
+ " --cpu-weight=WEIGHT Set CPU weight\n"
+ " --io-weight=WEIGHT Set IO weight\n"
+ "\n%4$sStorage User Record Properties:%5$s\n"
+ " --storage=STORAGE Storage type to use (luks, fscrypt, directory,\n"
+ " subvolume, cifs)\n"
+ " --image-path=PATH Path to image file/directory\n"
+ "\n%4$sLUKS Storage User Record Properties:%5$s\n"
+ " --fs-type=TYPE File system type to use in case of luks\n"
+ " storage (btrfs, ext4, xfs)\n"
+ " --luks-discard=BOOL Whether to use 'discard' feature of file system\n"
+ " when activated (mounted)\n"
+ " --luks-offline-discard=BOOL\n"
+ " Whether to trim file on logout\n"
+ " --luks-cipher=CIPHER Cipher to use for LUKS encryption\n"
+ " --luks-cipher-mode=MODE Cipher mode to use for LUKS encryption\n"
+ " --luks-volume-key-size=BITS\n"
+ " Volume key size to use for LUKS encryption\n"
+ " --luks-pbkdf-type=TYPE Password-based Key Derivation Function to use\n"
+ " --luks-pbkdf-hash-algorithm=ALGORITHM\n"
+ " PBKDF hash algorithm to use\n"
+ " --luks-pbkdf-time-cost=SECS\n"
+ " Time cost for PBKDF in seconds\n"
+ " --luks-pbkdf-memory-cost=BYTES\n"
+ " Memory cost for PBKDF in bytes\n"
+ " --luks-pbkdf-parallel-threads=NUMBER\n"
+ " Number of parallel threads for PKBDF\n"
+ "\n%4$sMounting User Record Properties:%5$s\n"
+ " --nosuid=BOOL Control the 'nosuid' flag of the home mount\n"
+ " --nodev=BOOL Control the 'nodev' flag of the home mount\n"
+ " --noexec=BOOL Control the 'noexec' flag of the home mount\n"
+ "\n%4$sCIFS User Record Properties:%5$s\n"
+ " --cifs-domain=DOMAIN CIFS (Windows) domain\n"
+ " --cifs-user-name=USER CIFS (Windows) user name\n"
+ " --cifs-service=SERVICE CIFS (Windows) service to mount as home area\n"
+ "\n%4$sLogin Behaviour User Record Properties:%5$s\n"
+ " --stop-delay=SECS How long to leave user services running after\n"
+ " logout\n"
+ " --kill-processes=BOOL Whether to kill user processes when sessions\n"
+ " terminate\n"
+ " --auto-login=BOOL Try to log this user in automatically\n"
+ "\nSee the %6$s for details.\n"
+ , program_invocation_short_name
+ , ansi_highlight(), ansi_normal()
+ , ansi_underline(), ansi_normal()
+ , link
+ );
+
+ return 0;
+}
+
+static int parse_argv(int argc, char *argv[]) {
+
+ enum {
+ ARG_VERSION = 0x100,
+ ARG_NO_PAGER,
+ ARG_NO_LEGEND,
+ ARG_NO_ASK_PASSWORD,
+ ARG_REALM,
+ ARG_EMAIL_ADDRESS,
+ ARG_DISK_SIZE,
+ ARG_ACCESS_MODE,
+ ARG_STORAGE,
+ ARG_FS_TYPE,
+ ARG_IMAGE_PATH,
+ ARG_UMASK,
+ ARG_LUKS_DISCARD,
+ ARG_LUKS_OFFLINE_DISCARD,
+ ARG_JSON,
+ ARG_SETENV,
+ ARG_TIMEZONE,
+ ARG_LANGUAGE,
+ ARG_LOCKED,
+ ARG_SSH_AUTHORIZED_KEYS,
+ ARG_LOCATION,
+ ARG_ICON_NAME,
+ ARG_PASSWORD_HINT,
+ ARG_NICE,
+ ARG_RLIMIT,
+ ARG_NOT_BEFORE,
+ ARG_NOT_AFTER,
+ ARG_LUKS_CIPHER,
+ ARG_LUKS_CIPHER_MODE,
+ ARG_LUKS_VOLUME_KEY_SIZE,
+ ARG_NOSUID,
+ ARG_NODEV,
+ ARG_NOEXEC,
+ ARG_CIFS_DOMAIN,
+ ARG_CIFS_USER_NAME,
+ ARG_CIFS_SERVICE,
+ ARG_TASKS_MAX,
+ ARG_MEMORY_HIGH,
+ ARG_MEMORY_MAX,
+ ARG_CPU_WEIGHT,
+ ARG_IO_WEIGHT,
+ ARG_LUKS_PBKDF_TYPE,
+ ARG_LUKS_PBKDF_HASH_ALGORITHM,
+ ARG_LUKS_PBKDF_TIME_COST,
+ ARG_LUKS_PBKDF_MEMORY_COST,
+ ARG_LUKS_PBKDF_PARALLEL_THREADS,
+ ARG_RATE_LIMIT_INTERVAL,
+ ARG_RATE_LIMIT_BURST,
+ ARG_STOP_DELAY,
+ ARG_KILL_PROCESSES,
+ ARG_ENFORCE_PASSWORD_POLICY,
+ ARG_PASSWORD_CHANGE_NOW,
+ ARG_PASSWORD_CHANGE_MIN,
+ ARG_PASSWORD_CHANGE_MAX,
+ ARG_PASSWORD_CHANGE_WARN,
+ ARG_PASSWORD_CHANGE_INACTIVE,
+ ARG_EXPORT_FORMAT,
+ ARG_AUTO_LOGIN,
+ ARG_PKCS11_TOKEN_URI,
+ ARG_FIDO2_DEVICE,
+ ARG_RECOVERY_KEY,
+ ARG_AND_RESIZE,
+ ARG_AND_CHANGE_PASSWORD,
+ };
+
+ static const struct option options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, ARG_VERSION },
+ { "no-pager", no_argument, NULL, ARG_NO_PAGER },
+ { "no-legend", no_argument, NULL, ARG_NO_LEGEND },
+ { "no-ask-password", no_argument, NULL, ARG_NO_ASK_PASSWORD },
+ { "host", required_argument, NULL, 'H' },
+ { "machine", required_argument, NULL, 'M' },
+ { "identity", required_argument, NULL, 'I' },
+ { "real-name", required_argument, NULL, 'c' },
+ { "comment", required_argument, NULL, 'c' }, /* Compat alias to keep thing in sync with useradd(8) */
+ { "realm", required_argument, NULL, ARG_REALM },
+ { "email-address", required_argument, NULL, ARG_EMAIL_ADDRESS },
+ { "location", required_argument, NULL, ARG_LOCATION },
+ { "password-hint", required_argument, NULL, ARG_PASSWORD_HINT },
+ { "icon-name", required_argument, NULL, ARG_ICON_NAME },
+ { "home-dir", required_argument, NULL, 'd' }, /* Compatible with useradd(8) */
+ { "uid", required_argument, NULL, 'u' }, /* Compatible with useradd(8) */
+ { "member-of", required_argument, NULL, 'G' },
+ { "groups", required_argument, NULL, 'G' }, /* Compat alias to keep thing in sync with useradd(8) */
+ { "skel", required_argument, NULL, 'k' }, /* Compatible with useradd(8) */
+ { "shell", required_argument, NULL, 's' }, /* Compatible with useradd(8) */
+ { "setenv", required_argument, NULL, ARG_SETENV },
+ { "timezone", required_argument, NULL, ARG_TIMEZONE },
+ { "language", required_argument, NULL, ARG_LANGUAGE },
+ { "locked", required_argument, NULL, ARG_LOCKED },
+ { "not-before", required_argument, NULL, ARG_NOT_BEFORE },
+ { "not-after", required_argument, NULL, ARG_NOT_AFTER },
+ { "expiredate", required_argument, NULL, 'e' }, /* Compat alias to keep thing in sync with useradd(8) */
+ { "ssh-authorized-keys", required_argument, NULL, ARG_SSH_AUTHORIZED_KEYS },
+ { "disk-size", required_argument, NULL, ARG_DISK_SIZE },
+ { "access-mode", required_argument, NULL, ARG_ACCESS_MODE },
+ { "umask", required_argument, NULL, ARG_UMASK },
+ { "nice", required_argument, NULL, ARG_NICE },
+ { "rlimit", required_argument, NULL, ARG_RLIMIT },
+ { "tasks-max", required_argument, NULL, ARG_TASKS_MAX },
+ { "memory-high", required_argument, NULL, ARG_MEMORY_HIGH },
+ { "memory-max", required_argument, NULL, ARG_MEMORY_MAX },
+ { "cpu-weight", required_argument, NULL, ARG_CPU_WEIGHT },
+ { "io-weight", required_argument, NULL, ARG_IO_WEIGHT },
+ { "storage", required_argument, NULL, ARG_STORAGE },
+ { "image-path", required_argument, NULL, ARG_IMAGE_PATH },
+ { "fs-type", required_argument, NULL, ARG_FS_TYPE },
+ { "luks-discard", required_argument, NULL, ARG_LUKS_DISCARD },
+ { "luks-offline-discard", required_argument, NULL, ARG_LUKS_OFFLINE_DISCARD },
+ { "luks-cipher", required_argument, NULL, ARG_LUKS_CIPHER },
+ { "luks-cipher-mode", required_argument, NULL, ARG_LUKS_CIPHER_MODE },
+ { "luks-volume-key-size", required_argument, NULL, ARG_LUKS_VOLUME_KEY_SIZE },
+ { "luks-pbkdf-type", required_argument, NULL, ARG_LUKS_PBKDF_TYPE },
+ { "luks-pbkdf-hash-algorithm", required_argument, NULL, ARG_LUKS_PBKDF_HASH_ALGORITHM },
+ { "luks-pbkdf-time-cost", required_argument, NULL, ARG_LUKS_PBKDF_TIME_COST },
+ { "luks-pbkdf-memory-cost", required_argument, NULL, ARG_LUKS_PBKDF_MEMORY_COST },
+ { "luks-pbkdf-parallel-threads", required_argument, NULL, ARG_LUKS_PBKDF_PARALLEL_THREADS },
+ { "nosuid", required_argument, NULL, ARG_NOSUID },
+ { "nodev", required_argument, NULL, ARG_NODEV },
+ { "noexec", required_argument, NULL, ARG_NOEXEC },
+ { "cifs-user-name", required_argument, NULL, ARG_CIFS_USER_NAME },
+ { "cifs-domain", required_argument, NULL, ARG_CIFS_DOMAIN },
+ { "cifs-service", required_argument, NULL, ARG_CIFS_SERVICE },
+ { "rate-limit-interval", required_argument, NULL, ARG_RATE_LIMIT_INTERVAL },
+ { "rate-limit-burst", required_argument, NULL, ARG_RATE_LIMIT_BURST },
+ { "stop-delay", required_argument, NULL, ARG_STOP_DELAY },
+ { "kill-processes", required_argument, NULL, ARG_KILL_PROCESSES },
+ { "enforce-password-policy", required_argument, NULL, ARG_ENFORCE_PASSWORD_POLICY },
+ { "password-change-now", required_argument, NULL, ARG_PASSWORD_CHANGE_NOW },
+ { "password-change-min", required_argument, NULL, ARG_PASSWORD_CHANGE_MIN },
+ { "password-change-max", required_argument, NULL, ARG_PASSWORD_CHANGE_MAX },
+ { "password-change-warn", required_argument, NULL, ARG_PASSWORD_CHANGE_WARN },
+ { "password-change-inactive", required_argument, NULL, ARG_PASSWORD_CHANGE_INACTIVE },
+ { "auto-login", required_argument, NULL, ARG_AUTO_LOGIN },
+ { "json", required_argument, NULL, ARG_JSON },
+ { "export-format", required_argument, NULL, ARG_EXPORT_FORMAT },
+ { "pkcs11-token-uri", required_argument, NULL, ARG_PKCS11_TOKEN_URI },
+ { "fido2-device", required_argument, NULL, ARG_FIDO2_DEVICE },
+ { "recovery-key", required_argument, NULL, ARG_RECOVERY_KEY },
+ { "and-resize", required_argument, NULL, ARG_AND_RESIZE },
+ { "and-change-password", required_argument, NULL, ARG_AND_CHANGE_PASSWORD },
+ {}
+ };
+
+ int r;
+
+ assert(argc >= 0);
+ assert(argv);
+
+ for (;;) {
+ int c;
+
+ c = getopt_long(argc, argv, "hH:M:I:c:d:u:k:s:e:G:jPE", options, NULL);
+ if (c < 0)
+ break;
+
+ switch (c) {
+
+ case 'h':
+ return help(0, NULL, NULL);
+
+ case ARG_VERSION:
+ return version();
+
+ case ARG_NO_PAGER:
+ arg_pager_flags |= PAGER_DISABLE;
+ break;
+
+ case ARG_NO_LEGEND:
+ arg_legend = false;
+ break;
+
+ case ARG_NO_ASK_PASSWORD:
+ arg_ask_password = false;
+ break;
+
+ case 'H':
+ arg_transport = BUS_TRANSPORT_REMOTE;
+ arg_host = optarg;
+ break;
+
+ case 'M':
+ arg_transport = BUS_TRANSPORT_MACHINE;
+ arg_host = optarg;
+ break;
+
+ case 'I':
+ arg_identity = optarg;
+ break;
+
+ case 'c':
+ if (isempty(optarg)) {
+ r = drop_from_identity("realName");
+ if (r < 0)
+ return r;
+
+ break;
+ }
+
+ if (!valid_gecos(optarg))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Real name '%s' not a valid GECOS field.", optarg);
+
+ r = json_variant_set_field_string(&arg_identity_extra, "realName", optarg);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set realName field: %m");
+
+ break;
+
+ case 'd': {
+ _cleanup_free_ char *hd = NULL;
+
+ if (isempty(optarg)) {
+ r = drop_from_identity("homeDirectory");
+ if (r < 0)
+ return r;
+
+ break;
+ }
+
+ r = parse_path_argument_and_warn(optarg, false, &hd);
+ if (r < 0)
+ return r;
+
+ if (!valid_home(hd))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Home directory '%s' not valid.", hd);
+
+ r = json_variant_set_field_string(&arg_identity_extra, "homeDirectory", hd);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set homeDirectory field: %m");
+
+ break;
+ }
+
+ case ARG_REALM:
+ if (isempty(optarg)) {
+ r = drop_from_identity("realm");
+ if (r < 0)
+ return r;
+
+ break;
+ }
+
+ r = dns_name_is_valid(optarg);
+ if (r < 0)
+ return log_error_errno(r, "Failed to determine whether realm '%s' is a valid DNS domain: %m", optarg);
+ if (r == 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Realm '%s' is not a valid DNS domain: %m", optarg);
+
+ r = json_variant_set_field_string(&arg_identity_extra, "realm", optarg);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set realm field: %m");
+ break;
+
+ case ARG_EMAIL_ADDRESS:
+ case ARG_LOCATION:
+ case ARG_ICON_NAME:
+ case ARG_CIFS_USER_NAME:
+ case ARG_CIFS_DOMAIN:
+ case ARG_CIFS_SERVICE: {
+
+ const char *field =
+ c == ARG_EMAIL_ADDRESS ? "emailAddress" :
+ c == ARG_LOCATION ? "location" :
+ c == ARG_ICON_NAME ? "iconName" :
+ c == ARG_CIFS_USER_NAME ? "cifsUserName" :
+ c == ARG_CIFS_DOMAIN ? "cifsDomain" :
+ c == ARG_CIFS_SERVICE ? "cifsService" :
+ NULL;
+
+ assert(field);
+
+ if (isempty(optarg)) {
+ r = drop_from_identity(field);
+ if (r < 0)
+ return r;
+
+ break;
+ }
+
+ r = json_variant_set_field_string(&arg_identity_extra, field, optarg);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set %s field: %m", field);
+
+ break;
+ }
+
+ case ARG_PASSWORD_HINT:
+ if (isempty(optarg)) {
+ r = drop_from_identity("passwordHint");
+ if (r < 0)
+ return r;
+
+ break;
+ }
+
+ r = json_variant_set_field_string(&arg_identity_extra_privileged, "passwordHint", optarg);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set passwordHint field: %m");
+
+ string_erase(optarg);
+ break;
+
+ case ARG_NICE: {
+ int nc;
+
+ if (isempty(optarg)) {
+ r = drop_from_identity("niceLevel");
+ if (r < 0)
+ return r;
+ break;
+ }
+
+ r = parse_nice(optarg, &nc);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse nice level: %s", optarg);
+
+ r = json_variant_set_field_integer(&arg_identity_extra, "niceLevel", nc);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set niceLevel field: %m");
+
+ break;
+ }
+
+ case ARG_RLIMIT: {
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *jcur = NULL, *jmax = NULL;
+ _cleanup_free_ char *field = NULL, *t = NULL;
+ const char *eq;
+ struct rlimit rl;
+ int l;
+
+ if (isempty(optarg)) {
+ /* Remove all resource limits */
+
+ r = drop_from_identity("resourceLimits");
+ if (r < 0)
+ return r;
+
+ arg_identity_filter_rlimits = strv_free(arg_identity_filter_rlimits);
+ arg_identity_extra_rlimits = json_variant_unref(arg_identity_extra_rlimits);
+ break;
+ }
+
+ eq = strchr(optarg, '=');
+ if (!eq)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Can't parse resource limit assignment: %s", optarg);
+
+ field = strndup(optarg, eq - optarg);
+ if (!field)
+ return log_oom();
+
+ l = rlimit_from_string_harder(field);
+ if (l < 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown resource limit type: %s", field);
+
+ if (isempty(eq + 1)) {
+ /* Remove only the specific rlimit */
+
+ r = strv_extend(&arg_identity_filter_rlimits, rlimit_to_string(l));
+ if (r < 0)
+ return r;
+
+ r = json_variant_filter(&arg_identity_extra_rlimits, STRV_MAKE(field));
+ if (r < 0)
+ return log_error_errno(r, "Failed to filter JSON identity data: %m");
+
+ break;
+ }
+
+ r = rlimit_parse(l, eq + 1, &rl);
+ if (r < 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to parse resource limit value: %s", eq + 1);
+
+ r = rl.rlim_cur == RLIM_INFINITY ? json_variant_new_null(&jcur) : json_variant_new_unsigned(&jcur, rl.rlim_cur);
+ if (r < 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to allocate current integer: %m");
+
+ r = rl.rlim_max == RLIM_INFINITY ? json_variant_new_null(&jmax) : json_variant_new_unsigned(&jmax, rl.rlim_max);
+ if (r < 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to allocate maximum integer: %m");
+
+ r = json_build(&v,
+ JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("cur", JSON_BUILD_VARIANT(jcur)),
+ JSON_BUILD_PAIR("max", JSON_BUILD_VARIANT(jmax))));
+ if (r < 0)
+ return log_error_errno(r, "Failed to build resource limit: %m");
+
+ t = strjoin("RLIMIT_", rlimit_to_string(l));
+ if (!t)
+ return log_oom();
+
+ r = json_variant_set_field(&arg_identity_extra_rlimits, t, v);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set %s field: %m", rlimit_to_string(l));
+
+ break;
+ }
+
+ case 'u': {
+ uid_t uid;
+
+ if (isempty(optarg)) {
+ r = drop_from_identity("uid");
+ if (r < 0)
+ return r;
+
+ break;
+ }
+
+ r = parse_uid(optarg, &uid);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse UID '%s'.", optarg);
+
+ if (uid_is_system(uid))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "UID " UID_FMT " is in system range, refusing.", uid);
+ if (uid_is_dynamic(uid))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "UID " UID_FMT " is in dynamic range, refusing.", uid);
+ if (uid == UID_NOBODY)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "UID " UID_FMT " is nobody UID, refusing.", uid);
+
+ r = json_variant_set_field_unsigned(&arg_identity_extra, "uid", uid);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set realm field: %m");
+
+ break;
+ }
+
+ case 'k':
+ case ARG_IMAGE_PATH: {
+ const char *field = c == 'k' ? "skeletonDirectory" : "imagePath";
+ _cleanup_free_ char *v = NULL;
+
+ if (isempty(optarg)) {
+ r = drop_from_identity(field);
+ if (r < 0)
+ return r;
+
+ break;
+ }
+
+ r = parse_path_argument_and_warn(optarg, false, &v);
+ if (r < 0)
+ return r;
+
+ r = json_variant_set_field_string(&arg_identity_extra_this_machine, field, v);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set %s field: %m", v);
+
+ break;
+ }
+
+ case 's':
+ if (isempty(optarg)) {
+ r = drop_from_identity("shell");
+ if (r < 0)
+ return r;
+
+ break;
+ }
+
+ if (!valid_shell(optarg))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Shell '%s' not valid.", optarg);
+
+ r = json_variant_set_field_string(&arg_identity_extra, "shell", optarg);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set shell field: %m");
+
+ break;
+
+ case ARG_SETENV: {
+ _cleanup_free_ char **l = NULL, **k = NULL;
+ _cleanup_(json_variant_unrefp) JsonVariant *ne = NULL;
+ JsonVariant *e;
+
+ if (isempty(optarg)) {
+ r = drop_from_identity("environment");
+ if (r < 0)
+ return r;
+
+ break;
+ }
+
+ if (!env_assignment_is_valid(optarg))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Environment assignment '%s' not valid.", optarg);
+
+ e = json_variant_by_key(arg_identity_extra, "environment");
+ if (e) {
+ r = json_variant_strv(e, &l);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse JSON environment field: %m");
+ }
+
+ k = strv_env_set(l, optarg);
+ if (!k)
+ return log_oom();
+
+ strv_sort(k);
+
+ r = json_variant_new_array_strv(&ne, k);
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate environment list JSON: %m");
+
+ r = json_variant_set_field(&arg_identity_extra, "environment", ne);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set environment list: %m");
+
+ break;
+ }
+
+ case ARG_TIMEZONE:
+
+ if (isempty(optarg)) {
+ r = drop_from_identity("timeZone");
+ if (r < 0)
+ return r;
+
+ break;
+ }
+
+ if (!timezone_is_valid(optarg, LOG_DEBUG))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Timezone '%s' is not valid.", optarg);
+
+ r = json_variant_set_field_string(&arg_identity_extra, "timeZone", optarg);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set timezone field: %m");
+
+ break;
+
+ case ARG_LANGUAGE:
+ if (isempty(optarg)) {
+ r = drop_from_identity("language");
+ if (r < 0)
+ return r;
+
+ break;
+ }
+
+ if (!locale_is_valid(optarg))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Locale '%s' is not valid.", optarg);
+
+ if (locale_is_installed(optarg) <= 0)
+ log_warning("Locale '%s' is not installed, accepting anyway.", optarg);
+
+ r = json_variant_set_field_string(&arg_identity_extra, "preferredLanguage", optarg);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set preferredLanguage field: %m");
+
+ break;
+
+ case ARG_NOSUID:
+ case ARG_NODEV:
+ case ARG_NOEXEC:
+ case ARG_LOCKED:
+ case ARG_KILL_PROCESSES:
+ case ARG_ENFORCE_PASSWORD_POLICY:
+ case ARG_AUTO_LOGIN:
+ case ARG_PASSWORD_CHANGE_NOW: {
+ const char *field =
+ c == ARG_LOCKED ? "locked" :
+ c == ARG_NOSUID ? "mountNoSuid" :
+ c == ARG_NODEV ? "mountNoDevices" :
+ c == ARG_NOEXEC ? "mountNoExecute" :
+ c == ARG_KILL_PROCESSES ? "killProcesses" :
+ c == ARG_ENFORCE_PASSWORD_POLICY ? "enforcePasswordPolicy" :
+ c == ARG_AUTO_LOGIN ? "autoLogin" :
+ c == ARG_PASSWORD_CHANGE_NOW ? "passwordChangeNow" :
+ NULL;
+
+ assert(field);
+
+ if (isempty(optarg)) {
+ r = drop_from_identity(field);
+ if (r < 0)
+ return r;
+
+ break;
+ }
+
+ r = parse_boolean(optarg);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse %s boolean: %m", field);
+
+ r = json_variant_set_field_boolean(&arg_identity_extra, field, r > 0);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set %s field: %m", field);
+
+ break;
+ }
+
+ case 'P':
+ r = json_variant_set_field_boolean(&arg_identity_extra, "enforcePasswordPolicy", false);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set enforcePasswordPolicy field: %m");
+
+ break;
+
+ case ARG_DISK_SIZE:
+ if (isempty(optarg)) {
+ r = drop_from_identity("diskSize");
+ if (r < 0)
+ return r;
+
+ r = drop_from_identity("diskSizeRelative");
+ if (r < 0)
+ return r;
+
+ arg_disk_size = arg_disk_size_relative = UINT64_MAX;
+ break;
+ }
+
+ r = parse_permille(optarg);
+ if (r < 0) {
+ r = parse_size(optarg, 1024, &arg_disk_size);
+ if (r < 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Disk size '%s' not valid.", optarg);
+
+ r = drop_from_identity("diskSizeRelative");
+ if (r < 0)
+ return r;
+
+ r = json_variant_set_field_unsigned(&arg_identity_extra_this_machine, "diskSize", arg_disk_size);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set diskSize field: %m");
+
+ arg_disk_size_relative = UINT64_MAX;
+ } else {
+ /* Normalize to UINT32_MAX == 100% */
+ arg_disk_size_relative = (uint64_t) r * UINT32_MAX / 1000U;
+
+ r = drop_from_identity("diskSize");
+ if (r < 0)
+ return r;
+
+ r = json_variant_set_field_unsigned(&arg_identity_extra_this_machine, "diskSizeRelative", arg_disk_size_relative);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set diskSizeRelative field: %m");
+
+ arg_disk_size = UINT64_MAX;
+ }
+
+ break;
+
+ case ARG_ACCESS_MODE: {
+ mode_t mode;
+
+ if (isempty(optarg)) {
+ r = drop_from_identity("accessMode");
+ if (r < 0)
+ return r;
+
+ break;
+ }
+
+ r = parse_mode(optarg, &mode);
+ if (r < 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Access mode '%s' not valid.", optarg);
+
+ r = json_variant_set_field_unsigned(&arg_identity_extra, "accessMode", mode);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set access mode field: %m");
+
+ break;
+ }
+
+ case ARG_LUKS_DISCARD:
+ if (isempty(optarg)) {
+ r = drop_from_identity("luksDiscard");
+ if (r < 0)
+ return r;
+
+ break;
+ }
+
+ r = parse_boolean(optarg);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse --luks-discard= parameter: %s", optarg);
+
+ r = json_variant_set_field_boolean(&arg_identity_extra, "luksDiscard", r);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set discard field: %m");
+
+ break;
+
+ case ARG_LUKS_OFFLINE_DISCARD:
+ if (isempty(optarg)) {
+ r = drop_from_identity("luksOfflineDiscard");
+ if (r < 0)
+ return r;
+
+ break;
+ }
+
+ r = parse_boolean(optarg);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse --luks-offline-discard= parameter: %s", optarg);
+
+ r = json_variant_set_field_boolean(&arg_identity_extra, "luksOfflineDiscard", r);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set offline discard field: %m");
+
+ break;
+
+ case ARG_LUKS_VOLUME_KEY_SIZE:
+ case ARG_LUKS_PBKDF_PARALLEL_THREADS:
+ case ARG_RATE_LIMIT_BURST: {
+ const char *field =
+ c == ARG_LUKS_VOLUME_KEY_SIZE ? "luksVolumeKeySize" :
+ c == ARG_LUKS_PBKDF_PARALLEL_THREADS ? "luksPbkdfParallelThreads" :
+ c == ARG_RATE_LIMIT_BURST ? "rateLimitBurst" : NULL;
+ unsigned n;
+
+ assert(field);
+
+ if (isempty(optarg)) {
+ r = drop_from_identity(field);
+ if (r < 0)
+ return r;
+ }
+
+ r = safe_atou(optarg, &n);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse %s parameter: %s", field, optarg);
+
+ r = json_variant_set_field_unsigned(&arg_identity_extra, field, n);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set %s field: %m", field);
+
+ break;
+ }
+
+ case ARG_UMASK: {
+ mode_t m;
+
+ if (isempty(optarg)) {
+ r = drop_from_identity("umask");
+ if (r < 0)
+ return r;
+
+ break;
+ }
+
+ r = parse_mode(optarg, &m);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse umask: %m");
+
+ r = json_variant_set_field_integer(&arg_identity_extra, "umask", m);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set umask field: %m");
+
+ break;
+ }
+
+ case ARG_SSH_AUTHORIZED_KEYS: {
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+ _cleanup_(strv_freep) char **l = NULL, **add = NULL;
+
+ if (isempty(optarg)) {
+ r = drop_from_identity("sshAuthorizedKeys");
+ if (r < 0)
+ return r;
+
+ break;
+ }
+
+ if (optarg[0] == '@') {
+ _cleanup_fclose_ FILE *f = NULL;
+
+ /* If prefixed with '@' read from a file */
+
+ f = fopen(optarg+1, "re");
+ if (!f)
+ return log_error_errno(errno, "Failed to open '%s': %m", optarg+1);
+
+ for (;;) {
+ _cleanup_free_ char *line = NULL;
+
+ r = read_line(f, LONG_LINE_MAX, &line);
+ if (r < 0)
+ return log_error_errno(r, "Failed to read from '%s': %m", optarg+1);
+ if (r == 0)
+ break;
+
+ if (isempty(line))
+ continue;
+
+ if (line[0] == '#')
+ continue;
+
+ r = strv_consume(&add, TAKE_PTR(line));
+ if (r < 0)
+ return log_oom();
+ }
+ } else {
+ /* Otherwise, assume it's a literal key. Let's do some superficial checks
+ * before accept it though. */
+
+ if (string_has_cc(optarg, NULL))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Authorized key contains control characters, refusing.");
+ if (optarg[0] == '#')
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Specified key is a comment?");
+
+ add = strv_new(optarg);
+ if (!add)
+ return log_oom();
+ }
+
+ v = json_variant_ref(json_variant_by_key(arg_identity_extra_privileged, "sshAuthorizedKeys"));
+ if (v) {
+ r = json_variant_strv(v, &l);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse SSH authorized keys list: %m");
+ }
+
+ r = strv_extend_strv(&l, add, true);
+ if (r < 0)
+ return log_oom();
+
+ v = json_variant_unref(v);
+
+ r = json_variant_new_array_strv(&v, l);
+ if (r < 0)
+ return log_oom();
+
+ r = json_variant_set_field(&arg_identity_extra_privileged, "sshAuthorizedKeys", v);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set authorized keys: %m");
+
+ break;
+ }
+
+ case ARG_NOT_BEFORE:
+ case ARG_NOT_AFTER:
+ case 'e': {
+ const char *field;
+ usec_t n;
+
+ field = c == ARG_NOT_BEFORE ? "notBeforeUSec" :
+ IN_SET(c, ARG_NOT_AFTER, 'e') ? "notAfterUSec" : NULL;
+
+ assert(field);
+
+ if (isempty(optarg)) {
+ r = drop_from_identity(field);
+ if (r < 0)
+ return r;
+
+ break;
+ }
+
+ /* Note the minor discrepancy regarding -e parsing here: we support that for compat
+ * reasons, and in the original useradd(8) implementation it accepts dates in the
+ * format YYYY-MM-DD. Coincidentally, we accept dates formatted like that too, but
+ * with greater precision. */
+ r = parse_timestamp(optarg, &n);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse %s parameter: %m", field);
+
+ r = json_variant_set_field_unsigned(&arg_identity_extra, field, n);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set %s field: %m", field);
+ break;
+ }
+
+ case ARG_PASSWORD_CHANGE_MIN:
+ case ARG_PASSWORD_CHANGE_MAX:
+ case ARG_PASSWORD_CHANGE_WARN:
+ case ARG_PASSWORD_CHANGE_INACTIVE: {
+ const char *field;
+ usec_t n;
+
+ field = c == ARG_PASSWORD_CHANGE_MIN ? "passwordChangeMinUSec" :
+ c == ARG_PASSWORD_CHANGE_MAX ? "passwordChangeMaxUSec" :
+ c == ARG_PASSWORD_CHANGE_WARN ? "passwordChangeWarnUSec" :
+ c == ARG_PASSWORD_CHANGE_INACTIVE ? "passwordChangeInactiveUSec" :
+ NULL;
+
+ assert(field);
+
+ if (isempty(optarg)) {
+ r = drop_from_identity(field);
+ if (r < 0)
+ return r;
+
+ break;
+ }
+
+ r = parse_sec(optarg, &n);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse %s parameter: %m", field);
+
+ r = json_variant_set_field_unsigned(&arg_identity_extra, field, n);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set %s field: %m", field);
+ break;
+ }
+
+ case ARG_STORAGE:
+ case ARG_FS_TYPE:
+ case ARG_LUKS_CIPHER:
+ case ARG_LUKS_CIPHER_MODE:
+ case ARG_LUKS_PBKDF_TYPE:
+ case ARG_LUKS_PBKDF_HASH_ALGORITHM: {
+
+ const char *field =
+ c == ARG_STORAGE ? "storage" :
+ c == ARG_FS_TYPE ? "fileSystemType" :
+ c == ARG_LUKS_CIPHER ? "luksCipher" :
+ c == ARG_LUKS_CIPHER_MODE ? "luksCipherMode" :
+ c == ARG_LUKS_PBKDF_TYPE ? "luksPbkdfType" :
+ c == ARG_LUKS_PBKDF_HASH_ALGORITHM ? "luksPbkdfHashAlgorithm" : NULL;
+
+ assert(field);
+
+ if (isempty(optarg)) {
+ r = drop_from_identity(field);
+ if (r < 0)
+ return r;
+
+ break;
+ }
+
+ if (!string_is_safe(optarg))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Parameter for %s field not valid: %s", field, optarg);
+
+ r = json_variant_set_field_string(
+ IN_SET(c, ARG_STORAGE, ARG_FS_TYPE) ?
+ &arg_identity_extra_this_machine :
+ &arg_identity_extra, field, optarg);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set %s field: %m", field);
+
+ break;
+ }
+
+ case ARG_LUKS_PBKDF_TIME_COST:
+ case ARG_RATE_LIMIT_INTERVAL:
+ case ARG_STOP_DELAY: {
+ const char *field =
+ c == ARG_LUKS_PBKDF_TIME_COST ? "luksPbkdfTimeCostUSec" :
+ c == ARG_RATE_LIMIT_INTERVAL ? "rateLimitIntervalUSec" :
+ c == ARG_STOP_DELAY ? "stopDelayUSec" :
+ NULL;
+ usec_t t;
+
+ assert(field);
+
+ if (isempty(optarg)) {
+ r = drop_from_identity(field);
+ if (r < 0)
+ return r;
+
+ break;
+ }
+
+ r = parse_sec(optarg, &t);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse %s field: %s", field, optarg);
+
+ r = json_variant_set_field_unsigned(&arg_identity_extra, field, t);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set %s field: %m", field);
+
+ break;
+ }
+
+ case 'G': {
+ const char *p = optarg;
+
+ if (isempty(p)) {
+ r = drop_from_identity("memberOf");
+ if (r < 0)
+ return r;
+
+ break;
+ }
+
+ for (;;) {
+ _cleanup_(json_variant_unrefp) JsonVariant *mo = NULL;
+ _cleanup_strv_free_ char **list = NULL;
+ _cleanup_free_ char *word = NULL;
+
+ r = extract_first_word(&p, &word, ",", 0);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse group list: %m");
+ if (r == 0)
+ break;
+
+ if (!valid_user_group_name(word, 0))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid group name %s.", word);
+
+ mo = json_variant_ref(json_variant_by_key(arg_identity_extra, "memberOf"));
+
+ r = json_variant_strv(mo, &list);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse group list: %m");
+
+ r = strv_extend(&list, word);
+ if (r < 0)
+ return log_oom();
+
+ strv_sort(list);
+ strv_uniq(list);
+
+ mo = json_variant_unref(mo);
+ r = json_variant_new_array_strv(&mo, list);
+ if (r < 0)
+ return log_error_errno(r, "Failed to create group list JSON: %m");
+
+ r = json_variant_set_field(&arg_identity_extra, "memberOf", mo);
+ if (r < 0)
+ return log_error_errno(r, "Failed to update group list: %m");
+ }
+
+ break;
+ }
+
+ case ARG_TASKS_MAX: {
+ uint64_t u;
+
+ if (isempty(optarg)) {
+ r = drop_from_identity("tasksMax");
+ if (r < 0)
+ return r;
+ break;
+ }
+
+ r = safe_atou64(optarg, &u);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse --tasks-max= parameter: %s", optarg);
+
+ r = json_variant_set_field_unsigned(&arg_identity_extra, "tasksMax", u);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set tasksMax field: %m");
+
+ break;
+ }
+
+ case ARG_MEMORY_MAX:
+ case ARG_MEMORY_HIGH:
+ case ARG_LUKS_PBKDF_MEMORY_COST: {
+ const char *field =
+ c == ARG_MEMORY_MAX ? "memoryMax" :
+ c == ARG_MEMORY_HIGH ? "memoryHigh" :
+ c == ARG_LUKS_PBKDF_MEMORY_COST ? "luksPbkdfMemoryCost" : NULL;
+
+ uint64_t u;
+
+ assert(field);
+
+ if (isempty(optarg)) {
+ r = drop_from_identity(field);
+ if (r < 0)
+ return r;
+ break;
+ }
+
+ r = parse_size(optarg, 1024, &u);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse %s parameter: %s", field, optarg);
+
+ r = json_variant_set_field_unsigned(&arg_identity_extra_this_machine, field, u);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set %s field: %m", field);
+
+ break;
+ }
+
+ case ARG_CPU_WEIGHT:
+ case ARG_IO_WEIGHT: {
+ const char *field = c == ARG_CPU_WEIGHT ? "cpuWeight" :
+ c == ARG_IO_WEIGHT ? "ioWeight" : NULL;
+ uint64_t u;
+
+ assert(field);
+
+ if (isempty(optarg)) {
+ r = drop_from_identity(field);
+ if (r < 0)
+ return r;
+ break;
+ }
+
+ r = safe_atou64(optarg, &u);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse --cpu-weight=/--io-weight= parameter: %s", optarg);
+
+ if (!CGROUP_WEIGHT_IS_OK(u))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Weight %" PRIu64 " is out of valid weight range.", u);
+
+ r = json_variant_set_field_unsigned(&arg_identity_extra, field, u);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set %s field: %m", field);
+
+ break;
+ }
+
+ case ARG_PKCS11_TOKEN_URI: {
+ const char *p;
+
+ if (streq(optarg, "list"))
+ return list_pkcs11_tokens();
+
+ /* If --pkcs11-token-uri= is specified we always drop everything old */
+ FOREACH_STRING(p, "pkcs11TokenUri", "pkcs11EncryptedKey") {
+ r = drop_from_identity(p);
+ if (r < 0)
+ return r;
+ }
+
+ if (isempty(optarg)) {
+ arg_pkcs11_token_uri = strv_free(arg_pkcs11_token_uri);
+ break;
+ }
+
+ if (streq(optarg, "auto")) {
+ _cleanup_free_ char *found = NULL;
+
+ r = find_pkcs11_token_auto(&found);
+ if (r < 0)
+ return r;
+ r = strv_consume(&arg_pkcs11_token_uri, TAKE_PTR(found));
+ } else {
+ if (!pkcs11_uri_valid(optarg))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Not a valid PKCS#11 URI: %s", optarg);
+
+ r = strv_extend(&arg_pkcs11_token_uri, optarg);
+ }
+ if (r < 0)
+ return r;
+
+ strv_uniq(arg_pkcs11_token_uri);
+ break;
+ }
+
+ case ARG_FIDO2_DEVICE: {
+ const char *p;
+
+ if (streq(optarg, "list"))
+ return list_fido2_devices();
+
+ FOREACH_STRING(p, "fido2HmacCredential", "fido2HmacSalt") {
+ r = drop_from_identity(p);
+ if (r < 0)
+ return r;
+ }
+
+ if (isempty(optarg)) {
+ arg_fido2_device = strv_free(arg_fido2_device);
+ break;
+ }
+
+ if (streq(optarg, "auto")) {
+ _cleanup_free_ char *found = NULL;
+
+ r = find_fido2_auto(&found);
+ if (r < 0)
+ return r;
+
+ r = strv_consume(&arg_fido2_device, TAKE_PTR(found));
+ } else
+ r = strv_extend(&arg_fido2_device, optarg);
+
+ if (r < 0)
+ return r;
+
+ strv_uniq(arg_fido2_device);
+ break;
+ }
+
+ case ARG_RECOVERY_KEY: {
+ const char *p;
+
+ r = parse_boolean(optarg);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse --recovery-key= argument: %s", optarg);
+
+ arg_recovery_key = r;
+
+ FOREACH_STRING(p, "recoveryKey", "recoveryKeyType") {
+ r = drop_from_identity(p);
+ if (r < 0)
+ return r;
+ }
+
+ break;
+ }
+
+ case 'j':
+ arg_json = true;
+ arg_json_format_flags = JSON_FORMAT_PRETTY_AUTO|JSON_FORMAT_COLOR_AUTO;
+ break;
+
+ case ARG_JSON:
+ if (streq(optarg, "pretty")) {
+ arg_json = true;
+ arg_json_format_flags = JSON_FORMAT_PRETTY|JSON_FORMAT_COLOR_AUTO;
+ } else if (streq(optarg, "short")) {
+ arg_json = true;
+ arg_json_format_flags = JSON_FORMAT_NEWLINE;
+ } else if (streq(optarg, "off")) {
+ arg_json = false;
+ arg_json_format_flags = 0;
+ } else if (streq(optarg, "help")) {
+ puts("pretty\n"
+ "short\n"
+ "off");
+ return 0;
+ } else
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown argument to --json=: %s", optarg);
+
+ break;
+
+ case 'E':
+ if (arg_export_format == EXPORT_FORMAT_FULL)
+ arg_export_format = EXPORT_FORMAT_STRIPPED;
+ else if (arg_export_format == EXPORT_FORMAT_STRIPPED)
+ arg_export_format = EXPORT_FORMAT_MINIMAL;
+ else
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Specifying -E more than twice is not supported.");
+
+ arg_json = true;
+ if (arg_json_format_flags == 0)
+ arg_json_format_flags = JSON_FORMAT_PRETTY_AUTO|JSON_FORMAT_COLOR_AUTO;
+ break;
+
+ case ARG_EXPORT_FORMAT:
+ if (streq(optarg, "full"))
+ arg_export_format = EXPORT_FORMAT_FULL;
+ else if (streq(optarg, "stripped"))
+ arg_export_format = EXPORT_FORMAT_STRIPPED;
+ else if (streq(optarg, "minimal"))
+ arg_export_format = EXPORT_FORMAT_MINIMAL;
+ else if (streq(optarg, "help")) {
+ puts("full\n"
+ "stripped\n"
+ "minimal");
+ return 0;
+ }
+
+ break;
+
+ case ARG_AND_RESIZE:
+ arg_and_resize = true;
+ break;
+
+ case ARG_AND_CHANGE_PASSWORD:
+ arg_and_change_password = true;
+ break;
+
+ case '?':
+ return -EINVAL;
+
+ default:
+ assert_not_reached("Unhandled option");
+ }
+ }
+
+ if (!strv_isempty(arg_pkcs11_token_uri) || !strv_isempty(arg_fido2_device))
+ arg_and_change_password = true;
+
+ if (arg_disk_size != UINT64_MAX || arg_disk_size_relative != UINT64_MAX)
+ arg_and_resize = true;
+
+ return 1;
+}
+
+static int redirect_bus_mgr(void) {
+ const char *suffix;
+
+ /* Talk to a different service if that's requested. (The same env var is also understood by homed, so
+ * that it is relatively easily possible to invoke a second instance of homed for debug purposes and
+ * have homectl talk to it, without colliding with the host version. This is handy when operating
+ * from a homed-managed account.) */
+
+ suffix = getenv("SYSTEMD_HOME_DEBUG_SUFFIX");
+ if (suffix) {
+ static BusLocator locator = {
+ .path = "/org/freedesktop/home1",
+ .interface = "org.freedesktop.home1.Manager",
+ };
+
+ /* Yes, we leak this memory, but there's little point to collect this, given that we only do
+ * this in a debug environment, do it only once, and the string shall live for out entire
+ * process runtime. */
+
+ locator.destination = strjoin("org.freedesktop.home1.", suffix);
+ if (!locator.destination)
+ return log_oom();
+
+ bus_mgr = &locator;
+ } else
+ bus_mgr = bus_home_mgr;
+
+ return 0;
+}
+
+static int run(int argc, char *argv[]) {
+ static const Verb verbs[] = {
+ { "help", VERB_ANY, VERB_ANY, 0, help },
+ { "list", VERB_ANY, 1, VERB_DEFAULT, list_homes },
+ { "activate", 2, VERB_ANY, 0, activate_home },
+ { "deactivate", 2, VERB_ANY, 0, deactivate_home },
+ { "inspect", VERB_ANY, VERB_ANY, 0, inspect_home },
+ { "authenticate", VERB_ANY, VERB_ANY, 0, authenticate_home },
+ { "create", VERB_ANY, 2, 0, create_home },
+ { "remove", 2, VERB_ANY, 0, remove_home },
+ { "update", VERB_ANY, 2, 0, update_home },
+ { "passwd", VERB_ANY, 2, 0, passwd_home },
+ { "resize", 2, 3, 0, resize_home },
+ { "lock", 2, VERB_ANY, 0, lock_home },
+ { "unlock", 2, VERB_ANY, 0, unlock_home },
+ { "with", 2, VERB_ANY, 0, with_home },
+ { "lock-all", VERB_ANY, 1, 0, lock_all_homes },
+ { "deactivate-all", VERB_ANY, 1, 0, deactivate_all_homes },
+ {}
+ };
+
+ int r;
+
+ log_setup_cli();
+
+ r = redirect_bus_mgr();
+ if (r < 0)
+ return r;
+
+ r = parse_argv(argc, argv);
+ if (r <= 0)
+ return r;
+
+ return dispatch_verb(argc, argv, verbs, NULL);
+}
+
+DEFINE_MAIN_FUNCTION(run);
diff --git a/src/home/homed-bus.c b/src/home/homed-bus.c
new file mode 100644
index 0000000..d70fda5
--- /dev/null
+++ b/src/home/homed-bus.c
@@ -0,0 +1,66 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#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
index 0000000..977679b
--- /dev/null
+++ b/src/home/homed-bus.h
@@ -0,0 +1,10 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#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-conf.c b/src/home/homed-conf.c
new file mode 100644
index 0000000..4f46b0c
--- /dev/null
+++ b/src/home/homed-conf.c
@@ -0,0 +1,54 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "conf-parser.h"
+#include "def.h"
+#include "home-util.h"
+#include "homed-conf.h"
+
+int manager_parse_config_file(Manager *m) {
+ int r;
+
+ assert(m);
+
+ r = config_parse_many_nulstr(
+ PKGSYSCONFDIR "/homed.conf",
+ CONF_PATHS_NULSTR("systemd/homed.conf.d"),
+ "Home\0",
+ config_item_perf_lookup, homed_gperf_lookup,
+ CONFIG_PARSE_WARN,
+ m,
+ NULL);
+ if (r < 0)
+ return r;
+
+ return 0;
+
+}
+
+DEFINE_CONFIG_PARSE_ENUM(config_parse_default_storage, user_storage, UserStorage, "Failed to parse default storage setting");
+
+int config_parse_default_file_system_type(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ char **s = data;
+
+ assert(rvalue);
+ assert(s);
+
+ if (!isempty(rvalue) && !supported_fstype(rvalue)) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0, "Unsupported file system, ignoring: %s", rvalue);
+ return 0;
+ }
+
+ return free_and_strdup_warn(s, empty_to_null(rvalue));
+
+}
diff --git a/src/home/homed-conf.h b/src/home/homed-conf.h
new file mode 100644
index 0000000..1defaa9
--- /dev/null
+++ b/src/home/homed-conf.h
@@ -0,0 +1,12 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "conf-parser.h"
+#include "homed-manager.h"
+
+int manager_parse_config_file(Manager *m);
+
+const struct ConfigPerfItem* homed_gperf_lookup(const char *key, GPERF_LEN_TYPE length);
+
+CONFIG_PARSER_PROTOTYPE(config_parse_default_storage);
+CONFIG_PARSER_PROTOTYPE(config_parse_default_file_system_type);
diff --git a/src/home/homed-gperf.gperf b/src/home/homed-gperf.gperf
new file mode 100644
index 0000000..970da5f
--- /dev/null
+++ b/src/home/homed-gperf.gperf
@@ -0,0 +1,21 @@
+%{
+#if __GNUC__ >= 7
+_Pragma("GCC diagnostic ignored \"-Wimplicit-fallthrough\"")
+#endif
+#include <stddef.h>
+#include "conf-parser.h"
+#include "homed-conf.h"
+%}
+struct ConfigPerfItem;
+%null_strings
+%language=ANSI-C
+%define slot-name section_and_lvalue
+%define hash-function-name homed_gperf_hash
+%define lookup-function-name homed_gperf_lookup
+%readonly-tables
+%omit-struct-type
+%struct-type
+%includes
+%%
+Home.DefaultStorage, config_parse_default_storage, 0, offsetof(Manager, default_storage)
+Home.DefaultFileSystemType, config_parse_default_file_system_type, 0, offsetof(Manager, default_file_system_type)
diff --git a/src/home/homed-home-bus.c b/src/home/homed-home-bus.c
new file mode 100644
index 0000000..5643a9a
--- /dev/null
+++ b/src/home/homed-home-bus.c
@@ -0,0 +1,953 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#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:
+ case HOME_DIRTY:
+ 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));
+
+int bus_home_path(Home *h, char **ret) {
+ assert(ret);
+
+ return sd_bus_path_encode("/org/freedesktop/home1/home", h->user_name, ret);
+}
+
+static 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;
+}
+
+static 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;
+ 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) {
+ r = bus_home_path(h, l + k);
+ if (r < 0)
+ return r;
+ }
+
+ *nodes = TAKE_PTR(l);
+ return 1;
+}
+
+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_WITH_NAMES("Activate",
+ "s",
+ SD_BUS_PARAM(secret),
+ 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_WITH_NAMES("Realize",
+ "s",
+ SD_BUS_PARAM(secret),
+ 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_WITH_NAMES("Fixate",
+ "s",
+ SD_BUS_PARAM(secret),
+ NULL,,
+ bus_home_method_fixate,
+ SD_BUS_VTABLE_SENSITIVE),
+ SD_BUS_METHOD_WITH_NAMES("Authenticate",
+ "s",
+ SD_BUS_PARAM(secret),
+ NULL,,
+ bus_home_method_authenticate,
+ SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),
+ SD_BUS_METHOD_WITH_NAMES("Update",
+ "s",
+ SD_BUS_PARAM(user_record),
+ NULL,,
+ bus_home_method_update,
+ SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),
+ SD_BUS_METHOD_WITH_NAMES("Resize",
+ "ts",
+ SD_BUS_PARAM(size)
+ SD_BUS_PARAM(secret),
+ NULL,,
+ bus_home_method_resize,
+ SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),
+ SD_BUS_METHOD_WITH_NAMES("ChangePassword",
+ "ss",
+ SD_BUS_PARAM(new_secret)
+ SD_BUS_PARAM(old_secret),
+ 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_WITH_NAMES("Unlock",
+ "s",
+ SD_BUS_PARAM(secret),
+ NULL,,
+ bus_home_method_unlock,
+ SD_BUS_VTABLE_SENSITIVE),
+ SD_BUS_METHOD_WITH_NAMES("Acquire",
+ "sb",
+ SD_BUS_PARAM(secret)
+ SD_BUS_PARAM(please_suspend),
+ "h",
+ SD_BUS_PARAM(send_fd),
+ bus_home_method_acquire,
+ SD_BUS_VTABLE_SENSITIVE),
+ SD_BUS_METHOD_WITH_NAMES("Ref",
+ "b",
+ SD_BUS_PARAM(please_suspend),
+ "h",
+ SD_BUS_PARAM(send_fd),
+ bus_home_method_ref,
+ 0),
+ SD_BUS_METHOD("Release", NULL, NULL, bus_home_method_release, 0),
+ SD_BUS_VTABLE_END
+};
+
+const BusObjectImplementation home_object = {
+ "/org/freedesktop/home1/home",
+ "org.freedesktop.home1.Home",
+ .fallback_vtables = BUS_FALLBACK_VTABLES({home_vtable, bus_home_object_find}),
+ .node_enumerator = bus_home_node_enumerator,
+ .manager = true,
+};
+
+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
index 0000000..5522178
--- /dev/null
+++ b/src/home/homed-home-bus.h
@@ -0,0 +1,34 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "sd-bus.h"
+
+#include "bus-object.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 BusObjectImplementation home_object;
+
+int bus_home_path(Home *h, char **ret);
+
+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
index 0000000..7f4532e
--- /dev/null
+++ b/src/home/homed-home.c
@@ -0,0 +1,2836 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#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-pwquality.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|WRITE_STRING_FILE_SYNC);
+ 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 = take_fdopen(&fd, "r");
+ if (!f)
+ return log_error_errno(errno, "Failed to reopen memfd: %m");
+
+ 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 contains 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 be shrunk");
+ case -ETXTBSY:
+ return sd_bus_error_setf(error, BUS_ERROR_BAD_HOME_SIZE, "File systems of this type can only be shrunk 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 -EREMOTEIO:
+ return sd_bus_error_setf(error, BUS_ERROR_BAD_RECOVERY_KEY, "Recovery key for home %s is incorrect or not sufficient for authentication.", 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 -EMEDIUMTYPE:
+ return sd_bus_error_setf(error, BUS_ERROR_TOKEN_USER_PRESENCE_NEEDED, "Security token requires user presence.");
+ case -ENOSTR:
+ return sd_bus_error_setf(error, BUS_ERROR_TOKEN_ACTION_TIMEOUT, "Token action timeout. (User was supposed to verify presence or similar, by interacting with the token, and didn't do that in time.)");
+ 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);
+ case -EKEYREVOKED:
+ return sd_bus_error_setf(error, BUS_ERROR_HOME_CANT_AUTHENTICATE, "Home %s has no password or other authentication mechanism defined.", 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) {
+ const char *homework, *suffix, *unix_path;
+
+ /* Child */
+
+ suffix = getenv("SYSTEMD_HOME_DEBUG_SUFFIX");
+ if (suffix)
+ unix_path = strjoina("/run/systemd/home/notify.", suffix);
+ else
+ unix_path = "/run/systemd/home/notify";
+
+ if (setenv("NOTIFY_SOCKET", unix_path, 1) < 0) {
+ log_error_errno(errno, "Failed to set $NOTIFY_SOCKET: %m");
+ _exit(EXIT_FAILURE);
+ }
+
+ if (h->manager->default_storage >= 0)
+ if (setenv("SYSTEMD_HOME_DEFAULT_STORAGE", user_storage_to_string(h->manager->default_storage), 1) < 0) {
+ log_error_errno(errno, "Failed to set $SYSTEMD_HOME_DEFAULT_STORAGE: %m");
+ _exit(EXIT_FAILURE);
+ }
+
+ if (h->manager->default_file_system_type)
+ if (setenv("SYSTEMD_HOME_DEFAULT_FILE_SYSTEM_TYPE", h->manager->default_file_system_type, 1) < 0) {
+ log_error_errno(errno, "Failed to set $SYSTEMD_HOME_DEFAULT_FILE_SYSTEM_TYPE: %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() */
+
+ /* Allow overriding the homework path via an environment variable, to make debugging
+ * easier. */
+ homework = getenv("SYSTEMD_HOMEWORK_PATH") ?: SYSTEMD_HOMEWORK_PATH;
+
+ execl(homework, homework, verb, NULL);
+ log_error_errno(errno, "Failed to invoke %s: %m", homework);
+ _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 (IN_SET(for_state, HOME_FIXATING_FOR_ACTIVATION, HOME_FIXATING_FOR_ACQUIRE)) {
+ /* 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_DIRTY:
+ 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:
+ case HOME_DIRTY:
+ 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_DIRTY:
+ 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:
+ case HOME_DIRTY:
+ 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: {
+ int t;
+
+ 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. */
+
+ t = user_record_test_image_path(h->record);
+ if (IN_SET(t, USER_TEST_MAYBE, USER_TEST_UNDEFINED))
+ break; /* And if the image path test isn't conclusive, let's also go on */
+
+ if (IN_SET(t, -EBADFD, -ENOTDIR))
+ return sd_bus_error_setf(error, BUS_ERROR_HOME_EXISTS, "Selected home image of user %s already exists or has wrong inode type.", h->user_name);
+
+ return sd_bus_error_setf(error, BUS_ERROR_HOME_EXISTS, "Selected home image of user %s already exists.", h->user_name);
+ }
+ case HOME_UNFIXATED:
+ case HOME_DIRTY:
+ 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 = user_record_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:
+ case HOME_DIRTY:
+ 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_DIRTY:
+ 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_DIRTY:
+ 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, "No 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, -ESTALE))
+ return 0; /* expired in some form, but changing 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_DIRTY:
+ 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 = user_record_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:
+ case HOME_DIRTY:
+ 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:
+ case HOME_DIRTY:
+ 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:
+ case HOME_DIRTY:
+ 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) {
+ int r;
+ 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 */
+ r = user_record_test_image_path(h->record);
+ if (r == USER_TEST_ABSENT)
+ return HOME_ABSENT;
+ if (r == USER_TEST_DIRTY)
+ return HOME_DIRTY;
+
+ /* 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 received 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:
+ case HOME_DIRTY:
+ 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:
+ case HOME_DIRTY:
+ r = 1; /* 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:
+ case HOME_DIRTY:
+ log_info("Home %s is not active, no locking necessary.", h->user_name);
+ r = 1; /* done */
+ break;
+
+ case HOME_LOCKED:
+ log_info("Home %s is already locked.", h->user_name);
+ r = 1; /* 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_deactivate_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_DEACTIVATE_ALL);
+
+ switch (home_get_state(h)) {
+
+ case HOME_UNFIXATED:
+ case HOME_ABSENT:
+ case HOME_INACTIVE:
+ case HOME_DIRTY:
+ log_info("Home %s is already deactivated.", h->user_name);
+ r = 1; /* done */
+ break;
+
+ case HOME_LOCKED:
+ log_info("Home %s is currently locked, not deactivating.", h->user_name);
+ r = 1; /* done */
+ break;
+
+ case HOME_ACTIVE:
+ log_info("Deactivating home %s.", h->user_name);
+ 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_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:
+ case HOME_DIRTY:
+ 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:
+ case HOME_DIRTY:
+ 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_DEACTIVATE_ALL] = home_dispatch_deactivate_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;
+}
+
+int home_wait_for_worker(Home *h) {
+ assert(h);
+
+ if (h->worker_pid <= 0)
+ return 0;
+
+ log_info("Worker process for home %s is still running while exiting. Waiting for it to finish.", h->user_name);
+ (void) wait_for_terminate(h->worker_pid, NULL);
+ (void) hashmap_remove_value(h->manager->homes_by_worker_pid, PID_TO_PTR(h->worker_pid), h);
+ h->worker_pid = 0;
+ return 1;
+}
+
+static const char* const home_state_table[_HOME_STATE_MAX] = {
+ [HOME_UNFIXATED] = "unfixated",
+ [HOME_ABSENT] = "absent",
+ [HOME_INACTIVE] = "inactive",
+ [HOME_DIRTY] = "dirty",
+ [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
index 0000000..4c24ee7
--- /dev/null
+++ b/src/home/homed-home.h
@@ -0,0 +1,171 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#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_DIRTY, /* like HOME_INACTIVE, but the home directory wasn't cleanly deactivated */
+ 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 area 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);
+
+int home_wait_for_worker(Home *h);
+
+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
index 0000000..d3ac98f
--- /dev/null
+++ b/src/home/homed-manager-bus.c
@@ -0,0 +1,899 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#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;
+ 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) {
+ _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, 0))
+ 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;
+ 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) {
+ _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, 0))
+ 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, 0))
+ 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;
+ 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) {
+
+ /* 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 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);
+}
+
+static int method_deactivate_all_homes(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ _cleanup_(operation_unrefp) Operation *o = NULL;
+ bool waiting = false;
+ Manager *m = userdata;
+ Home *h;
+ int r;
+
+ assert(m);
+
+ /* This is called from systemd-homed-activate.service's ExecStop= command to ensure that all home
+ * directories are shutdown before the system goes down. Note that we don't do this from
+ * systemd-homed.service itself since we want to allow restarting of it without tearing down all home
+ * directories. */
+
+ HASHMAP_FOREACH(h, m->homes_by_name) {
+
+ if (!o) {
+ o = operation_new(OPERATION_DEACTIVATE_ALL, message);
+ if (!o)
+ return -ENOMEM;
+ }
+
+ log_info("Automatically deactivating 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 deactivation operations completed. */
+ return 1;
+
+ return sd_bus_reply_method_return(message, NULL);
+}
+
+static 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_WITH_NAMES("GetHomeByName",
+ "s",
+ SD_BUS_PARAM(user_name),
+ "usussso",
+ SD_BUS_PARAM(uid)
+ SD_BUS_PARAM(home_state)
+ SD_BUS_PARAM(gid)
+ SD_BUS_PARAM(real_name)
+ SD_BUS_PARAM(home_directory)
+ SD_BUS_PARAM(shell)
+ SD_BUS_PARAM(bus_path),
+ method_get_home_by_name,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD_WITH_NAMES("GetHomeByUID",
+ "u",
+ SD_BUS_PARAM(uid),
+ "ssussso",
+ SD_BUS_PARAM(user_name)
+ SD_BUS_PARAM(home_state)
+ SD_BUS_PARAM(gid)
+ SD_BUS_PARAM(real_name)
+ SD_BUS_PARAM(home_directory)
+ SD_BUS_PARAM(shell)
+ SD_BUS_PARAM(bus_path),
+ method_get_home_by_uid,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD_WITH_NAMES("GetUserRecordByName",
+ "s",
+ SD_BUS_PARAM(user_name),
+ "sbo",
+ SD_BUS_PARAM(user_record)
+ SD_BUS_PARAM(incomplete)
+ SD_BUS_PARAM(bus_path),
+ method_get_user_record_by_name,
+ SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),
+ SD_BUS_METHOD_WITH_NAMES("GetUserRecordByUID",
+ "u",
+ SD_BUS_PARAM(uid),
+ "sbo",
+ SD_BUS_PARAM(user_record)
+ SD_BUS_PARAM(incomplete)
+ SD_BUS_PARAM(bus_path),
+ method_get_user_record_by_uid,
+ SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),
+ SD_BUS_METHOD_WITH_NAMES("ListHomes",
+ NULL,,
+ "a(susussso)",
+ SD_BUS_PARAM(home_areas),
+ method_list_homes,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+
+ /* The following methods directly execute an operation on a home area, without ref-counting, queueing
+ * or anything, and are accessible through homectl. */
+ SD_BUS_METHOD_WITH_NAMES("ActivateHome",
+ "ss",
+ SD_BUS_PARAM(user_name)
+ SD_BUS_PARAM(secret),
+ NULL,,
+ method_activate_home,
+ SD_BUS_VTABLE_SENSITIVE),
+ SD_BUS_METHOD_WITH_NAMES("DeactivateHome",
+ "s",
+ SD_BUS_PARAM(user_name),
+ NULL,,
+ method_deactivate_home,
+ 0),
+
+ /* Add the JSON record to homed, but don't create actual $HOME */
+ SD_BUS_METHOD_WITH_NAMES("RegisterHome",
+ "s",
+ SD_BUS_PARAM(user_record),
+ NULL,,
+ method_register_home,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+
+ /* Remove the JSON record from homed, but don't remove actual $HOME */
+ SD_BUS_METHOD_WITH_NAMES("UnregisterHome",
+ "s",
+ SD_BUS_PARAM(user_name),
+ NULL,,
+ method_unregister_home,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+
+ /* Add JSON record, and create $HOME for it */
+ SD_BUS_METHOD_WITH_NAMES("CreateHome",
+ "s",
+ SD_BUS_PARAM(user_record),
+ NULL,,
+ method_create_home,
+ SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),
+
+ /* Create $HOME for already registered JSON entry */
+ SD_BUS_METHOD_WITH_NAMES("RealizeHome",
+ "ss",
+ SD_BUS_PARAM(user_name)
+ SD_BUS_PARAM(secret),
+ NULL,,
+ method_realize_home,
+ SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),
+
+ /* Remove the JSON record and remove $HOME */
+ SD_BUS_METHOD_WITH_NAMES("RemoveHome",
+ "s",
+ SD_BUS_PARAM(user_name),
+ NULL,,
+ method_remove_home,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+
+ /* Investigate $HOME and propagate contained JSON record into our database */
+ SD_BUS_METHOD_WITH_NAMES("FixateHome",
+ "ss",
+ SD_BUS_PARAM(user_name)
+ SD_BUS_PARAM(secret),
+ NULL,,
+ method_fixate_home,
+ SD_BUS_VTABLE_SENSITIVE),
+
+ /* Just check credentials */
+ SD_BUS_METHOD_WITH_NAMES("AuthenticateHome",
+ "ss",
+ SD_BUS_PARAM(user_name)
+ SD_BUS_PARAM(secret),
+ NULL,,
+ method_authenticate_home,
+ SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),
+
+ /* Update the JSON record of existing user */
+ SD_BUS_METHOD_WITH_NAMES("UpdateHome",
+ "s",
+ SD_BUS_PARAM(user_record),
+ NULL,,
+ method_update_home,
+ SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),
+
+ SD_BUS_METHOD_WITH_NAMES("ResizeHome",
+ "sts",
+ SD_BUS_PARAM(user_name)
+ SD_BUS_PARAM(size)
+ SD_BUS_PARAM(secret),
+ NULL,,
+ method_resize_home,
+ SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),
+
+ SD_BUS_METHOD_WITH_NAMES("ChangePasswordHome",
+ "sss",
+ SD_BUS_PARAM(user_name)
+ SD_BUS_PARAM(new_secret)
+ SD_BUS_PARAM(old_secret),
+ NULL,,
+ method_change_password_home,
+ SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),
+
+ /* Prepare active home for system suspend: flush out passwords, suspend access */
+ SD_BUS_METHOD_WITH_NAMES("LockHome",
+ "s",
+ SD_BUS_PARAM(user_name),
+ NULL,,
+ method_lock_home,
+ 0),
+
+ /* Make $HOME usable after system resume again */
+ SD_BUS_METHOD_WITH_NAMES("UnlockHome",
+ "ss",
+ SD_BUS_PARAM(user_name)
+ SD_BUS_PARAM(secret),
+ NULL,,
+ method_unlock_home,
+ SD_BUS_VTABLE_SENSITIVE),
+
+ /* The following methods implement ref-counted activation, and are what the PAM module and "homectl
+ * with" use. 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 area, 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 the home area is already active. */
+ SD_BUS_METHOD_WITH_NAMES("AcquireHome",
+ "ssb",
+ SD_BUS_PARAM(user_name)
+ SD_BUS_PARAM(secret)
+ SD_BUS_PARAM(please_suspend),
+ "h",
+ SD_BUS_PARAM(send_fd),
+ method_acquire_home,
+ SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),
+ SD_BUS_METHOD_WITH_NAMES("RefHome",
+ "sb",
+ SD_BUS_PARAM(user_name)
+ SD_BUS_PARAM(please_suspend),
+ "h",
+ SD_BUS_PARAM(send_fd),
+ method_ref_home,
+ 0),
+ SD_BUS_METHOD_WITH_NAMES("ReleaseHome",
+ "s",
+ SD_BUS_PARAM(user_name),
+ 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_METHOD("DeactivateAllHomes", NULL, NULL, method_deactivate_all_homes, 0),
+
+ SD_BUS_VTABLE_END
+};
+
+const BusObjectImplementation manager_object = {
+ "/org/freedesktop/home1",
+ "org.freedesktop.home1.Manager",
+ .vtables = BUS_VTABLES(manager_vtable),
+ .children = BUS_IMPLEMENTATIONS(&home_object),
+};
+
+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
index 0000000..7db29fa
--- /dev/null
+++ b/src/home/homed-manager-bus.h
@@ -0,0 +1,6 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "bus-util.h"
+
+extern const BusObjectImplementation manager_object;
diff --git a/src/home/homed-manager.c b/src/home/homed-manager.c
new file mode 100644
index 0000000..365ea4d
--- /dev/null
+++ b/src/home/homed-manager.c
@@ -0,0 +1,1742 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#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-log-control-api.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-conf.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_CLOSE_WRITE | IN_MOVED_FROM)) != 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 = new(Manager, 1);
+ if (!m)
+ return -ENOMEM;
+
+ *m = (Manager) {
+ .default_storage = _USER_STORAGE_INVALID,
+ };
+
+ r = manager_parse_config_file(m);
+ if (r < 0)
+ return r;
+
+ 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) {
+ Home *h;
+
+ assert(m);
+
+ HASHMAP_FOREACH(h, m->homes_by_worker_pid)
+ (void) home_wait_for_worker(h);
+
+ 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);
+ free(m->userdb_service);
+
+ free(m->default_file_system_type);
+
+ return mfree(m);
+}
+
+int manager_verify_user_record(Manager *m, UserRecord *hr) {
+ EVP_PKEY *pkey;
+ 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) {
+ 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 = NULL;
+ unsigned line, column;
+ int r, is_signed;
+ struct stat st;
+ Home *h;
+
+ assert(m);
+ assert(name);
+ assert(fname);
+
+ if (fstatat(dir_fd, fname, &st, 0) < 0)
+ return log_error_errno(errno, "Failed to stat identity record %s: %m", fname);
+
+ if (!S_ISREG(st.st_mode)) {
+ log_debug("Identity record file %s is not a regular file, ignoring.", fname);
+ return 0;
+ }
+
+ if (st.st_size == 0)
+ goto unlink_this_file;
+
+ 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);
+
+ if (json_variant_is_blank_object(v))
+ goto unlink_this_file;
+
+ hr = user_record_new();
+ if (!hr)
+ return log_oom();
+
+ r = user_record_load(hr, v, USER_RECORD_LOAD_REFUSE_SECRET|USER_RECORD_LOG);
+ 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 distinguish 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;
+
+unlink_this_file:
+ /* If this is an empty file, then let's just remove it. An empty file is not useful in any case, and
+ * apparently xfs likes to leave empty files around when not unmounted cleanly (see
+ * https://github.com/systemd/systemd/issues/15178 for example). Note that we don't delete non-empty
+ * files even if they are invalid, because that's just too risky, we might delete data the user still
+ * needs. But empty files are never useful, hence let's just remove them. */
+
+ if (unlinkat(dir_fd, fname, 0) < 0)
+ return log_error_errno(errno, "Failed to remove empty user record file %s: %m", fname);
+
+ log_notice("Discovered empty user record file /var/lib/systemd/home/%s, removed automatically.", fname);
+ return 0;
+}
+
+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 if (ERRNO_IS_PRIVILEGE(r))
+ log_debug_errno(r, "UID quota support for %s prohibited, ignoring.", where);
+ else
+ log_warning_errno(r, "Failed to query quota on %s, ignoring: %m", 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 multiple images for 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 doesn'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) {
+ const char *suffix, *busname;
+ 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 = bus_add_implementation(m->bus, &manager_object, m);
+ if (r < 0)
+ return r;
+
+ suffix = getenv("SYSTEMD_HOME_DEBUG_SUFFIX");
+ if (suffix)
+ busname = strjoina("org.freedesktop.home1.", suffix);
+ else
+ busname = "org.freedesktop.home1";
+
+ r = sd_bus_request_name_async(m->bus, NULL, busname, 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) {
+ const char *suffix, *socket_path;
+ 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);
+
+ /* To make things easier to debug, when working from a homed managed home directory, let's optionally
+ * use a different varlink socket name */
+ suffix = getenv("SYSTEMD_HOME_DEBUG_SUFFIX");
+ if (suffix)
+ socket_path = strjoina("/run/systemd/userdb/io.systemd.Home.", suffix);
+ else
+ socket_path = "/run/systemd/userdb/io.systemd.Home";
+
+ r = varlink_server_listen_address(m->varlink_server, socket_path, 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");
+
+ assert(!m->userdb_service);
+ m->userdb_service = strdup(basename(socket_path));
+ if (!m->userdb_service)
+ return log_oom();
+
+ /* Avoid recursion */
+ if (setenv("SYSTEMD_BYPASS_USERDB", m->userdb_service, 1) < 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to set $SYSTEMD_BYPASS_USERDB: %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) {
+ CMSG_BUFFER_TYPE(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_safe(fd, &mh, MSG_DONTWAIT|MSG_CMSG_CLOEXEC);
+ if (m < 0)
+ return m;
+
+ 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("Received 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 = {
+ .un.sun_family = AF_UNIX,
+ .un.sun_path = "/run/systemd/home/notify",
+ };
+ const char *suffix;
+ int r;
+
+ assert(m);
+ assert(!m->notify_socket_event_source);
+
+ suffix = getenv("SYSTEMD_HOME_DEBUG_SUFFIX");
+ if (suffix) {
+ const char *unix_path;
+
+ unix_path = strjoina("/run/systemd/home/notify.", suffix);
+ r = sockaddr_un_set_path(&sa.un, unix_path);
+ if (r < 0)
+ return log_error_errno(r, "Socket path %s does not fit in sockaddr_un: %m", unix_path);
+ }
+
+ 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");
+
+ (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 to 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_sync_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 to 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_sync_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 private key file into place: %m");
+ }
+ temp_private = mfree(temp_private);
+
+ r = fsync_path_at(AT_FDCWD, "/var/lib/systemd/home/");
+ if (r < 0)
+ log_warning_errno(r, "Failed to sync /var/lib/systemd/home/, ignoring: %m");
+
+ 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 */
+
+ HASHMAP_FOREACH(h, m->homes_by_name)
+ 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 focused */
+
+ 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
index 0000000..851b302
--- /dev/null
+++ b/src/home/homed-manager.h
@@ -0,0 +1,70 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#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;
+ UserStorage default_storage;
+ char *default_file_system_type;
+
+ sd_event_source *inotify_event_source;
+
+ /* An event source we receive 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;
+ char *userdb_service;
+
+ 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
index 0000000..3847fc5
--- /dev/null
+++ b/src/home/homed-operation.c
@@ -0,0 +1,76 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#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 to 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
index 0000000..6721363
--- /dev/null
+++ b/src/home/homed-operation.h
@@ -0,0 +1,63 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#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_DEACTIVATE_ALL, /* enqueued on DeactivateAllHomes() */
+ 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
index 0000000..c429083
--- /dev/null
+++ b/src/home/homed-varlink.c
@@ -0,0 +1,366 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#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, m->userdb_service))
+ 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 {
+
+ /* 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) {
+
+ 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, m->userdb_service))
+ 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 {
+
+ HASHMAP_FOREACH(h, m->homes_by_name) {
+
+ 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, m->userdb_service))
+ 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;
+
+ HASHMAP_FOREACH(h, m->homes_by_name) {
+
+ 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;
+
+ HASHMAP_FOREACH(h, m->homes_by_name) {
+ 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
index 0000000..2e404f0
--- /dev/null
+++ b/src/home/homed-varlink.h
@@ -0,0 +1,8 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#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
index 0000000..e4d64bd
--- /dev/null
+++ b/src/home/homed.c
@@ -0,0 +1,51 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#include "bus-log-control-api.h"
+#include "daemon-util.h"
+#include "homed-manager.h"
+#include "homed-manager-bus.h"
+#include "log.h"
+#include "main-func.h"
+#include "service-util.h"
+#include "signal-util.h"
+
+static int run(int argc, char *argv[]) {
+ _cleanup_(manager_freep) Manager *m = NULL;
+ _cleanup_(notify_on_cleanup) const char *notify_stop = NULL;
+ int r;
+
+ log_setup_service();
+
+ r = service_parse_argv("systemd-homed.service",
+ "A service to create, remove, change or inspect home areas.",
+ BUS_IMPLEMENTATIONS(&manager_object,
+ &log_control_object),
+ argc, argv);
+ if (r <= 0)
+ return r;
+
+ umask(0022);
+
+ 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/homed.conf b/src/home/homed.conf
new file mode 100644
index 0000000..1b5dbed
--- /dev/null
+++ b/src/home/homed.conf
@@ -0,0 +1,16 @@
+# 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.
+#
+# Entries in this file show the compile time defaults.
+# You can change settings by editing this file.
+# Defaults can be restored by simply deleting this file.
+#
+# See homed.conf(5) for details
+
+[Home]
+#DefaultStorage=
+#DefaultFileSystemType=btrfs
diff --git a/src/home/homework-cifs.c b/src/home/homework-cifs.c
new file mode 100644
index 0000000..2736095
--- /dev/null
+++ b/src/home/homework-cifs.c
@@ -0,0 +1,213 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#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) {
+
+ 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;
+ char **pw;
+ int r;
+
+ r = home_unshare_and_mount(NULL, NULL, false, user_record_mount_flags(h));
+ 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(errno, "Failed to open home directory: %m");
+
+ return 0;
+}
+
+int home_activate_cifs(
+ UserRecord *h,
+ PasswordCache *cache,
+ 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, cache, 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;
+ _cleanup_close_ int copy = -1;
+ 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.");
+
+ 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 = take_fdopendir(&copy);
+ if (!d)
+ 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
index 0000000..da2e50a
--- /dev/null
+++ b/src/home/homework-cifs.h
@@ -0,0 +1,11 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#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, PasswordCache *cache, 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
index 0000000..2d80003
--- /dev/null
+++ b/src/home/homework-directory.c
@@ -0,0 +1,242 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#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,
+ PasswordCache *cache,
+ 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, cache, &setup, &header_home);
+ if (r < 0)
+ return r;
+
+ r = home_refresh(h, &setup, header_home, cache, 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_nofollow_verbose(LOG_ERR, ip, hd, NULL, MS_BIND, NULL);
+ if (r < 0)
+ return r;
+
+ r = mount_nofollow_verbose(LOG_ERR, NULL, hd, NULL, MS_BIND|MS_REMOUNT|user_record_mount_flags(h), NULL);
+ if (r < 0) {
+ (void) umount_verbose(LOG_ERR, hd, UMOUNT_NOFOLLOW);
+ 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,
+ PasswordCache *cache,
+ 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, cache, setup, NULL);
+ if (r < 0)
+ return r;
+
+ r = home_load_embedded_identity(h, setup->root_fd, NULL, USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL, cache, &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
index 0000000..27d640f
--- /dev/null
+++ b/src/home/homework-directory.h
@@ -0,0 +1,10 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#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, PasswordCache *cache, UserRecord **ret_home);
+int home_create_directory_or_subvolume(UserRecord *h, UserRecord **ret_home);
+int home_resize_directory(UserRecord *h, bool already_activated, PasswordCache *cache, HomeSetup *setup, UserRecord **ret_home);
diff --git a/src/home/homework-fido2.c b/src/home/homework-fido2.c
new file mode 100644
index 0000000..2f717a5
--- /dev/null
+++ b/src/home/homework-fido2.c
@@ -0,0 +1,197 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <fido.h>
+
+#include "hexdecoct.h"
+#include "homework-fido2.h"
+#include "strv.h"
+
+static int fido2_use_specific_token(
+ const char *path,
+ UserRecord *h,
+ UserRecord *secret,
+ const Fido2HmacSalt *salt,
+ char **ret) {
+
+ _cleanup_(fido_cbor_info_free) fido_cbor_info_t *di = NULL;
+ _cleanup_(fido_assert_free) fido_assert_t *a = NULL;
+ _cleanup_(fido_dev_free) fido_dev_t *d = NULL;
+ bool found_extension = false;
+ size_t n, hmac_size;
+ const void *hmac;
+ char **e;
+ int r;
+
+ d = fido_dev_new();
+ if (!d)
+ return log_oom();
+
+ r = fido_dev_open(d, path);
+ if (r != FIDO_OK)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO),
+ "Failed to open FIDO2 device %s: %s", path, fido_strerr(r));
+
+ if (!fido_dev_is_fido2(d))
+ return log_error_errno(SYNTHETIC_ERRNO(ENODEV),
+ "Specified device %s is not a FIDO2 device.", path);
+
+ di = fido_cbor_info_new();
+ if (!di)
+ return log_oom();
+
+ r = fido_dev_get_cbor_info(d, di);
+ if (r != FIDO_OK)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO),
+ "Failed to get CBOR device info for %s: %s", path, fido_strerr(r));
+
+ e = fido_cbor_info_extensions_ptr(di);
+ n = fido_cbor_info_extensions_len(di);
+
+ for (size_t i = 0; i < n; i++)
+ if (streq(e[i], "hmac-secret")) {
+ found_extension = true;
+ break;
+ }
+
+ if (!found_extension)
+ return log_error_errno(SYNTHETIC_ERRNO(ENODEV),
+ "Specified device %s is a FIDO2 device, but does not support the required HMAC-SECRET extension.", path);
+
+ a = fido_assert_new();
+ if (!a)
+ return log_oom();
+
+ r = fido_assert_set_extensions(a, FIDO_EXT_HMAC_SECRET);
+ if (r != FIDO_OK)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO),
+ "Failed to enable HMAC-SECRET extension on FIDO2 assertion: %s", fido_strerr(r));
+
+ r = fido_assert_set_hmac_salt(a, salt->salt, salt->salt_size);
+ if (r != FIDO_OK)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO),
+ "Failed to set salt on FIDO2 assertion: %s", fido_strerr(r));
+
+ r = fido_assert_set_rp(a, "io.systemd.home");
+ if (r != FIDO_OK)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO),
+ "Failed to set FIDO2 assertion ID: %s", fido_strerr(r));
+
+ r = fido_assert_set_clientdata_hash(a, (const unsigned char[32]) {}, 32);
+ if (r != FIDO_OK)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO),
+ "Failed to set FIDO2 assertion client data hash: %s", fido_strerr(r));
+
+ r = fido_assert_allow_cred(a, salt->credential.id, salt->credential.size);
+ if (r != FIDO_OK)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO),
+ "Failed to add FIDO2 assertion credential ID: %s", fido_strerr(r));
+
+ r = fido_assert_set_up(a, h->fido2_user_presence_permitted <= 0 ? FIDO_OPT_FALSE : FIDO_OPT_TRUE);
+ if (r != FIDO_OK)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO),
+ "Failed to set FIDO2 assertion user presence: %s", fido_strerr(r));
+
+ log_info("Asking FIDO2 token for authentication.");
+
+ r = fido_dev_get_assert(d, a, NULL); /* try without pin first */
+ if (r == FIDO_ERR_PIN_REQUIRED) {
+ char **i;
+
+ /* OK, we needed a pin, try with all pins in turn */
+ STRV_FOREACH(i, secret->token_pin) {
+ r = fido_dev_get_assert(d, a, *i);
+ if (r != FIDO_ERR_PIN_INVALID)
+ break;
+ }
+ }
+
+ switch (r) {
+ case FIDO_OK:
+ break;
+ case FIDO_ERR_NO_CREDENTIALS:
+ return log_error_errno(SYNTHETIC_ERRNO(EBADSLT),
+ "Wrong security token; needed credentials not present on token.");
+ case FIDO_ERR_PIN_REQUIRED:
+ return log_error_errno(SYNTHETIC_ERRNO(ENOANO),
+ "Security token requires PIN.");
+ case FIDO_ERR_PIN_AUTH_BLOCKED:
+ return log_error_errno(SYNTHETIC_ERRNO(EOWNERDEAD),
+ "PIN of security token is blocked, please remove/reinsert token.");
+ case FIDO_ERR_PIN_INVALID:
+ return log_error_errno(SYNTHETIC_ERRNO(ENOLCK),
+ "PIN of security token incorrect.");
+ case FIDO_ERR_UP_REQUIRED:
+ return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE),
+ "User presence required.");
+ case FIDO_ERR_ACTION_TIMEOUT:
+ return log_error_errno(SYNTHETIC_ERRNO(ENOSTR),
+ "Token action timeout. (User didn't interact with token quickly enough.)");
+ default:
+ return log_error_errno(SYNTHETIC_ERRNO(EIO),
+ "Failed to ask token for assertion: %s", fido_strerr(r));
+ }
+
+ hmac = fido_assert_hmac_secret_ptr(a, 0);
+ if (!hmac)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to retrieve HMAC secret.");
+
+ hmac_size = fido_assert_hmac_secret_len(a, 0);
+
+ r = base64mem(hmac, hmac_size, ret);
+ if (r < 0)
+ return log_error_errno(r, "Failed to base64 encode HMAC secret: %m");
+
+ return 0;
+}
+
+int fido2_use_token(UserRecord *h, UserRecord *secret, const Fido2HmacSalt *salt, char **ret) {
+ size_t allocated = 64, found = 0;
+ fido_dev_info_t *di = NULL;
+ int r;
+
+ di = fido_dev_info_new(allocated);
+ if (!di)
+ return log_oom();
+
+ r = fido_dev_info_manifest(di, allocated, &found);
+ if (r == FIDO_ERR_INTERNAL) {
+ /* The library returns FIDO_ERR_INTERNAL when no devices are found. I wish it wouldn't. */
+ r = log_debug_errno(SYNTHETIC_ERRNO(EAGAIN), "Got FIDO_ERR_INTERNAL, assuming no devices.");
+ goto finish;
+ }
+ if (r != FIDO_OK) {
+ r = log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to enumerate FIDO2 devices: %s", fido_strerr(r));
+ goto finish;
+ }
+
+ for (size_t i = 0; i < found; i++) {
+ const fido_dev_info_t *entry;
+ const char *path;
+
+ entry = fido_dev_info_ptr(di, i);
+ if (!entry) {
+ r = log_error_errno(SYNTHETIC_ERRNO(EIO),
+ "Failed to get device information for FIDO device %zu.", i);
+ goto finish;
+ }
+
+ path = fido_dev_info_path(entry);
+ if (!path) {
+ r = log_error_errno(SYNTHETIC_ERRNO(EIO),
+ "Failed to query FIDO device path.");
+ goto finish;
+ }
+
+ r = fido2_use_specific_token(path, h, secret, salt, ret);
+ if (!IN_SET(r,
+ -EBADSLT, /* device doesn't understand our credential hash */
+ -ENODEV /* device is not a FIDO2 device with HMAC-SECRET */))
+ goto finish;
+ }
+
+ r = -EAGAIN;
+
+finish:
+ fido_dev_info_free(&di, allocated);
+ return r;
+}
diff --git a/src/home/homework-fido2.h b/src/home/homework-fido2.h
new file mode 100644
index 0000000..a1dcba2
--- /dev/null
+++ b/src/home/homework-fido2.h
@@ -0,0 +1,6 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "user-record.h"
+
+int fido2_use_token(UserRecord *h, UserRecord *secret, const Fido2HmacSalt *salt, char **ret);
diff --git a/src/home/homework-fscrypt.c b/src/home/homework-fscrypt.c
new file mode 100644
index 0000000..d0676f8
--- /dev/null
+++ b/src/home/homework-fscrypt.c
@@ -0,0 +1,643 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#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(
+ const PasswordCache *cache,
+ 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;
+ char **list;
+ 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 = -ENOANO;
+ FOREACH_POINTER(list, cache->pkcs11_passwords, cache->fido2_passwords, password) {
+ r = fscrypt_slot_try_many(
+ list,
+ salt, salt_size,
+ encrypted, encrypted_size,
+ setup->fscrypt_key_descriptor,
+ ret_volume_key, ret_volume_key_size);
+ if (r != -ENOANO)
+ break;
+ }
+ 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,
+ PasswordCache *cache,
+ 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(
+ cache,
+ 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,
+ PasswordCache *cache, /* the passwords acquired via PKCS#11/FIDO2 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(
+ cache,
+ 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
index 0000000..50b0399
--- /dev/null
+++ b/src/home/homework-fscrypt.h
@@ -0,0 +1,10 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "homework.h"
+#include "user-record.h"
+
+int home_prepare_fscrypt(UserRecord *h, bool already_activated, PasswordCache *cache, HomeSetup *setup);
+int home_create_fscrypt(UserRecord *h, char **effective_passwords, UserRecord **ret_home);
+
+int home_passwd_fscrypt(UserRecord *h, HomeSetup *setup, PasswordCache *cache, char **effective_passwords);
diff --git a/src/home/homework-luks.c b/src/home/homework-luks.c
new file mode 100644
index 0000000..b0b2d80
--- /dev/null
+++ b/src/home/homework-luks.c
@@ -0,0 +1,3087 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <libfdisk.h>
+#include <linux/loop.h>
+#include <poll.h>
+#include <sys/file.h>
+#include <sys/ioctl.h>
+#include <sys/mount.h>
+#include <sys/xattr.h>
+
+#include "blkid-util.h"
+#include "blockdev-util.h"
+#include "btrfs-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 "home-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 "mkfs-util.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))
+
+int run_mark_dirty(int fd, bool b) {
+ char x = '1';
+ int r, ret;
+
+ /* Sets or removes the 'user.home-dirty' xattr on the specified file. We use this to detect when a
+ * home directory was not properly unmounted. */
+
+ assert(fd >= 0);
+
+ r = fd_verify_regular(fd);
+ if (r < 0)
+ return r;
+
+ if (b) {
+ ret = fsetxattr(fd, "user.home-dirty", &x, 1, XATTR_CREATE);
+ if (ret < 0 && errno != EEXIST)
+ return log_debug_errno(errno, "Could not mark home directory as dirty: %m");
+
+ } else {
+ r = fsync_full(fd);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to synchronize image before marking it clean: %m");
+
+ ret = fremovexattr(fd, "user.home-dirty");
+ if (ret < 0 && errno != ENODATA)
+ return log_debug_errno(errno, "Could not mark home directory as clean: %m");
+ }
+
+ r = fsync_full(fd);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to synchronize dirty flag to disk: %m");
+
+ return ret >= 0;
+}
+
+int run_mark_dirty_by_path(const char *path, bool b) {
+ _cleanup_close_ int fd = -1;
+
+ assert(path);
+
+ fd = open(path, O_RDWR|O_CLOEXEC|O_NOCTTY);
+ if (fd < 0)
+ return log_debug_errno(errno, "Failed to open %s to mark dirty or clean: %m", path);
+
+ return run_mark_dirty(fd, b);
+}
+
+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,
+ const PasswordCache *cache,
+ 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;
+ char **list;
+ 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");
+
+ cryptsetup_enable_logging(cd);
+
+ 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 = -ENOKEY;
+ FOREACH_POINTER(list, cache->pkcs11_passwords, cache->fido2_passwords, passwords) {
+ r = luks_try_passwords(cd, list, vk, &vks);
+ if (r != -ENOKEY)
+ break;
+ }
+ 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,
+ PasswordCache *cache,
+ 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;
+ char **list;
+ 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);
+
+ cryptsetup_enable_logging(cd);
+
+ 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 = -ENOKEY;
+ FOREACH_POINTER(list, cache->pkcs11_passwords, cache->fido2_passwords, passwords) {
+ r = luks_try_passwords(cd, list, vk, &vks);
+ if (r != -ENOKEY)
+ break;
+ }
+ 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 our expectations.");
+
+ *ret = cc;
+ return 0;
+}
+
+static int luks_validate_home_record(
+ struct crypt_device *cd,
+ UserRecord *h,
+ const void *volume_key,
+ PasswordCache *cache,
+ 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, cache, /* strict_verify= */ true);
+ if (r < 0)
+ return r;
+ assert(r > 0); /* Insist that a password was verified */
+
+ *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;
+}
+
+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. (Also, optionally, trim on logout) */
+
+ assert(root_fd >= 0);
+
+ if (ioctl(root_fd, FITRIM, &range) < 0) {
+ if (ERRNO_IS_NOT_SUPPORTED(errno) || errno == 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;
+}
+
+int run_fitrim_by_path(const char *root_path) {
+ _cleanup_close_ int root_fd = -1;
+
+ root_fd = open(root_path, O_RDONLY|O_DIRECTORY|O_CLOEXEC);
+ if (root_fd < 0)
+ return log_error_errno(errno, "Failed to open file system '%s' for trimming: %m", root_path);
+
+ return run_fitrim(root_fd);
+}
+
+int run_fallocate(int backing_fd, const struct stat *st) {
+ char buf[FORMAT_BYTES_MAX];
+ struct stat stbuf;
+
+ assert(backing_fd >= 0);
+
+ /* If discarding is off, let's allocate the whole image before mounting, so that the setting takes
+ * effect on activation */
+
+ if (!st) {
+ if (fstat(backing_fd, &stbuf) < 0)
+ return log_error_errno(errno, "Failed to fstat(): %m");
+
+ st = &stbuf;
+ }
+
+ 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 run_fallocate_by_path(const char *backing_path) {
+ _cleanup_close_ int backing_fd = -1;
+
+ backing_fd = open(backing_path, O_RDWR|O_CLOEXEC|O_NOCTTY|O_NONBLOCK);
+ if (backing_fd < 0)
+ return log_error_errno(errno, "Failed to open '%s' for fallocate(): %m", backing_path);
+
+ return run_fallocate(backing_fd, NULL);
+}
+
+int home_prepare_luks(
+ UserRecord *h,
+ bool already_activated,
+ const char *force_image_path,
+ PasswordCache *cache,
+ 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;
+ _cleanup_close_ int root_fd = -1, image_fd = -1;
+ bool dm_activated = false, mounted = false;
+ size_t volume_key_size = 0;
+ bool marked_dirty = false;
+ 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,
+ cache,
+ &cd,
+ &found_luks_uuid,
+ &volume_key,
+ &volume_key_size);
+ if (r < 0)
+ return r;
+
+ r = luks_validate_home_record(cd, h, volume_key, cache, &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;
+ 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();
+
+ 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 fstat() image file: %m");
+ if (!S_ISREG(st.st_mode) && !S_ISBLK(st.st_mode))
+ return log_error_errno(
+ S_ISDIR(st.st_mode) ? SYNTHETIC_ERRNO(EISDIR) : SYNTHETIC_ERRNO(EBADFD),
+ "Image file %s is not a regular file or block device: %m", ip);
+
+ r = luks_validate(image_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");
+
+ /* Everything before this point left the image untouched. We are now starting to make
+ * changes, hence mark the image dirty */
+ marked_dirty = run_mark_dirty(image_fd, true) > 0;
+
+ if (!user_record_luks_discard(h)) {
+ r = run_fallocate(image_fd, &st);
+ if (r < 0)
+ return r;
+ }
+
+ r = loop_device_make(image_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,
+ cache,
+ user_record_luks_discard(h) || user_record_luks_offline_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, cache, &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), user_record_mount_flags(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->image_fd = TAKE_FD(image_fd);
+ setup->do_offline_fallocate = !(setup->do_offline_fitrim = user_record_luks_offline_discard(h));
+ setup->do_mark_clean = marked_dirty;
+ }
+
+ 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(LOG_ERR, "/run/systemd/user-home-mount", UMOUNT_NOFOLLOW);
+
+ if (dm_activated)
+ (void) crypt_deactivate(cd, setup->dm_name);
+
+ if (image_fd >= 0 && marked_dirty)
+ (void) run_mark_dirty(image_fd, false);
+
+ 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,
+ PasswordCache *cache,
+ 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,
+ cache,
+ &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,
+ cache,
+ &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;
+ setup.do_offline_fitrim = false;
+
+ loop_device_relinquish(setup.loop);
+
+ r = crypt_deactivate_by_name(NULL, setup.dm_name, CRYPT_DEACTIVATE_DEFERRED);
+ if (r < 0)
+ log_warning_errno(r, "Failed to relinquish DM device, ignoring: %m");
+
+ setup.undo_dm = false;
+ setup.do_offline_fallocate = false;
+ setup.do_mark_clean = 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;
+ bool we_detached;
+ 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 has already been detached.", dm_name);
+ we_detached = false;
+ } else if (r < 0)
+ return log_error_errno(r, "Failed to initialize cryptsetup context for %s: %m", dm_name);
+ else {
+ log_info("Discovered used LUKS device %s.", dm_node);
+
+ cryptsetup_enable_logging(cd);
+
+ 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);
+ we_detached = false;
+ } else if (r < 0)
+ return log_info_errno(r, "LUKS device %s couldn't be deactivated: %m", dm_node);
+ else {
+ log_info("LUKS device detaching completed.");
+ we_detached = true;
+ }
+ }
+
+ if (user_record_luks_offline_discard(h))
+ log_debug("Not allocating on logout.");
+ else
+ (void) run_fallocate_by_path(user_record_image_path(h));
+
+ run_mark_dirty_by_path(user_record_image_path(h), false);
+ return we_detached;
+}
+
+int home_trim_luks(UserRecord *h) {
+ assert(h);
+
+ if (!user_record_luks_offline_discard(h)) {
+ log_debug("Not trimming on logout.");
+ return 0;
+ }
+
+ (void) run_fitrim_by_path(user_record_home_directory(h));
+ 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,
+ const PasswordCache *cache,
+ 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;
+ char suuid[ID128_UUID_STRING_MAX], **pp;
+ _cleanup_free_ char *text = NULL;
+ size_t volume_key_size;
+ 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");
+
+ cryptsetup_enable_logging(cd);
+
+ /* 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(cache->pkcs11_passwords, *pp) ||
+ strv_contains(cache->fido2_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[ID128_UUID_STRING_MAX];
+ 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 timeout. 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;
+}
+
+static int home_truncate(
+ UserRecord *h,
+ int fd,
+ const char *path,
+ uint64_t size) {
+
+ bool trunc;
+ int r;
+
+ assert(h);
+ assert(fd >= 0);
+ assert(path);
+
+ trunc = user_record_luks_discard(h);
+ if (!trunc) {
+ r = fallocate(fd, 0, 0, size);
+ if (r < 0 && ERRNO_IS_NOT_SUPPORTED(errno)) {
+ /* Some file systems do not support fallocate(), let's gracefully degrade
+ * (ZFS, reiserfs, …) and fall back to truncation */
+ log_notice_errno(errno, "Backing file system does not support fallocate(), falling back to ftruncate(), i.e. implicitly using non-discard mode.");
+ trunc = true;
+ }
+ }
+
+ if (trunc)
+ r = ftruncate(fd, size);
+
+ if (r < 0) {
+ if (ERRNO_IS_DISK_SPACE(errno)) {
+ log_error_errno(errno, "Not enough disk space to allocate home.");
+ return -ENOSPC; /* make recognizable */
+ }
+
+ return log_error_errno(errno, "Failed to truncate home image %s: %m", path);
+ }
+
+ return 0;
+}
+
+int home_create_luks(
+ UserRecord *h,
+ PasswordCache *cache,
+ 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", fstype);
+
+ r = mkfs_exists(fstype);
+ if (r < 0)
+ return log_error_errno(r, "Failed to check if mkfs binary for %s exists: %m", fstype);
+ if (r == 0) {
+ if (h->file_system_type || streq(fstype, "ext4") || !supported_fstype("ext4"))
+ return log_error_errno(SYNTHETIC_ERRNO(EPROTONOSUPPORT), "mkfs binary for file system type %s does not exist.", fstype);
+
+ /* If the record does not explicitly declare a file system to use, and the compiled-in
+ * default does not actually exist, than do an automatic fallback onto ext4, as the baseline
+ * fs of Linux. We won't search for a working fs type here beyond ext4, i.e. nothing fancier
+ * than a single, conservative fallback to baseline. This should be useful in minimal
+ * environments where mkfs.btrfs or so are not made available, but mkfs.ext4 as Linux' most
+ * boring, most basic fs is. */
+ log_info("Formatting tool for compiled-in default file system %s not available, falling back to ext4 instead.", fstype);
+ fstype = "ext4";
+ }
+
+ 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.", fstype);
+
+ /* 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) || user_record_luks_offline_discard(h)) {
+ /* If we want online or offline discard, discard once before we start using things. */
+
+ 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.", fstype);
+
+ 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_full_errno(ERRNO_IS_NOT_SUPPORTED(r) ? LOG_DEBUG : LOG_WARNING, r,
+ "Failed to set file attributes on %s, ignoring: %m", temporary_image_path);
+
+ r = home_truncate(h, image_fd, temporary_image_path, host_size);
+ if (r < 0)
+ 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),
+ cache,
+ effective_passwords,
+ user_record_luks_discard(h) || user_record_luks_offline_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 = make_filesystem(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), user_record_mount_flags(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;
+ }
+
+ /* Prefer using a btrfs subvolume if we can, fall back to directory otherwise */
+ r = btrfs_subvol_make_fallback(subdir, 0700);
+ if (r < 0) {
+ log_error_errno(r, "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;
+ }
+
+ if (user_record_luks_offline_discard(h)) {
+ r = run_fitrim(root_fd);
+ if (r < 0)
+ goto fail;
+ }
+
+ root_fd = safe_close(root_fd);
+
+ r = umount_verbose(LOG_ERR, "/run/systemd/user-home-mount", UMOUNT_NOFOLLOW);
+ 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;
+ }
+
+ crypt_free(cd);
+ cd = NULL;
+
+ dm_activated = false;
+
+ loop = loop_device_unref(loop);
+
+ if (!user_record_luks_offline_discard(h)) {
+ r = run_fallocate(image_fd, NULL /* refresh stat() data */);
+ if (r < 0)
+ goto fail;
+ }
+
+ /* Sync everything to disk before we move things into place under the final name. */
+ if (fsync(image_fd) < 0) {
+ r = log_error_errno(r, "Failed to synchronize image to disk: %m");
+ goto fail;
+ }
+
+ if (disk_uuid_path)
+ (void) ioctl(image_fd, BLKRRPART, 0);
+ else {
+ /* If we operate on a file, sync the containing directory too. */
+ r = fsync_directory_of_file(image_fd);
+ if (r < 0) {
+ log_error_errno(r, "Failed to synchronize directory of image file to disk: %m");
+ goto fail;
+ }
+ }
+
+ /* 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(LOG_WARNING, "/run/systemd/user-home-mount", UMOUNT_NOFOLLOW);
+
+ 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, unsigned long flags) {
+ _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(LOG_ERR, "/run/systemd/user-home-mount", UMOUNT_NOFOLLOW);
+ if (r < 0)
+ return r;
+
+ setup->undo_mount = false;
+ re_mount = true;
+ }
+
+ log_info("Temporary 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, flags);
+ 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 wiping 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,
+ PasswordCache *cache,
+ 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 %s is not a block device nor regular file: %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, cache, 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, cache, &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 */
+ r = home_truncate(h, image_fd, ip, new_image_size);
+ if (r < 0)
+ return r;
+
+ 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), user_record_mount_flags(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,
+ PasswordCache *cache, /* the passwords acquired via PKCS#11/FIDO2 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;
+ char **list;
+ 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 = -ENOKEY;
+ FOREACH_POINTER(list, cache->pkcs11_passwords, cache->fido2_passwords, h->password) {
+ r = luks_try_passwords(setup->crypt_device, list, volume_key, &volume_key_size);
+ if (r != -ENOKEY)
+ break;
+ }
+ 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_contains(cache->pkcs11_passwords, effective_passwords[i]) ||
+ strv_contains(cache->fido2_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);
+ cryptsetup_enable_logging(cd);
+
+ 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, PasswordCache *cache) {
+ _cleanup_free_ char *dm_name = NULL, *dm_node = NULL;
+ _cleanup_(crypt_freep) struct crypt_device *cd = NULL;
+ char **list;
+ 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);
+ cryptsetup_enable_logging(cd);
+
+ r = -ENOKEY;
+ FOREACH_POINTER(list, cache->pkcs11_passwords, cache->fido2_passwords, h->password) {
+ r = luks_try_resume(cd, dm_name, list);
+ if (r != -ENOKEY)
+ break;
+ }
+ 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
index 0000000..c43bdfc
--- /dev/null
+++ b/src/home/homework-luks.h
@@ -0,0 +1,46 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "cryptsetup-util.h"
+#include "homework.h"
+#include "user-record.h"
+
+int home_prepare_luks(UserRecord *h, bool already_activated, const char *force_image_path, PasswordCache *cache, HomeSetup *setup, UserRecord **ret_luks_home);
+
+int home_activate_luks(UserRecord *h, PasswordCache *cache, UserRecord **ret_home);
+int home_deactivate_luks(UserRecord *h);
+int home_trim_luks(UserRecord *h);
+
+int home_store_header_identity_luks(UserRecord *h, HomeSetup *setup, UserRecord *old_home);
+
+int home_create_luks(UserRecord *h, PasswordCache *cache, char **effective_passwords, UserRecord **ret_home);
+
+int home_validate_update_luks(UserRecord *h, HomeSetup *setup);
+
+int home_resize_luks(UserRecord *h, bool already_activated, PasswordCache *cache, HomeSetup *setup, UserRecord **ret_home);
+
+int home_passwd_luks(UserRecord *h, HomeSetup *setup, PasswordCache *cache, char **effective_passwords);
+
+int home_lock_luks(UserRecord *h);
+int home_unlock_luks(UserRecord *h, PasswordCache *cache);
+
+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;
+}
+
+int run_fitrim(int root_fd);
+int run_fitrim_by_path(const char *root_path);
+int run_fallocate(int backing_fd, const struct stat *st);
+int run_fallocate_by_path(const char *backing_path);
+int run_mark_dirty(int fd, bool b);
+int run_mark_dirty_by_path(const char *path, bool b);
diff --git a/src/home/homework-mount.c b/src/home/homework-mount.c
new file mode 100644
index 0000000..5e73768
--- /dev/null
+++ b/src/home/homework-mount.c
@@ -0,0 +1,96 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#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, unsigned long flags) {
+ _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_nofollow_verbose(LOG_ERR, node, "/run/systemd/user-home-mount", fstype, flags|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, unsigned long flags) {
+ int r;
+
+ if (unshare(CLONE_NEWNS) < 0)
+ return log_error_errno(errno, "Couldn't unshare file system namespace: %m");
+
+ r = mount_nofollow_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, flags);
+
+ 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_nofollow_verbose(LOG_ERR, d, target, NULL, MS_BIND, NULL);
+ if (r < 0)
+ return r;
+
+ r = umount_verbose(LOG_ERR, "/run/systemd/user-home-mount", UMOUNT_NOFOLLOW);
+ 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
index 0000000..2a4591c
--- /dev/null
+++ b/src/home/homework-mount.h
@@ -0,0 +1,8 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <stdbool.h>
+
+int home_mount_node(const char *node, const char *fstype, bool discard, unsigned long flags);
+int home_unshare_and_mount(const char *node, const char *fstype, bool discard, unsigned long flags);
+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
index 0000000..15402b1
--- /dev/null
+++ b/src/home/homework-pkcs11.c
@@ -0,0 +1,104 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#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("Successfully 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->token_pin))
+ return log_error_errno(SYNTHETIC_ERRNO(ENOANO), "Security token requires PIN.");
+
+ STRV_FOREACH(i, data->secret->token_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
index 0000000..c8674e0
--- /dev/null
+++ b/src/home/homework-pkcs11.h
@@ -0,0 +1,21 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#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
index 0000000..7001870
--- /dev/null
+++ b/src/home/homework-quota.c
@@ -0,0 +1,124 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#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
index 0000000..a21c9ba
--- /dev/null
+++ b/src/home/homework-quota.h
@@ -0,0 +1,8 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#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
index 0000000..b61f650
--- /dev/null
+++ b/src/home/homework.c
@@ -0,0 +1,1747 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <stddef.h>
+#include <sys/mount.h>
+
+#include "chown-recursive.h"
+#include "copy.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "fs-util.h"
+#include "home-util.h"
+#include "homework-cifs.h"
+#include "homework-directory.h"
+#include "homework-fido2.h"
+#include "homework-fscrypt.h"
+#include "homework-luks.h"
+#include "homework-mount.h"
+#include "homework-pkcs11.h"
+#include "homework.h"
+#include "libcrypt-util.h"
+#include "main-func.h"
+#include "memory-util.h"
+#include "missing_magic.h"
+#include "modhex.h"
+#include "mount-util.h"
+#include "path-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)
+
+void password_cache_free(PasswordCache *cache) {
+ if (!cache)
+ return;
+
+ cache->pkcs11_passwords = strv_free_erase(cache->pkcs11_passwords);
+ cache->fido2_passwords = strv_free_erase(cache->fido2_passwords);
+}
+
+int user_record_authenticate(
+ UserRecord *h,
+ UserRecord *secret,
+ PasswordCache *cache,
+ bool strict_verify) {
+
+ bool need_password = false, need_recovery_key = false, need_token = false, need_pin = false, need_protected_authentication_path_permitted = false, need_user_presence_permitted = false,
+ pin_locked = false, pin_incorrect = false, pin_incorrect_few_tries_left = false, pin_incorrect_one_try_left = false, token_action_timeout = false;
+ 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 or FIDO2 token is around and can unlock the record.
+ *
+ * Note that the 'cache' parameter is both an input and output parameter: it contains lists of
+ * configured, decrypted PKCS#11/FIDO2 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 and potentially interactive)
+ * PKCS#11/FIDO2 dance for the relevant token again and again. */
+
+ /* First, let's see if the supplied plain-text passwords work? */
+ r = user_record_test_password(h, secret);
+ if (r == -ENOKEY)
+ 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 1;
+ }
+
+ /* Similar, but test against the recovery keys */
+ r = user_record_test_recovery_key(h, secret);
+ if (r == -ENOKEY)
+ need_recovery_key = true;
+ else if (r == -ENXIO)
+ log_debug_errno(r, "User record has no recovery keys, plaintext passwords not tested against it.");
+ else if (r < 0)
+ return log_error_errno(r, "Failed to validate the recovery key of the record: %m");
+ else {
+ log_info("Provided password is a recovery key that unlocks the user record.");
+ return 1;
+ }
+
+ if (need_password && need_recovery_key)
+ log_info("None of the supplied plaintext passwords unlock the user record's hashed passwords or recovery keys.");
+ else if (need_password)
+ log_info("None of the supplied plaintext passwords unlock the user record's hashed passwords.");
+ else
+ log_info("None of the supplied plaintext passwords unlock the user record's hashed recovery keys.");
+
+ /* Second, test cached PKCS#11 passwords */
+ for (size_t n = 0; n < h->n_pkcs11_encrypted_key; n++) {
+ char **pp;
+
+ STRV_FOREACH(pp, cache->pkcs11_passwords) {
+ r = test_password_one(h->pkcs11_encrypted_key[n].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 1;
+ }
+ }
+ }
+
+ /* Third, test cached FIDO2 passwords */
+ for (size_t n = 0; n < h->n_fido2_hmac_salt; n++) {
+ char **pp;
+
+ /* See if any of the previously calculated passwords work */
+ STRV_FOREACH(pp, cache->fido2_passwords) {
+ r = test_password_one(h->fido2_hmac_salt[n].hashed_password, *pp);
+ if (r < 0)
+ return log_error_errno(r, "Failed to check supplied FIDO2 password: %m");
+ if (r > 0) {
+ log_info("Previously acquired FIDO2 password unlocks user record.");
+ return 0;
+ }
+ }
+ }
+
+ /* Fourth, let's see if any of the PKCS#11 security tokens are plugged in and help us */
+ for (size_t 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,
+ };
+
+ 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(&cache->pkcs11_passwords, data.decrypted_password);
+ if (r < 0)
+ return log_oom();
+
+ return 0;
+ }
+#else
+ need_token = true;
+ break;
+#endif
+ }
+
+ /* Fifth, let's see if any of the FIDO2 security tokens are plugged in and help us */
+ for (size_t n = 0; n < h->n_fido2_hmac_salt; n++) {
+#if HAVE_LIBFIDO2
+ _cleanup_(erase_and_freep) char *decrypted_password = NULL;
+
+ r = fido2_use_token(h, secret, h->fido2_hmac_salt + n, &decrypted_password);
+ switch (r) {
+ case -EAGAIN:
+ need_token = true;
+ break;
+ case -ENOANO:
+ need_pin = true;
+ break;
+ case -EOWNERDEAD:
+ pin_locked = true;
+ break;
+ case -ENOLCK:
+ pin_incorrect = true;
+ break;
+ case -EMEDIUMTYPE:
+ need_user_presence_permitted = true;
+ break;
+ case -ENOSTR:
+ token_action_timeout = true;
+ break;
+ default:
+ if (r < 0)
+ return r;
+
+ r = test_password_one(h->fido2_hmac_salt[n].hashed_password, decrypted_password);
+ if (r < 0)
+ return log_error_errno(r, "Failed to test FIDO2 password: %m");
+ if (r == 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EPERM), "Configured FIDO2 security token does not decrypt encrypted key correctly.");
+
+ log_info("Decrypted password from FIDO2 security token unlocks user record.");
+
+ r = strv_extend(&cache->fido2_passwords, decrypted_password);
+ if (r < 0)
+ return log_oom();
+
+ return 1;
+ }
+#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 (token_action_timeout)
+ return -ENOSTR;
+ if (need_protected_authentication_path_permitted)
+ return -ERFKILL;
+ if (need_user_presence_permitted)
+ return -EMEDIUMTYPE;
+ if (need_pin)
+ return -ENOANO;
+ if (need_token)
+ return -EBADSLT;
+ if (need_password)
+ return -ENOKEY;
+ if (need_recovery_key)
+ return -EREMOTEIO;
+
+ /* Hmm, this means neither PCKS#11/FIDO2 nor classic hashed passwords or recovery keys were supplied,
+ * we cannot authenticate this reasonably */
+ if (strict_verify)
+ return log_debug_errno(SYNTHETIC_ERRNO(EKEYREVOKED),
+ "No hashed passwords, no recovery keys and no PKCS#11/FIDO2 tokens defined, cannot authenticate user record, refusing.");
+
+ /* If strict verification is off this means we are possibly in the case where we encountered an
+ * unfixated record, i.e. a synthetic one that accordingly lacks any authentication data. In this
+ * case, allow the authentication to pass for now, so that the second (or third) authentication level
+ * (the ones of the user record in the LUKS header or inside the home directory) will then catch
+ * invalid passwords. The second/third authentication always runs in strict verification mode. */
+ log_debug("No hashed passwords, not recovery keys and no PKCS#11 tokens defined in record, cannot authenticate user record. "
+ "Deferring to embedded user record.");
+ return 0;
+}
+
+int home_setup_undo(HomeSetup *setup) {
+ int r = 0, q;
+
+ assert(setup);
+
+ if (setup->root_fd >= 0) {
+ if (setup->do_offline_fitrim) {
+ q = run_fitrim(setup->root_fd);
+ if (q < 0)
+ r = q;
+ }
+
+ setup->root_fd = safe_close(setup->root_fd);
+ }
+
+ if (setup->undo_mount) {
+ q = umount_verbose(LOG_DEBUG, "/run/systemd/user-home-mount", UMOUNT_NOFOLLOW);
+ 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;
+ }
+
+ if (setup->image_fd >= 0) {
+ if (setup->do_offline_fallocate) {
+ q = run_fallocate(setup->image_fd, NULL);
+ if (q < 0)
+ r = q;
+ }
+
+ if (setup->do_mark_clean) {
+ q = run_mark_dirty(setup->image_fd, false);
+ if (q < 0)
+ r = q;
+ }
+
+ setup->image_fd = safe_close(setup->image_fd);
+ }
+
+ setup->undo_mount = false;
+ setup->undo_dm = false;
+ setup->do_offline_fitrim = false;
+ setup->do_offline_fallocate = false;
+ setup->do_mark_clean = 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,
+ PasswordCache *cache,
+ 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, cache, 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, cache, 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 = take_fdopen(&identity_fd, "r");
+ if (!identity_file)
+ return log_oom();
+
+ 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 = take_fdopen(&identity_fd, "w");
+ if (!identity_file) {
+ r = log_oom();
+ goto fail;
+ }
+
+ 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,
+ PasswordCache *cache,
+ 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), "Embedded 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, cache, /* strict_verify= */ true);
+ if (r < 0)
+ return r;
+ assert(r > 0); /* Insist that a password was verified */
+
+ /* 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,
+ PasswordCache *cache,
+ 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, cache, &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_(password_cache_free) PasswordCache cache = {};
+ _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, &cache, /* strict_verify= */ false);
+ 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, &cache, &new_home);
+ if (r < 0)
+ return r;
+
+ break;
+
+ case USER_SUBVOLUME:
+ case USER_DIRECTORY:
+ case USER_FSCRYPT:
+ r = home_activate_directory(h, &cache, &new_home);
+ if (r < 0)
+ return r;
+
+ break;
+
+ case USER_CIFS:
+ r = home_activate_cifs(h, &cache, &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 (user_record_storage(h) == USER_LUKS) {
+ r = home_trim_luks(h);
+ if (r < 0)
+ return r;
+ }
+
+ 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,
+ PasswordCache *cache,
+ char ***ret_effective_passwords) {
+
+ _cleanup_(strv_free_erasep) char **effective = NULL;
+ size_t n;
+ char **i;
+ int r;
+
+ assert(h);
+ assert(cache);
+
+ /* 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(SYNTHETIC_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 plaintext 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_recovery_key; n++) {
+ bool found = false;
+ char **j;
+
+ log_debug("Looking for plaintext recovery key for: %s", h->recovery_key[n].hashed_password);
+
+ STRV_FOREACH(j, h->password) {
+ _cleanup_(erase_and_freep) char *mangled = NULL;
+ const char *p;
+
+ if (streq(h->recovery_key[n].type, "modhex64")) {
+
+ r = normalize_recovery_key(*j, &mangled);
+ if (r == -EINVAL) /* Not properly formatted, probably a regular password. */
+ continue;
+ if (r < 0)
+ return log_error_errno(r, "Failed to normalize recovery key: %m");
+
+ p = mangled;
+ } else
+ p = *j;
+
+ r = test_password_one(h->recovery_key[n].hashed_password, p);
+ if (r < 0)
+ return log_error_errno(r, "Failed to test plaintext recovery key: %m");
+ if (r > 0) {
+ if (ret_effective_passwords) {
+ r = strv_extend(&effective, p);
+ if (r < 0)
+ return log_oom();
+ }
+
+ log_debug("Found plaintext recovery key.");
+ found = true;
+ break;
+ }
+ }
+
+ if (!found)
+ return log_error_errno(SYNTHETIC_ERRNO(EREMOTEIO), "Missing plaintext recovery key for defined recovery key");
+ }
+
+ 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();
+ }
+
+ r = strv_extend(&cache->pkcs11_passwords, data.decrypted_password);
+ if (r < 0)
+ return log_oom();
+#else
+ return -EBADSLT;
+#endif
+ }
+
+ for (n = 0; n < h->n_fido2_hmac_salt; n++) {
+#if HAVE_LIBFIDO2
+ _cleanup_(erase_and_freep) char *decrypted_password = NULL;
+
+ r = fido2_use_token(h, h, h->fido2_hmac_salt + n, &decrypted_password);
+ if (r < 0)
+ return r;
+
+ r = test_password_one(h->fido2_hmac_salt[n].hashed_password, decrypted_password);
+ if (r < 0)
+ return log_error_errno(r, "Failed to test FIDO2 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, decrypted_password);
+ if (r < 0)
+ return log_oom();
+ }
+
+ r = strv_extend(&cache->fido2_passwords, decrypted_password);
+ if (r < 0)
+ return log_oom();
+#else
+ return -EBADSLT;
+#endif
+ }
+
+ if (ret_effective_passwords)
+ *ret_effective_passwords = TAKE_PTR(effective);
+
+ return 0;
+}
+
+static int determine_default_storage(UserStorage *ret) {
+ UserStorage storage = _USER_STORAGE_INVALID;
+ const char *e;
+ int r;
+
+ assert(ret);
+
+ /* homed tells us via an environment variable which default storage to use */
+ e = getenv("SYSTEMD_HOME_DEFAULT_STORAGE");
+ if (e) {
+ storage = user_storage_from_string(e);
+ if (storage < 0)
+ log_warning("$SYSTEMD_HOME_DEFAULT_STORAGE set to invalid storage type, ignoring: %s", e);
+ else {
+ log_info("Using configured default storage '%s'.", user_storage_to_string(storage));
+ *ret = storage;
+ return 0;
+ }
+ }
+
+ /* When neither user nor admin specified 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. Also, if /home is encrypted
+ * anyway, let's avoid duplicate encryption. 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. */
+
+ r = detect_container();
+ if (r < 0)
+ return log_error_errno(r, "Failed to determine whether we are in a container: %m");
+ if (r == 0) {
+ r = path_is_encrypted("/home");
+ if (r < 0)
+ log_warning_errno(r, "Failed to determine if /home is encrypted, ignoring: %m");
+ if (r <= 0) {
+ log_info("Using automatic default storage of '%s'.", user_storage_to_string(USER_LUKS));
+ *ret = USER_LUKS;
+ return 0;
+ }
+
+ log_info("/home is encrypted, not using '%s' storage, in order to avoid double encryption.", user_storage_to_string(USER_LUKS));
+ } else
+ log_info("Running in container, not using '%s' storage.", user_storage_to_string(USER_LUKS));
+
+ r = path_is_fs_type("/home", BTRFS_SUPER_MAGIC);
+ if (r < 0)
+ log_warning_errno(r, "Failed to determine file system of /home, ignoring: %m");
+ if (r > 0) {
+ log_info("/home is on btrfs, using '%s' as storage.", user_storage_to_string(USER_SUBVOLUME));
+ *ret = USER_SUBVOLUME;
+ } else {
+ log_info("/home is on simple file system, using '%s' as storage.", user_storage_to_string(USER_DIRECTORY));
+ *ret = USER_DIRECTORY;
+ }
+
+ return 0;
+}
+
+static int home_create(UserRecord *h, UserRecord **ret_home) {
+ _cleanup_(strv_free_erasep) char **effective_passwords = NULL;
+ _cleanup_(user_record_unrefp) UserRecord *new_home = NULL;
+ _cleanup_(password_cache_free) PasswordCache cache = {};
+ UserStorage new_storage = _USER_STORAGE_INVALID;
+ const char *new_fs = 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, &cache, &effective_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));
+
+ if (h->storage < 0) {
+ r = determine_default_storage(&new_storage);
+ if (r < 0)
+ return r;
+ }
+
+ if ((h->storage == USER_LUKS ||
+ (h->storage < 0 && new_storage == USER_LUKS)) &&
+ !h->file_system_type)
+ new_fs = getenv("SYSTEMD_HOME_DEFAULT_FILE_SYSTEM_TYPE");
+
+ if (new_storage >= 0 || new_fs) {
+ r = user_record_add_binding(
+ h,
+ new_storage,
+ NULL,
+ SD_ID128_NULL,
+ SD_ID128_NULL,
+ SD_ID128_NULL,
+ NULL,
+ NULL,
+ UINT64_MAX,
+ new_fs,
+ NULL,
+ UID_INVALID,
+ GID_INVALID);
+ if (r < 0)
+ return log_error_errno(r, "Failed to change storage type to LUKS: %m");
+ }
+
+ 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, &cache, 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
+ return log_notice_errno(SYNTHETIC_ERRNO(EALREADY),
+ "Nothing to remove.");
+
+ 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_(home_setup_undo) HomeSetup setup = HOME_SETUP_INIT;
+ _cleanup_(password_cache_free) PasswordCache cache = {};
+ bool already_activated = false;
+ int r;
+
+ assert(h);
+ assert(ret);
+
+ r = user_record_authenticate(h, h, &cache, /* strict_verify= */ true);
+ if (r < 0)
+ return r;
+ assert(r > 0); /* Insist that a password was verified */
+
+ r = home_validate_update(h, &setup);
+ if (r < 0)
+ return r;
+
+ already_activated = r > 0;
+
+ r = home_prepare(h, already_activated, &cache, &setup, &header_home);
+ if (r < 0)
+ return r;
+
+ r = home_load_embedded_identity(h, setup.root_fd, header_home, USER_RECONCILE_REQUIRE_NEWER, &cache, &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_(password_cache_free) PasswordCache cache = {};
+ 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, &cache, /* strict_verify= */ true);
+ if (r < 0)
+ return r;
+ assert(r > 0); /* Insist that a password was verified */
+
+ 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, &cache, &setup, ret);
+
+ case USER_DIRECTORY:
+ case USER_SUBVOLUME:
+ case USER_FSCRYPT:
+ return home_resize_directory(h, already_activated, &cache, &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;
+ _cleanup_(home_setup_undo) HomeSetup setup = HOME_SETUP_INIT;
+ _cleanup_(password_cache_free) PasswordCache cache = {};
+ 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, &cache, &effective_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, &cache, &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, &cache, &embedded_home, &new_home);
+ if (r < 0)
+ return r;
+
+ switch (user_record_storage(h)) {
+
+ case USER_LUKS:
+ r = home_passwd_luks(h, &setup, &cache, effective_passwords);
+ if (r < 0)
+ return r;
+ break;
+
+ case USER_FSCRYPT:
+ r = home_passwd_fscrypt(h, &setup, &cache, 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_(password_cache_free) PasswordCache cache = {};
+ bool already_activated = false;
+ int r;
+
+ assert(h);
+ assert(ret_home);
+
+ r = user_record_authenticate(h, h, &cache, /* strict_verify= */ false);
+ 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, &cache, &setup, &header_home);
+ if (r < 0)
+ return r;
+
+ r = home_load_embedded_identity(h, setup.root_fd, header_home, USER_RECONCILE_ANY, &cache, 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_(password_cache_free) PasswordCache cache = {};
+ 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, &cache, /* strict_verify= */ false);
+ if (r < 0)
+ return r;
+
+ r = home_unlock_luks(h, &cache);
+ 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 cannot be shrunk
+ * ETXTBSY → file systems of this type can only be shrunk 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)
+ * EREMOTEIO → recovery key incorrect (or not sufficeint, or not supplied — only if no passwords defined)
+ * 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/FIDO2 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
+ * EMEDIUMTYPE → suitable FIDO2 device found, but OK to ask for user presence not given
+ * ENOSTR → suitable FIDO2 device found, but user didn't react to action request on token quickly enough
+ * EOWNERDEAD → suitable PKCS#11/FIDO2 device found, but its PIN is locked
+ * ENOLCK → suitable PKCS#11/FIDO2 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
+ * EKEYREVOKED → user record has not suitable hashed password or pkcs#11 entry, we cannot authenticate
+ */
+
+ 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 (IN_SET(r, -ENOKEY, -EREMOTEIO) && !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
index 0000000..fb53fd4
--- /dev/null
+++ b/src/home/homework.h
@@ -0,0 +1,70 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#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;
+ int image_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;
+ bool do_offline_fitrim;
+ bool do_offline_fallocate;
+ bool do_mark_clean;
+
+ uint64_t partition_offset;
+ uint64_t partition_size;
+} HomeSetup;
+
+typedef struct PasswordCache {
+ /* Decoding passwords from security tokens is expensive and typically requires user interaction, hence cache any we already figured out. */
+ char **pkcs11_passwords;
+ char **fido2_passwords;
+} PasswordCache;
+
+void password_cache_free(PasswordCache *cache);
+
+#define HOME_SETUP_INIT \
+ { \
+ .root_fd = -1, \
+ .image_fd = -1, \
+ .partition_offset = UINT64_MAX, \
+ .partition_size = UINT64_MAX, \
+ }
+
+int home_setup_undo(HomeSetup *setup);
+
+int home_prepare(UserRecord *h, bool already_activated, PasswordCache *cache, HomeSetup *setup, UserRecord **ret_header_home);
+
+int home_refresh(UserRecord *h, HomeSetup *setup, UserRecord *header_home, PasswordCache *cache, 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, PasswordCache *cache, 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, PasswordCache *cache, bool strict_verify);
+
+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
index 0000000..444dc47
--- /dev/null
+++ b/src/home/meson.build
@@ -0,0 +1,122 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+systemd_homework_sources = files('''
+ home-util.c
+ home-util.h
+ homework-cifs.c
+ homework-cifs.h
+ homework-directory.c
+ homework-directory.h
+ homework-fido2.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
+ modhex.c
+ modhex.h
+ user-record-util.c
+ user-record-util.h
+'''.split())
+
+if conf.get('HAVE_P11KIT') == 1
+ systemd_homework_sources += files('homework-pkcs11.c')
+endif
+if conf.get('HAVE_LIBFIDO2') == 1
+ systemd_homework_sources += files('homework-fido2.c')
+endif
+
+systemd_homed_sources = files('''
+ home-util.c
+ home-util.h
+ homed-bus.c
+ homed-bus.h
+ homed-conf.c
+ homed-conf.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
+ modhex.c
+ modhex.h
+ user-record-pwquality.c
+ user-record-pwquality.h
+ user-record-sign.c
+ user-record-sign.h
+ user-record-util.c
+ user-record-util.h
+'''.split())
+
+homed_gperf_c = custom_target(
+ 'homed_gperf.c',
+ input : 'homed-gperf.gperf',
+ output : 'homed-gperf.c',
+ command : [gperf, '@INPUT@', '--output-file', '@OUTPUT@'])
+
+systemd_homed_sources += [homed_gperf_c]
+
+homectl_sources = files('''
+ home-util.c
+ home-util.h
+ homectl-fido2.c
+ homectl-fido2.h
+ homectl-pkcs11.c
+ homectl-pkcs11.h
+ homectl-recovery-key.c
+ homectl-recovery-key.h
+ homectl.c
+ modhex.c
+ modhex.h
+ user-record-pwquality.c
+ user-record-pwquality.h
+ user-record-util.c
+ user-record-util.h
+'''.split())
+
+pam_systemd_home_sym = 'src/home/pam_systemd_home.sym'
+pam_systemd_home_c = files('''
+ home-util.c
+ home-util.h
+ modhex.c
+ modhex.h
+ pam_systemd_home.c
+ user-record-util.c
+ user-record-util.h
+'''.split())
+
+if conf.get('ENABLE_HOMED') == 1
+ install_data('org.freedesktop.home1.conf',
+ install_dir : dbuspolicydir)
+ install_data('org.freedesktop.home1.service',
+ install_dir : dbussystemservicedir)
+ install_data('org.freedesktop.home1.policy',
+ install_dir : polkitpolicydir)
+
+ if install_sysconfdir
+ install_data('homed.conf',
+ install_dir : pkgsysconfdir)
+ endif
+endif
+
+tests += [
+ [['src/home/test-modhex.c',
+ 'src/home/modhex.c',
+ 'src/home/modhex.h'],
+ [],
+ []],
+]
diff --git a/src/home/modhex.c b/src/home/modhex.c
new file mode 100644
index 0000000..ae5f895
--- /dev/null
+++ b/src/home/modhex.c
@@ -0,0 +1,74 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <errno.h>
+
+#include "modhex.h"
+#include "macro.h"
+#include "memory-util.h"
+
+const char modhex_alphabet[16] = {
+ 'c', 'b', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'n', 'r', 't', 'u', 'v'
+};
+
+int decode_modhex_char(char x) {
+
+ for (size_t i = 0; i < ELEMENTSOF(modhex_alphabet); i++)
+ /* Check both upper and lowercase */
+ if (modhex_alphabet[i] == x || (modhex_alphabet[i] - 32) == x)
+ return i;
+
+ return -EINVAL;
+}
+
+int normalize_recovery_key(const char *password, char **ret) {
+ _cleanup_(erase_and_freep) char *mangled = NULL;
+ size_t l;
+
+ assert(password);
+ assert(ret);
+
+ l = strlen(password);
+ if (!IN_SET(l,
+ MODHEX_RAW_LENGTH*2, /* syntax without dashes */
+ MODHEX_FORMATTED_LENGTH-1)) /* syntax with dashes */
+ return -EINVAL;
+
+ mangled = new(char, MODHEX_FORMATTED_LENGTH);
+ if (!mangled)
+ return -ENOMEM;
+
+ for (size_t i = 0, j = 0; i < MODHEX_RAW_LENGTH; i++) {
+ size_t k;
+ int a, b;
+
+ if (l == MODHEX_RAW_LENGTH*2)
+ /* Syntax without dashes */
+ k = i * 2;
+ else {
+ /* Syntax with dashes */
+ assert(l == MODHEX_FORMATTED_LENGTH-1);
+ k = i * 2 + i / 4;
+
+ if (i > 0 && i % 4 == 0 && password[k-1] != '-')
+ return -EINVAL;
+ }
+
+ a = decode_modhex_char(password[k]);
+ if (a < 0)
+ return -EINVAL;
+ b = decode_modhex_char(password[k+1]);
+ if (b < 0)
+ return -EINVAL;
+
+ mangled[j++] = modhex_alphabet[a];
+ mangled[j++] = modhex_alphabet[b];
+
+ if (i % 4 == 3)
+ mangled[j++] = '-';
+ }
+
+ mangled[MODHEX_FORMATTED_LENGTH-1] = 0;
+
+ *ret = TAKE_PTR(mangled);
+ return 0;
+}
diff --git a/src/home/modhex.h b/src/home/modhex.h
new file mode 100644
index 0000000..7776ed0
--- /dev/null
+++ b/src/home/modhex.h
@@ -0,0 +1,14 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+/* 256 bit keys = 32 bytes */
+#define MODHEX_RAW_LENGTH 32
+
+/* Formatted as sequences of 64 modhex characters, with dashes inserted after multiples of 8 chars (incl. trailing NUL) */
+#define MODHEX_FORMATTED_LENGTH (MODHEX_RAW_LENGTH*2/8*9)
+
+extern const char modhex_alphabet[16];
+
+int decode_modhex_char(char x);
+
+int normalize_recovery_key(const char *password, char **ret);
diff --git a/src/home/org.freedesktop.home1.conf b/src/home/org.freedesktop.home1.conf
new file mode 100644
index 0000000..1975d5f
--- /dev/null
+++ b/src/home/org.freedesktop.home1.conf
@@ -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-or-later -->
+
+<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
index 0000000..10ad7c2
--- /dev/null
+++ b/src/home/org.freedesktop.home1.policy
@@ -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-or-later -->
+
+<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 area</description>
+ <message gettext-domain="systemd">Authentication is required to create a user's home area.</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 area</description>
+ <message gettext-domain="systemd">Authentication is required to remove a user's home area.</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 area</description>
+ <message gettext-domain="systemd">Authentication is required to check credentials against a user's home area.</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 area</description>
+ <message gettext-domain="systemd">Authentication is required to update a user's home area.</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 area</description>
+ <message gettext-domain="systemd">Authentication is required to resize a user's home area.</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 area</description>
+ <message gettext-domain="systemd">Authentication is required to change the password of a user's home area.</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
index 0000000..fb03914
--- /dev/null
+++ b/src/home/org.freedesktop.home1.service
@@ -0,0 +1,7 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+[D-BUS Service]
+Name=org.freedesktop.home1
+Exec=/bin/false
+User=root
+SystemdService=dbus-org.freedesktop.home1.service
diff --git a/src/home/pam_systemd_home.c b/src/home/pam_systemd_home.c
new file mode 100644
index 0000000..a91df91
--- /dev/null
+++ b/src/home/pam_systemd_home.c
@@ -0,0 +1,1070 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <security/pam_ext.h>
+#include <security/pam_modules.h>
+
+#include "sd-bus.h"
+
+#include "bus-common-errors.h"
+#include "bus-locator.h"
+#include "bus-util.h"
+#include "errno-util.h"
+#include "fd-util.h"
+#include "home-util.h"
+#include "memory-util.h"
+#include "pam-util.h"
+#include "parse-util.h"
+#include "strv.h"
+#include "user-record-util.h"
+#include "user-record.h"
+#include "user-util.h"
+
+static int parse_argv(
+ pam_handle_t *handle,
+ int argc, const char **argv,
+ bool *please_suspend,
+ bool *debug) {
+
+ int i;
+
+ assert(argc >= 0);
+ assert(argc == 0 || argv);
+
+ for (i = 0; i < argc; i++) {
+ const char *v;
+
+ if ((v = startswith(argv[i], "suspend="))) {
+ int k;
+
+ k = parse_boolean(v);
+ if (k < 0)
+ pam_syslog(handle, LOG_WARNING, "Failed to parse suspend= argument, ignoring: %s", v);
+ else if (please_suspend)
+ *please_suspend = k;
+
+ } else if (streq(argv[i], "debug")) {
+ if (debug)
+ *debug = true;
+
+ } else if ((v = startswith(argv[i], "debug="))) {
+ int k;
+ k = parse_boolean(v);
+ if (k < 0)
+ pam_syslog(handle, LOG_WARNING, "Failed to parse debug= argument, ignoring: %s", v);
+ else if (debug)
+ *debug = k;
+
+ } else
+ pam_syslog(handle, LOG_WARNING, "Unknown parameter '%s', ignoring", argv[i]);
+ }
+
+ return 0;
+}
+
+static int parse_env(
+ pam_handle_t *handle,
+ bool *please_suspend) {
+
+ const char *v;
+ int r;
+
+ /* Let's read the suspend setting from an env var in addition to the PAM command line. That makes it
+ * easy to declare the features of a display manager in code rather than configuration, and this is
+ * really a feature of code */
+
+ v = pam_getenv(handle, "SYSTEMD_HOME_SUSPEND");
+ if (!v) {
+ /* Also check the process env block, so that people can control this via an env var from the
+ * outside of our process. */
+ v = secure_getenv("SYSTEMD_HOME_SUSPEND");
+ if (!v)
+ return 0;
+ }
+
+ r = parse_boolean(v);
+ if (r < 0)
+ pam_syslog(handle, LOG_WARNING, "Failed to parse $SYSTEMD_HOME_SUSPEND argument, ignoring: %s", v);
+ else if (please_suspend)
+ *please_suspend = r;
+
+ return 0;
+}
+
+static int acquire_user_record(
+ pam_handle_t *handle,
+ const char *username,
+ UserRecord **ret_record) {
+
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+ _cleanup_(user_record_unrefp) UserRecord *ur = NULL;
+ _cleanup_(sd_bus_unrefp) sd_bus *bus = NULL;
+ _cleanup_free_ char *homed_field = NULL;
+ const char *json = NULL;
+ int r;
+
+ assert(handle);
+
+ if (!username) {
+ r = pam_get_user(handle, &username, NULL);
+ if (r != PAM_SUCCESS) {
+ pam_syslog(handle, LOG_ERR, "Failed to get user name: %s", pam_strerror(handle, r));
+ return r;
+ }
+
+ if (isempty(username)) {
+ pam_syslog(handle, LOG_ERR, "User name not set.");
+ return PAM_SERVICE_ERR;
+ }
+ }
+
+ /* Let's bypass all IPC complexity for the two user names we know for sure we don't manage, and for
+ * user names we don't consider valid. */
+ if (STR_IN_SET(username, "root", NOBODY_USER_NAME) || !valid_user_group_name(username, 0))
+ return PAM_USER_UNKNOWN;
+
+ /* We cache the user record in the PAM context. We use a field name that includes the username, since
+ * clients might change the user name associated with a PAM context underneath us. Notably, 'sudo'
+ * creates a single PAM context and first authenticates it with the user set to the originating user,
+ * then updates the user for the destination user and issues the session stack with the same PAM
+ * context. We thus must be prepared that the user record changes between calls and we keep any
+ * caching separate. */
+ homed_field = strjoin("systemd-home-user-record-", username);
+ if (!homed_field)
+ return pam_log_oom(handle);
+
+ /* Let's use the cache, so that we can share it between the session and the authentication hooks */
+ r = pam_get_data(handle, homed_field, (const void**) &json);
+ if (!IN_SET(r, PAM_SUCCESS, PAM_NO_MODULE_DATA)) {
+ pam_syslog(handle, LOG_ERR, "Failed to get PAM user record data: %s", pam_strerror(handle, r));
+ return r;
+ }
+ if (r == PAM_SUCCESS && json) {
+ /* We determined earlier that this is not a homed user? Then exit early. (We use -1 as
+ * negative cache indicator) */
+ if (json == POINTER_MAX)
+ return PAM_USER_UNKNOWN;
+ } else {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_free_ char *generic_field = NULL, *json_copy = NULL;
+
+ r = pam_acquire_bus_connection(handle, &bus);
+ if (r != PAM_SUCCESS)
+ return r;
+
+ r = bus_call_method(bus, bus_home_mgr, "GetUserRecordByName", &error, &reply, "s", username);
+ if (r < 0) {
+ if (bus_error_is_unknown_service(&error)) {
+ pam_syslog(handle, LOG_DEBUG, "systemd-homed is not available: %s", bus_error_message(&error, r));
+ goto user_unknown;
+ }
+
+ if (sd_bus_error_has_name(&error, BUS_ERROR_NO_SUCH_HOME)) {
+ pam_syslog(handle, LOG_DEBUG, "Not a user managed by systemd-homed: %s", bus_error_message(&error, r));
+ goto user_unknown;
+ }
+
+ pam_syslog(handle, LOG_ERR, "Failed to query user record: %s", bus_error_message(&error, r));
+ return PAM_SERVICE_ERR;
+ }
+
+ r = sd_bus_message_read(reply, "sbo", &json, NULL, NULL);
+ if (r < 0)
+ return pam_bus_log_parse_error(handle, r);
+
+ /* First copy: for the homed-specific data field, i.e. where we know the user record is from
+ * homed */
+ json_copy = strdup(json);
+ if (!json_copy)
+ return pam_log_oom(handle);
+
+ r = pam_set_data(handle, homed_field, json_copy, pam_cleanup_free);
+ if (r != PAM_SUCCESS) {
+ pam_syslog(handle, LOG_ERR, "Failed to set PAM user record data '%s': %s",
+ homed_field, pam_strerror(handle, r));
+ return r;
+ }
+
+ /* Take a second copy: for the generic data field, the one which we share with
+ * pam_systemd. While we insist on only reusing homed records, pam_systemd is fine with homed
+ * and non-homed user records. */
+ json_copy = strdup(json);
+ if (!json_copy)
+ return pam_log_oom(handle);
+
+ generic_field = strjoin("systemd-user-record-", username);
+ if (!generic_field)
+ return pam_log_oom(handle);
+
+ r = pam_set_data(handle, generic_field, json_copy, pam_cleanup_free);
+ if (r != PAM_SUCCESS) {
+ pam_syslog(handle, LOG_ERR, "Failed to set PAM user record data '%s': %s",
+ homed_field, pam_strerror(handle, r));
+ return r;
+ }
+
+ TAKE_PTR(json_copy);
+ }
+
+ r = json_parse(json, JSON_PARSE_SENSITIVE, &v, NULL, NULL);
+ if (r < 0) {
+ pam_syslog(handle, LOG_ERR, "Failed to parse JSON user record: %s", strerror_safe(r));
+ return PAM_SERVICE_ERR;
+ }
+
+ ur = user_record_new();
+ if (!ur)
+ return pam_log_oom(handle);
+
+ r = user_record_load(ur, v, USER_RECORD_LOAD_REFUSE_SECRET);
+ if (r < 0) {
+ pam_syslog(handle, LOG_ERR, "Failed to load user record: %s", strerror_safe(r));
+ return PAM_SERVICE_ERR;
+ }
+
+ /* Safety check if cached record actually matches what we are looking for */
+ if (!streq_ptr(username, ur->user_name)) {
+ pam_syslog(handle, LOG_ERR, "Acquired user record does not match user name.");
+ return PAM_SERVICE_ERR;
+ }
+
+ if (ret_record)
+ *ret_record = TAKE_PTR(ur);
+
+ return PAM_SUCCESS;
+
+user_unknown:
+ /* Cache this, so that we don't check again */
+ r = pam_set_data(handle, homed_field, POINTER_MAX, NULL);
+ if (r != PAM_SUCCESS)
+ pam_syslog(handle, LOG_ERR, "Failed to set PAM user record data '%s' to invalid, ignoring: %s",
+ homed_field, pam_strerror(handle, r));
+
+ return PAM_USER_UNKNOWN;
+}
+
+static int release_user_record(pam_handle_t *handle, const char *username) {
+ _cleanup_free_ char *homed_field = NULL, *generic_field = NULL;
+ int r, k;
+
+ assert(handle);
+ assert(username);
+
+ homed_field = strjoin("systemd-home-user-record-", username);
+ if (!homed_field)
+ return pam_log_oom(handle);
+
+ r = pam_set_data(handle, homed_field, NULL, NULL);
+ if (r != PAM_SUCCESS)
+ pam_syslog(handle, LOG_ERR, "Failed to release PAM user record data '%s': %s", homed_field, pam_strerror(handle, r));
+
+ generic_field = strjoin("systemd-user-record-", username);
+ if (!generic_field)
+ return pam_log_oom(handle);
+
+ k = pam_set_data(handle, generic_field, NULL, NULL);
+ if (k != PAM_SUCCESS)
+ pam_syslog(handle, LOG_ERR, "Failed to release PAM user record data '%s': %s", generic_field, pam_strerror(handle, k));
+
+ return IN_SET(r, PAM_SUCCESS, PAM_NO_MODULE_DATA) ? k : r;
+}
+
+static void cleanup_home_fd(pam_handle_t *handle, void *data, int error_status) {
+ safe_close(PTR_TO_FD(data));
+}
+
+static int handle_generic_user_record_error(
+ pam_handle_t *handle,
+ const char *user_name,
+ UserRecord *secret,
+ int ret,
+ const sd_bus_error *error) {
+
+ assert(user_name);
+ assert(secret);
+ assert(error);
+
+ int r;
+
+ /* Logs about all errors, except for PAM_CONV_ERR, i.e. when requesting more info failed. */
+
+ if (sd_bus_error_has_name(error, BUS_ERROR_HOME_ABSENT)) {
+ (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "Home of user %s is currently absent, please plug in the necessary storage device or backing file system.", user_name);
+ pam_syslog(handle, LOG_ERR, "Failed to acquire home for user %s: %s", user_name, bus_error_message(error, ret));
+ return PAM_PERM_DENIED;
+
+ } else if (sd_bus_error_has_name(error, BUS_ERROR_AUTHENTICATION_LIMIT_HIT)) {
+ (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "Too frequent unsuccessful login attempts for user %s, try again later.", user_name);
+ pam_syslog(handle, LOG_ERR, "Failed to acquire home for user %s: %s", user_name, bus_error_message(error, ret));
+ return PAM_MAXTRIES;
+
+ } else if (sd_bus_error_has_name(error, BUS_ERROR_BAD_PASSWORD)) {
+ _cleanup_(erase_and_freep) char *newp = NULL;
+
+ /* This didn't work? Ask for an (additional?) password */
+
+ if (strv_isempty(secret->password))
+ r = pam_prompt(handle, PAM_PROMPT_ECHO_OFF, &newp, "Password: ");
+ else {
+ (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "Password incorrect or not sufficient for authentication of user %s.", user_name);
+ r = pam_prompt(handle, PAM_PROMPT_ECHO_OFF, &newp, "Sorry, try again: ");
+ }
+ if (r != PAM_SUCCESS)
+ return PAM_CONV_ERR; /* no logging here */
+
+ if (isempty(newp)) {
+ pam_syslog(handle, LOG_DEBUG, "Password request aborted.");
+ return PAM_AUTHTOK_ERR;
+ }
+
+ r = user_record_set_password(secret, STRV_MAKE(newp), true);
+ if (r < 0) {
+ pam_syslog(handle, LOG_ERR, "Failed to store password: %s", strerror_safe(r));
+ return PAM_SERVICE_ERR;
+ }
+
+ } else if (sd_bus_error_has_name(error, BUS_ERROR_BAD_PASSWORD_AND_NO_TOKEN)) {
+ _cleanup_(erase_and_freep) char *newp = NULL;
+
+ if (strv_isempty(secret->password)) {
+ (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "Security token of user %s not inserted.", user_name);
+ r = pam_prompt(handle, PAM_PROMPT_ECHO_OFF, &newp, "Try again with password: ");
+ } else {
+ (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "Password incorrect or not sufficient, and configured security token of user %s not inserted.", user_name);
+ r = pam_prompt(handle, PAM_PROMPT_ECHO_OFF, &newp, "Try again with password: ");
+ }
+ if (r != PAM_SUCCESS)
+ return PAM_CONV_ERR; /* no logging here */
+
+ if (isempty(newp)) {
+ pam_syslog(handle, LOG_DEBUG, "Password request aborted.");
+ return PAM_AUTHTOK_ERR;
+ }
+
+ r = user_record_set_password(secret, STRV_MAKE(newp), true);
+ if (r < 0) {
+ pam_syslog(handle, LOG_ERR, "Failed to store password: %s", strerror_safe(r));
+ return PAM_SERVICE_ERR;
+ }
+
+ } else if (sd_bus_error_has_name(error, BUS_ERROR_TOKEN_PIN_NEEDED)) {
+ _cleanup_(erase_and_freep) char *newp = NULL;
+
+ r = pam_prompt(handle, PAM_PROMPT_ECHO_OFF, &newp, "Security token PIN: ");
+ if (r != PAM_SUCCESS)
+ return PAM_CONV_ERR; /* no logging here */
+
+ if (isempty(newp)) {
+ pam_syslog(handle, LOG_DEBUG, "PIN request aborted.");
+ return PAM_AUTHTOK_ERR;
+ }
+
+ r = user_record_set_token_pin(secret, STRV_MAKE(newp), false);
+ if (r < 0) {
+ pam_syslog(handle, LOG_ERR, "Failed to store PIN: %s", strerror_safe(r));
+ return PAM_SERVICE_ERR;
+ }
+
+ } else if (sd_bus_error_has_name(error, BUS_ERROR_TOKEN_PROTECTED_AUTHENTICATION_PATH_NEEDED)) {
+
+ (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "Please authenticate physically on security token of user %s.", user_name);
+
+ r = user_record_set_pkcs11_protected_authentication_path_permitted(secret, true);
+ if (r < 0) {
+ pam_syslog(handle, LOG_ERR, "Failed to set PKCS#11 protected authentication path permitted flag: %s", strerror_safe(r));
+ return PAM_SERVICE_ERR;
+ }
+
+ } else if (sd_bus_error_has_name(error, BUS_ERROR_TOKEN_USER_PRESENCE_NEEDED)) {
+
+ (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "Please verify presence on security token of user %s.", user_name);
+
+ r = user_record_set_fido2_user_presence_permitted(secret, true);
+ if (r < 0) {
+ pam_syslog(handle, LOG_ERR, "Failed to set FIDO2 user presence permitted flag: %s", strerror_safe(r));
+ return PAM_SERVICE_ERR;
+ }
+
+ } else if (sd_bus_error_has_name(error, BUS_ERROR_TOKEN_PIN_LOCKED)) {
+
+ (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "Security token PIN is locked, please unlock it first. (Hint: Removal and re-insertion might suffice.)");
+ return PAM_SERVICE_ERR;
+
+ } else if (sd_bus_error_has_name(error, BUS_ERROR_TOKEN_BAD_PIN)) {
+ _cleanup_(erase_and_freep) char *newp = NULL;
+
+ (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "Security token PIN incorrect for user %s.", user_name);
+ r = pam_prompt(handle, PAM_PROMPT_ECHO_OFF, &newp, "Sorry, retry security token PIN: ");
+ if (r != PAM_SUCCESS)
+ return PAM_CONV_ERR; /* no logging here */
+
+ if (isempty(newp)) {
+ pam_syslog(handle, LOG_DEBUG, "PIN request aborted.");
+ return PAM_AUTHTOK_ERR;
+ }
+
+ r = user_record_set_token_pin(secret, STRV_MAKE(newp), false);
+ if (r < 0) {
+ pam_syslog(handle, LOG_ERR, "Failed to store PIN: %s", strerror_safe(r));
+ return PAM_SERVICE_ERR;
+ }
+
+ } else if (sd_bus_error_has_name(error, BUS_ERROR_TOKEN_BAD_PIN_FEW_TRIES_LEFT)) {
+ _cleanup_(erase_and_freep) char *newp = NULL;
+
+ (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "Security token PIN of user %s incorrect (only a few tries left!)", user_name);
+ r = pam_prompt(handle, PAM_PROMPT_ECHO_OFF, &newp, "Sorry, retry security token PIN: ");
+ if (r != PAM_SUCCESS)
+ return PAM_CONV_ERR; /* no logging here */
+
+ if (isempty(newp)) {
+ pam_syslog(handle, LOG_DEBUG, "PIN request aborted.");
+ return PAM_AUTHTOK_ERR;
+ }
+
+ r = user_record_set_token_pin(secret, STRV_MAKE(newp), false);
+ if (r < 0) {
+ pam_syslog(handle, LOG_ERR, "Failed to store PIN: %s", strerror_safe(r));
+ return PAM_SERVICE_ERR;
+ }
+
+ } else if (sd_bus_error_has_name(error, BUS_ERROR_TOKEN_BAD_PIN_ONE_TRY_LEFT)) {
+ _cleanup_(erase_and_freep) char *newp = NULL;
+
+ (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "Security token PIN of user %s incorrect (only one try left!)", user_name);
+ r = pam_prompt(handle, PAM_PROMPT_ECHO_OFF, &newp, "Sorry, retry security token PIN: ");
+ if (r != PAM_SUCCESS)
+ return PAM_CONV_ERR; /* no logging here */
+
+ if (isempty(newp)) {
+ pam_syslog(handle, LOG_DEBUG, "PIN request aborted.");
+ return PAM_AUTHTOK_ERR;
+ }
+
+ r = user_record_set_token_pin(secret, STRV_MAKE(newp), false);
+ if (r < 0) {
+ pam_syslog(handle, LOG_ERR, "Failed to store PIN: %s", strerror_safe(r));
+ return PAM_SERVICE_ERR;
+ }
+
+ } else {
+ pam_syslog(handle, LOG_ERR, "Failed to acquire home for user %s: %s", user_name, bus_error_message(error, ret));
+ return PAM_SERVICE_ERR;
+ }
+
+ return PAM_SUCCESS;
+}
+
+static int acquire_home(
+ pam_handle_t *handle,
+ bool please_authenticate,
+ bool please_suspend,
+ bool debug) {
+
+ _cleanup_(user_record_unrefp) UserRecord *ur = NULL, *secret = NULL;
+ bool do_auth = please_authenticate, home_not_active = false, home_locked = false;
+ _cleanup_(sd_bus_unrefp) sd_bus *bus = NULL;
+ _cleanup_close_ int acquired_fd = -1;
+ _cleanup_free_ char *fd_field = NULL;
+ const void *home_fd_ptr = NULL;
+ const char *username = NULL;
+ unsigned n_attempts = 0;
+ int r;
+
+ assert(handle);
+
+ /* This acquires a reference to a home directory in one of two ways: if please_authenticate is true,
+ * then we'll call AcquireHome() after asking the user for a password. Otherwise it tries to call
+ * RefHome() and if that fails queries the user for a password and uses AcquireHome().
+ *
+ * The idea is that the PAM authentication hook sets please_authenticate and thus always
+ * authenticates, while the other PAM hooks unset it so that they can a ref of their own without
+ * authentication if possible, but with authentication if necessary. */
+
+ r = pam_get_user(handle, &username, NULL);
+ if (r != PAM_SUCCESS) {
+ pam_syslog(handle, LOG_ERR, "Failed to get user name: %s", pam_strerror(handle, r));
+ return r;
+ }
+
+ if (isempty(username)) {
+ pam_syslog(handle, LOG_ERR, "User name not set.");
+ return PAM_SERVICE_ERR;
+ }
+
+ /* If we already have acquired the fd, let's shortcut this */
+ fd_field = strjoin("systemd-home-fd-", username);
+ if (!fd_field)
+ return pam_log_oom(handle);
+
+ r = pam_get_data(handle, fd_field, &home_fd_ptr);
+ if (!IN_SET(r, PAM_SUCCESS, PAM_NO_MODULE_DATA)) {
+ pam_syslog(handle, LOG_ERR, "Failed to retrieve PAM home reference fd: %s", pam_strerror(handle, r));
+ return r;
+ }
+ if (r == PAM_SUCCESS && PTR_TO_FD(home_fd_ptr) >= 0)
+ return PAM_SUCCESS;
+
+ r = pam_acquire_bus_connection(handle, &bus);
+ if (r != PAM_SUCCESS)
+ return r;
+
+ r = acquire_user_record(handle, username, &ur);
+ if (r != PAM_SUCCESS)
+ return r;
+
+ /* Implement our own retry loop here instead of relying on the PAM client's one. That's because it
+ * might happen that the record we stored on the host does not match the encryption password of
+ * the LUKS image in case the image was used in a different system where the password was
+ * changed. In that case it will happen that the LUKS password and the host password are
+ * different, and we handle that by collecting and passing multiple passwords in that case. Hence we
+ * treat bad passwords as a request to collect one more password and pass the new all all previously
+ * used passwords again. */
+
+ for (;;) {
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL, *reply = NULL;
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+
+ if (do_auth && !secret) {
+ const char *cached_password = NULL;
+
+ secret = user_record_new();
+ if (!secret)
+ return pam_log_oom(handle);
+
+ /* If there's already a cached password, use it. But if not let's authenticate
+ * without anything, maybe some other authentication mechanism systemd-homed
+ * implements (such as PKCS#11) allows us to authenticate without anything else. */
+ r = pam_get_item(handle, PAM_AUTHTOK, (const void**) &cached_password);
+ if (!IN_SET(r, PAM_BAD_ITEM, PAM_SUCCESS)) {
+ pam_syslog(handle, LOG_ERR, "Failed to get cached password: %s", pam_strerror(handle, r));
+ return r;
+ }
+
+ if (!isempty(cached_password)) {
+ r = user_record_set_password(secret, STRV_MAKE(cached_password), true);
+ if (r < 0) {
+ pam_syslog(handle, LOG_ERR, "Failed to store password: %s", strerror_safe(r));
+ return PAM_SERVICE_ERR;
+ }
+ }
+ }
+
+ r = bus_message_new_method_call(bus, &m, bus_home_mgr, do_auth ? "AcquireHome" : "RefHome");
+ if (r < 0)
+ return pam_bus_log_create_error(handle, r);
+
+ r = sd_bus_message_append(m, "s", ur->user_name);
+ if (r < 0)
+ return pam_bus_log_create_error(handle, r);
+
+ if (do_auth) {
+ r = bus_message_append_secret(m, secret);
+ if (r < 0)
+ return pam_bus_log_create_error(handle, r);
+ }
+
+ r = sd_bus_message_append(m, "b", please_suspend);
+ if (r < 0)
+ return pam_bus_log_create_error(handle, r);
+
+ r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, &reply);
+ if (r < 0) {
+
+ if (sd_bus_error_has_name(&error, BUS_ERROR_HOME_NOT_ACTIVE))
+ /* Only on RefHome(): We can't access the home directory currently, unless
+ * it's unlocked with a password. Hence, let's try this again, this time with
+ * authentication. */
+ home_not_active = true;
+ else if (sd_bus_error_has_name(&error, BUS_ERROR_HOME_LOCKED))
+ home_locked = true; /* Similar */
+ else {
+ r = handle_generic_user_record_error(handle, ur->user_name, secret, r, &error);
+ if (r == PAM_CONV_ERR) {
+ /* Password/PIN prompts will fail in certain environments, for example when
+ * we are called from OpenSSH's account or session hooks, or in systemd's
+ * per-service PAM logic. In that case, print a friendly message and accept
+ * failure. */
+
+ if (home_not_active)
+ (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "Home of user %s is currently not active, please log in locally first.", ur->user_name);
+ if (home_locked)
+ (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "Home of user %s is currently locked, please unlock locally first.", ur->user_name);
+
+ pam_syslog(handle, please_authenticate ? LOG_ERR : LOG_DEBUG, "Failed to prompt for password/prompt.");
+
+ return home_not_active || home_locked ? PAM_PERM_DENIED : PAM_CONV_ERR;
+ }
+ if (r != PAM_SUCCESS)
+ return r;
+ }
+
+ } else {
+ int fd;
+
+ r = sd_bus_message_read(reply, "h", &fd);
+ if (r < 0)
+ return pam_bus_log_parse_error(handle, r);
+
+ acquired_fd = fcntl(fd, F_DUPFD_CLOEXEC, 3);
+ if (acquired_fd < 0) {
+ pam_syslog(handle, LOG_ERR, "Failed to duplicate acquired fd: %s", bus_error_message(&error, r));
+ return PAM_SERVICE_ERR;
+ }
+
+ break;
+ }
+
+ if (++n_attempts >= 5) {
+ (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "Too many unsuccessful login attempts for user %s, refusing.", ur->user_name);
+ pam_syslog(handle, LOG_ERR, "Failed to acquire home for user %s: %s", ur->user_name, bus_error_message(&error, r));
+ return PAM_MAXTRIES;
+ }
+
+ /* Try again, this time with authentication if we didn't do that before. */
+ do_auth = true;
+ }
+
+ /* Later PAM modules may need the auth token, but only during pam_authenticate. */
+ if (please_authenticate && !strv_isempty(secret->password)) {
+ r = pam_set_item(handle, PAM_AUTHTOK, *secret->password);
+ if (r < 0) {
+ pam_syslog(handle, LOG_ERR, "Failed to set PAM auth token: %s", pam_strerror(handle, r));
+ return r;
+ }
+ }
+
+ r = pam_set_data(handle, fd_field, FD_TO_PTR(acquired_fd), cleanup_home_fd);
+ if (r < 0) {
+ pam_syslog(handle, LOG_ERR, "Failed to set PAM bus data: %s", pam_strerror(handle, r));
+ return r;
+ }
+ TAKE_FD(acquired_fd);
+
+ if (do_auth) {
+ /* We likely just activated the home directory, let's flush out the user record, since a
+ * newer embedded user record might have been acquired from the activation. */
+
+ r = release_user_record(handle, ur->user_name);
+ if (!IN_SET(r, PAM_SUCCESS, PAM_NO_MODULE_DATA))
+ return r;
+ }
+
+ pam_syslog(handle, LOG_NOTICE, "Home for user %s successfully acquired.", ur->user_name);
+
+ return PAM_SUCCESS;
+}
+
+static int release_home_fd(pam_handle_t *handle, const char *username) {
+ _cleanup_free_ char *fd_field = NULL;
+ const void *home_fd_ptr = NULL;
+ int r;
+
+ assert(handle);
+ assert(username);
+
+ fd_field = strjoin("systemd-home-fd-", username);
+ if (!fd_field)
+ return pam_log_oom(handle);
+
+ r = pam_get_data(handle, fd_field, &home_fd_ptr);
+ if (r == PAM_NO_MODULE_DATA || (r == PAM_SUCCESS && PTR_TO_FD(home_fd_ptr) < 0))
+ return PAM_NO_MODULE_DATA;
+ if (r != PAM_SUCCESS) {
+ pam_syslog(handle, LOG_ERR, "Failed to retrieve PAM home reference fd: %s", pam_strerror(handle, r));
+ return r;
+ }
+
+ r = pam_set_data(handle, fd_field, NULL, NULL);
+ if (r != PAM_SUCCESS)
+ pam_syslog(handle, LOG_ERR, "Failed to release PAM home reference fd: %s", pam_strerror(handle, r));
+
+ return r;
+}
+
+_public_ PAM_EXTERN int pam_sm_authenticate(
+ pam_handle_t *handle,
+ int flags,
+ int argc, const char **argv) {
+
+ bool debug = false, suspend_please = false;
+
+ if (parse_env(handle, &suspend_please) < 0)
+ return PAM_AUTH_ERR;
+
+ if (parse_argv(handle,
+ argc, argv,
+ &suspend_please,
+ &debug) < 0)
+ return PAM_AUTH_ERR;
+
+ if (debug)
+ pam_syslog(handle, LOG_DEBUG, "pam-systemd-homed authenticating");
+
+ return acquire_home(handle, /* please_authenticate= */ true, suspend_please, debug);
+}
+
+_public_ PAM_EXTERN int pam_sm_setcred(pam_handle_t *pamh, int flags, int argc, const char **argv) {
+ return PAM_SUCCESS;
+}
+
+_public_ PAM_EXTERN int pam_sm_open_session(
+ pam_handle_t *handle,
+ int flags,
+ int argc, const char **argv) {
+
+ bool debug = false, suspend_please = false;
+ int r;
+
+ if (parse_env(handle, &suspend_please) < 0)
+ return PAM_SESSION_ERR;
+
+ if (parse_argv(handle,
+ argc, argv,
+ &suspend_please,
+ &debug) < 0)
+ return PAM_SESSION_ERR;
+
+ if (debug)
+ pam_syslog(handle, LOG_DEBUG, "pam-systemd-homed session start");
+
+ r = acquire_home(handle, /* please_authenticate = */ false, suspend_please, debug);
+ if (r == PAM_USER_UNKNOWN) /* Not managed by us? Don't complain. */
+ return PAM_SUCCESS;
+ if (r != PAM_SUCCESS)
+ return r;
+
+ r = pam_putenv(handle, "SYSTEMD_HOME=1");
+ if (r != PAM_SUCCESS) {
+ pam_syslog(handle, LOG_ERR, "Failed to set PAM environment variable $SYSTEMD_HOME: %s", pam_strerror(handle, r));
+ return r;
+ }
+
+ r = pam_putenv(handle, suspend_please ? "SYSTEMD_HOME_SUSPEND=1" : "SYSTEMD_HOME_SUSPEND=0");
+ if (r != PAM_SUCCESS) {
+ pam_syslog(handle, LOG_ERR, "Failed to set PAM environment variable $SYSTEMD_HOME_SUSPEND: %s", pam_strerror(handle, r));
+ return r;
+ }
+
+ /* Let's release the D-Bus connection, after all the session might live quite a long time, and we are
+ * not going to process the bus connection in that time, so let's better close before the daemon
+ * kicks us off because we are not processing anything. */
+ (void) pam_release_bus_connection(handle);
+ return PAM_SUCCESS;
+}
+
+_public_ PAM_EXTERN int pam_sm_close_session(
+ pam_handle_t *handle,
+ int flags,
+ int argc, const char **argv) {
+
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
+ _cleanup_(sd_bus_unrefp) sd_bus *bus = NULL;
+ const char *username = NULL;
+ bool debug = false;
+ int r;
+
+ if (parse_argv(handle,
+ argc, argv,
+ NULL,
+ &debug) < 0)
+ return PAM_SESSION_ERR;
+
+ if (debug)
+ pam_syslog(handle, LOG_DEBUG, "pam-systemd-homed session end");
+
+ r = pam_get_user(handle, &username, NULL);
+ if (r != PAM_SUCCESS) {
+ pam_syslog(handle, LOG_ERR, "Failed to get user name: %s", pam_strerror(handle, r));
+ return r;
+ }
+
+ if (isempty(username)) {
+ pam_syslog(handle, LOG_ERR, "User name not set.");
+ return PAM_SERVICE_ERR;
+ }
+
+ /* Let's explicitly drop the reference to the homed session, so that the subsequent ReleaseHome()
+ * call will be able to do its thing. */
+ r = release_home_fd(handle, username);
+ if (r == PAM_NO_MODULE_DATA) /* Nothing to do, we never acquired an fd */
+ return PAM_SUCCESS;
+ if (r != PAM_SUCCESS)
+ return r;
+
+ r = pam_acquire_bus_connection(handle, &bus);
+ if (r != PAM_SUCCESS)
+ return r;
+
+ r = bus_message_new_method_call(bus, &m, bus_home_mgr, "ReleaseHome");
+ if (r < 0)
+ return pam_bus_log_create_error(handle, r);
+
+ r = sd_bus_message_append(m, "s", username);
+ if (r < 0)
+ return pam_bus_log_create_error(handle, r);
+
+ r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
+ if (r < 0) {
+ if (sd_bus_error_has_name(&error, BUS_ERROR_HOME_BUSY))
+ pam_syslog(handle, LOG_NOTICE, "Not deactivating home directory of %s, as it is still used.", username);
+ else {
+ pam_syslog(handle, LOG_ERR, "Failed to release user home: %s", bus_error_message(&error, r));
+ return PAM_SESSION_ERR;
+ }
+ }
+
+ return PAM_SUCCESS;
+}
+
+_public_ PAM_EXTERN int pam_sm_acct_mgmt(
+ pam_handle_t *handle,
+ int flags,
+ int argc,
+ const char **argv) {
+
+ _cleanup_(user_record_unrefp) UserRecord *ur = NULL;
+ bool debug = false, please_suspend = false;
+ usec_t t;
+ int r;
+
+ if (parse_env(handle, &please_suspend) < 0)
+ return PAM_AUTH_ERR;
+
+ if (parse_argv(handle,
+ argc, argv,
+ &please_suspend,
+ &debug) < 0)
+ return PAM_AUTH_ERR;
+
+ if (debug)
+ pam_syslog(handle, LOG_DEBUG, "pam-systemd-homed account management");
+
+ r = acquire_home(handle, /* please_authenticate = */ false, please_suspend, debug);
+ if (r != PAM_SUCCESS)
+ return r;
+
+ r = acquire_user_record(handle, NULL, &ur);
+ if (r != PAM_SUCCESS)
+ return r;
+
+ r = user_record_test_blocked(ur);
+ switch (r) {
+
+ case -ESTALE:
+ pam_syslog(handle, LOG_WARNING, "User record for '%s' is newer than current system time, assuming incorrect system clock, allowing access.", ur->user_name);
+ break;
+
+ case -ENOLCK:
+ (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "User record is blocked, prohibiting access.");
+ return PAM_ACCT_EXPIRED;
+
+ case -EL2HLT:
+ (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "User record is not valid yet, prohibiting access.");
+ return PAM_ACCT_EXPIRED;
+
+ case -EL3HLT:
+ (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "User record is not valid anymore, prohibiting access.");
+ return PAM_ACCT_EXPIRED;
+
+ default:
+ if (r < 0) {
+ (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "User record not valid, prohibiting access.");
+ return PAM_ACCT_EXPIRED;
+ }
+
+ break;
+ }
+
+ t = user_record_ratelimit_next_try(ur);
+ if (t != USEC_INFINITY) {
+ usec_t n = now(CLOCK_REALTIME);
+
+ if (t > n) {
+ char buf[FORMAT_TIMESPAN_MAX];
+ (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "Too many logins, try again in %s.",
+ format_timespan(buf, sizeof(buf), t - n, USEC_PER_SEC));
+
+ return PAM_MAXTRIES;
+ }
+ }
+
+ r = user_record_test_password_change_required(ur);
+ switch (r) {
+
+ case -EKEYREVOKED:
+ (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "Password change required.");
+ return PAM_NEW_AUTHTOK_REQD;
+
+ case -EOWNERDEAD:
+ (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "Password expired, change requird.");
+ return PAM_NEW_AUTHTOK_REQD;
+
+ case -EKEYREJECTED:
+ /* Strictly speaking this is only about password expiration, and we might want to allow
+ * authentication via PKCS#11 or so, but let's ignore this fine distinction for now. */
+ (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "Password is expired, but can't change, refusing login.");
+ return PAM_AUTHTOK_EXPIRED;
+
+ case -EKEYEXPIRED:
+ (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "Password will expire soon, please change.");
+ break;
+
+ case -ESTALE:
+ /* If the system clock is wrong, let's log but continue */
+ pam_syslog(handle, LOG_WARNING, "Couldn't check if password change is required, last change is in the future, system clock likely wrong.");
+ break;
+
+ case -EROFS:
+ /* All good, just means the password if we wanted to change we couldn't, but we don't need to */
+ break;
+
+ default:
+ if (r < 0) {
+ (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "User record not valid, prohibiting access.");
+ return PAM_AUTHTOK_EXPIRED;
+ }
+
+ break;
+ }
+
+ return PAM_SUCCESS;
+}
+
+_public_ PAM_EXTERN int pam_sm_chauthtok(
+ pam_handle_t *handle,
+ int flags,
+ int argc,
+ const char **argv) {
+
+ _cleanup_(user_record_unrefp) UserRecord *ur = NULL, *old_secret = NULL, *new_secret = NULL;
+ const char *old_password = NULL, *new_password = NULL;
+ _cleanup_(sd_bus_unrefp) sd_bus *bus = NULL;
+ unsigned n_attempts = 0;
+ bool debug = false;
+ int r;
+
+ if (parse_argv(handle,
+ argc, argv,
+ NULL,
+ &debug) < 0)
+ return PAM_AUTH_ERR;
+
+ if (debug)
+ pam_syslog(handle, LOG_DEBUG, "pam-systemd-homed account management");
+
+ r = pam_acquire_bus_connection(handle, &bus);
+ if (r != PAM_SUCCESS)
+ return r;
+
+ r = acquire_user_record(handle, NULL, &ur);
+ if (r != PAM_SUCCESS)
+ return r;
+
+ /* Start with cached credentials */
+ r = pam_get_item(handle, PAM_OLDAUTHTOK, (const void**) &old_password);
+ if (!IN_SET(r, PAM_BAD_ITEM, PAM_SUCCESS)) {
+ pam_syslog(handle, LOG_ERR, "Failed to get old password: %s", pam_strerror(handle, r));
+ return r;
+ }
+ r = pam_get_item(handle, PAM_AUTHTOK, (const void**) &new_password);
+ if (!IN_SET(r, PAM_BAD_ITEM, PAM_SUCCESS)) {
+ pam_syslog(handle, LOG_ERR, "Failed to get cached password: %s", pam_strerror(handle, r));
+ return r;
+ }
+
+ if (isempty(new_password)) {
+ /* No, it's not cached, then let's ask for the password and its verification, and cache
+ * it. */
+
+ r = pam_get_authtok_noverify(handle, &new_password, "New password: ");
+ if (r != PAM_SUCCESS) {
+ pam_syslog(handle, LOG_ERR, "Failed to get new password: %s", pam_strerror(handle, r));
+ return r;
+ }
+ if (isempty(new_password)) {
+ pam_syslog(handle, LOG_DEBUG, "Password request aborted.");
+ return PAM_AUTHTOK_ERR;
+ }
+
+ r = pam_get_authtok_verify(handle, &new_password, "new password: "); /* Lower case, since PAM prefixes 'Repeat' */
+ if (r != PAM_SUCCESS) {
+ pam_syslog(handle, LOG_ERR, "Failed to get password again: %s", pam_strerror(handle, r));
+ return r;
+ }
+
+ // FIXME: pam_pwquality will ask for the password a third time. It really shouldn't do
+ // that, and instead assume the password was already verified once when it is found to be
+ // cached already. needs to be fixed in pam_pwquality
+ }
+
+ /* Now everything is cached and checked, let's exit from the preliminary check */
+ if (FLAGS_SET(flags, PAM_PRELIM_CHECK))
+ return PAM_SUCCESS;
+
+ old_secret = user_record_new();
+ if (!old_secret)
+ return pam_log_oom(handle);
+
+ if (!isempty(old_password)) {
+ r = user_record_set_password(old_secret, STRV_MAKE(old_password), true);
+ if (r < 0) {
+ pam_syslog(handle, LOG_ERR, "Failed to store old password: %s", strerror_safe(r));
+ return PAM_SERVICE_ERR;
+ }
+ }
+
+ new_secret = user_record_new();
+ if (!new_secret)
+ return pam_log_oom(handle);
+
+ r = user_record_set_password(new_secret, STRV_MAKE(new_password), true);
+ if (r < 0) {
+ pam_syslog(handle, LOG_ERR, "Failed to store new password: %s", strerror_safe(r));
+ return PAM_SERVICE_ERR;
+ }
+
+ for (;;) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
+
+ r = bus_message_new_method_call(bus, &m, bus_home_mgr, "ChangePasswordHome");
+ if (r < 0)
+ return pam_bus_log_create_error(handle, r);
+
+ r = sd_bus_message_append(m, "s", ur->user_name);
+ if (r < 0)
+ return pam_bus_log_create_error(handle, r);
+
+ r = bus_message_append_secret(m, new_secret);
+ if (r < 0)
+ return pam_bus_log_create_error(handle, r);
+
+ r = bus_message_append_secret(m, old_secret);
+ if (r < 0)
+ return pam_bus_log_create_error(handle, r);
+
+ r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
+ if (r < 0) {
+ r = handle_generic_user_record_error(handle, ur->user_name, old_secret, r, &error);
+ if (r == PAM_CONV_ERR) {
+ pam_syslog(handle, LOG_ERR, "Failed to prompt for password/prompt.");
+ return PAM_CONV_ERR;
+ }
+ if (r != PAM_SUCCESS)
+ return r;
+ } else {
+ pam_syslog(handle, LOG_NOTICE, "Successfully changed password for user %s.", ur->user_name);
+ return PAM_SUCCESS;
+ }
+
+ if (++n_attempts >= 5)
+ break;
+
+ /* Try again */
+ };
+
+ pam_syslog(handle, LOG_NOTICE, "Failed to change password for user %s: %m", ur->user_name);
+ return PAM_MAXTRIES;
+}
diff --git a/src/home/pam_systemd_home.sym b/src/home/pam_systemd_home.sym
new file mode 100644
index 0000000..293c06f
--- /dev/null
+++ b/src/home/pam_systemd_home.sym
@@ -0,0 +1,12 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+{
+global:
+ pam_sm_authenticate;
+ pam_sm_setcred;
+ pam_sm_open_session;
+ pam_sm_close_session;
+ pam_sm_acct_mgmt;
+ pam_sm_chauthtok;
+local: *;
+};
diff --git a/src/home/test-modhex.c b/src/home/test-modhex.c
new file mode 100644
index 0000000..1bd9061
--- /dev/null
+++ b/src/home/test-modhex.c
@@ -0,0 +1,51 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "modhex.h"
+#include "alloc-util.h"
+#include "string-util.h"
+
+static void test_normalize_recovery_key(const char *t, const char *expected) {
+ _cleanup_free_ char *z = NULL;
+ int r;
+
+ assert(t);
+
+ r = normalize_recovery_key(t, &z);
+ assert_se(expected ?
+ (r >= 0 && streq(z, expected)) :
+ (r == -EINVAL && z == NULL));
+}
+
+int main(int argc, char *arv[]) {
+
+ test_normalize_recovery_key("iefgcelh-biduvkjv-cjvuncnk-vlfchdid-jhtuhhde-urkllkeg-ilkjgbrt-hjkbgktj",
+ "iefgcelh-biduvkjv-cjvuncnk-vlfchdid-jhtuhhde-urkllkeg-ilkjgbrt-hjkbgktj");
+
+ test_normalize_recovery_key("iefgcelhbiduvkjvcjvuncnkvlfchdidjhtuhhdeurkllkegilkjgbrthjkbgktj",
+ "iefgcelh-biduvkjv-cjvuncnk-vlfchdid-jhtuhhde-urkllkeg-ilkjgbrt-hjkbgktj");
+
+ test_normalize_recovery_key("IEFGCELH-BIDUVKJV-CJVUNCNK-VLFCHDID-JHTUHHDE-URKLLKEG-ILKJGBRT-HJKBGKTJ",
+ "iefgcelh-biduvkjv-cjvuncnk-vlfchdid-jhtuhhde-urkllkeg-ilkjgbrt-hjkbgktj");
+
+ test_normalize_recovery_key("IEFGCELHBIDUVKJVCJVUNCNKVLFCHDIDJHTUHHDEURKLLKEGILKJGBRTHJKBGKTJ",
+ "iefgcelh-biduvkjv-cjvuncnk-vlfchdid-jhtuhhde-urkllkeg-ilkjgbrt-hjkbgktj");
+
+ test_normalize_recovery_key("Iefgcelh-Biduvkjv-Cjvuncnk-Vlfchdid-Jhtuhhde-Urkllkeg-Ilkjgbrt-Hjkbgktj",
+ "iefgcelh-biduvkjv-cjvuncnk-vlfchdid-jhtuhhde-urkllkeg-ilkjgbrt-hjkbgktj");
+
+ test_normalize_recovery_key("Iefgcelhbiduvkjvcjvuncnkvlfchdidjhtuhhdeurkllkegilkjgbrthjkbgktj",
+ "iefgcelh-biduvkjv-cjvuncnk-vlfchdid-jhtuhhde-urkllkeg-ilkjgbrt-hjkbgktj");
+
+ test_normalize_recovery_key("iefgcelh-biduvkjv-cjvuncnk-vlfchdid-jhtuhhde-urkllkeg-ilkjgbrt-hjkbgkt", NULL);
+ test_normalize_recovery_key("iefgcelhbiduvkjvcjvuncnkvlfchdidjhtuhhdeurkllkegilkjgbrthjkbgkt", NULL);
+ test_normalize_recovery_key("IEFGCELHBIDUVKJVCJVUNCNKVLFCHDIDJHTUHHDEURKLLKEGILKJGBRTHJKBGKT", NULL);
+
+ test_normalize_recovery_key("xefgcelh-biduvkjv-cjvuncnk-vlfchdid-jhtuhhde-urkllkeg-ilkjgbrt-hjkbgktj", NULL);
+ test_normalize_recovery_key("Xefgcelh-biduvkjv-cjvuncnk-vlfchdid-jhtuhhde-urkllkeg-ilkjgbrt-hjkbgktj", NULL);
+ test_normalize_recovery_key("iefgcelh+biduvkjv-cjvuncnk-vlfchdid-jhtuhhde-urkllkeg-ilkjgbrt-hjkbgktj", NULL);
+ test_normalize_recovery_key("iefgcelhebiduvkjv-cjvuncnk-vlfchdid-jhtuhhde-urkllkeg-ilkjgbrt-hjkbgktj", NULL);
+
+ test_normalize_recovery_key("", NULL);
+
+ return 0;
+}
diff --git a/src/home/user-record-pwquality.c b/src/home/user-record-pwquality.c
new file mode 100644
index 0000000..23c3357
--- /dev/null
+++ b/src/home/user-record-pwquality.c
@@ -0,0 +1,91 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "bus-common-errors.h"
+#include "errno-util.h"
+#include "home-util.h"
+#include "libcrypt-util.h"
+#include "pwquality-util.h"
+#include "strv.h"
+#include "user-record-pwquality.h"
+#include "user-record-util.h"
+
+#if HAVE_PWQUALITY
+
+int user_record_quality_check_password(
+ UserRecord *hr,
+ UserRecord *secret,
+ sd_bus_error *error) {
+
+ _cleanup_(sym_pwquality_free_settingsp) pwquality_settings_t *pwq = NULL;
+ char buf[PWQ_MAX_ERROR_MESSAGE_LEN], **pp;
+ void *auxerror;
+ int r;
+
+ assert(hr);
+ assert(secret);
+
+ r = pwq_allocate_context(&pwq);
+ if (ERRNO_IS_NOT_SUPPORTED(r))
+ return 0;
+ if (r < 0)
+ return log_debug_errno(r, "Failed to allocate libpwquality context: %m");
+
+ /* 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 = sym_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",
+ sym_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 = sym_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",
+ sym_pwquality_strerror(buf, sizeof(buf), r, auxerror));
+ }
+
+ return 1;
+}
+
+#else
+
+int user_record_quality_check_password(
+ UserRecord *hr,
+ UserRecord *secret,
+ sd_bus_error *error) {
+
+ return 0;
+}
+
+#endif
diff --git a/src/home/user-record-pwquality.h b/src/home/user-record-pwquality.h
new file mode 100644
index 0000000..b3b2690
--- /dev/null
+++ b/src/home/user-record-pwquality.h
@@ -0,0 +1,7 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "sd-bus.h"
+#include "user-record.h"
+
+int user_record_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
index 0000000..8cd3a46
--- /dev/null
+++ b/src/home/user-record-sign.c
@@ -0,0 +1,176 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#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
index 0000000..87c6813
--- /dev/null
+++ b/src/home/user-record-sign.h
@@ -0,0 +1,19 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#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
index 0000000..6bcbb56
--- /dev/null
+++ b/src/home/user-record-util.c
@@ -0,0 +1,1366 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <sys/xattr.h>
+
+#include "errno-util.h"
+#include "home-util.h"
+#include "id128-util.h"
+#include "libcrypt-util.h"
+#include "memory-util.h"
+#include "modhex.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, *description = 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;
+ }
+
+ description = strjoin("Primary Group of User ", un);
+ if (!description)
+ 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("description", JSON_BUILD_STRING(description)),
+ 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 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[ID128_UUID_STRING_MAX], luks_uuids[ID128_UUID_STRING_MAX], fs_uuids[ID128_UUID_STRING_MAX];
+ _cleanup_free_ char *ip = NULL, *hd = NULL, *ip_auto = NULL, *lc = NULL, *lcm = NULL, *fst = 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;
+ } else if (!h->image_path && storage >= 0) {
+ r = user_record_build_image_path(storage, user_record_user_name_and_realm(h), &ip_auto);
+ if (r < 0)
+ return r;
+ }
+
+ if (home_directory) {
+ hd = strdup(home_directory);
+ if (!hd)
+ return -ENOMEM;
+ }
+
+ if (file_system_type) {
+ fst = strdup(file_system_type);
+ if (!fst)
+ return -ENOMEM;
+ }
+
+ if (luks_cipher) {
+ lc = strdup(luks_cipher);
+ if (!lc)
+ return -ENOMEM;
+ }
+
+ if (luks_cipher_mode) {
+ lcm = strdup(luks_cipher_mode);
+ if (!lcm)
+ 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 (ip_auto)
+ free_and_replace(h->image_path_auto, ip_auto);
+
+ 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 (lc)
+ free_and_replace(h->luks_cipher, lc);
+ if (lcm)
+ free_and_replace(h->luks_cipher_mode, lcm);
+ if (luks_volume_key_size != UINT64_MAX)
+ h->luks_volume_key_size = luks_volume_key_size;
+
+ if (fst)
+ free_and_replace(h->file_system_type, fst);
+ if (hd)
+ free_and_replace(h->home_directory, hd);
+
+ if (uid_is_valid(uid))
+ h->uid = uid;
+ if (gid_is_valid(gid))
+ h->gid = gid;
+
+ 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)) {
+ ssize_t n;
+ char x[2];
+
+ n = getxattr(ip, "user.home-dirty", x, sizeof(x));
+ if (n < 0) {
+ if (errno != ENODATA)
+ log_debug_errno(errno, "Unable to read dirty xattr off image file, ignoring: %m");
+
+ } else if (n == 1 && x[0] == '1')
+ return USER_TEST_DIRTY;
+
+ 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 ambiguous 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_password(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_test_recovery_key(UserRecord *h, UserRecord *secret) {
+ char **i;
+ int r;
+
+ assert(h);
+
+ /* Checks whether any of the specified passwords matches any of the hashed recovery keys of the entry */
+
+ if (h->n_recovery_key == 0)
+ return -ENXIO;
+
+ STRV_FOREACH(i, secret->password) {
+ for (size_t j = 0; j < h->n_recovery_key; j++) {
+ _cleanup_(erase_and_freep) char *mangled = NULL;
+ const char *p;
+
+ if (streq(h->recovery_key[j].type, "modhex64")) {
+ /* If this key is for a modhex64 recovery key, then try to normalize the
+ * passphrase to make things more robust: that way the password becomes case
+ * insensitive and the dashes become optional. */
+
+ r = normalize_recovery_key(*i, &mangled);
+ if (r == -EINVAL) /* Not a valid modhex64 passphrase, don't bother */
+ continue;
+ if (r < 0)
+ return r;
+
+ p = mangled;
+ } else
+ p = *i; /* Unknown recovery key types process as is */
+
+ r = test_password_one(h->recovery_key[j].hashed_password, p);
+ 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_(erase_and_freep) char *hashed = NULL;
+
+ r = hash_password(*i, &hashed);
+ if (r < 0)
+ return r;
+
+ r = strv_consume(&np, TAKE_PTR(hashed));
+ 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);
+ 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, 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;
+
+ json_variant_sensitive(w);
+
+ 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_token_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->token_pin, true);
+ if (r < 0)
+ return r;
+
+ strv_uniq(e);
+
+ if (strv_equal(h->token_pin, e))
+ return 0;
+
+ } else {
+ if (strv_equal(h->token_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("tokenPin"));
+ 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, "tokenPin", l);
+ }
+ if (r < 0)
+ return r;
+
+ json_variant_sensitive(w);
+
+ r = json_variant_set_field(&h->json, "secret", w);
+ if (r < 0)
+ return r;
+
+ strv_free_and_replace(h->token_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 {
+ json_variant_sensitive(w);
+
+ 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;
+}
+
+int user_record_set_fido2_user_presence_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("fido2UserPresencePermitted"));
+ else
+ r = json_variant_set_field_boolean(&w, "fido2UserPresencePermitted", 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->fido2_user_presence_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_token_pin(h, secret->token_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;
+ }
+
+ if (secret->fido2_user_presence_permitted >= 0) {
+ r = user_record_set_fido2_user_presence_permitted(
+ h,
+ secret->fido2_user_presence_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, start-time is after the current time? If so, the RTC most likely doesn't work. */
+ new_ratelimit_begin_usec = usec;
+ new_ratelimit_count = 1;
+ log_debug("Rate limit timestamp is in the future, assuming incorrect system clock, resetting limit.");
+ } 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
index 0000000..302e7a5
--- /dev/null
+++ b/src/home/user-record-util.h
@@ -0,0 +1,61 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#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_DIRTY, /* Only applies to user_record_test_image_path(), when the image exists but is marked dirty */
+ 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_password(UserRecord *h, UserRecord *secret);
+int user_record_test_recovery_key(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_token_pin(UserRecord *h, char **pin, bool prepend);
+int user_record_set_pkcs11_protected_authentication_path_permitted(UserRecord *h, int b);
+int user_record_set_fido2_user_presence_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);