diff --git a/meson.build b/meson.build index 54820d3f6a..f0f9bdb0ce 100644 --- a/meson.build +++ b/meson.build @@ -243,6 +243,7 @@ conf.set_quoted('SYSTEMD_EXPORT_PATH', join_paths(rootlib conf.set_quoted('VENDOR_KEYRING_PATH', join_paths(rootlibexecdir, 'import-pubring.gpg')) conf.set_quoted('USER_KEYRING_PATH', join_paths(pkgsysconfdir, 'import-pubring.gpg')) conf.set_quoted('DOCUMENT_ROOT', join_paths(pkgdatadir, 'gatewayd')) +conf.set_quoted('SYSTEMD_HOMEWORK_PATH', join_paths(rootlibexecdir, 'systemd-homework')) conf.set_quoted('SYSTEMD_USERWORK_PATH', join_paths(rootlibexecdir, 'systemd-userwork')) conf.set10('MEMORY_ACCOUNTING_DEFAULT', memory_accounting_default) conf.set_quoted('MEMORY_ACCOUNTING_DEFAULT_YES_NO', memory_accounting_default ? 'yes' : 'no') @@ -884,6 +885,16 @@ else endif conf.set10('HAVE_LIBFDISK', have) +want_pwquality = get_option('pwquality') +if want_pwquality != 'false' and not skip_deps + libpwquality = dependency('pwquality', required : want_pwquality == 'true') + have = libpwquality.found() +else + have = false + libpwquality = [] +endif +conf.set10('HAVE_PWQUALITY', have) + want_seccomp = get_option('seccomp') if want_seccomp != 'false' and not skip_deps libseccomp = dependency('libseccomp', @@ -1011,6 +1022,9 @@ if want_libcryptsetup != 'false' and not skip_deps version : '>= 2.0.1', required : want_libcryptsetup == 'true') have = libcryptsetup.found() + + conf.set10('HAVE_CRYPT_SET_METADATA_SIZE', + have and cc.has_function('crypt_set_metadata_size', dependencies : libcryptsetup)) else have = false libcryptsetup = [] @@ -1316,6 +1330,19 @@ else endif conf.set10('ENABLE_IMPORTD', have) +want_homed = get_option('homed') +if want_homed != 'false' + have = (conf.get('HAVE_OPENSSL') == 1 and + conf.get('HAVE_LIBFDISK') == 1 and + conf.get('HAVE_LIBCRYPTSETUP') == 1) + if want_homed == 'true' and not have + error('homed support was requested, but dependencies are not available') + endif +else + have = false +endif +conf.set10('ENABLE_HOMED', have) + want_remote = get_option('remote') if want_remote != 'false' have_deps = [conf.get('HAVE_MICROHTTPD') == 1, @@ -1564,6 +1591,7 @@ subdir('src/locale') subdir('src/machine') subdir('src/portable') subdir('src/userdb') +subdir('src/home') subdir('src/nspawn') subdir('src/resolve') subdir('src/timedate') @@ -2034,6 +2062,35 @@ if conf.get('ENABLE_USERDB') == 1 install_dir : rootbindir) endif +if conf.get('ENABLE_HOMED') == 1 + executable('systemd-homework', + systemd_homework_sources, + include_directories : includes, + link_with : [libshared], + dependencies : [threads, + libcryptsetup, + libblkid, + libcrypt, + libopenssl, + libfdisk, + libp11kit], + install_rpath : rootlibexecdir, + install : true, + install_dir : rootlibexecdir) + + executable('systemd-homed', + systemd_homed_sources, + include_directories : includes, + link_with : [libshared], + dependencies : [threads, + libcrypt, + libopenssl, + libpwquality], + install_rpath : rootlibexecdir, + install : true, + install_dir : rootlibexecdir) +endif + foreach alias : ['halt', 'poweroff', 'reboot', 'runlevel', 'shutdown', 'telinit'] meson.add_install_script(meson_make_symlink, join_paths(rootbindir, 'systemctl'), @@ -3291,6 +3348,8 @@ missing = [] foreach tuple : [ ['libcryptsetup'], ['PAM'], + ['pwquality'], + ['fdisk'], ['p11kit'], ['AUDIT'], ['IMA'], @@ -3329,6 +3388,7 @@ foreach tuple : [ ['machined'], ['portabled'], ['userdb'], + ['homed'], ['importd'], ['hostnamed'], ['timedated'], diff --git a/meson_options.txt b/meson_options.txt index e512d25480..1434ae706f 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -98,6 +98,8 @@ option('portabled', type : 'boolean', description : 'install the systemd-portabled stack') option('userdb', type : 'boolean', description : 'install the systemd-userdbd stack') +option('homed', type : 'boolean', + description : 'install the systemd-homed stack') option('networkd', type : 'boolean', description : 'install the systemd-networkd stack') option('timedated', type : 'boolean', @@ -268,6 +270,8 @@ option('kmod', type : 'combo', choices : ['auto', 'true', 'false'], description : 'support for loadable modules') option('pam', type : 'combo', choices : ['auto', 'true', 'false'], description : 'PAM support') +option('pwquality', type : 'combo', choices : ['auto', 'true', 'false'], + description : 'libpwquality support') option('microhttpd', type : 'combo', choices : ['auto', 'true', 'false'], description : 'libµhttpd support') option('libcryptsetup', type : 'combo', choices : ['auto', 'true', 'false'], diff --git a/src/home/home-util.c b/src/home/home-util.c new file mode 100644 index 0000000000..bf4f238099 --- /dev/null +++ b/src/home/home-util.c @@ -0,0 +1,160 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include "dns-domain.h" +#include "errno-util.h" +#include "home-util.h" +#include "libcrypt-util.h" +#include "memory-util.h" +#include "path-util.h" +#include "string-util.h" +#include "strv.h" +#include "user-util.h" + +bool suitable_user_name(const char *name) { + + /* Checks whether the specified name is suitable for management via homed. Note that our client side + * usually validate susing a simple valid_user_group_name(), while server side we are a bit more + * restrictive, so that we can change the rules server side without having to update things client + * side, too. */ + + if (!valid_user_group_name(name)) + return false; + + /* We generally rely on NSS to tell us which users not to care for, but let's filter out some + * particularly well-known users. */ + if (STR_IN_SET(name, + "root", + "nobody", + NOBODY_USER_NAME, NOBODY_GROUP_NAME)) + return false; + + /* Let's also defend our own namespace, as well as Debian's (unwritten?) logic of prefixing system + * users with underscores. */ + if (STARTSWITH_SET(name, "systemd-", "_")) + return false; + + return true; +} + +int suitable_realm(const char *realm) { + _cleanup_free_ char *normalized = NULL; + int r; + + /* Similar to the above: let's validate the realm a bit stricter server-side than client side */ + + r = dns_name_normalize(realm, 0, &normalized); /* this also checks general validity */ + if (r == -EINVAL) + return 0; + if (r < 0) + return r; + + if (!streq(realm, normalized)) /* is this normalized? */ + return false; + + if (dns_name_is_root(realm)) /* Don't allow top level domain */ + return false; + + return true; +} + +int suitable_image_path(const char *path) { + + return !empty_or_root(path) && + path_is_valid(path) && + path_is_absolute(path); +} + +int split_user_name_realm(const char *t, char **ret_user_name, char **ret_realm) { + _cleanup_free_ char *user_name = NULL, *realm = NULL; + const char *c; + int r; + + assert(t); + assert(ret_user_name); + assert(ret_realm); + + c = strchr(t, '@'); + if (!c) { + user_name = strdup(t); + if (!user_name) + return -ENOMEM; + } else { + user_name = strndup(t, c - t); + if (!user_name) + return -ENOMEM; + + realm = strdup(c + 1); + if (!realm) + return -ENOMEM; + } + + if (!suitable_user_name(user_name)) + return -EINVAL; + + if (realm) { + r = suitable_realm(realm); + if (r < 0) + return r; + if (r == 0) + return -EINVAL; + } + + *ret_user_name = TAKE_PTR(user_name); + *ret_realm = TAKE_PTR(realm); + + return 0; +} + +int bus_message_append_secret(sd_bus_message *m, UserRecord *secret) { + _cleanup_(erase_and_freep) char *formatted = NULL; + JsonVariant *v; + int r; + + assert(m); + assert(secret); + + if (!FLAGS_SET(secret->mask, USER_RECORD_SECRET)) + return sd_bus_message_append(m, "s", "{}"); + + v = json_variant_by_key(secret->json, "secret"); + if (!v) + return -EINVAL; + + r = json_variant_format(v, 0, &formatted); + if (r < 0) + return r; + + return sd_bus_message_append(m, "s", formatted); +} + +int test_password_one(const char *hashed_password, const char *password) { + struct crypt_data cc = {}; + const char *k; + bool b; + + errno = 0; + k = crypt_r(password, hashed_password, &cc); + if (!k) { + explicit_bzero_safe(&cc, sizeof(cc)); + return errno_or_else(EINVAL); + } + + b = streq(k, hashed_password); + explicit_bzero_safe(&cc, sizeof(cc)); + return b; +} + +int test_password_many(char **hashed_password, const char *password) { + char **hpw; + int r; + + STRV_FOREACH(hpw, hashed_password) { + r = test_password_one(*hpw, password); + if (r < 0) + return r; + if (r > 0) + return true; + } + + return false; +} diff --git a/src/home/home-util.h b/src/home/home-util.h new file mode 100644 index 0000000000..df20c0af71 --- /dev/null +++ b/src/home/home-util.h @@ -0,0 +1,24 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include + +#include "sd-bus.h" + +#include "time-util.h" +#include "user-record.h" + +bool suitable_user_name(const char *name); +int suitable_realm(const char *realm); +int suitable_image_path(const char *path); + +int split_user_name_realm(const char *t, char **ret_user_name, char **ret_realm); + +int bus_message_append_secret(sd_bus_message *m, UserRecord *secret); + +/* Many of our operations might be slow due to crypto, fsck, recursive chown() and so on. For these + * operations permit a *very* long time-out */ +#define HOME_SLOW_BUS_CALL_TIMEOUT_USEC (2*USEC_PER_MINUTE) + +int test_password_one(const char *hashed_password, const char *password); +int test_password_many(char **hashed_password, const char *password); diff --git a/src/home/homed-bus.c b/src/home/homed-bus.c new file mode 100644 index 0000000000..0193089668 --- /dev/null +++ b/src/home/homed-bus.c @@ -0,0 +1,64 @@ +#include "homed-bus.h" +#include "strv.h" + +int bus_message_read_secret(sd_bus_message *m, UserRecord **ret, sd_bus_error *error) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *full = NULL; + _cleanup_(user_record_unrefp) UserRecord *hr = NULL; + unsigned line = 0, column = 0; + const char *json; + int r; + + assert(ret); + + r = sd_bus_message_read(m, "s", &json); + if (r < 0) + return r; + + r = json_parse(json, JSON_PARSE_SENSITIVE, &v, &line, &column); + if (r < 0) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Failed to parse JSON secret record at %u:%u: %m", line, column); + + r = json_build(&full, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("secret", JSON_BUILD_VARIANT(v)))); + if (r < 0) + return r; + + hr = user_record_new(); + if (!hr) + return -ENOMEM; + + r = user_record_load(hr, full, USER_RECORD_REQUIRE_SECRET); + if (r < 0) + return r; + + *ret = TAKE_PTR(hr); + return 0; +} + +int bus_message_read_home_record(sd_bus_message *m, UserRecordLoadFlags flags, UserRecord **ret, sd_bus_error *error) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + _cleanup_(user_record_unrefp) UserRecord *hr = NULL; + unsigned line = 0, column = 0; + const char *json; + int r; + + assert(ret); + + r = sd_bus_message_read(m, "s", &json); + if (r < 0) + return r; + + r = json_parse(json, JSON_PARSE_SENSITIVE, &v, &line, &column); + if (r < 0) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Failed to parse JSON identity record at %u:%u: %m", line, column); + + hr = user_record_new(); + if (!hr) + return -ENOMEM; + + r = user_record_load(hr, v, flags); + if (r < 0) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "JSON data is not a valid identity record"); + + *ret = TAKE_PTR(hr); + return 0; +} diff --git a/src/home/homed-bus.h b/src/home/homed-bus.h new file mode 100644 index 0000000000..20f13b43ad --- /dev/null +++ b/src/home/homed-bus.h @@ -0,0 +1,10 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include "sd-bus.h" + +#include "user-record.h" +#include "json.h" + +int bus_message_read_secret(sd_bus_message *m, UserRecord **ret, sd_bus_error *error); +int bus_message_read_home_record(sd_bus_message *m, UserRecordLoadFlags flags, UserRecord **ret, sd_bus_error *error); diff --git a/src/home/homed-home-bus.c b/src/home/homed-home-bus.c new file mode 100644 index 0000000000..02a87a5ec5 --- /dev/null +++ b/src/home/homed-home-bus.c @@ -0,0 +1,877 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include + +#include "bus-common-errors.h" +#include "bus-polkit.h" +#include "fd-util.h" +#include "homed-bus.h" +#include "homed-home-bus.h" +#include "homed-home.h" +#include "strv.h" +#include "user-record-util.h" +#include "user-util.h" + +static int property_get_unix_record( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error) { + + Home *h = userdata; + + assert(bus); + assert(reply); + assert(h); + + return sd_bus_message_append( + reply, "(suusss)", + h->user_name, + (uint32_t) h->uid, + h->record ? (uint32_t) user_record_gid(h->record) : GID_INVALID, + h->record ? user_record_real_name(h->record) : NULL, + h->record ? user_record_home_directory(h->record) : NULL, + h->record ? user_record_shell(h->record) : NULL); +} + +static int property_get_state( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error) { + + Home *h = userdata; + + assert(bus); + assert(reply); + assert(h); + + return sd_bus_message_append(reply, "s", home_state_to_string(home_get_state(h))); +} + +int bus_home_client_is_trusted(Home *h, sd_bus_message *message) { + _cleanup_(sd_bus_creds_unrefp) sd_bus_creds *creds = NULL; + uid_t euid; + int r; + + assert(h); + + if (!message) + return -EINVAL; + + r = sd_bus_query_sender_creds(message, SD_BUS_CREDS_EUID, &creds); + if (r < 0) + return r; + + r = sd_bus_creds_get_euid(creds, &euid); + if (r < 0) + return r; + + return euid == 0 || h->uid == euid; +} + +int bus_home_get_record_json( + Home *h, + sd_bus_message *message, + char **ret, + bool *ret_incomplete) { + + _cleanup_(user_record_unrefp) UserRecord *augmented = NULL; + UserRecordLoadFlags flags; + int r, trusted; + + assert(h); + assert(ret); + + trusted = bus_home_client_is_trusted(h, message); + if (trusted < 0) { + log_warning_errno(trusted, "Failed to determine whether client is trusted, assuming untrusted."); + trusted = false; + } + + flags = USER_RECORD_REQUIRE_REGULAR|USER_RECORD_ALLOW_PER_MACHINE|USER_RECORD_ALLOW_BINDING|USER_RECORD_STRIP_SECRET|USER_RECORD_ALLOW_STATUS|USER_RECORD_ALLOW_SIGNATURE; + if (trusted) + flags |= USER_RECORD_ALLOW_PRIVILEGED; + else + flags |= USER_RECORD_STRIP_PRIVILEGED; + + r = home_augment_status(h, flags, &augmented); + if (r < 0) + return r; + + r = json_variant_format(augmented->json, 0, ret); + if (r < 0) + return r; + + if (ret_incomplete) + *ret_incomplete = augmented->incomplete; + + return 0; +} + +static int property_get_user_record( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error) { + + _cleanup_free_ char *json = NULL; + Home *h = userdata; + bool incomplete; + int r; + + assert(bus); + assert(reply); + assert(h); + + r = bus_home_get_record_json(h, sd_bus_get_current_message(bus), &json, &incomplete); + if (r < 0) + return r; + + return sd_bus_message_append(reply, "(sb)", json, incomplete); +} + +int bus_home_method_activate( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + _cleanup_(user_record_unrefp) UserRecord *secret = NULL; + Home *h = userdata; + int r; + + assert(message); + assert(h); + + r = bus_message_read_secret(message, &secret, error); + if (r < 0) + return r; + + r = home_activate(h, secret, error); + if (r < 0) + return r; + + assert(r == 0); + assert(!h->current_operation); + + /* The operation is now in process, keep track of this message so that we can later reply to it. */ + r = home_set_current_message(h, message); + if (r < 0) + return r; + + return 1; +} + +int bus_home_method_deactivate( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + Home *h = userdata; + int r; + + assert(message); + assert(h); + + r = home_deactivate(h, false, error); + if (r < 0) + return r; + + assert(r == 0); + assert(!h->current_operation); + + r = home_set_current_message(h, message); + if (r < 0) + return r; + + return 1; +} + +int bus_home_method_unregister( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + Home *h = userdata; + int r; + + assert(message); + assert(h); + + r = bus_verify_polkit_async( + message, + CAP_SYS_ADMIN, + "org.freedesktop.home1.remove-home", + NULL, + true, + UID_INVALID, + &h->manager->polkit_registry, + error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Will call us back */ + + r = home_unregister(h, error); + if (r < 0) + return r; + + assert(r > 0); + + /* Note that home_unregister() destroyed 'h' here, so no more accesses */ + + return sd_bus_reply_method_return(message, NULL); +} + +int bus_home_method_realize( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + _cleanup_(user_record_unrefp) UserRecord *secret = NULL; + Home *h = userdata; + int r; + + assert(message); + assert(h); + + r = bus_message_read_secret(message, &secret, error); + if (r < 0) + return r; + + r = bus_verify_polkit_async( + message, + CAP_SYS_ADMIN, + "org.freedesktop.home1.create-home", + NULL, + true, + UID_INVALID, + &h->manager->polkit_registry, + error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Will call us back */ + + r = home_create(h, secret, error); + if (r < 0) + return r; + + assert(r == 0); + assert(!h->current_operation); + + h->unregister_on_failure = false; + + r = home_set_current_message(h, message); + if (r < 0) + return r; + + return 1; +} + +int bus_home_method_remove( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + Home *h = userdata; + int r; + + assert(message); + assert(h); + + r = bus_verify_polkit_async( + message, + CAP_SYS_ADMIN, + "org.freedesktop.home1.remove-home", + NULL, + true, + UID_INVALID, + &h->manager->polkit_registry, + error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Will call us back */ + + r = home_remove(h, error); + if (r < 0) + return r; + if (r > 0) /* Done already. Note that home_remove() destroyed 'h' here, so no more accesses */ + return sd_bus_reply_method_return(message, NULL); + + assert(!h->current_operation); + + r = home_set_current_message(h, message); + if (r < 0) + return r; + + return 1; +} + +int bus_home_method_fixate( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + _cleanup_(user_record_unrefp) UserRecord *secret = NULL; + Home *h = userdata; + int r; + + assert(message); + assert(h); + + r = bus_message_read_secret(message, &secret, error); + if (r < 0) + return r; + + r = home_fixate(h, secret, error); + if (r < 0) + return r; + + assert(r == 0); + assert(!h->current_operation); + + r = home_set_current_message(h, message); + if (r < 0) + return r; + + return 1; +} + +int bus_home_method_authenticate( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + _cleanup_(user_record_unrefp) UserRecord *secret = NULL; + Home *h = userdata; + int r; + + assert(message); + assert(h); + + r = bus_message_read_secret(message, &secret, error); + if (r < 0) + return r; + + r = bus_verify_polkit_async( + message, + CAP_SYS_ADMIN, + "org.freedesktop.home1.authenticate-home", + NULL, + true, + h->uid, + &h->manager->polkit_registry, + error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Will call us back */ + + r = home_authenticate(h, secret, error); + if (r < 0) + return r; + + assert(r == 0); + assert(!h->current_operation); + + r = home_set_current_message(h, message); + if (r < 0) + return r; + + return 1; +} + +int bus_home_method_update_record(Home *h, sd_bus_message *message, UserRecord *hr, sd_bus_error *error) { + int r; + + assert(h); + assert(message); + assert(hr); + + r = user_record_is_supported(hr, error); + if (r < 0) + return r; + + r = bus_verify_polkit_async( + message, + CAP_SYS_ADMIN, + "org.freedesktop.home1.update-home", + NULL, + true, + UID_INVALID, + &h->manager->polkit_registry, + error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Will call us back */ + + r = home_update(h, hr, error); + if (r < 0) + return r; + + assert(r == 0); + assert(!h->current_operation); + + r = home_set_current_message(h, message); + if (r < 0) + return r; + + return 1; +} + +int bus_home_method_update( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + _cleanup_(user_record_unrefp) UserRecord *hr = NULL; + Home *h = userdata; + int r; + + assert(message); + assert(h); + + r = bus_message_read_home_record(message, USER_RECORD_REQUIRE_REGULAR|USER_RECORD_REQUIRE_SECRET|USER_RECORD_ALLOW_PRIVILEGED|USER_RECORD_ALLOW_PER_MACHINE|USER_RECORD_ALLOW_SIGNATURE, &hr, error); + if (r < 0) + return r; + + return bus_home_method_update_record(h, message, hr, error); +} + +int bus_home_method_resize( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + _cleanup_(user_record_unrefp) UserRecord *secret = NULL; + Home *h = userdata; + uint64_t sz; + int r; + + assert(message); + assert(h); + + r = sd_bus_message_read(message, "t", &sz); + if (r < 0) + return r; + + r = bus_message_read_secret(message, &secret, error); + if (r < 0) + return r; + + r = bus_verify_polkit_async( + message, + CAP_SYS_ADMIN, + "org.freedesktop.home1.resize-home", + NULL, + true, + UID_INVALID, + &h->manager->polkit_registry, + error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Will call us back */ + + r = home_resize(h, sz, secret, error); + if (r < 0) + return r; + + assert(r == 0); + assert(!h->current_operation); + + r = home_set_current_message(h, message); + if (r < 0) + return r; + + return 1; +} + +int bus_home_method_change_password( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + _cleanup_(user_record_unrefp) UserRecord *new_secret = NULL, *old_secret = NULL; + Home *h = userdata; + int r; + + assert(message); + assert(h); + + r = bus_message_read_secret(message, &new_secret, error); + if (r < 0) + return r; + + r = bus_message_read_secret(message, &old_secret, error); + if (r < 0) + return r; + + r = bus_verify_polkit_async( + message, + CAP_SYS_ADMIN, + "org.freedesktop.home1.passwd-home", + NULL, + true, + h->uid, + &h->manager->polkit_registry, + error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Will call us back */ + + r = home_passwd(h, new_secret, old_secret, error); + if (r < 0) + return r; + + assert(r == 0); + assert(!h->current_operation); + + r = home_set_current_message(h, message); + if (r < 0) + return r; + + return 1; +} + +int bus_home_method_lock( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + Home *h = userdata; + int r; + + assert(message); + assert(h); + + r = home_lock(h, error); + if (r < 0) + return r; + if (r > 0) /* Done */ + return sd_bus_reply_method_return(message, NULL); + + /* The operation is now in process, keep track of this message so that we can later reply to it. */ + assert(!h->current_operation); + + r = home_set_current_message(h, message); + if (r < 0) + return r; + + return 1; +} + +int bus_home_method_unlock( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + _cleanup_(user_record_unrefp) UserRecord *secret = NULL; + Home *h = userdata; + int r; + + assert(message); + assert(h); + + r = bus_message_read_secret(message, &secret, error); + if (r < 0) + return r; + + r = home_unlock(h, secret, error); + if (r < 0) + return r; + + assert(r == 0); + assert(!h->current_operation); + + /* The operation is now in process, keep track of this message so that we can later reply to it. */ + r = home_set_current_message(h, message); + if (r < 0) + return r; + + return 1; +} + +int bus_home_method_acquire( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + _cleanup_(user_record_unrefp) UserRecord *secret = NULL; + _cleanup_(operation_unrefp) Operation *o = NULL; + _cleanup_close_ int fd = -1; + int r, please_suspend; + Home *h = userdata; + + assert(message); + assert(h); + + r = bus_message_read_secret(message, &secret, error); + if (r < 0) + return r; + + r = sd_bus_message_read(message, "b", &please_suspend); + if (r < 0) + return r; + + /* This operation might not be something we can executed immediately, hence queue it */ + fd = home_create_fifo(h, please_suspend); + if (fd < 0) + return sd_bus_reply_method_errnof(message, fd, "Failed to allocate fifo for %s: %m", h->user_name); + + o = operation_new(OPERATION_ACQUIRE, message); + if (!o) + return -ENOMEM; + + o->secret = TAKE_PTR(secret); + o->send_fd = TAKE_FD(fd); + + r = home_schedule_operation(h, o, error); + if (r < 0) + return r; + + return 1; +} + +int bus_home_method_ref( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + _cleanup_close_ int fd = -1; + Home *h = userdata; + HomeState state; + int please_suspend, r; + + assert(message); + assert(h); + + r = sd_bus_message_read(message, "b", &please_suspend); + if (r < 0) + return r; + + state = home_get_state(h); + switch (state) { + case HOME_ABSENT: + return sd_bus_error_setf(error, BUS_ERROR_HOME_ABSENT, "Home %s is currently missing or not plugged in.", h->user_name); + case HOME_UNFIXATED: + case HOME_INACTIVE: + return sd_bus_error_setf(error, BUS_ERROR_HOME_NOT_ACTIVE, "Home %s not active.", h->user_name); + case HOME_LOCKED: + return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name); + default: + if (HOME_STATE_IS_ACTIVE(state)) + break; + + return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "An operation on home %s is currently being executed.", h->user_name); + } + + fd = home_create_fifo(h, please_suspend); + if (fd < 0) + return sd_bus_reply_method_errnof(message, fd, "Failed to allocate fifo for %s: %m", h->user_name); + + return sd_bus_reply_method_return(message, "h", fd); +} + +int bus_home_method_release( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + _cleanup_(operation_unrefp) Operation *o = NULL; + Home *h = userdata; + int r; + + assert(message); + assert(h); + + o = operation_new(OPERATION_RELEASE, message); + if (!o) + return -ENOMEM; + + r = home_schedule_operation(h, o, error); + if (r < 0) + return r; + + return 1; +} + +/* We map a uid_t as uint32_t bus property, let's ensure this is safe. */ +assert_cc(sizeof(uid_t) == sizeof(uint32_t)); + +const sd_bus_vtable home_vtable[] = { + SD_BUS_VTABLE_START(0), + SD_BUS_PROPERTY("UserName", "s", NULL, offsetof(Home, user_name), SD_BUS_VTABLE_PROPERTY_CONST), + SD_BUS_PROPERTY("UID", "u", NULL, offsetof(Home, uid), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), + SD_BUS_PROPERTY("UnixRecord", "(suusss)", property_get_unix_record, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), + SD_BUS_PROPERTY("State", "s", property_get_state, 0, 0), + SD_BUS_PROPERTY("UserRecord", "(sb)", property_get_user_record, 0, SD_BUS_VTABLE_PROPERTY_EMITS_INVALIDATION|SD_BUS_VTABLE_SENSITIVE), + SD_BUS_METHOD("Activate", "s", NULL, bus_home_method_activate, SD_BUS_VTABLE_SENSITIVE), + SD_BUS_METHOD("Deactivate", NULL, NULL, bus_home_method_deactivate, 0), + SD_BUS_METHOD("Unregister", NULL, NULL, bus_home_method_unregister, SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD("Realize", "s", NULL, bus_home_method_realize, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE), + SD_BUS_METHOD("Remove", NULL, NULL, bus_home_method_remove, SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD("Fixate", "s", NULL, bus_home_method_fixate, SD_BUS_VTABLE_SENSITIVE), + SD_BUS_METHOD("Authenticate", "s", NULL, bus_home_method_authenticate, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE), + SD_BUS_METHOD("Update", "s", NULL, bus_home_method_update, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE), + SD_BUS_METHOD("Resize", "ts", NULL, bus_home_method_resize, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE), + SD_BUS_METHOD("ChangePassword", "ss", NULL, bus_home_method_change_password, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE), + SD_BUS_METHOD("Lock", NULL, NULL, bus_home_method_lock, 0), + SD_BUS_METHOD("Unlock", "s", NULL, bus_home_method_unlock, SD_BUS_VTABLE_SENSITIVE), + SD_BUS_METHOD("Acquire", "sb", "h", bus_home_method_acquire, SD_BUS_VTABLE_SENSITIVE), + SD_BUS_METHOD("Ref", "b", "h", bus_home_method_ref, 0), + SD_BUS_METHOD("Release", NULL, NULL, bus_home_method_release, 0), + SD_BUS_VTABLE_END +}; + +int bus_home_path(Home *h, char **ret) { + assert(ret); + + return sd_bus_path_encode("/org/freedesktop/home1/home", h->user_name, ret); +} + +int bus_home_object_find( + sd_bus *bus, + const char *path, + const char *interface, + void *userdata, + void **found, + sd_bus_error *error) { + + _cleanup_free_ char *e = NULL; + Manager *m = userdata; + uid_t uid; + Home *h; + int r; + + r = sd_bus_path_decode(path, "/org/freedesktop/home1/home", &e); + if (r <= 0) + return 0; + + if (parse_uid(e, &uid) >= 0) + h = hashmap_get(m->homes_by_uid, UID_TO_PTR(uid)); + else + h = hashmap_get(m->homes_by_name, e); + if (!h) + return 0; + + *found = h; + return 1; +} + +int bus_home_node_enumerator( + sd_bus *bus, + const char *path, + void *userdata, + char ***nodes, + sd_bus_error *error) { + + _cleanup_strv_free_ char **l = NULL; + Manager *m = userdata; + size_t k = 0; + Iterator i; + Home *h; + int r; + + assert(nodes); + + l = new0(char*, hashmap_size(m->homes_by_uid) + 1); + if (!l) + return -ENOMEM; + + HASHMAP_FOREACH(h, m->homes_by_uid, i) { + r = bus_home_path(h, l + k); + if (r < 0) + return r; + } + + *nodes = TAKE_PTR(l); + return 1; +} + +static int on_deferred_change(sd_event_source *s, void *userdata) { + _cleanup_free_ char *path = NULL; + Home *h = userdata; + int r; + + assert(h); + + h->deferred_change_event_source = sd_event_source_unref(h->deferred_change_event_source); + + r = bus_home_path(h, &path); + if (r < 0) { + log_warning_errno(r, "Failed to generate home bus path, ignoring: %m"); + return 0; + } + + if (h->announced) + r = sd_bus_emit_properties_changed_strv(h->manager->bus, path, "org.freedesktop.home1.Home", NULL); + else + r = sd_bus_emit_object_added(h->manager->bus, path); + if (r < 0) + log_warning_errno(r, "Failed to send home change event, ignoring: %m"); + else + h->announced = true; + + return 0; +} + +int bus_home_emit_change(Home *h) { + int r; + + assert(h); + + if (h->deferred_change_event_source) + return 1; + + if (!h->manager->event) + return 0; + + if (IN_SET(sd_event_get_state(h->manager->event), SD_EVENT_FINISHED, SD_EVENT_EXITING)) + return 0; + + r = sd_event_add_defer(h->manager->event, &h->deferred_change_event_source, on_deferred_change, h); + if (r < 0) + return log_error_errno(r, "Failed to allocate deferred change event source: %m"); + + r = sd_event_source_set_priority(h->deferred_change_event_source, SD_EVENT_PRIORITY_IDLE+5); + if (r < 0) + log_warning_errno(r, "Failed to tweak priority of event source, ignoring: %m"); + + (void) sd_event_source_set_description(h->deferred_change_event_source, "deferred-change-event"); + return 1; +} + +int bus_home_emit_remove(Home *h) { + _cleanup_free_ char *path = NULL; + int r; + + assert(h); + + if (!h->announced) + return 0; + + r = bus_home_path(h, &path); + if (r < 0) + return r; + + r = sd_bus_emit_object_removed(h->manager->bus, path); + if (r < 0) + return r; + + h->announced = false; + return 1; +} diff --git a/src/home/homed-home-bus.h b/src/home/homed-home-bus.h new file mode 100644 index 0000000000..20516b1205 --- /dev/null +++ b/src/home/homed-home-bus.h @@ -0,0 +1,36 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include "sd-bus.h" + +#include "homed-home.h" + +int bus_home_client_is_trusted(Home *h, sd_bus_message *message); +int bus_home_get_record_json(Home *h, sd_bus_message *message, char **ret, bool *ret_incomplete); + +int bus_home_method_activate(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_home_method_deactivate(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_home_method_unregister(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_home_method_realize(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_home_method_remove(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_home_method_fixate(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_home_method_authenticate(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_home_method_update(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_home_method_update_record(Home *home, sd_bus_message *message, UserRecord *hr, sd_bus_error *error); +int bus_home_method_resize(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_home_method_change_password(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_home_method_lock(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_home_method_unlock(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_home_method_acquire(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_home_method_ref(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_home_method_release(sd_bus_message *message, void *userdata, sd_bus_error *error); + +extern const sd_bus_vtable home_vtable[]; + +int bus_home_path(Home *h, char **ret); + +int bus_home_object_find(sd_bus *bus, const char *path, const char *interface, void *userdata, void **found, sd_bus_error *error); +int bus_home_node_enumerator(sd_bus *bus, const char *path, void *userdata, char ***nodes, sd_bus_error *error); + +int bus_home_emit_change(Home *h); +int bus_home_emit_remove(Home *h); diff --git a/src/home/homed-home.c b/src/home/homed-home.c new file mode 100644 index 0000000000..f50de26722 --- /dev/null +++ b/src/home/homed-home.c @@ -0,0 +1,2712 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#if HAVE_LINUX_MEMFD_H +#include +#endif + +#include +#include +#include + +#include "blockdev-util.h" +#include "btrfs-util.h" +#include "bus-common-errors.h" +#include "env-util.h" +#include "errno-list.h" +#include "errno-util.h" +#include "fd-util.h" +#include "fileio.h" +#include "home-util.h" +#include "homed-home-bus.h" +#include "homed-home.h" +#include "missing_syscall.h" +#include "mkdir.h" +#include "path-util.h" +#include "process-util.h" +#include "pwquality-util.h" +#include "quota-util.h" +#include "resize-fs.h" +#include "set.h" +#include "signal-util.h" +#include "stat-util.h" +#include "string-table.h" +#include "strv.h" +#include "user-record-sign.h" +#include "user-record-util.h" +#include "user-record.h" +#include "user-util.h" + +#define HOME_USERS_MAX 500 +#define PENDING_OPERATIONS_MAX 100 + +assert_cc(HOME_UID_MIN <= HOME_UID_MAX); +assert_cc(HOME_USERS_MAX <= (HOME_UID_MAX - HOME_UID_MIN + 1)); + +static int home_start_work(Home *h, const char *verb, UserRecord *hr, UserRecord *secret); + +DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(operation_hash_ops, void, trivial_hash_func, trivial_compare_func, Operation, operation_unref); + +static int suitable_home_record(UserRecord *hr) { + int r; + + assert(hr); + + if (!hr->user_name) + return -EUNATCH; + + /* We are a bit more restrictive with what we accept as homed-managed user than what we accept in + * home records in general. Let's enforce the stricter rule here. */ + if (!suitable_user_name(hr->user_name)) + return -EINVAL; + if (!uid_is_valid(hr->uid)) + return -EINVAL; + + /* Insist we are outside of the dynamic and system range */ + if (uid_is_system(hr->uid) || gid_is_system(user_record_gid(hr)) || + uid_is_dynamic(hr->uid) || gid_is_dynamic(user_record_gid(hr))) + return -EADDRNOTAVAIL; + + /* Insist that GID and UID match */ + if (user_record_gid(hr) != (gid_t) hr->uid) + return -EBADSLT; + + /* Similar for the realm */ + if (hr->realm) { + r = suitable_realm(hr->realm); + if (r < 0) + return r; + if (r == 0) + return -EINVAL; + } + + return 0; +} + +int home_new(Manager *m, UserRecord *hr, const char *sysfs, Home **ret) { + _cleanup_(home_freep) Home *home = NULL; + _cleanup_free_ char *nm = NULL, *ns = NULL; + int r; + + assert(m); + assert(hr); + + r = suitable_home_record(hr); + if (r < 0) + return r; + + if (hashmap_contains(m->homes_by_name, hr->user_name)) + return -EBUSY; + + if (hashmap_contains(m->homes_by_uid, UID_TO_PTR(hr->uid))) + return -EBUSY; + + if (sysfs && hashmap_contains(m->homes_by_sysfs, sysfs)) + return -EBUSY; + + if (hashmap_size(m->homes_by_name) >= HOME_USERS_MAX) + return -EUSERS; + + nm = strdup(hr->user_name); + if (!nm) + return -ENOMEM; + + if (sysfs) { + ns = strdup(sysfs); + if (!ns) + return -ENOMEM; + } + + home = new(Home, 1); + if (!home) + return -ENOMEM; + + *home = (Home) { + .manager = m, + .user_name = TAKE_PTR(nm), + .uid = hr->uid, + .state = _HOME_STATE_INVALID, + .worker_stdout_fd = -1, + .sysfs = TAKE_PTR(ns), + .signed_locally = -1, + }; + + r = hashmap_put(m->homes_by_name, home->user_name, home); + if (r < 0) + return r; + + r = hashmap_put(m->homes_by_uid, UID_TO_PTR(home->uid), home); + if (r < 0) + return r; + + if (home->sysfs) { + r = hashmap_put(m->homes_by_sysfs, home->sysfs, home); + if (r < 0) + return r; + } + + r = user_record_clone(hr, USER_RECORD_LOAD_MASK_SECRET, &home->record); + if (r < 0) + return r; + + (void) bus_manager_emit_auto_login_changed(m); + (void) bus_home_emit_change(home); + + if (ret) + *ret = TAKE_PTR(home); + else + TAKE_PTR(home); + + return 0; +} + +Home *home_free(Home *h) { + + if (!h) + return NULL; + + if (h->manager) { + (void) bus_home_emit_remove(h); + (void) bus_manager_emit_auto_login_changed(h->manager); + + if (h->user_name) + (void) hashmap_remove_value(h->manager->homes_by_name, h->user_name, h); + + if (uid_is_valid(h->uid)) + (void) hashmap_remove_value(h->manager->homes_by_uid, UID_TO_PTR(h->uid), h); + + if (h->sysfs) + (void) hashmap_remove_value(h->manager->homes_by_sysfs, h->sysfs, h); + + if (h->worker_pid > 0) + (void) hashmap_remove_value(h->manager->homes_by_worker_pid, PID_TO_PTR(h->worker_pid), h); + + if (h->manager->gc_focus == h) + h->manager->gc_focus = NULL; + } + + user_record_unref(h->record); + user_record_unref(h->secret); + + h->worker_event_source = sd_event_source_unref(h->worker_event_source); + safe_close(h->worker_stdout_fd); + free(h->user_name); + free(h->sysfs); + + h->ref_event_source_please_suspend = sd_event_source_unref(h->ref_event_source_please_suspend); + h->ref_event_source_dont_suspend = sd_event_source_unref(h->ref_event_source_dont_suspend); + + h->pending_operations = ordered_set_free(h->pending_operations); + h->pending_event_source = sd_event_source_unref(h->pending_event_source); + h->deferred_change_event_source = sd_event_source_unref(h->deferred_change_event_source); + + h->current_operation = operation_unref(h->current_operation); + + return mfree(h); +} + +int home_set_record(Home *h, UserRecord *hr) { + _cleanup_(user_record_unrefp) UserRecord *new_hr = NULL; + Home *other; + int r; + + assert(h); + assert(h->user_name); + assert(h->record); + assert(hr); + + if (user_record_equal(h->record, hr)) + return 0; + + r = suitable_home_record(hr); + if (r < 0) + return r; + + if (!user_record_compatible(h->record, hr)) + return -EREMCHG; + + if (!FLAGS_SET(hr->mask, USER_RECORD_REGULAR) || + FLAGS_SET(hr->mask, USER_RECORD_SECRET)) + return -EINVAL; + + if (FLAGS_SET(h->record->mask, USER_RECORD_STATUS)) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + + /* Hmm, the existing record has status fields? If so, copy them over */ + + v = json_variant_ref(hr->json); + r = json_variant_set_field(&v, "status", json_variant_by_key(h->record->json, "status")); + if (r < 0) + return r; + + new_hr = user_record_new(); + if (!new_hr) + return -ENOMEM; + + r = user_record_load(new_hr, v, USER_RECORD_LOAD_REFUSE_SECRET); + if (r < 0) + return r; + + hr = new_hr; + } + + other = hashmap_get(h->manager->homes_by_uid, UID_TO_PTR(hr->uid)); + if (other && other != h) + return -EBUSY; + + if (h->uid != hr->uid) { + r = hashmap_remove_and_replace(h->manager->homes_by_uid, UID_TO_PTR(h->uid), UID_TO_PTR(hr->uid), h); + if (r < 0) + return r; + } + + user_record_unref(h->record); + h->record = user_record_ref(hr); + h->uid = h->record->uid; + + /* The updated record might have a different autologin setting, trigger a PropertiesChanged event for it */ + (void) bus_manager_emit_auto_login_changed(h->manager); + (void) bus_home_emit_change(h); + + return 0; +} + +int home_save_record(Home *h) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + _cleanup_free_ char *text = NULL; + const char *fn; + int r; + + assert(h); + + v = json_variant_ref(h->record->json); + r = json_variant_normalize(&v); + if (r < 0) + log_warning_errno(r, "User record could not be normalized."); + + r = json_variant_format(v, JSON_FORMAT_PRETTY|JSON_FORMAT_NEWLINE, &text); + if (r < 0) + return r; + + (void) mkdir("/var/lib/systemd/", 0755); + (void) mkdir("/var/lib/systemd/home/", 0700); + + fn = strjoina("/var/lib/systemd/home/", h->user_name, ".identity"); + + r = write_string_file(fn, text, WRITE_STRING_FILE_ATOMIC|WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_MODE_0600); + if (r < 0) + return r; + + return 0; +} + +int home_unlink_record(Home *h) { + const char *fn; + + assert(h); + + fn = strjoina("/var/lib/systemd/home/", h->user_name, ".identity"); + if (unlink(fn) < 0 && errno != ENOENT) + return -errno; + + fn = strjoina("/run/systemd/home/", h->user_name, ".ref"); + if (unlink(fn) < 0 && errno != ENOENT) + return -errno; + + return 0; +} + +static void home_set_state(Home *h, HomeState state) { + HomeState old_state, new_state; + + assert(h); + + old_state = home_get_state(h); + h->state = state; + new_state = home_get_state(h); /* Query the new state, since the 'state' variable might be set to -1, + * in which case we synthesize an high-level state on demand */ + + log_info("%s: changing state %s → %s", h->user_name, + home_state_to_string(old_state), + home_state_to_string(new_state)); + + if (HOME_STATE_IS_EXECUTING_OPERATION(old_state) && !HOME_STATE_IS_EXECUTING_OPERATION(new_state)) { + /* If we just finished executing some operation, process the queue of pending operations. And + * enqueue it for GC too. */ + + home_schedule_operation(h, NULL, NULL); + manager_enqueue_gc(h->manager, h); + } +} + +static int home_parse_worker_stdout(int _fd, UserRecord **ret) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + _cleanup_close_ int fd = _fd; /* take possession, even on failure */ + _cleanup_(user_record_unrefp) UserRecord *hr = NULL; + _cleanup_fclose_ FILE *f = NULL; + unsigned line, column; + struct stat st; + int r; + + if (fstat(fd, &st) < 0) + return log_error_errno(errno, "Failed to stat stdout fd: %m"); + + assert(S_ISREG(st.st_mode)); + + if (st.st_size == 0) { /* empty record */ + *ret = NULL; + return 0; + } + + if (lseek(fd, SEEK_SET, 0) == (off_t) -1) + return log_error_errno(errno, "Failed to seek to beginning of memfd: %m"); + + f = fdopen(fd, "r"); + if (!f) + return log_error_errno(errno, "Failed to reopen memfd: %m"); + + TAKE_FD(fd); + + if (DEBUG_LOGGING) { + _cleanup_free_ char *text = NULL; + + r = read_full_stream(f, &text, NULL); + if (r < 0) + return log_error_errno(r, "Failed to read from client: %m"); + + log_debug("Got from worker: %s", text); + rewind(f); + } + + r = json_parse_file(f, "stdout", JSON_PARSE_SENSITIVE, &v, &line, &column); + if (r < 0) + return log_error_errno(r, "Failed to parse identity at %u:%u: %m", line, column); + + hr = user_record_new(); + if (!hr) + return log_oom(); + + r = user_record_load(hr, v, USER_RECORD_LOAD_REFUSE_SECRET); + if (r < 0) + return log_error_errno(r, "Failed to load home record identity: %m"); + + *ret = TAKE_PTR(hr); + return 1; +} + +static int home_verify_user_record(Home *h, UserRecord *hr, bool *ret_signed_locally, sd_bus_error *ret_error) { + int is_signed; + + assert(h); + assert(hr); + assert(ret_signed_locally); + + is_signed = manager_verify_user_record(h->manager, hr); + switch (is_signed) { + + case USER_RECORD_SIGNED_EXCLUSIVE: + log_info("Home %s is signed exclusively by our key, accepting.", hr->user_name); + *ret_signed_locally = true; + return 0; + + case USER_RECORD_SIGNED: + log_info("Home %s is signed by our key (and others), accepting.", hr->user_name); + *ret_signed_locally = false; + return 0; + + case USER_RECORD_FOREIGN: + log_info("Home %s is signed by foreign key we like, accepting.", hr->user_name); + *ret_signed_locally = false; + return 0; + + case USER_RECORD_UNSIGNED: + sd_bus_error_setf(ret_error, BUS_ERROR_BAD_SIGNATURE, "User record %s is not signed at all, refusing.", hr->user_name); + return log_error_errno(SYNTHETIC_ERRNO(EPERM), "Home %s contains user record that is not signed at all, refusing.", hr->user_name); + + case -ENOKEY: + sd_bus_error_setf(ret_error, BUS_ERROR_BAD_SIGNATURE, "User record %s is not signed by any known key, refusing.", hr->user_name); + return log_error_errno(is_signed, "Home %s contians user record that is not signed by any known key, refusing.", hr->user_name); + + default: + assert(is_signed < 0); + return log_error_errno(is_signed, "Failed to verify signature on user record for %s, refusing fixation: %m", hr->user_name); + } +} + +static int convert_worker_errno(Home *h, int e, sd_bus_error *error) { + /* Converts the error numbers the worker process returned into somewhat sensible dbus errors */ + + switch (e) { + + case -EMSGSIZE: + return sd_bus_error_setf(error, BUS_ERROR_BAD_HOME_SIZE, "File systems of this type cannot shrinked"); + case -ETXTBSY: + return sd_bus_error_setf(error, BUS_ERROR_BAD_HOME_SIZE, "File systems of this type can only be shrinked offline"); + case -ERANGE: + return sd_bus_error_setf(error, BUS_ERROR_BAD_HOME_SIZE, "File system size too small"); + case -ENOLINK: + return sd_bus_error_setf(error, SD_BUS_ERROR_NOT_SUPPORTED, "System does not support selected storage backend"); + case -EPROTONOSUPPORT: + return sd_bus_error_setf(error, SD_BUS_ERROR_NOT_SUPPORTED, "System does not support selected file system"); + case -ENOTTY: + return sd_bus_error_setf(error, SD_BUS_ERROR_NOT_SUPPORTED, "Operation not supported on storage backend"); + case -ESOCKTNOSUPPORT: + return sd_bus_error_setf(error, SD_BUS_ERROR_NOT_SUPPORTED, "Operation not supported on file system"); + case -ENOKEY: + return sd_bus_error_setf(error, BUS_ERROR_BAD_PASSWORD, "Password for home %s is incorrect or not sufficient for authentication.", h->user_name); + case -EBADSLT: + return sd_bus_error_setf(error, BUS_ERROR_BAD_PASSWORD_AND_NO_TOKEN, "Password for home %s is incorrect or not sufficient, and configured security token not found either.", h->user_name); + case -ENOANO: + return sd_bus_error_setf(error, BUS_ERROR_TOKEN_PIN_NEEDED, "PIN for security token required."); + case -ERFKILL: + return sd_bus_error_setf(error, BUS_ERROR_TOKEN_PROTECTED_AUTHENTICATION_PATH_NEEDED, "Security token requires protected authentication path."); + case -EOWNERDEAD: + return sd_bus_error_setf(error, BUS_ERROR_TOKEN_PIN_LOCKED, "PIN of security token locked."); + case -ENOLCK: + return sd_bus_error_setf(error, BUS_ERROR_TOKEN_BAD_PIN, "Bad PIN of security token."); + case -ETOOMANYREFS: + return sd_bus_error_setf(error, BUS_ERROR_TOKEN_BAD_PIN_FEW_TRIES_LEFT, "Bad PIN of security token, and only a few tries left."); + case -EUCLEAN: + return sd_bus_error_setf(error, BUS_ERROR_TOKEN_BAD_PIN_ONE_TRY_LEFT, "Bad PIN of security token, and only one try left."); + case -EBUSY: + return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "Home %s is currently being used, or an operation on home %s is currently being executed.", h->user_name, h->user_name); + case -ENOEXEC: + return sd_bus_error_setf(error, BUS_ERROR_HOME_NOT_ACTIVE, "Home %s is currently not active", h->user_name); + case -ENOSPC: + return sd_bus_error_setf(error, BUS_ERROR_NO_DISK_SPACE, "Not enough disk space for home %s", h->user_name); + } + + return 0; +} + +static void home_count_bad_authentication(Home *h, bool save) { + int r; + + assert(h); + + r = user_record_bad_authentication(h->record); + if (r < 0) { + log_warning_errno(r, "Failed to increase bad authentication counter, ignoring: %m"); + return; + } + + if (save) { + r = home_save_record(h); + if (r < 0) + log_warning_errno(r, "Failed to write home record to disk, ignoring: %m"); + } +} + +static void home_fixate_finish(Home *h, int ret, UserRecord *hr) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(user_record_unrefp) UserRecord *secret = NULL; + bool signed_locally; + int r; + + assert(h); + assert(IN_SET(h->state, HOME_FIXATING, HOME_FIXATING_FOR_ACTIVATION, HOME_FIXATING_FOR_ACQUIRE)); + + secret = TAKE_PTR(h->secret); /* Take possession */ + + if (ret < 0) { + if (ret == -ENOKEY) + (void) home_count_bad_authentication(h, false); + + (void) convert_worker_errno(h, ret, &error); + r = log_error_errno(ret, "Fixation failed: %m"); + goto fail; + } + if (!hr) { + r = log_error_errno(SYNTHETIC_ERRNO(EIO), "Did not receive user record from worker process, fixation failed."); + goto fail; + } + + r = home_verify_user_record(h, hr, &signed_locally, &error); + if (r < 0) + goto fail; + + r = home_set_record(h, hr); + if (r < 0) { + log_error_errno(r, "Failed to update home record: %m"); + goto fail; + } + + h->signed_locally = signed_locally; + + /* When we finished fixating (and don't follow-up with activation), let's count this as good authentication */ + if (h->state == HOME_FIXATING) { + r = user_record_good_authentication(h->record); + if (r < 0) + log_warning_errno(r, "Failed to increase good authentication counter, ignoring: %m"); + } + + r = home_save_record(h); + if (r < 0) + log_warning_errno(r, "Failed to write home record to disk, ignoring: %m"); + + if (IN_SET(h->state, HOME_FIXATING_FOR_ACTIVATION, HOME_FIXATING_FOR_ACQUIRE)) { + + r = home_start_work(h, "activate", h->record, secret); + if (r < 0) { + h->current_operation = operation_result_unref(h->current_operation, r, NULL); + home_set_state(h, _HOME_STATE_INVALID); + } else + home_set_state(h, h->state == HOME_FIXATING_FOR_ACTIVATION ? HOME_ACTIVATING : HOME_ACTIVATING_FOR_ACQUIRE); + + return; + } + + log_debug("Fixation of %s completed.", h->user_name); + + h->current_operation = operation_result_unref(h->current_operation, 0, NULL); + + /* Reset the state to "invalid", which makes home_get_state() test if the image exists and returns + * HOME_ABSENT vs. HOME_INACTIVE as necessary. */ + home_set_state(h, _HOME_STATE_INVALID); + return; + +fail: + /* If fixation fails, we stay in unfixated state! */ + h->current_operation = operation_result_unref(h->current_operation, r, &error); + home_set_state(h, HOME_UNFIXATED); +} + +static void home_activate_finish(Home *h, int ret, UserRecord *hr) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + int r; + + assert(h); + assert(IN_SET(h->state, HOME_ACTIVATING, HOME_ACTIVATING_FOR_ACQUIRE)); + + if (ret < 0) { + if (ret == -ENOKEY) + home_count_bad_authentication(h, true); + + (void) convert_worker_errno(h, ret, &error); + r = log_error_errno(ret, "Activation failed: %m"); + goto finish; + } + + if (hr) { + bool signed_locally; + + r = home_verify_user_record(h, hr, &signed_locally, &error); + if (r < 0) + goto finish; + + r = home_set_record(h, hr); + if (r < 0) { + log_error_errno(r, "Failed to update home record, ignoring: %m"); + goto finish; + } + + h->signed_locally = signed_locally; + + r = user_record_good_authentication(h->record); + if (r < 0) + log_warning_errno(r, "Failed to increase good authentication counter, ignoring: %m"); + + r = home_save_record(h); + if (r < 0) + log_warning_errno(r, "Failed to write home record to disk, ignoring: %m"); + } + + log_debug("Activation of %s completed.", h->user_name); + r = 0; + +finish: + h->current_operation = operation_result_unref(h->current_operation, r, &error); + home_set_state(h, _HOME_STATE_INVALID); +} + +static void home_deactivate_finish(Home *h, int ret, UserRecord *hr) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + int r; + + assert(h); + assert(h->state == HOME_DEACTIVATING); + assert(!hr); /* We don't expect a record on this operation */ + + if (ret < 0) { + (void) convert_worker_errno(h, ret, &error); + r = log_error_errno(ret, "Deactivation of %s failed: %m", h->user_name); + goto finish; + } + + log_debug("Deactivation of %s completed.", h->user_name); + r = 0; + +finish: + h->current_operation = operation_result_unref(h->current_operation, r, &error); + home_set_state(h, _HOME_STATE_INVALID); +} + +static void home_remove_finish(Home *h, int ret, UserRecord *hr) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + Manager *m; + int r; + + assert(h); + assert(h->state == HOME_REMOVING); + assert(!hr); /* We don't expect a record on this operation */ + + m = h->manager; + + if (ret < 0 && ret != -EALREADY) { + (void) convert_worker_errno(h, ret, &error); + r = log_error_errno(ret, "Removing %s failed: %m", h->user_name); + goto fail; + } + + /* For a couple of storage types we can't delete the actual data storage when called (such as LUKS on + * partitions like USB sticks, or so). Sometimes these storage locations are among those we normally + * automatically discover in /home or in udev. When such a home is deleted let's hence issue a rescan + * after completion, so that "unfixated" entries are rediscovered. */ + if (!IN_SET(user_record_test_image_path(h->record), USER_TEST_UNDEFINED, USER_TEST_ABSENT)) + manager_enqueue_rescan(m); + + /* The image is now removed from disk. Now also remove our stored record */ + r = home_unlink_record(h); + if (r < 0) { + log_error_errno(r, "Removing record file failed: %m"); + goto fail; + } + + log_debug("Removal of %s completed.", h->user_name); + h->current_operation = operation_result_unref(h->current_operation, 0, NULL); + + /* Unload this record from memory too now. */ + h = home_free(h); + return; + +fail: + h->current_operation = operation_result_unref(h->current_operation, r, &error); + home_set_state(h, _HOME_STATE_INVALID); +} + +static void home_create_finish(Home *h, int ret, UserRecord *hr) { + int r; + + assert(h); + assert(h->state == HOME_CREATING); + + if (ret < 0) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + + (void) convert_worker_errno(h, ret, &error); + log_error_errno(ret, "Operation on %s failed: %m", h->user_name); + h->current_operation = operation_result_unref(h->current_operation, ret, &error); + + if (h->unregister_on_failure) { + (void) home_unlink_record(h); + h = home_free(h); + return; + } + + home_set_state(h, _HOME_STATE_INVALID); + return; + } + + if (hr) { + r = home_set_record(h, hr); + if (r < 0) + log_warning_errno(r, "Failed to update home record, ignoring: %m"); + } + + r = home_save_record(h); + if (r < 0) + log_warning_errno(r, "Failed to save record to disk, ignoring: %m"); + + log_debug("Creation of %s completed.", h->user_name); + + h->current_operation = operation_result_unref(h->current_operation, 0, NULL); + home_set_state(h, _HOME_STATE_INVALID); +} + +static void home_change_finish(Home *h, int ret, UserRecord *hr) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + int r; + + assert(h); + + if (ret < 0) { + if (ret == -ENOKEY) + (void) home_count_bad_authentication(h, true); + + (void) convert_worker_errno(h, ret, &error); + r = log_error_errno(ret, "Change operation failed: %m"); + goto finish; + } + + if (hr) { + r = home_set_record(h, hr); + if (r < 0) + log_warning_errno(r, "Failed to update home record, ignoring: %m"); + else { + r = user_record_good_authentication(h->record); + if (r < 0) + log_warning_errno(r, "Failed to increase good authentication counter, ignoring: %m"); + + r = home_save_record(h); + if (r < 0) + log_warning_errno(r, "Failed to write home record to disk, ignoring: %m"); + } + } + + log_debug("Change operation of %s completed.", h->user_name); + r = 0; + +finish: + h->current_operation = operation_result_unref(h->current_operation, r, &error); + home_set_state(h, _HOME_STATE_INVALID); +} + +static void home_locking_finish(Home *h, int ret, UserRecord *hr) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + int r; + + assert(h); + assert(h->state == HOME_LOCKING); + + if (ret < 0) { + (void) convert_worker_errno(h, ret, &error); + r = log_error_errno(ret, "Locking operation failed: %m"); + goto finish; + } + + log_debug("Locking operation of %s completed.", h->user_name); + h->current_operation = operation_result_unref(h->current_operation, 0, NULL); + home_set_state(h, HOME_LOCKED); + return; + +finish: + /* If a specific home doesn't know the concept of locking, then that's totally OK, don't propagate + * the error if we are executing a LockAllHomes() operation. */ + + if (h->current_operation->type == OPERATION_LOCK_ALL && r == -ENOTTY) + h->current_operation = operation_result_unref(h->current_operation, 0, NULL); + else + h->current_operation = operation_result_unref(h->current_operation, r, &error); + + home_set_state(h, _HOME_STATE_INVALID); +} + +static void home_unlocking_finish(Home *h, int ret, UserRecord *hr) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + int r; + + assert(h); + assert(IN_SET(h->state, HOME_UNLOCKING, HOME_UNLOCKING_FOR_ACQUIRE)); + + if (ret < 0) { + if (ret == -ENOKEY) + (void) home_count_bad_authentication(h, true); + + (void) convert_worker_errno(h, ret, &error); + r = log_error_errno(ret, "Unlocking operation failed: %m"); + + /* Revert to locked state */ + home_set_state(h, HOME_LOCKED); + h->current_operation = operation_result_unref(h->current_operation, r, &error); + return; + } + + r = user_record_good_authentication(h->record); + if (r < 0) + log_warning_errno(r, "Failed to increase good authentication counter, ignoring: %m"); + else { + r = home_save_record(h); + if (r < 0) + log_warning_errno(r, "Failed to write home record to disk, ignoring: %m"); + } + + log_debug("Unlocking operation of %s completed.", h->user_name); + + h->current_operation = operation_result_unref(h->current_operation, r, &error); + home_set_state(h, _HOME_STATE_INVALID); + return; +} + +static void home_authenticating_finish(Home *h, int ret, UserRecord *hr) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + int r; + + assert(h); + assert(IN_SET(h->state, HOME_AUTHENTICATING, HOME_AUTHENTICATING_WHILE_ACTIVE, HOME_AUTHENTICATING_FOR_ACQUIRE)); + + if (ret < 0) { + if (ret == -ENOKEY) + (void) home_count_bad_authentication(h, true); + + (void) convert_worker_errno(h, ret, &error); + r = log_error_errno(ret, "Authentication failed: %m"); + goto finish; + } + + if (hr) { + r = home_set_record(h, hr); + if (r < 0) + log_warning_errno(r, "Failed to update home record, ignoring: %m"); + else { + r = user_record_good_authentication(h->record); + if (r < 0) + log_warning_errno(r, "Failed to increase good authentication counter, ignoring: %m"); + + r = home_save_record(h); + if (r < 0) + log_warning_errno(r, "Failed to write home record to disk, ignoring: %m"); + } + } + + log_debug("Authentication of %s completed.", h->user_name); + r = 0; + +finish: + h->current_operation = operation_result_unref(h->current_operation, r, &error); + home_set_state(h, _HOME_STATE_INVALID); +} + +static int home_on_worker_process(sd_event_source *s, const siginfo_t *si, void *userdata) { + _cleanup_(user_record_unrefp) UserRecord *hr = NULL; + Home *h = userdata; + int ret; + + assert(s); + assert(si); + assert(h); + + assert(h->worker_pid == si->si_pid); + assert(h->worker_event_source); + assert(h->worker_stdout_fd >= 0); + + (void) hashmap_remove_value(h->manager->homes_by_worker_pid, PID_TO_PTR(h->worker_pid), h); + + h->worker_pid = 0; + h->worker_event_source = sd_event_source_unref(h->worker_event_source); + + if (si->si_code != CLD_EXITED) { + assert(IN_SET(si->si_code, CLD_KILLED, CLD_DUMPED)); + ret = log_debug_errno(SYNTHETIC_ERRNO(EPROTO), "Worker process died abnormally with signal %s.", signal_to_string(si->si_status)); + } else if (si->si_status != EXIT_SUCCESS) { + /* If we received an error code via sd_notify(), use it */ + if (h->worker_error_code != 0) + ret = log_debug_errno(h->worker_error_code, "Worker reported error code %s.", errno_to_name(h->worker_error_code)); + else + ret = log_debug_errno(SYNTHETIC_ERRNO(EPROTO), "Worker exited with exit code %i.", si->si_status); + } else + ret = home_parse_worker_stdout(TAKE_FD(h->worker_stdout_fd), &hr); + + h->worker_stdout_fd = safe_close(h->worker_stdout_fd); + + switch (h->state) { + + case HOME_FIXATING: + case HOME_FIXATING_FOR_ACTIVATION: + case HOME_FIXATING_FOR_ACQUIRE: + home_fixate_finish(h, ret, hr); + break; + + case HOME_ACTIVATING: + case HOME_ACTIVATING_FOR_ACQUIRE: + home_activate_finish(h, ret, hr); + break; + + case HOME_DEACTIVATING: + home_deactivate_finish(h, ret, hr); + break; + + case HOME_LOCKING: + home_locking_finish(h, ret, hr); + break; + + case HOME_UNLOCKING: + case HOME_UNLOCKING_FOR_ACQUIRE: + home_unlocking_finish(h, ret, hr); + break; + + case HOME_CREATING: + home_create_finish(h, ret, hr); + break; + + case HOME_REMOVING: + home_remove_finish(h, ret, hr); + break; + + case HOME_UPDATING: + case HOME_UPDATING_WHILE_ACTIVE: + case HOME_RESIZING: + case HOME_RESIZING_WHILE_ACTIVE: + case HOME_PASSWD: + case HOME_PASSWD_WHILE_ACTIVE: + home_change_finish(h, ret, hr); + break; + + case HOME_AUTHENTICATING: + case HOME_AUTHENTICATING_WHILE_ACTIVE: + case HOME_AUTHENTICATING_FOR_ACQUIRE: + home_authenticating_finish(h, ret, hr); + break; + + default: + assert_not_reached("Unexpected state after worker exited"); + } + + return 0; +} + +static int home_start_work(Home *h, const char *verb, UserRecord *hr, UserRecord *secret) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + _cleanup_(erase_and_freep) char *formatted = NULL; + _cleanup_close_ int stdin_fd = -1, stdout_fd = -1; + pid_t pid = 0; + int r; + + assert(h); + assert(verb); + assert(hr); + + if (h->worker_pid != 0) + return -EBUSY; + + assert(h->worker_stdout_fd < 0); + assert(!h->worker_event_source); + + v = json_variant_ref(hr->json); + + if (secret) { + JsonVariant *sub = NULL; + + sub = json_variant_by_key(secret->json, "secret"); + if (!sub) + return -ENOKEY; + + r = json_variant_set_field(&v, "secret", sub); + if (r < 0) + return r; + } + + r = json_variant_format(v, 0, &formatted); + if (r < 0) + return r; + + stdin_fd = acquire_data_fd(formatted, strlen(formatted), 0); + if (stdin_fd < 0) + return stdin_fd; + + log_debug("Sending to worker: %s", formatted); + + stdout_fd = memfd_create("homework-stdout", MFD_CLOEXEC); + if (stdout_fd < 0) + return -errno; + + r = safe_fork_full("(sd-homework)", + (int[]) { stdin_fd, stdout_fd }, 2, + FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG|FORK_LOG, &pid); + if (r < 0) + return r; + if (r == 0) { + /* Child */ + + if (setenv("NOTIFY_SOCKET", "/run/systemd/home/notify", 1) < 0) { + log_error_errno(errno, "Failed to set $NOTIFY_SOCKET: %m"); + _exit(EXIT_FAILURE); + } + + r = rearrange_stdio(stdin_fd, stdout_fd, STDERR_FILENO); + if (r < 0) { + log_error_errno(r, "Failed to rearrange stdin/stdout/stderr: %m"); + _exit(EXIT_FAILURE); + } + + stdin_fd = stdout_fd = -1; /* have been invalidated by rearrange_stdio() */ + + execl(SYSTEMD_HOMEWORK_PATH, SYSTEMD_HOMEWORK_PATH, verb, NULL); + log_error_errno(errno, "Failed to invoke " SYSTEMD_HOMEWORK_PATH ": %m"); + _exit(EXIT_FAILURE); + } + + r = sd_event_add_child(h->manager->event, &h->worker_event_source, pid, WEXITED, home_on_worker_process, h); + if (r < 0) + return r; + + (void) sd_event_source_set_description(h->worker_event_source, "worker"); + + r = hashmap_put(h->manager->homes_by_worker_pid, PID_TO_PTR(pid), h); + if (r < 0) { + h->worker_event_source = sd_event_source_unref(h->worker_event_source); + return r; + } + + h->worker_stdout_fd = TAKE_FD(stdout_fd); + h->worker_pid = pid; + h->worker_error_code = 0; + + return 0; +} + +static int home_ratelimit(Home *h, sd_bus_error *error) { + int r, ret; + + assert(h); + + ret = user_record_ratelimit(h->record); + if (ret < 0) + return ret; + + if (h->state != HOME_UNFIXATED) { + r = home_save_record(h); + if (r < 0) + log_warning_errno(r, "Failed to save updated record, ignoring: %m"); + } + + if (ret == 0) { + char buf[FORMAT_TIMESPAN_MAX]; + usec_t t, n; + + n = now(CLOCK_REALTIME); + t = user_record_ratelimit_next_try(h->record); + + if (t != USEC_INFINITY && t > n) + return sd_bus_error_setf(error, BUS_ERROR_AUTHENTICATION_LIMIT_HIT, "Too many login attempts, please try again in %s!", + format_timespan(buf, sizeof(buf), t - n, USEC_PER_SEC)); + + return sd_bus_error_setf(error, BUS_ERROR_AUTHENTICATION_LIMIT_HIT, "Too many login attempts, please try again later."); + } + + return 0; +} + +static int home_fixate_internal( + Home *h, + UserRecord *secret, + HomeState for_state, + sd_bus_error *error) { + + int r; + + assert(h); + assert(IN_SET(for_state, HOME_FIXATING, HOME_FIXATING_FOR_ACTIVATION, HOME_FIXATING_FOR_ACQUIRE)); + + r = home_start_work(h, "inspect", h->record, secret); + if (r < 0) + return r; + + if (for_state == HOME_FIXATING_FOR_ACTIVATION) { + /* Remember the secret data, since we need it for the activation again, later on. */ + user_record_unref(h->secret); + h->secret = user_record_ref(secret); + } + + home_set_state(h, for_state); + return 0; +} + +int home_fixate(Home *h, UserRecord *secret, sd_bus_error *error) { + int r; + + assert(h); + + switch (home_get_state(h)) { + case HOME_ABSENT: + return sd_bus_error_setf(error, BUS_ERROR_HOME_ABSENT, "Home %s is currently missing or not plugged in.", h->user_name); + case HOME_INACTIVE: + case HOME_ACTIVE: + case HOME_LOCKED: + return sd_bus_error_setf(error, BUS_ERROR_HOME_ALREADY_FIXATED, "Home %s is already fixated.", h->user_name); + case HOME_UNFIXATED: + break; + default: + return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "An operation on home %s is currently being executed.", h->user_name); + } + + r = home_ratelimit(h, error); + if (r < 0) + return r; + + return home_fixate_internal(h, secret, HOME_FIXATING, error); +} + +static int home_activate_internal(Home *h, UserRecord *secret, HomeState for_state, sd_bus_error *error) { + int r; + + assert(h); + assert(IN_SET(for_state, HOME_ACTIVATING, HOME_ACTIVATING_FOR_ACQUIRE)); + + r = home_start_work(h, "activate", h->record, secret); + if (r < 0) + return r; + + home_set_state(h, for_state); + return 0; +} + +int home_activate(Home *h, UserRecord *secret, sd_bus_error *error) { + int r; + + assert(h); + + switch (home_get_state(h)) { + case HOME_UNFIXATED: + return home_fixate_internal(h, secret, HOME_FIXATING_FOR_ACTIVATION, error); + case HOME_ABSENT: + return sd_bus_error_setf(error, BUS_ERROR_HOME_ABSENT, "Home %s is currently missing or not plugged in.", h->user_name); + case HOME_ACTIVE: + return sd_bus_error_setf(error, BUS_ERROR_HOME_ALREADY_ACTIVE, "Home %s is already active.", h->user_name); + case HOME_LOCKED: + return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name); + case HOME_INACTIVE: + break; + default: + return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "An operation on home %s is currently being executed.", h->user_name); + } + + r = home_ratelimit(h, error); + if (r < 0) + return r; + + return home_activate_internal(h, secret, HOME_ACTIVATING, error); +} + +static int home_authenticate_internal(Home *h, UserRecord *secret, HomeState for_state, sd_bus_error *error) { + int r; + + assert(h); + assert(IN_SET(for_state, HOME_AUTHENTICATING, HOME_AUTHENTICATING_WHILE_ACTIVE, HOME_AUTHENTICATING_FOR_ACQUIRE)); + + r = home_start_work(h, "inspect", h->record, secret); + if (r < 0) + return r; + + home_set_state(h, for_state); + return 0; +} + +int home_authenticate(Home *h, UserRecord *secret, sd_bus_error *error) { + HomeState state; + int r; + + assert(h); + + state = home_get_state(h); + switch (state) { + case HOME_ABSENT: + return sd_bus_error_setf(error, BUS_ERROR_HOME_ABSENT, "Home %s is currently missing or not plugged in.", h->user_name); + case HOME_LOCKED: + return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name); + case HOME_UNFIXATED: + case HOME_INACTIVE: + case HOME_ACTIVE: + break; + default: + return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "An operation on home %s is currently being executed.", h->user_name); + } + + r = home_ratelimit(h, error); + if (r < 0) + return r; + + return home_authenticate_internal(h, secret, state == HOME_ACTIVE ? HOME_AUTHENTICATING_WHILE_ACTIVE : HOME_AUTHENTICATING, error); +} + +static int home_deactivate_internal(Home *h, bool force, sd_bus_error *error) { + int r; + + assert(h); + + r = home_start_work(h, force ? "deactivate-force" : "deactivate", h->record, NULL); + if (r < 0) + return r; + + home_set_state(h, HOME_DEACTIVATING); + return 0; +} + +int home_deactivate(Home *h, bool force, sd_bus_error *error) { + assert(h); + + switch (home_get_state(h)) { + case HOME_UNFIXATED: + case HOME_ABSENT: + case HOME_INACTIVE: + return sd_bus_error_setf(error, BUS_ERROR_HOME_NOT_ACTIVE, "Home %s not active.", h->user_name); + case HOME_LOCKED: + return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name); + case HOME_ACTIVE: + break; + default: + return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "An operation on home %s is currently being executed.", h->user_name); + } + + return home_deactivate_internal(h, force, error); +} + +int home_create(Home *h, UserRecord *secret, sd_bus_error *error) { + int r; + + assert(h); + + switch (home_get_state(h)) { + case HOME_INACTIVE: + if (h->record->storage < 0) + break; /* if no storage is defined we don't know what precisely to look for, hence + * HOME_INACTIVE is OK in that case too. */ + + if (IN_SET(user_record_test_image_path(h->record), USER_TEST_MAYBE, USER_TEST_UNDEFINED)) + break; /* And if the image path test isn't conclusive, let's also go on */ + + _fallthrough_; + case HOME_UNFIXATED: + return sd_bus_error_setf(error, BUS_ERROR_HOME_EXISTS, "Home of user %s already exists.", h->user_name); + case HOME_ABSENT: + break; + case HOME_ACTIVE: + case HOME_LOCKED: + default: + return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "Home %s is currently being used, or an operation on home %s is currently being executed.", h->user_name, h->user_name); + } + + if (h->record->enforce_password_policy == false) + log_debug("Password quality check turned off for account, skipping."); + else { + r = quality_check_password(h->record, secret, error); + if (r < 0) + return r; + } + + r = home_start_work(h, "create", h->record, secret); + if (r < 0) + return r; + + home_set_state(h, HOME_CREATING); + return 0; +} + +int home_remove(Home *h, sd_bus_error *error) { + HomeState state; + int r; + + assert(h); + + state = home_get_state(h); + switch (state) { + case HOME_ABSENT: /* If the home directory is absent, then this is just like unregistering */ + return home_unregister(h, error); + case HOME_LOCKED: + return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name); + case HOME_UNFIXATED: + case HOME_INACTIVE: + break; + case HOME_ACTIVE: + default: + return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "Home %s is currently being used, or an operation on home %s is currently being executed.", h->user_name, h->user_name); + } + + r = home_start_work(h, "remove", h->record, NULL); + if (r < 0) + return r; + + home_set_state(h, HOME_REMOVING); + return 0; +} + +static int user_record_extend_with_binding(UserRecord *hr, UserRecord *with_binding, UserRecordLoadFlags flags, UserRecord **ret) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + _cleanup_(user_record_unrefp) UserRecord *nr = NULL; + JsonVariant *binding; + int r; + + assert(hr); + assert(with_binding); + assert(ret); + + assert_se(v = json_variant_ref(hr->json)); + + binding = json_variant_by_key(with_binding->json, "binding"); + if (binding) { + r = json_variant_set_field(&v, "binding", binding); + if (r < 0) + return r; + } + + nr = user_record_new(); + if (!nr) + return -ENOMEM; + + r = user_record_load(nr, v, flags); + if (r < 0) + return r; + + *ret = TAKE_PTR(nr); + return 0; +} + +static int home_update_internal(Home *h, const char *verb, UserRecord *hr, UserRecord *secret, sd_bus_error *error) { + _cleanup_(user_record_unrefp) UserRecord *new_hr = NULL, *saved_secret = NULL, *signed_hr = NULL; + int r, c; + + assert(h); + assert(verb); + assert(hr); + + if (!user_record_compatible(hr, h->record)) + return sd_bus_error_setf(error, BUS_ERROR_HOME_RECORD_MISMATCH, "Updated user record is not compatible with existing one."); + c = user_record_compare_last_change(hr, h->record); /* refuse downgrades */ + if (c < 0) + return sd_bus_error_setf(error, BUS_ERROR_HOME_RECORD_DOWNGRADE, "Refusing to update to older home record."); + + if (!secret && FLAGS_SET(hr->mask, USER_RECORD_SECRET)) { + r = user_record_clone(hr, USER_RECORD_EXTRACT_SECRET, &saved_secret); + if (r < 0) + return r; + + secret = saved_secret; + } + + r = manager_verify_user_record(h->manager, hr); + switch (r) { + + case USER_RECORD_UNSIGNED: + if (h->signed_locally <= 0) /* If the existing record is not owned by us, don't accept an + * unsigned new record. i.e. only implicitly sign new records + * that where previously signed by us too. */ + return sd_bus_error_setf(error, BUS_ERROR_HOME_RECORD_SIGNED, "Home %s is signed and cannot be modified locally.", h->user_name); + + /* The updated record is not signed, then do so now */ + r = manager_sign_user_record(h->manager, hr, &signed_hr, error); + if (r < 0) + return r; + + hr = signed_hr; + break; + + case USER_RECORD_SIGNED_EXCLUSIVE: + case USER_RECORD_SIGNED: + case USER_RECORD_FOREIGN: + /* Has already been signed. Great! */ + break; + + case -ENOKEY: + default: + return r; + } + + r = user_record_extend_with_binding(hr, h->record, USER_RECORD_LOAD_MASK_SECRET, &new_hr); + if (r < 0) + return r; + + if (c == 0) { + /* different payload but same lastChangeUSec field? That's not cool! */ + + r = user_record_masked_equal(new_hr, h->record, USER_RECORD_REGULAR|USER_RECORD_PRIVILEGED|USER_RECORD_PER_MACHINE); + if (r < 0) + return r; + if (r == 0) + return sd_bus_error_setf(error, BUS_ERROR_HOME_RECORD_MISMATCH, "Home record different but timestamp remained the same, refusing."); + } + + r = home_start_work(h, verb, new_hr, secret); + if (r < 0) + return r; + + return 0; +} + +int home_update(Home *h, UserRecord *hr, sd_bus_error *error) { + HomeState state; + int r; + + assert(h); + assert(hr); + + state = home_get_state(h); + switch (state) { + case HOME_UNFIXATED: + return sd_bus_error_setf(error, BUS_ERROR_HOME_UNFIXATED, "Home %s has not been fixated yet.", h->user_name); + case HOME_ABSENT: + return sd_bus_error_setf(error, BUS_ERROR_HOME_ABSENT, "Home %s is currently missing or not plugged in.", h->user_name); + case HOME_LOCKED: + return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name); + case HOME_INACTIVE: + case HOME_ACTIVE: + break; + default: + return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "An operation on home %s is currently being executed.", h->user_name); + } + + r = home_ratelimit(h, error); + if (r < 0) + return r; + + r = home_update_internal(h, "update", hr, NULL, error); + if (r < 0) + return r; + + home_set_state(h, state == HOME_ACTIVE ? HOME_UPDATING_WHILE_ACTIVE : HOME_UPDATING); + return 0; +} + +int home_resize(Home *h, uint64_t disk_size, UserRecord *secret, sd_bus_error *error) { + _cleanup_(user_record_unrefp) UserRecord *c = NULL; + HomeState state; + int r; + + assert(h); + + state = home_get_state(h); + switch (state) { + case HOME_UNFIXATED: + return sd_bus_error_setf(error, BUS_ERROR_HOME_UNFIXATED, "Home %s has not been fixated yet.", h->user_name); + case HOME_ABSENT: + return sd_bus_error_setf(error, BUS_ERROR_HOME_ABSENT, "Home %s is currently missing or not plugged in.", h->user_name); + case HOME_LOCKED: + return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name); + case HOME_INACTIVE: + case HOME_ACTIVE: + break; + default: + return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "An operation on home %s is currently being executed.", h->user_name); + } + + r = home_ratelimit(h, error); + if (r < 0) + return r; + + if (disk_size == UINT64_MAX || disk_size == h->record->disk_size) { + if (h->record->disk_size == UINT64_MAX) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Not disk size to resize to specified."); + + c = user_record_ref(h->record); /* Shortcut if size is unspecified or matches the record */ + } else { + _cleanup_(user_record_unrefp) UserRecord *signed_c = NULL; + + if (h->signed_locally <= 0) /* Don't allow changing of records not signed only by us */ + return sd_bus_error_setf(error, BUS_ERROR_HOME_RECORD_SIGNED, "Home %s is signed and cannot be modified locally.", h->user_name); + + r = user_record_clone(h->record, USER_RECORD_LOAD_REFUSE_SECRET, &c); + if (r < 0) + return r; + + r = user_record_set_disk_size(c, disk_size); + if (r == -ERANGE) + return sd_bus_error_setf(error, BUS_ERROR_BAD_HOME_SIZE, "Requested size for home %s out of acceptable range.", h->user_name); + if (r < 0) + return r; + + r = user_record_update_last_changed(c, false); + if (r == -ECHRNG) + return sd_bus_error_setf(error, BUS_ERROR_HOME_RECORD_MISMATCH, "Record last change time of %s is newer than current time, cannot update.", h->user_name); + if (r < 0) + return r; + + r = manager_sign_user_record(h->manager, c, &signed_c, error); + if (r < 0) + return r; + + user_record_unref(c); + c = TAKE_PTR(signed_c); + } + + r = home_update_internal(h, "resize", c, secret, error); + if (r < 0) + return r; + + home_set_state(h, state == HOME_ACTIVE ? HOME_RESIZING_WHILE_ACTIVE : HOME_RESIZING); + return 0; +} + +static int home_may_change_password( + Home *h, + sd_bus_error *error) { + + int r; + + assert(h); + + r = user_record_test_password_change_required(h->record); + if (IN_SET(r, -EKEYREVOKED, -EOWNERDEAD, -EKEYEXPIRED)) + return 0; /* expired in some form, but chaning is allowed */ + if (IN_SET(r, -EKEYREJECTED, -EROFS)) + return sd_bus_error_setf(error, SD_BUS_ERROR_ACCESS_DENIED, "Expiration settings of account %s do not allow changing of password.", h->user_name); + if (r < 0) + return log_error_errno(r, "Failed to test password expiry: %m"); + + return 0; /* not expired */ +} + +int home_passwd(Home *h, + UserRecord *new_secret, + UserRecord *old_secret, + sd_bus_error *error) { + + _cleanup_(user_record_unrefp) UserRecord *c = NULL, *merged_secret = NULL, *signed_c = NULL; + HomeState state; + int r; + + assert(h); + + if (h->signed_locally <= 0) /* Don't allow changing of records not signed only by us */ + return sd_bus_error_setf(error, BUS_ERROR_HOME_RECORD_SIGNED, "Home %s is signed and cannot be modified locally.", h->user_name); + + state = home_get_state(h); + switch (state) { + case HOME_UNFIXATED: + return sd_bus_error_setf(error, BUS_ERROR_HOME_UNFIXATED, "Home %s has not been fixated yet.", h->user_name); + case HOME_ABSENT: + return sd_bus_error_setf(error, BUS_ERROR_HOME_ABSENT, "Home %s is currently missing or not plugged in.", h->user_name); + case HOME_LOCKED: + return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name); + case HOME_INACTIVE: + case HOME_ACTIVE: + break; + default: + return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "An operation on home %s is currently being executed.", h->user_name); + } + + r = home_ratelimit(h, error); + if (r < 0) + return r; + + r = home_may_change_password(h, error); + if (r < 0) + return r; + + r = user_record_clone(h->record, USER_RECORD_LOAD_REFUSE_SECRET, &c); + if (r < 0) + return r; + + merged_secret = user_record_new(); + if (!merged_secret) + return -ENOMEM; + + r = user_record_merge_secret(merged_secret, old_secret); + if (r < 0) + return r; + + r = user_record_merge_secret(merged_secret, new_secret); + if (r < 0) + return r; + + if (!strv_isempty(new_secret->password)) { + /* Update the password only if one is specified, otherwise let's just reuse the old password + * data. This is useful as a way to propagate updated user records into the LUKS backends + * properly. */ + + r = user_record_make_hashed_password(c, new_secret->password, /* extend = */ false); + if (r < 0) + return r; + + r = user_record_set_password_change_now(c, -1 /* remove */); + if (r < 0) + return r; + } + + r = user_record_update_last_changed(c, true); + if (r == -ECHRNG) + return sd_bus_error_setf(error, BUS_ERROR_HOME_RECORD_MISMATCH, "Record last change time of %s is newer than current time, cannot update.", h->user_name); + if (r < 0) + return r; + + r = manager_sign_user_record(h->manager, c, &signed_c, error); + if (r < 0) + return r; + + if (c->enforce_password_policy == false) + log_debug("Password quality check turned off for account, skipping."); + else { + r = quality_check_password(c, merged_secret, error); + if (r < 0) + return r; + } + + r = home_update_internal(h, "passwd", signed_c, merged_secret, error); + if (r < 0) + return r; + + home_set_state(h, state == HOME_ACTIVE ? HOME_PASSWD_WHILE_ACTIVE : HOME_PASSWD); + return 0; +} + +int home_unregister(Home *h, sd_bus_error *error) { + int r; + + assert(h); + + switch (home_get_state(h)) { + case HOME_UNFIXATED: + return sd_bus_error_setf(error, BUS_ERROR_HOME_UNFIXATED, "Home %s is not registered.", h->user_name); + case HOME_LOCKED: + return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name); + case HOME_ABSENT: + case HOME_INACTIVE: + break; + case HOME_ACTIVE: + default: + return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "Home %s is currently being used, or an operation on home %s is currently being executed.", h->user_name, h->user_name); + } + + r = home_unlink_record(h); + if (r < 0) + return r; + + /* And destroy the whole entry. The caller needs to be prepared for that. */ + h = home_free(h); + return 1; +} + +int home_lock(Home *h, sd_bus_error *error) { + int r; + + assert(h); + + switch (home_get_state(h)) { + case HOME_UNFIXATED: + case HOME_ABSENT: + case HOME_INACTIVE: + return sd_bus_error_setf(error, BUS_ERROR_HOME_NOT_ACTIVE, "Home %s is not active.", h->user_name); + case HOME_LOCKED: + return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is already locked.", h->user_name); + case HOME_ACTIVE: + break; + default: + return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "An operation on home %s is currently being executed.", h->user_name); + } + + r = home_start_work(h, "lock", h->record, NULL); + if (r < 0) + return r; + + home_set_state(h, HOME_LOCKING); + return 0; +} + +static int home_unlock_internal(Home *h, UserRecord *secret, HomeState for_state, sd_bus_error *error) { + int r; + + assert(h); + assert(IN_SET(for_state, HOME_UNLOCKING, HOME_UNLOCKING_FOR_ACQUIRE)); + + r = home_start_work(h, "unlock", h->record, secret); + if (r < 0) + return r; + + home_set_state(h, for_state); + return 0; +} + +int home_unlock(Home *h, UserRecord *secret, sd_bus_error *error) { + int r; + assert(h); + + r = home_ratelimit(h, error); + if (r < 0) + return r; + + switch (home_get_state(h)) { + case HOME_UNFIXATED: + case HOME_ABSENT: + case HOME_INACTIVE: + case HOME_ACTIVE: + return sd_bus_error_setf(error, BUS_ERROR_HOME_NOT_LOCKED, "Home %s is not locked.", h->user_name); + case HOME_LOCKED: + break; + default: + return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "An operation on home %s is currently being executed.", h->user_name); + } + + return home_unlock_internal(h, secret, HOME_UNLOCKING, error); +} + +HomeState home_get_state(Home *h) { + assert(h); + + /* When the state field is initialized, it counts. */ + if (h->state >= 0) + return h->state; + + /* Otherwise, let's see if the home directory is mounted. If so, we assume for sure the home + * directory is active */ + if (user_record_test_home_directory(h->record) == USER_TEST_MOUNTED) + return HOME_ACTIVE; + + /* And if we see the image being gone, we report this as absent */ + if (user_record_test_image_path(h->record) == USER_TEST_ABSENT) + return HOME_ABSENT; + + /* And for all other cases we return "inactive". */ + return HOME_INACTIVE; +} + +void home_process_notify(Home *h, char **l) { + const char *e; + int error; + int r; + + assert(h); + + e = strv_env_get(l, "ERRNO"); + if (!e) { + log_debug("Got notify message lacking ERRNO= field, ignoring."); + return; + } + + r = safe_atoi(e, &error); + if (r < 0) { + log_debug_errno(r, "Failed to parse receieved error number, ignoring: %s", e); + return; + } + if (error <= 0) { + log_debug("Error number is out of range: %i", error); + return; + } + + h->worker_error_code = error; +} + +int home_killall(Home *h) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_free_ char *unit = NULL; + int r; + + assert(h); + + if (!uid_is_valid(h->uid)) + return 0; + + assert(h->uid > 0); /* We never should be UID 0 */ + + /* Let's kill everything matching the specified UID */ + r = safe_fork("(sd-killer)", FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG|FORK_WAIT|FORK_LOG, NULL); + if (r < 0) + return r; + if (r == 0) { + gid_t gid; + + /* Child */ + + gid = user_record_gid(h->record); + if (setresgid(gid, gid, gid) < 0) { + log_error_errno(errno, "Failed to change GID to " GID_FMT ": %m", gid); + _exit(EXIT_FAILURE); + } + + if (setgroups(0, NULL) < 0) { + log_error_errno(errno, "Failed to reset auxiliary groups list: %m"); + _exit(EXIT_FAILURE); + } + + if (setresuid(h->uid, h->uid, h->uid) < 0) { + log_error_errno(errno, "Failed to change UID to " UID_FMT ": %m", h->uid); + _exit(EXIT_FAILURE); + } + + if (kill(-1, SIGKILL) < 0) { + log_error_errno(errno, "Failed to kill all processes of UID " UID_FMT ": %m", h->uid); + _exit(EXIT_FAILURE); + } + + _exit(EXIT_SUCCESS); + } + + /* Let's also kill everything in the user's slice */ + if (asprintf(&unit, "user-" UID_FMT ".slice", h->uid) < 0) + return log_oom(); + + r = sd_bus_call_method( + h->manager->bus, + "org.freedesktop.systemd1", + "/org/freedesktop/systemd1", + "org.freedesktop.systemd1.Manager", + "KillUnit", + &error, + NULL, + "ssi", unit, "all", SIGKILL); + if (r < 0) + log_full_errno(sd_bus_error_has_name(&error, BUS_ERROR_NO_SUCH_UNIT) ? LOG_DEBUG : LOG_WARNING, + r, "Failed to kill login processes of user, ignoring: %s", bus_error_message(&error, r)); + + return 1; +} + +static int home_get_disk_status_luks( + Home *h, + HomeState state, + uint64_t *ret_disk_size, + uint64_t *ret_disk_usage, + uint64_t *ret_disk_free, + uint64_t *ret_disk_ceiling, + uint64_t *ret_disk_floor) { + + uint64_t disk_size = UINT64_MAX, disk_usage = UINT64_MAX, disk_free = UINT64_MAX, + disk_ceiling = UINT64_MAX, disk_floor = UINT64_MAX, + stat_used = UINT64_MAX, fs_size = UINT64_MAX, header_size = 0; + + struct statfs sfs; + const char *hd; + int r; + + assert(h); + assert(ret_disk_size); + assert(ret_disk_usage); + assert(ret_disk_free); + assert(ret_disk_ceiling); + + if (state != HOME_ABSENT) { + const char *ip; + + ip = user_record_image_path(h->record); + if (ip) { + struct stat st; + + if (stat(ip, &st) < 0) + log_debug_errno(errno, "Failed to stat() %s, ignoring: %m", ip); + else if (S_ISREG(st.st_mode)) { + _cleanup_free_ char *parent = NULL; + + disk_size = st.st_size; + stat_used = st.st_blocks * 512; + + parent = dirname_malloc(ip); + if (!parent) + return log_oom(); + + if (statfs(parent, &sfs) < 0) + log_debug_errno(errno, "Failed to statfs() %s, ignoring: %m", parent); + else + disk_ceiling = stat_used + sfs.f_bsize * sfs.f_bavail; + + } else if (S_ISBLK(st.st_mode)) { + _cleanup_free_ char *szbuf = NULL; + char p[SYS_BLOCK_PATH_MAX("/size")]; + + /* Let's read the size off sysfs, so that we don't have to open the device */ + xsprintf_sys_block_path(p, "/size", st.st_rdev); + r = read_one_line_file(p, &szbuf); + if (r < 0) + log_debug_errno(r, "Failed to read %s, ignoring: %m", p); + else { + uint64_t sz; + + r = safe_atou64(szbuf, &sz); + if (r < 0) + log_debug_errno(r, "Failed to parse %s, ignoring: %s", p, szbuf); + else + disk_size = sz * 512; + } + } else + log_debug("Image path is not a block device or regular file, not able to acquire size."); + } + } + + if (!HOME_STATE_IS_ACTIVE(state)) + goto finish; + + hd = user_record_home_directory(h->record); + if (!hd) + goto finish; + + if (statfs(hd, &sfs) < 0) { + log_debug_errno(errno, "Failed to statfs() %s, ignoring: %m", hd); + goto finish; + } + + disk_free = sfs.f_bsize * sfs.f_bavail; + fs_size = sfs.f_bsize * sfs.f_blocks; + if (disk_size != UINT64_MAX && disk_size > fs_size) + header_size = disk_size - fs_size; + + /* We take a perspective from the user here (as opposed to from the host): the used disk space is the + * difference from the limit and what's free. This makes a difference if sparse mode is not used: in + * that case the image is pre-allocated and thus appears all used from the host PoV but is not used + * up at all yet from the user's PoV. + * + * That said, we use use the stat() reported loopback file size as upper boundary: our footprint can + * never be larger than what we take up on the lowest layers. */ + + if (disk_size != UINT64_MAX && disk_size > disk_free) { + disk_usage = disk_size - disk_free; + + if (stat_used != UINT64_MAX && disk_usage > stat_used) + disk_usage = stat_used; + } else + disk_usage = stat_used; + + /* If we have the magic, determine floor preferably by magic */ + disk_floor = minimal_size_by_fs_magic(sfs.f_type) + header_size; + +finish: + /* If we don't know the magic, go by file system name */ + if (disk_floor == UINT64_MAX) + disk_floor = minimal_size_by_fs_name(user_record_file_system_type(h->record)); + + *ret_disk_size = disk_size; + *ret_disk_usage = disk_usage; + *ret_disk_free = disk_free; + *ret_disk_ceiling = disk_ceiling; + *ret_disk_floor = disk_floor; + + return 0; +} + +static int home_get_disk_status_directory( + Home *h, + HomeState state, + uint64_t *ret_disk_size, + uint64_t *ret_disk_usage, + uint64_t *ret_disk_free, + uint64_t *ret_disk_ceiling, + uint64_t *ret_disk_floor) { + + uint64_t disk_size = UINT64_MAX, disk_usage = UINT64_MAX, disk_free = UINT64_MAX, + disk_ceiling = UINT64_MAX, disk_floor = UINT64_MAX; + struct statfs sfs; + struct dqblk req; + const char *path = NULL; + int r; + + assert(ret_disk_size); + assert(ret_disk_usage); + assert(ret_disk_free); + assert(ret_disk_ceiling); + assert(ret_disk_floor); + + if (HOME_STATE_IS_ACTIVE(state)) + path = user_record_home_directory(h->record); + + if (!path) { + if (state == HOME_ABSENT) + goto finish; + + path = user_record_image_path(h->record); + } + + if (!path) + goto finish; + + if (statfs(path, &sfs) < 0) + log_debug_errno(errno, "Failed to statfs() %s, ignoring: %m", path); + else { + disk_free = sfs.f_bsize * sfs.f_bavail; + disk_size = sfs.f_bsize * sfs.f_blocks; + + /* We don't initialize disk_usage from statfs() data here, since the device is likely not used + * by us alone, and disk_usage should only reflect our own use. */ + } + + if (IN_SET(h->record->storage, USER_CLASSIC, USER_DIRECTORY, USER_SUBVOLUME)) { + + r = btrfs_is_subvol(path); + if (r < 0) + log_debug_errno(r, "Failed to determine whether %s is a btrfs subvolume: %m", path); + else if (r > 0) { + BtrfsQuotaInfo qi; + + r = btrfs_subvol_get_subtree_quota(path, 0, &qi); + if (r < 0) + log_debug_errno(r, "Failed to query btrfs subtree quota, ignoring: %m"); + else { + disk_usage = qi.referenced; + + if (disk_free != UINT64_MAX) { + disk_ceiling = qi.referenced + disk_free; + + if (disk_size != UINT64_MAX && disk_ceiling > disk_size) + disk_ceiling = disk_size; + } + + if (qi.referenced_max != UINT64_MAX) { + if (disk_size != UINT64_MAX) + disk_size = MIN(qi.referenced_max, disk_size); + else + disk_size = qi.referenced_max; + } + + if (disk_size != UINT64_MAX) { + if (disk_size > disk_usage) + disk_free = disk_size - disk_usage; + else + disk_free = 0; + } + } + + goto finish; + } + } + + if (IN_SET(h->record->storage, USER_CLASSIC, USER_DIRECTORY, USER_FSCRYPT)) { + r = quotactl_path(QCMD_FIXED(Q_GETQUOTA, USRQUOTA), path, h->uid, &req); + if (r < 0) { + if (ERRNO_IS_NOT_SUPPORTED(r)) { + log_debug_errno(r, "No UID quota support on %s.", path); + goto finish; + } + + if (r != -ESRCH) { + log_debug_errno(r, "Failed to query disk quota for UID " UID_FMT ": %m", h->uid); + goto finish; + } + + disk_usage = 0; /* No record of this user? then nothing was used */ + } else { + if (FLAGS_SET(req.dqb_valid, QIF_SPACE) && disk_free != UINT64_MAX) { + disk_ceiling = req.dqb_curspace + disk_free; + + if (disk_size != UINT64_MAX && disk_ceiling > disk_size) + disk_ceiling = disk_size; + } + + if (FLAGS_SET(req.dqb_valid, QIF_BLIMITS)) { + uint64_t q; + + /* Take the minimum of the quota and the available disk space here */ + q = req.dqb_bhardlimit * QIF_DQBLKSIZE; + if (disk_size != UINT64_MAX) + disk_size = MIN(disk_size, q); + else + disk_size = q; + } + if (FLAGS_SET(req.dqb_valid, QIF_SPACE)) { + disk_usage = req.dqb_curspace; + + if (disk_size != UINT64_MAX) { + if (disk_size > disk_usage) + disk_free = disk_size - disk_usage; + else + disk_free = 0; + } + } + } + } + +finish: + *ret_disk_size = disk_size; + *ret_disk_usage = disk_usage; + *ret_disk_free = disk_free; + *ret_disk_ceiling = disk_ceiling; + *ret_disk_floor = disk_floor; + + return 0; +} + +int home_augment_status( + Home *h, + UserRecordLoadFlags flags, + UserRecord **ret) { + + uint64_t disk_size = UINT64_MAX, disk_usage = UINT64_MAX, disk_free = UINT64_MAX, disk_ceiling = UINT64_MAX, disk_floor = UINT64_MAX; + _cleanup_(json_variant_unrefp) JsonVariant *j = NULL, *v = NULL, *m = NULL, *status = NULL; + _cleanup_(user_record_unrefp) UserRecord *ur = NULL; + char ids[SD_ID128_STRING_MAX]; + HomeState state; + sd_id128_t id; + int r; + + assert(h); + assert(ret); + + /* We are supposed to add this, this can't be on hence. */ + assert(!FLAGS_SET(flags, USER_RECORD_STRIP_STATUS)); + + r = sd_id128_get_machine(&id); + if (r < 0) + return r; + + state = home_get_state(h); + + switch (h->record->storage) { + + case USER_LUKS: + r = home_get_disk_status_luks(h, state, &disk_size, &disk_usage, &disk_free, &disk_ceiling, &disk_floor); + if (r < 0) + return r; + + break; + + case USER_CLASSIC: + case USER_DIRECTORY: + case USER_SUBVOLUME: + case USER_FSCRYPT: + case USER_CIFS: + r = home_get_disk_status_directory(h, state, &disk_size, &disk_usage, &disk_free, &disk_ceiling, &disk_floor); + if (r < 0) + return r; + + break; + + default: + ; /* unset */ + } + + if (disk_floor == UINT64_MAX || (disk_usage != UINT64_MAX && disk_floor < disk_usage)) + disk_floor = disk_usage; + if (disk_floor == UINT64_MAX || disk_floor < USER_DISK_SIZE_MIN) + disk_floor = USER_DISK_SIZE_MIN; + if (disk_ceiling == UINT64_MAX || disk_ceiling > USER_DISK_SIZE_MAX) + disk_ceiling = USER_DISK_SIZE_MAX; + + r = json_build(&status, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("state", JSON_BUILD_STRING(home_state_to_string(state))), + JSON_BUILD_PAIR("service", JSON_BUILD_STRING("io.systemd.Home")), + JSON_BUILD_PAIR_CONDITION(disk_size != UINT64_MAX, "diskSize", JSON_BUILD_UNSIGNED(disk_size)), + JSON_BUILD_PAIR_CONDITION(disk_usage != UINT64_MAX, "diskUsage", JSON_BUILD_UNSIGNED(disk_usage)), + JSON_BUILD_PAIR_CONDITION(disk_free != UINT64_MAX, "diskFree", JSON_BUILD_UNSIGNED(disk_free)), + JSON_BUILD_PAIR_CONDITION(disk_ceiling != UINT64_MAX, "diskCeiling", JSON_BUILD_UNSIGNED(disk_ceiling)), + JSON_BUILD_PAIR_CONDITION(disk_floor != UINT64_MAX, "diskFloor", JSON_BUILD_UNSIGNED(disk_floor)), + JSON_BUILD_PAIR_CONDITION(h->signed_locally >= 0, "signedLocally", JSON_BUILD_BOOLEAN(h->signed_locally)) + )); + if (r < 0) + return r; + + j = json_variant_ref(h->record->json); + v = json_variant_ref(json_variant_by_key(j, "status")); + m = json_variant_ref(json_variant_by_key(v, sd_id128_to_string(id, ids))); + + r = json_variant_filter(&m, STRV_MAKE("diskSize", "diskUsage", "diskFree", "diskCeiling", "diskFloor", "signedLocally")); + if (r < 0) + return r; + + r = json_variant_merge(&m, status); + if (r < 0) + return r; + + r = json_variant_set_field(&v, ids, m); + if (r < 0) + return r; + + r = json_variant_set_field(&j, "status", v); + if (r < 0) + return r; + + ur = user_record_new(); + if (!ur) + return -ENOMEM; + + r = user_record_load(ur, j, flags); + if (r < 0) + return r; + + ur->incomplete = + FLAGS_SET(h->record->mask, USER_RECORD_PRIVILEGED) && + !FLAGS_SET(ur->mask, USER_RECORD_PRIVILEGED); + + *ret = TAKE_PTR(ur); + return 0; +} + +static int on_home_ref_eof(sd_event_source *s, int fd, uint32_t revents, void *userdata) { + _cleanup_(operation_unrefp) Operation *o = NULL; + Home *h = userdata; + + assert(s); + assert(h); + + if (h->ref_event_source_please_suspend == s) + h->ref_event_source_please_suspend = sd_event_source_disable_unref(h->ref_event_source_please_suspend); + + if (h->ref_event_source_dont_suspend == s) + h->ref_event_source_dont_suspend = sd_event_source_disable_unref(h->ref_event_source_dont_suspend); + + if (h->ref_event_source_dont_suspend || h->ref_event_source_please_suspend) + return 0; + + log_info("Got notification that all sessions of user %s ended, deactivating automatically.", h->user_name); + + o = operation_new(OPERATION_PIPE_EOF, NULL); + if (!o) { + log_oom(); + return 0; + } + + home_schedule_operation(h, o, NULL); + return 0; +} + +int home_create_fifo(Home *h, bool please_suspend) { + _cleanup_close_ int ret_fd = -1; + sd_event_source **ss; + const char *fn, *suffix; + int r; + + assert(h); + + if (please_suspend) { + suffix = ".please-suspend"; + ss = &h->ref_event_source_please_suspend; + } else { + suffix = ".dont-suspend"; + ss = &h->ref_event_source_dont_suspend; + } + + fn = strjoina("/run/systemd/home/", h->user_name, suffix); + + if (!*ss) { + _cleanup_close_ int ref_fd = -1; + + (void) mkdir("/run/systemd/home/", 0755); + if (mkfifo(fn, 0600) < 0 && errno != EEXIST) + return log_error_errno(errno, "Failed to create FIFO %s: %m", fn); + + ref_fd = open(fn, O_RDONLY|O_CLOEXEC|O_NONBLOCK); + if (ref_fd < 0) + return log_error_errno(errno, "Failed to open FIFO %s for reading: %m", fn); + + r = sd_event_add_io(h->manager->event, ss, ref_fd, 0, on_home_ref_eof, h); + if (r < 0) + return log_error_errno(r, "Failed to allocate reference FIFO event source: %m"); + + (void) sd_event_source_set_description(*ss, "acquire-ref"); + + r = sd_event_source_set_priority(*ss, SD_EVENT_PRIORITY_IDLE-1); + if (r < 0) + return r; + + r = sd_event_source_set_io_fd_own(*ss, true); + if (r < 0) + return log_error_errno(r, "Failed to pass ownership of FIFO event fd to event source: %m"); + + TAKE_FD(ref_fd); + } + + ret_fd = open(fn, O_WRONLY|O_CLOEXEC|O_NONBLOCK); + if (ret_fd < 0) + return log_error_errno(errno, "Failed to open FIFO %s for writing: %m", fn); + + return TAKE_FD(ret_fd); +} + +static int home_dispatch_acquire(Home *h, Operation *o) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + int (*call)(Home *h, UserRecord *secret, HomeState for_state, sd_bus_error *error) = NULL; + HomeState for_state; + int r; + + assert(h); + assert(o); + assert(o->type == OPERATION_ACQUIRE); + + switch (home_get_state(h)) { + + case HOME_UNFIXATED: + for_state = HOME_FIXATING_FOR_ACQUIRE; + call = home_fixate_internal; + break; + + case HOME_ABSENT: + r = sd_bus_error_setf(&error, BUS_ERROR_HOME_ABSENT, "Home %s is currently missing or not plugged in.", h->user_name); + break; + + case HOME_INACTIVE: + for_state = HOME_ACTIVATING_FOR_ACQUIRE; + call = home_activate_internal; + break; + + case HOME_ACTIVE: + for_state = HOME_AUTHENTICATING_FOR_ACQUIRE; + call = home_authenticate_internal; + break; + + case HOME_LOCKED: + for_state = HOME_UNLOCKING_FOR_ACQUIRE; + call = home_unlock_internal; + break; + + default: + /* All other cases means we are currently executing an operation, which means the job remains + * pending. */ + return 0; + } + + assert(!h->current_operation); + + if (call) { + r = home_ratelimit(h, &error); + if (r >= 0) + r = call(h, o->secret, for_state, &error); + } + + if (r != 0) /* failure or completed */ + operation_result(o, r, &error); + else /* ongoing */ + h->current_operation = operation_ref(o); + + return 1; +} + +static int home_dispatch_release(Home *h, Operation *o) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + int r; + + assert(h); + assert(o); + assert(o->type == OPERATION_RELEASE); + + if (h->ref_event_source_dont_suspend || h->ref_event_source_please_suspend) + /* If there's now a reference again, then let's abort the release attempt */ + r = sd_bus_error_setf(&error, BUS_ERROR_HOME_BUSY, "Home %s is currently referenced.", h->user_name); + else { + switch (home_get_state(h)) { + + case HOME_UNFIXATED: + case HOME_ABSENT: + case HOME_INACTIVE: + r = 0; /* done */ + break; + + case HOME_LOCKED: + r = sd_bus_error_setf(&error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name); + break; + + case HOME_ACTIVE: + r = home_deactivate_internal(h, false, &error); + break; + + default: + /* All other cases means we are currently executing an operation, which means the job remains + * pending. */ + return 0; + } + } + + assert(!h->current_operation); + + if (r <= 0) /* failure or completed */ + operation_result(o, r, &error); + else /* ongoing */ + h->current_operation = operation_ref(o); + + return 1; +} + +static int home_dispatch_lock_all(Home *h, Operation *o) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + int r; + + assert(h); + assert(o); + assert(o->type == OPERATION_LOCK_ALL); + + switch (home_get_state(h)) { + + case HOME_UNFIXATED: + case HOME_ABSENT: + case HOME_INACTIVE: + log_info("Home %s is not active, no locking necessary.", h->user_name); + r = 0; /* done */ + break; + + case HOME_LOCKED: + log_info("Home %s is already locked.", h->user_name); + r = 0; /* done */ + break; + + case HOME_ACTIVE: + log_info("Locking home %s.", h->user_name); + r = home_lock(h, &error); + break; + + default: + /* All other cases means we are currently executing an operation, which means the job remains + * pending. */ + return 0; + } + + assert(!h->current_operation); + + if (r != 0) /* failure or completed */ + operation_result(o, r, &error); + else /* ongoing */ + h->current_operation = operation_ref(o); + + return 1; +} + +static int home_dispatch_pipe_eof(Home *h, Operation *o) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + int r; + + assert(h); + assert(o); + assert(o->type == OPERATION_PIPE_EOF); + + if (h->ref_event_source_please_suspend || h->ref_event_source_dont_suspend) + return 1; /* Hmm, there's a reference again, let's cancel this */ + + switch (home_get_state(h)) { + + case HOME_UNFIXATED: + case HOME_ABSENT: + case HOME_INACTIVE: + log_info("Home %s already deactivated, no automatic deactivation needed.", h->user_name); + break; + + case HOME_DEACTIVATING: + log_info("Home %s is already being deactivated, automatic deactivated unnecessary.", h->user_name); + break; + + case HOME_ACTIVE: + r = home_deactivate_internal(h, false, &error); + if (r < 0) + log_warning_errno(r, "Failed to deactivate %s, ignoring: %s", h->user_name, bus_error_message(&error, r)); + break; + + case HOME_LOCKED: + default: + /* If the device is locked or any operation is being executed, let's leave this pending */ + return 0; + } + + /* Note that we don't call operation_fail() or operation_success() here, because this kind of + * operation has no message associated with it, and thus there's no need to propagate success. */ + + assert(!o->message); + return 1; +} + +static int home_dispatch_deactivate_force(Home *h, Operation *o) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + int r; + + assert(h); + assert(o); + assert(o->type == OPERATION_DEACTIVATE_FORCE); + + switch (home_get_state(h)) { + + case HOME_UNFIXATED: + case HOME_ABSENT: + case HOME_INACTIVE: + log_debug("Home %s already deactivated, no forced deactivation due to unplug needed.", h->user_name); + break; + + case HOME_DEACTIVATING: + log_debug("Home %s is already being deactivated, forced deactivation due to unplug unnecessary.", h->user_name); + break; + + case HOME_ACTIVE: + case HOME_LOCKED: + r = home_deactivate_internal(h, true, &error); + if (r < 0) + log_warning_errno(r, "Failed to forcibly deactivate %s, ignoring: %s", h->user_name, bus_error_message(&error, r)); + break; + + default: + /* If any operation is being executed, let's leave this pending */ + return 0; + } + + /* Note that we don't call operation_fail() or operation_success() here, because this kind of + * operation has no message associated with it, and thus there's no need to propagate success. */ + + assert(!o->message); + return 1; +} + +static int on_pending(sd_event_source *s, void *userdata) { + Home *h = userdata; + Operation *o; + int r; + + assert(s); + assert(h); + + o = ordered_set_first(h->pending_operations); + if (o) { + static int (* const operation_table[_OPERATION_MAX])(Home *h, Operation *o) = { + [OPERATION_ACQUIRE] = home_dispatch_acquire, + [OPERATION_RELEASE] = home_dispatch_release, + [OPERATION_LOCK_ALL] = home_dispatch_lock_all, + [OPERATION_PIPE_EOF] = home_dispatch_pipe_eof, + [OPERATION_DEACTIVATE_FORCE] = home_dispatch_deactivate_force, + }; + + assert(operation_table[o->type]); + r = operation_table[o->type](h, o); + if (r != 0) { + /* The operation completed, let's remove it from the pending list, and exit while + * leaving the event source enabled as it is. */ + assert_se(ordered_set_remove(h->pending_operations, o) == o); + operation_unref(o); + return 0; + } + } + + /* Nothing to do anymore, let's turn off this event source */ + r = sd_event_source_set_enabled(s, SD_EVENT_OFF); + if (r < 0) + return log_error_errno(r, "Failed to disable event source: %m"); + + return 0; +} + +int home_schedule_operation(Home *h, Operation *o, sd_bus_error *error) { + int r; + + assert(h); + + if (o) { + if (ordered_set_size(h->pending_operations) >= PENDING_OPERATIONS_MAX) + return sd_bus_error_setf(error, BUS_ERROR_TOO_MANY_OPERATIONS, "Too many client operations requested"); + + r = ordered_set_ensure_allocated(&h->pending_operations, &operation_hash_ops); + if (r < 0) + return r; + + r = ordered_set_put(h->pending_operations, o); + if (r < 0) + return r; + + operation_ref(o); + } + + if (!h->pending_event_source) { + r = sd_event_add_defer(h->manager->event, &h->pending_event_source, on_pending, h); + if (r < 0) + return log_error_errno(r, "Failed to allocate pending defer event source: %m"); + + (void) sd_event_source_set_description(h->pending_event_source, "pending"); + + r = sd_event_source_set_priority(h->pending_event_source, SD_EVENT_PRIORITY_IDLE); + if (r < 0) + return r; + } + + r = sd_event_source_set_enabled(h->pending_event_source, SD_EVENT_ON); + if (r < 0) + return log_error_errno(r, "Failed to trigger pending event source: %m"); + + return 0; +} + +static int home_get_image_path_seat(Home *h, char **ret) { + _cleanup_(sd_device_unrefp) sd_device *d = NULL; + _cleanup_free_ char *c = NULL; + const char *ip, *seat; + struct stat st; + int r; + + assert(h); + + if (user_record_storage(h->record) != USER_LUKS) + return -ENXIO; + + ip = user_record_image_path(h->record); + if (!ip) + return -ENXIO; + + if (!path_startswith(ip, "/dev/")) + return -ENXIO; + + if (stat(ip, &st) < 0) + return -errno; + + if (!S_ISBLK(st.st_mode)) + return -ENOTBLK; + + r = sd_device_new_from_devnum(&d, 'b', st.st_rdev); + if (r < 0) + return r; + + r = sd_device_get_property_value(d, "ID_SEAT", &seat); + if (r == -ENOENT) /* no property means seat0 */ + seat = "seat0"; + else if (r < 0) + return r; + + c = strdup(seat); + if (!c) + return -ENOMEM; + + *ret = TAKE_PTR(c); + return 0; +} + +int home_auto_login(Home *h, char ***ret_seats) { + _cleanup_free_ char *seat = NULL, *seat2 = NULL; + + assert(h); + assert(ret_seats); + + (void) home_get_image_path_seat(h, &seat); + + if (h->record->auto_login > 0 && !streq_ptr(seat, "seat0")) { + /* For now, when the auto-login boolean is set for a user, let's make it mean + * "seat0". Eventually we can extend the concept and allow configuration of any kind of seat, + * but let's keep simple initially, most likely the feature is interesting on single-user + * systems anyway, only. + * + * We filter out users marked for auto-login in we know for sure their home directory is + * absent. */ + + if (user_record_test_image_path(h->record) != USER_TEST_ABSENT) { + seat2 = strdup("seat0"); + if (!seat2) + return -ENOMEM; + } + } + + if (seat || seat2) { + _cleanup_strv_free_ char **list = NULL; + size_t i = 0; + + list = new(char*, 3); + if (!list) + return -ENOMEM; + + if (seat) + list[i++] = TAKE_PTR(seat); + if (seat2) + list[i++] = TAKE_PTR(seat2); + + list[i] = NULL; + *ret_seats = TAKE_PTR(list); + return 1; + } + + *ret_seats = NULL; + return 0; +} + +int home_set_current_message(Home *h, sd_bus_message *m) { + assert(h); + + if (!m) + return 0; + + if (h->current_operation) + return -EBUSY; + + h->current_operation = operation_new(OPERATION_IMMEDIATE, m); + if (!h->current_operation) + return -ENOMEM; + + return 1; +} + +static const char* const home_state_table[_HOME_STATE_MAX] = { + [HOME_UNFIXATED] = "unfixated", + [HOME_ABSENT] = "absent", + [HOME_INACTIVE] = "inactive", + [HOME_FIXATING] = "fixating", + [HOME_FIXATING_FOR_ACTIVATION] = "fixating-for-activation", + [HOME_FIXATING_FOR_ACQUIRE] = "fixating-for-acquire", + [HOME_ACTIVATING] = "activating", + [HOME_ACTIVATING_FOR_ACQUIRE] = "activating-for-acquire", + [HOME_DEACTIVATING] = "deactivating", + [HOME_ACTIVE] = "active", + [HOME_LOCKING] = "locking", + [HOME_LOCKED] = "locked", + [HOME_UNLOCKING] = "unlocking", + [HOME_UNLOCKING_FOR_ACQUIRE] = "unlocking-for-acquire", + [HOME_CREATING] = "creating", + [HOME_REMOVING] = "removing", + [HOME_UPDATING] = "updating", + [HOME_UPDATING_WHILE_ACTIVE] = "updating-while-active", + [HOME_RESIZING] = "resizing", + [HOME_RESIZING_WHILE_ACTIVE] = "resizing-while-active", + [HOME_PASSWD] = "passwd", + [HOME_PASSWD_WHILE_ACTIVE] = "passwd-while-active", + [HOME_AUTHENTICATING] = "authenticating", + [HOME_AUTHENTICATING_WHILE_ACTIVE] = "authenticating-while-active", + [HOME_AUTHENTICATING_FOR_ACQUIRE] = "authenticating-for-acquire", +}; + +DEFINE_STRING_TABLE_LOOKUP(home_state, HomeState); diff --git a/src/home/homed-home.h b/src/home/homed-home.h new file mode 100644 index 0000000000..c75b06722c --- /dev/null +++ b/src/home/homed-home.h @@ -0,0 +1,168 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +typedef struct Home Home; + +#include "homed-manager.h" +#include "homed-operation.h" +#include "list.h" +#include "ordered-set.h" +#include "user-record.h" + +typedef enum HomeState { + HOME_UNFIXATED, /* home exists, but local record does not */ + HOME_ABSENT, /* local record exists, but home does not */ + HOME_INACTIVE, /* record and home exist, but is not logged in */ + HOME_FIXATING, /* generating local record from home */ + HOME_FIXATING_FOR_ACTIVATION, /* fixating in order to activate soon */ + HOME_FIXATING_FOR_ACQUIRE, /* fixating because Acquire() was called */ + HOME_ACTIVATING, + HOME_ACTIVATING_FOR_ACQUIRE, /* activating because Acquire() was called */ + HOME_DEACTIVATING, + HOME_ACTIVE, /* logged in right now */ + HOME_LOCKING, + HOME_LOCKED, + HOME_UNLOCKING, + HOME_UNLOCKING_FOR_ACQUIRE, /* unlocking because Acquire() was called */ + HOME_CREATING, + HOME_REMOVING, + HOME_UPDATING, + HOME_UPDATING_WHILE_ACTIVE, + HOME_RESIZING, + HOME_RESIZING_WHILE_ACTIVE, + HOME_PASSWD, + HOME_PASSWD_WHILE_ACTIVE, + HOME_AUTHENTICATING, + HOME_AUTHENTICATING_WHILE_ACTIVE, + HOME_AUTHENTICATING_FOR_ACQUIRE, /* authenticating because Acquire() was called */ + _HOME_STATE_MAX, + _HOME_STATE_INVALID = -1 +} HomeState; + +static inline bool HOME_STATE_IS_ACTIVE(HomeState state) { + return IN_SET(state, + HOME_ACTIVE, + HOME_UPDATING_WHILE_ACTIVE, + HOME_RESIZING_WHILE_ACTIVE, + HOME_PASSWD_WHILE_ACTIVE, + HOME_AUTHENTICATING_WHILE_ACTIVE, + HOME_AUTHENTICATING_FOR_ACQUIRE); +} + +static inline bool HOME_STATE_IS_EXECUTING_OPERATION(HomeState state) { + return IN_SET(state, + HOME_FIXATING, + HOME_FIXATING_FOR_ACTIVATION, + HOME_FIXATING_FOR_ACQUIRE, + HOME_ACTIVATING, + HOME_ACTIVATING_FOR_ACQUIRE, + HOME_DEACTIVATING, + HOME_LOCKING, + HOME_UNLOCKING, + HOME_UNLOCKING_FOR_ACQUIRE, + HOME_CREATING, + HOME_REMOVING, + HOME_UPDATING, + HOME_UPDATING_WHILE_ACTIVE, + HOME_RESIZING, + HOME_RESIZING_WHILE_ACTIVE, + HOME_PASSWD, + HOME_PASSWD_WHILE_ACTIVE, + HOME_AUTHENTICATING, + HOME_AUTHENTICATING_WHILE_ACTIVE, + HOME_AUTHENTICATING_FOR_ACQUIRE); +} + +struct Home { + Manager *manager; + char *user_name; + uid_t uid; + + char *sysfs; /* When found via plugged in device, the sysfs path to it */ + + /* Note that the 'state' field is only set to a state while we are doing something (i.e. activating, + * deactivating, creating, removing, and such), or when the home is an "unfixated" one. When we are + * done with an operation we invalidate the state. This is hint for home_get_state() to check the + * state on request as needed from the mount table and similar.*/ + HomeState state; + int signed_locally; /* signed only by us */ + + UserRecord *record; + + pid_t worker_pid; + int worker_stdout_fd; + sd_event_source *worker_event_source; + int worker_error_code; + + /* The message we are currently processing, and thus need to reply to on completion */ + Operation *current_operation; + + /* Stores the raw, plaintext passwords, but only for short periods of time */ + UserRecord *secret; + + /* When we create a home and that fails, we should possibly unregister the record altogether + * again, which is remembered in this boolean. */ + bool unregister_on_failure; + + /* The reading side of a FIFO stored in /run/systemd/home/, the writing side being used for reference + * counting. The references dropped to zero as soon as we see EOF. This concept exists twice: once + * for clients that are fine if we suspend the home directory on system suspend, and once for cliets + * that are not ok with that. This allows us to determine for each home whether there are any clients + * that support unsuspend. */ + sd_event_source *ref_event_source_please_suspend; + sd_event_source *ref_event_source_dont_suspend; + + /* Any pending operations we still need to execute. These are for operations we want to queue if we + * can't execute them right-away. */ + OrderedSet *pending_operations; + + /* A defer event source that processes pending acquire/release/eof events. We have a common + * dispatcher that processes all three kinds of events. */ + sd_event_source *pending_event_source; + + /* Did we send out a D-Bus notification about this entry? */ + bool announced; + + /* Used to coalesce bus PropertiesChanged events */ + sd_event_source *deferred_change_event_source; +}; + +int home_new(Manager *m, UserRecord *hr, const char *sysfs, Home **ret); +Home *home_free(Home *h); + +DEFINE_TRIVIAL_CLEANUP_FUNC(Home*, home_free); + +int home_set_record(Home *h, UserRecord *hr); +int home_save_record(Home *h); +int home_unlink_record(Home *h); + +int home_fixate(Home *h, UserRecord *secret, sd_bus_error *error); +int home_activate(Home *h, UserRecord *secret, sd_bus_error *error); +int home_authenticate(Home *h, UserRecord *secret, sd_bus_error *error); +int home_deactivate(Home *h, bool force, sd_bus_error *error); +int home_create(Home *h, UserRecord *secret, sd_bus_error *error); +int home_remove(Home *h, sd_bus_error *error); +int home_update(Home *h, UserRecord *new_record, sd_bus_error *error); +int home_resize(Home *h, uint64_t disk_size, UserRecord *secret, sd_bus_error *error); +int home_passwd(Home *h, UserRecord *new_secret, UserRecord *old_secret, sd_bus_error *error); +int home_unregister(Home *h, sd_bus_error *error); +int home_lock(Home *h, sd_bus_error *error); +int home_unlock(Home *h, UserRecord *secret, sd_bus_error *error); + +HomeState home_get_state(Home *h); + +void home_process_notify(Home *h, char **l); + +int home_killall(Home *h); + +int home_augment_status(Home *h, UserRecordLoadFlags flags, UserRecord **ret); + +int home_create_fifo(Home *h, bool please_suspend); +int home_schedule_operation(Home *h, Operation *o, sd_bus_error *error); + +int home_auto_login(Home *h, char ***ret_seats); + +int home_set_current_message(Home *h, sd_bus_message *m); + +const char *home_state_to_string(HomeState state); +HomeState home_state_from_string(const char *s); diff --git a/src/home/homed-manager-bus.c b/src/home/homed-manager-bus.c new file mode 100644 index 0000000000..e1d6a996ec --- /dev/null +++ b/src/home/homed-manager-bus.c @@ -0,0 +1,690 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include + +#include "alloc-util.h" +#include "bus-common-errors.h" +#include "bus-polkit.h" +#include "format-util.h" +#include "homed-bus.h" +#include "homed-home-bus.h" +#include "homed-manager-bus.h" +#include "homed-manager.h" +#include "strv.h" +#include "user-record-sign.h" +#include "user-record-util.h" +#include "user-util.h" + +static int property_get_auto_login( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error) { + + Manager *m = userdata; + Iterator i; + Home *h; + int r; + + assert(bus); + assert(reply); + assert(m); + + r = sd_bus_message_open_container(reply, 'a', "(sso)"); + if (r < 0) + return r; + + HASHMAP_FOREACH(h, m->homes_by_name, i) { + _cleanup_(strv_freep) char **seats = NULL; + _cleanup_free_ char *home_path = NULL; + char **s; + + r = home_auto_login(h, &seats); + if (r < 0) { + log_debug_errno(r, "Failed to determine whether home '%s' is candidate for auto-login, ignoring: %m", h->user_name); + continue; + } + if (!r) + continue; + + r = bus_home_path(h, &home_path); + if (r < 0) + return log_error_errno(r, "Failed to generate home bus path: %m"); + + STRV_FOREACH(s, seats) { + r = sd_bus_message_append(reply, "(sso)", h->user_name, *s, home_path); + if (r < 0) + return r; + } + } + + return sd_bus_message_close_container(reply); +} + +static int method_get_home_by_name( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + _cleanup_free_ char *path = NULL; + const char *user_name; + Manager *m = userdata; + Home *h; + int r; + + assert(message); + assert(m); + + r = sd_bus_message_read(message, "s", &user_name); + if (r < 0) + return r; + if (!valid_user_group_name(user_name)) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "User name %s is not valid", user_name); + + h = hashmap_get(m->homes_by_name, user_name); + if (!h) + return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_HOME, "No home for user %s known", user_name); + + r = bus_home_path(h, &path); + if (r < 0) + return r; + + return sd_bus_reply_method_return( + message, "usussso", + (uint32_t) h->uid, + home_state_to_string(home_get_state(h)), + h->record ? (uint32_t) user_record_gid(h->record) : GID_INVALID, + h->record ? user_record_real_name(h->record) : NULL, + h->record ? user_record_home_directory(h->record) : NULL, + h->record ? user_record_shell(h->record) : NULL, + path); +} + +static int method_get_home_by_uid( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + _cleanup_free_ char *path = NULL; + Manager *m = userdata; + uint32_t uid; + int r; + Home *h; + + assert(message); + assert(m); + + r = sd_bus_message_read(message, "u", &uid); + if (r < 0) + return r; + if (!uid_is_valid(uid)) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "UID " UID_FMT " is not valid", uid); + + h = hashmap_get(m->homes_by_uid, UID_TO_PTR(uid)); + if (!h) + return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_HOME, "No home for UID " UID_FMT " known", uid); + + /* Note that we don't use bus_home_path() here, but build the path manually, since if we are queried + * for a UID we should also generate the bus path with a UID, and bus_home_path() uses our more + * typical bus path by name. */ + if (asprintf(&path, "/org/freedesktop/home1/home/" UID_FMT, h->uid) < 0) + return -ENOMEM; + + return sd_bus_reply_method_return( + message, "ssussso", + h->user_name, + home_state_to_string(home_get_state(h)), + h->record ? (uint32_t) user_record_gid(h->record) : GID_INVALID, + h->record ? user_record_real_name(h->record) : NULL, + h->record ? user_record_home_directory(h->record) : NULL, + h->record ? user_record_shell(h->record) : NULL, + path); +} + +static int method_list_homes( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + Manager *m = userdata; + Iterator i; + Home *h; + int r; + + assert(message); + assert(m); + + r = sd_bus_message_new_method_return(message, &reply); + if (r < 0) + return r; + + r = sd_bus_message_open_container(reply, 'a', "(susussso)"); + if (r < 0) + return r; + + HASHMAP_FOREACH(h, m->homes_by_uid, i) { + _cleanup_free_ char *path = NULL; + + r = bus_home_path(h, &path); + if (r < 0) + return r; + + r = sd_bus_message_append( + reply, "(susussso)", + h->user_name, + (uint32_t) h->uid, + home_state_to_string(home_get_state(h)), + h->record ? (uint32_t) user_record_gid(h->record) : GID_INVALID, + h->record ? user_record_real_name(h->record) : NULL, + h->record ? user_record_home_directory(h->record) : NULL, + h->record ? user_record_shell(h->record) : NULL, + path); + if (r < 0) + return r; + } + + r = sd_bus_message_close_container(reply); + if (r < 0) + return r; + + return sd_bus_send(NULL, reply, NULL); +} + +static int method_get_user_record_by_name( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + _cleanup_free_ char *json = NULL, *path = NULL; + Manager *m = userdata; + const char *user_name; + bool incomplete; + Home *h; + int r; + + assert(message); + assert(m); + + r = sd_bus_message_read(message, "s", &user_name); + if (r < 0) + return r; + if (!valid_user_group_name(user_name)) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "User name %s is not valid", user_name); + + h = hashmap_get(m->homes_by_name, user_name); + if (!h) + return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_HOME, "No home for user %s known", user_name); + + r = bus_home_get_record_json(h, message, &json, &incomplete); + if (r < 0) + return r; + + r = bus_home_path(h, &path); + if (r < 0) + return r; + + return sd_bus_reply_method_return( + message, "sbo", + json, + incomplete, + path); +} + +static int method_get_user_record_by_uid( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + _cleanup_free_ char *json = NULL, *path = NULL; + Manager *m = userdata; + bool incomplete; + uint32_t uid; + Home *h; + int r; + + assert(message); + assert(m); + + r = sd_bus_message_read(message, "u", &uid); + if (r < 0) + return r; + if (!uid_is_valid(uid)) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "UID " UID_FMT " is not valid", uid); + + h = hashmap_get(m->homes_by_uid, UID_TO_PTR(uid)); + if (!h) + return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_HOME, "No home for UID " UID_FMT " known", uid); + + r = bus_home_get_record_json(h, message, &json, &incomplete); + if (r < 0) + return r; + + if (asprintf(&path, "/org/freedesktop/home1/home/" UID_FMT, h->uid) < 0) + return -ENOMEM; + + return sd_bus_reply_method_return( + message, "sbo", + json, + incomplete, + path); +} + +static int generic_home_method( + Manager *m, + sd_bus_message *message, + sd_bus_message_handler_t handler, + sd_bus_error *error) { + + const char *user_name; + Home *h; + int r; + + r = sd_bus_message_read(message, "s", &user_name); + if (r < 0) + return r; + + if (!valid_user_group_name(user_name)) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "User name %s is not valid", user_name); + + h = hashmap_get(m->homes_by_name, user_name); + if (!h) + return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_HOME, "No home for user %s known", user_name); + + return handler(message, h, error); +} + +static int method_activate_home(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return generic_home_method(userdata, message, bus_home_method_activate, error); +} + +static int method_deactivate_home(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return generic_home_method(userdata, message, bus_home_method_deactivate, error); +} + +static int validate_and_allocate_home(Manager *m, UserRecord *hr, Home **ret, sd_bus_error *error) { + _cleanup_(user_record_unrefp) UserRecord *signed_hr = NULL; + struct passwd *pw; + struct group *gr; + bool signed_locally; + Home *other; + int r; + + assert(m); + assert(hr); + assert(ret); + + r = user_record_is_supported(hr, error); + if (r < 0) + return r; + + other = hashmap_get(m->homes_by_name, hr->user_name); + if (other) + return sd_bus_error_setf(error, BUS_ERROR_USER_NAME_EXISTS, "Specified user name %s exists already, refusing.", hr->user_name); + + pw = getpwnam(hr->user_name); + if (pw) + return sd_bus_error_setf(error, BUS_ERROR_USER_NAME_EXISTS, "Specified user name %s exists in the NSS user database, refusing.", hr->user_name); + + gr = getgrnam(hr->user_name); + if (gr) + return sd_bus_error_setf(error, BUS_ERROR_USER_NAME_EXISTS, "Specified user name %s conflicts with an NSS group by the same name, refusing.", hr->user_name); + + r = manager_verify_user_record(m, hr); + switch (r) { + + case USER_RECORD_UNSIGNED: + /* If the record is unsigned, then let's sign it with our own key */ + r = manager_sign_user_record(m, hr, &signed_hr, error); + if (r < 0) + return r; + + hr = signed_hr; + _fallthrough_; + + case USER_RECORD_SIGNED_EXCLUSIVE: + signed_locally = true; + break; + + case USER_RECORD_SIGNED: + case USER_RECORD_FOREIGN: + signed_locally = false; + break; + + case -ENOKEY: + return sd_bus_error_setf(error, BUS_ERROR_BAD_SIGNATURE, "Specified user record for %s is signed by a key we don't recognize, refusing.", hr->user_name); + + default: + return sd_bus_error_set_errnof(error, r, "Failed to validate signature for '%s': %m", hr->user_name); + } + + if (uid_is_valid(hr->uid)) { + other = hashmap_get(m->homes_by_uid, UID_TO_PTR(hr->uid)); + if (other) + return sd_bus_error_setf(error, BUS_ERROR_UID_IN_USE, "Specified UID " UID_FMT " already in use by home %s, refusing.", hr->uid, other->user_name); + + pw = getpwuid(hr->uid); + if (pw) + return sd_bus_error_setf(error, BUS_ERROR_UID_IN_USE, "Specified UID " UID_FMT " already in use by NSS user %s, refusing.", hr->uid, pw->pw_name); + + gr = getgrgid(hr->uid); + if (gr) + return sd_bus_error_setf(error, BUS_ERROR_UID_IN_USE, "Specified UID " UID_FMT " already in use as GID by NSS group %s, refusing.", hr->uid, gr->gr_name); + } else { + r = manager_augment_record_with_uid(m, hr); + if (r < 0) + return sd_bus_error_set_errnof(error, r, "Failed to acquire UID for '%s': %m", hr->user_name); + } + + r = home_new(m, hr, NULL, ret); + if (r < 0) + return r; + + (*ret)->signed_locally = signed_locally; + return r; +} + +static int method_register_home( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + _cleanup_(user_record_unrefp) UserRecord *hr = NULL; + Manager *m = userdata; + Home *h; + int r; + + assert(message); + assert(m); + + r = bus_message_read_home_record(message, USER_RECORD_LOAD_EMBEDDED, &hr, error); + if (r < 0) + return r; + + r = bus_verify_polkit_async( + message, + CAP_SYS_ADMIN, + "org.freedesktop.home1.create-home", + NULL, + true, + UID_INVALID, + &m->polkit_registry, + error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Will call us back */ + + r = validate_and_allocate_home(m, hr, &h, error); + if (r < 0) + return r; + + r = home_save_record(h); + if (r < 0) { + home_free(h); + return r; + } + + return sd_bus_reply_method_return(message, NULL); +} + +static int method_unregister_home(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return generic_home_method(userdata, message, bus_home_method_unregister, error); +} + +static int method_create_home( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + _cleanup_(user_record_unrefp) UserRecord *hr = NULL; + Manager *m = userdata; + Home *h; + int r; + + assert(message); + assert(m); + + r = bus_message_read_home_record(message, USER_RECORD_REQUIRE_REGULAR|USER_RECORD_ALLOW_SECRET|USER_RECORD_ALLOW_PRIVILEGED|USER_RECORD_ALLOW_PER_MACHINE|USER_RECORD_ALLOW_SIGNATURE, &hr, error); + if (r < 0) + return r; + + r = bus_verify_polkit_async( + message, + CAP_SYS_ADMIN, + "org.freedesktop.home1.create-home", + NULL, + true, + UID_INVALID, + &m->polkit_registry, + error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Will call us back */ + + r = validate_and_allocate_home(m, hr, &h, error); + if (r < 0) + return r; + + r = home_create(h, hr, error); + if (r < 0) + goto fail; + + assert(r == 0); + h->unregister_on_failure = true; + assert(!h->current_operation); + + r = home_set_current_message(h, message); + if (r < 0) + return r; + + return 1; + +fail: + (void) home_unlink_record(h); + h = home_free(h); + return r; +} + +static int method_realize_home(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return generic_home_method(userdata, message, bus_home_method_realize, error); +} + +static int method_remove_home(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return generic_home_method(userdata, message, bus_home_method_remove, error); +} + +static int method_fixate_home(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return generic_home_method(userdata, message, bus_home_method_fixate, error); +} + +static int method_authenticate_home(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return generic_home_method(userdata, message, bus_home_method_authenticate, error); +} + +static int method_update_home(sd_bus_message *message, void *userdata, sd_bus_error *error) { + _cleanup_(user_record_unrefp) UserRecord *hr = NULL; + Manager *m = userdata; + Home *h; + int r; + + assert(message); + assert(m); + + r = bus_message_read_home_record(message, USER_RECORD_REQUIRE_REGULAR|USER_RECORD_ALLOW_SECRET|USER_RECORD_ALLOW_PRIVILEGED|USER_RECORD_ALLOW_PER_MACHINE|USER_RECORD_ALLOW_SIGNATURE, &hr, error); + if (r < 0) + return r; + + assert(hr->user_name); + + h = hashmap_get(m->homes_by_name, hr->user_name); + if (!h) + return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_HOME, "No home for user %s known", hr->user_name); + + return bus_home_method_update_record(h, message, hr, error); +} + +static int method_resize_home(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return generic_home_method(userdata, message, bus_home_method_resize, error); +} + +static int method_change_password_home(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return generic_home_method(userdata, message, bus_home_method_change_password, error); +} + +static int method_lock_home(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return generic_home_method(userdata, message, bus_home_method_lock, error); +} + +static int method_unlock_home(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return generic_home_method(userdata, message, bus_home_method_unlock, error); +} + +static int method_acquire_home(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return generic_home_method(userdata, message, bus_home_method_acquire, error); +} + +static int method_ref_home(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return generic_home_method(userdata, message, bus_home_method_ref, error); +} + +static int method_release_home(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return generic_home_method(userdata, message, bus_home_method_release, error); +} + +static int method_lock_all_homes(sd_bus_message *message, void *userdata, sd_bus_error *error) { + _cleanup_(operation_unrefp) Operation *o = NULL; + bool waiting = false; + Manager *m = userdata; + Iterator i; + Home *h; + int r; + + assert(m); + + /* This is called from logind when we are preparing for system suspend. We enqueue a lock operation + * for every suitable home we have and only when all of them completed we send a reply indicating + * completion. */ + + HASHMAP_FOREACH(h, m->homes_by_name, i) { + + /* Automatically suspend all homes that have at least one client referencing it that asked + * for "please suspend", and no client that asked for "please do not suspend". */ + if (h->ref_event_source_dont_suspend || + !h->ref_event_source_please_suspend) + continue; + + if (!o) { + o = operation_new(OPERATION_LOCK_ALL, message); + if (!o) + return -ENOMEM; + } + + log_info("Automatically locking of home of user %s.", h->user_name); + + r = home_schedule_operation(h, o, error); + if (r < 0) + return r; + + waiting = true; + } + + if (waiting) /* At least one lock operation was enqeued, let's leave here without a reply: it will + * be sent as soon as the last of the lock operations completed. */ + return 1; + + return sd_bus_reply_method_return(message, NULL); +} + +const sd_bus_vtable manager_vtable[] = { + SD_BUS_VTABLE_START(0), + + SD_BUS_PROPERTY("AutoLogin", "a(sso)", property_get_auto_login, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), + + SD_BUS_METHOD("GetHomeByName", "s", "usussso", method_get_home_by_name, SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD("GetHomeByUID", "u", "ssussso", method_get_home_by_uid, SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD("GetUserRecordByName", "s", "sbo", method_get_user_record_by_name, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE), + SD_BUS_METHOD("GetUserRecordByUID", "u", "sbo", method_get_user_record_by_uid, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE), + SD_BUS_METHOD("ListHomes", NULL, "a(susussso)", method_list_homes, SD_BUS_VTABLE_UNPRIVILEGED), + + /* The following methods directly execute an operation on a home, without ref-counting, queing or + * anything, and are accessible through homectl. */ + SD_BUS_METHOD("ActivateHome", "ss", NULL, method_activate_home, SD_BUS_VTABLE_SENSITIVE), + SD_BUS_METHOD("DeactivateHome", "s", NULL, method_deactivate_home, 0), + SD_BUS_METHOD("RegisterHome", "s", NULL, method_register_home, SD_BUS_VTABLE_UNPRIVILEGED), /* Add JSON record to homed, but don't create actual $HOME */ + SD_BUS_METHOD("UnregisterHome", "s", NULL, method_unregister_home, SD_BUS_VTABLE_UNPRIVILEGED), /* Remove JSON record from homed, but don't remove actual $HOME */ + SD_BUS_METHOD("CreateHome", "s", NULL, method_create_home, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE), /* Add JSON record, and create $HOME for it */ + SD_BUS_METHOD("RealizeHome", "ss", NULL, method_realize_home, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE), /* Create $HOME for already registered JSON entry */ + SD_BUS_METHOD("RemoveHome", "s", NULL, method_remove_home, SD_BUS_VTABLE_UNPRIVILEGED), /* Remove JSON record and remove $HOME */ + SD_BUS_METHOD("FixateHome", "ss", NULL, method_fixate_home, SD_BUS_VTABLE_SENSITIVE), /* Investigate $HOME and propagate contained JSON record into our database */ + SD_BUS_METHOD("AuthenticateHome", "ss", NULL, method_authenticate_home, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE), /* Just check credentials */ + SD_BUS_METHOD("UpdateHome", "s", NULL, method_update_home, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE), /* Update JSON record of existing user */ + SD_BUS_METHOD("ResizeHome", "sts", NULL, method_resize_home, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE), + SD_BUS_METHOD("ChangePasswordHome", "sss", NULL, method_change_password_home, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE), + SD_BUS_METHOD("LockHome", "s", NULL, method_lock_home, 0), /* Prepare active home for system suspend: flush out passwords, suspend access */ + SD_BUS_METHOD("UnlockHome", "ss", NULL, method_unlock_home, SD_BUS_VTABLE_SENSITIVE), /* Make $HOME usable after system resume again */ + + /* The following methods implement ref-counted activation, and are what the PAM module calls (and + * what "homectl with" runs). In contrast to the methods above which fail if an operation is already + * being executed on a home directory, these ones will queue the request, and are thus more + * reliable. Moreover, they are a bit smarter: AcquireHome() will fixate, activate, unlock, or + * authenticate depending on the state of the home, so that the end result is always the same + * (i.e. the home directory is accessible), and we always validate the specified passwords. RefHome() + * will not authenticate, and thus only works if home is already active. */ + SD_BUS_METHOD("AcquireHome", "ssb", "h", method_acquire_home, SD_BUS_VTABLE_SENSITIVE), + SD_BUS_METHOD("RefHome", "sb", "h", method_ref_home, 0), + SD_BUS_METHOD("ReleaseHome", "s", NULL, method_release_home, 0), + + /* An operation that acts on all homes that allow it */ + SD_BUS_METHOD("LockAllHomes", NULL, NULL, method_lock_all_homes, 0), + + SD_BUS_VTABLE_END +}; + +static int on_deferred_auto_login(sd_event_source *s, void *userdata) { + Manager *m = userdata; + int r; + + assert(m); + + m->deferred_auto_login_event_source = sd_event_source_unref(m->deferred_auto_login_event_source); + + r = sd_bus_emit_properties_changed( + m->bus, + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "AutoLogin", NULL); + if (r < 0) + log_warning_errno(r, "Failed to send AutoLogin property change event, ignoring: %m"); + + return 0; +} + +int bus_manager_emit_auto_login_changed(Manager *m) { + int r; + assert(m); + + if (m->deferred_auto_login_event_source) + return 0; + + if (!m->event) + return 0; + + if (IN_SET(sd_event_get_state(m->event), SD_EVENT_FINISHED, SD_EVENT_EXITING)) + return 0; + + r = sd_event_add_defer(m->event, &m->deferred_auto_login_event_source, on_deferred_auto_login, m); + if (r < 0) + return log_error_errno(r, "Failed to allocate auto login event source: %m"); + + r = sd_event_source_set_priority(m->deferred_auto_login_event_source, SD_EVENT_PRIORITY_IDLE+10); + if (r < 0) + log_warning_errno(r, "Failed to tweak priority of event source, ignoring: %m"); + + (void) sd_event_source_set_description(m->deferred_auto_login_event_source, "deferred-auto-login"); + return 1; +} diff --git a/src/home/homed-manager-bus.h b/src/home/homed-manager-bus.h new file mode 100644 index 0000000000..40e1cc3d86 --- /dev/null +++ b/src/home/homed-manager-bus.h @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include "sd-bus.h" + +extern const sd_bus_vtable manager_vtable[]; diff --git a/src/home/homed-manager.c b/src/home/homed-manager.c new file mode 100644 index 0000000000..f4fec0e7c9 --- /dev/null +++ b/src/home/homed-manager.c @@ -0,0 +1,1672 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "btrfs-util.h" +#include "bus-common-errors.h" +#include "bus-error.h" +#include "bus-polkit.h" +#include "clean-ipc.h" +#include "conf-files.h" +#include "device-util.h" +#include "dirent-util.h" +#include "fd-util.h" +#include "fileio.h" +#include "format-util.h" +#include "fs-util.h" +#include "gpt.h" +#include "home-util.h" +#include "homed-home-bus.h" +#include "homed-home.h" +#include "homed-manager-bus.h" +#include "homed-manager.h" +#include "homed-varlink.h" +#include "io-util.h" +#include "mkdir.h" +#include "process-util.h" +#include "quota-util.h" +#include "random-util.h" +#include "socket-util.h" +#include "stat-util.h" +#include "strv.h" +#include "tmpfile-util.h" +#include "udev-util.h" +#include "user-record-sign.h" +#include "user-record-util.h" +#include "user-record.h" +#include "user-util.h" + +/* Where to look for private/public keys that are used to sign the user records. We are not using + * CONF_PATHS_NULSTR() here since we want to insert /var/lib/systemd/home/ in the middle. And we insert that + * since we want to auto-generate a persistent private/public key pair if we need to. */ +#define KEY_PATHS_NULSTR \ + "/etc/systemd/home/\0" \ + "/run/systemd/home/\0" \ + "/var/lib/systemd/home/\0" \ + "/usr/local/lib/systemd/home/\0" \ + "/usr/lib/systemd/home/\0" + +static bool uid_is_home(uid_t uid) { + return uid >= HOME_UID_MIN && uid <= HOME_UID_MAX; +} +/* Takes a value generated randomly or by hashing and turns it into a UID in the right range */ + +#define UID_CLAMP_INTO_HOME_RANGE(rnd) (((uid_t) (rnd) % (HOME_UID_MAX - HOME_UID_MIN + 1)) + HOME_UID_MIN) + +DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(homes_by_uid_hash_ops, void, trivial_hash_func, trivial_compare_func, Home, home_free); +DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(homes_by_name_hash_ops, char, string_hash_func, string_compare_func, Home, home_free); +DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(homes_by_worker_pid_hash_ops, void, trivial_hash_func, trivial_compare_func, Home, home_free); +DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(homes_by_sysfs_hash_ops, char, path_hash_func, path_compare, Home, home_free); + +static int on_home_inotify(sd_event_source *s, const struct inotify_event *event, void *userdata); +static int manager_gc_images(Manager *m); +static int manager_enumerate_images(Manager *m); +static int manager_assess_image(Manager *m, int dir_fd, const char *dir_path, const char *dentry_name); +static void manager_revalidate_image(Manager *m, Home *h); + +static void manager_watch_home(Manager *m) { + struct statfs sfs; + int r; + + assert(m); + + m->inotify_event_source = sd_event_source_unref(m->inotify_event_source); + m->scan_slash_home = false; + + if (statfs("/home/", &sfs) < 0) { + log_full_errno(errno == ENOENT ? LOG_DEBUG : LOG_WARNING, errno, + "Failed to statfs() /home/ directory, disabling automatic scanning."); + return; + } + + if (is_network_fs(&sfs)) { + log_info("/home/ is a network file system, disabling automatic scanning."); + return; + } + + if (is_fs_type(&sfs, AUTOFS_SUPER_MAGIC)) { + log_info("/home/ is on autofs, disabling automatic scanning."); + return; + } + + m->scan_slash_home = true; + + r = sd_event_add_inotify(m->event, &m->inotify_event_source, "/home/", IN_CREATE|IN_CLOSE_WRITE|IN_DELETE_SELF|IN_MOVE_SELF|IN_ONLYDIR|IN_MOVED_TO|IN_MOVED_FROM|IN_DELETE, on_home_inotify, m); + if (r < 0) + log_full_errno(r == -ENOENT ? LOG_DEBUG : LOG_WARNING, r, + "Failed to create inotify watch on /home/, ignoring."); + + (void) sd_event_source_set_description(m->inotify_event_source, "home-inotify"); +} + +static int on_home_inotify(sd_event_source *s, const struct inotify_event *event, void *userdata) { + Manager *m = userdata; + const char *e, *n; + + assert(m); + assert(event); + + if ((event->mask & (IN_Q_OVERFLOW|IN_MOVE_SELF|IN_DELETE_SELF|IN_IGNORED|IN_UNMOUNT)) != 0) { + + if (FLAGS_SET(event->mask, IN_Q_OVERFLOW)) + log_debug("/home/ inotify queue overflow, rescanning."); + else if (FLAGS_SET(event->mask, IN_MOVE_SELF)) + log_info("/home/ moved or renamed, recreating watch and rescanning."); + else if (FLAGS_SET(event->mask, IN_DELETE_SELF)) + log_info("/home/ deleted, recreating watch and rescanning."); + else if (FLAGS_SET(event->mask, IN_UNMOUNT)) + log_info("/home/ unmounted, recreating watch and rescanning."); + else if (FLAGS_SET(event->mask, IN_IGNORED)) + log_info("/home/ watch invalidated, recreating watch and rescanning."); + + manager_watch_home(m); + (void) manager_gc_images(m); + (void) manager_enumerate_images(m); + (void) bus_manager_emit_auto_login_changed(m); + return 0; + } + + /* For the other inotify events, let's ignore all events for file names that don't match our + * expectations */ + if (isempty(event->name)) + return 0; + e = endswith(event->name, FLAGS_SET(event->mask, IN_ISDIR) ? ".homedir" : ".home"); + if (!e) + return 0; + + n = strndupa(event->name, e - event->name); + if (!suitable_user_name(n)) + return 0; + + if ((event->mask & (IN_CREATE|IN_CLOSE_WRITE|IN_MOVED_TO)) != 0) { + if (FLAGS_SET(event->mask, IN_CREATE)) + log_debug("/home/%s has been created, having a look.", event->name); + else if (FLAGS_SET(event->mask, IN_CLOSE_WRITE)) + log_debug("/home/%s has been modified, having a look.", event->name); + else if (FLAGS_SET(event->mask, IN_MOVED_TO)) + log_debug("/home/%s has been moved in, having a look.", event->name); + + (void) manager_assess_image(m, -1, "/home/", event->name); + (void) bus_manager_emit_auto_login_changed(m); + } + + if ((event->mask & (IN_DELETE|IN_MOVED_FROM|IN_DELETE)) != 0) { + Home *h; + + if (FLAGS_SET(event->mask, IN_DELETE)) + log_debug("/home/%s has been deleted, revalidating.", event->name); + else if (FLAGS_SET(event->mask, IN_CLOSE_WRITE)) + log_debug("/home/%s has been closed after writing, revalidating.", event->name); + else if (FLAGS_SET(event->mask, IN_MOVED_FROM)) + log_debug("/home/%s has been moved away, revalidating.", event->name); + + h = hashmap_get(m->homes_by_name, n); + if (h) { + manager_revalidate_image(m, h); + (void) bus_manager_emit_auto_login_changed(m); + } + } + + return 0; +} + +int manager_new(Manager **ret) { + _cleanup_(manager_freep) Manager *m = NULL; + int r; + + assert(ret); + + m = new0(Manager, 1); + if (!m) + return -ENOMEM; + + r = sd_event_default(&m->event); + if (r < 0) + return r; + + r = sd_event_add_signal(m->event, NULL, SIGINT, NULL, NULL); + if (r < 0) + return r; + + r = sd_event_add_signal(m->event, NULL, SIGTERM, NULL, NULL); + if (r < 0) + return r; + + (void) sd_event_set_watchdog(m->event, true); + + m->homes_by_uid = hashmap_new(&homes_by_uid_hash_ops); + if (!m->homes_by_uid) + return -ENOMEM; + + m->homes_by_name = hashmap_new(&homes_by_name_hash_ops); + if (!m->homes_by_name) + return -ENOMEM; + + m->homes_by_worker_pid = hashmap_new(&homes_by_worker_pid_hash_ops); + if (!m->homes_by_worker_pid) + return -ENOMEM; + + m->homes_by_sysfs = hashmap_new(&homes_by_sysfs_hash_ops); + if (!m->homes_by_sysfs) + return -ENOMEM; + + *ret = TAKE_PTR(m); + return 0; +} + +Manager* manager_free(Manager *m) { + assert(m); + + hashmap_free(m->homes_by_uid); + hashmap_free(m->homes_by_name); + hashmap_free(m->homes_by_worker_pid); + hashmap_free(m->homes_by_sysfs); + + m->inotify_event_source = sd_event_source_unref(m->inotify_event_source); + + bus_verify_polkit_async_registry_free(m->polkit_registry); + + sd_bus_flush_close_unref(m->bus); + sd_event_unref(m->event); + + m->notify_socket_event_source = sd_event_source_unref(m->notify_socket_event_source); + m->device_monitor = sd_device_monitor_unref(m->device_monitor); + + m->deferred_rescan_event_source = sd_event_source_unref(m->deferred_rescan_event_source); + m->deferred_gc_event_source = sd_event_source_unref(m->deferred_gc_event_source); + m->deferred_auto_login_event_source = sd_event_source_unref(m->deferred_auto_login_event_source); + + if (m->private_key) + EVP_PKEY_free(m->private_key); + + hashmap_free(m->public_keys); + + varlink_server_unref(m->varlink_server); + + return mfree(m); +} + +int manager_verify_user_record(Manager *m, UserRecord *hr) { + EVP_PKEY *pkey; + Iterator i; + int r; + + assert(m); + assert(hr); + + if (!m->private_key && hashmap_isempty(m->public_keys)) { + r = user_record_has_signature(hr); + if (r < 0) + return r; + + return r ? -ENOKEY : USER_RECORD_UNSIGNED; + } + + /* Is it our own? */ + if (m->private_key) { + r = user_record_verify(hr, m->private_key); + switch (r) { + + case USER_RECORD_FOREIGN: + /* This record is not signed by this key, but let's see below */ + break; + + case USER_RECORD_SIGNED: /* Signed by us, but also by others, let's propagate that */ + case USER_RECORD_SIGNED_EXCLUSIVE: /* Signed by us, and nothing else, ditto */ + case USER_RECORD_UNSIGNED: /* Not signed at all, ditto */ + default: + return r; + } + } + + HASHMAP_FOREACH(pkey, m->public_keys, i) { + r = user_record_verify(hr, pkey); + switch (r) { + + case USER_RECORD_FOREIGN: + /* This record is not signed by this key, but let's see our other keys */ + break; + + case USER_RECORD_SIGNED: /* It's signed by this key we are happy with, but which is not our own. */ + case USER_RECORD_SIGNED_EXCLUSIVE: + return USER_RECORD_FOREIGN; + + case USER_RECORD_UNSIGNED: /* It's not signed at all */ + default: + return r; + } + } + + return -ENOKEY; +} + +static int manager_add_home_by_record( + Manager *m, + const char *name, + int dir_fd, + const char *fname) { + + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + _cleanup_(user_record_unrefp) UserRecord *hr; + unsigned line, column; + int r, is_signed; + Home *h; + + assert(m); + assert(name); + assert(fname); + + r = json_parse_file_at(NULL, dir_fd, fname, JSON_PARSE_SENSITIVE, &v, &line, &column); + if (r < 0) + return log_error_errno(r, "Failed to parse identity record at %s:%u%u: %m", fname, line, column); + + hr = user_record_new(); + if (!hr) + return log_oom(); + + r = user_record_load(hr, v, USER_RECORD_LOAD_REFUSE_SECRET); + if (r < 0) + return r; + + if (!streq_ptr(hr->user_name, name)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Identity's user name %s does not match file name %s, refusing.", hr->user_name, name); + + is_signed = manager_verify_user_record(m, hr); + switch (is_signed) { + + case -ENOKEY: + return log_warning_errno(is_signed, "User record %s is not signed by any accepted key, ignoring.", fname); + case USER_RECORD_UNSIGNED: + return log_warning_errno(SYNTHETIC_ERRNO(EPERM), "User record %s is not signed at all, ignoring.", fname); + case USER_RECORD_SIGNED: + log_info("User record %s is signed by us (and others), accepting.", fname); + break; + case USER_RECORD_SIGNED_EXCLUSIVE: + log_info("User record %s is signed only by us, accepting.", fname); + break; + case USER_RECORD_FOREIGN: + log_info("User record %s is signed by registered key from others, accepting.", fname); + break; + default: + assert(is_signed < 0); + return log_error_errno(is_signed, "Failed to verify signature of user record in %s: %m", fname); + } + + h = hashmap_get(m->homes_by_name, name); + if (h) { + r = home_set_record(h, hr); + if (r < 0) + return log_error_errno(r, "Failed to update home record for %s: %m", name); + + /* If we acquired a record now for a previously unallocated entry, then reset the state. This + * makes sure home_get_state() will check for the availability of the image file dynamically + * in order to detect to distuingish HOME_INACTIVE and HOME_ABSENT. */ + if (h->state == HOME_UNFIXATED) + h->state = _HOME_STATE_INVALID; + } else { + r = home_new(m, hr, NULL, &h); + if (r < 0) + return log_error_errno(r, "Failed to allocate new home object: %m"); + + log_info("Added registered home for user %s.", hr->user_name); + } + + /* Only entries we exclusively signed are writable to us, hence remember the result */ + h->signed_locally = is_signed == USER_RECORD_SIGNED_EXCLUSIVE; + + return 1; +} + +static int manager_enumerate_records(Manager *m) { + _cleanup_closedir_ DIR *d = NULL; + struct dirent *de; + + assert(m); + + d = opendir("/var/lib/systemd/home/"); + if (!d) + return log_full_errno(errno == ENOENT ? LOG_DEBUG : LOG_ERR, errno, + "Failed to open /var/lib/systemd/home/: %m"); + + FOREACH_DIRENT(de, d, return log_error_errno(errno, "Failed to read record directory: %m")) { + _cleanup_free_ char *n = NULL; + const char *e; + + if (!dirent_is_file(de)) + continue; + + e = endswith(de->d_name, ".identity"); + if (!e) + continue; + + n = strndup(de->d_name, e - de->d_name); + if (!n) + return log_oom(); + + if (!suitable_user_name(n)) + continue; + + (void) manager_add_home_by_record(m, n, dirfd(d), de->d_name); + } + + return 0; +} + +static int search_quota(uid_t uid, const char *exclude_quota_path) { + struct stat exclude_st = {}; + dev_t previous_devno = 0; + const char *where; + int r; + + /* Checks whether the specified UID owns any files on the files system, but ignore any file system + * backing the specified file. The file is used when operating on home directories, where it's OK if + * the UID of them already owns files. */ + + if (exclude_quota_path && stat(exclude_quota_path, &exclude_st) < 0) { + if (errno != ENOENT) + return log_warning_errno(errno, "Failed to stat %s, ignoring: %m", exclude_quota_path); + } + + /* Check a few usual suspects where regular users might own files. Note that this is by no means + * comprehensive, but should cover most cases. Note that in an ideal world every user would be + * registered in NSS and avoid our own UID range, but for all other cases, it's a good idea to be + * paranoid and check quota if we can. */ + FOREACH_STRING(where, "/home/", "/tmp/", "/var/", "/var/mail/", "/var/tmp/", "/var/spool/") { + struct dqblk req; + struct stat st; + + if (stat(where, &st) < 0) { + log_full_errno(errno == ENOENT ? LOG_DEBUG : LOG_ERR, errno, + "Failed to stat %s, ignoring: %m", where); + continue; + } + + if (major(st.st_dev) == 0) { + log_debug("Directory %s is not on a real block device, not checking quota for UID use.", where); + continue; + } + + if (st.st_dev == exclude_st.st_dev) { /* If an exclude path is specified, then ignore quota + * reported on the same block device as that path. */ + log_debug("Directory %s is where the home directory is located, not checking quota for UID use.", where); + continue; + } + + if (st.st_dev == previous_devno) { /* Does this directory have the same devno as the previous + * one we tested? If so, there's no point in testing this + * again. */ + log_debug("Directory %s is on same device as previous tested directory, not checking quota for UID use a second time.", where); + continue; + } + + previous_devno = st.st_dev; + + r = quotactl_devno(QCMD_FIXED(Q_GETQUOTA, USRQUOTA), st.st_dev, uid, &req); + if (r < 0) { + if (ERRNO_IS_NOT_SUPPORTED(r)) + log_debug_errno(r, "No UID quota support on %s, ignoring.", where); + else + log_warning_errno(r, "Failed to query quota on %s, ignoring.", where); + + continue; + } + + if ((FLAGS_SET(req.dqb_valid, QIF_SPACE) && req.dqb_curspace > 0) || + (FLAGS_SET(req.dqb_valid, QIF_INODES) && req.dqb_curinodes > 0)) { + log_debug_errno(errno, "Quota reports UID " UID_FMT " occupies disk space on %s.", uid, where); + return 1; + } + } + + return 0; +} + +static int manager_acquire_uid( + Manager *m, + uid_t start_uid, + const char *user_name, + const char *exclude_quota_path, + uid_t *ret) { + + static const uint8_t hash_key[] = { + 0xa3, 0xb8, 0x82, 0x69, 0x9a, 0x71, 0xf7, 0xa9, + 0xe0, 0x7c, 0xf6, 0xf1, 0x21, 0x69, 0xd2, 0x1e + }; + + enum { + PHASE_SUGGESTED, + PHASE_HASHED, + PHASE_RANDOM + } phase = PHASE_SUGGESTED; + + unsigned n_tries = 100; + int r; + + assert(m); + assert(ret); + + for (;;) { + struct passwd *pw; + struct group *gr; + uid_t candidate; + Home *other; + + if (--n_tries <= 0) + return -EBUSY; + + switch (phase) { + + case PHASE_SUGGESTED: + phase = PHASE_HASHED; + + if (!uid_is_home(start_uid)) + continue; + + candidate = start_uid; + break; + + case PHASE_HASHED: + phase = PHASE_RANDOM; + + if (!user_name) + continue; + + candidate = UID_CLAMP_INTO_HOME_RANGE(siphash24(user_name, strlen(user_name), hash_key)); + break; + + case PHASE_RANDOM: + random_bytes(&candidate, sizeof(candidate)); + candidate = UID_CLAMP_INTO_HOME_RANGE(candidate); + break; + + default: + assert_not_reached("unknown phase"); + } + + other = hashmap_get(m->homes_by_uid, UID_TO_PTR(candidate)); + if (other) { + log_debug("Candidate UID " UID_FMT " already used by another home directory (%s), let's try another.", candidate, other->user_name); + continue; + } + + pw = getpwuid(candidate); + if (pw) { + log_debug("Candidate UID " UID_FMT " already registered by another user in NSS (%s), let's try another.", candidate, pw->pw_name); + continue; + } + + gr = getgrgid((gid_t) candidate); + if (gr) { + log_debug("Candidate UID " UID_FMT " already registered by another group in NSS (%s), let's try another.", candidate, gr->gr_name); + continue; + } + + r = search_ipc(candidate, (gid_t) candidate); + if (r < 0) + continue; + if (r > 0) { + log_debug_errno(r, "Candidate UID " UID_FMT " already owns IPC objects, let's try another: %m", candidate); + continue; + } + + r = search_quota(candidate, exclude_quota_path); + if (r != 0) + continue; + + *ret = candidate; + return 0; + } +} + +static int manager_add_home_by_image( + Manager *m, + const char *user_name, + const char *realm, + const char *image_path, + const char *sysfs, + UserStorage storage, + uid_t start_uid) { + + _cleanup_(user_record_unrefp) UserRecord *hr = NULL; + uid_t uid; + Home *h; + int r; + + assert(m); + + assert(m); + assert(user_name); + assert(image_path); + assert(storage >= 0); + assert(storage < _USER_STORAGE_MAX); + + h = hashmap_get(m->homes_by_name, user_name); + if (h) { + bool same; + + if (h->state != HOME_UNFIXATED) { + log_debug("Found an image for user %s which already has a record, skipping.", user_name); + return 0; /* ignore images that synthesize a user we already have a record for */ + } + + same = user_record_storage(h->record) == storage; + if (same) { + if (h->sysfs && sysfs) + same = path_equal(h->sysfs, sysfs); + else if (!!h->sysfs != !!sysfs) + same = false; + else { + const char *p; + + p = user_record_image_path(h->record); + same = p && path_equal(p, image_path); + } + } + + if (!same) { + log_debug("Found a multiple images for a user '%s', ignoring image '%s'.", user_name, image_path); + return 0; + } + } else { + /* Check NSS, in case there's another user or group by this name */ + if (getpwnam(user_name) || getgrnam(user_name)) { + log_debug("Found an existing user or group by name '%s', ignoring image '%s'.", user_name, image_path); + return 0; + } + } + + if (h && uid_is_valid(h->uid)) + uid = h->uid; + else { + r = manager_acquire_uid(m, start_uid, user_name, IN_SET(storage, USER_SUBVOLUME, USER_DIRECTORY, USER_FSCRYPT) ? image_path : NULL, &uid); + if (r < 0) + return log_warning_errno(r, "Failed to acquire unused UID for %s: %m", user_name); + } + + hr = user_record_new(); + if (!hr) + return log_oom(); + + r = user_record_synthesize(hr, user_name, realm, image_path, storage, uid, (gid_t) uid); + if (r < 0) + return log_error_errno(r, "Failed to synthesize home record for %s (image %s): %m", user_name, image_path); + + if (h) { + r = home_set_record(h, hr); + if (r < 0) + return log_error_errno(r, "Failed to update home record for %s: %m", user_name); + } else { + r = home_new(m, hr, sysfs, &h); + if (r < 0) + return log_error_errno(r, "Failed to allocate new home object: %m"); + + h->state = HOME_UNFIXATED; + + log_info("Discovered new home for user %s through image %s.", user_name, image_path); + } + + return 1; +} + +int manager_augment_record_with_uid( + Manager *m, + UserRecord *hr) { + + const char *exclude_quota_path = NULL; + uid_t start_uid = UID_INVALID, uid; + int r; + + assert(m); + assert(hr); + + if (uid_is_valid(hr->uid)) + return 0; + + if (IN_SET(hr->storage, USER_CLASSIC, USER_SUBVOLUME, USER_DIRECTORY, USER_FSCRYPT)) { + const char * ip; + + ip = user_record_image_path(hr); + if (ip) { + struct stat st; + + if (stat(ip, &st) < 0) { + if (errno != ENOENT) + log_warning_errno(errno, "Failed to stat(%s): %m", ip); + } else if (uid_is_home(st.st_uid)) { + start_uid = st.st_uid; + exclude_quota_path = ip; + } + } + } + + r = manager_acquire_uid(m, start_uid, hr->user_name, exclude_quota_path, &uid); + if (r < 0) + return r; + + log_debug("Acquired new UID " UID_FMT " for %s.", uid, hr->user_name); + + r = user_record_add_binding( + hr, + _USER_STORAGE_INVALID, + NULL, + SD_ID128_NULL, + SD_ID128_NULL, + SD_ID128_NULL, + NULL, + NULL, + UINT64_MAX, + NULL, + NULL, + uid, + (gid_t) uid); + if (r < 0) + return r; + + return 1; +} + +static int manager_assess_image( + Manager *m, + int dir_fd, + const char *dir_path, + const char *dentry_name) { + + char *luks_suffix, *directory_suffix; + _cleanup_free_ char *path = NULL; + struct stat st; + int r; + + assert(m); + assert(dir_path); + assert(dentry_name); + + luks_suffix = endswith(dentry_name, ".home"); + if (luks_suffix) + directory_suffix = NULL; + else + directory_suffix = endswith(dentry_name, ".homedir"); + + /* Early filter out: by name */ + if (!luks_suffix && !directory_suffix) + return 0; + + path = path_join(dir_path, dentry_name); + if (!path) + return log_oom(); + + /* Follow symlinks here, to allow people to link in stuff to make them available locally. */ + if (dir_fd >= 0) + r = fstatat(dir_fd, dentry_name, &st, 0); + else + r = stat(path, &st); + if (r < 0) + return log_full_errno(errno == ENOENT ? LOG_DEBUG : LOG_WARNING, errno, + "Failed to stat directory entry '%s', ignoring: %m", dentry_name); + + if (S_ISREG(st.st_mode)) { + _cleanup_free_ char *n = NULL, *user_name = NULL, *realm = NULL; + + if (!luks_suffix) + return 0; + + n = strndup(dentry_name, luks_suffix - dentry_name); + if (!n) + return log_oom(); + + r = split_user_name_realm(n, &user_name, &realm); + if (r == -EINVAL) /* Not the right format: ignore */ + return 0; + if (r < 0) + return log_error_errno(r, "Failed to split image name into user name/realm: %m"); + + return manager_add_home_by_image(m, user_name, realm, path, NULL, USER_LUKS, UID_INVALID); + } + + if (S_ISDIR(st.st_mode)) { + _cleanup_free_ char *n = NULL, *user_name = NULL, *realm = NULL; + _cleanup_close_ int fd = -1; + UserStorage storage; + + if (!directory_suffix) + return 0; + + n = strndup(dentry_name, directory_suffix - dentry_name); + if (!n) + return log_oom(); + + r = split_user_name_realm(n, &user_name, &realm); + if (r == -EINVAL) /* Not the right format: ignore */ + return 0; + if (r < 0) + return log_error_errno(r, "Failed to split image name into user name/realm: %m"); + + if (dir_fd >= 0) + fd = openat(dir_fd, dentry_name, O_DIRECTORY|O_RDONLY|O_CLOEXEC); + else + fd = open(path, O_DIRECTORY|O_RDONLY|O_CLOEXEC); + if (fd < 0) + return log_full_errno(errno == ENOENT ? LOG_DEBUG : LOG_WARNING, errno, + "Failed to open directory '%s', ignoring: %m", path); + + if (fstat(fd, &st) < 0) + return log_warning_errno(errno, "Failed to fstat() %s, ignoring: %m", path); + + assert(S_ISDIR(st.st_mode)); /* Must hold, we used O_DIRECTORY above */ + + r = btrfs_is_subvol_fd(fd); + if (r < 0) + return log_warning_errno(errno, "Failed to determine whether %s is a btrfs subvolume: %m", path); + if (r > 0) + storage = USER_SUBVOLUME; + else { + struct fscrypt_policy policy; + + if (ioctl(fd, FS_IOC_GET_ENCRYPTION_POLICY, &policy) < 0) { + + if (errno == ENODATA) + log_debug_errno(errno, "Determined %s is not fscrypt encrypted.", path); + else if (ERRNO_IS_NOT_SUPPORTED(errno)) + log_debug_errno(errno, "Determined %s is not fscrypt encrypted because kernel or file system don't support it.", path); + else + log_debug_errno(errno, "FS_IOC_GET_ENCRYPTION_POLICY failed with unexpected error code on %s, ignoring: %m", path); + + storage = USER_DIRECTORY; + } else + storage = USER_FSCRYPT; + } + + return manager_add_home_by_image(m, user_name, realm, path, NULL, storage, st.st_uid); + } + + return 0; +} + +int manager_enumerate_images(Manager *m) { + _cleanup_closedir_ DIR *d = NULL; + struct dirent *de; + + assert(m); + + if (!m->scan_slash_home) + return 0; + + d = opendir("/home/"); + if (!d) + return log_full_errno(errno == ENOENT ? LOG_DEBUG : LOG_ERR, errno, + "Failed to open /home/: %m"); + + FOREACH_DIRENT(de, d, return log_error_errno(errno, "Failed to read /home/ directory: %m")) + (void) manager_assess_image(m, dirfd(d), "/home", de->d_name); + + return 0; +} + +static int manager_connect_bus(Manager *m) { + int r; + + assert(m); + assert(!m->bus); + + r = sd_bus_default_system(&m->bus); + if (r < 0) + return log_error_errno(r, "Failed to connect to system bus: %m"); + + r = sd_bus_add_object_vtable(m->bus, NULL, "/org/freedesktop/home1", "org.freedesktop.home1.Manager", manager_vtable, m); + if (r < 0) + return log_error_errno(r, "Failed to add manager object vtable: %m"); + + r = sd_bus_add_fallback_vtable(m->bus, NULL, "/org/freedesktop/home1/home", "org.freedesktop.home1.Home", home_vtable, bus_home_object_find, m); + if (r < 0) + return log_error_errno(r, "Failed to add image object vtable: %m"); + + r = sd_bus_add_node_enumerator(m->bus, NULL, "/org/freedesktop/home1/home", bus_home_node_enumerator, m); + if (r < 0) + return log_error_errno(r, "Failed to add image enumerator: %m"); + + r = sd_bus_add_object_manager(m->bus, NULL, "/org/freedesktop/home1/home"); + if (r < 0) + return log_error_errno(r, "Failed to add object manager: %m"); + + r = sd_bus_request_name_async(m->bus, NULL, "org.freedesktop.home1", 0, NULL, NULL); + if (r < 0) + return log_error_errno(r, "Failed to request name: %m"); + + r = sd_bus_attach_event(m->bus, m->event, 0); + if (r < 0) + return log_error_errno(r, "Failed to attach bus to event loop: %m"); + + (void) sd_bus_set_exit_on_disconnect(m->bus, true); + + return 0; +} + +static int manager_bind_varlink(Manager *m) { + int r; + + assert(m); + assert(!m->varlink_server); + + r = varlink_server_new(&m->varlink_server, VARLINK_SERVER_ACCOUNT_UID); + if (r < 0) + return log_error_errno(r, "Failed to allocate varlink server object: %m"); + + varlink_server_set_userdata(m->varlink_server, m); + + r = varlink_server_bind_method_many( + m->varlink_server, + "io.systemd.UserDatabase.GetUserRecord", vl_method_get_user_record, + "io.systemd.UserDatabase.GetGroupRecord", vl_method_get_group_record, + "io.systemd.UserDatabase.GetMemberships", vl_method_get_memberships); + if (r < 0) + return log_error_errno(r, "Failed to register varlink methods: %m"); + + (void) mkdir_p("/run/systemd/userdb", 0755); + + r = varlink_server_listen_address(m->varlink_server, "/run/systemd/userdb/io.systemd.Home", 0666); + if (r < 0) + return log_error_errno(r, "Failed to bind to varlink socket: %m"); + + r = varlink_server_attach_event(m->varlink_server, m->event, SD_EVENT_PRIORITY_NORMAL); + if (r < 0) + return log_error_errno(r, "Failed to attach varlink connection to event loop: %m"); + + return 0; +} + +static ssize_t read_datagram(int fd, struct ucred *ret_sender, void **ret) { + _cleanup_free_ void *buffer = NULL; + ssize_t n, m; + + assert(fd >= 0); + assert(ret_sender); + assert(ret); + + n = next_datagram_size_fd(fd); + if (n < 0) + return n; + + buffer = malloc(n + 2); + if (!buffer) + return -ENOMEM; + + if (ret_sender) { + union { + struct cmsghdr cmsghdr; + uint8_t buf[CMSG_SPACE(sizeof(struct ucred))]; + } control; + bool found_ucred = false; + struct cmsghdr *cmsg; + struct msghdr mh; + struct iovec iov; + + /* Pass one extra byte, as a size check */ + iov = IOVEC_MAKE(buffer, n + 1); + + mh = (struct msghdr) { + .msg_iov = &iov, + .msg_iovlen = 1, + .msg_control = &control, + .msg_controllen = sizeof(control), + }; + + m = recvmsg(fd, &mh, MSG_DONTWAIT|MSG_CMSG_CLOEXEC); + if (m < 0) + return -errno; + + cmsg_close_all(&mh); + + /* Ensure the size matches what we determined before */ + if (m != n) + return -EMSGSIZE; + + CMSG_FOREACH(cmsg, &mh) + if (cmsg->cmsg_level == SOL_SOCKET && + cmsg->cmsg_type == SCM_CREDENTIALS && + cmsg->cmsg_len == CMSG_LEN(sizeof(struct ucred))) { + + memcpy(ret_sender, CMSG_DATA(cmsg), sizeof(struct ucred)); + found_ucred = true; + } + + if (!found_ucred) + *ret_sender = (struct ucred) { + .pid = 0, + .uid = UID_INVALID, + .gid = GID_INVALID, + }; + } else { + m = recv(fd, buffer, n + 1, MSG_DONTWAIT); + if (m < 0) + return -errno; + + /* Ensure the size matches what we determined before */ + if (m != n) + return -EMSGSIZE; + } + + /* For safety reasons: let's always NUL terminate. */ + ((char*) buffer)[n] = 0; + *ret = TAKE_PTR(buffer); + + return 0; +} + +static int on_notify_socket(sd_event_source *s, int fd, uint32_t revents, void *userdata) { + _cleanup_strv_free_ char **l = NULL; + _cleanup_free_ void *datagram = NULL; + struct ucred sender; + Manager *m = userdata; + ssize_t n; + Home *h; + + assert(s); + assert(m); + + n = read_datagram(fd, &sender, &datagram); + if (IN_SET(n, -EAGAIN, -EINTR)) + return 0; + if (n < 0) + return log_error_errno(n, "Failed to read notify datagram: %m"); + + if (sender.pid <= 0) { + log_warning("Received notify datagram without valid sender PID, ignoring."); + return 0; + } + + h = hashmap_get(m->homes_by_worker_pid, PID_TO_PTR(sender.pid)); + if (!h) { + log_warning("Recieved notify datagram of unknown process, ignoring."); + return 0; + } + + l = strv_split(datagram, "\n"); + if (!l) + return log_oom(); + + home_process_notify(h, l); + return 0; +} + +static int manager_listen_notify(Manager *m) { + _cleanup_close_ int fd = -1; + union sockaddr_union sa; + int r; + + assert(m); + assert(!m->notify_socket_event_source); + + fd = socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0); + if (fd < 0) + return log_error_errno(errno, "Failed to create listening socket: %m"); + + r = sockaddr_un_set_path(&sa.un, "/run/systemd/home/notify"); + if (r < 0) + return log_error_errno(r, "Failed to set AF_UNIX socket path: %m"); + + (void) mkdir_parents(sa.un.sun_path, 0755); + (void) sockaddr_un_unlink(&sa.un); + + if (bind(fd, &sa.sa, SOCKADDR_UN_LEN(sa.un)) < 0) + return log_error_errno(errno, "Failed to bind to socket: %m"); + + r = setsockopt_int(fd, SOL_SOCKET, SO_PASSCRED, true); + if (r < 0) + return r; + + r = sd_event_add_io(m->event, &m->notify_socket_event_source, fd, EPOLLIN, on_notify_socket, m); + if (r < 0) + return log_error_errno(r, "Failed to allocate event source for notify socket: %m"); + + (void) sd_event_source_set_description(m->notify_socket_event_source, "notify-socket"); + + /* Make sure we process sd_notify() before SIGCHLD for any worker, so that we always know the error + * number of a client before it exits. */ + r = sd_event_source_set_priority(m->notify_socket_event_source, SD_EVENT_PRIORITY_NORMAL - 5); + if (r < 0) + return log_error_errno(r, "Failed to alter priority of NOTIFY_SOCKET event source: %m"); + + r = sd_event_source_set_io_fd_own(m->notify_socket_event_source, true); + if (r < 0) + return log_error_errno(r, "Failed to pass ownership of notify socket: %m"); + + return TAKE_FD(fd); +} + +static int manager_add_device(Manager *m, sd_device *d) { + _cleanup_free_ char *user_name = NULL, *realm = NULL, *node = NULL; + const char *tabletype, *parttype, *partname, *partuuid, *sysfs; + sd_id128_t id; + int r; + + assert(m); + assert(d); + + r = sd_device_get_syspath(d, &sysfs); + if (r < 0) + return log_error_errno(r, "Failed to acquire sysfs path of device: %m"); + + r = sd_device_get_property_value(d, "ID_PART_TABLE_TYPE", &tabletype); + if (r == -ENOENT) + return 0; + if (r < 0) + return log_error_errno(r, "Failed to acquire ID_PART_TABLE_TYPE device property, ignoring: %m"); + + if (!streq(tabletype, "gpt")) { + log_debug("Found partition (%s) on non-GPT table, ignoring.", sysfs); + return 0; + } + + r = sd_device_get_property_value(d, "ID_PART_ENTRY_TYPE", &parttype); + if (r == -ENOENT) + return 0; + if (r < 0) + return log_error_errno(r, "Failed to acquire ID_PART_ENTRY_TYPE device property, ignoring: %m"); + r = sd_id128_from_string(parttype, &id); + if (r < 0) + return log_debug_errno(r, "Failed to parse ID_PART_ENTRY_TYPE field '%s', ignoring: %m", parttype); + if (!sd_id128_equal(id, GPT_USER_HOME)) { + log_debug("Found partition (%s) we don't care about, ignoring.", sysfs); + return 0; + } + + r = sd_device_get_property_value(d, "ID_PART_ENTRY_NAME", &partname); + if (r < 0) + return log_warning_errno(r, "Failed to acquire ID_PART_ENTRY_NAME device property, ignoring: %m"); + + r = split_user_name_realm(partname, &user_name, &realm); + if (r == -EINVAL) + return log_warning_errno(r, "Found partition with correct partition type but a non-parsable partition name '%s', ignoring.", partname); + if (r < 0) + return log_error_errno(r, "Failed to validate partition name '%s': %m", partname); + + r = sd_device_get_property_value(d, "ID_FS_UUID", &partuuid); + if (r < 0) + return log_warning_errno(r, "Failed to acquire ID_FS_UUID device property, ignoring: %m"); + + r = sd_id128_from_string(partuuid, &id); + if (r < 0) + return log_warning_errno(r, "Failed to parse ID_FS_UUID field '%s', ignoring: %m", partuuid); + + if (asprintf(&node, "/dev/disk/by-uuid/" SD_ID128_UUID_FORMAT_STR, SD_ID128_FORMAT_VAL(id)) < 0) + return log_oom(); + + return manager_add_home_by_image(m, user_name, realm, node, sysfs, USER_LUKS, UID_INVALID); +} + +static int manager_on_device(sd_device_monitor *monitor, sd_device *d, void *userdata) { + Manager *m = userdata; + int r; + + assert(m); + assert(d); + + if (device_for_action(d, DEVICE_ACTION_REMOVE)) { + const char *sysfs; + Home *h; + + r = sd_device_get_syspath(d, &sysfs); + if (r < 0) { + log_warning_errno(r, "Failed to acquire sysfs path from device: %m"); + return 0; + } + + log_info("block device %s has been removed.", sysfs); + + /* Let's see if we previously synthesized a home record from this device, if so, let's just + * revalidate that. Otherwise let's revalidate them all, but asynchronously. */ + h = hashmap_get(m->homes_by_sysfs, sysfs); + if (h) + manager_revalidate_image(m, h); + else + manager_enqueue_gc(m, NULL); + } else + (void) manager_add_device(m, d); + + (void) bus_manager_emit_auto_login_changed(m); + return 0; +} + +static int manager_watch_devices(Manager *m) { + int r; + + assert(m); + assert(!m->device_monitor); + + r = sd_device_monitor_new(&m->device_monitor); + if (r < 0) + return log_error_errno(r, "Failed to allocate device monitor: %m"); + + r = sd_device_monitor_filter_add_match_subsystem_devtype(m->device_monitor, "block", NULL); + if (r < 0) + return log_error_errno(r, "Failed to configure device monitor match: %m"); + + r = sd_device_monitor_attach_event(m->device_monitor, m->event); + if (r < 0) + return log_error_errno(r, "Failed to attach device monitor to event loop: %m"); + + r = sd_device_monitor_start(m->device_monitor, manager_on_device, m); + if (r < 0) + return log_error_errno(r, "Failed to start device monitor: %m"); + + return 0; +} + +static int manager_enumerate_devices(Manager *m) { + _cleanup_(sd_device_enumerator_unrefp) sd_device_enumerator *e = NULL; + sd_device *d; + int r; + + assert(m); + + r = sd_device_enumerator_new(&e); + if (r < 0) + return r; + + r = sd_device_enumerator_add_match_subsystem(e, "block", true); + if (r < 0) + return r; + + FOREACH_DEVICE(e, d) + (void) manager_add_device(m, d); + + return 0; +} + +static int manager_load_key_pair(Manager *m) { + _cleanup_(fclosep) FILE *f = NULL; + struct stat st; + int r; + + assert(m); + + if (m->private_key) { + EVP_PKEY_free(m->private_key); + m->private_key = NULL; + } + + r = search_and_fopen_nulstr("local.private", "re", NULL, KEY_PATHS_NULSTR, &f); + if (r == -ENOENT) + return 0; + if (r < 0) + return log_error_errno(r, "Failed to read private key file: %m"); + + if (fstat(fileno(f), &st) < 0) + return log_error_errno(errno, "Failed to stat private key file: %m"); + + r = stat_verify_regular(&st); + if (r < 0) + return log_error_errno(r, "Private key file is not regular: %m"); + + if (st.st_uid != 0 || (st.st_mode & 0077) != 0) + return log_error_errno(SYNTHETIC_ERRNO(EPERM), "Private key file is readable by more than the root user"); + + m->private_key = PEM_read_PrivateKey(f, NULL, NULL, NULL); + if (!m->private_key) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to load private key pair"); + + log_info("Successfully loaded private key pair."); + + return 1; +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(EVP_PKEY_CTX*, EVP_PKEY_CTX_free); + +static int manager_generate_key_pair(Manager *m) { + _cleanup_(EVP_PKEY_CTX_freep) EVP_PKEY_CTX *ctx = NULL; + _cleanup_(unlink_and_freep) char *temp_public = NULL, *temp_private = NULL; + _cleanup_fclose_ FILE *fpublic = NULL, *fprivate = NULL; + int r; + + if (m->private_key) { + EVP_PKEY_free(m->private_key); + m->private_key = NULL; + } + + ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_ED25519, NULL); + if (!ctx) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to allocate Ed25519 key generation context."); + + if (EVP_PKEY_keygen_init(ctx) <= 0) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to initialize Ed25519 key generation context."); + + log_info("Generating key pair for signing local user identity records."); + + if (EVP_PKEY_keygen(ctx, &m->private_key) <= 0) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to generate Ed25519 key pair"); + + log_info("Successfully created Ed25519 key pair."); + + (void) mkdir_p("/var/lib/systemd/home", 0755); + + /* Write out public key (note that we only do that as a help to the user, we don't make use of this ever */ + r = fopen_temporary("/var/lib/systemd/home/local.public", &fpublic, &temp_public); + if (r < 0) + return log_error_errno(errno, "Failed ot open key file for writing: %m"); + + if (PEM_write_PUBKEY(fpublic, m->private_key) <= 0) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to write public key."); + + r = fflush_and_check(fpublic); + if (r < 0) + return log_error_errno(r, "Failed to write private key: %m"); + + fpublic = safe_fclose(fpublic); + + /* Write out the private key (this actually writes out both private and public, OpenSSL is confusing) */ + r = fopen_temporary("/var/lib/systemd/home/local.private", &fprivate, &temp_private); + if (r < 0) + return log_error_errno(errno, "Failed ot open key file for writing: %m"); + + if (PEM_write_PrivateKey(fprivate, m->private_key, NULL, NULL, 0, NULL, 0) <= 0) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to write private key pair."); + + r = fflush_and_check(fprivate); + if (r < 0) + return log_error_errno(r, "Failed to write private key: %m"); + + fprivate = safe_fclose(fprivate); + + /* Both are written now, move them into place */ + + if (rename(temp_public, "/var/lib/systemd/home/local.public") < 0) + return log_error_errno(errno, "Failed to move public key file into place: %m"); + temp_public = mfree(temp_public); + + if (rename(temp_private, "/var/lib/systemd/home/local.private") < 0) { + (void) unlink_noerrno("/var/lib/systemd/home/local.public"); /* try to remove the file we already created */ + return log_error_errno(errno, "Failed to move privtate key file into place: %m"); + } + temp_private = mfree(temp_private); + + return 1; +} + +int manager_acquire_key_pair(Manager *m) { + int r; + + assert(m); + + /* Already there? */ + if (m->private_key) + return 1; + + /* First try to load key off disk */ + r = manager_load_key_pair(m); + if (r != 0) + return r; + + /* Didn't work, generate a new one */ + return manager_generate_key_pair(m); +} + +int manager_sign_user_record(Manager *m, UserRecord *u, UserRecord **ret, sd_bus_error *error) { + int r; + + assert(m); + assert(u); + assert(ret); + + r = manager_acquire_key_pair(m); + if (r < 0) + return r; + if (r == 0) + return sd_bus_error_setf(error, BUS_ERROR_NO_PRIVATE_KEY, "Can't sign without local key."); + + return user_record_sign(u, m->private_key, ret); +} + +DEFINE_PRIVATE_HASH_OPS_FULL(public_key_hash_ops, char, string_hash_func, string_compare_func, free, EVP_PKEY, EVP_PKEY_free); +DEFINE_TRIVIAL_CLEANUP_FUNC(EVP_PKEY*, EVP_PKEY_free); + +static int manager_load_public_key_one(Manager *m, const char *path) { + _cleanup_(EVP_PKEY_freep) EVP_PKEY *pkey = NULL; + _cleanup_fclose_ FILE *f = NULL; + _cleanup_free_ char *fn = NULL; + struct stat st; + int r; + + assert(m); + + if (streq(basename(path), "local.public")) /* we already loaded the private key, which includes the public one */ + return 0; + + f = fopen(path, "re"); + if (!f) { + if (errno == ENOENT) + return 0; + + return log_error_errno(errno, "Failed to open public key %s: %m", path); + } + + if (fstat(fileno(f), &st) < 0) + return log_error_errno(errno, "Failed to stat public key %s: %m", path); + + r = stat_verify_regular(&st); + if (r < 0) + return log_error_errno(r, "Public key file %s is not a regular file: %m", path); + + if (st.st_uid != 0 || (st.st_mode & 0022) != 0) + return log_error_errno(SYNTHETIC_ERRNO(EPERM), "Public key file %s is writable by more than the root user, refusing.", path); + + r = hashmap_ensure_allocated(&m->public_keys, &public_key_hash_ops); + if (r < 0) + return log_oom(); + + pkey = PEM_read_PUBKEY(f, &pkey, NULL, NULL); + if (!pkey) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to parse public key file %s.", path); + + fn = strdup(basename(path)); + if (!fn) + return log_oom(); + + r = hashmap_put(m->public_keys, fn, pkey); + if (r < 0) + return log_error_errno(r, "Failed to add public key to set: %m"); + + TAKE_PTR(fn); + TAKE_PTR(pkey); + + return 0; +} + +static int manager_load_public_keys(Manager *m) { + _cleanup_strv_free_ char **files = NULL; + char **i; + int r; + + assert(m); + + m->public_keys = hashmap_free(m->public_keys); + + r = conf_files_list_nulstr( + &files, + ".public", + NULL, + CONF_FILES_REGULAR|CONF_FILES_FILTER_MASKED, + KEY_PATHS_NULSTR); + if (r < 0) + return log_error_errno(r, "Failed to assemble list of public key directories: %m"); + + STRV_FOREACH(i, files) + (void) manager_load_public_key_one(m, *i); + + return 0; +} + +int manager_startup(Manager *m) { + int r; + + assert(m); + + r = manager_listen_notify(m); + if (r < 0) + return r; + + r = manager_connect_bus(m); + if (r < 0) + return r; + + r = manager_bind_varlink(m); + if (r < 0) + return r; + + r = manager_load_key_pair(m); /* only try to load it, don't generate any */ + if (r < 0) + return r; + + r = manager_load_public_keys(m); + if (r < 0) + return r; + + manager_watch_home(m); + (void) manager_watch_devices(m); + + (void) manager_enumerate_records(m); + (void) manager_enumerate_images(m); + (void) manager_enumerate_devices(m); + + /* Let's clean up home directories whose devices got removed while we were not running */ + (void) manager_enqueue_gc(m, NULL); + + return 0; +} + +void manager_revalidate_image(Manager *m, Home *h) { + int r; + + assert(m); + assert(h); + + /* Frees an automatically discovered image, if it's synthetic and its image disappeared. Unmounts any + * image if it's mounted but it's image vanished. */ + + if (h->current_operation || !ordered_set_isempty(h->pending_operations)) + return; + + if (h->state == HOME_UNFIXATED) { + r = user_record_test_image_path(h->record); + if (r < 0) + log_warning_errno(r, "Can't determine if image of %s exists, freeing unfixated user: %m", h->user_name); + else if (r == USER_TEST_ABSENT) + log_info("Image for %s disappeared, freeing unfixated user.", h->user_name); + else + return; + + home_free(h); + + } else if (h->state < 0) { + + r = user_record_test_home_directory(h->record); + if (r < 0) { + log_warning_errno(r, "Unable to determine state of home directory, ignoring: %m"); + return; + } + + if (r == USER_TEST_MOUNTED) { + r = user_record_test_image_path(h->record); + if (r < 0) { + log_warning_errno(r, "Unable to determine state of image path, ignoring: %m"); + return; + } + + if (r == USER_TEST_ABSENT) { + _cleanup_(operation_unrefp) Operation *o = NULL; + + log_notice("Backing image disappeared while home directory %s was mounted, unmounting it forcibly.", h->user_name); + /* Wowza, the thing is mounted, but the device is gone? Act on it. */ + + r = home_killall(h); + if (r < 0) + log_warning_errno(r, "Failed to kill processes of user %s, ignoring: %m", h->user_name); + + /* We enqueue the operation here, after all the home directory might + * currently already run some operation, and we can deactivate it only after + * that's complete. */ + o = operation_new(OPERATION_DEACTIVATE_FORCE, NULL); + if (!o) { + log_oom(); + return; + } + + r = home_schedule_operation(h, o, NULL); + if (r < 0) + log_warning_errno(r, "Failed to enqueue forced home directory %s deactivation, ignoring: %m", h->user_name); + } + } + } +} + +int manager_gc_images(Manager *m) { + Home *h; + + assert_se(m); + + if (m->gc_focus) { + /* Focus on a specific home */ + + h = TAKE_PTR(m->gc_focus); + manager_revalidate_image(m, h); + } else { + /* Gc all */ + Iterator i; + + HASHMAP_FOREACH(h, m->homes_by_name, i) + manager_revalidate_image(m, h); + } + + return 0; +} + +static int on_deferred_rescan(sd_event_source *s, void *userdata) { + Manager *m = userdata; + + assert(m); + + m->deferred_rescan_event_source = sd_event_source_unref(m->deferred_rescan_event_source); + + manager_enumerate_devices(m); + manager_enumerate_images(m); + return 0; +} + +int manager_enqueue_rescan(Manager *m) { + int r; + + assert(m); + + if (m->deferred_rescan_event_source) + return 0; + + if (!m->event) + return 0; + + if (IN_SET(sd_event_get_state(m->event), SD_EVENT_FINISHED, SD_EVENT_EXITING)) + return 0; + + r = sd_event_add_defer(m->event, &m->deferred_rescan_event_source, on_deferred_rescan, m); + if (r < 0) + return log_error_errno(r, "Failed to allocate rescan event source: %m"); + + r = sd_event_source_set_priority(m->deferred_rescan_event_source, SD_EVENT_PRIORITY_IDLE+1); + if (r < 0) + log_warning_errno(r, "Failed to tweak priority of event source, ignoring: %m"); + + (void) sd_event_source_set_description(m->deferred_rescan_event_source, "deferred-rescan"); + return 1; +} + +static int on_deferred_gc(sd_event_source *s, void *userdata) { + Manager *m = userdata; + + assert(m); + + m->deferred_gc_event_source = sd_event_source_unref(m->deferred_gc_event_source); + + manager_gc_images(m); + return 0; +} + +int manager_enqueue_gc(Manager *m, Home *focus) { + int r; + + assert(m); + + /* This enqueues a request to GC dead homes. It may be called with focus=NULL in which case all homes + * will be scanned, or with the parameter set, in which case only that home is checked. */ + + if (!m->event) + return 0; + + if (IN_SET(sd_event_get_state(m->event), SD_EVENT_FINISHED, SD_EVENT_EXITING)) + return 0; + + /* If a focus home is specified, then remember to focus just on this home. Otherwise invalidate any + * focus that might be set to look at all homes. */ + + if (m->deferred_gc_event_source) { + if (m->gc_focus != focus) /* not the same focus, then look at everything */ + m->gc_focus = NULL; + + return 0; + } else + m->gc_focus = focus; /* start focussed */ + + r = sd_event_add_defer(m->event, &m->deferred_gc_event_source, on_deferred_gc, m); + if (r < 0) + return log_error_errno(r, "Failed to allocate gc event source: %m"); + + r = sd_event_source_set_priority(m->deferred_gc_event_source, SD_EVENT_PRIORITY_IDLE); + if (r < 0) + log_warning_errno(r, "Failed to tweak priority of event source, ignoring: %m"); + + (void) sd_event_source_set_description(m->deferred_gc_event_source, "deferred-gc"); + return 1; +} diff --git a/src/home/homed-manager.h b/src/home/homed-manager.h new file mode 100644 index 0000000000..00298a3d2d --- /dev/null +++ b/src/home/homed-manager.h @@ -0,0 +1,67 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include + +#include "sd-bus.h" +#include "sd-device.h" +#include "sd-event.h" + +typedef struct Manager Manager; + +#include "hashmap.h" +#include "homed-home.h" +#include "varlink.h" + +#define HOME_UID_MIN 60001 +#define HOME_UID_MAX 60513 + +struct Manager { + sd_event *event; + sd_bus *bus; + + Hashmap *polkit_registry; + + Hashmap *homes_by_uid; + Hashmap *homes_by_name; + Hashmap *homes_by_worker_pid; + Hashmap *homes_by_sysfs; + + bool scan_slash_home; + + sd_event_source *inotify_event_source; + + /* An even source we receieve sd_notify() messages from our worker from */ + sd_event_source *notify_socket_event_source; + + sd_device_monitor *device_monitor; + + sd_event_source *deferred_rescan_event_source; + sd_event_source *deferred_gc_event_source; + sd_event_source *deferred_auto_login_event_source; + + Home *gc_focus; + + VarlinkServer *varlink_server; + + EVP_PKEY *private_key; /* actually a pair of private and public key */ + Hashmap *public_keys; /* key name [char*] → publick key [EVP_PKEY*] */ +}; + +int manager_new(Manager **ret); +Manager* manager_free(Manager *m); +DEFINE_TRIVIAL_CLEANUP_FUNC(Manager*, manager_free); + +int manager_startup(Manager *m); + +int manager_augment_record_with_uid(Manager *m, UserRecord *hr); + +int manager_enqueue_rescan(Manager *m); +int manager_enqueue_gc(Manager *m, Home *focus); + +int manager_verify_user_record(Manager *m, UserRecord *hr); + +int manager_acquire_key_pair(Manager *m); +int manager_sign_user_record(Manager *m, UserRecord *u, UserRecord **ret, sd_bus_error *error); + +int bus_manager_emit_auto_login_changed(Manager *m); diff --git a/src/home/homed-operation.c b/src/home/homed-operation.c new file mode 100644 index 0000000000..80dc555cd0 --- /dev/null +++ b/src/home/homed-operation.c @@ -0,0 +1,76 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include "fd-util.h" +#include "homed-operation.h" + +Operation *operation_new(OperationType type, sd_bus_message *m) { + Operation *o; + + assert(type >= 0); + assert(type < _OPERATION_MAX); + + o = new(Operation, 1); + if (!o) + return NULL; + + *o = (Operation) { + .type = type, + .n_ref = 1, + .message = sd_bus_message_ref(m), + .send_fd = -1, + .result = -1, + }; + + return o; +} + +static Operation *operation_free(Operation *o) { + int r; + + if (!o) + return NULL; + + if (o->message && o->result >= 0) { + + if (o->result) { + /* Propagate success */ + if (o->send_fd < 0) + r = sd_bus_reply_method_return(o->message, NULL); + else + r = sd_bus_reply_method_return(o->message, "h", o->send_fd); + + } else { + /* Propagate failure */ + if (sd_bus_error_is_set(&o->error)) + r = sd_bus_reply_method_error(o->message, &o->error); + else + r = sd_bus_reply_method_errnof(o->message, o->ret, "Failed to execute operation: %m"); + } + if (r < 0) + log_warning_errno(r, "Failed ot reply to %s method call, ignoring: %m", sd_bus_message_get_member(o->message)); + } + + sd_bus_message_unref(o->message); + user_record_unref(o->secret); + safe_close(o->send_fd); + sd_bus_error_free(&o->error); + + return mfree(o); +} + +DEFINE_TRIVIAL_REF_UNREF_FUNC(Operation, operation, operation_free); + +void operation_result(Operation *o, int ret, const sd_bus_error *error) { + assert(o); + + if (ret >= 0) + o->result = true; + else { + o->ret = ret; + + sd_bus_error_free(&o->error); + sd_bus_error_copy(&o->error, error); + + o->result = false; + } +} diff --git a/src/home/homed-operation.h b/src/home/homed-operation.h new file mode 100644 index 0000000000..224de91852 --- /dev/null +++ b/src/home/homed-operation.h @@ -0,0 +1,62 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include + +#include "user-record.h" + +typedef enum OperationType { + OPERATION_ACQUIRE, /* enqueued on AcquireHome() */ + OPERATION_RELEASE, /* enqueued on ReleaseHome() */ + OPERATION_LOCK_ALL, /* enqueued on LockAllHomes() */ + OPERATION_PIPE_EOF, /* enqueued when we see EOF on the per-home reference pipes */ + OPERATION_DEACTIVATE_FORCE, /* enqueued on hard $HOME unplug */ + OPERATION_IMMEDIATE, /* this is never enqueued, it's just a marker we immediately started executing an operation without enqueuing anything first. */ + _OPERATION_MAX, + _OPERATION_INVALID = -1, +} OperationType; + +/* Encapsulates an operation on one or more home directories. This has two uses: + * + * 1) For queuing an operation when we need to execute one for some reason but there's already one being + * executed. + * + * 2) When executing an operation without enqueuing it first (OPERATION_IMMEDIATE) + * + * Note that a single operation object can encapsulate operations on multiple home directories. This is used + * for the LockAllHomes() operation, which is one operation but applies to all homes at once. In case the + * operation applies to multiple homes the reference counter is increased once for each, and thus the + * operation is fully completed only after it reached zero again. + * + * The object (optionally) contains a reference of the D-Bus message triggering the operation, which is + * replied to when the operation is fully completed, i.e. when n_ref reaches zero. + */ + +typedef struct Operation { + unsigned n_ref; + OperationType type; + sd_bus_message *message; + + UserRecord *secret; + int send_fd; /* pipe fd for AcquireHome() which is taken already when we start the operation */ + + int result; /* < 0 if not completed yet, == 0 on failure, > 0 on success */ + sd_bus_error error; + int ret; +} Operation; + +Operation *operation_new(OperationType type, sd_bus_message *m); +Operation *operation_ref(Operation *operation); +Operation *operation_unref(Operation *operation); + +DEFINE_TRIVIAL_CLEANUP_FUNC(Operation*, operation_unref); + +void operation_result(Operation *o, int ret, const sd_bus_error *error); + +static inline Operation* operation_result_unref(Operation *o, int ret, const sd_bus_error *error) { + if (!o) + return NULL; + + operation_result(o, ret, error); + return operation_unref(o); +} diff --git a/src/home/homed-varlink.c b/src/home/homed-varlink.c new file mode 100644 index 0000000000..c5bbba6852 --- /dev/null +++ b/src/home/homed-varlink.c @@ -0,0 +1,370 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include "group-record.h" +#include "homed-varlink.h" +#include "strv.h" +#include "user-record-util.h" +#include "user-record.h" +#include "user-util.h" +#include "format-util.h" + +typedef struct LookupParameters { + const char *user_name; + const char *group_name; + union { + uid_t uid; + gid_t gid; + }; + const char *service; +} LookupParameters; + +static bool client_is_trusted(Varlink *link, Home *h) { + uid_t peer_uid; + int r; + + assert(link); + assert(h); + + r = varlink_get_peer_uid(link, &peer_uid); + if (r < 0) { + log_debug_errno(r, "Unable to query peer UID, ignoring: %m"); + return false; + } + + return peer_uid == 0 || peer_uid == h->uid; +} + +static int build_user_json(Home *h, bool trusted, JsonVariant **ret) { + _cleanup_(user_record_unrefp) UserRecord *augmented = NULL; + UserRecordLoadFlags flags; + int r; + + assert(h); + assert(ret); + + flags = USER_RECORD_REQUIRE_REGULAR|USER_RECORD_ALLOW_PER_MACHINE|USER_RECORD_ALLOW_BINDING|USER_RECORD_STRIP_SECRET|USER_RECORD_ALLOW_STATUS|USER_RECORD_ALLOW_SIGNATURE; + if (trusted) + flags |= USER_RECORD_ALLOW_PRIVILEGED; + else + flags |= USER_RECORD_STRIP_PRIVILEGED; + + r = home_augment_status(h, flags, &augmented); + if (r < 0) + return r; + + return json_build(ret, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("record", JSON_BUILD_VARIANT(augmented->json)), + JSON_BUILD_PAIR("incomplete", JSON_BUILD_BOOLEAN(augmented->incomplete)))); +} + +static bool home_user_match_lookup_parameters(LookupParameters *p, Home *h) { + assert(p); + assert(h); + + if (p->user_name && !streq(p->user_name, h->user_name)) + return false; + + if (uid_is_valid(p->uid) && h->uid != p->uid) + return false; + + return true; +} + +int vl_method_get_user_record(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) { + + static const JsonDispatch dispatch_table[] = { + { "uid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(LookupParameters, uid), 0 }, + { "userName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, user_name), JSON_SAFE }, + { "service", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, service), 0 }, + {} + }; + + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + LookupParameters p = { + .uid = UID_INVALID, + }; + Manager *m = userdata; + bool trusted; + Home *h; + int r; + + assert(parameters); + assert(m); + + r = json_dispatch(parameters, dispatch_table, NULL, 0, &p); + if (r < 0) + return r; + + if (!streq_ptr(p.service, "io.systemd.Home")) + return varlink_error(link, "io.systemd.UserDatabase.BadService", NULL); + + if (uid_is_valid(p.uid)) + h = hashmap_get(m->homes_by_uid, UID_TO_PTR(p.uid)); + else if (p.user_name) + h = hashmap_get(m->homes_by_name, p.user_name); + else { + Iterator i; + + /* If neither UID nor name was specified, then dump all homes. Do so with varlink_notify() + * for all entries but the last, so that clients can stream the results, and easily process + * them piecemeal. */ + + HASHMAP_FOREACH(h, m->homes_by_name, i) { + + if (!home_user_match_lookup_parameters(&p, h)) + continue; + + if (v) { + /* An entry set from the previous iteration? Then send it now */ + r = varlink_notify(link, v); + if (r < 0) + return r; + + v = json_variant_unref(v); + } + + trusted = client_is_trusted(link, h); + + r = build_user_json(h, trusted, &v); + if (r < 0) + return r; + } + + if (!v) + return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); + + return varlink_reply(link, v); + } + + if (!h) + return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); + + if (!home_user_match_lookup_parameters(&p, h)) + return varlink_error(link, "io.systemd.UserDatabase.ConflictingRecordFound", NULL); + + trusted = client_is_trusted(link, h); + + r = build_user_json(h, trusted, &v); + if (r < 0) + return r; + + return varlink_reply(link, v); +} + +static int build_group_json(Home *h, JsonVariant **ret) { + _cleanup_(group_record_unrefp) GroupRecord *g = NULL; + int r; + + assert(h); + assert(ret); + + g = group_record_new(); + if (!g) + return -ENOMEM; + + r = group_record_synthesize(g, h->record); + if (r < 0) + return r; + + assert(!FLAGS_SET(g->mask, USER_RECORD_SECRET)); + assert(!FLAGS_SET(g->mask, USER_RECORD_PRIVILEGED)); + + return json_build(ret, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("record", JSON_BUILD_VARIANT(g->json)))); +} + +static bool home_group_match_lookup_parameters(LookupParameters *p, Home *h) { + assert(p); + assert(h); + + if (p->group_name && !streq(h->user_name, p->group_name)) + return false; + + if (gid_is_valid(p->gid) && h->uid != (uid_t) p->gid) + return false; + + return true; +} + +int vl_method_get_group_record(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) { + + static const JsonDispatch dispatch_table[] = { + { "gid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(LookupParameters, gid), 0 }, + { "groupName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, group_name), JSON_SAFE }, + { "service", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, service), 0 }, + {} + }; + + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + LookupParameters p = { + .gid = GID_INVALID, + }; + Manager *m = userdata; + Home *h; + int r; + + assert(parameters); + assert(m); + + r = json_dispatch(parameters, dispatch_table, NULL, 0, &p); + if (r < 0) + return r; + + if (!streq_ptr(p.service, "io.systemd.Home")) + return varlink_error(link, "io.systemd.UserDatabase.BadService", NULL); + + if (gid_is_valid(p.gid)) + h = hashmap_get(m->homes_by_uid, UID_TO_PTR((uid_t) p.gid)); + else if (p.group_name) + h = hashmap_get(m->homes_by_name, p.group_name); + else { + Iterator i; + + HASHMAP_FOREACH(h, m->homes_by_name, i) { + + if (!home_group_match_lookup_parameters(&p, h)) + continue; + + if (v) { + r = varlink_notify(link, v); + if (r < 0) + return r; + + v = json_variant_unref(v); + } + + r = build_group_json(h, &v); + if (r < 0) + return r; + } + + if (!v) + return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); + + return varlink_reply(link, v); + } + + if (!h) + return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); + + if (!home_group_match_lookup_parameters(&p, h)) + return varlink_error(link, "io.systemd.UserDatabase.ConflictingRecordFound", NULL); + + r = build_group_json(h, &v); + if (r < 0) + return r; + + return varlink_reply(link, v); +} + +int vl_method_get_memberships(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) { + + static const JsonDispatch dispatch_table[] = { + { "userName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, user_name), JSON_SAFE }, + { "groupName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, group_name), JSON_SAFE }, + { "service", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, service), 0 }, + {} + }; + + Manager *m = userdata; + LookupParameters p = {}; + Home *h; + int r; + + assert(parameters); + assert(m); + + r = json_dispatch(parameters, dispatch_table, NULL, 0, &p); + if (r < 0) + return r; + + if (!streq_ptr(p.service, "io.systemd.Home")) + return varlink_error(link, "io.systemd.UserDatabase.BadService", NULL); + + if (p.user_name) { + const char *last = NULL; + char **i; + + h = hashmap_get(m->homes_by_name, p.user_name); + if (!h) + return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); + + if (p.group_name) { + if (!strv_contains(h->record->member_of, p.group_name)) + return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); + + return varlink_replyb(link, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(h->user_name)), + JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(p.group_name)))); + } + + STRV_FOREACH(i, h->record->member_of) { + if (last) { + r = varlink_notifyb(link, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(h->user_name)), + JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(last)))); + if (r < 0) + return r; + } + + last = *i; + } + + if (last) + return varlink_replyb(link, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(h->user_name)), + JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(last)))); + + } else if (p.group_name) { + const char *last = NULL; + Iterator i; + + HASHMAP_FOREACH(h, m->homes_by_name, i) { + + if (!strv_contains(h->record->member_of, p.group_name)) + continue; + + if (last) { + r = varlink_notifyb(link, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(last)), + JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(p.group_name)))); + if (r < 0) + return r; + } + + last = h->user_name; + } + + if (last) + return varlink_replyb(link, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(last)), + JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(p.group_name)))); + } else { + const char *last_user_name = NULL, *last_group_name = NULL; + Iterator i; + + HASHMAP_FOREACH(h, m->homes_by_name, i) { + char **j; + + STRV_FOREACH(j, h->record->member_of) { + + if (last_user_name) { + assert(last_group_name); + + r = varlink_notifyb(link, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(last_user_name)), + JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(last_group_name)))); + + if (r < 0) + return r; + } + + last_user_name = h->user_name; + last_group_name = *j; + } + } + + if (last_user_name) { + assert(last_group_name); + return varlink_replyb(link, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(last_user_name)), + JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(last_group_name)))); + } + } + + return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); +} diff --git a/src/home/homed-varlink.h b/src/home/homed-varlink.h new file mode 100644 index 0000000000..4454d23442 --- /dev/null +++ b/src/home/homed-varlink.h @@ -0,0 +1,8 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include "homed-manager.h" + +int vl_method_get_user_record(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata); +int vl_method_get_group_record(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata); +int vl_method_get_memberships(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata); diff --git a/src/home/homed.c b/src/home/homed.c new file mode 100644 index 0000000000..ca43558269 --- /dev/null +++ b/src/home/homed.c @@ -0,0 +1,46 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include +#include + +#include "daemon-util.h" +#include "homed-manager.h" +#include "log.h" +#include "main-func.h" +#include "signal-util.h" + +static int run(int argc, char *argv[]) { + _cleanup_(notify_on_cleanup) const char *notify_stop = NULL; + _cleanup_(manager_freep) Manager *m = NULL; + int r; + + log_setup_service(); + + umask(0022); + + if (argc != 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "This program takes no arguments."); + + if (setenv("SYSTEMD_BYPASS_USERDB", "io.systemd.Home", 1) < 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to set $SYSTEMD_BYPASS_USERDB: %m"); + + assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGCHLD, SIGTERM, SIGINT, -1) >= 0); + + r = manager_new(&m); + if (r < 0) + return log_error_errno(r, "Could not create manager: %m"); + + r = manager_startup(m); + if (r < 0) + return log_error_errno(r, "Failed to start up daemon: %m"); + + notify_stop = notify_start(NOTIFY_READY, NOTIFY_STOPPING); + + r = sd_event_loop(m->event); + if (r < 0) + return log_error_errno(r, "Event loop failed: %m"); + + return 0; +} + +DEFINE_MAIN_FUNCTION(run); diff --git a/src/home/homework-cifs.c b/src/home/homework-cifs.c new file mode 100644 index 0000000000..f67e279eee --- /dev/null +++ b/src/home/homework-cifs.c @@ -0,0 +1,215 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include "dirent-util.h" +#include "fd-util.h" +#include "fileio.h" +#include "format-util.h" +#include "fs-util.h" +#include "homework-cifs.h" +#include "homework-mount.h" +#include "mount-util.h" +#include "process-util.h" +#include "strv.h" +#include "tmpfile-util.h" + +int home_prepare_cifs( + UserRecord *h, + bool already_activated, + HomeSetup *setup) { + + char **pw; + int r; + + assert(h); + assert(setup); + assert(user_record_storage(h) == USER_CIFS); + + if (already_activated) + setup->root_fd = open(user_record_home_directory(h), O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW); + else { + bool mounted = false; + + r = home_unshare_and_mount(NULL, NULL, false); + if (r < 0) + return r; + + STRV_FOREACH(pw, h->password) { + _cleanup_(unlink_and_freep) char *p = NULL; + _cleanup_free_ char *options = NULL; + _cleanup_(fclosep) FILE *f = NULL; + pid_t mount_pid; + int exit_status; + + r = fopen_temporary(NULL, &f, &p); + if (r < 0) + return log_error_errno(r, "Failed to create temporary credentials file: %m"); + + fprintf(f, + "username=%s\n" + "password=%s\n", + user_record_cifs_user_name(h), + *pw); + + if (h->cifs_domain) + fprintf(f, "domain=%s\n", h->cifs_domain); + + r = fflush_and_check(f); + if (r < 0) + return log_error_errno(r, "Failed to write temporary credentials file: %m"); + + f = safe_fclose(f); + + if (asprintf(&options, "credentials=%s,uid=" UID_FMT ",forceuid,gid=" UID_FMT ",forcegid,file_mode=0%3o,dir_mode=0%3o", + p, h->uid, h->uid, h->access_mode, h->access_mode) < 0) + return log_oom(); + + r = safe_fork("(mount)", FORK_RESET_SIGNALS|FORK_RLIMIT_NOFILE_SAFE|FORK_DEATHSIG|FORK_LOG|FORK_STDOUT_TO_STDERR, &mount_pid); + if (r < 0) + return r; + if (r == 0) { + /* Child */ + execl("/bin/mount", "/bin/mount", "-n", "-t", "cifs", + h->cifs_service, "/run/systemd/user-home-mount", + "-o", options, NULL); + + log_error_errno(errno, "Failed to execute fsck: %m"); + _exit(EXIT_FAILURE); + } + + exit_status = wait_for_terminate_and_check("mount", mount_pid, WAIT_LOG_ABNORMAL|WAIT_LOG_NON_ZERO_EXIT_STATUS); + if (exit_status < 0) + return exit_status; + if (exit_status != EXIT_SUCCESS) + return -EPROTO; + + mounted = true; + break; + } + + if (!mounted) + return log_error_errno(ENOKEY, "Failed to mount home directory with supplied password."); + + setup->root_fd = open("/run/systemd/user-home-mount", O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW); + } + if (setup->root_fd < 0) + return log_error_errno(r, "Failed to open home directory: %m"); + + return 0; +} + +int home_activate_cifs( + UserRecord *h, + char ***pkcs11_decrypted_passwords, + UserRecord **ret_home) { + + _cleanup_(home_setup_undo) HomeSetup setup = HOME_SETUP_INIT; + _cleanup_(user_record_unrefp) UserRecord *new_home = NULL; + const char *hdo, *hd; + int r; + + assert(h); + assert(user_record_storage(h) == USER_CIFS); + assert(ret_home); + + if (!h->cifs_service) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record lacks CIFS service, refusing."); + + assert_se(hdo = user_record_home_directory(h)); + hd = strdupa(hdo); /* copy the string out, since it might change later in the home record object */ + + r = home_prepare_cifs(h, false, &setup); + if (r < 0) + return r; + + r = home_refresh(h, &setup, NULL, pkcs11_decrypted_passwords, NULL, &new_home); + if (r < 0) + return r; + + setup.root_fd = safe_close(setup.root_fd); + + r = home_move_mount(NULL, hd); + if (r < 0) + return r; + + setup.undo_mount = false; + + log_info("Everything completed."); + + *ret_home = TAKE_PTR(new_home); + return 1; +} + +int home_create_cifs(UserRecord *h, UserRecord **ret_home) { + _cleanup_(home_setup_undo) HomeSetup setup = HOME_SETUP_INIT; + _cleanup_(user_record_unrefp) UserRecord *new_home = NULL; + _cleanup_(closedirp) DIR *d = NULL; + int r, copy; + + assert(h); + assert(user_record_storage(h) == USER_CIFS); + assert(ret_home); + + if (!h->cifs_service) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record lacks CIFS service, refusing."); + + if (access("/sbin/mount.cifs", F_OK) < 0) { + if (errno == ENOENT) + return log_error_errno(SYNTHETIC_ERRNO(ENOLINK), "/sbin/mount.cifs is missing."); + + return log_error_errno(errno, "Unable to detect whether /sbin/mount.cifs exists: %m"); + } + + r = home_prepare_cifs(h, false, &setup); + if (r < 0) + return r; + + copy = fcntl(setup.root_fd, F_DUPFD_CLOEXEC, 3); + if (copy < 0) + return -errno; + + d = fdopendir(copy); + if (!d) { + safe_close(copy); + return -errno; + } + + errno = 0; + if (readdir_no_dot(d)) + return log_error_errno(SYNTHETIC_ERRNO(ENOTEMPTY), "Selected CIFS directory not empty, refusing."); + if (errno != 0) + return log_error_errno(errno, "Failed to detect if CIFS directory is empty: %m"); + + r = home_populate(h, setup.root_fd); + if (r < 0) + return r; + + r = home_sync_and_statfs(setup.root_fd, NULL); + if (r < 0) + return r; + + r = user_record_clone(h, USER_RECORD_LOAD_MASK_SECRET, &new_home); + if (r < 0) + return log_error_errno(r, "Failed to clone record: %m"); + + r = user_record_add_binding( + new_home, + USER_CIFS, + NULL, + SD_ID128_NULL, + SD_ID128_NULL, + SD_ID128_NULL, + NULL, + NULL, + UINT64_MAX, + NULL, + NULL, + h->uid, + (gid_t) h->uid); + if (r < 0) + return log_error_errno(r, "Failed to add binding to record: %m"); + + log_info("Everything completed."); + + *ret_home = TAKE_PTR(new_home); + return 0; +} diff --git a/src/home/homework-cifs.h b/src/home/homework-cifs.h new file mode 100644 index 0000000000..346be8826e --- /dev/null +++ b/src/home/homework-cifs.h @@ -0,0 +1,11 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include "homework.h" +#include "user-record.h" + +int home_prepare_cifs(UserRecord *h, bool already_activated, HomeSetup *setup); + +int home_activate_cifs(UserRecord *h, char ***pkcs11_decrypted_passwords, UserRecord **ret_home); + +int home_create_cifs(UserRecord *h, UserRecord **ret_home); diff --git a/src/home/homework-directory.c b/src/home/homework-directory.c new file mode 100644 index 0000000000..8a4cb1732a --- /dev/null +++ b/src/home/homework-directory.c @@ -0,0 +1,242 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include + +#include "btrfs-util.h" +#include "fd-util.h" +#include "homework-directory.h" +#include "homework-quota.h" +#include "mkdir.h" +#include "mount-util.h" +#include "path-util.h" +#include "rm-rf.h" +#include "tmpfile-util.h" +#include "umask-util.h" + +int home_prepare_directory(UserRecord *h, bool already_activated, HomeSetup *setup) { + assert(h); + assert(setup); + + setup->root_fd = open(user_record_image_path(h), O_RDONLY|O_CLOEXEC|O_DIRECTORY); + if (setup->root_fd < 0) + return log_error_errno(errno, "Failed to open home directory: %m"); + + return 0; +} + +int home_activate_directory( + UserRecord *h, + char ***pkcs11_decrypted_passwords, + UserRecord **ret_home) { + + _cleanup_(user_record_unrefp) UserRecord *new_home = NULL, *header_home = NULL; + _cleanup_(home_setup_undo) HomeSetup setup = HOME_SETUP_INIT; + const char *hdo, *hd, *ipo, *ip; + int r; + + assert(h); + assert(IN_SET(user_record_storage(h), USER_DIRECTORY, USER_SUBVOLUME, USER_FSCRYPT)); + assert(ret_home); + + assert_se(ipo = user_record_image_path(h)); + ip = strdupa(ipo); /* copy out, since reconciliation might cause changing of the field */ + + assert_se(hdo = user_record_home_directory(h)); + hd = strdupa(hdo); + + r = home_prepare(h, false, pkcs11_decrypted_passwords, &setup, &header_home); + if (r < 0) + return r; + + r = home_refresh(h, &setup, header_home, pkcs11_decrypted_passwords, NULL, &new_home); + if (r < 0) + return r; + + setup.root_fd = safe_close(setup.root_fd); + + /* Create mount point to mount over if necessary */ + if (!path_equal(ip, hd)) + (void) mkdir_p(hd, 0700); + + /* Create a mount point (even if the directory is already placed correctly), as a way to indicate + * this mount point is now "activated". Moreover, we want to set per-user + * MS_NOSUID/MS_NOEXEC/MS_NODEV. */ + r = mount_verbose(LOG_ERR, ip, hd, NULL, MS_BIND, NULL); + if (r < 0) + return r; + + r = mount_verbose(LOG_ERR, NULL, hd, NULL, MS_BIND|MS_REMOUNT|user_record_mount_flags(h), NULL); + if (r < 0) { + (void) umount_verbose(hd); + return r; + } + + log_info("Everything completed."); + + *ret_home = TAKE_PTR(new_home); + return 0; +} + +int home_create_directory_or_subvolume(UserRecord *h, UserRecord **ret_home) { + _cleanup_(rm_rf_subvolume_and_freep) char *temporary = NULL; + _cleanup_(user_record_unrefp) UserRecord *new_home = NULL; + _cleanup_close_ int root_fd = -1; + _cleanup_free_ char *d = NULL; + const char *ip; + int r; + + assert(h); + assert(IN_SET(user_record_storage(h), USER_DIRECTORY, USER_SUBVOLUME)); + assert(ret_home); + + assert_se(ip = user_record_image_path(h)); + + r = tempfn_random(ip, "homework", &d); + if (r < 0) + return log_error_errno(r, "Failed to allocate temporary directory: %m"); + + (void) mkdir_parents(d, 0755); + + switch (user_record_storage(h)) { + + case USER_SUBVOLUME: + RUN_WITH_UMASK(0077) + r = btrfs_subvol_make(d); + + if (r >= 0) { + log_info("Subvolume created."); + + if (h->disk_size != UINT64_MAX) { + + /* Enable quota for the subvolume we just created. Note we don't check for + * errors here and only log about debug level about this. */ + r = btrfs_quota_enable(d, true); + if (r < 0) + log_debug_errno(r, "Failed to enable quota on %s, ignoring: %m", d); + + r = btrfs_subvol_auto_qgroup(d, 0, false); + if (r < 0) + log_debug_errno(r, "Failed to set up automatic quota group on %s, ignoring: %m", d); + + /* Actually configure the quota. We also ignore errors here, but we do log + * about them loudly, to keep things discoverable even though we don't + * consider lacking quota support in kernel fatal. */ + (void) home_update_quota_btrfs(h, d); + } + + break; + } + if (r != -ENOTTY) + return log_error_errno(r, "Failed to create temporary home directory subvolume %s: %m", d); + + log_info("Creating subvolume %s is not supported, as file system does not support subvolumes. Falling back to regular directory.", d); + _fallthrough_; + + case USER_DIRECTORY: + + if (mkdir(d, 0700) < 0) + return log_error_errno(errno, "Failed to create temporary home directory %s: %m", d); + + (void) home_update_quota_classic(h, d); + break; + + default: + assert_not_reached("unexpected storage"); + } + + temporary = TAKE_PTR(d); /* Needs to be destroyed now */ + + root_fd = open(temporary, O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW); + if (root_fd < 0) + return log_error_errno(errno, "Failed to open temporary home directory: %m"); + + r = home_populate(h, root_fd); + if (r < 0) + return r; + + r = home_sync_and_statfs(root_fd, NULL); + if (r < 0) + return r; + + r = user_record_clone(h, USER_RECORD_LOAD_MASK_SECRET, &new_home); + if (r < 0) + return log_error_errno(r, "Failed to clone record: %m"); + + r = user_record_add_binding( + new_home, + user_record_storage(h), + ip, + SD_ID128_NULL, + SD_ID128_NULL, + SD_ID128_NULL, + NULL, + NULL, + UINT64_MAX, + NULL, + NULL, + h->uid, + (gid_t) h->uid); + if (r < 0) + return log_error_errno(r, "Failed to add binding to record: %m"); + + if (rename(temporary, ip) < 0) + return log_error_errno(errno, "Failed to rename %s to %s: %m", temporary, ip); + + temporary = mfree(temporary); + + log_info("Everything completed."); + + *ret_home = TAKE_PTR(new_home); + return 0; +} + +int home_resize_directory( + UserRecord *h, + bool already_activated, + char ***pkcs11_decrypted_passwords, + HomeSetup *setup, + UserRecord **ret_home) { + + _cleanup_(user_record_unrefp) UserRecord *embedded_home = NULL, *new_home = NULL; + int r; + + assert(h); + assert(setup); + assert(ret_home); + assert(IN_SET(user_record_storage(h), USER_DIRECTORY, USER_SUBVOLUME, USER_FSCRYPT)); + + r = home_prepare(h, already_activated, pkcs11_decrypted_passwords, setup, NULL); + if (r < 0) + return r; + + r = home_load_embedded_identity(h, setup->root_fd, NULL, USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL, pkcs11_decrypted_passwords, &embedded_home, &new_home); + if (r < 0) + return r; + + r = home_update_quota_auto(h, NULL); + if (ERRNO_IS_NOT_SUPPORTED(r)) + return -ESOCKTNOSUPPORT; /* make recognizable */ + if (r < 0) + return r; + + r = home_store_embedded_identity(new_home, setup->root_fd, h->uid, embedded_home); + if (r < 0) + return r; + + r = home_extend_embedded_identity(new_home, h, setup); + if (r < 0) + return r; + + r = home_sync_and_statfs(setup->root_fd, NULL); + if (r < 0) + return r; + + r = home_setup_undo(setup); + if (r < 0) + return r; + + log_info("Everything completed."); + + *ret_home = TAKE_PTR(new_home); + return 0; +} diff --git a/src/home/homework-directory.h b/src/home/homework-directory.h new file mode 100644 index 0000000000..047c3a70a0 --- /dev/null +++ b/src/home/homework-directory.h @@ -0,0 +1,10 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include "homework.h" +#include "user-record.h" + +int home_prepare_directory(UserRecord *h, bool already_activated, HomeSetup *setup); +int home_activate_directory(UserRecord *h, char ***pkcs11_decrypted_passwords, UserRecord **ret_home); +int home_create_directory_or_subvolume(UserRecord *h, UserRecord **ret_home); +int home_resize_directory(UserRecord *h, bool already_activated, char ***pkcs11_decrypted_passwords, HomeSetup *setup, UserRecord **ret_home); diff --git a/src/home/homework-fscrypt.c b/src/home/homework-fscrypt.c new file mode 100644 index 0000000000..696e265397 --- /dev/null +++ b/src/home/homework-fscrypt.c @@ -0,0 +1,644 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include +#include +#include +#include +#include + +#include "errno-util.h" +#include "fd-util.h" +#include "hexdecoct.h" +#include "homework-fscrypt.h" +#include "homework-quota.h" +#include "memory-util.h" +#include "missing_keyctl.h" +#include "missing_syscall.h" +#include "mkdir.h" +#include "nulstr-util.h" +#include "openssl-util.h" +#include "parse-util.h" +#include "process-util.h" +#include "random-util.h" +#include "rm-rf.h" +#include "stdio-util.h" +#include "strv.h" +#include "tmpfile-util.h" +#include "user-util.h" +#include "xattr-util.h" + +static int fscrypt_upload_volume_key( + const uint8_t key_descriptor[static FS_KEY_DESCRIPTOR_SIZE], + const void *volume_key, + size_t volume_key_size, + key_serial_t where) { + + _cleanup_free_ char *hex = NULL; + const char *description; + struct fscrypt_key key; + key_serial_t serial; + + assert(key_descriptor); + assert(volume_key); + assert(volume_key_size > 0); + + if (volume_key_size > sizeof(key.raw)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Volume key too long."); + + hex = hexmem(key_descriptor, FS_KEY_DESCRIPTOR_SIZE); + if (!hex) + return log_oom(); + + description = strjoina("fscrypt:", hex); + + key = (struct fscrypt_key) { + .size = volume_key_size, + }; + memcpy(key.raw, volume_key, volume_key_size); + + /* Upload to the kernel */ + serial = add_key("logon", description, &key, sizeof(key), where); + explicit_bzero_safe(&key, sizeof(key)); + + if (serial < 0) + return log_error_errno(errno, "Failed to install master key in keyring: %m"); + + log_info("Uploaded encryption key to kernel."); + + return 0; +} + +static void calculate_key_descriptor( + const void *key, + size_t key_size, + uint8_t ret_key_descriptor[static FS_KEY_DESCRIPTOR_SIZE]) { + + uint8_t hashed[512 / 8] = {}, hashed2[512 / 8] = {}; + + /* Derive the key descriptor from the volume key via double SHA512, in order to be compatible with e4crypt */ + + assert_se(SHA512(key, key_size, hashed) == hashed); + assert_se(SHA512(hashed, sizeof(hashed), hashed2) == hashed2); + + assert_cc(sizeof(hashed2) >= FS_KEY_DESCRIPTOR_SIZE); + + memcpy(ret_key_descriptor, hashed2, FS_KEY_DESCRIPTOR_SIZE); +} + +static int fscrypt_slot_try_one( + const char *password, + const void *salt, size_t salt_size, + const void *encrypted, size_t encrypted_size, + const uint8_t match_key_descriptor[static FS_KEY_DESCRIPTOR_SIZE], + void **ret_decrypted, size_t *ret_decrypted_size) { + + + _cleanup_(EVP_CIPHER_CTX_freep) EVP_CIPHER_CTX *context = NULL; + _cleanup_(erase_and_freep) void *decrypted = NULL; + uint8_t key_descriptor[FS_KEY_DESCRIPTOR_SIZE]; + int decrypted_size_out1, decrypted_size_out2; + uint8_t derived[512 / 8] = {}; + size_t decrypted_size; + const EVP_CIPHER *cc; + int r; + + assert(password); + assert(salt); + assert(salt_size > 0); + assert(encrypted); + assert(encrypted_size > 0); + assert(match_key_descriptor); + + /* Our construction is like this: + * + * 1. In each key slot we store a salt value plus the encrypted volume key + * + * 2. Unlocking is via calculating PBKDF2-HMAC-SHA512 of the supplied password (in combination with + * the salt), then using the first 256 bit of the hash as key for decrypting the encrypted + * volume key in AES256 counter mode. + * + * 3. Writing a password is similar: calculate PBKDF2-HMAC-SHA512 of the supplied password (in + * combination with the salt), then encrypt the volume key in AES256 counter mode with the + * resulting hash. + */ + + if (PKCS5_PBKDF2_HMAC( + password, strlen(password), + salt, salt_size, + 0xFFFF, EVP_sha512(), + sizeof(derived), derived) != 1) { + r = log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "PBKDF2 failed"); + goto finish; + } + + context = EVP_CIPHER_CTX_new(); + if (!context) { + r = log_oom(); + goto finish; + } + + /* We use AES256 in counter mode */ + assert_se(cc = EVP_aes_256_ctr()); + + /* We only use the first half of the derived key */ + assert(sizeof(derived) >= (size_t) EVP_CIPHER_key_length(cc)); + + if (EVP_DecryptInit_ex(context, cc, NULL, derived, NULL) != 1) { + r = log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to initialize decryption context."); + goto finish; + } + + /* Flush out the derived key now, we don't need it anymore */ + explicit_bzero_safe(derived, sizeof(derived)); + + decrypted_size = encrypted_size + EVP_CIPHER_key_length(cc) * 2; + decrypted = malloc(decrypted_size); + if (!decrypted) + return log_oom(); + + if (EVP_DecryptUpdate(context, (uint8_t*) decrypted, &decrypted_size_out1, encrypted, encrypted_size) != 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to decrypt volume key."); + + assert((size_t) decrypted_size_out1 <= decrypted_size); + + if (EVP_DecryptFinal_ex(context, (uint8_t*) decrypted_size + decrypted_size_out1, &decrypted_size_out2) != 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to finish decryption of volume key."); + + assert((size_t) decrypted_size_out1 + (size_t) decrypted_size_out2 < decrypted_size); + decrypted_size = (size_t) decrypted_size_out1 + (size_t) decrypted_size_out2; + + calculate_key_descriptor(decrypted, decrypted_size, key_descriptor); + + if (memcmp(key_descriptor, match_key_descriptor, FS_KEY_DESCRIPTOR_SIZE) != 0) + return -ENOANO; /* don't log here */ + + r = fscrypt_upload_volume_key(key_descriptor, decrypted, decrypted_size, KEY_SPEC_THREAD_KEYRING); + if (r < 0) + return r; + + if (ret_decrypted) + *ret_decrypted = TAKE_PTR(decrypted); + if (ret_decrypted_size) + *ret_decrypted_size = decrypted_size; + + return 0; + +finish: + explicit_bzero_safe(derived, sizeof(derived)); + return r; +} + +static int fscrypt_slot_try_many( + char **passwords, + const void *salt, size_t salt_size, + const void *encrypted, size_t encrypted_size, + const uint8_t match_key_descriptor[static FS_KEY_DESCRIPTOR_SIZE], + void **ret_decrypted, size_t *ret_decrypted_size) { + + char **i; + int r; + + STRV_FOREACH(i, passwords) { + r = fscrypt_slot_try_one(*i, salt, salt_size, encrypted, encrypted_size, match_key_descriptor, ret_decrypted, ret_decrypted_size); + if (r != -ENOANO) + return r; + } + + return -ENOANO; +} + +static int fscrypt_setup( + char **pkcs11_decrypted_passwords, + char **password, + HomeSetup *setup, + void **ret_volume_key, + size_t *ret_volume_key_size) { + + _cleanup_free_ char *xattr_buf = NULL; + const char *xa; + int r; + + assert(setup); + assert(setup->root_fd >= 0); + + r = flistxattr_malloc(setup->root_fd, &xattr_buf); + if (r < 0) + return log_error_errno(errno, "Failed to retrieve xattr list: %m"); + + NULSTR_FOREACH(xa, xattr_buf) { + _cleanup_free_ void *salt = NULL, *encrypted = NULL; + _cleanup_free_ char *value = NULL; + size_t salt_size, encrypted_size; + const char *nr, *e; + int n; + + /* Check if this xattr has the format 'trusted.fscrypt_slot' where '' is a 32bit unsigned integer */ + nr = startswith(xa, "trusted.fscrypt_slot"); + if (!nr) + continue; + if (safe_atou32(nr, NULL) < 0) + continue; + + n = fgetxattr_malloc(setup->root_fd, xa, &value); + if (n == -ENODATA) /* deleted by now? */ + continue; + if (n < 0) + return log_error_errno(n, "Failed to read %s xattr: %m", xa); + + e = memchr(value, ':', n); + if (!e) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "xattr %s lacks ':' separator: %m", xa); + + r = unbase64mem(value, e - value, &salt, &salt_size); + if (r < 0) + return log_error_errno(r, "Failed to decode salt of %s: %m", xa); + r = unbase64mem(e+1, n - (e - value) - 1, &encrypted, &encrypted_size); + if (r < 0) + return log_error_errno(r, "Failed to decode encrypted key of %s: %m", xa); + + r = fscrypt_slot_try_many( + pkcs11_decrypted_passwords, + salt, salt_size, + encrypted, encrypted_size, + setup->fscrypt_key_descriptor, + ret_volume_key, ret_volume_key_size); + if (r == -ENOANO) + r = fscrypt_slot_try_many( + password, + salt, salt_size, + encrypted, encrypted_size, + setup->fscrypt_key_descriptor, + ret_volume_key, ret_volume_key_size); + if (r < 0) { + if (r != -ENOANO) + return r; + } else + return 0; + } + + return log_error_errno(SYNTHETIC_ERRNO(ENOKEY), "Failed to set up home directory with provided passwords."); +} + +int home_prepare_fscrypt( + UserRecord *h, + bool already_activated, + char ***pkcs11_decrypted_passwords, + HomeSetup *setup) { + + _cleanup_(erase_and_freep) void *volume_key = NULL; + struct fscrypt_policy policy = {}; + size_t volume_key_size = 0; + const char *ip; + int r; + + assert(h); + assert(setup); + assert(user_record_storage(h) == USER_FSCRYPT); + + assert_se(ip = user_record_image_path(h)); + + setup->root_fd = open(ip, O_RDONLY|O_CLOEXEC|O_DIRECTORY); + if (setup->root_fd < 0) + return log_error_errno(errno, "Failed to open home directory: %m"); + + if (ioctl(setup->root_fd, FS_IOC_GET_ENCRYPTION_POLICY, &policy) < 0) { + if (errno == ENODATA) + return log_error_errno(errno, "Home directory %s is not encrypted.", ip); + if (ERRNO_IS_NOT_SUPPORTED(errno)) { + log_error_errno(errno, "File system does not support fscrypt: %m"); + return -ENOLINK; /* make recognizable */ + } + return log_error_errno(errno, "Failed to acquire encryption policy of %s: %m", ip); + } + + memcpy(setup->fscrypt_key_descriptor, policy.master_key_descriptor, FS_KEY_DESCRIPTOR_SIZE); + + r = fscrypt_setup( + pkcs11_decrypted_passwords ? *pkcs11_decrypted_passwords : NULL, + h->password, + setup, + &volume_key, + &volume_key_size); + if (r < 0) + return r; + + /* Also install the access key in the user's own keyring */ + + if (uid_is_valid(h->uid)) { + r = safe_fork("(sd-addkey)", FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG|FORK_LOG|FORK_WAIT, NULL); + if (r < 0) + return log_error_errno(r, "Failed install encryption key in user's keyring: %m"); + if (r == 0) { + gid_t gid; + + /* Child */ + + gid = user_record_gid(h); + if (setresgid(gid, gid, gid) < 0) { + log_error_errno(errno, "Failed to change GID to " GID_FMT ": %m", gid); + _exit(EXIT_FAILURE); + } + + if (setgroups(0, NULL) < 0) { + log_error_errno(errno, "Failed to reset auxiliary groups list: %m"); + _exit(EXIT_FAILURE); + } + + if (setresuid(h->uid, h->uid, h->uid) < 0) { + log_error_errno(errno, "Failed to change UID to " UID_FMT ": %m", h->uid); + _exit(EXIT_FAILURE); + } + + r = fscrypt_upload_volume_key( + setup->fscrypt_key_descriptor, + volume_key, + volume_key_size, + KEY_SPEC_USER_KEYRING); + if (r < 0) + _exit(EXIT_FAILURE); + + _exit(EXIT_SUCCESS); + } + } + + return 0; +} + +static int fscrypt_slot_set( + int root_fd, + const void *volume_key, + size_t volume_key_size, + const char *password, + uint32_t nr) { + + _cleanup_free_ char *salt_base64 = NULL, *encrypted_base64 = NULL, *joined = NULL; + char label[STRLEN("trusted.fscrypt_slot") + DECIMAL_STR_MAX(nr) + 1]; + _cleanup_(EVP_CIPHER_CTX_freep) EVP_CIPHER_CTX *context = NULL; + int r, encrypted_size_out1, encrypted_size_out2; + uint8_t salt[64], derived[512 / 8] = {}; + _cleanup_free_ void *encrypted = NULL; + const EVP_CIPHER *cc; + size_t encrypted_size; + + r = genuine_random_bytes(salt, sizeof(salt), RANDOM_BLOCK); + if (r < 0) + return log_error_errno(r, "Failed to generate salt: %m"); + + if (PKCS5_PBKDF2_HMAC( + password, strlen(password), + salt, sizeof(salt), + 0xFFFF, EVP_sha512(), + sizeof(derived), derived) != 1) { + r = log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "PBKDF2 failed"); + goto finish; + } + + context = EVP_CIPHER_CTX_new(); + if (!context) { + r = log_oom(); + goto finish; + } + + /* We use AES256 in counter mode */ + cc = EVP_aes_256_ctr(); + + /* We only use the first half of the derived key */ + assert(sizeof(derived) >= (size_t) EVP_CIPHER_key_length(cc)); + + if (EVP_EncryptInit_ex(context, cc, NULL, derived, NULL) != 1) { + r = log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to initialize encryption context."); + goto finish; + } + + /* Flush out the derived key now, we don't need it anymore */ + explicit_bzero_safe(derived, sizeof(derived)); + + encrypted_size = volume_key_size + EVP_CIPHER_key_length(cc) * 2; + encrypted = malloc(encrypted_size); + if (!encrypted) + return log_oom(); + + if (EVP_EncryptUpdate(context, (uint8_t*) encrypted, &encrypted_size_out1, volume_key, volume_key_size) != 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to encrypt volume key."); + + assert((size_t) encrypted_size_out1 <= encrypted_size); + + if (EVP_EncryptFinal_ex(context, (uint8_t*) encrypted_size + encrypted_size_out1, &encrypted_size_out2) != 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to finish encryption of volume key."); + + assert((size_t) encrypted_size_out1 + (size_t) encrypted_size_out2 < encrypted_size); + encrypted_size = (size_t) encrypted_size_out1 + (size_t) encrypted_size_out2; + + r = base64mem(salt, sizeof(salt), &salt_base64); + if (r < 0) + return log_oom(); + + r = base64mem(encrypted, encrypted_size, &encrypted_base64); + if (r < 0) + return log_oom(); + + joined = strjoin(salt_base64, ":", encrypted_base64); + if (!joined) + return log_oom(); + + xsprintf(label, "trusted.fscrypt_slot%" PRIu32, nr); + if (fsetxattr(root_fd, label, joined, strlen(joined), 0) < 0) + return log_error_errno(errno, "Failed to write xattr %s: %m", label); + + log_info("Written key slot %s.", label); + + return 0; + +finish: + explicit_bzero_safe(derived, sizeof(derived)); + return r; +} + +int home_create_fscrypt( + UserRecord *h, + char **effective_passwords, + UserRecord **ret_home) { + + _cleanup_(rm_rf_physical_and_freep) char *temporary = NULL; + _cleanup_(user_record_unrefp) UserRecord *new_home = NULL; + _cleanup_(erase_and_freep) void *volume_key = NULL; + struct fscrypt_policy policy = {}; + size_t volume_key_size = 512 / 8; + _cleanup_close_ int root_fd = -1; + _cleanup_free_ char *d = NULL; + uint32_t nr = 0; + const char *ip; + char **i; + int r; + + assert(h); + assert(user_record_storage(h) == USER_FSCRYPT); + assert(ret_home); + + assert_se(ip = user_record_image_path(h)); + + r = tempfn_random(ip, "homework", &d); + if (r < 0) + return log_error_errno(r, "Failed to allocate temporary directory: %m"); + + (void) mkdir_parents(d, 0755); + + if (mkdir(d, 0700) < 0) + return log_error_errno(errno, "Failed to create temporary home directory %s: %m", d); + + temporary = TAKE_PTR(d); /* Needs to be destroyed now */ + + root_fd = open(temporary, O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW); + if (root_fd < 0) + return log_error_errno(errno, "Failed to open temporary home directory: %m"); + + if (ioctl(root_fd, FS_IOC_GET_ENCRYPTION_POLICY, &policy) < 0) { + if (ERRNO_IS_NOT_SUPPORTED(errno)) { + log_error_errno(errno, "File system does not support fscrypt: %m"); + return -ENOLINK; /* make recognizable */ + } + if (errno != ENODATA) + return log_error_errno(errno, "Failed to get fscrypt policy of directory: %m"); + } else + return log_error_errno(SYNTHETIC_ERRNO(EBUSY), "Parent of %s already encrypted, refusing.", d); + + volume_key = malloc(volume_key_size); + if (!volume_key) + return log_oom(); + + r = genuine_random_bytes(volume_key, volume_key_size, RANDOM_BLOCK); + if (r < 0) + return log_error_errno(r, "Failed to acquire volume key: %m"); + + log_info("Generated volume key of size %zu.", volume_key_size); + + policy = (struct fscrypt_policy) { + .contents_encryption_mode = FS_ENCRYPTION_MODE_AES_256_XTS, + .filenames_encryption_mode = FS_ENCRYPTION_MODE_AES_256_CTS, + .flags = FS_POLICY_FLAGS_PAD_32, + }; + + calculate_key_descriptor(volume_key, volume_key_size, policy.master_key_descriptor); + + r = fscrypt_upload_volume_key(policy.master_key_descriptor, volume_key, volume_key_size, KEY_SPEC_THREAD_KEYRING); + if (r < 0) + return r; + + log_info("Uploaded volume key to kernel."); + + if (ioctl(root_fd, FS_IOC_SET_ENCRYPTION_POLICY, &policy) < 0) + return log_error_errno(errno, "Failed to set fscrypt policy on directory: %m"); + + log_info("Encryption policy set."); + + STRV_FOREACH(i, effective_passwords) { + r = fscrypt_slot_set(root_fd, volume_key, volume_key_size, *i, nr); + if (r < 0) + return r; + + nr++; + } + + (void) home_update_quota_classic(h, temporary); + + r = home_populate(h, root_fd); + if (r < 0) + return r; + + r = home_sync_and_statfs(root_fd, NULL); + if (r < 0) + return r; + + r = user_record_clone(h, USER_RECORD_LOAD_MASK_SECRET, &new_home); + if (r < 0) + return log_error_errno(r, "Failed to clone record: %m"); + + r = user_record_add_binding( + new_home, + USER_FSCRYPT, + ip, + SD_ID128_NULL, + SD_ID128_NULL, + SD_ID128_NULL, + NULL, + NULL, + UINT64_MAX, + NULL, + NULL, + h->uid, + (gid_t) h->uid); + if (r < 0) + return log_error_errno(r, "Failed to add binding to record: %m"); + + if (rename(temporary, ip) < 0) + return log_error_errno(errno, "Failed to rename %s to %s: %m", temporary, ip); + + temporary = mfree(temporary); + + log_info("Everything completed."); + + *ret_home = TAKE_PTR(new_home); + return 0; +} + +int home_passwd_fscrypt( + UserRecord *h, + HomeSetup *setup, + char **pkcs11_decrypted_passwords, /* the passwords acquired via PKCS#11 security tokens */ + char **effective_passwords /* new passwords */) { + + _cleanup_(erase_and_freep) void *volume_key = NULL; + _cleanup_free_ char *xattr_buf = NULL; + size_t volume_key_size = 0; + uint32_t slot = 0; + const char *xa; + char **p; + int r; + + assert(h); + assert(user_record_storage(h) == USER_FSCRYPT); + assert(setup); + + r = fscrypt_setup( + pkcs11_decrypted_passwords, + h->password, + setup, + &volume_key, + &volume_key_size); + if (r < 0) + return r; + + STRV_FOREACH(p, effective_passwords) { + r = fscrypt_slot_set(setup->root_fd, volume_key, volume_key_size, *p, slot); + if (r < 0) + return r; + + slot++; + } + + r = flistxattr_malloc(setup->root_fd, &xattr_buf); + if (r < 0) + return log_error_errno(errno, "Failed to retrieve xattr list: %m"); + + NULSTR_FOREACH(xa, xattr_buf) { + const char *nr; + uint32_t z; + + /* Check if this xattr has the format 'trusted.fscrypt_slot' where '' is a 32bit unsigned integer */ + nr = startswith(xa, "trusted.fscrypt_slot"); + if (!nr) + continue; + if (safe_atou32(nr, &z) < 0) + continue; + + if (z < slot) + continue; + + if (fremovexattr(setup->root_fd, xa) < 0) + + if (errno != ENODATA) + log_warning_errno(errno, "Failed to remove xattr %s: %m", xa); + } + + return 0; +} diff --git a/src/home/homework-fscrypt.h b/src/home/homework-fscrypt.h new file mode 100644 index 0000000000..aa3bcd3a69 --- /dev/null +++ b/src/home/homework-fscrypt.h @@ -0,0 +1,10 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include "homework.h" +#include "user-record.h" + +int home_prepare_fscrypt(UserRecord *h, bool already_activated, char ***pkcs11_decrypted_passwords, HomeSetup *setup); +int home_create_fscrypt(UserRecord *h, char **effective_passwords, UserRecord **ret_home); + +int home_passwd_fscrypt(UserRecord *h, HomeSetup *setup, char **pkcs11_decrypted_passwords, char **effective_passwords); diff --git a/src/home/homework-luks.c b/src/home/homework-luks.c new file mode 100644 index 0000000000..0cd5902bff --- /dev/null +++ b/src/home/homework-luks.c @@ -0,0 +1,2954 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include +#include +#include +#include +#include + +#include "blkid-util.h" +#include "blockdev-util.h" +#include "chattr-util.h" +#include "dm-util.h" +#include "errno-util.h" +#include "fd-util.h" +#include "fileio.h" +#include "fs-util.h" +#include "fsck-util.h" +#include "homework-luks.h" +#include "homework-mount.h" +#include "id128-util.h" +#include "io-util.h" +#include "memory-util.h" +#include "missing_magic.h" +#include "mkdir.h" +#include "mount-util.h" +#include "openssl-util.h" +#include "parse-util.h" +#include "path-util.h" +#include "process-util.h" +#include "random-util.h" +#include "resize-fs.h" +#include "stat-util.h" +#include "strv.h" +#include "tmpfile-util.h" + +/* Round down to the nearest 1K size. Note that Linux generally handles block devices with 512 blocks only, + * but actually doesn't accept uneven numbers in many cases. To avoid any confusion around this we'll + * strictly round disk sizes down to the next 1K boundary.*/ +#define DISK_SIZE_ROUND_DOWN(x) ((x) & ~UINT64_C(1023)) + +static bool supported_fstype(const char *fstype) { + /* Limit the set of supported file systems a bit, as protection against little tested kernel file + * systems. Also, we only support the resize ioctls for these file systems. */ + return STR_IN_SET(fstype, "ext4", "btrfs", "xfs"); +} + +static int probe_file_system_by_fd( + int fd, + char **ret_fstype, + sd_id128_t *ret_uuid) { + + _cleanup_(blkid_free_probep) blkid_probe b = NULL; + _cleanup_free_ char *s = NULL; + const char *fstype = NULL, *uuid = NULL; + sd_id128_t id; + int r; + + assert(fd >= 0); + assert(ret_fstype); + assert(ret_uuid); + + b = blkid_new_probe(); + if (!b) + return -ENOMEM; + + errno = 0; + r = blkid_probe_set_device(b, fd, 0, 0); + if (r != 0) + return errno > 0 ? -errno : -ENOMEM; + + (void) blkid_probe_enable_superblocks(b, 1); + (void) blkid_probe_set_superblocks_flags(b, BLKID_SUBLKS_TYPE|BLKID_SUBLKS_UUID); + + errno = 0; + r = blkid_do_safeprobe(b); + if (IN_SET(r, -2, 1)) /* nothing found or ambiguous result */ + return -ENOPKG; + if (r != 0) + return errno > 0 ? -errno : -EIO; + + (void) blkid_probe_lookup_value(b, "TYPE", &fstype, NULL); + if (!fstype) + return -ENOPKG; + + (void) blkid_probe_lookup_value(b, "UUID", &uuid, NULL); + if (!uuid) + return -ENOPKG; + + r = sd_id128_from_string(uuid, &id); + if (r < 0) + return r; + + s = strdup(fstype); + if (!s) + return -ENOMEM; + + *ret_fstype = TAKE_PTR(s); + *ret_uuid = id; + + return 0; +} + +static int probe_file_system_by_path(const char *path, char **ret_fstype, sd_id128_t *ret_uuid) { + _cleanup_close_ int fd = -1; + + fd = open(path, O_RDONLY|O_CLOEXEC|O_NOCTTY|O_NONBLOCK); + if (fd < 0) + return -errno; + + return probe_file_system_by_fd(fd, ret_fstype, ret_uuid); +} + +static int block_get_size_by_fd(int fd, uint64_t *ret) { + struct stat st; + + assert(fd >= 0); + assert(ret); + + if (fstat(fd, &st) < 0) + return -errno; + + if (!S_ISBLK(st.st_mode)) + return -ENOTBLK; + + if (ioctl(fd, BLKGETSIZE64, ret) < 0) + return -errno; + + return 0; +} + +static int block_get_size_by_path(const char *path, uint64_t *ret) { + _cleanup_close_ int fd = -1; + + fd = open(path, O_RDONLY|O_CLOEXEC|O_NOCTTY|O_NONBLOCK); + if (fd < 0) + return -errno; + + return block_get_size_by_fd(fd, ret); +} + +static int run_fsck(const char *node, const char *fstype) { + int r, exit_status; + pid_t fsck_pid; + + assert(node); + assert(fstype); + + r = fsck_exists(fstype); + if (r < 0) + return log_error_errno(r, "Failed to check if fsck for file system %s exists: %m", fstype); + if (r == 0) { + log_warning("No fsck for file system %s installed, ignoring.", fstype); + return 0; + } + + r = safe_fork("(fsck)", FORK_RESET_SIGNALS|FORK_RLIMIT_NOFILE_SAFE|FORK_DEATHSIG|FORK_LOG|FORK_STDOUT_TO_STDERR, &fsck_pid); + if (r < 0) + return r; + if (r == 0) { + /* Child */ + execl("/sbin/fsck", "/sbin/fsck", "-aTl", node, NULL); + log_error_errno(errno, "Failed to execute fsck: %m"); + _exit(FSCK_OPERATIONAL_ERROR); + } + + exit_status = wait_for_terminate_and_check("fsck", fsck_pid, WAIT_LOG_ABNORMAL); + if (exit_status < 0) + return exit_status; + if ((exit_status & ~FSCK_ERROR_CORRECTED) != 0) { + log_warning("fsck failed with exit status %i.", exit_status); + + if ((exit_status & (FSCK_SYSTEM_SHOULD_REBOOT|FSCK_ERRORS_LEFT_UNCORRECTED)) != 0) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "File system is corrupted, refusing."); + + log_warning("Ignoring fsck error."); + } + + log_info("File system check completed."); + + return 1; +} + +static int luks_try_passwords( + struct crypt_device *cd, + char **passwords, + void *volume_key, + size_t *volume_key_size) { + + char **pp; + int r; + + assert(cd); + + STRV_FOREACH(pp, passwords) { + size_t vks = *volume_key_size; + + r = crypt_volume_key_get( + cd, + CRYPT_ANY_SLOT, + volume_key, + &vks, + *pp, + strlen(*pp)); + if (r >= 0) { + *volume_key_size = vks; + return 0; + } + + log_debug_errno(r, "Password %zu didn't work for unlocking LUKS superblock: %m", (size_t) (pp - passwords)); + } + + return -ENOKEY; +} + +static int luks_setup( + const char *node, + const char *dm_name, + sd_id128_t uuid, + const char *cipher, + const char *cipher_mode, + uint64_t volume_key_size, + char **passwords, + char **pkcs11_decrypted_passwords, + bool discard, + struct crypt_device **ret, + sd_id128_t *ret_found_uuid, + void **ret_volume_key, + size_t *ret_volume_key_size) { + + _cleanup_(crypt_freep) struct crypt_device *cd = NULL; + _cleanup_(erase_and_freep) void *vk = NULL; + sd_id128_t p; + size_t vks; + int r; + + assert(node); + assert(dm_name); + assert(ret); + + r = crypt_init(&cd, node); + if (r < 0) + return log_error_errno(r, "Failed to allocate libcryptsetup context: %m"); + + crypt_set_log_callback(cd, cryptsetup_log_glue, NULL); + + r = crypt_load(cd, CRYPT_LUKS2, NULL); + if (r < 0) + return log_error_errno(r, "Failed to load LUKS superblock: %m"); + + r = crypt_get_volume_key_size(cd); + if (r <= 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to determine LUKS volume key size"); + vks = (size_t) r; + + if (!sd_id128_is_null(uuid) || ret_found_uuid) { + const char *s; + + s = crypt_get_uuid(cd); + if (!s) + return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "LUKS superblock has no UUID."); + + r = sd_id128_from_string(s, &p); + if (r < 0) + return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "LUKS superblock has invalid UUID."); + + /* Check that the UUID matches, if specified */ + if (!sd_id128_is_null(uuid) && + !sd_id128_equal(uuid, p)) + return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "LUKS superblock has wrong UUID."); + } + + if (cipher && !streq_ptr(cipher, crypt_get_cipher(cd))) + return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "LUKS superblock declares wrong cipher."); + + if (cipher_mode && !streq_ptr(cipher_mode, crypt_get_cipher_mode(cd))) + return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "LUKS superblock declares wrong cipher mode."); + + if (volume_key_size != UINT64_MAX && vks != volume_key_size) + return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "LUKS superblock declares wrong volume key size."); + + vk = malloc(vks); + if (!vk) + return log_oom(); + + r = luks_try_passwords(cd, pkcs11_decrypted_passwords, vk, &vks); + if (r == -ENOKEY) { + r = luks_try_passwords(cd, passwords, vk, &vks); + if (r == -ENOKEY) + return log_error_errno(r, "No valid password for LUKS superblock."); + } + if (r < 0) + return log_error_errno(r, "Failed to unlocks LUKS superblock: %m"); + + r = crypt_activate_by_volume_key( + cd, + dm_name, + vk, vks, + discard ? CRYPT_ACTIVATE_ALLOW_DISCARDS : 0); + if (r < 0) + return log_error_errno(r, "Failed to unlock LUKS superblock: %m"); + + log_info("Setting up LUKS device /dev/mapper/%s completed.", dm_name); + + *ret = TAKE_PTR(cd); + + if (ret_found_uuid) /* Return the UUID actually found if the caller wants to know */ + *ret_found_uuid = p; + if (ret_volume_key) + *ret_volume_key = TAKE_PTR(vk); + if (ret_volume_key_size) + *ret_volume_key_size = vks; + + return 0; +} + +static int luks_open( + const char *dm_name, + char **passwords, + char **pkcs11_decrypted_passwords, + struct crypt_device **ret, + sd_id128_t *ret_found_uuid, + void **ret_volume_key, + size_t *ret_volume_key_size) { + + _cleanup_(crypt_freep) struct crypt_device *cd = NULL; + _cleanup_(erase_and_freep) void *vk = NULL; + sd_id128_t p; + size_t vks; + int r; + + assert(dm_name); + assert(ret); + + /* Opens a LUKS device that is already set up. Re-validates the password while doing so (which also + * provides us with the volume key, which we want). */ + + r = crypt_init_by_name(&cd, dm_name); + if (r < 0) + return log_error_errno(r, "Failed to initialize cryptsetup context for %s: %m", dm_name); + + crypt_set_log_callback(cd, cryptsetup_log_glue, NULL); + + r = crypt_load(cd, CRYPT_LUKS2, NULL); + if (r < 0) + return log_error_errno(r, "Failed to load LUKS superblock: %m"); + + r = crypt_get_volume_key_size(cd); + if (r <= 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to determine LUKS volume key size"); + vks = (size_t) r; + + if (ret_found_uuid) { + const char *s; + + s = crypt_get_uuid(cd); + if (!s) + return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "LUKS superblock has no UUID."); + + r = sd_id128_from_string(s, &p); + if (r < 0) + return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "LUKS superblock has invalid UUID."); + } + + vk = malloc(vks); + if (!vk) + return log_oom(); + + r = luks_try_passwords(cd, pkcs11_decrypted_passwords, vk, &vks); + if (r == -ENOKEY) { + r = luks_try_passwords(cd, passwords, vk, &vks); + if (r == -ENOKEY) + return log_error_errno(r, "No valid password for LUKS superblock."); + } + if (r < 0) + return log_error_errno(r, "Failed to unlocks LUKS superblock: %m"); + + log_info("Discovered used LUKS device /dev/mapper/%s, and validated password.", dm_name); + + /* This is needed so that crypt_resize() can operate correctly for pre-existing LUKS devices. We need + * to tell libcryptsetup the volume key explicitly, so that it is in the kernel keyring. */ + r = crypt_activate_by_volume_key(cd, NULL, vk, vks, CRYPT_ACTIVATE_KEYRING_KEY); + if (r < 0) + return log_error_errno(r, "Failed to upload volume key again: %m"); + + log_info("Successfully re-activated LUKS device."); + + *ret = TAKE_PTR(cd); + + if (ret_found_uuid) + *ret_found_uuid = p; + if (ret_volume_key) + *ret_volume_key = TAKE_PTR(vk); + if (ret_volume_key_size) + *ret_volume_key_size = vks; + + return 0; +} + +static int fs_validate( + const char *dm_node, + sd_id128_t uuid, + char **ret_fstype, + sd_id128_t *ret_found_uuid) { + + _cleanup_free_ char *fstype = NULL; + sd_id128_t u; + int r; + + assert(dm_node); + assert(ret_fstype); + + r = probe_file_system_by_path(dm_node, &fstype, &u); + if (r < 0) + return log_error_errno(r, "Failed to probe file system: %m"); + + /* Limit the set of supported file systems a bit, as protection against little tested kernel file + * systems. Also, we only support the resize ioctls for these file systems. */ + if (!supported_fstype(fstype)) + return log_error_errno(SYNTHETIC_ERRNO(EPROTONOSUPPORT), "Image contains unsupported file system: %s", strna(fstype)); + + if (!sd_id128_is_null(uuid) && + !sd_id128_equal(uuid, u)) + return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "File system has wrong UUID."); + + log_info("Probing file system completed (found %s).", fstype); + + *ret_fstype = TAKE_PTR(fstype); + + if (ret_found_uuid) /* Return the UUID actually found if the caller wants to know */ + *ret_found_uuid = u; + + return 0; +} + +static int make_dm_names(const char *user_name, char **ret_dm_name, char **ret_dm_node) { + _cleanup_free_ char *name = NULL, *node = NULL; + + assert(user_name); + assert(ret_dm_name); + assert(ret_dm_node); + + name = strjoin("home-", user_name); + if (!name) + return log_oom(); + + node = path_join("/dev/mapper/", name); + if (!node) + return log_oom(); + + *ret_dm_name = TAKE_PTR(name); + *ret_dm_node = TAKE_PTR(node); + return 0; +} + +static int luks_validate( + int fd, + const char *label, + sd_id128_t partition_uuid, + sd_id128_t *ret_partition_uuid, + uint64_t *ret_offset, + uint64_t *ret_size) { + + _cleanup_(blkid_free_probep) blkid_probe b = NULL; + sd_id128_t found_partition_uuid = SD_ID128_NULL; + const char *fstype = NULL, *pttype = NULL; + blkid_loff_t offset = 0, size = 0; + blkid_partlist pl; + bool found = false; + int r, i, n; + + assert(fd >= 0); + assert(label); + assert(ret_offset); + assert(ret_size); + + b = blkid_new_probe(); + if (!b) + return -ENOMEM; + + errno = 0; + r = blkid_probe_set_device(b, fd, 0, 0); + if (r != 0) + return errno > 0 ? -errno : -ENOMEM; + + (void) blkid_probe_enable_superblocks(b, 1); + (void) blkid_probe_set_superblocks_flags(b, BLKID_SUBLKS_TYPE); + (void) blkid_probe_enable_partitions(b, 1); + (void) blkid_probe_set_partitions_flags(b, BLKID_PARTS_ENTRY_DETAILS); + + errno = 0; + r = blkid_do_safeprobe(b); + if (IN_SET(r, -2, 1)) /* nothing found or ambiguous result */ + return -ENOPKG; + if (r != 0) + return errno > 0 ? -errno : -EIO; + + (void) blkid_probe_lookup_value(b, "TYPE", &fstype, NULL); + if (streq_ptr(fstype, "crypto_LUKS")) { + /* Directly a LUKS image */ + *ret_offset = 0; + *ret_size = UINT64_MAX; /* full disk */ + *ret_partition_uuid = SD_ID128_NULL; + return 0; + } else if (fstype) + return -ENOPKG; + + (void) blkid_probe_lookup_value(b, "PTTYPE", &pttype, NULL); + if (!streq_ptr(pttype, "gpt")) + return -ENOPKG; + + errno = 0; + pl = blkid_probe_get_partitions(b); + if (!pl) + return errno > 0 ? -errno : -ENOMEM; + + errno = 0; + n = blkid_partlist_numof_partitions(pl); + if (n < 0) + return errno > 0 ? -errno : -EIO; + + for (i = 0; i < n; i++) { + blkid_partition pp; + sd_id128_t id; + const char *sid; + + errno = 0; + pp = blkid_partlist_get_partition(pl, i); + if (!pp) + return errno > 0 ? -errno : -EIO; + + if (!streq_ptr(blkid_partition_get_type_string(pp), "773f91ef-66d4-49b5-bd83-d683bf40ad16")) + continue; + + if (!streq_ptr(blkid_partition_get_name(pp), label)) + continue; + + sid = blkid_partition_get_uuid(pp); + if (sid) { + r = sd_id128_from_string(sid, &id); + if (r < 0) + log_debug_errno(r, "Couldn't parse partition UUID %s, weird: %m", sid); + + if (!sd_id128_is_null(partition_uuid) && !sd_id128_equal(id, partition_uuid)) + continue; + } + + if (found) + return -ENOPKG; + + offset = blkid_partition_get_start(pp); + size = blkid_partition_get_size(pp); + found_partition_uuid = id; + + found = true; + } + + if (!found) + return -ENOPKG; + + if (offset < 0) + return -EINVAL; + if ((uint64_t) offset > UINT64_MAX / 512U) + return -EINVAL; + if (size <= 0) + return -EINVAL; + if ((uint64_t) size > UINT64_MAX / 512U) + return -EINVAL; + + *ret_offset = offset * 512U; + *ret_size = size * 512U; + *ret_partition_uuid = found_partition_uuid; + + return 0; +} + +static int crypt_device_to_evp_cipher(struct crypt_device *cd, const EVP_CIPHER **ret) { + _cleanup_free_ char *cipher_name = NULL; + const char *cipher, *cipher_mode, *e; + size_t key_size, key_bits; + const EVP_CIPHER *cc; + int r; + + assert(cd); + + /* Let's find the right OpenSSL EVP_CIPHER object that matches the encryption settings of the LUKS + * device */ + + cipher = crypt_get_cipher(cd); + if (!cipher) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Cannot get cipher from LUKS device."); + + cipher_mode = crypt_get_cipher_mode(cd); + if (!cipher_mode) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Cannot get cipher mode from LUKS device."); + + e = strchr(cipher_mode, '-'); + if (e) + cipher_mode = strndupa(cipher_mode, e - cipher_mode); + + r = crypt_get_volume_key_size(cd); + if (r <= 0) + return log_error_errno(r < 0 ? r : SYNTHETIC_ERRNO(EINVAL), "Cannot get volume key size from LUKS device."); + + key_size = r; + key_bits = key_size * 8; + if (streq(cipher_mode, "xts")) + key_bits /= 2; + + if (asprintf(&cipher_name, "%s-%zu-%s", cipher, key_bits, cipher_mode) < 0) + return log_oom(); + + cc = EVP_get_cipherbyname(cipher_name); + if (!cc) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Selected cipher mode '%s' not supported, can't encrypt JSON record.", cipher_name); + + /* Verify that our key length calculations match what OpenSSL thinks */ + r = EVP_CIPHER_key_length(cc); + if (r < 0 || (uint64_t) r != key_size) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Key size of selected cipher doesn't meet out expectations."); + + *ret = cc; + return 0; +} + +static int luks_validate_home_record( + struct crypt_device *cd, + UserRecord *h, + const void *volume_key, + char ***pkcs11_decrypted_passwords, + UserRecord **ret_luks_home_record) { + + int r, token; + + assert(cd); + assert(h); + + for (token = 0;; token++) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *rr = NULL; + _cleanup_(EVP_CIPHER_CTX_freep) EVP_CIPHER_CTX *context = NULL; + _cleanup_(user_record_unrefp) UserRecord *lhr = NULL; + _cleanup_free_ void *encrypted = NULL, *iv = NULL; + size_t decrypted_size, encrypted_size, iv_size; + int decrypted_size_out1, decrypted_size_out2; + _cleanup_free_ char *decrypted = NULL; + const char *text, *type; + crypt_token_info state; + JsonVariant *jr, *jiv; + unsigned line, column; + const EVP_CIPHER *cc; + + state = crypt_token_status(cd, token, &type); + if (state == CRYPT_TOKEN_INACTIVE) /* First unconfigured token, give up */ + break; + if (IN_SET(state, CRYPT_TOKEN_INTERNAL, CRYPT_TOKEN_INTERNAL_UNKNOWN, CRYPT_TOKEN_EXTERNAL)) + continue; + if (state != CRYPT_TOKEN_EXTERNAL_UNKNOWN) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unexpected token state of token %i: %i", token, (int) state); + + if (!streq(type, "systemd-homed")) + continue; + + r = crypt_token_json_get(cd, token, &text); + if (r < 0) + return log_error_errno(r, "Failed to read LUKS token %i: %m", token); + + r = json_parse(text, JSON_PARSE_SENSITIVE, &v, &line, &column); + if (r < 0) + return log_error_errno(r, "Failed to parse LUKS token JSON data %u:%u: %m", line, column); + + jr = json_variant_by_key(v, "record"); + if (!jr) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "LUKS token lacks 'record' field."); + jiv = json_variant_by_key(v, "iv"); + if (!jiv) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "LUKS token lacks 'iv' field."); + + r = json_variant_unbase64(jr, &encrypted, &encrypted_size); + if (r < 0) + return log_error_errno(r, "Failed to base64 decode record: %m"); + + r = json_variant_unbase64(jiv, &iv, &iv_size); + if (r < 0) + return log_error_errno(r, "Failed to base64 decode IV: %m"); + + r = crypt_device_to_evp_cipher(cd, &cc); + if (r < 0) + return r; + if (iv_size > INT_MAX || EVP_CIPHER_iv_length(cc) != (int) iv_size) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "IV size doesn't match."); + + context = EVP_CIPHER_CTX_new(); + if (!context) + return log_oom(); + + if (EVP_DecryptInit_ex(context, cc, NULL, volume_key, iv) != 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to initialize decryption context."); + + decrypted_size = encrypted_size + EVP_CIPHER_key_length(cc) * 2; + decrypted = new(char, decrypted_size); + if (!decrypted) + return log_oom(); + + if (EVP_DecryptUpdate(context, (uint8_t*) decrypted, &decrypted_size_out1, encrypted, encrypted_size) != 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to decrypt JSON record."); + + assert((size_t) decrypted_size_out1 <= decrypted_size); + + if (EVP_DecryptFinal_ex(context, (uint8_t*) decrypted + decrypted_size_out1, &decrypted_size_out2) != 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to finish decryption of JSON record."); + + assert((size_t) decrypted_size_out1 + (size_t) decrypted_size_out2 < decrypted_size); + decrypted_size = (size_t) decrypted_size_out1 + (size_t) decrypted_size_out2; + + if (memchr(decrypted, 0, decrypted_size)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Inner NUL byte in JSON record, refusing."); + + decrypted[decrypted_size] = 0; + + r = json_parse(decrypted, JSON_PARSE_SENSITIVE, &rr, NULL, NULL); + if (r < 0) + return log_error_errno(r, "Failed to parse decrypted JSON record, refusing."); + + lhr = user_record_new(); + if (!lhr) + return log_oom(); + + r = user_record_load(lhr, rr, USER_RECORD_LOAD_EMBEDDED); + if (r < 0) + return log_error_errno(r, "Failed to parse user record: %m"); + + if (!user_record_compatible(h, lhr)) + return log_error_errno(SYNTHETIC_ERRNO(EREMCHG), "LUKS home record not compatible with host record, refusing."); + + r = user_record_authenticate(lhr, h, pkcs11_decrypted_passwords); + if (r < 0) + return r; + + *ret_luks_home_record = TAKE_PTR(lhr); + return 0; + } + + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Couldn't find home record in LUKS2 header, refusing."); +} + +static int format_luks_token_text( + struct crypt_device *cd, + UserRecord *hr, + const void *volume_key, + char **ret) { + + int r, encrypted_size_out1 = 0, encrypted_size_out2 = 0, iv_size, key_size; + _cleanup_(EVP_CIPHER_CTX_freep) EVP_CIPHER_CTX *context = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + _cleanup_free_ void *iv = NULL, *encrypted = NULL; + size_t text_length, encrypted_size; + _cleanup_free_ char *text = NULL; + const EVP_CIPHER *cc; + + assert(cd); + assert(hr); + assert(volume_key); + assert(ret); + + r = crypt_device_to_evp_cipher(cd, &cc); + if (r < 0) + return r; + + key_size = EVP_CIPHER_key_length(cc); + iv_size = EVP_CIPHER_iv_length(cc); + + if (iv_size > 0) { + iv = malloc(iv_size); + if (!iv) + return log_oom(); + + r = genuine_random_bytes(iv, iv_size, RANDOM_BLOCK); + if (r < 0) + return log_error_errno(r, "Failed to generate IV: %m"); + } + + context = EVP_CIPHER_CTX_new(); + if (!context) + return log_oom(); + + if (EVP_EncryptInit_ex(context, cc, NULL, volume_key, iv) != 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to initialize encryption context."); + + r = json_variant_format(hr->json, 0, &text); + if (r < 0) + return log_error_errno(r,"Failed to format user record for LUKS: %m"); + + text_length = strlen(text); + encrypted_size = text_length + 2*key_size - 1; + + encrypted = malloc(encrypted_size); + if (!encrypted) + return log_oom(); + + if (EVP_EncryptUpdate(context, encrypted, &encrypted_size_out1, (uint8_t*) text, text_length) != 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to encrypt JSON record."); + + assert((size_t) encrypted_size_out1 <= encrypted_size); + + if (EVP_EncryptFinal_ex(context, (uint8_t*) encrypted + encrypted_size_out1, &encrypted_size_out2) != 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to finish encryption of JSON record. "); + + assert((size_t) encrypted_size_out1 + (size_t) encrypted_size_out2 <= encrypted_size); + + r = json_build(&v, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("type", JSON_BUILD_STRING("systemd-homed")), + JSON_BUILD_PAIR("keyslots", JSON_BUILD_EMPTY_ARRAY), + JSON_BUILD_PAIR("record", JSON_BUILD_BASE64(encrypted, encrypted_size_out1 + encrypted_size_out2)), + JSON_BUILD_PAIR("iv", JSON_BUILD_BASE64(iv, iv_size)))); + if (r < 0) + return log_error_errno(r, "Failed to prepare LUKS JSON token object: %m"); + + r = json_variant_format(v, 0, ret); + if (r < 0) + return log_error_errno(r, "Failed to format encrypted user record for LUKS: %m"); + + return 0; +} + +int home_store_header_identity_luks( + UserRecord *h, + HomeSetup *setup, + UserRecord *old_home) { + + _cleanup_(user_record_unrefp) UserRecord *header_home = NULL; + _cleanup_free_ char *text = NULL; + int token = 0, r; + + assert(h); + + if (!setup->crypt_device) + return 0; + + assert(setup->volume_key); + + /* Let's store the user's identity record in the LUKS2 "token" header data fields, in an encrypted + * fashion. Why that? If we'd rely on the record being embedded in the payload file system itself we + * would have to mount the file system before we can validate the JSON record, its signatures and + * whether it matches what we are looking for. However, kernel file system implementations are + * generally not ready to be used on untrusted media. Hence let's store the record independently of + * the file system, so that we can validate it first, and only then mount the file system. To keep + * things simple we use the same encryption settings for this record as for the file system itself. */ + + r = user_record_clone(h, USER_RECORD_EXTRACT_EMBEDDED, &header_home); + if (r < 0) + return log_error_errno(r, "Failed to determine new header record: %m"); + + if (old_home && user_record_equal(old_home, header_home)) { + log_debug("Not updating header home record."); + return 0; + } + + r = format_luks_token_text(setup->crypt_device, header_home, setup->volume_key, &text); + if (r < 0) + return r; + + for (;; token++) { + crypt_token_info state; + const char *type; + + state = crypt_token_status(setup->crypt_device, token, &type); + if (state == CRYPT_TOKEN_INACTIVE) /* First unconfigured token, we are done */ + break; + if (IN_SET(state, CRYPT_TOKEN_INTERNAL, CRYPT_TOKEN_INTERNAL_UNKNOWN, CRYPT_TOKEN_EXTERNAL)) + continue; /* Not ours */ + if (state != CRYPT_TOKEN_EXTERNAL_UNKNOWN) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unexpected token state of token %i: %i", token, (int) state); + + if (!streq(type, "systemd-homed")) + continue; + + r = crypt_token_json_set(setup->crypt_device, token, text); + if (r < 0) + return log_error_errno(r, "Failed to set JSON token for slot %i: %m", token); + + /* Now, let's free the text so that for all further matching tokens we all crypt_json_token_set() + * with a NULL text in order to invalidate the tokens. */ + text = mfree(text); + token++; + } + + if (text) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Didn't find any record token to update."); + + log_info("Wrote LUKS header user record."); + + return 1; +} + +static int run_fitrim(int root_fd) { + char buf[FORMAT_BYTES_MAX]; + struct fstrim_range range = { + .len = UINT64_MAX, + }; + + /* If discarding is on, discard everything right after mounting, so that the discard setting takes + * effect on activation. */ + + assert(root_fd >= 0); + + if (ioctl(root_fd, FITRIM, &range) < 0) { + if (IN_SET(errno, ENOTTY, EOPNOTSUPP, EBADF)) { + log_debug_errno(errno, "File system does not support FITRIM, not trimming."); + return 0; + } + + return log_warning_errno(errno, "Failed to invoke FITRIM, ignoring: %m"); + } + + log_info("Discarded unused %s.", + format_bytes(buf, sizeof(buf), range.len)); + return 1; +} + +static int run_fallocate(int backing_fd, const struct stat *st) { + char buf[FORMAT_BYTES_MAX]; + + assert(backing_fd >= 0); + assert(st); + + /* If discarding is off, let's allocate the whole image before mounting, so that the setting takes + * effect on activation */ + + if (!S_ISREG(st->st_mode)) + return 0; + + if (st->st_blocks >= DIV_ROUND_UP(st->st_size, 512)) { + log_info("Backing file is fully allocated already."); + return 0; + } + + if (fallocate(backing_fd, FALLOC_FL_KEEP_SIZE, 0, st->st_size) < 0) { + + if (ERRNO_IS_NOT_SUPPORTED(errno)) { + log_debug_errno(errno, "fallocate() not supported on file system, ignoring."); + return 0; + } + + if (ERRNO_IS_DISK_SPACE(errno)) { + log_debug_errno(errno, "Not enough disk space to fully allocate home."); + return -ENOSPC; /* make recognizable */ + } + + return log_error_errno(errno, "Failed to allocate backing file blocks: %m"); + } + + log_info("Allocated additional %s.", + format_bytes(buf, sizeof(buf), (DIV_ROUND_UP(st->st_size, 512) - st->st_blocks) * 512)); + return 1; +} + +int home_prepare_luks( + UserRecord *h, + bool already_activated, + const char *force_image_path, + char ***pkcs11_decrypted_passwords, + HomeSetup *setup, + UserRecord **ret_luks_home) { + + sd_id128_t found_partition_uuid, found_luks_uuid, found_fs_uuid; + _cleanup_(user_record_unrefp) UserRecord *luks_home = NULL; + _cleanup_(loop_device_unrefp) LoopDevice *loop = NULL; + _cleanup_(crypt_freep) struct crypt_device *cd = NULL; + _cleanup_(erase_and_freep) void *volume_key = NULL; + bool dm_activated = false, mounted = false; + _cleanup_close_ int root_fd = -1; + size_t volume_key_size = 0; + uint64_t offset, size; + int r; + + assert(h); + assert(setup); + assert(setup->dm_name); + assert(setup->dm_node); + + assert(user_record_storage(h) == USER_LUKS); + + if (already_activated) { + struct loop_info64 info; + const char *n; + + r = luks_open(setup->dm_name, + h->password, + pkcs11_decrypted_passwords ? *pkcs11_decrypted_passwords : NULL, + &cd, + &found_luks_uuid, + &volume_key, + &volume_key_size); + if (r < 0) + return r; + + r = luks_validate_home_record(cd, h, volume_key, pkcs11_decrypted_passwords, &luks_home); + if (r < 0) + return r; + + n = crypt_get_device_name(cd); + if (!n) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to determine backing device for DM %s.", setup->dm_name); + + r = loop_device_open(n, O_RDWR, &loop); + if (r < 0) + return log_error_errno(r, "Failed to open loopback device %s: %m", n); + + if (ioctl(loop->fd, LOOP_GET_STATUS64, &info) < 0) { + _cleanup_free_ char *sysfs = NULL; + struct stat st; + + if (!IN_SET(errno, ENOTTY, EINVAL)) + return log_error_errno(errno, "Failed to get block device metrics of %s: %m", n); + + if (ioctl(loop->fd, BLKGETSIZE64, &size) < 0) + return log_error_errno(r, "Failed to read block device size of %s: %m", n); + + if (fstat(loop->fd, &st) < 0) + return log_error_errno(r, "Failed to stat block device %s: %m", n); + assert(S_ISBLK(st.st_mode)); + + if (asprintf(&sysfs, "/sys/dev/block/%u:%u/partition", major(st.st_rdev), minor(st.st_rdev)) < 0) + return log_oom(); + + if (access(sysfs, F_OK) < 0) { + if (errno != ENOENT) + return log_error_errno(errno, "Failed to determine whether %s exists: %m", sysfs); + + offset = 0; + } else { + _cleanup_free_ char *buffer = NULL; + + if (asprintf(&sysfs, "/sys/dev/block/%u:%u/start", major(st.st_rdev), minor(st.st_rdev)) < 0) + return log_oom(); + + r = read_one_line_file(sysfs, &buffer); + if (r < 0) + return log_error_errno(r, "Failed to read partition start offset: %m"); + + r = safe_atou64(buffer, &offset); + if (r < 0) + return log_error_errno(r, "Failed to parse partition start offset: %m"); + + if (offset > UINT64_MAX / 512U) + return log_error_errno(SYNTHETIC_ERRNO(E2BIG), "Offset too large for 64 byte range, refusing."); + + offset *= 512U; + } + } else { + offset = info.lo_offset; + size = info.lo_sizelimit; + } + + found_partition_uuid = found_fs_uuid = SD_ID128_NULL; + + log_info("Discovered used loopback device %s.", loop->node); + + root_fd = open(user_record_home_directory(h), O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW); + if (root_fd < 0) { + r = log_error_errno(r, "Failed to open home directory: %m"); + goto fail; + } + } else { + _cleanup_free_ char *fstype = NULL, *subdir = NULL; + _cleanup_close_ int fd = -1; + const char *ip; + struct stat st; + + ip = force_image_path ?: user_record_image_path(h); + + subdir = path_join("/run/systemd/user-home-mount/", user_record_user_name_and_realm(h)); + if (!subdir) + return log_oom(); + + fd = open(ip, O_RDWR|O_CLOEXEC|O_NOCTTY|O_NONBLOCK); + if (fd < 0) + return log_error_errno(errno, "Failed to open image file %s: %m", ip); + + if (fstat(fd, &st) < 0) + return log_error_errno(errno, "Failed to fstat() image file: %m"); + if (!S_ISREG(st.st_mode) && !S_ISBLK(st.st_mode)) + return log_error_errno(errno, "Image file %s is not a regular file or block device: %m", ip); + + r = luks_validate(fd, user_record_user_name_and_realm(h), h->partition_uuid, &found_partition_uuid, &offset, &size); + if (r < 0) + return log_error_errno(r, "Failed to validate disk label: %m"); + + if (!user_record_luks_discard(h)) { + r = run_fallocate(fd, &st); + if (r < 0) + return r; + } + + r = loop_device_make(fd, O_RDWR, offset, size, 0, &loop); + if (r == -ENOENT) { + log_error_errno(r, "Loopback block device support is not available on this system."); + return -ENOLINK; /* make recognizable */ + } + if (r < 0) + return log_error_errno(r, "Failed to allocate loopback context: %m"); + + log_info("Setting up loopback device %s completed.", loop->node ?: ip); + + r = luks_setup(loop->node ?: ip, + setup->dm_name, + h->luks_uuid, + h->luks_cipher, + h->luks_cipher_mode, + h->luks_volume_key_size, + h->password, + pkcs11_decrypted_passwords ? *pkcs11_decrypted_passwords : NULL, + user_record_luks_discard(h), + &cd, + &found_luks_uuid, + &volume_key, + &volume_key_size); + if (r < 0) + return r; + + dm_activated = true; + + r = luks_validate_home_record(cd, h, volume_key, pkcs11_decrypted_passwords, &luks_home); + if (r < 0) + goto fail; + + r = fs_validate(setup->dm_node, h->file_system_uuid, &fstype, &found_fs_uuid); + if (r < 0) + goto fail; + + r = run_fsck(setup->dm_node, fstype); + if (r < 0) + goto fail; + + r = home_unshare_and_mount(setup->dm_node, fstype, user_record_luks_discard(h)); + if (r < 0) + goto fail; + + mounted = true; + + root_fd = open(subdir, O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW); + if (root_fd < 0) { + r = log_error_errno(r, "Failed to open home directory: %m"); + goto fail; + } + + if (user_record_luks_discard(h)) + (void) run_fitrim(root_fd); + } + + setup->loop = TAKE_PTR(loop); + setup->crypt_device = TAKE_PTR(cd); + setup->root_fd = TAKE_FD(root_fd); + setup->found_partition_uuid = found_partition_uuid; + setup->found_luks_uuid = found_luks_uuid; + setup->found_fs_uuid = found_fs_uuid; + setup->partition_offset = offset; + setup->partition_size = size; + setup->volume_key = TAKE_PTR(volume_key); + setup->volume_key_size = volume_key_size; + + setup->undo_mount = mounted; + setup->undo_dm = dm_activated; + + if (ret_luks_home) + *ret_luks_home = TAKE_PTR(luks_home); + + return 0; + +fail: + if (mounted) + (void) umount_verbose("/run/systemd/user-home-mount"); + + if (dm_activated) + (void) crypt_deactivate(cd, setup->dm_name); + + return r; +} + +static void print_size_summary(uint64_t host_size, uint64_t encrypted_size, struct statfs *sfs) { + char buffer1[FORMAT_BYTES_MAX], buffer2[FORMAT_BYTES_MAX], buffer3[FORMAT_BYTES_MAX], buffer4[FORMAT_BYTES_MAX]; + + assert(sfs); + + log_info("Image size is %s, file system size is %s, file system payload size is %s, file system free is %s.", + format_bytes(buffer1, sizeof(buffer1), host_size), + format_bytes(buffer2, sizeof(buffer2), encrypted_size), + format_bytes(buffer3, sizeof(buffer3), (uint64_t) sfs->f_blocks * (uint64_t) sfs->f_frsize), + format_bytes(buffer4, sizeof(buffer4), (uint64_t) sfs->f_bfree * (uint64_t) sfs->f_frsize)); +} + +int home_activate_luks( + UserRecord *h, + char ***pkcs11_decrypted_passwords, + UserRecord **ret_home) { + + _cleanup_(user_record_unrefp) UserRecord *new_home = NULL, *luks_home_record = NULL; + _cleanup_(home_setup_undo) HomeSetup setup = HOME_SETUP_INIT; + uint64_t host_size, encrypted_size; + const char *hdo, *hd; + struct statfs sfs; + int r; + + assert(h); + assert(user_record_storage(h) == USER_LUKS); + assert(ret_home); + + assert_se(hdo = user_record_home_directory(h)); + hd = strdupa(hdo); /* copy the string out, since it might change later in the home record object */ + + r = make_dm_names(h->user_name, &setup.dm_name, &setup.dm_node); + if (r < 0) + return r; + + r = access(setup.dm_node, F_OK); + if (r < 0) { + if (errno != ENOENT) + return log_error_errno(errno, "Failed to determine whether %s exists: %m", setup.dm_node); + } else + return log_error_errno(SYNTHETIC_ERRNO(EEXIST), "Device mapper device %s already exists, refusing.", setup.dm_node); + + r = home_prepare_luks( + h, + false, + NULL, + pkcs11_decrypted_passwords, + &setup, + &luks_home_record); + if (r < 0) + return r; + + r = block_get_size_by_fd(setup.loop->fd, &host_size); + if (r < 0) + return log_error_errno(r, "Failed to get loopback block device size: %m"); + + r = block_get_size_by_path(setup.dm_node, &encrypted_size); + if (r < 0) + return log_error_errno(r, "Failed to get LUKS block device size: %m"); + + r = home_refresh( + h, + &setup, + luks_home_record, + pkcs11_decrypted_passwords, + &sfs, + &new_home); + if (r < 0) + return r; + + r = home_extend_embedded_identity(new_home, h, &setup); + if (r < 0) + return r; + + setup.root_fd = safe_close(setup.root_fd); + + r = home_move_mount(user_record_user_name_and_realm(h), hd); + if (r < 0) + return r; + + setup.undo_mount = false; + + loop_device_relinquish(setup.loop); + + r = dm_deferred_remove(setup.dm_name); + if (r < 0) + log_warning_errno(r, "Failed to relinquish dm device, ignoring: %m"); + + setup.undo_dm = false; + + log_info("Everything completed."); + + print_size_summary(host_size, encrypted_size, &sfs); + + *ret_home = TAKE_PTR(new_home); + return 1; +} + +int home_deactivate_luks(UserRecord *h) { + _cleanup_(crypt_freep) struct crypt_device *cd = NULL; + _cleanup_free_ char *dm_name = NULL, *dm_node = NULL; + int r; + + /* Note that the DM device and loopback device are set to auto-detach, hence strictly speaking we + * don't have to explicitly have to detach them. However, we do that nonetheless (in case of the DM + * device), to avoid races: by explicitly detaching them we know when the detaching is complete. We + * don't bother about the loopback device because unlike the DM device it doesn't have a fixed + * name. */ + + r = make_dm_names(h->user_name, &dm_name, &dm_node); + if (r < 0) + return r; + + r = crypt_init_by_name(&cd, dm_name); + if (IN_SET(r, -ENODEV, -EINVAL, -ENOENT)) { + log_debug_errno(r, "LUKS device %s is already detached.", dm_name); + return false; + } else if (r < 0) + return log_error_errno(r, "Failed to initialize cryptsetup context for %s: %m", dm_name); + + log_info("Discovered used LUKS device %s.", dm_node); + + crypt_set_log_callback(cd, cryptsetup_log_glue, NULL); + + r = crypt_deactivate(cd, dm_name); + if (IN_SET(r, -ENODEV, -EINVAL, -ENOENT)) + log_debug_errno(r, "LUKS device %s is already detached.", dm_node); + else if (r < 0) + return log_info_errno(r, "LUKS device %s couldn't be deactivated: %m", dm_node); + + log_info("LUKS device detaching completed."); + return true; +} + +static int run_mkfs( + const char *node, + const char *fstype, + const char *label, + sd_id128_t uuid, + bool discard) { + + int r; + + assert(node); + assert(fstype); + assert(label); + + r = mkfs_exists(fstype); + if (r < 0) + return log_error_errno(r, "Failed to check if mkfs for file system %s exists: %m", fstype); + if (r == 0) + return log_error_errno(SYNTHETIC_ERRNO(EPROTONOSUPPORT), "Nt mkfs for file system %s installed.", fstype); + + r = safe_fork("(mkfs)", FORK_RESET_SIGNALS|FORK_RLIMIT_NOFILE_SAFE|FORK_DEATHSIG|FORK_LOG|FORK_WAIT|FORK_STDOUT_TO_STDERR, NULL); + if (r < 0) + return r; + if (r == 0) { + const char *mkfs; + char suuid[37]; + + /* Child */ + + mkfs = strjoina("mkfs.", fstype); + id128_to_uuid_string(uuid, suuid); + + if (streq(fstype, "ext4")) + execlp(mkfs, mkfs, + "-L", label, + "-U", suuid, + "-I", "256", + "-O", "has_journal", + "-m", "0", + "-E", discard ? "lazy_itable_init=1,discard" : "lazy_itable_init=1,nodiscard", + node, NULL); + else if (streq(fstype, "btrfs")) { + if (discard) + execlp(mkfs, mkfs, "-L", label, "-U", suuid, node, NULL); + else + execlp(mkfs, mkfs, "-L", label, "-U", suuid, "--nodiscard", node, NULL); + } else if (streq(fstype, "xfs")) { + const char *j; + + j = strjoina("uuid=", suuid); + if (discard) + execlp(mkfs, mkfs, "-L", label, "-m", j, "-m", "reflink=1", node, NULL); + else + execlp(mkfs, mkfs, "-L", label, "-m", j, "-m", "reflink=1", "-K", node, NULL); + } else { + log_error("Cannot make file system: %s", fstype); + _exit(EXIT_FAILURE); + } + + log_error_errno(errno, "Failed to execute %s: %m", mkfs); + _exit(EXIT_FAILURE); + } + + return 0; +} + +static struct crypt_pbkdf_type* build_good_pbkdf(struct crypt_pbkdf_type *buffer, UserRecord *hr) { + assert(buffer); + assert(hr); + + *buffer = (struct crypt_pbkdf_type) { + .hash = user_record_luks_pbkdf_hash_algorithm(hr), + .type = user_record_luks_pbkdf_type(hr), + .time_ms = user_record_luks_pbkdf_time_cost_usec(hr) / USEC_PER_MSEC, + .max_memory_kb = user_record_luks_pbkdf_memory_cost(hr) / 1024, + .parallel_threads = user_record_luks_pbkdf_parallel_threads(hr), + }; + + return buffer; +} + +static struct crypt_pbkdf_type* build_minimal_pbkdf(struct crypt_pbkdf_type *buffer, UserRecord *hr) { + assert(buffer); + assert(hr); + + /* For PKCS#11 derived keys (which are generated randomly and are of high quality already) we use a + * minimal PBKDF */ + *buffer = (struct crypt_pbkdf_type) { + .hash = user_record_luks_pbkdf_hash_algorithm(hr), + .type = CRYPT_KDF_PBKDF2, + .iterations = 1, + .time_ms = 1, + }; + + return buffer; +} + +static int luks_format( + const char *node, + const char *dm_name, + sd_id128_t uuid, + const char *label, + char **pkcs11_decrypted_passwords, + char **effective_passwords, + bool discard, + UserRecord *hr, + struct crypt_device **ret) { + + _cleanup_(user_record_unrefp) UserRecord *reduced = NULL; + _cleanup_(crypt_freep) struct crypt_device *cd = NULL; + _cleanup_(erase_and_freep) void *volume_key = NULL; + struct crypt_pbkdf_type good_pbkdf, minimal_pbkdf; + _cleanup_free_ char *text = NULL; + size_t volume_key_size; + char suuid[37], **pp; + int slot = 0, r; + + assert(node); + assert(dm_name); + assert(hr); + assert(ret); + + r = crypt_init(&cd, node); + if (r < 0) + return log_error_errno(r, "Failed to allocate libcryptsetup context: %m"); + + crypt_set_log_callback(cd, cryptsetup_log_glue, NULL); + + /* Normally we'd, just leave volume key generation to libcryptsetup. However, we can't, since we + * can't extract the volume key from the library again, but we need it in order to encrypt the JSON + * record. Hence, let's generate it on our own, so that we can keep track of it. */ + + volume_key_size = user_record_luks_volume_key_size(hr); + volume_key = malloc(volume_key_size); + if (!volume_key) + return log_oom(); + + r = genuine_random_bytes(volume_key, volume_key_size, RANDOM_BLOCK); + if (r < 0) + return log_error_errno(r, "Failed to generate volume key: %m"); + +#if HAVE_CRYPT_SET_METADATA_SIZE + /* Increase the metadata space to 4M, the largest LUKS2 supports */ + r = crypt_set_metadata_size(cd, 4096U*1024U, 0); + if (r < 0) + return log_error_errno(r, "Failed to change LUKS2 metadata size: %m"); +#endif + + build_good_pbkdf(&good_pbkdf, hr); + build_minimal_pbkdf(&minimal_pbkdf, hr); + + r = crypt_format(cd, + CRYPT_LUKS2, + user_record_luks_cipher(hr), + user_record_luks_cipher_mode(hr), + id128_to_uuid_string(uuid, suuid), + volume_key, + volume_key_size, + &(struct crypt_params_luks2) { + .label = label, + .subsystem = "systemd-home", + .sector_size = 512U, + .pbkdf = &good_pbkdf, + }); + if (r < 0) + return log_error_errno(r, "Failed to format LUKS image: %m"); + + log_info("LUKS formatting completed."); + + STRV_FOREACH(pp, effective_passwords) { + + if (strv_contains(pkcs11_decrypted_passwords, *pp)) { + log_debug("Using minimal PBKDF for slot %i", slot); + r = crypt_set_pbkdf_type(cd, &minimal_pbkdf); + } else { + log_debug("Using good PBKDF for slot %i", slot); + r = crypt_set_pbkdf_type(cd, &good_pbkdf); + } + if (r < 0) + return log_error_errno(r, "Failed to tweak PBKDF for slot %i: %m", slot); + + r = crypt_keyslot_add_by_volume_key( + cd, + slot, + volume_key, + volume_key_size, + *pp, + strlen(*pp)); + if (r < 0) + return log_error_errno(r, "Failed to set up LUKS password for slot %i: %m", slot); + + log_info("Writing password to LUKS keyslot %i completed.", slot); + slot++; + } + + r = crypt_activate_by_volume_key( + cd, + dm_name, + volume_key, + volume_key_size, + discard ? CRYPT_ACTIVATE_ALLOW_DISCARDS : 0); + if (r < 0) + return log_error_errno(r, "Failed to activate LUKS superblock: %m"); + + log_info("LUKS activation by volume key succeeded."); + + r = user_record_clone(hr, USER_RECORD_EXTRACT_EMBEDDED, &reduced); + if (r < 0) + return log_error_errno(r, "Failed to prepare home record for LUKS: %m"); + + r = format_luks_token_text(cd, reduced, volume_key, &text); + if (r < 0) + return r; + + r = crypt_token_json_set(cd, CRYPT_ANY_TOKEN, text); + if (r < 0) + return log_error_errno(r, "Failed to set LUKS JSON token: %m"); + + log_info("Writing user record as LUKS token completed."); + + if (ret) + *ret = TAKE_PTR(cd); + + return 0; +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(struct fdisk_context*, fdisk_unref_context); +DEFINE_TRIVIAL_CLEANUP_FUNC(struct fdisk_partition*, fdisk_unref_partition); +DEFINE_TRIVIAL_CLEANUP_FUNC(struct fdisk_parttype*, fdisk_unref_parttype); +DEFINE_TRIVIAL_CLEANUP_FUNC(struct fdisk_table*, fdisk_unref_table); + +static int make_partition_table( + int fd, + const char *label, + sd_id128_t uuid, + uint64_t *ret_offset, + uint64_t *ret_size, + sd_id128_t *ret_disk_uuid) { + + _cleanup_(fdisk_unref_partitionp) struct fdisk_partition *p = NULL, *q = NULL; + _cleanup_(fdisk_unref_parttypep) struct fdisk_parttype *t = NULL; + _cleanup_(fdisk_unref_contextp) struct fdisk_context *c = NULL; + _cleanup_free_ char *path = NULL, *disk_uuid_as_string = NULL; + uint64_t offset, size; + sd_id128_t disk_uuid; + char uuids[37]; + int r; + + assert(fd >= 0); + assert(label); + assert(ret_offset); + assert(ret_size); + + t = fdisk_new_parttype(); + if (!t) + return log_oom(); + + r = fdisk_parttype_set_typestr(t, "773f91ef-66d4-49b5-bd83-d683bf40ad16"); + if (r < 0) + return log_error_errno(r, "Failed to initialize partition type: %m"); + + c = fdisk_new_context(); + if (!c) + return log_oom(); + + if (asprintf(&path, "/proc/self/fd/%i", fd) < 0) + return log_oom(); + + r = fdisk_assign_device(c, path, 0); + if (r < 0) + return log_error_errno(r, "Failed to open device: %m"); + + r = fdisk_create_disklabel(c, "gpt"); + if (r < 0) + return log_error_errno(r, "Failed to create gpt disk label: %m"); + + p = fdisk_new_partition(); + if (!p) + return log_oom(); + + r = fdisk_partition_set_type(p, t); + if (r < 0) + return log_error_errno(r, "Failed to set partition type: %m"); + + r = fdisk_partition_start_follow_default(p, 1); + if (r < 0) + return log_error_errno(r, "Failed to place partition at beginning of space: %m"); + + r = fdisk_partition_partno_follow_default(p, 1); + if (r < 0) + return log_error_errno(r, "Failed to place partition at first free partition index: %m"); + + r = fdisk_partition_end_follow_default(p, 1); + if (r < 0) + return log_error_errno(r, "Failed to make partition cover all free space: %m"); + + r = fdisk_partition_set_name(p, label); + if (r < 0) + return log_error_errno(r, "Failed to set partition name: %m"); + + r = fdisk_partition_set_uuid(p, id128_to_uuid_string(uuid, uuids)); + if (r < 0) + return log_error_errno(r, "Failed to set partition UUID: %m"); + + r = fdisk_add_partition(c, p, NULL); + if (r < 0) + return log_error_errno(r, "Failed to add partition: %m"); + + r = fdisk_write_disklabel(c); + if (r < 0) + return log_error_errno(r, "Failed to write disk label: %m"); + + r = fdisk_get_disklabel_id(c, &disk_uuid_as_string); + if (r < 0) + return log_error_errno(r, "Failed to determine disk label UUID: %m"); + + r = sd_id128_from_string(disk_uuid_as_string, &disk_uuid); + if (r < 0) + return log_error_errno(r, "Failed to parse disk label UUID: %m"); + + r = fdisk_get_partition(c, 0, &q); + if (r < 0) + return log_error_errno(r, "Failed to read created partition metadata: %m"); + + assert(fdisk_partition_has_start(q)); + offset = fdisk_partition_get_start(q); + if (offset > UINT64_MAX / 512U) + return log_error_errno(SYNTHETIC_ERRNO(ERANGE), "Partition offset too large."); + + assert(fdisk_partition_has_size(q)); + size = fdisk_partition_get_size(q); + if (size > UINT64_MAX / 512U) + return log_error_errno(SYNTHETIC_ERRNO(ERANGE), "Partition size too large."); + + *ret_offset = offset * 512U; + *ret_size = size * 512U; + *ret_disk_uuid = disk_uuid; + + return 0; +} + +static bool supported_fs_size(const char *fstype, uint64_t host_size) { + uint64_t m; + + m = minimal_size_by_fs_name(fstype); + if (m == UINT64_MAX) + return false; + + return host_size >= m; +} + +static int wait_for_devlink(const char *path) { + _cleanup_close_ int inotify_fd = -1; + usec_t until; + int r; + + /* let's wait for a device link to show up in /dev, with a time-out. This is good to do since we + * return a /dev/disk/by-uuid/… link to our callers and they likely want to access it right-away, + * hence let's wait until udev has caught up with our changes, and wait for the symlink to be + * created. */ + + until = usec_add(now(CLOCK_MONOTONIC), 45 * USEC_PER_SEC); + + for (;;) { + _cleanup_free_ char *dn = NULL; + usec_t w; + + if (laccess(path, F_OK) < 0) { + if (errno != ENOENT) + return log_error_errno(errno, "Failed to determine whether %s exists: %m", path); + } else + return 0; /* Found it */ + + if (inotify_fd < 0) { + /* We need to wait for the device symlink to show up, let's create an inotify watch for it */ + inotify_fd = inotify_init1(IN_NONBLOCK|IN_CLOEXEC); + if (inotify_fd < 0) + return log_error_errno(errno, "Failed to allocate inotify fd: %m"); + } + + dn = dirname_malloc(path); + for (;;) { + if (!dn) + return log_oom(); + + log_info("Watching %s", dn); + + if (inotify_add_watch(inotify_fd, dn, IN_CREATE|IN_MOVED_TO|IN_ONLYDIR|IN_DELETE_SELF|IN_MOVE_SELF) < 0) { + if (errno != ENOENT) + return log_error_errno(errno, "Failed to add watch on %s: %m", dn); + } else + break; + + if (empty_or_root(dn)) + break; + + dn = dirname_malloc(dn); + } + + w = now(CLOCK_MONOTONIC); + if (w >= until) + return log_error_errno(SYNTHETIC_ERRNO(ETIMEDOUT), "Device link %s still hasn't shown up, giving up.", path); + + r = fd_wait_for_event(inotify_fd, POLLIN, usec_sub_unsigned(until, w)); + if (r < 0) + return log_error_errno(r, "Failed to watch inotify: %m"); + + (void) flush_fd(inotify_fd); + } +} + +static int calculate_disk_size(UserRecord *h, const char *parent_dir, uint64_t *ret) { + char buf[FORMAT_BYTES_MAX]; + struct statfs sfs; + uint64_t m; + + assert(h); + assert(parent_dir); + assert(ret); + + if (h->disk_size != UINT64_MAX) { + *ret = DISK_SIZE_ROUND_DOWN(h->disk_size); + return 0; + } + + if (statfs(parent_dir, &sfs) < 0) + return log_error_errno(errno, "statfs() on %s failed: %m", parent_dir); + + m = sfs.f_bsize * sfs.f_bavail; + + if (h->disk_size_relative == UINT64_MAX) { + + if (m > UINT64_MAX / USER_DISK_SIZE_DEFAULT_PERCENT) + return log_error_errno(SYNTHETIC_ERRNO(EOVERFLOW), "Disk size too large."); + + *ret = DISK_SIZE_ROUND_DOWN(m * USER_DISK_SIZE_DEFAULT_PERCENT / 100); + + log_info("Sizing home to %u%% of available disk space, which is %s.", + USER_DISK_SIZE_DEFAULT_PERCENT, + format_bytes(buf, sizeof(buf), *ret)); + } else { + *ret = DISK_SIZE_ROUND_DOWN((uint64_t) ((double) m * (double) h->disk_size_relative / (double) UINT32_MAX)); + + log_info("Sizing home to %" PRIu64 ".%01" PRIu64 "%% of available disk space, which is %s.", + (h->disk_size_relative * 100) / UINT32_MAX, + ((h->disk_size_relative * 1000) / UINT32_MAX) % 10, + format_bytes(buf, sizeof(buf), *ret)); + } + + if (*ret < USER_DISK_SIZE_MIN) + *ret = USER_DISK_SIZE_MIN; + + return 0; +} + +int home_create_luks( + UserRecord *h, + char **pkcs11_decrypted_passwords, + char **effective_passwords, + UserRecord **ret_home) { + + _cleanup_free_ char *dm_name = NULL, *dm_node = NULL, *subdir = NULL, *disk_uuid_path = NULL, *temporary_image_path = NULL; + uint64_t host_size, encrypted_size, partition_offset, partition_size; + bool image_created = false, dm_activated = false, mounted = false; + _cleanup_(user_record_unrefp) UserRecord *new_home = NULL; + sd_id128_t partition_uuid, fs_uuid, luks_uuid, disk_uuid; + _cleanup_(loop_device_unrefp) LoopDevice *loop = NULL; + _cleanup_(crypt_freep) struct crypt_device *cd = NULL; + _cleanup_close_ int image_fd = -1, root_fd = -1; + const char *fstype, *ip; + struct statfs sfs; + int r; + + assert(h); + assert(h->storage < 0 || h->storage == USER_LUKS); + assert(ret_home); + + assert_se(ip = user_record_image_path(h)); + + fstype = user_record_file_system_type(h); + if (!supported_fstype(fstype)) + return log_error_errno(SYNTHETIC_ERRNO(EPROTONOSUPPORT), "Unsupported file system type: %s", h->file_system_type); + + if (sd_id128_is_null(h->partition_uuid)) { + r = sd_id128_randomize(&partition_uuid); + if (r < 0) + return log_error_errno(r, "Failed to acquire partition UUID: %m"); + } else + partition_uuid = h->partition_uuid; + + if (sd_id128_is_null(h->luks_uuid)) { + r = sd_id128_randomize(&luks_uuid); + if (r < 0) + return log_error_errno(r, "Failed to acquire LUKS UUID: %m"); + } else + luks_uuid = h->luks_uuid; + + if (sd_id128_is_null(h->file_system_uuid)) { + r = sd_id128_randomize(&fs_uuid); + if (r < 0) + return log_error_errno(r, "Failed to acquire file system UUID: %m"); + } else + fs_uuid = h->file_system_uuid; + + r = make_dm_names(h->user_name, &dm_name, &dm_node); + if (r < 0) + return r; + + r = access(dm_node, F_OK); + if (r < 0) { + if (errno != ENOENT) + return log_error_errno(errno, "Failed to determine whether %s exists: %m", dm_node); + } else + return log_error_errno(SYNTHETIC_ERRNO(EEXIST), "Device mapper device %s already exists, refusing.", dm_node); + + if (path_startswith(ip, "/dev/")) { + _cleanup_free_ char *sysfs = NULL; + uint64_t block_device_size; + struct stat st; + + /* Let's place the home directory on a real device, i.e. an USB stick or such */ + + image_fd = open(ip, O_RDWR|O_CLOEXEC|O_NOCTTY|O_NONBLOCK); + if (image_fd < 0) + return log_error_errno(errno, "Failed to open device %s: %m", ip); + + if (fstat(image_fd, &st) < 0) + return log_error_errno(errno, "Failed to stat device %s: %m", ip); + if (!S_ISBLK(st.st_mode)) + return log_error_errno(SYNTHETIC_ERRNO(ENOTBLK), "Device is not a block device, refusing."); + + if (asprintf(&sysfs, "/sys/dev/block/%u:%u/partition", major(st.st_rdev), minor(st.st_rdev)) < 0) + return log_oom(); + if (access(sysfs, F_OK) < 0) { + if (errno != ENOENT) + return log_error_errno(errno, "Failed to check whether %s exists: %m", sysfs); + } else + return log_error_errno(SYNTHETIC_ERRNO(ENOTBLK), "Operating on partitions is currently not supported, sorry. Please specify a top-level block device."); + + if (flock(image_fd, LOCK_EX) < 0) /* make sure udev doesn't read from it while we operate on the device */ + return log_error_errno(errno, "Failed to lock block device %s: %m", ip); + + if (ioctl(image_fd, BLKGETSIZE64, &block_device_size) < 0) + return log_error_errno(errno, "Failed to read block device size: %m"); + + if (h->disk_size == UINT64_MAX) { + + /* If a relative disk size is requested, apply it relative to the block device size */ + if (h->disk_size_relative < UINT32_MAX) + host_size = CLAMP(DISK_SIZE_ROUND_DOWN(block_device_size * h->disk_size_relative / UINT32_MAX), + USER_DISK_SIZE_MIN, USER_DISK_SIZE_MAX); + else + host_size = block_device_size; /* Otherwise, take the full device */ + + } else if (h->disk_size > block_device_size) + return log_error_errno(SYNTHETIC_ERRNO(EMSGSIZE), "Selected disk size larger than backing block device, refusing."); + else + host_size = DISK_SIZE_ROUND_DOWN(h->disk_size); + + if (!supported_fs_size(fstype, host_size)) + return log_error_errno(SYNTHETIC_ERRNO(ERANGE), "Selected file system size too small for %s.", h->file_system_type); + + /* After creation we should reference this partition by its UUID instead of the block + * device. That's preferable since the user might have specified a device node such as + * /dev/sdb to us, which might look very different when replugged. */ + if (asprintf(&disk_uuid_path, "/dev/disk/by-uuid/" SD_ID128_UUID_FORMAT_STR, SD_ID128_FORMAT_VAL(luks_uuid)) < 0) + return log_oom(); + + if (user_record_luks_discard(h)) { + if (ioctl(image_fd, BLKDISCARD, (uint64_t[]) { 0, block_device_size }) < 0) + log_full_errno(errno == EOPNOTSUPP ? LOG_DEBUG : LOG_WARNING, errno, + "Failed to issue full-device BLKDISCARD on device, ignoring: %m"); + else + log_info("Full device discard completed."); + } + } else { + _cleanup_free_ char *parent = NULL; + + parent = dirname_malloc(ip); + if (!parent) + return log_oom(); + + r = mkdir_p(parent, 0755); + if (r < 0) + return log_error_errno(r, "Failed to create parent directory %s: %m", parent); + + r = calculate_disk_size(h, parent, &host_size); + if (r < 0) + return r; + + if (!supported_fs_size(fstype, host_size)) + return log_error_errno(SYNTHETIC_ERRNO(ERANGE), "Selected file system size too small for %s.", h->file_system_type); + + r = tempfn_random(ip, "homework", &temporary_image_path); + if (r < 0) + return log_error_errno(r, "Failed to derive temporary file name for %s: %m", ip); + + image_fd = open(temporary_image_path, O_RDWR|O_CREAT|O_EXCL|O_CLOEXEC|O_NOCTTY|O_NOFOLLOW, 0600); + if (image_fd < 0) + return log_error_errno(errno, "Failed to create home image %s: %m", temporary_image_path); + + image_created = true; + + r = chattr_fd(image_fd, FS_NOCOW_FL, FS_NOCOW_FL, NULL); + if (r < 0) + log_warning_errno(r, "Failed to set file attributes on %s, ignoring: %m", temporary_image_path); + + if (user_record_luks_discard(h)) + r = ftruncate(image_fd, host_size); + else + r = fallocate(image_fd, 0, 0, host_size); + if (r < 0) { + if (ERRNO_IS_DISK_SPACE(errno)) { + log_debug_errno(errno, "Not enough disk space to allocate home."); + r = -ENOSPC; /* make recognizable */ + goto fail; + } + + r = log_error_errno(errno, "Failed to truncate home image %s: %m", temporary_image_path); + goto fail; + } + + log_info("Allocating image file completed."); + } + + r = make_partition_table( + image_fd, + user_record_user_name_and_realm(h), + partition_uuid, + &partition_offset, + &partition_size, + &disk_uuid); + if (r < 0) + goto fail; + + log_info("Writing of partition table completed."); + + r = loop_device_make(image_fd, O_RDWR, partition_offset, partition_size, 0, &loop); + if (r < 0) { + if (r == -ENOENT) { /* this means /dev/loop-control doesn't exist, i.e. we are in a container + * or similar and loopback bock devices are not available, return a + * recognizable error in this case. */ + log_error_errno(r, "Loopback block device support is not available on this system."); + r = -ENOLINK; + goto fail; + } + + log_error_errno(r, "Failed to set up loopback device for %s: %m", temporary_image_path); + goto fail; + } + + r = loop_device_flock(loop, LOCK_EX); /* make sure udev won't read before we are done */ + if (r < 0) { + log_error_errno(r, "Failed to take lock on loop device: %m"); + goto fail; + } + + log_info("Setting up loopback device %s completed.", loop->node ?: ip); + + r = luks_format(loop->node, + dm_name, + luks_uuid, + user_record_user_name_and_realm(h), + pkcs11_decrypted_passwords, + effective_passwords, + user_record_luks_discard(h), + h, + &cd); + if (r < 0) + goto fail; + + dm_activated = true; + + r = block_get_size_by_path(dm_node, &encrypted_size); + if (r < 0) { + log_error_errno(r, "Failed to get encrypted block device size: %m"); + goto fail; + } + + log_info("Setting up LUKS device %s completed.", dm_node); + + r = run_mkfs(dm_node, fstype, user_record_user_name_and_realm(h), fs_uuid, user_record_luks_discard(h)); + if (r < 0) + goto fail; + + log_info("Formatting file system completed."); + + r = home_unshare_and_mount(dm_node, fstype, user_record_luks_discard(h)); + if (r < 0) + goto fail; + + mounted = true; + + subdir = path_join("/run/systemd/user-home-mount/", user_record_user_name_and_realm(h)); + if (!subdir) { + r = log_oom(); + goto fail; + } + + if (mkdir(subdir, 0700) < 0) { + r = log_error_errno(errno, "Failed to create user directory in mounted image file: %m"); + goto fail; + } + + root_fd = open(subdir, O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW); + if (root_fd < 0) { + r = log_error_errno(errno, "Failed to open user directory in mounted image file: %m"); + goto fail; + } + + r = home_populate(h, root_fd); + if (r < 0) + goto fail; + + r = home_sync_and_statfs(root_fd, &sfs); + if (r < 0) + goto fail; + + r = user_record_clone(h, USER_RECORD_LOAD_MASK_SECRET|USER_RECORD_LOG, &new_home); + if (r < 0) { + log_error_errno(r, "Failed to clone record: %m"); + goto fail; + } + + r = user_record_add_binding( + new_home, + USER_LUKS, + disk_uuid_path ?: ip, + partition_uuid, + luks_uuid, + fs_uuid, + crypt_get_cipher(cd), + crypt_get_cipher_mode(cd), + luks_volume_key_size_convert(cd), + fstype, + NULL, + h->uid, + (gid_t) h->uid); + if (r < 0) { + log_error_errno(r, "Failed to add binding to record: %m"); + goto fail; + } + + root_fd = safe_close(root_fd); + + r = umount_verbose("/run/systemd/user-home-mount"); + if (r < 0) + goto fail; + + mounted = false; + + r = crypt_deactivate(cd, dm_name); + if (r < 0) { + log_error_errno(r, "Failed to deactivate LUKS device: %m"); + goto fail; + } + + dm_activated = false; + + loop = loop_device_unref(loop); + + if (disk_uuid_path) + (void) ioctl(image_fd, BLKRRPART, 0); + + /* Let's close the image fd now. If we are operating on a real block device this will release the BSD + * lock that ensures udev doesn't interfere with what we are doing */ + image_fd = safe_close(image_fd); + + if (temporary_image_path) { + if (rename(temporary_image_path, ip) < 0) { + log_error_errno(errno, "Failed to rename image file: %m"); + goto fail; + } + + log_info("Moved image file into place."); + } + + if (disk_uuid_path) + (void) wait_for_devlink(disk_uuid_path); + + log_info("Everything completed."); + + print_size_summary(host_size, encrypted_size, &sfs); + + *ret_home = TAKE_PTR(new_home); + return 0; + +fail: + /* Let's close all files before we unmount the file system, to avoid EBUSY */ + root_fd = safe_close(root_fd); + + if (mounted) + (void) umount_verbose("/run/systemd/user-home-mount"); + + if (dm_activated) + (void) crypt_deactivate(cd, dm_name); + + loop = loop_device_unref(loop); + + if (image_created) + (void) unlink(temporary_image_path); + + return r; +} + +int home_validate_update_luks(UserRecord *h, HomeSetup *setup) { + _cleanup_free_ char *dm_name = NULL, *dm_node = NULL; + int r; + + assert(h); + assert(setup); + + r = make_dm_names(h->user_name, &dm_name, &dm_node); + if (r < 0) + return r; + + r = access(dm_node, F_OK); + if (r < 0 && errno != ENOENT) + return log_error_errno(errno, "Failed to determine whether %s exists: %m", dm_node); + + free_and_replace(setup->dm_name, dm_name); + free_and_replace(setup->dm_node, dm_node); + + return r >= 0; +} + +enum { + CAN_RESIZE_ONLINE, + CAN_RESIZE_OFFLINE, +}; + +static int can_resize_fs(int fd, uint64_t old_size, uint64_t new_size) { + struct statfs sfs; + + assert(fd >= 0); + + /* Filter out bogus requests early */ + if (old_size == 0 || old_size == UINT64_MAX || + new_size == 0 || new_size == UINT64_MAX) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid resize parameters."); + + if ((old_size & 511) != 0 || (new_size & 511) != 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Resize parameters not multiple of 512."); + + if (fstatfs(fd, &sfs) < 0) + return log_error_errno(errno, "Failed to fstatfs() file system: %m"); + + if (is_fs_type(&sfs, BTRFS_SUPER_MAGIC)) { + + if (new_size < BTRFS_MINIMAL_SIZE) + return log_error_errno(SYNTHETIC_ERRNO(ERANGE), "New file system size too small for btrfs (needs to be 256M at least."); + + /* btrfs can grow and shrink online */ + + } else if (is_fs_type(&sfs, XFS_SB_MAGIC)) { + + if (new_size < XFS_MINIMAL_SIZE) + return log_error_errno(SYNTHETIC_ERRNO(ERANGE), "New file system size too small for xfs (needs to be 14M at least)."); + + /* XFS can grow, but not shrink */ + if (new_size < old_size) + return log_error_errno(SYNTHETIC_ERRNO(EMSGSIZE), "Shrinking this type of file system is not supported."); + + } else if (is_fs_type(&sfs, EXT4_SUPER_MAGIC)) { + + if (new_size < EXT4_MINIMAL_SIZE) + return log_error_errno(SYNTHETIC_ERRNO(ERANGE), "New file system size too small for ext4 (needs to be 1M at least)."); + + /* ext4 can grow online, and shrink offline */ + if (new_size < old_size) + return CAN_RESIZE_OFFLINE; + + } else + return log_error_errno(SYNTHETIC_ERRNO(ESOCKTNOSUPPORT), "Resizing this type of file system is not supported."); + + return CAN_RESIZE_ONLINE; +} + +static int ext4_offline_resize_fs(HomeSetup *setup, uint64_t new_size, bool discard) { + _cleanup_free_ char *size_str = NULL; + bool re_open = false, re_mount = false; + pid_t resize_pid, fsck_pid; + int r, exit_status; + + assert(setup); + assert(setup->dm_node); + + /* First, unmount the file system */ + if (setup->root_fd >= 0) { + setup->root_fd = safe_close(setup->root_fd); + re_open = true; + } + + if (setup->undo_mount) { + r = umount_verbose("/run/systemd/user-home-mount"); + if (r < 0) + return r; + + setup->undo_mount = false; + re_mount = true; + } + + log_info("Temporarary unmounting of file system completed."); + + /* resize2fs requires that the file system is force checked first, do so. */ + r = safe_fork("(e2fsck)", FORK_RESET_SIGNALS|FORK_RLIMIT_NOFILE_SAFE|FORK_DEATHSIG|FORK_LOG|FORK_STDOUT_TO_STDERR, &fsck_pid); + if (r < 0) + return r; + if (r == 0) { + /* Child */ + execlp("e2fsck" ,"e2fsck", "-fp", setup->dm_node, NULL); + log_error_errno(errno, "Failed to execute e2fsck: %m"); + _exit(EXIT_FAILURE); + } + + exit_status = wait_for_terminate_and_check("e2fsck", fsck_pid, WAIT_LOG_ABNORMAL); + if (exit_status < 0) + return exit_status; + if ((exit_status & ~FSCK_ERROR_CORRECTED) != 0) { + log_warning("e2fsck failed with exit status %i.", exit_status); + + if ((exit_status & (FSCK_SYSTEM_SHOULD_REBOOT|FSCK_ERRORS_LEFT_UNCORRECTED)) != 0) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "File system is corrupted, refusing."); + + log_warning("Ignoring fsck error."); + } + + log_info("Forced file system check completed."); + + /* We use 512 sectors here, because resize2fs doesn't do byte sizes */ + if (asprintf(&size_str, "%" PRIu64 "s", new_size / 512) < 0) + return log_oom(); + + /* Resize the thing */ + r = safe_fork("(e2resize)", FORK_RESET_SIGNALS|FORK_RLIMIT_NOFILE_SAFE|FORK_DEATHSIG|FORK_LOG|FORK_WAIT|FORK_STDOUT_TO_STDERR, &resize_pid); + if (r < 0) + return r; + if (r == 0) { + /* Child */ + execlp("resize2fs" ,"resize2fs", setup->dm_node, size_str, NULL); + log_error_errno(errno, "Failed to execute resize2fs: %m"); + _exit(EXIT_FAILURE); + } + + log_info("Offline file system resize completed."); + + /* Re-establish mounts and reopen the directory */ + if (re_mount) { + r = home_mount_node(setup->dm_node, "ext4", discard); + if (r < 0) + return r; + + setup->undo_mount = true; + } + + if (re_open) { + setup->root_fd = open("/run/systemd/user-home-mount", O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW); + if (setup->root_fd < 0) + return log_error_errno(errno, "Failed to reopen file system: %m"); + } + + log_info("File system mounted again."); + + return 0; +} + +static int prepare_resize_partition( + int fd, + uint64_t partition_offset, + uint64_t old_partition_size, + uint64_t new_partition_size, + sd_id128_t *ret_disk_uuid, + struct fdisk_table **ret_table) { + + _cleanup_(fdisk_unref_contextp) struct fdisk_context *c = NULL; + _cleanup_(fdisk_unref_tablep) struct fdisk_table *t = NULL; + _cleanup_free_ char *path = NULL, *disk_uuid_as_string = NULL; + size_t n_partitions, i; + sd_id128_t disk_uuid; + bool found = false; + int r; + + assert(fd >= 0); + assert(ret_disk_uuid); + assert(ret_table); + + assert((partition_offset & 511) == 0); + assert((old_partition_size & 511) == 0); + assert((new_partition_size & 511) == 0); + assert(UINT64_MAX - old_partition_size >= partition_offset); + assert(UINT64_MAX - new_partition_size >= partition_offset); + + if (partition_offset == 0) { + /* If the offset is at the beginning we assume no partition table, let's exit early. */ + log_debug("Not rewriting partition table, operating on naked device."); + *ret_disk_uuid = SD_ID128_NULL; + *ret_table = NULL; + return 0; + } + + c = fdisk_new_context(); + if (!c) + return log_oom(); + + if (asprintf(&path, "/proc/self/fd/%i", fd) < 0) + return log_oom(); + + r = fdisk_assign_device(c, path, 0); + if (r < 0) + return log_error_errno(r, "Failed to open device: %m"); + + if (!fdisk_is_labeltype(c, FDISK_DISKLABEL_GPT)) + return log_error_errno(SYNTHETIC_ERRNO(ENOMEDIUM), "Disk has no GPT partition table."); + + r = fdisk_get_disklabel_id(c, &disk_uuid_as_string); + if (r < 0) + return log_error_errno(r, "Failed to acquire disk UUID: %m"); + + r = sd_id128_from_string(disk_uuid_as_string, &disk_uuid); + if (r < 0) + return log_error_errno(r, "Failed parse disk UUID: %m"); + + r = fdisk_get_partitions(c, &t); + if (r < 0) + return log_error_errno(r, "Failed to acquire partition table: %m"); + + n_partitions = fdisk_table_get_nents(t); + for (i = 0; i < n_partitions; i++) { + struct fdisk_partition *p; + + p = fdisk_table_get_partition(t, i); + if (!p) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to read partition metadata: %m"); + + if (fdisk_partition_is_used(p) <= 0) + continue; + if (fdisk_partition_has_start(p) <= 0 || fdisk_partition_has_size(p) <= 0 || fdisk_partition_has_end(p) <= 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Found partition without a size."); + + if (fdisk_partition_get_start(p) == partition_offset / 512U && + fdisk_partition_get_size(p) == old_partition_size / 512U) { + + if (found) + return log_error_errno(SYNTHETIC_ERRNO(ENOTUNIQ), "Partition found twice, refusing."); + + /* Found our partition, now patch it */ + r = fdisk_partition_size_explicit(p, 1); + if (r < 0) + return log_error_errno(r, "Failed to enable explicit partition size: %m"); + + r = fdisk_partition_set_size(p, new_partition_size / 512U); + if (r < 0) + return log_error_errno(r, "Failed to change partition size: %m"); + + found = true; + continue; + + } else { + if (fdisk_partition_get_start(p) < partition_offset + new_partition_size / 512U && + fdisk_partition_get_end(p) >= partition_offset / 512) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Can't extend, conflicting partition found."); + } + } + + if (!found) + return log_error_errno(SYNTHETIC_ERRNO(ENOPKG), "Failed to find matching partition to resize."); + + *ret_table = TAKE_PTR(t); + *ret_disk_uuid = disk_uuid; + + return 1; +} + +static int ask_cb(struct fdisk_context *c, struct fdisk_ask *ask, void *userdata) { + char *result; + + assert(c); + + switch (fdisk_ask_get_type(ask)) { + + case FDISK_ASKTYPE_STRING: + result = new(char, 37); + if (!result) + return log_oom(); + + fdisk_ask_string_set_result(ask, id128_to_uuid_string(*(sd_id128_t*) userdata, result)); + break; + + default: + log_debug("Unexpected question from libfdisk, ignoring."); + } + + return 0; +} + +static int apply_resize_partition(int fd, sd_id128_t disk_uuids, struct fdisk_table *t) { + _cleanup_(fdisk_unref_contextp) struct fdisk_context *c = NULL; + _cleanup_free_ void *two_zero_lbas = NULL; + _cleanup_free_ char *path = NULL; + ssize_t n; + int r; + + assert(fd >= 0); + + if (!t) /* no partition table to apply, exit early */ + return 0; + + two_zero_lbas = malloc0(1024U); + if (!two_zero_lbas) + return log_oom(); + + /* libfdisk appears to get confused by the existing PMBR. Let's explicitly flush it out. */ + n = pwrite(fd, two_zero_lbas, 1024U, 0); + if (n < 0) + return log_error_errno(errno, "Failed to wipe partition table: %m"); + if (n != 1024) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Short write while whiping partition table."); + + c = fdisk_new_context(); + if (!c) + return log_oom(); + + if (asprintf(&path, "/proc/self/fd/%i", fd) < 0) + return log_oom(); + + r = fdisk_assign_device(c, path, 0); + if (r < 0) + return log_error_errno(r, "Failed to open device: %m"); + + r = fdisk_create_disklabel(c, "gpt"); + if (r < 0) + return log_error_errno(r, "Failed to create GPT disk label: %m"); + + r = fdisk_apply_table(c, t); + if (r < 0) + return log_error_errno(r, "Failed to apply partition table: %m"); + + r = fdisk_set_ask(c, ask_cb, &disk_uuids); + if (r < 0) + return log_error_errno(r, "Failed to set libfdisk query function: %m"); + + r = fdisk_set_disklabel_id(c); + if (r < 0) + return log_error_errno(r, "Failed to change disklabel ID: %m"); + + r = fdisk_write_disklabel(c); + if (r < 0) + return log_error_errno(r, "Failed to write disk label: %m"); + + return 1; +} + +int home_resize_luks( + UserRecord *h, + bool already_activated, + char ***pkcs11_decrypted_passwords, + HomeSetup *setup, + UserRecord **ret_home) { + + char buffer1[FORMAT_BYTES_MAX], buffer2[FORMAT_BYTES_MAX], buffer3[FORMAT_BYTES_MAX], + buffer4[FORMAT_BYTES_MAX], buffer5[FORMAT_BYTES_MAX], buffer6[FORMAT_BYTES_MAX]; + uint64_t old_image_size, new_image_size, old_fs_size, new_fs_size, crypto_offset, new_partition_size; + _cleanup_(user_record_unrefp) UserRecord *header_home = NULL, *embedded_home = NULL, *new_home = NULL; + _cleanup_(fdisk_unref_tablep) struct fdisk_table *table = NULL; + _cleanup_free_ char *whole_disk = NULL; + _cleanup_close_ int image_fd = -1; + sd_id128_t disk_uuid; + const char *ip, *ipo; + struct statfs sfs; + struct stat st; + int r, resize_type; + + assert(h); + assert(user_record_storage(h) == USER_LUKS); + assert(setup); + assert(ret_home); + + assert_se(ipo = user_record_image_path(h)); + ip = strdupa(ipo); /* copy out since original might change later in home record object */ + + image_fd = open(ip, O_RDWR|O_CLOEXEC|O_NOCTTY|O_NONBLOCK); + if (image_fd < 0) + return log_error_errno(errno, "Failed to open image file %s: %m", ip); + + if (fstat(image_fd, &st) < 0) + return log_error_errno(errno, "Failed to stat image file %s: %m", ip); + if (S_ISBLK(st.st_mode)) { + dev_t parent; + + r = block_get_whole_disk(st.st_rdev, &parent); + if (r < 0) + return log_error_errno(r, "Failed to acquire whole block device for %s: %m", ip); + if (r > 0) { + /* If we shall resize a file system on a partition device, then let's figure out the + * whole disk device and operate on that instead, since we need to rewrite the + * partition table to resize the partition. */ + + log_info("Operating on partition device %s, using parent device.", ip); + + r = device_path_make_major_minor(st.st_mode, parent, &whole_disk); + if (r < 0) + return log_error_errno(r, "Failed to derive whole disk path for %s: %m", ip); + + safe_close(image_fd); + + image_fd = open(whole_disk, O_RDWR|O_CLOEXEC|O_NOCTTY|O_NONBLOCK); + if (image_fd < 0) + return log_error_errno(errno, "Failed to open whole block device %s: %m", whole_disk); + + if (fstat(image_fd, &st) < 0) + return log_error_errno(errno, "Failed to stat whole block device %s: %m", whole_disk); + if (!S_ISBLK(st.st_mode)) + return log_error_errno(SYNTHETIC_ERRNO(ENOTBLK), "Whole block device %s is not actually a block device, refusing.", whole_disk); + } else + log_info("Operating on whole block device %s.", ip); + + if (ioctl(image_fd, BLKGETSIZE64, &old_image_size) < 0) + return log_error_errno(errno, "Failed to determine size of original block device: %m"); + + if (flock(image_fd, LOCK_EX) < 0) /* make sure udev doesn't read from it while we operate on the device */ + return log_error_errno(errno, "Failed to lock block device %s: %m", ip); + + new_image_size = old_image_size; /* we can't resize physical block devices */ + } else { + r = stat_verify_regular(&st); + if (r < 0) + return log_error_errno(r, "Image file %s is not a block device nor regular: %m", ip); + + old_image_size = st.st_size; + + /* Note an asymetry here: when we operate on loopback files the specified disk size we get we + * apply onto the loopback file as a whole. When we operate on block devices we instead apply + * to the partition itself only. */ + + new_image_size = DISK_SIZE_ROUND_DOWN(h->disk_size); + if (new_image_size == old_image_size) { + log_info("Image size already matching, skipping operation."); + return 0; + } + } + + r = home_prepare_luks(h, already_activated, whole_disk, pkcs11_decrypted_passwords, setup, &header_home); + if (r < 0) + return r; + + r = home_load_embedded_identity(h, setup->root_fd, header_home, USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL, pkcs11_decrypted_passwords, &embedded_home, &new_home); + if (r < 0) + return r; + + log_info("offset = %" PRIu64 ", size = %" PRIu64 ", image = %" PRIu64, setup->partition_offset, setup->partition_size, old_image_size); + + if ((UINT64_MAX - setup->partition_offset) < setup->partition_size || + setup->partition_offset + setup->partition_size > old_image_size) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Old partition doesn't fit in backing storage, refusing."); + + if (S_ISREG(st.st_mode)) { + uint64_t partition_table_extra; + + partition_table_extra = old_image_size - setup->partition_size; + if (new_image_size <= partition_table_extra) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "New size smaller than partition table metadata."); + + new_partition_size = new_image_size - partition_table_extra; + } else { + assert(S_ISBLK(st.st_mode)); + + new_partition_size = DISK_SIZE_ROUND_DOWN(h->disk_size); + if (new_partition_size == setup->partition_size) { + log_info("Partition size already matching, skipping operation."); + return 0; + } + } + + if ((UINT64_MAX - setup->partition_offset) < new_partition_size || + setup->partition_offset + new_partition_size > new_image_size) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "New partition doesn't fit into backing storage, refusing."); + + crypto_offset = crypt_get_data_offset(setup->crypt_device); + if (setup->partition_size / 512U <= crypto_offset) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Weird, old crypto payload offset doesn't actually fit in partition size?"); + if (new_partition_size / 512U <= crypto_offset) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "New size smaller than crypto payload offset?"); + + old_fs_size = (setup->partition_size / 512U - crypto_offset) * 512U; + new_fs_size = (new_partition_size / 512U - crypto_offset) * 512U; + + /* Before we start doing anything, let's figure out if we actually can */ + resize_type = can_resize_fs(setup->root_fd, old_fs_size, new_fs_size); + if (resize_type < 0) + return resize_type; + if (resize_type == CAN_RESIZE_OFFLINE && already_activated) + return log_error_errno(SYNTHETIC_ERRNO(ETXTBSY), "File systems of this type can only be resized offline, but is currently online."); + + log_info("Ready to resize image size %s → %s, partition size %s → %s, file system size %s → %s.", + format_bytes(buffer1, sizeof(buffer1), old_image_size), + format_bytes(buffer2, sizeof(buffer2), new_image_size), + format_bytes(buffer3, sizeof(buffer3), setup->partition_size), + format_bytes(buffer4, sizeof(buffer4), new_partition_size), + format_bytes(buffer5, sizeof(buffer5), old_fs_size), + format_bytes(buffer6, sizeof(buffer6), new_fs_size)); + + r = prepare_resize_partition( + image_fd, + setup->partition_offset, + setup->partition_size, + new_partition_size, + &disk_uuid, + &table); + if (r < 0) + return r; + + if (new_fs_size > old_fs_size) { + + if (S_ISREG(st.st_mode)) { + /* Grow file size */ + + if (user_record_luks_discard(h)) + r = ftruncate(image_fd, new_image_size); + else + r = fallocate(image_fd, 0, 0, new_image_size); + if (r < 0) { + if (ERRNO_IS_DISK_SPACE(errno)) { + log_debug_errno(errno, "Not enough disk space to grow home."); + return -ENOSPC; /* make recognizable */ + } + + return log_error_errno(errno, "Failed to grow image file %s: %m", ip); + } + + log_info("Growing of image file completed."); + } + + /* Make sure loopback device sees the new bigger size */ + r = loop_device_refresh_size(setup->loop, UINT64_MAX, new_partition_size); + if (r == -ENOTTY) + log_debug_errno(r, "Device is not a loopback device, not refreshing size."); + else if (r < 0) + return log_error_errno(r, "Failed to refresh loopback device size: %m"); + else + log_info("Refreshing loop device size completed."); + + r = apply_resize_partition(image_fd, disk_uuid, table); + if (r < 0) + return r; + if (r > 0) + log_info("Growing of partition completed."); + + if (ioctl(image_fd, BLKRRPART, 0) < 0) + log_debug_errno(errno, "BLKRRPART failed on block device, ignoring: %m"); + + /* Tell LUKS about the new bigger size too */ + r = crypt_resize(setup->crypt_device, setup->dm_name, new_fs_size / 512U); + if (r < 0) + return log_error_errno(r, "Failed to grow LUKS device: %m"); + + log_info("LUKS device growing completed."); + } else { + r = home_store_embedded_identity(new_home, setup->root_fd, h->uid, embedded_home); + if (r < 0) + return r; + + if (S_ISREG(st.st_mode)) { + if (user_record_luks_discard(h)) + /* Before we shrink, let's trim the file system, so that we need less space on disk during the shrinking */ + (void) run_fitrim(setup->root_fd); + else { + /* If discard is off, let's ensure all backing blocks are allocated, so that our resize operation doesn't fail half-way */ + r = run_fallocate(image_fd, &st); + if (r < 0) + return r; + } + } + } + + /* Now resize the file system */ + if (resize_type == CAN_RESIZE_ONLINE) + r = resize_fs(setup->root_fd, new_fs_size, NULL); + else + r = ext4_offline_resize_fs(setup, new_fs_size, user_record_luks_discard(h)); + if (r < 0) + return log_error_errno(r, "Failed to resize file system: %m"); + + log_info("File system resizing completed."); + + /* Immediately sync afterwards */ + r = home_sync_and_statfs(setup->root_fd, NULL); + if (r < 0) + return r; + + if (new_fs_size < old_fs_size) { + + /* Shrink the LUKS device now, matching the new file system size */ + r = crypt_resize(setup->crypt_device, setup->dm_name, new_fs_size / 512); + if (r < 0) + return log_error_errno(r, "Failed to shrink LUKS device: %m"); + + log_info("LUKS device shrinking completed."); + + if (S_ISREG(st.st_mode)) { + /* Shrink the image file */ + if (ftruncate(image_fd, new_image_size) < 0) + return log_error_errno(errno, "Failed to shrink image file %s: %m", ip); + + log_info("Shrinking of image file completed."); + } + + /* Refresh the loop devices size */ + r = loop_device_refresh_size(setup->loop, UINT64_MAX, new_partition_size); + if (r == -ENOTTY) + log_debug_errno(r, "Device is not a loopback device, not refreshing size."); + else if (r < 0) + return log_error_errno(r, "Failed to refresh loopback device size: %m"); + else + log_info("Refreshing loop device size completed."); + + r = apply_resize_partition(image_fd, disk_uuid, table); + if (r < 0) + return r; + if (r > 0) + log_info("Shrinking of partition completed."); + + if (ioctl(image_fd, BLKRRPART, 0) < 0) + log_debug_errno(errno, "BLKRRPART failed on block device, ignoring: %m"); + } else { + r = home_store_embedded_identity(new_home, setup->root_fd, h->uid, embedded_home); + if (r < 0) + return r; + } + + r = home_store_header_identity_luks(new_home, setup, header_home); + if (r < 0) + return r; + + r = home_extend_embedded_identity(new_home, h, setup); + if (r < 0) + return r; + + if (user_record_luks_discard(h)) + (void) run_fitrim(setup->root_fd); + + r = home_sync_and_statfs(setup->root_fd, &sfs); + if (r < 0) + return r; + + r = home_setup_undo(setup); + if (r < 0) + return r; + + log_info("Everything completed."); + + print_size_summary(new_image_size, new_fs_size, &sfs); + + *ret_home = TAKE_PTR(new_home); + return 0; +} + +int home_passwd_luks( + UserRecord *h, + HomeSetup *setup, + char **pkcs11_decrypted_passwords, /* the passwords acquired via PKCS#11 security tokens */ + char **effective_passwords /* new passwords */) { + + size_t volume_key_size, i, max_key_slots, n_effective; + _cleanup_(erase_and_freep) void *volume_key = NULL; + struct crypt_pbkdf_type good_pbkdf, minimal_pbkdf; + const char *type; + int r; + + assert(h); + assert(user_record_storage(h) == USER_LUKS); + assert(setup); + + type = crypt_get_type(setup->crypt_device); + if (!type) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to determine crypto device type."); + + r = crypt_keyslot_max(type); + if (r <= 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to determine number of key slots."); + max_key_slots = r; + + r = crypt_get_volume_key_size(setup->crypt_device); + if (r <= 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to determine volume key size."); + volume_key_size = (size_t) r; + + volume_key = malloc(volume_key_size); + if (!volume_key) + return log_oom(); + + r = luks_try_passwords(setup->crypt_device, pkcs11_decrypted_passwords, volume_key, &volume_key_size); + if (r == -ENOKEY) { + r = luks_try_passwords(setup->crypt_device, h->password, volume_key, &volume_key_size); + if (r == -ENOKEY) + return log_error_errno(SYNTHETIC_ERRNO(ENOKEY), "Failed to unlock LUKS superblock with supplied passwords."); + } + if (r < 0) + return log_error_errno(r, "Failed to unlocks LUKS superblock: %m"); + + n_effective = strv_length(effective_passwords); + + build_good_pbkdf(&good_pbkdf, h); + build_minimal_pbkdf(&minimal_pbkdf, h); + + for (i = 0; i < max_key_slots; i++) { + r = crypt_keyslot_destroy(setup->crypt_device, i); + if (r < 0 && !IN_SET(r, -ENOENT, -EINVAL)) /* Returns EINVAL or ENOENT if there's no key in this slot already */ + return log_error_errno(r, "Failed to destroy LUKS password: %m"); + + if (i >= n_effective) { + if (r >= 0) + log_info("Destroyed LUKS key slot %zu.", i); + continue; + } + + if (strv_find(pkcs11_decrypted_passwords, effective_passwords[i])) { + log_debug("Using minimal PBKDF for slot %zu", i); + r = crypt_set_pbkdf_type(setup->crypt_device, &minimal_pbkdf); + } else { + log_debug("Using good PBKDF for slot %zu", i); + r = crypt_set_pbkdf_type(setup->crypt_device, &good_pbkdf); + } + if (r < 0) + return log_error_errno(r, "Failed to tweak PBKDF for slot %zu: %m", i); + + r = crypt_keyslot_add_by_volume_key( + setup->crypt_device, + i, + volume_key, + volume_key_size, + effective_passwords[i], + strlen(effective_passwords[i])); + if (r < 0) + return log_error_errno(r, "Failed to set up LUKS password: %m"); + + log_info("Updated LUKS key slot %zu.", i); + } + + return 1; +} + +int home_lock_luks(UserRecord *h) { + _cleanup_(crypt_freep) struct crypt_device *cd = NULL; + _cleanup_free_ char *dm_name = NULL, *dm_node = NULL; + _cleanup_close_ int root_fd = -1; + const char *p; + int r; + + assert(h); + + assert_se(p = user_record_home_directory(h)); + root_fd = open(p, O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW); + if (root_fd < 0) + return log_error_errno(errno, "Failed to open home directory: %m"); + + r = make_dm_names(h->user_name, &dm_name, &dm_node); + if (r < 0) + return r; + + r = crypt_init_by_name(&cd, dm_name); + if (r < 0) + return log_error_errno(r, "Failed to initialize cryptsetup context for %s: %m", dm_name); + + log_info("Discovered used LUKS device %s.", dm_node); + crypt_set_log_callback(cd, cryptsetup_log_glue, NULL); + + if (syncfs(root_fd) < 0) /* Snake oil, but let's better be safe than sorry */ + return log_error_errno(errno, "Failed to synchronize file system %s: %m", p); + + root_fd = safe_close(root_fd); + + log_info("File system synchronized."); + + /* Note that we don't invoke FIFREEZE here, it appears libcryptsetup/device-mapper already does that on its own for us */ + + r = crypt_suspend(cd, dm_name); + if (r < 0) + return log_error_errno(r, "Failed to suspend cryptsetup device: %s: %m", dm_node); + + log_info("LUKS device suspended."); + return 0; +} + +static int luks_try_resume( + struct crypt_device *cd, + const char *dm_name, + char **password) { + + char **pp; + int r; + + assert(cd); + assert(dm_name); + + STRV_FOREACH(pp, password) { + r = crypt_resume_by_passphrase( + cd, + dm_name, + CRYPT_ANY_SLOT, + *pp, + strlen(*pp)); + if (r >= 0) { + log_info("Resumed LUKS device %s.", dm_name); + return 0; + } + + log_debug_errno(r, "Password %zu didn't work for resuming device: %m", (size_t) (pp - password)); + } + + return -ENOKEY; +} + +int home_unlock_luks(UserRecord *h, char ***pkcs11_decrypted_passwords) { + _cleanup_free_ char *dm_name = NULL, *dm_node = NULL; + _cleanup_(crypt_freep) struct crypt_device *cd = NULL; + int r; + + assert(h); + + r = make_dm_names(h->user_name, &dm_name, &dm_node); + if (r < 0) + return r; + + r = crypt_init_by_name(&cd, dm_name); + if (r < 0) + return log_error_errno(r, "Failed to initialize cryptsetup context for %s: %m", dm_name); + + log_info("Discovered used LUKS device %s.", dm_node); + crypt_set_log_callback(cd, cryptsetup_log_glue, NULL); + + r = luks_try_resume(cd, dm_name, pkcs11_decrypted_passwords ? *pkcs11_decrypted_passwords : NULL); + if (r == -ENOKEY) { + r = luks_try_resume(cd, dm_name, h->password); + if (r == -ENOKEY) + return log_error_errno(r, "No valid password for LUKS superblock."); + } + if (r < 0) + return log_error_errno(r, "Failed to resume LUKS superblock: %m"); + + log_info("LUKS device resumed."); + return 0; +} diff --git a/src/home/homework-luks.h b/src/home/homework-luks.h new file mode 100644 index 0000000000..581255a223 --- /dev/null +++ b/src/home/homework-luks.h @@ -0,0 +1,38 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include "crypt-util.h" +#include "homework.h" +#include "user-record.h" + +int home_prepare_luks(UserRecord *h, bool already_activated, const char *force_image_path, char ***pkcs11_decrypted_passwords, HomeSetup *setup, UserRecord **ret_luks_home); + +int home_activate_luks(UserRecord *h, char ***pkcs11_decrypted_passwords, UserRecord **ret_home); +int home_deactivate_luks(UserRecord *h); + +int home_store_header_identity_luks(UserRecord *h, HomeSetup *setup, UserRecord *old_home); + +int home_create_luks(UserRecord *h, char **pkcs11_decrypted_passwords, char **effective_passwords, UserRecord **ret_home); + +int home_validate_update_luks(UserRecord *h, HomeSetup *setup); + +int home_resize_luks(UserRecord *h, bool already_activated, char ***pkcs11_decrypted_passwords, HomeSetup *setup, UserRecord **ret_home); + +int home_passwd_luks(UserRecord *h, HomeSetup *setup, char **pkcs11_decrypted_passwords, char **effective_passwords); + +int home_lock_luks(UserRecord *h); +int home_unlock_luks(UserRecord *h, char ***pkcs11_decrypted_passwords); + +static inline uint64_t luks_volume_key_size_convert(struct crypt_device *cd) { + int k; + + assert(cd); + + /* Convert the "int" to uint64_t, which we usually use for byte sizes stored on disk. */ + + k = crypt_get_volume_key_size(cd); + if (k <= 0) + return UINT64_MAX; + + return (uint64_t) k; +} diff --git a/src/home/homework-mount.c b/src/home/homework-mount.c new file mode 100644 index 0000000000..9e1116840d --- /dev/null +++ b/src/home/homework-mount.c @@ -0,0 +1,96 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include +#include + +#include "alloc-util.h" +#include "homework-mount.h" +#include "mkdir.h" +#include "mount-util.h" +#include "path-util.h" +#include "string-util.h" + +static const char *mount_options_for_fstype(const char *fstype) { + if (streq(fstype, "ext4")) + return "noquota,user_xattr"; + if (streq(fstype, "xfs")) + return "noquota"; + if (streq(fstype, "btrfs")) + return "noacl"; + return NULL; +} + +int home_mount_node(const char *node, const char *fstype, bool discard) { + _cleanup_free_ char *joined = NULL; + const char *options, *discard_option; + int r; + + options = mount_options_for_fstype(fstype); + + discard_option = discard ? "discard" : "nodiscard"; + + if (options) { + joined = strjoin(options, ",", discard_option); + if (!joined) + return log_oom(); + + options = joined; + } else + options = discard_option; + + r = mount_verbose(LOG_ERR, node, "/run/systemd/user-home-mount", fstype, MS_NODEV|MS_NOSUID|MS_RELATIME, strempty(options)); + if (r < 0) + return r; + + log_info("Mounting file system completed."); + return 0; +} + +int home_unshare_and_mount(const char *node, const char *fstype, bool discard) { + int r; + + if (unshare(CLONE_NEWNS) < 0) + return log_error_errno(errno, "Couldn't unshare file system namespace: %m"); + + r = mount_verbose(LOG_ERR, "/run", "/run", NULL, MS_SLAVE|MS_REC, NULL); /* Mark /run as MS_SLAVE in our new namespace */ + if (r < 0) + return r; + + (void) mkdir_p("/run/systemd/user-home-mount", 0700); + + if (node) + return home_mount_node(node, fstype, discard); + + return 0; +} + +int home_move_mount(const char *user_name_and_realm, const char *target) { + _cleanup_free_ char *subdir = NULL; + const char *d; + int r; + + assert(user_name_and_realm); + assert(target); + + if (user_name_and_realm) { + subdir = path_join("/run/systemd/user-home-mount/", user_name_and_realm); + if (!subdir) + return log_oom(); + + d = subdir; + } else + d = "/run/systemd/user-home-mount/"; + + (void) mkdir_p(target, 0700); + + r = mount_verbose(LOG_ERR, d, target, NULL, MS_BIND, NULL); + if (r < 0) + return r; + + r = umount_verbose("/run/systemd/user-home-mount"); + if (r < 0) + return r; + + log_info("Moving to final mount point %s completed.", target); + return 0; +} diff --git a/src/home/homework-mount.h b/src/home/homework-mount.h new file mode 100644 index 0000000000..d926756f7b --- /dev/null +++ b/src/home/homework-mount.h @@ -0,0 +1,8 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include + +int home_mount_node(const char *node, const char *fstype, bool discard); +int home_unshare_and_mount(const char *node, const char *fstype, bool discard); +int home_move_mount(const char *user_name_and_realm, const char *target); diff --git a/src/home/homework-pkcs11.c b/src/home/homework-pkcs11.c new file mode 100644 index 0000000000..941ba23b3c --- /dev/null +++ b/src/home/homework-pkcs11.c @@ -0,0 +1,104 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include "hexdecoct.h" +#include "homework-pkcs11.h" +#include "pkcs11-util.h" +#include "strv.h" + +int pkcs11_callback( + CK_FUNCTION_LIST *m, + CK_SESSION_HANDLE session, + CK_SLOT_ID slot_id, + const CK_SLOT_INFO *slot_info, + const CK_TOKEN_INFO *token_info, + P11KitUri *uri, + void *userdata) { + + _cleanup_(erase_and_freep) void *decrypted_key = NULL; + struct pkcs11_callback_data *data = userdata; + _cleanup_free_ char *token_label = NULL; + CK_TOKEN_INFO updated_token_info; + size_t decrypted_key_size; + CK_OBJECT_HANDLE object; + char **i; + CK_RV rv; + int r; + + assert(m); + assert(slot_info); + assert(token_info); + assert(uri); + assert(data); + + /* Special return values: + * + * -ENOANO → if we need a PIN but have none + * -ERFKILL → if a "protected authentication path" is needed but we have no OK to use it + * -EOWNERDEAD → if the PIN is locked + * -ENOLCK → if the supplied PIN is incorrect + * -ETOOMANYREFS → ditto, but only a few tries left + * -EUCLEAN → ditto, but only a single try left + */ + + token_label = pkcs11_token_label(token_info); + if (!token_label) + return log_oom(); + + if (FLAGS_SET(token_info->flags, CKF_PROTECTED_AUTHENTICATION_PATH)) { + + if (data->secret->pkcs11_protected_authentication_path_permitted <= 0) + return log_error_errno(SYNTHETIC_ERRNO(ERFKILL), "Security token requires authentication through protected authentication path."); + + rv = m->C_Login(session, CKU_USER, NULL, 0); + if (rv != CKR_OK) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to log into security token '%s': %s", token_label, p11_kit_strerror(rv)); + + log_info("Successully logged into security token '%s' via protected authentication path.", token_label); + goto decrypt; + } + + if (!FLAGS_SET(token_info->flags, CKF_LOGIN_REQUIRED)) { + log_info("No login into security token '%s' required.", token_label); + goto decrypt; + } + + if (strv_isempty(data->secret->pkcs11_pin)) + return log_error_errno(SYNTHETIC_ERRNO(ENOANO), "Security Token requires PIN."); + + STRV_FOREACH(i, data->secret->pkcs11_pin) { + rv = m->C_Login(session, CKU_USER, (CK_UTF8CHAR*) *i, strlen(*i)); + if (rv == CKR_OK) { + log_info("Successfully logged into security token '%s' with PIN.", token_label); + goto decrypt; + } + if (rv == CKR_PIN_LOCKED) + return log_error_errno(SYNTHETIC_ERRNO(EOWNERDEAD), "PIN of security token is blocked. Please unblock it first."); + if (!IN_SET(rv, CKR_PIN_INCORRECT, CKR_PIN_LEN_RANGE)) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to log into security token '%s': %s", token_label, p11_kit_strerror(rv)); + } + + rv = m->C_GetTokenInfo(slot_id, &updated_token_info); + if (rv != CKR_OK) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to acquire updated security token information for slot %lu: %s", slot_id, p11_kit_strerror(rv)); + + if (FLAGS_SET(updated_token_info.flags, CKF_USER_PIN_FINAL_TRY)) + return log_error_errno(SYNTHETIC_ERRNO(EUCLEAN), "PIN of security token incorrect, only a single try left."); + if (FLAGS_SET(updated_token_info.flags, CKF_USER_PIN_COUNT_LOW)) + return log_error_errno(SYNTHETIC_ERRNO(ETOOMANYREFS), "PIN of security token incorrect, only a few tries left."); + + return log_error_errno(SYNTHETIC_ERRNO(ENOLCK), "PIN of security token incorrect."); + +decrypt: + r = pkcs11_token_find_private_key(m, session, uri, &object); + if (r < 0) + return r; + + r = pkcs11_token_decrypt_data(m, session, object, data->encrypted_key->data, data->encrypted_key->size, &decrypted_key, &decrypted_key_size); + if (r < 0) + return r; + + if (base64mem(decrypted_key, decrypted_key_size, &data->decrypted_password) < 0) + return log_oom(); + + return 1; +} diff --git a/src/home/homework-pkcs11.h b/src/home/homework-pkcs11.h new file mode 100644 index 0000000000..469ba7152f --- /dev/null +++ b/src/home/homework-pkcs11.h @@ -0,0 +1,21 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#if HAVE_P11KIT +#include "memory-util.h" +#include "user-record.h" +#include "pkcs11-util.h" + +struct pkcs11_callback_data { + UserRecord *user_record; + UserRecord *secret; + Pkcs11EncryptedKey *encrypted_key; + char *decrypted_password; +}; + +static inline void pkcs11_callback_data_release(struct pkcs11_callback_data *data) { + erase_and_free(data->decrypted_password); +} + +int pkcs11_callback(CK_FUNCTION_LIST *m, CK_SESSION_HANDLE session, CK_SLOT_ID slot_id, const CK_SLOT_INFO *slot_info, const CK_TOKEN_INFO *token_info, P11KitUri *uri, void *userdata); +#endif diff --git a/src/home/homework-quota.c b/src/home/homework-quota.c new file mode 100644 index 0000000000..ba3917b9ce --- /dev/null +++ b/src/home/homework-quota.c @@ -0,0 +1,124 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#include + +#include "blockdev-util.h" +#include "btrfs-util.h" +#include "errno-util.h" +#include "format-util.h" +#include "homework-quota.h" +#include "missing_magic.h" +#include "quota-util.h" +#include "stat-util.h" +#include "user-util.h" + +int home_update_quota_btrfs(UserRecord *h, const char *path) { + int r; + + assert(h); + assert(path); + + if (h->disk_size == UINT64_MAX) + return 0; + + /* If the user wants quota, enable it */ + r = btrfs_quota_enable(path, true); + if (r == -ENOTTY) + return log_error_errno(r, "No btrfs quota support on subvolume %s.", path); + if (r < 0) + return log_error_errno(r, "Failed to enable btrfs quota support on %s.", path); + + r = btrfs_qgroup_set_limit(path, 0, h->disk_size); + if (r < 0) + return log_error_errno(r, "Faled to set disk quota on subvolume %s: %m", path); + + log_info("Set btrfs quota."); + + return 0; +} + +int home_update_quota_classic(UserRecord *h, const char *path) { + struct dqblk req; + dev_t devno; + int r; + + assert(h); + assert(uid_is_valid(h->uid)); + assert(path); + + if (h->disk_size == UINT64_MAX) + return 0; + + r = get_block_device(path, &devno); + if (r < 0) + return log_error_errno(r, "Failed to determine block device of %s: %m", path); + if (devno == 0) + return log_error_errno(SYNTHETIC_ERRNO(ENODEV), "File system %s not backed by a block device.", path); + + r = quotactl_devno(QCMD_FIXED(Q_GETQUOTA, USRQUOTA), devno, h->uid, &req); + if (r < 0) { + if (ERRNO_IS_NOT_SUPPORTED(r)) + return log_error_errno(r, "No UID quota support on %s.", path); + + if (r != -ESRCH) + return log_error_errno(r, "Failed to query disk quota for UID " UID_FMT ": %m", h->uid); + + zero(req); + } else { + /* Shortcut things if everything is set up properly already */ + if (FLAGS_SET(req.dqb_valid, QIF_BLIMITS) && h->disk_size / QIF_DQBLKSIZE == req.dqb_bhardlimit) { + log_info("Configured quota already matches the intended setting, not updating quota."); + return 0; + } + } + + req.dqb_valid = QIF_BLIMITS; + req.dqb_bsoftlimit = req.dqb_bhardlimit = h->disk_size / QIF_DQBLKSIZE; + + r = quotactl_devno(QCMD_FIXED(Q_SETQUOTA, USRQUOTA), devno, h->uid, &req); + if (r < 0) { + if (r == -ESRCH) + return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), "UID quota not available on %s.", path); + + return log_error_errno(r, "Failed to set disk quota for UID " UID_FMT ": %m", h->uid); + } + + log_info("Updated per-UID quota."); + + return 0; +} + +int home_update_quota_auto(UserRecord *h, const char *path) { + struct statfs sfs; + int r; + + assert(h); + + if (h->disk_size == UINT64_MAX) + return 0; + + if (!path) { + path = user_record_image_path(h); + if (!path) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Home record lacks image path."); + } + + if (statfs(path, &sfs) < 0) + return log_error_errno(errno, "Failed to statfs() file system: %m"); + + if (is_fs_type(&sfs, XFS_SB_MAGIC) || + is_fs_type(&sfs, EXT4_SUPER_MAGIC)) + return home_update_quota_classic(h, path); + + if (is_fs_type(&sfs, BTRFS_SUPER_MAGIC)) { + + r = btrfs_is_subvol(path); + if (r < 0) + return log_error_errno(errno, "Failed to test if %s is a subvolume: %m", path); + if (r == 0) + return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), "Directory %s is not a subvolume, cannot apply quota.", path); + + return home_update_quota_btrfs(h, path); + } + + return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), "Type of directory %s not known, cannot apply quota.", path); +} diff --git a/src/home/homework-quota.h b/src/home/homework-quota.h new file mode 100644 index 0000000000..e6cc16df50 --- /dev/null +++ b/src/home/homework-quota.h @@ -0,0 +1,8 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include "user-record.h" + +int home_update_quota_btrfs(UserRecord *h, const char *path); +int home_update_quota_classic(UserRecord *h, const char *path); +int home_update_quota_auto(UserRecord *h, const char *path); diff --git a/src/home/homework.c b/src/home/homework.c new file mode 100644 index 0000000000..ecf07ffb48 --- /dev/null +++ b/src/home/homework.c @@ -0,0 +1,1482 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include +#include + +#include "chown-recursive.h" +#include "copy.h" +#include "fd-util.h" +#include "fileio.h" +#include "home-util.h" +#include "homework-cifs.h" +#include "homework-directory.h" +#include "homework-fscrypt.h" +#include "homework-luks.h" +#include "homework-mount.h" +#include "homework-pkcs11.h" +#include "homework.h" +#include "main-func.h" +#include "memory-util.h" +#include "missing_magic.h" +#include "mount-util.h" +#include "path-util.h" +#include "pkcs11-util.h" +#include "rm-rf.h" +#include "stat-util.h" +#include "strv.h" +#include "tmpfile-util.h" +#include "user-util.h" +#include "virt.h" + +/* Make sure a bad password always results in a 3s delay, no matter what */ +#define BAD_PASSWORD_DELAY_USEC (3 * USEC_PER_SEC) + +int user_record_authenticate( + UserRecord *h, + UserRecord *secret, + char ***pkcs11_decrypted_passwords) { + + bool need_password = false, need_token = false, need_pin = false, need_protected_authentication_path_permitted = false, + pin_locked = false, pin_incorrect = false, pin_incorrect_few_tries_left = false, pin_incorrect_one_try_left = false; + size_t n; + int r; + + assert(h); + assert(secret); + + /* Tries to authenticate a user record with the supplied secrets. i.e. checks whether at least one + * supplied plaintext passwords matches a hashed password field of the user record. Or if a + * configured PKCS#11 token is around and can unlock the record. + * + * Note that the pkcs11_decrypted_passwords parameter is both an input and and output parameter: it + * is a list of configured, decrypted PKCS#11 passwords. We typically have to call this function + * multiple times over the course of an operation (think: on login we authenticate the host user + * record, the record embedded in the LUKS record and the one embedded in $HOME). Hence we keep a + * list of passwords we already decrypted, so that we don't have to do the (slow an potentially + * interactive) PKCS#11 dance for the relevant token again and again. */ + + /* First, let's see if the supplied plain-text passwords work? */ + r = user_record_test_secret(h, secret); + if (r == -ENOKEY) { + log_info_errno(r, "None of the supplied plaintext passwords unlocks the user record's hashed passwords."); + need_password = true; + } else if (r == -ENXIO) + log_debug_errno(r, "User record has no hashed passwords, plaintext passwords not tested."); + else if (r < 0) + return log_error_errno(r, "Failed to validate password of record: %m"); + else { + log_info("Provided password unlocks user record."); + return 0; + } + + /* Second, let's see if any of the PKCS#11 security tokens are plugged in and help us */ + for (n = 0; n < h->n_pkcs11_encrypted_key; n++) { +#if HAVE_P11KIT + _cleanup_(pkcs11_callback_data_release) struct pkcs11_callback_data data = { + .user_record = h, + .secret = secret, + .encrypted_key = h->pkcs11_encrypted_key + n, + }; + char **pp; + + /* See if any of the previously calculated passwords work */ + STRV_FOREACH(pp, *pkcs11_decrypted_passwords) { + r = test_password_one(data.encrypted_key->hashed_password, *pp); + if (r < 0) + return log_error_errno(r, "Failed to check supplied PKCS#11 password: %m"); + if (r > 0) { + log_info("Previously acquired PKCS#11 password unlocks user record."); + return 0; + } + } + + r = pkcs11_find_token(data.encrypted_key->uri, pkcs11_callback, &data); + switch (r) { + case -EAGAIN: + need_token = true; + break; + case -ENOANO: + need_pin = true; + break; + case -ERFKILL: + need_protected_authentication_path_permitted = true; + break; + case -EOWNERDEAD: + pin_locked = true; + break; + case -ENOLCK: + pin_incorrect = true; + break; + case -ETOOMANYREFS: + pin_incorrect = pin_incorrect_few_tries_left = true; + break; + case -EUCLEAN: + pin_incorrect = pin_incorrect_few_tries_left = pin_incorrect_one_try_left = true; + break; + default: + if (r < 0) + return r; + + r = test_password_one(data.encrypted_key->hashed_password, data.decrypted_password); + if (r < 0) + return log_error_errno(r, "Failed to test PKCS#11 password: %m"); + if (r == 0) + return log_error_errno(SYNTHETIC_ERRNO(EPERM), "Configured PKCS#11 security token %s does not decrypt encrypted key correctly.", data.encrypted_key->uri); + + log_info("Decrypted password from PKCS#11 security token %s unlocks user record.", data.encrypted_key->uri); + + r = strv_extend(pkcs11_decrypted_passwords, data.decrypted_password); + if (r < 0) + return log_oom(); + + return 0; + } +#else + need_token = true; + break; +#endif + } + + /* Ordered by "relevance", i.e. the most "important" or "interesting" error condition is returned. */ + if (pin_incorrect_one_try_left) + return -EUCLEAN; + if (pin_incorrect_few_tries_left) + return -ETOOMANYREFS; + if (pin_incorrect) + return -ENOLCK; + if (pin_locked) + return -EOWNERDEAD; + if (need_protected_authentication_path_permitted) + return -ERFKILL; + if (need_pin) + return -ENOANO; + if (need_token) + return -EBADSLT; + if (need_password) + return -ENOKEY; + + /* Hmm, this means neither PCKS#11 nor classic hashed passwords were supplied, we cannot authenticate this reasonably */ + return log_debug_errno(SYNTHETIC_ERRNO(EKEYREVOKED), "No hashed passwords and no PKCS#11 tokens defined, cannot authenticate user record."); +} + +int home_setup_undo(HomeSetup *setup) { + int r = 0, q; + + assert(setup); + + setup->root_fd = safe_close(setup->root_fd); + + if (setup->undo_mount) { + q = umount_verbose("/run/systemd/user-home-mount"); + if (q < 0) + r = q; + } + + if (setup->undo_dm && setup->crypt_device && setup->dm_name) { + q = crypt_deactivate(setup->crypt_device, setup->dm_name); + if (q < 0) + r = q; + } + + setup->undo_mount = false; + setup->undo_dm = false; + + setup->dm_name = mfree(setup->dm_name); + setup->dm_node = mfree(setup->dm_node); + + setup->loop = loop_device_unref(setup->loop); + crypt_free(setup->crypt_device); + setup->crypt_device = NULL; + + explicit_bzero_safe(setup->volume_key, setup->volume_key_size); + setup->volume_key = mfree(setup->volume_key); + setup->volume_key_size = 0; + + return r; +} + +int home_prepare( + UserRecord *h, + bool already_activated, + char ***pkcs11_decrypted_passwords, + HomeSetup *setup, + UserRecord **ret_header_home) { + + int r; + + assert(h); + assert(setup); + assert(!setup->loop); + assert(!setup->crypt_device); + assert(setup->root_fd < 0); + assert(!setup->undo_dm); + assert(!setup->undo_mount); + + /* Makes a home directory accessible (through the root_fd file descriptor, not by path!). */ + + switch (user_record_storage(h)) { + + case USER_LUKS: + return home_prepare_luks(h, already_activated, NULL, pkcs11_decrypted_passwords, setup, ret_header_home); + + case USER_SUBVOLUME: + case USER_DIRECTORY: + r = home_prepare_directory(h, already_activated, setup); + break; + + case USER_FSCRYPT: + r = home_prepare_fscrypt(h, already_activated, pkcs11_decrypted_passwords, setup); + break; + + case USER_CIFS: + r = home_prepare_cifs(h, already_activated, setup); + break; + + default: + return log_error_errno(SYNTHETIC_ERRNO(ENOLINK), "Processing home directories of type '%s' currently not supported.", user_storage_to_string(user_record_storage(h))); + } + + if (r < 0) + return r; + + if (ret_header_home) + *ret_header_home = NULL; + + return r; +} + +int home_sync_and_statfs(int root_fd, struct statfs *ret) { + assert(root_fd >= 0); + + /* Let's sync this to disk, so that the disk space reported by fstatfs() below is accurate (for file + * systems such as btrfs where this is determined lazily). */ + + if (syncfs(root_fd) < 0) + return log_error_errno(errno, "Failed to synchronize file system: %m"); + + if (ret) + if (fstatfs(root_fd, ret) < 0) + return log_error_errno(errno, "Failed to statfs() file system: %m"); + + log_info("Synchronized disk."); + + return 0; +} + +static int read_identity_file(int root_fd, JsonVariant **ret) { + _cleanup_(fclosep) FILE *identity_file = NULL; + _cleanup_close_ int identity_fd = -1; + unsigned line, column; + int r; + + assert(root_fd >= 0); + assert(ret); + + identity_fd = openat(root_fd, ".identity", O_RDONLY|O_CLOEXEC|O_NOCTTY|O_NOFOLLOW|O_NONBLOCK); + if (identity_fd < 0) + return log_error_errno(errno, "Failed to open .identity file in home directory: %m"); + + r = fd_verify_regular(identity_fd); + if (r < 0) + return log_error_errno(r, "Embedded identity file is not a regular file, refusing: %m"); + + identity_file = fdopen(identity_fd, "r"); + if (!identity_file) + return log_oom(); + + identity_fd = -1; + + r = json_parse_file(identity_file, ".identity", JSON_PARSE_SENSITIVE, ret, &line, &column); + if (r < 0) + return log_error_errno(r, "[.identity:%u:%u] Failed to parse JSON data: %m", line, column); + + log_info("Read embedded .identity file."); + + return 0; +} + +static int write_identity_file(int root_fd, JsonVariant *v, uid_t uid) { + _cleanup_(json_variant_unrefp) JsonVariant *normalized = NULL; + _cleanup_(fclosep) FILE *identity_file = NULL; + _cleanup_close_ int identity_fd = -1; + _cleanup_free_ char *fn = NULL; + int r; + + assert(root_fd >= 0); + assert(v); + + normalized = json_variant_ref(v); + + r = json_variant_normalize(&normalized); + if (r < 0) + log_warning_errno(r, "Failed to normalize user record, ignoring: %m"); + + r = tempfn_random(".identity", NULL, &fn); + if (r < 0) + return r; + + identity_fd = openat(root_fd, fn, O_WRONLY|O_CREAT|O_EXCL|O_CLOEXEC|O_NOCTTY|O_NOFOLLOW, 0600); + if (identity_fd < 0) + return log_error_errno(errno, "Failed to create .identity file in home directory: %m"); + + identity_file = fdopen(identity_fd, "w"); + if (!identity_file) { + r = log_oom(); + goto fail; + } + + identity_fd = -1; + + json_variant_dump(normalized, JSON_FORMAT_PRETTY, identity_file, NULL); + + r = fflush_and_check(identity_file); + if (r < 0) { + log_error_errno(r, "Failed to write .identity file: %m"); + goto fail; + } + + if (fchown(fileno(identity_file), uid, uid) < 0) { + log_error_errno(r, "Failed to change ownership of identity file: %m"); + goto fail; + } + + if (renameat(root_fd, fn, root_fd, ".identity") < 0) { + r = log_error_errno(errno, "Failed to move identity file into place: %m"); + goto fail; + } + + log_info("Wrote embedded .identity file."); + + return 0; + +fail: + (void) unlinkat(root_fd, fn, 0); + return r; +} + +int home_load_embedded_identity( + UserRecord *h, + int root_fd, + UserRecord *header_home, + UserReconcileMode mode, + char ***pkcs11_decrypted_passwords, + UserRecord **ret_embedded_home, + UserRecord **ret_new_home) { + + _cleanup_(user_record_unrefp) UserRecord *embedded_home = NULL, *intermediate_home = NULL, *new_home = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + int r; + + assert(h); + assert(root_fd >= 0); + + r = read_identity_file(root_fd, &v); + if (r < 0) + return r; + + embedded_home = user_record_new(); + if (!embedded_home) + return log_oom(); + + r = user_record_load(embedded_home, v, USER_RECORD_LOAD_EMBEDDED); + if (r < 0) + return r; + + if (!user_record_compatible(h, embedded_home)) + return log_error_errno(SYNTHETIC_ERRNO(EREMCHG), "Hmbedded home record not compatible with host record, refusing."); + + /* Insist that credentials the user supplies also unlocks any embedded records. */ + r = user_record_authenticate(embedded_home, h, pkcs11_decrypted_passwords); + if (r < 0) + return r; + + /* At this point we have three records to deal with: + * + * · The record we got passed from the host + * · The record included in the LUKS header (only if LUKS is used) + * · The record in the home directory itself (~.identity) + * + * Now we have to reconcile all three, and let the newest one win. */ + + if (header_home) { + /* Note we relax the requirements here. Instead of insisting that the host record is strictly + * newer, let's also be OK if its equally new. If it is, we'll however insist that the + * embedded record must be newer, so that we update at least one of the two. */ + + r = user_record_reconcile(h, header_home, mode == USER_RECONCILE_REQUIRE_NEWER ? USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL : mode, &intermediate_home); + if (r == -EREMCHG) /* this was supposed to be checked earlier already, but let's check this again */ + return log_error_errno(r, "Identity stored on host and in header don't match, refusing."); + if (r == -ESTALE) + return log_error_errno(r, "Embedded identity record is newer than supplied record, refusing."); + if (r < 0) + return log_error_errno(r, "Failed to reconcile host and header identities: %m"); + if (r == USER_RECONCILE_EMBEDDED_WON) + log_info("Reconciling header user identity completed (header version was newer)."); + else if (r == USER_RECONCILE_HOST_WON) { + log_info("Reconciling header user identity completed (host version was newer)."); + + if (mode == USER_RECONCILE_REQUIRE_NEWER) /* Host version is newer than the header + * version, hence we'll update + * something. This means we can relax the + * requirements on the embedded + * identity. */ + mode = USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL; + } else { + assert(r == USER_RECONCILE_IDENTICAL); + log_info("Reconciling user identities completed (host and header version were identical)."); + } + + h = intermediate_home; + } + + r = user_record_reconcile(h, embedded_home, mode, &new_home); + if (r == -EREMCHG) + return log_error_errno(r, "Identity stored on host and in home don't match, refusing."); + if (r == -ESTALE) + return log_error_errno(r, "Embedded identity record is equally new or newer than supplied record, refusing."); + if (r < 0) + return log_error_errno(r, "Failed to reconcile host and embedded identities: %m"); + if (r == USER_RECONCILE_EMBEDDED_WON) + log_info("Reconciling embedded user identity completed (embedded version was newer)."); + else if (r == USER_RECONCILE_HOST_WON) + log_info("Reconciling embedded user identity completed (host version was newer)."); + else { + assert(r == USER_RECONCILE_IDENTICAL); + log_info("Reconciling embedded user identity completed (host and embedded version were identical)."); + } + + if (ret_embedded_home) + *ret_embedded_home = TAKE_PTR(embedded_home); + + if (ret_new_home) + *ret_new_home = TAKE_PTR(new_home); + + return 0; +} + +int home_store_embedded_identity(UserRecord *h, int root_fd, uid_t uid, UserRecord *old_home) { + _cleanup_(user_record_unrefp) UserRecord *embedded = NULL; + int r; + + assert(h); + assert(root_fd >= 0); + assert(uid_is_valid(uid)); + + r = user_record_clone(h, USER_RECORD_EXTRACT_EMBEDDED, &embedded); + if (r < 0) + return log_error_errno(r, "Failed to determine new embedded record: %m"); + + if (old_home && user_record_equal(old_home, embedded)) { + log_debug("Not updating embedded home record."); + return 0; + } + + /* The identity has changed, let's update it in the image */ + r = write_identity_file(root_fd, embedded->json, h->uid); + if (r < 0) + return r; + + return 1; +} + +static const char *file_system_type_fd(int fd) { + struct statfs sfs; + + assert(fd >= 0); + + if (fstatfs(fd, &sfs) < 0) { + log_debug_errno(errno, "Failed to statfs(): %m"); + return NULL; + } + + if (is_fs_type(&sfs, XFS_SB_MAGIC)) + return "xfs"; + if (is_fs_type(&sfs, EXT4_SUPER_MAGIC)) + return "ext4"; + if (is_fs_type(&sfs, BTRFS_SUPER_MAGIC)) + return "btrfs"; + + return NULL; +} + +int home_extend_embedded_identity(UserRecord *h, UserRecord *used, HomeSetup *setup) { + int r; + + assert(h); + assert(used); + assert(setup); + + r = user_record_add_binding( + h, + user_record_storage(used), + user_record_image_path(used), + setup->found_partition_uuid, + setup->found_luks_uuid, + setup->found_fs_uuid, + setup->crypt_device ? crypt_get_cipher(setup->crypt_device) : NULL, + setup->crypt_device ? crypt_get_cipher_mode(setup->crypt_device) : NULL, + setup->crypt_device ? luks_volume_key_size_convert(setup->crypt_device) : UINT64_MAX, + file_system_type_fd(setup->root_fd), + user_record_home_directory(used), + used->uid, + (gid_t) used->uid); + if (r < 0) + return log_error_errno(r, "Failed to update binding in record: %m"); + + return 0; +} + +static int chown_recursive_directory(int root_fd, uid_t uid) { + int r; + + assert(root_fd >= 0); + assert(uid_is_valid(uid)); + + r = fd_chown_recursive(root_fd, uid, (gid_t) uid, 0777); + if (r < 0) + return log_error_errno(r, "Failed to change ownership of files and directories: %m"); + if (r == 0) + log_info("Recursive changing of ownership not necessary, skipped."); + else + log_info("Recursive changing of ownership completed."); + + return 0; +} + +int home_refresh( + UserRecord *h, + HomeSetup *setup, + UserRecord *header_home, + char ***pkcs11_decrypted_passwords, + struct statfs *ret_statfs, + UserRecord **ret_new_home) { + + _cleanup_(user_record_unrefp) UserRecord *embedded_home = NULL, *new_home = NULL; + int r; + + assert(h); + assert(setup); + assert(ret_new_home); + + /* When activating a home directory, does the identity work: loads the identity from the $HOME + * directory, reconciles it with our idea, chown()s everything. */ + + r = home_load_embedded_identity(h, setup->root_fd, header_home, USER_RECONCILE_ANY, pkcs11_decrypted_passwords, &embedded_home, &new_home); + if (r < 0) + return r; + + r = home_store_header_identity_luks(new_home, setup, header_home); + if (r < 0) + return r; + + r = home_store_embedded_identity(new_home, setup->root_fd, h->uid, embedded_home); + if (r < 0) + return r; + + r = chown_recursive_directory(setup->root_fd, h->uid); + if (r < 0) + return r; + + r = home_sync_and_statfs(setup->root_fd, ret_statfs); + if (r < 0) + return r; + + *ret_new_home = TAKE_PTR(new_home); + return 0; +} + +static int home_activate(UserRecord *h, UserRecord **ret_home) { + _cleanup_(strv_free_erasep) char **pkcs11_decrypted_passwords = NULL; + _cleanup_(user_record_unrefp) UserRecord *new_home = NULL; + int r; + + assert(h); + + if (!h->user_name) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record lacks user name, refusing."); + if (!uid_is_valid(h->uid)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record lacks UID, refusing."); + if (!IN_SET(user_record_storage(h), USER_LUKS, USER_DIRECTORY, USER_SUBVOLUME, USER_FSCRYPT, USER_CIFS)) + return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), "Activating home directories of type '%s' currently not supported.", user_storage_to_string(user_record_storage(h))); + + r = user_record_authenticate(h, h, &pkcs11_decrypted_passwords); + if (r < 0) + return r; + + r = user_record_test_home_directory_and_warn(h); + if (r < 0) + return r; + if (r == USER_TEST_MOUNTED) + return log_error_errno(SYNTHETIC_ERRNO(EALREADY), "Home directory %s is already mounted, refusing.", user_record_home_directory(h)); + + r = user_record_test_image_path_and_warn(h); + if (r < 0) + return r; + if (r == USER_TEST_ABSENT) + return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Image path %s is missing, refusing.", user_record_image_path(h)); + + switch (user_record_storage(h)) { + + case USER_LUKS: + r = home_activate_luks(h, &pkcs11_decrypted_passwords, &new_home); + if (r < 0) + return r; + + break; + + case USER_SUBVOLUME: + case USER_DIRECTORY: + case USER_FSCRYPT: + r = home_activate_directory(h, &pkcs11_decrypted_passwords, &new_home); + if (r < 0) + return r; + + break; + + case USER_CIFS: + r = home_activate_cifs(h, &pkcs11_decrypted_passwords, &new_home); + if (r < 0) + return r; + + break; + + default: + assert_not_reached("unexpected type"); + } + + /* Note that the returned object might either be a reference to an updated version of the existing + * home object, or a reference to a newly allocated home object. The caller has to be able to deal + * with both, and consider the old object out-of-date. */ + if (user_record_equal(h, new_home)) { + *ret_home = NULL; + return 0; /* no identity change */ + } + + *ret_home = TAKE_PTR(new_home); + return 1; /* identity updated */ +} + +static int home_deactivate(UserRecord *h, bool force) { + bool done = false; + int r; + + assert(h); + + if (!h->user_name) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record incomplete, refusing."); + if (!IN_SET(user_record_storage(h), USER_LUKS, USER_DIRECTORY, USER_SUBVOLUME, USER_FSCRYPT, USER_CIFS)) + return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), "Deactivating home directories of type '%s' currently not supported.", user_storage_to_string(user_record_storage(h))); + + r = user_record_test_home_directory_and_warn(h); + if (r < 0) + return r; + if (r == USER_TEST_MOUNTED) { + if (umount2(user_record_home_directory(h), UMOUNT_NOFOLLOW | (force ? MNT_FORCE|MNT_DETACH : 0)) < 0) + return log_error_errno(errno, "Failed to unmount %s: %m", user_record_home_directory(h)); + + log_info("Unmounting completed."); + done = true; + } else + log_info("Directory %s is already unmounted.", user_record_home_directory(h)); + + if (user_record_storage(h) == USER_LUKS) { + r = home_deactivate_luks(h); + if (r < 0) + return r; + if (r > 0) + done = true; + } + + if (!done) + return log_error_errno(SYNTHETIC_ERRNO(ENOEXEC), "Home is not active."); + + log_info("Everything completed."); + return 0; +} + +static int copy_skel(int root_fd, const char *skel) { + int r; + + assert(root_fd >= 0); + + r = copy_tree_at(AT_FDCWD, skel, root_fd, ".", UID_INVALID, GID_INVALID, COPY_MERGE|COPY_REPLACE); + if (r == -ENOENT) { + log_info("Skeleton directory %s missing, ignoring.", skel); + return 0; + } + if (r < 0) + return log_error_errno(r, "Failed to copy in %s: %m", skel); + + log_info("Copying in %s completed.", skel); + return 0; +} + +static int change_access_mode(int root_fd, mode_t m) { + assert(root_fd >= 0); + + if (fchmod(root_fd, m) < 0) + return log_error_errno(errno, "Failed to change access mode of top-level directory: %m"); + + log_info("Changed top-level directory access mode to 0%o.", m); + return 0; +} + +int home_populate(UserRecord *h, int dir_fd) { + int r; + + assert(h); + assert(dir_fd >= 0); + + r = copy_skel(dir_fd, user_record_skeleton_directory(h)); + if (r < 0) + return r; + + r = home_store_embedded_identity(h, dir_fd, h->uid, NULL); + if (r < 0) + return r; + + r = chown_recursive_directory(dir_fd, h->uid); + if (r < 0) + return r; + + r = change_access_mode(dir_fd, user_record_access_mode(h)); + if (r < 0) + return r; + + return 0; +} + +static int user_record_compile_effective_passwords( + UserRecord *h, + char ***ret_effective_passwords, + char ***ret_pkcs11_decrypted_passwords) { + + _cleanup_(strv_free_erasep) char **effective = NULL, **pkcs11_passwords = NULL; + size_t n; + char **i; + int r; + + assert(h); + + /* We insist on at least one classic hashed password to be defined in addition to any PKCS#11 one, as + * a safe fallback, but also to simplify the password changing algorithm: there we require providing + * the old literal password only (and do not care for the old PKCS#11 token) */ + + if (strv_isempty(h->hashed_password)) + return log_error_errno(EINVAL, "User record has no hashed passwords, refusing."); + + /* Generates the list of plaintext passwords to propagate to LUKS/fscrypt devices, and checks whether + * we have a plaintext password for each hashed one. If we are missing one we'll fail, since we + * couldn't sync fscrypt/LUKS to the login account properly. */ + + STRV_FOREACH(i, h->hashed_password) { + bool found = false; + char **j; + + log_debug("Looking for plaintext password for: %s", *i); + + /* Let's scan all provided plaintext passwords */ + STRV_FOREACH(j, h->password) { + r = test_password_one(*i, *j); + if (r < 0) + return log_error_errno(r, "Failed to test plain text password: %m"); + if (r > 0) { + if (ret_effective_passwords) { + r = strv_extend(&effective, *j); + if (r < 0) + return log_oom(); + } + + log_debug("Found literal plaintext password."); + found = true; + break; + } + } + + if (!found) + return log_error_errno(SYNTHETIC_ERRNO(ENOKEY), "Missing plaintext password for defined hashed password"); + } + + for (n = 0; n < h->n_pkcs11_encrypted_key; n++) { +#if HAVE_P11KIT + _cleanup_(pkcs11_callback_data_release) struct pkcs11_callback_data data = { + .user_record = h, + .secret = h, + .encrypted_key = h->pkcs11_encrypted_key + n, + }; + + r = pkcs11_find_token(data.encrypted_key->uri, pkcs11_callback, &data); + if (r == -EAGAIN) + return -EBADSLT; + if (r < 0) + return r; + + r = test_password_one(data.encrypted_key->hashed_password, data.decrypted_password); + if (r < 0) + return log_error_errno(r, "Failed to test PKCS#11 password: %m"); + if (r == 0) + return log_error_errno(SYNTHETIC_ERRNO(EPERM), "Decrypted password from token is not correct, refusing."); + + if (ret_effective_passwords) { + r = strv_extend(&effective, data.decrypted_password); + if (r < 0) + return log_oom(); + } + + if (ret_pkcs11_decrypted_passwords) { + r = strv_extend(&pkcs11_passwords, data.decrypted_password); + if (r < 0) + return log_oom(); + } +#else + return -EBADSLT; +#endif + } + + if (ret_effective_passwords) + *ret_effective_passwords = TAKE_PTR(effective); + if (ret_pkcs11_decrypted_passwords) + *ret_pkcs11_decrypted_passwords = TAKE_PTR(pkcs11_passwords); + + return 0; +} + +static int home_create(UserRecord *h, UserRecord **ret_home) { + _cleanup_(strv_free_erasep) char **effective_passwords = NULL, **pkcs11_decrypted_passwords = NULL; + _cleanup_(user_record_unrefp) UserRecord *new_home = NULL; + int r; + + assert(h); + + if (!h->user_name) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record lacks name, refusing."); + if (!uid_is_valid(h->uid)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record lacks UID, refusing."); + + r = user_record_compile_effective_passwords(h, &effective_passwords, &pkcs11_decrypted_passwords); + if (r < 0) + return r; + + r = user_record_test_home_directory_and_warn(h); + if (r < 0) + return r; + if (r != USER_TEST_ABSENT) + return log_error_errno(SYNTHETIC_ERRNO(EEXIST), "Home directory %s already exists, refusing.", user_record_home_directory(h)); + + /* When the user didn't specify the storage type to use, fix it to be LUKS -- unless we run in a + * container where loopback devices and LUKS/DM are not available. Note that we typically default to + * the assumption of "classic" storage for most operations. However, if we create a new home, then + * let's user LUKS if nothing is specified. */ + if (h->storage < 0) { + UserStorage new_storage; + + r = detect_container(); + if (r < 0) + return log_error_errno(r, "Failed to determine whether we are in a container: %m"); + if (r > 0) { + new_storage = USER_DIRECTORY; + + r = path_is_fs_type("/home", BTRFS_SUPER_MAGIC); + if (r < 0) + log_debug_errno(r, "Failed to determine file system of /home, ignoring: %m"); + + new_storage = r > 0 ? USER_SUBVOLUME : USER_DIRECTORY; + } else + new_storage = USER_LUKS; + + r = user_record_add_binding( + h, + new_storage, + NULL, + SD_ID128_NULL, + SD_ID128_NULL, + SD_ID128_NULL, + NULL, + NULL, + UINT64_MAX, + NULL, + NULL, + UID_INVALID, + GID_INVALID); + if (r < 0) + return log_error_errno(r, "Failed to change storage type to LUKS: %m"); + + if (!h->image_path_auto) { + h->image_path_auto = strjoin("/home/", user_record_user_name_and_realm(h), new_storage == USER_LUKS ? ".home" : ".homedir"); + if (!h->image_path_auto) + return log_oom(); + } + } + + r = user_record_test_image_path_and_warn(h); + if (r < 0) + return r; + if (!IN_SET(r, USER_TEST_ABSENT, USER_TEST_UNDEFINED, USER_TEST_MAYBE)) + return log_error_errno(SYNTHETIC_ERRNO(EEXIST), "Image path %s already exists, refusing.", user_record_image_path(h)); + + switch (user_record_storage(h)) { + + case USER_LUKS: + r = home_create_luks(h, pkcs11_decrypted_passwords, effective_passwords, &new_home); + break; + + case USER_DIRECTORY: + case USER_SUBVOLUME: + r = home_create_directory_or_subvolume(h, &new_home); + break; + + case USER_FSCRYPT: + r = home_create_fscrypt(h, effective_passwords, &new_home); + break; + + case USER_CIFS: + r = home_create_cifs(h, &new_home); + break; + + default: + return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), + "Creating home directories of type '%s' currently not supported.", user_storage_to_string(user_record_storage(h))); + } + if (r < 0) + return r; + + if (user_record_equal(h, new_home)) { + *ret_home = NULL; + return 0; + } + + *ret_home = TAKE_PTR(new_home); + return 1; +} + +static int home_remove(UserRecord *h) { + bool deleted = false; + const char *ip, *hd; + int r; + + assert(h); + + if (!h->user_name) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record lacks user name, refusing."); + if (!IN_SET(user_record_storage(h), USER_LUKS, USER_DIRECTORY, USER_SUBVOLUME, USER_FSCRYPT, USER_CIFS)) + return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), "Removing home directories of type '%s' currently not supported.", user_storage_to_string(user_record_storage(h))); + + hd = user_record_home_directory(h); + + r = user_record_test_home_directory_and_warn(h); + if (r < 0) + return r; + if (r == USER_TEST_MOUNTED) + return log_error_errno(SYNTHETIC_ERRNO(EBUSY), "Directory %s is still mounted, refusing.", hd); + + assert(hd); + + r = user_record_test_image_path_and_warn(h); + if (r < 0) + return r; + + ip = user_record_image_path(h); + + switch (user_record_storage(h)) { + + case USER_LUKS: { + struct stat st; + + assert(ip); + + if (stat(ip, &st) < 0) { + if (errno != -ENOENT) + return log_error_errno(errno, "Failed to stat %s: %m", ip); + + } else { + if (S_ISREG(st.st_mode)) { + if (unlink(ip) < 0) { + if (errno != ENOENT) + return log_error_errno(errno, "Failed to remove %s: %m", ip); + } else + deleted = true; + + } else if (S_ISBLK(st.st_mode)) + log_info("Not removing file system on block device %s.", ip); + else + return log_error_errno(SYNTHETIC_ERRNO(ENOTBLK), "Image file %s is neither block device, nor regular, refusing removal.", ip); + } + + break; + } + + case USER_SUBVOLUME: + case USER_DIRECTORY: + case USER_FSCRYPT: + assert(ip); + + r = rm_rf(ip, REMOVE_ROOT|REMOVE_PHYSICAL|REMOVE_SUBVOLUME); + if (r < 0) { + if (r != -ENOENT) + return log_warning_errno(r, "Failed to remove %s: %m", ip); + } else + deleted = true; + + /* If the image path and the home directory are the same invalidate the home directory, so + * that we don't remove it anymore */ + if (path_equal(ip, hd)) + hd = NULL; + + break; + + case USER_CIFS: + /* Nothing else to do here: we won't remove remote stuff. */ + log_info("Not removing home directory on remote server."); + break; + + default: + assert_not_reached("unknown storage type"); + } + + if (hd) { + if (rmdir(hd) < 0) { + if (errno != ENOENT) + return log_error_errno(errno, "Failed to remove %s, ignoring: %m", hd); + } else + deleted = true; + } + + if (deleted) + log_info("Everything completed."); + else { + log_notice("Nothing to remove."); + return -EALREADY; + } + + return 0; +} + +static int home_validate_update(UserRecord *h, HomeSetup *setup) { + bool has_mount = false; + int r; + + assert(h); + assert(setup); + + if (!h->user_name) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record lacks user name, refusing."); + if (!uid_is_valid(h->uid)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record lacks UID, refusing."); + if (!IN_SET(user_record_storage(h), USER_LUKS, USER_DIRECTORY, USER_SUBVOLUME, USER_FSCRYPT, USER_CIFS)) + return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), "Processing home directories of type '%s' currently not supported.", user_storage_to_string(user_record_storage(h))); + + r = user_record_test_home_directory_and_warn(h); + if (r < 0) + return r; + + has_mount = r == USER_TEST_MOUNTED; + + r = user_record_test_image_path_and_warn(h); + if (r < 0) + return r; + if (r == USER_TEST_ABSENT) + return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Image path %s does not exist", user_record_image_path(h)); + + switch (user_record_storage(h)) { + + case USER_DIRECTORY: + case USER_SUBVOLUME: + case USER_FSCRYPT: + case USER_CIFS: + break; + + case USER_LUKS: { + r = home_validate_update_luks(h, setup); + if (r < 0) + return r; + if ((r > 0) != has_mount) + return log_error_errno(SYNTHETIC_ERRNO(EBUSY), "Home mount incompletely set up."); + + break; + } + + default: + assert_not_reached("unexpected storage type"); + } + + return has_mount; /* return true if the home record is already active */ +} + +static int home_update(UserRecord *h, UserRecord **ret) { + _cleanup_(user_record_unrefp) UserRecord *new_home = NULL, *header_home = NULL, *embedded_home = NULL; + _cleanup_(strv_free_erasep) char **pkcs11_decrypted_passwords = NULL; + _cleanup_(home_setup_undo) HomeSetup setup = HOME_SETUP_INIT; + bool already_activated = false; + int r; + + assert(h); + assert(ret); + + r = user_record_authenticate(h, h, &pkcs11_decrypted_passwords); + if (r < 0) + return r; + + r = home_validate_update(h, &setup); + if (r < 0) + return r; + + already_activated = r > 0; + + r = home_prepare(h, already_activated, &pkcs11_decrypted_passwords, &setup, &header_home); + if (r < 0) + return r; + + r = home_load_embedded_identity(h, setup.root_fd, header_home, USER_RECONCILE_REQUIRE_NEWER, &pkcs11_decrypted_passwords, &embedded_home, &new_home); + if (r < 0) + return r; + + r = home_store_header_identity_luks(new_home, &setup, header_home); + if (r < 0) + return r; + + r = home_store_embedded_identity(new_home, setup.root_fd, h->uid, embedded_home); + if (r < 0) + return r; + + r = home_extend_embedded_identity(new_home, h, &setup); + if (r < 0) + return r; + + r = home_sync_and_statfs(setup.root_fd, NULL); + if (r < 0) + return r; + + r = home_setup_undo(&setup); + if (r < 0) + return r; + + log_info("Everything completed."); + + *ret = TAKE_PTR(new_home); + return 0; +} + +static int home_resize(UserRecord *h, UserRecord **ret) { + _cleanup_(home_setup_undo) HomeSetup setup = HOME_SETUP_INIT; + _cleanup_(strv_free_erasep) char **pkcs11_decrypted_passwords = NULL; + bool already_activated = false; + int r; + + assert(h); + assert(ret); + + if (h->disk_size == UINT64_MAX) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No target size specified, refusing."); + + r = user_record_authenticate(h, h, &pkcs11_decrypted_passwords); + if (r < 0) + return r; + + r = home_validate_update(h, &setup); + if (r < 0) + return r; + + already_activated = r > 0; + + switch (user_record_storage(h)) { + + case USER_LUKS: + return home_resize_luks(h, already_activated, &pkcs11_decrypted_passwords, &setup, ret); + + case USER_DIRECTORY: + case USER_SUBVOLUME: + case USER_FSCRYPT: + return home_resize_directory(h, already_activated, &pkcs11_decrypted_passwords, &setup, ret); + + default: + return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), "Resizing home directories of type '%s' currently not supported.", user_storage_to_string(user_record_storage(h))); + } +} + +static int home_passwd(UserRecord *h, UserRecord **ret_home) { + _cleanup_(user_record_unrefp) UserRecord *header_home = NULL, *embedded_home = NULL, *new_home = NULL; + _cleanup_(strv_free_erasep) char **effective_passwords = NULL, **pkcs11_decrypted_passwords = NULL; + _cleanup_(home_setup_undo) HomeSetup setup = HOME_SETUP_INIT; + bool already_activated = false; + int r; + + assert(h); + assert(ret_home); + + if (!IN_SET(user_record_storage(h), USER_LUKS, USER_DIRECTORY, USER_SUBVOLUME, USER_FSCRYPT)) + return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), "Changing password of home directories of type '%s' currently not supported.", user_storage_to_string(user_record_storage(h))); + + r = user_record_compile_effective_passwords(h, &effective_passwords, &pkcs11_decrypted_passwords); + if (r < 0) + return r; + + r = home_validate_update(h, &setup); + if (r < 0) + return r; + + already_activated = r > 0; + + r = home_prepare(h, already_activated, &pkcs11_decrypted_passwords, &setup, &header_home); + if (r < 0) + return r; + + r = home_load_embedded_identity(h, setup.root_fd, header_home, USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL, &pkcs11_decrypted_passwords, &embedded_home, &new_home); + if (r < 0) + return r; + + switch (user_record_storage(h)) { + + case USER_LUKS: + r = home_passwd_luks(h, &setup, pkcs11_decrypted_passwords, effective_passwords); + if (r < 0) + return r; + break; + + case USER_FSCRYPT: + r = home_passwd_fscrypt(h, &setup, pkcs11_decrypted_passwords, effective_passwords); + if (r < 0) + return r; + break; + + default: + break; + } + + r = home_store_header_identity_luks(new_home, &setup, header_home); + if (r < 0) + return r; + + r = home_store_embedded_identity(new_home, setup.root_fd, h->uid, embedded_home); + if (r < 0) + return r; + + r = home_extend_embedded_identity(new_home, h, &setup); + if (r < 0) + return r; + + r = home_sync_and_statfs(setup.root_fd, NULL); + if (r < 0) + return r; + + r = home_setup_undo(&setup); + if (r < 0) + return r; + + log_info("Everything completed."); + + *ret_home = TAKE_PTR(new_home); + return 1; +} + +static int home_inspect(UserRecord *h, UserRecord **ret_home) { + _cleanup_(user_record_unrefp) UserRecord *header_home = NULL, *new_home = NULL; + _cleanup_(home_setup_undo) HomeSetup setup = HOME_SETUP_INIT; + _cleanup_(strv_free_erasep) char **pkcs11_decrypted_passwords = NULL; + bool already_activated = false; + int r; + + assert(h); + assert(ret_home); + + r = user_record_authenticate(h, h, &pkcs11_decrypted_passwords); + if (r < 0) + return r; + + r = home_validate_update(h, &setup); + if (r < 0) + return r; + + already_activated = r > 0; + + r = home_prepare(h, already_activated, &pkcs11_decrypted_passwords, &setup, &header_home); + if (r < 0) + return r; + + r = home_load_embedded_identity(h, setup.root_fd, header_home, USER_RECONCILE_ANY, &pkcs11_decrypted_passwords, NULL, &new_home); + if (r < 0) + return r; + + r = home_extend_embedded_identity(new_home, h, &setup); + if (r < 0) + return r; + + r = home_setup_undo(&setup); + if (r < 0) + return r; + + log_info("Everything completed."); + + *ret_home = TAKE_PTR(new_home); + return 1; +} + +static int home_lock(UserRecord *h) { + int r; + + assert(h); + + if (!h->user_name) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record incomplete, refusing."); + if (user_record_storage(h) != USER_LUKS) + return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), "Locking home directories of type '%s' currently not supported.", user_storage_to_string(user_record_storage(h))); + + r = user_record_test_home_directory_and_warn(h); + if (r < 0) + return r; + if (r != USER_TEST_MOUNTED) + return log_error_errno(SYNTHETIC_ERRNO(ENOEXEC), "Home directory of %s is not mounted, can't lock.", h->user_name); + + r = home_lock_luks(h); + if (r < 0) + return r; + + log_info("Everything completed."); + return 1; +} + +static int home_unlock(UserRecord *h) { + _cleanup_(strv_free_erasep) char **pkcs11_decrypted_passwords = NULL; + int r; + + assert(h); + + if (!h->user_name) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record incomplete, refusing."); + if (user_record_storage(h) != USER_LUKS) + return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), "Unlocking home directories of type '%s' currently not supported.", user_storage_to_string(user_record_storage(h))); + + /* Note that we don't check if $HOME is actually mounted, since we want to avoid disk accesses on + * that mount until we have resumed the device. */ + + r = user_record_authenticate(h, h, &pkcs11_decrypted_passwords); + if (r < 0) + return r; + + r = home_unlock_luks(h, &pkcs11_decrypted_passwords); + if (r < 0) + return r; + + log_info("Everything completed."); + return 1; +} + +static int run(int argc, char *argv[]) { + _cleanup_(user_record_unrefp) UserRecord *home = NULL, *new_home = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + _cleanup_(fclosep) FILE *opened_file = NULL; + unsigned line = 0, column = 0; + const char *json_path = NULL; + FILE *json_file; + usec_t start; + int r; + + start = now(CLOCK_MONOTONIC); + + log_setup_service(); + + umask(0022); + + if (argc < 2 || argc > 3) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "This program takes one or two arguments."); + + if (argc > 2) { + json_path = argv[2]; + + opened_file = fopen(json_path, "re"); + if (!opened_file) + return log_error_errno(errno, "Failed to open %s: %m", json_path); + + json_file = opened_file; + } else { + json_path = ""; + json_file = stdin; + } + + r = json_parse_file(json_file, json_path, JSON_PARSE_SENSITIVE, &v, &line, &column); + if (r < 0) + return log_error_errno(r, "[%s:%u:%u] Failed to parse JSON data: %m", json_path, line, column); + + home = user_record_new(); + if (!home) + return log_oom(); + + r = user_record_load(home, v, USER_RECORD_LOAD_FULL|USER_RECORD_LOG); + if (r < 0) + return r; + + /* Well known return values of these operations, that systemd-homed knows and converts to proper D-Bus errors: + * + * EMSGSIZE → file systems of this type cannnot be shrinked + * ETXTBSY → file systems of this type can only be shrinked offline + * ERANGE → file system size too small + * ENOLINK → system does not support selected storage backend + * EPROTONOSUPPORT → system does not support selected file system + * ENOTTY → operation not support on this storage + * ESOCKTNOSUPPORT → operation not support on this file system + * ENOKEY → password incorrect (or not sufficient, or not supplied) + * EBADSLT → similar, but PKCS#11 device is defined and might be able to provide password, if it was plugged in which it is not + * ENOANO → suitable PKCS#11 device found, but PIN is missing to unlock it + * ERFKILL → suitable PKCS#11 device found, but OK to ask for on-device interactive authentication not given + * EOWNERDEAD → suitable PKCS#11 device found, but its PIN is locked + * ENOLCK → suitable PKCS#11 device found, but PIN incorrect + * ETOOMANYREFS → suitable PKCS#11 device found, but PIN incorrect, and only few tries left + * EUCLEAN → suitable PKCS#11 device found, but PIN incorrect, and only one try left + * EBUSY → file system is currently active + * ENOEXEC → file system is currently not active + * ENOSPC → not enough disk space for operation + */ + + if (streq(argv[1], "activate")) + r = home_activate(home, &new_home); + else if (streq(argv[1], "deactivate")) + r = home_deactivate(home, false); + else if (streq(argv[1], "deactivate-force")) + r = home_deactivate(home, true); + else if (streq(argv[1], "create")) + r = home_create(home, &new_home); + else if (streq(argv[1], "remove")) + r = home_remove(home); + else if (streq(argv[1], "update")) + r = home_update(home, &new_home); + else if (streq(argv[1], "resize")) + r = home_resize(home, &new_home); + else if (streq(argv[1], "passwd")) + r = home_passwd(home, &new_home); + else if (streq(argv[1], "inspect")) + r = home_inspect(home, &new_home); + else if (streq(argv[1], "lock")) + r = home_lock(home); + else if (streq(argv[1], "unlock")) + r = home_unlock(home); + else + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown verb '%s'.", argv[1]); + if (r == -ENOKEY && !strv_isempty(home->password)) { /* There were passwords specified but they were incorrect */ + usec_t end, n, d; + + /* Make sure bad password replies always take at least 3s, and if longer multiples of 3s, so + * that it's not clear how long we actually needed for our calculations. */ + n = now(CLOCK_MONOTONIC); + assert(n >= start); + + d = usec_sub_unsigned(n, start); + if (d > BAD_PASSWORD_DELAY_USEC) + end = start + DIV_ROUND_UP(d, BAD_PASSWORD_DELAY_USEC) * BAD_PASSWORD_DELAY_USEC; + else + end = start + BAD_PASSWORD_DELAY_USEC; + + if (n < end) + (void) usleep(usec_sub_unsigned(end, n)); + } + if (r < 0) + return r; + + /* We always pass the new record back, regardless if it changed or not. This allows our caller to + * prepare a fresh record, send to us, and only if it works use it without having to keep a local + * copy. */ + if (new_home) + json_variant_dump(new_home->json, JSON_FORMAT_NEWLINE, stdout, NULL); + + return 0; +} + +DEFINE_MAIN_FUNCTION(run); diff --git a/src/home/homework.h b/src/home/homework.h new file mode 100644 index 0000000000..81698b7601 --- /dev/null +++ b/src/home/homework.h @@ -0,0 +1,57 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include +#include + +#include "sd-id128.h" + +#include "loop-util.h" +#include "user-record.h" +#include "user-record-util.h" + +typedef struct HomeSetup { + char *dm_name; + char *dm_node; + + LoopDevice *loop; + struct crypt_device *crypt_device; + int root_fd; + sd_id128_t found_partition_uuid; + sd_id128_t found_luks_uuid; + sd_id128_t found_fs_uuid; + + uint8_t fscrypt_key_descriptor[FS_KEY_DESCRIPTOR_SIZE]; + + void *volume_key; + size_t volume_key_size; + + bool undo_dm; + bool undo_mount; + + uint64_t partition_offset; + uint64_t partition_size; +} HomeSetup; + +#define HOME_SETUP_INIT \ + { \ + .root_fd = -1, \ + .partition_offset = UINT64_MAX, \ + .partition_size = UINT64_MAX, \ + } + +int home_setup_undo(HomeSetup *setup); + +int home_prepare(UserRecord *h, bool already_activated, char ***pkcs11_decrypted_passwords, HomeSetup *setup, UserRecord **ret_header_home); + +int home_refresh(UserRecord *h, HomeSetup *setup, UserRecord *header_home, char ***pkcs11_decrypted_passwords, struct statfs *ret_statfs, UserRecord **ret_new_home); + +int home_populate(UserRecord *h, int dir_fd); + +int home_load_embedded_identity(UserRecord *h, int root_fd, UserRecord *header_home, UserReconcileMode mode, char ***pkcs11_decrypted_passwords, UserRecord **ret_embedded_home, UserRecord **ret_new_home); +int home_store_embedded_identity(UserRecord *h, int root_fd, uid_t uid, UserRecord *old_home); +int home_extend_embedded_identity(UserRecord *h, UserRecord *used, HomeSetup *setup); + +int user_record_authenticate(UserRecord *h, UserRecord *secret, char ***pkcs11_decrypted_passwords); + +int home_sync_and_statfs(int root_fd, struct statfs *ret); diff --git a/src/home/meson.build b/src/home/meson.build new file mode 100644 index 0000000000..82c6735894 --- /dev/null +++ b/src/home/meson.build @@ -0,0 +1,62 @@ +# SPDX-License-Identifier: LGPL-2.1+ + +systemd_homework_sources = files(''' + home-util.c + home-util.h + homework-cifs.c + homework-cifs.h + homework-directory.c + homework-directory.h + homework-fscrypt.c + homework-fscrypt.h + homework-luks.c + homework-luks.h + homework-mount.c + homework-mount.h + homework-pkcs11.h + homework-quota.c + homework-quota.h + homework.c + homework.h + user-record-util.c + user-record-util.h +'''.split()) + +if conf.get('HAVE_P11KIT') == 1 + systemd_homework_sources += files('homework-pkcs11.c') +endif + +systemd_homed_sources = files(''' + home-util.c + home-util.h + homed-bus.c + homed-bus.h + homed-home-bus.c + homed-home-bus.h + homed-home.c + homed-home.h + homed-manager-bus.c + homed-manager-bus.h + homed-manager.c + homed-manager.h + homed-operation.c + homed-operation.h + homed-varlink.c + homed-varlink.h + homed.c + pwquality-util.c + pwquality-util.h + user-record-sign.c + user-record-sign.h + user-record-util.c + user-record-util.h +'''.split()) + +if conf.get('ENABLE_HOMED') == 1 + install_data('org.freedesktop.home1.conf', + install_dir : dbuspolicydir) + install_data('org.freedesktop.home1.service', + install_dir : dbussystemservicedir) + install_data('org.freedesktop.home1.policy', + install_dir : polkitpolicydir) +endif diff --git a/src/home/org.freedesktop.home1.conf b/src/home/org.freedesktop.home1.conf new file mode 100644 index 0000000000..d615501054 --- /dev/null +++ b/src/home/org.freedesktop.home1.conf @@ -0,0 +1,193 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/home/org.freedesktop.home1.policy b/src/home/org.freedesktop.home1.policy new file mode 100644 index 0000000000..66ef8e0e9d --- /dev/null +++ b/src/home/org.freedesktop.home1.policy @@ -0,0 +1,72 @@ + + + + + + + + The systemd Project + http://www.freedesktop.org/wiki/Software/systemd + + + Create a home + Authentication is required for creating a user's home. + + auth_admin_keep + auth_admin_keep + auth_admin_keep + + + + + Remove a home + Authentication is required for removing a user's home. + + auth_admin_keep + auth_admin_keep + auth_admin_keep + + + + + Check credentials of a home + Authentication is required for checking credentials against a user's home. + + auth_admin_keep + auth_admin_keep + auth_admin_keep + + + + + Update a home + Authentication is required for updating a user's home. + + auth_admin_keep + auth_admin_keep + auth_admin_keep + + + + + Resize a home + Authentication is required for resizing a user's home. + + auth_admin_keep + auth_admin_keep + auth_admin_keep + + + + + Change password of a home + Authentication is required for changing the password of a user's home. + + auth_admin_keep + auth_admin_keep + auth_admin_keep + + + + diff --git a/src/home/org.freedesktop.home1.service b/src/home/org.freedesktop.home1.service new file mode 100644 index 0000000000..cff19b3861 --- /dev/null +++ b/src/home/org.freedesktop.home1.service @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: LGPL-2.1+ + +[D-BUS Service] +Name=org.freedesktop.home1 +Exec=/bin/false +User=root +SystemdService=dbus-org.freedesktop.home1.service diff --git a/src/home/pwquality-util.c b/src/home/pwquality-util.c new file mode 100644 index 0000000000..c814c8f11e --- /dev/null +++ b/src/home/pwquality-util.c @@ -0,0 +1,140 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include + +#if HAVE_PWQUALITY +/* pwquality.h uses size_t but doesn't include sys/types.h on its own */ +#include +#include +#endif + +#include "bus-common-errors.h" +#include "home-util.h" +#include "pwquality-util.h" +#include "strv.h" + +#if HAVE_PWQUALITY +DEFINE_TRIVIAL_CLEANUP_FUNC(pwquality_settings_t*, pwquality_free_settings); + +static void pwquality_maybe_disable_dictionary( + pwquality_settings_t *pwq) { + + char buf[PWQ_MAX_ERROR_MESSAGE_LEN]; + const char *path; + int r; + + r = pwquality_get_str_value(pwq, PWQ_SETTING_DICT_PATH, &path); + if (r < 0) { + log_warning("Failed to read libpwquality dictionary path, ignoring: %s", pwquality_strerror(buf, sizeof(buf), r, NULL)); + return; + } + + // REMOVE THIS AS SOON AS https://github.com/libpwquality/libpwquality/pull/21 IS MERGED AND RELEASED + if (isempty(path)) + path = "/usr/share/cracklib/pw_dict.pwd.gz"; + + if (isempty(path)) { + log_warning("Weird, no dictionary file configured, ignoring."); + return; + } + + if (access(path, F_OK) >= 0) + return; + + if (errno != ENOENT) { + log_warning_errno(errno, "Failed to check if dictionary file %s exists, ignoring: %m", path); + return; + } + + r = pwquality_set_int_value(pwq, PWQ_SETTING_DICT_CHECK, 0); + if (r < 0) { + log_warning("Failed to disable libpwquality dictionary check, ignoring: %s", pwquality_strerror(buf, sizeof(buf), r, NULL)); + return; + } +} + +int quality_check_password( + UserRecord *hr, + UserRecord *secret, + sd_bus_error *error) { + + _cleanup_(pwquality_free_settingsp) pwquality_settings_t *pwq = NULL; + char buf[PWQ_MAX_ERROR_MESSAGE_LEN], **pp; + void *auxerror; + int r; + + assert(hr); + assert(secret); + + pwq = pwquality_default_settings(); + if (!pwq) + return log_oom(); + + r = pwquality_read_config(pwq, NULL, &auxerror); + if (r < 0) + log_warning_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to read libpwquality configuation, ignoring: %s", + pwquality_strerror(buf, sizeof(buf), r, auxerror)); + + pwquality_maybe_disable_dictionary(pwq); + + /* This is a bit more complex than one might think at first. pwquality_check() would like to know the + * old password to make security checks. We support arbitrary numbers of passwords however, hence we + * call the function once for each combination of old and new password. */ + + /* Iterate through all new passwords */ + STRV_FOREACH(pp, secret->password) { + bool called = false; + char **old; + + r = test_password_many(hr->hashed_password, *pp); + if (r < 0) + return r; + if (r == 0) /* This is an old password as it isn't listed in the hashedPassword field, skip it */ + continue; + + /* Check this password against all old passwords */ + STRV_FOREACH(old, secret->password) { + + if (streq(*pp, *old)) + continue; + + r = test_password_many(hr->hashed_password, *old); + if (r < 0) + return r; + if (r > 0) /* This is a new password, not suitable as old password */ + continue; + + r = pwquality_check(pwq, *pp, *old, hr->user_name, &auxerror); + if (r < 0) + return sd_bus_error_setf(error, BUS_ERROR_LOW_PASSWORD_QUALITY, "Password too weak: %s", + pwquality_strerror(buf, sizeof(buf), r, auxerror)); + + called = true; + } + + if (called) + continue; + + /* If there are no old passwords, let's call pwquality_check() without any. */ + r = pwquality_check(pwq, *pp, NULL, hr->user_name, &auxerror); + if (r < 0) + return sd_bus_error_setf(error, BUS_ERROR_LOW_PASSWORD_QUALITY, "Password too weak: %s", + pwquality_strerror(buf, sizeof(buf), r, auxerror)); + } + + return 0; +} + +#else + +int quality_check_password( + UserRecord *hr, + UserRecord *secret, + sd_bus_error *error) { + + assert(hr); + assert(secret); + + return 0; +} +#endif diff --git a/src/home/pwquality-util.h b/src/home/pwquality-util.h new file mode 100644 index 0000000000..b44150b305 --- /dev/null +++ b/src/home/pwquality-util.h @@ -0,0 +1,7 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include "sd-bus.h" +#include "user-record.h" + +int quality_check_password(UserRecord *hr, UserRecord *secret, sd_bus_error *error); diff --git a/src/home/user-record-sign.c b/src/home/user-record-sign.c new file mode 100644 index 0000000000..91f8639997 --- /dev/null +++ b/src/home/user-record-sign.c @@ -0,0 +1,174 @@ +#include + +#include "fd-util.h" +#include "user-record-sign.h" +#include "fileio.h" + +static int user_record_signable_json(UserRecord *ur, char **ret) { + _cleanup_(user_record_unrefp) UserRecord *reduced = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *j = NULL; + int r; + + assert(ur); + assert(ret); + + r = user_record_clone(ur, USER_RECORD_REQUIRE_REGULAR|USER_RECORD_ALLOW_PRIVILEGED|USER_RECORD_ALLOW_PER_MACHINE|USER_RECORD_STRIP_SECRET|USER_RECORD_STRIP_BINDING|USER_RECORD_STRIP_STATUS|USER_RECORD_STRIP_SIGNATURE, &reduced); + if (r < 0) + return r; + + j = json_variant_ref(reduced->json); + + r = json_variant_normalize(&j); + if (r < 0) + return r; + + return json_variant_format(j, 0, ret); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(EVP_MD_CTX*, EVP_MD_CTX_free); + +int user_record_sign(UserRecord *ur, EVP_PKEY *private_key, UserRecord **ret) { + _cleanup_(json_variant_unrefp) JsonVariant *encoded = NULL, *v = NULL; + _cleanup_(user_record_unrefp) UserRecord *signed_ur = NULL; + _cleanup_(EVP_MD_CTX_freep) EVP_MD_CTX *md_ctx = NULL; + _cleanup_free_ char *text = NULL, *key = NULL; + size_t signature_size = 0, key_size = 0; + _cleanup_free_ void *signature = NULL; + _cleanup_fclose_ FILE *mf = NULL; + int r; + + assert(ur); + assert(private_key); + assert(ret); + + r = user_record_signable_json(ur, &text); + if (r < 0) + return r; + + md_ctx = EVP_MD_CTX_new(); + if (!md_ctx) + return -ENOMEM; + + if (EVP_DigestSignInit(md_ctx, NULL, NULL, NULL, private_key) <= 0) + return -EIO; + + /* Request signature size */ + if (EVP_DigestSign(md_ctx, NULL, &signature_size, (uint8_t*) text, strlen(text)) <= 0) + return -EIO; + + signature = malloc(signature_size); + if (!signature) + return -ENOMEM; + + if (EVP_DigestSign(md_ctx, signature, &signature_size, (uint8_t*) text, strlen(text)) <= 0) + return -EIO; + + mf = open_memstream_unlocked(&key, &key_size); + if (!mf) + return -ENOMEM; + + if (PEM_write_PUBKEY(mf, private_key) <= 0) + return -EIO; + + r = fflush_and_check(mf); + if (r < 0) + return r; + + r = json_build(&encoded, JSON_BUILD_ARRAY( + JSON_BUILD_OBJECT(JSON_BUILD_PAIR("data", JSON_BUILD_BASE64(signature, signature_size)), + JSON_BUILD_PAIR("key", JSON_BUILD_STRING(key))))); + if (r < 0) + return r; + + v = json_variant_ref(ur->json); + + r = json_variant_set_field(&v, "signature", encoded); + if (r < 0) + return r; + + if (DEBUG_LOGGING) + json_variant_dump(v, JSON_FORMAT_PRETTY|JSON_FORMAT_COLOR_AUTO, NULL, NULL); + + signed_ur = user_record_new(); + if (!signed_ur) + return log_oom(); + + r = user_record_load(signed_ur, v, USER_RECORD_LOAD_FULL); + if (r < 0) + return r; + + *ret = TAKE_PTR(signed_ur); + return 0; +} + +int user_record_verify(UserRecord *ur, EVP_PKEY *public_key) { + _cleanup_free_ char *text = NULL; + unsigned n_good = 0, n_bad = 0; + JsonVariant *array, *e; + int r; + + assert(ur); + assert(public_key); + + array = json_variant_by_key(ur->json, "signature"); + if (!array) + return USER_RECORD_UNSIGNED; + + if (!json_variant_is_array(array)) + return -EINVAL; + + if (json_variant_elements(array) == 0) + return USER_RECORD_UNSIGNED; + + r = user_record_signable_json(ur, &text); + if (r < 0) + return r; + + JSON_VARIANT_ARRAY_FOREACH(e, array) { + _cleanup_(EVP_MD_CTX_freep) EVP_MD_CTX *md_ctx = NULL; + _cleanup_free_ void *signature = NULL; + size_t signature_size = 0; + JsonVariant *data; + + if (!json_variant_is_object(e)) + return -EINVAL; + + data = json_variant_by_key(e, "data"); + if (!data) + return -EINVAL; + + r = json_variant_unbase64(data, &signature, &signature_size); + if (r < 0) + return r; + + md_ctx = EVP_MD_CTX_new(); + if (!md_ctx) + return -ENOMEM; + + if (EVP_DigestVerifyInit(md_ctx, NULL, NULL, NULL, public_key) <= 0) + return -EIO; + + if (EVP_DigestVerify(md_ctx, signature, signature_size, (uint8_t*) text, strlen(text)) <= 0) { + n_bad ++; + continue; + } + + n_good ++; + } + + return n_good > 0 ? (n_bad == 0 ? USER_RECORD_SIGNED_EXCLUSIVE : USER_RECORD_SIGNED) : + (n_bad == 0 ? USER_RECORD_UNSIGNED : USER_RECORD_FOREIGN); +} + +int user_record_has_signature(UserRecord *ur) { + JsonVariant *array; + + array = json_variant_by_key(ur->json, "signature"); + if (!array) + return false; + + if (!json_variant_is_array(array)) + return -EINVAL; + + return json_variant_elements(array) > 0; +} diff --git a/src/home/user-record-sign.h b/src/home/user-record-sign.h new file mode 100644 index 0000000000..f045c8837b --- /dev/null +++ b/src/home/user-record-sign.h @@ -0,0 +1,19 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include + +#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 0000000000..cb840f910b --- /dev/null +++ b/src/home/user-record-util.c @@ -0,0 +1,1225 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include "errno-util.h" +#include "home-util.h" +#include "id128-util.h" +#include "libcrypt-util.h" +#include "mountpoint-util.h" +#include "path-util.h" +#include "stat-util.h" +#include "user-record-util.h" +#include "user-util.h" + +int user_record_synthesize( + UserRecord *h, + const char *user_name, + const char *realm, + const char *image_path, + UserStorage storage, + uid_t uid, + gid_t gid) { + + _cleanup_free_ char *hd = NULL, *un = NULL, *ip = NULL, *rr = NULL, *user_name_and_realm = NULL; + char smid[SD_ID128_STRING_MAX]; + sd_id128_t mid; + int r; + + assert(h); + assert(user_name); + assert(image_path); + assert(IN_SET(storage, USER_LUKS, USER_SUBVOLUME, USER_FSCRYPT, USER_DIRECTORY)); + assert(uid_is_valid(uid)); + assert(gid_is_valid(gid)); + + /* Fill in a home record from just a username and an image path. */ + + if (h->json) + return -EBUSY; + + if (!suitable_user_name(user_name)) + return -EINVAL; + + if (realm) { + r = suitable_realm(realm); + if (r < 0) + return r; + if (r == 0) + return -EINVAL; + } + + if (!suitable_image_path(image_path)) + return -EINVAL; + + r = sd_id128_get_machine(&mid); + if (r < 0) + return r; + + un = strdup(user_name); + if (!un) + return -ENOMEM; + + if (realm) { + rr = strdup(realm); + if (!rr) + return -ENOMEM; + + user_name_and_realm = strjoin(user_name, "@", realm); + if (!user_name_and_realm) + return -ENOMEM; + } + + ip = strdup(image_path); + if (!ip) + return -ENOMEM; + + hd = path_join("/home/", user_name); + if (!hd) + return -ENOMEM; + + r = json_build(&h->json, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(user_name)), + JSON_BUILD_PAIR_CONDITION(!!rr, "realm", JSON_BUILD_STRING(realm)), + JSON_BUILD_PAIR("disposition", JSON_BUILD_STRING("regular")), + JSON_BUILD_PAIR("binding", JSON_BUILD_OBJECT( + JSON_BUILD_PAIR(sd_id128_to_string(mid, smid), JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("imagePath", JSON_BUILD_STRING(image_path)), + JSON_BUILD_PAIR("homeDirectory", JSON_BUILD_STRING(hd)), + JSON_BUILD_PAIR("storage", JSON_BUILD_STRING(user_storage_to_string(storage))), + JSON_BUILD_PAIR("uid", JSON_BUILD_UNSIGNED(uid)), + JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(gid)))))))); + if (r < 0) + return r; + + free_and_replace(h->user_name, un); + free_and_replace(h->realm, rr); + free_and_replace(h->user_name_and_realm_auto, user_name_and_realm); + free_and_replace(h->image_path, ip); + free_and_replace(h->home_directory, hd); + h->storage = storage; + h->uid = uid; + + h->mask = USER_RECORD_REGULAR|USER_RECORD_BINDING; + return 0; +} + +int group_record_synthesize(GroupRecord *g, UserRecord *h) { + _cleanup_free_ char *un = NULL, *rr = NULL, *group_name_and_realm = NULL; + char smid[SD_ID128_STRING_MAX]; + sd_id128_t mid; + int r; + + assert(g); + assert(h); + + if (g->json) + return -EBUSY; + + r = sd_id128_get_machine(&mid); + if (r < 0) + return r; + + un = strdup(h->user_name); + if (!un) + return -ENOMEM; + + if (h->realm) { + rr = strdup(h->realm); + if (!rr) + return -ENOMEM; + + group_name_and_realm = strjoin(un, "@", rr); + if (!group_name_and_realm) + return -ENOMEM; + } + + r = json_build(&g->json, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(un)), + JSON_BUILD_PAIR_CONDITION(!!rr, "realm", JSON_BUILD_STRING(rr)), + JSON_BUILD_PAIR("binding", JSON_BUILD_OBJECT( + JSON_BUILD_PAIR(sd_id128_to_string(mid, smid), JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(user_record_gid(h))))))), + JSON_BUILD_PAIR_CONDITION(h->disposition >= 0, "disposition", JSON_BUILD_STRING(user_disposition_to_string(user_record_disposition(h)))), + JSON_BUILD_PAIR("status", JSON_BUILD_OBJECT( + JSON_BUILD_PAIR(sd_id128_to_string(mid, smid), JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("service", JSON_BUILD_STRING("io.systemd.Home")))))))); + if (r < 0) + return r; + + free_and_replace(g->group_name, un); + free_and_replace(g->realm, rr); + free_and_replace(g->group_name_and_realm_auto, group_name_and_realm); + g->gid = user_record_gid(h); + g->disposition = h->disposition; + + g->mask = USER_RECORD_REGULAR|USER_RECORD_BINDING; + return 0; +} + +int user_record_reconcile( + UserRecord *host, + UserRecord *embedded, + UserReconcileMode mode, + UserRecord **ret) { + + int r, result; + + /* Reconciles the identity record stored on the host with the one embedded in a $HOME + * directory. Returns the following error codes: + * + * -EINVAL: one of the records not valid + * -REMCHG: identity records are not about the same user + * -ESTALE: embedded identity record is equally new or newer than supplied record + * + * Return the new record to use, which is either the the embedded record updated with the host + * binding or the host record. In both cases the secret data is stripped. */ + + assert(host); + assert(embedded); + + /* Make sure both records are initialized */ + if (!host->json || !embedded->json) + return -EINVAL; + + /* Ensure these records actually contain user data */ + if (!(embedded->mask & host->mask & USER_RECORD_REGULAR)) + return -EINVAL; + + /* Make sure the user name and realm matches */ + if (!user_record_compatible(host, embedded)) + return -EREMCHG; + + /* Embedded identities may not contain secrets or binding info*/ + if ((embedded->mask & (USER_RECORD_SECRET|USER_RECORD_BINDING)) != 0) + return -EINVAL; + + /* The embedded record checked out, let's now figure out which of the two identities we'll consider + * in effect from now on. We do this by checking the last change timestamp, and in doubt always let + * the embedded data win. */ + if (host->last_change_usec != UINT64_MAX && + (embedded->last_change_usec == UINT64_MAX || host->last_change_usec > embedded->last_change_usec)) + + /* The host version is definitely newer, either because it has a version at all and the + * embedded version doesn't or because it is numerically newer. */ + result = USER_RECONCILE_HOST_WON; + + else if (host->last_change_usec == embedded->last_change_usec) { + + /* The nominal version number of the host and the embedded identity is the same. If so, let's + * verify that, and tell the caller if we are ignoring embedded data. */ + + r = user_record_masked_equal(host, embedded, USER_RECORD_REGULAR|USER_RECORD_PRIVILEGED|USER_RECORD_PER_MACHINE); + if (r < 0) + return r; + if (r > 0) { + if (mode == USER_RECONCILE_REQUIRE_NEWER) + return -ESTALE; + + result = USER_RECONCILE_IDENTICAL; + } else + result = USER_RECONCILE_HOST_WON; + } else { + _cleanup_(json_variant_unrefp) JsonVariant *extended = NULL; + _cleanup_(user_record_unrefp) UserRecord *merged = NULL; + JsonVariant *e; + + /* The embedded version is newer */ + + if (mode == USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL) + return -ESTALE; + + /* Copy in the binding data */ + extended = json_variant_ref(embedded->json); + + e = json_variant_by_key(host->json, "binding"); + if (e) { + r = json_variant_set_field(&extended, "binding", e); + if (r < 0) + return r; + } + + merged = user_record_new(); + if (!merged) + return -ENOMEM; + + r = user_record_load(merged, extended, USER_RECORD_LOAD_MASK_SECRET); + if (r < 0) + return r; + + *ret = TAKE_PTR(merged); + return USER_RECONCILE_EMBEDDED_WON; /* update */ + } + + /* Strip out secrets */ + r = user_record_clone(host, USER_RECORD_LOAD_MASK_SECRET, ret); + if (r < 0) + return r; + + return result; +} + +int user_record_add_binding( + UserRecord *h, + UserStorage storage, + const char *image_path, + sd_id128_t partition_uuid, + sd_id128_t luks_uuid, + sd_id128_t fs_uuid, + const char *luks_cipher, + const char *luks_cipher_mode, + uint64_t luks_volume_key_size, + const char *file_system_type, + const char *home_directory, + uid_t uid, + gid_t gid) { + + _cleanup_(json_variant_unrefp) JsonVariant *new_binding_entry = NULL, *binding = NULL; + char smid[SD_ID128_STRING_MAX], partition_uuids[37], luks_uuids[37], fs_uuids[37]; + _cleanup_free_ char *ip = NULL, *hd = NULL; + sd_id128_t mid; + int r; + + assert(h); + + if (!h->json) + return -EUNATCH; + + r = sd_id128_get_machine(&mid); + if (r < 0) + return r; + sd_id128_to_string(mid, smid); + + if (image_path) { + ip = strdup(image_path); + if (!ip) + return -ENOMEM; + } + + if (home_directory) { + hd = strdup(home_directory); + if (!hd) + return -ENOMEM; + } + + r = json_build(&new_binding_entry, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_CONDITION(!!image_path, "imagePath", JSON_BUILD_STRING(image_path)), + JSON_BUILD_PAIR_CONDITION(!sd_id128_is_null(partition_uuid), "partitionUuid", JSON_BUILD_STRING(id128_to_uuid_string(partition_uuid, partition_uuids))), + JSON_BUILD_PAIR_CONDITION(!sd_id128_is_null(luks_uuid), "luksUuid", JSON_BUILD_STRING(id128_to_uuid_string(luks_uuid, luks_uuids))), + JSON_BUILD_PAIR_CONDITION(!sd_id128_is_null(fs_uuid), "fileSystemUuid", JSON_BUILD_STRING(id128_to_uuid_string(fs_uuid, fs_uuids))), + JSON_BUILD_PAIR_CONDITION(!!luks_cipher, "luksCipher", JSON_BUILD_STRING(luks_cipher)), + JSON_BUILD_PAIR_CONDITION(!!luks_cipher_mode, "luksCipherMode", JSON_BUILD_STRING(luks_cipher_mode)), + JSON_BUILD_PAIR_CONDITION(luks_volume_key_size != UINT64_MAX, "luksVolumeKeySize", JSON_BUILD_UNSIGNED(luks_volume_key_size)), + JSON_BUILD_PAIR_CONDITION(!!file_system_type, "fileSystemType", JSON_BUILD_STRING(file_system_type)), + JSON_BUILD_PAIR_CONDITION(!!home_directory, "homeDirectory", JSON_BUILD_STRING(home_directory)), + JSON_BUILD_PAIR_CONDITION(uid_is_valid(uid), "uid", JSON_BUILD_UNSIGNED(uid)), + JSON_BUILD_PAIR_CONDITION(gid_is_valid(gid), "gid", JSON_BUILD_UNSIGNED(gid)), + JSON_BUILD_PAIR_CONDITION(storage >= 0, "storage", JSON_BUILD_STRING(user_storage_to_string(storage))))); + if (r < 0) + return r; + + binding = json_variant_ref(json_variant_by_key(h->json, "binding")); + if (binding) { + _cleanup_(json_variant_unrefp) JsonVariant *be = NULL; + + /* Merge the new entry with an old one, if that exists */ + be = json_variant_ref(json_variant_by_key(binding, smid)); + if (be) { + r = json_variant_merge(&be, new_binding_entry); + if (r < 0) + return r; + + json_variant_unref(new_binding_entry); + new_binding_entry = TAKE_PTR(be); + } + } + + r = json_variant_set_field(&binding, smid, new_binding_entry); + if (r < 0) + return r; + + r = json_variant_set_field(&h->json, "binding", binding); + if (r < 0) + return r; + + if (storage >= 0) + h->storage = storage; + + if (ip) + free_and_replace(h->image_path, ip); + + if (!sd_id128_is_null(partition_uuid)) + h->partition_uuid = partition_uuid; + + if (!sd_id128_is_null(luks_uuid)) + h->luks_uuid = luks_uuid; + + if (!sd_id128_is_null(fs_uuid)) + h->file_system_uuid = fs_uuid; + + if (hd) + free_and_replace(h->home_directory, hd); + + if (uid_is_valid(uid)) + h->uid = uid; + + h->mask |= USER_RECORD_BINDING; + return 1; +} + +int user_record_test_home_directory(UserRecord *h) { + const char *hd; + int r; + + assert(h); + + /* Returns one of USER_TEST_ABSENT, USER_TEST_MOUNTED, USER_TEST_EXISTS on success */ + + hd = user_record_home_directory(h); + if (!hd) + return -ENXIO; + + r = is_dir(hd, false); + if (r == -ENOENT) + return USER_TEST_ABSENT; + if (r < 0) + return r; + if (r == 0) + return -ENOTDIR; + + r = path_is_mount_point(hd, NULL, 0); + if (r < 0) + return r; + if (r > 0) + return USER_TEST_MOUNTED; + + /* If the image path and the home directory are identical, then it's OK if the directory is + * populated. */ + if (IN_SET(user_record_storage(h), USER_CLASSIC, USER_DIRECTORY, USER_SUBVOLUME, USER_FSCRYPT)) { + const char *ip; + + ip = user_record_image_path(h); + if (ip && path_equal(ip, hd)) + return USER_TEST_EXISTS; + } + + /* Otherwise it's not OK */ + r = dir_is_empty(hd); + if (r < 0) + return r; + if (r == 0) + return -EBUSY; + + return USER_TEST_EXISTS; +} + +int user_record_test_home_directory_and_warn(UserRecord *h) { + int r; + + assert(h); + + r = user_record_test_home_directory(h); + if (r == -ENXIO) + return log_error_errno(r, "User record lacks home directory, refusing."); + if (r == -ENOTDIR) + return log_error_errno(r, "Home directory %s is not a directory, refusing.", user_record_home_directory(h)); + if (r == -EBUSY) + return log_error_errno(r, "Home directory %s exists, is not mounted but populated, refusing.", user_record_home_directory(h)); + if (r < 0) + return log_error_errno(r, "Failed to test whether the home directory %s exists: %m", user_record_home_directory(h)); + + return r; +} + +int user_record_test_image_path(UserRecord *h) { + const char *ip; + struct stat st; + + assert(h); + + if (user_record_storage(h) == USER_CIFS) + return USER_TEST_UNDEFINED; + + ip = user_record_image_path(h); + if (!ip) + return -ENXIO; + + if (stat(ip, &st) < 0) { + if (errno == ENOENT) + return USER_TEST_ABSENT; + + return -errno; + } + + switch (user_record_storage(h)) { + + case USER_LUKS: + if (S_ISREG(st.st_mode)) + return USER_TEST_EXISTS; + if (S_ISBLK(st.st_mode)) { + /* For block devices we can't really be sure if the device referenced actually is the + * fs we look for or some other file system (think: what does /dev/sdb1 refer + * to?). Hence, let's return USER_TEST_MAYBE as an ambigious return value for these + * case, except if the device path used is one of the paths that is based on a + * filesystem or partition UUID or label, because in those cases we can be sure we + * are referring to the right device. */ + + if (PATH_STARTSWITH_SET(ip, + "/dev/disk/by-uuid/", + "/dev/disk/by-partuuid/", + "/dev/disk/by-partlabel/", + "/dev/disk/by-label/")) + return USER_TEST_EXISTS; + + return USER_TEST_MAYBE; + } + + return -EBADFD; + + case USER_CLASSIC: + case USER_DIRECTORY: + case USER_SUBVOLUME: + case USER_FSCRYPT: + if (S_ISDIR(st.st_mode)) + return USER_TEST_EXISTS; + + return -ENOTDIR; + + default: + assert_not_reached("Unexpected record type"); + } +} + +int user_record_test_image_path_and_warn(UserRecord *h) { + int r; + + assert(h); + + r = user_record_test_image_path(h); + if (r == -ENXIO) + return log_error_errno(r, "User record lacks image path, refusing."); + if (r == -EBADFD) + return log_error_errno(r, "Image path %s is not a regular file or block device, refusing.", user_record_image_path(h)); + if (r == -ENOTDIR) + return log_error_errno(r, "Image path %s is not a directory, refusing.", user_record_image_path(h)); + if (r < 0) + return log_error_errno(r, "Failed to test whether image path %s exists: %m", user_record_image_path(h)); + + return r; +} + +int user_record_test_secret(UserRecord *h, UserRecord *secret) { + char **i; + int r; + + assert(h); + + /* Checks whether any of the specified passwords matches any of the hashed passwords of the entry */ + + if (strv_isempty(h->hashed_password)) + return -ENXIO; + + STRV_FOREACH(i, secret->password) { + r = test_password_many(h->hashed_password, *i); + if (r < 0) + return r; + if (r > 0) + return 0; + } + + return -ENOKEY; +} + +int user_record_set_disk_size(UserRecord *h, uint64_t disk_size) { + _cleanup_(json_variant_unrefp) JsonVariant *new_per_machine = NULL, *midv = NULL, *midav = NULL, *ne = NULL; + _cleanup_free_ JsonVariant **array = NULL; + char smid[SD_ID128_STRING_MAX]; + size_t idx = SIZE_MAX, n; + JsonVariant *per_machine; + sd_id128_t mid; + int r; + + assert(h); + + if (!h->json) + return -EUNATCH; + + if (disk_size < USER_DISK_SIZE_MIN || disk_size > USER_DISK_SIZE_MAX) + return -ERANGE; + + r = sd_id128_get_machine(&mid); + if (r < 0) + return r; + + sd_id128_to_string(mid, smid); + + r = json_variant_new_string(&midv, smid); + if (r < 0) + return r; + + r = json_variant_new_array(&midav, (JsonVariant*[]) { midv }, 1); + if (r < 0) + return r; + + per_machine = json_variant_by_key(h->json, "perMachine"); + if (per_machine) { + size_t i; + + if (!json_variant_is_array(per_machine)) + return -EINVAL; + + n = json_variant_elements(per_machine); + + array = new(JsonVariant*, n + 1); + if (!array) + return -ENOMEM; + + for (i = 0; i < n; i++) { + JsonVariant *m; + + array[i] = json_variant_by_index(per_machine, i); + + if (!json_variant_is_object(array[i])) + return -EINVAL; + + m = json_variant_by_key(array[i], "matchMachineId"); + if (!m) { + /* No machineId field? Let's ignore this, but invalidate what we found so far */ + idx = SIZE_MAX; + continue; + } + + if (json_variant_equal(m, midv) || + json_variant_equal(m, midav)) { + /* Matches exactly what we are looking for. Let's use this */ + idx = i; + continue; + } + + r = per_machine_id_match(m, JSON_PERMISSIVE); + if (r < 0) + return r; + if (r > 0) + /* Also matches what we are looking for, but with a broader match. In this + * case let's ignore this entry, and add a new specific one to the end. */ + idx = SIZE_MAX; + } + + if (idx == SIZE_MAX) + idx = n++; /* Nothing suitable found, place new entry at end */ + else + ne = json_variant_ref(array[idx]); + + } else { + array = new(JsonVariant*, 1); + if (!array) + return -ENOMEM; + + idx = 0; + n = 1; + } + + if (!ne) { + r = json_variant_set_field(&ne, "matchMachineId", midav); + if (r < 0) + return r; + } + + r = json_variant_set_field_unsigned(&ne, "diskSize", disk_size); + if (r < 0) + return r; + + assert(idx < n); + array[idx] = ne; + + r = json_variant_new_array(&new_per_machine, array, n); + if (r < 0) + return r; + + r = json_variant_set_field(&h->json, "perMachine", new_per_machine); + if (r < 0) + return r; + + h->disk_size = disk_size; + h->mask |= USER_RECORD_PER_MACHINE; + return 0; +} + +int user_record_update_last_changed(UserRecord *h, bool with_password) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + usec_t n; + int r; + + assert(h); + + if (!h->json) + return -EUNATCH; + + n = now(CLOCK_REALTIME); + + /* refuse downgrading */ + if (h->last_change_usec != UINT64_MAX && h->last_change_usec >= n) + return -ECHRNG; + if (h->last_password_change_usec != UINT64_MAX && h->last_password_change_usec >= n) + return -ECHRNG; + + v = json_variant_ref(h->json); + + r = json_variant_set_field_unsigned(&v, "lastChangeUSec", n); + if (r < 0) + return r; + + if (with_password) { + r = json_variant_set_field_unsigned(&v, "lastPasswordChangeUSec", n); + if (r < 0) + return r; + + h->last_password_change_usec = n; + } + + h->last_change_usec = n; + + json_variant_unref(h->json); + h->json = TAKE_PTR(v); + + h->mask |= USER_RECORD_REGULAR; + return 0; +} + +int user_record_make_hashed_password(UserRecord *h, char **secret, bool extend) { + _cleanup_(json_variant_unrefp) JsonVariant *priv = NULL; + _cleanup_strv_free_ char **np = NULL; + char **i; + int r; + + assert(h); + assert(secret); + + /* Initializes the hashed password list from the specified plaintext passwords */ + + if (extend) { + np = strv_copy(h->hashed_password); + if (!np) + return -ENOMEM; + + strv_uniq(np); + } + + STRV_FOREACH(i, secret) { + _cleanup_free_ char *salt = NULL; + struct crypt_data cd = {}; + char *k; + + r = make_salt(&salt); + if (r < 0) + return r; + + errno = 0; + k = crypt_r(*i, salt, &cd); + if (!k) + return errno_or_else(EINVAL); + + r = strv_extend(&np, k); + if (r < 0) + return r; + } + + priv = json_variant_ref(json_variant_by_key(h->json, "privileged")); + + if (strv_isempty(np)) + r = json_variant_filter(&priv, STRV_MAKE("hashedPassword")); + else { + _cleanup_(json_variant_unrefp) JsonVariant *new_array = NULL; + + r = json_variant_new_array_strv(&new_array, np); + if (r < 0) + return r; + + r = json_variant_set_field(&priv, "hashedPassword", new_array); + } + + r = json_variant_set_field(&h->json, "privileged", priv); + if (r < 0) + return r; + + strv_free_and_replace(h->hashed_password, np); + + SET_FLAG(h->mask, USER_RECORD_PRIVILEGED, !json_variant_is_blank_object(priv)); + return 0; +} + +int user_record_set_hashed_password(UserRecord *h, char **hashed_password) { + _cleanup_(json_variant_unrefp) JsonVariant *priv = NULL; + _cleanup_strv_free_ char **copy = NULL; + int r; + + assert(h); + + priv = json_variant_ref(json_variant_by_key(h->json, "privileged")); + + if (strv_isempty(hashed_password)) + r = json_variant_filter(&priv, STRV_MAKE("hashedPassword")); + else { + _cleanup_(json_variant_unrefp) JsonVariant *array = NULL; + + copy = strv_copy(hashed_password); + if (!copy) + return -ENOMEM; + + strv_uniq(copy); + + r = json_variant_new_array_strv(&array, copy); + if (r < 0) + return r; + + r = json_variant_set_field(&priv, "hashedPassword", array); + } + if (r < 0) + return r; + + r = json_variant_set_field(&h->json, "privileged", priv); + if (r < 0) + return r; + + strv_free_and_replace(h->hashed_password, copy); + + SET_FLAG(h->mask, USER_RECORD_PRIVILEGED, !json_variant_is_blank_object(priv)); + return 0; +} + +int user_record_set_password(UserRecord *h, char **password, bool prepend) { + _cleanup_(json_variant_unrefp) JsonVariant *w = NULL; + _cleanup_(strv_free_erasep) char **e = NULL; + int r; + + assert(h); + + if (prepend) { + e = strv_copy(password); + if (!e) + return -ENOMEM; + + r = strv_extend_strv(&e, h->password, true); + if (r < 0) + return r; + + strv_uniq(e); + + if (strv_equal(h->password, e)) + return 0; + + } else { + if (strv_equal(h->password, password)) + return 0; + + e = strv_copy(password); + if (!e) + return -ENOMEM; + + strv_uniq(e); + } + + w = json_variant_ref(json_variant_by_key(h->json, "secret")); + + if (strv_isempty(e)) + r = json_variant_filter(&w, STRV_MAKE("password")); + else { + _cleanup_(json_variant_unrefp) JsonVariant *l = NULL; + + r = json_variant_new_array_strv(&l, e); + if (r < 0) + return r; + + json_variant_sensitive(l); + + r = json_variant_set_field(&w, "password", l); + } + if (r < 0) + return r; + + r = json_variant_set_field(&h->json, "secret", w); + if (r < 0) + return r; + + strv_free_and_replace(h->password, e); + + SET_FLAG(h->mask, USER_RECORD_SECRET, !json_variant_is_blank_object(w)); + return 0; +} + +int user_record_set_pkcs11_pin(UserRecord *h, char **pin, bool prepend) { + _cleanup_(json_variant_unrefp) JsonVariant *w = NULL; + _cleanup_(strv_free_erasep) char **e = NULL; + int r; + + assert(h); + + if (prepend) { + e = strv_copy(pin); + if (!e) + return -ENOMEM; + + r = strv_extend_strv(&e, h->pkcs11_pin, true); + if (r < 0) + return r; + + strv_uniq(e); + + if (strv_equal(h->pkcs11_pin, e)) + return 0; + + } else { + if (strv_equal(h->pkcs11_pin, pin)) + return 0; + + e = strv_copy(pin); + if (!e) + return -ENOMEM; + + strv_uniq(e); + } + + w = json_variant_ref(json_variant_by_key(h->json, "secret")); + + if (strv_isempty(e)) + r = json_variant_filter(&w, STRV_MAKE("pkcs11Pin")); + else { + _cleanup_(json_variant_unrefp) JsonVariant *l = NULL; + + r = json_variant_new_array_strv(&l, e); + if (r < 0) + return r; + + json_variant_sensitive(l); + + r = json_variant_set_field(&w, "pkcs11Pin", l); + } + if (r < 0) + return r; + + r = json_variant_set_field(&h->json, "secret", w); + if (r < 0) + return r; + + strv_free_and_replace(h->pkcs11_pin, e); + + SET_FLAG(h->mask, USER_RECORD_SECRET, !json_variant_is_blank_object(w)); + return 0; +} + +int user_record_set_pkcs11_protected_authentication_path_permitted(UserRecord *h, int b) { + _cleanup_(json_variant_unrefp) JsonVariant *w = NULL; + int r; + + assert(h); + + w = json_variant_ref(json_variant_by_key(h->json, "secret")); + + if (b < 0) + r = json_variant_filter(&w, STRV_MAKE("pkcs11ProtectedAuthenticationPathPermitted")); + else + r = json_variant_set_field_boolean(&w, "pkcs11ProtectedAuthenticationPathPermitted", b); + if (r < 0) + return r; + + if (json_variant_is_blank_object(w)) + r = json_variant_filter(&h->json, STRV_MAKE("secret")); + else + r = json_variant_set_field(&h->json, "secret", w); + if (r < 0) + return r; + + h->pkcs11_protected_authentication_path_permitted = b; + + SET_FLAG(h->mask, USER_RECORD_SECRET, !json_variant_is_blank_object(w)); + return 0; +} + +static bool per_machine_entry_empty(JsonVariant *v) { + const char *k; + _unused_ JsonVariant *e; + + JSON_VARIANT_OBJECT_FOREACH(k, e, v) + if (!STR_IN_SET(k, "matchMachineId", "matchHostname")) + return false; + + return true; +} + +int user_record_set_password_change_now(UserRecord *h, int b) { + _cleanup_(json_variant_unrefp) JsonVariant *w = NULL; + JsonVariant *per_machine; + int r; + + assert(h); + + w = json_variant_ref(h->json); + + if (b < 0) + r = json_variant_filter(&w, STRV_MAKE("passwordChangeNow")); + else + r = json_variant_set_field_boolean(&w, "passwordChangeNow", b); + if (r < 0) + return r; + + /* Also drop the field from all perMachine entries */ + per_machine = json_variant_by_key(w, "perMachine"); + if (per_machine) { + _cleanup_(json_variant_unrefp) JsonVariant *array = NULL; + JsonVariant *e; + + JSON_VARIANT_ARRAY_FOREACH(e, per_machine) { + _cleanup_(json_variant_unrefp) JsonVariant *z = NULL; + + if (!json_variant_is_object(e)) + return -EINVAL; + + z = json_variant_ref(e); + + r = json_variant_filter(&z, STRV_MAKE("passwordChangeNow")); + if (r < 0) + return r; + + if (per_machine_entry_empty(z)) + continue; + + r = json_variant_append_array(&array, z); + if (r < 0) + return r; + } + + if (json_variant_is_blank_array(array)) + r = json_variant_filter(&w, STRV_MAKE("perMachine")); + else + r = json_variant_set_field(&w, "perMachine", array); + if (r < 0) + return r; + + SET_FLAG(h->mask, USER_RECORD_PER_MACHINE, !json_variant_is_blank_array(array)); + } + + json_variant_unref(h->json); + h->json = TAKE_PTR(w); + + h->password_change_now = b; + + return 0; +} + +int user_record_merge_secret(UserRecord *h, UserRecord *secret) { + int r; + + assert(h); + + /* Merges the secrets from 'secret' into 'h'. */ + + r = user_record_set_password(h, secret->password, true); + if (r < 0) + return r; + + r = user_record_set_pkcs11_pin(h, secret->pkcs11_pin, true); + if (r < 0) + return r; + + if (secret->pkcs11_protected_authentication_path_permitted >= 0) { + r = user_record_set_pkcs11_protected_authentication_path_permitted(h, secret->pkcs11_protected_authentication_path_permitted); + if (r < 0) + return r; + } + + return 0; +} + +int user_record_good_authentication(UserRecord *h) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *w = NULL, *z = NULL; + char buf[SD_ID128_STRING_MAX]; + uint64_t counter, usec; + sd_id128_t mid; + int r; + + assert(h); + + switch (h->good_authentication_counter) { + case UINT64_MAX: + counter = 1; + break; + case UINT64_MAX-1: + counter = h->good_authentication_counter; /* saturate */ + break; + default: + counter = h->good_authentication_counter + 1; + break; + } + + usec = now(CLOCK_REALTIME); + + r = sd_id128_get_machine(&mid); + if (r < 0) + return r; + + v = json_variant_ref(h->json); + w = json_variant_ref(json_variant_by_key(v, "status")); + z = json_variant_ref(json_variant_by_key(w, sd_id128_to_string(mid, buf))); + + r = json_variant_set_field_unsigned(&z, "goodAuthenticationCounter", counter); + if (r < 0) + return r; + + r = json_variant_set_field_unsigned(&z, "lastGoodAuthenticationUSec", usec); + if (r < 0) + return r; + + r = json_variant_set_field(&w, buf, z); + if (r < 0) + return r; + + r = json_variant_set_field(&v, "status", w); + if (r < 0) + return r; + + json_variant_unref(h->json); + h->json = TAKE_PTR(v); + + h->good_authentication_counter = counter; + h->last_good_authentication_usec = usec; + + h->mask |= USER_RECORD_STATUS; + return 0; +} + +int user_record_bad_authentication(UserRecord *h) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *w = NULL, *z = NULL; + char buf[SD_ID128_STRING_MAX]; + uint64_t counter, usec; + sd_id128_t mid; + int r; + + assert(h); + + switch (h->bad_authentication_counter) { + case UINT64_MAX: + counter = 1; + break; + case UINT64_MAX-1: + counter = h->bad_authentication_counter; /* saturate */ + break; + default: + counter = h->bad_authentication_counter + 1; + break; + } + + usec = now(CLOCK_REALTIME); + + r = sd_id128_get_machine(&mid); + if (r < 0) + return r; + + v = json_variant_ref(h->json); + w = json_variant_ref(json_variant_by_key(v, "status")); + z = json_variant_ref(json_variant_by_key(w, sd_id128_to_string(mid, buf))); + + r = json_variant_set_field_unsigned(&z, "badAuthenticationCounter", counter); + if (r < 0) + return r; + + r = json_variant_set_field_unsigned(&z, "lastBadAuthenticationUSec", usec); + if (r < 0) + return r; + + r = json_variant_set_field(&w, buf, z); + if (r < 0) + return r; + + r = json_variant_set_field(&v, "status", w); + if (r < 0) + return r; + + json_variant_unref(h->json); + h->json = TAKE_PTR(v); + + h->bad_authentication_counter = counter; + h->last_bad_authentication_usec = usec; + + h->mask |= USER_RECORD_STATUS; + return 0; +} + +int user_record_ratelimit(UserRecord *h) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *w = NULL, *z = NULL; + usec_t usec, new_ratelimit_begin_usec, new_ratelimit_count; + char buf[SD_ID128_STRING_MAX]; + sd_id128_t mid; + int r; + + assert(h); + + usec = now(CLOCK_REALTIME); + + if (h->ratelimit_begin_usec != UINT64_MAX && h->ratelimit_begin_usec > usec) + /* Hmm, time is running backwards? Say no! */ + return 0; + else if (h->ratelimit_begin_usec == UINT64_MAX || + usec_add(h->ratelimit_begin_usec, user_record_ratelimit_interval_usec(h)) <= usec) { + /* Fresh start */ + new_ratelimit_begin_usec = usec; + new_ratelimit_count = 1; + } else if (h->ratelimit_count < user_record_ratelimit_burst(h)) { + /* Count up */ + new_ratelimit_begin_usec = h->ratelimit_begin_usec; + new_ratelimit_count = h->ratelimit_count + 1; + } else + /* Limit hit */ + return 0; + + r = sd_id128_get_machine(&mid); + if (r < 0) + return r; + + v = json_variant_ref(h->json); + w = json_variant_ref(json_variant_by_key(v, "status")); + z = json_variant_ref(json_variant_by_key(w, sd_id128_to_string(mid, buf))); + + r = json_variant_set_field_unsigned(&z, "rateLimitBeginUSec", new_ratelimit_begin_usec); + if (r < 0) + return r; + + r = json_variant_set_field_unsigned(&z, "rateLimitCount", new_ratelimit_count); + if (r < 0) + return r; + + r = json_variant_set_field(&w, buf, z); + if (r < 0) + return r; + + r = json_variant_set_field(&v, "status", w); + if (r < 0) + return r; + + json_variant_unref(h->json); + h->json = TAKE_PTR(v); + + h->ratelimit_begin_usec = new_ratelimit_begin_usec; + h->ratelimit_count = new_ratelimit_count; + + h->mask |= USER_RECORD_STATUS; + return 1; +} + +int user_record_is_supported(UserRecord *hr, sd_bus_error *error) { + assert(hr); + + if (hr->disposition >= 0 && hr->disposition != USER_REGULAR) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Cannot manage anything but regular users."); + + if (hr->storage >= 0 && !IN_SET(hr->storage, USER_LUKS, USER_DIRECTORY, USER_SUBVOLUME, USER_FSCRYPT, USER_CIFS)) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "User record has storage type this service cannot manage."); + + if (gid_is_valid(hr->gid) && hr->uid != (uid_t) hr->gid) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "User record has to have matching UID/GID fields."); + + if (hr->service && !streq(hr->service, "io.systemd.Home")) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Not accepted with service not matching io.systemd.Home."); + + return 0; +} diff --git a/src/home/user-record-util.h b/src/home/user-record-util.h new file mode 100644 index 0000000000..6afc8df19a --- /dev/null +++ b/src/home/user-record-util.h @@ -0,0 +1,58 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include "sd-bus.h" + +#include "user-record.h" +#include "group-record.h" + +int user_record_synthesize(UserRecord *h, const char *user_name, const char *realm, const char *image_path, UserStorage storage, uid_t uid, gid_t gid); +int group_record_synthesize(GroupRecord *g, UserRecord *u); + +typedef enum UserReconcileMode { + USER_RECONCILE_ANY, + USER_RECONCILE_REQUIRE_NEWER, /* host version must be newer than embedded version */ + USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL, /* similar, but may also be equal */ + _USER_RECONCILE_MODE_MAX, + _USER_RECONCILE_MODE_INVALID = -1, +} UserReconcileMode; + +enum { /* return values */ + USER_RECONCILE_HOST_WON, + USER_RECONCILE_EMBEDDED_WON, + USER_RECONCILE_IDENTICAL, +}; + +int user_record_reconcile(UserRecord *host, UserRecord *embedded, UserReconcileMode mode, UserRecord **ret); +int user_record_add_binding(UserRecord *h, UserStorage storage, const char *image_path, sd_id128_t partition_uuid, sd_id128_t luks_uuid, sd_id128_t fs_uuid, const char *luks_cipher, const char *luks_cipher_mode, uint64_t luks_volume_key_size, const char *file_system_type, const char *home_directory, uid_t uid, gid_t gid); + +/* Results of the two test functions below. */ +enum { + USER_TEST_UNDEFINED, /* Returned by user_record_test_image_path() if the storage type knows no image paths */ + USER_TEST_ABSENT, + USER_TEST_EXISTS, + USER_TEST_MOUNTED, /* Only applies to user_record_test_home_directory(), when the home directory exists. */ + USER_TEST_MAYBE, /* Only applies to LUKS devices: block device exists, but we don't know if it's the right one */ +}; + +int user_record_test_home_directory(UserRecord *h); +int user_record_test_home_directory_and_warn(UserRecord *h); +int user_record_test_image_path(UserRecord *h); +int user_record_test_image_path_and_warn(UserRecord *h); + +int user_record_test_secret(UserRecord *h, UserRecord *secret); + +int user_record_update_last_changed(UserRecord *h, bool with_password); +int user_record_set_disk_size(UserRecord *h, uint64_t disk_size); +int user_record_set_password(UserRecord *h, char **password, bool prepend); +int user_record_make_hashed_password(UserRecord *h, char **password, bool extend); +int user_record_set_hashed_password(UserRecord *h, char **hashed_password); +int user_record_set_pkcs11_pin(UserRecord *h, char **pin, bool prepend); +int user_record_set_pkcs11_protected_authentication_path_permitted(UserRecord *h, int b); +int user_record_set_password_change_now(UserRecord *h, int b); +int user_record_merge_secret(UserRecord *h, UserRecord *secret); +int user_record_good_authentication(UserRecord *h); +int user_record_bad_authentication(UserRecord *h); +int user_record_ratelimit(UserRecord *h); + +int user_record_is_supported(UserRecord *hr, sd_bus_error *error); diff --git a/src/libsystemd/sd-bus/bus-common-errors.c b/src/libsystemd/sd-bus/bus-common-errors.c index 4e23edd923..174f1228af 100644 --- a/src/libsystemd/sd-bus/bus-common-errors.c +++ b/src/libsystemd/sd-bus/bus-common-errors.c @@ -105,5 +105,35 @@ BUS_ERROR_MAP_ELF_REGISTER const sd_bus_error_map bus_common_errors[] = { SD_BUS_ERROR_MAP(BUS_ERROR_SPEED_METER_INACTIVE, EOPNOTSUPP), SD_BUS_ERROR_MAP(BUS_ERROR_UNMANAGED_INTERFACE, EOPNOTSUPP), + SD_BUS_ERROR_MAP(BUS_ERROR_NO_SUCH_HOME, EEXIST), + SD_BUS_ERROR_MAP(BUS_ERROR_UID_IN_USE, EEXIST), + SD_BUS_ERROR_MAP(BUS_ERROR_USER_NAME_EXISTS, EEXIST), + SD_BUS_ERROR_MAP(BUS_ERROR_HOME_EXISTS, EEXIST), + SD_BUS_ERROR_MAP(BUS_ERROR_HOME_ALREADY_ACTIVE, EALREADY), + SD_BUS_ERROR_MAP(BUS_ERROR_HOME_ALREADY_FIXATED, EALREADY), + SD_BUS_ERROR_MAP(BUS_ERROR_HOME_UNFIXATED, EADDRNOTAVAIL), + SD_BUS_ERROR_MAP(BUS_ERROR_HOME_NOT_ACTIVE, EALREADY), + SD_BUS_ERROR_MAP(BUS_ERROR_HOME_ABSENT, EREMOTE), + SD_BUS_ERROR_MAP(BUS_ERROR_HOME_BUSY, EBUSY), + SD_BUS_ERROR_MAP(BUS_ERROR_BAD_PASSWORD, ENOKEY), + SD_BUS_ERROR_MAP(BUS_ERROR_LOW_PASSWORD_QUALITY, EUCLEAN), + SD_BUS_ERROR_MAP(BUS_ERROR_BAD_PASSWORD_AND_NO_TOKEN, EBADSLT), + SD_BUS_ERROR_MAP(BUS_ERROR_TOKEN_PIN_NEEDED, ENOANO), + SD_BUS_ERROR_MAP(BUS_ERROR_TOKEN_PROTECTED_AUTHENTICATION_PATH_NEEDED, ERFKILL), + SD_BUS_ERROR_MAP(BUS_ERROR_TOKEN_PIN_LOCKED, EOWNERDEAD), + SD_BUS_ERROR_MAP(BUS_ERROR_TOKEN_BAD_PIN, ENOLCK), + SD_BUS_ERROR_MAP(BUS_ERROR_TOKEN_BAD_PIN_FEW_TRIES_LEFT, ETOOMANYREFS), + SD_BUS_ERROR_MAP(BUS_ERROR_TOKEN_BAD_PIN_ONE_TRY_LEFT, EUCLEAN), + SD_BUS_ERROR_MAP(BUS_ERROR_BAD_SIGNATURE, EKEYREJECTED), + SD_BUS_ERROR_MAP(BUS_ERROR_HOME_RECORD_MISMATCH, EUCLEAN), + SD_BUS_ERROR_MAP(BUS_ERROR_HOME_RECORD_DOWNGRADE, ESTALE), + SD_BUS_ERROR_MAP(BUS_ERROR_HOME_RECORD_SIGNED, EROFS), + SD_BUS_ERROR_MAP(BUS_ERROR_BAD_HOME_SIZE, ERANGE), + SD_BUS_ERROR_MAP(BUS_ERROR_NO_PRIVATE_KEY, ENOPKG), + SD_BUS_ERROR_MAP(BUS_ERROR_HOME_LOCKED, ENOEXEC), + SD_BUS_ERROR_MAP(BUS_ERROR_HOME_NOT_LOCKED, ENOEXEC), + SD_BUS_ERROR_MAP(BUS_ERROR_TOO_MANY_OPERATIONS, ENOBUFS), + SD_BUS_ERROR_MAP(BUS_ERROR_AUTHENTICATION_LIMIT_HIT, ETOOMANYREFS), + SD_BUS_ERROR_MAP_END }; diff --git a/src/libsystemd/sd-bus/bus-common-errors.h b/src/libsystemd/sd-bus/bus-common-errors.h index 8da56551f6..e5f92b9ec2 100644 --- a/src/libsystemd/sd-bus/bus-common-errors.h +++ b/src/libsystemd/sd-bus/bus-common-errors.h @@ -84,4 +84,35 @@ #define BUS_ERROR_SPEED_METER_INACTIVE "org.freedesktop.network1.SpeedMeterInactive" #define BUS_ERROR_UNMANAGED_INTERFACE "org.freedesktop.network1.UnmanagedInterface" +#define BUS_ERROR_NO_SUCH_HOME "org.freedesktop.home1.NoSuchHome" +#define BUS_ERROR_UID_IN_USE "org.freedesktop.home1.UIDInUse" +#define BUS_ERROR_USER_NAME_EXISTS "org.freedesktop.home1.UserNameExists" +#define BUS_ERROR_HOME_EXISTS "org.freedesktop.home1.HomeExists" +#define BUS_ERROR_HOME_ALREADY_ACTIVE "org.freedesktop.home1.HomeAlreadyActive" +#define BUS_ERROR_HOME_ALREADY_FIXATED "org.freedesktop.home1.HomeAlreadyFixated" +#define BUS_ERROR_HOME_UNFIXATED "org.freedesktop.home1.HomeUnfixated" +#define BUS_ERROR_HOME_NOT_ACTIVE "org.freedesktop.home1.HomeNotActive" +#define BUS_ERROR_HOME_ABSENT "org.freedesktop.home1.HomeAbsent" +#define BUS_ERROR_HOME_BUSY "org.freedesktop.home1.HomeBusy" +#define BUS_ERROR_BAD_PASSWORD "org.freedesktop.home1.BadPassword" +#define BUS_ERROR_LOW_PASSWORD_QUALITY "org.freedesktop.home1.LowPasswordQuality" +#define BUS_ERROR_BAD_PASSWORD_AND_NO_TOKEN "org.freedesktop.home1.BadPasswordAndNoToken" +#define BUS_ERROR_TOKEN_PIN_NEEDED "org.freedesktop.home1.TokenPinNeeded" +#define BUS_ERROR_TOKEN_PROTECTED_AUTHENTICATION_PATH_NEEDED "org.freedesktop.home1.TokenProtectedAuthenticationPathNeeded" +#define BUS_ERROR_TOKEN_PIN_LOCKED "org.freedesktop.home1.TokenPinLocked" +#define BUS_ERROR_TOKEN_BAD_PIN "org.freedesktop.home1.BadPin" +#define BUS_ERROR_TOKEN_BAD_PIN_FEW_TRIES_LEFT "org.freedesktop.home1.BadPinFewTriesLeft" +#define BUS_ERROR_TOKEN_BAD_PIN_ONE_TRY_LEFT "org.freedesktop.home1.BadPinOneTryLeft" +#define BUS_ERROR_BAD_SIGNATURE "org.freedesktop.home1.BadSignature" +#define BUS_ERROR_HOME_RECORD_MISMATCH "org.freedesktop.home1.RecordMismatch" +#define BUS_ERROR_HOME_RECORD_DOWNGRADE "org.freedesktop.home1.RecordDowngrade" +#define BUS_ERROR_HOME_RECORD_SIGNED "org.freedesktop.home1.RecordSigned" +#define BUS_ERROR_BAD_HOME_SIZE "org.freedesktop.home1.BadHomeSize" +#define BUS_ERROR_NO_PRIVATE_KEY "org.freedesktop.home1.NoPrivateKey" +#define BUS_ERROR_HOME_LOCKED "org.freedesktop.home1.HomeLocked" +#define BUS_ERROR_HOME_NOT_LOCKED "org.freedesktop.home1.HomeNotLocked" +#define BUS_ERROR_NO_DISK_SPACE "org.freedesktop.home1.NoDiskSpace" +#define BUS_ERROR_TOO_MANY_OPERATIONS "org.freedesktop.home1.TooManyOperations" +#define BUS_ERROR_AUTHENTICATION_LIMIT_HIT "org.freedesktop.home1.AuthenticationLimitHit" + BUS_ERROR_MAP_ELF_USE(bus_common_errors); diff --git a/src/shared/gpt.h b/src/shared/gpt.h index dcceb076d6..9dc649d8d9 100644 --- a/src/shared/gpt.h +++ b/src/shared/gpt.h @@ -23,6 +23,7 @@ #define GPT_SRV SD_ID128_MAKE(3b,8f,84,25,20,e0,4f,3b,90,7f,1a,25,a7,6f,98,e8) #define GPT_VAR SD_ID128_MAKE(4d,21,b0,16,b5,34,45,c2,a9,fb,5c,16,e0,91,fd,2d) #define GPT_TMP SD_ID128_MAKE(7e,c6,f5,57,3b,c5,4a,ca,b2,93,16,ef,5d,f6,39,d1) +#define GPT_USER_HOME SD_ID128_MAKE(77,3f,91,ef,66,d4,49,b5,bd,83,d6,83,bf,40,ad,16) /* Verity partitions for the root partitions above (we only define them for the root partitions, because only they are * are commonly read-only and hence suitable for verity). */ diff --git a/units/meson.build b/units/meson.build index 581f44f99e..d99cafb39f 100644 --- a/units/meson.build +++ b/units/meson.build @@ -195,6 +195,8 @@ in_units = [ ['systemd-portabled.service', 'ENABLE_PORTABLED', 'dbus-org.freedesktop.portable1.service'], ['systemd-userdbd.service', 'ENABLE_USERDB'], + ['systemd-homed.service', 'ENABLE_HOMED', + 'multi-user.target.wants/ dbus-org.freedesktop.home1.service'], ['systemd-quotacheck.service', 'ENABLE_QUOTACHECK'], ['systemd-random-seed.service', 'ENABLE_RANDOMSEED', 'sysinit.target.wants/'], diff --git a/units/systemd-homed.service.in b/units/systemd-homed.service.in new file mode 100644 index 0000000000..512804cf0e --- /dev/null +++ b/units/systemd-homed.service.in @@ -0,0 +1,36 @@ +# SPDX-License-Identifier: LGPL-2.1+ +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=Home Manager +Documentation=man:systemd-homed.service(8) +RequiresMountsFor=/home + +[Service] +BusName=org.freedesktop.home1 +CapabilityBoundingSet=CAP_SYS_ADMIN CAP_CHOWN CAP_DAC_OVERRIDE CAP_FOWNER CAP_FSETID CAP_SETGID CAP_SETUID +DeviceAllow=/dev/loop-control rw +DeviceAllow=/dev/mapper/control rw +DeviceAllow=block-* rw +ExecStart=@rootlibexecdir@/systemd-homed +IPAddressDeny=any +KillMode=mixed +LimitNOFILE=@HIGH_RLIMIT_NOFILE@ +LockPersonality=yes +MemoryDenyWriteExecute=yes +NoNewPrivileges=yes +PrivateNetwork=yes +RestrictAddressFamilies=AF_UNIX AF_NETLINK AF_ALG +RestrictNamespaces=mnt +RestrictRealtime=yes +StateDirectory=systemd/home +SystemCallArchitectures=native +SystemCallErrorNumber=EPERM +SystemCallFilter=@system-service @mount +@SERVICE_WATCHDOG@