summaryrefslogtreecommitdiffstats
path: root/src/home
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-10 20:49:52 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-10 20:49:52 +0000
commit55944e5e40b1be2afc4855d8d2baf4b73d1876b5 (patch)
tree33f869f55a1b149e9b7c2b7e201867ca5dd52992 /src/home
parentInitial commit. (diff)
downloadsystemd-55944e5e40b1be2afc4855d8d2baf4b73d1876b5.tar.xz
systemd-55944e5e40b1be2afc4855d8d2baf4b73d1876b5.zip
Adding upstream version 255.4.upstream/255.4
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/home')
-rw-r--r--src/home/home-util.c139
-rw-r--r--src/home/home-util.h37
-rw-r--r--src/home/homectl-fido2.c211
-rw-r--r--src/home/homectl-fido2.h7
-rw-r--r--src/home/homectl-pkcs11.c218
-rw-r--r--src/home/homectl-pkcs11.h11
-rw-r--r--src/home/homectl-recovery-key.c165
-rw-r--r--src/home/homectl-recovery-key.h6
-rw-r--r--src/home/homectl.c3875
-rw-r--r--src/home/homed-bus.c66
-rw-r--r--src/home/homed-bus.h10
-rw-r--r--src/home/homed-conf.c42
-rw-r--r--src/home/homed-conf.h12
-rw-r--r--src/home/homed-gperf.gperf22
-rw-r--r--src/home/homed-home-bus.c926
-rw-r--r--src/home/homed-home-bus.h34
-rw-r--r--src/home/homed-home.c3214
-rw-r--r--src/home/homed-home.h224
-rw-r--r--src/home/homed-manager-bus.c859
-rw-r--r--src/home/homed-manager-bus.h6
-rw-r--r--src/home/homed-manager.c2224
-rw-r--r--src/home/homed-manager.h93
-rw-r--r--src/home/homed-operation.c76
-rw-r--r--src/home/homed-operation.h63
-rw-r--r--src/home/homed-varlink.c359
-rw-r--r--src/home/homed-varlink.h8
-rw-r--r--src/home/homed.c51
-rw-r--r--src/home/homed.conf21
-rw-r--r--src/home/homework-cifs.c254
-rw-r--r--src/home/homework-cifs.h11
-rw-r--r--src/home/homework-directory.c313
-rw-r--r--src/home/homework-directory.h10
-rw-r--r--src/home/homework-fido2.c74
-rw-r--r--src/home/homework-fido2.h6
-rw-r--r--src/home/homework-fscrypt.c674
-rw-r--r--src/home/homework-fscrypt.h11
-rw-r--r--src/home/homework-luks.c3925
-rw-r--r--src/home/homework-luks.h49
-rw-r--r--src/home/homework-mount.c309
-rw-r--r--src/home/homework-mount.h10
-rw-r--r--src/home/homework-password-cache.c57
-rw-r--r--src/home/homework-password-cache.h28
-rw-r--r--src/home/homework-pkcs11.c102
-rw-r--r--src/home/homework-pkcs11.h21
-rw-r--r--src/home/homework-quota.c118
-rw-r--r--src/home/homework-quota.h8
-rw-r--r--src/home/homework.c1979
-rw-r--r--src/home/homework.h97
-rw-r--r--src/home/meson.build140
-rw-r--r--src/home/org.freedesktop.home1.conf201
-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.c1064
-rw-r--r--src/home/pam_systemd_home.sym12
-rw-r--r--src/home/user-record-password-quality.c87
-rw-r--r--src/home/user-record-password-quality.h7
-rw-r--r--src/home/user-record-sign.c161
-rw-r--r--src/home/user-record-sign.h19
-rw-r--r--src/home/user-record-util.c1512
-rw-r--r--src/home/user-record-util.h65
60 files changed, 24382 insertions, 0 deletions
diff --git a/src/home/home-util.c b/src/home/home-util.c
new file mode 100644
index 0000000..c777d7b
--- /dev/null
+++ b/src/home/home-util.c
@@ -0,0 +1,139 @@
+/* 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);
+}
+
+const char *home_record_dir(void) {
+ return secure_getenv("SYSTEMD_HOME_RECORD_DIR") ?: "/var/lib/systemd/home/";
+}
diff --git a/src/home/home-util.h b/src/home/home-util.h
new file mode 100644
index 0000000..36b301d
--- /dev/null
+++ b/src/home/home-util.h
@@ -0,0 +1,37 @@
+/* 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"
+
+/* Put some limits on disk sizes: not less than 5M, not more than 5T */
+#define USER_DISK_SIZE_MIN (UINT64_C(5)*1024*1024)
+#define USER_DISK_SIZE_MAX (UINT64_C(5)*1024*1024*1024*1024)
+
+/* The default disk size to use when nothing else is specified, relative to free disk space. We calculate
+ * this from the default rebalancing weights, so that what we create initially doesn't immediately require
+ * rebalancing. */
+#define USER_DISK_SIZE_DEFAULT_PERCENT ((unsigned) ((100 * REBALANCE_WEIGHT_DEFAULT) / (REBALANCE_WEIGHT_DEFAULT + REBALANCE_WEIGHT_BACKING)))
+
+/* This should be 83% right now, i.e. 100 of (100 + 20). Let's protect us against accidental changes. */
+assert_cc(USER_DISK_SIZE_DEFAULT_PERCENT == 83U);
+
+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)
+
+const char *home_record_dir(void);
diff --git a/src/home/homectl-fido2.c b/src/home/homectl-fido2.c
new file mode 100644
index 0000000..3cbdf91
--- /dev/null
+++ b/src/home/homectl-fido2.c
@@ -0,0 +1,211 @@
+/* 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 "libfido2-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;
+ ssize_t escaped_size;
+ int r;
+
+ assert(v);
+ assert(cid);
+
+ escaped_size = base64mem(cid, cid_size, &escaped);
+ if (escaped_size < 0)
+ return log_error_errno(escaped_size, "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,
+ Fido2EnrollFlags lock_with) {
+
+ _cleanup_(json_variant_unrefp) JsonVariant *l = NULL, *w = NULL, *e = NULL;
+ _cleanup_(erase_and_freep) char *base64_encoded = NULL, *hashed = NULL;
+ ssize_t base64_encoded_size;
+ 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 */
+ base64_encoded_size = base64mem(secret, secret_size, &base64_encoded);
+ if (base64_encoded_size < 0)
+ return log_error_errno(base64_encoded_size, "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)),
+ JSON_BUILD_PAIR("up", JSON_BUILD_BOOLEAN(FLAGS_SET(lock_with, FIDO2ENROLL_UP))),
+ JSON_BUILD_PAIR("uv", JSON_BUILD_BOOLEAN(FLAGS_SET(lock_with, FIDO2ENROLL_UV))),
+ JSON_BUILD_PAIR("clientPin", JSON_BUILD_BOOLEAN(FLAGS_SET(lock_with, FIDO2ENROLL_PIN)))));
+
+ 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
+
+int identity_add_fido2_parameters(
+ JsonVariant **v,
+ const char *device,
+ Fido2EnrollFlags lock_with,
+ int cred_alg) {
+
+#if HAVE_LIBFIDO2
+ JsonVariant *un, *realm, *rn;
+ _cleanup_(erase_and_freep) void *secret = NULL, *salt = NULL;
+ _cleanup_(erase_and_freep) char *used_pin = NULL;
+ size_t cid_size, salt_size, secret_size;
+ _cleanup_free_ void *cid = NULL;
+ const char *fido_un;
+ int r;
+
+ assert(v);
+ assert(device);
+
+ 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 = fido2_generate_hmac_hash(
+ device,
+ /* rp_id= */ "io.systemd.home",
+ /* rp_name= */ "Home Directory",
+ /* user_id= */ fido_un, strlen(fido_un), /* We pass the user ID and name as the same */
+ /* user_name= */ fido_un,
+ /* user_display_name= */ rn ? json_variant_string(rn) : NULL,
+ /* user_icon_name= */ NULL,
+ /* askpw_icon_name= */ "user-home",
+ lock_with,
+ cred_alg,
+ &cid, &cid_size,
+ &salt, &salt_size,
+ &secret, &secret_size,
+ &used_pin,
+ &lock_with);
+ if (r < 0)
+ return r;
+
+ r = add_fido2_credential_id(
+ v,
+ cid,
+ cid_size);
+ if (r < 0)
+ return r;
+
+ r = add_fido2_salt(
+ v,
+ cid,
+ cid_size,
+ salt,
+ salt_size,
+ secret,
+ secret_size,
+ lock_with);
+ 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
+}
diff --git a/src/home/homectl-fido2.h b/src/home/homectl-fido2.h
new file mode 100644
index 0000000..558c674
--- /dev/null
+++ b/src/home/homectl-fido2.h
@@ -0,0 +1,7 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "json.h"
+#include "libfido2-util.h"
+
+int identity_add_fido2_parameters(JsonVariant **v, const char *device, Fido2EnrollFlags lock_with, int cred_alg);
diff --git a/src/home/homectl-pkcs11.c b/src/home/homectl-pkcs11.c
new file mode 100644
index 0000000..2539af0
--- /dev/null
+++ b/src/home/homectl-pkcs11.c
@@ -0,0 +1,218 @@
+/* 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"
+
+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;
+ ssize_t base64_encoded_size;
+ 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 */
+ base64_encoded_size = base64mem(decrypted_key, decrypted_key_size, &base64_encoded);
+ if (base64_encoded_size < 0)
+ return log_error_errno(base64_encoded_size, "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_erase_ 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_contains(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;
+}
+
+static int acquire_pkcs11_certificate(
+ const char *uri,
+ const char *askpw_friendly_name,
+ const char *askpw_icon_name,
+ X509 **ret_cert,
+ char **ret_pin_used) {
+#if HAVE_P11KIT
+ return pkcs11_acquire_certificate(uri, askpw_friendly_name, askpw_icon_name, ret_cert, ret_pin_used);
+#else
+ return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
+ "PKCS#11 tokens not supported on this build.");
+#endif
+}
+
+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;
+ int r;
+
+ assert(v);
+
+ r = acquire_pkcs11_certificate(uri, "home directory operation", "user-home", &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.");
+
+ r = rsa_pkey_to_suitable_key_size(pkey, &decrypted_key_size);
+ if (r < 0)
+ return log_error_errno(r, "Failed to extract RSA key size from X509 certificate.");
+
+ log_debug("Generating %zu bytes random key.", decrypted_key_size);
+
+ decrypted_key = malloc(decrypted_key_size);
+ if (!decrypted_key)
+ return log_oom();
+
+ r = crypto_random_bytes(decrypted_key, decrypted_key_size);
+ if (r < 0)
+ return log_error_errno(r, "Failed to generate random key: %m");
+
+ r = rsa_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;
+}
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..bf18ae4
--- /dev/null
+++ b/src/home/homectl-recovery-key.c
@@ -0,0 +1,165 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "errno-util.h"
+#include "glyph-util.h"
+#include "homectl-recovery-key.h"
+#include "libcrypt-util.h"
+#include "memory-util.h"
+#include "qrcode-util.h"
+#include "random-util.h"
+#include "recovery-key.h"
+#include "strv.h"
+#include "terminal-util.h"
+
+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_CONST_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_erase_ 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 log_error_errno(r, "Failed to generate recovery key: %m");
+
+ /* 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..a6951c8
--- /dev/null
+++ b/src/home/homectl.c
@@ -0,0 +1,3875 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <getopt.h>
+
+#include "sd-bus.h"
+
+#include "ask-password-api.h"
+#include "build.h"
+#include "bus-common-errors.h"
+#include "bus-error.h"
+#include "bus-locator.h"
+#include "cap-list.h"
+#include "capability-util.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 "fs-util.h"
+#include "glyph-util.h"
+#include "home-util.h"
+#include "homectl-fido2.h"
+#include "homectl-pkcs11.h"
+#include "homectl-recovery-key.h"
+#include "libfido2-util.h"
+#include "locale-util.h"
+#include "main-func.h"
+#include "memory-util.h"
+#include "pager.h"
+#include "parse-argument.h"
+#include "parse-util.h"
+#include "password-quality-util.h"
+#include "path-util.h"
+#include "percent-util.h"
+#include "pkcs11-util.h"
+#include "pretty-print.h"
+#include "process-util.h"
+#include "rlimit-util.h"
+#include "spawn-polkit-agent.h"
+#include "terminal-util.h"
+#include "uid-alloc-range.h"
+#include "user-record.h"
+#include "user-record-password-quality.h"
+#include "user-record-show.h"
+#include "user-record-util.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 Fido2EnrollFlags arg_fido2_lock_with = FIDO2ENROLL_PIN | FIDO2ENROLL_UP;
+#if HAVE_LIBFIDO2
+static int arg_fido2_cred_alg = COSE_ES256;
+#else
+static int arg_fido2_cred_alg = 0;
+#endif
+static bool arg_recovery_key = false;
+static JsonFormatFlags arg_json_format_flags = JSON_FORMAT_OFF;
+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 uint64_t arg_capability_bounding_set = UINT64_MAX;
+static uint64_t arg_capability_ambient_set = UINT64_MAX;
+
+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, RUNTIME_SCOPE_SYSTEM, bus);
+ if (r < 0)
+ return bus_log_connect_error(r, arg_transport);
+
+ (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;
+
+ 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 || !FLAGS_SET(arg_json_format_flags, JSON_FORMAT_OFF)) {
+ r = table_set_sort(table, (size_t) 0);
+ if (r < 0)
+ return table_log_sort_error(r);
+
+ r = table_print_with_pager(table, arg_json_format_flags, arg_pager_flags, arg_legend);
+ if (r < 0)
+ return r;
+ }
+
+ if (arg_legend && (arg_json_format_flags & JSON_FORMAT_OFF)) {
+ 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,
+ AskPasswordFlags flags) {
+
+ _cleanup_strv_free_erase_ char **password = NULL;
+ _cleanup_(erase_and_freep) char *envpw = NULL;
+ _cleanup_free_ char *question = NULL;
+ int r;
+
+ assert(user_name);
+ assert(hr);
+
+ r = getenv_steal_erase("PASSWORD", &envpw);
+ if (r < 0)
+ return log_error_errno(r, "Failed to acquire password from environment: %m");
+ if (r > 0) {
+ /* 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(envpw), true);
+ if (r < 0)
+ return log_error_errno(r, "Failed to store password: %m");
+
+ return 1;
+ }
+
+ /* If this is not our own user, then don't use the password cache */
+ if (is_this_me(user_name) <= 0)
+ SET_FLAG(flags, ASK_PASSWORD_ACCEPT_CACHED|ASK_PASSWORD_PUSH_CACHE, false);
+
+ 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,
+ /* icon= */ "user-home",
+ NULL,
+ /* key_name= */ "home-password",
+ /* credential_name= */ "home.password",
+ USEC_INFINITY,
+ flags,
+ &password);
+ if (r == -EUNATCH) { /* EUNATCH is returned if no password was found and asking interactively was
+ * disabled via the flags. Not an error for us. */
+ log_debug_errno(r, "No passwords acquired.");
+ return 0;
+ }
+ 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 1;
+}
+
+static int acquire_recovery_key(
+ const char *user_name,
+ UserRecord *hr,
+ AskPasswordFlags flags) {
+
+ _cleanup_strv_free_erase_ char **recovery_key = NULL;
+ _cleanup_(erase_and_freep) char *envpw = NULL;
+ _cleanup_free_ char *question = NULL;
+ int r;
+
+ assert(user_name);
+ assert(hr);
+
+ r = getenv_steal_erase("PASSWORD", &envpw);
+ if (r < 0)
+ return log_error_errno(r, "Failed to acquire password from environment: %m");
+ if (r > 0) {
+ /* People really shouldn't use environment variables for passing secrets. 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(envpw), true); /* recovery keys are stored in the record exactly like regular passwords! */
+ if (r < 0)
+ return log_error_errno(r, "Failed to store recovery key: %m");
+
+ return 1;
+ }
+
+ /* If this is not our own user, then don't use the password cache */
+ if (is_this_me(user_name) <= 0)
+ SET_FLAG(flags, ASK_PASSWORD_ACCEPT_CACHED|ASK_PASSWORD_PUSH_CACHE, false);
+
+ if (asprintf(&question, "Please enter recovery key for user %s:", user_name) < 0)
+ return log_oom();
+
+ r = ask_password_auto(question,
+ /* icon= */ "user-home",
+ NULL,
+ /* key_name= */ "home-recovery-key",
+ /* credential_name= */ "home.recovery-key",
+ USEC_INFINITY,
+ flags,
+ &recovery_key);
+ if (r == -EUNATCH) { /* EUNATCH is returned if no recovery key was found and asking interactively was
+ * disabled via the flags. Not an error for us. */
+ log_debug_errno(r, "No recovery keys acquired.");
+ return 0;
+ }
+ if (r < 0)
+ return log_error_errno(r, "Failed to acquire recovery keys: %m");
+
+ r = user_record_set_password(hr, recovery_key, true);
+ if (r < 0)
+ return log_error_errno(r, "Failed to store recovery keys: %m");
+
+ return 1;
+}
+
+static int acquire_token_pin(
+ const char *user_name,
+ UserRecord *hr,
+ AskPasswordFlags flags) {
+
+ _cleanup_strv_free_erase_ char **pin = NULL;
+ _cleanup_(erase_and_freep) char *envpin = NULL;
+ _cleanup_free_ char *question = NULL;
+ int r;
+
+ assert(user_name);
+ assert(hr);
+
+ r = getenv_steal_erase("PIN", &envpin);
+ if (r < 0)
+ return log_error_errno(r, "Failed to acquire PIN from environment: %m");
+ if (r > 0) {
+ r = user_record_set_token_pin(hr, STRV_MAKE(envpin), false);
+ if (r < 0)
+ return log_error_errno(r, "Failed to store token PIN: %m");
+
+ return 1;
+ }
+
+ /* If this is not our own user, then don't use the password cache */
+ if (is_this_me(user_name) <= 0)
+ SET_FLAG(flags, ASK_PASSWORD_ACCEPT_CACHED|ASK_PASSWORD_PUSH_CACHE, false);
+
+ if (asprintf(&question, "Please enter security token PIN for user %s:", user_name) < 0)
+ return log_oom();
+
+ r = ask_password_auto(
+ question,
+ /* icon= */ "user-home",
+ NULL,
+ /* key_name= */ "token-pin",
+ /* credential_name= */ "home.token-pin",
+ USEC_INFINITY,
+ flags,
+ &pin);
+ if (r == -EUNATCH) { /* EUNATCH is returned if no PIN was found and asking interactively was disabled
+ * via the flags. Not an error for us. */
+ log_debug_errno(r, "No security token PINs acquired.");
+ return 0;
+ }
+ 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 1;
+}
+
+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 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.");
+
+ /* Don't consume cache entries or credentials here, we already tried that unsuccessfully. But
+ * let's push what we acquire here into the cache */
+ r = acquire_existing_password(
+ user_name,
+ hr,
+ emphasize_current_password,
+ ASK_PASSWORD_PUSH_CACHE | ASK_PASSWORD_NO_CREDENTIAL);
+ if (r < 0)
+ return r;
+
+ } else if (sd_bus_error_has_name(error, BUS_ERROR_BAD_RECOVERY_KEY)) {
+
+ if (!strv_isempty(hr->password))
+ log_notice("Recovery key incorrect or not sufficient, please try again.");
+
+ /* Don't consume cache entries or credentials here, we already tried that unsuccessfully. But
+ * let's push what we acquire here into the cache */
+ r = acquire_recovery_key(
+ user_name,
+ hr,
+ ASK_PASSWORD_PUSH_CACHE | ASK_PASSWORD_NO_CREDENTIAL);
+ 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,
+ ASK_PASSWORD_PUSH_CACHE | ASK_PASSWORD_NO_CREDENTIAL);
+ if (r < 0)
+ return r;
+
+ } else if (sd_bus_error_has_name(error, BUS_ERROR_TOKEN_PIN_NEEDED)) {
+
+ /* First time the PIN is requested, let's accept cached data, and allow using credential store */
+ r = acquire_token_pin(
+ user_name,
+ hr,
+ ASK_PASSWORD_ACCEPT_CACHED | ASK_PASSWORD_PUSH_CACHE);
+ 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%sPlease confirm presence 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_USER_VERIFICATION_NEEDED)) {
+
+ log_notice("%s%sPlease verify user on security token.",
+ emoji_enabled() ? special_glyph(SPECIAL_GLYPH_TOUCH) : "",
+ emoji_enabled() ? " " : "");
+
+ r = user_record_set_fido2_user_verification_permitted(hr, true);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set FIDO2 user verification 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.");
+
+ /* If the previous PIN was wrong don't accept cached info anymore, but add to cache. Also, don't use the credential data */
+ r = acquire_token_pin(
+ user_name,
+ hr,
+ ASK_PASSWORD_PUSH_CACHE | ASK_PASSWORD_NO_CREDENTIAL);
+ 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,
+ ASK_PASSWORD_PUSH_CACHE | ASK_PASSWORD_NO_CREDENTIAL);
+ 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,
+ ASK_PASSWORD_PUSH_CACHE | ASK_PASSWORD_NO_CREDENTIAL);
+ 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 acquire_passed_secrets(const char *user_name, UserRecord **ret) {
+ _cleanup_(user_record_unrefp) UserRecord *secret = NULL;
+ int r;
+
+ assert(ret);
+
+ /* Generates an initial secret objects that contains passwords supplied via $PASSWORD, the password
+ * cache or the credentials subsystem, but excluding any interactive stuff. If nothing is passed,
+ * returns an empty secret object. */
+
+ secret = user_record_new();
+ if (!secret)
+ return log_oom();
+
+ r = acquire_existing_password(
+ user_name,
+ secret,
+ /* emphasize_current_password = */ false,
+ ASK_PASSWORD_ACCEPT_CACHED | ASK_PASSWORD_NO_TTY | ASK_PASSWORD_NO_AGENT);
+ if (r < 0)
+ return r;
+
+ r = acquire_token_pin(
+ user_name,
+ secret,
+ ASK_PASSWORD_ACCEPT_CACHED | ASK_PASSWORD_NO_TTY | ASK_PASSWORD_NO_AGENT);
+ if (r < 0)
+ return r;
+
+ r = acquire_recovery_key(
+ user_name,
+ secret,
+ ASK_PASSWORD_ACCEPT_CACHED | ASK_PASSWORD_NO_TTY | ASK_PASSWORD_NO_AGENT);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(secret);
+ 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;
+
+ r = acquire_bus(&bus);
+ if (r < 0)
+ return r;
+
+ STRV_FOREACH(i, strv_skip(argv, 1)) {
+ _cleanup_(user_record_unrefp) UserRecord *secret = NULL;
+
+ r = acquire_passed_secrets(*i, &secret);
+ 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, "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, /* emphasize_current_password= */ 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;
+
+ 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_format_flags & JSON_FORMAT_OFF)
+ user_record_show(hr, true);
+ else {
+ _cleanup_(user_record_unrefp) UserRecord *stripped = NULL;
+
+ if (arg_export_format == EXPORT_FORMAT_STRIPPED)
+ r = user_record_clone(hr, USER_RECORD_EXTRACT_EMBEDDED|USER_RECORD_PERMISSIVE, &stripped);
+ else if (arg_export_format == EXPORT_FORMAT_MINIMAL)
+ r = user_record_clone(hr, USER_RECORD_EXTRACT_SIGNABLE|USER_RECORD_PERMISSIVE, &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);
+ }
+}
+
+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_free_ char **mangled_list = NULL;
+ int r, ret = 0;
+ char **items;
+
+ 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|USER_RECORD_PERMISSIVE);
+ 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_free_ char **mangled_list = NULL;
+ int r, ret = 0;
+ char **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;
+
+ r = acquire_passed_secrets(*i, &secret);
+ 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, "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) {
+ uint64_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) {
+ uint64_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_object(&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;
+ 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));
+ 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_object(&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_object(&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_object(&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_object(&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;
+ 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, arg_fido2_lock_with, arg_fido2_cred_alg);
+ 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|USER_RECORD_PERMISSIVE);
+ 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) {
+
+ _cleanup_(erase_and_freep) char *envpw = NULL;
+ unsigned i = 5;
+ int r;
+
+ assert(user_name);
+ assert(hr);
+
+ r = getenv_steal_erase("NEWPASSWORD", &envpw);
+ if (r < 0)
+ return log_error_errno(r, "Failed to acquire password from environment: %m");
+ if (r > 0) {
+ /* As above, this is not for use, just for testing */
+
+ r = user_record_set_password(hr, STRV_MAKE(envpw), /* prepend = */ true);
+ if (r < 0)
+ return log_error_errno(r, "Failed to store password: %m");
+
+ if (ret)
+ *ret = TAKE_PTR(envpw);
+
+ return 0;
+ }
+
+ if (suggest)
+ (void) suggest_passwords();
+
+ for (;;) {
+ _cleanup_strv_free_erase_ 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,
+ /* icon= */ "user-home",
+ NULL,
+ /* key_name= */ "home-password",
+ /* credential_name= */ "home.new-password",
+ USEC_INFINITY,
+ 0, /* no caching, we want to collect a new password here after all */
+ &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,
+ /* icon= */ "user-home",
+ NULL,
+ /* key_name= */ "home-password",
+ /* credential_name= */ "home.new-password",
+ USEC_INFINITY,
+ 0, /* no caching */
+ &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,
+ ASK_PASSWORD_ACCEPT_CACHED | ASK_PASSWORD_PUSH_CACHE);
+ 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_check_password_quality(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;
+
+ 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;
+ 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, arg_fido2_lock_with, arg_fido2_cred_alg);
+ 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|USER_RECORD_PERMISSIVE);
+ 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");
+
+ r = user_record_set_fido2_user_verification_permitted(hr, -1);
+ if (r < 0)
+ return log_error_errno(r, "Failed to reset FIDO2 user verification 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, *secret = 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;
+
+ /* Add in all secrets we can acquire cheaply */
+ r = acquire_passed_secrets(username, &secret);
+ if (r < 0)
+ return r;
+
+ r = user_record_merge_secret(hr, secret);
+ 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=%s'.",
+ special_glyph(SPECIAL_GLYPH_ELLIPSIS));
+ if (arg_fido2_device)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "To change the FIDO2 security token use 'homectl update --fido2-device=%s'.",
+ special_glyph(SPECIAL_GLYPH_ELLIPSIS));
+ 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);
+
+ r = acquire_passed_secrets(username, &old_secret);
+ if (r < 0)
+ return r;
+
+ 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 parse_disk_size(const char *t, uint64_t *ret) {
+ int r;
+
+ assert(t);
+ assert(ret);
+
+ if (streq(t, "min"))
+ *ret = 0;
+ else if (streq(t, "max"))
+ *ret = UINT64_MAX-1; /* Largest size that isn't UINT64_MAX special marker */
+ else {
+ uint64_t ds;
+
+ r = parse_size(t, 1024, &ds);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse disk size parameter: %s", t);
+
+ if (ds >= UINT64_MAX) /* UINT64_MAX has special meaning for us ("dont change"), refuse */
+ return log_error_errno(SYNTHETIC_ERRNO(ERANGE), "Disk size out of range: %s", t);
+
+ *ret = ds;
+ }
+
+ 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_permyriad(argv[2]) >= 0))
+ return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
+ "Relative disk size specification currently not supported when resizing.");
+
+ if (argc > 2) {
+ r = parse_disk_size(argv[2], &ds);
+ if (r < 0)
+ return r;
+ }
+
+ 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;
+ }
+
+ r = acquire_passed_secrets(argv[1], &secret);
+ 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, "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;
+
+ 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;
+
+ r = acquire_bus(&bus);
+ if (r < 0)
+ return r;
+
+ STRV_FOREACH(i, strv_skip(argv, 1)) {
+ _cleanup_(user_record_unrefp) UserRecord *secret = NULL;
+
+ r = acquire_passed_secrets(*i, &secret);
+ 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, "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 = -EBADF;
+ _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();
+
+ r = acquire_passed_secrets(argv[1], &secret);
+ if (r < 0)
+ return r;
+
+ 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_SIGTERM|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 rebalance(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, "Rebalance");
+ 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_REBALANCE_NOT_NEEDED))
+ log_info("No homes needed rebalancing.");
+ else
+ return log_error_errno(r, "Failed to rebalance: %s", bus_error_message(&error, r));
+ } else
+ log_info("Completed rebalancing.");
+
+ 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;
+
+ 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"
+ " rebalance Rebalance free space between 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"
+ " --capability-bounding-set=CAPS\n"
+ " Bounding POSIX capability set\n"
+ " --capability-ambient-set=CAPS\n"
+ " Ambient POSIX capability set\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"
+ " --fido2-with-client-pin=BOOL\n"
+ " Whether to require entering a PIN to unlock the\n"
+ " account\n"
+ " --fido2-with-user-presence=BOOL\n"
+ " Whether to require user presence to unlock the\n"
+ " account\n"
+ " --fido2-with-user-verification=BOOL\n"
+ " Whether to require user verification to unlock\n"
+ " the account\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 Same as --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"
+ " --drop-caches=BOOL Whether to automatically drop caches on logout\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"
+ " --luks-sector-size=BYTES\n"
+ " Sector size for LUKS encryption in bytes\n"
+ " --luks-extra-mount-options=OPTIONS\n"
+ " LUKS extra mount options\n"
+ " --auto-resize-mode=MODE Automatically grow/shrink home on login/logout\n"
+ " --rebalance-weight=WEIGHT Weight while rebalancing\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"
+ " --cifs-extra-mount-options=OPTIONS\n"
+ " CIFS (Windows) extra mount options\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_CIFS_EXTRA_MOUNT_OPTIONS,
+ 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_FORCE_ITERATIONS,
+ ARG_LUKS_PBKDF_TIME_COST,
+ ARG_LUKS_PBKDF_MEMORY_COST,
+ ARG_LUKS_PBKDF_PARALLEL_THREADS,
+ ARG_LUKS_SECTOR_SIZE,
+ 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_FIDO2_WITH_PIN,
+ ARG_FIDO2_WITH_UP,
+ ARG_FIDO2_WITH_UV,
+ ARG_RECOVERY_KEY,
+ ARG_AND_RESIZE,
+ ARG_AND_CHANGE_PASSWORD,
+ ARG_DROP_CACHES,
+ ARG_LUKS_EXTRA_MOUNT_OPTIONS,
+ ARG_AUTO_RESIZE_MODE,
+ ARG_REBALANCE_WEIGHT,
+ ARG_FIDO2_CRED_ALG,
+ ARG_CAPABILITY_BOUNDING_SET,
+ ARG_CAPABILITY_AMBIENT_SET,
+ };
+
+ 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-force-iterations", required_argument, NULL, ARG_LUKS_PBKDF_FORCE_ITERATIONS },
+ { "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 },
+ { "luks-sector-size", required_argument, NULL, ARG_LUKS_SECTOR_SIZE },
+ { "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 },
+ { "cifs-extra-mount-options", required_argument, NULL, ARG_CIFS_EXTRA_MOUNT_OPTIONS },
+ { "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-credential-algorithm", required_argument, NULL, ARG_FIDO2_CRED_ALG },
+ { "fido2-device", required_argument, NULL, ARG_FIDO2_DEVICE },
+ { "fido2-with-client-pin", required_argument, NULL, ARG_FIDO2_WITH_PIN },
+ { "fido2-with-user-presence", required_argument, NULL, ARG_FIDO2_WITH_UP },
+ { "fido2-with-user-verification",required_argument, NULL, ARG_FIDO2_WITH_UV },
+ { "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 },
+ { "drop-caches", required_argument, NULL, ARG_DROP_CACHES },
+ { "luks-extra-mount-options", required_argument, NULL, ARG_LUKS_EXTRA_MOUNT_OPTIONS },
+ { "auto-resize-mode", required_argument, NULL, ARG_AUTO_RESIZE_MODE },
+ { "rebalance-weight", required_argument, NULL, ARG_REBALANCE_WEIGHT },
+ { "capability-bounding-set", required_argument, NULL, ARG_CAPABILITY_BOUNDING_SET },
+ { "capability-ambient-set", required_argument, NULL, ARG_CAPABILITY_AMBIENT_SET },
+ {}
+ };
+
+ 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(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_EXTRA_MOUNT_OPTIONS:
+ case ARG_LUKS_EXTRA_MOUNT_OPTIONS: {
+
+ 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_EXTRA_MOUNT_OPTIONS ? "cifsExtraMountOptions" :
+ c == ARG_LUKS_EXTRA_MOUNT_OPTIONS ? "luksExtraMountOptions" :
+ 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_CIFS_SERVICE:
+ if (isempty(optarg)) {
+ r = drop_from_identity("cifsService");
+ if (r < 0)
+ return r;
+
+ break;
+ }
+
+ r = parse_cifs_service(optarg, NULL, NULL, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to validate CIFS service name: %s", optarg);
+
+ r = json_variant_set_field_string(&arg_identity_extra, "cifsService", optarg);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set cifsService field: %m");
+
+ 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 *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(l, "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");
+
+ t = strjoin("RLIMIT_", rlimit_to_string(l));
+ if (!t)
+ return log_oom();
+
+ r = json_variant_set_fieldb(
+ &arg_identity_extra_rlimits, t,
+ 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 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(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;
+ _cleanup_(json_variant_unrefp) JsonVariant *ne = NULL;
+ JsonVariant *e;
+
+ if (isempty(optarg)) {
+ r = drop_from_identity("environment");
+ if (r < 0)
+ return r;
+
+ break;
+ }
+
+ 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");
+ }
+
+ r = strv_env_replace_strdup_passthrough(&l, optarg);
+ if (r < 0)
+ return log_error_errno(r, "Cannot assign environment variable %s: %m", optarg);
+
+ strv_sort(l);
+
+ r = json_variant_new_array_strv(&ne, l);
+ 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)) {
+ FOREACH_STRING(prop, "diskSize", "diskSizeRelative", "rebalanceWeight") {
+ r = drop_from_identity(prop);
+ if (r < 0)
+ return r;
+ }
+
+ arg_disk_size = arg_disk_size_relative = UINT64_MAX;
+ break;
+ }
+
+ r = parse_permyriad(optarg);
+ if (r < 0) {
+ r = parse_disk_size(optarg, &arg_disk_size);
+ if (r < 0)
+ return r;
+
+ 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 = UINT32_SCALE_FROM_PERMYRIAD(r);
+
+ 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;
+ }
+
+ /* Automatically turn off the rebalance logic if user configured a size explicitly */
+ r = json_variant_set_field_unsigned(&arg_identity_extra_this_machine, "rebalanceWeight", REBALANCE_WEIGHT_OFF);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set rebalanceWeight field: %m");
+
+ 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_FORCE_ITERATIONS:
+ 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_FORCE_ITERATIONS ? "luksPbkdfForceIterations" :
+ 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_LUKS_SECTOR_SIZE: {
+ uint64_t ss;
+
+ if (isempty(optarg)) {
+ r = drop_from_identity("luksSectorSize");
+ if (r < 0)
+ return r;
+
+ break;
+ }
+
+ r = parse_sector_size(optarg, &ss);
+ if (r < 0)
+ return r;
+
+ r = json_variant_set_field_unsigned(&arg_identity_extra, "luksSectorSize", ss);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set sector size field: %m");
+
+ 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_free_ 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:
+ if (streq(optarg, "list"))
+ return pkcs11_list_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 = pkcs11_find_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_CRED_ALG:
+ r = parse_fido2_algorithm(optarg, &arg_fido2_cred_alg);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse COSE algorithm: %s", optarg);
+ break;
+
+ case ARG_FIDO2_DEVICE:
+ if (streq(optarg, "list"))
+ return fido2_list_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 = fido2_find_device_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_FIDO2_WITH_PIN:
+ r = parse_boolean_argument("--fido2-with-client-pin=", optarg, NULL);
+ if (r < 0)
+ return r;
+
+ SET_FLAG(arg_fido2_lock_with, FIDO2ENROLL_PIN, r);
+ break;
+
+ case ARG_FIDO2_WITH_UP:
+ r = parse_boolean_argument("--fido2-with-user-presence=", optarg, NULL);
+ if (r < 0)
+ return r;
+
+ SET_FLAG(arg_fido2_lock_with, FIDO2ENROLL_UP, r);
+ break;
+
+ case ARG_FIDO2_WITH_UV:
+ r = parse_boolean_argument("--fido2-with-user-verification=", optarg, NULL);
+ if (r < 0)
+ return r;
+
+ SET_FLAG(arg_fido2_lock_with, FIDO2ENROLL_UV, r);
+ break;
+
+ case ARG_RECOVERY_KEY:
+ 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 ARG_AUTO_RESIZE_MODE:
+ if (isempty(optarg)) {
+ r = drop_from_identity("autoResizeMode");
+ if (r < 0)
+ return r;
+
+ break;
+ }
+
+ r = auto_resize_mode_from_string(optarg);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse --auto-resize-mode= argument: %s", optarg);
+
+ r = json_variant_set_field_string(&arg_identity_extra, "autoResizeMode", auto_resize_mode_to_string(r));
+ if (r < 0)
+ return log_error_errno(r, "Failed to set autoResizeMode field: %m");
+
+ break;
+
+ case ARG_REBALANCE_WEIGHT: {
+ uint64_t u;
+
+ if (isempty(optarg)) {
+ r = drop_from_identity("rebalanceWeight");
+ if (r < 0)
+ return r;
+ break;
+ }
+
+ if (streq(optarg, "off"))
+ u = REBALANCE_WEIGHT_OFF;
+ else {
+ r = safe_atou64(optarg, &u);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse --rebalance-weight= argument: %s", optarg);
+
+ if (u < REBALANCE_WEIGHT_MIN || u > REBALANCE_WEIGHT_MAX)
+ return log_error_errno(SYNTHETIC_ERRNO(ERANGE), "Rebalancing weight out of valid range %" PRIu64 "%s%" PRIu64 ": %s",
+ REBALANCE_WEIGHT_MIN, special_glyph(SPECIAL_GLYPH_ELLIPSIS), REBALANCE_WEIGHT_MAX, optarg);
+ }
+
+ /* Drop from per machine stuff and everywhere */
+ r = drop_from_identity("rebalanceWeight");
+ if (r < 0)
+ return r;
+
+ /* Add to main identity */
+ r = json_variant_set_field_unsigned(&arg_identity_extra, "rebalanceWeight", u);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set rebalanceWeight field: %m");
+
+ break;
+ }
+
+ case 'j':
+ arg_json_format_flags = JSON_FORMAT_PRETTY_AUTO|JSON_FORMAT_COLOR_AUTO;
+ break;
+
+ case ARG_JSON:
+ r = parse_json_argument(optarg, &arg_json_format_flags);
+ if (r <= 0)
+ return r;
+
+ 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_format_flags &= ~JSON_FORMAT_OFF;
+ 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 ARG_DROP_CACHES: {
+ if (isempty(optarg)) {
+ r = drop_from_identity("dropCaches");
+ if (r < 0)
+ return r;
+ break;
+ }
+
+ r = parse_boolean_argument("--drop-caches=", optarg, NULL);
+ if (r < 0)
+ return r;
+
+ r = json_variant_set_field_boolean(&arg_identity_extra, "dropCaches", r);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set drop caches field: %m");
+
+ break;
+ }
+
+ case ARG_CAPABILITY_AMBIENT_SET:
+ case ARG_CAPABILITY_BOUNDING_SET: {
+ _cleanup_strv_free_ char **l = NULL;
+ bool subtract = false;
+ uint64_t parsed, *which, updated;
+ const char *p, *field;
+
+ if (c == ARG_CAPABILITY_AMBIENT_SET) {
+ which = &arg_capability_ambient_set;
+ field = "capabilityAmbientSet";
+ } else {
+ assert(c == ARG_CAPABILITY_BOUNDING_SET);
+ which = &arg_capability_bounding_set;
+ field = "capabilityBoundingSet";
+ }
+
+ if (isempty(optarg)) {
+ r = drop_from_identity(field);
+ if (r < 0)
+ return r;
+
+ *which = UINT64_MAX;
+ break;
+ }
+
+ p = optarg;
+ if (*p == '~') {
+ subtract = true;
+ p++;
+ }
+
+ r = capability_set_from_string(p, &parsed);
+ if (r == 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid capabilities in capability string '%s'.", p);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse capability string '%s': %m", p);
+
+ if (*which == UINT64_MAX)
+ updated = subtract ? all_capabilities() & ~parsed : parsed;
+ else if (subtract)
+ updated = *which & ~parsed;
+ else
+ updated = *which | parsed;
+
+ if (capability_set_to_strv(updated, &l) < 0)
+ return log_oom();
+
+ r = json_variant_set_field_strv(&arg_identity_extra, field, l);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set %s field: %m", field);
+
+ *which = updated;
+ break;
+ }
+
+ case '?':
+ return -EINVAL;
+
+ default:
+ assert_not_reached();
+ }
+ }
+
+ 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 },
+ { "rebalance", VERB_ANY, 1, 0, rebalance },
+ {}
+ };
+
+ int r;
+
+ log_setup();
+
+ 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_WITH_POSITIVE_FAILURE(run);
diff --git a/src/home/homed-bus.c b/src/home/homed-bus.c
new file mode 100644
index 0000000..24b421a
--- /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|USER_RECORD_PERMISSIVE);
+ 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_set(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..ffa4bb3
--- /dev/null
+++ b/src/home/homed-conf.c
@@ -0,0 +1,42 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "conf-parser.h"
+#include "constants.h"
+#include "home-util.h"
+#include "homed-conf.h"
+
+int manager_parse_config_file(Manager *m) {
+
+ assert(m);
+
+ return config_parse_config_file("homed.conf", "Home\0",
+ config_item_perf_lookup, homed_gperf_lookup,
+ CONFIG_PARSE_WARN, m);
+}
+
+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 = ASSERT_PTR(data);
+
+ assert(rvalue);
+
+ 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..39aca35
--- /dev/null
+++ b/src/home/homed-gperf.gperf
@@ -0,0 +1,22 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+%{
+#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..a47f4d8
--- /dev/null
+++ b/src/home/homed-home-bus.c
@@ -0,0 +1,926 @@
+/* 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 = ASSERT_PTR(userdata);
+
+ assert(bus);
+ assert(reply);
+
+ 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 = ASSERT_PTR(userdata);
+
+ assert(bus);
+ assert(reply);
+
+ 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|USER_RECORD_PERMISSIVE;
+ 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 = ASSERT_PTR(userdata);
+ bool incomplete;
+ int r;
+
+ assert(bus);
+ assert(reply);
+
+ 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 = ASSERT_PTR(userdata);
+ int r;
+
+ assert(message);
+
+ 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 = ASSERT_PTR(userdata);
+ int r;
+
+ assert(message);
+
+ 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 = ASSERT_PTR(userdata);
+ int r;
+
+ assert(message);
+
+ 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 = ASSERT_PTR(userdata);
+ int r;
+
+ assert(message);
+
+ 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 = ASSERT_PTR(userdata);
+ int r;
+
+ assert(message);
+
+ 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 = ASSERT_PTR(userdata);
+ int r;
+
+ assert(message);
+
+ 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 = ASSERT_PTR(userdata);
+ int r;
+
+ assert(message);
+
+ 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 = ASSERT_PTR(userdata);
+ int r;
+
+ assert(message);
+
+ 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|USER_RECORD_PERMISSIVE, &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 = ASSERT_PTR(userdata);
+ uint64_t sz;
+ int r;
+
+ assert(message);
+
+ 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, /* automatic= */ 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_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 = ASSERT_PTR(userdata);
+ int r;
+
+ assert(message);
+
+ 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 = ASSERT_PTR(userdata);
+ int r;
+
+ assert(message);
+
+ 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 = ASSERT_PTR(userdata);
+ int r;
+
+ assert(message);
+
+ 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 = -EBADF;
+ int r, please_suspend;
+ Home *h = ASSERT_PTR(userdata);
+
+ assert(message);
+
+ 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 = -EBADF;
+ Home *h = ASSERT_PTR(userdata);
+ HomeState state;
+ int please_suspend, r;
+
+ assert(message);
+
+ 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 = ASSERT_PTR(userdata);
+ int r;
+
+ assert(message);
+
+ 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;
+
+ k++;
+ }
+
+ *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_ARGS("Activate",
+ SD_BUS_ARGS("s", secret),
+ SD_BUS_NO_RESULT,
+ 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_ARGS("Realize",
+ SD_BUS_ARGS("s", secret),
+ SD_BUS_NO_RESULT,
+ 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_ARGS("Fixate",
+ SD_BUS_ARGS("s", secret),
+ SD_BUS_NO_RESULT,
+ bus_home_method_fixate,
+ SD_BUS_VTABLE_SENSITIVE),
+ SD_BUS_METHOD_WITH_ARGS("Authenticate",
+ SD_BUS_ARGS("s", secret),
+ SD_BUS_NO_RESULT,
+ bus_home_method_authenticate,
+ SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),
+ SD_BUS_METHOD_WITH_ARGS("Update",
+ SD_BUS_ARGS("s", user_record),
+ SD_BUS_NO_RESULT,
+ bus_home_method_update,
+ SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),
+ SD_BUS_METHOD_WITH_ARGS("Resize",
+ SD_BUS_ARGS("t", size, "s", secret),
+ SD_BUS_NO_RESULT,
+ bus_home_method_resize,
+ SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),
+ SD_BUS_METHOD_WITH_ARGS("ChangePassword",
+ SD_BUS_ARGS("s", new_secret, "s", old_secret),
+ SD_BUS_NO_RESULT,
+ 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_ARGS("Unlock",
+ SD_BUS_ARGS("s", secret),
+ SD_BUS_NO_RESULT,
+ bus_home_method_unlock,
+ SD_BUS_VTABLE_SENSITIVE),
+ SD_BUS_METHOD_WITH_ARGS("Acquire",
+ SD_BUS_ARGS("s", secret, "b", please_suspend),
+ SD_BUS_RESULT("h", send_fd),
+ bus_home_method_acquire,
+ SD_BUS_VTABLE_SENSITIVE),
+ SD_BUS_METHOD_WITH_ARGS("Ref",
+ SD_BUS_ARGS("b", please_suspend),
+ SD_BUS_RESULT("h", 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 = ASSERT_PTR(userdata);
+ int r;
+
+ h->deferred_change_event_source = sd_event_source_disable_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;
+
+ if (!h->manager)
+ return 0;
+
+ if (!h->manager->bus)
+ 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..37b3270
--- /dev/null
+++ b/src/home/homed-home.c
@@ -0,0 +1,3214 @@
+/* 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 "bus-locator.h"
+#include "data-fd-util.h"
+#include "env-util.h"
+#include "errno-list.h"
+#include "errno-util.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "filesystems.h"
+#include "fs-util.h"
+#include "glyph-util.h"
+#include "home-util.h"
+#include "homed-home-bus.h"
+#include "homed-home.h"
+#include "memfd-util.h"
+#include "missing_magic.h"
+#include "missing_mman.h"
+#include "missing_syscall.h"
+#include "mkdir.h"
+#include "path-util.h"
+#include "process-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 "uid-alloc-range.h"
+#include "user-record-password-quality.h"
+#include "user-record-sign.h"
+#include "user-record-util.h"
+#include "user-record.h"
+#include "user-util.h"
+
+/* Retry to deactivate home directories again and again every 15s until it works */
+#define RETRY_DEACTIVATE_USEC (15U * USEC_PER_SEC)
+
+#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 = -EBADF,
+ .sysfs = TAKE_PTR(ns),
+ .signed_locally = -1,
+ .pin_fd = -EBADF,
+ .luks_lock_fd = -EBADF,
+ };
+
+ 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|USER_RECORD_PERMISSIVE, &home->record);
+ if (r < 0)
+ return r;
+
+ (void) bus_manager_emit_auto_login_changed(m);
+ (void) bus_home_emit_change(home);
+ (void) manager_schedule_rebalance(m, /* immediately= */ false);
+
+ 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;
+
+ (void) manager_schedule_rebalance(h->manager, /* immediately= */ false);
+ }
+
+ user_record_unref(h->record);
+ user_record_unref(h->secret);
+
+ h->worker_event_source = sd_event_source_disable_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_disable_unref(h->ref_event_source_please_suspend);
+ h->ref_event_source_dont_suspend = sd_event_source_disable_unref(h->ref_event_source_dont_suspend);
+
+ h->pending_operations = ordered_set_free(h->pending_operations);
+ h->pending_event_source = sd_event_source_disable_unref(h->pending_event_source);
+ h->deferred_change_event_source = sd_event_source_disable_unref(h->deferred_change_event_source);
+
+ h->current_operation = operation_unref(h->current_operation);
+
+ safe_close(h->pin_fd);
+ safe_close(h->luks_lock_fd);
+
+ h->retry_deactivate_event_source = sd_event_source_disable_unref(h->retry_deactivate_event_source);
+
+ 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|USER_RECORD_PERMISSIVE);
+ 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(home_record_dir(), 0700);
+
+ fn = strjoina(home_record_dir(), "/", 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(home_record_dir(), "/", 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_unpin(Home *h) {
+ assert(h);
+
+ if (h->pin_fd < 0)
+ return;
+
+ h->pin_fd = safe_close(h->pin_fd);
+ log_debug("Successfully closed pin fd on home for %s.", h->user_name);
+}
+
+static void home_pin(Home *h) {
+ const char *path;
+
+ assert(h);
+
+ if (h->pin_fd >= 0) /* Already pinned? */
+ return;
+
+ path = user_record_home_directory(h->record);
+ if (!path) {
+ log_warning("No home directory path to pin for %s, ignoring.", h->user_name);
+ return;
+ }
+
+ h->pin_fd = open(path, O_RDONLY|O_DIRECTORY|O_CLOEXEC);
+ if (h->pin_fd < 0) {
+ log_warning_errno(errno, "Couldn't open home directory '%s' for pinning, ignoring: %m", path);
+ return;
+ }
+
+ log_debug("Successfully pinned home directory '%s'.", path);
+}
+
+static void home_update_pin_fd(Home *h, HomeState state) {
+ assert(h);
+
+ if (state < 0)
+ state = home_get_state(h);
+
+ return HOME_STATE_SHALL_PIN(state) ? home_pin(h) : home_unpin(h);
+}
+
+static void home_maybe_close_luks_lock_fd(Home *h, HomeState state) {
+ assert(h);
+
+ if (h->luks_lock_fd < 0)
+ return;
+
+ if (state < 0)
+ state = home_get_state(h);
+
+ /* Keep the lock as long as the home dir is active or has some operation going */
+ if (HOME_STATE_IS_EXECUTING_OPERATION(state) || HOME_STATE_IS_ACTIVE(state) || state == HOME_LOCKED)
+ return;
+
+ h->luks_lock_fd = safe_close(h->luks_lock_fd);
+ log_debug("Successfully closed LUKS backing file lock for %s.", h->user_name);
+}
+
+static void home_maybe_stop_retry_deactivate(Home *h, HomeState state) {
+ assert(h);
+
+ /* Free the deactivation retry event source if we won't need it anymore. Specifically, we'll free the
+ * event source whenever the home directory is already deactivated (and we thus where successful) or
+ * if we start executing an operation that indicates that the home directory is going to be used or
+ * operated on again. Also, if the home is referenced again stop the timer */
+
+ if (HOME_STATE_MAY_RETRY_DEACTIVATE(state) &&
+ !h->ref_event_source_dont_suspend &&
+ !h->ref_event_source_please_suspend)
+ return;
+
+ h->retry_deactivate_event_source = sd_event_source_disable_unref(h->retry_deactivate_event_source);
+}
+
+static int home_deactivate_internal(Home *h, bool force, sd_bus_error *error);
+static void home_start_retry_deactivate(Home *h);
+
+static int home_on_retry_deactivate(sd_event_source *s, uint64_t usec, void *userdata) {
+ Home *h = ASSERT_PTR(userdata);
+ HomeState state;
+
+ assert(s);
+
+ /* 15s after the last attempt to deactivate the home directory passed. Let's try it one more time. */
+
+ h->retry_deactivate_event_source = sd_event_source_disable_unref(h->retry_deactivate_event_source);
+
+ state = home_get_state(h);
+ if (!HOME_STATE_MAY_RETRY_DEACTIVATE(state))
+ return 0;
+
+ if (IN_SET(state, HOME_ACTIVE, HOME_LINGERING)) {
+ log_info("Again trying to deactivate home directory.");
+
+ /* If we are not executing any operation, let's start deactivating now. Note that this will
+ * restart our timer again, we are gonna be called again if this doesn't work. */
+ (void) home_deactivate_internal(h, /* force= */ false, NULL);
+ } else
+ /* if we are executing an operation (specifically, area already running a deactivation
+ * operation), then simply reque the timer, so that we retry again. */
+ home_start_retry_deactivate(h);
+
+ return 0;
+}
+
+static void home_start_retry_deactivate(Home *h) {
+ int r;
+
+ assert(h);
+ assert(h->manager);
+
+ /* Already allocated? */
+ if (h->retry_deactivate_event_source)
+ return;
+
+ /* If the home directory is being used now don't start the timer */
+ if (h->ref_event_source_dont_suspend || h->ref_event_source_please_suspend)
+ return;
+
+ r = sd_event_add_time_relative(
+ h->manager->event,
+ &h->retry_deactivate_event_source,
+ CLOCK_MONOTONIC,
+ RETRY_DEACTIVATE_USEC,
+ 1*USEC_PER_MINUTE,
+ home_on_retry_deactivate,
+ h);
+ if (r < 0)
+ return (void) log_warning_errno(r, "Failed to install retry-deactivate event source, ignoring: %m");
+
+ (void) sd_event_source_set_description(h->retry_deactivate_event_source, "retry-deactivate");
+}
+
+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 %s", h->user_name,
+ home_state_to_string(old_state),
+ special_glyph(SPECIAL_GLYPH_ARROW_RIGHT),
+ home_state_to_string(new_state));
+
+ home_update_pin_fd(h, new_state);
+ home_maybe_close_luks_lock_fd(h, new_state);
+ home_maybe_stop_retry_deactivate(h, 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_reschedule_rebalance(h->manager);
+ 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) < 0)
+ 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|USER_RECORD_PERMISSIVE);
+ 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_set(error, BUS_ERROR_BAD_HOME_SIZE, "File systems of this type cannot be shrunk");
+ case -ETXTBSY:
+ return sd_bus_error_set(error, BUS_ERROR_BAD_HOME_SIZE, "File systems of this type can only be shrunk offline");
+ case -ERANGE:
+ return sd_bus_error_set(error, BUS_ERROR_BAD_HOME_SIZE, "File system size too small");
+ case -ENOLINK:
+ return sd_bus_error_set(error, SD_BUS_ERROR_NOT_SUPPORTED, "System does not support selected storage backend");
+ case -EPROTONOSUPPORT:
+ return sd_bus_error_set(error, SD_BUS_ERROR_NOT_SUPPORTED, "System does not support selected file system");
+ case -ENOTTY:
+ return sd_bus_error_set(error, SD_BUS_ERROR_NOT_SUPPORTED, "Operation not supported on storage backend");
+ case -ESOCKTNOSUPPORT:
+ return sd_bus_error_set(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_set(error, BUS_ERROR_TOKEN_PIN_NEEDED, "PIN for security token required.");
+ case -ERFKILL:
+ return sd_bus_error_set(error, BUS_ERROR_TOKEN_PROTECTED_AUTHENTICATION_PATH_NEEDED, "Security token requires protected authentication path.");
+ case -EMEDIUMTYPE:
+ return sd_bus_error_set(error, BUS_ERROR_TOKEN_USER_PRESENCE_NEEDED, "Security token requires presence confirmation.");
+ case -ENOCSI:
+ return sd_bus_error_set(error, BUS_ERROR_TOKEN_USER_VERIFICATION_NEEDED, "Security token requires user verification.");
+ case -ENOSTR:
+ return sd_bus_error_set(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_set(error, BUS_ERROR_TOKEN_PIN_LOCKED, "PIN of security token locked.");
+ case -ENOLCK:
+ return sd_bus_error_set(error, BUS_ERROR_TOKEN_BAD_PIN, "Bad PIN of security token.");
+ case -ETOOMANYREFS:
+ return sd_bus_error_set(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_set(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);
+ case -EADDRINUSE:
+ return sd_bus_error_setf(error, BUS_ERROR_HOME_IN_USE, "Home %s is currently being used elsewhere.", 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);
+ (void) manager_schedule_rebalance(h->manager, /* immediately= */ false);
+ 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);
+
+ if (r >= 0)
+ (void) manager_schedule_rebalance(h->manager, /* immediately= */ true);
+}
+
+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);
+
+ if (r >= 0)
+ (void) manager_schedule_rebalance(h->manager, /* immediately= */ true);
+}
+
+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);
+
+ (void) manager_schedule_rebalance(m, /* immediately= */ true);
+ 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);
+
+ (void) manager_schedule_rebalance(h->manager, /* immediately= */ true);
+}
+
+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);
+ (void) manager_schedule_rebalance(h->manager, /* immediately= */ false);
+ 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 = ASSERT_PTR(userdata);
+ int ret;
+
+ assert(s);
+ assert(si);
+
+ 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_disable_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();
+ }
+
+ 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 = -EBADF, stdout_fd = -EBADF;
+ 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_wrapper("homework-stdout", MFD_CLOEXEC | MFD_NOEXEC_SEAL);
+ if (stdout_fd < 0)
+ return stdout_fd;
+
+ r = safe_fork_full("(sd-homework)",
+ (int[]) { stdin_fd, stdout_fd, STDERR_FILENO },
+ NULL, 0,
+ FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG_SIGTERM|FORK_REARRANGE_STDIO|FORK_LOG|FORK_REOPEN_LOG, &pid);
+ if (r < 0)
+ return r;
+ if (r == 0) {
+ _cleanup_free_ char *joined = NULL;
+ const char *homework, *suffix, *unix_path;
+
+ /* Child */
+
+ suffix = getenv("SYSTEMD_HOME_DEBUG_SUFFIX");
+ if (suffix) {
+ joined = strjoin("/run/systemd/home/notify.", suffix);
+ if (!joined)
+ return log_oom();
+ unix_path = joined;
+ } 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 we haven't locked the device yet, ask for a lock to be taken and be passed back to us via sd_notify(). */
+ if (setenv("SYSTEMD_LUKS_LOCK", one_zero(h->luks_lock_fd < 0), 1) < 0) {
+ log_error_errno(errno, "Failed to set $SYSTEMD_LUKS_LOCK: %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 = setenv_systemd_exec_pid(true);
+ if (r < 0)
+ log_warning_errno(r, "Failed to update $SYSTEMD_EXEC_PID, ignoring: %m");
+
+ /* 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_disable_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) {
+ 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(t - n, USEC_PER_SEC));
+
+ return sd_bus_error_set(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_LINGERING:
+ 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_LINGERING:
+ /* If we are lingering, i.e. active but are supposed to be deactivated, then cancel this
+ * timer if the user explicitly asks us to be active */
+ h->retry_deactivate_event_source = sd_event_source_disable_unref(h->retry_deactivate_event_source);
+ return 0;
+ 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:
+ case HOME_LINGERING:
+ 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, HOME_STATE_IS_ACTIVE(state) ? HOME_AUTHENTICATING_WHILE_ACTIVE : HOME_AUTHENTICATING, error);
+}
+
+static int home_deactivate_internal(Home *h, bool force, sd_bus_error *error) {
+ int r;
+
+ assert(h);
+
+ home_unpin(h); /* unpin so that we can deactivate */
+
+ r = home_start_work(h, force ? "deactivate-force" : "deactivate", h->record, NULL);
+ if (r < 0)
+ /* Operation failed before it even started, reacquire pin fd, if state still dictates so */
+ home_update_pin_fd(h, _HOME_STATE_INVALID);
+ else {
+ home_set_state(h, HOME_DEACTIVATING);
+ r = 0;
+ }
+
+ /* Let's start a timer to retry deactivation in 15. We'll stop the timer once we manage to deactivate
+ * the home directory again, or we start any other operation. */
+ home_start_retry_deactivate(h);
+
+ return r;
+}
+
+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:
+ case HOME_LINGERING:
+ 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, -EBADF, -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_LINGERING:
+ 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_check_password_quality(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:
+ case HOME_LINGERING:
+ 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_set(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_set(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|USER_RECORD_PERMISSIVE, &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|USER_RECORD_PERMISSIVE, &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_set(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:
+ case HOME_LINGERING:
+ 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, HOME_STATE_IS_ACTIVE(state) ? HOME_UPDATING_WHILE_ACTIVE : HOME_UPDATING);
+ return 0;
+}
+
+int home_resize(Home *h,
+ uint64_t disk_size,
+ UserRecord *secret,
+ bool automatic,
+ 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:
+ case HOME_LINGERING:
+ 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 the user didn't specify any size explicitly and rebalancing is on, then the disk size is
+ * determined by automatic rebalancing and hence not user configured but determined by us and thus
+ * applied anyway. */
+ if (disk_size == UINT64_MAX && h->record->rebalance_weight != REBALANCE_WEIGHT_OFF)
+ return sd_bus_error_set(error, SD_BUS_ERROR_INVALID_ARGS, "Disk size is being determined by automatic disk space rebalancing.");
+
+ if (disk_size == UINT64_MAX || disk_size == h->record->disk_size) {
+ if (h->record->disk_size == UINT64_MAX)
+ return sd_bus_error_set(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|USER_RECORD_PERMISSIVE, &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;
+
+ /* If user picked an explicit size, then turn off rebalancing, so that we don't undo what user chose */
+ r = user_record_set_rebalance_weight(c, REBALANCE_WEIGHT_OFF);
+ 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, automatic ? "resize-auto" : "resize", c, secret, error);
+ if (r < 0)
+ return r;
+
+ home_set_state(h, HOME_STATE_IS_ACTIVE(state) ? 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:
+ case HOME_LINGERING:
+ 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|USER_RECORD_PERMISSIVE, &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_check_password_quality(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, HOME_STATE_IS_ACTIVE(state) ? 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:
+ case HOME_LINGERING:
+ 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:
+ case HOME_LINGERING:
+ 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_LINGERING:
+ 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 h->retry_deactivate_event_source ? HOME_LINGERING : 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, int fd) {
+ _cleanup_close_ int taken_fd = TAKE_FD(fd);
+ const char *e;
+ int error;
+ int r;
+
+ assert(h);
+
+ e = strv_env_get(l, "SYSTEMD_LUKS_LOCK_FD");
+ if (e) {
+ r = parse_boolean(e);
+ if (r < 0)
+ return (void) log_debug_errno(r, "Failed to parse SYSTEMD_LUKS_LOCK_FD value: %m");
+ if (r > 0) {
+ if (taken_fd < 0)
+ return (void) log_debug("Got notify message with SYSTEMD_LUKS_LOCK_FD=1 but no fd passed, ignoring: %m");
+
+ close_and_replace(h->luks_lock_fd, taken_fd);
+
+ log_debug("Successfully acquired LUKS lock fd from worker.");
+
+ /* Immediately check if we actually want to keep it */
+ home_maybe_close_luks_lock_fd(h, _HOME_STATE_INVALID);
+ } else {
+ if (taken_fd >= 0)
+ return (void) log_debug("Got notify message with SYSTEMD_LUKS_LOCK_FD=0 but fd passed, ignoring: %m");
+
+ h->luks_lock_fd = safe_close(h->luks_lock_fd);
+ }
+
+ return;
+ }
+
+ e = strv_env_get(l, "ERRNO");
+ if (!e)
+ return (void) log_debug("Got notify message lacking both ERRNO= and SYSTEMD_LUKS_LOCK_FD= field, ignoring.");
+
+ r = safe_atoi(e, &error);
+ if (r < 0)
+ return (void) log_debug_errno(r, "Failed to parse received error number, ignoring: %s", e);
+ if (error <= 0)
+ return (void) log_debug("Error number is out of range: %i", error);
+
+ 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_SIGKILL|FORK_WAIT|FORK_LOG|FORK_REOPEN_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 = bus_call_method(h->manager->bus, bus_systemd_mgr, "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,
+ statfs_f_type_t *ret_fstype,
+ mode_t *ret_access_mode) {
+
+ 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;
+ mode_t access_mode = MODE_INVALID;
+ statfs_f_type_t fstype = 0;
+ struct statfs sfs;
+ struct stat st;
+ const char *hd;
+ int r;
+
+ assert(h);
+
+ if (state != HOME_ABSENT) {
+ const char *ip;
+
+ ip = user_record_image_path(h->record);
+ if (ip) {
+ 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;
+
+ r = path_extract_directory(ip, &parent);
+ if (r < 0)
+ return log_error_errno(r, "Failed to extract parent directory from image path '%s': %m", ip);
+
+ 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 (stat(hd, &st) < 0) {
+ log_debug_errno(errno, "Failed to stat() %s, ignoring: %m", hd);
+ goto finish;
+ }
+
+ r = stat_verify_directory(&st);
+ if (r < 0) {
+ log_debug_errno(r, "Home directory %s is not a directory, ignoring: %m", hd);
+ goto finish;
+ }
+
+ access_mode = st.st_mode & 07777;
+
+ if (statfs(hd, &sfs) < 0) {
+ log_debug_errno(errno, "Failed to statfs() %s, ignoring: %m", hd);
+ goto finish;
+ }
+
+ fstype = sfs.f_type;
+
+ 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 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));
+
+ if (ret_disk_size)
+ *ret_disk_size = disk_size;
+ if (ret_disk_usage)
+ *ret_disk_usage = disk_usage;
+ if (ret_disk_free)
+ *ret_disk_free = disk_free;
+ if (ret_disk_ceiling)
+ *ret_disk_ceiling = disk_ceiling;
+ if (ret_disk_floor)
+ *ret_disk_floor = disk_floor;
+ if (ret_fstype)
+ *ret_fstype = fstype;
+ if (ret_access_mode)
+ *ret_access_mode = access_mode;
+
+ 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,
+ statfs_f_type_t *ret_fstype,
+ mode_t *ret_access_mode) {
+
+ uint64_t disk_size = UINT64_MAX, disk_usage = UINT64_MAX, disk_free = UINT64_MAX,
+ disk_ceiling = UINT64_MAX, disk_floor = UINT64_MAX;
+ mode_t access_mode = MODE_INVALID;
+ statfs_f_type_t fstype = 0;
+ struct statfs sfs;
+ struct dqblk req;
+ const char *path = NULL;
+ int r;
+
+ assert(h);
+
+ 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. */
+
+ fstype = sfs.f_type;
+ }
+
+ 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:
+ if (ret_disk_size)
+ *ret_disk_size = disk_size;
+ if (ret_disk_usage)
+ *ret_disk_usage = disk_usage;
+ if (ret_disk_free)
+ *ret_disk_free = disk_free;
+ if (ret_disk_ceiling)
+ *ret_disk_ceiling = disk_ceiling;
+ if (ret_disk_floor)
+ *ret_disk_floor = disk_floor;
+ if (ret_fstype)
+ *ret_fstype = fstype;
+ if (ret_access_mode)
+ *ret_access_mode = access_mode;
+
+ return 0;
+}
+
+static int home_get_disk_status_internal(
+ 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,
+ statfs_f_type_t *ret_fstype,
+ mode_t *ret_access_mode) {
+
+ assert(h);
+ assert(h->record);
+
+ switch (h->record->storage) {
+
+ case USER_LUKS:
+ return home_get_disk_status_luks(h, state, ret_disk_size, ret_disk_usage, ret_disk_free, ret_disk_ceiling, ret_disk_floor, ret_fstype, ret_access_mode);
+
+ case USER_CLASSIC:
+ case USER_DIRECTORY:
+ case USER_SUBVOLUME:
+ case USER_FSCRYPT:
+ case USER_CIFS:
+ return home_get_disk_status_directory(h, state, ret_disk_size, ret_disk_usage, ret_disk_free, ret_disk_ceiling, ret_disk_floor, ret_fstype, ret_access_mode);
+
+ default:
+ /* don't know */
+
+ if (ret_disk_size)
+ *ret_disk_size = UINT64_MAX;
+ if (ret_disk_usage)
+ *ret_disk_usage = UINT64_MAX;
+ if (ret_disk_free)
+ *ret_disk_free = UINT64_MAX;
+ if (ret_disk_ceiling)
+ *ret_disk_ceiling = UINT64_MAX;
+ if (ret_disk_floor)
+ *ret_disk_floor = UINT64_MAX;
+ if (ret_fstype)
+ *ret_fstype = 0;
+ if (ret_access_mode)
+ *ret_access_mode = MODE_INVALID;
+
+ return 0;
+ }
+}
+
+int home_get_disk_status(
+ Home *h,
+ 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,
+ statfs_f_type_t *ret_fstype,
+ mode_t *ret_access_mode) {
+
+ assert(h);
+
+ return home_get_disk_status_internal(
+ h,
+ home_get_state(h),
+ ret_disk_size,
+ ret_disk_usage,
+ ret_disk_free,
+ ret_disk_ceiling,
+ ret_disk_floor,
+ ret_fstype,
+ ret_access_mode);
+}
+
+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;
+ statfs_f_type_t magic;
+ const char *fstype;
+ mode_t access_mode;
+ 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);
+
+ r = home_get_disk_status_internal(
+ h, state,
+ &disk_size,
+ &disk_usage,
+ &disk_free,
+ &disk_ceiling,
+ &disk_floor,
+ &magic,
+ &access_mode);
+ if (r < 0)
+ return r;
+
+ fstype = fs_type_to_string(magic);
+
+ 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_CONST_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)),
+ JSON_BUILD_PAIR_CONDITION(fstype, "fileSystemType", JSON_BUILD_STRING(fstype)),
+ JSON_BUILD_PAIR_CONDITION(access_mode != MODE_INVALID, "accessMode", JSON_BUILD_UNSIGNED(access_mode))
+ ));
+ 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)));
+
+ r = json_variant_filter(&m, STRV_MAKE("diskSize", "diskUsage", "diskFree", "diskCeiling", "diskFloor", "signedLocally"));
+ if (r < 0)
+ return r;
+
+ r = json_variant_merge_object(&m, status);
+ if (r < 0)
+ return r;
+
+ r = json_variant_set_field(&v, SD_ID128_TO_STRING(id), 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 = ASSERT_PTR(userdata);
+
+ assert(s);
+
+ 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 = -EBADF;
+ 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 = -EBADF;
+
+ (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);
+ goto check;
+
+ case HOME_INACTIVE:
+ case HOME_DIRTY:
+ for_state = HOME_ACTIVATING_FOR_ACQUIRE;
+ call = home_activate_internal;
+ break;
+
+ case HOME_ACTIVE:
+ case HOME_LINGERING:
+ 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);
+
+ r = home_ratelimit(h, &error);
+ if (r >= 0)
+ r = call(h, o->secret, for_state, &error);
+
+ check:
+ 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:
+ case HOME_LINGERING:
+ 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:
+ case HOME_LINGERING:
+ 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:
+ case HOME_LINGERING:
+ 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:
+ case HOME_LINGERING:
+ 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:
+ case HOME_LINGERING:
+ 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 = ASSERT_PTR(userdata);
+ Operation *o;
+ int r;
+
+ assert(s);
+
+ 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");
+
+ /* No operations pending anymore, maybe this is a good time to trigger a rebalancing */
+ manager_reschedule_rebalance(h->manager);
+ 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_set(error, BUS_ERROR_TOO_MANY_OPERATIONS, "Too many client operations requested");
+
+ r = ordered_set_ensure_put(&h->pending_operations, &operation_hash_ops, 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_stat_rdev(&d, &st);
+ 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) {
+ int r;
+
+ 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);
+
+ r = wait_for_terminate_with_timeout(h->worker_pid, 30 * USEC_PER_SEC);
+ if (r == -ETIMEDOUT)
+ log_warning_errno(r, "Waiting for worker process for home %s timed out. Ignoring.", h->user_name);
+ else if (r < 0)
+ log_warning_errno(r, "Failed to wait for worker process for home %s. Ignoring.", h->user_name);
+
+ (void) hashmap_remove_value(h->manager->homes_by_worker_pid, PID_TO_PTR(h->worker_pid), h);
+ h->worker_pid = 0;
+ return 1;
+}
+
+bool home_shall_rebalance(Home *h) {
+ HomeState state;
+
+ assert(h);
+
+ /* Determines if the home directory is a candidate for rebalancing */
+
+ if (!user_record_shall_rebalance(h->record))
+ return false;
+
+ state = home_get_state(h);
+ if (!HOME_STATE_SHALL_REBALANCE(state))
+ return false;
+
+ return true;
+}
+
+bool home_is_busy(Home *h) {
+ assert(h);
+
+ if (h->current_operation)
+ return true;
+
+ if (!ordered_set_isempty(h->pending_operations))
+ return true;
+
+ return HOME_STATE_IS_EXECUTING_OPERATION(home_get_state(h));
+}
+
+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_LINGERING] = "lingering",
+ [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..0f314aa
--- /dev/null
+++ b/src/home/homed-home.h
@@ -0,0 +1,224 @@
+/* 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 "stat-util.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_LINGERING, /* not logged in anymore, but we didn't manage to deactivate (because some process keeps it busy?) but we'll keep trying */
+ 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 = -EINVAL,
+} HomeState;
+
+static inline bool HOME_STATE_IS_ACTIVE(HomeState state) {
+ return IN_SET(state,
+ HOME_ACTIVE,
+ HOME_LINGERING,
+ 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);
+}
+
+static inline bool HOME_STATE_SHALL_PIN(HomeState state) {
+ /* Like HOME_STATE_IS_ACTIVE() – but HOME_LINGERING is missing! */
+ 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);
+}
+
+#define HOME_STATE_SHALL_REBALANCE(state) HOME_STATE_SHALL_PIN(state)
+
+static inline bool HOME_STATE_MAY_RETRY_DEACTIVATE(HomeState state) {
+ /* Indicates when to leave the deactivate retry timer active */
+ return IN_SET(state,
+ HOME_ACTIVE,
+ HOME_LINGERING,
+ HOME_DEACTIVATING,
+ HOME_LOCKING,
+ HOME_UNLOCKING,
+ HOME_UNLOCKING_FOR_ACQUIRE,
+ HOME_UPDATING_WHILE_ACTIVE,
+ HOME_RESIZING_WHILE_ACTIVE,
+ HOME_PASSWD_WHILE_ACTIVE,
+ 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 clients
+ * 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;
+
+ /* An fd to the top-level home directory we keep while logged in, to keep the dir busy */
+ int pin_fd;
+
+ /* A time event used to repeatedly try to unmount home dir after use if it didn't work on first try */
+ sd_event_source *retry_deactivate_event_source;
+
+ /* An fd that locks the backing file of LUKS home dirs with a BSD lock. */
+ int luks_lock_fd;
+
+ /* Space metrics during rebalancing */
+ uint64_t rebalance_size, rebalance_usage, rebalance_free, rebalance_min, rebalance_weight, rebalance_goal;
+
+ /* Whether a rebalance operation is pending */
+ bool rebalance_pending;
+};
+
+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, bool automatic, 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);
+
+int home_get_disk_status(Home *h, 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, statfs_f_type_t *ret_fstype, mode_t *ret_access_mode);
+
+void home_process_notify(Home *h, char **l, int fd);
+
+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);
+
+bool home_shall_rebalance(Home *h);
+
+bool home_is_busy(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..7cf5439
--- /dev/null
+++ b/src/home/homed-manager-bus.c
@@ -0,0 +1,859 @@
+/* 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 = ASSERT_PTR(userdata);
+ Home *h;
+ int r;
+
+ assert(bus);
+ assert(reply);
+
+ r = sd_bus_message_open_container(reply, 'a', "(sso)");
+ if (r < 0)
+ return r;
+
+ HASHMAP_FOREACH(h, m->homes_by_name) {
+ _cleanup_strv_free_ char **seats = NULL;
+ _cleanup_free_ char *home_path = NULL;
+
+ 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 = ASSERT_PTR(userdata);
+ Home *h;
+ int r;
+
+ assert(message);
+
+ 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 = ASSERT_PTR(userdata);
+ uint32_t uid;
+ int r;
+ Home *h;
+
+ assert(message);
+
+ 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 = ASSERT_PTR(userdata);
+ Home *h;
+ int r;
+
+ assert(message);
+
+ 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 = ASSERT_PTR(userdata);
+ const char *user_name;
+ bool incomplete;
+ Home *h;
+ int r;
+
+ assert(message);
+
+ 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 = ASSERT_PTR(userdata);
+ bool incomplete;
+ uint32_t uid;
+ Home *h;
+ int r;
+
+ assert(message);
+
+ 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 = ASSERT_PTR(userdata);
+ _cleanup_(home_freep) Home *h = NULL;
+ int r;
+
+ assert(message);
+
+ r = bus_message_read_home_record(message, USER_RECORD_LOAD_EMBEDDED|USER_RECORD_PERMISSIVE, &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)
+ return r;
+
+ TAKE_PTR(h);
+
+ 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 = ASSERT_PTR(userdata);
+ Home *h;
+ int r;
+
+ assert(message);
+
+ 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 = ASSERT_PTR(userdata);
+ Home *h;
+ int r;
+
+ assert(message);
+
+ 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|USER_RECORD_PERMISSIVE, &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 = ASSERT_PTR(userdata);
+ Home *h;
+ int r;
+
+ /* 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 = ASSERT_PTR(userdata);
+ Home *h;
+ int r;
+
+ /* 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 int method_rebalance(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ Manager *m = ASSERT_PTR(userdata);
+ int r;
+
+ r = manager_schedule_rebalance(m, /* immediately= */ true);
+ if (r == 0)
+ return sd_bus_reply_method_errorf(message, BUS_ERROR_REBALANCE_NOT_NEEDED, "No home directories need rebalancing.");
+ if (r < 0)
+ return r;
+
+ /* Keep a reference to this message, so that we can reply to it once we are done */
+ r = set_ensure_put(&m->rebalance_queued_method_calls, &bus_message_hash_ops, message);
+ if (r < 0)
+ return log_error_errno(r, "Failed to track rebalance bus message: %m");
+
+ sd_bus_message_ref(message);
+ return 1;
+}
+
+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_ARGS("GetHomeByName",
+ SD_BUS_ARGS("s", user_name),
+ SD_BUS_RESULT("u", uid,
+ "s", home_state,
+ "u", gid,
+ "s", real_name,
+ "s", home_directory,
+ "s", shell,
+ "o", bus_path),
+ method_get_home_by_name,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD_WITH_ARGS("GetHomeByUID",
+ SD_BUS_ARGS("u", uid),
+ SD_BUS_RESULT("s", user_name,
+ "s", home_state,
+ "u", gid,
+ "s", real_name,
+ "s", home_directory,
+ "s", shell,
+ "o", bus_path),
+ method_get_home_by_uid,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD_WITH_ARGS("GetUserRecordByName",
+ SD_BUS_ARGS("s", user_name),
+ SD_BUS_RESULT("s", user_record, "b", incomplete, "o", bus_path),
+ method_get_user_record_by_name,
+ SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),
+ SD_BUS_METHOD_WITH_ARGS("GetUserRecordByUID",
+ SD_BUS_ARGS("u", uid),
+ SD_BUS_RESULT("s", user_record, "b", incomplete, "o", bus_path),
+ method_get_user_record_by_uid,
+ SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),
+ SD_BUS_METHOD_WITH_ARGS("ListHomes",
+ SD_BUS_NO_ARGS,
+ SD_BUS_RESULT("a(susussso)", 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_ARGS("ActivateHome",
+ SD_BUS_ARGS("s", user_name, "s", secret),
+ SD_BUS_NO_RESULT,
+ method_activate_home,
+ SD_BUS_VTABLE_SENSITIVE),
+ SD_BUS_METHOD_WITH_ARGS("DeactivateHome",
+ SD_BUS_ARGS("s", user_name),
+ SD_BUS_NO_RESULT,
+ method_deactivate_home,
+ 0),
+
+ /* Add the JSON record to homed, but don't create actual $HOME */
+ SD_BUS_METHOD_WITH_ARGS("RegisterHome",
+ SD_BUS_ARGS("s", user_record),
+ SD_BUS_NO_RESULT,
+ method_register_home,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+
+ /* Remove the JSON record from homed, but don't remove actual $HOME */
+ SD_BUS_METHOD_WITH_ARGS("UnregisterHome",
+ SD_BUS_ARGS("s", user_name),
+ SD_BUS_NO_RESULT,
+ method_unregister_home,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+
+ /* Add JSON record, and create $HOME for it */
+ SD_BUS_METHOD_WITH_ARGS("CreateHome",
+ SD_BUS_ARGS("s", user_record),
+ SD_BUS_NO_RESULT,
+ method_create_home,
+ SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),
+
+ /* Create $HOME for already registered JSON entry */
+ SD_BUS_METHOD_WITH_ARGS("RealizeHome",
+ SD_BUS_ARGS("s", user_name, "s", secret),
+ SD_BUS_NO_RESULT,
+ method_realize_home,
+ SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),
+
+ /* Remove the JSON record and remove $HOME */
+ SD_BUS_METHOD_WITH_ARGS("RemoveHome",
+ SD_BUS_ARGS("s", user_name),
+ SD_BUS_NO_RESULT,
+ method_remove_home,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+
+ /* Investigate $HOME and propagate contained JSON record into our database */
+ SD_BUS_METHOD_WITH_ARGS("FixateHome",
+ SD_BUS_ARGS("s", user_name, "s", secret),
+ SD_BUS_NO_RESULT,
+ method_fixate_home,
+ SD_BUS_VTABLE_SENSITIVE),
+
+ /* Just check credentials */
+ SD_BUS_METHOD_WITH_ARGS("AuthenticateHome",
+ SD_BUS_ARGS("s", user_name, "s", secret),
+ SD_BUS_NO_RESULT,
+ method_authenticate_home,
+ SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),
+
+ /* Update the JSON record of existing user */
+ SD_BUS_METHOD_WITH_ARGS("UpdateHome",
+ SD_BUS_ARGS("s", user_record),
+ SD_BUS_NO_RESULT,
+ method_update_home,
+ SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),
+
+ SD_BUS_METHOD_WITH_ARGS("ResizeHome",
+ SD_BUS_ARGS("s", user_name, "t", size, "s", secret),
+ SD_BUS_NO_RESULT,
+ method_resize_home,
+ SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),
+
+ SD_BUS_METHOD_WITH_ARGS("ChangePasswordHome",
+ SD_BUS_ARGS("s", user_name, "s", new_secret, "s", old_secret),
+ SD_BUS_NO_RESULT,
+ 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_ARGS("LockHome",
+ SD_BUS_ARGS("s", user_name),
+ SD_BUS_NO_RESULT,
+ method_lock_home,
+ 0),
+
+ /* Make $HOME usable after system resume again */
+ SD_BUS_METHOD_WITH_ARGS("UnlockHome",
+ SD_BUS_ARGS("s", user_name, "s", secret),
+ SD_BUS_NO_RESULT,
+ 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_ARGS("AcquireHome",
+ SD_BUS_ARGS("s", user_name, "s", secret, "b", please_suspend),
+ SD_BUS_RESULT("h", send_fd),
+ method_acquire_home,
+ SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),
+ SD_BUS_METHOD_WITH_ARGS("RefHome",
+ SD_BUS_ARGS("s", user_name, "b", please_suspend),
+ SD_BUS_RESULT("h", send_fd),
+ method_ref_home,
+ 0),
+ SD_BUS_METHOD_WITH_ARGS("ReleaseHome",
+ SD_BUS_ARGS("s", user_name),
+ SD_BUS_NO_RESULT,
+ 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_METHOD("Rebalance", NULL, NULL, method_rebalance, 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 = ASSERT_PTR(userdata);
+ int r;
+
+ m->deferred_auto_login_event_source = sd_event_source_disable_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..c452531
--- /dev/null
+++ b/src/home/homed-manager.c
@@ -0,0 +1,2224 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <grp.h>
+#include <linux/fs.h>
+#include <linux/magic.h>
+#include <math.h>
+#include <openssl/pem.h>
+#include <pwd.h>
+#include <sys/ioctl.h>
+#include <sys/quota.h>
+#include <sys/stat.h>
+
+#include "sd-id128.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 "common-signal.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 "glyph-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 "openssl-util.h"
+#include "process-util.h"
+#include "quota-util.h"
+#include "random-util.h"
+#include "resize-fs.h"
+#include "socket-util.h"
+#include "sort-util.h"
+#include "stat-util.h"
+#include "strv.h"
+#include "sync-util.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"
+#include "varlink-io.systemd.UserDatabase.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_disable_unref(m->inotify_event_source);
+ m->scan_slash_home = false;
+
+ if (statfs(get_home_root(), &sfs) < 0) {
+ log_full_errno(errno == ENOENT ? LOG_DEBUG : LOG_WARNING, errno,
+ "Failed to statfs() %s directory, disabling automatic scanning.", get_home_root());
+ return;
+ }
+
+ if (is_network_fs(&sfs)) {
+ log_info("%s is a network file system, disabling automatic scanning.", get_home_root());
+ return;
+ }
+
+ if (is_fs_type(&sfs, AUTOFS_SUPER_MAGIC)) {
+ log_info("%s is on autofs, disabling automatic scanning.", get_home_root());
+ return;
+ }
+
+ m->scan_slash_home = true;
+
+ r = sd_event_add_inotify(m->event, &m->inotify_event_source, get_home_root(),
+ 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 %s, ignoring.", get_home_root());
+
+ (void) sd_event_source_set_description(m->inotify_event_source, "home-inotify");
+
+ log_info("Watching %s.", get_home_root());
+}
+
+static int on_home_inotify(sd_event_source *s, const struct inotify_event *event, void *userdata) {
+ _cleanup_free_ char *j = NULL;
+ Manager *m = ASSERT_PTR(userdata);
+ const char *e, *n;
+
+ 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("%s inotify queue overflow, rescanning.", get_home_root());
+ else if (FLAGS_SET(event->mask, IN_MOVE_SELF))
+ log_info("%s moved or renamed, recreating watch and rescanning.", get_home_root());
+ else if (FLAGS_SET(event->mask, IN_DELETE_SELF))
+ log_info("%s deleted, recreating watch and rescanning.", get_home_root());
+ else if (FLAGS_SET(event->mask, IN_UNMOUNT))
+ log_info("%s unmounted, recreating watch and rescanning.", get_home_root());
+ else if (FLAGS_SET(event->mask, IN_IGNORED))
+ log_info("%s watch invalidated, recreating watch and rescanning.", get_home_root());
+
+ 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_safe(event->name, e - event->name);
+ if (!suitable_user_name(n))
+ return 0;
+
+ j = path_join(get_home_root(), event->name);
+ if (!j)
+ return log_oom();
+
+ if ((event->mask & (IN_CREATE|IN_CLOSE_WRITE|IN_MOVED_TO)) != 0) {
+ if (FLAGS_SET(event->mask, IN_CREATE))
+ log_debug("%s has been created, having a look.", j);
+ else if (FLAGS_SET(event->mask, IN_CLOSE_WRITE))
+ log_debug("%s has been modified, having a look.", j);
+ else if (FLAGS_SET(event->mask, IN_MOVED_TO))
+ log_debug("%s has been moved in, having a look.", j);
+
+ (void) manager_assess_image(m, -1, get_home_root(), 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("%s has been deleted, revalidating.", j);
+ else if (FLAGS_SET(event->mask, IN_CLOSE_WRITE))
+ log_debug("%s has been closed after writing, revalidating.", j);
+ else if (FLAGS_SET(event->mask, IN_MOVED_FROM))
+ log_debug("%s has been moved away, revalidating.", j);
+
+ 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,
+ .rebalance_interval_usec = 2 * USEC_PER_MINUTE, /* initially, rebalance every 2min */
+ };
+
+ 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;
+
+ r = sd_event_add_memory_pressure(m->event, NULL, NULL, NULL);
+ if (r < 0)
+ log_full_errno(ERRNO_IS_NOT_SUPPORTED(r) || ERRNO_IS_PRIVILEGE(r) || (r == -EHOSTDOWN) ? LOG_DEBUG : LOG_WARNING, r,
+ "Failed to allocate memory pressure watch, ignoring: %m");
+
+ r = sd_event_add_signal(m->event, NULL, SIGRTMIN+18, sigrtmin18_handler, 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);
+
+ m->bus = sd_bus_flush_close_unref(m->bus);
+ m->polkit_registry = bus_verify_polkit_async_registry_free(m->polkit_registry);
+
+ m->device_monitor = sd_device_monitor_unref(m->device_monitor);
+
+ m->inotify_event_source = sd_event_source_unref(m->inotify_event_source);
+ m->notify_socket_event_source = sd_event_source_unref(m->notify_socket_event_source);
+ 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);
+ m->rebalance_event_source = sd_event_source_unref(m->rebalance_event_source);
+
+ m->event = sd_event_unref(m->event);
+
+ m->homes_by_uid = hashmap_free(m->homes_by_uid);
+ m->homes_by_name = hashmap_free(m->homes_by_name);
+ m->homes_by_worker_pid = hashmap_free(m->homes_by_worker_pid);
+ m->homes_by_sysfs = hashmap_free(m->homes_by_sysfs);
+
+ 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|USER_RECORD_PERMISSIVE);
+ 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 %s/%s, removed automatically.", home_record_dir(), fname);
+ return 0;
+}
+
+static int manager_enumerate_records(Manager *m) {
+ _cleanup_closedir_ DIR *d = NULL;
+
+ assert(m);
+
+ d = opendir(home_record_dir());
+ if (!d)
+ return log_full_errno(errno == ENOENT ? LOG_DEBUG : LOG_ERR, errno,
+ "Failed to open %s: %m", home_record_dir());
+
+ 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;
+ 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, get_home_root(), "/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_devnum(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();
+ }
+
+ 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 = -EBADF;
+ 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;
+
+ assert(m);
+
+ if (!m->scan_slash_home)
+ return 0;
+
+ d = opendir(get_home_root());
+ if (!d)
+ return log_full_errno(errno == ENOENT ? LOG_DEBUG : LOG_ERR, errno,
+ "Failed to open %s: %m", get_home_root());
+
+ FOREACH_DIRENT(de, d, return log_error_errno(errno, "Failed to read %s directory: %m", get_home_root()))
+ (void) manager_assess_image(m, dirfd(d), get_home_root(), de->d_name);
+
+ return 0;
+}
+
+static int manager_connect_bus(Manager *m) {
+ _cleanup_free_ char *b = NULL;
+ 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;
+
+ r = bus_log_control_api_register(m->bus);
+ if (r < 0)
+ return r;
+
+ suffix = getenv("SYSTEMD_HOME_DEBUG_SUFFIX");
+ if (suffix) {
+ b = strjoin("org.freedesktop.home1.", suffix);
+ if (!b)
+ return log_oom();
+ busname = b;
+ } 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) {
+ _cleanup_free_ char *p = NULL;
+ const char *suffix, *socket_path;
+ int r;
+
+ assert(m);
+ assert(!m->varlink_server);
+
+ r = varlink_server_new(&m->varlink_server, VARLINK_SERVER_ACCOUNT_UID|VARLINK_SERVER_INHERIT_USERDATA);
+ 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_add_interface(m->varlink_server, &vl_interface_io_systemd_UserDatabase);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add UserDatabase interface to 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) {
+ p = strjoin("/run/systemd/userdb/io.systemd.Home.", suffix);
+ if (!p)
+ return log_oom();
+ socket_path = p;
+ } 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);
+ r = path_extract_filename(socket_path, &m->userdb_service);
+ if (r < 0)
+ return log_error_errno(r, "Failed to extra filename from socket path '%s': %m", socket_path);
+
+ /* 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,
+ int *ret_passed_fd) {
+
+ CMSG_BUFFER_TYPE(CMSG_SPACE(sizeof(struct ucred)) + CMSG_SPACE(sizeof(int))) control;
+ _cleanup_free_ void *buffer = NULL;
+ _cleanup_close_ int passed_fd = -EBADF;
+ struct ucred *sender = NULL;
+ struct cmsghdr *cmsg;
+ struct msghdr mh;
+ struct iovec iov;
+ ssize_t n, m;
+
+ assert(fd >= 0);
+ assert(ret_sender);
+ assert(ret);
+ assert(ret_passed_fd);
+
+ n = next_datagram_size_fd(fd);
+ if (n < 0)
+ return n;
+
+ buffer = malloc(n + 2);
+ if (!buffer)
+ return -ENOMEM;
+
+ /* 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;
+
+ /* Ensure the size matches what we determined before */
+ if (m != n) {
+ cmsg_close_all(&mh);
+ 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))) {
+ assert(!sender);
+ sender = CMSG_TYPED_DATA(cmsg, struct ucred);
+ }
+
+ if (cmsg->cmsg_level == SOL_SOCKET &&
+ cmsg->cmsg_type == SCM_RIGHTS) {
+
+ if (cmsg->cmsg_len != CMSG_LEN(sizeof(int))) {
+ cmsg_close_all(&mh);
+ return -EMSGSIZE;
+ }
+
+ assert(passed_fd < 0);
+ passed_fd = *CMSG_TYPED_DATA(cmsg, int);
+ }
+ }
+
+ if (sender)
+ *ret_sender = *sender;
+ else
+ *ret_sender = (struct ucred) UCRED_INVALID;
+
+ *ret_passed_fd = TAKE_FD(passed_fd);
+
+ /* 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;
+ _cleanup_close_ int passed_fd = -EBADF;
+ struct ucred sender = UCRED_INVALID;
+ Manager *m = ASSERT_PTR(userdata);
+ ssize_t n;
+ Home *h;
+
+ assert(s);
+
+ n = read_datagram(fd, &sender, &datagram, &passed_fd);
+ if (n < 0) {
+ if (ERRNO_IS_TRANSIENT(n))
+ return 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, TAKE_FD(passed_fd));
+ return 0;
+}
+
+static int manager_listen_notify(Manager *m) {
+ _cleanup_close_ int fd = -EBADF;
+ 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) {
+ _cleanup_free_ char *unix_path = NULL;
+
+ unix_path = strjoin("/run/systemd/home/notify.", suffix);
+ if (!unix_path)
+ return log_oom();
+ 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");
+ if (sd_id128_string_equal(parttype, SD_GPT_USER_HOME) <= 0) {
+ 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 = ASSERT_PTR(userdata);
+ int r;
+
+ assert(d);
+
+ if (device_for_action(d, SD_DEVICE_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;
+ 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_fclose_ 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, NULL);
+ 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;
+}
+
+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);
+
+ r = RET_NERRNO(rename(temp_private, "/var/lib/systemd/home/local.private"));
+ if (r < 0) {
+ (void) unlink("/var/lib/systemd/home/local.public"); /* try to remove the file we already created */
+ return log_error_errno(r, "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_set(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);
+
+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);
+
+ r = path_extract_filename(path, &fn);
+ if (r < 0)
+ return log_error_errno(r, "Failed to extract filename of path '%s': %m", path);
+
+ if (streq(fn, "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);
+
+ 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;
+ 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 its 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 = ASSERT_PTR(userdata);
+
+ m->deferred_rescan_event_source = sd_event_source_disable_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 = ASSERT_PTR(userdata);
+
+ m->deferred_gc_event_source = sd_event_source_disable_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;
+}
+
+static bool manager_shall_rebalance(Manager *m) {
+ Home *h;
+
+ assert(m);
+
+ if (IN_SET(m->rebalance_state, REBALANCE_PENDING, REBALANCE_SHRINKING, REBALANCE_GROWING))
+ return true;
+
+ HASHMAP_FOREACH(h, m->homes_by_name)
+ if (home_shall_rebalance(h))
+ return true;
+
+ return false;
+}
+
+static int home_cmp(Home *const*a, Home *const*b) {
+ int r;
+
+ assert(a);
+ assert(*a);
+ assert(b);
+ assert(*b);
+
+ /* Order user records by their weight (and by their name, to make things stable). We put the records
+ * with the highest weight last, since we distribute space from the beginning and round down, hence
+ * later entries tend to get slightly more than earlier entries. */
+
+ r = CMP(user_record_rebalance_weight((*a)->record), user_record_rebalance_weight((*b)->record));
+ if (r != 0)
+ return r;
+
+ return strcmp((*a)->user_name, (*b)->user_name);
+}
+
+static int manager_rebalance_calculate(Manager *m) {
+ uint64_t weight_sum, free_sum, usage_sum = 0, min_free = UINT64_MAX;
+ _cleanup_free_ Home **array = NULL;
+ bool relevant = false;
+ struct statfs sfs;
+ int c = 0, r;
+ Home *h;
+
+ assert(m);
+
+ if (statfs(get_home_root(), &sfs) < 0)
+ return log_error_errno(errno, "Failed to statfs() /home: %m");
+
+ free_sum = (uint64_t) sfs.f_bsize * sfs.f_bavail; /* This much free space is available on the
+ * underlying pool directory */
+
+ weight_sum = REBALANCE_WEIGHT_BACKING; /* Grant the underlying pool directory a fixed weight of 20
+ * (home dirs get 100 by default, i.e. 5x more). This weight
+ * is not configurable, the per-home weights are. */
+
+ HASHMAP_FOREACH(h, m->homes_by_name) {
+ statfs_f_type_t fstype;
+ h->rebalance_pending = false; /* First, reset the flag, we only want it to be true for the
+ * homes that qualify for rebalancing */
+
+ if (!home_shall_rebalance(h)) /* Only look at actual candidates */
+ continue;
+
+ if (home_is_busy(h))
+ return -EBUSY; /* Let's not rebalance if there's a busy home directory. */
+
+ r = home_get_disk_status(
+ h,
+ &h->rebalance_size,
+ &h->rebalance_usage,
+ &h->rebalance_free,
+ NULL,
+ NULL,
+ &fstype,
+ NULL);
+ if (r < 0) {
+ log_warning_errno(r, "Failed to get free space of home '%s', ignoring.", h->user_name);
+ continue;
+ }
+
+ if (h->rebalance_free > UINT64_MAX - free_sum)
+ return log_error_errno(SYNTHETIC_ERRNO(EOVERFLOW), "Rebalance free overflow");
+ free_sum += h->rebalance_free;
+
+ if (h->rebalance_usage > UINT64_MAX - usage_sum)
+ return log_error_errno(SYNTHETIC_ERRNO(EOVERFLOW), "Rebalance usage overflow");
+ usage_sum += h->rebalance_usage;
+
+ h->rebalance_weight = user_record_rebalance_weight(h->record);
+ if (h->rebalance_weight > UINT64_MAX - weight_sum)
+ return log_error_errno(SYNTHETIC_ERRNO(EOVERFLOW), "Rebalance weight overflow");
+ weight_sum += h->rebalance_weight;
+
+ h->rebalance_min = minimal_size_by_fs_magic(fstype);
+
+ if (!GREEDY_REALLOC(array, c+1))
+ return log_oom();
+
+ array[c++] = h;
+ }
+
+ if (c == 0) {
+ log_debug("No homes to rebalance.");
+ return 0;
+ }
+
+ assert(weight_sum > 0);
+
+ log_debug("Disk space usage by all home directories to rebalance: %s — available disk space: %s",
+ FORMAT_BYTES(usage_sum), FORMAT_BYTES(free_sum));
+
+ /* Bring the home directories in a well-defined order, so that we distribute space in a reproducible
+ * way for the same parameters. */
+ typesafe_qsort(array, c, home_cmp);
+
+ for (int i = 0; i < c; i++) {
+ uint64_t new_free;
+ double d;
+
+ h = array[i];
+
+ assert(h->rebalance_free <= free_sum);
+ assert(h->rebalance_usage <= usage_sum);
+ assert(h->rebalance_weight <= weight_sum);
+
+ d = ((double) (free_sum / 4096) * (double) h->rebalance_weight) / (double) weight_sum; /* Calculate new space for this home in units of 4K */
+
+ /* Convert from units of 4K back to bytes */
+ if (d >= (double) (UINT64_MAX/4096))
+ new_free = UINT64_MAX;
+ else
+ new_free = (uint64_t) d * 4096;
+
+ /* Subtract the weight and assigned space from the sums now, to distribute the rounding noise
+ * to the remaining home dirs */
+ free_sum = LESS_BY(free_sum, new_free);
+ weight_sum = LESS_BY(weight_sum, h->rebalance_weight);
+
+ /* Keep track of home directory with the least amount of space left: we want to schedule the
+ * next rebalance more quickly if this is low */
+ if (new_free < min_free)
+ min_free = h->rebalance_size;
+
+ if (new_free > UINT64_MAX - h->rebalance_usage)
+ h->rebalance_goal = UINT64_MAX-1; /* maximum size */
+ else {
+ h->rebalance_goal = h->rebalance_usage + new_free;
+
+ if (h->rebalance_min != UINT64_MAX && h->rebalance_goal < h->rebalance_min)
+ h->rebalance_goal = h->rebalance_min;
+ }
+
+ /* Skip over this home if the state doesn't match the operation */
+ if ((m->rebalance_state == REBALANCE_SHRINKING && h->rebalance_goal > h->rebalance_size) ||
+ (m->rebalance_state == REBALANCE_GROWING && h->rebalance_goal < h->rebalance_size))
+ h->rebalance_pending = false;
+ else {
+ log_debug("Rebalancing home directory '%s' %s %s %s.", h->user_name,
+ FORMAT_BYTES(h->rebalance_size),
+ special_glyph(SPECIAL_GLYPH_ARROW_RIGHT),
+ FORMAT_BYTES(h->rebalance_goal));
+ h->rebalance_pending = true;
+ }
+
+ if ((fabs((double) h->rebalance_size - (double) h->rebalance_goal) * 100 / (double) h->rebalance_size) >= 5.0)
+ relevant = true;
+ }
+
+ /* Scale next rebalancing interval based on the least amount of space of any of the home
+ * directories. We pick a time in the range 1min … 15min, scaled by log2(min_free), so that:
+ * 10M → ~0.7min, 100M → ~2.7min, 1G → ~4.6min, 10G → ~6.5min, 100G ~8.4 */
+ m->rebalance_interval_usec = (usec_t) CLAMP((LESS_BY(log2(min_free), 22)*15*USEC_PER_MINUTE)/26,
+ 1 * USEC_PER_MINUTE,
+ 15 * USEC_PER_MINUTE);
+
+
+ log_debug("Rebalancing interval set to %s.", FORMAT_TIMESPAN(m->rebalance_interval_usec, USEC_PER_MSEC));
+
+ /* Let's suppress small resizes, growing/shrinking file systems isn't free after all */
+ if (!relevant) {
+ log_debug("Skipping rebalancing, since all calculated size changes are below ±5%%.");
+ return 0;
+ }
+
+ return c;
+}
+
+static int manager_rebalance_apply(Manager *m) {
+ int c = 0, r;
+ Home *h;
+
+ assert(m);
+
+ HASHMAP_FOREACH(h, m->homes_by_name) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+
+ if (!h->rebalance_pending)
+ continue;
+
+ h->rebalance_pending = false;
+
+ r = home_resize(h, h->rebalance_goal, /* secret= */ NULL, /* automatic= */ true, &error);
+ if (r < 0)
+ log_warning_errno(r, "Failed to resize home '%s' for rebalancing, ignoring: %s",
+ h->user_name, bus_error_message(&error, r));
+ else
+ c++;
+ }
+
+ return c;
+}
+
+static void manager_rebalance_reply_messages(Manager *m) {
+ int r;
+
+ assert(m);
+
+ for (;;) {
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *msg =
+ set_steal_first(m->rebalance_pending_method_calls);
+
+ if (!msg)
+ break;
+
+ r = sd_bus_reply_method_return(msg, NULL);
+ if (r < 0)
+ log_debug_errno(r, "Failed to reply to rebalance method call, ignoring: %m");
+ }
+}
+
+static int manager_rebalance_now(Manager *m) {
+ RebalanceState busy_state; /* the state to revert to when operation fails if busy */
+ int r;
+
+ assert(m);
+
+ log_debug("Rebalancing now...");
+
+ /* We maintain a simple state engine here to keep track of what we are doing. We'll first shrink all
+ * homes that shall be shrunk and then grow all homes that shall be grown, so that they can take up
+ * the space now freed. */
+
+ for (;;) {
+ switch (m->rebalance_state) {
+
+ case REBALANCE_IDLE:
+ case REBALANCE_PENDING:
+ case REBALANCE_WAITING:
+ /* First shrink large home dirs */
+ m->rebalance_state = REBALANCE_SHRINKING;
+ busy_state = REBALANCE_PENDING;
+
+ /* We are initiating the next rebalancing cycle now, let's make the queued methods
+ * calls the pending ones, and flush out any pending ones (which shouldn't exist at
+ * this time anyway) */
+ set_clear(m->rebalance_pending_method_calls);
+ SWAP_TWO(m->rebalance_pending_method_calls, m->rebalance_queued_method_calls);
+
+ log_debug("Shrinking phase..");
+ break;
+
+ case REBALANCE_SHRINKING:
+ /* Then grow small home dirs */
+ m->rebalance_state = REBALANCE_GROWING;
+ busy_state = REBALANCE_SHRINKING;
+ log_debug("Growing phase..");
+ break;
+
+ case REBALANCE_GROWING:
+ /* Finally, we are done */
+ log_info("Rebalancing complete.");
+ m->rebalance_state = REBALANCE_IDLE;
+ r = 0;
+ goto finish;
+
+ case REBALANCE_OFF:
+ default:
+ assert_not_reached();
+ }
+
+ r = manager_rebalance_calculate(m);
+ if (r == -EBUSY) {
+ /* Calculations failed because one home directory is currently busy. Revert to a state that
+ * tells us what to do next. */
+ log_debug("Can't enter phase, busy.");
+ m->rebalance_state = busy_state;
+ return r;
+ }
+ if (r < 0)
+ goto finish;
+ if (r == 0)
+ continue; /* got to next step immediately, if there's nothing to do */
+
+ r = manager_rebalance_apply(m);
+ if (r < 0)
+ goto finish;
+ if (r > 0)
+ break; /* At least one resize operation is now pending, we are done for now */
+
+ /* If there was nothing to apply, go for next state right-away */
+ }
+
+ return 0;
+
+finish:
+ /* Reset state and schedule next rebalance */
+ m->rebalance_state = REBALANCE_IDLE;
+ manager_rebalance_reply_messages(m);
+ (void) manager_schedule_rebalance(m, /* immediately= */ false);
+ return r;
+}
+
+static int on_rebalance_timer(sd_event_source *s, usec_t t, void *userdata) {
+ Manager *m = ASSERT_PTR(userdata);
+
+ assert(s);
+ assert(IN_SET(m->rebalance_state, REBALANCE_WAITING, REBALANCE_PENDING, REBALANCE_SHRINKING, REBALANCE_GROWING));
+
+ (void) manager_rebalance_now(m);
+ return 0;
+}
+
+int manager_schedule_rebalance(Manager *m, bool immediately) {
+ int r;
+
+ assert(m);
+
+ /* Check if there are any records where rebalancing is requested */
+ if (!manager_shall_rebalance(m)) {
+ log_debug("Not scheduling rebalancing, not needed.");
+ r = 0; /* report that we didn't schedule anything because nothing needed it */
+ goto turn_off;
+ }
+
+ if (immediately) {
+ /* If we are told to rebalance immediately, then mark a rebalance as pending (even if we area
+ * already running one) */
+
+ if (m->rebalance_event_source) {
+ r = sd_event_source_set_time(m->rebalance_event_source, 0);
+ if (r < 0) {
+ log_error_errno(r, "Failed to schedule immediate rebalancing: %m");
+ goto turn_off;
+ }
+
+ r = sd_event_source_set_enabled(m->rebalance_event_source, SD_EVENT_ONESHOT);
+ if (r < 0) {
+ log_error_errno(r, "Failed to enable rebalancing event source: %m");
+ goto turn_off;
+ }
+ } else {
+ r = sd_event_add_time(m->event, &m->rebalance_event_source, CLOCK_MONOTONIC, 0, USEC_PER_SEC, on_rebalance_timer, m);
+ if (r < 0) {
+ log_error_errno(r, "Failed to allocate rebalance event source: %m");
+ goto turn_off;
+ }
+
+ r = sd_event_source_set_priority(m->rebalance_event_source, SD_EVENT_PRIORITY_IDLE + 10);
+ if (r < 0) {
+ log_error_errno(r, "Failed to set rebalance event source priority: %m");
+ goto turn_off;
+ }
+
+ (void) sd_event_source_set_description(m->rebalance_event_source, "rebalance");
+
+ }
+
+ if (!IN_SET(m->rebalance_state, REBALANCE_PENDING, REBALANCE_SHRINKING, REBALANCE_GROWING))
+ m->rebalance_state = REBALANCE_PENDING;
+
+ log_debug("Scheduled immediate rebalancing...");
+ return 1; /* report that we scheduled something */
+ }
+
+ /* If we are told to schedule a rebalancing eventually, then do so only if we are not executing
+ * anything yet. Also if we have something scheduled already, leave it in place */
+ if (!IN_SET(m->rebalance_state, REBALANCE_OFF, REBALANCE_IDLE))
+ return 1; /* report that there's already something scheduled */
+
+ if (m->rebalance_event_source) {
+ r = sd_event_source_set_time_relative(m->rebalance_event_source, m->rebalance_interval_usec);
+ if (r < 0) {
+ log_error_errno(r, "Failed to schedule immediate rebalancing: %m");
+ goto turn_off;
+ }
+
+ r = sd_event_source_set_enabled(m->rebalance_event_source, SD_EVENT_ONESHOT);
+ if (r < 0) {
+ log_error_errno(r, "Failed to enable rebalancing event source: %m");
+ goto turn_off;
+ }
+ } else {
+ r = sd_event_add_time_relative(m->event, &m->rebalance_event_source, CLOCK_MONOTONIC, m->rebalance_interval_usec, USEC_PER_SEC, on_rebalance_timer, m);
+ if (r < 0) {
+ log_error_errno(r, "Failed to allocate rebalance event source: %m");
+ goto turn_off;
+ }
+
+ r = sd_event_source_set_priority(m->rebalance_event_source, SD_EVENT_PRIORITY_IDLE + 10);
+ if (r < 0) {
+ log_error_errno(r, "Failed to set rebalance event source priority: %m");
+ goto turn_off;
+ }
+
+ (void) sd_event_source_set_description(m->rebalance_event_source, "rebalance");
+ }
+
+ m->rebalance_state = REBALANCE_WAITING; /* We managed to enqueue a timer event, we now wait until it fires */
+ log_debug("Scheduled rebalancing in %s...", FORMAT_TIMESPAN(m->rebalance_interval_usec, 0));
+ return 1; /* report that we scheduled something */
+
+turn_off:
+ m->rebalance_event_source = sd_event_source_disable_unref(m->rebalance_event_source);
+ m->rebalance_state = REBALANCE_OFF;
+ manager_rebalance_reply_messages(m);
+ return r;
+}
+
+int manager_reschedule_rebalance(Manager *m) {
+ int r;
+
+ assert(m);
+
+ /* If a rebalance is pending reschedules it so it gets executed immediately */
+
+ if (!IN_SET(m->rebalance_state, REBALANCE_PENDING, REBALANCE_SHRINKING, REBALANCE_GROWING))
+ return 0;
+
+ r = manager_schedule_rebalance(m, /* immediately= */ true);
+ if (r < 0)
+ return r;
+
+ return 1;
+}
diff --git a/src/home/homed-manager.h b/src/home/homed-manager.h
new file mode 100644
index 0000000..20bbb4c
--- /dev/null
+++ b/src/home/homed-manager.h
@@ -0,0 +1,93 @@
+/* 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"
+
+/* The LUKS free disk space rebalancing logic goes through this state machine */
+typedef enum RebalanceState {
+ REBALANCE_OFF, /* No rebalancing enabled */
+ REBALANCE_IDLE, /* Rebalancing enabled, but currently nothing scheduled */
+ REBALANCE_WAITING, /* Rebalancing has been requested for a later point in time */
+ REBALANCE_PENDING, /* Rebalancing has been requested and will be executed ASAP */
+ REBALANCE_SHRINKING, /* Rebalancing ongoing, and we are running all shrinking operations */
+ REBALANCE_GROWING, /* Rebalancing ongoign, and we are running all growing operations */
+ _REBALANCE_STATE_MAX,
+ _REBALANCE_STATE_INVALID = -1,
+} RebalanceState;
+
+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;
+
+ sd_event_source *rebalance_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*] → public key [EVP_PKEY*] */
+
+ RebalanceState rebalance_state;
+ usec_t rebalance_interval_usec;
+
+ /* In order to allow synchronous rebalance requests via bus calls we maintain two pools of bus
+ * messages: 'rebalance_pending_methods' are the method calls we are currently operating on and
+ * running a rebalancing operation for. 'rebalance_queued_method_calls' are the method calls that
+ * have been queued since then and that we'll operate on once we complete the current run. */
+ Set *rebalance_pending_method_calls, *rebalance_queued_method_calls;
+};
+
+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_schedule_rebalance(Manager *m, bool immediately);
+int manager_reschedule_rebalance(Manager *m);
+
+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..618e920
--- /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 = -EBADF,
+ .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..004246a
--- /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 = -EINVAL,
+} 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..1cef25f
--- /dev/null
+++ b/src/home/homed-varlink.c
@@ -0,0 +1,359 @@
+/* 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|USER_RECORD_PERMISSIVE;
+ 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 = ASSERT_PTR(userdata);
+ bool trusted;
+ Home *h;
+ int r;
+
+ assert(parameters);
+
+ r = varlink_dispatch(link, parameters, dispatch_table, &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 = ASSERT_PTR(userdata);
+ Home *h;
+ int r;
+
+ assert(parameters);
+
+ r = varlink_dispatch(link, parameters, dispatch_table, &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 = ASSERT_PTR(userdata);
+ LookupParameters p = {};
+ Home *h;
+ int r;
+
+ assert(parameters);
+
+ r = varlink_dispatch(link, parameters, dispatch_table, &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;
+
+ 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)
+ 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..04d9b56
--- /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;
+ _unused_ _cleanup_(notify_on_cleanup) const char *notify_stop = NULL;
+ int r;
+
+ log_setup();
+
+ 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, SIGRTMIN+18, -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..993122b
--- /dev/null
+++ b/src/home/homed.conf
@@ -0,0 +1,21 @@
+# 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. Local configuration
+# should be created by either modifying this file (or a copy of it placed in
+# /etc/ if the original file is shipped in /usr/), or by creating "drop-ins" in
+# the /etc/systemd/homed.conf.d/ directory. The latter is generally
+# recommended. Defaults can be restored by simply deleting the main
+# configuration file and all drop-ins located in /etc/.
+#
+# Use 'systemd-analyze cat-config systemd/homed.conf' to display the full config.
+#
+# 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..19f1cd5
--- /dev/null
+++ b/src/home/homework-cifs.c
@@ -0,0 +1,254 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <sys/mount.h>
+#if WANT_LINUX_FS_H
+#include <linux/fs.h>
+#endif
+
+#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 "mkdir.h"
+#include "mount-util.h"
+#include "process-util.h"
+#include "stat-util.h"
+#include "strv.h"
+#include "tmpfile-util.h"
+
+int home_setup_cifs(
+ UserRecord *h,
+ HomeSetupFlags flags,
+ HomeSetup *setup) {
+
+ _cleanup_free_ char *chost = NULL, *cservice = NULL, *cdir = NULL, *chost_and_service = NULL, *j = NULL;
+ int r;
+
+ assert(h);
+ assert(user_record_storage(h) == USER_CIFS);
+ assert(setup);
+ assert(!setup->undo_mount);
+ assert(setup->root_fd < 0);
+
+ if (FLAGS_SET(flags, HOME_SETUP_ALREADY_ACTIVATED)) {
+ setup->root_fd = open(user_record_home_directory(h), 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;
+ }
+
+ if (!h->cifs_service)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record lacks CIFS service, refusing.");
+
+ r = parse_cifs_service(h->cifs_service, &chost, &cservice, &cdir);
+ if (r < 0)
+ return log_error_errno(r, "Failed parse CIFS service specification: %m");
+
+ /* Just the host and service part, without the directory */
+ chost_and_service = strjoin("//", chost, "/", cservice);
+ if (!chost_and_service)
+ return log_oom();
+
+ r = home_unshare_and_mkdir();
+ if (r < 0)
+ return r;
+
+ STRV_FOREACH(pw, h->password) {
+ _cleanup_(unlink_and_freep) char *p = NULL;
+ _cleanup_free_ char *options = NULL;
+ _cleanup_fclose_ FILE *f = NULL;
+ pid_t mount_pid;
+ int exit_status;
+
+ r = fopen_temporary_child(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=" GID_FMT ",forcegid,file_mode=0%3o,dir_mode=0%3o",
+ p, h->uid, user_record_gid(h), user_record_access_mode(h), user_record_access_mode(h)) < 0)
+ return log_oom();
+
+ if (h->cifs_extra_mount_options)
+ if (!strextend_with_separator(&options, ",", h->cifs_extra_mount_options))
+ return log_oom();
+
+ r = safe_fork("(mount)", FORK_RESET_SIGNALS|FORK_RLIMIT_NOFILE_SAFE|FORK_DEATHSIG_SIGTERM|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",
+ chost_and_service, HOME_RUNTIME_WORK_DIR,
+ "-o", options, NULL);
+
+ log_error_errno(errno, "Failed to execute mount: %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) {
+ setup->undo_mount = true;
+ break;
+ }
+
+ if (pw[1])
+ log_info("CIFS mount failed with password #%zu, trying next password.", (size_t) (pw - h->password) + 1);
+ }
+
+ if (!setup->undo_mount)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOKEY),
+ "Failed to mount home directory, supplied password(s) possibly wrong.");
+
+ /* Adjust MS_SUID and similar flags */
+ r = mount_nofollow_verbose(LOG_ERR, NULL, HOME_RUNTIME_WORK_DIR, NULL, MS_BIND|MS_REMOUNT|user_record_mount_flags(h), NULL);
+ if (r < 0)
+ return r;
+
+ if (cdir) {
+ j = path_join(HOME_RUNTIME_WORK_DIR, cdir);
+ if (!j)
+ return log_oom();
+
+ if (FLAGS_SET(flags, HOME_SETUP_CIFS_MKDIR)) {
+ setup->root_fd = open_mkdir_at(AT_FDCWD, j, O_CLOEXEC, 0700);
+ if (setup->root_fd < 0)
+ return log_error_errno(setup->root_fd, "Failed to create CIFS subdirectory: %m");
+ }
+ }
+
+ if (setup->root_fd < 0) {
+ setup->root_fd = open(j ?: HOME_RUNTIME_WORK_DIR, O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW);
+ if (setup->root_fd < 0)
+ return log_error_errno(errno, "Failed to open home directory: %m");
+ }
+
+ setup->mount_suffix = TAKE_PTR(cdir);
+ return 0;
+}
+
+int home_activate_cifs(
+ UserRecord *h,
+ HomeSetupFlags flags,
+ HomeSetup *setup,
+ PasswordCache *cache,
+ UserRecord **ret_home) {
+
+ _cleanup_(user_record_unrefp) UserRecord *new_home = NULL, *header_home = NULL;
+ const char *hdo, *hd;
+ int r;
+
+ assert(h);
+ assert(user_record_storage(h) == USER_CIFS);
+ assert(setup);
+ assert(ret_home);
+
+ assert_se(hdo = user_record_home_directory(h));
+ hd = strdupa_safe(hdo); /* copy the string out, since it might change later in the home record object */
+
+ r = home_setup(h, 0, setup, cache, &header_home);
+ if (r < 0)
+ return r;
+
+ r = home_refresh(h, flags, setup, header_home, cache, NULL, &new_home);
+ if (r < 0)
+ return r;
+
+ setup->root_fd = safe_close(setup->root_fd);
+
+ r = home_move_mount(setup->mount_suffix, hd);
+ if (r < 0)
+ return r;
+
+ setup->undo_mount = false;
+ setup->do_drop_caches = false;
+
+ log_info("Everything completed.");
+
+ *ret_home = TAKE_PTR(new_home);
+ return 1;
+}
+
+int home_create_cifs(UserRecord *h, HomeSetup *setup, UserRecord **ret_home) {
+ _cleanup_(user_record_unrefp) UserRecord *new_home = NULL;
+ int r;
+
+ assert(h);
+ assert(user_record_storage(h) == USER_CIFS);
+ assert(setup);
+ 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_setup_cifs(h, HOME_SETUP_CIFS_MKDIR, setup);
+ if (r < 0)
+ return r;
+
+ r = dir_is_empty_at(setup->root_fd, NULL, /* ignore_hidden_or_backup= */ false);
+ if (r < 0)
+ return log_error_errno(r, "Failed to detect if CIFS directory is empty: %m");
+ if (r == 0)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOTEMPTY), "Selected CIFS directory not empty, refusing.");
+
+ 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|USER_RECORD_PERMISSIVE, &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..af8c466
--- /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_setup_cifs(UserRecord *h, HomeSetupFlags flags, HomeSetup *setup);
+
+int home_activate_cifs(UserRecord *h, HomeSetupFlags flags, HomeSetup *setup, PasswordCache *cache, UserRecord **ret_home);
+
+int home_create_cifs(UserRecord *h, HomeSetup *setup, UserRecord **ret_home);
diff --git a/src/home/homework-directory.c b/src/home/homework-directory.c
new file mode 100644
index 0000000..6870ae9
--- /dev/null
+++ b/src/home/homework-directory.c
@@ -0,0 +1,313 @@
+/* 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-mount.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"
+#include "user-util.h"
+
+int home_setup_directory(UserRecord *h, HomeSetup *setup) {
+ const char *ip;
+ int r;
+
+ assert(h);
+ assert(IN_SET(user_record_storage(h), USER_DIRECTORY, USER_SUBVOLUME));
+ assert(setup);
+ assert(!setup->undo_mount);
+ assert(setup->root_fd < 0);
+
+ /* We'll bind mount the image directory to a new mount point where we'll start adjusting it. Only
+ * once that's complete we'll move the thing to its final place eventually. */
+ r = home_unshare_and_mkdir();
+ if (r < 0)
+ return r;
+
+ assert_se(ip = user_record_image_path(h));
+
+ r = mount_follow_verbose(LOG_ERR, ip, HOME_RUNTIME_WORK_DIR, NULL, MS_BIND, NULL);
+ if (r < 0)
+ return r;
+
+ setup->undo_mount = true;
+
+ /* Turn off any form of propagation for this */
+ r = mount_nofollow_verbose(LOG_ERR, NULL, HOME_RUNTIME_WORK_DIR, NULL, MS_PRIVATE, NULL);
+ if (r < 0)
+ return r;
+
+ /* Adjust MS_SUID and similar flags */
+ r = mount_nofollow_verbose(LOG_ERR, NULL, HOME_RUNTIME_WORK_DIR, NULL, MS_BIND|MS_REMOUNT|user_record_mount_flags(h), NULL);
+ if (r < 0)
+ return r;
+
+ setup->root_fd = open(HOME_RUNTIME_WORK_DIR, 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_directory(
+ UserRecord *h,
+ HomeSetupFlags flags,
+ HomeSetup *setup,
+ PasswordCache *cache,
+ UserRecord **ret_home) {
+
+ _cleanup_(user_record_unrefp) UserRecord *new_home = NULL, *header_home = NULL;
+ const char *hd, *hdo;
+ int r;
+
+ assert(h);
+ assert(IN_SET(user_record_storage(h), USER_DIRECTORY, USER_SUBVOLUME, USER_FSCRYPT));
+ assert(setup);
+ assert(ret_home);
+
+ assert_se(hdo = user_record_home_directory(h));
+ hd = strdupa_safe(hdo);
+
+ r = home_setup(h, flags, setup, cache, &header_home);
+ if (r < 0)
+ return r;
+
+ r = home_refresh(h, flags, setup, header_home, cache, NULL, &new_home);
+ if (r < 0)
+ return r;
+
+ r = home_extend_embedded_identity(new_home, h, setup);
+ if (r < 0)
+ return r;
+
+ /* Close fd to private mount before moving mount */
+ setup->root_fd = safe_close(setup->root_fd);
+
+ /* We are now done with everything, move the mount into place */
+ r = home_move_mount(NULL, hd);
+ if (r < 0)
+ return r;
+
+ setup->undo_mount = false;
+
+ setup->do_drop_caches = false;
+
+ log_info("Everything completed.");
+
+ *ret_home = TAKE_PTR(new_home);
+ return 0;
+}
+
+int home_create_directory_or_subvolume(UserRecord *h, HomeSetup *setup, UserRecord **ret_home) {
+ _cleanup_(rm_rf_subvolume_and_freep) char *temporary = NULL;
+ _cleanup_(user_record_unrefp) UserRecord *new_home = NULL;
+ _cleanup_close_ int mount_fd = -EBADF;
+ _cleanup_free_ char *d = NULL;
+ bool is_subvolume = false;
+ const char *ip;
+ int r;
+
+ assert(h);
+ assert(IN_SET(user_record_storage(h), USER_DIRECTORY, USER_SUBVOLUME));
+ assert(setup);
+ 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:
+ WITH_UMASK(0077)
+ r = btrfs_subvol_make(AT_FDCWD, d);
+
+ if (r >= 0) {
+ log_info("Subvolume created.");
+ is_subvolume = true;
+
+ 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();
+ }
+
+ temporary = TAKE_PTR(d); /* Needs to be destroyed now */
+
+ /* Let's decouple namespaces now, so that we can possibly mount a UID map mount into
+ * /run/systemd/user-home-mount/ that no one will see but us. */
+ r = home_unshare_and_mkdir();
+ if (r < 0)
+ return r;
+
+ setup->root_fd = open(temporary, O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW);
+ if (setup->root_fd < 0)
+ return log_error_errno(errno, "Failed to open temporary home directory: %m");
+
+ /* Try to apply a UID shift, so that the directory is actually owned by "nobody", and is only mapped
+ * to the proper UID while active. — Well, that's at least the theory. Unfortunately, only btrfs does
+ * per-subvolume quota. The others do per-uid quota. Which means mapping all home directories to the
+ * same UID of "nobody" makes quota impossible. Hence unless we actually managed to create a btrfs
+ * subvolume for this user we'll map the user's UID to itself. Now you might ask: why bother mapping
+ * at all? It's because we want to restrict the UIDs used on the home directory: we leave all other
+ * UIDs of the homed UID range unmapped, thus making them unavailable to programs accessing the
+ * mount. */
+ r = home_shift_uid(setup->root_fd, HOME_RUNTIME_WORK_DIR, is_subvolume ? UID_NOBODY : h->uid, h->uid, &mount_fd);
+ if (r > 0)
+ setup->undo_mount = true; /* If uidmaps worked we have a mount to undo again */
+
+ if (mount_fd >= 0) {
+ /* If we have established a new mount, then we can use that as new root fd to our home directory. */
+ safe_close(setup->root_fd);
+
+ setup->root_fd = fd_reopen(mount_fd, O_RDONLY|O_CLOEXEC|O_DIRECTORY);
+ if (setup->root_fd < 0)
+ return log_error_errno(setup->root_fd, "Unable to convert mount fd into proper directory fd: %m");
+
+ mount_fd = safe_close(mount_fd);
+ }
+
+ 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|USER_RECORD_PERMISSIVE, &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");
+
+ setup->root_fd = safe_close(setup->root_fd);
+
+ /* Unmount mapped mount before we move the dir into place */
+ r = home_setup_undo_mount(setup, LOG_ERR);
+ if (r < 0)
+ return r;
+
+ 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,
+ HomeSetupFlags flags,
+ HomeSetup *setup,
+ PasswordCache *cache,
+ 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_setup(h, flags, setup, cache, 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_maybe_shift_uid(h, flags, setup);
+ if (r < 0)
+ return r;
+
+ r = home_update_quota_auto(h, NULL);
+ if (ERRNO_IS_NEG_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_done(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..fe03e5d
--- /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_setup_directory(UserRecord *h, HomeSetup *setup);
+int home_activate_directory(UserRecord *h, HomeSetupFlags flags, HomeSetup *setup, PasswordCache *cache, UserRecord **ret_home);
+int home_create_directory_or_subvolume(UserRecord *h, HomeSetup *setup, UserRecord **ret_home);
+int home_resize_directory(UserRecord *h, HomeSetupFlags flags, HomeSetup *setup, PasswordCache *cache, UserRecord **ret_home);
diff --git a/src/home/homework-fido2.c b/src/home/homework-fido2.c
new file mode 100644
index 0000000..5c7cd52
--- /dev/null
+++ b/src/home/homework-fido2.c
@@ -0,0 +1,74 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <fido.h>
+
+#include "hexdecoct.h"
+#include "homework-fido2.h"
+#include "libfido2-util.h"
+#include "memory-util.h"
+#include "strv.h"
+
+int fido2_use_token(
+ UserRecord *h,
+ UserRecord *secret,
+ const Fido2HmacSalt *salt,
+ char **ret) {
+
+ _cleanup_(erase_and_freep) void *hmac = NULL;
+ size_t hmac_size;
+ Fido2EnrollFlags flags = 0;
+ ssize_t ss;
+ int r;
+
+ assert(h);
+ assert(secret);
+ assert(salt);
+ assert(ret);
+
+ /* If we know the up/uv/clientPin settings used during enrollment, let's pass this on for
+ * authentication, or generate errors immediately if interactivity of the specified kind is not
+ * allowed. */
+
+ if (salt->up > 0) {
+ if (h->fido2_user_presence_permitted <= 0)
+ return -EMEDIUMTYPE;
+
+ flags |= FIDO2ENROLL_UP;
+ } else if (salt->up < 0) /* unset? */
+ flags |= FIDO2ENROLL_UP_IF_NEEDED; /* compat with pre-248 */
+
+ if (salt->uv > 0) {
+ if (h->fido2_user_verification_permitted <= 0)
+ return -ENOCSI;
+
+ flags |= FIDO2ENROLL_UV;
+ } else if (salt->uv < 0)
+ flags |= FIDO2ENROLL_UV_OMIT; /* compat with pre-248 */
+
+ if (salt->client_pin > 0) {
+
+ if (strv_isempty(secret->token_pin))
+ return -ENOANO;
+
+ flags |= FIDO2ENROLL_PIN;
+ } else if (salt->client_pin < 0)
+ flags |= FIDO2ENROLL_PIN_IF_NEEDED; /* compat with pre-248 */
+
+ r = fido2_use_hmac_hash(
+ NULL,
+ "io.systemd.home",
+ salt->salt, salt->salt_size,
+ salt->credential.id, salt->credential.size,
+ secret->token_pin,
+ flags,
+ &hmac,
+ &hmac_size);
+ if (r < 0)
+ return r;
+
+ ss = base64mem(hmac, hmac_size, ret);
+ if (ss < 0)
+ return log_error_errno(ss, "Failed to base64 encode HMAC secret: %m");
+
+ return 0;
+}
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..6aae1d2
--- /dev/null
+++ b/src/home/homework-fscrypt.c
@@ -0,0 +1,674 @@
+/* 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-mount.h"
+#include "homework-quota.h"
+#include "memory-util.h"
+#include "missing_keyctl.h"
+#include "missing_syscall.h"
+#include "mkdir.h"
+#include "mount-util.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);
+
+ CLEANUP_ERASE(key);
+
+ /* Upload to the kernel */
+ serial = add_key("logon", description, &key, sizeof(key), where);
+ 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.
+ */
+
+ CLEANUP_ERASE(derived);
+
+ if (PKCS5_PBKDF2_HMAC(
+ password, strlen(password),
+ salt, salt_size,
+ 0xFFFF, EVP_sha512(),
+ sizeof(derived), derived) != 1)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "PBKDF2 failed");
+
+ context = EVP_CIPHER_CTX_new();
+ if (!context)
+ return log_oom();
+
+ /* 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)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to initialize decryption context.");
+
+ 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;
+}
+
+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) {
+
+ 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;
+ 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 32-bit 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_setup_fscrypt(
+ UserRecord *h,
+ HomeSetup *setup,
+ const PasswordCache *cache) {
+
+ _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(user_record_storage(h) == USER_FSCRYPT);
+ assert(setup);
+ assert(setup->root_fd < 0);
+
+ 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_SIGTERM|FORK_LOG|FORK_WAIT|FORK_REOPEN_LOG,
+ 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);
+ }
+ }
+
+ /* We'll bind mount the image directory to a new mount point where we'll start adjusting it. Only
+ * once that's complete we'll move the thing to its final place eventually. */
+ r = home_unshare_and_mkdir();
+ if (r < 0)
+ return r;
+
+ r = mount_follow_verbose(LOG_ERR, ip, HOME_RUNTIME_WORK_DIR, NULL, MS_BIND, NULL);
+ if (r < 0)
+ return r;
+
+ setup->undo_mount = true;
+
+ /* Turn off any form of propagation for this */
+ r = mount_nofollow_verbose(LOG_ERR, NULL, HOME_RUNTIME_WORK_DIR, NULL, MS_PRIVATE, NULL);
+ if (r < 0)
+ return r;
+
+ /* Adjust MS_SUID and similar flags */
+ r = mount_nofollow_verbose(LOG_ERR, NULL, HOME_RUNTIME_WORK_DIR, NULL, MS_BIND|MS_REMOUNT|user_record_mount_flags(h), NULL);
+ if (r < 0)
+ return r;
+
+ safe_close(setup->root_fd);
+ setup->root_fd = open(HOME_RUNTIME_WORK_DIR, 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;
+}
+
+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;
+ ssize_t ss;
+
+ r = crypto_random_bytes(salt, sizeof(salt));
+ if (r < 0)
+ return log_error_errno(r, "Failed to generate salt: %m");
+
+ CLEANUP_ERASE(derived);
+
+ if (PKCS5_PBKDF2_HMAC(
+ password, strlen(password),
+ salt, sizeof(salt),
+ 0xFFFF, EVP_sha512(),
+ sizeof(derived), derived) != 1)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "PBKDF2 failed");
+
+ context = EVP_CIPHER_CTX_new();
+ if (!context)
+ return log_oom();
+
+ /* 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)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to initialize encryption context.");
+
+ 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;
+
+ ss = base64mem(salt, sizeof(salt), &salt_base64);
+ if (ss < 0)
+ return log_oom();
+
+ ss = base64mem(encrypted, encrypted_size, &encrypted_base64);
+ if (ss < 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;
+}
+
+int home_create_fscrypt(
+ UserRecord *h,
+ HomeSetup *setup,
+ 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;
+ _cleanup_close_ int mount_fd = -EBADF;
+ struct fscrypt_policy policy = {};
+ size_t volume_key_size = 512 / 8;
+ _cleanup_free_ char *d = NULL;
+ uint32_t nr = 0;
+ const char *ip;
+ int r;
+
+ assert(h);
+ assert(user_record_storage(h) == USER_FSCRYPT);
+ assert(setup);
+ 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 */
+
+ r = home_unshare_and_mkdir();
+ if (r < 0)
+ return r;
+
+ setup->root_fd = open(temporary, O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW);
+ if (setup->root_fd < 0)
+ return log_error_errno(errno, "Failed to open temporary home directory: %m");
+
+ if (ioctl(setup->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 = crypto_random_bytes(volume_key, volume_key_size);
+ 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(setup->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(setup->root_fd, volume_key, volume_key_size, *i, nr);
+ if (r < 0)
+ return r;
+
+ nr++;
+ }
+
+ (void) home_update_quota_classic(h, temporary);
+
+ r = home_shift_uid(setup->root_fd, HOME_RUNTIME_WORK_DIR, h->uid, h->uid, &mount_fd);
+ if (r > 0)
+ setup->undo_mount = true; /* If uidmaps worked we have a mount to undo again */
+
+ if (mount_fd >= 0) {
+ /* If we have established a new mount, then we can use that as new root fd to our home directory. */
+ safe_close(setup->root_fd);
+
+ setup->root_fd = fd_reopen(mount_fd, O_RDONLY|O_CLOEXEC|O_DIRECTORY);
+ if (setup->root_fd < 0)
+ return log_error_errno(setup->root_fd, "Unable to convert mount fd into proper directory fd: %m");
+
+ mount_fd = safe_close(mount_fd);
+ }
+
+ 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|USER_RECORD_PERMISSIVE, &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");
+
+ setup->root_fd = safe_close(setup->root_fd);
+
+ r = home_setup_undo_mount(setup, LOG_ERR);
+ if (r < 0)
+ return r;
+
+ 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,
+ const 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;
+ 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 32-bit 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..7c2d7aa
--- /dev/null
+++ b/src/home/homework-fscrypt.h
@@ -0,0 +1,11 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "homework.h"
+#include "user-record.h"
+
+int home_setup_fscrypt(UserRecord *h, HomeSetup *setup, const PasswordCache *cache);
+
+int home_create_fscrypt(UserRecord *h, HomeSetup *setup, char **effective_passwords, UserRecord **ret_home);
+
+int home_passwd_fscrypt(UserRecord *h, HomeSetup *setup, const 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..5bd78a0
--- /dev/null
+++ b/src/home/homework-luks.c
@@ -0,0 +1,3925 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <linux/loop.h>
+#include <poll.h>
+#include <sys/file.h>
+#include <sys/ioctl.h>
+#include <sys/xattr.h>
+
+#if HAVE_VALGRIND_MEMCHECK_H
+#include <valgrind/memcheck.h>
+#endif
+
+#include "sd-daemon.h"
+#include "sd-device.h"
+#include "sd-event.h"
+#include "sd-id128.h"
+
+#include "blkid-util.h"
+#include "blockdev-util.h"
+#include "btrfs-util.h"
+#include "chattr-util.h"
+#include "device-util.h"
+#include "devnum-util.h"
+#include "dm-util.h"
+#include "env-util.h"
+#include "errno-util.h"
+#include "fd-util.h"
+#include "fdisk-util.h"
+#include "fileio.h"
+#include "filesystems.h"
+#include "fs-util.h"
+#include "fsck-util.h"
+#include "glyph-util.h"
+#include "gpt.h"
+#include "home-util.h"
+#include "homework-luks.h"
+#include "homework-mount.h"
+#include "io-util.h"
+#include "keyring-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 "strv.h"
+#include "sync-util.h"
+#include "tmpfile-util.h"
+#include "udev-util.h"
+#include "user-util.h"
+
+/* Round down to the nearest 4K size. Given that newer hardware generally prefers 4K sectors, let's align our
+ * partitions to that too. In the worst case we'll waste 3.5K per partition that way, but I think I can live
+ * with that. */
+#define DISK_SIZE_ROUND_DOWN(x) ((x) & ~UINT64_C(4095))
+
+/* Rounds up to the nearest 4K boundary. Returns UINT64_MAX on overflow */
+#define DISK_SIZE_ROUND_UP(x) \
+ ({ \
+ uint64_t _x = (x); \
+ _x > UINT64_MAX - 4095U ? UINT64_MAX : (_x + 4095U) & ~UINT64_C(4095); \
+ })
+
+/* How much larger will the image on disk be than the fs inside it, i.e. the space we pay for the GPT and
+ * LUKS2 envelope. (As measured on cryptsetup 2.4.1) */
+#define GPT_LUKS2_OVERHEAD UINT64_C(18874368)
+
+static int resize_image_loop(UserRecord *h, HomeSetup *setup, uint64_t old_image_size, uint64_t new_image_size, uint64_t *ret_image_size);
+
+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_IS_XATTR_ABSENT(errno))
+ 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 = -EBADF;
+
+ 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_or_else(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 (r == _BLKID_SAFEPROBE_ERROR)
+ return errno_or_else(EIO);
+ if (IN_SET(r, _BLKID_SAFEPROBE_AMBIGUOUS, _BLKID_SAFEPROBE_NOT_FOUND))
+ return -ENOPKG;
+
+ assert(r == _BLKID_SAFEPROBE_FOUND);
+
+ (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 = -EBADF;
+
+ fd = open(path, O_RDONLY|O_CLOEXEC|O_NOCTTY|O_NONBLOCK);
+ if (fd < 0)
+ return negative_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;
+
+ return RET_NERRNO(ioctl(fd, BLKGETSIZE64, ret));
+}
+
+static int block_get_size_by_path(const char *path, uint64_t *ret) {
+ _cleanup_close_ int fd = -EBADF;
+
+ 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_for_fstype(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_SIGTERM|FORK_LOG|FORK_STDOUT_TO_STDERR|FORK_CLOSE_ALL_FDS,
+ &fsck_pid);
+ if (r < 0)
+ return r;
+ if (r == 0) {
+ /* Child */
+ execlp("fsck", "fsck", "-aTl", node, NULL);
+ log_open();
+ 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;
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(key_serial_t, keyring_unlink, -1);
+
+static int upload_to_keyring(
+ UserRecord *h,
+ const char *password,
+ key_serial_t *ret_key_serial) {
+
+ _cleanup_free_ char *name = NULL;
+ key_serial_t serial;
+
+ assert(h);
+ assert(password);
+
+ /* If auto-shrink-on-logout is turned on, we need to keep the key we used to unlock the LUKS volume
+ * around, since we'll need it when automatically resizing (since we can't ask the user there
+ * again). We do this by uploading it into the kernel keyring, specifically the "session" one. This
+ * is done under the assumption systemd-homed gets its private per-session keyring (i.e. default
+ * service behaviour, given that KeyringMode=private is the default). It will survive between our
+ * systemd-homework invocations that way.
+ *
+ * If auto-shrink-on-logout is disabled we'll skip this step, to be frugal with sensitive data. */
+
+ if (user_record_auto_resize_mode(h) != AUTO_RESIZE_SHRINK_AND_GROW) { /* Won't need it */
+ if (ret_key_serial)
+ *ret_key_serial = -1;
+ return 0;
+ }
+
+ name = strjoin("homework-user-", h->user_name);
+ if (!name)
+ return -ENOMEM;
+
+ serial = add_key("user", name, password, strlen(password), KEY_SPEC_SESSION_KEYRING);
+ if (serial == -1)
+ return -errno;
+
+ if (ret_key_serial)
+ *ret_key_serial = serial;
+
+ return 1;
+}
+
+static int luks_try_passwords(
+ UserRecord *h,
+ struct crypt_device *cd,
+ char **passwords,
+ void *volume_key,
+ size_t *volume_key_size,
+ key_serial_t *ret_key_serial) {
+
+ int r;
+
+ assert(h);
+ assert(cd);
+
+ STRV_FOREACH(pp, passwords) {
+ size_t vks = *volume_key_size;
+
+ r = sym_crypt_volume_key_get(
+ cd,
+ CRYPT_ANY_SLOT,
+ volume_key,
+ &vks,
+ *pp,
+ strlen(*pp));
+ if (r >= 0) {
+ if (ret_key_serial) {
+ /* If ret_key_serial is non-NULL, let's try to upload the password that
+ * worked, and return its serial. */
+ r = upload_to_keyring(h, *pp, ret_key_serial);
+ if (r < 0) {
+ log_debug_errno(r, "Failed to upload LUKS password to kernel keyring, ignoring: %m");
+ *ret_key_serial = -1;
+ }
+ }
+
+ *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(
+ UserRecord *h,
+ 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,
+ key_serial_t *ret_key_serial) {
+
+ _cleanup_(keyring_unlinkp) key_serial_t key_serial = -1;
+ _cleanup_(sym_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(h);
+ assert(node);
+ assert(dm_name);
+ assert(ret);
+
+ r = sym_crypt_init(&cd, node);
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate libcryptsetup context: %m");
+
+ cryptsetup_enable_logging(cd);
+
+ r = sym_crypt_load(cd, CRYPT_LUKS2, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to load LUKS superblock: %m");
+
+ r = sym_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 = sym_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, sym_crypt_get_cipher(cd)))
+ return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "LUKS superblock declares wrong cipher.");
+
+ if (cipher_mode && !streq_ptr(cipher_mode, sym_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 ? cache->keyring_passswords : NULL,
+ cache ? cache->pkcs11_passwords : NULL,
+ cache ? cache->fido2_passwords : NULL,
+ passwords) {
+ r = luks_try_passwords(h, cd, list, vk, &vks, ret_key_serial ? &key_serial : NULL);
+ 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 unlock LUKS superblock: %m");
+
+ r = sym_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;
+ if (ret_key_serial)
+ *ret_key_serial = TAKE_KEY_SERIAL(key_serial);
+
+ return 0;
+}
+
+static int make_dm_names(UserRecord *h, HomeSetup *setup) {
+ assert(h);
+ assert(h->user_name);
+ assert(setup);
+
+ if (!setup->dm_name) {
+ setup->dm_name = strjoin("home-", h->user_name);
+ if (!setup->dm_name)
+ return log_oom();
+ }
+
+ if (!setup->dm_node) {
+ setup->dm_node = path_join("/dev/mapper/", setup->dm_name);
+ if (!setup->dm_node)
+ return log_oom();
+ }
+
+ return 0;
+}
+
+static int acquire_open_luks_device(
+ UserRecord *h,
+ HomeSetup *setup,
+ bool graceful) {
+
+ _cleanup_(sym_crypt_freep) struct crypt_device *cd = NULL;
+ int r;
+
+ assert(h);
+ assert(setup);
+ assert(!setup->crypt_device);
+
+ r = dlopen_cryptsetup();
+ if (r < 0)
+ return r;
+
+ r = make_dm_names(h, setup);
+ if (r < 0)
+ return r;
+
+ r = sym_crypt_init_by_name(&cd, setup->dm_name);
+ if ((ERRNO_IS_NEG_DEVICE_ABSENT(r) || r == -EINVAL) && graceful)
+ return 0;
+ if (r < 0)
+ return log_error_errno(r, "Failed to initialize cryptsetup context for %s: %m", setup->dm_name);
+
+ cryptsetup_enable_logging(cd);
+
+ setup->crypt_device = TAKE_PTR(cd);
+ return 1;
+}
+
+static int luks_open(
+ UserRecord *h,
+ HomeSetup *setup,
+ const PasswordCache *cache,
+ sd_id128_t *ret_found_uuid,
+ void **ret_volume_key,
+ size_t *ret_volume_key_size) {
+
+ _cleanup_(erase_and_freep) void *vk = NULL;
+ sd_id128_t p;
+ char **list;
+ size_t vks;
+ int r;
+
+ assert(h);
+ assert(setup);
+ assert(!setup->crypt_device);
+
+ /* 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 = acquire_open_luks_device(h, setup, /* graceful= */ false);
+ if (r < 0)
+ return r;
+
+ r = sym_crypt_load(setup->crypt_device, CRYPT_LUKS2, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to load LUKS superblock: %m");
+
+ r = sym_crypt_get_volume_key_size(setup->crypt_device);
+ 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 = sym_crypt_get_uuid(setup->crypt_device);
+ 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 ? cache->keyring_passswords : NULL,
+ cache ? cache->pkcs11_passwords : NULL,
+ cache ? cache->fido2_passwords : NULL,
+ h->password) {
+ r = luks_try_passwords(h, setup->crypt_device, list, vk, &vks, NULL);
+ 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 unlock LUKS superblock: %m");
+
+ log_info("Discovered used LUKS device /dev/mapper/%s, and validated password.", setup->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 = sym_crypt_activate_by_volume_key(setup->crypt_device, 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.");
+
+ 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 = SD_ID128_NULL; /* avoid false maybe-unitialized warning */
+ 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 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, 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_or_else(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 (r == _BLKID_SAFEPROBE_ERROR)
+ return errno_or_else(EIO);
+ if (IN_SET(r, _BLKID_SAFEPROBE_AMBIGUOUS, _BLKID_SAFEPROBE_NOT_FOUND))
+ return -ENOPKG;
+
+ assert(r == _BLKID_SAFEPROBE_FOUND);
+
+ (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_or_else(ENOMEM);
+
+ errno = 0;
+ n = blkid_partlist_numof_partitions(pl);
+ if (n < 0)
+ return errno_or_else(EIO);
+
+ for (int i = 0; i < n; i++) {
+ sd_id128_t id = SD_ID128_NULL;
+ blkid_partition pp;
+
+ errno = 0;
+ pp = blkid_partlist_get_partition(pl, i);
+ if (!pp)
+ return errno_or_else(EIO);
+
+ if (sd_id128_string_equal(blkid_partition_get_type_string(pp), SD_GPT_USER_HOME) <= 0)
+ continue;
+
+ if (!streq_ptr(blkid_partition_get_name(pp), label))
+ continue;
+
+
+ r = blkid_partition_get_uuid_id128(pp, &id);
+ if (r < 0)
+ log_debug_errno(r, "Failed to read partition UUID, ignoring: %m");
+ else 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 = sym_crypt_get_cipher(cd);
+ if (!cipher)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Cannot get cipher from LUKS device.");
+
+ cipher_mode = sym_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_safe(cipher_mode, e - cipher_mode);
+
+ r = sym_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;
+
+ assert(cd);
+ assert(h);
+
+ for (int token = 0; token < sym_crypt_token_max(CRYPT_LUKS2); 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 = sym_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 = sym_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|USER_RECORD_PERMISSIVE);
+ 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 = crypto_random_bytes(iv, iv_size);
+ 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_CONST_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 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|USER_RECORD_PERMISSIVE, &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 (int token = 0; token < sym_crypt_token_max(CRYPT_LUKS2); token++) {
+ crypt_token_info state;
+ const char *type;
+
+ state = sym_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 = sym_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);
+ }
+
+ 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) {
+ 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(range.len));
+ return 1;
+}
+
+int run_fallocate(int backing_fd, const struct stat *st) {
+ 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((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 = -EBADF;
+
+ 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);
+}
+
+static int lock_image_fd(int image_fd, const char *ip) {
+ int r;
+
+ /* If the $SYSTEMD_LUKS_LOCK environment variable is set we'll take an exclusive BSD lock on the
+ * image file, and send it to our parent. homed will keep it open to ensure no other instance of
+ * homed (across the network or such) will also mount the file. */
+
+ assert(image_fd >= 0);
+ assert(ip);
+
+ r = getenv_bool("SYSTEMD_LUKS_LOCK");
+ if (r == -ENXIO)
+ return 0;
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse $SYSTEMD_LUKS_LOCK environment variable: %m");
+ if (r == 0)
+ return 0;
+
+ if (flock(image_fd, LOCK_EX|LOCK_NB) < 0) {
+
+ if (errno == EAGAIN)
+ log_error_errno(errno, "Image file '%s' already locked, can't use.", ip);
+ else
+ log_error_errno(errno, "Failed to lock image file '%s': %m", ip);
+
+ return errno != EAGAIN ? -errno : -EADDRINUSE; /* Make error recognizable */
+ }
+
+ log_info("Successfully locked image file '%s'.", ip);
+
+ /* Now send it to our parent to keep safe while the home dir is active */
+ r = sd_pid_notify_with_fds(0, false, "SYSTEMD_LUKS_LOCK_FD=1", &image_fd, 1);
+ if (r < 0)
+ log_warning_errno(r, "Failed to send LUKS lock fd to parent, ignoring: %m");
+
+ return 0;
+}
+
+static int open_image_file(
+ UserRecord *h,
+ const char *force_image_path,
+ struct stat *ret_stat) {
+
+ _cleanup_close_ int image_fd = -EBADF;
+ struct stat st;
+ const char *ip;
+ int r;
+
+ assert(h || force_image_path);
+
+ ip = force_image_path ?: user_record_image_path(h);
+
+ 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);
+
+ /* Locking block devices doesn't really make sense, as this might interfere with
+ * udev's workings, and these locks aren't network propagated anyway, hence not what
+ * we are after here. */
+ if (S_ISREG(st.st_mode)) {
+ r = lock_image_fd(image_fd, ip);
+ if (r < 0)
+ return r;
+ }
+
+ if (ret_stat)
+ *ret_stat = st;
+
+ return TAKE_FD(image_fd);
+}
+
+int home_setup_luks(
+ UserRecord *h,
+ HomeSetupFlags flags,
+ const char *force_image_path,
+ HomeSetup *setup,
+ PasswordCache *cache,
+ UserRecord **ret_luks_home) {
+
+ sd_id128_t found_partition_uuid, found_fs_uuid = SD_ID128_NULL, found_luks_uuid = SD_ID128_NULL;
+ _cleanup_(user_record_unrefp) UserRecord *luks_home = NULL;
+ _cleanup_(erase_and_freep) void *volume_key = NULL;
+ size_t volume_key_size = 0;
+ uint64_t offset, size;
+ struct stat st;
+ int r;
+
+ assert(h);
+ assert(setup);
+ assert(user_record_storage(h) == USER_LUKS);
+
+ r = dlopen_cryptsetup();
+ if (r < 0)
+ return r;
+
+ r = make_dm_names(h, setup);
+ if (r < 0)
+ return r;
+
+ /* Reuse the image fd if it has already been opened by an earlier step */
+ if (setup->image_fd < 0) {
+ setup->image_fd = open_image_file(h, force_image_path, &st);
+ if (setup->image_fd < 0)
+ return setup->image_fd;
+ } else if (fstat(setup->image_fd, &st) < 0)
+ return log_error_errno(errno, "Failed to stat image: %m");
+
+ if (FLAGS_SET(flags, HOME_SETUP_ALREADY_ACTIVATED)) {
+ struct loop_info64 info;
+ const char *n;
+
+ if (!setup->crypt_device) {
+ r = luks_open(h,
+ setup,
+ cache,
+ &found_luks_uuid,
+ &volume_key,
+ &volume_key_size);
+ if (r < 0)
+ return r;
+ }
+
+ if (ret_luks_home) {
+ r = luks_validate_home_record(setup->crypt_device, h, volume_key, cache, &luks_home);
+ if (r < 0)
+ return r;
+ }
+
+ n = sym_crypt_get_device_name(setup->crypt_device);
+ if (!n)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to determine backing device for DM %s.", setup->dm_name);
+
+ if (!setup->loop) {
+ r = loop_device_open_from_path(n, O_RDWR, LOCK_UN, &setup->loop);
+ if (r < 0)
+ return log_error_errno(r, "Failed to open loopback device %s: %m", n);
+ }
+
+ if (ioctl(setup->loop->fd, LOOP_GET_STATUS64, &info) < 0) {
+ _cleanup_free_ char *sysfs = NULL;
+
+ if (!IN_SET(errno, ENOTTY, EINVAL))
+ return log_error_errno(errno, "Failed to get block device metrics of %s: %m", n);
+
+ if (ioctl(setup->loop->fd, BLKGETSIZE64, &size) < 0)
+ return log_error_errno(r, "Failed to read block device size of %s: %m", n);
+
+ if (fstat(setup->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/" DEVNUM_FORMAT_STR "/partition", DEVNUM_FORMAT_VAL(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/" DEVNUM_FORMAT_STR "/start", DEVNUM_FORMAT_VAL(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 {
+#if HAVE_VALGRIND_MEMCHECK_H
+ VALGRIND_MAKE_MEM_DEFINED(&info, sizeof(info));
+#endif
+
+ offset = info.lo_offset;
+ size = info.lo_sizelimit;
+ }
+
+ found_partition_uuid = found_fs_uuid = SD_ID128_NULL;
+
+ log_info("Discovered used loopback device %s.", setup->loop->node);
+
+ if (setup->root_fd < 0) {
+ setup->root_fd = open(user_record_home_directory(h), O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW);
+ if (setup->root_fd < 0)
+ return log_error_errno(errno, "Failed to open home directory: %m");
+ }
+ } else {
+ _cleanup_free_ char *fstype = NULL, *subdir = NULL;
+ const char *ip;
+
+ /* When we aren't reopening the home directory we are allocating it fresh, hence the relevant
+ * objects can't be allocated yet. */
+ assert(setup->root_fd < 0);
+ assert(!setup->crypt_device);
+ assert(!setup->loop);
+
+ ip = force_image_path ?: user_record_image_path(h);
+
+ subdir = path_join(HOME_RUNTIME_WORK_DIR, user_record_user_name_and_realm(h));
+ if (!subdir)
+ return log_oom();
+
+ r = luks_validate(setup->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 */
+ if (run_mark_dirty(setup->image_fd, true) > 0)
+ setup->do_mark_clean = true;
+
+ if (!user_record_luks_discard(h)) {
+ r = run_fallocate(setup->image_fd, &st);
+ if (r < 0)
+ return r;
+ }
+
+ r = loop_device_make(
+ setup->image_fd,
+ O_RDWR,
+ offset,
+ size,
+ h->luks_sector_size == UINT64_MAX ? UINT32_MAX : user_record_luks_sector_size(h), /* if sector size is not specified, select UINT32_MAX, i.e. auto-probe */
+ /* loop_flags= */ 0,
+ LOCK_UN,
+ &setup->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.", setup->loop->node ?: ip);
+
+ r = luks_setup(h,
+ 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),
+ &setup->crypt_device,
+ &found_luks_uuid,
+ &volume_key,
+ &volume_key_size,
+ &setup->key_serial);
+ if (r < 0)
+ return r;
+
+ setup->undo_dm = true;
+
+ if (ret_luks_home) {
+ r = luks_validate_home_record(setup->crypt_device, h, volume_key, cache, &luks_home);
+ if (r < 0)
+ return r;
+ }
+
+ r = fs_validate(setup->dm_node, h->file_system_uuid, &fstype, &found_fs_uuid);
+ if (r < 0)
+ return r;
+
+ r = run_fsck(setup->dm_node, fstype);
+ if (r < 0)
+ return r;
+
+ r = home_unshare_and_mount(setup->dm_node, fstype, user_record_luks_discard(h), user_record_mount_flags(h), h->luks_extra_mount_options);
+ if (r < 0)
+ return r;
+
+ setup->undo_mount = true;
+
+ setup->root_fd = open(subdir, O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW);
+ if (setup->root_fd < 0)
+ return log_error_errno(errno, "Failed to open home directory: %m");
+
+ if (user_record_luks_discard(h))
+ (void) run_fitrim(setup->root_fd);
+
+ setup->do_offline_fallocate = !(setup->do_offline_fitrim = user_record_luks_offline_discard(h));
+ }
+
+ if (!sd_id128_is_null(found_partition_uuid))
+ setup->found_partition_uuid = found_partition_uuid;
+ if (!sd_id128_is_null(found_luks_uuid))
+ setup->found_luks_uuid = found_luks_uuid;
+ if (!sd_id128_is_null(found_fs_uuid))
+ setup->found_fs_uuid = found_fs_uuid;
+
+ setup->partition_offset = offset;
+ setup->partition_size = size;
+
+ if (volume_key) {
+ erase_and_free(setup->volume_key);
+ setup->volume_key = TAKE_PTR(volume_key);
+ setup->volume_key_size = volume_key_size;
+ }
+
+ if (ret_luks_home)
+ *ret_luks_home = TAKE_PTR(luks_home);
+
+ return 0;
+}
+
+static void print_size_summary(uint64_t host_size, uint64_t encrypted_size, const struct statfs *sfs) {
+ 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(host_size),
+ FORMAT_BYTES(encrypted_size),
+ FORMAT_BYTES((uint64_t) sfs->f_blocks * (uint64_t) sfs->f_frsize),
+ FORMAT_BYTES((uint64_t) sfs->f_bfree * (uint64_t) sfs->f_frsize));
+}
+
+static int home_auto_grow_luks(
+ UserRecord *h,
+ HomeSetup *setup,
+ PasswordCache *cache) {
+
+ struct statfs sfs;
+
+ assert(h);
+ assert(setup);
+
+ if (!IN_SET(user_record_auto_resize_mode(h), AUTO_RESIZE_GROW, AUTO_RESIZE_SHRINK_AND_GROW))
+ return 0;
+
+ assert(setup->root_fd >= 0);
+
+ if (fstatfs(setup->root_fd, &sfs) < 0)
+ return log_error_errno(errno, "Failed to statfs home directory: %m");
+
+ if (!fs_can_online_shrink_and_grow(sfs.f_type)) {
+ log_debug("Not auto-grow file system, since selected file system cannot do both online shrink and grow.");
+ return 0;
+ }
+
+ log_debug("Initiating auto-grow...");
+
+ return home_resize_luks(
+ h,
+ HOME_SETUP_ALREADY_ACTIVATED|
+ HOME_SETUP_RESIZE_DONT_SYNC_IDENTITIES|
+ HOME_SETUP_RESIZE_DONT_SHRINK|
+ HOME_SETUP_RESIZE_DONT_UNDO,
+ setup,
+ cache,
+ NULL);
+}
+
+int home_activate_luks(
+ UserRecord *h,
+ HomeSetupFlags flags,
+ HomeSetup *setup,
+ PasswordCache *cache,
+ UserRecord **ret_home) {
+
+ _cleanup_(user_record_unrefp) UserRecord *new_home = NULL, *luks_home_record = NULL;
+ 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(setup);
+ assert(ret_home);
+
+ r = dlopen_cryptsetup();
+ if (r < 0)
+ return r;
+
+ assert_se(hdo = user_record_home_directory(h));
+ hd = strdupa_safe(hdo); /* copy the string out, since it might change later in the home record object */
+
+ r = home_get_state_luks(h, setup);
+ if (r < 0)
+ return r;
+ if (r > 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EEXIST), "Device mapper device %s already exists, refusing.", setup->dm_node);
+
+ r = home_setup_luks(
+ h,
+ 0,
+ NULL,
+ setup,
+ cache,
+ &luks_home_record);
+ if (r < 0)
+ return r;
+
+ r = home_auto_grow_luks(h, setup, cache);
+ 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,
+ flags,
+ 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 = sym_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;
+ setup->do_drop_caches = false;
+ TAKE_KEY_SERIAL(setup->key_serial); /* Leave key in kernel keyring */
+
+ log_info("Activation completed.");
+
+ print_size_summary(host_size, encrypted_size, &sfs);
+
+ *ret_home = TAKE_PTR(new_home);
+ return 1;
+}
+
+int home_deactivate_luks(UserRecord *h, HomeSetup *setup) {
+ bool we_detached = false;
+ int r;
+
+ assert(h);
+ assert(setup);
+
+ /* 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. */
+
+ if (!setup->crypt_device) {
+ r = acquire_open_luks_device(h, setup, /* graceful= */ true);
+ if (r < 0)
+ return log_error_errno(r, "Failed to initialize cryptsetup context for %s: %m", setup->dm_name);
+ if (r == 0)
+ log_debug("LUKS device %s has already been detached.", setup->dm_name);
+ }
+
+ if (setup->crypt_device) {
+ log_info("Discovered used LUKS device %s.", setup->dm_node);
+
+ cryptsetup_enable_logging(setup->crypt_device);
+
+ r = sym_crypt_deactivate_by_name(setup->crypt_device, setup->dm_name, 0);
+ if (ERRNO_IS_NEG_DEVICE_ABSENT(r) || r == -EINVAL)
+ log_debug_errno(r, "LUKS device %s is already detached.", setup->dm_node);
+ else if (r < 0)
+ return log_info_errno(r, "LUKS device %s couldn't be deactivated: %m", setup->dm_node);
+ else {
+ log_info("LUKS device detaching completed.");
+ we_detached = true;
+ }
+ }
+
+ (void) wait_for_block_device_gone(setup, USEC_PER_SEC * 30);
+ setup->undo_dm = false;
+
+ 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, HomeSetup *setup) {
+ assert(h);
+ assert(setup);
+ assert(setup->root_fd >= 0);
+
+ if (!user_record_luks_offline_discard(h)) {
+ log_debug("Not trimming on logout.");
+ return 0;
+ }
+
+ (void) run_fitrim(setup->root_fd);
+ return 0;
+}
+
+static struct crypt_pbkdf_type* build_good_pbkdf(struct crypt_pbkdf_type *buffer, UserRecord *hr) {
+ assert(buffer);
+ assert(hr);
+
+ bool benchmark = user_record_luks_pbkdf_force_iterations(hr) == UINT64_MAX;
+
+ *buffer = (struct crypt_pbkdf_type) {
+ .hash = user_record_luks_pbkdf_hash_algorithm(hr),
+ .type = user_record_luks_pbkdf_type(hr),
+ .time_ms = benchmark ? user_record_luks_pbkdf_time_cost_usec(hr) / USEC_PER_MSEC : 0,
+ .iterations = benchmark ? 0 : user_record_luks_pbkdf_force_iterations(hr),
+ .max_memory_kb = user_record_luks_pbkdf_memory_cost(hr) / 1024,
+ .parallel_threads = user_record_luks_pbkdf_parallel_threads(hr),
+ .flags = benchmark ? 0 : CRYPT_PBKDF_NO_BENCHMARK,
+ };
+
+ 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_(sym_crypt_freep) struct crypt_device *cd = NULL;
+ _cleanup_(erase_and_freep) void *volume_key = NULL;
+ struct crypt_pbkdf_type good_pbkdf, minimal_pbkdf;
+ _cleanup_free_ char *text = NULL;
+ size_t volume_key_size;
+ int slot = 0, r;
+
+ assert(node);
+ assert(dm_name);
+ assert(hr);
+ assert(ret);
+
+ r = sym_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 = crypto_random_bytes(volume_key, volume_key_size);
+ 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 = sym_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 = sym_crypt_format(
+ cd,
+ CRYPT_LUKS2,
+ user_record_luks_cipher(hr),
+ user_record_luks_cipher_mode(hr),
+ SD_ID128_TO_UUID_STRING(uuid),
+ volume_key,
+ volume_key_size,
+ &(struct crypt_params_luks2) {
+ .label = label,
+ .subsystem = "systemd-home",
+ .sector_size = user_record_luks_sector_size(hr),
+ .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 (password_cache_contains(cache, *pp)) { /* is this a fido2 or pkcs11 password? */
+ log_debug("Using minimal PBKDF for slot %i", slot);
+ r = sym_crypt_set_pbkdf_type(cd, &minimal_pbkdf);
+ } else {
+ log_debug("Using good PBKDF for slot %i", slot);
+ r = sym_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 = sym_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 = sym_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|USER_RECORD_PERMISSIVE, &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 = sym_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;
+}
+
+static int make_partition_table(
+ int fd,
+ uint32_t sector_size,
+ 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 *disk_uuid_as_string = NULL;
+ uint64_t offset, size, first_lba, start, last_lba, end;
+ sd_id128_t disk_uuid;
+ 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, SD_GPT_USER_HOME_STR);
+ if (r < 0)
+ return log_error_errno(r, "Failed to initialize partition type: %m");
+
+ r = fdisk_new_context_at(fd, /* path= */ NULL, /* read_only= */ false, sector_size, &c);
+ 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_partno_follow_default(p, 1);
+ if (r < 0)
+ return log_error_errno(r, "Failed to place partition at first free partition index: %m");
+
+ first_lba = fdisk_get_first_lba(c); /* Boundary where usable space starts */
+ assert(first_lba <= UINT64_MAX/512);
+ start = DISK_SIZE_ROUND_UP(first_lba * 512); /* Round up to multiple of 4K */
+
+ log_debug("Starting partition at offset %" PRIu64, start);
+
+ if (start == UINT64_MAX)
+ return log_error_errno(SYNTHETIC_ERRNO(ERANGE), "Overflow while rounding up start LBA.");
+
+ last_lba = fdisk_get_last_lba(c); /* One sector before boundary where usable space ends */
+ assert(last_lba < UINT64_MAX/512);
+ end = DISK_SIZE_ROUND_DOWN((last_lba + 1) * 512); /* Round down to multiple of 4K */
+
+ if (end <= start)
+ return log_error_errno(SYNTHETIC_ERRNO(ERANGE), "Resulting partition size zero or negative.");
+
+ r = fdisk_partition_set_start(p, start / 512);
+ if (r < 0)
+ return log_error_errno(r, "Failed to place partition at offset %" PRIu64 ": %m", start);
+
+ r = fdisk_partition_set_size(p, (end - start) / 512);
+ if (r < 0)
+ return log_error_errno(r, "Failed to end partition at offset %" PRIu64 ": %m", end);
+
+ 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, SD_ID128_TO_UUID_STRING(uuid));
+ 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 = -EBADF;
+ 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");
+ }
+
+ r = path_extract_directory(path, &dn);
+ if (r < 0)
+ return log_error_errno(r, "Failed to extract directory from device node path '%s': %m", path);
+ for (;;) {
+ _cleanup_free_ char *ndn = NULL;
+
+ 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;
+
+ r = path_extract_directory(dn, &ndn);
+ if (r == -EADDRNOTAVAIL) /* Arrived at the top? */
+ break;
+ if (r < 0)
+ return log_error_errno(r, "Failed to extract directory from device node path '%s': %m", dn);
+
+ free_and_replace(dn, ndn);
+ }
+
+ 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, until - w);
+ if (ERRNO_IS_NEG_TRANSIENT(r))
+ continue;
+ if (r < 0)
+ return log_error_errno(r, "Failed to watch inotify: %m");
+
+ (void) flush_fd(inotify_fd);
+ }
+}
+
+static int calculate_initial_image_size(UserRecord *h, int image_fd, const char *fstype, uint64_t *ret) {
+ uint64_t upper_boundary, lower_boundary;
+ struct statfs sfs;
+
+ assert(h);
+ assert(image_fd >= 0);
+ assert(ret);
+
+ if (fstatfs(image_fd, &sfs) < 0)
+ return log_error_errno(errno, "statfs() on image failed: %m");
+
+ upper_boundary = DISK_SIZE_ROUND_DOWN((uint64_t) sfs.f_bsize * sfs.f_bavail);
+
+ if (h->disk_size != UINT64_MAX)
+ *ret = MIN(DISK_SIZE_ROUND_DOWN(h->disk_size), upper_boundary);
+ else if (h->disk_size_relative == UINT64_MAX) {
+
+ if (upper_boundary > UINT64_MAX / USER_DISK_SIZE_DEFAULT_PERCENT)
+ return log_error_errno(SYNTHETIC_ERRNO(EOVERFLOW), "Disk size too large.");
+
+ *ret = DISK_SIZE_ROUND_DOWN(upper_boundary * 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(*ret));
+ } else {
+ *ret = DISK_SIZE_ROUND_DOWN((uint64_t) ((double) upper_boundary * (double) CLAMP(h->disk_size_relative, 0U, UINT32_MAX) / (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(*ret));
+ }
+
+ lower_boundary = minimal_size_by_fs_name(fstype);
+ if (lower_boundary != UINT64_MAX) {
+ assert(GPT_LUKS2_OVERHEAD < UINT64_MAX - lower_boundary);
+ lower_boundary += GPT_LUKS2_OVERHEAD;
+ }
+ if (lower_boundary == UINT64_MAX || lower_boundary < USER_DISK_SIZE_MIN)
+ lower_boundary = USER_DISK_SIZE_MIN;
+
+ if (*ret < lower_boundary)
+ *ret = lower_boundary;
+
+ return 0;
+}
+
+static int home_truncate(
+ UserRecord *h,
+ int fd,
+ uint64_t size) {
+
+ bool trunc;
+ int r;
+
+ assert(h);
+ assert(fd >= 0);
+
+ 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_debug_errno(errno, "Not enough disk space to allocate home of size %s.", FORMAT_BYTES(size));
+ return -ENOSPC; /* make recognizable */
+ }
+
+ return log_error_errno(errno, "Failed to truncate home image: %m");
+ }
+
+ return !trunc; /* Return == 0 if we managed to truncate, > 0 if we managed to allocate */
+}
+
+int home_create_luks(
+ UserRecord *h,
+ HomeSetup *setup,
+ const PasswordCache *cache,
+ char **effective_passwords,
+ UserRecord **ret_home) {
+
+ _cleanup_free_ char *subdir = NULL, *disk_uuid_path = NULL;
+ uint64_t encrypted_size,
+ host_size = 0, partition_offset = 0, partition_size = 0; /* Unnecessary initialization to appease gcc */
+ _cleanup_(user_record_unrefp) UserRecord *new_home = NULL;
+ sd_id128_t partition_uuid, fs_uuid, luks_uuid, disk_uuid;
+ _cleanup_close_ int mount_fd = -EBADF;
+ const char *fstype, *ip;
+ struct statfs sfs;
+ int r;
+ _cleanup_strv_free_ char **extra_mkfs_options = NULL;
+
+ assert(h);
+ assert(h->storage < 0 || h->storage == USER_LUKS);
+ assert(setup);
+ assert(!setup->temporary_image_path);
+ assert(setup->image_fd < 0);
+ assert(ret_home);
+
+ r = dlopen_cryptsetup();
+ if (r < 0)
+ return r;
+
+ 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, setup);
+ 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);
+
+ 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. a USB stick or such */
+
+ setup->image_fd = open_image_file(h, ip, &st);
+ if (setup->image_fd < 0)
+ return setup->image_fd;
+
+ 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/" DEVNUM_FORMAT_STR "/partition", DEVNUM_FORMAT_VAL(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(setup->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(setup->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, LESS_BY(host_size, GPT_LUKS2_OVERHEAD)))
+ 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(setup->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 *t = NULL;
+
+ r = mkdir_parents(ip, 0755);
+ if (r < 0)
+ return log_error_errno(r, "Failed to create parent directory of %s: %m", ip);
+
+ r = tempfn_random(ip, "homework", &t);
+ if (r < 0)
+ return log_error_errno(r, "Failed to derive temporary file name for %s: %m", ip);
+
+ setup->image_fd = open(t, O_RDWR|O_CREAT|O_EXCL|O_CLOEXEC|O_NOCTTY|O_NOFOLLOW, 0600);
+ if (setup->image_fd < 0)
+ return log_error_errno(errno, "Failed to create home image %s: %m", t);
+
+ setup->temporary_image_path = TAKE_PTR(t);
+
+ r = chattr_full(setup->image_fd, NULL, FS_NOCOW_FL|FS_NOCOMP_FL, FS_NOCOW_FL|FS_NOCOMP_FL, NULL, NULL, CHATTR_FALLBACK_BITWISE);
+ if (r < 0 && r != -ENOANO) /* ENOANO → some bits didn't work; which we skip logging about because chattr_full() already debug logs about those flags */
+ log_full_errno(ERRNO_IS_NOT_SUPPORTED(r) ? LOG_DEBUG : LOG_WARNING, r,
+ "Failed to set file attributes on %s, ignoring: %m", setup->temporary_image_path);
+
+ r = calculate_initial_image_size(h, setup->image_fd, fstype, &host_size);
+ if (r < 0)
+ return r;
+
+ r = resize_image_loop(h, setup, 0, host_size, &host_size);
+ if (r < 0)
+ return r;
+
+ log_info("Allocating image file completed.");
+ }
+
+ r = make_partition_table(
+ setup->image_fd,
+ user_record_luks_sector_size(h),
+ user_record_user_name_and_realm(h),
+ partition_uuid,
+ &partition_offset,
+ &partition_size,
+ &disk_uuid);
+ if (r < 0)
+ return r;
+
+ log_info("Writing of partition table completed.");
+
+ r = loop_device_make(
+ setup->image_fd,
+ O_RDWR,
+ partition_offset,
+ partition_size,
+ user_record_luks_sector_size(h),
+ 0,
+ LOCK_EX,
+ &setup->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.");
+ return -ENOLINK; /* Make recognizable */
+ }
+
+ return log_error_errno(r, "Failed to set up loopback device for %s: %m", setup->temporary_image_path);
+ }
+
+ log_info("Setting up loopback device %s completed.", setup->loop->node ?: ip);
+
+ r = luks_format(setup->loop->node,
+ setup->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,
+ &setup->crypt_device);
+ if (r < 0)
+ return r;
+
+ setup->undo_dm = true;
+
+ r = block_get_size_by_path(setup->dm_node, &encrypted_size);
+ if (r < 0)
+ return log_error_errno(r, "Failed to get encrypted block device size: %m");
+
+ log_info("Setting up LUKS device %s completed.", setup->dm_node);
+
+ r = mkfs_options_from_env("HOME", fstype, &extra_mkfs_options);
+ if (r < 0)
+ return log_error_errno(r, "Failed to determine mkfs command line options for '%s': %m", fstype);
+
+ r = make_filesystem(setup->dm_node,
+ fstype,
+ user_record_user_name_and_realm(h),
+ /* root = */ NULL,
+ fs_uuid,
+ user_record_luks_discard(h),
+ /* quiet = */ true,
+ /* sector_size = */ 0,
+ extra_mkfs_options);
+ if (r < 0)
+ return r;
+
+ log_info("Formatting file system completed.");
+
+ r = home_unshare_and_mount(setup->dm_node, fstype, user_record_luks_discard(h), user_record_mount_flags(h), h->luks_extra_mount_options);
+ if (r < 0)
+ return r;
+
+ setup->undo_mount = true;
+
+ subdir = path_join(HOME_RUNTIME_WORK_DIR, user_record_user_name_and_realm(h));
+ if (!subdir)
+ return log_oom();
+
+ /* Prefer using a btrfs subvolume if we can, fall back to directory otherwise */
+ r = btrfs_subvol_make_fallback(AT_FDCWD, subdir, 0700);
+ if (r < 0)
+ return log_error_errno(r, "Failed to create user directory in mounted image file: %m");
+
+ setup->root_fd = open(subdir, O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW);
+ if (setup->root_fd < 0)
+ return log_error_errno(errno, "Failed to open user directory in mounted image file: %m");
+
+ (void) home_shift_uid(setup->root_fd, NULL, UID_NOBODY, h->uid, &mount_fd);
+
+ if (mount_fd >= 0) {
+ /* If we have established a new mount, then we can use that as new root fd to our home directory. */
+ safe_close(setup->root_fd);
+
+ setup->root_fd = fd_reopen(mount_fd, O_RDONLY|O_CLOEXEC|O_DIRECTORY);
+ if (setup->root_fd < 0)
+ return log_error_errno(setup->root_fd, "Unable to convert mount fd into proper directory fd: %m");
+
+ mount_fd = safe_close(mount_fd);
+ }
+
+ r = home_populate(h, setup->root_fd);
+ if (r < 0)
+ return r;
+
+ r = home_sync_and_statfs(setup->root_fd, &sfs);
+ if (r < 0)
+ return r;
+
+ r = user_record_clone(h, USER_RECORD_LOAD_MASK_SECRET|USER_RECORD_LOG|USER_RECORD_PERMISSIVE, &new_home);
+ if (r < 0)
+ return log_error_errno(r, "Failed to clone record: %m");
+
+ r = user_record_add_binding(
+ new_home,
+ USER_LUKS,
+ disk_uuid_path ?: ip,
+ partition_uuid,
+ luks_uuid,
+ fs_uuid,
+ sym_crypt_get_cipher(setup->crypt_device),
+ sym_crypt_get_cipher_mode(setup->crypt_device),
+ luks_volume_key_size_convert(setup->crypt_device),
+ fstype,
+ NULL,
+ h->uid,
+ (gid_t) h->uid);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add binding to record: %m");
+
+ if (user_record_luks_offline_discard(h)) {
+ r = run_fitrim(setup->root_fd);
+ if (r < 0)
+ return r;
+ }
+
+ setup->root_fd = safe_close(setup->root_fd);
+
+ r = home_setup_undo_mount(setup, LOG_ERR);
+ if (r < 0)
+ return r;
+
+ r = home_setup_undo_dm(setup, LOG_ERR);
+ if (r < 0)
+ return r;
+
+ setup->loop = loop_device_unref(setup->loop);
+
+ if (!user_record_luks_offline_discard(h)) {
+ r= run_fallocate(setup->image_fd, NULL /* refresh stat() data */);
+ if (r < 0)
+ return r;
+ }
+
+ /* Sync everything to disk before we move things into place under the final name. */
+ if (fsync(setup->image_fd) < 0)
+ return log_error_errno(r, "Failed to synchronize image to disk: %m");
+
+ if (disk_uuid_path)
+ /* Reread partition table if this is a block device */
+ (void) ioctl(setup->image_fd, BLKRRPART, 0);
+ else {
+ assert(setup->temporary_image_path);
+
+ if (rename(setup->temporary_image_path, ip) < 0)
+ return log_error_errno(errno, "Failed to rename image file: %m");
+
+ setup->temporary_image_path = mfree(setup->temporary_image_path);
+
+ /* If we operate on a file, sync the containing directory too. */
+ r = fsync_directory_of_file(setup->image_fd);
+ if (r < 0)
+ return log_error_errno(r, "Failed to synchronize directory of image file to disk: %m");
+
+ log_info("Moved image file into place.");
+ }
+
+ /* 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 */
+ setup->image_fd = safe_close(setup->image_fd);
+
+ if (disk_uuid_path)
+ (void) wait_for_devlink(disk_uuid_path);
+
+ log_info("Creation completed.");
+
+ print_size_summary(host_size, encrypted_size, &sfs);
+
+ log_debug("GPT + LUKS2 overhead is %" PRIu64 " (expected %" PRIu64 ")", host_size - encrypted_size, GPT_LUKS2_OVERHEAD);
+
+ *ret_home = TAKE_PTR(new_home);
+ return 0;
+}
+
+int home_get_state_luks(UserRecord *h, HomeSetup *setup) {
+ int r;
+
+ assert(h);
+ assert(setup);
+
+ r = make_dm_names(h, setup);
+ if (r < 0)
+ return r;
+
+ r = access(setup->dm_node, F_OK);
+ if (r < 0 && errno != ENOENT)
+ return log_error_errno(errno, "Failed to determine whether %s exists: %m", setup->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,
+ const char *extra_mount_options) {
+
+ _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 = home_setup_undo_mount(setup, LOG_ERR);
+ if (r < 0)
+ return r;
+
+ 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_SIGTERM|FORK_LOG|FORK_STDOUT_TO_STDERR|FORK_CLOSE_ALL_FDS,
+ &fsck_pid);
+ if (r < 0)
+ return r;
+ if (r == 0) {
+ /* Child */
+ execlp("e2fsck" ,"e2fsck", "-fp", setup->dm_node, NULL);
+ log_open();
+ 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_SIGTERM|FORK_LOG|FORK_WAIT|FORK_STDOUT_TO_STDERR|FORK_CLOSE_ALL_FDS,
+ &resize_pid);
+ if (r < 0)
+ return r;
+ if (r == 0) {
+ /* Child */
+ execlp("resize2fs" ,"resize2fs", setup->dm_node, size_str, NULL);
+ log_open();
+ 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, extra_mount_options);
+ if (r < 0)
+ return r;
+
+ setup->undo_mount = true;
+ }
+
+ if (re_open) {
+ setup->root_fd = open(HOME_RUNTIME_WORK_DIR, 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,
+ sd_id128_t *ret_disk_uuid,
+ struct fdisk_table **ret_table,
+ struct fdisk_partition **ret_partition) {
+
+ _cleanup_(fdisk_unref_contextp) struct fdisk_context *c = NULL;
+ _cleanup_(fdisk_unref_tablep) struct fdisk_table *t = NULL;
+ _cleanup_free_ char *disk_uuid_as_string = NULL;
+ struct fdisk_partition *found = NULL;
+ sd_id128_t disk_uuid;
+ size_t n_partitions;
+ int r;
+
+ assert(fd >= 0);
+ assert(ret_disk_uuid);
+ assert(ret_table);
+
+ assert((partition_offset & 511) == 0);
+ assert((old_partition_size & 511) == 0);
+ assert(UINT64_MAX - old_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;
+ *ret_partition = NULL;
+ return 0;
+ }
+
+ r = fdisk_new_context_at(fd, /* path= */ NULL, /* read_only= */ false, UINT32_MAX, &c);
+ 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 (size_t 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 = p;
+ } else if (fdisk_partition_get_end(p) > partition_offset / 512U)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Can't extend, not last partition in image.");
+ }
+
+ if (!found)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOPKG), "Failed to find matching partition to resize.");
+
+ *ret_disk_uuid = disk_uuid;
+ *ret_table = TAKE_PTR(t);
+ *ret_partition = found;
+
+ return 1;
+}
+
+static int get_maximum_partition_size(
+ int fd,
+ struct fdisk_partition *p,
+ uint64_t *ret_maximum_partition_size) {
+
+ _cleanup_(fdisk_unref_contextp) struct fdisk_context *c = NULL;
+ uint64_t start_lba, start, last_lba, end;
+ int r;
+
+ assert(fd >= 0);
+ assert(p);
+ assert(ret_maximum_partition_size);
+
+ r = fdisk_new_context_at(fd, /* path= */ NULL, /* read_only= */ true, /* sector_size= */ UINT32_MAX, &c);
+ if (r < 0)
+ return log_error_errno(r, "Failed to create fdisk context: %m");
+
+ start_lba = fdisk_partition_get_start(p);
+ assert(start_lba <= UINT64_MAX/512);
+ start = start_lba * 512;
+
+ last_lba = fdisk_get_last_lba(c); /* One sector before boundary where usable space ends */
+ assert(last_lba < UINT64_MAX/512);
+ end = DISK_SIZE_ROUND_DOWN((last_lba + 1) * 512); /* Round down to multiple of 4K */
+
+ if (start > end)
+ return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Last LBA is before partition start.");
+
+ *ret_maximum_partition_size = DISK_SIZE_ROUND_DOWN(end - start);
+
+ 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, sd_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,
+ struct fdisk_partition *p,
+ size_t new_partition_size) {
+
+ _cleanup_(fdisk_unref_contextp) struct fdisk_context *c = NULL;
+ _cleanup_free_ void *two_zero_lbas = NULL;
+ uint32_t ssz;
+ ssize_t n;
+ int r;
+
+ assert(fd >= 0);
+ assert(!t == !p);
+
+ if (!t) /* no partition table to apply, exit early */
+ return 0;
+
+ assert(p);
+
+ /* Before writing our partition patch the final size in */
+ 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");
+
+ r = probe_sector_size(fd, &ssz);
+ if (r < 0)
+ return log_error_errno(r, "Failed to determine current sector size: %m");
+
+ two_zero_lbas = malloc0(ssz * 2);
+ 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, ssz * 2, 0);
+ if (n < 0)
+ return log_error_errno(errno, "Failed to wipe partition table: %m");
+ if ((size_t) n != ssz * 2)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO), "Short write while wiping partition table.");
+
+ r = fdisk_new_context_at(fd, /* path= */ NULL, /* read_only= */ false, ssz, &c);
+ 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;
+}
+
+/* Always keep at least 16M free, so that we can safely log in and update the user record while doing so */
+#define HOME_MIN_FREE (16U*1024U*1024U)
+
+static int get_smallest_fs_size(int fd, uint64_t *ret) {
+ uint64_t minsz, needed;
+ struct statfs sfs;
+
+ assert(fd >= 0);
+ assert(ret);
+
+ /* Determines the minimal disk size we might be able to shrink the file system referenced by the fd to. */
+
+ if (syncfs(fd) < 0) /* let's sync before we query the size, so that the values returned are accurate */
+ return log_error_errno(errno, "Failed to synchronize home file system: %m");
+
+ if (fstatfs(fd, &sfs) < 0)
+ return log_error_errno(errno, "Failed to statfs() home file system: %m");
+
+ /* Let's determine the minimal file system size of the used fstype */
+ minsz = minimal_size_by_fs_magic(sfs.f_type);
+ if (minsz == UINT64_MAX)
+ return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Don't know minimum file system size of file system type '%s' of home directory.", fs_type_to_string(sfs.f_type));
+
+ if (minsz < USER_DISK_SIZE_MIN)
+ minsz = USER_DISK_SIZE_MIN;
+
+ if (sfs.f_bfree > sfs.f_blocks)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Detected amount of free blocks is greater than the total amount of file system blocks. Refusing.");
+
+ /* Calculate how much disk space is currently in use. */
+ needed = sfs.f_blocks - sfs.f_bfree;
+ if (needed > UINT64_MAX / sfs.f_bsize)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "File system size out of range.");
+
+ needed *= sfs.f_bsize;
+
+ /* Add some safety margin of free space we'll always keep */
+ if (needed > UINT64_MAX - HOME_MIN_FREE) /* Check for overflow */
+ needed = UINT64_MAX;
+ else
+ needed += HOME_MIN_FREE;
+
+ *ret = DISK_SIZE_ROUND_UP(MAX(needed, minsz));
+ return 0;
+}
+
+static int get_largest_image_size(int fd, const struct stat *st, uint64_t *ret) {
+ uint64_t used, avail, sum;
+ struct statfs sfs;
+ int r;
+
+ assert(fd >= 0);
+ assert(st);
+ assert(ret);
+
+ /* Determines the maximum file size we might be able to grow the image file referenced by the fd to. */
+
+ r = stat_verify_regular(st);
+ if (r < 0)
+ return log_error_errno(r, "Image file is not a regular file, refusing: %m");
+
+ if (syncfs(fd) < 0)
+ return log_error_errno(errno, "Failed to synchronize file system backing image file: %m");
+
+ if (fstatfs(fd, &sfs) < 0)
+ return log_error_errno(errno, "Failed to statfs() image file: %m");
+
+ used = (uint64_t) st->st_blocks * 512;
+ avail = (uint64_t) sfs.f_bsize * sfs.f_bavail;
+
+ if (avail > UINT64_MAX - used)
+ sum = UINT64_MAX;
+ else
+ sum = avail + used;
+
+ *ret = DISK_SIZE_ROUND_DOWN(MIN(sum, USER_DISK_SIZE_MAX));
+ return 0;
+}
+
+static int resize_fs_loop(
+ UserRecord *h,
+ HomeSetup *setup,
+ int resize_type,
+ uint64_t old_fs_size,
+ uint64_t new_fs_size,
+ uint64_t *ret_fs_size) {
+
+ uint64_t current_fs_size;
+ unsigned n_iterations = 0;
+ int r;
+
+ assert(h);
+ assert(setup);
+ assert(setup->root_fd >= 0);
+
+ /* A bisection loop trying to find the closest size to what the user asked for. (Well, we bisect like
+ * this only when we *shrink* the fs — if we grow the fs there's no need to bisect.) */
+
+ current_fs_size = old_fs_size;
+ for (uint64_t lower_boundary = new_fs_size, upper_boundary = old_fs_size, try_fs_size = new_fs_size;;) {
+ bool worked;
+
+ n_iterations++;
+
+ /* Now resize the file system */
+ if (resize_type == CAN_RESIZE_ONLINE) {
+ r = resize_fs(setup->root_fd, try_fs_size, NULL);
+ if (r < 0) {
+ if (!ERRNO_IS_DISK_SPACE(r) || new_fs_size > old_fs_size) /* Not a disk space issue? Not trying to shrink? */
+ return log_error_errno(r, "Failed to resize file system: %m");
+
+ log_debug_errno(r, "Shrinking from %s to %s didn't work, not enough space for contained data.", FORMAT_BYTES(current_fs_size), FORMAT_BYTES(try_fs_size));
+ worked = false;
+ } else {
+ log_debug("Successfully resized from %s to %s.", FORMAT_BYTES(current_fs_size), FORMAT_BYTES(try_fs_size));
+ current_fs_size = try_fs_size;
+ worked = true;
+ }
+
+ /* If we hit a disk space issue and are shrinking the fs, then maybe it helps to
+ * increase the image size. */
+ } else {
+ r = ext4_offline_resize_fs(setup, try_fs_size, user_record_luks_discard(h), user_record_mount_flags(h), h->luks_extra_mount_options);
+ if (r < 0)
+ return r;
+
+ /* For now, when we fail to shrink an ext4 image we'll not try again via the
+ * bisection logic. We might add that later, but given this involves shelling out
+ * multiple programs, it's a bit too cumbersome for my taste. */
+
+ worked = true;
+ current_fs_size = try_fs_size;
+ }
+
+ if (new_fs_size > old_fs_size) /* If we are growing we are done after one iteration */
+ break;
+
+ /* If we are shrinking then let's adjust our bisection boundaries and try again. */
+ if (worked)
+ upper_boundary = MIN(upper_boundary, try_fs_size);
+ else
+ lower_boundary = MAX(lower_boundary, try_fs_size);
+
+ /* OK, this attempt to shrink didn't work. Let's try between the old size and what worked. */
+ if (lower_boundary >= upper_boundary) {
+ log_debug("Image can't be shrunk further (range to try is empty).");
+ break;
+ }
+
+ /* Let's find a new value to try half-way between the lower boundary and the upper boundary
+ * to try now. */
+ try_fs_size = DISK_SIZE_ROUND_DOWN(lower_boundary + (upper_boundary - lower_boundary) / 2);
+ if (try_fs_size <= lower_boundary || try_fs_size >= upper_boundary) {
+ log_debug("Image can't be shrunk further (remaining range to try too small).");
+ break;
+ }
+ }
+
+ log_debug("Bisection loop completed after %u iterations.", n_iterations);
+
+ if (ret_fs_size)
+ *ret_fs_size = current_fs_size;
+
+ return 0;
+}
+
+static int resize_image_loop(
+ UserRecord *h,
+ HomeSetup *setup,
+ uint64_t old_image_size,
+ uint64_t new_image_size,
+ uint64_t *ret_image_size) {
+
+ uint64_t current_image_size;
+ unsigned n_iterations = 0;
+ int r;
+
+ assert(h);
+ assert(setup);
+ assert(setup->image_fd >= 0);
+
+ /* A bisection loop trying to find the closest size to what the user asked for. (Well, we bisect like
+ * this only when we *grow* the image — if we shrink the image then there's no need to bisect.) */
+
+ current_image_size = old_image_size;
+ for (uint64_t lower_boundary = old_image_size, upper_boundary = new_image_size, try_image_size = new_image_size;;) {
+ bool worked;
+
+ n_iterations++;
+
+ r = home_truncate(h, setup->image_fd, try_image_size);
+ if (r < 0) {
+ if (!ERRNO_IS_DISK_SPACE(r) || new_image_size < old_image_size) /* Not a disk space issue? Not trying to grow? */
+ return r;
+
+ log_debug_errno(r, "Growing from %s to %s didn't work, not enough space on backing disk.", FORMAT_BYTES(current_image_size), FORMAT_BYTES(try_image_size));
+ worked = false;
+ } else if (r > 0) { /* Success: allocation worked */
+ log_debug("Resizing from %s to %s via allocation worked successfully.", FORMAT_BYTES(current_image_size), FORMAT_BYTES(try_image_size));
+ current_image_size = try_image_size;
+ worked = true;
+ } else { /* Success, but through truncation, not allocation. */
+ log_debug("Resizing from %s to %s via truncation worked successfully.", FORMAT_BYTES(old_image_size), FORMAT_BYTES(try_image_size));
+ current_image_size = try_image_size;
+ break; /* there's no point in the bisection logic if this was plain truncation and
+ * not allocation, let's exit immediately. */
+ }
+
+ if (new_image_size < old_image_size) /* If we are shrinking we are done after one iteration */
+ break;
+
+ /* If we are growing then let's adjust our bisection boundaries and try again */
+ if (worked)
+ lower_boundary = MAX(lower_boundary, try_image_size);
+ else
+ upper_boundary = MIN(upper_boundary, try_image_size);
+
+ if (lower_boundary >= upper_boundary) {
+ log_debug("Image can't be grown further (range to try is empty).");
+ break;
+ }
+
+ try_image_size = DISK_SIZE_ROUND_DOWN(lower_boundary + (upper_boundary - lower_boundary) / 2);
+ if (try_image_size <= lower_boundary || try_image_size >= upper_boundary) {
+ log_debug("Image can't be grown further (remaining range to try too small).");
+ break;
+ }
+ }
+
+ log_debug("Bisection loop completed after %u iterations.", n_iterations);
+
+ if (ret_image_size)
+ *ret_image_size = current_image_size;
+
+ return 0;
+}
+
+int home_resize_luks(
+ UserRecord *h,
+ HomeSetupFlags flags,
+ HomeSetup *setup,
+ PasswordCache *cache,
+ UserRecord **ret_home) {
+
+ uint64_t old_image_size, new_image_size, old_fs_size, new_fs_size, crypto_offset, crypto_offset_bytes,
+ new_partition_size, smallest_fs_size, resized_fs_size;
+ _cleanup_(user_record_unrefp) UserRecord *header_home = NULL, *embedded_home = NULL, *new_home = NULL;
+ _cleanup_(fdisk_unref_tablep) struct fdisk_table *table = NULL;
+ struct fdisk_partition *partition = NULL;
+ _cleanup_close_ int opened_image_fd = -EBADF;
+ _cleanup_free_ char *whole_disk = NULL;
+ int r, resize_type, image_fd = -EBADF;
+ sd_id128_t disk_uuid;
+ const char *ip, *ipo;
+ struct statfs sfs;
+ struct stat st;
+ enum {
+ INTENTION_DONT_KNOW = 0, /* These happen to match the return codes of CMP() */
+ INTENTION_SHRINK = -1,
+ INTENTION_GROW = 1,
+ } intention = INTENTION_DONT_KNOW;
+
+ assert(h);
+ assert(user_record_storage(h) == USER_LUKS);
+ assert(setup);
+
+ r = dlopen_cryptsetup();
+ if (r < 0)
+ return r;
+
+ assert_se(ipo = user_record_image_path(h));
+ ip = strdupa_safe(ipo); /* copy out since original might change later in home record object */
+
+ if (setup->image_fd < 0) {
+ setup->image_fd = open_image_file(h, NULL, &st);
+ if (setup->image_fd < 0)
+ return setup->image_fd;
+ } else {
+ if (fstat(setup->image_fd, &st) < 0)
+ return log_error_errno(errno, "Failed to stat image file %s: %m", ip);
+ }
+
+ image_fd = setup->image_fd;
+
+ 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);
+
+ opened_image_fd = r = device_open_from_devnum(S_IFBLK, parent, O_RDWR|O_CLOEXEC|O_NOCTTY|O_NONBLOCK, &whole_disk);
+ if (r < 0)
+ return log_error_errno(r, "Failed to open whole block device for %s: %m", ip);
+
+ image_fd = opened_image_fd;
+
+ if (fstat(image_fd, &st) < 0)
+ return log_error_errno(errno, "Failed to stat whole block device %s: %m", 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 asymmetry 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. */
+
+ if (FLAGS_SET(flags, HOME_SETUP_RESIZE_MINIMIZE)) {
+ new_image_size = 0;
+ intention = INTENTION_SHRINK;
+ } else {
+ uint64_t new_image_size_rounded;
+
+ new_image_size_rounded = DISK_SIZE_ROUND_DOWN(h->disk_size);
+
+ if (old_image_size >= new_image_size_rounded && old_image_size <= h->disk_size) {
+ /* If exact match, or a match after we rounded down, don't do a thing */
+ log_info("Image size already matching, skipping operation.");
+ return 0;
+ }
+
+ new_image_size = new_image_size_rounded;
+ intention = CMP(new_image_size, old_image_size); /* Is this a shrink */
+ }
+ }
+
+ r = home_setup_luks(
+ h,
+ flags,
+ whole_disk,
+ setup,
+ cache,
+ FLAGS_SET(flags, HOME_SETUP_RESIZE_DONT_SYNC_IDENTITIES) ? NULL : &header_home);
+ if (r < 0)
+ return r;
+
+ if (!FLAGS_SET(flags, HOME_SETUP_RESIZE_DONT_SYNC_IDENTITIES)) {
+ 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;
+ }
+
+ r = home_maybe_shift_uid(h, flags, setup);
+ 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.");
+
+ /* Get target partition information in here for new_partition_size calculation */
+ r = prepare_resize_partition(
+ image_fd,
+ setup->partition_offset,
+ setup->partition_size,
+ &disk_uuid,
+ &table,
+ &partition);
+ if (r < 0)
+ return r;
+
+ if (S_ISREG(st.st_mode)) {
+ uint64_t partition_table_extra, largest_size;
+
+ partition_table_extra = old_image_size - setup->partition_size;
+
+ r = get_largest_image_size(setup->image_fd, &st, &largest_size);
+ if (r < 0)
+ return r;
+ if (new_image_size > largest_size)
+ new_image_size = largest_size;
+
+ if (new_image_size < partition_table_extra)
+ new_image_size = partition_table_extra;
+
+ new_partition_size = DISK_SIZE_ROUND_DOWN(new_image_size - partition_table_extra);
+ } else {
+ assert(S_ISBLK(st.st_mode));
+
+ if (FLAGS_SET(flags, HOME_SETUP_RESIZE_MINIMIZE)) {
+ new_partition_size = 0;
+ intention = INTENTION_SHRINK;
+ } else {
+ uint64_t new_partition_size_rounded = DISK_SIZE_ROUND_DOWN(h->disk_size);
+
+ if (h->disk_size == UINT64_MAX && partition) {
+ r = get_maximum_partition_size(image_fd, partition, &new_partition_size_rounded);
+ if (r < 0)
+ return r;
+ }
+
+ if (setup->partition_size >= new_partition_size_rounded &&
+ setup->partition_size <= h->disk_size) {
+ log_info("Partition size already matching, skipping operation.");
+ return 0;
+ }
+
+ new_partition_size = new_partition_size_rounded;
+ intention = CMP(new_partition_size, setup->partition_size);
+ }
+ }
+
+ 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 = sym_crypt_get_data_offset(setup->crypt_device);
+ if (crypto_offset > UINT64_MAX/512U)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "LUKS2 data offset out of range, refusing.");
+ crypto_offset_bytes = (uint64_t) crypto_offset * 512U;
+ if (setup->partition_size <= crypto_offset_bytes)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Weird, old crypto payload offset doesn't actually fit in partition size?");
+
+ /* Make sure at least the LUKS header fit in */
+ if (new_partition_size <= crypto_offset_bytes) {
+ uint64_t add;
+
+ add = DISK_SIZE_ROUND_UP(crypto_offset_bytes) - new_partition_size;
+ new_partition_size += add;
+ if (S_ISREG(st.st_mode))
+ new_image_size += add;
+ }
+
+ old_fs_size = setup->partition_size - crypto_offset_bytes;
+ new_fs_size = DISK_SIZE_ROUND_DOWN(new_partition_size - crypto_offset_bytes);
+
+ r = get_smallest_fs_size(setup->root_fd, &smallest_fs_size);
+ if (r < 0)
+ return r;
+
+ if (new_fs_size < smallest_fs_size) {
+ uint64_t add;
+
+ add = DISK_SIZE_ROUND_UP(smallest_fs_size) - new_fs_size;
+ new_fs_size += add;
+ new_partition_size += add;
+ if (S_ISREG(st.st_mode))
+ new_image_size += add;
+ }
+
+ if (new_fs_size == old_fs_size) {
+ log_info("New file system size identical to old file system size, skipping operation.");
+ return 0;
+ }
+
+ if (FLAGS_SET(flags, HOME_SETUP_RESIZE_DONT_GROW) && new_fs_size > old_fs_size) {
+ log_info("New file system size would be larger than old, but shrinking requested, skipping operation.");
+ return 0;
+ }
+
+ if (FLAGS_SET(flags, HOME_SETUP_RESIZE_DONT_SHRINK) && new_fs_size < old_fs_size) {
+ log_info("New file system size would be smaller than old, but growing requested, skipping operation.");
+ return 0;
+ }
+
+ if (CMP(new_fs_size, old_fs_size) != intention) {
+ if (intention < 0)
+ log_info("Shrink operation would enlarge file system, skipping operation.");
+ else {
+ assert(intention > 0);
+ log_info("Grow operation would shrink file system, skipping operation.");
+ }
+ return 0;
+ }
+
+ /* 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 && FLAGS_SET(flags, HOME_SETUP_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 %s, partition size %s %s %s, file system size %s %s %s.",
+ FORMAT_BYTES(old_image_size),
+ special_glyph(SPECIAL_GLYPH_ARROW_RIGHT),
+ FORMAT_BYTES(new_image_size),
+ FORMAT_BYTES(setup->partition_size),
+ special_glyph(SPECIAL_GLYPH_ARROW_RIGHT),
+ FORMAT_BYTES(new_partition_size),
+ FORMAT_BYTES(old_fs_size),
+ special_glyph(SPECIAL_GLYPH_ARROW_RIGHT),
+ FORMAT_BYTES(new_fs_size));
+
+ if (new_fs_size > old_fs_size) { /* → Grow */
+
+ if (S_ISREG(st.st_mode)) {
+ uint64_t resized_image_size;
+
+ /* Grow file size */
+ r = resize_image_loop(h, setup, old_image_size, new_image_size, &resized_image_size);
+ if (r < 0)
+ return r;
+
+ if (resized_image_size == old_image_size) {
+ log_info("Couldn't change image size.");
+ return 0;
+ }
+
+ assert(resized_image_size > old_image_size);
+
+ log_info("Growing of image file from %s to %s completed.", FORMAT_BYTES(old_image_size), FORMAT_BYTES(resized_image_size));
+
+ if (resized_image_size < new_image_size) {
+ uint64_t sub;
+
+ /* If the growing we managed to do is smaller than what we wanted we need to
+ * adjust the partition/file system sizes we are going for, too */
+ sub = new_image_size - resized_image_size;
+ assert(new_partition_size >= sub);
+ new_partition_size -= sub;
+ assert(new_fs_size >= sub);
+ new_fs_size -= sub;
+ }
+
+ new_image_size = resized_image_size;
+ } else {
+ assert(S_ISBLK(st.st_mode));
+ assert(new_image_size == old_image_size);
+ }
+
+ /* 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, partition, new_partition_size);
+ if (r < 0)
+ return r;
+ if (r > 0)
+ log_info("Growing of partition completed.");
+
+ if (S_ISBLK(st.st_mode) && 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 = sym_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 {
+ /* → Shrink */
+
+ if (!FLAGS_SET(flags, HOME_SETUP_RESIZE_DONT_SYNC_IDENTITIES)) {
+ 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 try to resize the file system. The requested size might not always be possible, in which case
+ * we'll try to get as close as we can get. The result is returned in 'resized_fs_size' */
+ r = resize_fs_loop(h, setup, resize_type, old_fs_size, new_fs_size, &resized_fs_size);
+ if (r < 0)
+ return r;
+
+ if (resized_fs_size == old_fs_size) {
+ log_info("Couldn't change file system size.");
+ return 0;
+ }
+
+ log_info("File system resizing from %s to %s completed.", FORMAT_BYTES(old_fs_size), FORMAT_BYTES(resized_fs_size));
+
+ if (resized_fs_size > new_fs_size) {
+ uint64_t add;
+
+ /* If the shrinking we managed to do is larger than what we wanted we need to adjust the partition/image sizes. */
+ add = resized_fs_size - new_fs_size;
+ new_partition_size += add;
+ if (S_ISREG(st.st_mode))
+ new_image_size += add;
+ }
+
+ new_fs_size = resized_fs_size;
+
+ /* 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 */
+
+ /* Shrink the LUKS device now, matching the new file system size */
+ r = sym_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.");
+
+ /* 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.");
+
+ 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.");
+ } else {
+ assert(S_ISBLK(st.st_mode));
+ assert(new_image_size == old_image_size);
+ }
+
+ r = apply_resize_partition(image_fd, disk_uuid, table, partition, new_partition_size);
+ if (r < 0)
+ return r;
+ if (r > 0)
+ log_info("Shrinking of partition completed.");
+
+ if (S_ISBLK(st.st_mode) && ioctl(image_fd, BLKRRPART, 0) < 0)
+ log_debug_errno(errno, "BLKRRPART failed on block device, ignoring: %m");
+
+ } else { /* → Grow */
+ if (!FLAGS_SET(flags, HOME_SETUP_RESIZE_DONT_SYNC_IDENTITIES)) {
+ r = home_store_embedded_identity(new_home, setup->root_fd, h->uid, embedded_home);
+ if (r < 0)
+ return r;
+ }
+ }
+
+ if (!FLAGS_SET(flags, HOME_SETUP_RESIZE_DONT_SYNC_IDENTITIES)) {
+ 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;
+
+ if (!FLAGS_SET(flags, HOME_SETUP_RESIZE_DONT_UNDO)) {
+ r = home_setup_done(setup);
+ if (r < 0)
+ return r;
+ }
+
+ log_info("Resizing completed.");
+
+ print_size_summary(new_image_size, new_fs_size, &sfs);
+
+ if (ret_home)
+ *ret_home = TAKE_PTR(new_home);
+
+ return 0;
+}
+
+int home_passwd_luks(
+ UserRecord *h,
+ HomeSetupFlags flags,
+ HomeSetup *setup,
+ const PasswordCache *cache, /* the passwords acquired via PKCS#11/FIDO2 security tokens */
+ char **effective_passwords /* new passwords */) {
+
+ size_t volume_key_size, 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);
+
+ r = dlopen_cryptsetup();
+ if (r < 0)
+ return r;
+
+ type = sym_crypt_get_type(setup->crypt_device);
+ if (!type)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to determine crypto device type.");
+
+ r = sym_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 = sym_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 ? cache->keyring_passswords : NULL,
+ cache ? cache->pkcs11_passwords : NULL,
+ cache ? cache->fido2_passwords : NULL,
+ h->password) {
+
+ r = luks_try_passwords(h, setup->crypt_device, list, volume_key, &volume_key_size, NULL);
+ 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 unlock LUKS superblock: %m");
+
+ n_effective = strv_length(effective_passwords);
+
+ build_good_pbkdf(&good_pbkdf, h);
+ build_minimal_pbkdf(&minimal_pbkdf, h);
+
+ for (size_t i = 0; i < max_key_slots; i++) {
+ r = sym_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 (password_cache_contains(cache, effective_passwords[i])) { /* Is this a FIDO2 or PKCS#11 password? */
+ log_debug("Using minimal PBKDF for slot %zu", i);
+ r = sym_crypt_set_pbkdf_type(setup->crypt_device, &minimal_pbkdf);
+ } else {
+ log_debug("Using good PBKDF for slot %zu", i);
+ r = sym_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 = sym_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);
+
+ /* If we changed the password, then make sure to update the copy in the keyring, so that
+ * auto-rebalance continues to work. We only do this if we operate on an active home dir. */
+ if (i == 0 && FLAGS_SET(flags, HOME_SETUP_ALREADY_ACTIVATED))
+ upload_to_keyring(h, effective_passwords[i], NULL);
+ }
+
+ return 1;
+}
+
+int home_lock_luks(UserRecord *h, HomeSetup *setup) {
+ const char *p;
+ int r;
+
+ assert(h);
+ assert(setup);
+ assert(setup->root_fd < 0);
+ assert(!setup->crypt_device);
+
+ r = acquire_open_luks_device(h, setup, /* graceful= */ false);
+ if (r < 0)
+ return r;
+
+ log_info("Discovered used LUKS device %s.", setup->dm_node);
+
+ assert_se(p = user_record_home_directory(h));
+ r = syncfs_path(AT_FDCWD, p);
+ if (r < 0) /* Snake oil, but let's better be safe than sorry */
+ return log_error_errno(r, "Failed to synchronize file system %s: %m", p);
+
+ 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 = sym_crypt_suspend(setup->crypt_device, setup->dm_name);
+ if (r < 0)
+ return log_error_errno(r, "Failed to suspend cryptsetup device: %s: %m", setup->dm_node);
+
+ log_info("LUKS device suspended.");
+ return 0;
+}
+
+static int luks_try_resume(
+ struct crypt_device *cd,
+ const char *dm_name,
+ char **password) {
+
+ int r;
+
+ assert(cd);
+ assert(dm_name);
+
+ STRV_FOREACH(pp, password) {
+ r = sym_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, HomeSetup *setup, const PasswordCache *cache) {
+ char **list;
+ int r;
+
+ assert(h);
+ assert(setup);
+ assert(!setup->crypt_device);
+
+ r = acquire_open_luks_device(h, setup, /* graceful= */ false);
+ if (r < 0)
+ return r;
+
+ log_info("Discovered used LUKS device %s.", setup->dm_node);
+
+ r = -ENOKEY;
+ FOREACH_POINTER(list,
+ cache ? cache->pkcs11_passwords : NULL,
+ cache ? cache->fido2_passwords : NULL,
+ h->password) {
+ r = luks_try_resume(setup->crypt_device, setup->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;
+}
+
+static int device_is_gone(HomeSetup *setup) {
+ _cleanup_(sd_device_unrefp) sd_device *d = NULL;
+ struct stat st;
+ int r;
+
+ assert(setup);
+
+ if (!setup->dm_node)
+ return true;
+
+ if (stat(setup->dm_node, &st) < 0) {
+ if (errno != ENOENT)
+ return log_error_errno(errno, "Failed to stat block device node %s: %m", setup->dm_node);
+
+ return true;
+ }
+
+ r = sd_device_new_from_stat_rdev(&d, &st);
+ if (r < 0) {
+ if (r != -ENODEV)
+ return log_error_errno(errno, "Failed to allocate device object from block device node %s: %m", setup->dm_node);
+
+ return true;
+ }
+
+ return false;
+}
+
+static int device_monitor_handler(sd_device_monitor *monitor, sd_device *device, void *userdata) {
+ HomeSetup *setup = ASSERT_PTR(userdata);
+ int r;
+
+ if (!device_for_action(device, SD_DEVICE_REMOVE))
+ return 0;
+
+ /* We don't really care for the device object passed to us, we just check if the device node still
+ * exists */
+
+ r = device_is_gone(setup);
+ if (r < 0)
+ return r;
+ if (r > 0) /* Yay! we are done! */
+ (void) sd_event_exit(sd_device_monitor_get_event(monitor), 0);
+
+ return 0;
+}
+
+int wait_for_block_device_gone(HomeSetup *setup, usec_t timeout_usec) {
+ _cleanup_(sd_device_monitor_unrefp) sd_device_monitor *m = NULL;
+ _cleanup_(sd_event_unrefp) sd_event *event = NULL;
+ int r;
+
+ assert(setup);
+
+ /* So here's the thing: we enable "deferred deactivation" on our dm-crypt volumes. This means they
+ * are automatically torn down once not used anymore (i.e. once unmounted). Which is great. It also
+ * means that when we deactivate a home directory and try to tear down the volume that backs it, it
+ * possibly is already torn down or in the process of being torn down, since we race against the
+ * automatic tearing down. Which is fine, we handle errors from that. However, we lose the ability to
+ * naturally wait for the tear down operation to complete: if we are not the ones who tear down the
+ * device we are also not the ones who naturally block on that operation. Hence let's add some code
+ * to actively wait for the device to go away, via sd-device. We'll call this whenever tearing down a
+ * LUKS device, to ensure the device is really really gone before we proceed. Net effect: "homectl
+ * deactivate foo && homectl activate foo" will work reliably, i.e. deactivation immediately followed
+ * by activation will work. Also, by the time deactivation completes we can guarantee that all data
+ * is sync'ed down to the lowest block layer as all higher levels are fully and entirely
+ * destructed. */
+
+ if (!setup->dm_name)
+ return 0;
+
+ assert(setup->dm_node);
+ log_debug("Waiting until %s disappears.", setup->dm_node);
+
+ r = sd_event_new(&event);
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate event loop: %m");
+
+ r = sd_device_monitor_new(&m);
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate device monitor: %m");
+
+ r = sd_device_monitor_filter_add_match_subsystem_devtype(m, "block", "disk");
+ if (r < 0)
+ return log_error_errno(r, "Failed to configure device monitor match: %m");
+
+ r = sd_device_monitor_attach_event(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_handler, setup);
+ if (r < 0)
+ return log_error_errno(r, "Failed to start device monitor: %m");
+
+ r = device_is_gone(setup);
+ if (r < 0)
+ return r;
+ if (r > 0) {
+ log_debug("%s has already disappeared before entering wait loop.", setup->dm_node);
+ return 0; /* gone already */
+ }
+
+ if (timeout_usec != USEC_INFINITY) {
+ r = sd_event_add_time_relative(event, NULL, CLOCK_MONOTONIC, timeout_usec, 0, NULL, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add timer event: %m");
+ }
+
+ r = sd_event_loop(event);
+ if (r < 0)
+ return log_error_errno(r, "Failed to run event loop: %m");
+
+ r = device_is_gone(setup);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return log_error_errno(r, "Device %s still around.", setup->dm_node);
+
+ log_debug("Successfully waited until device %s disappeared.", setup->dm_node);
+ return 0;
+}
+
+int home_auto_shrink_luks(UserRecord *h, HomeSetup *setup, PasswordCache *cache) {
+ struct statfs sfs;
+ int r;
+
+ assert(h);
+ assert(user_record_storage(h) == USER_LUKS);
+ assert(setup);
+ assert(setup->root_fd >= 0);
+
+ if (user_record_auto_resize_mode(h) != AUTO_RESIZE_SHRINK_AND_GROW)
+ return 0;
+
+ if (fstatfs(setup->root_fd, &sfs) < 0)
+ return log_error_errno(errno, "Failed to statfs home directory: %m");
+
+ if (!fs_can_online_shrink_and_grow(sfs.f_type)) {
+ log_debug("Not auto-shrinking file system, since selected file system cannot do both online shrink and grow.");
+ return 0;
+ }
+
+ r = home_resize_luks(
+ h,
+ HOME_SETUP_ALREADY_ACTIVATED|
+ HOME_SETUP_RESIZE_DONT_SYNC_IDENTITIES|
+ HOME_SETUP_RESIZE_MINIMIZE|
+ HOME_SETUP_RESIZE_DONT_GROW|
+ HOME_SETUP_RESIZE_DONT_UNDO,
+ setup,
+ cache,
+ NULL);
+ if (r < 0)
+ return r;
+
+ return 1;
+}
diff --git a/src/home/homework-luks.h b/src/home/homework-luks.h
new file mode 100644
index 0000000..0218de8
--- /dev/null
+++ b/src/home/homework-luks.h
@@ -0,0 +1,49 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "cryptsetup-util.h"
+#include "homework.h"
+#include "user-record.h"
+
+int home_setup_luks(UserRecord *h, HomeSetupFlags flags, const char *force_image_path, HomeSetup *setup, PasswordCache *cache, UserRecord **ret_luks_home);
+
+int home_activate_luks(UserRecord *h, HomeSetupFlags flags, HomeSetup *setup, PasswordCache *cache, UserRecord **ret_home);
+int home_deactivate_luks(UserRecord *h, HomeSetup *setup);
+int home_trim_luks(UserRecord *h, HomeSetup *setup);
+
+int home_store_header_identity_luks(UserRecord *h, HomeSetup *setup, UserRecord *old_home);
+
+int home_create_luks(UserRecord *h, HomeSetup *setup, const PasswordCache *cache, char **effective_passwords, UserRecord **ret_home);
+
+int home_get_state_luks(UserRecord *h, HomeSetup *setup);
+
+int home_resize_luks(UserRecord *h, HomeSetupFlags flags, HomeSetup *setup, PasswordCache *cache, UserRecord **ret_home);
+
+int home_passwd_luks(UserRecord *h, HomeSetupFlags flags, HomeSetup *setup, const PasswordCache *cache, char **effective_passwords);
+
+int home_lock_luks(UserRecord *h, HomeSetup *setup);
+int home_unlock_luks(UserRecord *h, HomeSetup *setup, const PasswordCache *cache);
+
+int home_auto_shrink_luks(UserRecord *h, HomeSetup *setup, 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 = sym_crypt_get_volume_key_size(cd);
+ if (k <= 0)
+ return UINT64_MAX;
+
+ return (uint64_t) k;
+}
+
+int run_fitrim(int root_fd);
+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);
+
+int wait_for_block_device_gone(HomeSetup *setup, usec_t timeout_usec);
diff --git a/src/home/homework-mount.c b/src/home/homework-mount.c
new file mode 100644
index 0000000..28f09b9
--- /dev/null
+++ b/src/home/homework-mount.c
@@ -0,0 +1,309 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <sched.h>
+#include <sys/mount.h>
+#if WANT_LINUX_FS_H
+#include <linux/fs.h>
+#endif
+
+#include "alloc-util.h"
+#include "fd-util.h"
+#include "format-util.h"
+#include "glyph-util.h"
+#include "home-util.h"
+#include "homework-mount.h"
+#include "homework.h"
+#include "missing_mount.h"
+#include "missing_syscall.h"
+#include "mkdir.h"
+#include "mount-util.h"
+#include "namespace-util.h"
+#include "path-util.h"
+#include "string-util.h"
+#include "user-util.h"
+
+static const char *mount_options_for_fstype(const char *fstype) {
+ const char *e;
+ char *n;
+
+ assert(fstype);
+
+ /* Allow overriding our built-in defaults with an environment variable */
+ n = strjoina("SYSTEMD_HOME_MOUNT_OPTIONS_", fstype);
+ e = getenv(ascii_strupper(n));
+ if (e)
+ return e;
+
+ if (streq(fstype, "ext4"))
+ return "noquota,user_xattr";
+ if (streq(fstype, "xfs"))
+ return "noquota";
+ if (streq(fstype, "btrfs"))
+ return "noacl,compress=zstd:1";
+ return NULL;
+}
+
+int home_mount_node(
+ const char *node,
+ const char *fstype,
+ bool discard,
+ unsigned long flags,
+ const char *extra_mount_options) {
+
+ _cleanup_free_ char *joined = NULL;
+ const char *default_options;
+ int r;
+
+ assert(node);
+ assert(fstype);
+
+ default_options = mount_options_for_fstype(fstype);
+ if (default_options) {
+ if (!strextend_with_separator(&joined, ",", default_options))
+ return log_oom();
+ }
+
+ if (!strextend_with_separator(&joined, ",", discard ? "discard" : "nodiscard"))
+ return log_oom();
+
+ if (extra_mount_options) {
+ if (!strextend_with_separator(&joined, ",", extra_mount_options))
+ return log_oom();
+ }
+
+ r = mount_nofollow_verbose(LOG_ERR, node, HOME_RUNTIME_WORK_DIR, fstype, flags|MS_RELATIME, joined);
+ if (r < 0)
+ return r;
+
+ log_info("Mounting file system completed.");
+ return 0;
+}
+
+int home_unshare_and_mkdir(void) {
+ int r;
+
+ if (unshare(CLONE_NEWNS) < 0)
+ return log_error_errno(errno, "Couldn't unshare file system namespace: %m");
+
+ assert(path_startswith(HOME_RUNTIME_WORK_DIR, "/run"));
+
+ 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(HOME_RUNTIME_WORK_DIR, 0700);
+ return 0;
+}
+
+int home_unshare_and_mount(
+ const char *node,
+ const char *fstype,
+ bool discard,
+ unsigned long flags,
+ const char *extra_mount_options) {
+
+ int r;
+
+ assert(node);
+ assert(fstype);
+
+ r = home_unshare_and_mkdir();
+ if (r < 0)
+ return r;
+
+ r = home_mount_node(node, fstype, discard, flags, extra_mount_options);
+ if (r < 0)
+ return r;
+
+ r = mount_nofollow_verbose(LOG_ERR, NULL, HOME_RUNTIME_WORK_DIR, NULL, MS_PRIVATE, NULL);
+ if (r < 0) {
+ (void) umount_verbose(LOG_ERR, HOME_RUNTIME_WORK_DIR, UMOUNT_NOFOLLOW);
+ return r;
+ }
+
+ return 0;
+}
+
+int home_move_mount(const char *mount_suffix, const char *target) {
+ _cleanup_free_ char *subdir = NULL;
+ const char *d;
+ int r;
+
+ assert(target);
+
+ /* If 'mount_suffix' is set, then we'll mount a subdir of the source mount into the host. If it's
+ * NULL we'll move the mount itself */
+ if (mount_suffix) {
+ subdir = path_join(HOME_RUNTIME_WORK_DIR, mount_suffix);
+ if (!subdir)
+ return log_oom();
+
+ d = subdir;
+ } else
+ d = HOME_RUNTIME_WORK_DIR;
+
+ (void) mkdir_p(target, 0700);
+
+ r = mount_nofollow_verbose(LOG_ERR, d, target, NULL, MS_BIND, NULL);
+ if (r < 0)
+ return r;
+
+ r = umount_recursive(HOME_RUNTIME_WORK_DIR, 0);
+ if (r < 0)
+ return log_error_errno(r, "Failed to unmount %s: %m", HOME_RUNTIME_WORK_DIR);
+
+ log_info("Moving to final mount point %s completed.", target);
+ return 0;
+}
+
+static int append_identity_range(char **text, uid_t start, uid_t next_start, uid_t exclude) {
+ /* Creates an identity range ranging from 'start' to 'next_start-1'. Excludes the UID specified by 'exclude' if
+ * it is in that range. */
+
+ assert(text);
+
+ if (next_start <= start) /* Empty range? */
+ return 0;
+
+ if (exclude < start || exclude >= next_start) /* UID to exclude it outside of the range? */
+ return strextendf(text, UID_FMT " " UID_FMT " " UID_FMT "\n", start, start, next_start - start);
+
+ if (start == exclude && next_start == exclude + 1) /* The only UID in the range is the one to exclude? */
+ return 0;
+
+ if (exclude == start) /* UID to exclude at beginning of range? */
+ return strextendf(text, UID_FMT " " UID_FMT " " UID_FMT "\n", start+1, start+1, next_start - start - 1);
+
+ if (exclude == next_start - 1) /* UID to exclude at end of range? */
+ return strextendf(text, UID_FMT " " UID_FMT " " UID_FMT "\n", start, start, next_start - start - 1);
+
+ return strextendf(text,
+ UID_FMT " " UID_FMT " " UID_FMT "\n"
+ UID_FMT " " UID_FMT " " UID_FMT "\n",
+ start, start, exclude - start,
+ exclude + 1, exclude + 1, next_start - exclude - 1);
+}
+
+static int make_home_userns(uid_t stored_uid, uid_t exposed_uid) {
+ _cleanup_free_ char *text = NULL;
+ _cleanup_close_ int userns_fd = -EBADF;
+ int r;
+
+ assert(uid_is_valid(stored_uid));
+ assert(uid_is_valid(exposed_uid));
+
+ assert_cc(HOME_UID_MIN <= HOME_UID_MAX);
+ assert_cc(HOME_UID_MAX < UID_NOBODY);
+
+ /* Map everything below the homed UID range to itself (except for the UID we actually care about if
+ * it is inside this range) */
+ r = append_identity_range(&text, 0, HOME_UID_MIN, stored_uid);
+ if (r < 0)
+ return log_oom();
+
+ /* Now map the UID we are doing this for to the target UID. */
+ r = strextendf(&text, UID_FMT " " UID_FMT " " UID_FMT "\n", stored_uid, exposed_uid, 1u);
+ if (r < 0)
+ return log_oom();
+
+ /* Map everything above the homed UID range to itself (again, excluding the UID we actually care
+ * about if it is in that range). Also we leave "nobody" itself excluded) */
+ r = append_identity_range(&text, HOME_UID_MAX, UID_NOBODY, stored_uid);
+ if (r < 0)
+ return log_oom();
+
+ /* Also map the container range. People can use that to place containers owned by high UIDs in their
+ * home directories if they really want. We won't manage this UID range for them but pass it through
+ * 1:1, and it will lose its meaning once migrated between hosts. */
+ r = append_identity_range(&text, CONTAINER_UID_BASE_MIN, CONTAINER_UID_BASE_MAX+1, stored_uid);
+ if (r < 0)
+ return log_oom();
+
+ /* Map nspawn's mapped root UID as identity mapping so that people can run nspawn uidmap mounted
+ * containers off $HOME, if they want. */
+ r = strextendf(&text, UID_FMT " " UID_FMT " " UID_FMT "\n", UID_MAPPED_ROOT, UID_MAPPED_ROOT, 1u);
+ if (r < 0)
+ return log_oom();
+
+ /* Leave everything else unmapped, starting from UID_NOBODY itself. Specifically, this means the
+ * whole space outside of 16-bit remains unmapped */
+
+ log_debug("Creating userns with mapping:\n%s", text);
+
+ userns_fd = userns_acquire(text, text); /* same uid + gid mapping */
+ if (userns_fd < 0)
+ return log_error_errno(userns_fd, "Failed to allocate user namespace: %m");
+
+ return TAKE_FD(userns_fd);
+}
+
+int home_shift_uid(int dir_fd, const char *target, uid_t stored_uid, uid_t exposed_uid, int *ret_mount_fd) {
+ _cleanup_close_ int mount_fd = -EBADF, userns_fd = -EBADF;
+ int r;
+
+ assert(dir_fd >= 0);
+ assert(uid_is_valid(stored_uid));
+ assert(uid_is_valid(exposed_uid));
+
+ /* Let's try to set up a UID mapping for this directory. This is called when first creating a home
+ * directory or when activating it again. We do this as optimization only, to avoid having to
+ * recursively chown() things on each activation. If the kernel or file system doesn't support this
+ * scheme we'll handle this gracefully, and not do anything, so that the later recursive chown()ing
+ * then fixes up things for us. Note that the chown()ing is smart enough to skip things if they look
+ * alright already.
+ *
+ * Note that this always creates a new mount (i.e. we use OPEN_TREE_CLONE), since applying idmaps is
+ * not allowed once the mount is put in place. */
+
+ mount_fd = open_tree(dir_fd, "", AT_EMPTY_PATH | OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC);
+ if (mount_fd < 0) {
+ if (ERRNO_IS_NOT_SUPPORTED(errno)) {
+ log_debug_errno(errno, "The open_tree() syscall is not supported, not setting up UID shift mount: %m");
+
+ if (ret_mount_fd)
+ *ret_mount_fd = -EBADF;
+
+ return 0;
+ }
+
+ return log_error_errno(errno, "Failed to open tree of home directory: %m");
+ }
+
+ userns_fd = make_home_userns(stored_uid, exposed_uid);
+ if (userns_fd < 0)
+ return userns_fd;
+
+ /* Set the user namespace mapping attribute on the cloned mount point */
+ if (mount_setattr(mount_fd, "", AT_EMPTY_PATH,
+ &(struct mount_attr) {
+ .attr_set = MOUNT_ATTR_IDMAP,
+ .userns_fd = userns_fd,
+ }, MOUNT_ATTR_SIZE_VER0) < 0) {
+
+ if (ERRNO_IS_NOT_SUPPORTED(errno) || errno == EINVAL) { /* EINVAL is documented in mount_attr() as fs doesn't support idmapping */
+ log_debug_errno(errno, "UID/GID mapping for shifted mount not available, not setting it up: %m");
+
+ if (ret_mount_fd)
+ *ret_mount_fd = -EBADF;
+
+ return 0;
+ }
+
+ return log_error_errno(errno, "Failed to apply UID/GID mapping: %m");
+ }
+
+ if (target)
+ r = move_mount(mount_fd, "", AT_FDCWD, target, MOVE_MOUNT_F_EMPTY_PATH);
+ else
+ r = move_mount(mount_fd, "", dir_fd, "", MOVE_MOUNT_F_EMPTY_PATH|MOVE_MOUNT_T_EMPTY_PATH);
+ if (r < 0)
+ return log_error_errno(errno, "Failed to apply UID/GID map: %m");
+
+ log_debug("Applied uidmap mount to %s. Mapping is " UID_FMT " %s " UID_FMT ".",
+ strna(target), stored_uid, special_glyph(SPECIAL_GLYPH_ARROW_RIGHT), exposed_uid);
+
+ if (ret_mount_fd)
+ *ret_mount_fd = TAKE_FD(mount_fd);
+
+ return 1;
+}
diff --git a/src/home/homework-mount.h b/src/home/homework-mount.h
new file mode 100644
index 0000000..255df26
--- /dev/null
+++ b/src/home/homework-mount.h
@@ -0,0 +1,10 @@
+/* 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, const char *extra_mount_options);
+int home_unshare_and_mkdir(void);
+int home_unshare_and_mount(const char *node, const char *fstype, bool discard, unsigned long flags, const char *extra_mount_options);
+int home_move_mount(const char *user_name_and_realm, const char *target);
+int home_shift_uid(int dir_fd, const char *target, uid_t stored_uid, uid_t exposed_uid, int *ret_mount_fd);
diff --git a/src/home/homework-password-cache.c b/src/home/homework-password-cache.c
new file mode 100644
index 0000000..00a0f69
--- /dev/null
+++ b/src/home/homework-password-cache.c
@@ -0,0 +1,57 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "homework-password-cache.h"
+#include "keyring-util.h"
+#include "missing_syscall.h"
+#include "user-record.h"
+
+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);
+ cache->keyring_passswords = strv_free_erase(cache->keyring_passswords);
+}
+
+void password_cache_load_keyring(UserRecord *h, PasswordCache *cache) {
+ _cleanup_(erase_and_freep) void *p = NULL;
+ _cleanup_free_ char *name = NULL;
+ char **strv;
+ key_serial_t serial;
+ size_t sz;
+ int r;
+
+ assert(h);
+ assert(cache);
+
+ /* Loads the password we need to for automatic resizing from the kernel keyring */
+
+ name = strjoin("homework-user-", h->user_name);
+ if (!name)
+ return (void) log_oom();
+
+ serial = request_key("user", name, NULL, 0);
+ if (serial == -1)
+ return (void) log_debug_errno(errno, "Failed to request key '%s', ignoring: %m", name);
+
+ r = keyring_read(serial, &p, &sz);
+ if (r < 0)
+ return (void) log_debug_errno(r, "Failed to read keyring key '%s', ignoring: %m", name);
+
+ if (memchr(p, 0, sz))
+ return (void) log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Cached password contains embedded NUL byte, ignoring.");
+
+ strv = new(char*, 2);
+ if (!strv)
+ return (void) log_oom();
+
+ strv[0] = TAKE_PTR(p); /* Note that keyring_read() will NUL terminate implicitly, hence we don't have
+ * to NUL terminate manually here: it's a valid string. */
+ strv[1] = NULL;
+
+ strv_free_erase(cache->keyring_passswords);
+ cache->keyring_passswords = strv;
+
+ log_debug("Successfully acquired home key from kernel keyring.");
+}
diff --git a/src/home/homework-password-cache.h b/src/home/homework-password-cache.h
new file mode 100644
index 0000000..fdfbcfe
--- /dev/null
+++ b/src/home/homework-password-cache.h
@@ -0,0 +1,28 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "strv.h"
+#include "user-record.h"
+
+typedef struct PasswordCache {
+ /* Passwords acquired from the kernel keyring */
+ char **keyring_passswords;
+
+ /* 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);
+
+static inline bool password_cache_contains(const PasswordCache *cache, const char *p) {
+ if (!cache)
+ return false;
+
+ return strv_contains(cache->pkcs11_passwords, p) ||
+ strv_contains(cache->fido2_passwords, p) ||
+ strv_contains(cache->keyring_passswords, p);
+}
+
+void password_cache_load_keyring(UserRecord *h, PasswordCache *cache);
diff --git a/src/home/homework-pkcs11.c b/src/home/homework-pkcs11.c
new file mode 100644
index 0000000..f371994
--- /dev/null
+++ b/src/home/homework-pkcs11.c
@@ -0,0 +1,102 @@
+/* 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 = ASSERT_PTR(userdata);
+ _cleanup_free_ char *token_label = NULL;
+ CK_TOKEN_INFO updated_token_info;
+ size_t decrypted_key_size;
+ CK_OBJECT_HANDLE object;
+ CK_RV rv;
+ int r;
+
+ assert(m);
+ assert(slot_info);
+ assert(token_info);
+ assert(uri);
+
+ /* 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, sym_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, sym_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, sym_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..508c0c0
--- /dev/null
+++ b/src/home/homework-quota.c
@@ -0,0 +1,118 @@
+/* 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, "Failed 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_devnum(QCMD_FIXED(Q_GETQUOTA, USRQUOTA), devno, h->uid, &req);
+ if (r == -ESRCH)
+ zero(req);
+ else if (ERRNO_IS_NEG_NOT_SUPPORTED(r))
+ return log_error_errno(r, "No UID quota support on %s.", path);
+ else if (r < 0)
+ return log_error_errno(r, "Failed to query disk quota for UID " UID_FMT ": %m", h->uid);
+ else if (FLAGS_SET(req.dqb_valid, QIF_BLIMITS) && h->disk_size / QIF_DQBLKSIZE == req.dqb_bhardlimit) {
+ /* Shortcut things if everything is set up properly already */
+ 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_devnum(QCMD_FIXED(Q_SETQUOTA, USRQUOTA), devno, h->uid, &req);
+ if (r == -ESRCH)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), "UID quota not available on %s.", path);
+ if (r < 0)
+ 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..066483e
--- /dev/null
+++ b/src/home/homework.c
@@ -0,0 +1,1979 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <stddef.h>
+#include <sys/mount.h>
+
+#include "blockdev-util.h"
+#include "chown-recursive.h"
+#include "copy.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "filesystems.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 "mount-util.h"
+#include "path-util.h"
+#include "recovery-key.h"
+#include "rm-rf.h"
+#include "stat-util.h"
+#include "strv.h"
+#include "sync-util.h"
+#include "tmpfile-util.h"
+#include "user-util.h"
+#include "virt.h"
+
+/* Make sure a bad password always results in a 3s delay, no matter what */
+#define BAD_PASSWORD_DELAY_USEC (3 * USEC_PER_SEC)
+
+int user_record_authenticate(
+ UserRecord *h,
+ UserRecord *secret,
+ 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,
+ need_user_verification_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++)
+ 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++)
+ /* 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 1;
+ }
+ }
+
+ /* 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 1;
+ }
+#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 -ENOCSI:
+ need_user_verification_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_user_verification_permitted)
+ return -ENOCSI;
+ 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;
+}
+
+static void drop_caches_now(void) {
+ int r;
+
+ /* Drop file system caches now. See https://docs.kernel.org/admin-guide/sysctl/vm.html
+ * for details. We write "2" into /proc/sys/vm/drop_caches to ensure dentries/inodes are flushed, but
+ * not more. */
+
+ r = write_string_file("/proc/sys/vm/drop_caches", "2\n", WRITE_STRING_FILE_DISABLE_BUFFER);
+ if (r < 0)
+ log_warning_errno(r, "Failed to drop caches, ignoring: %m");
+ else
+ log_debug("Dropped caches.");
+}
+
+int home_setup_undo_mount(HomeSetup *setup, int level) {
+ int r;
+
+ assert(setup);
+
+ if (!setup->undo_mount)
+ return 0;
+
+ r = umount_recursive(HOME_RUNTIME_WORK_DIR, 0);
+ if (r < 0) {
+ if (level >= LOG_DEBUG) /* umount_recursive() does debug level logging anyway, no need to
+ * repeat that here */
+ return r;
+
+ /* If a higher log level is requested, the generate a non-debug message here too. */
+ return log_full_errno(level, r, "Failed to unmount mount tree below %s: %m", HOME_RUNTIME_WORK_DIR);
+ }
+
+ setup->undo_mount = false;
+ return 1;
+}
+
+int home_setup_undo_dm(HomeSetup *setup, int level) {
+ int r, ret;
+
+ assert(setup);
+
+ if (setup->undo_dm) {
+ assert(setup->crypt_device);
+ assert(setup->dm_name);
+
+ r = sym_crypt_deactivate_by_name(setup->crypt_device, setup->dm_name, 0);
+ if (r < 0)
+ return log_full_errno(level, r, "Failed to deactivate LUKS device: %m");
+
+ /* In case the device was already remove asynchronously by an early unmount via the deferred
+ * remove logic, let's wait for it */
+ (void) wait_for_block_device_gone(setup, USEC_PER_SEC * 30);
+
+ setup->undo_dm = false;
+ ret = 1;
+ } else
+ ret = 0;
+
+ if (setup->crypt_device) {
+ sym_crypt_free(setup->crypt_device);
+ setup->crypt_device = NULL;
+ }
+
+ return ret;
+}
+
+int keyring_unlink(key_serial_t k) {
+
+ if (k == -1) /* already invalidated? */
+ return -1;
+
+ if (keyctl(KEYCTL_UNLINK, k, KEY_SPEC_SESSION_KEYRING, 0, 0) < 0)
+ log_debug_errno(errno, "Failed to unlink key from session kernel keyring, ignoring: %m");
+
+ return -1; /* Always return the key_serial_t value for "invalid" */
+}
+
+static int keyring_flush(UserRecord *h) {
+ _cleanup_free_ char *name = NULL;
+ long serial;
+
+ assert(h);
+
+ name = strjoin("homework-user-", h->user_name);
+ if (!name)
+ return log_oom();
+
+ serial = keyctl(KEYCTL_SEARCH, (unsigned long) KEY_SPEC_SESSION_KEYRING, (unsigned long) "user", (unsigned long) name, 0);
+ if (serial == -1)
+ return log_debug_errno(errno, "Failed to find kernel keyring entry for user, ignoring: %m");
+
+ return keyring_unlink(serial);
+}
+
+int home_setup_done(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;
+ }
+
+ if (syncfs(setup->root_fd) < 0)
+ log_debug_errno(errno, "Failed to synchronize home directory, ignoring: %m");
+
+ setup->root_fd = safe_close(setup->root_fd);
+ }
+
+ q = home_setup_undo_mount(setup, LOG_DEBUG);
+ if (q < 0)
+ r = q;
+
+ q = home_setup_undo_dm(setup, LOG_DEBUG);
+ 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);
+ }
+
+ if (setup->temporary_image_path) {
+ if (unlink(setup->temporary_image_path) < 0)
+ log_debug_errno(errno, "Failed to remove temporary image file '%s', ignoring: %m",
+ setup->temporary_image_path);
+
+ setup->temporary_image_path = mfree(setup->temporary_image_path);
+ }
+
+ setup->key_serial = keyring_unlink(setup->key_serial);
+
+ 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);
+
+ setup->volume_key = erase_and_free(setup->volume_key);
+ setup->volume_key_size = 0;
+
+ if (setup->do_drop_caches)
+ drop_caches_now();
+
+ setup->mount_suffix = mfree(setup->mount_suffix);
+
+ return r;
+}
+
+int home_setup(
+ UserRecord *h,
+ HomeSetupFlags flags,
+ HomeSetup *setup,
+ PasswordCache *cache,
+ 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!). */
+
+ if (!FLAGS_SET(flags, HOME_SETUP_ALREADY_ACTIVATED)) /* If we set up the directory, we should also drop caches once we are done */
+ setup->do_drop_caches = setup->do_drop_caches || user_record_drop_caches(h);
+
+ switch (user_record_storage(h)) {
+
+ case USER_LUKS:
+ return home_setup_luks(h, flags, NULL, setup, cache, ret_header_home);
+
+ case USER_SUBVOLUME:
+ case USER_DIRECTORY:
+ r = home_setup_directory(h, setup);
+ break;
+
+ case USER_FSCRYPT:
+ r = home_setup_fscrypt(h, setup, cache);
+ break;
+
+ case USER_CIFS:
+ r = home_setup_cifs(h, flags, 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_fclose_ FILE *identity_file = NULL;
+ _cleanup_close_ int identity_fd = -EBADF;
+ 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_fclose_ FILE *identity_file = NULL;
+ _cleanup_close_ int identity_fd = -EBADF;
+ _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) {
+ r = log_error_errno(errno, "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|USER_RECORD_PERMISSIVE);
+ 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|USER_RECORD_PERMISSIVE, &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;
+ }
+
+ return fs_type_to_string(sfs.f_type);
+}
+
+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 ? sym_crypt_get_cipher(setup->crypt_device) : NULL,
+ setup->crypt_device ? sym_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_maybe_shift_uid(
+ UserRecord *h,
+ HomeSetupFlags flags,
+ HomeSetup *setup) {
+
+ _cleanup_close_ int mount_fd = -EBADF;
+ struct stat st;
+
+ assert(h);
+ assert(setup);
+ assert(setup->root_fd >= 0);
+
+ /* If the home dir is already activated, then the UID shift is already applied. */
+ if (FLAGS_SET(flags, HOME_SETUP_ALREADY_ACTIVATED))
+ return 0;
+
+ if (fstat(setup->root_fd, &st) < 0)
+ return log_error_errno(errno, "Failed to stat() home directory: %m");
+
+ /* Let's shift UIDs of this mount. Hopefully this makes the later chowning unnecessary. (Note that we
+ * also prefer to do UID mapping even if the UID already matches our goal UID. That's because we want
+ * to leave UIDs in the homed managed range unmapped.) */
+ (void) home_shift_uid(setup->root_fd, NULL, st.st_uid, h->uid, &mount_fd);
+
+ /* If this worked, then we'll have a reference to the mount now, which we can also use like an O_PATH
+ * fd to the new dir. Let's convert it into a proper O_DIRECTORY fd. */
+ if (mount_fd >= 0) {
+ safe_close(setup->root_fd);
+
+ setup->root_fd = fd_reopen(mount_fd, O_RDONLY|O_CLOEXEC|O_DIRECTORY);
+ if (setup->root_fd < 0)
+ return log_error_errno(setup->root_fd, "Failed to convert mount fd into regular directory fd: %m");
+ }
+
+ return 0;
+}
+
+int home_refresh(
+ UserRecord *h,
+ HomeSetupFlags flags,
+ 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_maybe_shift_uid(h, flags, setup);
+ 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_(home_setup_done) HomeSetup setup = HOME_SETUP_INIT;
+ _cleanup_(user_record_unrefp) UserRecord *new_home = NULL;
+ _cleanup_(password_cache_free) PasswordCache cache = {};
+ HomeSetupFlags flags = 0;
+ 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, flags, &setup, &cache, &new_home);
+ if (r < 0)
+ return r;
+
+ break;
+
+ case USER_SUBVOLUME:
+ case USER_DIRECTORY:
+ case USER_FSCRYPT:
+ r = home_activate_directory(h, flags, &setup, &cache, &new_home);
+ if (r < 0)
+ return r;
+
+ break;
+
+ case USER_CIFS:
+ r = home_activate_cifs(h, flags, &setup, &cache, &new_home);
+ if (r < 0)
+ return r;
+
+ break;
+
+ default:
+ assert_not_reached();
+ }
+
+ /* 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) {
+ _cleanup_(home_setup_done) HomeSetup setup = HOME_SETUP_INIT;
+ _cleanup_(password_cache_free) PasswordCache cache = {};
+ 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) {
+ /* Before we do anything, let's move the home mount away. */
+ r = home_unshare_and_mkdir();
+ if (r < 0)
+ return r;
+
+ r = mount_nofollow_verbose(LOG_ERR, user_record_home_directory(h), HOME_RUNTIME_WORK_DIR, NULL, MS_BIND, NULL);
+ if (r < 0)
+ return r;
+
+ setup.undo_mount = true; /* remember to unmount the new bind mount from HOME_RUNTIME_WORK_DIR */
+
+ /* Let's explicitly open the new root fs, using the moved path */
+ setup.root_fd = open(HOME_RUNTIME_WORK_DIR, O_RDONLY|O_DIRECTORY|O_CLOEXEC);
+ if (setup.root_fd < 0)
+ return log_error_errno(errno, "Failed to open moved home directory: %m");
+
+ /* Now get rid of the home at its original place (we only keep the bind mount we created above) */
+ r = umount_verbose(LOG_ERR, user_record_home_directory(h), UMOUNT_NOFOLLOW | (force ? MNT_FORCE|MNT_DETACH : 0));
+ if (r < 0)
+ return r;
+
+ if (user_record_storage(h) == USER_LUKS) {
+ /* Automatically shrink on logout if that's enabled. To be able to shrink we need the
+ * keys to the device. */
+ password_cache_load_keyring(h, &cache);
+ (void) home_trim_luks(h, &setup);
+ }
+
+ /* Sync explicitly, so that the drop caches logic below can work as documented */
+ if (syncfs(setup.root_fd) < 0)
+ log_debug_errno(errno, "Failed to synchronize home directory, ignoring: %m");
+ else
+ log_info("Syncing completed.");
+
+ if (user_record_storage(h) == USER_LUKS)
+ (void) home_auto_shrink_luks(h, &setup, &cache);
+
+ setup.root_fd = safe_close(setup.root_fd);
+
+ /* Now get rid of the bind mount, too */
+ r = umount_verbose(LOG_ERR, HOME_RUNTIME_WORK_DIR, UMOUNT_NOFOLLOW | (force ? MNT_FORCE|MNT_DETACH : 0));
+ if (r < 0)
+ return r;
+
+ setup.undo_mount = false; /* Remember that the bind mount doesn't need to be unmounted anymore */
+
+ if (user_record_drop_caches(h))
+ setup.do_drop_caches = true;
+
+ 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, &setup);
+ if (r < 0)
+ return r;
+ if (r > 0)
+ done = true;
+ }
+
+ /* Explicitly flush any per-user key from the keyring */
+ (void) keyring_flush(h);
+
+ if (!done)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOEXEC), "Home is not active.");
+
+ if (setup.do_drop_caches) {
+ setup.do_drop_caches = false;
+ drop_caches_now();
+ }
+
+ 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, NULL, NULL);
+ 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_erase_ char **effective = NULL;
+ size_t n;
+ 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;
+
+ 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;
+
+ 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(get_home_root());
+ if (r > 0)
+ log_info("%s is encrypted, not using '%s' storage, in order to avoid double encryption.", get_home_root(), user_storage_to_string(USER_LUKS));
+ else {
+ if (r < 0)
+ log_warning_errno(r, "Failed to determine if %s is encrypted, ignoring: %m", get_home_root());
+
+ r = dlopen_cryptsetup();
+ if (r < 0)
+ log_info("Not using '%s' storage, since libcryptsetup could not be loaded.", user_storage_to_string(USER_LUKS));
+ else {
+ log_info("Using automatic default storage of '%s'.", user_storage_to_string(USER_LUKS));
+ *ret = USER_LUKS;
+ return 0;
+ }
+ }
+ } else
+ log_info("Running in container, not using '%s' storage.", user_storage_to_string(USER_LUKS));
+
+ r = path_is_fs_type(get_home_root(), BTRFS_SUPER_MAGIC);
+ if (r < 0)
+ log_warning_errno(r, "Failed to determine file system of %s, ignoring: %m", get_home_root());
+ if (r > 0) {
+ log_info("%s is on btrfs, using '%s' as storage.", get_home_root(), user_storage_to_string(USER_SUBVOLUME));
+ *ret = USER_SUBVOLUME;
+ } else {
+ log_info("%s is on simple file system, using '%s' as storage.", get_home_root(), user_storage_to_string(USER_DIRECTORY));
+ *ret = USER_DIRECTORY;
+ }
+
+ return 0;
+}
+
+static int home_create(UserRecord *h, UserRecord **ret_home) {
+ _cleanup_strv_free_erase_ char **effective_passwords = NULL;
+ _cleanup_(home_setup_done) HomeSetup setup = HOME_SETUP_INIT;
+ _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, &setup, &cache, effective_passwords, &new_home);
+ break;
+
+ case USER_DIRECTORY:
+ case USER_SUBVOLUME:
+ r = home_create_directory_or_subvolume(h, &setup, &new_home);
+ break;
+
+ case USER_FSCRYPT:
+ r = home_create_fscrypt(h, &setup, effective_passwords, &new_home);
+ break;
+
+ case USER_CIFS:
+ r = home_create_cifs(h, &setup, &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 {
+ _cleanup_free_ char *parent = NULL;
+
+ deleted = true;
+
+ r = path_extract_directory(ip, &parent);
+ if (r < 0)
+ log_debug_errno(r, "Failed to determine parent directory of '%s': %m", ip);
+ else {
+ r = fsync_path_at(AT_FDCWD, parent);
+ if (r < 0)
+ log_debug_errno(r, "Failed to synchronize disk after deleting '%s', ignoring: %m", ip);
+ }
+ }
+
+ } 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|REMOVE_SYNCFS);
+ 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();
+ }
+
+ 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) {
+ if (user_record_drop_caches(h))
+ drop_caches_now();
+
+ 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, HomeSetupFlags *flags) {
+ 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_get_state_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();
+ }
+
+ if (flags)
+ SET_FLAG(*flags, HOME_SETUP_ALREADY_ACTIVATED, has_mount);
+
+ 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_done) HomeSetup setup = HOME_SETUP_INIT;
+ _cleanup_(password_cache_free) PasswordCache cache = {};
+ HomeSetupFlags flags = 0;
+ 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, &flags);
+ if (r < 0)
+ return r;
+
+ r = home_setup(h, flags, &setup, &cache, &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_maybe_shift_uid(h, flags, &setup);
+ 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_done(&setup);
+ if (r < 0)
+ return r;
+
+ log_info("Everything completed.");
+
+ *ret = TAKE_PTR(new_home);
+ return 0;
+}
+
+static int home_resize(UserRecord *h, bool automatic, UserRecord **ret) {
+ _cleanup_(home_setup_done) HomeSetup setup = HOME_SETUP_INIT;
+ _cleanup_(password_cache_free) PasswordCache cache = {};
+ HomeSetupFlags flags = 0;
+ int r;
+
+ assert(h);
+ assert(ret);
+
+ if (h->disk_size == UINT64_MAX)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No target size specified, refusing.");
+
+ if (automatic)
+ /* In automatic mode don't want to ask the user for the password, hence load it from the kernel keyring */
+ password_cache_load_keyring(h, &cache);
+ else {
+ /* In manual mode let's ensure the user is fully authenticated */
+ 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, &flags);
+ if (r < 0)
+ return r;
+
+ /* In automatic mode let's skip syncing identities, because we can't validate them, since we can't
+ * ask the user for reauthentication */
+ if (automatic)
+ flags |= HOME_SETUP_RESIZE_DONT_SYNC_IDENTITIES;
+
+ switch (user_record_storage(h)) {
+
+ case USER_LUKS:
+ return home_resize_luks(h, flags, &setup, &cache, ret);
+
+ case USER_DIRECTORY:
+ case USER_SUBVOLUME:
+ case USER_FSCRYPT:
+ return home_resize_directory(h, flags, &setup, &cache, 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_erase_ char **effective_passwords = NULL;
+ _cleanup_(home_setup_done) HomeSetup setup = HOME_SETUP_INIT;
+ _cleanup_(password_cache_free) PasswordCache cache = {};
+ HomeSetupFlags flags = 0;
+ 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, &flags);
+ if (r < 0)
+ return r;
+
+ r = home_setup(h, flags, &setup, &cache, &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;
+
+ r = home_maybe_shift_uid(h, flags, &setup);
+ if (r < 0)
+ return r;
+
+ switch (user_record_storage(h)) {
+
+ case USER_LUKS:
+ r = home_passwd_luks(h, flags, &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_done(&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_done) HomeSetup setup = HOME_SETUP_INIT;
+ _cleanup_(password_cache_free) PasswordCache cache = {};
+ HomeSetupFlags flags = 0;
+ 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, &flags);
+ if (r < 0)
+ return r;
+
+ r = home_setup(h, flags, &setup, &cache, &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_done(&setup);
+ if (r < 0)
+ return r;
+
+ log_info("Everything completed.");
+
+ *ret_home = TAKE_PTR(new_home);
+ return 1;
+}
+
+static int home_lock(UserRecord *h) {
+ _cleanup_(home_setup_done) HomeSetup setup = HOME_SETUP_INIT;
+ 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, &setup);
+ if (r < 0)
+ return r;
+
+ log_info("Everything completed.");
+ return 1;
+}
+
+static int home_unlock(UserRecord *h) {
+ _cleanup_(home_setup_done) HomeSetup setup = HOME_SETUP_INIT;
+ _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, &setup, &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_fclose_ 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();
+
+ cryptsetup_enable_logging(NULL);
+
+ 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|USER_RECORD_PERMISSIVE);
+ 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
+ * ENOCSI → suitable FIDO2 device found, but OK to ask for user verification 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
+ * EADDRINUSE → home image is already used elsewhere (lock taken)
+ */
+
+ 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")) /* Resize on user request */
+ r = home_resize(home, false, &new_home);
+ else if (streq(argv[1], "resize-auto")) /* Automatic resize */
+ r = home_resize(home, true, &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_safe(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..cef3f4e
--- /dev/null
+++ b/src/home/homework.h
@@ -0,0 +1,97 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <sys/vfs.h>
+
+#include "sd-id128.h"
+
+#include "cryptsetup-util.h"
+#include "homework-password-cache.h"
+#include "loop-util.h"
+#include "missing_fs.h" /* for FS_KEY_DESCRIPTOR_SIZE, do not include linux/fs.h */
+#include "missing_keyctl.h"
+#include "missing_syscall.h"
+#include "user-record.h"
+#include "user-record-util.h"
+
+typedef struct HomeSetup {
+ char *dm_name; /* "home-<username>" */
+ char *dm_node; /* "/dev/mapper/home-<username>" */
+
+ 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;
+
+ key_serial_t key_serial;
+
+ bool undo_dm:1;
+ bool undo_mount:1; /* Whether to unmount /run/systemd/user-home-mount */
+ bool do_offline_fitrim:1;
+ bool do_offline_fallocate:1;
+ bool do_mark_clean:1;
+ bool do_drop_caches:1;
+
+ uint64_t partition_offset;
+ uint64_t partition_size;
+
+ char *mount_suffix; /* The directory to use as home dir is this path below /run/systemd/user-home-mount */
+
+ char *temporary_image_path;
+} HomeSetup;
+
+#define HOME_SETUP_INIT \
+ { \
+ .root_fd = -EBADF, \
+ .image_fd = -EBADF, \
+ .partition_offset = UINT64_MAX, \
+ .partition_size = UINT64_MAX, \
+ .key_serial = -1, \
+ }
+
+/* Various flags for the operation of setting up a home directory */
+typedef enum HomeSetupFlags {
+ HOME_SETUP_ALREADY_ACTIVATED = 1 << 0, /* Open an already activated home, rather than activate it afresh */
+
+ /* CIFS backend: */
+ HOME_SETUP_CIFS_MKDIR = 1 << 1, /* Create CIFS subdir when missing */
+
+ /* Applies only for resize operations */
+ HOME_SETUP_RESIZE_DONT_SYNC_IDENTITIES = 1 << 2, /* Don't sync identity records into home and LUKS header */
+ HOME_SETUP_RESIZE_MINIMIZE = 1 << 3, /* Shrink to minimal size */
+ HOME_SETUP_RESIZE_DONT_GROW = 1 << 4, /* If the resize would grow, gracefully terminate operation */
+ HOME_SETUP_RESIZE_DONT_SHRINK = 1 << 5, /* If the resize would shrink, gracefully terminate operation */
+ HOME_SETUP_RESIZE_DONT_UNDO = 1 << 6, /* Leave loopback/DM device context open after successful operation */
+} HomeSetupFlags;
+
+int home_setup_done(HomeSetup *setup);
+
+int home_setup_undo_mount(HomeSetup *setup, int level);
+int home_setup_undo_dm(HomeSetup *setup, int level);
+
+int keyring_unlink(key_serial_t k);
+
+int home_setup(UserRecord *h, HomeSetupFlags flags, HomeSetup *setup, PasswordCache *cache, UserRecord **ret_header_home);
+
+int home_refresh(UserRecord *h, HomeSetupFlags flags, HomeSetup *setup, UserRecord *header_home, PasswordCache *cache, struct statfs *ret_statfs, UserRecord **ret_new_home);
+
+int home_maybe_shift_uid(UserRecord *h, HomeSetupFlags flags, HomeSetup *setup);
+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);
+
+#define HOME_RUNTIME_WORK_DIR "/run/systemd/user-home-mount"
diff --git a/src/home/meson.build b/src/home/meson.build
new file mode 100644
index 0000000..09831de
--- /dev/null
+++ b/src/home/meson.build
@@ -0,0 +1,140 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+systemd_homework_sources = files(
+ 'home-util.c',
+ 'homework-cifs.c',
+ 'homework-directory.c',
+ 'homework-fscrypt.c',
+ 'homework-luks.c',
+ 'homework-mount.c',
+ 'homework-password-cache.c',
+ 'homework-quota.c',
+ 'homework.c',
+ 'user-record-util.c',
+)
+
+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',
+ 'homed-bus.c',
+ 'homed-conf.c',
+ 'homed-home-bus.c',
+ 'homed-home.c',
+ 'homed-manager-bus.c',
+ 'homed-manager.c',
+ 'homed-operation.c',
+ 'homed-varlink.c',
+ 'homed.c',
+ 'user-record-password-quality.c',
+ 'user-record-sign.c',
+ 'user-record-util.c',
+)
+
+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',
+ 'homectl-fido2.c',
+ 'homectl-pkcs11.c',
+ 'homectl-recovery-key.c',
+ 'homectl.c',
+ 'user-record-password-quality.c',
+ 'user-record-util.c',
+)
+
+pam_systemd_home_sources = files(
+ 'home-util.c',
+ 'pam_systemd_home.c',
+ 'user-record-util.c',
+)
+
+executables += [
+ libexec_template + {
+ 'name' : 'systemd-homework',
+ 'conditions' : ['ENABLE_HOMED'],
+ 'sources' : systemd_homework_sources,
+ 'link_with' : [
+ libshared,
+ libshared_fdisk
+ ],
+ 'dependencies' : [
+ libblkid,
+ libcrypt,
+ libfdisk,
+ libopenssl,
+ libp11kit_cflags,
+ threads,
+ ],
+ },
+ libexec_template + {
+ 'name' : 'systemd-homed',
+ 'dbus' : true,
+ 'conditions' : ['ENABLE_HOMED'],
+ 'sources' : systemd_homed_sources,
+ 'include_directories' : includes +
+ include_directories('.'),
+ 'dependencies' : [
+ libcrypt,
+ libm,
+ libopenssl,
+ threads,
+ ],
+ },
+ executable_template + {
+ 'name' : 'homectl',
+ 'public' : true,
+ 'conditions' : ['ENABLE_HOMED'],
+ 'sources' : homectl_sources,
+ 'dependencies' : [
+ libcrypt,
+ libdl,
+ libopenssl,
+ libp11kit_cflags,
+ threads,
+ ],
+ },
+]
+
+modules += [
+ pam_template + {
+ 'name' : 'pam_systemd_home',
+ 'conditions' : [
+ 'ENABLE_HOMED',
+ 'HAVE_PAM',
+ ],
+ 'sources' : pam_systemd_home_sources,
+ 'dependencies' : [
+ libcrypt,
+ libpam_misc,
+ libpam,
+ threads,
+ ],
+ 'version-script' : meson.current_source_dir() / 'pam_systemd_home.sym',
+ },
+]
+
+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_samples
+ install_data('homed.conf',
+ install_dir : pkgconfigfiledir)
+ endif
+endif
diff --git a/src/home/org.freedesktop.home1.conf b/src/home/org.freedesktop.home1.conf
new file mode 100644
index 0000000..5af1a68
--- /dev/null
+++ b/src/home/org.freedesktop.home1.conf
@@ -0,0 +1,201 @@
+<?xml version="1.0"?> <!--*-nxml-*-->
+<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
+ "https://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"/>
+
+ <allow send_destination="org.freedesktop.home1"
+ send_interface="org.freedesktop.home1.Manager"
+ send_member="DeactivateAllHomes"/>
+
+ <allow send_destination="org.freedesktop.home1"
+ send_interface="org.freedesktop.home1.Manager"
+ send_member="Rebalance"/>
+
+ <!-- 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..a337b32
--- /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"
+ "https://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">
+
+<!-- SPDX-License-Identifier: LGPL-2.1-or-later -->
+
+<policyconfig>
+
+ <vendor>The systemd Project</vendor>
+ <vendor_url>https://systemd.io</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..ba8d8f6
--- /dev/null
+++ b/src/home/pam_systemd_home.c
@@ -0,0 +1,1064 @@
+/* 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 "locale-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) {
+
+ assert(argc >= 0);
+ assert(argc == 0 || argv);
+
+ for (int 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,
+ bool debug,
+ UserRecord **ret_record,
+ PamBusData **bus_data) {
+
+ _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)
+ return pam_syslog_pam_error(handle, LOG_ERR, r, "Failed to get user name: @PAMERR@");
+
+ if (isempty(username))
+ return pam_syslog_pam_error(handle, LOG_ERR, PAM_SERVICE_ERR, "User name not set.");
+ }
+
+ /* 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))
+ return pam_syslog_pam_error(handle, LOG_ERR, r, "Failed to get PAM user record data: @PAMERR@");
+ 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, "pam-systemd-home", &bus, bus_data);
+ 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_debug_syslog(handle, 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_debug_syslog(handle, 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)
+ return pam_syslog_pam_error(handle, LOG_ERR, r,
+ "Failed to set PAM user record data '%s': @PAMERR@", homed_field);
+
+ /* 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)
+ return pam_syslog_pam_error(handle, LOG_ERR, r,
+ "Failed to set PAM user record data '%s': @PAMERR@", homed_field);
+
+ TAKE_PTR(json_copy);
+ }
+
+ r = json_parse(json, JSON_PARSE_SENSITIVE, &v, NULL, NULL);
+ if (r < 0)
+ return pam_syslog_errno(handle, LOG_ERR, r, "Failed to parse JSON user record: %m");
+
+ ur = user_record_new();
+ if (!ur)
+ return pam_log_oom(handle);
+
+ r = user_record_load(ur, v, USER_RECORD_LOAD_REFUSE_SECRET|USER_RECORD_PERMISSIVE);
+ if (r < 0)
+ return pam_syslog_errno(handle, LOG_ERR, r, "Failed to load user record: %m");
+
+ /* Safety check if cached record actually matches what we are looking for */
+ if (!streq_ptr(username, ur->user_name))
+ return pam_syslog_pam_error(handle, LOG_ERR, PAM_SERVICE_ERR,
+ "Acquired user record does not match user name.");
+
+ 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_pam_error(handle, LOG_ERR, r,
+ "Failed to set PAM user record data '%s' to invalid, ignoring: @PAMERR@",
+ homed_field);
+
+ 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_pam_error(handle, LOG_ERR, r,
+ "Failed to release PAM user record data '%s': @PAMERR@", homed_field);
+
+ 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_pam_error(handle, LOG_ERR, k,
+ "Failed to release PAM user record data '%s': @PAMERR@", generic_field);
+
+ 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,
+ bool debug) {
+
+ assert(user_name);
+ 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);
+ return pam_syslog_pam_error(handle, LOG_ERR, PAM_PERM_DENIED,
+ "Failed to acquire home for user %s: %s", user_name, bus_error_message(error, ret));
+
+ } else if (sd_bus_error_has_name(error, BUS_ERROR_AUTHENTICATION_LIMIT_HIT)) {
+ (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, _("Too frequent login attempts for user %s, try again later."), user_name);
+ return pam_syslog_pam_error(handle, LOG_ERR, PAM_MAXTRIES,
+ "Failed to acquire home for user %s: %s", user_name, bus_error_message(error, ret));
+
+ } else if (sd_bus_error_has_name(error, BUS_ERROR_BAD_PASSWORD)) {
+ _cleanup_(erase_and_freep) char *newp = NULL;
+
+ assert(secret);
+
+ /* 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_debug_syslog(handle, debug, "Password request aborted.");
+ return PAM_AUTHTOK_ERR;
+ }
+
+ r = user_record_set_password(secret, STRV_MAKE(newp), true);
+ if (r < 0)
+ return pam_syslog_errno(handle, LOG_ERR, r, "Failed to store password: %m");
+
+ } else if (sd_bus_error_has_name(error, BUS_ERROR_BAD_RECOVERY_KEY)) {
+ _cleanup_(erase_and_freep) char *newp = NULL;
+
+ assert(secret);
+
+ /* Hmm, homed asks for recovery key (because no regular password is defined maybe)? Provide it. */
+
+ if (strv_isempty(secret->password))
+ r = pam_prompt(handle, PAM_PROMPT_ECHO_OFF, &newp, _("Recovery key: "));
+ else {
+ (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, _("Password/recovery key incorrect or not sufficient for authentication of user %s."), user_name);
+ r = pam_prompt(handle, PAM_PROMPT_ECHO_OFF, &newp, _("Sorry, reenter recovery key: "));
+ }
+ if (r != PAM_SUCCESS)
+ return PAM_CONV_ERR; /* no logging here */
+
+ if (isempty(newp)) {
+ pam_debug_syslog(handle, debug, "Recovery key request aborted.");
+ return PAM_AUTHTOK_ERR;
+ }
+
+ r = user_record_set_password(secret, STRV_MAKE(newp), true);
+ if (r < 0)
+ return pam_syslog_errno(handle, LOG_ERR, r, "Failed to store recovery key: %m");
+
+ } else if (sd_bus_error_has_name(error, BUS_ERROR_BAD_PASSWORD_AND_NO_TOKEN)) {
+ _cleanup_(erase_and_freep) char *newp = NULL;
+
+ assert(secret);
+
+ 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_debug_syslog(handle, debug, "Password request aborted.");
+ return PAM_AUTHTOK_ERR;
+ }
+
+
+ r = user_record_set_password(secret, STRV_MAKE(newp), true);
+ if (r < 0)
+ return pam_syslog_errno(handle, LOG_ERR, r, "Failed to store password: %m");
+
+ } else if (sd_bus_error_has_name(error, BUS_ERROR_TOKEN_PIN_NEEDED)) {
+ _cleanup_(erase_and_freep) char *newp = NULL;
+
+ assert(secret);
+
+ 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_debug_syslog(handle, debug, "PIN request aborted.");
+ return PAM_AUTHTOK_ERR;
+ }
+
+ r = user_record_set_token_pin(secret, STRV_MAKE(newp), false);
+ if (r < 0)
+ return pam_syslog_errno(handle, LOG_ERR, r, "Failed to store PIN: %m");
+
+ } else if (sd_bus_error_has_name(error, BUS_ERROR_TOKEN_PROTECTED_AUTHENTICATION_PATH_NEEDED)) {
+
+ assert(secret);
+
+ (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)
+ return pam_syslog_errno(handle, LOG_ERR, 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)) {
+
+ assert(secret);
+
+ (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, _("Please confirm presence on security token of user %s."), user_name);
+
+ r = user_record_set_fido2_user_presence_permitted(secret, true);
+ if (r < 0)
+ return pam_syslog_errno(handle, LOG_ERR, r,
+ "Failed to set FIDO2 user presence permitted flag: %m");
+
+ } else if (sd_bus_error_has_name(error, BUS_ERROR_TOKEN_USER_VERIFICATION_NEEDED)) {
+
+ assert(secret);
+
+ (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, _("Please verify user on security token of user %s."), user_name);
+
+ r = user_record_set_fido2_user_verification_permitted(secret, true);
+ if (r < 0)
+ return pam_syslog_errno(handle, LOG_ERR, r,
+ "Failed to set FIDO2 user verification permitted flag: %m");
+
+ } 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;
+
+ assert(secret);
+
+ (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_debug_syslog(handle, debug, "PIN request aborted.");
+ return PAM_AUTHTOK_ERR;
+ }
+
+ r = user_record_set_token_pin(secret, STRV_MAKE(newp), false);
+ if (r < 0)
+ return pam_syslog_errno(handle, LOG_ERR, r, "Failed to store PIN: %m");
+
+ } else if (sd_bus_error_has_name(error, BUS_ERROR_TOKEN_BAD_PIN_FEW_TRIES_LEFT)) {
+ _cleanup_(erase_and_freep) char *newp = NULL;
+
+ assert(secret);
+
+ (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_debug_syslog(handle, debug, "PIN request aborted.");
+ return PAM_AUTHTOK_ERR;
+ }
+
+ r = user_record_set_token_pin(secret, STRV_MAKE(newp), false);
+ if (r < 0)
+ return pam_syslog_errno(handle, LOG_ERR, r, "Failed to store PIN: %m");
+
+ } else if (sd_bus_error_has_name(error, BUS_ERROR_TOKEN_BAD_PIN_ONE_TRY_LEFT)) {
+ _cleanup_(erase_and_freep) char *newp = NULL;
+
+ assert(secret);
+
+ (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_debug_syslog(handle, debug, "PIN request aborted.");
+ return PAM_AUTHTOK_ERR;
+ }
+
+ r = user_record_set_token_pin(secret, STRV_MAKE(newp), false);
+ if (r < 0)
+ return pam_syslog_errno(handle, LOG_ERR, r, "Failed to store PIN: %m");
+
+ } else
+ return pam_syslog_pam_error(handle, LOG_ERR, PAM_SERVICE_ERR,
+ "Failed to acquire home for user %s: %s", user_name, bus_error_message(error, ret));
+
+ return PAM_SUCCESS;
+}
+
+static int acquire_home(
+ pam_handle_t *handle,
+ bool please_authenticate,
+ bool please_suspend,
+ bool debug,
+ PamBusData **bus_data) {
+
+ _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 = -EBADF;
+ _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)
+ return pam_syslog_pam_error(handle, LOG_ERR, r, "Failed to get user name: @PAMERR@");
+
+ if (isempty(username))
+ return pam_syslog_pam_error(handle, LOG_ERR, PAM_SERVICE_ERR, "User name not set.");
+
+ /* 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))
+ return pam_syslog_pam_error(handle, LOG_ERR, r,
+ "Failed to retrieve PAM home reference fd: @PAMERR@");
+ if (r == PAM_SUCCESS && PTR_TO_FD(home_fd_ptr) >= 0)
+ return PAM_SUCCESS;
+
+ r = pam_acquire_bus_connection(handle, "pam-systemd-home", &bus, bus_data);
+ if (r != PAM_SUCCESS)
+ return r;
+
+ r = acquire_user_record(handle, username, debug, &ur, bus_data);
+ 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))
+ return pam_syslog_pam_error(handle, LOG_ERR, r,
+ "Failed to get cached password: @PAMERR@");
+
+ if (!isempty(cached_password)) {
+ r = user_record_set_password(secret, STRV_MAKE(cached_password), true);
+ if (r < 0)
+ return pam_syslog_errno(handle, LOG_ERR, r, "Failed to store password: %m");
+ }
+ }
+
+ 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, debug);
+ 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);
+
+ if (please_authenticate || debug)
+ 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)
+ return pam_syslog_errno(handle, LOG_ERR, errno,
+ "Failed to duplicate acquired fd: %m");
+ 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);
+ return pam_syslog_pam_error(handle, LOG_ERR, PAM_MAXTRIES,
+ "Failed to acquire home for user %s: %s", ur->user_name, bus_error_message(&error, r));
+ }
+
+ /* 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 != PAM_SUCCESS)
+ return pam_syslog_pam_error(handle, LOG_ERR, r, "Failed to set PAM auth token: @PAMERR@");
+ }
+
+ r = pam_set_data(handle, fd_field, FD_TO_PTR(acquired_fd), cleanup_home_fd);
+ if (r != PAM_SUCCESS)
+ return pam_syslog_pam_error(handle, LOG_ERR, r, "Failed to set PAM bus data: @PAMERR@");
+ 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)
+ return pam_syslog_pam_error(handle, LOG_ERR, r, "Failed to retrieve PAM home reference fd: @PAMERR@");
+
+ r = pam_set_data(handle, fd_field, NULL, NULL);
+ if (r != PAM_SUCCESS)
+ return pam_syslog_pam_error(handle, LOG_ERR, r, "Failed to release PAM home reference fd: @PAMERR@");
+
+ return PAM_SUCCESS;
+}
+
+_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;
+
+ pam_debug_syslog(handle, debug, "pam-systemd-homed authenticating");
+
+ return acquire_home(handle, /* please_authenticate= */ true, suspend_please, debug, NULL);
+}
+
+_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) {
+
+ /* Let's release the D-Bus connection once this function exits, 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. */
+ _cleanup_(pam_bus_data_disconnectp) PamBusData *d = NULL;
+ 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;
+
+ pam_debug_syslog(handle, debug, "pam-systemd-homed session start");
+
+ r = acquire_home(handle, /* please_authenticate = */ false, suspend_please, debug, &d);
+ 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)
+ return pam_syslog_pam_error(handle, LOG_ERR, r,
+ "Failed to set PAM environment variable $SYSTEMD_HOME: @PAMERR@");
+
+ r = pam_putenv(handle, suspend_please ? "SYSTEMD_HOME_SUSPEND=1" : "SYSTEMD_HOME_SUSPEND=0");
+ if (r != PAM_SUCCESS)
+ return pam_syslog_pam_error(handle, LOG_ERR, r,
+ "Failed to set PAM environment variable $SYSTEMD_HOME_SUSPEND: @PAMERR@");
+
+ 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;
+
+ pam_debug_syslog(handle, debug, "pam-systemd-homed session end");
+
+ r = pam_get_user(handle, &username, NULL);
+ if (r != PAM_SUCCESS)
+ return pam_syslog_pam_error(handle, LOG_ERR, r, "Failed to get user name: @PAMERR@");
+
+ if (isempty(username))
+ return pam_syslog_pam_error(handle, LOG_ERR, PAM_SERVICE_ERR, "User name not set.");
+
+ /* 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, "pam-systemd-home", &bus, NULL);
+ 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))
+ return pam_syslog_pam_error(handle, LOG_ERR, PAM_SESSION_ERR,
+ "Failed to release user home: %s", bus_error_message(&error, r));
+
+ pam_syslog(handle, LOG_NOTICE, "Not deactivating home directory of %s, as it is still used.", username);
+ }
+
+ 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;
+
+ pam_debug_syslog(handle, debug, "pam-systemd-homed account management");
+
+ r = acquire_home(handle, /* please_authenticate = */ false, please_suspend, debug, NULL);
+ if (r != PAM_SUCCESS)
+ return r;
+
+ r = acquire_user_record(handle, NULL, debug, &ur, NULL);
+ 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) {
+ (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, _("Too many logins, try again in %s."),
+ FORMAT_TIMESPAN(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 required."));
+ return PAM_NEW_AUTHTOK_REQD;
+
+ /* 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. */
+ case -EKEYREJECTED:
+ (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;
+
+ pam_debug_syslog(handle, debug, "pam-systemd-homed account management");
+
+ r = pam_acquire_bus_connection(handle, "pam-systemd-home", &bus, NULL);
+ if (r != PAM_SUCCESS)
+ return r;
+
+ r = acquire_user_record(handle, NULL, debug, &ur, NULL);
+ 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))
+ return pam_syslog_pam_error(handle, LOG_ERR, r, "Failed to get old password: @PAMERR@");
+
+ r = pam_get_item(handle, PAM_AUTHTOK, (const void**) &new_password);
+ if (!IN_SET(r, PAM_BAD_ITEM, PAM_SUCCESS))
+ return pam_syslog_pam_error(handle, LOG_ERR, r, "Failed to get cached password: @PAMERR@");
+
+ 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)
+ return pam_syslog_pam_error(handle, LOG_ERR, r, "Failed to get new password: @PAMERR@");
+
+ if (isempty(new_password)) {
+ pam_debug_syslog(handle, 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)
+ return pam_syslog_pam_error(handle, LOG_ERR, r, "Failed to get password again: @PAMERR@");
+
+ // 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)
+ return pam_syslog_errno(handle, LOG_ERR, r, "Failed to store old password: %m");
+ }
+
+ 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)
+ return pam_syslog_errno(handle, LOG_ERR, r, "Failed to store new password: %m");
+
+ 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, debug);
+ if (r == PAM_CONV_ERR)
+ return pam_syslog_pam_error(handle, LOG_ERR, r,
+ "Failed to prompt for password/prompt.");
+ if (r != PAM_SUCCESS)
+ return r;
+ } else
+ return pam_syslog_pam_error(handle, LOG_NOTICE, PAM_SUCCESS,
+ "Successfully changed password for user %s.", ur->user_name);
+
+ if (++n_attempts >= 5)
+ break;
+
+ /* Try again */
+ };
+
+ return pam_syslog_pam_error(handle, LOG_NOTICE, PAM_MAXTRIES,
+ "Failed to change password for user %s: @PAMERR@", ur->user_name);
+}
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/user-record-password-quality.c b/src/home/user-record-password-quality.c
new file mode 100644
index 0000000..38f4acb
--- /dev/null
+++ b/src/home/user-record-password-quality.c
@@ -0,0 +1,87 @@
+/* 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 "password-quality-util.h"
+#include "strv.h"
+#include "user-record-password-quality.h"
+#include "user-record-util.h"
+
+#if HAVE_PASSWDQC || HAVE_PWQUALITY
+
+int user_record_check_password_quality(
+ UserRecord *hr,
+ UserRecord *secret,
+ sd_bus_error *error) {
+
+ _cleanup_free_ char *auxerror = NULL;
+ int r;
+
+ assert(hr);
+ assert(secret);
+
+ /* This is a bit more complex than one might think at first. check_password_quality() 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;
+
+ 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 = check_password_quality(*pp, *old, hr->user_name, &auxerror);
+ if (r <= 0)
+ goto error;
+
+ called = true;
+ }
+
+ if (called)
+ continue;
+
+ /* If there are no old passwords, let's call check_password_quality() without any. */
+ r = check_password_quality(*pp, /* old */ NULL, hr->user_name, &auxerror);
+ if (r <= 0)
+ goto error;
+ }
+ return 1;
+
+error:
+ if (r == 0)
+ return sd_bus_error_setf(error, BUS_ERROR_LOW_PASSWORD_QUALITY,
+ "Password too weak: %s", auxerror);
+ if (ERRNO_IS_NOT_SUPPORTED(r))
+ return 0;
+ return log_debug_errno(r, "Failed to check password quality: %m");
+}
+
+#else
+
+int user_record_check_password_quality(
+ UserRecord *hr,
+ UserRecord *secret,
+ sd_bus_error *error) {
+
+ return 0;
+}
+
+#endif
diff --git a/src/home/user-record-password-quality.h b/src/home/user-record-password-quality.h
new file mode 100644
index 0000000..c7d6ec6
--- /dev/null
+++ b/src/home/user-record-password-quality.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_check_password_quality(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..dd099a0
--- /dev/null
+++ b/src/home/user-record-sign.c
@@ -0,0 +1,161 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <openssl/pem.h>
+
+#include "fd-util.h"
+#include "fileio.h"
+#include "memstream-util.h"
+#include "openssl-util.h"
+#include "user-record-sign.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|USER_RECORD_PERMISSIVE, &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);
+}
+
+int user_record_sign(UserRecord *ur, EVP_PKEY *private_key, UserRecord **ret) {
+ _cleanup_(memstream_done) MemStream m = {};
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+ _cleanup_(user_record_unrefp) UserRecord *signed_ur = NULL;
+ _cleanup_free_ char *text = NULL, *key = NULL;
+ _cleanup_free_ void *signature = NULL;
+ size_t signature_size = 0;
+ FILE *f;
+ int r;
+
+ assert(ur);
+ assert(private_key);
+ assert(ret);
+
+ r = user_record_signable_json(ur, &text);
+ if (r < 0)
+ return r;
+
+ r = digest_and_sign(/* md= */ NULL, private_key, text, SIZE_MAX, &signature, &signature_size);
+ if (r < 0)
+ return r;
+
+ f = memstream_init(&m);
+ if (!f)
+ return -ENOMEM;
+
+ if (PEM_write_PUBKEY(f, private_key) <= 0)
+ return -EIO;
+
+ r = memstream_finalize(&m, &key, NULL);
+ if (r < 0)
+ return r;
+
+ v = json_variant_ref(ur->json);
+
+ r = json_variant_set_fieldb(
+ &v,
+ "signature",
+ 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;
+
+ 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|USER_RECORD_PERMISSIVE);
+ 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..089cbb1
--- /dev/null
+++ b/src/home/user-record-util.c
@@ -0,0 +1,1512 @@
+/* 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 "recovery-key.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;
+ 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(get_home_root(), 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_CONST_STRING("regular")),
+ JSON_BUILD_PAIR("binding", JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR(SD_ID128_TO_STRING(mid), 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;
+ 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), 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), JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("service", JSON_BUILD_CONST_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|USER_RECORD_PERMISSIVE);
+ 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|USER_RECORD_PERMISSIVE, 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;
+ _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;
+
+ 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(SD_ID128_TO_UUID_STRING(partition_uuid))),
+ JSON_BUILD_PAIR_CONDITION(!sd_id128_is_null(luks_uuid), "luksUuid", JSON_BUILD_STRING(SD_ID128_TO_UUID_STRING(luks_uuid))),
+ JSON_BUILD_PAIR_CONDITION(!sd_id128_is_null(fs_uuid), "fileSystemUuid", JSON_BUILD_STRING(SD_ID128_TO_UUID_STRING(fs_uuid))),
+ 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, SD_ID128_TO_STRING(mid)));
+ if (be) {
+ r = json_variant_merge_object(&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, SD_ID128_TO_STRING(mid), 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, /* ignore_hidden_or_backup= */ false);
+ 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_IS_XATTR_ABSENT(errno))
+ 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();
+ }
+}
+
+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) {
+ 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) {
+ 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;
+ size_t idx = SIZE_MAX, n;
+ JsonVariant *per_machine;
+ sd_id128_t mid;
+ int r;
+
+ assert(h);
+
+ if (!h->json)
+ return -EUNATCH;
+
+ r = sd_id128_get_machine(&mid);
+ if (r < 0)
+ return r;
+
+ r = json_variant_new_string(&midv, SD_ID128_TO_STRING(mid));
+ 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;
+ 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_erase_ 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_erase_ 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;
+}
+
+int user_record_set_fido2_user_verification_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("fido2UserVerificationPermitted"));
+ else
+ r = json_variant_set_field_boolean(&w, "fido2UserVerificationPermitted", 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_verification_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;
+ }
+
+ if (secret->fido2_user_verification_permitted >= 0) {
+ r = user_record_set_fido2_user_verification_permitted(
+ h,
+ secret->fido2_user_verification_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;
+ 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)));
+
+ 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, SD_ID128_TO_STRING(mid), 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;
+ 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)));
+
+ 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, SD_ID128_TO_STRING(mid), 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;
+ 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)));
+
+ 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, SD_ID128_TO_STRING(mid), 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_set(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_set(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_set(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_set(error, SD_BUS_ERROR_INVALID_ARGS, "Not accepted with service not matching io.systemd.Home.");
+
+ return 0;
+}
+
+bool user_record_shall_rebalance(UserRecord *h) {
+ assert(h);
+
+ if (user_record_rebalance_weight(h) == REBALANCE_WEIGHT_OFF)
+ return false;
+
+ if (user_record_storage(h) != USER_LUKS)
+ return false;
+
+ if (!path_startswith(user_record_image_path(h), get_home_root())) /* This is the only pool we rebalance in */
+ return false;
+
+ return true;
+}
+
+int user_record_set_rebalance_weight(UserRecord *h, uint64_t weight) {
+ _cleanup_(json_variant_unrefp) JsonVariant *new_per_machine_array = NULL, *machine_id_variant = NULL,
+ *machine_id_array = NULL, *per_machine_entry = NULL;
+ _cleanup_free_ JsonVariant **array = NULL;
+ size_t idx = SIZE_MAX, n;
+ JsonVariant *per_machine;
+ sd_id128_t mid;
+ int r;
+
+ assert(h);
+
+ if (!h->json)
+ return -EUNATCH;
+
+ r = sd_id128_get_machine(&mid);
+ if (r < 0)
+ return r;
+
+ r = json_variant_new_id128(&machine_id_variant, mid);
+ if (r < 0)
+ return r;
+
+ r = json_variant_new_array(&machine_id_array, (JsonVariant*[]) { machine_id_variant }, 1);
+ if (r < 0)
+ return r;
+
+ per_machine = json_variant_by_key(h->json, "perMachine");
+ if (per_machine) {
+ 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 (size_t 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, machine_id_variant) ||
+ json_variant_equal(m, machine_id_array)) {
+ /* 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
+ per_machine_entry = json_variant_ref(array[idx]);
+
+ } else {
+ array = new(JsonVariant*, 1);
+ if (!array)
+ return -ENOMEM;
+
+ idx = 0;
+ n = 1;
+ }
+
+ if (!per_machine_entry) {
+ r = json_variant_set_field(&per_machine_entry, "matchMachineId", machine_id_array);
+ if (r < 0)
+ return r;
+ }
+
+ if (weight == REBALANCE_WEIGHT_UNSET)
+ r = json_variant_set_field(&per_machine_entry, "rebalanceWeight", NULL); /* set explicitly to NULL (so that the perMachine setting we are setting here can override the global setting) */
+ else
+ r = json_variant_set_field_unsigned(&per_machine_entry, "rebalanceWeight", weight);
+ if (r < 0)
+ return r;
+
+ assert(idx < n);
+ array[idx] = per_machine_entry;
+
+ r = json_variant_new_array(&new_per_machine_array, array, n);
+ if (r < 0)
+ return r;
+
+ r = json_variant_set_field(&h->json, "perMachine", new_per_machine_array);
+ if (r < 0)
+ return r;
+
+ h->rebalance_weight = weight;
+ h->mask |= USER_RECORD_PER_MACHINE;
+ return 0;
+}
diff --git a/src/home/user-record-util.h b/src/home/user-record-util.h
new file mode 100644
index 0000000..508e2bd
--- /dev/null
+++ b/src/home/user-record-util.h
@@ -0,0 +1,65 @@
+/* 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 = -EINVAL,
+} 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_fido2_user_verification_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);
+
+bool user_record_shall_rebalance(UserRecord *h);
+int user_record_set_rebalance_weight(UserRecord *h, uint64_t weight);